Bläddra i källkod

revert/ just complete do pick order

MergeProblem1
CANCERYS\kw093 10 timmar sedan
förälder
incheckning
9bcfe9c380
8 ändrade filer med 572 tillägg och 346 borttagningar
  1. +2
    -0
      src/app/api/do/actions.tsx
  2. +56
    -1
      src/app/api/pickOrder/actions.ts
  3. +380
    -300
      src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx
  4. +24
    -4
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  5. +81
    -39
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  6. +12
    -1
      src/components/SearchBox/SearchBox.tsx
  7. +3
    -0
      src/i18n/zh/pickOrder.json
  8. +14
    -1
      src/i18n/zh/ticketReleaseTable.json

+ 2
- 0
src/app/api/do/actions.tsx Visa fil

@@ -134,6 +134,8 @@ export interface getTicketReleaseTable {
requiredDeliveryDate: string | null;
handlerName: string | null;
numberOfFGItems: number;
/** 進行中 do_pick_order 為 true,才可呼叫 force-complete / revert-assignment(id 為 do_pick_order 主鍵) */
isActiveDoPickOrder?: boolean;
}

export interface TruckScheduleDashboardItem {


+ 56
- 1
src/app/api/pickOrder/actions.ts Visa fil

@@ -377,6 +377,8 @@ export interface CompletedDoPickOrderSearchParams {
targetDate?: string;
shopName?: string;
deliveryNoteCode?: string;
/** 卡車/車道(後端 truckLanceCode 模糊匹配) */
truckLanceCode?: string;
}
export interface PickExecutionIssue {
id: number;
@@ -670,7 +672,10 @@ export const fetchCompletedDoPickOrders = async (
if (searchParams?.targetDate) {
params.append('targetDate', searchParams.targetDate);
}
if (searchParams?.truckLanceCode) {
params.append("truckLanceCode", searchParams.truckLanceCode);
}

const queryString = params.toString();
const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders/${userId}${queryString ? `?${queryString}` : ''}`;
@@ -680,6 +685,56 @@ export const fetchCompletedDoPickOrders = async (
return response;
};

/** 全部已完成 DO 提貨記錄(不限經手人),需後端 `/completed-do-pick-orders-all` */
export const fetchCompletedDoPickOrdersAll = async (
searchParams?: CompletedDoPickOrderSearchParams
): Promise<CompletedDoPickOrderResponse[]> => {
const params = new URLSearchParams();

if (searchParams?.deliveryNoteCode) {
params.append("deliveryNoteCode", searchParams.deliveryNoteCode);
}
if (searchParams?.shopName) {
params.append("shopName", searchParams.shopName);
}
if (searchParams?.targetDate) {
params.append("targetDate", searchParams.targetDate);
}
if (searchParams?.truckLanceCode) {
params.append("truckLanceCode", searchParams.truckLanceCode);
}

const queryString = params.toString();
const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders-all${queryString ? `?${queryString}` : ""}`;

const response = await serverFetchJson<CompletedDoPickOrderResponse[]>(url, {
method: "GET",
});

return response;
};

/** 強制完成進行中的 do_pick_order(僅改狀態並歸檔,不調整揀貨數量) */
export const forceCompleteDoPickOrder = async (
doPickOrderId: number,
): Promise<PostPickOrderResponse> => {
return serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/doPickOrder/force-complete/${doPickOrderId}`,
{ method: "POST", headers: { "Content-Type": "application/json" } },
);
};

/** 撤銷使用者領取,可再次分配 */
export const revertDoPickOrderAssignment = async (
doPickOrderId: number,
): Promise<PostPickOrderResponse> => {
return serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/doPickOrder/revert-assignment/${doPickOrderId}`,
{ method: "POST", headers: { "Content-Type": "application/json" } },
);
};

export const updatePickOrderHideStatus = async (pickOrderId: number, hide: boolean) => {
const response = await serverFetchJson<UpdateDoPickOrderHideStatusRequest>(
`${BASE_API_URL}/pickOrder/update-hide-status/${pickOrderId}?hide=${hide}`,


+ 380
- 300
src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx Visa fil

@@ -1,6 +1,14 @@
"use client";

import React, { useState, useEffect, useCallback, useMemo } from 'react';
/**
* 權限說明(與全站一致):
* - 登入後 JWT / session 帶有 `abilities: string[]`(見 config/authConfig、authorities.ts)。
* - 導航「Finished Good Order」等使用 `requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN]`。
* - 本表「撤銷領取 / 強制完成」僅允許具 **ADMIN** 能力者操作(專案內以 ADMIN 作為管理員層級權限)。
* - 一般使用者可進入本頁與檢視列表;按鈕會 disabled 並以 Tooltip 提示。
*/

import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Box,
Typography,
@@ -20,16 +28,46 @@ import {
Paper,
CircularProgress,
TablePagination,
Chip
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { arrayToDayjs } from '@/app/utils/formatUtil';
import { fetchTicketReleaseTable, getTicketReleaseTable } from '@/app/api/do/actions';
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 [selectedDate, setSelectedDate] = useState<string>("today");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const abilities = session?.abilities ?? session?.user?.abilities ?? [];
const canManageDoPickOps = abilities.includes(AUTH.ADMIN);

const [queryDate, setQueryDate] = useState<Dayjs>(() => dayjs());
const [selectedFloor, setSelectedFloor] = useState<string>("");
const [selectedStatus, setSelectedStatus] = useState<string>("released");

@@ -41,89 +79,77 @@ const FGPickOrderTicketReleaseTable: React.FC = () => {
});

const [now, setNow] = useState(dayjs());
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
const formatTime = (timeData: any): string => {
if (!timeData) return '';
const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);

const formatTime = (timeData: unknown): string => {
if (!timeData) return "";

let hour: number;
let minute: number;
if (typeof timeData === 'string') {

const parts = timeData.split(':');
if (typeof timeData === "string") {
const parts = timeData.split(":");
hour = parseInt(parts[0], 10);
minute = parseInt(parts[1] || '0', 10);
minute = parseInt(parts[1] || "0", 10);
} else if (Array.isArray(timeData)) {

hour = timeData[0] || 0;
minute = timeData[1] || 0;
}
else {
return '';
} else {
return "";
}
const formattedHour = hour.toString().padStart(2, '0');
const formattedMinute = minute.toString().padStart(2, '0');
return `${formattedHour}:${formattedMinute}`;
};
const getDateLabel = (offset: number) => {
return dayjs().add(offset, 'day').format('YYYY-MM-DD');
};

const getDateRange = () => {
const today = dayjs().format('YYYY-MM-DD');
const dayAfterTomorrow = dayjs().add(2, 'day').format('YYYY-MM-DD');
return { startDate: today, endDate: dayAfterTomorrow };
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 { startDate, endDate } = getDateRange();
const result = await fetchTicketReleaseTable(startDate, endDate);
setData(result);
setLastDataRefreshTime(dayjs());
} catch (error) {
console.error('Error fetching ticket release table:', error);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
loadData();
const id = setInterval(loadData, 5 * 60 * 1000);
return () => clearInterval(id);
}, [loadData]);

const filteredData = data.filter((item) => {
// Filter by floor if selected
if (selectedFloor && item.storeId !== selectedFloor) {
return false;
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]);

// Filter by date if selected
if (selectedDate && item.requiredDeliveryDate) {
const itemDate = dayjs(item.requiredDeliveryDate).format('YYYY-MM-DD');
const targetDate = getDateLabel(
selectedDate === "today" ? 0 : selectedDate === "tomorrow" ? 1 : 2
);
if (itemDate !== targetDate) {
return false;
}
}
useEffect(() => {
loadData();
const id = setInterval(loadData, 5 * 60 * 1000);
return () => clearInterval(id);
}, [loadData]);

// Filter by status if selected
if (selectedStatus && item.ticketStatus?.toLowerCase() !== selectedStatus.toLowerCase()) {
return false;
}
useEffect(() => {
const tick = setInterval(() => setNow(dayjs()), 30 * 1000);
return () => clearInterval(tick);
}, []);

const dayStr = queryDate.format("YYYY-MM-DD");

return true;
},[data, selectedDate, selectedFloor, selectedStatus]);
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 => ({
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController((prev) => ({
...prev,
pageNum: newPage,
}));
@@ -144,265 +170,319 @@ useEffect(() => {
}, [filteredData, paginationController]);

useEffect(() => {
setPaginationController(prev => ({ ...prev, pageNum: 0 }));
}, [selectedDate, selectedFloor, selectedStatus]);
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 (
<Card sx={{ mb: 2 }}>
<CardContent>
{/* Title */}
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
{t("Ticket Release Table")}
</Typography>

{/* Dropdown Menus */}
<Stack direction="row" spacing={2} sx={{ mb: 3, flexWrap: 'wrap' }}>
<FormControl sx={{ minWidth: 250 }} size="small">
<InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
<Select
labelId="date-select-label"
id="date-select"
value={selectedDate}
label={t("Select Date")}
onChange={(e) => setSelectedDate(e.target.value)}
>
<MenuItem value="today">
{t("Today")} ({getDateLabel(0)})
</MenuItem>
<MenuItem value="tomorrow">
{t("Tomorrow")} ({getDateLabel(1)})
</MenuItem>
<MenuItem value="dayAfterTomorrow">
{t("Day After Tomorrow")} ({getDateLabel(2)})
</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel
id="floor-select-label"
shrink={true}
>
{t("Floor")}
</InputLabel>
<Select
labelId="floor-select-label"
id="floor-select"
value={selectedFloor}
label={t("Floor")}
onChange={(e) => setSelectedFloor(e.target.value)}
displayEmpty
>
<MenuItem value="">{t("All Floors")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel
id="status-select-label"
shrink={true}
>
{t("Status")}
</InputLabel>
<Select
labelId="status-select-label"
id="status-select"
value={selectedStatus}
label={t("Status")}
onChange={(e) => setSelectedStatus(e.target.value)}
displayEmpty
>
<MenuItem value="">{t("All Statuses")}</MenuItem>
<MenuItem value="pending">{t("pending")}</MenuItem>
<MenuItem value="released">{t("released")}</MenuItem>
<MenuItem value="completed">{t("completed")}</MenuItem>
</Select>
</FormControl>

<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={2} sx={{ flexShrink: 0, alignSelf: 'center' }}>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t("Now")}: {now.format('HH:mm')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'}
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
<Card sx={{ mb: 2 }}>
<CardContent>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
{t("Ticket Release Table")}
</Typography>

<Stack direction="row" spacing={2} sx={{ mb: 3, flexWrap: "wrap", alignItems: "center" }}>
<DatePicker
label={t("Target Date")}
value={queryDate}
onChange={(v) => v && setQueryDate(v)}
slotProps={{ textField: { size: "small", sx: { minWidth: 180 } } }}
/>
<Button variant="outlined" size="small" onClick={() => void loadData()}>
{t("Reload data")}
</Button>

<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel id="floor-select-label" shrink>
{t("Floor")}
</InputLabel>
<Select
labelId="floor-select-label"
value={selectedFloor}
label={t("Floor")}
onChange={(e) => setSelectedFloor(e.target.value)}
displayEmpty
>
<MenuItem value="">{t("All Floors")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 150 }} size="small">
<InputLabel id="status-select-label" shrink>
{t("Status")}
</InputLabel>
<Select
labelId="status-select-label"
value={selectedStatus}
label={t("Status")}
onChange={(e) => setSelectedStatus(e.target.value)}
displayEmpty
>
<MenuItem value="">{t("All Statuses")}</MenuItem>
<MenuItem value="pending">{t("pending")}</MenuItem>
<MenuItem value="released">{t("released")}</MenuItem>
<MenuItem value="completed">{t("completed")}</MenuItem>
</Select>
</FormControl>

<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" spacing={2} sx={{ flexShrink: 0, alignSelf: "center" }}>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{t("Now")}: {now.format("HH:mm")}
</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{t("Auto-refresh every 5 minutes")} | {t("Last updated")}:{" "}
{lastDataRefreshTime ? lastDataRefreshTime.format("HH:mm:ss") : "--:--:--"}
</Typography>
</Stack>
</Stack>
</Stack>

<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}>
<Table size="small" sx={{ minWidth: 650 }}>
<TableHead>
<TableRow sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}>
<TableCell>{t("Store ID")}</TableCell>
<TableCell>{t("Required Delivery Date")}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Truck Information")}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{t("Truck Lane Code")} - {t("Departure Time")}
</Typography>
</Box>
</TableCell>
{/*<TableCell>{t("Truck Departure Time")}</TableCell>
<TableCell>{t("Truck Lane Code")}</TableCell>*/}
<TableCell sx={{ minWidth: 200, width: '20%' }}>{t("Shop Name")}</TableCell>
<TableCell align="right">{t("Loading Sequence")}</TableCell>
{/*<TableCell>{t("Delivery Order Code(s)")}</TableCell>
<TableCell>{t("Pick Order Code(s)")}</TableCell>
<TableCell>{t("Ticket Number")}</TableCell>
<TableCell>{t("Ticket Release Time")}</TableCell>
<TableCell>{t("Ticket Complete Date Time")}</TableCell>
<TableCell>{t("Ticket Status")}</TableCell>*/}
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Ticket Information")}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{t("Ticket No.")} ({t("Status")})
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{t("Released Time")} - {t("Completed Time")}
</Typography>
</Box>
</TableCell>
<TableCell>{t("Handler Name")}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%', whiteSpace: 'nowrap' }}>{t("Number of FG Items (Order Item(s) Count)")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={12} align="center">
{t("No data available")}

<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<TableContainer component={Paper} sx={{ maxHeight: 440, overflow: "auto" }}>
<Table size="small" sx={{ minWidth: 650 }}>
<TableHead>
<TableRow
sx={{
position: "sticky",
top: 0,
zIndex: 1,
backgroundColor: "grey.100",
}}
>
<TableCell>{t("Store ID")}</TableCell>
<TableCell>{t("Required Delivery Date")}</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Truck Information")}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{t("Truck Lane Code")} - {t("Departure Time")}
</Typography>
</Box>
</TableCell>
<TableCell sx={{ minWidth: 200, width: "20%" }}>{t("Shop Name")}</TableCell>
<TableCell align="right">{t("Loading Sequence")}</TableCell>
<TableCell>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Ticket Information")}
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{t("Ticket No.")} ({t("Status")})
</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{t("Released Time")} - {t("Completed Time")}
</Typography>
</Box>
</TableCell>
<TableCell>{t("Handler Name")}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: "8%", whiteSpace: "nowrap" }}>
{t("Number of FG Items (Order Item(s) Count)")}
</TableCell>
<TableCell align="center" sx={{ minWidth: 200 }}>
{t("Actions")}
</TableCell>
</TableRow>
) : (
paginatedData.map((row) => {
return (
<TableRow key={row.id}>
<TableCell>{row.storeId || '-'}</TableCell>
</TableHead>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
{t("No data available")}
</TableCell>
</TableRow>
) : (
paginatedData.map((row) => (
<TableRow key={`${row.id}-${row.ticketNo}-${row.requiredDeliveryDate}`}>
<TableCell>{row.storeId || "-"}</TableCell>
<TableCell>
{row.requiredDeliveryDate
? dayjs(row.requiredDeliveryDate).format('YYYY-MM-DD')
: '-'}
? dayjs(row.requiredDeliveryDate).format("YYYY-MM-DD")
: "-"}
</TableCell>

<TableCell>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
{row.truckLanceCode && (
<Chip
label={row.truckLanceCode}
size="small"
color="primary"
/>
)}
{row.truckDepartureTime && (
<Chip
label={formatTime(row.truckDepartureTime)}
size="small"
color="secondary"
/>
)}
{!row.truckLanceCode && !row.truckDepartureTime && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
-
</Typography>
)}
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap", alignItems: "center" }}>
{row.truckLanceCode && (
<Chip label={row.truckLanceCode} size="small" color="primary" />
)}
{row.truckDepartureTime && (
<Chip label={formatTime(row.truckDepartureTime)} size="small" color="secondary" />
)}
{!row.truckLanceCode && !row.truckDepartureTime && (
<Typography variant="body2" sx={{ color: "text.secondary" }}>
-
</Typography>
)}
</Box>
</TableCell>

<TableCell sx={{ minWidth: 200, width: '20%' }}>{row.shopName || '-'}</TableCell>
<TableCell align="right">{row.loadingSequence || '-'}</TableCell>
{/*<TableCell>{row.deliveryOrderCode || '-'}</TableCell>
<TableCell align="right">{row.pickOrderCode || '-'}</TableCell>
<TableCell>{row.ticketNo || '-'}</TableCell>
<TableCell>
{row.ticketReleaseTime
? dayjs(row.ticketReleaseTime).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</TableCell>
<TableCell>
{row.ticketCompleteDateTime
? dayjs(row.ticketCompleteDateTime).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</TableCell>
<TableCell>{row.ticketStatus || '-'}</TableCell>*/}
<TableCell sx={{ minWidth: 200, width: "20%" }}>{row.shopName || "-"}</TableCell>
<TableCell align="right">{row.loadingSequence ?? "-"}</TableCell>

<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
<Typography variant="body2">
{row.ticketNo || '-'} ({row.ticketStatus ? t(row.ticketStatus.toLowerCase()) : '-'})
{row.ticketNo || "-"} ({row.ticketStatus ? t(row.ticketStatus.toLowerCase()) : "-"})
</Typography>
<Typography variant="body2">
{t("Released Time")}: {row.ticketReleaseTime
{t("Released Time")}:{" "}
{row.ticketReleaseTime
? (() => {
if (Array.isArray(row.ticketReleaseTime)) {
return arrayToDayjs(row.ticketReleaseTime, true).format('HH:mm');
return arrayToDayjs(row.ticketReleaseTime, true).format("HH:mm");
}
const parsedDate = dayjs(row.ticketReleaseTime, 'YYYYMMDDHHmmss');
const parsedDate = dayjs(row.ticketReleaseTime, "YYYYMMDDHHmmss");
if (!parsedDate.isValid()) {
return dayjs(row.ticketReleaseTime).format('HH:mm');
return dayjs(row.ticketReleaseTime).format("HH:mm");
}
return parsedDate.format('HH:mm');
return parsedDate.format("HH:mm");
})()
: '-'}
: "-"}
</Typography>
<Typography variant="body2">
{t("Completed Time")}: {row.ticketCompleteDateTime
{t("Completed Time")}:{" "}
{row.ticketCompleteDateTime
? (() => {
if (Array.isArray(row.ticketCompleteDateTime)) {
return arrayToDayjs(row.ticketCompleteDateTime, true).format('HH:mm');
return arrayToDayjs(row.ticketCompleteDateTime, true).format("HH:mm");
}
const parsedDate = dayjs(row.ticketCompleteDateTime, 'YYYYMMDDHHmmss');
const parsedDate = dayjs(row.ticketCompleteDateTime, "YYYYMMDDHHmmss");
if (!parsedDate.isValid()) {
return dayjs(row.ticketCompleteDateTime).format('HH:mm');
return dayjs(row.ticketCompleteDateTime).format("HH:mm");
}
return parsedDate.format('HH:mm');
return parsedDate.format("HH:mm");
})()
: '-'}
: "-"}
</Typography>
</Box>
</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%' }}>{row.handlerName ?? 0}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%' }}>{row.numberOfFGItems ?? 0}</TableCell>
<TableCell>{row.handlerName ?? "-"}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: "8%" }}>
{row.numberOfFGItems ?? 0}
</TableCell>
<TableCell align="center">
{showDoPickOpsButtons(row) ? (
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap" useFlexGap>
<Tooltip title={opsTooltip}>
<span>
<Button
size="small"
variant="outlined"
color="warning"
disabled={!canManageDoPickOps}
onClick={() => void handleRevert(row)}
>
{t("Revert assignment")}
</Button>
</span>
</Tooltip>
<Tooltip title={opsTooltip}>
<span>
<Button
size="small"
variant="outlined"
color="primary"
disabled={!canManageDoPickOps}
onClick={() => void handleForceComplete(row)}
>
{t("Force complete DO")}
</Button>
</span>
</Tooltip>
</Stack>
) : (
<Typography variant="caption" color="text.secondary">
</Typography>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
{filteredData.length > 0 && (
<TablePagination
component="div"
count={filteredData.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 15]}
labelRowsPerPage={t("Rows per page")}
/>
)}
</>
)}
</Box>
</CardContent>
</Card>
))
)}
</TableBody>
</Table>
</TableContainer>
{filteredData.length > 0 && (
<TablePagination
component="div"
count={filteredData.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 15]}
labelRowsPerPage={t("Rows per page")}
/>
)}
</>
)}
</Box>
</CardContent>
</Card>
</LocalizationProvider>
);
};

