Cold Chain Temp Monitor
blocklogisticsRefrigerated pharmaceutical & perishable cargo IoT temperature logger and excursion monitoring with multi-reading time-series trend chart, upper/lower threshold guidelines, probe logging history table, and critical excursion alert protocol.
Installation
$pnpm dlx uipkge-ng@latest add cold-chain-temp-monitor$npx uipkge-ng@latest add cold-chain-temp-monitor$yarn dlx uipkge-ng@latest add cold-chain-temp-monitor$bunx uipkge-ng@latest add cold-chain-temp-monitorManifest:
https://uipkge.dev/r/angular/cold-chain-temp-monitor.jsonInstalls to:src/app/components/blocks/cold-chain-temp-monitor/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.
LogEntryinterface LogEntry {
id: string
timestamp: string
timeAgo: string
internalTemp: number
ambientTemp: number
deltaT: number
probeLocation: string
probeId: string
humidity: number
status: 'optimal' | 'in-spec' | 'warning' | 'breach'
isLatest?: boolean
}ChartReadinginterface ChartReading {
time: string
hourLabel: string
internalTemp: number
ambientTemp: number
milestone: string
}Files installed (2)
src/app/components/blocks/cold-chain-temp-monitor/cold-chain-temp-monitor.component.ts39 kB
// Refrigerated pharmaceutical & perishable cargo IoT temperature logger and excursion // monitoring with multi-reading time-series trend chart, upper/lower threshold // guidelines, probe logging history table, and critical excursion alert protocol. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, Input, signal } from '@angular/core' import { Activity, Battery, Bell, Bluetooth, CheckCircle2, Download, Droplets, Flame, LucideAngularModule, PhoneCall, ShieldAlert, ShieldCheck, Snowflake, ThermometerSnowflake, ThermometerSun, } 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, 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 LogEntry { id: string timestamp: string timeAgo: string internalTemp: number ambientTemp: number deltaT: number probeLocation: string probeId: string humidity: number status: 'optimal' | 'in-spec' | 'warning' | 'breach' isLatest?: boolean } export interface ChartReading { time: string hourLabel: string internalTemp: number ambientTemp: number milestone: string } const defaultChartData: ChartReading[] = [ { time: '00:00', hourLabel: '00:00 UTC', internalTemp: -19.8, ambientTemp: 18.2, milestone: 'FRA Departure Tarmac' }, { time: '03:00', hourLabel: '03:00 UTC', internalTemp: -20.4, ambientTemp: -4.5, milestone: 'Flight EK046 Climb' }, { time: '06:00', hourLabel: '06:00 UTC', internalTemp: -20.8, ambientTemp: -6.2, milestone: 'Cruise Flight Level 380', }, { time: '09:00', hourLabel: '09:00 UTC', internalTemp: -20.1, ambientTemp: 12.4, milestone: 'DXB Descent & Approach', }, { time: '12:00', hourLabel: '12:00 UTC', internalTemp: -19.2, ambientTemp: 38.6, milestone: 'DXB Ramp Tarmac Transfer', }, { time: '15:00', hourLabel: '15:00 UTC', internalTemp: -19.6, ambientTemp: 32.0, milestone: 'Perishable Hub Staging', }, { time: '18:00', hourLabel: '18:00 UTC', internalTemp: -19.8, ambientTemp: 26.5, milestone: 'Active Reefer Bay #4' }, { time: '21:00', hourLabel: '21:00 UTC', internalTemp: -19.5, ambientTemp: 25.0, milestone: 'Pre-Connection QA Inspection', }, { time: '24:00', hourLabel: '14:30 UTC', internalTemp: -19.4, ambientTemp: 24.8, milestone: 'Dubai Hub Terminal 2' }, ] const defaultLogs: LogEntry[] = [ { id: 'log-1', timestamp: 'Aug 21, 14:30 UTC', timeAgo: '24s ago', internalTemp: -19.4, ambientTemp: 24.8, deltaT: 44.2, probeLocation: 'Pallet Center Core', probeId: 'Probe #A1', humidity: 18, status: 'optimal', isLatest: true, }, { id: 'log-2', timestamp: 'Aug 21, 14:00 UTC', timeAgo: '30m ago', internalTemp: -19.5, ambientTemp: 25.2, deltaT: 44.7, probeLocation: 'Upper Cargo Lid', probeId: 'Probe #B2', humidity: 18, status: 'optimal', }, { id: 'log-3', timestamp: 'Aug 21, 13:30 UTC', timeAgo: '1h ago', internalTemp: -19.2, ambientTemp: 28.4, deltaT: 47.6, probeLocation: 'Lower Ventral Core', probeId: 'Probe #C1', humidity: 19, status: 'in-spec', }, { id: 'log-4', timestamp: 'Aug 21, 13:00 UTC', timeAgo: '1.5h ago', internalTemp: -19.8, ambientTemp: 36.1, deltaT: 55.9, probeLocation: 'Pallet Center Core', probeId: 'Probe #A1', humidity: 20, status: 'optimal', }, { id: 'log-5', timestamp: 'Aug 21, 12:30 UTC', timeAgo: '2h ago', internalTemp: -20.1, ambientTemp: 38.6, deltaT: 58.7, probeLocation: 'Outer Thermal Jacket', probeId: 'Probe #D4', humidity: 19, status: 'optimal', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cold-chain-temp-monitor, [ui-cold-chain-temp-monitor]', 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, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="cold-chain-temp-monitor" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="flex items-center gap-2"> <div class="border-info/30 bg-info/10 text-info text-info flex size-9 items-center justify-center rounded-lg border shadow-xs" > <lucide-icon [img]="Snowflake" class="size-5" /> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> {{ shipmentTitle }} </h1> </div> <!-- Temperature Class Badge in Sky Blue --> <span ui-badge wrap variant="outline" class="border-info/30 bg-info/10 text-info gap-1.5 text-xs font-medium" > <span class="bg-info size-1.5 rounded-full"></span> <span>{{ tempClass }}</span> </span> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Bluetooth" class="text-primary size-3.5" /> <span class="text-foreground font-medium">{{ beaconId }}</span> </div> <span>·</span> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Battery" class="text-success size-3.5" /> <span class="text-foreground font-medium tabular-nums">{{ batteryPercent }}%</span> <span>({{ batteryDays }} days remaining)</span> </div> <span>·</span> <div class="flex items-center gap-1.5"> <span class="relative flex size-2"> <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span> <span class="bg-success relative inline-flex size-2 rounded-full"></span> </span> <span class="text-foreground font-medium">Live Telemetry</span> <span>· DXB Terminal 2 Transit</span> </div> </div> </div> <!-- Action Button --> <div class="flex flex-wrap items-center gap-2.5"> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="gap-2 text-xs font-medium shadow-xs" [disabled]="isExporting()" (click)="handleExportCSV()" > <lucide-icon [img]="Download" [class]="exportIconClass" /> <span>{{ isExporting() ? 'Exporting...' : 'Export Audit Log CSV' }}</span> </button> </div> </div> <!-- Export Success Banner --> @if (exportNotification()) { <div class="border-border bg-card text-card-foreground flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs shadow-xs" role="status" > <div class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> <span class="text-foreground font-medium"> Cold-chain telemetry audit log exported successfully with cryptographic SHA-256 chain-of-custody seal. </span> </div> <button ui-button aria-label="Dismiss notification" variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-6" (click)="exportNotification.set(false)" > <span class="text-xs">✕</span> </button> </div> } <!-- 4 Telemetry Overview Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- 1. Current Internal Temperature --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="border-info/20 bg-info/10 text-info flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="ThermometerSnowflake" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Internal Core Temp</h3> </div> <span ui-badge wrap variant="success" class="shrink-0 text-xs">In Target Zone</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums"> {{ formatTemp(currentInternalTemp) }}°C </span> <span class="text-muted-foreground text-xs font-normal">Payload</span> </div> <span class="text-success text-success text-xs font-medium tabular-nums">±0.6°C setpoint</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Target Corridor:</span> <span class="text-foreground font-medium tabular-nums">-22.0°C to -18.0°C</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">Probe #A1 (Pallet Center Core)</p> </div> </div> </div> <!-- 2. Ambient External Temperature --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="border-warning/20 bg-warning/10 text-warning flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="ThermometerSun" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Ambient External</h3> </div> <span ui-badge wrap variant="outline" class="shrink-0 text-xs">Dubai Tarmac</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums"> {{ formatTemp(currentAmbientTemp) }}°C </span> <span class="text-muted-foreground text-xs font-normal">Air Temp</span> </div> <div class="text-muted-foreground flex items-center gap-1 text-xs font-medium tabular-nums"> <lucide-icon [img]="Flame" class="text-warning size-3" /> <span>ΔT 44.2°C</span> </div> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Transfer Node:</span> <span class="text-foreground font-medium">DXB Cargo T2</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">VIP Cryo-Insulation Envelope Active</p> </div> </div> </div> <!-- 3. Relative Humidity --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-teal-500/20 bg-teal-500/10 text-teal-600 dark:text-teal-400" > <lucide-icon [img]="Droplets" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Cargo Humidity</h3> </div> <span ui-badge wrap variant="success" class="shrink-0 text-xs">Optimal Dry</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums" >{{ currentHumidity }}%</span > <span class="text-muted-foreground text-xs font-normal">RH</span> </div> <span class="text-xs font-medium text-teal-600 dark:text-teal-400">Zero Condensation</span> </div> <div class="space-y-1"> <div ui-progress [value]="currentHumidity" class="h-1.5 [&_[data-slot=progress-indicator]]:bg-teal-500" ></div> <div class="text-muted-foreground flex items-center justify-between gap-x-2 text-xs"> <span>Dew Point: <span class="text-foreground font-medium tabular-nums">-32.4°C</span></span> <span>Spec < 35%</span> </div> </div> </div> </div> <!-- 4. Total Excursions / Breach Events --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2.5"> <div class="border-success/20 bg-success/10 text-success flex size-8 shrink-0 items-center justify-center rounded-lg border" > <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> <h3 ui-card-title class="truncate text-sm font-medium">Excursion Events</h3> </div> <span ui-badge wrap variant="success" class="shrink-0 text-xs">100% Compliant</span> </div> </div> <div ui-card-content class="space-y-2.5 pt-1"> <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums">{{ totalExcursions }}</span> <span class="text-muted-foreground text-xs font-normal">Breaches</span> </div> <span class="text-success text-success text-xs font-medium tabular-nums"> {{ monitoringHours }}h in-transit </span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Mean Kinetic Temp (MKT):</span> <span class="text-foreground font-medium tabular-nums">-19.7°C</span> </div> <p class="text-muted-foreground mt-0.5 text-xs">0 cumulative seconds out-of-spec</p> </div> </div> </div> </div> <!-- 24-Hour Temperature Curve / Telemetry Graph --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <lucide-icon [img]="Activity" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">24-Hour Payload & Ambient Telemetry Curve</h3> </div> <p ui-card-description class="text-xs"> Continuous thermal gradient monitoring comparing internal core payload against ambient flight & tarmac temperatures with strict excursion threshold corridors. </p> </div> <!-- Legend & Timeframe --> <div class="flex flex-wrap items-center gap-3"> <div class="text-muted-foreground flex flex-wrap items-center gap-3 text-xs"> <div class="flex items-center gap-1.5"> <span class="bg-info size-2 rounded-full"></span> <span class="text-foreground font-medium">Internal Core</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-warning size-2 rounded-full"></span> <span>Ambient Air</span> </div> <div class="flex items-center gap-1.5"> <span class="border-destructive bg-destructive h-0.5 w-3 border-b border-dashed"></span> <span>Upper Limit (-18°C)</span> </div> <div class="flex items-center gap-1.5"> <span class="border-info bg-info h-0.5 w-3 border-b border-dashed"></span> <span>Lower Limit (-22°C)</span> </div> </div> <div class="bg-muted border-border/60 flex items-center rounded-lg border p-0.5 text-xs"> @for (tf of timeframes; track tf) { <button type="button" [class]="timeframeClass(tf)" (click)="activeTimeframe.set(tf)"> {{ tf }} </button> } </div> </div> </div> </div> <div ui-card-content class="space-y-4"> <!-- SVG Chart Area --> <div class="bg-muted/20 border-border/60 relative w-full overflow-x-auto rounded-lg border p-2 sm:p-4"> <svg class="h-64 w-full max-w-[600px] min-w-full" viewBox="0 0 740 220" preserveAspectRatio="none"> <defs> <linearGradient id="grad-internal-angular" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.25" /> <stop offset="100%" stop-color="var(--color-sky-500, #0ea5e9)" stop-opacity="0.0" /> </linearGradient> </defs> <!-- Target Corridor Shaded Band (-18°C [y=65] to -22°C [y=155]) --> <rect x="40" y="65" width="660" height="90" fill="var(--color-sky-500, #0ea5e9)" fill-opacity="0.06" rx="4" /> <!-- Grid Lines --> <line x1="40" y1="30" x2="700" y2="30" stroke="currentColor" class="text-border/40" stroke-width="1" /> <line x1="40" y1="65" x2="700" y2="65" stroke="var(--color-rose-500, #f43f5e)" stroke-width="1.5" stroke-dasharray="4 4" /> <line x1="40" y1="110" x2="700" y2="110" stroke="var(--color-emerald-500, #10b981)" stroke-width="1" stroke-dasharray="2 3" stroke-opacity="0.6" /> <line x1="40" y1="155" x2="700" y2="155" stroke="var(--color-sky-600, #0284c7)" stroke-width="1.5" stroke-dasharray="4 4" /> <line x1="40" y1="200" x2="700" y2="200" stroke="currentColor" class="text-border/40" stroke-width="1" /> <!-- Y-Axis Threshold Labels --> <text x="36" y="69" text-anchor="end" class="fill-destructive text-xs font-medium">-18°C (Max)</text> <text x="36" y="113" text-anchor="end" class="dark:fill-success fill-emerald-600 text-xs"> -20°C (Set) </text> <text x="36" y="159" text-anchor="end" class="dark:fill-info fill-sky-600 text-xs font-medium"> -22°C (Min) </text> <!-- Ambient Temperature Curve (Dashed Amber) --> <path [attr.d]="ambientPath()" fill="none" stroke="var(--color-amber-500, #f59e0b)" stroke-width="2" stroke-dasharray="3 3" stroke-linecap="round" /> <!-- Internal Temperature Gradient Area & Curve (Solid Sky) --> <path [attr.d]="internalArea()" fill="url(#grad-internal-angular)" /> <path [attr.d]="internalPath()" fill="none" stroke="var(--color-sky-500, #0ea5e9)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" /> <!-- Data Point Markers --> @for (pt of chartPoints(); track $index) { <g> <!-- Internal Point --> <circle [attr.cx]="pt.x" [attr.cy]="pt.yInternal" r="3.5" class="stroke-background fill-info cursor-pointer stroke-2 transition-transform hover:scale-125" (mouseenter)="selectedReadingIndex.set($index)" /> <!-- Ambient Point --> <circle [attr.cx]="pt.x" [attr.cy]="pt.yAmbient" r="2.5" class="stroke-background fill-warning stroke-1" /> </g> } </svg> <!-- X-Axis Timeline Milestones --> <div class="mt-2 grid grid-cols-3 gap-2 text-xs sm:grid-cols-5 lg:grid-cols-9"> @for (pt of chartPoints(); track $index) { <div [class]="milestoneClass($index)"> <p class="text-foreground font-mono text-xs font-semibold tabular-nums">{{ pt.time }}</p> <p class="text-muted-foreground truncate text-xs">{{ pt.milestone }}</p> <p class="text-info text-info font-mono text-xs font-medium tabular-nums"> {{ formatTemp(pt.internalTemp) }}°C </p> </div> } </div> </div> <!-- Metric Summary Strip --> <div class="grid grid-cols-2 gap-3 text-xs sm:grid-cols-4 lg:grid-cols-5"> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Min Internal Temp</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">-20.8°C</p> <span class="text-muted-foreground text-xs">At cruise altitude</span> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Max Internal Temp</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">-19.2°C</p> <span class="text-muted-foreground text-xs">During DXB ramp transfer</span> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">24h Mean Temp</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">-19.7°C</p> <span class="text-success text-xs font-medium">Optimal Setpoint</span> </div> <div class="border-border bg-muted/30 rounded-lg border p-2.5"> <span class="text-muted-foreground">Standard Deviation</span> <p class="text-foreground font-mono text-sm font-semibold tabular-nums">±0.4°C</p> <span class="text-muted-foreground text-xs">Tight thermal hold</span> </div> <div class="border-border bg-muted/30 col-span-2 rounded-lg border p-2.5 sm:col-span-4 lg:col-span-1"> <span class="text-muted-foreground">Peak External Heat</span> <p class="text-warning text-warning font-mono text-sm font-semibold tabular-nums">+38.6°C</p> <span class="text-muted-foreground text-xs">DXB Tarmac 12:00 UTC</span> </div> </div> </div> </div> <!-- Temperature Logging History Table & Protocol Card --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-3"> <!-- Table: 2 cols on lg --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-2"> <div ui-card-header class="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-base font-semibold">Temperature Logging History</h3> <p ui-card-description class="text-xs"> Calibrated multi-probe sensor telemetry pings across core pallet positions. </p> </div> <span ui-badge wrap variant="outline" class="w-fit text-xs font-normal tabular-nums"> {{ activeLogs().length }} Recent Pings </span> </div> </div> <div ui-card-content class="p-0 sm:p-6 sm:pt-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="min-w-[140px]">Timestamp</th> <th ui-table-head class="min-w-[110px]">Internal Temp</th> <th ui-table-head class="min-w-[100px]">Ambient Air</th> <th ui-table-head class="min-w-[90px]">Delta-T</th> <th ui-table-head class="min-w-[150px]">Probe Location</th> <th ui-table-head class="min-w-[70px]">RH %</th> <th ui-table-head class="min-w-[90px] text-right">Status</th> </tr> </thead> <tbody ui-table-body> @for (log of activeLogs(); track log.id) { <tr ui-table-row [class]="log.isLatest ? 'bg-muted/30 font-medium' : ''"> <td ui-table-cell class="text-foreground text-xs whitespace-nowrap"> <div class="flex items-center gap-1.5"> <span class="tabular-nums">{{ log.timestamp }}</span> @if (log.isLatest) { <span ui-badge wrap variant="secondary" class="h-4 px-1 text-xs font-normal">Now</span> } </div> </td> <td ui-table-cell class="text-xs whitespace-nowrap"> <span class="text-info text-info font-semibold tabular-nums"> {{ formatTemp(log.internalTemp) }}°C </span> </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap tabular-nums"> {{ formatTemp(log.ambientTemp) }}°C </td> <td ui-table-cell class="text-foreground text-xs font-medium whitespace-nowrap tabular-nums"> {{ log.deltaT }}°C </td> <td ui-table-cell class="text-xs whitespace-nowrap"> <span class="text-foreground">{{ log.probeLocation }}</span> <span class="text-muted-foreground ml-1 font-mono text-xs">({{ log.probeId }})</span> </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap tabular-nums"> {{ log.humidity }}% </td> <td ui-table-cell class="text-right"> <span ui-badge wrap [variant]="statusVariant(log.status)" class="text-xs capitalize" > {{ log.status }} </span> </td> </tr> } </tbody> </table> </div> </div> </div> <!-- Critical Excursion Alert Protocol Card --> <div ui-card class="border-border bg-card text-card-foreground shadow-xs lg:col-span-1"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <lucide-icon [img]="Bell" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Excursion Alert Protocol</h3> </div> <span ui-badge wrap variant="success" class="gap-1 text-xs"> <lucide-icon [img]="ShieldAlert" class="size-3" /> <span>Armed</span> </span> </div> <p ui-card-description class="text-xs"> Automated escalation rules and dry ice reserve endurance monitoring. </p> </div> <div ui-card-content class="space-y-4"> <!-- Escalation Rule 1 --> <div class="border-border/70 bg-muted/30 space-y-1.5 rounded-lg border p-3 text-xs"> <div class="flex items-center justify-between gap-x-2"> <span class="text-foreground font-semibold">Urgent SMS / Pager Alert Trigger:</span> <span ui-badge wrap variant="destructive" class="font-mono text-xs">> -15.0°C</span> </div> <p class="text-muted-foreground leading-relaxed"> Triggers priority emergency dispatch if payload core temperature rises above -15.0°C for ≥ 30 consecutive minutes. </p> </div> <!-- Cryogenic Dry Ice Reserve --> <div class="space-y-2 text-xs"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Snowflake" class="text-info size-3.5" /> <span class="text-foreground font-medium">Dry Ice Solid CO2 Reserve</span> </div> <span class="text-foreground font-mono font-semibold tabular-nums" >{{ dryIceEnduranceHours() }}h hold</span > </div> <div ui-progress [value]="dryIceCapacityPercent()" class="[&_[data-slot=progress-indicator]]:bg-info h-2" ></div> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Sublimation: 0.38 kg/hr</span> <span>42 kg solid reserve</span> </div> </div> <div ui-separator></div> <!-- Emergency Response Logistics Desk --> <div class="border-border bg-muted/40 space-y-2 rounded-lg border p-3 text-xs"> <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <lucide-icon [img]="PhoneCall" class="text-primary size-3.5" /> <span class="text-foreground font-semibold">Rapid Intervention Desk:</span> </div> <span class="text-foreground font-medium">DXB Hub Team</span> </div> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Emergency Pager:</span> <span class="text-foreground font-mono font-medium">+971 4 216 4890</span> </div> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Re-Icing Intervention SLA:</span> <span class="text-success font-medium">< 20 minutes</span> </div> <div class="text-muted-foreground border-border/50 flex items-center justify-between gap-x-2 border-t pt-1.5" > <span>Standard Operating SOP:</span> <span class="text-foreground font-mono">SOP-BIO-904</span> </div> </div> </div> </div> </div> </div> `, }) export class UiColdChainTempMonitorComponent { protected readonly Activity = Activity protected readonly Battery = Battery protected readonly Bell = Bell protected readonly Bluetooth = Bluetooth protected readonly CheckCircle2 = CheckCircle2 protected readonly Download = Download protected readonly Droplets = Droplets protected readonly Flame = Flame protected readonly PhoneCall = PhoneCall protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly Snowflake = Snowflake protected readonly ThermometerSnowflake = ThermometerSnowflake protected readonly ThermometerSun = ThermometerSun protected readonly timeframes = ['24h', '48h', '96h'] as const @Input() shipmentId = '#CC-9482' @Input() shipmentTitle = 'Vaccine Cold-Chain Consignment #CC-9482' @Input() tempClass = 'Ultra-Cold Frozen · Target: -20°C ± 2°C' @Input() beaconId = 'SensorTag #BT-4892 · BLE 5.3' @Input() protocolVersion = 'IATA CEIV / WHO-PQS Standard' @Input() batteryPercent = 94 @Input() batteryDays = 180 @Input() currentInternalTemp = -19.4 @Input() currentAmbientTemp = 24.8 @Input() currentHumidity = 18 @Input() totalExcursions = 0 @Input() monitoringHours = 96 @Input() set logs(v: LogEntry[] | undefined) { this.logsOverride.set(v) } @Input() set chartData(v: ChartReading[] | undefined) { this.chartDataOverride.set(v) } @Input('class') className?: string private readonly logsOverride = signal<LogEntry[] | undefined>(undefined) private readonly chartDataOverride = signal<ChartReading[] | undefined>(undefined) readonly activeLogs = computed(() => this.logsOverride() ?? defaultLogs) readonly activeChartData = computed(() => this.chartDataOverride() ?? defaultChartData) readonly isExporting = signal(false) readonly exportNotification = signal(false) readonly activeTimeframe = signal<'24h' | '48h' | '96h'>('24h') readonly selectedReadingIndex = signal<number | null>(null) readonly dryIceEnduranceHours = signal(72) readonly dryIceCapacityPercent = signal(78) private exportTimers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => this.exportTimers.forEach((t) => clearTimeout(t))) } handleExportCSV(): void { this.isExporting.set(true) this.exportTimers.push( setTimeout(() => { this.isExporting.set(false) this.exportNotification.set(true) this.exportTimers.push( setTimeout(() => { this.exportNotification.set(false) }, 4500), ) }, 500), ) } // Coordinate mapping for SVG telemetry chart // Internal Temp scale: -24°C (Y: 200) to -16°C (Y: 20) // Setpoint -20°C -> Y: 110 // Upper threshold -18°C -> Y: 65 // Lower threshold -22°C -> Y: 155 readonly chartPoints = computed(() => { const data = this.activeChartData() const svgWidth = 740 const paddingX = 40 const usableWidth = svgWidth - paddingX * 2 return data.map((d, i) => { const x = paddingX + (i / (data.length - 1)) * usableWidth // Map -24°C..-16°C to 195..25 // formula: y = 110 - (temp - (-20)) * 22.5 const yInternal = 110 - (d.internalTemp - -20) * 22.5 // Map ambient -10°C..+45°C to 200..30 const yAmbient = 180 - ((d.ambientTemp - -10) / 55) * 150 return { x, yInternal, yAmbient, ...d, } }) }) readonly internalPath = computed(() => { const pts = this.chartPoints() if (!pts.length) return '' let path = `M ${pts[0].x},${pts[0].yInternal}` for (let i = 1; i < pts.length; i++) { const prev = pts[i - 1] const curr = pts[i] const cx = (prev.x + curr.x) / 2 path += ` C ${cx},${prev.yInternal} ${cx},${curr.yInternal} ${curr.x},${curr.yInternal}` } return path }) readonly internalArea = computed(() => { const pts = this.chartPoints() if (!pts.length) return '' const firstX = pts[0].x const lastX = pts[pts.length - 1].x return `${this.internalPath()} L ${lastX},210 L ${firstX},210 Z` }) readonly ambientPath = computed(() => { const pts = this.chartPoints() if (!pts.length) return '' let path = `M ${pts[0].x},${pts[0].yAmbient}` for (let i = 1; i < pts.length; i++) { const prev = pts[i - 1] const curr = pts[i] const cx = (prev.x + curr.x) / 2 path += ` C ${cx},${prev.yAmbient} ${cx},${curr.yAmbient} ${curr.x},${curr.yAmbient}` } return path }) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } get exportIconClass(): string { return cn('size-3.5', this.isExporting() && 'animate-bounce') } formatTemp(value: number): string { return value > 0 ? `+${value}` : `${value}` } statusVariant(status: LogEntry['status']): 'success' | 'outline' | 'warning' { if (status === 'optimal') return 'success' if (status === 'in-spec') return 'outline' return 'warning' } timeframeClass(tf: '24h' | '48h' | '96h'): string { return cn( 'rounded-md px-2.5 py-1 font-medium transition-colors', this.activeTimeframe() === tf ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } milestoneClass(idx: number): string { return cn( 'rounded p-1 text-center transition-colors', this.selectedReadingIndex() === idx && 'bg-muted border-border/80 border', ) } }src/app/components/blocks/cold-chain-temp-monitor/index.ts0.1 kB
export { UiColdChainTempMonitorComponent, type LogEntry, type ChartReading } from './cold-chain-temp-monitor.component'
Raw manifest:https://uipkge.dev/r/angular/cold-chain-temp-monitor.json