feat(tickets): add summary cards above ticket list
This commit is contained in:
138
src/app/(modules)/ticket/components/TicketSummaryCards.tsx
Normal file
138
src/app/(modules)/ticket/components/TicketSummaryCards.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user