export default FGPickOrderTicketReleaseTable;
export default FGPickOrderTicketReleaseTable;

+ 24
- 4
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Visa fil

@@ -315,8 +315,8 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo

// ✅ 新增:处理标签页切换时的打印按钮状态重置
useEffect(() => {
// 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
if (tabIndex === 2) {
// 当切换到成品提貨記錄標籤時,重置打印按钮状态
if (tabIndex === 2 || tabIndex === 4) {
setPrintButtonsEnabled(false);
console.log("Reset print buttons for Pick Execution Record tab");
}
@@ -709,6 +709,7 @@ const handleAssignByLane = useCallback(async (
<Tab label={t("Finished Good Detail")} iconPosition="end" />
<Tab label={t("Finished Good Record")} iconPosition="end" />
<Tab label={t("Ticket Release Table")} iconPosition="end" />
<Tab label={t("Finished Good Record (All)")} iconPosition="end" />
</Tabs>
</Box>
@@ -742,8 +743,27 @@ const handleAssignByLane = useCallback(async (
onRefreshReleasedOrderCount={fetchReleasedOrderCount}
/>
) }
{tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} printerCombo={printerCombo} a4Printer={selectedPrinterForAllDraft} labelPrinter={selectedPrinterForDraft} />}
{tabIndex === 3 && <FGPickOrderTicketReleaseTable/>}
{tabIndex === 2 && (
<GoodPickExecutionRecord
filterArgs={filterArgs}
printerCombo={printerCombo}
a4Printer={selectedPrinterForAllDraft}
labelPrinter={selectedPrinterForDraft}
recordTabIndex={2}
listScope="mine"
/>
)}
{tabIndex === 3 && <FGPickOrderTicketReleaseTable />}
{tabIndex === 4 && (
<GoodPickExecutionRecord
filterArgs={filterArgs}
printerCombo={printerCombo}
a4Printer={selectedPrinterForAllDraft}
labelPrinter={selectedPrinterForDraft}
recordTabIndex={4}
listScope="all"
/>
)}
</Box>
</Box>
);


+ 81
- 39
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx Visa fil

@@ -44,6 +44,7 @@ import {
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode,
fetchCompletedDoPickOrders,
fetchCompletedDoPickOrdersAll,
CompletedDoPickOrderResponse,
CompletedDoPickOrderSearchParams,
fetchLotDetailsByDoPickOrderRecordId
@@ -74,6 +75,10 @@ interface Props {
printerCombo: PrinterCombo[];
a4Printer: PrinterCombo | null; // A4 打印机(DN 用)
labelPrinter: PrinterCombo | null;
/** 與 FinishedGoodSearch 標籤索引一致,用於 pickOrderCompletionStatus 事件 */
recordTabIndex?: number;
/** mine:僅本人經手的完成記錄;all:全部人員的完成記錄(新分頁) */
listScope?: "mine" | "all";
}


@@ -87,7 +92,14 @@ interface PickOrderData {
lots: any[];
}

const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4Printer, labelPrinter }) => {
const GoodPickExecutionRecord: React.FC<Props> = ({
filterArgs,
printerCombo,
a4Printer,
labelPrinter,
recordTabIndex = 2,
listScope = "mine",
}) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -103,8 +115,10 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4
const [showDetailView, setShowDetailView] = useState(false);
const [detailLotData, setDetailLotData] = useState<any[]>([]);
// 新增:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
// 新增:搜索状态(預設為今日)
const [searchQuery, setSearchQuery] = useState<Record<string, any>>(() => ({
targetDate: dayjs().format("YYYY-MM-DD"),
}));
const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
// 新增:分页状态
@@ -353,14 +367,17 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4

// 修改:使用新的 API 获取已完成的 DO Pick Orders
const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
if (!currentUserId) return;
if (listScope === "mine" && !currentUserId) return;
setCompletedDoPickOrdersLoading(true);
try {
console.log("🔍 Fetching completed DO pick orders with params:", searchParams);
const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams);

const completedDoPickOrders =
listScope === "all"
? await fetchCompletedDoPickOrdersAll(searchParams)
: await fetchCompletedDoPickOrders(currentUserId!, searchParams);

setCompletedDoPickOrders(completedDoPickOrders);
setFilteredDoPickOrders(completedDoPickOrders);
console.log(" Fetched completed DO pick orders:", completedDoPickOrders);
@@ -371,14 +388,19 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4
} finally {
setCompletedDoPickOrdersLoading(false);
}
}, [currentUserId]);
}, [currentUserId, listScope]);

