feat(07.1-05): user-tz on shared client components

- 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.
This commit is contained in:
lorentz 2026-05-07 08:43:27 -04:00
parent 91b876310e
commit 8f955a0ff9
17 changed files with 94 additions and 53 deletions

View file

@ -22,6 +22,7 @@ import {
paletteClass, paletteClass,
} from '@/lib/status-registry'; } from '@/lib/status-registry';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
// ── Live lookup types (fetched from DB) ─────────────────────────────────────── // ── Live lookup types (fetched from DB) ───────────────────────────────────────
@ -132,7 +133,7 @@ const COMPANY_GROUPS: FieldGroup[] = [
// ── Helpers ──────────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────────
function resolveLabel(key: string, value: any, type: FieldType | undefined, lookups: Lookups): { display: React.ReactNode; isEmpty: boolean } { function resolveLabel(key: string, value: any, type: FieldType | undefined, lookups: Lookups, tz: string): { display: React.ReactNode; isEmpty: boolean } {
if (value === null || value === undefined || value === '') { if (value === null || value === undefined || value === '') {
return { display: <span className="text-muted-foreground/70 italic text-xs"></span>, isEmpty: true }; return { display: <span className="text-muted-foreground/70 italic text-xs"></span>, isEmpty: true };
} }
@ -157,7 +158,7 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look
display: ( display: (
<span className="inline-flex items-center gap-1.5 text-sm"> <span className="inline-flex items-center gap-1.5 text-sm">
<Calendar className="w-3.5 h-3.5 text-muted-foreground" /> <Calendar className="w-3.5 h-3.5 text-muted-foreground" />
{d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} {d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz })}
</span> </span>
), ),
isEmpty: false, isEmpty: false,
@ -246,7 +247,7 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look
} }
if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/)) { if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/)) {
return resolveLabel(key, value, 'date', lookups); return resolveLabel(key, value, 'date', lookups, tz);
} }
return { display: <span className="text-sm">{String(value)}</span>, isEmpty: false }; return { display: <span className="text-sm">{String(value)}</span>, isEmpty: false };
@ -271,6 +272,7 @@ interface DetailModalProps {
} }
export default function DetailModal({ open, onOpenChange, title, data, fields }: DetailModalProps) { export default function DetailModal({ open, onOpenChange, title, data, fields }: DetailModalProps) {
const tz = useUserTimezone();
const [copiedField, setCopiedField] = useState<string | null>(null); const [copiedField, setCopiedField] = useState<string | null>(null);
const [lookups, setLookups] = useState<Lookups>(EMPTY_LOOKUPS); const [lookups, setLookups] = useState<Lookups>(EMPTY_LOOKUPS);
const [lookupsLoading, setLookupsLoading] = useState(false); const [lookupsLoading, setLookupsLoading] = useState(false);
@ -427,7 +429,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
<div className="flex flex-wrap gap-x-6 gap-y-3 px-4 py-3"> <div className="flex flex-wrap gap-x-6 gap-y-3 px-4 py-3">
{visibleFields.map((field) => { {visibleFields.map((field) => {
const value = data[field.key]; const value = data[field.key];
const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz);
if (isEmpty) return null; if (isEmpty) return null;
return ( return (
<div key={field.key} className="flex items-center gap-1.5"> <div key={field.key} className="flex items-center gap-1.5">
@ -457,7 +459,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
<div className="rounded-lg border overflow-hidden"> <div className="rounded-lg border overflow-hidden">
{fields.map((field, idx) => { {fields.map((field, idx) => {
const value = data[field.key]; const value = data[field.key];
const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz);
const stringValue = value !== null && value !== undefined ? String(value) : ''; const stringValue = value !== null && value !== undefined ? String(value) : '';
return ( return (
<div key={field.key}> <div key={field.key}>
@ -504,7 +506,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
<div className="rounded-lg border overflow-hidden"> <div className="rounded-lg border overflow-hidden">
{visibleFields.map((field, idx) => { {visibleFields.map((field, idx) => {
const value = data[field.key]; const value = data[field.key];
const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups); const { display, isEmpty } = resolveLabel(field.key, value, field.type, lookups, tz);
const stringValue = value !== null && value !== undefined ? String(value) : ''; const stringValue = value !== null && value !== undefined ? String(value) : '';
return ( return (
<div key={field.key}> <div key={field.key}>
@ -607,7 +609,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{entry.entry_date && ( {entry.entry_date && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground"> <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
{new Date(entry.entry_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} {new Date(entry.entry_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', timeZone: tz })}
</span> </span>
)} )}
</div> </div>
@ -653,9 +655,9 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{note.create_date_time && ( {note.create_date_time && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground shrink-0"> <span className="inline-flex items-center gap-1 text-xs text-muted-foreground shrink-0">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
{new Date(note.create_date_time).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} {new Date(note.create_date_time).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz })}
{' '} {' '}
{new Date(note.create_date_time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} {new Date(note.create_date_time).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', timeZone: tz })}
</span> </span>
)} )}
</div> </div>

View file

@ -8,6 +8,7 @@ import {
CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2, CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2,
Server, HardDrive, Cpu, Clock, Server, HardDrive, Cpu, Clock,
} from 'lucide-react'; } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) { function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) {
if (!ok) return <span className="inline-block w-2 h-2 rounded-full bg-red-500" />; if (!ok) return <span className="inline-block w-2 h-2 rounded-full bg-red-500" />;
@ -31,12 +32,12 @@ function StatCard({ label, value, sub, icon: Icon, cls }: {
); );
} }
function fmtDate(d: string | null) { function fmtDate(d: string | null, tz: string) {
if (!d) return 'Never'; if (!d) return 'Never';
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
} }
function VeeamTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) { function VeeamTab({ data, onSync, syncing, tz }: { data: any; onSync: () => void; syncing: boolean; tz: string }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>; if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
const aj = data.agentJobs ?? {}; const aj = data.agentJobs ?? {};
const bj = data.backupJobs ?? {}; const bj = data.backupJobs ?? {};
@ -50,7 +51,7 @@ function VeeamTab({ data, onSync, syncing }: { data: any; onSync: () => void; sy
<StatusDot ok={data.configured} warn={totalFailed > 0 || totalWarning > 0} /> <StatusDot ok={data.configured} warn={totalFailed > 0 || totalWarning > 0} />
<div> <div>
<p className="text-sm font-medium">{data.configured ? 'Connected to VSPC' : 'Not configured'}</p> <p className="text-sm font-medium">{data.configured ? 'Connected to VSPC' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p> <p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync, tz)}</p>
</div> </div>
</div> </div>
<Button size="sm" onClick={onSync} disabled={syncing || !data.configured}> <Button size="sm" onClick={onSync} disabled={syncing || !data.configured}>
@ -191,6 +192,7 @@ function AddigyTab({ data }: { data: any }) {
} }
export default function IntegrationStatusTabs() { export default function IntegrationStatusTabs() {
const tz = useUserTimezone();
const [status, setStatus] = useState<any>(null); const [status, setStatus] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [veeamSyncing, setVeeamSyncing] = useState(false); const [veeamSyncing, setVeeamSyncing] = useState(false);
@ -274,7 +276,7 @@ export default function IntegrationStatusTabs() {
</TabsList> </TabsList>
<TabsContent value="veeam" className="mt-6"> <TabsContent value="veeam" className="mt-6">
<VeeamTab data={status?.veeam} onSync={handleVeeamSync} syncing={veeamSyncing} /> <VeeamTab data={status?.veeam} onSync={handleVeeamSync} syncing={veeamSyncing} tz={tz} />
</TabsContent> </TabsContent>
<TabsContent value="datto" className="mt-6"> <TabsContent value="datto" className="mt-6">
<DattoRmmTab data={status?.dattoRmm} onSync={handleRmmSync} syncing={rmmSyncing} /> <DattoRmmTab data={status?.dattoRmm} onSync={handleRmmSync} syncing={rmmSyncing} />

View file

@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Clock, Play, Pause, Trash2, Plus, Calendar, AlertCircle, CheckCircle2, XCircle, RefreshCw } from 'lucide-react'; import { Clock, Play, Pause, Trash2, Plus, Calendar, AlertCircle, CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ScheduleConfig { interface ScheduleConfig {
id: string; id: string;
@ -36,6 +37,7 @@ interface ScheduleStatus {
} }
export default function SyncScheduler() { export default function SyncScheduler() {
const tz = useUserTimezone();
const [schedules, setSchedules] = useState<ScheduleStatus[]>([]); const [schedules, setSchedules] = useState<ScheduleStatus[]>([]);
const [reloading, setReloading] = useState(false); const [reloading, setReloading] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -227,7 +229,7 @@ export default function SyncScheduler() {
const formatDate = (dateString?: string) => { const formatDate = (dateString?: string) => {
if (!dateString) return 'Never'; if (!dateString) return 'Never';
return new Date(dateString).toLocaleString(); return new Date(dateString).toLocaleString(undefined, { timeZone: tz });
}; };
const formatNextRun = (dateString?: string) => { const formatNextRun = (dateString?: string) => {

View file

@ -25,6 +25,7 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { useUserTimezone } from "@/lib/hooks/use-user-timezone";
interface AuditLog { interface AuditLog {
id: string; id: string;
@ -64,6 +65,7 @@ const actionColors: Record<string, string> = {
}; };
export function AuditLogTable() { export function AuditLogTable() {
const tz = useUserTimezone();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -183,7 +185,7 @@ export function AuditLogTable() {
logs.map((log) => ( logs.map((log) => (
<TableRow key={log.id}> <TableRow key={log.id}>
<TableCell className="text-muted-foreground whitespace-nowrap"> <TableCell className="text-muted-foreground whitespace-nowrap">
{new Date(log.timestamp).toLocaleString()} {new Date(log.timestamp).toLocaleString(undefined, { timeZone: tz })}
</TableCell> </TableCell>
<TableCell> <TableCell>
{log.user_name || log.user_email || "System"} {log.user_name || log.user_email || "System"}

View file

@ -24,6 +24,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { useUserTimezone } from "@/lib/hooks/use-user-timezone";
interface Session { interface Session {
id: string; id: string;
@ -52,6 +53,7 @@ function parseUserAgent(ua: string): { device: string; browser: string } {
} }
export function UserSessions({ sessions, userId }: UserSessionsProps) { export function UserSessions({ sessions, userId }: UserSessionsProps) {
const tz = useUserTimezone();
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false); const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);
@ -158,10 +160,10 @@ export function UserSessions({ sessions, userId }: UserSessionsProps) {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{new Date(session.created_at).toLocaleString()} {new Date(session.created_at).toLocaleString(undefined, { timeZone: tz })}
</TableCell> </TableCell>
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{new Date(session.expires_at).toLocaleString()} {new Date(session.expires_at).toLocaleString(undefined, { timeZone: tz })}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button

View file

@ -24,6 +24,7 @@ import { Badge } from "@/components/ui/badge";
import { RoleBadge } from "./role-badge"; import { RoleBadge } from "./role-badge";
import { UserActions } from "./user-actions"; import { UserActions } from "./user-actions";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { useUserTimezone } from "@/lib/hooks/use-user-timezone";
interface User { interface User {
id: string; id: string;
@ -43,6 +44,7 @@ interface Pagination {
} }
export function UserTable() { export function UserTable() {
const tz = useUserTimezone();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { data: session } = useSession(); const { data: session } = useSession();
@ -202,7 +204,7 @@ export function UserTable() {
)} )}
</TableCell> </TableCell>
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{new Date(user.created_at).toLocaleDateString()} {new Date(user.created_at).toLocaleDateString(undefined, { timeZone: tz })}
</TableCell> </TableCell>
<TableCell> <TableCell>
<UserActions <UserActions

View file

@ -24,6 +24,7 @@ import {
TimeEntryAnalysis, TimeEntryAnalysis,
AggregateAnalysis AggregateAnalysis
} from '@/lib/types/analytics'; } from '@/lib/types/analytics';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ScoreCardProps { interface ScoreCardProps {
title: string; title: string;
@ -424,6 +425,7 @@ interface AggregateScoreCardProps {
} }
export function AggregateScoreCard({ analysis, className }: AggregateScoreCardProps) { export function AggregateScoreCard({ analysis, className }: AggregateScoreCardProps) {
const tz = useUserTimezone();
return ( return (
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>
@ -483,7 +485,7 @@ export function AggregateScoreCard({ analysis, className }: AggregateScoreCardPr
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Date Range:</span> <span className="text-gray-600">Date Range:</span>
<span className="font-medium"> <span className="font-medium">
{analysis.dateRange.latest.toLocaleDateString()} {analysis.dateRange.latest.toLocaleDateString(undefined, { timeZone: tz })}
</span> </span>
</div> </div>
</div> </div>

View file

@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Calendar, Clock, Users, ChevronDown, ChevronRight, Activity, AlertCircle, CheckCircle } from 'lucide-react'; import { Calendar, Clock, Users, ChevronDown, ChevronRight, Activity, AlertCircle, CheckCircle } from 'lucide-react';
import { TimelineEvent, TimelineView as TimelineViewType } from '@/lib/types/analytics'; import { TimelineEvent, TimelineView as TimelineViewType } from '@/lib/types/analytics';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface TimelineViewProps { interface TimelineViewProps {
events: TimelineEvent[]; events: TimelineEvent[];
@ -18,13 +19,14 @@ interface TimelineViewProps {
className?: string; className?: string;
} }
export function TimelineView({ export function TimelineView({
events, events,
timeRange, timeRange,
onTimeRangeChange, onTimeRangeChange,
loading = false, loading = false,
className className
}: TimelineViewProps) { }: TimelineViewProps) {
const tz = useUserTimezone();
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set()); const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
const [selectedEvent, setSelectedEvent] = useState<TimelineEvent | null>(null); const [selectedEvent, setSelectedEvent] = useState<TimelineEvent | null>(null);
@ -135,12 +137,14 @@ export function TimelineView({
day: 'numeric', day: 'numeric',
hour: 'numeric', hour: 'numeric',
hour12: true, hour12: true,
timeZone: tz,
}); });
case 'day': case 'day':
return new Date(groupKey).toLocaleDateString('en-US', { return new Date(groupKey).toLocaleDateString('en-US', {
weekday: 'long', weekday: 'long',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
timeZone: tz,
}); });
case 'week': case 'week':
return groupKey; return groupKey;
@ -148,6 +152,7 @@ export function TimelineView({
return new Date(groupKey + '-01').toLocaleDateString('en-US', { return new Date(groupKey + '-01').toLocaleDateString('en-US', {
month: 'long', month: 'long',
year: 'numeric', year: 'numeric',
timeZone: tz,
}); });
default: default:
return groupKey; return groupKey;
@ -300,7 +305,7 @@ export function TimelineView({
<div className="flex items-center gap-4 text-xs text-gray-500"> <div className="flex items-center gap-4 text-xs text-gray-500">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{new Date(event.timestamp).toLocaleTimeString()} {new Date(event.timestamp).toLocaleTimeString(undefined, { timeZone: tz })}
</span> </span>
{event.duration && ( {event.duration && (

View file

@ -22,6 +22,7 @@ import { ShareModal } from './share-modal';
import { AnalyzeButton } from './analyze-button'; import { AnalyzeButton } from './analyze-button';
import { AnalysisMarkdown } from './analysis-markdown'; import { AnalysisMarkdown } from './analysis-markdown';
import type { PersistedAnalysis, Visibility } from '@/lib/types/analyzer'; import type { PersistedAnalysis, Visibility } from '@/lib/types/analyzer';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface AnalysisViewProps { interface AnalysisViewProps {
analysis: PersistedAnalysis; analysis: PersistedAnalysis;
@ -70,6 +71,7 @@ function ModelBadges({ a }: { a: PersistedAnalysis }) {
} }
export function AnalysisView({ analysis: a }: AnalysisViewProps) { export function AnalysisView({ analysis: a }: AnalysisViewProps) {
const tz = useUserTimezone();
const [expandedEvent, setExpandedEvent] = useState<number | null>(null); const [expandedEvent, setExpandedEvent] = useState<number | null>(null);
const [nextStepOpen, setNextStepOpen] = useState(false); const [nextStepOpen, setNextStepOpen] = useState(false);
@ -112,7 +114,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) {
)} )}
</div> </div>
<CardTitle className="text-xl"> <CardTitle className="text-xl">
AI Analysis &middot; {new Date(a.triggeredAt).toLocaleString()} AI Analysis &middot; {new Date(a.triggeredAt).toLocaleString(undefined, { timeZone: tz })}
</CardTitle> </CardTitle>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
{a.totalInputTokens.toLocaleString()} in /{' '} {a.totalInputTokens.toLocaleString()} in /{' '}
@ -219,7 +221,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) {
{VISIBILITY_MARKER[event.visibility]} {VISIBILITY_MARKER[event.visibility]}
</span> </span>
<span className="font-mono text-xs text-muted-foreground shrink-0"> <span className="font-mono text-xs text-muted-foreground shrink-0">
{new Date(event.timestamp).toLocaleString()} {new Date(event.timestamp).toLocaleString(undefined, { timeZone: tz })}
</span> </span>
<span className="font-medium">{event.actor}</span> <span className="font-medium">{event.actor}</span>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
@ -307,7 +309,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) {
onClick={() => jumpToEvent(ts)} onClick={() => jumpToEvent(ts)}
className="underline mr-2 font-mono" className="underline mr-2 font-mono"
> >
{new Date(ts).toLocaleString()} {new Date(ts).toLocaleString(undefined, { timeZone: tz })}
</button> </button>
))} ))}
</div> </div>
@ -398,7 +400,7 @@ export function AnalysisView({ analysis: a }: AnalysisViewProps) {
<CardContent className="space-y-2 text-sm"> <CardContent className="space-y-2 text-sm">
<p> <p>
<strong>{a.timeline[expandedEvent].actor}</strong> ·{' '} <strong>{a.timeline[expandedEvent].actor}</strong> ·{' '}
{new Date(a.timeline[expandedEvent].timestamp).toLocaleString()} {new Date(a.timeline[expandedEvent].timestamp).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
<p>{a.timeline[expandedEvent].action}</p> <p>{a.timeline[expandedEvent].action}</p>
<Separator /> <Separator />

View file

@ -12,6 +12,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface CompanyBackupDetailProps { interface CompanyBackupDetailProps {
companyId: number | null; companyId: number | null;
@ -43,12 +44,13 @@ function formatBytes(bytes: number | null): string {
return `${val.toFixed(1)} ${units[i]}`; return `${val.toFixed(1)} ${units[i]}`;
} }
function formatDate(dateStr: string | null): string { function formatDate(dateStr: string | null, tz: string): string {
if (!dateStr) return 'Never'; if (!dateStr) return 'Never';
return new Date(dateStr).toLocaleString(); return new Date(dateStr).toLocaleString(undefined, { timeZone: tz });
} }
export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDetailProps) { export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDetailProps) {
const tz = useUserTimezone();
const [workloads, setWorkloads] = useState<any[]>([]); const [workloads, setWorkloads] = useState<any[]>([]);
const [jobs, setJobs] = useState<{ serverJobs: any[]; agentJobs: any[] }>({ serverJobs: [], agentJobs: [] }); const [jobs, setJobs] = useState<{ serverJobs: any[]; agentJobs: any[] }>({ serverJobs: [], agentJobs: [] });
const [compliance, setCompliance] = useState<any[]>([]); const [compliance, setCompliance] = useState<any[]>([]);
@ -110,7 +112,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet
<TableRow key={w.instance_uid}> <TableRow key={w.instance_uid}>
<TableCell className="font-medium">{w.name}</TableCell> <TableCell className="font-medium">{w.name}</TableCell>
<TableCell>{w.restore_points ?? '-'}</TableCell> <TableCell>{w.restore_points ?? '-'}</TableCell>
<TableCell className="text-sm">{formatDate(w.latest_restore_point_date)}</TableCell> <TableCell className="text-sm">{formatDate(w.latest_restore_point_date, tz)}</TableCell>
<TableCell className="text-sm">{formatBytes(w.used_source_size)}</TableCell> <TableCell className="text-sm">{formatBytes(w.used_source_size)}</TableCell>
</TableRow> </TableRow>
))} ))}
@ -147,7 +149,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet
<TableCell className="font-medium">{j.name}</TableCell> <TableCell className="font-medium">{j.name}</TableCell>
<TableCell className="text-sm">{j.type || 'Server'}</TableCell> <TableCell className="text-sm">{j.type || 'Server'}</TableCell>
<TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell> <TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell>
<TableCell className="text-sm">{formatDate(j.last_run)}</TableCell> <TableCell className="text-sm">{formatDate(j.last_run, tz)}</TableCell>
<TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell> <TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell>
</TableRow> </TableRow>
))} ))}
@ -156,7 +158,7 @@ export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDet
<TableCell className="font-medium">{j.name}</TableCell> <TableCell className="font-medium">{j.name}</TableCell>
<TableCell className="text-sm">{j.backup_mode || 'Agent'}</TableCell> <TableCell className="text-sm">{j.backup_mode || 'Agent'}</TableCell>
<TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell> <TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell>
<TableCell className="text-sm">{formatDate(j.last_run)}</TableCell> <TableCell className="text-sm">{formatDate(j.last_run, tz)}</TableCell>
<TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell> <TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell>
</TableRow> </TableRow>
))} ))}

View file

@ -19,6 +19,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Search, CheckCircle2, XCircle, Loader2, ExternalLink, Package } from 'lucide-react'; import { Search, CheckCircle2, XCircle, Loader2, ExternalLink, Package } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ComplianceMismatch { interface ComplianceMismatch {
id: number; id: number;
@ -87,11 +88,13 @@ function ContractCoverageModal({
companyName, companyName,
open, open,
onClose, onClose,
tz,
}: { }: {
contractId: number | null; contractId: number | null;
companyName: string | null; companyName: string | null;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
tz: string;
}) { }) {
const [data, setData] = useState<{ contract: ContractDetail; services: ContractService[] } | null>(null); const [data, setData] = useState<{ contract: ContractDetail; services: ContractService[] } | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -167,10 +170,10 @@ function ContractCoverageModal({
</div> </div>
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground"> <div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
{contract.start_date && ( {contract.start_date && (
<span>Start: {new Date(contract.start_date).toLocaleDateString()}</span> <span>Start: {new Date(contract.start_date).toLocaleDateString(undefined, { timeZone: tz })}</span>
)} )}
{contract.end_date && ( {contract.end_date && (
<span>End: {new Date(contract.end_date).toLocaleDateString()}</span> <span>End: {new Date(contract.end_date).toLocaleDateString(undefined, { timeZone: tz })}</span>
)} )}
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className={`inline-block h-1.5 w-1.5 rounded-full ${contract.status === 1 ? 'bg-green-500' : 'bg-gray-400'}`} /> <span className={`inline-block h-1.5 w-1.5 rounded-full ${contract.status === 1 ? 'bg-green-500' : 'bg-gray-400'}`} />
@ -238,6 +241,7 @@ function ServiceTable({ services, highlight }: { services: ContractService[]; hi
} }
export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps) { export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps) {
const tz = useUserTimezone();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all'); const [typeFilter, setTypeFilter] = useState<string>('all');
const [modalContractId, setModalContractId] = useState<number | null>(null); const [modalContractId, setModalContractId] = useState<number | null>(null);
@ -370,6 +374,7 @@ export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps
companyName={modalCompanyName} companyName={modalCompanyName}
open={modalOpen} open={modalOpen}
onClose={() => setModalOpen(false)} onClose={() => setModalOpen(false)}
tz={tz}
/> />
</div> </div>
); );

View file

@ -15,12 +15,14 @@ import {
AlertCircle AlertCircle
} from 'lucide-react'; } from 'lucide-react';
import { AddigyDevice } from '@/lib/types/addigy'; import { AddigyDevice } from '@/lib/types/addigy';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface AddigyTabProps { interface AddigyTabProps {
device?: AddigyDevice; device?: AddigyDevice;
} }
export function AddigyTab({ device }: AddigyTabProps) { export function AddigyTab({ device }: AddigyTabProps) {
const tz = useUserTimezone();
if (!device) { if (!device) {
return ( return (
<Card className="border-0 shadow-lg"> <Card className="border-0 shadow-lg">
@ -104,7 +106,7 @@ export function AddigyTab({ device }: AddigyTabProps) {
<div> <div>
<Label>Last Check In</Label> <Label>Last Check In</Label>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{new Date(device['Last Check In']).toLocaleString()} {new Date(device['Last Check In']).toLocaleString(undefined, { timeZone: tz })}
</p> </p>
</div> </div>
)} )}
@ -324,7 +326,7 @@ export function AddigyTab({ device }: AddigyTabProps) {
<div> <div>
<Label>Warranty</Label> <Label>Warranty</Label>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Expires: {new Date(device['Warranty Expiration Date']).toLocaleDateString()} Expires: {new Date(device['Warranty Expiration Date']).toLocaleDateString(undefined, { timeZone: tz })}
{device['Warranty Days Left'] !== undefined && ( {device['Warranty Days Left'] !== undefined && (
<span className="ml-2">({device['Warranty Days Left']} days left)</span> <span className="ml-2">({device['Warranty Days Left']} days left)</span>
)} )}

View file

@ -12,6 +12,7 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ResolutionPoint { interface ResolutionPoint {
date: string; date: string;
@ -23,18 +24,19 @@ interface ResolutionTrendProps {
height?: number; height?: number;
} }
function fmtDate(iso: string) { function fmtDate(iso: string, tz: string) {
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: tz });
} }
export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) { export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) {
const tz = useUserTimezone();
return ( return (
<ResponsiveContainer width="100%" height={height}> <ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 0 }}> <LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 0 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="2 4" vertical={false} /> <CartesianGrid stroke="var(--border)" strokeDasharray="2 4" vertical={false} />
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={fmtDate} tickFormatter={(iso: string) => fmtDate(iso, tz)}
interval="preserveStartEnd" interval="preserveStartEnd"
minTickGap={48} minTickGap={48}
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
@ -55,7 +57,7 @@ export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) {
borderRadius: 6, borderRadius: 6,
fontSize: 12, fontSize: 12,
}} }}
labelFormatter={(value) => fmtDate(String(value))} labelFormatter={(value) => fmtDate(String(value), tz)}
formatter={(value) => formatter={(value) =>
value == null ? ['—', 'avg'] : [`${Number(value).toFixed(1)} h`, 'avg'] value == null ? ['—', 'avg'] : [`${Number(value).toFixed(1)} h`, 'avg']
} }

View file

@ -13,6 +13,7 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface VolumePoint { interface VolumePoint {
date: string; date: string;
@ -24,11 +25,12 @@ interface VolumeTrendProps {
height?: number; height?: number;
} }
function fmtDate(iso: string) { function fmtDate(iso: string, tz: string) {
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: tz });
} }
export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) { export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) {
const tz = useUserTimezone();
return ( return (
<ResponsiveContainer width="100%" height={height}> <ResponsiveContainer width="100%" height={height}>
<AreaChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 0 }}> <AreaChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 0 }}>
@ -40,7 +42,7 @@ export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) {
</defs> </defs>
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={fmtDate} tickFormatter={(iso: string) => fmtDate(iso, tz)}
interval="preserveStartEnd" interval="preserveStartEnd"
minTickGap={48} minTickGap={48}
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
@ -61,7 +63,7 @@ export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) {
borderRadius: 6, borderRadius: 6,
fontSize: 12, fontSize: 12,
}} }}
labelFormatter={(value) => fmtDate(String(value))} labelFormatter={(value) => fmtDate(String(value), tz)}
formatter={(value) => [value ?? 0, 'opened']} formatter={(value) => [value ?? 0, 'opened']}
/> />
<Area <Area

View file

@ -30,6 +30,7 @@ import {
Mail, Mail,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
// noteType values that are system/automated (workflow rules, monitoring, auto-close, etc.) // noteType values that are system/automated (workflow rules, monitoring, auto-close, etc.)
const SYSTEM_NOTE_TYPES = new Set([13, 91, 93, 94, 99, 101]); const SYSTEM_NOTE_TYPES = new Set([13, 91, 93, 94, 99, 101]);
@ -97,6 +98,7 @@ type TimelineItem =
| { kind: 'time'; ts: number; data: any }; | { kind: 'time'; ts: number; data: any };
export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDetailModalProps) { export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDetailModalProps) {
const tz = useUserTimezone();
const [ticket, setTicket] = useState<any>(null); const [ticket, setTicket] = useState<any>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -230,7 +232,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
if (!dateString) return 'N/A'; if (!dateString) return 'N/A';
return new Date(dateString).toLocaleString('en-US', { return new Date(dateString).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric', month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: tz,
}); });
}; };

View file

@ -5,6 +5,7 @@ import { Loader2, Monitor, Smartphone, Trash2, Globe } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { useUserTimezone } from "@/lib/hooks/use-user-timezone";
interface Session { interface Session {
id: string; id: string;
@ -32,6 +33,7 @@ function parseUserAgent(ua: string): { device: string; browser: string } {
} }
export function ActiveSessions({ userId }: ActiveSessionsProps) { export function ActiveSessions({ userId }: ActiveSessionsProps) {
const tz = useUserTimezone();
const [sessions, setSessions] = useState<Session[]>([]); const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [revokingId, setRevokingId] = useState<string | null>(null); const [revokingId, setRevokingId] = useState<string | null>(null);
@ -115,7 +117,7 @@ export function ActiveSessions({ userId }: ActiveSessionsProps) {
<Globe className="h-3 w-3" /> <Globe className="h-3 w-3" />
{session.ip_address || "Unknown IP"} {session.ip_address || "Unknown IP"}
<span></span> <span></span>
{new Date(session.created_at).toLocaleDateString()} {new Date(session.created_at).toLocaleDateString(undefined, { timeZone: tz })}
</div> </div>
</div> </div>
</div> </div>

View file

@ -13,6 +13,7 @@
'use client'; 'use client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ActivityBucket { interface ActivityBucket {
hour: string; hour: string;
@ -26,10 +27,11 @@ interface ActivitySparklineProps {
height?: number; height?: number;
} }
function fmtHour(iso: string): string { function fmtHour(iso: string, tz: string): string {
return new Date(iso).toLocaleTimeString(undefined, { return new Date(iso).toLocaleTimeString(undefined, {
hour: 'numeric', hour: 'numeric',
minute: '2-digit', minute: '2-digit',
timeZone: tz,
}); });
} }
@ -38,6 +40,7 @@ export function ActivitySparkline({
className, className,
height = 32, height = 32,
}: ActivitySparklineProps) { }: ActivitySparklineProps) {
const tz = useUserTimezone();
if (data.length === 0) { if (data.length === 0) {
return null; return null;
} }
@ -57,10 +60,10 @@ export function ActivitySparkline({
key={bucket.hour} key={bucket.hour}
title={ title={
empty empty
? `${fmtHour(bucket.hour)} · idle` ? `${fmtHour(bucket.hour, tz)} · idle`
: `${fmtHour(bucket.hour)} · ${bucket.success} ok · ${bucket.failure} fail` : `${fmtHour(bucket.hour, tz)} · ${bucket.success} ok · ${bucket.failure} fail`
} }
aria-label={`${fmtHour(bucket.hour)}: ${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" className="relative flex-1 min-w-[1px] flex flex-col-reverse rounded-[1px] overflow-hidden"
style={{ height: `${empty ? 12 : Math.max(totalPct, 8)}%` }} style={{ height: `${empty ? 12 : Math.max(totalPct, 8)}%` }}
data-bucket-index={i} data-bucket-index={i}