feat(07.1-04): migrate mobile finance + ticket detail to useUserTimezone
- app/mobile/finance/page.tsx: thread tz through fmtDate, setLastSync, monthLabel — 3 formatter callsites now pass timeZone - app/mobile/tickets/[id]/page.tsx: thread tz through fmtDate (5 callsites) and TimelineCard prop - All toLocaleDateString / toLocaleString calls in both files now render in user.timezone, not browser local zone - Resolves TZ-02 on the directly-reported bug surface (mobile finance + ticket detail)
This commit is contained in:
parent
2ac2db7a23
commit
14f4da3483
2 changed files with 19 additions and 15 deletions
|
|
@ -20,6 +20,7 @@ import {
|
|||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
interface AgingBucket { balance: number; count: number; }
|
||||
interface FinanceData {
|
||||
|
|
@ -39,11 +40,12 @@ interface FinanceData {
|
|||
function fmt$(n: number) {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
|
||||
}
|
||||
function fmtDate(ts: string) {
|
||||
return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
function fmtDate(ts: string, tz: string) {
|
||||
return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: tz });
|
||||
}
|
||||
|
||||
export default function MobileFinance() {
|
||||
const tz = useUserTimezone();
|
||||
const [data, setData] = useState<FinanceData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -72,7 +74,7 @@ export default function MobileFinance() {
|
|||
if (r.ok) {
|
||||
const d = await r.json();
|
||||
const ts = d.lastSync?.invoices;
|
||||
setLastSync(ts ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : null);
|
||||
setLastSync(ts ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: tz }) : null);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
|
@ -315,7 +317,7 @@ export default function MobileFinance() {
|
|||
amountTone={inv.status === 'Overdue' ? 'destructive' : 'default'}
|
||||
secondary={
|
||||
<>
|
||||
#{inv.doc_number} · Due {fmtDate(inv.due_date)}
|
||||
#{inv.doc_number} · Due {fmtDate(inv.due_date, tz)}
|
||||
{inv.days_overdue > 0 && (
|
||||
<span className="text-destructive ml-1">({inv.days_overdue}d overdue)</span>
|
||||
)}
|
||||
|
|
@ -354,7 +356,7 @@ export default function MobileFinance() {
|
|||
primary={p.customer_ref_name}
|
||||
amount={fmt$(p.total_amt)}
|
||||
amountTone="positive"
|
||||
secondary={fmtDate(p.txn_date)}
|
||||
secondary={fmtDate(p.txn_date, tz)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
@ -370,7 +372,7 @@ export default function MobileFinance() {
|
|||
<h2 className="text-sm font-semibold mb-2">Revenue — last 12 months</h2>
|
||||
<div className="rounded-xl border divide-y overflow-hidden">
|
||||
{monthly_revenue.map((m) => {
|
||||
const monthLabel = new Date(m.month).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||
const monthLabel = new Date(m.month).toLocaleDateString('en-US', { month: 'short', year: 'numeric', timeZone: tz });
|
||||
return (
|
||||
<div key={m.month} className="px-4 py-3 flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-semibold">{monthLabel}</p>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ArrowLeft, RefreshCw, Clock, FileText, Timer, CheckCircle2,
|
||||
ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, ExternalLink, Mail, AlignLeft, Code2,
|
||||
} from 'lucide-react';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
const PRIORITY_LABEL: Record<number, string> = {
|
||||
1: 'Standard', 2: 'Medium', 3: 'Standard', 4: 'Critical',
|
||||
|
|
@ -45,8 +46,8 @@ type TimelineItem =
|
|||
| { kind: 'time'; ts: string; data: { id: number; hours_worked: string; notes: string; billable: boolean; resource_name: string } }
|
||||
| { kind: 'resolved'; ts: string };
|
||||
|
||||
function fmtDate(ts: string) {
|
||||
return new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
|
||||
function fmtDate(ts: string, tz: string) {
|
||||
return new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: tz });
|
||||
}
|
||||
function fmtHours(h: string | number) {
|
||||
const n = parseFloat(String(h));
|
||||
|
|
@ -91,7 +92,7 @@ function renderContent(s: string): React.ReactNode[] {
|
|||
return nodes;
|
||||
}
|
||||
|
||||
function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defaultOpen?: boolean }) {
|
||||
function TimelineCard({ item, tz, defaultOpen = false }: { item: TimelineItem; tz: string; defaultOpen?: boolean }) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
if (item.kind === 'created') {
|
||||
|
|
@ -104,7 +105,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
|
|||
<div className="w-px flex-1 bg-border mt-1" />
|
||||
</div>
|
||||
<div className="pb-4 flex-1 min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts)}</p>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts, tz)}</p>
|
||||
<p className="text-sm font-medium">Ticket created</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,7 +121,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
|
|||
</div>
|
||||
</div>
|
||||
<div className="pb-4 flex-1">
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts)}</p>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts, tz)}</p>
|
||||
<p className="text-sm font-medium text-green-600">Ticket resolved</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -138,7 +139,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
|
|||
<div className="w-px flex-1 bg-border mt-1" />
|
||||
</div>
|
||||
<div className="pb-4 flex-1 min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts)}</p>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts, tz)}</p>
|
||||
<button onClick={() => setOpen(o => !o)}
|
||||
className="w-full text-left flex items-center gap-2">
|
||||
<p className="text-sm font-medium flex-1">
|
||||
|
|
@ -179,7 +180,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
|
|||
<div className="w-px flex-1 bg-border mt-1" />
|
||||
</div>
|
||||
<div className="pb-4 flex-1 min-w-0">
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts)}</p>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">{fmtDate(item.ts, tz)}</p>
|
||||
<button onClick={() => setOpen(o => !o)} className="w-full text-left">
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -207,6 +208,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
|
|||
export default function TicketTimeline({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const tz = useUserTimezone();
|
||||
const [data, setData] = useState<{ ticket: Ticket; timeline: TimelineItem[]; total_hours: number } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -284,7 +286,7 @@ export default function TicketTimeline({ params }: { params: Promise<{ id: strin
|
|||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5" />Created {fmtDate(ticket.create_date)}
|
||||
<Clock className="w-3.5 h-3.5" />Created {fmtDate(ticket.create_date, tz)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -370,7 +372,7 @@ export default function TicketTimeline({ params }: { params: Promise<{ id: strin
|
|||
return true;
|
||||
})
|
||||
.map((item, i) => (
|
||||
<TimelineCard key={i} item={item} defaultOpen={item.kind === 'note' && i === timeline.length - 1} />
|
||||
<TimelineCard key={i} item={item} tz={tz} defaultOpen={item.kind === 'note' && i === timeline.length - 1} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue