// StockBatches.tsx import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Head } from '@inertiajs/react'; import AppLayout from '@/Layouts/AppLayout'; import axios from 'axios'; import { toast, Toaster } from 'react-hot-toast'; import { MaterialReactTable, type MRT_ColumnDef, type MRT_PaginationState, type MRT_SortingState, } from 'material-react-table'; import { Combobox } from '@headlessui/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faQrcode, faListOl } from '@fortawesome/free-solid-svg-icons'; import {PhysicalItem, StockBatch, StockSection, Supplier} from '@/types'; import { size } from 'lodash'; import { router } from '@inertiajs/react'; import BatchInfoWindow from '@/Components/BatchInfoWindow'; interface DropdownOption { id: number; name: string; } interface FileWithType { file: File; fileType: 'invoice' | 'label' | 'other'; } interface StockEntry { id: number; physical_item_id: number | null; supplier_id: number | null; count: number; price: number | null; bought: string | null; description: string | null; note: string | null; stock_position_id: number | null; country_of_origin_id: number | null; on_the_way: boolean; surplus_item: boolean; stock_batch_id: number; physical_item?: PhysicalItem; supplier?: DropdownOption; stock_position?: { id: number; line: string; rack: string; shelf: string; position: string }; sections?: (StockSection & { pivot: { section_id: number; count: number; created_at: string; updated_at: string | null }; })[]; } const defaultBatchForm = { supplierId: null as number | null, tracking_number: '', arrival_date: '' }; const defaultEntryForm: Omit = { physical_item_id: null, supplier_id: null, count: 0, price: null, bought: null, description: null, note: null, country_of_origin_id: null, on_the_way: false, surplus_item: false, stock_batch_id: null, }; export default function StockBatches() { const [batches, setBatches] = useState([]); const [suppliers, setSuppliers] = useState([]); const [batchLoading, setBatchLoading] = useState(false); const [batchCount, setBatchCount] = useState(0); const [batchPagination, setBatchPagination] = useState({ pageIndex: 0, pageSize: 10 }); const [batchSorting, setBatchSorting] = useState([{ id: 'updatedAt', desc: true }]); const [batchFilter, setBatchFilter] = useState(''); const createDialogRef = useRef(null); const [batchForm, setBatchForm] = useState(defaultBatchForm); const [batchFiles, setBatchFiles] = useState([]); const viewDialogRef = useRef(null); const [selectedBatch, setSelectedBatch] = useState(null); const [entries, setEntries] = useState([]); const [entriesLoading, setEntriesLoading] = useState(false); const [entriesCount, setEntriesCount] = useState(0); const [entriesPagination, setEntriesPagination] = useState({ pageIndex: 0, pageSize: 10 }); const [entriesSorting, setEntriesSorting] = useState([{ id: 'id', desc: false }]); const [entriesFilter, setEntriesFilter] = useState(''); const [onTheWayEntries, setOnTheWayEntries] = useState([]); const [entriesOnTheWayLoading, setEntriesOnTheWayLoading] = useState(false); const [onTheWayEntriesCount, setOnTheWayEntriesCount] = useState(0); const [onTheWayEntriesPagination, setOnTheWayEntriesPagination] = useState({ pageIndex: 0, pageSize: 10, }); const [onTheWayEntriesSorting, setOnTheWayEntriesSorting] = useState([{ id: 'id', desc: false }]); const [onTheWayEntriesFilter, setOnTheWayEntriesFilter] = useState(''); const [onTheWayEntriesSelections, setOnTheWayEntriesSelections] = useState([]); const entryDialogRef = useRef(null); const [editingEntry, setEditingEntry] = useState(null); const [entryForm, setEntryForm] = useState>({ ...defaultEntryForm }); const [physicalItems, setPhysicalItems] = useState([]); const [positions, setPositions] = useState([]); const [countries, setCountries] = useState([]); const [itemQuery, setItemQuery] = useState(''); const filteredItems = useMemo( () => itemQuery === '' ? physicalItems : physicalItems.filter((i) => i.name.toLowerCase().includes(itemQuery.toLowerCase())), [itemQuery, physicalItems] ); // Add this state alongside your other hooks: const [positionRows, setPositionRows] = useState<{ stock_position_id: number | null; count: number | null; }[]>([{ stock_position_id: null, count: null }]); // Handler to update a row const handlePositionRowChange = ( index: number, field: 'stock_position_id' | 'count', value: number | null ) => { setPositionRows((prev) => { const rows = prev.map((r, i) => (i === index ? { ...r, [field]: value } : r)); // if editing the last row's count, and it's a valid number, append a new empty row if (field === 'count' && index === prev.length - 1 && value && value > 0) { rows.push({ stock_position_id: null, count: null }); } return rows; }); }; const formatDate = (value: string, withTime = true) => { const d = new Date(value); const dd = String(d.getDate()).padStart(2, '0'); const mm = String(d.getMonth() + 1).padStart(2, '0'); const yyyy = d.getFullYear(); if (!withTime) return `${dd}-${mm}-${yyyy}`; const hh = String(d.getHours()).padStart(2, '0'); const mi = String(d.getMinutes()).padStart(2, '0'); return `${dd}-${mm}-${yyyy} ${hh}:${mi}`; }; const batchColumns = useMemo[]>( () => [ { accessorKey: 'id', header: 'ID' }, { accessorKey: 'supplier.name', header: 'Supplier' }, { accessorKey: 'tracking_number', header: 'Tracking #' }, { accessorKey: 'arrival_date', header: 'Arrival Date', Cell: ({ cell }) => formatDate(cell.getValue(), false), }, { accessorFn: (r) => r.files?.length ?? 0, id: 'files', header: 'Files' }, { accessorFn: (r) => r.stock_entries?.length ?? 0, id: 'items', header: 'Items' }, { accessorKey: 'created_at', header: 'Created', Cell: ({ cell }) => formatDate(cell.getValue()), }, { accessorKey: 'updated_at', header: 'Updated', Cell: ({ cell }) => formatDate(cell.getValue()), }, ], [] ); const entryOnTheWayColumns = useMemo[]>( () => [ { accessorKey: 'select', header: 'Select', Cell: ({ row }) => { const id = row.original.id; return ( setOnTheWayEntriesSelections((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] ) } /> ); }, }, { accessorKey: 'id', header: 'ID' }, { accessorKey: 'physical_item.name', header: 'Item' }, { accessorKey: 'supplier.name', header: 'Supplier' }, ], [onTheWayEntriesSelections] ); useEffect(() => { fetchOptions(); fetchEntriesOnTheWay(); }, []); useEffect(() => { fetchBatches(); }, [batchPagination, batchSorting, batchFilter]); useEffect(() => { if (selectedBatch) fetchEntries(selectedBatch.id); }, [entriesPagination, entriesSorting, entriesFilter]); async function fetchSuppliers() { try { const res = await axios.get('/api/stockBatches/options'); setSuppliers(res.data.suppliers); } catch { toast.error('Cannot load suppliers'); } } async function fetchOptions() { try { const res = await axios.get('/api/stockData/options'); setSuppliers(res.data.suppliers); setPositions(res.data.stockPositions); setCountries(res.data.countriesOrigin); } catch { toast.error('Cannot load entry options'); } } async function fetchBatches() { setBatchLoading(true); try { const res = await axios.get('/api/stockBatches', { params: { page: batchPagination.pageIndex + 1, perPage: batchPagination.pageSize, sortField: batchSorting[0].id, sortOrder: batchSorting[0].desc ? 'desc' : 'asc', filter: batchFilter, }, }); console.log(res.data.data); setBatches(res.data.data); setBatchCount(res.data.meta.total); } catch { toast.error('Cannot fetch batches'); } finally { setBatchLoading(false); } } async function fetchEntries(batchId: number) { setEntriesLoading(true); try { const res = await axios.get(`/api/stockBatches/${batchId}/entries`, { params: { page: entriesPagination.pageIndex + 1, perPage: entriesPagination.pageSize, sortField: entriesSorting[0].id, sortOrder: entriesSorting[0].desc ? 'desc' : 'asc', filter: entriesFilter, }, }); setEntries(res.data.data); setEntriesCount(size(res.data.data)); setSelectedBatch((b) => b && b.id === batchId ? { ...b, stock_entries: res.data.data } : b ) } catch (error) { toast.error('Cannot fetch entries'); console.error(error); } finally { setEntriesLoading(false); } } async function fetchEntriesOnTheWay() { setEntriesOnTheWayLoading(true); try { const res = await axios.get(`/api/stockDataOnTheWay`); setOnTheWayEntries(res.data.data); setOnTheWayEntriesCount(size(res.data.data)); } catch (error) { toast.error('Cannot fetch entries'); console.error(error); } finally { setEntriesOnTheWayLoading(false); } } const openCreate = () => createDialogRef.current?.showModal(); const closeCreate = () => { createDialogRef.current?.close(); setBatchForm(defaultBatchForm); setBatchFiles([]); }; const openView = (batch: StockBatch) => { setSelectedBatch(batch); fetchEntries(batch.id); viewDialogRef.current?.showModal(); }; const closeView = () => { viewDialogRef.current?.close(); setEntries([]); setSelectedBatch(null); }; const openEntry = (entry?: StockEntry) => { fetchEntriesOnTheWay(); if (entry) { setEditingEntry(entry); setEntryForm({ ...entry }); // Build rows from whatever pivot’d sections came back const rows = entry.sections?.map((sec) => ({ stock_position_id: sec.pivot.section_id, count: sec.pivot.count, })) || []; setPositionRows([...rows, { stock_position_id: null, count: null }]); } else { setEditingEntry(null); setEntryForm({ ...defaultEntryForm, stock_batch_id: selectedBatch!.id }); setPositionRows([{ stock_position_id: null, count: null }]); } entryDialogRef.current?.showModal(); }; const closeEntry = () => { entryDialogRef.current?.close(); setEditingEntry(null); }; const handleBatchInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setBatchForm((prev) => ({ ...prev, [name]: name === 'supplierId' ? (value ? parseInt(value) : null) : value, })); }; const handleBatchFileChange = (e: React.ChangeEvent) => { if (e.target.files) { setBatchFiles(Array.from(e.target.files).map((file) => ({ file, fileType: 'other' }))); } }; const handleBatchFileTypeChange = (idx: number, type: FileWithType['fileType']) => { setBatchFiles((prev) => prev.map((f, i) => (i === idx ? { ...f, fileType: type } : f))); }; const removeBatchFile = (idx: number) => { setBatchFiles((prev) => prev.filter((_, i) => i !== idx)); }; const handleBatchSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const fd = new FormData(); if (batchForm.supplierId) fd.append('supplier_id', batchForm.supplierId.toString()); if (batchForm.tracking_number) fd.append('tracking_number', batchForm.tracking_number); if (batchForm.arrival_date) fd.append('arrival_date', batchForm.arrival_date); batchFiles.forEach(({ file, fileType }) => { fd.append('files[]', file); // note the [] fd.append('file_types[]', fileType); // note the [] }); // await axios.post('/api/stockBatches', fd, { headers: { 'Content-Type': 'multipart/form-data' } }); await axios.post('/api/stockBatches', fd); toast.success('Batch created'); closeCreate(); fetchBatches(); } catch (err: any) { if (axios.isAxiosError(err) && err.response?.status === 422) { Object.values(err.response.data.errors) .flat() .forEach((m: string) => toast.error(m)); } else { toast.error('Failed to create batch'); } } }; const handleBatchAddEntries = async (e: React.FormEvent) => { e.preventDefault(); try { await axios.put(`/api/stockBatches/${selectedBatch!.id}/entries`, { ids: onTheWayEntriesSelections }); toast.success('Batch entries updated successfully'); closeEntry(); fetchBatches(); setSelectedBatch(null); setOnTheWayEntriesSelections([]); closeView(); } catch { toast.error('Batch entries update failed'); } }; const handleEntryInputChange = (e: React.ChangeEvent) => { const { name, value, type, checked } = e.target; setEntryForm((prev) => ({ ...prev, [name]: type === 'checkbox' ? checked : type === 'number' ? parseFloat(value) || null : value || null, } as any)); }; const handleEntrySubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingEntry) { await axios.put(`/api/stockData/${editingEntry.id}`, { entryForm, sections: positionRows.filter((r) => r.stock_position_id && r.count), }); } else { entryForm.supplier_id = selectedBatch?.supplier_id || null; await axios.post(`/api/stockData`, entryForm); } toast.success(editingEntry ? 'Entry updated' : 'Entry created'); fetchEntries(selectedBatch!.id); closeEntry(); } catch { toast.error('Cannot save entry'); } }; async function fetchPhysicalItems(query: string) { try { const res = await axios.get('/api/stockData/options/items', { params: { item_name: query } }); setPhysicalItems(res.data.physicalItems); } catch { toast.error('Failed to load items'); } } useEffect(() => { const delay = setTimeout(() => itemQuery && fetchPhysicalItems(itemQuery), 500); return () => clearTimeout(delay); }, [itemQuery]); return (

