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,
} from '@/lib/status-registry';
import { useState, useEffect } from 'react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
// ── Live lookup types (fetched from DB) ───────────────────────────────────────
@ -132,7 +133,7 @@ const COMPANY_GROUPS: FieldGroup[] = [
// ── 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 === '') {
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: (
<span className="inline-flex items-center gap-1.5 text-sm">
<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>
),
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}/)) {
return resolveLabel(key, value, 'date', lookups);
return resolveLabel(key, value, 'date', lookups, tz);
}
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) {
const tz = useUserTimezone();
const [copiedField, setCopiedField] = useState<string | null>(null);
const [lookups, setLookups] = useState<Lookups>(EMPTY_LOOKUPS);
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">
{visibleFields.map((field) => {
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;
return (
<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">
{fields.map((field, idx) => {
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) : '';
return (
<div key={field.key}>
@ -504,7 +506,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
<div className="rounded-lg border overflow-hidden">
{visibleFields.map((field, idx) => {
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) : '';
return (
<div key={field.key}>
@ -607,7 +609,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{entry.entry_date && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<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>
)}
</div>
@ -653,9 +655,9 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{note.create_date_time && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground shrink-0">
<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>
)}
</div>

View file

@ -8,6 +8,7 @@ import {
CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2,
Server, HardDrive, Cpu, Clock,
} from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) {
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';
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>;
const aj = data.agentJobs ?? {};
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} />
<div>
<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>
<Button size="sm" onClick={onSync} disabled={syncing || !data.configured}>
@ -191,6 +192,7 @@ function AddigyTab({ data }: { data: any }) {
}
export default function IntegrationStatusTabs() {
const tz = useUserTimezone();
const [status, setStatus] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [veeamSyncing, setVeeamSyncing] = useState(false);
@ -274,7 +276,7 @@ export default function IntegrationStatusTabs() {
</TabsList>
<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 value="datto" className="mt-6">
<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 { Alert, AlertDescription } from '@/components/ui/alert';
import { Clock, Play, Pause, Trash2, Plus, Calendar, AlertCircle, CheckCircle2, XCircle, RefreshCw } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ScheduleConfig {
id: string;
@ -36,6 +37,7 @@ interface ScheduleStatus {
}
export default function SyncScheduler() {
const tz = useUserTimezone();
const [schedules, setSchedules] = useState<ScheduleStatus[]>([]);
const [reloading, setReloading] = useState(false);
const [loading, setLoading] = useState(true);
@ -227,7 +229,7 @@ export default function SyncScheduler() {
const formatDate = (dateString?: string) => {
if (!dateString) return 'Never';
return new Date(dateString).toLocaleString();
return new Date(dateString).toLocaleString(undefined, { timeZone: tz });
};
const formatNextRun = (dateString?: string) => {

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import {
TimeEntryAnalysis,
AggregateAnalysis
} from '@/lib/types/analytics';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ScoreCardProps {
title: string;
@ -424,6 +425,7 @@ interface AggregateScoreCardProps {
}
export function AggregateScoreCard({ analysis, className }: AggregateScoreCardProps) {
const tz = useUserTimezone();
return (
<Card className={className}>
<CardHeader>
@ -483,7 +485,7 @@ export function AggregateScoreCard({ analysis, className }: AggregateScoreCardPr
<div className="flex justify-between">
<span className="text-gray-600">Date Range:</span>
<span className="font-medium">
{analysis.dateRange.latest.toLocaleDateString()}
{analysis.dateRange.latest.toLocaleDateString(undefined, { timeZone: tz })}
</span>
</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 { TimelineEvent, TimelineView as TimelineViewType } from '@/lib/types/analytics';
import { cn } from '@/lib/utils';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface TimelineViewProps {
events: TimelineEvent[];
@ -25,6 +26,7 @@ export function TimelineView({
loading = false,
className
}: TimelineViewProps) {
const tz = useUserTimezone();
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
const [selectedEvent, setSelectedEvent] = useState<TimelineEvent | null>(null);
@ -135,12 +137,14 @@ export function TimelineView({
day: 'numeric',
hour: 'numeric',
hour12: true,
timeZone: tz,
});
case 'day':
return new Date(groupKey).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
timeZone: tz,
});
case 'week':
return groupKey;
@ -148,6 +152,7 @@ export function TimelineView({
return new Date(groupKey + '-01').toLocaleDateString('en-US', {
month: 'long',
year: 'numeric',
timeZone: tz,
});
default:
return groupKey;
@ -300,7 +305,7 @@ export function TimelineView({
<div className="flex items-center gap-4 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(event.timestamp).toLocaleTimeString()}
{new Date(event.timestamp).toLocaleTimeString(undefined, { timeZone: tz })}
</span>
{event.duration && (

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { Search, CheckCircle2, XCircle, Loader2, ExternalLink, Package } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ComplianceMismatch {
id: number;
@ -87,11 +88,13 @@ function ContractCoverageModal({
companyName,
open,
onClose,
tz,
}: {
contractId: number | null;
companyName: string | null;
open: boolean;
onClose: () => void;
tz: string;
}) {
const [data, setData] = useState<{ contract: ContractDetail; services: ContractService[] } | null>(null);
const [loading, setLoading] = useState(false);
@ -167,10 +170,10 @@ function ContractCoverageModal({
</div>
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
{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 && (
<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={`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) {
const tz = useUserTimezone();
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [modalContractId, setModalContractId] = useState<number | null>(null);
@ -370,6 +374,7 @@ export function ComplianceDetailTable({ mismatches }: ComplianceDetailTableProps
companyName={modalCompanyName}
open={modalOpen}
onClose={() => setModalOpen(false)}
tz={tz}
/>
</div>
);

View file

@ -15,12 +15,14 @@ import {
AlertCircle
} from 'lucide-react';
import { AddigyDevice } from '@/lib/types/addigy';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface AddigyTabProps {
device?: AddigyDevice;
}
export function AddigyTab({ device }: AddigyTabProps) {
const tz = useUserTimezone();
if (!device) {
return (
<Card className="border-0 shadow-lg">
@ -104,7 +106,7 @@ export function AddigyTab({ device }: AddigyTabProps) {
<div>
<Label>Last Check In</Label>
<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>
</div>
)}
@ -324,7 +326,7 @@ export function AddigyTab({ device }: AddigyTabProps) {
<div>
<Label>Warranty</Label>
<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 && (
<span className="ml-2">({device['Warranty Days Left']} days left)</span>
)}

View file

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

View file

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

View file

@ -30,6 +30,7 @@ import {
Mail,
X,
} from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
// noteType values that are system/automated (workflow rules, monitoring, auto-close, etc.)
const SYSTEM_NOTE_TYPES = new Set([13, 91, 93, 94, 99, 101]);
@ -97,6 +98,7 @@ type TimelineItem =
| { kind: 'time'; ts: number; data: any };
export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDetailModalProps) {
const tz = useUserTimezone();
const [ticket, setTicket] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -230,7 +232,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleString('en-US', {
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 { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useUserTimezone } from "@/lib/hooks/use-user-timezone";
interface Session {
id: string;
@ -32,6 +33,7 @@ function parseUserAgent(ua: string): { device: string; browser: string } {
}
export function ActiveSessions({ userId }: ActiveSessionsProps) {
const tz = useUserTimezone();
const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [revokingId, setRevokingId] = useState<string | null>(null);
@ -115,7 +117,7 @@ export function ActiveSessions({ userId }: ActiveSessionsProps) {
<Globe className="h-3 w-3" />
{session.ip_address || "Unknown IP"}
<span></span>
{new Date(session.created_at).toLocaleDateString()}
{new Date(session.created_at).toLocaleDateString(undefined, { timeZone: tz })}
</div>
</div>
</div>

View file

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