Training Compliance Tracker
blockhrAnnual mandatory employee compliance training dashboard with certification badges, renewal deadlines, assessment scorecards, verified credential preview modal, and certificate export controls.
Installation
$pnpm dlx uipkge-ng@latest add training-compliance-tracker$npx uipkge-ng@latest add training-compliance-tracker$yarn dlx uipkge-ng@latest add training-compliance-tracker$bunx uipkge-ng@latest add training-compliance-trackerManifest:
https://uipkge.dev/r/angular/training-compliance-tracker.jsonInstalls to:src/app/components/blocks/training-compliance-tracker/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.
ComplianceCourseinterface 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
export { UiTrainingComplianceTrackerComponent, type ComplianceCourse } from './training-compliance-tracker.component'
Raw manifest:https://uipkge.dev/r/angular/training-compliance-tracker.json