From 4d9cbb49dd9fe6a17caca826897c49a4348d8ca4 Mon Sep 17 00:00:00 2001 From: tommy Date: Thu, 11 Jun 2026 16:45:37 +0800 Subject: [PATCH] translate and re-schedule truck --- src/app/api/shop/actions.ts | 12 + src/app/api/shop/client.ts | 8 + src/components/Shop/RouteBoard.tsx | 54 ++-- .../Shop/ScheduleTaskHistoryModal.tsx | 259 +++++++++++++++--- src/i18n/en/routeboard.json | 3 - src/i18n/en/shop.json | 8 +- src/i18n/zh/routeboard.json | 33 +-- src/i18n/zh/shop.json | 38 +-- 8 files changed, 322 insertions(+), 93 deletions(-) diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index 6339c99..8a6ff34 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -855,6 +855,18 @@ export const retryFailedTruckLaneScheduleAction = async ( }); }; +export const reactivateCancelledTruckLaneScheduleAction = async ( + id: number, + body?: RetryFailedTruckLaneScheduleRequest, +): Promise => { + const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/reactivate`; + return serverFetchJson(endpoint, { + method: "POST", + body: JSON.stringify(body ?? {}), + headers: { "Content-Type": "application/json" }, + }); +}; + export const ignoreTruckLaneScheduleAction = async ( id: number, ): Promise => { diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index d7b710f..67963e2 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -38,6 +38,7 @@ import { cancelTruckLaneScheduleAction, applyNowTruckLaneScheduleAction, retryFailedTruckLaneScheduleAction, + reactivateCancelledTruckLaneScheduleAction, ignoreTruckLaneScheduleAction, type RetryFailedTruckLaneScheduleRequest, parseTruckLaneScheduleExcelAction, @@ -255,6 +256,13 @@ export const retryFailedTruckLaneScheduleClient = async ( return await retryFailedTruckLaneScheduleAction(id, body); }; +export const reactivateCancelledTruckLaneScheduleClient = async ( + id: number, + body?: RetryFailedTruckLaneScheduleRequest, +): Promise => { + return await reactivateCancelledTruckLaneScheduleAction(id, body); +}; + export const ignoreTruckLaneScheduleClient = async ( id: number, ): Promise => { diff --git a/src/components/Shop/RouteBoard.tsx b/src/components/Shop/RouteBoard.tsx index 1c609b0..22b51c6 100644 --- a/src/components/Shop/RouteBoard.tsx +++ b/src/components/Shop/RouteBoard.tsx @@ -2425,6 +2425,9 @@ const RouteBoard: React.FC = () => { }; const handleDeleteTruckRow = async (truckRowId: number) => { + if (truckRowId > 0 && scheduledShopIdSet.has(truckRowId)) { + return; + } if (truckRowId < 0) { if (!window.confirm(t("confirm_discardDraftShop"))) return; setError(null); @@ -2488,6 +2491,11 @@ const RouteBoard: React.FC = () => { /** 清空整桶店鋪:與單筆刪除相同,僅標記 dirtyDeletes,按「儲存更改」才 deleteTruckLaneClient */ const handleClearLaneShops = (lane: Lane) => { if (lane.shops.length === 0) return; + if ( + lane.shops.some((s) => s.id > 0 && scheduledShopIdSet.has(s.id)) + ) { + return; + } if ( !window.confirm( t("confirm_clearLane", { @@ -5845,13 +5853,6 @@ const RouteBoard: React.FC = () => { setDistrictEditError(null); }} error={Boolean(districtEditError)} - helperText={ - districtEditError || - (districtEditCtx?.mode === "rename" && - districtEditCtx.oldDisplay === "未分類" - ? t("district_help_null") - : t("district_help_mapped")) - } InputLabelProps={{ shrink: true }} /> @@ -5915,13 +5916,6 @@ const RouteBoard: React.FC = () => { inputProps={{ step: 1 }} sx={{ mt: 1 }} /> - - {t("seqDialog_hint")} - @@ -7843,7 +7837,13 @@ const RouteBoard: React.FC = () => { spacing={0.5} alignItems="center" > - + { }} disabled={ loading || - dirtyDeletes.has(shop.id) + dirtyDeletes.has(shop.id) || + isScheduledMove } > @@ -7944,12 +7945,29 @@ const RouteBoard: React.FC = () => { > {t("btn_addShopToLane")} - + + s.id > 0 && scheduledShopIdSet.has(s.id), + ) + ? t("schedule_shop_locked") + : t("tooltip_clearLaneShops") + } + > 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), + ) + } > diff --git a/src/components/Shop/ScheduleTaskHistoryModal.tsx b/src/components/Shop/ScheduleTaskHistoryModal.tsx index 4914d05..7b420a4 100644 --- a/src/components/Shop/ScheduleTaskHistoryModal.tsx +++ b/src/components/Shop/ScheduleTaskHistoryModal.tsx @@ -14,6 +14,7 @@ import { DialogTitle, IconButton, Stack, + TextField, Typography, } from "@mui/material"; import { @@ -38,16 +39,23 @@ import { applyNowTruckLaneScheduleClient, cancelTruckLaneScheduleClient, createTruckLaneScheduleClient, + reactivateCancelledTruckLaneScheduleClient, getTruckLaneScheduleClient, ignoreTruckLaneScheduleClient, listTruckLaneSchedulesClient, retryFailedTruckLaneScheduleClient, + type TruckLaneScheduleLineRequest, type TruckLaneScheduleLineResponse, type TruckLaneScheduleResponse, } from "@/app/api/shop/client"; import type { ScheduleLaneOption } from "@/components/Shop/ScheduleChangeModal"; import { extractApiErrorMessage } from "@/components/Shop/scheduleClientHelpers"; -import { resolveRescheduleExecuteAt, formatScheduleDisplayDateTime } from "@/components/Shop/scheduleExecuteAt"; +import { + buildExecuteAtIso, + isExecuteAtTooEarly, + resolveRescheduleExecuteAt, + formatScheduleDisplayDateTime, +} from "@/components/Shop/scheduleExecuteAt"; import { buildScheduleLineDescription, } from "@/components/Shop/scheduleLineDisplay"; @@ -84,7 +92,7 @@ function uiStatusFromLines( 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( task: TruckLaneScheduleResponse, detail?: TruckLaneScheduleResponse | null, @@ -93,11 +101,6 @@ function resolveUiStatus( if (st === "CANCELLED") return "cancelled"; if (st === "IGNORED") return "ignored"; - const fromDetailLines = detail?.lines?.length - ? uiStatusFromLines(detail.lines) - : null; - if (fromDetailLines) return fromDetailLines; - if (task.lineCounts) { const fromCounts = uiStatusFromLineCounts(task.lineCounts); if (fromCounts) return fromCounts; @@ -108,6 +111,12 @@ function resolveUiStatus( if (st === "PARTIAL") { 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"; return "pending"; } @@ -118,13 +127,34 @@ function canManagePendingSchedule( ): boolean { const st = String(task.status ?? "").toUpperCase(); 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 ?? []; if (lines.length > 0) { 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 } { @@ -163,6 +193,12 @@ const ScheduleTaskHistoryModal: React.FC = ({ const [actionId, setActionId] = useState(null); const [actionError, setActionError] = useState(null); const [actionNotice, setActionNotice] = useState(null); + const [redoTarget, setRedoTarget] = useState( + null, + ); + const [redoDate, setRedoDate] = useState(""); + const [redoTime, setRedoTime] = useState(""); + const [redoSubmitting, setRedoSubmitting] = useState(false); const laneById = useMemo( () => new Map(lanes.map((l) => [l.id, l])), @@ -218,6 +254,9 @@ const ScheduleTaskHistoryModal: React.FC = ({ setDetailById({}); setActionError(null); setActionNotice(null); + setRedoTarget(null); + setRedoDate(""); + setRedoTime(""); void loadTasks(); }, [open, loadTasks]); @@ -238,15 +277,22 @@ const ScheduleTaskHistoryModal: React.FC = ({ return tasks.filter((task) => { const detail = detailById[task.id]; const ui = resolveUiStatus(task, detail); - if (filter === "all") return ui !== "cancelled"; + if (filter === "all") return true; if (filter === "pending") return ui === "pending"; if (filter === "success") return ui === "success"; return ui === "failed"; }); }, [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); try { const detail = await getTruckLaneScheduleClient(id); @@ -255,8 +301,31 @@ const ScheduleTaskHistoryModal: React.FC = ({ } finally { 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) => { if (expandedId === id) { setExpandedId(null); @@ -271,8 +340,7 @@ const ScheduleTaskHistoryModal: React.FC = ({ setActionError(null); try { await cancelTruckLaneScheduleClient(id); - await onAfterChange?.(); - await loadTasks(); + await reloadAfterMutation(id); } catch (err: unknown) { setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); } finally { @@ -285,8 +353,7 @@ const ScheduleTaskHistoryModal: React.FC = ({ setActionError(null); try { await applyNowTruckLaneScheduleClient(id); - await onAfterChange?.(); - await loadTasks(); + await reloadAfterMutation(id); } catch (err: unknown) { setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); } finally { @@ -294,6 +361,59 @@ const ScheduleTaskHistoryModal: React.FC = ({ } }; + 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) => { if (String(task.status ?? "").toUpperCase() === "PARTIAL") { setActionError(t("schedule_retry_rejects_partial")); @@ -315,18 +435,7 @@ const ScheduleTaskHistoryModal: React.FC = ({ (l) => l.lineStatus === "FAILED" || l.lineStatus === "PENDING", ); 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 }); } if (adjusted) { @@ -334,8 +443,7 @@ const ScheduleTaskHistoryModal: React.FC = ({ t("schedule_reschedule_time_adjusted", { at: executeAt }), ); } - await onAfterChange?.(); - await loadTasks(); + await reloadAfterMutation(task.id); } catch (err: unknown) { setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); } finally { @@ -348,8 +456,7 @@ const ScheduleTaskHistoryModal: React.FC = ({ setActionError(null); try { await ignoreTruckLaneScheduleClient(id); - await onAfterChange?.(); - await loadTasks(); + await reloadAfterMutation(id); } catch (err: unknown) { setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic")); } finally { @@ -452,6 +559,22 @@ const ScheduleTaskHistoryModal: React.FC = ({ /> ); } + if (ui === "cancelled") { + return ( + } + label={t("schedule_history_status_cancelled")} + sx={{ + fontWeight: 700, + bgcolor: "grey.100", + color: "text.secondary", + border: 1, + borderColor: "grey.400", + }} + /> + ); + } return ( = ({ {t("schedule_history_status_ignored")} )} + {ui === "cancelled" && ( + + )} @@ -988,6 +1129,58 @@ const ScheduleTaskHistoryModal: React.FC = ({ {t("schedule_history_close")} + + + + {t("schedule_reschedule_dialog_title")} + + + + setRedoDate(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + setRedoTime(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + + + + + + ); }; diff --git a/src/i18n/en/routeboard.json b/src/i18n/en/routeboard.json index f2de673..453fad3 100644 --- a/src/i18n/en/routeboard.json +++ b/src/i18n/en/routeboard.json @@ -175,8 +175,6 @@ "district_dialog_edit": "Edit district", "district_name_label": "District display name", "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_seqLabel": "Load sequence (Seq)", "route_new_code_label": "Lane code", @@ -206,7 +204,6 @@ "departureDialog_title": "Edit departure time", "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", "seqDialog_title": "Edit load sequence", - "seqDialog_hint": "Press \"Save changes\" to persist to truck rows.", "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.", "tooltip_openLaneBoard": "Open this lane on the route board", diff --git a/src/i18n/en/shop.json b/src/i18n/en/shop.json index 72449c8..502fc89 100644 --- a/src/i18n/en/shop.json +++ b/src/i18n/en/shop.json @@ -196,8 +196,6 @@ "district_err_exists": "This district already exists", "district_err_name": "Enter a district name", "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_ph": "Blank means \"Unclassified\"", "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_status_failed": "Failed", "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_success": "Succeeded", "schedule_history_subtitle": "Monitor, run, or troubleshoot scheduled shop-lane changes", @@ -406,7 +409,6 @@ "schedule_tab_import": "Import route Excel", "schedule_tab_manual": "Drag scheduling", "schedule_target_unset": "Not set", - "seqDialog_hint": "Press \"Save changes\" to persist to truck rows.", "seqDialog_title": "Edit load sequence", "seq_edit_departureLabel": "Departure time", "seq_edit_seqLabel": "Load sequence (Seq)", diff --git a/src/i18n/zh/routeboard.json b/src/i18n/zh/routeboard.json index 2192651..cbced1c 100644 --- a/src/i18n/zh/routeboard.json +++ b/src/i18n/zh/routeboard.json @@ -34,7 +34,7 @@ "exportRoutes": "匯出車線", "routeReport": "車線報告", "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", - "departureTooltipEditSave": "編輯出車時間(按「儲存更改」寫回)", + "departureTooltipEditSave": "編輯出車時間", "departureEditAria": "編輯出車時間", "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", "cancel": "取消", @@ -80,9 +80,9 @@ "confirm_restoreDiscardsEdits": "排程版本還原會捨棄目前看板上其他未儲存變更(拖曳、刪除、新增店鋪/車線、物流欄等)。確定繼續?", "diff_restoreScheduled": "已排程還原至版本 #{{versionId}};請按「儲存更改」寫入後端。", "diff_restoreAlreadyPending": "此版本已在排程還原中;請按「儲存更改」套用。", - "restore_applied": "已從 snapshot 還原並重新載入看板。", - "restore_appliedDroppedStaging": "已套用 snapshot 還原;本次儲存略過其他暫存變更(請重新編輯)。", - "confirm_restoreSaveWillDropStaging": "儲存時將先套用 snapshot 還原,本次其他暫存變更會被略過。確定繼續?", + "restore_applied": "已從 版本還原並重新載入看板。", + "restore_appliedDroppedStaging": "已套用 版本還原;本次儲存略過其他暫存變更(請重新編輯)。", + "confirm_restoreSaveWillDropStaging": "儲存時將先套用 版本還原,本次其他暫存變更會被略過。確定繼續?", "diff_noOlderCompare": "沒有上一筆版本可比較(請選擇較新的版本)", "logistic_needMasterTpl": "「{{name}}」尚無對應物流公司,請先用「新增物流商」建立。", "diffField_logisticsCompany": "物流公司", @@ -140,11 +140,11 @@ "diff_shopList_title": "店鋪異動清單", "diff_staged_serverCountsOnly": "上列四格為「後端相鄰兩版快照」統計,不含看板上尚未儲存的編輯。", "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", - "diff_staged_section_title": "看板未儲存/已排程(尚未寫入後端)", + "diff_staged_section_title": "看板未儲存/已排程", "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端快照)混淆。", "diff_staged_tag_unsaved": "未儲存", "diff_staged_tag_scheduled": "已排程", - "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}(須按「儲存更改」才會呼叫 restore)。", + "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}。", "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", "diff_staged_newLane": "新增車線(未儲存):{{lane}}", "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", @@ -154,9 +154,9 @@ "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{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": "匯入將取代目前看板上未儲存的變更,是否繼續?", - "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。按「儲存更改」才會寫入後端。", + "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。", "err_importEmpty": "匯入檔案無有效車線資料列", "diff_logisticMaster_section": "物流公司異動", "diff_logisticMaster_added": "新增", @@ -175,10 +175,8 @@ "district_dialog_edit": "編輯地區", "district_name_label": "地區顯示名稱", "district_name_ph": "空白表示「未分類」", - "district_help_null": "未分類對應後端 districtReference 為 null", - "district_help_mapped": "顯示名稱經 toDistrictRawValue 寫入各店鋪 districtReference;按「儲存更改」才打 API", "seq_edit_departureLabel": "出車時間", - "seq_edit_seqLabel": "裝車順序 (Seq)", + "seq_edit_seqLabel": "裝車順序", "route_new_code_label": "車線編號", "route_new_time_label": "出車時間", "route_new_logistic_label": "物流公司", @@ -206,30 +204,29 @@ "departureDialog_title": "編輯出車時間", "departureDialog_hint": "套用至此車線所有店鋪列。", "seqDialog_title": "編輯裝車順序", - "seqDialog_hint": "按「儲存更改」後寫入。", "logistics_colLaneCount": "{{count}} 條車線", "logistics_masterNoLanes": "主檔已建立,尚無綁定車線;至「車線看板」新增/編輯車線時可填此公司名稱。", "tooltip_openLaneBoard": "在車線看板開此車線", "aria_openLaneBoard": "開啟車線看板", "tooltip_removeFromLane": "從此車線移除", - "tooltip_clearLaneShops": "清空此車線所有店鋪(按「儲存更改」才寫入)", + "tooltip_clearLaneShops": "清空此車線所有店鋪", "tooltip_pickLane": "選擇車線(加入看板勾選並捲動到該欄)", "aria_pickLane": "選擇車線", "aria_searchLanes": "搜索車線", "logistics_colShopCount": "{{count}} 家店鋪", - "tooltip_editLogisticsDb": "編輯物流公司(須按「儲存更改」寫入)", - "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", + "tooltip_editLogisticsDb": "編輯物流公司", + "tooltip_deleteLogistics": "刪除物流公司", "aria_editLogistics": "編輯物流公司", "aria_deleteLogistics": "刪除物流公司", - "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?須按「儲存更改」寫入。", + "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?", "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", "logistic_btn_apply": "套用", - "tooltip_editDistrict": "編輯地區名稱(按「儲存更改」才寫入)", + "tooltip_editDistrict": "編輯地區名稱", "aria_editDistrict": "編輯地區", "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", "aria_removeEmptyDistrict": "移除空區", - "tooltip_editSeq": "編輯裝車順序(按「儲存更改」寫回)", + "tooltip_editSeq": "編輯裝車順序", "aria_editSeq": "編輯裝車順序", "diff_moveFrom": "從 {{lane}}", "logistics_dirtyColumnBadge": "有未儲存物流更改", diff --git a/src/i18n/zh/shop.json b/src/i18n/zh/shop.json index 483cd88..2964d66 100644 --- a/src/i18n/zh/shop.json +++ b/src/i18n/zh/shop.json @@ -34,7 +34,7 @@ "exportRoutes": "匯出車線", "routeReport": "車線報告", "departureTooltipNeedShops": "先新增店鋪才能設定出車時間", - "departureTooltipEditSave": "編輯出車時間(按「儲存更改」寫回)", + "departureTooltipEditSave": "編輯出車時間", "departureEditAria": "編輯出車時間", "saveDisabledTooltip": "請先拖曳、編輯出車時間/裝車順序/物流商等變更後再儲存", "cancel": "取消", @@ -139,11 +139,11 @@ "diff_summary_fieldChange": "欄位變更", "diff_shopList_title": "店鋪變更清單", "diff_staged_boardPendingLine": "看板另有 {{count}} 筆未儲存/已排程項(見下方清單)。", - "diff_staged_section_title": "看板未儲存/已排程(尚未寫入後端)", + "diff_staged_section_title": "看板未儲存/已排程", "diff_staged_section_subtitle": "下列與「儲存更改」後才會落庫的狀態一致;與上方版本 diff 分開列,避免與 Excel(僅後端版本)混淆。", "diff_staged_tag_unsaved": "未儲存", "diff_staged_tag_scheduled": "已排程", - "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}(須按「儲存更改」才會呼叫 restore)。", + "diff_staged_restoreScheduled": "已排程還原至版本 #{{versionId}}。", "diff_staged_deleteUnknown": "刪除 truck id={{id}}(未儲存;明細請先儲存或取消後重試)", "diff_staged_newLane": "新增車線(未儲存):{{lane}}", "diff_staged_laneLogistic": "整線物流(未儲存):{{lane}} → {{company}}", @@ -153,9 +153,9 @@ "diff_staged_shopDistrictOnly": "{{name}}({{code}})— 車線 {{lane}}:地區 {{from}}→{{to}}(未儲存;按「儲存更改」寫入)", "diff_staged_pendingLogisticMaster": "新增物流公司(未落庫):{{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": "匯入將取代目前看板上未儲存的變更,是否繼續?", - "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。按「儲存更改」才會寫入後端。", + "import_staged_preview": "已載入匯入預覽:{{file}}({{sheets}} 表 / {{rows}} 列)。", "err_importEmpty": "匯入檔案無有效車線資料列", "diff_logisticMaster_section": "物流公司變更", "diff_logisticMaster_added": "新增", @@ -175,10 +175,8 @@ "district_dialog_edit": "編輯地區", "district_name_label": "地區顯示名稱", "district_name_ph": "空白表示「未分類」", - "district_help_null": "未分類對應後端 districtReference 為 null", - "district_help_mapped": "顯示名稱經 toDistrictRawValue 寫入各店鋪 districtReference;按「儲存更改」才打 API", "seq_edit_departureLabel": "出車時間", - "seq_edit_seqLabel": "裝車順序 (Seq)", + "seq_edit_seqLabel": "裝車順序", "route_new_code_label": "車線編號", "route_new_time_label": "出車時間", "route_new_logistic_label": "物流公司", @@ -206,7 +204,6 @@ "departureDialog_title": "編輯出車時間", "departureDialog_hint": "套用至此車線所有店鋪列;按上方「儲存更改」寫入後端。", "seqDialog_title": "編輯裝車順序", - "seqDialog_hint": "按「儲存更改」後寫入 truck 列。", "logistics_colLaneCount": "{{count}} 條車線", "tooltip_openLaneBoard": "在車線看板開此車線", "aria_openLaneBoard": "開啟車線看板", @@ -220,15 +217,15 @@ "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", "aria_editLogistics": "編輯物流公司", "aria_deleteLogistics": "刪除物流公司", - "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?須按「儲存更改」寫入。", + "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?", "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", "logistic_btn_apply": "套用", - "tooltip_editDistrict": "編輯地區名稱(按「儲存更改」才寫入)", + "tooltip_editDistrict": "編輯地區名稱", "aria_editDistrict": "編輯地區", "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)", "aria_removeEmptyDistrict": "移除空區", - "tooltip_editSeq": "編輯裝車順序(按「儲存更改」寫回)", + "tooltip_editSeq": "編輯裝車順序", "aria_editSeq": "編輯裝車順序", "diff_moveFrom": "從 {{lane}}", "logistics_dirtyColumnBadge": "有未儲存物流更改", @@ -312,7 +309,7 @@ "schedule_review_seq": "裝載順序:", "schedule_drop_hint": "請拖曳店鋪卡片至此車線", "schedule_moved_badge": "移入", - "schedule_drag_seq": "裝載順序 Seq: {{seq}}", + "schedule_drag_seq": "裝載順序: {{seq}}", "schedule_seq_edit_btn": "編輯裝載順序", "schedule_seq_dialog_hint": "變更會加入右側預覽佇列,確認排程後才套用。", "schedule_planned_label": "執行預定", @@ -320,7 +317,7 @@ "schedule_applied_snackbar": "已登記 {{count}} 筆預約變更並更新看板,請按「儲存更改」寫入後端。", "schedule_err_conflict": "部分店舖無法移至目標車線(重複店鋪或草稿列)。", "schedule_err_execute_at_past": "排程執行時間已過去,請選擇未來的日期與時間。", - "schedule_err_open_pending": "店舖列 #{{id}} 已有待執行的排程。", + "schedule_err_open_pending": "店舖已有待執行的排程。", "schedule_err_duplicate_shop": "目標車線上已有店舖 {{shop}}。", "schedule_err_target_lane_missing": "找不到目標車線 {{lane}}。", "schedule_err_target_lane_empty": "目標車線 {{lane}} 尚無店舖,請先加入店舖。", @@ -329,7 +326,7 @@ "schedule_reschedule_time_adjusted": "原執行時間已過去,已自動調整為 {{at}}。", "schedule_shop_badge": "已排程變更", "schedule_shop_locked": "排程執行中,此店鋪暫不可手改", - "schedule_retry_rejects_partial": "PARTIAL 排程不可重試,請先還原看板後重新建立排程", + "schedule_retry_rejects_partial": "部分排程不可重試,請先還原看板後重新建立排程", "schedule_registered_snackbar": "已登記 {{count}} 筆預約變更,將於執行時間由伺服器自動套用。", "schedule_import_parse_summary": "解析完成:有效 {{valid}} 筆,錯誤 {{errors}} 筆", "schedule_import_confirm_btn": "確認匯入並建立排程", @@ -349,8 +346,8 @@ "schedule_history_applied_at": "成功執行時間:{{at}}", "schedule_history_view_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_cancel": "取消排程", "schedule_history_archived": "已執行", @@ -358,8 +355,13 @@ "schedule_history_reschedule": "重新排程", "schedule_history_ignore": "忽略", "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": "關閉視窗", - "PENDING": "待處理", + "PENDING": "待執行", "APPLYING": "執行中", "APPLIED": "已套用", "PARTIAL": "部分完成",