From a688bdafd62fa8d280d32f6723e9e3094cc123d7 Mon Sep 17 00:00:00 2001 From: tommy Date: Mon, 26 Oct 2026 17:24:46 +0800 Subject: [PATCH] truckname rename --- src/app/api/shop/actions.ts | 72 ++++++ src/app/api/shop/client.ts | 21 +- src/components/Shop/RouteBoard.tsx | 369 +++++++++++++++++++++++++---- src/i18n/en/shop.json | 23 +- src/i18n/zh/shop.json | 23 +- 5 files changed, 463 insertions(+), 45 deletions(-) diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index abea44e1..d26404e0 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -924,4 +924,76 @@ export const importTruckLaneScheduleExcelAction = async ( ); } return (await response.json()) as TruckLaneScheduleResponse[]; +}; + +export interface PreviewTruckLaneRenameRequest { + truckLanceCode: string; + storeId: string; + remark?: string | null; +} + +export interface PreviewTruckLaneRenameResponse { + unfinishedCount: number; + completedCount: number; + handlerNames: string[]; + openScheduleCount: number; +} + +export interface RenameTruckLaneRequest { + truckLanceCode: string; + storeId: string; + remark?: string | null; + newTruckLanceCode: string; + departureTimeChanged: boolean; + newDepartureTime?: string | null; +} + +export interface RenameTruckLaneResponse { + truckLanceCode: string; + ticketCount: number; + scheduleLineCount: number; +} + +export interface TruckLaneRenameLogResponse { + id: number; + created?: string | null; + createdBy?: string | null; + oldCode: string; + newCode: string; + storeId: string; + remark?: string | null; + oldDepartureTime?: string | null; + newDepartureTime?: string | null; + ticketCount: number; +} + +export const previewTruckLaneRenameAction = async ( + data: PreviewTruckLaneRenameRequest, +) => { + return serverFetchJson( + `${BASE_API_URL}/truck/previewTruckLaneRename`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}; + +export const renameTruckLaneAction = async (data: RenameTruckLaneRequest) => { + return serverFetchJson( + `${BASE_API_URL}/truck/renameTruckLane`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); +}; + +export const listTruckLaneRenameLogsAction = async () => { + return serverFetchJson( + `${BASE_API_URL}/truck/truckLaneRenameLog`, + { method: "GET", headers: { "Content-Type": "application/json" } }, + ); }; \ No newline at end of file diff --git a/src/app/api/shop/client.ts b/src/app/api/shop/client.ts index 67963e27..80356270 100644 --- a/src/app/api/shop/client.ts +++ b/src/app/api/shop/client.ts @@ -66,7 +66,12 @@ import { type SaveTruckRequest, type UpdateTruckShopDetailsRequest, type CreateTruckWithoutShopRequest, - type MessageResponse + type MessageResponse, + previewTruckLaneRenameAction, + renameTruckLaneAction, + listTruckLaneRenameLogsAction, + type PreviewTruckLaneRenameRequest, + type RenameTruckLaneRequest, } from "./actions"; export const fetchAllShopsClient = async (params?: Record) => { @@ -276,6 +281,20 @@ export const parseTruckLaneScheduleExcelClient = async ( return await parseTruckLaneScheduleExcelAction(formData, defaultExecuteAt); }; +export const previewTruckLaneRenameClient = async ( + data: PreviewTruckLaneRenameRequest, +) => { + return await previewTruckLaneRenameAction(data); +}; + +export const renameTruckLaneClient = async (data: RenameTruckLaneRequest) => { + return await renameTruckLaneAction(data); +}; + +export const listTruckLaneRenameLogsClient = async () => { + return await listTruckLaneRenameLogsAction(); +}; + export const importTruckLaneScheduleExcelClient = async ( formData: FormData, defaultExecuteAt?: string | null, diff --git a/src/components/Shop/RouteBoard.tsx b/src/components/Shop/RouteBoard.tsx index 0dc16fcb..45fc18e9 100644 --- a/src/components/Shop/RouteBoard.tsx +++ b/src/components/Shop/RouteBoard.tsx @@ -97,6 +97,9 @@ import { createTruckClient, createTruckWithoutShopClient, deleteTruckLaneClient, + previewTruckLaneRenameClient, + renameTruckLaneClient, + listTruckLaneRenameLogsClient, } from "@/app/api/shop/client"; import { deleteLogisticClient, @@ -113,6 +116,7 @@ import type { SaveTruckLane, Truck, TruckLaneVersionDiffLine, + TruckLaneRenameLogResponse, } from "@/app/api/shop/actions"; import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; import { @@ -132,6 +136,7 @@ import { import { computeTruckLaneWarnings, appendSyntheticPendingShopRow, + parseWeekdayFromTruckLanceCode, type TruckLaneWarning, type TruckLaneWarningInputRow, type TruckLaneWarningLaneRef, @@ -220,6 +225,12 @@ type Lane = { storeId: string; remark?: string | null; shops: ShopCard[]; + /** Server name before a local rename. Absent until the user edits the name. */ + originalTruckLanceCode?: string; + /** Server departure time before a local time edit in the same unsaved session. */ + originalStartTime?: string; + /** False for a lane that exists only in this unsaved session. */ + persistedLane?: boolean; }; type PendingNewLane = { @@ -693,9 +704,31 @@ function buildLaneFromTruckRows( storeId: storeId || "2F", remark, shops, + persistedLane: true, }; } +function laneRenamePending(lane: Lane): boolean { + if (lane.persistedLane === false) return false; + const orig = String(lane.originalTruckLanceCode ?? "").trim(); + const cur = String(lane.truckLanceCode ?? "").trim(); + return orig !== "" && orig !== cur; +} + +function renameErrorText(raw: string, t: TFunction): string { + const codes = [ + "LANE_NAME_EMPTY", + "LANE_NAME_TOO_LONG", + "LANE_NAME_RESERVED", + "LANE_NAME_TAKEN", + "LANE_NAME_REMARK_AMBIGUOUS", + "LANE_NOT_FOUND", + "LANE_TIME_INVALID", + ]; + const code = codes.find((c) => raw.includes(c)); + return code ? t(`renameErr_${code}`) : raw; +} + async function fetchLaneByKey( truckLanceCode: string, remark: string | null, @@ -939,6 +972,11 @@ const RouteBoard: React.FC = () => { null, ); const [departureEditDraft, setDepartureEditDraft] = useState(""); + const [departureEditNameDraft, setDepartureEditNameDraft] = useState(""); + const [departureEditBusy, setDepartureEditBusy] = useState(false); + const [renameLogs, setRenameLogs] = useState( + [], + ); const [seqEditTarget, setSeqEditTarget] = useState<{ laneId: string; shopId: number; @@ -1234,6 +1272,7 @@ const RouteBoard: React.FC = () => { storeId: storeNorm, remark: remarkRaw, shops: [], + persistedLane: false, }; }, [logisticRowsEffective], @@ -2922,6 +2961,7 @@ const RouteBoard: React.FC = () => { pendingLogisticMasterDeletes.size > 0 || pendingImportMeta != null || pendingRestoreVersionId != null || + lanes.some(laneRenamePending) || Object.values(pendingEmptyDistrictsByLane).some( (a) => (a?.length ?? 0) > 0, ); @@ -3193,7 +3233,8 @@ const RouteBoard: React.FC = () => { pendingLogisticMasterEditsSnapshot.size > 0 || pendingLogisticMasterDeletesSnapshot.size > 0 || pendingImportFile != null || - pendingRestoreId != null; + pendingRestoreId != null || + lanesRef.current.some(laneRenamePending); const hasPendingEmptyDistrictsOnly = !hasPersistedWork && Object.values(pendingEmptyDistrictsByLane).some( @@ -3267,6 +3308,54 @@ const RouteBoard: React.FC = () => { return; } + const renameTargets = lanesRef.current.filter(laneRenamePending); + if (renameTargets.length > 0) { + const renamed: string[] = []; + let renameFailure: string | null = null; + for (const lane of renameTargets) { + const orig = String(lane.originalTruckLanceCode ?? "").trim(); + const timeBaseline = lane.originalStartTime ?? lane.startTime; + const departureTimeChanged = + parseTimeForBackend(timeBaseline) !== parseTimeForBackend(lane.startTime); + try { + await renameTruckLaneClient({ + truckLanceCode: orig, + storeId: normalizeStoreId(lane.storeId), + remark: lane.remark ?? null, + newTruckLanceCode: lane.truckLanceCode.trim(), + departureTimeChanged, + newDepartureTime: departureTimeChanged ? lane.startTime : null, + }); + renamed.push(`${orig} → ${lane.truckLanceCode.trim()}`); + const committed = lanesRef.current.map((row) => + row.id === lane.id + ? { + ...row, + originalTruckLanceCode: row.truckLanceCode, + originalStartTime: row.startTime, + } + : row, + ); + lanesRef.current = committed; + setLanes(committed); + } catch (e: any) { + renameFailure = `${lane.truckLanceCode.trim()}:${renameErrorText( + e?.message ?? String(e), + t, + )}`; + break; + } + } + if (renameFailure) { + const ok = renamed.length > 0 ? renamed.join("、") : t("rename_none"); + setSaveResult({ + ok: false, + message: t("rename_savePartial", { ok, fail: renameFailure }), + }); + return; + } + } + if (pendingLogisticMasterEditsSnapshot.size > 0) { for (const [id, req] of Array.from( pendingLogisticMasterEditsSnapshot.entries(), @@ -3657,47 +3746,188 @@ const RouteBoard: React.FC = () => { const openDepartureEdit = (lane: Lane) => { setDepartureEditDraft(toTimeInputValue(lane.startTime)); + setDepartureEditNameDraft(lane.truckLanceCode); setDepartureEditLaneId(lane.id); }; - const applyDepartureEdit = () => { - if (!departureEditLaneId) return; - const backendTime = parseTimeForBackend(departureEditDraft); - const targetLane = lanesRef.current.find( - (l) => l.id === departureEditLaneId, + const retargetLaneId = (oldId: string, newId: string, newCode: string) => { + if (oldId === newId) return; + setDirtyMoves((prev) => { + const next = new Map(prev); + for (const [shopId, laneId] of next) { + if (laneId === oldId) next.set(shopId, newId); + } + return next; + }); + setPendingShopAdds((prev) => + prev.map((row) => (row.laneId === oldId ? { ...row, laneId: newId } : row)), ); - const shopIds = targetLane?.shops.map((s) => s.id) ?? []; - if (shopIds.length === 0) { + setPendingEmptyDistrictsByLane((prev) => { + if (!(oldId in prev)) return prev; + const next = { ...prev, [newId]: prev[oldId] }; + delete next[oldId]; + return next; + }); + setSelectedLaneIds((prev) => prev.map((id) => (id === oldId ? newId : id))); + setPendingNewLanes((prev) => + prev.map((row) => + row.laneKey === oldId + ? { + laneKey: newId, + payload: { ...row.payload, truckLanceCode: newCode }, + } + : row, + ), + ); + if (laneLogisticBaselineRef.current.has(oldId)) { + const value = laneLogisticBaselineRef.current.get(oldId) ?? null; + laneLogisticBaselineRef.current.delete(oldId); + laneLogisticBaselineRef.current.set(newId, value); + } + if (laneDisplayOverlayRef.current.has(oldId)) { + const value = laneDisplayOverlayRef.current.get(oldId); + laneDisplayOverlayRef.current.delete(oldId); + if (value) laneDisplayOverlayRef.current.set(newId, value); + } + if (lanesNeedingRefreshOnSaveRef.current.delete(oldId)) { + lanesNeedingRefreshOnSaveRef.current.add(newId); + } + }; + + const applyDepartureEdit = async () => { + if (!departureEditLaneId || departureEditBusy) return; + const backendTime = parseTimeForBackend(departureEditDraft); + const targetLane = lanesRef.current.find((l) => l.id === departureEditLaneId); + if (!targetLane) { closeDepartureEdit(); return; } - const nextLanes = lanesRef.current.map((lane) => - lane.id !== departureEditLaneId - ? lane - : { - ...lane, - startTime: backendTime, - shops: lane.shops.map((s) => ({ - ...s, - departureTime: backendTime, - })), - }, - ); - const wr = computeTruckLaneWarnings(lanesToWarningInputRows(nextLanes)); - if (wr.warnings.length > 0) { - const ok = window.confirm( - t("confirm_departureConflict", { count: wr.warnings.length }), - ); - if (!ok) return; + const newCode = departureEditNameDraft.trim(); + const oldCode = targetLane.truckLanceCode.trim(); + if (!newCode) { + window.alert(t("renameErr_LANE_NAME_EMPTY")); + return; } - setLanes(nextLanes); - setDirtyMoves((prev) => { - const next = new Map(prev); - for (const id of shopIds) { - if (id > 0) next.set(id, departureEditLaneId); + if (newCode.length > 50) { + window.alert(t("renameErr_LANE_NAME_TOO_LONG")); + return; + } + if (newCode === "車線-X" || oldCode === "車線-X") { + window.alert(t("renameErr_LANE_NAME_RESERVED")); + return; + } + const nameChanged = newCode !== oldCode; + const shopIds = targetLane.shops.map((s) => s.id); + if (!nameChanged && shopIds.length === 0) { + closeDepartureEdit(); + return; + } + if (nameChanged) { + const floor = normalizeStoreId(targetLane.storeId); + const remarkKey = + floor === "4F" ? String(targetLane.remark ?? "").trim() : ""; + const clash = lanesRef.current.find((lane) => { + if (lane.id === targetLane.id) return false; + if (normalizeStoreId(lane.storeId) !== floor) return false; + const otherRemark = + floor === "4F" ? String(lane.remark ?? "").trim() : ""; + const otherCode = lane.truckLanceCode.trim(); + if (otherRemark === remarkKey && otherCode.toLowerCase() === newCode.toLowerCase()) { + return true; + } + return otherRemark !== remarkKey && otherCode === newCode; + }); + if (clash) { + const sameRemark = + floor !== "4F" || String(clash.remark ?? "").trim() === remarkKey; + window.alert( + sameRemark + ? t("renameErr_LANE_NAME_TAKEN") + : t("renameErr_LANE_NAME_REMARK_AMBIGUOUS"), + ); + return; } - return next; + if (floor === "4F") { + const before = parseWeekdayFromTruckLanceCode(oldCode); + const after = parseWeekdayFromTruckLanceCode(newCode); + if (before !== after) { + if (!window.confirm(t("confirm_weekdayToken"))) return; + } + } + if (targetLane.persistedLane !== false) { + setDepartureEditBusy(true); + try { + const preview = await previewTruckLaneRenameClient({ + truckLanceCode: targetLane.originalTruckLanceCode?.trim() || oldCode, + storeId: floor, + remark: targetLane.remark ?? null, + }); + const handlers = + preview.handlerNames.length > 0 + ? preview.handlerNames.slice(0, 8).join("、") + : t("rename_noHandlers"); + const ok = window.confirm( + t("confirm_renameLane", { + from: targetLane.originalTruckLanceCode?.trim() || oldCode, + to: newCode, + unfinished: preview.unfinishedCount, + completed: preview.completedCount, + handlers, + schedules: preview.openScheduleCount, + }), + ); + if (!ok) return; + } catch (e: any) { + window.alert( + renameErrorText(e?.message ?? String(e), t) || t("renameErr_preview"), + ); + return; + } finally { + setDepartureEditBusy(false); + } + } + } + const newId = nameChanged + ? encodeLaneId(newCode, targetLane.remark ?? null) + : targetLane.id; + const nextLanes = lanesRef.current.map((lane) => { + if (lane.id !== targetLane.id) return lane; + const timeChanged = toTimeInputValue(lane.startTime) !== departureEditDraft; + return { + ...lane, + id: newId, + truckLanceCode: nameChanged ? newCode : lane.truckLanceCode, + originalTruckLanceCode: nameChanged + ? lane.originalTruckLanceCode ?? oldCode + : lane.originalTruckLanceCode, + originalStartTime: timeChanged + ? lane.originalStartTime ?? lane.startTime + : lane.originalStartTime, + startTime: backendTime, + shops: lane.shops.map((s) => ({ ...s, departureTime: backendTime })), + }; }); + if (shopIds.length > 0) { + const wr = computeTruckLaneWarnings(lanesToWarningInputRows(nextLanes)); + if (wr.warnings.length > 0) { + const ok = window.confirm( + t("confirm_departureConflict", { count: wr.warnings.length }), + ); + if (!ok) return; + } + } + lanesRef.current = nextLanes; + setLanes(nextLanes); + if (nameChanged) retargetLaneId(targetLane.id, newId, newCode); + if (shopIds.length > 0 && (nameChanged || toTimeInputValue(targetLane.startTime) !== departureEditDraft)) { + setDirtyMoves((prev) => { + const next = new Map(prev); + for (const id of shopIds) { + if (id > 0) next.set(id, newId); + } + return next; + }); + } setSaveResult(null); closeDepartureEdit(); }; @@ -3832,6 +4062,9 @@ const RouteBoard: React.FC = () => { setLoadingVersions(true); try { const arr = await refreshLogVersions(); + void listTruckLaneRenameLogsClient() + .then((rows) => setRenameLogs(Array.isArray(rows) ? rows : [])) + .catch(() => setRenameLogs([])); const head = resolveHeadVersionId(arr); setSelectedLogVersionId(head); if (head != null) { @@ -3922,6 +4155,8 @@ const RouteBoard: React.FC = () => { if (hasOtherPending && !window.confirm(t("confirm_restoreDiscardsEdits"))) return; + if (!window.confirm(t("confirm_restoreKeepsTickets"))) return; + if (!hasOtherPending && pendingRestoreVersionId != null) { laneDisplayOverlayRef.current.clear(); setPendingRestoreVersionId(versionId); @@ -4169,11 +4404,7 @@ const RouteBoard: React.FC = () => { {t("Departure")}: {lane.startTime || "-"} { e.stopPropagation(); openDepartureEdit(lane); }} - disabled={loading || lane.shops.length === 0} + disabled={loading} aria-label={t("departureEditAria")} > @@ -4710,6 +4941,33 @@ const RouteBoard: React.FC = () => { + {renameLogs.length > 0 && ( + + + {t("renameLog_title")} + + {renameLogs.slice(0, 8).map((row) => ( + + {t("renameLog_line", { + when: row.created ?? "", + who: row.createdBy ?? "", + from: row.oldCode, + to: row.newCode, + floor: row.storeId, + tickets: row.ticketCount, + })} + + ))} + + )} {loadingVersions ? ( { > {t("departureDialog_title")} + setDepartureEditNameDraft(e.target.value)} + inputProps={{ maxLength: 50 }} + sx={{ mt: 1 }} + /> + {departureEditLaneId && + normalizeStoreId( + lanes.find((l) => l.id === departureEditLaneId)?.storeId, + ) === "4F" && + String( + lanes.find((l) => l.id === departureEditLaneId)?.remark ?? "", + ).trim() !== "" && ( + + {t("departureDialog_remarkReadonly")}: + {String( + lanes.find((l) => l.id === departureEditLaneId)?.remark ?? "", + ).trim()} + + )} { - - + diff --git a/src/i18n/en/shop.json b/src/i18n/en/shop.json index d0fdfb34..908e6348 100644 --- a/src/i18n/en/shop.json +++ b/src/i18n/en/shop.json @@ -132,8 +132,27 @@ "confirm_restoreDiscardsEdits": "Scheduling a version restore will discard other unsaved board changes (drags, deletes, pending shops/lanes, logistics fields, etc.). Continue?", "confirm_restoreSaveWillDropStaging": "Save will apply the snapshot restore first; other staged edits in this save will be skipped. Continue?", "confirm_schedule_removeShop": "Remove this shop from the lane? It will be scheduled for deletion when you submit.", - "departureDialog_hint": "Applies to all shop rows on this lane; press \"Save changes\" above to persist.", - "departureDialog_title": "Edit departure time", + "departureDialog_hint": "Name and departure apply to every master row on this lane. Press \"Save changes\" to write them. Completed delivery tickets keep the old name.", + "departureDialog_title": "Edit lane", + "departureDialog_nameLabel": "Lane name", + "departureDialog_remarkReadonly": "Remark (not edited here)", + "laneEditTooltip": "Edit lane name and departure time (written when you press Save changes)", + "confirm_weekdayToken": "The weekday token in the lane name will change or become unreadable. 4F assignment matches Mon–Sun inside the name. Apply anyway?", + "confirm_renameLane": "Rename \"{{from}}\" to \"{{to}}\".\n{{unfinished}} unfinished delivery ticket(s) will follow (handlers: {{handlers}}).\n{{completed}} completed ticket(s) keep the old name.\n{{schedules}} open schedule line(s) will change from/to.\nNothing is written until you press Save changes.", + "confirm_restoreKeepsTickets": "Restore puts lane master data, and open schedules that point at the current name, back to this snapshot. Unfinished delivery tickets stay on the current name.", + "rename_noHandlers": "none", + "rename_none": "none", + "rename_savePartial": "Renamed: {{ok}}. Failed: {{fail}}. The failed lane was not written. Successful lanes stay renamed. Refresh, then save again.", + "renameErr_preview": "Could not load ticket and schedule counts, so this edit was not applied.", + "renameErr_LANE_NAME_EMPTY": "Lane name cannot be blank", + "renameErr_LANE_NAME_TOO_LONG": "Lane name is limited to 50 characters", + "renameErr_LANE_NAME_RESERVED": "\"車線-X\" cannot be used or renamed", + "renameErr_LANE_NAME_TAKEN": "This floor and remark already has that lane", + "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "This floor already has the same name with a different remark. Delivery tickets cannot tell them apart.", + "renameErr_LANE_NOT_FOUND": "No live lane rows were found, so nothing was written", + "renameErr_LANE_TIME_INVALID": "Departure time is not a valid time", + "renameLog_title": "Lane renames", + "renameLog_line": "{{when}} {{who}}: {{from}} → {{to}} ({{floor}}, tickets {{tickets}})", "departureEditAria": "Edit departure time", "departureTooltipEditSave": "Edit departure time (saved with \"Save changes\")", "departureTooltipNeedShops": "Add shops before setting departure time", diff --git a/src/i18n/zh/shop.json b/src/i18n/zh/shop.json index 27eee2dd..bfd753d0 100644 --- a/src/i18n/zh/shop.json +++ b/src/i18n/zh/shop.json @@ -200,8 +200,27 @@ "dialog_editLogisticsTitle": "編輯物流公司", "btn_apply": "套用", "addShop_confirm": "確認", - "departureDialog_title": "編輯出車時間", - "departureDialog_hint": "套用至此車線所有店鋪列。", + "departureDialog_title": "編輯車線", + "departureDialog_nameLabel": "車線名稱", + "departureDialog_remarkReadonly": "備註(這次不改)", + "departureDialog_hint": "名稱與出車時間會套用到此車線全部主檔。按上方「儲存更改」才寫入。已完成的出倉票不會改名。", + "laneEditTooltip": "編輯車線名稱與出車時間(按「儲存更改」才寫入)", + "confirm_weekdayToken": "車線名稱裡的星期代碼會改變,或改完後無法辨認。4F 派車是用名稱裡的 Mon–Sun 對日子。仍要套用?", + "confirm_renameLane": "把「{{from}}」改成「{{to}}」。\n未完成出倉票 {{unfinished}} 張會改成新名稱(負責人:{{handlers}})。\n已完成 {{completed}} 張維持舊名稱。\n未完成排程 {{schedules}} 筆的起迄名稱會改。\n按「儲存更改」後才寫入。", + "confirm_restoreKeepsTickets": "還原會把車線主檔,以及指向目前名稱的未完成排程,改回此版本的名稱。未完成的出倉票會留在現在的名稱,不會跟回去。", + "rename_noHandlers": "無", + "rename_none": "無", + "rename_savePartial": "已改名:{{ok}}。失敗:{{fail}}。失敗的那一桶沒有寫入;已經成功的不會自動改回去。請重新整理後再存。", + "renameErr_preview": "讀不到出倉票與排程數量,這次沒有套用。", + "renameErr_LANE_NAME_EMPTY": "車線名稱不可空白", + "renameErr_LANE_NAME_TOO_LONG": "車線名稱最多 50 字", + "renameErr_LANE_NAME_RESERVED": "不能使用或改掉「車線-X」", + "renameErr_LANE_NAME_TAKEN": "同一樓層、同一備註已有這條車線", + "renameErr_LANE_NAME_REMARK_AMBIGUOUS": "同一樓層已有相同名稱但備註不同,出倉票無法分辨,已拒絕", + "renameErr_LANE_NOT_FOUND": "找不到這條車線的主檔,沒有寫入", + "renameErr_LANE_TIME_INVALID": "出車時間格式不正確", + "renameLog_title": "車線改名紀錄", + "renameLog_line": "{{when}} {{who}}:{{from}} → {{to}}({{floor}},票 {{tickets}})", "seqDialog_title": "編輯裝車順序", "logistics_colLaneCount": "{{count}} 條車線", "tooltip_openLaneBoard": "在車線看板開此車線",