vat_wms/resources/js/Pages/StockBatch.tsx
2025-06-02 07:36:24 +02:00

712 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {StockBatch, StockSection, Supplier} from '@/types';
import {size} from "lodash";
import { router } from "@inertiajs/react";
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;
stock_batch_id: number;
physical_item?: DropdownOption;
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<StockEntry, 'id'> = {
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<StockBatch[]>([]);
const [statuses, setStatuses] = useState<[]>([]);
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [batchLoading, setBatchLoading] = useState(false);
const [batchCount, setBatchCount] = useState(0);
const [batchPagination, setBatchPagination] = useState<MRT_PaginationState>({ pageIndex: 0, pageSize: 10 });
const [batchSorting, setBatchSorting] = useState<MRT_SortingState>([{ id: 'updatedAt', desc: true }]);
const [batchFilter, setBatchFilter] = useState('');
const createDialogRef = useRef<HTMLDialogElement>(null);
const [batchForm, setBatchForm] = useState(defaultBatchForm);
const [batchFiles, setBatchFiles] = useState<FileWithType[]>([]);
const viewDialogRef = useRef<HTMLDialogElement>(null);
const [selectedBatch, setSelectedBatch] = useState<StockBatch | null>(null);
const [entries, setEntries] = useState<StockEntry[]>([]);
const [entriesLoading, setEntriesLoading] = useState(false);
const [entriesCount, setEntriesCount] = useState(0);
const [entriesPagination, setEntriesPagination] = useState<MRT_PaginationState>({ pageIndex: 0, pageSize: 10 });
const [entriesSorting, setEntriesSorting] = useState<MRT_SortingState>([{ id: 'id', desc: false }]);
const [entriesFilter, setEntriesFilter] = useState('');
const [onTheWayEntries, setOnTheWayEntries] = useState<StockEntry[]>([]);
const [entriesOnTheWayLoading, setEntriesOnTheWayLoading] = useState(false);
const [onTheWayEntriesCount, setOnTheWayEntriesCount] = useState(0);
const [onTheWayEntriesPagination, setOnTheWayEntriesPagination] = useState<MRT_PaginationState>({ pageIndex: 0, pageSize: 10 });
const [onTheWayEntriesSorting, setOnTheWayEntriesSorting] = useState<MRT_SortingState>([{ id: 'id', desc: false }]);
const [onTheWayEntriesFilter, setOnTheWayEntriesFilter] = useState('');
const [onTheWayEntriesSelections, setOnTheWayEntriesSelections] = useState<number[]>([]);
const entryDialogRef = useRef<HTMLDialogElement>(null);
const [editingEntry, setEditingEntry] = useState<StockEntry | null>(null);
const [entryForm, setEntryForm] = useState<Omit<StockEntry, 'id'>>({ ...defaultEntryForm });
const [physicalItems, setPhysicalItems] = useState<DropdownOption[]>([]);
const [positions, setPositions] = useState<DropdownOption[]>([]);
const [countries, setCountries] = useState<DropdownOption[]>([]);
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<MRT_ColumnDef<StockBatch>[]>(() => [
{ 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<string>(), 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<string>()) },
{ accessorKey: 'updated_at', header: 'Updated', Cell: ({ cell }) => formatDate(cell.getValue<string>()) },
], []);
const entryColumns = useMemo<MRT_ColumnDef<StockEntry>[]>(
() => [
{ accessorKey: 'id', header: 'ID', size: 80 },
{ accessorKey: 'physical_item.name', header: 'Physical Item', size: 200 },
{ accessorKey: 'supplier.name', header: 'Supplier', size: 150 },
{ accessorKey: 'physical_item.manufacturer.name', header: 'Brand', size: 150 },
{ accessorKey: 'count', header: 'Count', size: 100 },
{ accessorKey: 'price', header: 'Price', size: 100, Cell: ({ cell }) => cell.getValue<number>()?.toFixed(2) || '-' },
{ accessorKey: 'bought', header: 'Bought Date', size: 120 },
{ accessorKey: 'on_the_way', header: 'On The Way', size: 100, Cell: ({ cell }) => cell.getValue<boolean>() ? '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 },
],
[],
);
const entryOnTheWayColumns = useMemo<MRT_ColumnDef<StockEntry>[]>(
() => [
{
accessorKey: 'select',
header: 'Select',
Cell: ({ row }) => {
const id = row.original.id;
return (
<input
type="checkbox"
checked={onTheWayEntriesSelections.includes(id)}
onChange={() => 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(); fetchStatusList();}, []);
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);
console.log(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);
console.log("fetching entries for batch ", batchId);
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 } });
console.log(res.data.data);setEntries(res.data.data); setEntriesCount(size(res.data.data));
} catch (error) { toast.error('Cannot fetch entries'); console.error(error); }
finally { setEntriesLoading(false); }
}
async function fetchEntriesOnTheWay() {
setEntriesOnTheWayLoading(true);
console.log("fetching entries on the way ");
try {
const res = await axios.get(`/api/stockDataOnTheWay`);
console.log(res.data.data); 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([]); }
const openEntry = (entry?: StockEntry) => {
fetchEntriesOnTheWay();
if (entry) {
setEditingEntry(entry);
setEntryForm({ ...entry });
// Build rows from whatever pivotd sections came back
const rows = entry.sections?.map(sec => ({
stock_position_id: sec.pivot.section_id,
count: sec.pivot.count,
})) || [];
console.log(entry.sections);
// Append an empty row so the user can add more
setPositionRows([...rows, { stock_position_id: null, count: null }]);
} else {
setEditingEntry(null);
setEntryForm({ ...defaultEntryForm, stock_batch_id: selectedBatch!.id });
// brand-new: start with one empty row
setPositionRows([{ stock_position_id: null, count: null }]);
}
entryDialogRef.current?.showModal();
};
const closeEntry = () => { entryDialogRef.current?.close(); setEditingEntry(null); };
const handleBatchInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setBatchForm(prev => ({ ...prev, [name]: name === 'supplierId' ? (value ? parseInt(value) : null) : value }));
};
const handleBatchFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 }, i) => { fd.append(`files[${i}]`, file); fd.append(`file_types[${i}]`, fileType); });
await axios.post('/api/stockBatches', fd, { headers: { 'Content-Type': 'multipart/form-data' } });
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();
console.log(selectedBatch);
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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
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 await axios.post(`/api/stockData`, entryForm);
toast.success(editingEntry ? 'Entry updated' : 'Entry created');
fetchEntries(selectedBatch!.id); closeEntry();
} catch { toast.error('Cannot save entry'); }
};
// Before your return JSX, replace the old flag with this:
const hasNonCountedStatus = selectedBatch?.stock_entries?.some(stockEntry => {
// 1. Only statuses with no section:
const nullSectionStatuses = stockEntry.status_history?.filter(h => h.section_id === null) ?? [];
if (nullSectionStatuses.length === 0) return true;
// 2. Find the *latest* one by timestamp:
const latest = nullSectionStatuses.reduce((prev, curr) =>
new Date(prev.created_at) > new Date(curr.created_at) ? prev : curr
);
// 3. Check if that status isnt “COUNTED” (id === 2)
return latest.stock_entries_status_id !== 2;
});
function calculateStatusRatio(
entries: StockEntry[],
statusId: number
): { count: number; total: number; ratio: number } {
const total = entries.length;
const count = entries.filter((entry) =>
entry.status_history?.some((h) => h.stock_entries_status_id === statusId)
).length;
const ratio = total > 0 ? count / total : 0;
return { count, total, ratio };
}
async function fetchStatusList() {
try {
const res = await axios.get('/api/stockStatusList');
setStatuses(res.data.statuses);
} catch {
toast.error('Failed to load items');
}
}
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]);
const statusIds = [2, 6, 7, 8]
return (
<AppLayout title="Stock Batches" renderHeader={() => <h2 className="font-semibold text-xl">Stock Batches</h2>}>
<Head title="Stock Batches" />
<Toaster position="top-center" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 shadow-xl sm:rounded-lg p-6">
<div className="flex justify-between mb-4">
<h1 className="text-2xl font-bold">Batches</h1>
<button onClick={openCreate} className="btn btn-primary">Add New Batch</button>
</div>
<MaterialReactTable
columns={batchColumns}
data={batches}
manualPagination manualSorting enableGlobalFilter
onPaginationChange={setBatchPagination}
onSortingChange={setBatchSorting}
onGlobalFilterChange={setBatchFilter}
rowCount={batchCount}
state={{ isLoading: batchLoading, pagination: batchPagination, sorting: batchSorting, globalFilter: batchFilter }}
muiTableBodyRowProps={({ row }) => ({ onClick: () => openView(row.original), style: { cursor: 'pointer' } })}
/>
</div>
</div>
</div>
<dialog ref={createDialogRef} className="modal">
<form onSubmit={handleBatchSubmit} className="modal-box p-6">
<button type="button" onClick={closeCreate} className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<h3 className="font-bold text-lg mb-4">New Stock Batch</h3>
<div className="form-control mb-4">
<label htmlFor="supplierId" className="label"><span className="label-text">Supplier</span></label>
<select id="supplierId" name="supplierId" value={batchForm.supplierId ?? ''} onChange={handleBatchInputChange} className="select">
<option value="">Select supplier...</option>
{suppliers.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</div>
<div className="form-control mb-4">
<label htmlFor="tracking_number" className="label"><span className="label-text">Tracking Number</span></label>
<input id="tracking_number" name="tracking_number" type="text" value={batchForm.tracking_number} onChange={handleBatchInputChange} className="input" />
</div>
<div className="form-control mb-4">
<label htmlFor="arrival_date" className="label"><span className="label-text">Arrival Date</span></label>
<input id="arrival_date" name="arrival_date" type="date" value={batchForm.arrival_date} onChange={handleBatchInputChange} className="input" />
</div>
<div className="form-control mb-4">
<label htmlFor="files" className="label"><span className="label-text">Upload Files</span></label>
<input id="files" type="file" multiple onChange={handleBatchFileChange} className="file-input" />
</div>
{batchFiles.map((f, i) => (
<div key={i} className="flex items-center mb-2 space-x-2">
<span>{f.file.name}</span>
<select value={f.fileType} onChange={e => handleBatchFileTypeChange(i, e.target.value as any)} className="select select-sm">
<option value="invoice">Invoice</option><option value="label">Label</option><option value="other">Other</option>
</select>
<button type="button" onClick={() => removeBatchFile(i)} className="btn btn-sm btn-error">Remove</button>
</div>
))}
<div className="modal-action">
<button type="submit" className="btn btn-primary">Create Batch</button>
</div>
</form>
</dialog>
<dialog ref={viewDialogRef} className="modal">
<div className="modal-box max-w-6xl flex space-x-6 p-6 relative">
<button type="button" onClick={closeView} className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<div className="w-1/3 flex flex-col justify-between">
<div>
<h3 className="font-bold text-lg mb-2">Batch Details</h3>
{selectedBatch && <ul className="list-disc list-inside">
<li><strong>ID:</strong> {selectedBatch.id}</li>
<li><strong>Supplier:</strong> {selectedBatch.supplier.name}</li>
<li><strong>Tracking #:</strong> {selectedBatch.tracking_number}</li>
<li><strong>Arrival:</strong> {formatDate(selectedBatch.arrival_date, false)}</li>
</ul>}
<div>
{/* existing status-history list */}
{/*{selectedBatch?.stock_entries?.map((entry) => (*/}
{/* <div key={entry.id} style={{ marginBottom: 16 }}>*/}
{/* <h4>Stock Entry {entry.id}</h4>*/}
{/* <ul>*/}
{/* {entry.status_history?.map((history, idx) => (*/}
{/* <li key={idx}>{history.status.name}</li>*/}
{/* ))}*/}
{/* </ul>*/}
{/* </div>*/}
{/*))}*/}
{/* ratio displays */}
<div style={{ marginTop: 24 }}>
<h3>Status Ratios</h3>
{selectedBatch && statusIds.map((id) => {
const { count, total, ratio } = calculateStatusRatio(selectedBatch.stock_entries, id);
const status_data = statuses.filter(s => s.id === id)[0];
return (
<p key={id}>
{status_data.name}: {count} / {total} (
{(ratio * 100).toFixed(1)}%)
</p>
);
})}
</div>
</div>
</div>
<div>
<div className="tooltip" data-tip="batch QR kod">
<button className="btn bg-[#666666] text-[19px]">
<FontAwesomeIcon icon={faQrcode} />
</button>
</div>
{hasNonCountedStatus && (
<div className="tooltip" data-tip="Prepocitat">
<button
className="btn btn-warning text-[19px]"
onClick={() => {
router.get(route('batchCounting', { selectedBatch: selectedBatch.id }));
}}
>
<FontAwesomeIcon icon={faListOl} />
</button>
</div>
)}
</div>
</div>
<div className="w-2/3 flex flex-col">
<h3 className="font-bold text-lg mb-2">Stock Entries</h3>
<div className="flex-1 overflow-auto">
<MaterialReactTable
columns={entryColumns}
data={entries}
manualPagination manualSorting enableGlobalFilter
onPaginationChange={setEntriesPagination}
onSortingChange={setEntriesSorting}
onGlobalFilterChange={setEntriesFilter}
rowCount={entriesCount}
state={{ isLoading: entriesLoading, pagination: entriesPagination, sorting: entriesSorting, globalFilter: entriesFilter }}
muiTableBodyRowProps={({ row }) => ({ onClick: () => openEntry(row.original), style: { cursor: 'pointer' }})}
/>
</div>
<div className="mt-4 flex justify-between">
<div>
</div>
<div>
<button onClick={() => openEntry()} className="btn btn-secondary">Edit Items</button>
</div>
</div>
</div>
</div>
</dialog>
<dialog ref={entryDialogRef} className="modal">
<form onSubmit={handleEntrySubmit} className={`modal-box flex space-x-4 p-6 ${!editingEntry ? 'max-w-full' : ''}`}>
<button type="button" onClick={closeEntry} className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
{!editingEntry &&
<div className="w-2/3">
<h3 className="font-bold text-lg mb-2">Select Incoming Items</h3>
<MaterialReactTable
columns={entryOnTheWayColumns}
data={onTheWayEntries.filter(e => e.on_the_way)}
manualPagination manualSorting enableGlobalFilter
onPaginationChange={setEntriesPagination}
onSortingChange={setEntriesSorting}
onGlobalFilterChange={setEntriesFilter}
rowCount={entriesCount}
state={{ isLoading: entriesOnTheWayLoading, pagination: onTheWayEntriesPagination, sorting: onTheWayEntriesSorting, globalFilter: onTheWayEntriesFilter }}
/>
</div>
}
<div className={!editingEntry ? 'w-1/3' : 'w-full'}>
<h3 className="font-bold text-lg mb-4">{editingEntry ? 'Edit Entry' : 'New Entry'}</h3>
{!editingEntry &&
<div className="form-control mb-4">
<Combobox value={entryForm.physical_item_id} onChange={val => setEntryForm(prev => ({ ...prev, physical_item_id: val }))}>
<Combobox.Input
onChange={e => setItemQuery(e.target.value)}
displayValue={id => physicalItems.find(i => i.id === id)?.name || ''}
placeholder="Select item..."
className="input"
/>
<Combobox.Options className="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-h-60 overflow-auto">
{filteredItems.map(item => (
<Combobox.Option key={item.id} value={item.id} className="cursor-pointer p-2 hover:bg-gray-200">
<div className="flex justify-between items-center">
<span>{item.name}</span>{entryForm.physical_item_id === item.id && <FontAwesomeIcon icon={faCheck} />}
</div>
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
</div>
}
<div className="form-control mb-4">
<label htmlFor="supplier_id" className="label"><span className="label-text">Supplier</span></label>
<select id="supplier_id" name="supplier_id" value={entryForm.supplier_id ?? ''} onChange={handleEntryInputChange} className="select">
<option value="">Select supplier...</option>
{suppliers.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="form-control">
<label htmlFor="count" className="label"><span className="label-text">Count</span></label>
<input id="count" name="count" type="number" value={entryForm.count} onChange={handleEntryInputChange} className="input" />
</div>
<div className="form-control">
<label htmlFor="price" className="label"><span className="label-text">Price</span></label>
<input id="price" name="price" type="number" step="0.01" value={entryForm.price || ''} onChange={handleEntryInputChange} className="input" />
</div>
<div className="form-control">
<label htmlFor="bought" className="label"><span className="label-text">Bought Date</span></label>
<input id="bought" name="bought" type="date" value={entryForm.bought || ''} onChange={handleEntryInputChange} className="input" />
</div>
</div>
<div className="form-control mb-4">
<label htmlFor="description" className="label"><span className="label-text">Description</span></label>
<textarea id="description" name="description" value={entryForm.description || ''} onChange={handleEntryInputChange} className="textarea" />
</div>
<div className="form-control mb-4">
<label htmlFor="note" className="label"><span className="label-text">Note</span></label>
<textarea id="note" name="note" value={entryForm.note || ''} onChange={handleEntryInputChange} className="textarea" />
</div>
<div className="grid grid-cols-2 gap-4 items-end mb-4">
<div className="form-control">
<label htmlFor="country_of_origin_id" className="label"><span className="label-text">Country</span></label>
<select id="country_of_origin_id" name="country_of_origin_id" value={entryForm.country_of_origin_id || ''} onChange={handleEntryInputChange} className="select">
<option value="">Select country...</option>
{countries.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
<div className="form-control flex items-center">
<label className="label cursor-pointer"><input type="checkbox" name="on_the_way" checked={entryForm.on_the_way} onChange={handleEntryInputChange} className="checkbox mr-2" /><span className="label-text">On The Way</span></label>
</div>
<div className="form-control flex items-center">
<label className="label cursor-pointer"><input type="checkbox" name="surplus_item" checked={entryForm.surplus_item} onChange={handleEntryInputChange} className="checkbox mr-2" /><span className="label-text">Extra produkt</span></label>
</div>
</div>
<div className="grid grid-cols-1 gap-4 items-end mb-4 border-t-2 border-gray-600 pt-4">
{/* Dynamic stock position rows */}
<div>
Stock positions
</div>
<div className="space-y-4 mb-4">
{positionRows.map((row, idx) => {
console.log(row);
// filter out positions already used in other rows
const available = positions.filter(p =>
// if were editing, show everything; otherwise only unoccupied slots
(editingEntry ? true : !p.occupied)
// then still exclude any that are already picked in other rows
&& !positionRows.some((r,i) => i !== idx && r.stock_position_id === p.id)
);
return available.length > 0 ? (
<div key={idx} className="grid grid-cols-2 gap-4 items-end">
<div className="form-control">
<label className="label"><span
className="label-text">Position</span></label>
<select
value={row.stock_position_id || ''}
onChange={e => handlePositionRowChange(idx, 'stock_position_id', e.target.value ? parseInt(e.target.value) : null)}
className="select"
>
<option value="">Select position...</option>
{available.map(p => <option key={p.id}
value={p.id}>{p.name}</option>)}
</select>
</div>
<div className="form-control">
<label className="label"><span
className="label-text">Count</span></label>
<input
type="number"
value={row.count ?? ''}
onChange={e => handlePositionRowChange(idx, 'count', e.target.value ? parseInt(e.target.value) : null)}
className="input"
/>
</div>
</div>
): null;
})}
</div>
</div>
<div className="modal-action">
{!editingEntry && (
<button
type="button"
className="btn btn-primary"
disabled={onTheWayEntriesSelections.length === 0}
onClick={handleBatchAddEntries}
>
Add to batch
</button>
)}
<button type="submit" className="btn btn-primary">{editingEntry ? 'Update Entry' : 'Create Entry'}</button>
</div>
</div>
</form>
</dialog>
</AppLayout>
);
}