223 lines
9.0 KiB
TypeScript
223 lines
9.0 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
import { Item } from '@/lib/db';
|
|
import { inventoryApi, buildPhotoUrl } from '@/lib/api';
|
|
import ItemPhotoUpload from '@/components/ItemPhotoUpload';
|
|
import { DebugRotationPanel } from '@/components/DebugRotationPanel';
|
|
import { X, Camera, Trash2, Wrench } from 'lucide-react';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
interface ItemDetailModalProps {
|
|
item: Item;
|
|
onClose: () => void;
|
|
onPhotoUpdated?: (photo: { thumbnail_url: string; full_url: string; uploaded_at: string }) => void;
|
|
onItemRefresh?: () => void;
|
|
backendUrl?: string;
|
|
}
|
|
|
|
export default function ItemDetailModal({
|
|
item,
|
|
onClose,
|
|
onPhotoUpdated,
|
|
onItemRefresh,
|
|
backendUrl = '',
|
|
}: ItemDetailModalProps) {
|
|
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
|
|
const [showDebugPanel, setShowDebugPanel] = useState(false);
|
|
const [currentPhoto, setCurrentPhoto] = useState<{ thumbnail_url: string; full_url: string } | null>(
|
|
item.photo_path && item.photo_thumbnail_path
|
|
? { thumbnail_url: item.photo_thumbnail_path, full_url: item.photo_path }
|
|
: item.image_url
|
|
? { thumbnail_url: item.image_url, full_url: item.image_url }
|
|
: null
|
|
);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const photoUploadRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handlePhotoUploadSuccess = (photo: { thumbnail_url: string; full_url: string; uploaded_at: string }) => {
|
|
setCurrentPhoto({ thumbnail_url: photo.thumbnail_url, full_url: photo.full_url });
|
|
setShowPhotoUpload(false);
|
|
onPhotoUpdated?.(photo);
|
|
onItemRefresh?.();
|
|
};
|
|
|
|
const handlePhotoUploadError = (errorMessage: string) => {
|
|
toast.error(errorMessage);
|
|
};
|
|
|
|
const handleDeletePhoto = async () => {
|
|
if (!item.id) return;
|
|
|
|
if (!window.confirm('Delete this photo? This cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
setIsDeleting(true);
|
|
try {
|
|
await inventoryApi.deleteItemPhoto(item.id);
|
|
setCurrentPhoto(null);
|
|
toast.success('Photo deleted successfully');
|
|
onItemRefresh?.();
|
|
} catch (error: any) {
|
|
const errorMsg = error?.message || 'Failed to delete photo';
|
|
toast.error(errorMsg);
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 font-sans">
|
|
<div className="bg-[#0A0A0A] border border-[#222222] rounded-none max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-none">
|
|
{/* Header */}
|
|
<div className="sticky top-0 bg-[#0A0A0A] border-b border-[#222222] p-4 md:p-6 flex items-center justify-between">
|
|
<h2 className="text-xl md:text-2xl font-normal text-white truncate">{item.name}</h2>
|
|
<div className="flex items-center gap-2">
|
|
{currentPhoto && (
|
|
<button
|
|
onClick={() => setShowDebugPanel(true)}
|
|
className="p-2 hover:bg-[#1A1A1A] rounded-none text-yellow-400 hover:text-yellow-300 transition-colors border border-transparent hover:border-[#222222]"
|
|
title="Debug rotation & crop"
|
|
aria-label="Debug panel"
|
|
>
|
|
<Wrench size={20} />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-[#1A1A1A] rounded-none text-[#888888] hover:text-white transition-colors border border-transparent hover:border-[#222222]"
|
|
aria-label="Close modal"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-4 md:p-6 space-y-6">
|
|
{/* Item Details */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-xs text-[#888888] mb-1">Category</p>
|
|
<p className="text-sm text-white">{item.category}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#888888] mb-1">Type</p>
|
|
<p className="text-sm text-white">{item.type || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#888888] mb-1">Quantity</p>
|
|
<p className="text-sm text-white">{item.quantity}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-[#888888] mb-1">Part Number</p>
|
|
<p className="text-sm text-white">{item.part_number || 'N/A'}</p>
|
|
</div>
|
|
{item.barcode && (
|
|
<div className="col-span-2">
|
|
<p className="text-xs text-[#888888] mb-1">Barcode</p>
|
|
<p className="text-sm text-white font-mono">{item.barcode}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Photo Section */}
|
|
<div className="border-t border-[#222222] pt-6">
|
|
<h3 className="text-lg font-normal text-white mb-4 flex items-center gap-2">
|
|
<Camera size={18} className="text-primary" />
|
|
Photo
|
|
</h3>
|
|
|
|
{!showPhotoUpload ? (
|
|
<>
|
|
{currentPhoto ? (
|
|
<div className="space-y-3">
|
|
<div className="relative bg-black border border-[#222222] rounded-none overflow-hidden aspect-video max-h-96">
|
|
<img
|
|
src={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
|
|
alt={item.name}
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setShowPhotoUpload(true)}
|
|
className="flex-1 px-4 py-2 bg-primary/20 border border-primary/50 text-primary rounded-none hover:bg-primary/30 transition-colors font-normal text-sm"
|
|
>
|
|
Replace Photo
|
|
</button>
|
|
<button
|
|
onClick={handleDeletePhoto}
|
|
disabled={isDeleting}
|
|
className="px-4 py-2 bg-rose-500/20 border border-rose-500/50 text-rose-400 rounded-none hover:bg-rose-500/30 disabled:opacity-50 transition-colors font-normal text-sm"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-black border border-[#222222] rounded-none p-8 text-center">
|
|
<p className="text-[#888888] text-sm mb-4">No Photo Uploaded</p>
|
|
<button
|
|
onClick={() => setShowPhotoUpload(true)}
|
|
className="px-4 py-2 bg-primary/20 border border-primary/50 text-primary rounded-none hover:bg-primary/30 transition-colors font-normal text-sm"
|
|
>
|
|
Upload Photo
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div ref={photoUploadRef} className="bg-black border border-[#222222] rounded-none p-4 md:p-6">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h4 className="font-normal text-white">Upload New Photo</h4>
|
|
<button
|
|
onClick={() => setShowPhotoUpload(false)}
|
|
className="p-1 hover:bg-[#1A1A1A] rounded-none text-[#888888] hover:text-white transition-colors border border-transparent hover:border-[#222222]"
|
|
aria-label="Cancel upload"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
{item.id && (
|
|
<ItemPhotoUpload
|
|
itemId={item.id}
|
|
onUploadSuccess={handlePhotoUploadSuccess}
|
|
onError={handlePhotoUploadError}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Debug Rotation Panel */}
|
|
{showDebugPanel && currentPhoto && (
|
|
<DebugRotationPanel
|
|
item={item}
|
|
imageUrl={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
|
|
originalPhotoPath={
|
|
item.labels_data
|
|
? (() => {
|
|
try {
|
|
const parsed = JSON.parse(item.labels_data);
|
|
const origPath = parsed.image_processing?.original_photo_path;
|
|
return origPath ? buildPhotoUrl(backendUrl, origPath) : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
})()
|
|
: undefined
|
|
}
|
|
originalCropBounds={item.labels_data ? JSON.parse(item.labels_data).image_processing?.crop_bounds : undefined}
|
|
originalRotation={item.labels_data ? JSON.parse(item.labels_data).image_processing?.rotation_degrees : undefined}
|
|
imageProcessing={item.labels_data ? JSON.parse(item.labels_data).image_processing : undefined}
|
|
onClose={() => setShowDebugPanel(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|