|
- "use client";
-
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
- import {
- Box,
- Typography,
- Card,
- CardContent,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- CircularProgress,
- Button,
- Chip
- } from '@mui/material';
- import { useTranslation } from 'react-i18next';
- import dayjs from 'dayjs';
- import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
- import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
- import { DatePicker } from '@mui/x-date-pickers/DatePicker';
- import { fetchGoodsReceiptStatusClient, type GoodsReceiptStatusRow } from '@/app/api/dashboard/client';
-
- const REFRESH_MS = 15 * 60 * 1000;
-
- const GoodsReceiptStatusNew: React.FC = () => {
- const { t } = useTranslation("dashboard");
- const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs());
- const [data, setData] = useState<GoodsReceiptStatusRow[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const [lastUpdated, setLastUpdated] = useState<dayjs.Dayjs | null>(null);
- const [screenCleared, setScreenCleared] = useState<boolean>(false);
-
- const loadData = useCallback(async () => {
- if (screenCleared) return;
- try {
- setLoading(true);
- const dateParam = selectedDate.format('YYYY-MM-DD');
- const result = await fetchGoodsReceiptStatusClient(dateParam);
- setData(result ?? []);
- setLastUpdated(dayjs());
- } catch (error) {
- console.error('Error fetching goods receipt status:', error);
- setData([]);
- } finally {
- setLoading(false);
- }
- }, [selectedDate, screenCleared]);
-
- useEffect(() => {
- if (screenCleared) return;
- loadData();
-
- const refreshInterval = setInterval(() => {
- loadData();
- }, REFRESH_MS);
-
- return () => clearInterval(refreshInterval);
- }, [loadData, screenCleared]);
-
-
- const selectedDateLabel = useMemo(() => {
- return selectedDate.format('YYYY-MM-DD');
- }, [selectedDate]);
-
- // Sort rows by supplier code alphabetically (A -> Z)
- const sortedData = useMemo(() => {
- return [...data].sort((a, b) => {
- const codeA = (a.supplierCode || '').toUpperCase();
- const codeB = (b.supplierCode || '').toUpperCase();
- if (codeA < codeB) return -1;
- if (codeA > codeB) return 1;
- return 0;
- });
- }, [data]);
-
- const totalStatistics = useMemo(() => {
- // Overall statistics should count ALL POs, including those hidden from the table
- const totalReceived = sortedData.reduce((sum, row) => sum + (row.noOfOrdersReceivedAtDock || 0), 0);
- const totalExpected = sortedData.reduce((sum, row) => sum + (row.expectedNoOfDelivery || 0), 0);
- return { received: totalReceived, expected: totalExpected };
- }, [sortedData]);
-
- type StatusKey = 'pending' | 'receiving' | 'accepted';
-
- const getStatusKey = useCallback((row: GoodsReceiptStatusRow): StatusKey => {
- // Only when the whole PO is processed (all items finished IQC and PO completed)
- // should we treat it as "accepted" (已收貨).
- if (row.noOfOrdersReceivedAtDock === 1) {
- return 'accepted';
- }
-
- // If some items have been inspected or put away but the order is not fully processed,
- // treat as "receiving" / "processing".
- if ((row.noOfItemsInspected ?? 0) > 0 || (row.noOfItemsCompletedPutAwayAtStore ?? 0) > 0) {
- return 'receiving';
- }
-
- // Otherwise, nothing has started yet -> "pending".
- return 'pending';
- }, []);
-
- const renderStatusChip = useCallback((row: GoodsReceiptStatusRow) => {
- const statusKey = getStatusKey(row);
- const label = t(statusKey);
-
- // Color mapping: pending -> red, receiving -> yellow, accepted -> default/green-ish
- const color =
- statusKey === 'pending'
- ? 'error'
- : statusKey === 'receiving'
- ? 'warning'
- : 'success';
-
- return (
- <Chip
- label={label}
- color={color}
- size="small"
- sx={{
- minWidth: 64,
- fontWeight: 500,
- ...(statusKey === 'pending'
- ? {
- bgcolor: 'error.light',
- color: 'common.white',
- }
- : {}),
- }}
- />
- );
- }, [getStatusKey, t]);
-
- if (screenCleared) {
- return (
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <Stack direction="row" spacing={2} justifyContent="space-between" alignItems="center">
- <Typography variant="body2" color="text.secondary">
- {t("Screen cleared")}
- </Typography>
- <Button variant="contained" onClick={() => setScreenCleared(false)}>
- {t("Restore Screen")}
- </Button>
- </Stack>
- </CardContent>
- </Card>
- );
- }
-
- return (
- <Card sx={{ mb: 2 }}>
- <CardContent>
- {/* Header */}
- <Stack direction="row" spacing={2} sx={{ mb: 2 }} alignItems="center" flexWrap="wrap">
- <Stack direction="row" spacing={1} alignItems="center">
- <Typography variant="body2" sx={{ fontWeight: 600 }}>
- {t("Date")}:
- </Typography>
- <LocalizationProvider dateAdapter={AdapterDayjs}>
- <DatePicker
- value={selectedDate}
- onChange={(value) => {
- if (!value) return;
- setSelectedDate(value);
- }}
- slotProps={{
- textField: {
- size: "small",
- sx: { minWidth: 160 }
- }
- }}
- />
- </LocalizationProvider>
- <Typography variant="body2" sx={{ ml: 1 }}>
- 訂單已處理: {totalStatistics.received}/{totalStatistics.expected}
- </Typography>
- </Stack>
-
- <Box sx={{ flexGrow: 1 }} />
-
- <Typography variant="body2" sx={{ color: 'text.secondary' }}>
- {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'}
- </Typography>
-
- <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}>
- {t("Exit Screen")}
- </Button>
- </Stack>
-
- {/* Table */}
- <Box sx={{ mt: 2 }}>
- {loading ? (
- <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <TableContainer component={Paper}>
- <Table
- size="small"
- sx={{
- minWidth: 560,
- tableLayout: 'fixed',
- }}
- >
- <TableHead>
- <TableRow sx={{ backgroundColor: 'grey.100' }}>
- <TableCell sx={{ fontWeight: 600, width: '32%', padding: '4px 6px' }}>{t("Supplier")}</TableCell>
- <TableCell sx={{ fontWeight: 600, width: '30%', padding: '4px 6px' }}>{t("Purchase Order Code")}</TableCell>
- <TableCell sx={{ fontWeight: 600, width: '14%', padding: '4px 4px' }}>{t("Status")}</TableCell>
- <TableCell sx={{ fontWeight: 600, width: '24%', padding: '4px 4px' }} align="right">{t("No. of Items with IQC Issue")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {sortedData.length === 0 ? (
- <TableRow>
- <TableCell colSpan={4} align="center">
- <Typography variant="body2" color="text.secondary">
- {t("No data available")} ({selectedDateLabel})
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- sortedData
- .filter((row) => !row.hideFromDashboard) // hide completed/rejected POs from table only
- .map((row, index) => (
- <TableRow
- key={`${row.supplierId ?? 'na'}-${index}`}
- sx={{
- '&:hover': { backgroundColor: 'grey.50' }
- }}
- >
- <TableCell sx={{ padding: '4px 6px' }}>
- <Box sx={{ display: 'flex', alignItems: 'center' }}>
- <Typography
- component="span"
- variant="body2"
- sx={{
- fontWeight: 500,
- minWidth: '60px'
- }}
- >
- {row.supplierCode || '-'}
- </Typography>
- <Typography
- component="span"
- variant="body2"
- sx={{ color: 'text.secondary',ml: 0.5, mr: 1 }}
- >
- -
- </Typography>
- <Typography
- component="span"
- variant="body2"
- >
- {row.supplierName || '-'}
- </Typography>
- </Box>
- </TableCell>
- <TableCell sx={{ padding: '4px 6px' }}>
- {row.purchaseOrderCode || '-'}
- </TableCell>
- <TableCell sx={{ padding: '4px 4px' }}>
- {renderStatusChip(row)}
- </TableCell>
- <TableCell sx={{ padding: '4px 6px' }} align="right">
- {row.noOfItemsWithIqcIssue ?? 0}
- </TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </Box>
- </CardContent>
- </Card>
- );
- };
-
- export default GoodsReceiptStatusNew;
- 4
|