|
- "use client";
- import React, { useState, useEffect, useCallback, useRef } from "react";
- import {
- Box,
- Typography,
- Card,
- CardContent,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- CircularProgress,
- Stack,
- IconButton,
- Collapse,
- Link as MuiLink,
- } from "@mui/material";
- import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
- import ExpandLessIcon from "@mui/icons-material/ExpandLess";
- import NextLink from "next/link";
- import { useTranslation } from "react-i18next";
- import dayjs from "dayjs";
- import type { Dayjs } from "dayjs";
- import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
- import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
- import {
- fetchDrinkProductionQty,
- DrinkProductionQtyResponse,
- } from "@/app/api/jo/actions";
-
- const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 分鐘
-
- const formatQty = (qty: number | null | undefined): string => {
- if (qty === null || qty === undefined || Number.isNaN(qty)) return "-";
- return qty.toLocaleString(undefined, { maximumFractionDigits: 2 });
- };
-
- const formatProductionDate = (value: string | null | undefined): string => {
- if (!value) return "-";
- const parsed = dayjs(value);
- return parsed.isValid() ? parsed.format("YYYY-MM-DD") : value;
- };
-
- const getRowKey = (row: DrinkProductionQtyResponse, idx: number): string =>
- `${row.itemCode || "unknown"}-${idx}`;
-
- const DrinkProductionQtyDashboard: React.FC = () => {
- const { t } = useTranslation(["common", "jo", "productionProcess"]);
- const [data, setData] = useState<DrinkProductionQtyResponse[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
- const [expandedRowKeys, setExpandedRowKeys] = useState<Set<string>>(
- new Set(),
- );
- const refreshCountRef = useRef<number>(0);
- const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>(
- null,
- );
-
- const loadData = useCallback(async () => {
- setLoading(true);
- try {
- const result = await fetchDrinkProductionQty(
- selectedDate.format("YYYY-MM-DD"),
- );
- setData(result || []);
- setExpandedRowKeys(new Set());
- setLastDataRefreshTime(dayjs());
- refreshCountRef.current += 1;
- } catch (error) {
- console.error("Error fetching drink production qty:", error);
- setData([]);
- setExpandedRowKeys(new Set());
- } finally {
- setLoading(false);
- }
- }, [selectedDate]);
-
- useEffect(() => {
- loadData();
- const interval = setInterval(() => {
- loadData();
- }, REFRESH_INTERVAL);
- return () => clearInterval(interval);
- }, [loadData]);
-
- const toggleRowExpanded = (rowKey: string) => {
- setExpandedRowKeys((prev) => {
- const next = new Set(prev);
- if (next.has(rowKey)) {
- next.delete(rowKey);
- } else {
- next.add(rowKey);
- }
- return next;
- });
- };
-
- return (
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
- {t("Drink Production Qty Dashboard")}
- </Typography>
-
- <Stack direction="row" spacing={2} sx={{ mb: 3, alignItems: "center" }}>
- <LocalizationProvider dateAdapter={AdapterDayjs}>
- <DatePicker
- label={t("Date")}
- value={selectedDate}
- onChange={(newValue) => {
- if (newValue) setSelectedDate(newValue);
- }}
- format="YYYY-MM-DD"
- slotProps={{
- textField: { size: "small", sx: { minWidth: 160 } },
- }}
- />
- </LocalizationProvider>
-
- <Box sx={{ flexGrow: 1 }} />
-
- <Typography
- variant="body2"
- sx={{ color: "text.secondary" }}
- suppressHydrationWarning
- >
- {t("Auto-refresh every 10 minutes")} |
- {t("Last updated")}:{" "}
- {lastDataRefreshTime
- ? lastDataRefreshTime.format("HH:mm:ss")
- : "--:--:--"}
- </Typography>
- </Stack>
-
- {loading ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <TableContainer
- sx={{
- border: "3px solid #135fed",
- overflowX: "auto",
- maxHeight: 440,
- overflow: "auto",
- }}
- component={Paper}
- >
- <Table size="small" sx={{ minWidth: 650 }}>
- <TableHead>
- <TableRow
- sx={{
- bgcolor: "#424242",
- "& th": {
- borderBottom: "none",
- py: 1.5,
- position: "sticky",
- top: 0,
- zIndex: 1,
- },
- }}
- >
- <TableCell sx={{ width: 48 }} />
- <TableCell sx={{ width: 160 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Item Code")}
- </Typography>
- </TableCell>
- <TableCell sx={{ width: 260 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Goods Name")}
- </Typography>
- </TableCell>
- <TableCell sx={{ width: 120 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Unit")}
- </Typography>
- </TableCell>
- <TableCell align="right" sx={{ width: 120 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Stock Req. Qty")}
- </Typography>
- </TableCell>
- <TableCell align="right" sx={{ width: 140 }}>
- <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
- {t("Production Qty")}
- </Typography>
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {data.length === 0 ? (
- <TableRow>
- <TableCell colSpan={6} align="center">
- <Typography
- variant="body2"
- sx={{ py: 2, color: "text.secondary" }}
- >
- {t("No data available")}
- </Typography>
- </TableCell>
- </TableRow>
- ) : (
- data.map((row, idx) => {
- const rowKey = getRowKey(row, idx);
- const jobOrders = row.jobOrders ?? [];
- const isExpanded = expandedRowKeys.has(rowKey);
- const hasJobOrders = jobOrders.length > 0;
-
- return (
- <React.Fragment key={rowKey}>
- <TableRow hover={hasJobOrders}>
- <TableCell padding="checkbox">
- {hasJobOrders ? (
- <IconButton
- size="small"
- aria-label={
- isExpanded
- ? t("Collapse job order details")
- : t("Expand job order details")
- }
- onClick={() => toggleRowExpanded(rowKey)}
- >
- {isExpanded ? (
- <ExpandLessIcon fontSize="small" />
- ) : (
- <ExpandMoreIcon fontSize="small" />
- )}
- </IconButton>
- ) : null}
- </TableCell>
- <TableCell>
- <Typography variant="body2">
- {row.itemCode || "-"}
- </Typography>
- </TableCell>
- <TableCell>
- <Typography variant="body2">
- {row.itemName || "-"}
- </Typography>
- </TableCell>
- <TableCell>
- <Typography variant="body2">
- {row.uom || "-"}
- </Typography>
- </TableCell>
- <TableCell align="right">
- <Typography variant="body2">
- {formatQty(row.totalReqQty)}
- </Typography>
- </TableCell>
- <TableCell align="right">
- <Typography variant="body2">
- {formatQty(row.totalQty)}
- </Typography>
- </TableCell>
- </TableRow>
- {hasJobOrders && (
- <TableRow>
- <TableCell
- colSpan={6}
- sx={{ py: 0, borderBottom: 0 }}
- >
- <Collapse
- in={isExpanded}
- timeout="auto"
- unmountOnExit
- >
- <Box sx={{ py: 1.5, pl: 6, pr: 2 }}>
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell sx={{ fontWeight: 600 }}>
- {t("Job Order Code")}
- </TableCell>
- <TableCell sx={{ fontWeight: 600 }}>
- {t("Production Date")}
- </TableCell>
- <TableCell
- align="right"
- sx={{ fontWeight: 600 }}
- >
- {t("Stock Req. Qty")}
- </TableCell>
- <TableCell
- align="right"
- sx={{ fontWeight: 600 }}
- >
- {t("Production Qty")}
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {jobOrders.map((jo) => (
- <TableRow
- key={`${rowKey}-jo-${jo.jobOrderId}`}
- >
- <TableCell>
- {jo.jobOrderId > 0 ? (
- <MuiLink
- component={NextLink}
- href={`/jo/edit?id=${jo.jobOrderId}`}
- underline="hover"
- >
- {jo.jobOrderCode ||
- `JO-${jo.jobOrderId}`}
- </MuiLink>
- ) : (
- jo.jobOrderCode || "-"
- )}
- </TableCell>
- <TableCell>
- {formatProductionDate(
- jo.productionDate,
- )}
- </TableCell>
- <TableCell align="right">
- {formatQty(jo.reqQty)}
- </TableCell>
- <TableCell align="right">
- {formatQty(jo.productionQty)}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </Box>
- </Collapse>
- </TableCell>
- </TableRow>
- )}
- </React.Fragment>
- );
- })
- )}
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </CardContent>
- </Card>
- );
- };
-
- export default DrinkProductionQtyDashboard;
|