From a567a52e26a011429292a6682bbef13438d36e0a Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Fri, 19 Jun 2026 11:22:41 +0530 Subject: [PATCH] feat(ticket): refine repair report and history timeline UI --- .../components/TicketHistoryCard.tsx | 125 +++++++ .../components/TicketOverviewCard.tsx | 92 +++++ .../components/TicketRepairReportCard.tsx | 41 +++ .../components/TicketStatusActions.tsx | 24 ++ .../components/actions/AssignTicketAction.tsx | 57 +++ .../components/actions/NoTicketAction.tsx | 42 +++ .../components/actions/RepairEvidenceForm.tsx | 305 ++++++++++++++++ .../components/actions/ReviewRepairAction.tsx | 63 ++++ .../components/actions/SubmitRepairAction.tsx | 16 + .../components/actions/TicketActionCard.tsx | 29 ++ .../repair-report/RepairNotesCard.tsx | 45 +++ .../repair-report/RepairProofImagesGrid.tsx | 120 ++++++ .../repair-report/RepairProofVideoCard.tsx | 138 +++++++ .../repair-report/ReviewerCommentsCard.tsx | 63 ++++ .../repair-report/repairReportUtils.tsx | 69 ++++ src/app/(modules)/ticket/[ticketId]/page.tsx | 345 +----------------- .../(modules)/ticket/hooks/useTicketEvents.ts | 6 +- .../ticket/hooks/useTicketQueries.ts | 60 ++- .../lookups/AssignableWorkerSelect.tsx | 71 ++++ .../lookups/AssignableWorkerSelect.types.ts | 5 + src/components/ui/avatar.tsx | 84 ++++- src/constants/apiRoutes.ts | 2 + src/hooks/useProtectedMedia.ts | 39 ++ src/services/api/index.ts | 1 + src/services/api/media.service.ts | 10 + src/services/api/ticket.service.ts | 36 +- src/types/ticket.ts | 25 +- 27 files changed, 1550 insertions(+), 363 deletions(-) create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/AssignTicketAction.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/NoTicketAction.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/RepairEvidenceForm.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/ReviewRepairAction.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/SubmitRepairAction.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/TicketActionCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairNotesCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofImagesGrid.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofVideoCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/ReviewerCommentsCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/repairReportUtils.tsx create mode 100644 src/components/lookups/AssignableWorkerSelect.tsx create mode 100644 src/components/lookups/AssignableWorkerSelect.types.ts create mode 100644 src/hooks/useProtectedMedia.ts create mode 100644 src/services/api/media.service.ts 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 ( + +
+
+ + +
+
+ +