Ver a proveniência

Merge branch 'production' into stockTakeUpdate

stockTakeUpdate
CANCERYS\kw093 há 1 semana
ascendente
cometimento
5127fd25a3
5 ficheiros alterados com 463 adições e 45 eliminações
  1. +72
    -0
      src/app/api/shop/actions.ts
  2. +20
    -1
      src/app/api/shop/client.ts
  3. +329
    -40
      src/components/Shop/RouteBoard.tsx
  4. +21
    -2
      src/i18n/en/shop.json
  5. +21
    -2
      src/i18n/zh/shop.json

+ 72
- 0
src/app/api/shop/actions.ts Ver ficheiro

@@ -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<PreviewTruckLaneRenameResponse>(
`${BASE_API_URL}/truck/previewTruckLaneRename`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
};

export const renameTruckLaneAction = async (data: RenameTruckLaneRequest) => {
return serverFetchJson<RenameTruckLaneResponse>(
`${BASE_API_URL}/truck/renameTruckLane`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
};

export const listTruckLaneRenameLogsAction = async () => {
return serverFetchJson<TruckLaneRenameLogResponse[]>(
`${BASE_API_URL}/truck/truckLaneRenameLog`,
{ method: "GET", headers: { "Content-Type": "application/json" } },
);
};

+ 20
- 1
src/app/api/shop/client.ts Ver ficheiro

@@ -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<string, string | number | null>) => {
@@ -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,


+ 329
- 40
src/components/Shop/RouteBoard.tsx Ver ficheiro

@@ -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<TruckLaneRenameLogResponse[]>(
[],
);
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 || "-"}
</Typography>
<Tooltip
title={
lane.shops.length === 0
? t("departureTooltipNeedShops")
: t("departureTooltipEditSave")
}
title={t("laneEditTooltip")}
>
<span>
<IconButton
@@ -4182,7 +4413,7 @@ const RouteBoard: React.FC = () => {
e.stopPropagation();
openDepartureEdit(lane);
}}
disabled={loading || lane.shops.length === 0}
disabled={loading}
aria-label={t("departureEditAria")}
>
<Pencil size={14} />
@@ -4710,6 +4941,33 @@ const RouteBoard: React.FC = () => {
</IconButton>
</Stack>
</Box>
{renameLogs.length > 0 && (
<Box
sx={{
px: 2,
py: 1,
borderBottom: 1,
borderColor: "divider",
bgcolor: "background.paper",
}}
>
<Typography variant="overline" sx={{ fontWeight: 800, color: "text.secondary" }}>
{t("renameLog_title")}
</Typography>
{renameLogs.slice(0, 8).map((row) => (
<Typography key={row.id} variant="caption" sx={{ display: "block" }}>
{t("renameLog_line", {
when: row.created ?? "",
who: row.createdBy ?? "",
from: row.oldCode,
to: row.newCode,
floor: row.storeId,
tickets: row.ticketCount,
})}
</Typography>
))}
</Box>
)}
<Box sx={{ flex: 1, overflow: "auto", p: 2 }}>
{loadingVersions ? (
<Box
@@ -5883,6 +6141,29 @@ const RouteBoard: React.FC = () => {
>
<DialogTitle>{t("departureDialog_title")}</DialogTitle>
<DialogContent>
<TextField
margin="dense"
fullWidth
label={t("departureDialog_nameLabel")}
value={departureEditNameDraft}
onChange={(e) => 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() !== "" && (
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 0.5 }}>
{t("departureDialog_remarkReadonly")}:
{String(
lanes.find((l) => l.id === departureEditLaneId)?.remark ?? "",
).trim()}
</Typography>
)}
<TextField
margin="dense"
fullWidth
@@ -5902,8 +6183,16 @@ const RouteBoard: React.FC = () => {
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={closeDepartureEdit}>{t("cancel")}</Button>
<Button variant="contained" onClick={applyDepartureEdit}>
<Button onClick={closeDepartureEdit} disabled={departureEditBusy}>
{t("cancel")}
</Button>
<Button
variant="contained"
onClick={() => {
void applyDepartureEdit();
}}
disabled={departureEditBusy}
>
{t("filter_apply")}
</Button>
</DialogActions>


+ 21
- 2
src/i18n/en/shop.json Ver ficheiro

@@ -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",


+ 21
- 2
src/i18n/zh/shop.json Ver ficheiro

@@ -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": "在車線看板開此車線",


Carregando…
Cancelar
Guardar