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

244 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// resources/js/Pages/BatchCounting.tsx
import React, { useMemo, useState } from 'react'
import { Head, usePage } from '@inertiajs/react'
import AppLayout from '@/Layouts/AppLayout'
import axios from 'axios'
import { toast, Toaster } from 'react-hot-toast'
import {
MaterialReactTable,
type MRT_ColumnDef,
} from 'material-react-table'
import { StockEntry, StockBatch } from '@/types'
const statusMapping = {
CORRECT: 2, // NEW_GOODS_COUNTED
MISSING_ITEMS: 7, // NEW_GOODS_MISSING
BROKEN_ITEMS: 6, // NEW_GOODS_DAMAGED
}
export default function BatchCounting() {
const { selectedBatch } = usePage<{ selectedBatch: StockBatch }>().props
const [entries, setEntries] = useState<StockEntry[]>(selectedBatch.stock_entries)
// pin items without any of (2,6,7) to top
const data = useMemo(() => {
const noStatus: StockEntry[] = []
const withStatus: StockEntry[] = []
selectedBatch.stock_entries.forEach((e) => {
const has = e.status_history.some(h =>
[2, 6, 7].includes(h.stock_entries_status_id)
)
if (has) withStatus.push(e)
else noStatus.push(e)
})
return [...noStatus, ...withStatus]
}, [selectedBatch.stock_entries])
const getLatestRelevant = (history: any[]) => {
const relevant = history
.filter(h => [2, 6, 7].includes(h.stock_entries_status_id))
.sort(
(a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
return relevant.length ? relevant[relevant.length - 1] : null;
};
const columns = useMemo<MRT_ColumnDef<StockEntry>[]>(() => [
{
accessorKey: 'status',
header: 'Status',
size: 60,
Cell: ({ row }) => {
const latest = getLatestRelevant(row.original.status_history);
if (!latest) return null;
switch (latest.stock_entries_status_id) {
case 2:
return <span className="text-green-600"></span>;
case 6:
return <span className="text-orange-600"></span>;
case 7:
return <span className="text-red-600"></span>;
default:
return null;
}
},
},
{ accessorFn: e => e.id, header: 'ID' },
{ accessorFn: e => e.physical_item.name, header: 'Item' },
{ accessorFn: e => e.supplier.name, header: 'Supplier' },
{ accessorFn: e => e.quantity, header: 'Qty' },
], [])
// modal state
const [modalOpen, setModalOpen] = useState(false)
const [selectedEntry, setSelectedEntry] = useState<StockEntry | null>(null)
const [selectedStatus, setSelectedStatus] = useState<keyof typeof statusMapping | null>(null)
const [count, setCount] = useState<number>(0)
const openModal = (entry: StockEntry) => {
setSelectedEntry(entry)
setSelectedStatus(null)
setCount(0)
setModalOpen(true)
}
const submitStatus = async () => {
if (!selectedEntry || !selectedStatus) return
try {
const response = await axios.post(`/api/stockActions/${selectedEntry.id}/status`, {
status: statusMapping[selectedStatus],
count: selectedStatus === 'CORRECT' ? undefined : count,
})
const newHist: typeof selectedEntry.status_history[0] = response.data.history
// 2. Append the new history into our entries state
setEntries(prev =>
prev.map((e) =>
e.id === selectedEntry.id
? { ...e, status_history: [...e.status_history, newHist] }
: e
)
)
toast.success('Status updated successfully!')
setModalOpen(false)
} catch (error: any) {
console.error('Failed to update status:', error)
toast.error(
error.response?.data?.message ||
'Failed to update status. Please try again.'
)
}
}
return (
<AppLayout
title="Stock Entries"
renderHeader={() => (
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Stock Entries
</h2>
)}
>
<Head title="Batch counting" />
<Toaster position="top-center" />
<div className="p-4">
<h3 className="text-lg font-medium mb-4">
Batch: {selectedBatch.name}
</h3>
<MaterialReactTable
columns={columns}
data={[
...entries.filter(e => !e.status_history.some(h => [2,6,7].includes(h.stock_entries_status_id))),
...entries.filter(e => e.status_history.some(h => [2,6,7].includes(h.stock_entries_status_id))),
]}
enableRowSelection={false}
muiTableBodyRowProps={({ row }) => {
const latest = getLatestRelevant(row.original.status_history);
let bgColor: string | undefined;
if (latest) {
switch (latest.stock_entries_status_id) {
case 2:
bgColor = 'rgba(220, 253, 213, 0.5)'; // green-50
break;
case 6:
bgColor = 'rgba(255, 247, 237, 0.5)'; // orange-50
break;
case 7:
bgColor = 'rgba(254, 226, 226, 0.5)'; // red-50
break;
}
}
return {
onClick: () => openModal(row.original),
sx: {
cursor: 'pointer',
backgroundColor: bgColor,
},
};
}}
/>
{modalOpen && selectedEntry && (
<div className="modal modal-open">
<div className="modal-box">
<h3 className="font-bold text-lg">
Update "{selectedEntry.physical_item.name}"
</h3>
<div className="grid grid-cols-3 gap-2 my-4">
<button
className={`btn btn-success ${
selectedStatus === 'CORRECT' ? '' : 'btn-outline'
}`}
onClick={() => { setSelectedStatus('CORRECT'); setCount(0) }}
>
CORRECT
</button>
<button
className={`btn btn-error ${
selectedStatus === 'MISSING_ITEMS' ? '' : 'btn-outline'
}`}
onClick={() => setSelectedStatus('MISSING_ITEMS')}
>
MISSING ITEMS
</button>
<button
className={`btn btn-warning ${
selectedStatus === 'BROKEN_ITEMS' ? '' : 'btn-outline'
}`}
onClick={() => setSelectedStatus('BROKEN_ITEMS')}
>
BROKEN ITEMS
</button>
</div>
{selectedStatus && selectedStatus !== 'CORRECT' && (
<div className="form-control mb-4">
<label className="label">
<span className="label-text">Count</span>
</label>
<input
type="number"
className="input input-bordered"
value={count}
min={0}
onChange={e => setCount(Number(e.target.value))}
/>
</div>
)}
<div className="modal-action">
<button
className="btn btn-primary"
disabled={
!selectedStatus ||
(selectedStatus !== 'CORRECT' && count <= 0)
}
onClick={submitStatus}
>
Submit
</button>
<button
className="btn"
onClick={() => setModalOpen(false)}
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
</AppLayout>
)
}