From a731fca5e032582a4a8498a4568fdb38141ad08e Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Tue, 23 Jun 2026 10:29:09 +0530 Subject: [PATCH] refactor: video section ui modification --- next-env.d.ts | 2 +- .../components/TicketDetailHeader.tsx | 53 ++++++ .../components/TicketHistoryCard.tsx | 20 +- .../components/TicketOverviewCard.tsx | 174 ++++++++++++------ .../components/TicketRepairReportCard.tsx | 34 ++-- .../components/TicketStatusActions.tsx | 5 + .../components/actions/ClosedTicketAction.tsx | 155 ++++++++++++++++ .../components/actions/NoTicketAction.tsx | 11 +- .../repair-report/RepairNotesCard.tsx | 45 ----- .../repair-report/RepairProofImagesGrid.tsx | 31 ++-- .../repair-report/RepairProofVideoCard.tsx | 138 -------------- .../repair-report/RepairReportPanel.tsx | 104 +++++++++++ .../repair-report/RepairVideoComparison.tsx | 75 ++++++++ .../repair-report/ReviewerCommentsCard.tsx | 63 ------- src/app/(modules)/ticket/[ticketId]/page.tsx | 23 +-- .../components/UploadSettingsSection.tsx | 1 + src/components/media/ProtectedVideoPlayer.tsx | 112 +++++++++++ 17 files changed, 668 insertions(+), 378 deletions(-) create mode 100644 src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx delete mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairNotesCard.tsx delete mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofVideoCard.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairReportPanel.tsx create mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairVideoComparison.tsx delete mode 100644 src/app/(modules)/ticket/[ticketId]/components/repair-report/ReviewerCommentsCard.tsx create mode 100644 src/components/media/ProtectedVideoPlayer.tsx diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx new file mode 100644 index 0000000..4eeb54e --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketDetailHeader.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { ArrowLeft, BarChart3 } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +import { Button } from '@/components/ui/button'; +import { PERMISSIONS } from '@/constants/permissions'; +import { PermissionGuard } from '@/guards'; +import type { TicketDetail } from '@/types'; + +import { TicketStatusBadge } from '../../components/TicketStatusBadge'; + +export function TicketDetailHeader({ ticket }: { ticket: TicketDetail }) { + const router = useRouter(); + + return ( +
+
+
+

+ Ticket Detail +

+ +
+

Ticket ID: {ticket.id}

+
+ +
+ {ticket.video_id ? ( + + + + ) : null} + + +
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx index 05ccb26..ea1c1a5 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketHistoryCard.tsx @@ -1,14 +1,13 @@ 'use client'; -import { History } from 'lucide-react'; +import { History, MessageSquareText } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { TicketDetail, TicketHistoryItem, TicketStatus } from '@/types'; +import { formatDate } from '@/utils/date'; import { formatTicketStatus } from '../../components/TicketStatusBadge'; -import { formatDate } from '@/utils/date'; - function getActorLabel(item: TicketHistoryItem) { return item.actor?.name || item.actor?.email || 'System'; } @@ -61,7 +60,7 @@ function HistoryEntry({ /> -
+

{getLifecycleTitle(item.to_status)} @@ -74,12 +73,17 @@ function HistoryEntry({

-

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

+ Updated By{' '} + {getActorLabel(item)}

-

{getActorLabel(item)}

+ {item.note ? ( +
+ +

{item.note}

+
+ ) : null}
); diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx index e96d0fc..af2693e 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketOverviewCard.tsx @@ -1,86 +1,138 @@ 'use client'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import type { TicketActor, TicketDetail } from '@/types'; - -import { TicketStatusBadge } from '../../components/TicketStatusBadge'; - import { formatDate } from '@/utils/date'; -function OverviewMeta({ - label, - value, -}: { - label: string; - value: string | number | null | undefined; -}) { +import { + TicketStatusBadge, +} from '../../components/TicketStatusBadge'; + +function formatCreatedDate(value: string | null | undefined) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { dateStyle: 'medium' }).format( + new Date(value), + ); +} + +function getInitials(person: TicketActor | null | undefined) { + const source = person?.name || person?.email || '?'; + return source + .split(/[\s@._-]+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part.charAt(0).toUpperCase()) + .join(''); +} + +function getPersonLabel(person: TicketActor | null | undefined) { + return person?.name || person?.email || '-'; +} + +function MetaLabel({ children }: { children: React.ReactNode }) { return ( -
-

{label}

-

{value ?? '-'}

+

+ {children} +

+ ); +} + +function MetaValue({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +function PersonMetaValue({ + person, + accentClassName = 'bg-primary/15 text-primary', +}: { + person: TicketActor | null | undefined; + accentClassName?: string; +}) { + if (!person) { + return -; + } + + return ( +
+ + {getInitials(person)} + +

{getPersonLabel(person)}

); } -function PersonMeta({ +function OverviewField({ label, - person, + children, }: { label: string; - person: TicketActor | null | undefined; + children: React.ReactNode; }) { return (
-

{label}

- {person ? ( -
-

{person.name || '-'}

- {person.email && ( -

- {person.email} -

- )} -
- ) : ( -

-

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

AI Detections

-

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

-

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

-
+ const detectionCount = ticket.ai_result?.detection_count; -
- - - - -
+ return ( +
+
+

+ Overview +

+
+ +
+ + + {detectionCount != null ? `${detectionCount} Occurrences` : '-'} + + + + + {ticket.chainage_id || '-'} + + + + + + + + {formatCreatedDate(ticket.timestamps.created_at)} + + + + + + + + + + + + {formatDate(ticket.worker?.assigned_at)} + + +
+ + {ticket.ai_result?.completed_at ? ( +
+ AI analysis completed {formatDate(ticket.ai_result.completed_at)}
- - + ) : null} +
); } diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx index af0b6b9..53c0473 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketRepairReportCard.tsx @@ -1,41 +1,33 @@ 'use client'; +import { ImageIcon } from 'lucide-react'; + 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'; +import { RepairReportPanel } from './repair-report/RepairReportPanel'; +import { RepairVideoComparison } from './repair-report/RepairVideoComparison'; export function TicketRepairReportCard({ ticket }: { ticket: TicketDetail }) { const repair = ticket.repair; return ( -
-

- Evidence & Progress -

+
+ -
- + + +
+

+ + Repair Images +

- -
- - -
); } diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx index 189467d..e0a08a3 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx @@ -6,6 +6,7 @@ import type { TicketDetail } from '@/types'; import type { TicketDetailLiveState } from '../../hooks/useTicketDetailEvents'; import { AssignTicketAction } from './actions/AssignTicketAction'; +import { ClosedTicketAction } from './actions/ClosedTicketAction'; import { NoTicketAction } from './actions/NoTicketAction'; import { ProcessingTicketAction } from './actions/ProcessingTicketAction'; import { ReviewRepairAction } from './actions/ReviewRepairAction'; @@ -62,5 +63,9 @@ export function TicketStatusActions({ ); } + if (ticket.status === 'closed') { + return ; + } + return ; } diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx new file mode 100644 index 0000000..d0777c2 --- /dev/null +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/ClosedTicketAction.tsx @@ -0,0 +1,155 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { Lock, MessageSquare } from 'lucide-react'; + +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { cn } from '@/lib/utils'; +import type { TicketDetail } from '@/types'; + +function getInitials(name?: string | null, email?: string | null) { + const source = name || email || '?'; + return source + .split(/[\s@._-]+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part.charAt(0).toUpperCase()) + .join(''); +} + +function getPersonLabel(person: TicketDetail['reviewer']) { + return person?.name || person?.email || 'Unknown'; +} + +function formatReviewDate(value: string | null | undefined) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { dateStyle: 'medium' }).format( + new Date(value), + ); +} + +function getClosedAt(ticket: TicketDetail) { + const closedEntry = ticket.history?.find( + (item) => item.to_status === 'closed', + ); + return ( + closedEntry?.created_at ?? + ticket.reviewer?.reviewed_at ?? + ticket.timestamps.updated_at + ); +} + +function MetaField({ + label, + value, + valueClassName, +}: { + label: string; + value: ReactNode; + valueClassName?: string; +}) { + return ( +
+

+ {label} +

+
+ {value} +
+
+ ); +} + +function FinalCommentsPanel({ + comment, +}: { + comment: string | null | undefined; +}) { + if (!comment?.trim()) { + return ( +
+ +

No comments added yet

+
+ ); + } + + return ( +
+

+ {comment.trim()} +

+
+ ); +} + +export function ClosedTicketAction({ ticket }: { ticket: TicketDetail }) { + const reviewer = ticket.reviewer; + const reviewedAt = reviewer?.reviewed_at ?? getClosedAt(ticket); + + return ( + + + Resolution Details + Review record for {ticket.id} + + + +
+ + + + {getInitials(reviewer?.name, reviewer?.email)} + + +
+

{getPersonLabel(reviewer)}

+ {reviewer?.email ? ( +

+ {reviewer.email} +

+ ) : null} +
+
+ } + /> + + +
+ +
+

+ Review Comment +

+ +
+ + +
+
+ ); +} diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/NoTicketAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/NoTicketAction.tsx index 8af9790..8e883d0 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/actions/NoTicketAction.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/actions/NoTicketAction.tsx @@ -1,13 +1,13 @@ 'use client'; -import { CircleOff, Clock, Lock } from 'lucide-react'; +import { CircleOff, Clock } from 'lucide-react'; import type { TicketStatus } from '@/types'; import { TicketActionCard } from './TicketActionCard'; const statusConfig: Record< - 'processing' | 'closed', + 'processing', { icon: typeof Clock; title: string; message: string } > = { processing: { @@ -15,16 +15,11 @@ const statusConfig: Record< title: 'Processing', message: 'This ticket is still being processed. Check back shortly.', }, - closed: { - icon: Lock, - title: 'Ticket Closed', - message: 'This ticket is closed. No further actions are available.', - }, }; export function NoTicketAction({ status }: { status: TicketStatus }) { const config = - status === 'processing' || status === 'closed' + status === 'processing' ? statusConfig[status] : { icon: CircleOff, diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairNotesCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairNotesCard.tsx deleted file mode 100644 index 9740f3b..0000000 --- a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairNotesCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -import { MessageSquareQuote, ShieldCheck } from 'lucide-react'; - -import type { TicketDetail } from '@/types'; - -import { - EmptyPanel, - getPersonLabel, - ReportSurface, - SectionLabel, -} from './repairReportUtils'; - -export function RepairNotesCard({ - notes, - worker, -}: { - notes: string | null; - worker: TicketDetail['worker']; -}) { - return ( - - Repair Notes - {notes ? ( - <> -
-

- {notes} -

-
-
- - Self-certified by {getPersonLabel(worker)} -
- - ) : ( - - )} -
- ); -} diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofImagesGrid.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofImagesGrid.tsx index 74fee79..71959b0 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofImagesGrid.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofImagesGrid.tsx @@ -11,19 +11,21 @@ import { formatRepairDate, ReportSurface } from './repairReportUtils'; const MAX_MEDIA_SLOTS = 5; const imageBadgeStyles = [ - 'bg-slate-700/90 text-white', - 'bg-violet-600/90 text-white', - 'bg-amber-600/90 text-white', - 'bg-stone-600/90 text-white', - 'bg-indigo-600/90 text-white', + 'bg-primary/90 text-primary-foreground', + 'bg-secondary text-secondary-foreground', + 'bg-accent text-accent-foreground', + 'bg-muted text-muted-foreground', + 'bg-primary/70 text-primary-foreground', ] as const; export function RepairProofImagesGrid({ imageUrls, submittedAt, + layout = 'row', }: { imageUrls: string[]; submittedAt: string | null; + layout?: 'row' | 'sidebar'; }) { const imageSlots = Array.from( { length: MAX_MEDIA_SLOTS }, @@ -31,7 +33,14 @@ export function RepairProofImagesGrid({ ); return ( -
+
{imageSlots.map((url, index) => ( - - Proof {index + 1} -

{submittedAt ? formatRepairDate(submittedAt) : `Image ${index + 1}`} diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofVideoCard.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofVideoCard.tsx deleted file mode 100644 index b0458ed..0000000 --- a/src/app/(modules)/ticket/[ticketId]/components/repair-report/RepairProofVideoCard.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import { useRef, useState } from 'react'; -import { CircleDashed, Play, Video } from 'lucide-react'; - -import { Skeleton } from '@/components/ui/skeleton'; -import { useProtectedMediaObjectUrl } from '@/hooks/useProtectedMedia'; - -import { EmptyPanel, ReportSurface } from './repairReportUtils'; - -interface RepairProofVideoCardProps { - ticketId: string; - proofVideoUrl: string | null; - hasRepair: boolean; -} - -export function RepairProofVideoCard({ - ticketId, - proofVideoUrl, - hasRepair, -}: RepairProofVideoCardProps) { - if (!proofVideoUrl) { - return ( - - - - ); - } - - return ( - - ); -} - -function ProtectedRepairVideo({ - url, - label, - meta, -}: { - url: string; - label: string; - meta: string; -}) { - const videoRef = useRef(null); - const [isPlaying, setIsPlaying] = useState(false); - const { objectUrl, isLoading, isError } = useProtectedMediaObjectUrl(url); - - if (isLoading) { - return ; - } - - if (isError || !objectUrl) { - return ( - - - - ); - } - - const togglePlay = () => { - const video = videoRef.current; - if (!video) return; - - if (video.paused) { - void video.play(); - setIsPlaying(true); - return; - } - - video.pause(); - setIsPlaying(false); - }; - - return ( -

-