feat(tickets): add summary cards above ticket list

This commit is contained in:
2026-08-05 16:57:54 +05:30
parent 44e9dc4ec8
commit 7ee621b790
10 changed files with 206 additions and 1 deletions

View File

@@ -0,0 +1,138 @@
import {
CalendarClock,
CircleCheck,
ClipboardCheck,
ListChecks,
Ticket,
TriangleAlert,
type LucideIcon,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import type { TicketSummaryResponse } from '@/types';
interface TicketSummaryCardsProps {
summary?: TicketSummaryResponse;
isLoading: boolean;
isError: boolean;
}
interface SummaryMetric {
label: string;
value: number;
icon: LucideIcon;
iconClassName: string;
}
function SummaryCard({ metric }: { metric: SummaryMetric }) {
const Icon = metric.icon;
return (
<Card size="sm" className="py-4">
<CardContent className="flex items-start justify-between gap-3 px-4">
<div className="min-w-0">
<p className="text-2xl font-semibold tracking-tight tabular-nums">
{metric.value.toLocaleString()}
</p>
<p className="mt-1 truncate text-xs font-medium text-muted-foreground">
{metric.label}
</p>
</div>
<div className={`rounded-lg p-2 ${metric.iconClassName}`}>
<Icon className="size-4" aria-hidden="true" />
</div>
</CardContent>
</Card>
);
}
function TicketSummarySkeleton() {
return (
<div
className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5"
aria-label="Loading ticket summary"
>
{Array.from({ length: 5 }, (_, index) => (
<Card key={index} size="sm" className="py-4">
<CardContent className="flex items-start justify-between gap-3 px-4">
<div className="space-y-2">
<Skeleton className="h-7 w-14" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="size-8" />
</CardContent>
</Card>
))}
</div>
);
}
export function TicketSummaryCards({
summary,
isLoading,
isError,
}: TicketSummaryCardsProps) {
if (isLoading) return <TicketSummarySkeleton />;
if (isError || !summary) {
return (
<div
role="status"
className="flex items-center gap-2 rounded-lg border border-dashed px-4 py-3 text-sm text-muted-foreground"
>
<TriangleAlert className="size-4" aria-hidden="true" />
Ticket summary is temporarily unavailable.
</div>
);
}
const metrics: SummaryMetric[] = [
{
label: 'Total Tickets',
value: summary.total_tickets,
icon: Ticket,
iconClassName:
'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
},
{
label: 'In Progress',
value: summary.in_progress_tickets,
icon: ListChecks,
iconClassName:
'bg-indigo-100 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300',
},
{
label: 'Repair Reviews',
value: summary.pending_actions.repair_reviews,
icon: ClipboardCheck,
iconClassName:
'bg-orange-100 text-orange-700 dark:bg-orange-950/60 dark:text-orange-300',
},
{
label: 'Extension Requests',
value: summary.pending_actions.extension_requests,
icon: CalendarClock,
iconClassName:
'bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300',
},
{
label: 'Completed',
value: summary.completed_tickets,
icon: CircleCheck,
iconClassName:
'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300',
},
];
return (
<section
aria-label="Ticket summary"
className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5"
>
{metrics.map((metric) => (
<SummaryCard key={metric.label} metric={metric} />
))}
</section>
);
}