| @@ -855,6 +855,18 @@ export const retryFailedTruckLaneScheduleAction = async ( | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const reactivateCancelledTruckLaneScheduleAction = async ( | |||||
| id: number, | |||||
| body?: RetryFailedTruckLaneScheduleRequest, | |||||
| ): Promise<TruckLaneScheduleResponse> => { | |||||
| const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/reactivate`; | |||||
| return serverFetchJson<TruckLaneScheduleResponse>(endpoint, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(body ?? {}), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| }; | |||||
| export const ignoreTruckLaneScheduleAction = async ( | export const ignoreTruckLaneScheduleAction = async ( | ||||
| id: number, | id: number, | ||||
| ): Promise<TruckLaneScheduleResponse> => { | ): Promise<TruckLaneScheduleResponse> => { | ||||
| @@ -38,6 +38,7 @@ import { | |||||
| cancelTruckLaneScheduleAction, | cancelTruckLaneScheduleAction, | ||||
| applyNowTruckLaneScheduleAction, | applyNowTruckLaneScheduleAction, | ||||
| retryFailedTruckLaneScheduleAction, | retryFailedTruckLaneScheduleAction, | ||||
| reactivateCancelledTruckLaneScheduleAction, | |||||
| ignoreTruckLaneScheduleAction, | ignoreTruckLaneScheduleAction, | ||||
| type RetryFailedTruckLaneScheduleRequest, | type RetryFailedTruckLaneScheduleRequest, | ||||
| parseTruckLaneScheduleExcelAction, | parseTruckLaneScheduleExcelAction, | ||||
| @@ -255,6 +256,13 @@ export const retryFailedTruckLaneScheduleClient = async ( | |||||
| return await retryFailedTruckLaneScheduleAction(id, body); | return await retryFailedTruckLaneScheduleAction(id, body); | ||||
| }; | }; | ||||
| export const reactivateCancelledTruckLaneScheduleClient = async ( | |||||
| id: number, | |||||
| body?: RetryFailedTruckLaneScheduleRequest, | |||||
| ): Promise<TruckLaneScheduleResponse> => { | |||||
| return await reactivateCancelledTruckLaneScheduleAction(id, body); | |||||
| }; | |||||
| export const ignoreTruckLaneScheduleClient = async ( | export const ignoreTruckLaneScheduleClient = async ( | ||||
| id: number, | id: number, | ||||
| ): Promise<TruckLaneScheduleResponse> => { | ): Promise<TruckLaneScheduleResponse> => { | ||||
| @@ -2425,6 +2425,9 @@ const RouteBoard: React.FC = () => { | |||||
| }; | }; | ||||
| const handleDeleteTruckRow = async (truckRowId: number) => { | const handleDeleteTruckRow = async (truckRowId: number) => { | ||||
| if (truckRowId > 0 && scheduledShopIdSet.has(truckRowId)) { | |||||
| return; | |||||
| } | |||||
| if (truckRowId < 0) { | if (truckRowId < 0) { | ||||
| if (!window.confirm(t("confirm_discardDraftShop"))) return; | if (!window.confirm(t("confirm_discardDraftShop"))) return; | ||||
| setError(null); | setError(null); | ||||
| @@ -2488,6 +2491,11 @@ const RouteBoard: React.FC = () => { | |||||
| /** 清空整桶店鋪:與單筆刪除相同,僅標記 dirtyDeletes,按「儲存更改」才 deleteTruckLaneClient */ | /** 清空整桶店鋪:與單筆刪除相同,僅標記 dirtyDeletes,按「儲存更改」才 deleteTruckLaneClient */ | ||||
| const handleClearLaneShops = (lane: Lane) => { | const handleClearLaneShops = (lane: Lane) => { | ||||
| if (lane.shops.length === 0) return; | if (lane.shops.length === 0) return; | ||||
| if ( | |||||
| lane.shops.some((s) => s.id > 0 && scheduledShopIdSet.has(s.id)) | |||||
| ) { | |||||
| return; | |||||
| } | |||||
| if ( | if ( | ||||
| !window.confirm( | !window.confirm( | ||||
| t("confirm_clearLane", { | t("confirm_clearLane", { | ||||
| @@ -5845,13 +5853,6 @@ const RouteBoard: React.FC = () => { | |||||
| setDistrictEditError(null); | setDistrictEditError(null); | ||||
| }} | }} | ||||
| error={Boolean(districtEditError)} | error={Boolean(districtEditError)} | ||||
| helperText={ | |||||
| districtEditError || | |||||
| (districtEditCtx?.mode === "rename" && | |||||
| districtEditCtx.oldDisplay === "未分類" | |||||
| ? t("district_help_null") | |||||
| : t("district_help_mapped")) | |||||
| } | |||||
| InputLabelProps={{ shrink: true }} | InputLabelProps={{ shrink: true }} | ||||
| /> | /> | ||||
| </DialogContent> | </DialogContent> | ||||
| @@ -5915,13 +5916,6 @@ const RouteBoard: React.FC = () => { | |||||
| inputProps={{ step: 1 }} | inputProps={{ step: 1 }} | ||||
| sx={{ mt: 1 }} | sx={{ mt: 1 }} | ||||
| /> | /> | ||||
| <Typography | |||||
| variant="caption" | |||||
| color="text.secondary" | |||||
| sx={{ mt: 1, display: "block" }} | |||||
| > | |||||
| {t("seqDialog_hint")} | |||||
| </Typography> | |||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={closeSeqEdit}>{t("cancel")}</Button> | <Button onClick={closeSeqEdit}>{t("cancel")}</Button> | ||||
| @@ -7843,7 +7837,13 @@ const RouteBoard: React.FC = () => { | |||||
| spacing={0.5} | spacing={0.5} | ||||
| alignItems="center" | alignItems="center" | ||||
| > | > | ||||
| <Tooltip title={t("tooltip_removeFromLane")}> | |||||
| <Tooltip | |||||
| title={ | |||||
| isScheduledMove | |||||
| ? t("schedule_shop_locked") | |||||
| : t("tooltip_removeFromLane") | |||||
| } | |||||
| > | |||||
| <span> | <span> | ||||
| <IconButton | <IconButton | ||||
| size="small" | size="small" | ||||
| @@ -7858,7 +7858,8 @@ const RouteBoard: React.FC = () => { | |||||
| }} | }} | ||||
| disabled={ | disabled={ | ||||
| loading || | loading || | ||||
| dirtyDeletes.has(shop.id) | |||||
| dirtyDeletes.has(shop.id) || | |||||
| isScheduledMove | |||||
| } | } | ||||
| > | > | ||||
| <Trash2 size={16} /> | <Trash2 size={16} /> | ||||
| @@ -7944,12 +7945,29 @@ const RouteBoard: React.FC = () => { | |||||
| > | > | ||||
| {t("btn_addShopToLane")} | {t("btn_addShopToLane")} | ||||
| </Button> | </Button> | ||||
| <Tooltip title={t("tooltip_clearLaneShops")}> | |||||
| <Tooltip | |||||
| title={ | |||||
| lane.shops.some( | |||||
| (s) => | |||||
| s.id > 0 && scheduledShopIdSet.has(s.id), | |||||
| ) | |||||
| ? t("schedule_shop_locked") | |||||
| : t("tooltip_clearLaneShops") | |||||
| } | |||||
| > | |||||
| <span> | <span> | ||||
| <IconButton | <IconButton | ||||
| size="small" | size="small" | ||||
| onClick={() => handleClearLaneShops(lane)} | onClick={() => handleClearLaneShops(lane)} | ||||
| disabled={loading || lane.shops.length === 0} | |||||
| disabled={ | |||||
| loading || | |||||
| lane.shops.length === 0 || | |||||
| lane.shops.some( | |||||
| (s) => | |||||
| s.id > 0 && | |||||
| scheduledShopIdSet.has(s.id), | |||||
| ) | |||||
| } | |||||
| > | > | ||||
| <Trash2 size={16} /> | <Trash2 size={16} /> | ||||
| </IconButton> | </IconButton> | ||||
| @@ -14,6 +14,7 @@ import { | |||||
| DialogTitle, | DialogTitle, | ||||
| IconButton, | IconButton, | ||||
| Stack, | Stack, | ||||
| TextField, | |||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { | import { | ||||
| @@ -38,16 +39,23 @@ import { | |||||
| applyNowTruckLaneScheduleClient, | applyNowTruckLaneScheduleClient, | ||||
| cancelTruckLaneScheduleClient, | cancelTruckLaneScheduleClient, | ||||
| createTruckLaneScheduleClient, | createTruckLaneScheduleClient, | ||||
| reactivateCancelledTruckLaneScheduleClient, | |||||
| getTruckLaneScheduleClient, | getTruckLaneScheduleClient, | ||||
| ignoreTruckLaneScheduleClient, | ignoreTruckLaneScheduleClient, | ||||
| listTruckLaneSchedulesClient, | listTruckLaneSchedulesClient, | ||||
| retryFailedTruckLaneScheduleClient, | retryFailedTruckLaneScheduleClient, | ||||
| type TruckLaneScheduleLineRequest, | |||||
| type TruckLaneScheduleLineResponse, | type TruckLaneScheduleLineResponse, | ||||
| type TruckLaneScheduleResponse, | type TruckLaneScheduleResponse, | ||||
| } from "@/app/api/shop/client"; | } from "@/app/api/shop/client"; | ||||
| import type { ScheduleLaneOption } from "@/components/Shop/ScheduleChangeModal"; | import type { ScheduleLaneOption } from "@/components/Shop/ScheduleChangeModal"; | ||||
| import { extractApiErrorMessage } from "@/components/Shop/scheduleClientHelpers"; | import { extractApiErrorMessage } from "@/components/Shop/scheduleClientHelpers"; | ||||
| import { resolveRescheduleExecuteAt, formatScheduleDisplayDateTime } from "@/components/Shop/scheduleExecuteAt"; | |||||
| import { | |||||
| buildExecuteAtIso, | |||||
| isExecuteAtTooEarly, | |||||
| resolveRescheduleExecuteAt, | |||||
| formatScheduleDisplayDateTime, | |||||
| } from "@/components/Shop/scheduleExecuteAt"; | |||||
| import { | import { | ||||
| buildScheduleLineDescription, | buildScheduleLineDescription, | ||||
| } from "@/components/Shop/scheduleLineDisplay"; | } from "@/components/Shop/scheduleLineDisplay"; | ||||
| @@ -84,7 +92,7 @@ function uiStatusFromLines( | |||||
| return null; | return null; | ||||
| } | } | ||||
| /** Prefer line outcomes over header status when they disagree (e.g. PARTIAL still stored as PENDING). */ | |||||
| /** Prefer fresh list aggregates over cached detail lines (detail can be stale after mutations). */ | |||||
| function resolveUiStatus( | function resolveUiStatus( | ||||
| task: TruckLaneScheduleResponse, | task: TruckLaneScheduleResponse, | ||||
| detail?: TruckLaneScheduleResponse | null, | detail?: TruckLaneScheduleResponse | null, | ||||
| @@ -93,11 +101,6 @@ function resolveUiStatus( | |||||
| if (st === "CANCELLED") return "cancelled"; | if (st === "CANCELLED") return "cancelled"; | ||||
| if (st === "IGNORED") return "ignored"; | if (st === "IGNORED") return "ignored"; | ||||
| const fromDetailLines = detail?.lines?.length | |||||
| ? uiStatusFromLines(detail.lines) | |||||
| : null; | |||||
| if (fromDetailLines) return fromDetailLines; | |||||
| if (task.lineCounts) { | if (task.lineCounts) { | ||||
| const fromCounts = uiStatusFromLineCounts(task.lineCounts); | const fromCounts = uiStatusFromLineCounts(task.lineCounts); | ||||
| if (fromCounts) return fromCounts; | if (fromCounts) return fromCounts; | ||||
| @@ -108,6 +111,12 @@ function resolveUiStatus( | |||||
| if (st === "PARTIAL") { | if (st === "PARTIAL") { | ||||
| return (task.lineCounts?.failed ?? 0) > 0 ? "failed" : "success"; | return (task.lineCounts?.failed ?? 0) > 0 ? "failed" : "success"; | ||||
| } | } | ||||
| const fromDetailLines = detail?.lines?.length | |||||
| ? uiStatusFromLines(detail.lines) | |||||
| : null; | |||||
| if (fromDetailLines) return fromDetailLines; | |||||
| if (st === "PENDING" || st === "APPLYING") return "pending"; | if (st === "PENDING" || st === "APPLYING") return "pending"; | ||||
| return "pending"; | return "pending"; | ||||
| } | } | ||||
| @@ -118,13 +127,34 @@ function canManagePendingSchedule( | |||||
| ): boolean { | ): boolean { | ||||
| const st = String(task.status ?? "").toUpperCase(); | const st = String(task.status ?? "").toUpperCase(); | ||||
| if (st !== "PENDING" && st !== "APPLYING") return false; | if (st !== "PENDING" && st !== "APPLYING") return false; | ||||
| const c = task.lineCounts; | |||||
| if (c) { | |||||
| return c.pending > 0 && c.applied === 0 && c.failed === 0; | |||||
| } | |||||
| const lines = detail?.lines ?? []; | const lines = detail?.lines ?? []; | ||||
| if (lines.length > 0) { | if (lines.length > 0) { | ||||
| return lines.every((l) => l.lineStatus === "PENDING"); | return lines.every((l) => l.lineStatus === "PENDING"); | ||||
| } | } | ||||
| const c = task.lineCounts; | |||||
| if (!c) return true; | |||||
| return c.pending > 0 && c.applied === 0 && c.failed === 0; | |||||
| return true; | |||||
| } | |||||
| function scheduleLinesToRequests( | |||||
| lines: TruckLaneScheduleLineResponse[], | |||||
| ): TruckLaneScheduleLineRequest[] { | |||||
| return lines | |||||
| .filter((l) => l.lineStatus !== "APPLIED") | |||||
| .map((l) => ({ | |||||
| action: l.action, | |||||
| truckRowId: l.truckRowId, | |||||
| toTruckLanceCode: l.toTruckLanceCode, | |||||
| toRemark: l.toRemark, | |||||
| toStoreId: l.toStoreId, | |||||
| toLoadingSequence: l.toLoadingSequence ?? 0, | |||||
| toDistrictReference: l.toDistrictReference ?? null, | |||||
| shopCode: l.shopCode, | |||||
| shopName: l.shopName, | |||||
| departureTime: l.departureTime, | |||||
| })); | |||||
| } | } | ||||
| function splitExecuteAt(executeAt: string): { date: string; time: string } { | function splitExecuteAt(executeAt: string): { date: string; time: string } { | ||||
| @@ -163,6 +193,12 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| const [actionId, setActionId] = useState<number | null>(null); | const [actionId, setActionId] = useState<number | null>(null); | ||||
| const [actionError, setActionError] = useState<string | null>(null); | const [actionError, setActionError] = useState<string | null>(null); | ||||
| const [actionNotice, setActionNotice] = useState<string | null>(null); | const [actionNotice, setActionNotice] = useState<string | null>(null); | ||||
| const [redoTarget, setRedoTarget] = useState<TruckLaneScheduleResponse | null>( | |||||
| null, | |||||
| ); | |||||
| const [redoDate, setRedoDate] = useState(""); | |||||
| const [redoTime, setRedoTime] = useState(""); | |||||
| const [redoSubmitting, setRedoSubmitting] = useState(false); | |||||
| const laneById = useMemo( | const laneById = useMemo( | ||||
| () => new Map(lanes.map((l) => [l.id, l])), | () => new Map(lanes.map((l) => [l.id, l])), | ||||
| @@ -218,6 +254,9 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| setDetailById({}); | setDetailById({}); | ||||
| setActionError(null); | setActionError(null); | ||||
| setActionNotice(null); | setActionNotice(null); | ||||
| setRedoTarget(null); | |||||
| setRedoDate(""); | |||||
| setRedoTime(""); | |||||
| void loadTasks(); | void loadTasks(); | ||||
| }, [open, loadTasks]); | }, [open, loadTasks]); | ||||
| @@ -238,15 +277,22 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| return tasks.filter((task) => { | return tasks.filter((task) => { | ||||
| const detail = detailById[task.id]; | const detail = detailById[task.id]; | ||||
| const ui = resolveUiStatus(task, detail); | const ui = resolveUiStatus(task, detail); | ||||
| if (filter === "all") return ui !== "cancelled"; | |||||
| if (filter === "all") return true; | |||||
| if (filter === "pending") return ui === "pending"; | if (filter === "pending") return ui === "pending"; | ||||
| if (filter === "success") return ui === "success"; | if (filter === "success") return ui === "success"; | ||||
| return ui === "failed"; | return ui === "failed"; | ||||
| }); | }); | ||||
| }, [tasks, filter, detailById]); | }, [tasks, filter, detailById]); | ||||
| const ensureDetail = async (id: number) => { | |||||
| if (detailById[id]) return detailById[id]; | |||||
| const invalidateTaskDetail = useCallback((id: number) => { | |||||
| setDetailById((prev) => { | |||||
| if (!prev[id]) return prev; | |||||
| const { [id]: _removed, ...rest } = prev; | |||||
| return rest; | |||||
| }); | |||||
| }, []); | |||||
| const refreshTaskDetail = useCallback(async (id: number) => { | |||||
| setDetailLoadingId(id); | setDetailLoadingId(id); | ||||
| try { | try { | ||||
| const detail = await getTruckLaneScheduleClient(id); | const detail = await getTruckLaneScheduleClient(id); | ||||
| @@ -255,8 +301,31 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| } finally { | } finally { | ||||
| setDetailLoadingId(null); | setDetailLoadingId(null); | ||||
| } | } | ||||
| }, []); | |||||
| const ensureDetail = async (id: number) => { | |||||
| if (detailById[id]) return detailById[id]; | |||||
| return refreshTaskDetail(id); | |||||
| }; | }; | ||||
| const reloadAfterMutation = useCallback( | |||||
| async (id: number) => { | |||||
| invalidateTaskDetail(id); | |||||
| await onAfterChange?.(); | |||||
| await loadTasks(); | |||||
| if (expandedId === id) { | |||||
| await refreshTaskDetail(id); | |||||
| } | |||||
| }, | |||||
| [ | |||||
| expandedId, | |||||
| invalidateTaskDetail, | |||||
| loadTasks, | |||||
| onAfterChange, | |||||
| refreshTaskDetail, | |||||
| ], | |||||
| ); | |||||
| const toggleExpand = async (id: number) => { | const toggleExpand = async (id: number) => { | ||||
| if (expandedId === id) { | if (expandedId === id) { | ||||
| setExpandedId(null); | setExpandedId(null); | ||||
| @@ -271,8 +340,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| setActionError(null); | setActionError(null); | ||||
| try { | try { | ||||
| await cancelTruckLaneScheduleClient(id); | await cancelTruckLaneScheduleClient(id); | ||||
| await onAfterChange?.(); | |||||
| await loadTasks(); | |||||
| await reloadAfterMutation(id); | |||||
| } catch (err: unknown) { | } catch (err: unknown) { | ||||
| setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | ||||
| } finally { | } finally { | ||||
| @@ -285,8 +353,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| setActionError(null); | setActionError(null); | ||||
| try { | try { | ||||
| await applyNowTruckLaneScheduleClient(id); | await applyNowTruckLaneScheduleClient(id); | ||||
| await onAfterChange?.(); | |||||
| await loadTasks(); | |||||
| await reloadAfterMutation(id); | |||||
| } catch (err: unknown) { | } catch (err: unknown) { | ||||
| setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | ||||
| } finally { | } finally { | ||||
| @@ -294,6 +361,59 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| } | } | ||||
| }; | }; | ||||
| const openRedoDialog = (task: TruckLaneScheduleResponse) => { | |||||
| const { executeAt } = resolveRescheduleExecuteAt(task.executeAt); | |||||
| const { date, time } = splitExecuteAt(executeAt); | |||||
| setRedoTarget(task); | |||||
| setRedoDate(date !== "-" ? date : ""); | |||||
| setRedoTime(time && time !== "-" ? time : ""); | |||||
| setActionError(null); | |||||
| }; | |||||
| const closeRedoDialog = () => { | |||||
| if (redoSubmitting) return; | |||||
| setRedoTarget(null); | |||||
| setRedoDate(""); | |||||
| setRedoTime(""); | |||||
| }; | |||||
| const handleConfirmRedo = async () => { | |||||
| if (!redoTarget) return; | |||||
| const executeAt = buildExecuteAtIso(redoDate, redoTime); | |||||
| if (!executeAt) { | |||||
| setActionError(t("schedule_err_execute_at_past")); | |||||
| return; | |||||
| } | |||||
| if (isExecuteAtTooEarly(executeAt)) { | |||||
| setActionError(t("schedule_err_execute_at_past")); | |||||
| return; | |||||
| } | |||||
| setRedoSubmitting(true); | |||||
| setActionId(redoTarget.id); | |||||
| setActionError(null); | |||||
| setActionNotice(null); | |||||
| try { | |||||
| const updated = await reactivateCancelledTruckLaneScheduleClient( | |||||
| redoTarget.id, | |||||
| { executeAt }, | |||||
| ); | |||||
| setActionNotice( | |||||
| t("schedule_reschedule_created", { | |||||
| id: updated.id, | |||||
| at: formatScheduleDisplayDateTime(executeAt), | |||||
| }), | |||||
| ); | |||||
| closeRedoDialog(); | |||||
| await reloadAfterMutation(redoTarget.id); | |||||
| } catch (err: unknown) { | |||||
| setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | |||||
| } finally { | |||||
| setRedoSubmitting(false); | |||||
| setActionId(null); | |||||
| } | |||||
| }; | |||||
| const handleReschedule = async (task: TruckLaneScheduleResponse) => { | const handleReschedule = async (task: TruckLaneScheduleResponse) => { | ||||
| if (String(task.status ?? "").toUpperCase() === "PARTIAL") { | if (String(task.status ?? "").toUpperCase() === "PARTIAL") { | ||||
| setActionError(t("schedule_retry_rejects_partial")); | setActionError(t("schedule_retry_rejects_partial")); | ||||
| @@ -315,18 +435,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| (l) => l.lineStatus === "FAILED" || l.lineStatus === "PENDING", | (l) => l.lineStatus === "FAILED" || l.lineStatus === "PENDING", | ||||
| ); | ); | ||||
| if (retryLines.length === 0) throw retryErr; | if (retryLines.length === 0) throw retryErr; | ||||
| const lines = retryLines.map((l) => ({ | |||||
| action: l.action, | |||||
| truckRowId: l.truckRowId, | |||||
| toTruckLanceCode: l.toTruckLanceCode, | |||||
| toRemark: l.toRemark, | |||||
| toStoreId: l.toStoreId, | |||||
| toLoadingSequence: l.toLoadingSequence ?? 0, | |||||
| toDistrictReference: l.toDistrictReference ?? null, | |||||
| shopCode: l.shopCode, | |||||
| shopName: l.shopName, | |||||
| departureTime: l.departureTime, | |||||
| })); | |||||
| const lines = scheduleLinesToRequests(retryLines); | |||||
| await createTruckLaneScheduleClient({ executeAt, lines }); | await createTruckLaneScheduleClient({ executeAt, lines }); | ||||
| } | } | ||||
| if (adjusted) { | if (adjusted) { | ||||
| @@ -334,8 +443,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| t("schedule_reschedule_time_adjusted", { at: executeAt }), | t("schedule_reschedule_time_adjusted", { at: executeAt }), | ||||
| ); | ); | ||||
| } | } | ||||
| await onAfterChange?.(); | |||||
| await loadTasks(); | |||||
| await reloadAfterMutation(task.id); | |||||
| } catch (err: unknown) { | } catch (err: unknown) { | ||||
| setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | ||||
| } finally { | } finally { | ||||
| @@ -348,8 +456,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| setActionError(null); | setActionError(null); | ||||
| try { | try { | ||||
| await ignoreTruckLaneScheduleClient(id); | await ignoreTruckLaneScheduleClient(id); | ||||
| await onAfterChange?.(); | |||||
| await loadTasks(); | |||||
| await reloadAfterMutation(id); | |||||
| } catch (err: unknown) { | } catch (err: unknown) { | ||||
| setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); | ||||
| } finally { | } finally { | ||||
| @@ -452,6 +559,22 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| /> | /> | ||||
| ); | ); | ||||
| } | } | ||||
| if (ui === "cancelled") { | |||||
| return ( | |||||
| <Chip | |||||
| size="small" | |||||
| icon={<Ban size={12} />} | |||||
| label={t("schedule_history_status_cancelled")} | |||||
| sx={{ | |||||
| fontWeight: 700, | |||||
| bgcolor: "grey.100", | |||||
| color: "text.secondary", | |||||
| border: 1, | |||||
| borderColor: "grey.400", | |||||
| }} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| return ( | return ( | ||||
| <Chip | <Chip | ||||
| size="small" | size="small" | ||||
| @@ -966,6 +1089,24 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| {t("schedule_history_status_ignored")} | {t("schedule_history_status_ignored")} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| {ui === "cancelled" && ( | |||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| disabled={busy} | |||||
| startIcon={ | |||||
| busy ? ( | |||||
| <CircularProgress size={14} color="inherit" /> | |||||
| ) : ( | |||||
| <RotateCcw size={14} /> | |||||
| ) | |||||
| } | |||||
| onClick={() => openRedoDialog(task)} | |||||
| sx={{ fontWeight: 700 }} | |||||
| > | |||||
| {t("schedule_history_reschedule")} | |||||
| </Button> | |||||
| )} | |||||
| </Stack> | </Stack> | ||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| @@ -988,6 +1129,58 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({ | |||||
| {t("schedule_history_close")} | {t("schedule_history_close")} | ||||
| </Button> | </Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| <Dialog | |||||
| open={redoTarget != null} | |||||
| onClose={closeRedoDialog} | |||||
| maxWidth="xs" | |||||
| fullWidth | |||||
| > | |||||
| <DialogTitle sx={{ fontWeight: 800 }}> | |||||
| {t("schedule_reschedule_dialog_title")} | |||||
| </DialogTitle> | |||||
| <DialogContent> | |||||
| <Stack direction={{ xs: "column", sm: "row" }} spacing={2}> | |||||
| <TextField | |||||
| label={t("schedule_exec_date")} | |||||
| type="date" | |||||
| size="small" | |||||
| fullWidth | |||||
| value={redoDate} | |||||
| onChange={(e) => setRedoDate(e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| /> | |||||
| <TextField | |||||
| label={t("schedule_exec_time")} | |||||
| type="time" | |||||
| size="small" | |||||
| fullWidth | |||||
| value={redoTime} | |||||
| onChange={(e) => setRedoTime(e.target.value)} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| /> | |||||
| </Stack> | |||||
| </DialogContent> | |||||
| <DialogActions sx={{ px: 2, pb: 2 }}> | |||||
| <Button onClick={closeRedoDialog} disabled={redoSubmitting}> | |||||
| {t("cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| disabled={ | |||||
| redoSubmitting || !redoDate.trim() || !redoTime.trim() | |||||
| } | |||||
| onClick={() => void handleConfirmRedo()} | |||||
| sx={{ fontWeight: 700 }} | |||||
| > | |||||
| {redoSubmitting ? ( | |||||
| <CircularProgress size={18} color="inherit" /> | |||||
| ) : ( | |||||
| t("schedule_reschedule_dialog_confirm") | |||||
| )} | |||||
| </Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| </Dialog> | </Dialog> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -175,8 +175,6 @@ | |||||
| "district_dialog_edit": "Edit district", | "district_dialog_edit": "Edit district", | ||||
| "district_name_label": "District display name", | "district_name_label": "District display name", | ||||
| "district_name_ph": "Blank means \"Unclassified\"", | "district_name_ph": "Blank means \"Unclassified\"", | ||||
| "district_help_null": "Unclassified maps to districtReference = null on server", | |||||
| "district_help_mapped": "Display name is written via toDistrictRawValue to each shop's districtReference; API runs on \"Save changes\"", | |||||
| "seq_edit_departureLabel": "Departure time", | "seq_edit_departureLabel": "Departure time", | ||||
| "seq_edit_seqLabel": "Load sequence (Seq)", | "seq_edit_seqLabel": "Load sequence (Seq)", | ||||
| "route_new_code_label": "Lane code", | "route_new_code_label": "Lane code", | ||||
| @@ -206,7 +204,6 @@ | |||||
| "departureDialog_title": "Edit departure time", | "departureDialog_title": "Edit departure time", | ||||
| "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", | "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", | ||||
| "seqDialog_title": "Edit load sequence", | "seqDialog_title": "Edit load sequence", | ||||
| "seqDialog_hint": "Press \"Save changes\" to persist to truck rows.", | |||||
| "logistics_colLaneCount": "{{count}} lane(s)", | "logistics_colLaneCount": "{{count}} lane(s)", | ||||
| "logistics_masterNoLanes": "Master record exists but no lanes are bound yet; pick this company when adding/editing lanes on the route board.", | "logistics_masterNoLanes": "Master record exists but no lanes are bound yet; pick this company when adding/editing lanes on the route board.", | ||||
| "tooltip_openLaneBoard": "Open this lane on the route board", | "tooltip_openLaneBoard": "Open this lane on the route board", | ||||
| @@ -196,8 +196,6 @@ | |||||
| "district_err_exists": "This district already exists", | "district_err_exists": "This district already exists", | ||||
| "district_err_name": "Enter a district name", | "district_err_name": "Enter a district name", | ||||
| "district_err_reserved": "\"Unclassified\" is built-in; do not add it again", | "district_err_reserved": "\"Unclassified\" is built-in; do not add it again", | ||||
| "district_help_mapped": "Display name is written via toDistrictRawValue to each shop's districtReference; API runs on \"Save changes\"", | |||||
| "district_help_null": "Unclassified maps to districtReference = null on server", | |||||
| "district_name_label": "District display name", | "district_name_label": "District display name", | ||||
| "district_name_ph": "Blank means \"Unclassified\"", | "district_name_ph": "Blank means \"Unclassified\"", | ||||
| "drag_blockDraftShop": "Unsaved \"new shop\" rows must be saved with \"Save changes\" or removed from the card before dragging.", | "drag_blockDraftShop": "Unsaved \"new shop\" rows must be saved with \"Save changes\" or removed from the card before dragging.", | ||||
| @@ -324,6 +322,11 @@ | |||||
| "schedule_history_reschedule": "Reschedule", | "schedule_history_reschedule": "Reschedule", | ||||
| "schedule_history_status_failed": "Failed", | "schedule_history_status_failed": "Failed", | ||||
| "schedule_history_status_ignored": "Ignored", | "schedule_history_status_ignored": "Ignored", | ||||
| "schedule_history_status_cancelled": "Cancelled", | |||||
| "schedule_reschedule_dialog_title": "Reschedule", | |||||
| "schedule_reschedule_dialog_subtitle": "Pick a new run date and time. This cancelled task will be updated in place.", | |||||
| "schedule_reschedule_dialog_confirm": "Update schedule", | |||||
| "schedule_reschedule_created": "Schedule #{{id}} updated to run at {{at}}.", | |||||
| "schedule_history_status_pending": "Scheduled", | "schedule_history_status_pending": "Scheduled", | ||||
| "schedule_history_status_success": "Succeeded", | "schedule_history_status_success": "Succeeded", | ||||
| "schedule_history_subtitle": "Monitor, run, or troubleshoot scheduled shop-lane changes", | "schedule_history_subtitle": "Monitor, run, or troubleshoot scheduled shop-lane changes", | ||||
| @@ -406,7 +409,6 @@ | |||||
| "schedule_tab_import": "Import route Excel", | "schedule_tab_import": "Import route Excel", | ||||
| "schedule_tab_manual": "Drag scheduling", | "schedule_tab_manual": "Drag scheduling", | ||||
| "schedule_target_unset": "Not set", | "schedule_target_unset": "Not set", | ||||
| "seqDialog_hint": "Press \"Save changes\" to persist to truck rows.", | |||||
| "seqDialog_title": "Edit load sequence", | "seqDialog_title": "Edit load sequence", | ||||
| "seq_edit_departureLabel": "Departure time", | "seq_edit_departureLabel": "Departure time", | ||||
| "seq_edit_seqLabel": "Load sequence (Seq)", | "seq_edit_seqLabel": "Load sequence (Seq)", | ||||
| @@ -34,7 +34,7 @@ | |||||
| "exportRoutes": "匯出車線", | "exportRoutes": "匯出車線", | ||||
| "routeReport": "車線報告", | "routeReport": "車線報告", | ||||
| "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", | "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", | ||||
| "departureTooltipEditSave": "編輯出車時間(按「儲存更改」寫回)", | |||||
| "departureTooltipEditSave": "編輯出車時間", | |||||
| "departureEditAria": "編輯出車時間", | "departureEditAria": "編輯出車時間", | ||||
| "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", | "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", | ||||
| "cancel": "取消", | "cancel": "取消", | ||||
| @@ -80,9 +80,9 @@ | |||||
| "confirm_restoreDiscardsEdits": "排程版本還原會捨棄目前看板上其他未儲存變更(拖曳、刪除、新增店鋪/車線、物流欄等)。確定繼續?", | "confirm_restoreDiscardsEdits": "排程版本還原會捨棄目前看板上其他未儲存變更(拖曳、刪除、新增店鋪/車線、物流欄等)。確定繼續?", | ||||
| "diff_restoreScheduled": "已排程還原至版本 #{{versionId}};請按「儲存更改」寫入後端。", | "diff_restoreScheduled": "已排程還原至版本 #{{versionId}};請按「儲存更改」寫入後端。", | ||||
| "diff_restoreAlreadyPending": "此版本已在排程還原中;請按「儲存更改」套用。", | "diff_restoreAlreadyPending": "此版本已在排程還原中;請按「儲存更改」套用。", | ||||
| "restore_applied": "已從 snapshot 還原並重新載入看板。", | |||||
| "restore_appliedDroppedStaging": "已套用 snapshot 還原;本次儲存略過其他暫存變更(請重新編輯)。", | |||||
| "confirm_restoreSaveWillDropStaging": "儲存時將先套用 snapshot 還原,本次其他暫存變更會被略過。確定繼續?", | |||||
| "restore_applied": "已從 版本還原並重新載入看板。", | |||||
| "restore_appliedDroppedStaging": "已套用 版本還原;本次儲存略過其他暫存變更(請重新編輯)。", | |||||
| "confirm_restoreSaveWillDropStaging": "儲存時將先套用 版本還原,本次其他暫存變更會被略過。確定繼續?", | |||||
| "diff_noOlderCompare": "沒有上一筆版本可比較(請選擇較新的版本)", | "diff_noOlderCompare": "沒有上一筆版本可比較(請選擇較新的版本)", | ||||
| "logistic_needMasterTpl": "「{{name}}」尚無對應物流公司,請先用「新增物流商」建立。", | "logistic_needMasterTpl": "「{{name}}」尚無對應物流公司,請先用「新增物流商」建立。", | ||||
| "diffField_logisticsCompany": "物流公司", | "diffField_logisticsCompany": "物流公司", | ||||
| @@ -140,11 +140,11 @@ | |||||
| "diff_shopList_title": "店鋪異動清單", | "diff_shopList_title": "店鋪異動清單", | ||||
| "diff_staged_serverCountsOnly": "上列四格為「後端相鄰兩版快照」統計,不含看板上尚未儲存的編輯。", | "diff_staged_serverCountsOnly": "上列四格為「後端相鄰兩版快照」統計,不含看板上尚未儲存的編輯。", | ||||
| "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", | "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", | ||||
| "diff_staged_section_title": "看板未儲存/已排程(尚未寫入後端)", | |||||
| "diff_staged_section_title": "看板未儲存/已排程", | |||||
| "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端快照)混淆。", | "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端快照)混淆。", | ||||
| "diff_staged_tag_unsaved": "未儲存", | "diff_staged_tag_unsaved": "未儲存", | ||||
| "diff_staged_tag_scheduled": "已排程", | "diff_staged_tag_scheduled": "已排程", | ||||
| "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}(須按「儲存更改」才會呼叫 restore)。", | |||||
| "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}。", | |||||
| "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", | "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", | ||||
| "diff_staged_newLane": "新增車線(未儲存):{{lane}}", | "diff_staged_newLane": "新增車線(未儲存):{{lane}}", | ||||
| "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", | "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", | ||||
| @@ -154,9 +154,9 @@ | |||||
| "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", | "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", | ||||
| "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{name}}(車牌 {{plate}});將與路線一併於「儲存更改」寫入", | "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{name}}(車牌 {{plate}});將與路線一併於「儲存更改」寫入", | ||||
| "diff_staged_editLogisticMaster": "修改物流公司(未落庫):{{fromName}}({{fromPlate}})→ {{name}}({{plate}})", | "diff_staged_editLogisticMaster": "修改物流公司(未落庫):{{fromName}}({{fromPlate}})→ {{name}}({{plate}})", | ||||
| "diff_staged_importPending": "匯入 Excel(未落庫):{{file}} — {{sheets}} 個工作表、{{rows}} 列(按「儲存更改」寫入)", | |||||
| "diff_staged_importPending": "匯入 Excel(未落庫):{{file}} — {{sheets}} 個工作表、{{rows}} 列", | |||||
| "confirm_importDiscardEdits": "匯入將取代目前看板上未儲存的變更,是否繼續?", | "confirm_importDiscardEdits": "匯入將取代目前看板上未儲存的變更,是否繼續?", | ||||
| "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。按「儲存更改」才會寫入後端。", | |||||
| "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。", | |||||
| "err_importEmpty": "匯入檔案無有效車線資料列", | "err_importEmpty": "匯入檔案無有效車線資料列", | ||||
| "diff_logisticMaster_section": "物流公司異動", | "diff_logisticMaster_section": "物流公司異動", | ||||
| "diff_logisticMaster_added": "新增", | "diff_logisticMaster_added": "新增", | ||||
| @@ -175,10 +175,8 @@ | |||||
| "district_dialog_edit": "編輯地區", | "district_dialog_edit": "編輯地區", | ||||
| "district_name_label": "地區顯示名稱", | "district_name_label": "地區顯示名稱", | ||||
| "district_name_ph": "空白表示「未分類」", | "district_name_ph": "空白表示「未分類」", | ||||
| "district_help_null": "未分類對應後端 districtReference 為 null", | |||||
| "district_help_mapped": "顯示名稱經 toDistrictRawValue 寫入各店鋪 districtReference;按「儲存更改」才打 API", | |||||
| "seq_edit_departureLabel": "出車時間", | "seq_edit_departureLabel": "出車時間", | ||||
| "seq_edit_seqLabel": "裝車順序 (Seq)", | |||||
| "seq_edit_seqLabel": "裝車順序", | |||||
| "route_new_code_label": "車線編號", | "route_new_code_label": "車線編號", | ||||
| "route_new_time_label": "出車時間", | "route_new_time_label": "出車時間", | ||||
| "route_new_logistic_label": "物流公司", | "route_new_logistic_label": "物流公司", | ||||
| @@ -206,30 +204,29 @@ | |||||
| "departureDialog_title": "編輯出車時間", | "departureDialog_title": "編輯出車時間", | ||||
| "departureDialog_hint": "套用至此車線所有店鋪列。", | "departureDialog_hint": "套用至此車線所有店鋪列。", | ||||
| "seqDialog_title": "編輯裝車順序", | "seqDialog_title": "編輯裝車順序", | ||||
| "seqDialog_hint": "按「儲存更改」後寫入。", | |||||
| "logistics_colLaneCount": "{{count}} 條車線", | "logistics_colLaneCount": "{{count}} 條車線", | ||||
| "logistics_masterNoLanes": "主檔已建立,尚無綁定車線;至「車線看板」新增/編輯車線時可填此公司名稱。", | "logistics_masterNoLanes": "主檔已建立,尚無綁定車線;至「車線看板」新增/編輯車線時可填此公司名稱。", | ||||
| "tooltip_openLaneBoard": "在車線看板開此車線", | "tooltip_openLaneBoard": "在車線看板開此車線", | ||||
| "aria_openLaneBoard": "開啟車線看板", | "aria_openLaneBoard": "開啟車線看板", | ||||
| "tooltip_removeFromLane": "從此車線移除", | "tooltip_removeFromLane": "從此車線移除", | ||||
| "tooltip_clearLaneShops": "清空此車線所有店鋪(按「儲存更改」才寫入)", | |||||
| "tooltip_clearLaneShops": "清空此車線所有店鋪", | |||||
| "tooltip_pickLane": "選擇車線(加入看板勾選並捲動到該欄)", | "tooltip_pickLane": "選擇車線(加入看板勾選並捲動到該欄)", | ||||
| "aria_pickLane": "選擇車線", | "aria_pickLane": "選擇車線", | ||||
| "aria_searchLanes": "搜索車線", | "aria_searchLanes": "搜索車線", | ||||
| "logistics_colShopCount": "{{count}} 家店鋪", | "logistics_colShopCount": "{{count}} 家店鋪", | ||||
| "tooltip_editLogisticsDb": "編輯物流公司(須按「儲存更改」寫入)", | |||||
| "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", | |||||
| "tooltip_editLogisticsDb": "編輯物流公司", | |||||
| "tooltip_deleteLogistics": "刪除物流公司", | |||||
| "aria_editLogistics": "編輯物流公司", | "aria_editLogistics": "編輯物流公司", | ||||
| "aria_deleteLogistics": "刪除物流公司", | "aria_deleteLogistics": "刪除物流公司", | ||||
| "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?須按「儲存更改」寫入。", | |||||
| "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?", | |||||
| "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", | "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", | ||||
| "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", | "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", | ||||
| "logistic_btn_apply": "套用", | "logistic_btn_apply": "套用", | ||||
| "tooltip_editDistrict": "編輯地區名稱(按「儲存更改」才寫入)", | |||||
| "tooltip_editDistrict": "編輯地區名稱", | |||||
| "aria_editDistrict": "編輯地區", | "aria_editDistrict": "編輯地區", | ||||
| "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", | "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", | ||||
| "aria_removeEmptyDistrict": "移除空區", | "aria_removeEmptyDistrict": "移除空區", | ||||
| "tooltip_editSeq": "編輯裝車順序(按「儲存更改」寫回)", | |||||
| "tooltip_editSeq": "編輯裝車順序", | |||||
| "aria_editSeq": "編輯裝車順序", | "aria_editSeq": "編輯裝車順序", | ||||
| "diff_moveFrom": "從 {{lane}}", | "diff_moveFrom": "從 {{lane}}", | ||||
| "logistics_dirtyColumnBadge": "有未儲存物流更改", | "logistics_dirtyColumnBadge": "有未儲存物流更改", | ||||
| @@ -34,7 +34,7 @@ | |||||
| "exportRoutes": "匯出車線", | "exportRoutes": "匯出車線", | ||||
| "routeReport": "車線報告", | "routeReport": "車線報告", | ||||
| "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", | "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", | ||||
| "departureTooltipEditSave": "編輯出車時間(按「儲存更改」寫回)", | |||||
| "departureTooltipEditSave": "編輯出車時間", | |||||
| "departureEditAria": "編輯出車時間", | "departureEditAria": "編輯出車時間", | ||||
| "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", | "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", | ||||
| "cancel": "取消", | "cancel": "取消", | ||||
| @@ -139,11 +139,11 @@ | |||||
| "diff_summary_fieldChange": "欄位變更", | "diff_summary_fieldChange": "欄位變更", | ||||
| "diff_shopList_title": "店鋪變更清單", | "diff_shopList_title": "店鋪變更清單", | ||||
| "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", | "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", | ||||
| "diff_staged_section_title": "看板未儲存/已排程(尚未寫入後端)", | |||||
| "diff_staged_section_title": "看板未儲存/已排程", | |||||
| "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端版本)混淆。", | "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端版本)混淆。", | ||||
| "diff_staged_tag_unsaved": "未儲存", | "diff_staged_tag_unsaved": "未儲存", | ||||
| "diff_staged_tag_scheduled": "已排程", | "diff_staged_tag_scheduled": "已排程", | ||||
| "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}(須按「儲存更改」才會呼叫 restore)。", | |||||
| "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}。", | |||||
| "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", | "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", | ||||
| "diff_staged_newLane": "新增車線(未儲存):{{lane}}", | "diff_staged_newLane": "新增車線(未儲存):{{lane}}", | ||||
| "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", | "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", | ||||
| @@ -153,9 +153,9 @@ | |||||
| "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", | "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", | ||||
| "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{name}}(車牌 {{plate}});將與路線一併於「儲存更改」寫入", | "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{name}}(車牌 {{plate}});將與路線一併於「儲存更改」寫入", | ||||
| "diff_staged_editLogisticMaster": "修改物流公司(未落庫):{{fromName}}({{fromPlate}})→ {{name}}({{plate}})", | "diff_staged_editLogisticMaster": "修改物流公司(未落庫):{{fromName}}({{fromPlate}})→ {{name}}({{plate}})", | ||||
| "diff_staged_importPending": "匯入 Excel(未落庫):{{file}} — {{sheets}} 個工作表、{{rows}} 列(按「儲存更改」寫入)", | |||||
| "diff_staged_importPending": "匯入 Excel(未落庫):{{file}} — {{sheets}} 個工作表、{{rows}} 列", | |||||
| "confirm_importDiscardEdits": "匯入將取代目前看板上未儲存的變更,是否繼續?", | "confirm_importDiscardEdits": "匯入將取代目前看板上未儲存的變更,是否繼續?", | ||||
| "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。按「儲存更改」才會寫入後端。", | |||||
| "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。", | |||||
| "err_importEmpty": "匯入檔案無有效車線資料列", | "err_importEmpty": "匯入檔案無有效車線資料列", | ||||
| "diff_logisticMaster_section": "物流公司變更", | "diff_logisticMaster_section": "物流公司變更", | ||||
| "diff_logisticMaster_added": "新增", | "diff_logisticMaster_added": "新增", | ||||
| @@ -175,10 +175,8 @@ | |||||
| "district_dialog_edit": "編輯地區", | "district_dialog_edit": "編輯地區", | ||||
| "district_name_label": "地區顯示名稱", | "district_name_label": "地區顯示名稱", | ||||
| "district_name_ph": "空白表示「未分類」", | "district_name_ph": "空白表示「未分類」", | ||||
| "district_help_null": "未分類對應後端 districtReference 為 null", | |||||
| "district_help_mapped": "顯示名稱經 toDistrictRawValue 寫入各店鋪 districtReference;按「儲存更改」才打 API", | |||||
| "seq_edit_departureLabel": "出車時間", | "seq_edit_departureLabel": "出車時間", | ||||
| "seq_edit_seqLabel": "裝車順序 (Seq)", | |||||
| "seq_edit_seqLabel": "裝車順序", | |||||
| "route_new_code_label": "車線編號", | "route_new_code_label": "車線編號", | ||||
| "route_new_time_label": "出車時間", | "route_new_time_label": "出車時間", | ||||
| "route_new_logistic_label": "物流公司", | "route_new_logistic_label": "物流公司", | ||||
| @@ -206,7 +204,6 @@ | |||||
| "departureDialog_title": "編輯出車時間", | "departureDialog_title": "編輯出車時間", | ||||
| "departureDialog_hint": "套用至此車線所有店鋪列;按上方「儲存更改」寫入後端。", | "departureDialog_hint": "套用至此車線所有店鋪列;按上方「儲存更改」寫入後端。", | ||||
| "seqDialog_title": "編輯裝車順序", | "seqDialog_title": "編輯裝車順序", | ||||
| "seqDialog_hint": "按「儲存更改」後寫入 truck 列。", | |||||
| "logistics_colLaneCount": "{{count}} 條車線", | "logistics_colLaneCount": "{{count}} 條車線", | ||||
| "tooltip_openLaneBoard": "在車線看板開此車線", | "tooltip_openLaneBoard": "在車線看板開此車線", | ||||
| "aria_openLaneBoard": "開啟車線看板", | "aria_openLaneBoard": "開啟車線看板", | ||||
| @@ -220,15 +217,15 @@ | |||||
| "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", | "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", | ||||
| "aria_editLogistics": "編輯物流公司", | "aria_editLogistics": "編輯物流公司", | ||||
| "aria_deleteLogistics": "刪除物流公司", | "aria_deleteLogistics": "刪除物流公司", | ||||
| "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?須按「儲存更改」寫入。", | |||||
| "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?", | |||||
| "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", | "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", | ||||
| "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", | "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", | ||||
| "logistic_btn_apply": "套用", | "logistic_btn_apply": "套用", | ||||
| "tooltip_editDistrict": "編輯地區名稱(按「儲存更改」才寫入)", | |||||
| "tooltip_editDistrict": "編輯地區名稱", | |||||
| "aria_editDistrict": "編輯地區", | "aria_editDistrict": "編輯地區", | ||||
| "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", | "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", | ||||
| "aria_removeEmptyDistrict": "移除空區", | "aria_removeEmptyDistrict": "移除空區", | ||||
| "tooltip_editSeq": "編輯裝車順序(按「儲存更改」寫回)", | |||||
| "tooltip_editSeq": "編輯裝車順序", | |||||
| "aria_editSeq": "編輯裝車順序", | "aria_editSeq": "編輯裝車順序", | ||||
| "diff_moveFrom": "從 {{lane}}", | "diff_moveFrom": "從 {{lane}}", | ||||
| "logistics_dirtyColumnBadge": "有未儲存物流更改", | "logistics_dirtyColumnBadge": "有未儲存物流更改", | ||||
| @@ -312,7 +309,7 @@ | |||||
| "schedule_review_seq": "裝載順序:", | "schedule_review_seq": "裝載順序:", | ||||
| "schedule_drop_hint": "請拖曳店鋪卡片至此車線", | "schedule_drop_hint": "請拖曳店鋪卡片至此車線", | ||||
| "schedule_moved_badge": "移入", | "schedule_moved_badge": "移入", | ||||
| "schedule_drag_seq": "裝載順序 Seq: {{seq}}", | |||||
| "schedule_drag_seq": "裝載順序: {{seq}}", | |||||
| "schedule_seq_edit_btn": "編輯裝載順序", | "schedule_seq_edit_btn": "編輯裝載順序", | ||||
| "schedule_seq_dialog_hint": "變更會加入右側預覽佇列,確認排程後才套用。", | "schedule_seq_dialog_hint": "變更會加入右側預覽佇列,確認排程後才套用。", | ||||
| "schedule_planned_label": "執行預定", | "schedule_planned_label": "執行預定", | ||||
| @@ -320,7 +317,7 @@ | |||||
| "schedule_applied_snackbar": "已登記 {{count}} 筆預約變更並更新看板,請按「儲存更改」寫入後端。", | "schedule_applied_snackbar": "已登記 {{count}} 筆預約變更並更新看板,請按「儲存更改」寫入後端。", | ||||
| "schedule_err_conflict": "部分店舖無法移至目標車線(重複店鋪或草稿列)。", | "schedule_err_conflict": "部分店舖無法移至目標車線(重複店鋪或草稿列)。", | ||||
| "schedule_err_execute_at_past": "排程執行時間已過去,請選擇未來的日期與時間。", | "schedule_err_execute_at_past": "排程執行時間已過去,請選擇未來的日期與時間。", | ||||
| "schedule_err_open_pending": "店舖列 #{{id}} 已有待執行的排程。", | |||||
| "schedule_err_open_pending": "店舖已有待執行的排程。", | |||||
| "schedule_err_duplicate_shop": "目標車線上已有店舖 {{shop}}。", | "schedule_err_duplicate_shop": "目標車線上已有店舖 {{shop}}。", | ||||
| "schedule_err_target_lane_missing": "找不到目標車線 {{lane}}。", | "schedule_err_target_lane_missing": "找不到目標車線 {{lane}}。", | ||||
| "schedule_err_target_lane_empty": "目標車線 {{lane}} 尚無店舖,請先加入店舖。", | "schedule_err_target_lane_empty": "目標車線 {{lane}} 尚無店舖,請先加入店舖。", | ||||
| @@ -329,7 +326,7 @@ | |||||
| "schedule_reschedule_time_adjusted": "原執行時間已過去,已自動調整為 {{at}}。", | "schedule_reschedule_time_adjusted": "原執行時間已過去,已自動調整為 {{at}}。", | ||||
| "schedule_shop_badge": "已排程變更", | "schedule_shop_badge": "已排程變更", | ||||
| "schedule_shop_locked": "排程執行中,此店鋪暫不可手改", | "schedule_shop_locked": "排程執行中,此店鋪暫不可手改", | ||||
| "schedule_retry_rejects_partial": "PARTIAL 排程不可重試,請先還原看板後重新建立排程", | |||||
| "schedule_retry_rejects_partial": "部分排程不可重試,請先還原看板後重新建立排程", | |||||
| "schedule_registered_snackbar": "已登記 {{count}} 筆預約變更,將於執行時間由伺服器自動套用。", | "schedule_registered_snackbar": "已登記 {{count}} 筆預約變更,將於執行時間由伺服器自動套用。", | ||||
| "schedule_import_parse_summary": "解析完成:有效 {{valid}} 筆,錯誤 {{errors}} 筆", | "schedule_import_parse_summary": "解析完成:有效 {{valid}} 筆,錯誤 {{errors}} 筆", | ||||
| "schedule_import_confirm_btn": "確認匯入並建立排程", | "schedule_import_confirm_btn": "確認匯入並建立排程", | ||||
| @@ -349,8 +346,8 @@ | |||||
| "schedule_history_applied_at": "成功執行時間:{{at}}", | "schedule_history_applied_at": "成功執行時間:{{at}}", | ||||
| "schedule_history_view_lines": "檢視店舖明細", | "schedule_history_view_lines": "檢視店舖明細", | ||||
| "schedule_history_no_lines": "尚無明細資料", | "schedule_history_no_lines": "尚無明細資料", | ||||
| "schedule_history_failed_lines": "{{count}} 筆店舖移線失敗", | |||||
| "schedule_history_success_lines": "{{count}} 筆店舖已成功移線", | |||||
| "schedule_history_failed_lines": "{{count}} 筆店舖移動失敗", | |||||
| "schedule_history_success_lines": "{{count}} 筆店舖已成功移動", | |||||
| "schedule_history_apply_now": "立即執行", | "schedule_history_apply_now": "立即執行", | ||||
| "schedule_history_cancel": "取消排程", | "schedule_history_cancel": "取消排程", | ||||
| "schedule_history_archived": "已執行", | "schedule_history_archived": "已執行", | ||||
| @@ -358,8 +355,13 @@ | |||||
| "schedule_history_reschedule": "重新排程", | "schedule_history_reschedule": "重新排程", | ||||
| "schedule_history_ignore": "忽略", | "schedule_history_ignore": "忽略", | ||||
| "schedule_history_status_ignored": "已忽略", | "schedule_history_status_ignored": "已忽略", | ||||
| "schedule_history_status_cancelled": "已取消", | |||||
| "schedule_reschedule_dialog_title": "重新排程", | |||||
| "schedule_reschedule_dialog_subtitle": "選擇新的執行日期與時間,將更新此筆已取消的預約任務。", | |||||
| "schedule_reschedule_dialog_confirm": "更新排程", | |||||
| "schedule_reschedule_created": "排程 #{{id}} 已更新,將於 {{at}} 執行。", | |||||
| "schedule_history_close": "關閉視窗", | "schedule_history_close": "關閉視窗", | ||||
| "PENDING": "待處理", | |||||
| "PENDING": "待執行", | |||||
| "APPLYING": "執行中", | "APPLYING": "執行中", | ||||
| "APPLIED": "已套用", | "APPLIED": "已套用", | ||||
| "PARTIAL": "部分完成", | "PARTIAL": "部分完成", | ||||