179 lines
7.4 KiB
TypeScript
179 lines
7.4 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle,
|
|
} from '@/Components/ui/dialog';
|
|
import { Input } from '@/Components/ui/input';
|
|
import {
|
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
} from '@/Components/ui/table';
|
|
import { Badge } from '@/Components/ui/badge';
|
|
import { Package, Search } from 'lucide-react';
|
|
|
|
interface MaterialOption {
|
|
id: number;
|
|
ulid: string;
|
|
name: string;
|
|
sku?: string;
|
|
unit: string;
|
|
unit_cost: string;
|
|
category?: string;
|
|
}
|
|
|
|
interface MaterialBrowserModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
title?: string;
|
|
materials: MaterialOption[];
|
|
}
|
|
|
|
const formatCurrency = (v: string) =>
|
|
new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v));
|
|
|
|
type SortKey = 'name' | 'sku' | 'category' | 'unit' | 'unit_cost';
|
|
type SortDir = 'asc' | 'desc';
|
|
|
|
export default function MaterialBrowserModal({
|
|
open,
|
|
onOpenChange,
|
|
title = 'Materials',
|
|
materials,
|
|
}: MaterialBrowserModalProps) {
|
|
const [search, setSearch] = useState('');
|
|
const [sortKey, setSortKey] = useState<SortKey>('name');
|
|
const [sortDir, setSortDir] = useState<SortDir>('asc');
|
|
|
|
const handleSort = (key: SortKey) => {
|
|
if (sortKey === key) {
|
|
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortKey(key);
|
|
setSortDir('asc');
|
|
}
|
|
};
|
|
|
|
const sortIndicator = (key: SortKey) => {
|
|
if (sortKey !== key) return null;
|
|
return <span className="ml-1 text-xs">{sortDir === 'asc' ? '↑' : '↓'}</span>;
|
|
};
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = search.toLowerCase().trim();
|
|
let list = materials;
|
|
|
|
if (q) {
|
|
list = list.filter(
|
|
(m) =>
|
|
m.name.toLowerCase().includes(q) ||
|
|
(m.sku && m.sku.toLowerCase().includes(q)) ||
|
|
(m.category && m.category.toLowerCase().includes(q))
|
|
);
|
|
}
|
|
|
|
return [...list].sort((a, b) => {
|
|
const aVal = (a[sortKey] ?? '').toString().toLowerCase();
|
|
const bVal = (b[sortKey] ?? '').toString().toLowerCase();
|
|
|
|
if (sortKey === 'unit_cost') {
|
|
const diff = Number(a.unit_cost) - Number(b.unit_cost);
|
|
return sortDir === 'asc' ? diff : -diff;
|
|
}
|
|
|
|
const cmp = aVal.localeCompare(bVal);
|
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
}, [materials, search, sortKey, sortDir]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-gray-500" />
|
|
{title}
|
|
<Badge variant="outline" className="ml-1">{materials.length}</Badge>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
<Input
|
|
placeholder="Search by name, SKU, or category..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
<div className="overflow-y-auto flex-1 -mx-4 px-4">
|
|
{filtered.length === 0 ? (
|
|
<div className="text-center py-10">
|
|
<Package className="mx-auto h-10 w-10 text-gray-300 mb-3" />
|
|
<p className="text-gray-500">
|
|
{search ? 'No materials match your search.' : 'No materials in this group.'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead
|
|
className="cursor-pointer select-none"
|
|
onClick={() => handleSort('name')}
|
|
>
|
|
Material {sortIndicator('name')}
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer select-none"
|
|
onClick={() => handleSort('sku')}
|
|
>
|
|
SKU {sortIndicator('sku')}
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer select-none"
|
|
onClick={() => handleSort('category')}
|
|
>
|
|
Category {sortIndicator('category')}
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer select-none"
|
|
onClick={() => handleSort('unit')}
|
|
>
|
|
Unit {sortIndicator('unit')}
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer select-none text-right"
|
|
onClick={() => handleSort('unit_cost')}
|
|
>
|
|
Unit Cost {sortIndicator('unit_cost')}
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filtered.map((m) => (
|
|
<TableRow key={m.id}>
|
|
<TableCell className="font-medium text-sm">{m.name}</TableCell>
|
|
<TableCell className="text-sm text-gray-500">{m.sku || '-'}</TableCell>
|
|
<TableCell className="text-sm">
|
|
<Badge variant="outline">{m.category || '-'}</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{m.unit}</TableCell>
|
|
<TableCell className="text-right text-sm tabular-nums">
|
|
{formatCurrency(m.unit_cost)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
|
|
{filtered.length > 0 && filtered.length !== materials.length && (
|
|
<p className="text-xs text-gray-400 text-center pt-2">
|
|
Showing {filtered.length} of {materials.length} materials
|
|
</p>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|