veza/apps/web/src/features/inventory/components/gear/GearDetailModal.tsx

250 lines
11 KiB
TypeScript

import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
X,
ShoppingBag,
Activity,
Tag,
Wrench,
AlertTriangle,
ClipboardList,
DollarSign,
Shield,
FileText,
FileCheck,
Download,
Plus,
} from 'lucide-react';
import type { GearItem } from '@/types';
import { getWarrantyStatus } from './gearUtils';
import { cn } from '@/lib/utils';
export interface GearDetailModalProps {
item: GearItem;
onClose: () => void;
onSellOnMarketplace?: (item: GearItem) => void;
onLogMaintenance?: (item: GearItem) => void;
onContactSupport?: (item: GearItem) => void;
onUploadDocument?: (item: GearItem) => void;
className?: string;
}
export function GearDetailModal({
item,
onClose,
onSellOnMarketplace,
onLogMaintenance,
onContactSupport,
onUploadDocument,
className,
}: GearDetailModalProps) {
const warranty = getWarrantyStatus(item.warrantyExpire);
return (
<div className={cn('fixed inset-0 z-50 flex items-center justify-center p-4', className)}>
<div
className="absolute inset-0 bg-background/90 backdrop-blur-sm"
onClick={onClose}
aria-hidden
/>
<div className="relative w-full max-w-5xl bg-card border border-border rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-screen">
{/* Modal Header */}
<div className="p-8 border-b border-border bg-muted/30 flex justify-between items-start shrink-0">
<div className="flex gap-8">
<div className="w-32 h-32 bg-muted rounded-lg overflow-hidden border border-border shrink-0">
{item.image ? (
<img src={item.image} className="w-full h-full object-cover" alt="" />
) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground text-sm">No image</div>
)}
</div>
<div>
<div className="flex items-center gap-4 mb-1">
<Badge label={item.category} variant="cyan" />
<span className="text-muted-foreground text-xs font-mono uppercase">{item.serialNumber ?? '—'}</span>
</div>
<h2 className="text-3xl font-display font-bold text-foreground">{item.name}</h2>
<h3 className="text-xl text-primary font-medium mb-4">
{item.brand} {item.model}
</h3>
<div className="flex gap-4">
<Button
variant="glass"
size="sm"
icon={<ShoppingBag className="w-4 h-4" />}
onClick={() => onSellOnMarketplace?.(item)}
>
SELL ON MARKETPLACE
</Button>
<Button
variant="secondary"
size="sm"
icon={<Activity className="w-4 h-4" />}
onClick={() => onLogMaintenance?.(item)}
>
LOG MAINTENANCE
</Button>
</div>
</div>
</div>
<button
type="button"
onClick={onClose}
className="text-muted-foreground hover:text-foreground p-1"
aria-label="Close"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Modal Body */}
<div className="flex-1 overflow-y-auto p-8 min-h-0">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-foreground mb-4 border-b border-border pb-2">
<Tag className="w-4 h-4 text-muted-foreground" /> Specifications
</h4>
<div className="grid grid-cols-2 gap-y-4 gap-x-8">
{item.specs && Object.keys(item.specs).length > 0 ? (
Object.entries(item.specs).map(([key, val]) => (
<div key={key} className="flex justify-between border-b border-border pb-1">
<span className="text-muted-foreground text-sm">{key}</span>
<span className="text-foreground text-sm font-medium">{val}</span>
</div>
))
) : (
<p className="text-muted-foreground italic col-span-2">No specs available.</p>
)}
</div>
</Card>
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-foreground mb-4 border-b border-border pb-2">
<Wrench className="w-4 h-4 text-amber-500" /> Maintenance & Support (SAV)
</h4>
{item.status === 'Maintenance' && item.notes && (
<div className="bg-amber-500/10 border border-amber-500/30 p-4 rounded mb-4 flex items-center gap-4">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0" />
<div>
<div className="text-sm font-bold text-amber-500">Currently in Repair</div>
<div className="text-xs text-muted-foreground">{item.notes}</div>
</div>
</div>
)}
<div className="space-y-4">
{item.maintenanceHistory?.map((log) => (
<div
key={log.id}
className="flex items-start gap-4 p-4 bg-muted/30 rounded hover:bg-muted/50 transition-colors"
>
<div className="bg-muted p-2 rounded text-muted-foreground shrink-0">
<ClipboardList className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between gap-2">
<span className="font-bold text-foreground text-sm">{log.type}</span>
<span className="text-xs text-muted-foreground shrink-0">{log.date}</span>
</div>
<p className="text-xs text-muted-foreground mt-1">{log.notes}</p>
{log.cost != null && (
<div className="text-xs text-destructive mt-1 font-mono">Cost: ${log.cost}</div>
)}
</div>
</div>
))}
{(!item.maintenanceHistory || item.maintenanceHistory.length === 0) && (
<p className="text-sm text-muted-foreground italic">No maintenance history recorded.</p>
)}
</div>
</Card>
</div>
<div className="space-y-8">
<Card variant="glass">
<h4 className="flex items-center gap-2 font-bold text-foreground mb-4 border-b border-border pb-2">
<DollarSign className="w-4 h-4 text-primary" /> Purchase Info
</h4>
<div className="space-y-4 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Price Paid</span>
<span className="text-foreground font-mono">
{item.currency} {item.purchasePrice}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Date</span>
<span className="text-foreground">{item.purchaseDate}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Vendor</span>
<span className="text-foreground">{item.vendor ?? '—'}</span>
</div>
{item.orderNumber && (
<div className="flex justify-between">
<span className="text-muted-foreground">Order #</span>
<span className="text-muted-foreground font-mono">{item.orderNumber}</span>
</div>
)}
</div>
</Card>
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-foreground mb-4 border-b border-border pb-2">
<Shield className="w-4 h-4 text-green-600" /> Warranty
</h4>
<div className={cn('p-4 rounded text-center mb-4', warranty.bg)}>
<div className={cn('text-lg font-bold', warranty.color)}>{warranty.label}</div>
<div className="text-xs text-muted-foreground">Expires: {item.warrantyExpire ?? 'N/A'}</div>
</div>
{item.supportContact && (
<Button
variant="ghost"
size="sm"
className="w-full text-xs"
onClick={() => onContactSupport?.(item)}
>
Contact Support
</Button>
)}
</Card>
<Card variant="default">
<h4 className="flex items-center gap-2 font-bold text-foreground mb-4 border-b border-border pb-2">
<FileText className="w-4 h-4 text-muted-foreground" /> Documentation
</h4>
<div className="space-y-2">
{item.documents?.map((doc, i) => (
<div
key={i}
className="flex items-center justify-between p-2 hover:bg-muted/50 rounded cursor-pointer group"
>
<div className="flex items-center gap-4 overflow-hidden min-w-0">
{doc.type === 'manual' ? (
<FileText className="w-4 h-4 text-muted-foreground shrink-0" />
) : (
<FileCheck className="w-4 h-4 text-primary shrink-0" />
)}
<span className="text-sm text-foreground truncate">{doc.name}</span>
</div>
<Download className="w-4 h-4 text-muted-foreground group-hover:text-foreground shrink-0" />
</div>
))}
<Button
variant="ghost"
size="sm"
className="w-full mt-2 text-xs border border-dashed border-border"
onClick={() => onUploadDocument?.(item)}
>
<Plus className="w-3 h-3 mr-1" /> Upload Document
</Button>
</div>
</Card>
</div>
</div>
</div>
</div>
</div>
);
}