|
|
|
@@ -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; |