wulf-pulse/app/analyzer/reports/page.tsx
lorentz 96edfb4444 feat(07.1-05): user-tz on analyzer pages
- itglue/applications, applications/[id], configurations,
  configurations/[id], sites/[companyId], queue, ticket/[ticketNumber],
  tickets, reports, reports/[id]: useUserTimezone() in default export;
  thread tz into every inline toLocale*String call.
- analyzer/tickets/page.tsx converts module-scope formatRelative(iso)
  helper to formatRelative(iso, tz); updates 1 callsite.

Migrates 16 of 81 audit leak callsites.
2026-05-07 08:34:58 -04:00

154 lines
5.6 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
interface ReportSummary {
id: string;
generatedAt: string;
reportTitle: string | null;
ticketCount: number;
status: 'pending' | 'running' | 'complete' | 'failed';
estimatedCostUsd: number | null;
modelUsed: string | null;
generatedByUserId: string | null;
}
export default function ReportsListPage() {
const tz = useUserTimezone();
const [reports, setReports] = useState<ReportSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetch('/api/analyzer/aggregate-reports')
.then((r) => (r.ok ? r.json() : Promise.reject(r)))
.then((data: { reports: ReportSummary[] }) => {
if (!cancelled) setReports(data.reports);
})
.catch((err) => {
if (!cancelled)
setError(err instanceof Error ? err.message : 'Failed to load reports');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return (
<div className="container mx-auto px-4 py-6 space-y-6 max-w-screen-xl">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Aggregate reports
</h1>
<p className="text-muted-foreground text-sm mt-1">
Cross-ticket pattern analysis. Generate a new one from the browse
page.
</p>
</div>
<Button asChild>
<Link href="/analyzer/tickets">Browse tickets</Link>
</Button>
</div>
<Card>
<CardContent className="p-0">
{loading ? (
<div className="p-6 text-sm text-muted-foreground">Loading</div>
) : error ? (
<div className="p-6 text-sm text-destructive">{error}</div>
) : reports.length === 0 ? (
<div className="p-12 text-center text-muted-foreground">
<p>No reports yet.</p>
<p className="text-xs mt-1">
Pick tickets on the browse page and click{' '}
<em>Generate aggregate report</em>.
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[110px]">Status</TableHead>
<TableHead>Title</TableHead>
<TableHead className="w-[100px]">Tickets</TableHead>
<TableHead className="w-[120px]">Model</TableHead>
<TableHead className="w-[100px]">Cost</TableHead>
<TableHead className="w-[160px]">Generated</TableHead>
<TableHead className="w-[100px] text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{reports.map((r) => (
<TableRow key={r.id}>
<TableCell>
{r.status === 'complete' ? (
<Badge
variant="outline"
className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-400"
>
<CheckCircle2 className="w-3 h-3 mr-1" />
Complete
</Badge>
) : r.status === 'failed' ? (
<Badge variant="destructive">
<XCircle className="w-3 h-3 mr-1" />
Failed
</Badge>
) : (
<Badge variant="secondary">
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
{r.status}
</Badge>
)}
</TableCell>
<TableCell className="text-sm">
{r.reportTitle ?? (
<span className="text-muted-foreground italic">Untitled</span>
)}
</TableCell>
<TableCell className="text-sm">{r.ticketCount}</TableCell>
<TableCell className="text-xs text-muted-foreground">
{r.modelUsed ?? '—'}
</TableCell>
<TableCell className="text-xs">
{r.estimatedCostUsd === null
? '—'
: `$${r.estimatedCostUsd.toFixed(4)}`}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{new Date(r.generatedAt).toLocaleString(undefined, { timeZone: tz })}
</TableCell>
<TableCell className="text-right">
<Button variant="outline" size="sm" asChild>
<Link href={`/analyzer/reports/${r.id}`}>View</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}