veza/apps/web/src/components/views/GearView.tsx

341 lines
No EOL
20 KiB
TypeScript

import React, { useState } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { SearchInput } from '../ui/input';
import {
Plus, Wrench, FileText, DollarSign, Shield, Tag,
Download, Activity, AlertTriangle,
ShoppingBag, X, FileCheck, ClipboardList
} from 'lucide-react';
import { GearItem } from '../../types';
import { useToast } from '../../context/ToastContext';
// --- MOCK DATA ---
const INVENTORY: GearItem[] = [
{
id: '1', name: 'Prophet-6', category: 'Synth', brand: 'Sequential', model: 'Prophet-6 Desktop',
serialNumber: 'SQ-P6-99281', purchaseDate: '2023-01-15', purchasePrice: 2499, currency: 'USD',
status: 'Active', condition: 'Mint', vendor: 'Sweetwater', orderNumber: 'SW-8821002',
warrantyExpire: '2025-01-15', warrantyType: 'Manufacturer', supportContact: 'support@sequential.com',
image: 'https://picsum.photos/id/100/400/400',
specs: { 'Polyphony': '6 Voices', 'Oscillators': '2 Discrete VCOs', 'Filter': 'Low-pass + High-pass', 'Sequencer': '64-step' },
documents: [
{ name: 'User Manual', type: 'manual', url: '#', size: '4.2 MB' },
{ name: 'Purchase Receipt', type: 'receipt', url: '#', size: '150 KB' }
]
},
{
id: '2', name: 'Apollo Twin X', category: 'Interface', brand: 'Universal Audio', model: 'Twin X Duo',
serialNumber: 'UA-TWX-2210', purchaseDate: '2022-11-20', purchasePrice: 999, currency: 'USD',
status: 'Active', condition: 'Good', vendor: 'Thomann',
warrantyExpire: '2023-11-20', warrantyType: 'Manufacturer',
image: 'https://picsum.photos/id/101/400/400',
specs: { 'Inputs': '2 Mic/Line', 'Outputs': '4 Line', 'Connection': 'Thunderbolt 3', 'DSP': 'Duo Core' },
documents: [
{ name: 'Firmware v1.2', type: 'firmware', url: '#', size: '120 MB' }
],
maintenanceHistory: [
{ id: 'm1', date: '2023-05-10', type: 'Cleaning', notes: 'Potentiometer de-oxidizing', cost: 0 }
]
},
{
id: '3', name: 'SM7B', category: 'Microphone', brand: 'Shure', model: 'SM7B Dynamic',
serialNumber: 'SH-SM7-004', purchaseDate: '2021-05-10', purchasePrice: 399, currency: 'USD',
status: 'Maintenance', condition: 'Fair', vendor: 'Guitar Center',
warrantyExpire: '2023-05-10', warrantyType: 'None',
image: 'https://picsum.photos/id/102/400/400',
notes: 'XLR connector feels loose. Sent for repair.',
maintenanceHistory: [
{ id: 'm2', date: '2024-02-15', type: 'Repair', notes: 'XLR Jack Replacement', cost: 45, provider: 'Local Shop' }
]
},
];
export const GearView: React.FC = () => {
const { addToast } = useToast();
const [selectedItem, setSelectedItem] = useState<GearItem | null>(null);
const [filter, setFilter] = useState('All');
const [search, setSearch] = useState('');
// Filtering Logic
const filteredInventory = INVENTORY.filter(item => {
const matchesFilter = filter === 'All' || item.category === filter || item.status === filter;
const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase()) || item.brand.toLowerCase().includes(search.toLowerCase());
return matchesFilter && matchesSearch;
});
const getWarrantyStatus = (dateStr?: string) => {
if (!dateStr) return { label: 'Unknown', color: 'text-gray-500', bg: 'bg-gray-500/10' };
const expiry = new Date(dateStr);
const now = new Date();
const daysLeft = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysLeft < 0) return { label: 'Expired', color: 'text-kodo-red', bg: 'bg-kodo-red/10' };
if (daysLeft < 90) return { label: `Expiring (${daysLeft}d)`, color: 'text-kodo-gold', bg: 'bg-kodo-gold/10' };
return { label: 'Active', color: 'text-kodo-lime', bg: 'bg-kodo-lime/10' };
};
const handleListOnMarketplace = (item: GearItem) => {
addToast(`Draft listing created for ${item.brand} ${item.name}`, 'success');
setSelectedItem(null);
};
return (
<div className="space-y-6 animate-fadeIn relative">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h2 className="text-3xl font-display font-bold text-white mb-2">GEAR LOCKER</h2>
<p className="text-gray-400 font-mono text-sm">Manage hardware assets, documentation, and warranties.</p>
</div>
<div className="flex gap-3">
<Button variant="ghost" icon={<Download className="w-4 h-4" />} onClick={() => addToast("Exporting Inventory CSV...")}>EXPORT CSV</Button>
<Button variant="gaming" icon={<Plus className="w-4 h-4" />}>REGISTER GEAR</Button>
</div>
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4 items-center bg-kodo-ink/50 p-4 rounded-xl border border-kodo-steel/50">
<div className="w-full md:w-64">
<SearchInput placeholder="Search brand, model, serial..." value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className="flex gap-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0">
{['All', 'Synth', 'Interface', 'Microphone', 'Active', 'Maintenance', 'Sold'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-colors border ${filter === f ? 'bg-kodo-cyan text-black border-kodo-cyan' : 'bg-kodo-slate text-gray-400 border-transparent hover:border-gray-500'}`}
>
{f}
</button>
))}
</div>
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredInventory.map((item) => {
const warranty = getWarrantyStatus(item.warrantyExpire);
return (
<Card
key={item.id}
variant="gaming"
className="group cursor-pointer hover:border-kodo-cyan/50 transition-all hover:-translate-y-1"
onClick={() => setSelectedItem(item)}
>
<div className="flex items-start justify-between mb-4">
<Badge label={item.category} variant="terminal" />
<div className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${item.status === 'Active' ? 'bg-kodo-lime/10 text-kodo-lime' : 'bg-kodo-orange/10 text-kodo-orange'}`}>
{item.status}
</div>
</div>
<div className="flex gap-4 mb-4">
<div className="w-24 h-24 bg-gray-800 rounded-lg border border-gray-700 overflow-hidden flex-shrink-0">
<img src={item.image} className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-white truncate">{item.name}</h3>
<p className="text-kodo-gold text-sm font-mono mb-1">{item.brand}</p>
<p className="text-xs text-gray-500 truncate">S/N: {item.serialNumber}</p>
<div className="mt-2 flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${warranty.color.replace('text', 'bg')}`}></span>
<span className={`text-[10px] font-bold ${warranty.color}`}>{warranty.label} Warranty</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 mt-4 pt-4 border-t border-gray-800">
<div className="text-center p-2 bg-white/5 rounded">
<div className="text-[10px] text-gray-500 uppercase">Purchased</div>
<div className="text-sm font-bold text-white">{item.purchaseDate}</div>
</div>
<div className="text-center p-2 bg-white/5 rounded">
<div className="text-[10px] text-gray-500 uppercase">Condition</div>
<div className="text-sm font-bold text-white">{item.condition}</div>
</div>
</div>
</Card>
);
})}
{/* Add New Placeholder */}
<div className="border-2 border-dashed border-kodo-steel rounded-xl flex flex-col items-center justify-center p-8 hover:bg-kodo-slate/20 transition-colors cursor-pointer text-gray-500 hover:text-kodo-cyan hover:border-kodo-cyan min-h-[280px]" onClick={() => addToast("Opens Registration Form")}>
<Plus className="w-12 h-12 mb-4 opacity-50" />
<span className="font-mono font-bold">REGISTER NEW HARDWARE</span>
</div>
</div>
{/* --- GEAR DETAIL MODAL --- */}
{selectedItem && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm" onClick={() => setSelectedItem(null)}></div>
<div className="relative w-full max-w-5xl bg-kodo-graphite border border-kodo-steel rounded-2xl shadow-2xl animate-scaleIn overflow-hidden flex flex-col max-h-[90vh]">
{/* Modal Header */}
<div className="p-6 border-b border-kodo-steel bg-kodo-ink flex justify-between items-start">
<div className="flex gap-6">
<div className="w-32 h-32 bg-gray-800 rounded-lg overflow-hidden border border-kodo-steel shadow-lg">
<img src={selectedItem.image} className="w-full h-full object-cover" />
</div>
<div>
<div className="flex items-center gap-3 mb-1">
<Badge label={selectedItem.category} variant="cyan" />
<span className="text-gray-500 text-xs font-mono uppercase">{selectedItem.serialNumber}</span>
</div>
<h2 className="text-3xl font-display font-bold text-white">{selectedItem.name}</h2>
<h3 className="text-xl text-kodo-gold font-medium mb-4">{selectedItem.brand} {selectedItem.model}</h3>
<div className="flex gap-3">
<Button variant="gaming" size="sm" icon={<ShoppingBag className="w-4 h-4" />} onClick={() => handleListOnMarketplace(selectedItem)}>
SELL ON MARKETPLACE
</Button>
<Button variant="secondary" size="sm" icon={<Activity className="w-4 h-4" />} onClick={() => addToast("Maintenance Log Updated")}>
LOG MAINTENANCE
</Button>
</div>
</div>
</div>
<button onClick={() => setSelectedItem(null)} className="text-gray-400 hover:text-white"><X className="w-6 h-6" /></button>
</div>
{/* Modal Body */}
<div className="flex-1 overflow-y-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left: Specs & Info */}
<div className="lg:col-span-2 space-y-6">
{/* Specifications */}
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-white mb-4 border-b border-gray-700 pb-2">
<Tag className="w-4 h-4 text-kodo-cyan" /> Specifications
</h4>
<div className="grid grid-cols-2 gap-y-4 gap-x-8">
{selectedItem.specs ? Object.entries(selectedItem.specs).map(([key, val]) => (
<div key={key} className="flex justify-between border-b border-gray-800 pb-1">
<span className="text-gray-400 text-sm">{key}</span>
<span className="text-white text-sm font-medium">{val}</span>
</div>
)) : <p className="text-gray-500 italic">No specs available.</p>}
</div>
</Card>
{/* Maintenance Log */}
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-white mb-4 border-b border-gray-700 pb-2">
<Wrench className="w-4 h-4 text-kodo-orange" /> Maintenance & Support (SAV)
</h4>
{selectedItem.status === 'Maintenance' && (
<div className="bg-kodo-orange/10 border border-kodo-orange/30 p-3 rounded mb-4 flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-kodo-orange" />
<div>
<div className="text-sm font-bold text-kodo-orange">Currently in Repair</div>
<div className="text-xs text-gray-400">{selectedItem.notes}</div>
</div>
</div>
)}
<div className="space-y-3">
{selectedItem.maintenanceHistory?.map(log => (
<div key={log.id} className="flex items-start gap-4 p-3 bg-white/5 rounded hover:bg-white/10 transition-colors">
<div className="bg-gray-800 p-2 rounded text-gray-400"><ClipboardList className="w-4 h-4" /></div>
<div className="flex-1">
<div className="flex justify-between">
<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>
{log.cost && <div className="text-xs text-kodo-red mt-1 font-mono">Cost: ${log.cost}</div>}
</div>
</div>
))}
{(!selectedItem.maintenanceHistory || selectedItem.maintenanceHistory.length === 0) && (
<p className="text-sm text-gray-500 italic">No maintenance history recorded.</p>
)}
</div>
</Card>
</div>
{/* Right: Purchase & Docs */}
<div className="space-y-6">
{/* Purchase Info */}
<Card variant="glass">
<h4 className="flex items-center gap-2 font-bold text-white mb-4 border-b border-gray-700 pb-2">
<DollarSign className="w-4 h-4 text-kodo-gold" /> Purchase Info
</h4>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Price Paid</span>
<span className="text-white font-mono">{selectedItem.currency} {selectedItem.purchasePrice}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Date</span>
<span className="text-white">{selectedItem.purchaseDate}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Vendor</span>
<span className="text-white">{selectedItem.vendor}</span>
</div>
{selectedItem.orderNumber && (
<div className="flex justify-between">
<span className="text-gray-400">Order #</span>
<span className="text-kodo-cyan">{selectedItem.orderNumber}</span>
</div>
)}
</div>
</Card>
{/* Warranty */}
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-white mb-4 border-b border-gray-700 pb-2">
<Shield className="w-4 h-4 text-kodo-lime" /> Warranty
</h4>
<div className={`p-3 rounded text-center mb-4 ${getWarrantyStatus(selectedItem.warrantyExpire).bg}`}>
<div className={`text-lg font-bold ${getWarrantyStatus(selectedItem.warrantyExpire).color}`}>
{getWarrantyStatus(selectedItem.warrantyExpire).label}
</div>
<div className="text-xs text-gray-400">Expires: {selectedItem.warrantyExpire || 'N/A'}</div>
</div>
{selectedItem.supportContact && (
<Button variant="ghost" size="sm" className="w-full text-xs" onClick={() => addToast(`Contacting ${selectedItem.supportContact}`)}>
Contact Support
</Button>
)}
</Card>
{/* Documents */}
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-white mb-4 border-b border-gray-700 pb-2">
<FileText className="w-4 h-4 text-gray-400" /> Documentation
</h4>
<div className="space-y-2">
{selectedItem.documents?.map((doc, i) => (
<div key={i} className="flex items-center justify-between p-2 hover:bg-white/5 rounded cursor-pointer group">
<div className="flex items-center gap-3 overflow-hidden">
{doc.type === 'manual' ? <FileText className="w-4 h-4 text-kodo-cyan" /> : <FileCheck className="w-4 h-4 text-kodo-gold" />}
<span className="text-sm text-gray-300 truncate">{doc.name}</span>
</div>
<Download className="w-4 h-4 text-gray-500 group-hover:text-white" />
</div>
))}
<Button variant="ghost" size="sm" className="w-full mt-2 text-xs border border-dashed border-gray-600">
<Plus className="w-3 h-3 mr-1" /> Upload Document
</Button>
</div>
</Card>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};