124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { History, MessageSquareText } from 'lucide-react';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { cn } from '@/lib/utils';
|
|
import type { TicketDetail, TicketHistoryItem, TicketStatus } from '@/types';
|
|
import { formatDate } from '@/utils/date';
|
|
|
|
function getActorLabel(item: TicketHistoryItem) {
|
|
return item.actor?.name || item.actor?.email || 'System';
|
|
}
|
|
|
|
function getLifecycleTitle(status: TicketStatus) {
|
|
const titles: Record<TicketStatus, string> = {
|
|
unassigned: 'Awaiting Assignment',
|
|
assigned: 'Dispatched to Maintenance',
|
|
under_review: 'Maintenance Finalized',
|
|
closed: 'Ticket Closed',
|
|
};
|
|
|
|
return titles[status];
|
|
}
|
|
|
|
function HistoryEntry({
|
|
item,
|
|
isLast,
|
|
}: {
|
|
item: TicketHistoryItem;
|
|
isLast: boolean;
|
|
}) {
|
|
const isClosed = item.to_status === 'closed';
|
|
|
|
return (
|
|
<div className="relative flex gap-4 pb-8 last:pb-0">
|
|
{!isLast ? (
|
|
<span
|
|
aria-hidden
|
|
className="absolute top-6 left-2.75 h-[calc(100%-10px)] w-px bg-border"
|
|
/>
|
|
) : null}
|
|
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
'relative z-10 mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border-2 bg-background',
|
|
isClosed
|
|
? 'border-primary bg-primary text-primary-foreground'
|
|
: 'border-primary/60',
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
'size-2 rounded-full bg-primary',
|
|
isClosed &&
|
|
'h-1.5 w-2.5 -rotate-45 rounded-none border-b-2 border-l-2 border-current bg-transparent',
|
|
)}
|
|
/>
|
|
</span>
|
|
|
|
<div className="min-w-0 flex-1 space-y-2">
|
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
<h3 className="text-sm font-semibold text-foreground">
|
|
{getLifecycleTitle(item.to_status)}
|
|
</h3>
|
|
<time
|
|
dateTime={item.created_at}
|
|
className="text-xs font-semibold text-muted-foreground tabular-nums"
|
|
>
|
|
{formatDate(item.created_at)}
|
|
</time>
|
|
</div>
|
|
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
Updated By{' '}
|
|
<span className="text-foreground/90">{getActorLabel(item)}</span>
|
|
</p>
|
|
|
|
{item.note ? (
|
|
<div className="flex gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm leading-relaxed text-foreground">
|
|
<MessageSquareText className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
<p>{item.note}</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) {
|
|
const history = ticket.history ?? [];
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<History className="size-5 text-primary" />
|
|
Audit & Lifecycle Timeline
|
|
</CardTitle>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{history.length ? (
|
|
<div className="pl-0.5">
|
|
{history.map((item, index) => (
|
|
<HistoryEntry
|
|
key={item.id}
|
|
item={item}
|
|
isLast={index === history.length - 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-lg border border-dashed border-border/80 bg-muted/10 px-4 py-8 text-center">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
No timeline entries yet
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|