- DetailModal: thread tz through resolveLabel(...) module helper + default export's 3 inline date/time calls. - IntegrationStatusTabs: thread tz through fmtDate helper + VeeamTab sub-component prop. - SyncScheduler: thread tz into closure-scoped formatDate helper. - audit-log-table, user-table, user-sessions, active-sessions: inline toLocale calls in component body. - analysis-view: useUserTimezone in AnalysisView; thread tz into 4 toLocaleString calls. - resolution-trend, volume-trend (recharts): module-scope fmtDate(iso) → fmtDate(iso, tz); useUserTimezone in named export; thread tz into axis tickFormatter + tooltip labelFormatter. - ticket-detail-modal: thread tz into formatDate arrow inside TicketDetailModal. - TimelineView: useUserTimezone; thread tz into 4 toLocale*String calls (hour/day/month/event-time formatters). - ScoreCard: useUserTimezone in AggregateScoreCard; thread tz into the date-range latest call. - addigy-tab: useUserTimezone in AddigyTab; thread tz into 2 inline calls. - activity-sparkline: module-scope fmtHour(iso) → fmtHour(iso, tz); useUserTimezone in ActivitySparkline; update 3 callsites in title/aria. - compliance-detail-table: thread tz from ComplianceDetailTable into ContractCoverageModal sub-component (2 inline date calls). - company-backup-detail: module-scope formatDate(d) → formatDate(d, tz); useUserTimezone in CompanyBackupDetail; update 3 callsites. Migrates 31 of 81 audit leak callsites.
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
/* ActivitySparkline — 24-bucket success/failure strip for a worker.
|
|
*
|
|
* Each column is one hour of activity. Successes stack from the top
|
|
* down in brand blue; failures stack from the top down in destructive
|
|
* red over the success column so the worst hours read first. Heights
|
|
* scale to the loudest hour in the series so a quiet worker still
|
|
* shows shape.
|
|
*
|
|
* No tooltip — hover-title gives the count. At 24px tall this is a
|
|
* stacked bar histogram, not a line chart, so absolute counts read
|
|
* directly. */
|
|
|
|
'use client';
|
|
|
|
import { cn } from '@/lib/utils';
|
|
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
|
|
|
interface ActivityBucket {
|
|
hour: string;
|
|
success: number;
|
|
failure: number;
|
|
}
|
|
|
|
interface ActivitySparklineProps {
|
|
data: ActivityBucket[];
|
|
className?: string;
|
|
height?: number;
|
|
}
|
|
|
|
function fmtHour(iso: string, tz: string): string {
|
|
return new Date(iso).toLocaleTimeString(undefined, {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
timeZone: tz,
|
|
});
|
|
}
|
|
|
|
export function ActivitySparkline({
|
|
data,
|
|
className,
|
|
height = 32,
|
|
}: ActivitySparklineProps) {
|
|
const tz = useUserTimezone();
|
|
if (data.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const max = Math.max(1, ...data.map((d) => d.success + d.failure));
|
|
|
|
return (
|
|
<div className={cn('flex items-end gap-px w-full', className)} style={{ height }}>
|
|
{data.map((bucket, i) => {
|
|
const total = bucket.success + bucket.failure;
|
|
const totalPct = (total / max) * 100;
|
|
const failPct = total > 0 ? (bucket.failure / total) * 100 : 0;
|
|
const succPct = 100 - failPct;
|
|
const empty = total === 0;
|
|
return (
|
|
<span
|
|
key={bucket.hour}
|
|
title={
|
|
empty
|
|
? `${fmtHour(bucket.hour, tz)} · idle`
|
|
: `${fmtHour(bucket.hour, tz)} · ${bucket.success} ok · ${bucket.failure} fail`
|
|
}
|
|
aria-label={`${fmtHour(bucket.hour, tz)}: ${bucket.success} ok, ${bucket.failure} fail`}
|
|
className="relative flex-1 min-w-[1px] flex flex-col-reverse rounded-[1px] overflow-hidden"
|
|
style={{ height: `${empty ? 12 : Math.max(totalPct, 8)}%` }}
|
|
data-bucket-index={i}
|
|
>
|
|
{/* Success segment (bottom) */}
|
|
{bucket.success > 0 && (
|
|
<span
|
|
className="bg-primary/70"
|
|
style={{ height: `${succPct}%` }}
|
|
/>
|
|
)}
|
|
{/* Failure segment (top) */}
|
|
{bucket.failure > 0 && (
|
|
<span
|
|
className="bg-destructive"
|
|
style={{ height: `${failPct}%` }}
|
|
/>
|
|
)}
|
|
{/* Idle hour — render a thin baseline */}
|
|
{empty && (
|
|
<span className="bg-border/60 h-px self-end w-full" />
|
|
)}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|