Subpoena Legal Hold Manager
blocklegalEnterprise e-discovery litigation hold notice tracker, custodian preservation receipts, and compliance audit log.
Installation
$pnpm dlx uipkge-ng@latest add subpoena-legal-hold-manager$npx uipkge-ng@latest add subpoena-legal-hold-manager$yarn dlx uipkge-ng@latest add subpoena-legal-hold-manager$bunx uipkge-ng@latest add subpoena-legal-hold-managerManifest:
https://uipkge.dev/r/angular/subpoena-legal-hold-manager.jsonInstalls to:src/app/components/blocks/subpoena-legal-hold-manager/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CustodianRecordinterface CustodianRecord {
id: string
name: string
role: string
department: string
departmentCategory: 'engineering' | 'product' | 'operations'
email: string
avatarInitials: string
noticeSentDate: string
acknowledgedDate: string
status: 'acknowledged' | 'pending' | 're-certified'
statusLabel: string
sha256Signature: string
ipAddress: string
dataSources: string[]
preservedAssets: string
}PreservationSysteminterface PreservationSystem {
id: string
name: string
integrationType: string
status: 'active' | 'warning' | 'synced'
statusLabel: string
scope: string
lockProtocol: string
lastSync: string
iconName: 'slack' | 'aws' | 'google' | 'github'
}npm dependencies
Files installed (2)
src/app/components/blocks/subpoena-legal-hold-manager/subpoena-legal-hold-manager.component.ts60.6 kB
// Enterprise e-discovery litigation hold notice tracker, custodian preservation receipts, // and compliance audit log. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, OnInit, computed, inject, signal } from '@angular/core' import { AlertTriangle, BadgeCheck, Calendar, CalendarClock, Check, CheckCircle2, Clock, Copy, Download, FileCheck, FileSpreadsheet, Info, Lock, LucideAngularModule, Mail, MoreHorizontal, Plus, RefreshCw, RotateCcw, Scale, Search, Send, Server, Share2, Shield, ShieldCheck, UserX, Users, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface CustodianRecord { id: string name: string role: string department: string departmentCategory: 'engineering' | 'product' | 'operations' email: string avatarInitials: string noticeSentDate: string acknowledgedDate: string status: 'acknowledged' | 'pending' | 're-certified' statusLabel: string sha256Signature: string ipAddress: string dataSources: string[] preservedAssets: string } export interface PreservationSystem { id: string name: string integrationType: string status: 'active' | 'warning' | 'synced' statusLabel: string scope: string lockProtocol: string lastSync: string iconName: 'slack' | 'aws' | 'google' | 'github' } const custodiansData: CustodianRecord[] = [ { id: 'CUST-8801', name: 'Marcus Vance', role: 'VP of Engineering', department: 'Engineering & Architecture', departmentCategory: 'engineering', email: '[email protected]', avatarInitials: 'MV', noticeSentDate: 'Aug 14, 2026', acknowledgedDate: 'Aug 15, 2026 · 09:12 PST', status: 'acknowledged', statusLabel: 'Acknowledged & Compliant', sha256Signature: '4f8a9e62b11d87e0fa1943892c55b38d019f2a4e87dc261904a8b71239c0812e', ipAddress: '198.51.100.42 (Corp Gateway)', dataSources: ['Google Workspace', 'Slack Channels', 'GitHub PRs', 'Local Drive'], preservedAssets: '1,420 Commits · 84 Channels · 3.2 GB Mail', }, { id: 'CUST-8802', name: 'Elena Rostova', role: 'VP of Product', department: 'Product & Design', departmentCategory: 'product', email: '[email protected]', avatarInitials: 'ER', noticeSentDate: 'Aug 14, 2026', acknowledgedDate: 'Aug 15, 2026 · 10:45 PST', status: 'acknowledged', statusLabel: 'Acknowledged & Compliant', sha256Signature: '8b3c1d9a7402ef1904a8b71239c0812e4f8a9e62b11d87e0fa1943892c55b38d', ipAddress: '198.51.100.58 (Corp Gateway)', dataSources: ['Google Workspace', 'Notion', 'Slack Channels', 'Jira'], preservedAssets: '312 PRDs · 62 Channels · 1.8 GB Mail', }, { id: 'CUST-8803', name: 'David Chen', role: 'VP of Operations', department: 'Operations & Legal', departmentCategory: 'operations', email: '[email protected]', avatarInitials: 'DC', noticeSentDate: 'Aug 14, 2026', acknowledgedDate: 'Aug 15, 2026 · 11:20 PST', status: 'acknowledged', statusLabel: 'Acknowledged & Compliant', sha256Signature: '1a9e7f3c82904a8b71239c0812e4f8a9e62b11d87e0fa1943892c55b38d8b3c1', ipAddress: '198.51.100.19 (Corp Gateway)', dataSources: ['Google Workspace', 'Box Drive', 'Slack Channels', 'Salesforce'], preservedAssets: '94 Vendor Contracts · 48 Channels · 4.1 GB Mail', }, { id: 'CUST-8804', name: 'Alex Rivera', role: 'Lead Architect', department: 'Engineering & Architecture', departmentCategory: 'engineering', email: '[email protected]', avatarInitials: 'AR', noticeSentDate: 'Aug 14, 2026', acknowledgedDate: 'Aug 16, 2026 · 08:30 PST', status: 'acknowledged', statusLabel: 'Acknowledged & Compliant', sha256Signature: '7e2d5c0b912e4f8a9e62b11d87e0fa1943892c55b38d8b3c1d9a7402ef1904a8', ipAddress: '198.51.100.73 (Corp Gateway)', dataSources: ['Google Workspace', 'GitHub Repos', 'Figma Files', 'Slack'], preservedAssets: '890 Commits · 52 Figma Files · 2.4 GB Mail', }, { id: 'CUST-8805', name: 'Sarah Jenkins', role: 'DevOps Lead', department: 'Platform & Infrastructure', departmentCategory: 'engineering', email: '[email protected]', avatarInitials: 'SJ', noticeSentDate: 'Aug 14, 2026', acknowledgedDate: 'Aug 16, 2026 · 14:05 PST', status: 'acknowledged', statusLabel: 'Acknowledged & Compliant', sha256Signature: '5c0b3a7f4802ef1904a8b71239c0812e4f8a9e62b11d87e0fa1943892c55b38d', ipAddress: '198.51.100.91 (Corp Gateway)', dataSources: ['AWS CloudTrail', 'GitHub Repos', 'Slack', 'Terraform Cloud'], preservedAssets: '340 TF Workspaces · CloudTrail WORM Logs', }, ] const preservationSystemsData: PreservationSystem[] = [ { id: 'sys-slack', name: 'Slack Enterprise Grid', integrationType: 'e-Discovery Legal Hold Lock', status: 'active', statusLabel: 'Active Hold Lock · Auto-Tombstoning Halted', scope: 'All direct messages, private channels, and file attachments preserved.', lockProtocol: 'Immutable Hold Policy #LIT-842', lastSync: 'Today at 08:15 UTC', iconName: 'slack', }, { id: 'sys-aws', name: 'AWS S3 Object Lock', integrationType: 'WORM Compliance Storage Mode', status: 'active', statusLabel: 'Compliance Mode Verified · Deletions Prohibited', scope: 'Production log buckets and database snapshot retention override active.', lockProtocol: 'SEC Rule 17a-4 / FINRA WORM Policy', lastSync: 'Today at 07:30 UTC', iconName: 'aws', }, { id: 'sys-google', name: 'Google Workspace Vault', integrationType: 'Continuous Matter Retention Hold', status: 'active', statusLabel: 'Indefinite Preservation Active', scope: 'Gmail threads, Google Drive revisions, and Google Chat transcripts frozen.', lockProtocol: 'Vault Discovery Rule #LIT-2026-0842', lastSync: 'Today at 09:00 UTC', iconName: 'google', }, { id: 'sys-github', name: 'GitHub Enterprise Server', integrationType: 'Repository & Code Audit Preservation', status: 'active', statusLabel: 'Audit Lock Active · Force-Pushing Blocked', scope: 'Commit trees, Pull Request reviews, and issue histories locked against deletion.', lockProtocol: 'Branch & Audit Immutability Policy', lastSync: 'Today at 06:45 UTC', iconName: 'github', }, ] const filterCategories = [ { id: 'all', label: 'All Custodians', count: 5 }, { id: 'engineering', label: 'Engineering', count: 3 }, { id: 'product', label: 'Product & Design', count: 1 }, { id: 'operations', label: 'Operations & Legal', count: 1 }, ] const defaultScopeNotes = 'All source code repositories, direct messages, and design specifications relevant to Matter #LIT-2026-0842.' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-subpoena-legal-hold-manager, [ui-subpoena-legal-hold-manager]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiProgressComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="subpoena-legal-hold-manager" [class]="rootClass"> <!-- Header Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-content class="p-5 sm:p-6"> <div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between"> <!-- Left Column: Matter Details & Compliance Badges --> <div class="space-y-3"> <div class="flex flex-wrap items-center gap-2"> <span class="text-muted-foreground text-xs font-medium"> Enterprise e-Discovery & Litigation Readiness </span> <span ui-badge wrap variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium" > Fed. R. Civ. P. 37(e) Active Hold </span> </div> <div> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Matter #LIT-2026-0842: In re UI Component Intellectual Property </h2> <p class="text-muted-foreground mt-1 text-sm font-medium"> Federal Civil Litigation · US District Court SDNY </p> </div> <!-- Matter Metadata Strip --> <div class="text-muted-foreground flex flex-wrap items-center gap-3 pt-1 text-xs"> <div class="flex items-center gap-1.5 font-mono tabular-nums"> <span class="text-muted-foreground">Docket:</span> <span class="text-foreground font-medium">1:26-cv-04892-RMB</span> </div> <ui-separator orientation="vertical" class="h-3.5" /> <div class="flex items-center gap-1"> <lucide-icon [img]="Scale" class="text-muted-foreground size-3.5" /> <span>Hon. Richard M. Berman</span> </div> <ui-separator orientation="vertical" class="h-3.5" /> <div class="flex items-center gap-1"> <lucide-icon [img]="Shield" class="text-muted-foreground size-3.5" /> <span>Counsel: Vance & Sterling LLP</span> </div> <ui-separator orientation="vertical" class="h-3.5" /> <div class="flex items-center gap-1.5 font-mono tabular-nums"> <lucide-icon [img]="Calendar" class="text-muted-foreground size-3.5" /> <span>Issued: Aug 14, 2026</span> </div> </div> </div> <!-- Right Column: Active Status Pill & Primary Action Buttons --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center lg:flex-col lg:items-end"> <!-- Hold Status Pill --> <div class="border-success/30 bg-success/10 text-success dark:bg-success/10 text-success inline-flex items-center gap-2.5 rounded-lg border px-3.5 py-2 text-xs font-semibold shadow-xs" > <lucide-icon [img]="ShieldCheck" class="text-success size-4.5 shrink-0" /> <div class="flex flex-col text-left"> <span>Active Preservation Hold · 100% Custodian Compliance</span> <span class="text-success/90 text-xs font-normal">18/18 Signed Receipts · Auto-Purge Suspended</span> </div> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="hover:bg-accent gap-1.5 text-xs shadow-xs" [disabled]="isExportingLog()" (click)="handleExportAuditLog()" > @if (isExportingLog()) { <lucide-icon [img]="RefreshCw" class="size-3.5 animate-spin" /> } @else { <lucide-icon [img]="FileSpreadsheet" class="text-foreground size-3.5" /> } <span>{{ isExportingLog() ? 'Exporting Package...' : 'Export Custodian Audit Log' }}</span> </button> <button ui-button variant="default" size="sm" class="gap-1.5 text-xs shadow-xs" (click)="handleOpenIssueHoldModal()" > <lucide-icon [img]="Plus" class="size-3.5" /> <span>Issue New Legal Hold</span> </button> </div> </div> </div> <!-- Dynamic notification banner --> @if (notificationBanner(); as banner) { <div [class]="bannerClass(banner.type)"> @if (banner.type === 'success') { <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> } @else if (banner.type === 'info') { <lucide-icon [img]="Info" class="text-info size-4 shrink-0" /> } @else { <lucide-icon [img]="AlertTriangle" class="text-warning size-4 shrink-0" /> } <span>{{ banner.message }}</span> </div> } </div> </div> <!-- 4 Legal Hold Metric Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Card 1: Total Custodians Notified --> <div ui-card class="border-border bg-card shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm" > <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Custodians Notified</h3> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Users" class="size-4.5" /> </div> </div> <div ui-card-content class="space-y-3"> <div> <div class="text-foreground flex items-baseline gap-1.5 font-mono text-2xl font-bold tracking-tight tabular-nums" > 18 Custodians </div> <p class="text-muted-foreground text-xs">18 / 18 custodians active under hold notice</p> </div> <div class="space-y-1"> <ui-progress [value]="100" class="bg-primary/20 h-1.5" /> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Notice Delivery</span> <span class="font-mono tabular-nums">100% delivered</span> </div> </div> </div> </div> <!-- Card 2: Acknowledgements Received --> <div ui-card class="border-border bg-card shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm" > <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Acknowledgements Received</h3> <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="FileCheck" class="size-4.5" /> </div> </div> <div ui-card-content class="space-y-3"> <div> <div class="text-success text-success flex items-baseline gap-1.5 font-mono text-2xl font-bold tracking-tight tabular-nums" > 18 / 18 Signed · 100% </div> <p class="text-muted-foreground text-xs">Zero overdue · 100% compliant response rate</p> </div> <div class="space-y-1"> <ui-progress [value]="100" class="bg-success/20 h-1.5" /> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="text-success font-medium">Zero Non-Responsive</span> <span class="font-mono tabular-nums">0 pending</span> </div> </div> </div> </div> <!-- Card 3: Auto-Purge Suspended Systems --> <div ui-card class="border-border bg-card shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm" > <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Auto-Purge Suspended Systems</h3> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Lock" class="size-4.5" /> </div> </div> <div ui-card-content class="space-y-3"> <div> <div class="text-foreground font-mono text-lg font-bold tracking-tight tabular-nums sm:text-xl"> 4 Systems Active </div> <p class="text-info truncate text-xs font-medium">Gmail, Slack, S3, GitHub</p> </div> <div class="border-border bg-muted/40 flex items-center justify-between rounded border px-2 py-1 text-xs"> <span class="text-muted-foreground">Purge Status</span> <span ui-badge wrap variant="outline" class="border-info/30 text-info font-mono text-xs" >Retention Paused</span > </div> </div> </div> <!-- Card 4: Next Annual Re-certification --> <div ui-card class="border-border bg-card shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm" > <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Next Re-certification</h3> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="CalendarClock" class="size-4.5" /> </div> </div> <div ui-card-content class="space-y-3"> <div> <div class="text-foreground font-mono text-lg font-bold tracking-tight tabular-nums sm:text-xl"> In 6 Months · Feb 2027 </div> <p class="text-muted-foreground text-xs">Semi-annual custodian confirmation cycle</p> </div> <div class="border-border bg-muted/40 flex items-center justify-between rounded border px-2 py-1 text-xs"> <span class="text-muted-foreground">Countdown</span> <span class="text-warning text-warning font-mono font-medium tabular-nums">178 days remaining</span> </div> </div> </div> </div> <!-- Custodian Filter Toolbar & Search --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <!-- Category Filter Pills --> <div class="flex flex-wrap items-center gap-1.5"> @for (cat of filterCategories; track cat.id) { <button type="button" [class]="filterPillClass(cat.id)" (click)="activeFilter.set(cat.id)"> <span>{{ cat.label }}</span> <span [class]="filterCountClass(cat.id)">{{ cat.count }}</span> </button> } </div> <!-- Search Input --> <div class="relative w-full sm:w-72"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <input [value]="searchQuery()" (input)="searchQuery.set($any($event.target).value)" type="text" placeholder="Filter custodians, roles, data sources..." class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border pr-8 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none" /> @if (searchQuery()) { <button aria-label="Clear search" type="button" class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2" (click)="searchQuery.set('')" > <lucide-icon [img]="X" class="size-3.5" /> </button> } </div> </div> <!-- Custodian Acknowledgement Roster Table Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="p-5 pb-3"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-foreground text-base font-semibold tracking-tight"> Custodian Acknowledgement Roster & Preservation Status </h3> <p ui-card-description class="text-muted-foreground text-xs"> Active enterprise custodians, certified preservation receipts, and electronic data source scopes. </p> </div> <div class="flex items-center gap-2 pt-2 sm:pt-0"> <span class="text-muted-foreground text-xs"> Showing <span class="text-foreground font-mono font-medium tabular-nums">{{ filteredCustodians().length }}</span> of <span class="text-foreground font-mono font-medium tabular-nums">5</span> sample custodians </span> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="text-muted-foreground min-w-[260px] py-3 text-xs font-semibold tracking-wider uppercase" > Custodian & Department </th> <th ui-table-head class="text-muted-foreground min-w-[130px] py-3 text-xs font-semibold tracking-wider uppercase" > Notice Sent Date </th> <th ui-table-head class="text-muted-foreground min-w-[170px] py-3 text-xs font-semibold tracking-wider uppercase" > Acknowledged Date </th> <th ui-table-head class="text-muted-foreground min-w-[170px] py-3 text-xs font-semibold tracking-wider uppercase" > Compliance Status </th> <th ui-table-head class="text-muted-foreground min-w-[280px] py-3 text-xs font-semibold tracking-wider uppercase" > Data Sources Preserved </th> <th ui-table-head class="text-muted-foreground min-w-[140px] py-3 text-right text-xs font-semibold tracking-wider uppercase" > Actions </th> </tr> </thead> <tbody ui-table-body> @for (custodian of filteredCustodians(); track custodian.id) { <tr ui-table-row class="hover:bg-muted/30 transition-colors"> <!-- Custodian Avatar, Name & Department --> <td ui-table-cell class="py-3.5 align-middle"> <div class="flex items-center gap-3"> <ui-avatar class="size-8 shrink-0"> <ui-avatar-fallback class="text-primary bg-primary/10 text-xs font-semibold"> {{ custodian.avatarInitials }} </ui-avatar-fallback> </ui-avatar> <div class="min-w-0 space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground truncate text-sm font-semibold">{{ custodian.name }}</span> <span class="text-muted-foreground font-mono text-xs tabular-nums">{{ custodian.id }}</span> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-xs"> <span class="text-foreground font-medium">{{ custodian.role }}</span> <span>·</span> <span>{{ custodian.department }}</span> </div> </div> </div> </td> <!-- Notice Sent Date --> <td ui-table-cell class="py-3.5 align-middle"> <div class="text-foreground flex items-center gap-1.5 font-mono text-xs tabular-nums"> <lucide-icon [img]="Send" class="text-muted-foreground size-3.5" /> <span>{{ custodian.noticeSentDate }}</span> </div> </td> <!-- Acknowledged Date --> <td ui-table-cell class="py-3.5 align-middle"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-1.5 font-mono text-xs font-medium tabular-nums" > <lucide-icon [img]="Clock" class="text-success size-3.5 shrink-0" /> <span>{{ custodian.acknowledgedDate }}</span> </div> <div class="text-muted-foreground max-w-[160px] truncate font-mono text-xs" [attr.title]="custodian.ipAddress" > IP: {{ custodian.ipAddress }} </div> </div> </td> <!-- Compliance Status Badge --> <td ui-table-cell class="py-3.5 align-middle"> <div class="space-y-1"> <div class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-semibold" > <lucide-icon [img]="BadgeCheck" class="text-success size-3.5 shrink-0" /> <span>{{ custodian.statusLabel }}</span> </div> <p class="text-muted-foreground text-xs">Receipt Verified</p> </div> </td> <!-- Data Sources Preserved --> <td ui-table-cell class="py-3.5 align-middle"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-1"> @for (source of custodian.dataSources; track source) { <span ui-badge wrap variant="secondary" class="px-2 py-0.5 text-xs font-normal"> {{ source }} </span> } </div> <p class="text-muted-foreground font-mono text-xs tabular-nums"> Scope: {{ custodian.preservedAssets }} </p> </div> </td> <!-- Actions Dropdown --> <td ui-table-cell class="py-3.5 text-right align-middle"> <div class="flex items-center justify-end gap-1.5"> <button ui-button variant="outline" size="xs" class="hover:bg-accent h-7 gap-1 px-2.5 text-xs font-medium shadow-2xs" (click)="openReceiptModal(custodian)" > <lucide-icon [img]="FileCheck" class="text-muted-foreground size-3" /> <span>Receipt</span> </button> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-7 p-0" > <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Open custodian options for {{ custodian.name }}</span> </button> <ui-dropdown-menu-content align="end" class="w-52"> <ui-dropdown-menu-label class="text-xs font-semibold" >Custodian Actions</ui-dropdown-menu-label > <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="cursor-pointer text-xs" (select)="openReceiptModal(custodian)" > <lucide-icon [img]="FileCheck" class="text-success mr-2 size-3.5" /> <span>View Signed Receipt</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="cursor-pointer text-xs" (select)="handleSendReminder(custodian)" > <lucide-icon [img]="Mail" class="text-muted-foreground mr-2 size-3.5" /> <span>Send Re-affirmation Notice</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="cursor-pointer text-xs" (select)="handleRecertify(custodian)"> <lucide-icon [img]="RotateCcw" class="text-muted-foreground mr-2 size-3.5" /> <span>Initiate Re-certification</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive cursor-pointer text-xs" (select)="handleReleaseFromHold(custodian)" > <lucide-icon [img]="UserX" class="text-destructive mr-2 size-3.5" /> <span>Release from Hold...</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </div> </td> </tr> } @if (filteredCustodians().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="6" class="text-muted-foreground py-8 text-center text-xs"> No custodians match your search or filter criteria. </td> </tr> } </tbody> </table> </div> </div> <!-- Table Sub-Footer --> <div ui-card-footer class="border-border bg-muted/20 text-muted-foreground flex flex-col gap-2 border-t p-4 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-success size-4 shrink-0" /> <span> All custodian preservation receipts are immutably timestamped and hashed under SEC & FRCP 37(e) evidentiary rules. </span> </div> <div class="flex items-center gap-2 font-mono tabular-nums"> <span>Roster Hash: e7d9-4081-9bf2</span> <span class="text-success font-medium">18 Total Certified</span> </div> </div> </div> <!-- System Preservation Integrations Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="p-5 pb-3"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="Server" class="text-primary size-4" /> <h3 ui-card-title class="text-foreground text-base font-semibold tracking-tight"> System Preservation Integrations & Automated Storage Locks </h3> </div> <p ui-card-description class="text-muted-foreground text-xs"> Automated retention policy overrides and immutable WORM lock status across enterprise infrastructure. </p> </div> <span ui-badge wrap variant="outline" class="border-info/30 text-info w-fit text-xs" >4 / 4 Systems Enforced</span > </div> </div> <div ui-card-content class="p-5 pt-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> @for (system of preservationSystems; track system.id) { <div class="border-border bg-muted/30 hover:bg-muted/50 flex flex-col justify-between space-y-3 rounded-lg border p-4 transition-colors" > <div class="space-y-2"> <div class="flex items-start justify-between gap-2"> <div class="flex items-center gap-2.5"> <div class="border-border bg-card text-foreground flex size-8 items-center justify-center rounded-md border shadow-2xs" > <lucide-icon [img]="Lock" class="text-success size-4" /> </div> <div> <h4 class="text-foreground text-sm font-semibold">{{ system.name }}</h4> <p class="text-muted-foreground text-xs">{{ system.integrationType }}</p> </div> </div> <div class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-semibold" > <span class="bg-success size-1.5 animate-pulse rounded-full"></span> <span>Locked</span> </div> </div> <p class="text-muted-foreground text-xs leading-relaxed">{{ system.scope }}</p> </div> <div class="border-border/60 space-y-1.5 border-t pt-2.5 text-xs"> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Enforcement:</span> <span class="text-foreground font-mono font-medium">{{ system.lockProtocol }}</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Last Audit Sync:</span> <span class="text-muted-foreground font-mono tabular-nums">{{ system.lastSync }}</span> </div> </div> </div> } </div> </div> </div> <!-- Custodian Signed Receipt & Cryptographic Audit Modal --> @if (selectedReceipt(); as receipt) { <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs transition-opacity" (click)="closeReceiptModal()" > <div class="border-border bg-card text-card-foreground animate-in fade-in zoom-in-95 relative w-full max-w-2xl overflow-hidden rounded-xl border shadow-sm duration-200" (click)="$event.stopPropagation()" > <!-- Modal Top Bar --> <div class="border-border bg-muted/40 flex items-center justify-between border-b px-5 py-3.5"> <div class="flex items-center gap-2.5"> <div class="bg-success/10 text-success flex size-7 items-center justify-center rounded-full"> <lucide-icon [img]="BadgeCheck" class="size-4" /> </div> <div> <h3 class="text-foreground text-xs font-semibold">Preservation Acknowledgment Receipt</h3> <p class="text-muted-foreground font-mono text-xs tabular-nums"> Record ID: {{ receipt.id }} · Matter #LIT-2026-0842 </p> </div> </div> <button type="button" class="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-md p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" aria-label="Close receipt modal" (click)="closeReceiptModal()" > <lucide-icon [img]="X" class="size-4" /> </button> </div> <!-- Receipt Document Body --> <div class="max-h-[75vh] space-y-5 overflow-y-auto p-6"> <!-- Legal Receipt Certificate Frame --> <div class="border-success/30 bg-success/5 dark:bg-success/10 relative rounded-lg border-2 border-dashed p-5" > <div class="border-border/80 flex items-center justify-between border-b pb-3"> <div class="space-y-0.5"> <span class="text-success text-xs font-semibold tracking-wide"> Certified Custodian E-Discovery Receipt </span> <h4 class="text-foreground text-base font-bold"> Litigation Hold Notice & Preservation Directive </h4> </div> <span ui-badge wrap variant="outline" class="border-success/40 text-success text-xs font-semibold"> 100% Valid & Attested </span> </div> <div class="text-foreground mt-4 space-y-3 text-xs leading-relaxed"> <div class="bg-card/60 border-border/60 grid grid-cols-2 gap-3 rounded-md border p-3 sm:grid-cols-3"> <div> <span class="text-muted-foreground block text-xs">Custodian Name:</span> <span class="text-foreground text-sm font-semibold">{{ receipt.name }}</span> </div> <div> <span class="text-muted-foreground block text-xs">Title & Role:</span> <span class="text-foreground font-medium">{{ receipt.role }}</span> </div> <div> <span class="text-muted-foreground block text-xs">Corporate Email:</span> <span class="text-foreground font-mono">{{ receipt.email }}</span> </div> </div> <!-- Legal Notice Text --> <div class="text-muted-foreground bg-muted/40 border-border/60 space-y-1.5 rounded-md border p-3 text-xs" > <p class="text-foreground font-semibold">Acknowledgment Declaration & Legal Affirmation:</p> <p> "I hereby confirm receipt and comprehension of the Formal Legal Hold Notice for Matter #LIT-2026-0842. I have immediately ceased all automated or manual deletion, purging, and overwriting of relevant electronic documents, chat logs, code commits, emails, and physical files within my custody or control." </p> </div> <!-- Preserved Repositories --> <div class="space-y-1 pt-1"> <span class="text-foreground text-xs font-semibold">Mandatory Preserved Repositories:</span> <div class="flex flex-wrap gap-1.5 pt-0.5"> @for (source of receipt.dataSources; track source) { <span ui-badge wrap variant="secondary" class="text-xs">{{ source }}</span> } </div> </div> </div> <!-- Attestation Timestamps --> <div class="border-border/60 mt-4 grid grid-cols-2 gap-3 border-t pt-3 text-xs sm:grid-cols-3"> <div> <span class="text-muted-foreground block">Notice Dispatched:</span> <span class="text-foreground font-mono font-medium tabular-nums">{{ receipt.noticeSentDate }}</span> </div> <div> <span class="text-muted-foreground block">Signed & Acknowledged:</span> <span class="text-success text-success font-mono font-medium tabular-nums"> {{ receipt.acknowledgedDate }} </span> </div> <div> <span class="text-muted-foreground block">Origin Host IP:</span> <span class="text-foreground font-mono tabular-nums">{{ receipt.ipAddress }}</span> </div> </div> </div> <!-- Cryptographic SHA-256 Signature Box --> <div class="border-border bg-muted/40 space-y-2 rounded-lg border p-3.5 text-xs"> <div class="flex items-center justify-between"> <div class="text-foreground flex items-center gap-1.5 font-medium"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> <span>Digital Signature Digest (SHA-256)</span> </div> <button type="button" class="text-primary flex items-center gap-1 text-xs font-medium hover:underline" (click)="copyToClipboard(receipt.sha256Signature, 'signature')" > @if (copiedSignature()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ copiedSignature() ? 'Copied' : 'Copy Hash' }}</span> </button> </div> <p class="text-muted-foreground bg-background/80 border-border rounded border p-2 font-mono text-xs break-all" > {{ receipt.sha256Signature }} </p> <div class="border-border/50 text-muted-foreground flex flex-wrap items-center justify-between gap-2 border-t pt-1" > <span>Attestation Engine: UIPKGE Legal Vault v4</span> <span class="font-mono tabular-nums">Evidentiary Standard: FRCP 37(e)</span> </div> </div> </div> <!-- Modal Actions Footer --> <div class="border-border bg-muted/40 flex flex-col-reverse gap-2 border-t p-4 sm:flex-row sm:items-center sm:justify-between" > <button ui-button variant="ghost" size="sm" class="text-xs" (click)="copyToClipboard(verifyLink(receipt.id), 'verifyLink')" > @if (copiedVerifyLink()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Share2" class="size-3.5" /> } <span>{{ copiedVerifyLink() ? 'Link Copied!' : 'Copy Verification Link' }}</span> </button> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="text-xs" (click)="closeReceiptModal()"> Close </button> <button ui-button aria-label="Download attachment" variant="default" size="sm" class="gap-1.5 text-xs shadow-xs" (click)="handleExportAndClose()" > <lucide-icon [img]="Download" class="size-3.5" /> <span>Download Signed PDF</span> </button> </div> </div> </div> </div> } <!-- Issue New Legal Hold Modal --> @if (isIssueHoldModalOpen()) { <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs transition-opacity" (click)="handleCloseIssueHoldModal()" > <div class="border-border bg-card text-card-foreground animate-in fade-in zoom-in-95 relative w-full max-w-lg overflow-hidden rounded-xl border shadow-sm duration-200" (click)="$event.stopPropagation()" > <div class="border-border bg-muted/40 flex items-center justify-between border-b px-5 py-3.5"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-full"> <lucide-icon [img]="Plus" class="size-4" /> </div> <div> <h3 class="text-foreground text-sm font-bold">Issue New Legal Hold Notice</h3> <p class="text-muted-foreground text-xs"> Matter #LIT-2026-0842 · Dispatch formal preservation notice </p> </div> </div> <button aria-label="Close issue hold dialog" type="button" class="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-md p-1.5 focus-visible:ring-2 focus-visible:outline-none" (click)="handleCloseIssueHoldModal()" > <lucide-icon [img]="X" class="size-4" /> </button> </div> <div class="space-y-4 p-5 text-xs"> <div class="border-info/30 bg-info/10 text-foreground rounded-md border p-3"> <div class="flex gap-2"> <lucide-icon [img]="Info" class="text-info size-4 shrink-0" /> <p class="leading-relaxed"> Issuing a notice automatically suspends auto-tombstoning and scheduled purges across Gmail, Slack, and cloud storage for this custodian. </p> </div> </div> <div class="space-y-3"> <div> <label class="text-foreground mb-1 block font-semibold">Custodian Full Name</label> <input [value]="newHoldForm().custodianName" (input)="patchHoldForm({ custodianName: $any($event.target).value })" type="text" placeholder="e.g. Jordan Mitchell" class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-2 focus-visible:outline-none" /> </div> <div> <label class="text-foreground mb-1 block font-semibold">Corporate Email Address</label> <input [value]="newHoldForm().custodianEmail" (input)="patchHoldForm({ custodianEmail: $any($event.target).value })" type="email" placeholder="e.g. [email protected]" class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-2 focus-visible:outline-none" /> </div> <div class="grid grid-cols-2 gap-3"> <div> <label class="text-foreground mb-1 block font-semibold">Title & Role</label> <input [value]="newHoldForm().custodianRole" (input)="patchHoldForm({ custodianRole: $any($event.target).value })" type="text" placeholder="e.g. Principal Engineer" class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-2 focus-visible:outline-none" /> </div> <div> <label class="text-foreground mb-1 block font-semibold">Department</label> <select [value]="newHoldForm().department" (change)="patchHoldForm({ department: $any($event.target).value })" class="border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border px-2 text-xs focus-visible:ring-2 focus-visible:outline-none" > <option value="engineering">Engineering</option> <option value="product">Product & Design</option> <option value="operations">Operations & Legal</option> </select> </div> </div> <div> <label class="text-foreground mb-1 block font-semibold"> Preservation Scope & Custom Instructions </label> <textarea [value]="newHoldForm().scopeNotes" (input)="patchHoldForm({ scopeNotes: $any($event.target).value })" rows="3" class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 w-full rounded-md border p-2.5 text-xs focus-visible:ring-2 focus-visible:outline-none" ></textarea> </div> </div> </div> <div class="border-border bg-muted/40 flex items-center justify-end gap-2 border-t p-4"> <button ui-button aria-label="Close issue hold dialog" variant="outline" size="sm" class="text-xs" (click)="handleCloseIssueHoldModal()" > Cancel </button> <button ui-button variant="default" size="sm" class="gap-1.5 text-xs shadow-xs" (click)="handleIssueHoldSubmit()" > <lucide-icon [img]="Send" class="size-3.5" /> <span>Dispatch Notice & Apply Lock</span> </button> </div> </div> </div> } </div> `, }) export class UiSubpoenaLegalHoldManagerComponent implements OnInit { protected readonly AlertTriangle = AlertTriangle protected readonly BadgeCheck = BadgeCheck protected readonly Calendar = Calendar protected readonly CalendarClock = CalendarClock protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Download = Download protected readonly FileCheck = FileCheck protected readonly FileSpreadsheet = FileSpreadsheet protected readonly Info = Info protected readonly Lock = Lock protected readonly Mail = Mail protected readonly MoreHorizontal = MoreHorizontal protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly RotateCcw = RotateCcw protected readonly Scale = Scale protected readonly Search = Search protected readonly Send = Send protected readonly Server = Server protected readonly Share2 = Share2 protected readonly Shield = Shield protected readonly ShieldCheck = ShieldCheck protected readonly UserX = UserX protected readonly Users = Users protected readonly X = X protected readonly filterCategories = filterCategories protected readonly preservationSystems = preservationSystemsData @Input() set defaultFilter(v: string) { this.activeFilter.set(v ?? 'all') } @Input('class') className?: string readonly activeFilter = signal<string>('all') readonly searchQuery = signal<string>('') readonly selectedReceipt = signal<CustodianRecord | null>(null) readonly isExportingLog = signal(false) readonly notificationBanner = signal<{ message: string; type: 'success' | 'info' | 'warning' } | null>(null) readonly isIssueHoldModalOpen = signal(false) readonly newHoldForm = signal({ custodianName: '', custodianRole: '', custodianEmail: '', department: 'engineering', scopeNotes: defaultScopeNotes, }) readonly copiedSignature = signal(false) readonly copiedVerifyLink = signal(false) private readonly onKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Escape') { if (this.selectedReceipt()) this.closeReceiptModal() if (this.isIssueHoldModalOpen()) this.handleCloseIssueHoldModal() } } constructor() { const destroyRef = inject(DestroyRef) destroyRef.onDestroy(() => { if (typeof window !== 'undefined') window.removeEventListener('keydown', this.onKeyDown) }) } ngOnInit(): void { if (typeof window !== 'undefined') window.addEventListener('keydown', this.onKeyDown) } readonly filteredCustodians = computed(() => { const filter = this.activeFilter() const query = this.searchQuery().trim().toLowerCase() return custodiansData.filter((custodian) => { let matchesCategory = true if (filter === 'engineering') matchesCategory = custodian.departmentCategory === 'engineering' else if (filter === 'product') matchesCategory = custodian.departmentCategory === 'product' else if (filter === 'operations') matchesCategory = custodian.departmentCategory === 'operations' if (!query) return matchesCategory const matchesSearch = custodian.name.toLowerCase().includes(query) || custodian.role.toLowerCase().includes(query) || custodian.department.toLowerCase().includes(query) || custodian.email.toLowerCase().includes(query) || custodian.dataSources.some((source) => source.toLowerCase().includes(query)) || custodian.id.toLowerCase().includes(query) return matchesCategory && matchesSearch }) }) get rootClass(): string { return cn('w-full space-y-6', this.className) } bannerClass(type: 'success' | 'info' | 'warning'): string { return cn( 'mt-4 flex items-center gap-2 rounded-md border p-2.5 text-xs transition-colors', type === 'success' && 'border-success/30 bg-success/10 text-success dark:bg-success/10 dark:text-foreground', type === 'info' && 'border-info/30 bg-info/10 text-info dark:bg-info/10 dark:text-foreground', type === 'warning' && 'border-warning/30 bg-warning/10 text-warning dark:bg-warning/10 dark:text-foreground', ) } filterPillClass(id: string): string { return cn( 'focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none', this.activeFilter() === id ? 'bg-primary text-primary-foreground shadow-xs' : 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground border', ) } filterCountClass(id: string): string { return cn( 'py-0.2 rounded-full px-1.5 font-mono text-xs tabular-nums', this.activeFilter() === id ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground', ) } verifyLink(id: string): string { return `https://uipkge.dev/legal/receipt/${id}` } showNotification(message: string, type: 'success' | 'info' | 'warning' = 'success', duration = 4000): void { this.notificationBanner.set({ message, type }) setTimeout(() => { if (this.notificationBanner()?.message === message) this.notificationBanner.set(null) }, duration) } handleExportAuditLog(): void { if (this.isExportingLog()) return this.isExportingLog.set(true) this.showNotification( 'Compiling full litigation hold preservation receipts and cryptographic audit ledger...', 'info', 2000, ) setTimeout(() => { this.isExportingLog.set(false) this.showNotification( 'Export complete: LIT-2026-0842_Custodian_Preservation_Audit_Package_2026-08-21.zip (2.4 MB · 18 Custodian Receipts)', 'success', 5000, ) }, 1400) } openReceiptModal(custodian: CustodianRecord): void { this.selectedReceipt.set(custodian) this.copiedSignature.set(false) this.copiedVerifyLink.set(false) } handleExportAndClose(): void { this.handleExportAuditLog() this.closeReceiptModal() } closeReceiptModal(): void { this.selectedReceipt.set(null) } handleSendReminder(custodian: CustodianRecord): void { this.showNotification( `Re-affirmation reminder & notice receipt dispatched to ${custodian.name} (${custodian.email}).`, 'info', ) } handleRecertify(custodian: CustodianRecord): void { this.showNotification( `Semi-annual re-certification workflow initiated for ${custodian.name}. Acknowledgment due within 5 business days.`, 'success', ) } handleReleaseFromHold(custodian: CustodianRecord): void { this.showNotification( `Hold release request queued for ${custodian.name} (${custodian.id}). Pending final authorization from Lead Litigation Counsel.`, 'warning', ) } handleOpenIssueHoldModal(): void { this.isIssueHoldModalOpen.set(true) } handleCloseIssueHoldModal(): void { this.isIssueHoldModalOpen.set(false) this.newHoldForm.set({ custodianName: '', custodianRole: '', custodianEmail: '', department: 'engineering', scopeNotes: defaultScopeNotes, }) } patchHoldForm( patch: Partial<{ custodianName: string custodianRole: string custodianEmail: string department: string scopeNotes: string }>, ): void { this.newHoldForm.update((prev) => ({ ...prev, ...patch })) } handleIssueHoldSubmit(): void { const form = this.newHoldForm() if (!form.custodianName || !form.custodianEmail) { this.showNotification('Please enter custodian name and corporate email address.', 'warning') return } const name = form.custodianName this.handleCloseIssueHoldModal() this.showNotification( `Legal Hold Notice #LIT-2026-0842-N6 dispatched to ${name}. Auto-purge suspension lock applied across endpoints.`, 'success', 5000, ) } copyToClipboard(text: string, type: 'signature' | 'verifyLink'): void { try { navigator.clipboard?.writeText(text) } catch { // Clipboard fallback } if (type === 'signature') { this.copiedSignature.set(true) setTimeout(() => this.copiedSignature.set(false), 2000) } else if (type === 'verifyLink') { this.copiedVerifyLink.set(true) setTimeout(() => this.copiedVerifyLink.set(false), 2000) } } }src/app/components/blocks/subpoena-legal-hold-manager/index.ts0.1 kB
export { UiSubpoenaLegalHoldManagerComponent, type CustodianRecord, type PreservationSystem, } from './subpoena-legal-hold-manager.component'
Raw manifest:https://uipkge.dev/r/angular/subpoena-legal-hold-manager.json