diff --git a/src/app/api/logistic/actions.ts b/src/app/api/logistic/actions.ts index 3121c7c..56b8c3c 100644 --- a/src/app/api/logistic/actions.ts +++ b/src/app/api/logistic/actions.ts @@ -48,3 +48,18 @@ export const saveLogisticsBatchCreateAction = async ( headers: { "Content-Type": "application/json" }, }); }; + +export type DeleteLogisticRequest = { + id: number; +}; + +export const deleteLogisticAction = async ( + data: DeleteLogisticRequest, +): Promise<{ message?: string }> => { + const endpoint = `${BASE_API_URL}/logistic/delete`; + return serverFetchJson<{ message?: string }>(endpoint, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); +}; diff --git a/src/app/api/logistic/client.ts b/src/app/api/logistic/client.ts index 1f4f9ac..62b962c 100644 --- a/src/app/api/logistic/client.ts +++ b/src/app/api/logistic/client.ts @@ -4,6 +4,7 @@ import { findAllLogisticsAction, saveLogisticAction, saveLogisticsBatchCreateAction, + deleteLogisticAction, type LogisticRow, type SaveLogisticRequest, } from "./actions"; @@ -25,3 +26,7 @@ export const saveLogisticClient = async ( ): Promise => { return await saveLogisticAction(data); }; + +export const deleteLogisticClient = async (id: number): Promise => { + await deleteLogisticAction({ id }); +}; diff --git a/src/app/api/shop/actions.ts b/src/app/api/shop/actions.ts index 7fae0b3..a60950b 100644 --- a/src/app/api/shop/actions.ts +++ b/src/app/api/shop/actions.ts @@ -519,6 +519,9 @@ export type TruckLaneVersionDiffLine = { truckRowId: number; shopCode: string | null; changes: DiffFieldChange[]; + /** 快照車線(僅欄位異動時 changes 不含 truckLanceCode/remark) */ + truckLanceCode?: string | null; + remark?: string | null; }; export type LogisticMasterDiffLine = { diff --git a/src/components/Shop/RouteBoard.tsx b/src/components/Shop/RouteBoard.tsx index 546f4f5..dee873e 100644 --- a/src/components/Shop/RouteBoard.tsx +++ b/src/components/Shop/RouteBoard.tsx @@ -96,6 +96,7 @@ import { deleteTruckLaneClient, } from "@/app/api/shop/client"; import { + deleteLogisticClient, findAllLogisticsClient, saveLogisticClient, saveLogisticsBatchCreateClient, @@ -116,9 +117,11 @@ import { diffLinesToShopRows, formatLaneLabel, resolveHeadVersionId, + resolveVersionLogLaneLabel, resolveVersionLogShopHeadline, splitVersionCreated, summarizeVersionRows, + VERSION_LOG_LOADING_SEQUENCE_LABEL, type StagedDeleteMeta, type ShopRowBaseline, } from "@/components/Shop/routeBoardVersionLog"; @@ -1088,6 +1091,8 @@ const RouteBoard: React.FC = () => { const [pendingLogisticMasterEdits, setPendingLogisticMasterEdits] = useState< Map >(new Map()); + const [pendingLogisticMasterDeletes, setPendingLogisticMasterDeletes] = + useState>(new Set()); const nextPendingLogisticTempIdRef = useRef(-1); const addLogisticInFlightRef = useRef(false); const logisticRowsEffective = useMemo(() => { @@ -1098,20 +1103,27 @@ const RouteBoard: React.FC = () => { driverName: p.driverName, driverNumber: p.driverNumber, })); - const dbWithEdits = logisticRowsFromDb.map((r) => { - const id = Number(r.id); - const edit = pendingLogisticMasterEdits.get(id); - if (!edit) return r; - return { - ...r, - logisticName: edit.logisticName, - carPlate: edit.carPlate, - driverName: edit.driverName, - driverNumber: edit.driverNumber, - }; - }); + const dbWithEdits = logisticRowsFromDb + .filter((r) => !pendingLogisticMasterDeletes.has(Number(r.id))) + .map((r) => { + const id = Number(r.id); + const edit = pendingLogisticMasterEdits.get(id); + if (!edit) return r; + return { + ...r, + logisticName: edit.logisticName, + carPlate: edit.carPlate, + driverName: edit.driverName, + driverNumber: edit.driverNumber, + }; + }); return [...pendingRows, ...dbWithEdits]; - }, [pendingLogisticMasterAdds, pendingLogisticMasterEdits, logisticRowsFromDb]); + }, [ + pendingLogisticMasterAdds, + pendingLogisticMasterEdits, + pendingLogisticMasterDeletes, + logisticRowsFromDb, + ]); const logisticNameById = useMemo(() => { const m = new Map(); for (const r of logisticRowsEffective) { @@ -1453,6 +1465,7 @@ const RouteBoard: React.FC = () => { syncShopDistrictBaselineFromLanes(nextBoard); setPendingLogisticMasterAdds([]); setPendingLogisticMasterEdits(new Map()); + setPendingLogisticMasterDeletes(new Set()); pendingImportFileRef.current = null; setPendingImportMeta(null); // default: select none (user will pick lanes) @@ -1675,6 +1688,34 @@ const RouteBoard: React.FC = () => { } }; + const stageDeleteLogistic = useCallback( + (row: LogisticRow, companyLabel: string) => { + const id = Number(row.id); + if (!Number.isFinite(id)) return; + const name = + String(companyLabel || row.logisticName || "").trim() || "—"; + if (!window.confirm(t("confirm_deleteLogistic", { name }))) return; + if (id < 0) { + setPendingLogisticMasterAdds((prev) => + prev.filter((p) => p.tempId !== id), + ); + } else { + setPendingLogisticMasterDeletes((prev) => { + const next = new Set(prev); + next.add(id); + return next; + }); + } + setPendingLogisticMasterEdits((prev) => { + const next = new Map(prev); + next.delete(id); + return next; + }); + setSaveResult(null); + }, + [t], + ); + const handleExportSelectedLanesExcel = useCallback(async () => { if (selectedLaneIds.length === 0) { setError(t("err_exportNeedSelection")); @@ -1725,6 +1766,7 @@ const RouteBoard: React.FC = () => { pendingNewLanes.length > 0 || pendingLogisticMasterAdds.length > 0 || pendingLogisticMasterEdits.size > 0 || + pendingLogisticMasterDeletes.size > 0 || pendingRestoreVersionId != null || pendingImportMeta != null || Object.values(pendingEmptyDistrictsByLane).some( @@ -1748,6 +1790,7 @@ const RouteBoard: React.FC = () => { setPendingNewLanes([]); setPendingLogisticMasterAdds([]); setPendingLogisticMasterEdits(new Map()); + setPendingLogisticMasterDeletes(new Set()); setPendingRestoreVersionId(null); setPendingEmptyDistrictsByLane({}); setDistrictEditOpen(false); @@ -1902,17 +1945,29 @@ const RouteBoard: React.FC = () => { const n = String(p.logisticName ?? "").trim(); if (n) set.add(n); } - for (const n of logisticNamesFromDb) { - const s = String(n ?? "").trim(); + for (const r of logisticRowsFromDb) { + const id = Number(r.id); + if (pendingLogisticMasterDeletes.has(id)) continue; + const s = String(r.logisticName ?? "").trim(); if (s) set.add(s); } return Array.from(set).sort((a, b) => a.localeCompare(b, "zh-Hant")); - }, [pendingLogisticMasterAdds, logisticNamesFromDb]); + }, [ + pendingLogisticMasterAdds, + pendingLogisticMasterDeletes, + logisticRowsFromDb, + ]); /** 依左欄篩選後的車線,再依物流公司名稱(含 overlay)分組;併入 GET /logistic/all 有、但尚未掛車線的公司 */ const lanesByLogisticsCompany = useMemo(() => { const map = new Map(); for (const lane of visibleLaneOptions) { + if ( + selectedLaneIds.length > 0 && + !selectedLaneIds.includes(lane.id) + ) { + continue; + } const company = String(lane.logisticsCompany ?? "").trim() || "未分配物流商"; const arr = map.get(company) ?? []; @@ -1930,7 +1985,7 @@ const RouteBoard: React.FC = () => { return a[0].localeCompare(b[0], "zh-Hant"); }); return entries; - }, [visibleLaneOptions, logisticNamesEffective]); + }, [visibleLaneOptions, logisticNamesEffective, selectedLaneIds]); const addShopCandidates = useMemo(() => { if (!addShopLaneId) return []; @@ -2720,6 +2775,7 @@ const RouteBoard: React.FC = () => { pendingNewLanes.length > 0 || pendingLogisticMasterAdds.length > 0 || pendingLogisticMasterEdits.size > 0 || + pendingLogisticMasterDeletes.size > 0 || pendingImportMeta != null || pendingRestoreVersionId != null || Object.values(pendingEmptyDistrictsByLane).some( @@ -2796,6 +2852,15 @@ const RouteBoard: React.FC = () => { fromPlate: prev?.carPlate ?? "", }; }), + pendingLogisticMasterDeletes: Array.from( + pendingLogisticMasterDeletes, + ).map((id) => { + const prev = logisticRowsFromDb.find((r) => Number(r.id) === id); + return { + id, + logisticName: String(prev?.logisticName ?? "").trim() || "—", + }; + }), pendingImport: pendingImportMeta, laneLogisticChanges: laneLogisticChangesForStaged, lanes, @@ -2813,6 +2878,7 @@ const RouteBoard: React.FC = () => { pendingNewLanes, pendingLogisticMasterAdds, pendingLogisticMasterEdits, + pendingLogisticMasterDeletes, pendingImportMeta, laneLogisticChangesForStaged, lanes, @@ -2964,6 +3030,9 @@ const RouteBoard: React.FC = () => { const pendingLogisticMasterEditsSnapshot = new Map( pendingLogisticMasterEdits, ); + const pendingLogisticMasterDeletesSnapshot = new Set( + pendingLogisticMasterDeletes, + ); const pendingImportFile = pendingImportFileRef.current; let laneLogisticChangesLive = getLaneLogisticChanges(); @@ -2978,6 +3047,7 @@ const RouteBoard: React.FC = () => { laneLogisticChangesLive.length > 0 || pendingLogisticMasterAddsSnapshot.length > 0 || pendingLogisticMasterEditsSnapshot.size > 0 || + pendingLogisticMasterDeletesSnapshot.size > 0 || pendingImportFile != null || pendingRestoreId != null; const hasPendingEmptyDistrictsOnly = @@ -3012,6 +3082,7 @@ const RouteBoard: React.FC = () => { laneLogisticChangesLive.length > 0 || pendingLogisticMasterAddsSnapshot.length > 0 || pendingLogisticMasterEditsSnapshot.size > 0 || + pendingLogisticMasterDeletesSnapshot.size > 0 || pendingImportFile != null; if (hadRestoreWithOtherWork) { if (!window.confirm(t("confirm_restoreSaveWillDropStaging"))) @@ -3026,6 +3097,7 @@ const RouteBoard: React.FC = () => { setPendingNewLanes([]); setPendingLogisticMasterAdds([]); setPendingLogisticMasterEdits(new Map()); + setPendingLogisticMasterDeletes(new Set()); pendingImportFileRef.current = null; setPendingImportMeta(null); setPendingEmptyDistrictsByLane({}); @@ -3055,12 +3127,21 @@ const RouteBoard: React.FC = () => { for (const [id, req] of Array.from( pendingLogisticMasterEditsSnapshot.entries(), )) { + if (pendingLogisticMasterDeletesSnapshot.has(id)) continue; await saveLogisticClient({ ...req, id }); } setPendingLogisticMasterEdits(new Map()); await reloadLogisticNamesFromDb(); } + if (pendingLogisticMasterDeletesSnapshot.size > 0) { + for (const id of Array.from(pendingLogisticMasterDeletesSnapshot)) { + await deleteLogisticClient(id); + } + setPendingLogisticMasterDeletes(new Set()); + await reloadLogisticNamesFromDb(); + } + if (pendingLogisticMasterAddsSnapshot.length > 0) { const savedRows = await saveLogisticsBatchCreateClient( pendingLogisticMasterAddsSnapshot.map((p) => ({ @@ -3296,6 +3377,7 @@ const RouteBoard: React.FC = () => { truckLanceCode: null, note: "board save", }); + await refreshLogVersions(); } catch (snapErr: any) { console.warn("Auto snapshot after board save failed:", snapErr); } @@ -3354,6 +3436,7 @@ const RouteBoard: React.FC = () => { setPendingNewLanes([]); setPendingLogisticMasterAdds([]); setPendingLogisticMasterEdits(new Map()); + setPendingLogisticMasterDeletes(new Set()); pendingImportFileRef.current = null; setPendingImportMeta(null); pendingLaneKeys.forEach((id) => @@ -3528,6 +3611,9 @@ const RouteBoard: React.FC = () => { truckRowId, shopCode, changes: changes.filter((c) => c.field), + truckLanceCode: + line?.truckLanceCode != null ? String(line.truckLanceCode) : null, + remark: line?.remark != null ? String(line.remark) : null, }; }) .filter((x) => Number.isFinite(x.truckRowId) && x.truckRowId > 0); @@ -3576,6 +3662,22 @@ const RouteBoard: React.FC = () => { } }; + const refreshLogVersions = useCallback(async () => { + try { + const list = await listTruckLaneVersionsClient(); + const arr = Array.isArray(list) ? list : []; + setLogVersions(arr); + return arr; + } catch (e) { + console.warn("Failed to load truck lane versions:", e); + return []; + } + }, []); + + useEffect(() => { + void refreshLogVersions(); + }, [refreshLogVersions]); + const openLogDialog = async () => { setLogDialogOpen(true); setDiffError(null); @@ -3585,9 +3687,7 @@ const RouteBoard: React.FC = () => { setLoadingVersions(true); try { - const list = await listTruckLaneVersionsClient(); - const arr = Array.isArray(list) ? list : []; - setLogVersions(arr); + const arr = await refreshLogVersions(); const head = resolveHeadVersionId(arr); setSelectedLogVersionId(head); if (head != null) { @@ -3668,6 +3768,7 @@ const RouteBoard: React.FC = () => { pendingNewLanes.length > 0 || pendingLogisticMasterAdds.length > 0 || pendingLogisticMasterEdits.size > 0 || + pendingLogisticMasterDeletes.size > 0 || pendingImportMeta != null || getLaneLogisticChanges().length > 0 || Object.values(pendingEmptyDistrictsByLane).some( @@ -3989,26 +4090,17 @@ const RouteBoard: React.FC = () => { alignItems="center" spacing={2} > - - - - - - - {t("pageTitle")} - - - {t("Current version")}: 2026-04-16 {t("new arrangement")} - - - + + + {t("pageTitle")} + + + {t("Current version")}:{" "} + {headVersionId != null + ? t("version_ui_id", { id: headVersionId }) + : t("version_ui_none")} + + { display: "flex", alignItems: "center", justifyContent: "space-between", + py: 1.5, + pl: 2, pr: 1, }} > - - - - {t("versionLogDialogTitle")} - - + + {t("versionLogDialogTitle")} + { direction="row" spacing={1} alignItems="center" - sx={{ mb: 1 }} + sx={{ mb: 1, color: "primary.dark" }} > {t("diff_summary_title")} @@ -4995,6 +5083,7 @@ const RouteBoard: React.FC = () => { | "diff_staged_shopDistrictOnly" | "diff_staged_pendingLogisticMaster" | "diff_staged_editLogisticMaster" + | "diff_staged_deleteLogisticMaster" | "diff_staged_importPending", entry.titleParams as Record< string, @@ -5158,6 +5247,8 @@ const RouteBoard: React.FC = () => { row, shopNameByCodeMap, ); + const laneLabelForFields = + resolveVersionLogLaneLabel(row); return ( { fe.to, ) : fe.to; + const isLoadingSeq = + fe.label === + VERSION_LOG_LOADING_SEQUENCE_LABEL || + fe.label === + "loadingSequence"; + const showLaneOnSeq = + isLoadingSeq && + laneLabelForFields != + null && + laneLabelForFields !== + ""; return ( { t, )} + {showLaneOnSeq && ( + + {" "} + ( + {t( + "diff_onLane", + { + lane: laneLabelForFields, + }, + )} + ) + + )} {":"} {from} → {to} @@ -5910,7 +6032,6 @@ const RouteBoard: React.FC = () => { driverPhone: e.target.value, })) } - helperText={t("logistic_phone_helper")} InputProps={{ startAdornment: ( @@ -6032,7 +6153,6 @@ const RouteBoard: React.FC = () => { driverPhone: e.target.value, })) } - helperText={t("logistic_phone_helper")} InputProps={{ startAdornment: ( @@ -6060,7 +6180,7 @@ const RouteBoard: React.FC = () => { > {editLogisticSubmitting ? t("Submitting...") - : t("logistic_btn_saveDb")} + : t("logistic_btn_apply")} @@ -6122,6 +6242,295 @@ const RouteBoard: React.FC = () => { + {(() => { + const laneIds = visibleLaneOptions.map((l) => l.id); + const total = laneIds.length; + const selectedVisible = laneIds.filter((id) => + selectedLaneIds.includes(id), + ).length; + const filterActive = + laneFilter.floor !== "all" || + String(laneFilter.query || "").trim() !== ""; + return ( + + + {t("lane_selectTitle")} + + + + + setLaneFilterAnchor(null)} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} + transformOrigin={{ vertical: "top", horizontal: "left" }} + > + + + + {t("floor_label")} + + + + + + + + + + + {routeBoardTab === "board" && ( + + )} + + ); + })()} + {routeBoardTab === "logistics" && ( { {t("btn_addLogistics")} - {lanesByLogisticsCompany.map(([company, list]) => ( - - - {company} - - - - ))} - {lanesByLogisticsCompany.length === 0 && ( - - {t("logistics_sidebarEmpty")} - - )} - - - )} - - {routeBoardTab === "board" && - (() => { - const laneIds = visibleLaneOptions.map((l) => l.id); - const total = laneIds.length; - const selectedVisible = laneIds.filter((id) => - selectedLaneIds.includes(id), - ).length; - const filterActive = - laneFilter.floor !== "all" || - String(laneFilter.query || "").trim() !== ""; - return ( - <> - - - {t("lane_selectTitle")} - - - setLaneFilterAnchor(e.currentTarget)} - disabled={(lanes || []).length === 0} - > - - - - + {lanesByLogisticsCompany.map(([company, list]) => { + const logisticMaster = resolveLogisticMasterRow( + company, + list, + logisticRowsEffective, + ); + const canManage = + company !== "未分配物流商" && logisticMaster != null; + const masterId = + logisticMaster != null + ? Number(logisticMaster.id) + : null; + const isPendingDelete = + masterId != null && + Number.isFinite(masterId) && + masterId > 0 && + pendingLogisticMasterDeletes.has(masterId); + const canDelete = + canManage && list.length === 0 && !isPendingDelete; - - - setLaneFilterAnchor(null)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - transformOrigin={{ vertical: "top", horizontal: "right" }} - > - - - - {t("floor_label")} - - - - - - - + {t("diff_staged_tag_unsaved")} ·{" "} + {t("diff_staged_deleteLogisticMaster", { + name: company, + })} + + )} - - - - - ); - })()} + ); + })} + {lanesByLogisticsCompany.length === 0 && ( + + {t("logistics_sidebarEmpty")} + + )} + + + )} {routeBoardTab !== "logistics" && ( <> @@ -6639,9 +6890,9 @@ const RouteBoard: React.FC = () => { { fontSize: "1.1rem", flex: 1, minWidth: 0, + lineHeight: 1.25, }} > {company} @@ -6661,32 +6913,22 @@ const RouteBoard: React.FC = () => { sx={{ height: 22, fontWeight: 800 }} /> )} - {logisticMaster && ( - - - openEditLogistic(logisticMaster) - } - aria-label={t("aria_editLogistics")} - sx={{ flexShrink: 0 }} - > - - - - )} {logisticMaster && ( @@ -6707,13 +6949,17 @@ const RouteBoard: React.FC = () => { @@ -6735,13 +6981,17 @@ const RouteBoard: React.FC = () => { @@ -7027,19 +7277,6 @@ const RouteBoard: React.FC = () => { ); })} - - - {t("logistics_dataSource")} - - ); })} diff --git a/src/components/Shop/routeBoardVersionLog.ts b/src/components/Shop/routeBoardVersionLog.ts index d3fdeb6..05efb07 100644 --- a/src/components/Shop/routeBoardVersionLog.ts +++ b/src/components/Shop/routeBoardVersionLog.ts @@ -27,9 +27,11 @@ function pickField( return changes.find((c) => c.field === field); } +export const VERSION_LOG_LOADING_SEQUENCE_LABEL = "裝載順序"; + const VERSION_LOG_FIELD_LABEL: Record = { departureTime: "發車時段", - loadingSequence: "裝載順序", + loadingSequence: VERSION_LOG_LOADING_SEQUENCE_LABEL, branchName: "分店名稱", districtReference: "區域", shopCode: "店鋪代碼", @@ -91,6 +93,19 @@ function buildFieldEditsForRow( return out.length > 0 ? out : undefined; } +/** 同車線編輯(如僅裝載順序)時用於 UI 顯示車線 */ +export function resolveVersionLogLaneLabel( + row: Pick, +): string | undefined { + const lane = + row.toLane && row.toLane !== "—" + ? row.toLane + : row.fromLane && row.fromLane !== "—" + ? row.fromLane + : undefined; + return lane; +} + export function formatLaneLabel( truckLanceCode?: string | null, remark?: string | null, @@ -138,8 +153,17 @@ export function diffLineToShopRow( const code = line.shopCode != null ? String(line.shopCode) : ""; const shopName = String(br?.to ?? br?.from ?? (code || "—")).trim() || "—"; - const fromLane = formatLaneLabel(tc?.from, rem?.from); - const toLane = formatLaneLabel(tc?.to, rem?.to); + const ctxCode = + line.truckLanceCode != null && String(line.truckLanceCode).trim() !== "" + ? String(line.truckLanceCode).trim() + : undefined; + const ctxRem = + line.remark != null && String(line.remark).trim() !== "" + ? String(line.remark).trim() + : undefined; + + const fromLane = formatLaneLabel(tc?.from ?? ctxCode, rem?.from ?? ctxRem); + const toLane = formatLaneLabel(tc?.to ?? ctxCode, rem?.to ?? ctxRem); const fromEmpty = fromLane === "—"; const toEmpty = toLane === "—"; @@ -248,6 +272,10 @@ export function buildStagedBoardLogEntries(input: { fromName?: string; fromPlate?: string; }>; + pendingLogisticMasterDeletes?: Array<{ + id: number; + logisticName: string; + }>; pendingImport?: { fileName: string; sheetCount: number; @@ -283,6 +311,7 @@ export function buildStagedBoardLogEntries(input: { pendingNewLanes, pendingLogisticMasterAdds, pendingLogisticMasterEdits, + pendingLogisticMasterDeletes, pendingImport, laneLogisticChanges, lanes, @@ -349,6 +378,17 @@ export function buildStagedBoardLogEntries(input: { }); } + for (const p of pendingLogisticMasterDeletes ?? []) { + const name = String(p.logisticName ?? "").trim() || "—"; + out.push({ + key: `plog-del-${p.id}`, + tag: "unsaved", + kind: "text", + titleKey: "diff_staged_deleteLogisticMaster", + titleParams: { name }, + }); + } + const dirtyMoveShopIds = new Set(); dirtyMoves.forEach((laneId, shopId) => { @@ -416,6 +456,8 @@ export function buildStagedBoardLogEntries(input: { type: "edited", shopName, shopCode, + fromLane: laneLabel, + toLane: laneLabel, truckRowId: shopId, fieldEdits, }, diff --git a/src/i18n/en/routeboard.json b/src/i18n/en/routeboard.json index 90580b8..c312f88 100644 --- a/src/i18n/en/routeboard.json +++ b/src/i18n/en/routeboard.json @@ -87,6 +87,7 @@ "logistic_needMasterTpl": "\"{{name}}\" has no logistics master id—create it with \"Add logistics\" first.", "diffField_logisticsCompany": "Logistics company", "diffLogistic_unassigned": "Unassigned", + "diff_onLane": "Lane {{lane}}", "diff_moveTo": "Move to {{lane}}", "diff_addedToLane": "Added to lane {{lane}}", "diff_removedFromLane": "Removed from {{lane}}", @@ -119,6 +120,7 @@ "version_ui_listAria": "Version history list", "version_ui_snapshotBadge": "Current snapshot", "version_ui_id": "Version #{{id}}", + "version_ui_none": "No snapshot yet", "version_ui_editedBy": "Editor: {{name}}", "version_note_placeholder": "Note (saved on blur)", "version_note_saving": "Saving…", @@ -187,7 +189,6 @@ "logistic_plate": "Plate", "logistic_driver": "Driver name", "logistic_phone": "Phone", - "logistic_phone_helper": "Server stores Int digits (e.g. 9811-5780); +852 8-digit local numbers are OK", "logistic_btn_save": "Save", "logistic_btn_saveDb": "Save to database", "shop_autocomplete_label": "Select shop", @@ -209,7 +210,6 @@ "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.", - "logistics_dataSource": "Data: board lanes (including left filter)", "tooltip_openLaneBoard": "Open this lane on the route board", "aria_openLaneBoard": "Open lane on route board", "tooltip_removeFromLane": "Remove from this lane", @@ -218,8 +218,14 @@ "aria_pickLane": "Pick lane", "aria_searchLanes": "Search lanes", "logistics_colShopCount": "{{count}} shop(s)", - "tooltip_editLogisticsDb": "Edit logistics master (save to database)", - "aria_editLogistics": "Edit logistics master", + "tooltip_editLogisticsDb": "Edit logistics company (persists after Save changes)", + "tooltip_deleteLogistics": "Delete logistics company (persists after Save changes)", + "aria_editLogistics": "Edit logistics company", + "aria_deleteLogistics": "Delete logistics company", + "confirm_deleteLogistic": "Delete logistics company \"{{name}}\"? Press \"Save changes\" to persist.", + "err_logisticDeleteHasLanes": "This company still has {{count}} lane(s). Reassign or remove them first.", + "diff_staged_deleteLogisticMaster": "Delete logistics company (not saved): {{name}}", + "logistic_btn_apply": "Apply", "tooltip_editDistrict": "Edit district name (press \"Save changes\" to persist)", "aria_editDistrict": "Edit district", "tooltip_removeEmptyDistrict": "Remove this staged empty block (deletable before save)", diff --git a/src/i18n/zh/routeboard.json b/src/i18n/zh/routeboard.json index fc0bd18..d8bb546 100644 --- a/src/i18n/zh/routeboard.json +++ b/src/i18n/zh/routeboard.json @@ -78,7 +78,7 @@ "versionNote_saveFail": "備註儲存失敗", "diff_restoreFail": "恢復失敗", "confirm_restoreDiscardsEdits": "排程版本還原會捨棄目前看板上其他未儲存變更(拖曳、刪除、新增店鋪/車線、物流欄等)。確定繼續?", - "diff_restoreScheduled": "已排程還原至版本 #{{versionId}};請按「儲存更改」才會寫入後端。", + "diff_restoreScheduled": "已排程還原至版本 #{{versionId}};請按「儲存更改」寫入後端。", "diff_restoreAlreadyPending": "此版本已在排程還原中;請按「儲存更改」套用。", "restore_applied": "已從 snapshot 還原並重新載入看板。", "restore_appliedDroppedStaging": "已套用 snapshot 還原;本次儲存略過其他暫存變更(請重新編輯)。", @@ -87,6 +87,7 @@ "logistic_needMasterTpl": "「{{name}}」尚無對應物流公司,請先用「新增物流商」建立。", "diffField_logisticsCompany": "物流公司", "diffLogistic_unassigned": "未分配", + "diff_onLane": "車線 {{lane}}", "diff_moveTo": "移至 {{lane}}", "diff_addedToLane": "新加入車線 {{lane}}", "diff_removedFromLane": "自 {{lane}} 移除", @@ -118,7 +119,8 @@ "version_ui_filterAria": "版本列表篩選", "version_ui_listAria": "版本歷史列表", "version_ui_snapshotBadge": "目前快照", - "version_ui_id": "版本 #{{id}}", + "version_ui_id": "版本#{{id}}", + "version_ui_none": "尚無快照", "version_ui_editedBy": "編輯者:{{name}}", "version_note_placeholder": "備註(離開欄位即儲存)", "version_note_saving": "儲存中…", @@ -162,7 +164,7 @@ "diff_noShopDiffHasBoardStaged": "與上一版快照相比,店鋪列無差異;下列為看板上尚未按「儲存更改」寫入的變更(含新增物流公司)。", "diff_export_blockedTooltip": "匯出檔為後端兩版快照比對,不含看板未儲存變更。請先按「儲存更改」或取消變更後再匯出。", "diff_export_blockedError": "有看板未儲存變更時無法匯出(Excel 僅含已落庫快照)。", - "diff_markedCount": "已標記 {{count}} 筆 truck 異動(看板可對照)", + "diff_markedCount": "已標記 {{count}} 筆車線異動(看板可對照)", "diff_noDiffFromPrev": "與上一版無差異", "diff_loadingEllipsis": "…", "addShop_dialogTitle": "新增店鋪到車線", @@ -187,7 +189,6 @@ "logistic_plate": "車牌", "logistic_driver": "司機姓名", "logistic_phone": "聯絡電話", - "logistic_phone_helper": "後端為 Int:輸入數字即可(含 9811-5780);+852 八位本地號亦可", "logistic_btn_save": "儲存", "logistic_btn_saveDb": "儲存至資料庫", "shop_autocomplete_label": "選擇店鋪", @@ -209,7 +210,6 @@ "seqDialog_hint": "按「儲存更改」後寫入 truck 列。", "logistics_colLaneCount": "{{count}} 條車線", "logistics_masterNoLanes": "主檔已建立,尚無綁定車線;至「車線看板」新增/編輯車線時可填此公司名稱。", - "logistics_dataSource": "資料來源:看板車線(含左欄篩選)", "tooltip_openLaneBoard": "在車線看板開此車線", "aria_openLaneBoard": "開啟車線看板", "tooltip_removeFromLane": "從此車線移除", @@ -218,8 +218,14 @@ "aria_pickLane": "選擇車線", "aria_searchLanes": "搜尋車線", "logistics_colShopCount": "{{count}} 家店鋪", - "tooltip_editLogisticsDb": "編輯物流公司(寫入資料庫)", + "tooltip_editLogisticsDb": "編輯物流公司(須按「儲存更改」寫入)", + "tooltip_deleteLogistics": "刪除物流公司(須按「儲存更改」寫入)", "aria_editLogistics": "編輯物流公司", + "aria_deleteLogistics": "刪除物流公司", + "confirm_deleteLogistic": "確定刪除物流公司「{{name}}」?須按「儲存更改」寫入。", + "err_logisticDeleteHasLanes": "此物流公司尚有 {{count}} 條車線,請先移走或改派後再刪除。", + "diff_staged_deleteLogisticMaster": "刪除物流公司(未落庫):{{name}}", + "logistic_btn_apply": "套用", "tooltip_editDistrict": "編輯地區名稱(按「儲存更改」才寫入)", "aria_editDistrict": "編輯地區", "tooltip_removeEmptyDistrict": "移除此暫存區塊(未儲存前可刪)",