diff --git a/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx index 4be1d3f..ef54280 100644 --- a/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx +++ b/src/app/(modules)/ticket/[ticketId]/[defectClass]/review/components/IssueEvidencePanel.tsx @@ -1,4 +1,4 @@ -import { CalendarCheck2, CheckCircle2, ImageIcon, MapPin } from 'lucide-react'; +import { CalendarCheck2, ImageIcon, MapPin, Smartphone } from 'lucide-react'; import DetectionLocationMap from '@/components/map/detectionLocationMap'; import { Badge } from '@/components/ui/badge'; @@ -130,8 +130,8 @@ export function IssueEvidencePanel({

{repairProof?.submitted ? ( - - Repair Submitted + + Uploaded from App ) : null} @@ -151,7 +151,8 @@ export function IssueEvidencePanel({ {repairProof.submitted_at ? (

- Submitted {formatDate(repairProof.submitted_at)} + Uploaded from the field app on{' '} + {formatDate(repairProof.submitted_at)}

) : null} diff --git a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx index f57846a..1b02c7f 100644 --- a/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx +++ b/src/app/(modules)/ticket/[ticketId]/components/TicketStatusActions.tsx @@ -9,8 +9,8 @@ import { AssignTicketAction } from './actions/AssignTicketAction'; import { ClosedTicketAction } from './actions/ClosedTicketAction'; import { NoTicketAction } from './actions/NoTicketAction'; import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction'; +import { RequestExtensionAction } from './actions/RequestExtensionAction'; import { ReviewExtensionRequestAction } from './actions/ReviewExtensionRequestAction'; -import { SubmitRepairAction } from './actions/SubmitRepairAction'; interface TicketStatusActionsProps { ticket: TicketDetail; @@ -37,7 +37,7 @@ function getTicketAction(ticket: TicketDetail) { permissions={PERMISSIONS.TICKET.WORK} fallback={} > - + } > diff --git a/src/app/(modules)/ticket/[ticketId]/components/actions/RepairEvidenceForm.tsx b/src/app/(modules)/ticket/[ticketId]/components/actions/RepairEvidenceForm.tsx deleted file mode 100644 index f321147..0000000 --- a/src/app/(modules)/ticket/[ticketId]/components/actions/RepairEvidenceForm.tsx +++ /dev/null @@ -1,485 +0,0 @@ -'use client'; - -import { useEffect, useRef, useState, type ReactNode } from 'react'; -import { - Camera, - ImageIcon, - Loader2, - Play, - Plus, - Send, - Video, - X, -} from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { cn } from '@/lib/utils'; - -import { useSubmitRepairUploadMutation } from '../../../hooks/useTicketQueries'; - -const MAX_IMAGES = 30; -const MAX_NOTES = 500; - -function RequiredMark() { - return *; -} - -function formatVideoDuration(seconds: number) { - const totalSeconds = Math.floor(seconds); - const minutes = Math.floor(totalSeconds / 60); - const remainingSeconds = totalSeconds % 60; - return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`; -} - -interface MediaSectionHeaderProps { - icon: ReactNode; - title: string; - addLabel: string; - onAdd: () => void; - disabled?: boolean; -} - -function MediaSectionHeader({ - icon, - title, - addLabel, - onAdd, - disabled = false, -}: MediaSectionHeaderProps) { - return ( -
-
- {icon} - {title} -
- -
- ); -} - -function MediaHint({ children }: { children: ReactNode }) { - return ( -

- {children} -

- ); -} - -interface EmptyMediaPlaceholderProps { - icon: ReactNode; - title: string; - description: string; - className?: string; - disabled?: boolean; - onClick: () => void; -} - -function EmptyMediaPlaceholder({ - icon, - title, - description, - className, - disabled = false, - onClick, -}: EmptyMediaPlaceholderProps) { - return ( - - ); -} - -interface ImageThumbnailProps { - file: File; - previewUrl: string; - disabled?: boolean; - onRemove: () => void; -} - -function ImageThumbnail({ - file, - previewUrl, - disabled = false, - onRemove, -}: ImageThumbnailProps) { - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {file.name} - -
- ); -} - -interface VideoThumbnailProps { - file: File; - previewUrl: string; - duration: string | null; - disabled?: boolean; - onRemove: () => void; -} - -function VideoThumbnail({ - file, - previewUrl, - duration, - disabled = false, - onRemove, -}: VideoThumbnailProps) { - return ( -
-
- ); -} - -interface RepairEvidenceFormProps { - ticketId: string; - defectClass: string; -} - -export function RepairEvidenceForm({ - ticketId, - defectClass, -}: RepairEvidenceFormProps) { - const imageInputRef = useRef(null); - const videoInputRef = useRef(null); - const [notes, setNotes] = useState(''); - const [videoFile, setVideoFile] = useState(null); - const [videoPreview, setVideoPreview] = useState(null); - const [videoDuration, setVideoDuration] = useState(null); - const [imageFiles, setImageFiles] = useState([]); - const [imagePreviews, setImagePreviews] = useState([]); - const [error, setError] = useState(null); - const submitRepairUploadMutation = useSubmitRepairUploadMutation(ticketId); - const isSubmitting = submitRepairUploadMutation.isPending; - const canSubmit = Boolean(notes.trim() && videoFile); - const canAddMoreImages = imageFiles.length < MAX_IMAGES; - - useEffect(() => { - const urls = imageFiles.map((file) => URL.createObjectURL(file)); - setImagePreviews(urls); - return () => { - urls.forEach((url) => URL.revokeObjectURL(url)); - }; - }, [imageFiles]); - - useEffect(() => { - if (!videoFile) { - setVideoPreview(null); - setVideoDuration(null); - return; - } - - const url = URL.createObjectURL(videoFile); - setVideoPreview(url); - setVideoDuration(null); - - let cancelled = false; - const video = document.createElement('video'); - video.preload = 'metadata'; - video.onloadedmetadata = () => { - if (cancelled || !Number.isFinite(video.duration)) return; - setVideoDuration(formatVideoDuration(video.duration)); - }; - video.src = url; - - return () => { - cancelled = true; - URL.revokeObjectURL(url); - }; - }, [videoFile]); - - const addImages = (files: FileList | null) => { - if (!files?.length) return; - - const remaining = MAX_IMAGES - imageFiles.length; - if (remaining <= 0) { - setError(`Only ${MAX_IMAGES} images can be uploaded.`); - return; - } - - const nextImages = [ - ...imageFiles, - ...Array.from(files).slice(0, remaining), - ]; - setImageFiles(nextImages); - setError( - files.length > remaining - ? `Only ${MAX_IMAGES} images can be uploaded.` - : null, - ); - }; - - const selectVideo = (files: FileList | null) => { - setVideoFile(files?.[0] ?? null); - setError(null); - }; - - const removeVideo = () => { - setVideoFile(null); - setError(null); - }; - - const removeImage = (index: number) => { - setImageFiles((current) => - current.filter((_, itemIndex) => itemIndex !== index), - ); - setError(null); - }; - - const handleSubmit = async () => { - if (!notes.trim() || !videoFile) { - setError('Repair notes and a video are required.'); - return; - } - - setError(null); - try { - await submitRepairUploadMutation.mutateAsync({ - defect_class: defectClass, - notes: notes.trim(), - video: videoFile, - images: imageFiles, - }); - setNotes(''); - setVideoFile(null); - setImageFiles([]); - } catch (err) { - setError(err instanceof Error ? err.message : 'Upload failed'); - } - }; - - const openImagePicker = () => { - if (!isSubmitting && canAddMoreImages) { - imageInputRef.current?.click(); - } - }; - - const openVideoPicker = () => { - if (!isSubmitting) { - videoInputRef.current?.click(); - } - }; - - return ( -
-
- -
-

Instructions

-

- Upload clear photos and one video of the repaired area and provide - relevant details. -

-
-
- - { - addImages(event.target.files); - event.target.value = ''; - }} - /> - { - selectVideo(event.target.files); - event.target.value = ''; - }} - /> - -
- } - title="Photos" - addLabel="Add Photos" - onAdd={openImagePicker} - disabled={isSubmitting || !canAddMoreImages} - /> - -
- {imageFiles.length === 0 ? ( - } - title="No photos added yet" - description="Tap to upload repair photos" - disabled={isSubmitting} - onClick={openImagePicker} - className="min-h-26" - /> - ) : ( -
- {imageFiles.map((file, index) => ( - removeImage(index)} - /> - ))} -
- )} -
- - - You can upload up to {MAX_IMAGES} images (JPG, PNG) - -
- -
-
-
- -
- {!videoFile || !videoPreview ? ( - } - title="No video added yet" - description="Tap to upload a repair video" - disabled={isSubmitting} - onClick={openVideoPicker} - className="min-h-[104px]" - /> - ) : ( - - )} -
- - You can upload one video (MP4, MOV, AVI) -
- -
- -
-