|
- 'use client';
-
- import { InventoryResult } from '@/app/api/inventory';
- import { fetchInventoryQtyLots, InventoryQtyLotRow } from '@/app/api/inventory/actions';
- import {
- Box,
- Button,
- CircularProgress,
- Dialog,
- DialogContent,
- DialogTitle,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableRow,
- Typography,
- } from '@mui/material';
- import { useEffect, useRef, useState } from 'react';
- import { useTranslation } from 'react-i18next';
-
- const formatQty = (value: number) => {
- if (!Number.isFinite(value)) return '0';
- return Number.isInteger(value)
- ? value.toLocaleString()
- : value.toLocaleString(undefined, { maximumFractionDigits: 2 });
- };
-
- type Bucket = 'jo' | 'do' | 'other' | 'unavailable';
-
- const reasonLabel = (t: (key: string) => string, row: InventoryQtyLotRow) => {
- const label =
- row.reason === 'expired'
- ? t('Expired lot')
- : row.reason === 'joHold'
- ? t('Jo hold qty')
- : row.reason === 'doHold'
- ? t('Do hold qty')
- : row.reason === 'otherHold' || row.reason === 'onHold'
- ? t('Other hold qty')
- : t('Unavailable lot');
- return row.detail ? `${label}:${row.detail}` : label;
- };
-
- const sumSource = (rows: InventoryQtyLotRow[], source: string) =>
- rows.reduce((sum, row) => (row.bucket === 'onHold' && row.source === source ? sum + Number(row.qty || 0) : sum), 0);
-
- const InventoryQtyStory: React.FC<{ inventory: InventoryResult }> = ({ inventory }) => {
- const { t } = useTranslation('inventory');
- const onHand = Number(inventory.onHandQty ?? 0);
- const onHold = Number(inventory.onHoldQty ?? 0);
- const unavailable = Number(inventory.unavailableQty ?? 0);
- const available = Number(inventory.availableQty ?? onHand - onHold - unavailable);
- const shortUom = inventory.uomShortDesc || '';
- const [open, setOpen] = useState(false);
- const [bucket, setBucket] = useState<Bucket>('unavailable');
- const [loading, setLoading] = useState(false);
- const [rows, setRows] = useState<InventoryQtyLotRow[]>([]);
- const [splitReady, setSplitReady] = useState(false);
- const requestRef = useRef(0);
-
- useEffect(() => {
- setOpen(false);
- setRows([]);
- setSplitReady(onHold === 0);
- if (onHold === 0 || inventory.itemId == null) return;
- const requestId = ++requestRef.current;
- fetchInventoryQtyLots(inventory.itemId)
- .then((res) => {
- if (requestId !== requestRef.current) return;
- setRows(res ?? []);
- setSplitReady(true);
- })
- .catch(() => {
- if (requestId !== requestRef.current) return;
- setRows([]);
- setSplitReady(false);
- });
- }, [inventory.itemId, onHold]);
-
- const joHold = splitReady ? sumSource(rows, 'jo') : 0;
- const doHold = splitReady ? sumSource(rows, 'do') : 0;
- const otherHold = splitReady ? sumSource(rows, 'other') : 0;
-
- const openDetail = (next: Bucket) => {
- const qty =
- next === 'jo' ? joHold : next === 'do' ? doHold : next === 'other' ? otherHold : unavailable;
- if (qty === 0) return;
- setBucket(next);
- setOpen(true);
- if (rows.length > 0 || next !== 'unavailable') {
- setLoading(false);
- return;
- }
- const requestId = ++requestRef.current;
- setLoading(true);
- fetchInventoryQtyLots(inventory.itemId)
- .then((res) => {
- if (requestId !== requestRef.current) return;
- setRows(res ?? []);
- })
- .catch(() => {
- if (requestId !== requestRef.current) return;
- setRows([]);
- })
- .finally(() => {
- if (requestId === requestRef.current) setLoading(false);
- });
- };
-
- const visibleRows = rows.filter((row) =>
- bucket === 'unavailable' ? row.bucket === 'unavailable' : row.bucket === 'onHold' && row.source === bucket,
- );
- const title =
- bucket === 'jo'
- ? t('Jo hold qty')
- : bucket === 'do'
- ? t('Do hold qty')
- : bucket === 'other'
- ? t('Other hold qty')
- : t('Unavailable Qty');
-
- return (
- <Box sx={{ py: 1 }}>
- <Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', columnGap: 0.75, rowGap: 0.5 }}>
- <Typography variant="body2" component="span">
- {`${t('On Hand Qty')} ${formatQty(onHand)} −`}
- </Typography>
- {splitReady ? (
- <>
- <QtyLink
- label={`${t('Jo hold qty')} ${formatQty(joHold)}`}
- enabled={joHold !== 0}
- onClick={() => openDetail('jo')}
- />
- <Typography variant="body2" component="span">−</Typography>
- <QtyLink
- label={`${t('Do hold qty')} ${formatQty(doHold)}`}
- enabled={doHold !== 0}
- onClick={() => openDetail('do')}
- />
- {otherHold !== 0 && (
- <>
- <Typography variant="body2" component="span">−</Typography>
- <QtyLink
- label={`${t('Other hold qty')} ${formatQty(otherHold)}`}
- enabled
- onClick={() => openDetail('other')}
- />
- </>
- )}
- </>
- ) : (
- <QtyLink
- label={`${t('On Hold Qty')} ${formatQty(onHold)}`}
- enabled={false}
- onClick={() => undefined}
- />
- )}
- <Typography variant="body2" component="span">−</Typography>
- <QtyLink
- label={`${t('Unavailable Qty')} ${formatQty(unavailable)}`}
- enabled={unavailable !== 0}
- onClick={() => openDetail('unavailable')}
- />
- <Typography variant="body2" component="span">
- {'= '}
- <Box component="span" sx={{ fontWeight: 700 }}>
- {`${t('Available Qty')} ${formatQty(available)}${shortUom}`}
- </Box>
- </Typography>
- </Box>
- <Dialog open={open} onClose={() => setOpen(false)} maxWidth="md" fullWidth>
- <DialogTitle>{`${title} · ${t('Qty lot detail')}`}</DialogTitle>
- <DialogContent>
- {loading ? (
- <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
- <CircularProgress size={28} />
- </Box>
- ) : visibleRows.length === 0 ? (
- <Typography sx={{ py: 2 }}>{t('No qty lots')}</Typography>
- ) : (
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell>{t('Lot No')}</TableCell>
- <TableCell>{t('Expiry Date')}</TableCell>
- <TableCell align="right">{t('Qty')}</TableCell>
- <TableCell>{t('Warehouse')}</TableCell>
- <TableCell>{t('Why')}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {visibleRows.map((row) => (
- <TableRow key={`${row.bucket}-${row.source}-${row.lotLineId}-${row.detail}`}>
- <TableCell sx={{ whiteSpace: 'nowrap' }}>{row.lotNo}</TableCell>
- <TableCell sx={{ whiteSpace: 'nowrap' }}>{row.expiryDate}</TableCell>
- <TableCell align="right" sx={{ whiteSpace: 'nowrap' }}>{formatQty(row.qty)}</TableCell>
- <TableCell sx={{ whiteSpace: 'nowrap' }}>{row.warehouseCode}</TableCell>
- <TableCell>{reasonLabel(t, row)}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- )}
- </DialogContent>
- </Dialog>
- </Box>
- );
- };
-
- const QtyLink = ({
- label,
- enabled,
- onClick,
- }: {
- label: string;
- enabled: boolean;
- onClick: () => void;
- }) => {
- if (!enabled) {
- return (
- <Typography variant="body2" component="span">
- {label}
- </Typography>
- );
- }
- return (
- <Button
- variant="text"
- onClick={onClick}
- sx={{
- minWidth: 0,
- p: 0,
- textTransform: 'none',
- fontWeight: 400,
- fontSize: '0.875rem',
- lineHeight: 1.43,
- verticalAlign: 'baseline',
- }}
- >
- {label}
- </Button>
- );
- };
-
- export default InventoryQtyStory;
|