import React, {useState, useEffect, useMemo, useRef} from 'react'; import {Head} from '@inertiajs/react'; import AppLayout from '@/Layouts/AppLayout'; import { MaterialReactTable, type MRT_ColumnDef, type MRT_PaginationState, type MRT_SortingState, type MRT_ColumnFiltersState, } from 'material-react-table'; import axios from 'axios'; import {toast} from 'react-hot-toast'; import {Combobox} from '@headlessui/react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faChevronDown, faCheck} from '@fortawesome/free-solid-svg-icons'; import {Autocomplete, TextField} from '@mui/material'; // --- Interfaces --- interface DropdownOption { id: number; name: string; } 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; stock_batch_id: number | null; created_by?: number; updated_by?: number; physical_item?: DropdownOption; supplier?: DropdownOption; stock_position?: { id: number; line: string; rack: string; shelf: string; position: string }; } interface StockEntryFormData { 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; stock_batch_id: number | null; } const defaultForm = { physical_item_id: null, supplier_id: null, count: 0, price: null, bought: null, description: null, note: null, stock_position_id: null, country_of_origin_id: null, on_the_way: false, stock_batch_id: null, }; export default function StockEntries() { // Table state const [data, setData] = useState([]); const [isError, setIsError] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isRefetching, setIsRefetching] = useState(false); const [rowCount, setRowCount] = useState(0); // Dropdowns const [stockPositions, setStockPositions] = useState([]); const [suppliers, setSuppliers] = useState([]); const [manufacturers, setManufacturers] = useState([]); const [physicalItems, setPhysicalItems] = useState([]); const [originCountries, setOriginCountries] = useState([]); // Modal/form state const [editingEntry, setEditingEntry] = useState(null); const [formData, setFormData] = useState(defaultForm); // Batch mode state const [isBatchMode, setIsBatchMode] = useState(false); const [batchSelections, setBatchSelections] = useState([]); // Combobox search const [itemQuery, setItemQuery] = useState(''); const filteredItems = useMemo( () => itemQuery === '' ? physicalItems : physicalItems.filter(item => item.name.toLowerCase().includes(itemQuery.toLowerCase()), ), [itemQuery, physicalItems], ); // Pagination/sorting/filtering const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 50}); const [sorting, setSorting] = useState([{id: 'updated_at', desc: true}]); const [globalFilter, setGlobalFilter] = useState(''); const [columnFilters, setColumnFilters] = useState([]) // holds the full objects for whatever the user has chosen const [physicalItemFilterOptions, setPhysicalItemFilterOptions] = useState([]); // Modal ref const dialogRef = useRef(null); const openModal = () => dialogRef.current?.showModal(); const closeModal = () => dialogRef.current?.close(); // Columns const columns = useMemo[]>( () => [ {accessorKey: 'id', header: 'ID', size: 80}, { id: 'physical_item_id', header: 'Physical Item', // normal table‐cell display accessorFn: row => row.physical_item?.name ?? '-', cell: ({row}) => row.original.physical_item?.name ?? '-', enableColumnFilter: true, Filter: ({column}) => { // the numeric IDs MRT is storing under the hood const selectedIds = (column.getFilterValue() as number[]) ?? []; // 1) our “persistent” list of chosen DropdownOption objects const selectedOptions = physicalItemFilterOptions; // 2) the transient search results // (you already have itemQuery & filteredItems above) const suggestionOptions = filteredItems .filter(item => !selectedIds.includes(item.id)); // merge them so selected stay in the list: const options = [...selectedOptions, ...suggestionOptions]; return (
{/* Show chosen items as chips */}
{selectedOptions.map(item => ( // remove from both MRT and our state { const newIds = selectedIds.filter(id => id !== item.id); column.setFilterValue(newIds); setPhysicalItemFilterOptions(opts => opts.filter(o => o.id !== item.id) ); } } > {item.name} × ))}
{/* The multi‐select Combobox */} { // 1) tell MRT about the new array of IDs column.setFilterValue(newSelection.map(i => i.id)); // 2) persist the objects for future renders setPhysicalItemFilterOptions(newSelection); }} >
setItemQuery(e.target.value)} displayValue={() => ''} // keep the input box empty /> {options.map(item => ( {item.name} {selectedIds.includes(item.id) && } ))}
); }, filterFn: (row, _columnId, filterValue: number[]) => filterValue.length === 0 || filterValue.includes(row.original.physical_item_id!), }, // 2) Supplier { id: 'supplier_id', header: 'Supplier', accessorFn: row => row.supplier?.name ?? '-', cell: ({row}) => row.original.supplier?.name ?? '-', enableColumnFilter: true, Filter: ({column}) => ( opt.name} isOptionEqualToValue={(opt, val) => opt.id === val.id} value={ suppliers.filter(s => (column.getFilterValue() as number[] || []).includes(s.id) ) } onChange={(_, v) => column.setFilterValue(v.map(s => s.id)) } renderInput={params => ( )} /> ), filterFn: (row, _columnId, filterValue: number[]) => filterValue.length === 0 || filterValue.includes(row.original.supplier_id!), }, // 3) Brand / Manufacturer { id: 'manufacturer_id', header: 'Brand', accessorFn: row => row.physical_item?.manufacturer?.name ?? '-', cell: ({row}) => row.original.physical_item?.manufacturer?.name ?? '-', enableColumnFilter: true, Filter: ({column}) => ( opt.name} isOptionEqualToValue={(opt, val) => opt.id === val.id} value={ manufacturers.filter(m => (column.getFilterValue() as number[] || []).includes(m.id) ) } onChange={(_, v) => column.setFilterValue(v.map(m => m.id)) } renderInput={params => ( )} /> ), filterFn: (row, _columnId, filterValue: number[]) => filterValue.length === 0 || filterValue.includes(row.original.physical_item?.manufacturer?.id!), }, {accessorKey: 'count', header: 'Count', size: 100}, { accessorKey: 'price', header: 'Price', size: 100, Cell: ({cell}) => cell.getValue()?.toFixed(2) || '-' }, {accessorKey: 'bought', header: 'Bought Date', size: 120}, { accessorKey: 'on_the_way', header: 'On The Way', size: 100, Cell: ({cell}) => cell.getValue() ? 'Yes' : 'No' }, { accessorKey: 'stock_position', header: 'Position', size: 150, Cell: ({row}) => { const pos = row.original.stock_position; return pos ? `${pos.line}-${pos.rack}-${pos.shelf}-${pos.position}` : '-'; } }, {accessorKey: 'updated_at', header: 'Last Updated', size: 150}, ], [suppliers, manufacturers, physicalItems, itemQuery, physicalItemFilterOptions], ); // Batch columns const batchColumns = useMemo[]>( () => [ { accessorKey: 'select', header: 'Select', Cell: ({row}) => { const id = row.original.id; return ( setBatchSelections(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'}, ], [batchSelections], ); // Fetch options & data useEffect(() => { fetchData(); }, [pagination.pageIndex, pagination.pageSize, sorting, globalFilter, columnFilters]); useEffect(() => { fetchOptions(); }, []); useEffect(() => { setPagination((old) => ({...old, pageIndex: 0})); }, [globalFilter, columnFilters]); async function fetchOptions() { try { const [ {data: opts}, {data: mfrData}, ] = await Promise.all([ axios.get('/api/stockData/options'), axios.get('/api/stockData/manufacturers'), // ← new endpoint ]); setStockPositions(opts.stockPositions); setSuppliers(opts.suppliers); setOriginCountries(opts.countriesOrigin); setManufacturers(mfrData.manufacturers); // ← set manufacturers } catch { toast.error('Failed to load form options'); } } async function fetchData() { // trigger loading states setIsLoading(!data.length); setIsRefetching(!!data.length); const supplierFilter = columnFilters.find(f => f.id === 'supplier_id')?.value const brandFilter = columnFilters.find(f => f.id === 'manufacturer_id')?.value const itemFilter = columnFilters.find(f => f.id === 'physical_item_id')?.value console.log(columnFilters); try { const res = await axios.get('/api/stockData', { params: { page: pagination.pageIndex + 1, per_page: pagination.pageSize, sort_field: sorting[0]?.id || 'updated_at', sort_direction: sorting[0]?.desc ? 'desc' : 'asc', search: globalFilter, supplier_id: supplierFilter ?? undefined, manufacturer_id: brandFilter ?? undefined, physical_item_id: itemFilter ?? undefined, }, }); // update rows and total count setData(res.data.data); setRowCount(res.data.meta.total); // (optional) keep your pagination widget in sync // setPagination(old => ({ // ...old, // pageIndex: res.data.meta.current_page - 1, // })); } catch (error) { setIsError(true); toast.error('Failed to fetch stock entries'); } finally { setIsLoading(false); setIsRefetching(false); } } 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]); // Input change const handleInputChange = (e: React.ChangeEvent) => { const {name, value, type} = e.target; setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : type === 'number' ? parseFloat(value) || null : value || null, })); }; // Submit const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { let res; if (editingEntry) { res = await axios.put(`/api/stockData/${editingEntry.id}`, formData); toast.success('Stock entry updated'); } else { res = await axios.post('/api/stockData', formData); toast.success('Stock entry created'); } const newEntry: StockEntry = res.data; if (isBatchMode) { // Add to batch and keep modal open setBatchSelections(prev => [...prev, newEntry.id]); setFormData({...defaultForm, on_the_way: true}); } else { closeModal(); } fetchData(); } catch { toast.error('Failed to save'); } }; // Batch submit const handleBatchSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await axios.post('/api/stockData/batch', {ids: batchSelections}); toast.success('Batch created'); closeModal(); fetchData(); } catch { toast.error('Batch submit failed'); } }; const handleEdit = (entry: StockEntry) => { setEditingEntry(entry); setFormData({ physical_item_id: entry.physical_item_id, supplier_id: entry.supplier_id, count: entry.count, price: entry.price, bought: entry.bought, description: entry.description, note: entry.note, stock_position_id: entry.stock_position_id, country_of_origin_id: entry.country_of_origin_id, on_the_way: entry.on_the_way, stock_batch_id: entry.stock_batch_id, }); // make sure our combobox can render the current item if (entry.physical_item) { setPhysicalItems(prev => prev.some(i => i.id === entry.physical_item!.id) ? prev : [...prev, entry.physical_item!] ); } openModal(); }; const handleDelete = async (id: number) => { if (!confirm('Are you sure?')) return; try { await axios.delete(`/api/stockData/${id}`); toast.success('Deleted'); fetchData(); } catch { toast.error('Failed to delete'); } }; const handleAdd = () => { setIsBatchMode(false); setEditingEntry(null); setFormData(defaultForm); openModal(); }; const handleNewBatch = () => { setIsBatchMode(true); setEditingEntry(null); setBatchSelections([]); setFormData({...defaultForm, on_the_way: true}); openModal(); }; return (

Stock Entries

[ , , ]} muiTablePaginationProps={{ rowsPerPageOptions: [25, 50, 100, 200, 500, 1000, 2000, 5000], rowsPerPage: pagination.pageSize, }} />
{/* Modal (Add/Edit & Batch) */}
{isBatchMode && (

Select Incoming Items

e.on_the_way)} enablePagination={false} enableSorting={false} enableGlobalFilter={false} getRowId={row => row.id.toString()} />
)}

{isBatchMode ? 'New Batch Entry' : (editingEntry ? 'Edit Stock Entry' : 'New Stock Entry')}

{/* Physical Item */}
setFormData(prev => ({...prev, physical_item_id: val}))}> setItemQuery(e.target.value)} displayValue={id => physicalItems.find(i => i.id === id)?.name || ''} placeholder="Select item..." className="input" /> {filteredItems.map(item => (
{item.name} {formData.physical_item_id === item.id && }
))}
{/* Supplier */}
{/* Count, Price, Bought Date */}
{/* Description & Note */}