244 lines
9.8 KiB
TypeScript
244 lines
9.8 KiB
TypeScript
// 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>
|
||
)
|
||
}
|