// 初始化时获取数据
// 初始化时获取数据(預設依「今日」篩選)
useEffect(() => {
if (currentUserId) {
fetchCompletedDoPickOrdersData();
const todayParams: CompletedDoPickOrderSearchParams = {
targetDate: dayjs().format("YYYY-MM-DD"),
};
if (listScope === "all") {
fetchCompletedDoPickOrdersData(todayParams);
} else if (currentUserId) {
fetchCompletedDoPickOrdersData(todayParams);
}
}, [currentUserId, fetchCompletedDoPickOrdersData]);
}, [currentUserId, listScope, fetchCompletedDoPickOrdersData]);

// 修改:搜索功能使用新的 API
const handleSearch = useCallback((query: Record<string, any>) => {
@@ -389,7 +411,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4
targetDate: query.targetDate || undefined,
shopName: query.shopName || undefined,
deliveryNoteCode: query.deliveryNoteCode || undefined,
//ticketNo: query.ticketNo || undefined,
truckLanceCode: query.truckLanceCode || undefined,
};

// 使用新的 API 进行搜索
@@ -398,8 +420,9 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4

// 修复:重命名函数避免重复声明
const handleSearchReset = useCallback(() => {
setSearchQuery({});
fetchCompletedDoPickOrdersData(); // 重新获取所有数据
const today = dayjs().format("YYYY-MM-DD");
setSearchQuery({ targetDate: today });
fetchCompletedDoPickOrdersData({ targetDate: today });
}, [fetchCompletedDoPickOrdersData]);

// 分页功能
@@ -425,24 +448,42 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs, printerCombo, a4
return filteredDoPickOrders.slice(startIndex, endIndex);
}, [filteredDoPickOrders, paginationController]);

// 搜索条件
const searchCriteria: Criterion<any>[] = [
{
label: t("Delivery Note Code"),
paramName: "deliveryNoteCode",
type: "text",
},
{
label: t("Shop Name"),
paramName: "shopName",
type: "text",
},
{
label: t("Target Date"),
paramName: "targetDate",
type: "date",
// 搜索条件(目標日期預設為今日)
const searchCriteria: Criterion<any>[] = useMemo(
() => [
{
label: t("Delivery Note Code"),
paramName: "deliveryNoteCode",
type: "text",
},
{
label: t("Shop Name"),
paramName: "shopName",
type: "text",
},
{
label: t("Truck Lance Code"),
paramName: "truckLanceCode",
type: "text",
},
{
label: t("Target Date"),
paramName: "targetDate",
type: "date",
defaultValue: dayjs().format("YYYY-MM-DD"),
},
],
[t],
);

const searchDateDisplay = useMemo(() => {
const raw = searchQuery.targetDate;
if (raw && String(raw).trim() !== "") {
const d = dayjs(raw);
return d.isValid() ? d.format(OUTPUT_DATE_FORMAT) : t("All dates");
}
];
return t("All dates");
}, [searchQuery.targetDate, t]);

const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrderResponse) => {
setSelectedDoPickOrder(doPickOrder);
@@ -540,7 +581,7 @@ setDetailLotData(flatLotData);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 2
tabIndex: recordTabIndex
}
}));
@@ -551,11 +592,11 @@ setDetailLotData(flatLotData);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
tabIndex: 2
tabIndex: recordTabIndex
}
}));
}
}, []);
}, [recordTabIndex]);


