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:
parent
91b876310e
commit
8f955a0ff9
17 changed files with 94 additions and 53 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
@ -18,13 +19,14 @@ interface TimelineViewProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export function TimelineView({
|
||||
events,
|
||||
timeRange,
|
||||
onTimeRangeChange,
|
||||
export function TimelineView({
|
||||
events,
|
||||
timeRange,
|
||||
onTimeRangeChange,
|
||||
loading = false,
|
||||
className
|
||||
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 && (
|
||||
|
|
|
|||
|
|
@ -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 · {new Date(a.triggeredAt).toLocaleString()}
|
||||
AI Analysis · {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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue