Detection Image
-
@@ -265,7 +308,7 @@ export function TicketClassDetectionPreview({
label={`${displayName} - Frame ${activeDetection.frame.number}`}
title="Location on Map"
variant="plain"
- aspectRatio={mediaAspectRatio}
+ aspectRatio="16 / 9"
showCoordinates={false}
/>
@@ -287,6 +330,12 @@ export function TicketClassDetectionPreview({
]}
/>
+ {activeDetection.repair_proof ? (
+
- ) : null;
-
- return (
-
- {submittedProof}
- {action}
-
- );
+ return
{getTicketAction(ticket)}
;
}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/SubmittedRepairProofAction.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/SubmittedRepairProofAction.tsx
deleted file mode 100644
index f839860..0000000
--- a/src/app/(modules)/ticket/[ticketId]/components/actions/SubmittedRepairProofAction.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { RepairProofSummary as SubmittedRepairProofAction } from '../repair/RepairProofSummary';
diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofGallery.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofGallery.tsx
index 2e98778..4f9992d 100644
--- a/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofGallery.tsx
+++ b/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofGallery.tsx
@@ -11,11 +11,15 @@ import { protectedMediaService } from '@/services/api';
interface RepairProofGalleryProps {
imageUrls: string[];
+ maxVisibleImages?: number;
}
const MAX_VISIBLE_IMAGES = 8;
-export function RepairProofGallery({ imageUrls }: RepairProofGalleryProps) {
+export function RepairProofGallery({
+ imageUrls,
+ maxVisibleImages = MAX_VISIBLE_IMAGES,
+}: RepairProofGalleryProps) {
const queries = useQueries({
queries: imageUrls.map((url) => ({
queryKey: ['protected-media', 'v2', url],
@@ -44,7 +48,7 @@ export function RepairProofGallery({ imageUrls }: RepairProofGalleryProps) {
if (queries.some((query) => query.isLoading)) {
return (
- {imageUrls.slice(0, MAX_VISIBLE_IMAGES).map((url, index) => (
+ {imageUrls.slice(0, maxVisibleImages).map((url, index) => (
url ? [{ url, originalIndex: index }] : [],
);
- const visibleCount = Math.min(MAX_VISIBLE_IMAGES, images.length);
+ const visibleCount = Math.min(maxVisibleImages, images.length);
const overflowCount = images.length - visibleCount;
if (images.length === 0) {
@@ -102,8 +106,8 @@ export function RepairProofGallery({ imageUrls }: RepairProofGalleryProps) {
className="size-full object-cover transition duration-300 group-hover:scale-[1.03] group-hover:brightness-90"
/>
{isOverflowTile ? (
-
- +{overflowCount}
+
+ +{overflowCount} more
) : null}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofSummary.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofSummary.tsx
deleted file mode 100644
index 62a1110..0000000
--- a/src/app/(modules)/ticket/[ticketId]/components/repair/RepairProofSummary.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { CheckCircle2, HardHat, Wrench, ToolCase } from 'lucide-react';
-
-import { ProtectedVideoPlayer } from '@/components/media/ProtectedVideoPlayer';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import type { TicketDetail } from '@/types';
-
-import { formatDate } from '@/utils/date';
-import { RepairProofGallery } from './RepairProofGallery';
-
-function SubmittedBanner({
- submittedAt,
- submitterName,
-}: {
- submittedAt: string;
- submitterName: string;
-}) {
- return (
-
-
-
-
- Repair Submitted
-
-
- {formatDate(submittedAt)} by {submitterName}
-
-
-
- );
-}
-
-function ImageGallery({
- imageUrls,
- hasVideo,
-}: {
- imageUrls: string[];
- hasVideo: boolean;
-}) {
- if (imageUrls.length === 0) {
- return hasVideo ? null : (
-
-
- No media attached
-
- );
- }
-
- return ;
-}
-
-function Notes({ notes }: { notes: string | null | undefined }) {
- return (
-
-
Repair Notes
-
- {notes?.trim() || 'No repair notes provided.'}
-
-
- );
-}
-
-export function RepairProofSummary({ ticket }: { ticket: TicketDetail }) {
- const repair = ticket.repair;
-
- if (!repair?.submitted_at) {
- return null;
- }
-
- const submitterName = ticket.worker?.name?.trim() || 'Assigned worker';
-
- return (
-
-
-
-
- Repair Proof
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(modules)/ticket/[ticketId]/components/repair/RepairSubmittedBanner.tsx b/src/app/(modules)/ticket/[ticketId]/components/repair/RepairSubmittedBanner.tsx
new file mode 100644
index 0000000..4820845
--- /dev/null
+++ b/src/app/(modules)/ticket/[ticketId]/components/repair/RepairSubmittedBanner.tsx
@@ -0,0 +1,27 @@
+import { CheckCircle2 } from 'lucide-react';
+
+import { formatDate } from '@/utils/date';
+
+interface RepairSubmittedBannerProps {
+ submittedAt: string;
+ submitterName: string;
+}
+
+export function RepairSubmittedBanner({
+ submittedAt,
+ submitterName,
+}: RepairSubmittedBannerProps) {
+ return (
+
+
+
+
+ Repair Submitted
+
+
+ {formatDate(submittedAt)} by {submitterName}
+
+
+
+ );
+}
diff --git a/src/components/video/annotatedDetectionImage.tsx b/src/components/video/annotatedDetectionImage.tsx
index e65c0cf..44e0881 100644
--- a/src/components/video/annotatedDetectionImage.tsx
+++ b/src/components/video/annotatedDetectionImage.tsx
@@ -1,8 +1,10 @@
'use client';
+import { useState } from 'react';
import { AlertTriangle, ImageIcon, Loader2 } from 'lucide-react';
import Image from 'next/image';
+import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useProtectedMediaObjectUrl } from '@/hooks/useProtectedMedia';
import type { BoundingBoxOverlayItem, DetectionResultItem } from '@/types';
@@ -12,13 +14,18 @@ interface AnnotatedDetectionImageProps {
detection?: DetectionResultItem;
videoWidth: number;
videoHeight: number;
+ aspectRatio?: string;
+ enableLightbox?: boolean;
}
export default function AnnotatedDetectionImage({
detection,
videoWidth,
videoHeight,
+ aspectRatio: aspectRatioOverride,
+ enableLightbox = false,
}: AnnotatedDetectionImageProps) {
+ const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const { objectUrl, isLoading, isError } = useProtectedMediaObjectUrl(
detection?.detection.context_image_url,
);
@@ -35,9 +42,65 @@ export default function AnnotatedDetectionImage({
: [];
const aspectRatio =
- videoWidth > 0 && videoHeight > 0
+ aspectRatioOverride ??
+ (videoWidth > 0 && videoHeight > 0
? `${videoWidth} / ${videoHeight}`
- : '16 / 9';
+ : '16 / 9');
+
+ const annotatedMedia = objectUrl ? (
+ <>
+
+
+ >
+ ) : null;
+
+ if (detection && objectUrl && enableLightbox) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
return (
Failed to load the detection image.
) : (
- <>
-
-
- >
+ annotatedMedia
)}
);
diff --git a/src/types/detection.ts b/src/types/detection.ts
index c81b316..f3ee7fa 100644
--- a/src/types/detection.ts
+++ b/src/types/detection.ts
@@ -22,6 +22,16 @@ export type BoundingBoxOverlayItem = {
bounding_box: DetectionBoundingBox;
};
+export type DetectionRepairProof = {
+ submitted: boolean;
+ review_status: 'pending' | 'approved' | 'rejected';
+ notes: string | null;
+ proof_image_urls: string[];
+ submitted_at: string | null;
+ reviewed_at: string | null;
+ review_comment: string | null;
+};
+
export type DetectionResultItem = {
id: string;
detection: {
@@ -42,6 +52,7 @@ export type DetectionResultItem = {
latitude: number | null;
longitude: number | null;
};
+ repair_proof: DetectionRepairProof | null;
};
export type DetectionResultLog = DetectionResultItem & {