ifixkart-admin/components/ui/MediaProofModal.tsx

224 lines
8.9 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { Upload, Video, Image as ImageIcon, X, Loader2, CheckCircle2 } from 'lucide-react';
import { toast } from 'sonner';
import { adminService } from '@/services/api/adminService';
interface MediaProofModalProps {
open: boolean;
onClose: () => void;
jobId: string;
targetStatus: 'INSPECTION_COMPLETED' | 'READY_FOR_DELIVERY';
onSuccess: () => void;
}
export function MediaProofModal({
open,
onClose,
jobId,
targetStatus,
onSuccess,
}: MediaProofModalProps) {
const [files, setFiles] = useState<File[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<string>('');
if (!open) return null;
const isInspection = targetStatus === 'INSPECTION_COMPLETED';
const title = isInspection ? 'Inspection Done Proof Required' : 'Ready for Delivery Proof Required';
const category = isInspection ? 'INSPECTION_DONE' : 'READY_FOR_DELIVERY';
const subtitle = isInspection
? 'Upload 1 short video (max 30 seconds) OR up to 4 photos of the inspected product condition before proceeding.'
: 'Upload video or photo proof of the completed repair showing the device fully functional and ready for delivery.';
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || e.target.files.length === 0) return;
const selectedList = Array.from(e.target.files);
const hasVideo = selectedList.some((f) => f.type.startsWith('video/'));
const images = selectedList.filter((f) => f.type.startsWith('image/'));
if (hasVideo && selectedList.length > 1) {
toast.error('Please upload either 1 video OR photos (not combined).');
return;
}
if (hasVideo) {
const videoFile = selectedList.find((f) => f.type.startsWith('video/'))!;
const videoEl = document.createElement('video');
videoEl.preload = 'metadata';
videoEl.onloadedmetadata = () => {
window.URL.revokeObjectURL(videoEl.src);
if (videoEl.duration > 30.5) {
toast.error(`Video length is ${Math.round(videoEl.duration)}s. Maximum allowed video length is 30 seconds.`);
} else {
setFiles([videoFile]);
toast.success(`Video verified (${Math.round(videoEl.duration)}s). Ready to upload.`);
}
};
videoEl.onerror = () => {
toast.error('Failed to parse video file duration.');
};
videoEl.src = URL.createObjectURL(videoFile);
return;
}
if (images.length > 4) {
toast.error('Maximum 4 photos allowed.');
setFiles(images.slice(0, 4));
} else {
setFiles(images);
}
};
const removeFile = (index: number) => {
setFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleSubmit = async () => {
if (files.length === 0) {
toast.warning('Please select at least 1 video or photo proof to continue.');
return;
}
setUploading(true);
setUploadProgress('Starting upload...');
try {
const fileIds: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileSizeMb = (file.size / (1024 * 1024)).toFixed(1);
setUploadProgress(`Uploading file ${i + 1} of ${files.length} (${fileSizeMb} MB)...`);
const res = await adminService.uploadMediaFile(file);
if (res.file_id) {
fileIds.push(res.file_id);
}
}
if (fileIds.length === 0) {
throw new Error('Media file upload failed.');
}
setUploadProgress('Saving job status & linking proof...');
await adminService.attachJobMedia(jobId, category, fileIds);
await adminService.updateServiceJobStatus(jobId, targetStatus);
toast.success(`${isInspection ? 'Inspection proof' : 'Ready for delivery proof'} uploaded successfully.`);
onSuccess();
onClose();
} catch (err: any) {
toast.error(err.message || 'Failed to attach media proof.');
} finally {
setUploading(false);
setUploadProgress('');
}
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-2xl max-w-lg w-full p-6 border border-slate-200 dark:border-slate-800 animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-start justify-between border-b border-slate-100 dark:border-slate-800 pb-4">
<div>
<div className="flex items-center gap-2">
<span className="p-2 rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-950 dark:text-blue-400">
{isInspection ? <Video className="w-5 h-5" /> : <CheckCircle2 className="w-5 h-5" />}
</span>
<h3 className="text-lg font-bold text-slate-900 dark:text-white">{title}</h3>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">{subtitle}</p>
</div>
<button
onClick={onClose}
disabled={uploading}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="my-6 space-y-4">
<label className="border-2 border-dashed border-slate-300 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-400 rounded-xl p-6 flex flex-col items-center justify-center cursor-pointer bg-slate-50/50 dark:bg-slate-800/50 hover:bg-blue-50/30 dark:hover:bg-blue-950/20 transition group">
<Upload className="w-8 h-8 text-slate-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 mb-2 transition-transform group-hover:-translate-y-1" />
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">
Click to select 30s Video or Photos
</span>
<span className="text-xs text-slate-400 mt-1">
Supports MP4, WEBM (Max 30s) or JPG, PNG (Max 4 photos)
</span>
<input
type="file"
accept="video/*,image/*"
multiple
onChange={handleFileChange}
disabled={uploading}
className="hidden"
/>
</label>
{files.length > 0 && (
<div className="space-y-2">
<span className="text-xs font-semibold text-slate-500 uppercase tracking-wider">
Selected Proof Files ({files.length})
</span>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto pr-1">
{files.map((file, idx) => (
<div
key={idx}
className="flex items-center justify-between p-2 rounded-lg bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-xs"
>
<div className="flex items-center gap-2 truncate pr-2">
{file.type.startsWith('video/') ? (
<Video className="w-4 h-4 text-blue-500 shrink-0" />
) : (
<ImageIcon className="w-4 h-4 text-emerald-500 shrink-0" />
)}
<span className="truncate text-slate-700 dark:text-slate-300 font-medium">
{file.name}
</span>
</div>
{!uploading && (
<button
onClick={() => removeFile(idx)}
className="text-slate-400 hover:text-red-500 p-0.5 rounded transition"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
))}
</div>
</div>
)}
</div>
<div className="flex items-center justify-end gap-3 border-t border-slate-100 dark:border-slate-800 pt-4">
<button
type="button"
onClick={onClose}
disabled={uploading}
className="px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition"
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={uploading || files.length === 0}
className="px-5 py-2 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 rounded-lg shadow-sm flex items-center gap-2 transition"
>
{uploading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{uploadProgress || 'Uploading...'}
</>
) : (
'Confirm & Save Status'
)}
</button>
</div>
</div>
</div>
);
}