"use client"; /** * 權限說明(與全站一致): * - 登入後 JWT / session 帶有 `abilities: string[]`(見 config/authConfig、authorities.ts)。 * - 導航「Finished Good Order」等使用 `requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN]`。 * - 本表「撤銷領取 / 強制完成」僅允許具 **ADMIN** 能力者操作(對應 DB `authority.authority = 'ADMIN'`,例如 `user_authority` 對應 id=8 之權限列)。 * - 一般使用者可進入本頁與檢視列表;按鈕會 disabled 並以 Tooltip 提示。 */ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Card, CardContent, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, CircularProgress, TablePagination, Chip, Button, Tooltip, } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useSession } from "next-auth/react"; import dayjs, { Dayjs } from "dayjs"; import { arrayToDayjs } from "@/app/utils/formatUtil"; import { fetchTicketReleaseTable, getTicketReleaseTable } from "@/app/api/do/actions"; 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 { forceCompleteDoPickOrder, revertDoPickOrderAssignment, } from "@/app/api/pickOrder/actions"; import Swal from "sweetalert2"; import { AUTH } from "@/authorities"; import { SessionWithTokens } from "@/config/authConfig"; function isCompletedStatus(status: string | null | undefined): boolean { return (status ?? "").toLowerCase() === "completed"; } /** 已領取(有負責人)的進行中單據才可撤銷或強制完成;未領取不可強制完成 */ function showDoPickOpsButtons(row: getTicketReleaseTable): boolean { return ( row.isActiveDoPickOrder === true && !isCompletedStatus(row.ticketStatus) && row.handledBy != null ); } const FGPickOrderTicketReleaseTable: React.FC = () => { const { t } = useTranslation("ticketReleaseTable"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const abilities = session?.abilities ?? session?.user?.abilities ?? []; // 依照 DB `authority.authority = 'ADMIN'` 的逻辑:仅 abilities 明確包含 ADMIN 才允許操作 // (避免 abilities 裡出現前後空白導致 includes 判斷失效) const canManageDoPickOps = abilities.some((a) => a.trim() === AUTH.ADMIN); const [queryDate, setQueryDate] = useState(() => dayjs()); const [selectedFloor, setSelectedFloor] = useState(""); const [selectedStatus, setSelectedStatus] = useState("released"); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: 5, }); const [now, setNow] = useState(dayjs()); const [lastDataRefreshTime, setLastDataRefreshTime] = useState(null); const formatTime = (timeData: unknown): string => { if (!timeData) return ""; let hour: number; let minute: number; if (typeof timeData === "string") { const parts = timeData.split(":"); hour = parseInt(parts[0], 10); minute = parseInt(parts[1] || "0", 10); } else if (Array.isArray(timeData)) { hour = timeData[0] || 0; minute = timeData[1] || 0; } else { return ""; } const formattedHour = hour.toString().padStart(2, "0"); const formattedMinute = minute.toString().padStart(2, "0"); return `${formattedHour}:${formattedMinute}`; }; const loadData = useCallback(async () => { setLoading(true); try { const dayStr = queryDate.format("YYYY-MM-DD"); const result = await fetchTicketReleaseTable(dayStr, dayStr); setData(result); setLastDataRefreshTime(dayjs()); } catch (error) { console.error("Error fetching ticket release table:", error); } finally { setLoading(false); } }, [queryDate]); useEffect(() => { loadData(); const id = setInterval(loadData, 5 * 60 * 1000); return () => clearInterval(id); }, [loadData]); useEffect(() => { const tick = setInterval(() => setNow(dayjs()), 30 * 1000); return () => clearInterval(tick); }, []); const dayStr = queryDate.format("YYYY-MM-DD"); const filteredData = useMemo(() => { return data.filter((item) => { if (selectedFloor && item.storeId !== selectedFloor) { return false; } if (item.requiredDeliveryDate) { const itemDate = dayjs(item.requiredDeliveryDate).format("YYYY-MM-DD"); if (itemDate !== dayStr) { return false; } } if (selectedStatus && item.ticketStatus?.toLowerCase() !== selectedStatus.toLowerCase()) { return false; } return true; }); }, [data, dayStr, selectedFloor, selectedStatus]); const handlePageChange = useCallback((event: unknown, newPage: number) => { setPaginationController((prev) => ({ ...prev, pageNum: newPage, })); }, []); const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); setPaginationController({ pageNum: 0, pageSize: newPageSize, }); }, []); const paginatedData = useMemo(() => { const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return filteredData.slice(startIndex, endIndex); }, [filteredData, paginationController]); useEffect(() => { setPaginationController((prev) => ({ ...prev, pageNum: 0 })); }, [queryDate, selectedFloor, selectedStatus]); const handleRevert = async (row: getTicketReleaseTable) => { if (!canManageDoPickOps) return; const r = await Swal.fire({ title: t("Confirm revert assignment"), text: t("Revert assignment hint"), icon: "warning", showCancelButton: true, confirmButtonText: t("Confirm"), cancelButtonText: t("Cancel"), }); if (!r.isConfirmed) return; try { const res = await revertDoPickOrderAssignment(row.id); if (res.code === "SUCCESS") { await Swal.fire({ icon: "success", text: t("Operation succeeded"), timer: 1500, showConfirmButton: false }); await loadData(); } else { await Swal.fire({ icon: "error", title: res.code ?? "", text: res.message ?? "" }); } } catch (e) { console.error(e); await Swal.fire({ icon: "error", text: String(e) }); } }; const handleForceComplete = async (row: getTicketReleaseTable) => { if (!canManageDoPickOps) return; const r = await Swal.fire({ title: t("Confirm force complete"), text: t("Force complete hint"), icon: "warning", showCancelButton: true, confirmButtonText: t("Confirm"), cancelButtonText: t("Cancel"), }); if (!r.isConfirmed) return; try { const res = await forceCompleteDoPickOrder(row.id); if (res.code === "SUCCESS") { await Swal.fire({ icon: "success", text: t("Operation succeeded"), timer: 1500, showConfirmButton: false }); await loadData(); } else { await Swal.fire({ icon: "error", title: res.code ?? "", text: res.message ?? "" }); } } catch (e) { console.error(e); await Swal.fire({ icon: "error", text: String(e) }); } }; const opsTooltip = !canManageDoPickOps ? t("Manager only hint") : ""; return ( {t("Ticket Release Table")} v && setQueryDate(v)} slotProps={{ textField: { size: "small", sx: { minWidth: 180 } } }} /> {t("Floor")} {t("Status")} {t("Now")}: {now.format("HH:mm")} {t("Auto-refresh every 5 minutes")} | {t("Last updated")}:{" "} {lastDataRefreshTime ? lastDataRefreshTime.format("HH:mm:ss") : "--:--:--"} {loading ? ( ) : ( <> {t("Store ID")} {t("Required Delivery Date")} {t("Truck Information")} {t("Truck Lane Code")} - {t("Departure Time")} {t("Shop Name")} {t("Loading Sequence")} {t("Ticket Information")} {t("Ticket No.")} ({t("Status")}) {t("Released Time")} - {t("Completed Time")} {t("Handler Name")} {t("Number of FG Items (Order Item(s) Count)")} {t("Actions")} {paginatedData.length === 0 ? ( {t("No data available")} ) : ( paginatedData.map((row) => ( {row.storeId || "-"} {row.requiredDeliveryDate ? dayjs(row.requiredDeliveryDate).format("YYYY-MM-DD") : "-"} {row.truckLanceCode && ( )} {row.truckDepartureTime && ( )} {!row.truckLanceCode && !row.truckDepartureTime && ( - )} {row.shopName || "-"} {row.loadingSequence ?? "-"} {row.ticketNo || "-"} ({row.ticketStatus ? t(row.ticketStatus.toLowerCase()) : "-"}) {t("Released Time")}:{" "} {row.ticketReleaseTime ? (() => { if (Array.isArray(row.ticketReleaseTime)) { return arrayToDayjs(row.ticketReleaseTime, true).format("HH:mm"); } const parsedDate = dayjs(row.ticketReleaseTime, "YYYYMMDDHHmmss"); if (!parsedDate.isValid()) { return dayjs(row.ticketReleaseTime).format("HH:mm"); } return parsedDate.format("HH:mm"); })() : "-"} {t("Completed Time")}:{" "} {row.ticketCompleteDateTime ? (() => { if (Array.isArray(row.ticketCompleteDateTime)) { return arrayToDayjs(row.ticketCompleteDateTime, true).format("HH:mm"); } const parsedDate = dayjs(row.ticketCompleteDateTime, "YYYYMMDDHHmmss"); if (!parsedDate.isValid()) { return dayjs(row.ticketCompleteDateTime).format("HH:mm"); } return parsedDate.format("HH:mm"); })() : "-"} {row.handlerName ?? "-"} {row.numberOfFGItems ?? 0} {showDoPickOpsButtons(row) ? ( ) : ( )} )) )}
{filteredData.length > 0 && ( )} )}
); }; export default FGPickOrderTicketReleaseTable;