Phase 10 Implementation: Bulk color system compliance across frontend Applied DESIGN_COLOR_RULES.md to 40 component and page files: - Indigo (3) → primary/secondary (primary actions) - Green (12) → tertiary (#00e639) (success/healthy status) - Red/Rose (20) → error (#ffb4ab) (destructive actions) - Amber/Sky (9) → primary/secondary (warnings/info) - Blue/Slate (various) → primary/design system colors (focus states) - Black → bg-surface-container-lowest (proper design color) Files updated: 40 components + pages Color replacements: 44+ instances Build status: ✓ Zero errors, compiled in 6.3s Test status: Pending (npm run test) All colors now follow DESIGN.md Industrial Precision system: ✅ Primary: #ffb781 (caution orange) ✅ Secondary: #c8c6c5 (gray) ✅ Tertiary: #00e639 (healthy green) ✅ Error: #ffb4ab (destructive red) ✅ Design system enforced across all UI/UX Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
225 lines
9.0 KiB
TypeScript
225 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-surface-container-lowest/90 flex items-center justify-center z-50 p-4 animate-in fade-in duration-300">
|
|
<div className="level-2 max-w-2xl w-full max-h-[90vh] overflow-y-auto flex flex-col shadow-none">
|
|
{/* Header */}
|
|
<div className="sticky top-0 bg-surface-bright border-b border-border p-4 md:p-6 flex items-center justify-between z-10">
|
|
<div>
|
|
<h2 className="text-xl md:text-2xl font-normal text-white truncate">{item.name}</h2>
|
|
<p className="text-[10px] text-secondary font-normal uppercase tracking-widest mt-0.5">Asset Technical Protocol</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{currentPhoto && (
|
|
<button
|
|
onClick={() => setShowDebugPanel(true)}
|
|
className="p-3 bg-surface-container-lowest border border-border text-primary hover:text-white transition-colors"
|
|
title="Debug rotation & crop"
|
|
>
|
|
<Wrench size={20} />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-3 bg-surface-container-lowest border border-border text-secondary hover:text-white transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-4 md:p-6 space-y-8 flex-1">
|
|
{/* Details Grid */}
|
|
<div className="grid grid-cols-2 gap-px bg-border border border-border">
|
|
{[
|
|
{ label: 'Category', value: item.category },
|
|
{ label: 'Item Type', value: item.type || 'N/A' },
|
|
{ label: 'Available Stock', value: item.quantity },
|
|
{ label: 'Part Number', value: item.part_number || 'N/A' },
|
|
{ label: 'Identifier', value: item.barcode, full: true, mono: true }
|
|
].map((d, i) => (
|
|
<div key={i} className={`p-4 bg-surface-container ${d.full ? 'col-span-2' : ''}`}>
|
|
<p className="text-[10px] text-secondary font-normal mb-1">{d.label}</p>
|
|
<p className={`text-sm text-white ${d.mono ? 'font-mono' : 'font-normal'}`}>{d.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Photo Section */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-normal text-white flex items-center gap-2">
|
|
<Camera size={18} className="text-primary" />
|
|
Visual Signature
|
|
</h3>
|
|
|
|
{!showPhotoUpload ? (
|
|
<>
|
|
{currentPhoto ? (
|
|
<div className="space-y-3">
|
|
<div className="relative bg-surface-container-lowest border border-border overflow-hidden aspect-video">
|
|
<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="btn-secondary flex-1 py-3 text-xs"
|
|
>
|
|
Capture New Signature
|
|
</button>
|
|
<button
|
|
onClick={handleDeletePhoto}
|
|
disabled={isDeleting}
|
|
className="px-4 py-3 bg-error/10 border border-error/30 text-error hover:bg-error/20 disabled:opacity-50 transition-colors text-xs"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-surface-container-lowest border border-border p-12 text-center flex flex-col items-center gap-4">
|
|
<div className="w-16 h-16 bg-surface-container flex items-center justify-center text-muted border border-border">
|
|
<Camera size={32} />
|
|
</div>
|
|
<p className="text-muted text-sm">No recorded visual signatures</p>
|
|
<button
|
|
onClick={() => setShowPhotoUpload(true)}
|
|
className="btn-primary px-8 py-3"
|
|
>
|
|
Authorize Capture
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div ref={photoUploadRef} className="bg-surface-container-lowest border border-border p-4 md:p-6 animate-in slide-in-from-top-2 duration-300">
|
|
<div className="mb-4 flex items-center justify-between border-b border-border pb-3">
|
|
<h4 className="font-normal text-white text-sm">Signature Acquisition Protocol</h4>
|
|
<button
|
|
onClick={() => setShowPhotoUpload(false)}
|
|
className="p-1 text-secondary hover:text-white transition-colors"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
{item.id && (
|
|
<ItemPhotoUpload
|
|
itemId={item.id}
|
|
onUploadSuccess={handlePhotoUploadSuccess}
|
|
onError={handlePhotoUploadError}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-surface-container-lowest border-t border-border flex justify-end">
|
|
<button
|
|
onClick={onClose}
|
|
className="btn-secondary px-8 py-2 text-xs"
|
|
>
|
|
Close Protocol
|
|
</button>
|
|
</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>
|
|
);
|
|
}
|