Files
GSB-Construction/resources/js/Components/MaterialBrowserModal.tsx

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>
);
}