370 lines
14 KiB
TypeScript
370 lines
14 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,
|
||
} from 'material-react-table';
|
||
import axios from 'axios';
|
||
import {toast, Toaster} from 'react-hot-toast';
|
||
import type { StockBatch, Supplier } from '@/types';
|
||
|
||
interface FileWithType {
|
||
file: File;
|
||
fileType: 'invoice' | 'label' | 'other';
|
||
}
|
||
|
||
export default function StockBatches() {
|
||
const [data, setData] = useState<StockBatch[]>([]);
|
||
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [rowCount, setRowCount] = useState(0);
|
||
|
||
const [pagination, setPagination] = useState<MRT_PaginationState>({
|
||
pageIndex: 0,
|
||
pageSize: 10,
|
||
});
|
||
const [sorting, setSorting] = useState<MRT_SortingState>([
|
||
{ id: 'updatedAt', desc: true },
|
||
]);
|
||
const [globalFilter, setGlobalFilter] = useState('');
|
||
|
||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||
|
||
const defaultFormData = {
|
||
supplierId: null as number | null,
|
||
tracking_number: '' as string,
|
||
arrival_date: '' as string,
|
||
};
|
||
const [formData, setFormData] = useState(defaultFormData);
|
||
const [files, setFiles] = useState<FileWithType[]>([]);
|
||
|
||
// inside your component, above the useMemo…
|
||
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 columns = useMemo<MRT_ColumnDef<StockBatch>[]>(() => [
|
||
{ accessorKey: 'id', header: 'ID', size: 80 },
|
||
{ accessorKey: 'supplier.name', header: 'Supplier', size: 150 },
|
||
{ accessorKey: 'tracking_number', header: 'Tracking #', size: 120 },
|
||
{
|
||
accessorKey: 'arrival_date',
|
||
header: 'Arrival Date',
|
||
size: 120,
|
||
Cell: ({ cell }) => formatDate(cell.getValue<string>() || '', false),
|
||
},
|
||
{
|
||
accessorFn: row => row.files?.length ?? 0,
|
||
id: 'files',
|
||
header: 'Files',
|
||
size: 80,
|
||
},
|
||
{
|
||
accessorKey: 'created_at',
|
||
header: 'Created At',
|
||
size: 150,
|
||
Cell: ({ cell }) => formatDate(cell.getValue<string>() || '', true),
|
||
},
|
||
{
|
||
accessorKey: 'updated_at',
|
||
header: 'Updated At',
|
||
size: 150,
|
||
Cell: ({ cell }) => formatDate(cell.getValue<string>() || '', true),
|
||
},
|
||
], []);
|
||
|
||
useEffect(() => {
|
||
fetchSuppliers();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, [pagination.pageIndex, pagination.pageSize, sorting, globalFilter]);
|
||
|
||
async function fetchSuppliers() {
|
||
try {
|
||
const res = await axios.get('/api/stockBatches/options');
|
||
setSuppliers(res.data.suppliers);
|
||
} catch {
|
||
toast.error('Failed to load suppliers');
|
||
}
|
||
}
|
||
|
||
async function fetchData() {
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await axios.get('/api/stockBatches', {
|
||
params: {
|
||
page: pagination.pageIndex + 1,
|
||
perPage: pagination.pageSize,
|
||
sortField: sorting[0]?.id,
|
||
sortOrder: sorting[0]?.desc ? 'desc' : 'asc',
|
||
filter: globalFilter,
|
||
},
|
||
});
|
||
setData(res.data.data);
|
||
console.log(res.data.data);
|
||
setRowCount(res.data.meta.total);
|
||
} catch {
|
||
toast.error('Failed to load batches');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
|
||
function openModal() {
|
||
dialogRef.current?.showModal();
|
||
}
|
||
function closeModal() {
|
||
dialogRef.current?.close();
|
||
setFormData(defaultFormData);
|
||
setFiles([]);
|
||
}
|
||
|
||
const handleInputChange = (
|
||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||
) => {
|
||
const { name, value } = e.target;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]:
|
||
name === 'supplierId'
|
||
? value
|
||
? parseInt(value)
|
||
: null
|
||
: value,
|
||
}));
|
||
};
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files) {
|
||
setFiles(
|
||
Array.from(e.target.files).map(file => ({
|
||
file,
|
||
fileType: 'other' as const,
|
||
}))
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleFileTypeChange = (
|
||
index: number,
|
||
fileType: FileWithType['fileType']
|
||
) => {
|
||
setFiles(prev =>
|
||
prev.map((f, i) => (i === index ? { ...f, fileType } : f))
|
||
);
|
||
};
|
||
|
||
const removeFile = (index: number) => {
|
||
setFiles(prev => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
const fd = new FormData();
|
||
if (formData.supplierId !== null) {
|
||
fd.append('supplier_id', formData.supplierId.toString());
|
||
}
|
||
if (formData.tracking_number) {
|
||
fd.append('tracking_number', formData.tracking_number);
|
||
}
|
||
if (formData.arrival_date) {
|
||
fd.append('arrival_date', formData.arrival_date);
|
||
}
|
||
files.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');
|
||
closeModal();
|
||
fetchData();
|
||
// inside your catch block…
|
||
} catch (error: any) {
|
||
if (axios.isAxiosError(error)) {
|
||
// if it’s a 422 validation error
|
||
if (error.response?.status === 422 && error.response.data.errors) {
|
||
const errs = error.response.data.errors as Record<string, string[]>;
|
||
// flat-map all the messages and toast each one
|
||
Object.values(errs)
|
||
.flat()
|
||
.forEach(msg => toast.error(msg, {
|
||
duration: 5000,
|
||
}));
|
||
} else {
|
||
// some other HTTP error
|
||
toast.error(error.response?.statusText || 'Unknown error', {
|
||
duration: 5000,
|
||
});
|
||
toast.error(error.response?.data?.errors || '', {
|
||
duration: 5000,
|
||
});
|
||
}
|
||
} else {
|
||
// non-Axios error
|
||
toast.error('Failed to create batch');
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<AppLayout
|
||
title="Stock Batches"
|
||
renderHeader={() => (
|
||
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||
Stock Batches
|
||
</h2>
|
||
)}
|
||
>
|
||
<Head title="Stock Batches" />
|
||
<Toaster
|
||
position="top-center"
|
||
reverseOrder={false}
|
||
/>
|
||
<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 overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||
<div className="mb-4 flex justify-between items-center">
|
||
<h1 className="text-2xl font-bold">Batches</h1>
|
||
<button onClick={openModal} className="btn btn-primary">
|
||
Add New Batch
|
||
</button>
|
||
</div>
|
||
<MaterialReactTable
|
||
columns={columns}
|
||
data={data}
|
||
manualPagination
|
||
manualSorting
|
||
enableGlobalFilter
|
||
onPaginationChange={setPagination}
|
||
onSortingChange={setSorting}
|
||
onGlobalFilterChange={setGlobalFilter}
|
||
rowCount={rowCount}
|
||
state={{ isLoading, pagination, sorting, globalFilter }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<dialog ref={dialogRef} className="modal">
|
||
<form onSubmit={handleSubmit} className="modal-box p-6">
|
||
<button
|
||
type="button"
|
||
onClick={closeModal}
|
||
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={formData.supplierId ?? ''}
|
||
onChange={handleInputChange}
|
||
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={formData.tracking_number}
|
||
onChange={handleInputChange}
|
||
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={formData.arrival_date}
|
||
onChange={handleInputChange}
|
||
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={handleFileChange}
|
||
className="file-input"
|
||
/>
|
||
</div>
|
||
|
||
{files.map((f, idx) => (
|
||
<div key={idx} className="flex items-center mb-2 space-x-2">
|
||
<span>{f.file.name}</span>
|
||
<select
|
||
value={f.fileType}
|
||
onChange={e =>
|
||
handleFileTypeChange(idx, e.target.value as FileWithType['fileType'])
|
||
}
|
||
className="select select-sm"
|
||
>
|
||
<option value="invoice">Invoice</option>
|
||
<option value="label">Label</option>
|
||
<option value="other">Other</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeFile(idx)}
|
||
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>
|
||
</AppLayout>
|
||
);
|
||
}
|