import { useState, useMemo } from 'react'; import { router } from '@inertiajs/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/Components/ui/dialog'; import { Input } from '@/Components/ui/input'; import { Button } from '@/Components/ui/button'; import { Badge } from '@/Components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/Components/ui/table'; import { Checkbox } from '@/Components/ui/checkbox'; import { Package, Search, ShoppingCart, AlertTriangle } from 'lucide-react'; interface MaterialOption { id: number; ulid: string; name: string; sku?: string; unit: string; unit_cost: string; category?: string; available_qty?: number; on_hand_qty?: number; allocated_qty?: number; } interface MaterialPickerModalProps { open: boolean; onOpenChange: (open: boolean) => void; projectUlid: string; taskUlid: string; availableMaterials: MaterialOption[]; existingMaterialIds: string[]; } const formatCurrency = (v: string) => new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v)); const formatQty = (v: number) => new Intl.NumberFormat('en-PH', { maximumFractionDigits: 2 }).format(v); export default function MaterialPickerModal({ open, onOpenChange, projectUlid, taskUlid, availableMaterials, existingMaterialIds, }: MaterialPickerModalProps) { const [search, setSearch] = useState(''); const [selected, setSelected] = useState>({}); const [submitting, setSubmitting] = useState(false); const materials = useMemo(() => { const q = search.toLowerCase().trim(); if (!q) return availableMaterials; return availableMaterials.filter(m => m.name.toLowerCase().includes(q) || (m.sku && m.sku.toLowerCase().includes(q)) || (m.category && m.category.toLowerCase().includes(q)) ); }, [availableMaterials, search]); const selectedCount = Object.keys(selected).length; const toggleMaterial = (ulid: string) => { setSelected(prev => { const next = { ...prev }; if (next[ulid]) { delete next[ulid]; } else { next[ulid] = '1'; } return next; }); }; const setQty = (ulid: string, qty: string) => { setSelected(prev => ({ ...prev, [ulid]: qty })); }; const toggleAll = () => { const selectableMaterials = materials.filter(m => !existingMaterialIds.includes(m.ulid)); const allSelected = selectableMaterials.every(m => selected[m.ulid]); if (allSelected) { setSelected(prev => { const next = { ...prev }; selectableMaterials.forEach(m => delete next[m.ulid]); return next; }); } else { setSelected(prev => { const next = { ...prev }; selectableMaterials.forEach(m => { if (!next[m.ulid]) next[m.ulid] = '1'; }); return next; }); } }; // Check if any selected qty exceeds available const hasQtyError = useMemo(() => { return Object.entries(selected).some(([ulid, qty]) => { const mat = availableMaterials.find(m => m.ulid === ulid); if (!mat || mat.available_qty == null) return false; return Number(qty) > mat.available_qty; }); }, [selected, availableMaterials]); const handleSubmit = () => { if (hasQtyError) return; const entries = Object.entries(selected) .filter(([, qty]) => Number(qty) > 0) .map(([material_id, planned_qty]) => ({ material_id, planned_qty })); if (entries.length === 0) return; setSubmitting(true); router.post( route('projects.tasks.materials.storeBulk', [projectUlid, taskUlid]), { materials: entries }, { preserveScroll: true, onSuccess: () => { setSelected({}); setSearch(''); onOpenChange(false); }, onFinish: () => setSubmitting(false), } ); }; const handleOpenChange = (isOpen: boolean) => { if (!isOpen) { setSelected({}); setSearch(''); } onOpenChange(isOpen); }; const selectableMaterials = materials.filter(m => !existingMaterialIds.includes(m.ulid)); const allVisibleSelected = selectableMaterials.length > 0 && selectableMaterials.every(m => selected[m.ulid]); return ( Add Materials to Task {selectedCount > 0 && ( {selectedCount} selected )} {/* Search */}
setSearch(e.target.value)} className="pl-10" />
{/* Table */}
{materials.length === 0 ? (

{search ? 'No materials match your search.' : 'No materials on site. Dispatch materials from a warehouse first.'}

) : ( Material Category Unit Unit Cost On Site Planned Qty {materials.map((m) => { const isExisting = existingMaterialIds.includes(m.ulid); const isChecked = !!selected[m.ulid]; const availableQty = m.available_qty ?? 0; const enteredQty = isChecked ? Number(selected[m.ulid]) : 0; const exceedsAvailable = isChecked && enteredQty > availableQty; return ( { if (!isExisting) toggleMaterial(m.ulid); }} > e.stopPropagation()}> { if (!isExisting) toggleMaterial(m.ulid); }} /> {m.name} {m.sku && ( {m.sku} )} {isExisting && ( Already added )} {(m as any).type === 'kit' && (m as any).components && (m as any).components.length > 0 && (
{(m as any).components.map((comp: any) => (
{comp.quantity * (enteredQty || 1)}x {comp.component?.name} (from {comp.quantity} / kit)
))}
)}
{m.category || '-'} {m.unit} {formatCurrency(m.unit_cost)} {formatQty(availableQty)} {availableQty > 0 && availableQty <= (m.on_hand_qty ?? 0) * 0.1 && ( Low )} e.stopPropagation()}> {isChecked && !isExisting && (
setQty(m.ulid, e.target.value)} className={`w-24 ml-auto text-right h-8 text-sm ${exceedsAvailable ? 'border-red-400 focus-visible:ring-red-400' : ''}`} /> {exceedsAvailable && ( Max {formatQty(availableQty)} )}
)}
); })}
)}
{/* Footer */}

{selectedCount > 0 ? `${selectedCount} material${selectedCount > 1 ? 's' : ''} selected` : 'Select materials to add'} {hasQtyError && ( — Fix quantity errors first )}

); }