diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx new file mode 100644 index 0000000..e0573c9 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { History } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import type { TicketDetail, TicketHistoryItem, TicketStatus } from '@/types'; + +import { formatTicketStatus } from '../../components/TicketStatusBadge'; + +function formatDate(value: string | null | undefined) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(value)); +} + +function getActorLabel(item: TicketHistoryItem) { + return item.actor?.name || item.actor?.email || 'System'; +} + +function getLifecycleTitle(status: TicketStatus) { + const titles: Record = { + processing: 'Incident Ticket Created', + unassigned: 'Awaiting Assignment', + assigned: 'Dispatched to Maintenance', + under_review: 'Maintenance Finalized', + closed: 'Ticket Closed', + }; + + return titles[status] ?? formatTicketStatus(status); +} + +function HistoryEntry({ + item, + isLast, +}: { + item: TicketHistoryItem; + isLast: boolean; +}) { + const isClosed = item.to_status === 'closed'; + + return ( +
+ {!isLast ? ( + + ) : null} + + + + + +
+
+

+ {getLifecycleTitle(item.to_status)} +

+ +
+ +

+ {item.note || + `${formatTicketStatus(item.to_status)} by ${getActorLabel(item)}.`} +

+ +

{getActorLabel(item)}

+
+
+ ); +} + +export function TicketHistoryCard({ ticket }: { ticket: TicketDetail }) { + const history = ticket.history ?? []; + + return ( +
+
+ +

+ Audit & Lifecycle Timeline +

+
+ + {history.length ? ( +
+ {history.map((item, index) => ( + + ))} +
+ ) : ( +
+

+ No timeline entries yet +

+
+ )} +
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx new file mode 100644 index 0000000..1e3ce78 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { TicketActor, TicketDetail } from '@/types'; + +import { TicketStatusBadge } from '../../components/TicketStatusBadge'; + +function formatDate(value: string | null | undefined) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(value)); +} + +function OverviewMeta({ + label, + value, +}: { + label: string; + value: string | number | null | undefined; +}) { + return ( +
+

{label}

+

{value ?? '-'}

+
+ ); +} + +function PersonMeta({ + label, + person, +}: { + label: string; + person: TicketActor | null | undefined; +}) { + return ( +
+

{label}

+ {person ? ( +
+

{person.name || '-'}

+ {person.email && ( +

+ {person.email} +

+ )} +
+ ) : ( +

-

+ )} +
+ ); +} + +export function TicketOverviewCard({ ticket }: { ticket: TicketDetail }) { + return ( + + + Overview + + + +
+
+

AI Detections

+

+ {ticket.ai_result?.detection_count ?? '-'} +

+

+ Completed {formatDate(ticket.ai_result?.completed_at)} +

+
+ +
+ + + + +
+
+
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx new file mode 100644 index 0000000..af0b6b9 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx @@ -0,0 +1,41 @@ +'use client'; + +import type { TicketDetail } from '@/types'; + +import { RepairNotesCard } from './repair-report/RepairNotesCard'; +import { RepairProofImagesGrid } from './repair-report/RepairProofImagesGrid'; +import { RepairProofVideoCard } from './repair-report/RepairProofVideoCard'; +import { ReviewerCommentsCard } from './repair-report/ReviewerCommentsCard'; + +export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) { + const repair = ticket.repair; + + return ( +
+

+ Evidence & Progress +

+ +
+ + + +
+ +
+ + +
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx new file mode 100644 index 0000000..2acf037 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx @@ -0,0 +1,24 @@ +'use client'; + +import type { TicketDetail } from '@/types'; + +import { AssignTicketAction } from './actions/AssignTicketAction'; +import { NoTicketAction } from './actions/NoTicketAction'; +import { ReviewRepairAction } from './actions/ReviewRepairAction'; +import { SubmitRepairAction } from './actions/SubmitRepairAction'; + +export function TicketStatusActions({ ticket }: { ticket: TicketDetail }) { + if (ticket.status === 'unassigned') { + return ; + } + + if (ticket.status === 'assigned') { + return ; + } + + if (ticket.status === 'under_review') { + return ; + } + + return ; +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx new file mode 100644 index 0000000..1eeb024 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useState } from 'react'; +import { Send, UserPlus } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { AssignableWorkerSelect } from '@/components/lookups/AssignableWorkerSelect'; +import type { TicketDetail } from '@/types'; + +import { useAssignTicketMutation } from '../../../hooks/useTicketQueries'; +import { TicketActionCard } from './TicketActionCard'; + +export function AssignTicketAction({ ticket }: { ticket: TicketDetail }) { + const [selectedUserId, setSelectedUserId] = useState(''); + const [assignNote, setAssignNote] = useState(''); + + const assignMutation = useAssignTicketMutation(ticket.id); + + return ( + +
+
+ + +
+
+ +