718 lines
32 KiB
TypeScript
718 lines
32 KiB
TypeScript
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<StockEntry[]>([]);
|
||
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<DropdownOption[]>([]);
|
||
const [suppliers, setSuppliers] = useState<DropdownOption[]>([]);
|
||
const [manufacturers, setManufacturers] = useState<DropdownOption[]>([]);
|
||
const [physicalItems, setPhysicalItems] = useState<DropdownOption[]>([]);
|
||
const [originCountries, setOriginCountries] = useState<DropdownOption[]>([]);
|
||
|
||
// Modal/form state
|
||
const [editingEntry, setEditingEntry] = useState<StockEntry | null>(null);
|
||
const [formData, setFormData] = useState<StockEntryFormData>(defaultForm);
|
||
|
||
// Batch mode state
|
||
const [isBatchMode, setIsBatchMode] = useState(false);
|
||
const [batchSelections, setBatchSelections] = useState<number[]>([]);
|
||
|
||
// 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<MRT_PaginationState>({pageIndex: 0, pageSize: 50});
|
||
const [sorting, setSorting] = useState<MRT_SortingState>([{id: 'updated_at', desc: true}]);
|
||
const [globalFilter, setGlobalFilter] = useState('');
|
||
const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>([])
|
||
// holds the full objects for whatever the user has chosen
|
||
const [physicalItemFilterOptions, setPhysicalItemFilterOptions] = useState<DropdownOption[]>([]);
|
||
|
||
// Modal ref
|
||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||
const openModal = () => dialogRef.current?.showModal();
|
||
const closeModal = () => dialogRef.current?.close();
|
||
|
||
// Columns
|
||
const columns = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
||
() => [
|
||
{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 (
|
||
<div className="space-y-1">
|
||
{/* Show chosen items as chips */}
|
||
<div className="flex flex-wrap gap-1">
|
||
{selectedOptions.map(item => (
|
||
<span
|
||
key={item.id}
|
||
className="badge badge-sm badge-primary cursor-pointer"
|
||
onClick={() =>
|
||
// 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} ×
|
||
</span>
|
||
))}
|
||
</div>
|
||
|
||
{/* The multi‐select Combobox */}
|
||
<Combobox
|
||
multiple
|
||
value={selectedOptions}
|
||
onChange={(newSelection: DropdownOption[]) => {
|
||
// 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);
|
||
}}
|
||
>
|
||
<div className="relative">
|
||
<Combobox.Input
|
||
className="input w-full"
|
||
placeholder="Type to search…"
|
||
onChange={e => setItemQuery(e.target.value)}
|
||
displayValue={() => ''} // keep the input box empty
|
||
/>
|
||
<Combobox.Options
|
||
className="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-h-60 overflow-auto">
|
||
{options.map(item => (
|
||
<Combobox.Option
|
||
key={item.id}
|
||
value={item}
|
||
className="cursor-pointer p-2 hover:bg-gray-200 flex justify-between items-center"
|
||
>
|
||
{item.name}
|
||
{selectedIds.includes(item.id) && <FontAwesomeIcon icon={faCheck}/>}
|
||
</Combobox.Option>
|
||
))}
|
||
</Combobox.Options>
|
||
</div>
|
||
</Combobox>
|
||
</div>
|
||
);
|
||
},
|
||
|
||
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}) => (
|
||
<Autocomplete
|
||
multiple
|
||
size="small"
|
||
options={suppliers}
|
||
getOptionLabel={opt => 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 => (
|
||
<TextField {...params} variant="standard" placeholder="Filter suppliers…"/>
|
||
)}
|
||
/>
|
||
),
|
||
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}) => (
|
||
<Autocomplete
|
||
multiple
|
||
size="small"
|
||
options={manufacturers}
|
||
getOptionLabel={opt => 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 => (
|
||
<TextField {...params} variant="standard" placeholder="Filter brands…"/>
|
||
)}
|
||
/>
|
||
),
|
||
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<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},
|
||
],
|
||
[suppliers, manufacturers, physicalItems, itemQuery, physicalItemFilterOptions],
|
||
);
|
||
|
||
// Batch columns
|
||
const batchColumns = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
||
() => [
|
||
{
|
||
accessorKey: 'select',
|
||
header: 'Select',
|
||
Cell: ({row}) => {
|
||
const id = row.original.id;
|
||
return (
|
||
<input
|
||
type="checkbox"
|
||
checked={batchSelections.includes(id)}
|
||
onChange={() => 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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
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 (
|
||
<AppLayout title="Stock Entries">
|
||
<Head title="Stock Entries"/>
|
||
<div className="py-6">
|
||
<div className="mx-auto sm:px-6 lg:px-8">
|
||
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||
<div className="mb-4 flex justify-between items-center">
|
||
<h1 className="text-2xl font-bold">Stock Entries</h1>
|
||
<div>
|
||
<button className="btn mr-2" onClick={handleNewBatch}>New batch</button>
|
||
<button className="btn" onClick={handleAdd}>Add New Entry</button>
|
||
</div>
|
||
</div>
|
||
<MaterialReactTable
|
||
columns={columns}
|
||
data={data}
|
||
enableTopToolbar
|
||
columnFilters={columnFilters}
|
||
onColumnFiltersChange={setColumnFilters}
|
||
onPaginationChange={setPagination}
|
||
onSortingChange={setSorting}
|
||
onGlobalFilterChange={setGlobalFilter}
|
||
manualPagination
|
||
rowCount={rowCount}
|
||
state={{
|
||
isLoading,
|
||
pagination,
|
||
showProgressBars: isRefetching,
|
||
sorting,
|
||
globalFilter,
|
||
columnFilters
|
||
}}
|
||
enableRowActions
|
||
renderRowActionMenuItems={({row}) => [
|
||
<button key="edit" onClick={() => handleEdit(row.original)}
|
||
className="menu-item">Edit</button>,
|
||
<button key="delete" onClick={() => handleDelete(row.original.id)}
|
||
className="menu-item text-red-600">Delete</button>,
|
||
]}
|
||
muiTablePaginationProps={{
|
||
rowsPerPageOptions: [25, 50, 100, 200, 500, 1000, 2000, 5000],
|
||
rowsPerPage: pagination.pageSize,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modal (Add/Edit & Batch) */}
|
||
<dialog ref={dialogRef} className="modal">
|
||
<form onSubmit={handleSubmit}
|
||
className={`modal-box flex space-x-4 p-6 ${isBatchMode ? 'max-w-full' : ''}`}>
|
||
<button type="button" onClick={closeModal}
|
||
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕
|
||
</button>
|
||
{isBatchMode && (
|
||
<div className="w-2/3">
|
||
<h3 className="font-bold text-lg mb-2">Select Incoming Items</h3>
|
||
<MaterialReactTable
|
||
columns={batchColumns}
|
||
data={data.filter(e => e.on_the_way)}
|
||
enablePagination={false}
|
||
enableSorting={false}
|
||
enableGlobalFilter={false}
|
||
getRowId={row => row.id.toString()}
|
||
/>
|
||
</div>
|
||
)}
|
||
<div className={isBatchMode ? 'w-1/3' : 'w-full'}>
|
||
<h3 className="font-bold text-lg mb-4">{isBatchMode ? 'New Batch Entry' : (editingEntry ? 'Edit Stock Entry' : 'New Stock Entry')}</h3>
|
||
{/* Physical Item */}
|
||
<div className="form-control">
|
||
<Combobox value={formData.physical_item_id}
|
||
onChange={val => setFormData(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>
|
||
{formData.physical_item_id === item.id &&
|
||
<FontAwesomeIcon icon={faCheck}/>}
|
||
</div>
|
||
</Combobox.Option>
|
||
))}
|
||
</Combobox.Options>
|
||
</Combobox>
|
||
</div>
|
||
|
||
{/* Supplier */}
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="supplier_id">
|
||
<span className="label-text">Supplier</span>
|
||
</label>
|
||
<select id="supplier_id" name="supplier_id" value={formData.supplier_id || ''}
|
||
onChange={handleInputChange} className="select">
|
||
<option value="">Select supplier...</option>
|
||
{suppliers.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Count, Price, Bought Date */}
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="count"><span
|
||
className="label-text">Count</span></label>
|
||
<input id="count" name="count" type="number" value={formData.count}
|
||
onChange={handleInputChange} className="input"/>
|
||
</div>
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="price"><span
|
||
className="label-text">Price</span></label>
|
||
<input id="price" name="price" type="number" step="0.01" value={formData.price || ''}
|
||
onChange={handleInputChange} className="input"/>
|
||
</div>
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="bought"><span
|
||
className="label-text">Bought Date</span></label>
|
||
<input id="bought" name="bought" type="date" value={formData.bought || ''}
|
||
onChange={handleInputChange} className="input"/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Description & Note */}
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="description"><span
|
||
className="label-text">Description</span></label>
|
||
<textarea id="description" name="description" value={formData.description || ''}
|
||
onChange={handleInputChange} className="textarea"/>
|
||
</div>
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="note"><span className="label-text">Note</span></label>
|
||
<textarea id="note" name="note" value={formData.note || ''} onChange={handleInputChange}
|
||
className="textarea"/>
|
||
</div>
|
||
|
||
{/* Stock Position & Country & On The Way */}
|
||
<div className="grid grid-cols-3 gap-4 items-end">
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="stock_position_id"><span
|
||
className="label-text">Position</span></label>
|
||
<select id="stock_position_id" name="stock_position_id"
|
||
value={formData.stock_position_id || ''} onChange={handleInputChange}
|
||
className="select">
|
||
<option value="">Select...</option>
|
||
{stockPositions.map(pos => <option key={pos.id} value={pos.id}>{pos.name}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-control">
|
||
<label className="label" htmlFor="supplier_id">
|
||
<span className="label-text">Country</span>
|
||
</label>
|
||
<select id="country_of_origin_id" name="country_of_origin_id"
|
||
value={formData.country_of_origin_id || ''} onChange={handleInputChange}
|
||
className="select">
|
||
<option value="">Select country...</option>
|
||
{originCountries.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="form-control flex items-center">
|
||
<label className="label cursor-pointer">
|
||
<input type="checkbox" name="on_the_way" checked={formData.on_the_way}
|
||
onChange={handleInputChange} className="checkbox mr-2"/>
|
||
<span className="label-text">On The Way</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Submit */}
|
||
<div className="modal-action flex justify-between">
|
||
{isBatchMode && (
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
disabled={batchSelections.length === 0}
|
||
onClick={handleBatchSubmit}
|
||
>
|
||
Create Batch
|
||
</button>
|
||
)}
|
||
|
||
<button type="submit" className="btn btn-primary">
|
||
{isBatchMode ? 'Create Entry' : (editingEntry ? 'Update Entry' : 'Create Entry')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</dialog>
|
||
</AppLayout>
|
||
);
|
||
}
|