Menu

Change language

GitHub repo

Training Compliance Tracker

blockhr

Annual mandatory employee compliance training dashboard with certification badges, renewal deadlines, assessment scorecards, verified credential preview modal, and certificate export controls.

Installation

$npx uipkge-ng@latest add training-compliance-tracker
Manifest:https://uipkge.dev/r/angular/training-compliance-tracker.jsonInstalls to:src/app/components/blocks/training-compliance-tracker/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ComplianceCourse
interface ComplianceCourse {
  id: string
  code: string
  title: string
  category: string
  categoryKey: 'security' | 'ethics' | 'governance' | 'privacy' | 'finance'
  standard: string
  duration: string
  durationHours: number
  score: number
  scoreStatus: string
  completionDate: string
  expirationDate: string
  validityMonths: number
  daysUntilExpiry: number
  credentialId: string
  sha256Digest: string
  verified: boolean
  instructor: string
  accreditationBody: string
  description: string
}

Files installed (2)

  • src/app/components/blocks/training-compliance-tracker/training-compliance-tracker.component.ts45.9 kB
    // Annual mandatory employee compliance training dashboard with certification badges,
    // renewal deadlines, assessment scorecards, verified credential preview modal, and
    // certificate export controls. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, HostListener, Input, computed, signal } from '@angular/core'
    import {
      AlertCircle,
      Award,
      BadgeCheck,
      Building2,
      CalendarClock,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Download,
      FileCheck,
      FileDown,
      LucideAngularModule,
      PlayCircle,
      RefreshCw,
      RotateCcw,
      Search,
      Share2,
      Shield,
      ShieldCheck,
      Users,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    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 { 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 ComplianceCourse {
      id: string
      code: string
      title: string
      category: string
      categoryKey: 'security' | 'ethics' | 'governance' | 'privacy' | 'finance'
      standard: string
      duration: string
      durationHours: number
      score: number
      scoreStatus: string
      completionDate: string
      expirationDate: string
      validityMonths: number
      daysUntilExpiry: number
      credentialId: string
      sha256Digest: string
      verified: boolean
      instructor: string
      accreditationBody: string
      description: string
    }
    
    const coursesData: ComplianceCourse[] = [
      {
        id: 'course-sec-201',
        code: 'SEC-201',
        title: 'Information Security & Data Protection (SOC2 / ISO 27001)',
        category: 'Security & Privacy',
        categoryKey: 'security',
        standard: 'AICPA SOC 2 Type II (CC6.1–CC6.8) & ISO/IEC 27001:2022 ISMS',
        duration: '1.5 Hours · Video + Exam',
        durationHours: 1.5,
        score: 100,
        scoreStatus: '100% · Passed',
        completionDate: 'Aug 10, 2026',
        expirationDate: 'Aug 10, 2027',
        validityMonths: 12,
        daysUntilExpiry: 354,
        credentialId: 'CERT-SEC-8921',
        sha256Digest: '7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069',
        verified: true,
        instructor: 'Dr. Marcus Vance (Chief Information Security Officer)',
        accreditationBody: 'AICPA & International Organization for Standardization (ISO)',
        description:
          'Covers zero-trust access, threat modeling, cryptographic key storage, cloud security postures, automated patch cycles, and incident response SLAs.',
      },
      {
        id: 'course-eth-104',
        code: 'ETH-104',
        title: 'Workplace Harassment Prevention (California AB 1825)',
        category: 'Ethics & Legal',
        categoryKey: 'ethics',
        standard: 'California Government Code § 12950.1 / AB 1825 & SB 1343',
        duration: '2.0 Hours · Interactive Scenario',
        durationHours: 2.0,
        score: 100,
        scoreStatus: '100% · Passed',
        completionDate: 'Jul 14, 2026',
        expirationDate: 'Jul 14, 2027',
        validityMonths: 12,
        daysUntilExpiry: 327,
        credentialId: 'CERT-ETH-4412',
        sha256Digest: '3d9e8471b0219ca66219f85c18174a10df84b1fa3d677284addd200126d83120',
        verified: true,
        instructor: 'Sarah Jenkins, Esq. (Director of People & Legal Compliance)',
        accreditationBody: 'California Civil Rights Department (CRD / DFEH)',
        description:
          'Mandatory supervisory training on workplace culture, protected characteristics, bystander intervention techniques, retaliation prevention, and complaint escalations.',
      },
      {
        id: 'course-gov-302',
        code: 'GOV-302',
        title: 'Anti-Bribery & Foreign Corrupt Practices Act (FCPA)',
        category: 'Corporate Governance',
        categoryKey: 'governance',
        standard: 'US Foreign Corrupt Practices Act (15 U.S.C. § 78dd-1) & OECD Guidelines',
        duration: '1.0 Hour · Case Studies + Exam',
        durationHours: 1.0,
        score: 96,
        scoreStatus: '96% · Passed',
        completionDate: 'Apr 15, 2026',
        expirationDate: 'Apr 15, 2027',
        validityMonths: 12,
        daysUntilExpiry: 237,
        credentialId: 'CERT-GOV-3108',
        sha256Digest: '19fa4429bb56f082c18148a1d65dfc2d4b1fa3d677284addd200126d409218a1',
        verified: true,
        instructor: 'David K. Thorne (Global Ethics & Trade Compliance Counsel)',
        accreditationBody: 'U.S. Department of Justice & OECD Anti-Bribery Directorate',
        description:
          'Anti-corruption controls, third-party intermediary vetting, prohibition of facilitation payments, gift/hospitality thresholds, and internal books and records integrity.',
      },
      {
        id: 'course-reg-205',
        code: 'REG-205',
        title: 'HIPAA Security & Health Privacy Standards',
        category: 'Regulatory & Privacy',
        categoryKey: 'privacy',
        standard: '45 CFR Part 160 & Part 164 Subparts A, C, and E (ePHI Safeguards)',
        duration: '1.0 Hour · Video + Assessment',
        durationHours: 1.0,
        score: 98,
        scoreStatus: '98% · Passed',
        completionDate: 'Jun 02, 2026',
        expirationDate: 'Jun 02, 2027',
        validityMonths: 12,
        daysUntilExpiry: 285,
        credentialId: 'CERT-HPA-7729',
        sha256Digest: '55ca0982df43b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200',
        verified: true,
        instructor: 'Rachel Sterling (Healthcare Privacy & HIPAA Compliance Officer)',
        accreditationBody: 'U.S. Department of Health & Human Services (HHS OCR)',
        description:
          'Handling of Protected Health Information (PHI/ePHI), minimum necessary standard, physical and technical safeguards, Business Associate Agreements, and breach notifications.',
      },
      {
        id: 'course-fin-401',
        code: 'FIN-401',
        title: 'Insider Trading & Fair Disclosure (SEC)',
        category: 'Financial Compliance',
        categoryKey: 'finance',
        standard: 'Securities Exchange Act Rule 10b-5 & SEC Regulation Fair Disclosure (Reg FD)',
        duration: '1.0 Hour · Video + Exam',
        durationHours: 1.0,
        score: 100,
        scoreStatus: '100% · Passed',
        completionDate: 'May 19, 2026',
        expirationDate: 'May 19, 2027',
        validityMonths: 12,
        daysUntilExpiry: 271,
        credentialId: 'CERT-SEC-6043',
        sha256Digest: '91ea2d4719fa4429bb56f082c18148a1d65dfc2d4b1fa3d677284addd200126d',
        verified: true,
        instructor: 'Jonathan Meyer (Chief Legal Officer & General Counsel)',
        accreditationBody: 'U.S. Securities and Exchange Commission (SEC)',
        description:
          'Material Non-Public Information (MNPI) boundaries, pre-clearance procedures, quarterly trading blackouts, Rule 10b5-1 executive trading plans, and non-selective disclosure protocols.',
      },
    ]
    
    const filterCategories = [
      { id: 'all', label: 'All Modules', count: 5 },
      { id: 'security', label: 'Security & Privacy', count: 2 },
      { id: 'ethics', label: 'Ethics & Legal', count: 1 },
      { id: 'governance', label: 'Corporate Governance', count: 1 },
      { id: 'finance', label: 'Financial Compliance', count: 1 },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-training-compliance-tracker, [ui-training-compliance-tracker]',
      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,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiProgressComponent,
        UiSeparatorComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="training-compliance-tracker" [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-center lg:justify-between">
                <div class="space-y-3">
                  <div class="flex flex-wrap items-center gap-2">
                    <span class="text-muted-foreground text-xs font-medium"> Enterprise HRMS & Regulatory Assurance </span>
                    <span
                      ui-badge
                      wrap
                      variant="outline"
                      class="border-success/30 bg-success/10 text-success text-xs font-medium"
                    >
                      FY 2026–2027 Annual Cycle
                    </span>
                  </div>
    
                  <div>
                    <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                      Mandatory Compliance Training & Certifications
                    </h2>
                    <p class="text-muted-foreground mt-1 text-sm">
                      Official employee regulatory transcripts, verified digital credentials, and annual recertification
                      schedules.
                    </p>
                  </div>
    
                  <div class="text-muted-foreground flex flex-wrap items-center gap-3 pt-1 text-xs">
                    <div
                      class="border-border bg-muted/40 text-foreground flex items-center gap-2 rounded-md border px-2.5 py-1.5 font-medium"
                    >
                      <div
                        class="bg-primary/10 text-primary flex size-5 items-center justify-center rounded-full text-xs font-bold"
                      >
                        ER
                      </div>
                      <span>Elena Rostova · Senior Staff Engineer</span>
                    </div>
                    <div class="flex items-center gap-1.5 font-mono tabular-nums">
                      <span class="text-muted-foreground">ID:</span>
                      <span class="text-foreground font-medium">EMP-4092</span>
                    </div>
                    <ui-separator orientation="vertical" class="h-3.5" />
                    <div class="flex items-center gap-1">
                      <lucide-icon [img]="Building2" class="text-muted-foreground size-3.5" />
                      <span>Core Platform Infrastructure</span>
                    </div>
                    <ui-separator orientation="vertical" class="h-3.5" />
                    <div class="flex items-center gap-1">
                      <span>San Francisco, CA (HQ)</span>
                    </div>
                  </div>
                </div>
    
                <div class="flex flex-col gap-3 sm:flex-row sm:items-center lg:flex-col lg:items-end">
                  <div
                    class="border-success/30 bg-success/10 text-success dark:bg-success/10 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">
                      <span>100% Compliant · All 5 Annual Certifications Valid</span>
                      <span class="text-success/90 text-xs font-normal">Zero overdue requirements · Audit attested</span>
                    </div>
                  </div>
    
                  <button
                    ui-button
                    aria-label="Download attachment"
                    variant="outline"
                    size="sm"
                    class="hover:bg-accent gap-2 shadow-xs"
                    [disabled]="isDownloadingAll()"
                    (click)="handleDownloadAllPdf()"
                  >
                    @if (isDownloadingAll()) {
                      <lucide-icon [img]="RefreshCw" class="size-3.5 animate-spin" />
                    } @else {
                      <lucide-icon [img]="FileDown" class="text-foreground size-3.5" />
                    }
                    <span>{{ isDownloadingAll() ? 'Generating PDF Bundle...' : 'Download All Certificates PDF' }}</span>
                  </button>
                </div>
              </div>
    
              @if (downloadBannerMessage()) {
                <div
                  class="border-success/30 bg-success/10 text-success dark:bg-success/10 dark:text-foreground mt-4 flex items-center gap-2 rounded-md border p-2.5 text-xs transition-colors"
                >
                  <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" />
                  <span>{{ downloadBannerMessage() }}</span>
                </div>
              }
    
              @if (retakeSuccessMessage()) {
                <div
                  class="border-info/30 bg-info/10 text-info dark:bg-info/10 dark:text-foreground mt-4 flex items-center gap-2 rounded-md border p-2.5 text-xs transition-colors"
                >
                  <lucide-icon [img]="RotateCcw" class="text-info size-4 shrink-0" />
                  <span>{{ retakeSuccessMessage() }}</span>
                </div>
              }
            </div>
          </div>
    
          <!-- 4 Training Overview KPI Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <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">Completed Certifications</h3>
                <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="Award" 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"
                  >
                    5 / 5
                  </div>
                  <p class="text-muted-foreground text-xs">Modules Completed · 100% Finished</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">SOC2 & ISO Ready</span>
                    <span class="font-mono tabular-nums">5/5 passed</span>
                  </div>
                </div>
              </div>
            </div>
    
            <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 Training Hours</h3>
                <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md">
                  <lucide-icon [img]="Clock" 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"
                  >
                    6.5
                    <span class="text-muted-foreground text-sm font-normal">Hours</span>
                  </div>
                  <p class="text-muted-foreground text-xs">6.5 Hours Logged in 2026</p>
                </div>
                <div class="space-y-1">
                  <ui-progress [value]="100" class="bg-info/20 h-1.5" />
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span class="text-info font-medium">Quota: 6.0h Target</span>
                    <span class="font-mono tabular-nums">108% logged</span>
                  </div>
                </div>
              </div>
            </div>
    
            <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 Renewal Due</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">
                    April 15, 2027
                  </div>
                  <p class="text-warning text-xs font-medium">In 8 months · Earliest Renewal</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 truncate">FCPA Anti-Bribery</span>
                  <span ui-badge wrap variant="outline" class="border-warning/30 text-warning font-mono text-xs">
                    237 days
                  </span>
                </div>
              </div>
            </div>
    
            <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">Company-Wide Compliance</h3>
                <div class="bg-chart-1/10 text-chart-1 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"
                  >
                    98.4%
                  </div>
                  <p class="text-muted-foreground text-xs">98.4% Staff Certified (1,420/1,443)</p>
                </div>
                <div class="space-y-1">
                  <ui-progress [value]="98.4" class="bg-chart-1/20 h-1.5" />
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span class="text-chart-1 font-medium">+1.2% this quarter</span>
                    <span class="font-mono tabular-nums">23 pending</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Filter Toolbar & Search -->
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <div class="flex flex-wrap items-center gap-1.5">
              @for (cat of categories; 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>
    
            <div class="relative w-full sm:w-64">
              <lucide-icon
                [img]="Search"
                class="text-muted-foreground 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 courses..."
                class="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full min-w-0 rounded-md border pr-3 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none"
              />
              @if (searchQuery()) {
                <button
                  type="button"
                  class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2"
                  aria-label="Clear search"
                  (click)="searchQuery.set('')"
                >
                  <lucide-icon [img]="X" class="size-3.5" />
                </button>
              }
            </div>
          </div>
    
          <!-- Mandatory Compliance Modules 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">
                    Mandatory Compliance Modules
                  </h3>
                  <p ui-card-description class="text-muted-foreground text-xs">
                    Annual regulatory curriculum required under corporate governance and SOC 2 / ISO 27001 audit controls.
                  </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">{{ filteredCourses().length }}</span>
                    of
                    <span class="text-foreground font-mono font-medium tabular-nums">5</span> modules
                  </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-[280px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Course Title & Category
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[140px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Duration
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[140px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Score on Final Assessment
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[120px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Completion Date
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[120px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Expiration Date
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[160px] py-3 text-xs font-semibold tracking-wider uppercase"
                      >
                        Certificate Badge
                      </th>
                      <th
                        ui-table-head
                        class="text-muted-foreground min-w-[180px] py-3 text-right text-xs font-semibold tracking-wider uppercase"
                      >
                        Actions
                      </th>
                    </tr>
                  </thead>
    
                  <tbody ui-table-body>
                    @for (course of filteredCourses(); track course.id) {
                      <tr ui-table-row class="hover:bg-muted/30 transition-colors">
                        <td ui-table-cell class="py-3.5 align-middle">
                          <div class="space-y-1">
                            <div class="flex items-center gap-2">
                              <span class="text-foreground text-sm font-medium">{{ course.title }}</span>
                            </div>
                            <div class="flex flex-wrap items-center gap-2">
                              <span ui-badge wrap variant="secondary" class="text-xs font-medium">
                                {{ course.category }}
                              </span>
                              <span class="text-muted-foreground font-mono text-xs tabular-nums">
                                Code: {{ course.code }}
                              </span>
                              <span
                                class="text-muted-foreground max-w-[200px] truncate text-xs"
                                [attr.title]="course.standard"
                              >
                                {{ course.standard }}
                              </span>
                            </div>
                          </div>
                        </td>
    
                        <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]="Clock" class="text-muted-foreground size-3.5" />
                            <span>{{ course.duration }}</span>
                          </div>
                        </td>
    
                        <td ui-table-cell class="py-3.5 align-middle">
                          <div class="space-y-1">
                            <div
                              class="text-success flex items-center gap-1.5 font-mono text-sm font-semibold tabular-nums"
                            >
                              <lucide-icon [img]="CheckCircle2" class="size-3.5 shrink-0" />
                              <span>{{ course.scoreStatus }}</span>
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <span>Pass threshold: 80%</span>
                            </div>
                          </div>
                        </td>
    
                        <td ui-table-cell class="py-3.5 align-middle">
                          <div class="text-muted-foreground font-mono text-xs tabular-nums">
                            {{ course.completionDate }}
                          </div>
                        </td>
    
                        <td ui-table-cell class="py-3.5 align-middle">
                          <div class="space-y-0.5">
                            <div class="text-foreground font-mono text-xs font-medium tabular-nums">
                              {{ course.expirationDate }}
                            </div>
                            <div class="text-success font-mono text-xs tabular-nums">
                              Valid ({{ course.daysUntilExpiry }}d remaining)
                            </div>
                          </div>
                        </td>
    
                        <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 rounded-full border px-2 py-0.5 text-xs font-semibold"
                            >
                              <lucide-icon [img]="BadgeCheck" class="text-success size-3.5" />
                              <span>Verified Credential</span>
                            </div>
                            <div class="text-muted-foreground font-mono text-xs tabular-nums">
                              {{ course.credentialId }}
                            </div>
                          </div>
                        </td>
    
                        <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)="openCertificateModal(course)"
                            >
                              <lucide-icon [img]="FileCheck" class="text-muted-foreground size-3" />
                              <span>View Certificate</span>
                            </button>
    
                            <button
                              ui-button
                              variant="ghost"
                              size="xs"
                              class="text-muted-foreground hover:text-foreground h-7 gap-1 px-2 text-xs font-medium"
                              (click)="openRetakeModal(course)"
                            >
                              <lucide-icon [img]="RotateCcw" class="size-3" />
                              <span>Retake</span>
                            </button>
                          </div>
                        </td>
                      </tr>
                    }
    
                    @if (filteredCourses().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell [attr.colspan]="7" class="text-muted-foreground py-8 text-center text-xs">
                          No compliance modules match your search or filter criteria.
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
    
            <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]="Shield" class="text-success size-4 shrink-0" />
                <span>Audited & Attested by Schellman & Co., LLC for SOC 2 Type II & AICPA Security Trust Criteria.</span>
              </div>
              <div class="flex items-center gap-2 font-mono tabular-nums">
                <span>Ledger Sync: Aug 21, 2026 09:42 UTC</span>
                <span class="text-success font-medium">100% In-Policy</span>
              </div>
            </div>
          </div>
    
          <!-- Certificate Inspection Modal -->
          @if (selectedCertificate(); as cert) {
            <div
              class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs transition-opacity"
              (click)="onOverlayClick($event, 'certificate')"
            >
              <div
                class="border-border bg-card text-card-foreground relative w-full max-w-2xl overflow-hidden rounded-xl border shadow-sm"
              >
                <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-success/10 text-success flex size-6 items-center justify-center rounded-full">
                      <lucide-icon [img]="BadgeCheck" class="size-4" />
                    </div>
                    <div>
                      <h3 class="text-foreground text-xs font-semibold">Verified Compliance Credential</h3>
                      <p class="text-muted-foreground font-mono text-xs tabular-nums">
                        Credential ID: {{ cert.credentialId }}
                      </p>
                    </div>
                  </div>
    
                  <button
                    type="button"
                    class="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-md p-1 transition-colors focus-visible:ring-2 focus-visible:outline-none"
                    aria-label="Close modal"
                    (click)="closeCertificateModal()"
                  >
                    <lucide-icon [img]="X" class="size-4" />
                  </button>
                </div>
    
                <div class="space-y-5 p-6">
                  <div
                    class="border-success/30 bg-success/5 dark:bg-success/10 relative rounded-lg border-2 border-dashed p-6 text-center"
                  >
                    <div
                      class="border-success/40 bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full border shadow-xs"
                    >
                      <lucide-icon [img]="Award" class="size-6" />
                    </div>
    
                    <div class="mt-3 space-y-1">
                      <span class="text-success text-xs font-semibold tracking-wide">
                        Official Certificate of Mastery & Regulatory Compliance
                      </span>
                      <h4 class="text-foreground text-lg font-bold">{{ cert.title }}</h4>
                      <p class="text-muted-foreground text-xs">{{ cert.standard }}</p>
                    </div>
    
                    <ui-separator class="my-4" />
    
                    <div class="space-y-1.5">
                      <p class="text-muted-foreground text-xs">This certifies that</p>
                      <p class="text-foreground text-base font-bold">Elena Rostova</p>
                      <p class="text-muted-foreground text-xs">Senior Staff Engineer · EMP-4092</p>
                      <p class="text-muted-foreground/90 mx-auto max-w-lg pt-1 text-xs leading-relaxed">
                        {{ cert.description }}
                      </p>
                    </div>
    
                    <div class="border-border/60 mt-5 grid grid-cols-2 gap-3 border-t pt-4 text-left sm:grid-cols-4">
                      <div class="space-y-0.5">
                        <span class="text-muted-foreground text-xs">Exam Score</span>
                        <p class="text-success font-mono text-xs font-bold tabular-nums">{{ cert.scoreStatus }}</p>
                      </div>
                      <div class="space-y-0.5">
                        <span class="text-muted-foreground text-xs">Duration</span>
                        <p class="text-foreground font-mono text-xs font-semibold tabular-nums">
                          {{ cert.durationHours }} Hours
                        </p>
                      </div>
                      <div class="space-y-0.5">
                        <span class="text-muted-foreground text-xs">Issued Date</span>
                        <p class="text-foreground font-mono text-xs tabular-nums">{{ cert.completionDate }}</p>
                      </div>
                      <div class="space-y-0.5">
                        <span class="text-muted-foreground text-xs">Valid Until</span>
                        <p class="text-foreground font-mono text-xs font-medium tabular-nums">{{ cert.expirationDate }}</p>
                      </div>
                    </div>
                  </div>
    
                  <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>Cryptographic Digest (SHA-256)</span>
                      </div>
                      <button
                        type="button"
                        class="text-primary flex items-center gap-1 text-xs hover:underline"
                        (click)="copyToClipboard(cert.sha256Digest, 'digest')"
                      >
                        @if (copiedDigest()) {
                          <lucide-icon [img]="Check" class="text-success size-3" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-3" />
                        }
                        <span>{{ copiedDigest() ? 'Copied' : 'Copy Hash' }}</span>
                      </button>
                    </div>
                    <p class="text-muted-foreground font-mono text-xs break-all">{{ cert.sha256Digest }}</p>
                    <div
                      class="border-border/50 text-muted-foreground flex flex-wrap items-center justify-between gap-2 border-t pt-1"
                    >
                      <span>Accreditation: {{ cert.accreditationBody }}</span>
                      <span class="font-mono tabular-nums">Ledger: OpenAttestation v3</span>
                    </div>
                  </div>
                </div>
    
                <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('https://uipkge.dev/verify/' + cert.credentialId, 'url')"
                  >
                    @if (copiedVerifyUrl()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Share2" class="size-3.5" />
                    }
                    <span>{{ copiedVerifyUrl() ? '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)="closeCertificateModal()">
                      Close
                    </button>
                    <button
                      ui-button
                      aria-label="Download attachment"
                      variant="default"
                      size="sm"
                      class="gap-1.5 text-xs shadow-xs"
                      (click)="downloadFromCertificate()"
                    >
                      <lucide-icon [img]="Download" class="size-3.5" />
                      <span>Download PDF Certificate</span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- Retake Course Confirmation Modal -->
          @if (retakeModalCourse(); as retake) {
            <div
              class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs transition-opacity"
              (click)="onOverlayClick($event, 'retake')"
            >
              <div
                class="border-border bg-card text-card-foreground relative w-full max-w-lg overflow-hidden rounded-xl border shadow-sm"
              >
                <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-6 items-center justify-center rounded-full">
                      <lucide-icon [img]="RotateCcw" class="size-3.5" />
                    </div>
                    <h3 class="text-foreground text-sm font-bold">Launch Refresher Assessment</h3>
                  </div>
                  <button
                    aria-label="Close retake modal"
                    type="button"
                    class="text-muted-foreground hover:bg-accent hover:text-foreground rounded-md p-1"
                    (click)="closeRetakeModal()"
                  >
                    <lucide-icon [img]="X" class="size-4" />
                  </button>
                </div>
    
                <div class="space-y-4 p-5 text-xs">
                  <div class="space-y-1">
                    <span class="text-muted-foreground font-mono text-xs font-medium">Module: {{ retake.code }}</span>
                    <h4 class="text-foreground text-sm font-semibold">{{ retake.title }}</h4>
                  </div>
    
                  <div class="border-warning/30 bg-warning/10 text-warning rounded-md border p-3">
                    <div class="flex gap-2">
                      <lucide-icon [img]="AlertCircle" class="text-warning size-4 shrink-0" />
                      <div class="space-y-1 leading-relaxed">
                        <p class="font-semibold">Notice regarding existing valid credential:</p>
                        <p>
                          Your current passing score ({{ retake.score }}%) and verified credential ({{
                            retake.credentialId
                          }}) will remain active and valid in company compliance audits until the new assessment is
                          submitted.
                        </p>
                      </div>
                    </div>
                  </div>
    
                  <div class="border-border bg-muted/30 grid grid-cols-2 gap-2 rounded-md border p-3 font-mono">
                    <div>
                      <span class="text-muted-foreground">Estimated Time:</span>
                      <p class="text-foreground font-semibold">{{ retake.duration }}</p>
                    </div>
                    <div>
                      <span class="text-muted-foreground">Passing Threshold:</span>
                      <p class="text-success font-semibold">80% or Higher</p>
                    </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 retake modal"
                    variant="outline"
                    size="sm"
                    class="text-xs"
                    (click)="closeRetakeModal()"
                  >
                    Cancel
                  </button>
                  <button
                    ui-button
                    variant="default"
                    size="sm"
                    class="gap-1.5 text-xs shadow-xs"
                    (click)="handleConfirmRetake()"
                  >
                    <lucide-icon [img]="PlayCircle" class="size-3.5" />
                    <span>Launch Course Environment</span>
                  </button>
                </div>
              </div>
            </div>
          }
        </div>
      `,
    })
    export class UiTrainingComplianceTrackerComponent {
      protected readonly AlertCircle = AlertCircle
      protected readonly Award = Award
      protected readonly BadgeCheck = BadgeCheck
      protected readonly Building2 = Building2
      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 FileDown = FileDown
      protected readonly PlayCircle = PlayCircle
      protected readonly RefreshCw = RefreshCw
      protected readonly RotateCcw = RotateCcw
      protected readonly Search = Search
      protected readonly Share2 = Share2
      protected readonly Shield = Shield
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Users = Users
      protected readonly X = X
    
      protected readonly categories = filterCategories
    
      @Input() set defaultFilter(v: string | undefined) {
        if (v !== undefined) this.activeFilter.set(v)
      }
      @Input('class') className?: string
    
      readonly activeFilter = signal('all')
      readonly searchQuery = signal('')
      readonly selectedCertificate = signal<ComplianceCourse | null>(null)
      readonly retakeModalCourse = signal<ComplianceCourse | null>(null)
      readonly isDownloadingAll = signal(false)
      readonly downloadBannerMessage = signal<string | null>(null)
      readonly copiedCertId = signal(false)
      readonly copiedDigest = signal(false)
      readonly copiedVerifyUrl = signal(false)
      readonly retakeSuccessMessage = signal<string | null>(null)
    
      readonly filteredCourses = computed(() => {
        const filter = this.activeFilter()
        const query = this.searchQuery().trim().toLowerCase()
        return coursesData.filter((course) => {
          let matchesCategory = true
          if (filter === 'security') {
            matchesCategory = course.categoryKey === 'security' || course.categoryKey === 'privacy'
          } else if (filter === 'ethics') {
            matchesCategory = course.categoryKey === 'ethics'
          } else if (filter === 'governance') {
            matchesCategory = course.categoryKey === 'governance' || course.categoryKey === 'finance'
          } else if (filter === 'finance') {
            matchesCategory = course.categoryKey === 'finance'
          }
          if (!query) return matchesCategory
          const matchesSearch =
            course.title.toLowerCase().includes(query) ||
            course.code.toLowerCase().includes(query) ||
            course.category.toLowerCase().includes(query) ||
            course.standard.toLowerCase().includes(query) ||
            course.credentialId.toLowerCase().includes(query) ||
            course.instructor.toLowerCase().includes(query)
          return matchesCategory && matchesSearch
        })
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      filterPillClass(id: string): string {
        const isActive = this.activeFilter() === id
        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',
          isActive
            ? '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 {
        const isActive = this.activeFilter() === id
        return cn(
          'py-0.2 rounded-full px-1.5 font-mono text-xs tabular-nums',
          isActive ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground',
        )
      }
    
      handleDownloadAllPdf(): void {
        if (this.isDownloadingAll()) return
        this.isDownloadingAll.set(true)
        this.downloadBannerMessage.set('Compiling 5 verified compliance certificates into archival PDF...')
        setTimeout(() => {
          this.isDownloadingAll.set(false)
          this.downloadBannerMessage.set(
            '✓ Download complete: Elena_Rostova_Annual_Compliance_Certificates_2026.pdf (1.8 MB)',
          )
          setTimeout(() => this.downloadBannerMessage.set(null), 4500)
        }, 1600)
      }
    
      downloadFromCertificate(): void {
        this.handleDownloadAllPdf()
        this.closeCertificateModal()
      }
    
      openCertificateModal(course: ComplianceCourse): void {
        this.selectedCertificate.set(course)
        this.copiedCertId.set(false)
        this.copiedDigest.set(false)
        this.copiedVerifyUrl.set(false)
      }
    
      closeCertificateModal(): void {
        this.selectedCertificate.set(null)
      }
    
      openRetakeModal(course: ComplianceCourse): void {
        this.retakeModalCourse.set(course)
      }
    
      closeRetakeModal(): void {
        this.retakeModalCourse.set(null)
      }
    
      handleConfirmRetake(): void {
        const course = this.retakeModalCourse()
        if (!course) return
        const title = course.title
        this.closeRetakeModal()
        this.retakeSuccessMessage.set(
          `Interactive refresher environment launched for "${title}". Your existing credential remains valid.`,
        )
        setTimeout(() => this.retakeSuccessMessage.set(null), 4500)
      }
    
      copyToClipboard(text: string, type: 'certId' | 'digest' | 'url'): void {
        try {
          void navigator.clipboard?.writeText(text)
        } catch {
          // Clipboard unavailable — still show copied feedback.
        }
        if (type === 'certId') {
          this.copiedCertId.set(true)
          setTimeout(() => this.copiedCertId.set(false), 2000)
        } else if (type === 'digest') {
          this.copiedDigest.set(true)
          setTimeout(() => this.copiedDigest.set(false), 2000)
        } else if (type === 'url') {
          this.copiedVerifyUrl.set(true)
          setTimeout(() => this.copiedVerifyUrl.set(false), 2000)
        }
      }
    
      onOverlayClick(event: MouseEvent, which: 'certificate' | 'retake'): void {
        if (event.target === event.currentTarget) {
          if (which === 'certificate') this.closeCertificateModal()
          else this.closeRetakeModal()
        }
      }
    
      @HostListener('window:keydown', ['$event'])
      onKeyDown(event: KeyboardEvent): void {
        if (event.key === 'Escape') {
          if (this.selectedCertificate()) this.closeCertificateModal()
          if (this.retakeModalCourse()) this.closeRetakeModal()
        }
      }
    }
    
  • src/app/components/blocks/training-compliance-tracker/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/training-compliance-tracker.json