Stock Batches

}> {/* ===== Main Batches Table ===== */}

Batches

({ onClick: () => openView(row.original), style: { cursor: 'pointer' }, })} />
{/* ===== Create New Batch Dialog ===== */}

New Stock Batch

{batchFiles.map((f, i) => (
{f.file.name}
))}
{/* ===== View Batch Details Dialog ===== */}
{/* Replace inline batch‐details/table with */} {selectedBatch && ( route(name, params)} entries={entries} openEntry={openEntry} recountEnabled={true} /> )}
{/* ===== Entry Dialog (unchanged) ===== */}
{!editingEntry && (

Select Incoming Items

e.on_the_way)} manualPagination manualSorting enableGlobalFilter onPaginationChange={setEntriesPagination} onSortingChange={setEntriesSorting} onGlobalFilterChange={setEntriesFilter} rowCount={entriesCount} state={{ isLoading: entriesOnTheWayLoading, pagination: onTheWayEntriesPagination, sorting: onTheWayEntriesSorting, globalFilter: onTheWayEntriesFilter, }} />
)}

{editingEntry ? 'Edit Entry' : 'New Entry'}

{!editingEntry && (
setEntryForm((prev) => ({ ...prev, physical_item_id: val }))} > setItemQuery(e.target.value)} displayValue={id => { const itm = physicalItems.find(i => i.id === id) return itm ? `${itm.name} - ${itm.type._name}` : '' }} placeholder="Select item..." className="input" /> {filteredItems.map((item) => { return (
{item.name} - {item.type._name} {entryForm.physical_item_id === item.id && ( )}
); })}
)}