diff --git a/src/app/(modules)/results/[videoId]/page.tsx b/src/app/(modules)/results/[videoId]/page.tsx index 37f5ef8..2de7fda 100644 --- a/src/app/(modules)/results/[videoId]/page.tsx +++ b/src/app/(modules)/results/[videoId]/page.tsx @@ -1,10 +1,11 @@ 'use client'; import { useMemo } from 'react'; -import { useParams } from 'next/navigation'; -import { Loader2, TrendingUp } from 'lucide-react'; +import { useParams, useRouter } from 'next/navigation'; +import { ArrowLeft, Loader2, TrendingUp } from 'lucide-react'; import VideoPlayerSection from '@/components/videoPlayerSection'; import { PageHeader } from '@/components/page-header'; +import { Button } from '@/components/ui/button'; import { CompletedVideoResult, DetectionType } from '@/types'; import { getDetectionModeConfig } from '@/constants/detectionModeConfig'; import { useVideoResultsQuery } from '../hooks/useVideoResults'; @@ -21,6 +22,7 @@ const inferDetectionType = (data: CompletedVideoResult): DetectionType => { }; export default function VideoResultsPage() { + const router = useRouter(); const { videoId } = useParams() as { videoId: string }; const { @@ -31,7 +33,8 @@ export default function VideoResultsPage() { } = useVideoResultsQuery(videoId); const detectionType = useMemo( - () => (detectionData ? inferDetectionType(detectionData) : 'pothole-detection'), + () => + detectionData ? inferDetectionType(detectionData) : 'pothole-detection', [detectionData], ); @@ -68,6 +71,12 @@ export default function VideoResultsPage() { title={getTitle()} description={`Video ID: ${videoId}`} icon={TrendingUp} + actions={ + + } /> {detectionData && } diff --git a/src/app/(modules)/ticket/[ticketId]/page.tsx b/src/app/(modules)/ticket/[ticketId]/page.tsx new file mode 100644 index 0000000..2d1a8fd --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/page.tsx @@ -0,0 +1,403 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { ArrowLeft, Check, Loader2, Send, Ticket, X } from 'lucide-react'; + +import { PageHeader } from '@/components/page-header'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import type { AssignableTicketUser } from '@/types'; + +import { + TicketStatusBadge, + formatTicketStatus, +} from '../components/TicketStatusBadge'; +import { useTicketDetailEvents } from '../hooks/useTicketEvents'; +import { + useAssignableTicketUsersQuery, + useAssignTicketMutation, + useReviewRepairMutation, + useSubmitRepairMutation, + useTicketDetailQuery, +} from '../hooks/useTicketQueries'; + +function formatDate(value: string | null | undefined) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(value)); +} + +function Field({ + label, + value, +}: { + label: string; + value: string | number | null | undefined; +}) { + return ( +
+

{label}

+

{value ?? '-'}

+
+ ); +} + +export default function TicketDetailPage() { + const router = useRouter(); + const { ticketId } = useParams() as { ticketId: string }; + const [selectedUserId, setSelectedUserId] = useState(''); + const [assignNote, setAssignNote] = useState(''); + const [repairNotes, setRepairNotes] = useState(''); + const [proofPath, setProofPath] = useState(''); + const [reviewComment, setReviewComment] = useState(''); + + const ticketQuery = useTicketDetailQuery(ticketId); + const ticket = ticketQuery.data; + const assignableUsersQuery = useAssignableTicketUsersQuery( + ticket?.status === 'unassigned', + ); + const assignMutation = useAssignTicketMutation(ticketId); + const submitRepairMutation = useSubmitRepairMutation(ticketId); + const reviewRepairMutation = useReviewRepairMutation(ticketId); + + useTicketDetailEvents(ticketId); + + useEffect(() => { + if (!ticket) return; + setRepairNotes(ticket.repair?.notes ?? ''); + setProofPath(ticket.repair?.proof_path ?? ''); + setReviewComment(ticket.reviewer?.review_comment ?? ''); + }, [ticket]); + + const assignableUsers = assignableUsersQuery.data?.items ?? []; + const selectedUser = useMemo( + () => + assignableUsers.find((user) => String(user.id) === selectedUserId) as + | AssignableTicketUser + | undefined, + [assignableUsers, selectedUserId], + ); + + const isBusy = + assignMutation.isPending || + submitRepairMutation.isPending || + reviewRepairMutation.isPending; + + if (ticketQuery.isLoading) { + return ( +
+ +
+ ); + } + + if (ticketQuery.isError || !ticket) { + return ( +
+

Failed to load ticket.

+ +
+ ); + } + + const canAssign = ticket.status === 'unassigned'; + const canSubmitRepair = ticket.status === 'assigned'; + const canReview = ticket.status === 'under_review'; + + return ( +
+ router.push('/ticket')} + > + + Back + + } + /> + +
+
+ + + Overview + + + + + + + + + + + + + + + + + + + + Repair Review + + + + + + + + + + + + + + History + + +
+ {ticket.history.length ? ( + ticket.history.map((item) => ( +
+
+ + {item.from_status + ? `${formatTicketStatus(item.from_status)} -> ` + : ''} + {formatTicketStatus(item.to_status)} + + + {formatDate(item.created_at)} + +
+

+ {item.note || '-'} +

+

+ {item.actor?.name || item.actor?.email || 'System'} +

+
+ )) + ) : ( +

+ No history yet. +

+ )} +
+
+
+
+ +
+ {canAssign && ( + + + Assign Ticket + + +
+ + +
+
+ +