vat_wms/resources/js/Pages/StockBatch.tsx

903 lines
40 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.

// 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<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 [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<PhysicalItem[]>([]);
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 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();
}, []);
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));
} 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 pivotd 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<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 }) => {
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<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 {
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 (
<AppLayout title="Stock Batches" renderHeader={() => <h2 className="font-semibold text-xl">Stock Batches</h2>}>
<Head title="Stock Batches" />
<Toaster position="top-center" />
{/* ===== Main Batches Table ===== */}
<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>
{/* ===== Create New Batch Dialog ===== */}
<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"
name="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>
{/* ===== View Batch Details Dialog ===== */}
<dialog ref={viewDialogRef} className="modal">
<div className="modal-box max-w-6xl flex flex-col 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>
{/* Replace inline batchdetails/table with <BatchInfoWindow /> */}
{selectedBatch && (
<BatchInfoWindow
selectedBatch={selectedBatch}
formatDate={formatDate}
router={router}
route={(name, params) => route(name, params)}
entries={entries}
openEntry={openEntry}
recountEnabled={true}
/>
)}
<div className="mt-4 flex justify-between">
<div />
<div>
<button onClick={() => openEntry()} className="btn btn-secondary">
Edit Items
</button>
</div>
</div>
</div>
</dialog>
{/* ===== Entry Dialog (unchanged) ===== */}
<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 + " - " + physicalItems.find((i) => i.id === id)?.type._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) => {
return (
<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} - {item.type._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={selectedBatch?.supplier.id ?? ''}
disabled={true}
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>
<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>
{!selectedBatch && (
<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>
)}
{selectedBatch && (
<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) => {
const available = positions.filter(
(p) =>
(editingEntry ? true : !p.occupied) &&
!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"
disabled={true}
>
<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"
disabled={true}
/>
</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>
);
}