// 返回列表视图
@@ -568,10 +609,10 @@ setDetailLotData(flatLotData);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
tabIndex: 2
tabIndex: recordTabIndex
}
}));
}, []);
}, [recordTabIndex]);


// 如果显示详情视图,渲染类似 GoodPickExecution 的表格
@@ -723,7 +764,8 @@ if (showDetailView && selectedDoPickOrder) {
<Box>
{/* 结果统计 */}
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Completed DO pick orders: ")} {filteredDoPickOrders.length}
{t("Search date")}: {searchDateDisplay} | {t("Completed DO pick orders: ")}{" "}
{filteredDoPickOrders.length}
</Typography>

{/* 列表 */}


+ 12
- 1
src/components/SearchBox/SearchBox.tsx Visa fil

@@ -164,6 +164,12 @@ function SearchBox<T extends string>({
[`${c.paramName}To`]: c.defaultValueTo ?? "",
};
}
if (c.type === "date") {
tempCriteria = {
...tempCriteria,
[c.paramName]: c.defaultValue ?? "",
};
}
return tempCriteria;
},
{} as Record<T | `${T}To`, string>,
@@ -274,7 +280,7 @@ function SearchBox<T extends string>({
}, []);

const handleReset = () => {
setInputs(defaultInputs);
setInputs(preFilledInputs);
onReset?.();
setIsReset(!isReset);
};
@@ -555,6 +561,11 @@ function SearchBox<T extends string>({
label={t(c.label)}
onChange={makeDateChangeHandler(c.paramName)}
disabled={disabled}
value={
inputs[c.paramName] && dayjs(inputs[c.paramName]).isValid()
? dayjs(inputs[c.paramName])
: null
}
/>
</FormControl>
</Box>


+ 3
- 0
src/i18n/zh/pickOrder.json Visa fil

@@ -350,6 +350,9 @@
"Enter the number of cartons:": "請輸入總箱數",
"Finished Good Detail": "成品提貨詳情",
"Finished Good Record": "成品提貨記錄",
"Finished Good Record (All)": "成品提貨記錄(全部)",
"All dates": "全部日期",
"Search date": "搜索日期",
"Hide Completed: OFF": "完成: OFF",
"Hide Completed: ON": "完成: ON",
"Number must be at least 1": "數量至少為1",


+ 14
- 1
src/i18n/zh/ticketReleaseTable.json Visa fil

@@ -31,5 +31,18 @@
"pending": "待撳單",
"released": "提貨中",
"completed": "已完成",
"All Statuses": "所有提貨狀態"
"All Statuses": "所有提貨狀態",
"Target Date": "目標日期",
"Reload data": "重新載入",
"Manager only hint": "僅管理員(ADMIN 權限)可使用",
"Actions": "操作",
"Revert assignment": "撤銷領取",
"Force complete DO": "強制完成提貨單",
"Confirm revert assignment": "確認撤銷領取?",
"Revert assignment hint": "將清空負責人,單據回到待分配,其他人可再領取。",
"Confirm force complete": "確認強制完成?",
"Force complete hint": "僅將狀態標為完成並歸檔,不修改已揀數量;適用於已全部提交但系統未完成的情況。",
"Operation succeeded": "操作成功",
"Confirm": "確認",
"Cancel": "取消"
}

Laddar…
Avbryt
Spara