Files
tfm_ainventory/frontend/components/ItemDetailModal.tsx
2026-04-25 12:43:41 +03:00

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>
);
}