veza/apps/web/src/components/inventory/EquipmentDetailView.tsx

279 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Button } from '../ui/button';
import { Card } from '../ui/card';
import { Badge } from '../ui/badge';
import { GearItem } from '../../types';
import {
ArrowLeft,
Edit3,
Trash2,
Tag,
ShieldCheck,
FileText,
Wrench,
Download,
ChevronLeft,
ChevronRight,
Loader2,
} from 'lucide-react';
import { useToast } from '../../context/ToastContext';
import { gearService } from '../../services/gearService';
import { logger } from '@/utils/logger';
interface EquipmentDetailViewProps {
itemId: string;
onBack: () => void;
}
export const EquipmentDetailView: React.FC<EquipmentDetailViewProps> = ({
itemId,
onBack,
}) => {
const { addToast } = useToast();
const [item, setItem] = useState<GearItem | null>(null);
const [loading, setLoading] = useState(true);
const [activeImgIndex, setActiveImgIndex] = useState(0);
useEffect(() => {
const fetchItem = async () => {
try {
setLoading(true);
const data = await gearService.get(itemId);
setItem(data);
} catch (e) {
logger.error('Failed to load equipment details', {
error: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
itemId,
});
addToast('Failed to load equipment details', 'error');
} finally {
setLoading(false);
}
};
fetchItem();
}, [itemId]);
if (loading)
return (
<div className="flex h-[50vh] items-center justify-center">
<Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" />
</div>
);
if (!item)
return (
<div className="text-center py-20 text-gray-500">Item not found</div>
);
const images =
item.images && item.images.length > 0 ? item.images : [item.image || ''];
const nextImage = () =>
setActiveImgIndex((prev) => (prev + 1) % images.length);
const prevImage = () =>
setActiveImgIndex((prev) => (prev - 1 + images.length) % images.length);
return (
<div className="animate-fadeIn max-w-6xl mx-auto pb-20">
{/* Nav */}
<div className="flex justify-between items-center mb-6">
<Button
variant="ghost"
onClick={onBack}
className="pl-0 text-gray-400 hover:text-white"
>
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Inventory
</Button>
<div className="flex gap-2">
<Button variant="secondary" icon={<Edit3 className="w-4 h-4" />}>
Edit
</Button>
<Button
variant="ghost"
className="text-kodo-red hover:bg-kodo-red/10"
icon={<Trash2 className="w-4 h-4" />}
>
Delete
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left: Photos & Key Info */}
<div className="space-y-6">
<div className="relative aspect-video bg-black rounded-xl overflow-hidden border border-kodo-steel group">
<img
src={images[activeImgIndex]}
className="w-full h-full object-contain"
/>
{images.length > 1 && (
<>
<button
onClick={prevImage}
className="absolute left-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<ChevronLeft className="w-6 h-6" />
</button>
<button
onClick={nextImage}
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<ChevronRight className="w-6 h-6" />
</button>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
{images.map((_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${i === activeImgIndex ? 'bg-kodo-cyan' : 'bg-gray-600'}`}
></div>
))}
</div>
</>
)}
</div>
<Card variant="default">
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
<Tag className="w-4 h-4 text-kodo-cyan" /> Core Specifications
</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
{item.specs ? (
Object.entries(item.specs).map(([key, val]) => (
<div key={key}>
<span className="text-gray-500 block text-xs uppercase">
{key}
</span>
<span className="text-white font-medium">{val}</span>
</div>
))
) : (
<p className="text-gray-500">No specs defined.</p>
)}
</div>
</Card>
</div>
{/* Right: Details & History */}
<div className="space-y-6">
<div>
<div className="flex items-center gap-3 mb-2">
<Badge label={item.category} variant="terminal" />
<span
className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${item.status === 'Active' ? 'bg-kodo-lime/10 text-kodo-lime' : 'bg-gray-700 text-gray-300'}`}
>
{item.status}
</span>
</div>
<h1 className="text-4xl font-display font-bold text-white mb-1">
{item.name}
</h1>
<p className="text-xl text-kodo-gold font-mono mb-4">
{item.brand} {item.model}
</p>
<div className="flex gap-6 text-sm text-gray-400 mb-6 font-mono bg-kodo-ink p-4 rounded-lg border border-kodo-steel/50">
<div className="flex flex-col">
<span className="text-[10px] uppercase font-bold text-gray-500">
Serial
</span>
<span className="text-white">{item.serialNumber}</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] uppercase font-bold text-gray-500">
Purchased
</span>
<span className="text-white">{item.purchaseDate}</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] uppercase font-bold text-gray-500">
Value
</span>
<span className="text-kodo-cyan font-bold">
${item.purchasePrice}
</span>
</div>
</div>
</div>
<Card variant="gaming">
<h3 className="font-bold text-white mb-4 border-b border-gray-700 pb-2 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-kodo-lime" /> Warranty &
Support
</h3>
<div className="flex justify-between items-center mb-4 p-3 bg-kodo-ink rounded border border-kodo-steel/30">
<div>
<span className="block text-xs text-gray-500 uppercase">
Expires
</span>
<span className="font-bold text-white">
{item.warrantyExpire || 'N/A'}
</span>
</div>
<Badge label={item.warrantyType || 'Standard'} variant="cyan" />
</div>
{item.supportContact && (
<div className="text-sm">
<span className="text-gray-500">Support: </span>
<a
href={`mailto:${item.supportContact}`}
className="text-kodo-cyan hover:underline"
>
{item.supportContact}
</a>
</div>
)}
</Card>
<Card variant="default">
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
<FileText className="w-4 h-4 text-gray-400" /> Documentation
</h3>
<div className="space-y-2">
{item.documents?.map((doc, i) => (
<div
key={i}
className="flex items-center justify-between p-2 hover:bg-white/5 rounded cursor-pointer group transition-colors"
>
<div className="flex items-center gap-3">
<FileText className="w-4 h-4 text-kodo-cyan" />
<span className="text-sm text-gray-300">{doc.name}</span>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-gray-500 hover:text-white"
>
<Download className="w-4 h-4" />
</Button>
</div>
))}
</div>
</Card>
<Card variant="default">
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
<Wrench className="w-4 h-4 text-kodo-orange" /> Service History
</h3>
<div className="space-y-4 relative">
<div className="absolute left-2 top-2 bottom-2 w-px bg-kodo-steel"></div>
{item.maintenanceHistory?.map((log) => (
<div key={log.id} className="relative pl-6">
<div className="absolute left-0 top-1.5 w-4 h-4 bg-kodo-graphite border border-kodo-orange rounded-full flex items-center justify-center">
<div className="w-1.5 h-1.5 bg-kodo-orange rounded-full"></div>
</div>
<div className="flex justify-between items-start">
<span className="font-bold text-white text-sm">
{log.type}
</span>
<span className="text-xs text-gray-500">{log.date}</span>
</div>
<p className="text-xs text-gray-400 mt-1">{log.notes}</p>
</div>
))}
</div>
</Card>
</div>
</div>
</div>
);
};