210 lines
8.5 KiB
TypeScript
210 lines
8.5 KiB
TypeScript
// components/BatchInfoWindow.tsx
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
MaterialReactTable,
|
|
type MRT_ColumnDef,
|
|
type MRT_PaginationState,
|
|
type MRT_SortingState,
|
|
} from 'material-react-table'
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
import { faQrcode, faListOl } from '@fortawesome/free-solid-svg-icons'
|
|
import { NextRouter } from 'next/router'
|
|
import { StockBatch, StockEntry, Supplier } from '@/types'
|
|
import axios from 'axios'
|
|
import { toast } from 'react-hot-toast'
|
|
import { size } from 'lodash'
|
|
|
|
import {downloadBatchBarcode} from "@/functions/functions"
|
|
|
|
interface Status {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
interface BatchInfoWindowProps {
|
|
selectedBatch: StockBatch | null
|
|
formatDate?: (value: string, withTime?: boolean) => string
|
|
router: NextRouter
|
|
route: (name: string, params: Record<string, any>) => string
|
|
|
|
/** We now allow `entries` to be null, but internally we coalesce to [] */
|
|
entries: StockEntry[] | null
|
|
|
|
/** Any other props you had (for pagination / sorting, etc.) */
|
|
entryColumns?: MRT_ColumnDef<StockEntry>[]
|
|
entriesCount?: number
|
|
entriesLoading?: boolean
|
|
entriesPagination?: MRT_PaginationState
|
|
entriesSorting?: MRT_SortingState
|
|
entriesFilter?: string
|
|
setEntriesPagination?: (updater: MRT_PaginationState) => void
|
|
setEntriesSorting?: (sorter: MRT_SortingState) => void
|
|
setEntriesFilter?: (filter: string) => void
|
|
|
|
openEntry?: (entry: StockEntry) => void
|
|
recountEnabled?: boolean
|
|
}
|
|
|
|
const defaultFormatter = (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 statusIds = [2, 6, 7, 8]
|
|
|
|
export default function BatchInfoWindow({
|
|
selectedBatch,
|
|
formatDate = defaultFormatter,
|
|
router,
|
|
route,
|
|
openEntry,
|
|
recountEnabled
|
|
}: BatchInfoWindowProps) {
|
|
const [statuses, setStatuses] = useState<Status[]>([])
|
|
/** Always keep an array here (never null). */
|
|
|
|
/** Fetch statuses once */
|
|
useEffect(() => {
|
|
console.log(selectedBatch);
|
|
axios
|
|
.get('/api/stockStatusList')
|
|
.then((res) => setStatuses(res.data.statuses))
|
|
.catch(() => toast.error('Failed to load status list'))
|
|
}, [])
|
|
|
|
/** Example helper to calculate ratios */
|
|
function calculateStatusRatio(entries: StockEntry[], statusId: number) {
|
|
const total = entries.length
|
|
const count = entries.filter((e) =>
|
|
e.status_history?.some((h: any) => h.stock_entries_status_id === statusId)
|
|
).length
|
|
const ratio = total > 0 ? count / total : 0
|
|
return { count, total, ratio }
|
|
}
|
|
|
|
const entryColumnsMemo = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
|
() => [
|
|
{ accessorKey: 'id', header: 'ID', size: 80 },
|
|
{ accessorKey: 'physical_item.name', header: 'Physical Item', size: 200 },
|
|
{ accessorKey: 'supplier.name', header: 'Supplier', size: 150 },
|
|
{
|
|
accessorKey: 'price',
|
|
header: 'Price',
|
|
size: 100,
|
|
Cell: ({ cell }) => cell.getValue<number>()?.toFixed(2) || '-',
|
|
},
|
|
// …add the rest of your columns here…
|
|
],
|
|
[]
|
|
)
|
|
|
|
const hasNonCountedStatus = selectedBatch?.stock_entries?.some((stockEntry) => {
|
|
const nullSectionStatuses =
|
|
stockEntry.status_history?.filter((h: any) => h.section_id === null) ?? []
|
|
if (nullSectionStatuses.length === 0) return true
|
|
const latest = nullSectionStatuses.reduce((prev, curr) =>
|
|
new Date(prev.created_at) > new Date(curr.created_at) ? prev : curr
|
|
)
|
|
return latest.stock_entries_status_id !== 2
|
|
})
|
|
|
|
return (
|
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:space-x-4">
|
|
{/* Left: Batch Details & Ratios */}
|
|
<div className="sm:w-1/3 flex flex-col justify-between">
|
|
<div>
|
|
<h3 className="font-bold text-lg mb-2">Batch Details</h3>
|
|
|
|
{selectedBatch ? (
|
|
<ul className="list-disc list-inside">
|
|
<li>
|
|
<strong>ID:</strong> {selectedBatch.id}
|
|
</li>
|
|
<li>
|
|
<strong>Supplier:</strong> {selectedBatch.supplier.name}
|
|
</li>
|
|
<li>
|
|
<strong>Tracking #:</strong> {selectedBatch.tracking_number}
|
|
</li>
|
|
<li>
|
|
<strong>Arrival:</strong>{' '}
|
|
{formatDate(selectedBatch.arrival_date, false)}
|
|
</li>
|
|
</ul>
|
|
) : (
|
|
<p className="text-sm text-gray-500">No batch selected.</p>
|
|
)}
|
|
|
|
{selectedBatch && (
|
|
<div style={{ marginTop: 24 }}>
|
|
<h3 className="font-semibold text-md mb-2">Status Ratios</h3>
|
|
{statusIds.map((id) => {
|
|
const { count, total, ratio } = calculateStatusRatio(
|
|
selectedBatch.stock_entries,
|
|
id
|
|
)
|
|
const statusData = statuses.find((s) => s.id === id)
|
|
return (
|
|
<p key={id} className="text-sm">
|
|
{statusData?.name}: {count} / {total} (
|
|
{(ratio * 100).toFixed(1)}%)
|
|
</p>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-4 flex space-x-2">
|
|
<div className="tooltip" data-tip="Batch QR kód">
|
|
<button className="btn bg-[#666666] text-[19px]" onClick={() => downloadBatchBarcode(selectedBatch?.id)}>
|
|
<FontAwesomeIcon icon={faQrcode} />
|
|
</button>
|
|
</div>
|
|
|
|
{hasNonCountedStatus && selectedBatch && recountEnabled && (
|
|
<div className="tooltip" data-tip="Přepočítat">
|
|
<button
|
|
className="btn btn-warning text-[19px]"
|
|
onClick={() =>
|
|
router.get(
|
|
route('batchCounting', { selectedBatch: selectedBatch.id })
|
|
)
|
|
}
|
|
>
|
|
<FontAwesomeIcon icon={faListOl} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Stock Entries Table (never pass null!) */}
|
|
<div className="sm:w-2/3 flex flex-col sm:pl-6">
|
|
<h3 className="font-bold text-lg mb-2">Stock Entries</h3>
|
|
<div className="flex-1 overflow-auto">
|
|
<MaterialReactTable
|
|
columns={entryColumnsMemo}
|
|
data={selectedBatch?.stock_entries} // ← guaranteed array, never null
|
|
manualPagination
|
|
manualSorting
|
|
enableGlobalFilter
|
|
initialState={{ pagination: { pageSize: 100 } }}
|
|
muiTableBodyRowProps={({ row }) => ({
|
|
onClick: () => openEntry?.(row.original),
|
|
style: { cursor: 'pointer' },
|
|
})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|