148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { SearchInput } from '../ui/input';
|
|
import { EquipmentCard } from './EquipmentCard';
|
|
import { GearItem } from '../../types';
|
|
import { Plus, Filter, Download, Box, Loader2 } from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { gearService } from '../../services/gearService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
interface InventoryViewProps {
|
|
onNavigate: (view: string, id?: string) => void;
|
|
}
|
|
|
|
export const InventoryView: React.FC<InventoryViewProps> = ({ onNavigate }) => {
|
|
const { addToast } = useToast();
|
|
const [search, setSearch] = useState('');
|
|
const [filterCat, setFilterCat] = useState('All');
|
|
const [filterStatus, setFilterStatus] = useState('All');
|
|
const [inventory, setInventory] = useState<GearItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadInventory();
|
|
}, []);
|
|
|
|
const loadInventory = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await gearService.list();
|
|
setInventory(data);
|
|
} catch (e) {
|
|
logger.error('Failed to load gear', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filteredItems = inventory.filter((item) => {
|
|
const matchSearch =
|
|
item.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
item.brand.toLowerCase().includes(search.toLowerCase());
|
|
const matchCat = filterCat === 'All' || item.category === filterCat;
|
|
const matchStatus = filterStatus === 'All' || item.status === filterStatus;
|
|
return matchSearch && matchCat && matchStatus;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fadeIn pb-20">
|
|
{/* Header */}
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-kodo-steel/50 pb-6 gap-4">
|
|
<div>
|
|
<h2 className="text-3xl font-display font-bold text-white mb-2">
|
|
GEAR INVENTORY
|
|
</h2>
|
|
<p className="text-gray-400 font-mono text-sm">
|
|
Track hardware, warranties, and maintenance.
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="ghost"
|
|
icon={<Download className="w-4 h-4" />}
|
|
onClick={() => addToast('Exporting CSV...')}
|
|
>
|
|
Export
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
icon={<Plus className="w-4 h-4" />}
|
|
onClick={() => onNavigate('inventory/add')}
|
|
>
|
|
Add Equipment
|
|
</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-96">
|
|
<SearchInput
|
|
placeholder="Search gear..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
|
<div className="flex items-center gap-2 bg-kodo-void rounded-lg p-1 border border-kodo-steel">
|
|
<Filter className="w-4 h-4 text-gray-500 ml-2" />
|
|
<select
|
|
className="bg-transparent text-sm text-gray-300 focus:outline-none p-1 cursor-pointer"
|
|
value={filterCat}
|
|
onChange={(e) => setFilterCat(e.target.value)}
|
|
>
|
|
<option value="All">All Categories</option>
|
|
<option value="Synth">Synths</option>
|
|
<option value="Interface">Interfaces</option>
|
|
<option value="Microphone">Microphones</option>
|
|
<option value="Computer">Computers</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 bg-kodo-void rounded-lg p-1 border border-kodo-steel">
|
|
<Box className="w-4 h-4 text-gray-500 ml-2" />
|
|
<select
|
|
className="bg-transparent text-sm text-gray-300 focus:outline-none p-1 cursor-pointer"
|
|
value={filterStatus}
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|
>
|
|
<option value="All">All Status</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Maintenance">Maintenance</option>
|
|
<option value="Sold">Sold</option>
|
|
<option value="Wishlist">Wishlist</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Grid */}
|
|
{loading ? (
|
|
<div className="flex justify-center py-20">
|
|
<Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" />
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{filteredItems.map((item) => (
|
|
<EquipmentCard
|
|
key={item.id}
|
|
item={item}
|
|
onClick={() => onNavigate('inventory/detail', item.id)}
|
|
/>
|
|
))}
|
|
{filteredItems.length === 0 && (
|
|
<div className="col-span-full text-center py-20 text-gray-500">
|
|
<Box className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
<p>No equipment found.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|