Sfoglia il codice sorgente

translate and re-schedule truck

production
tommy 1 settimana fa
parent
commit
4d9cbb49dd
8 ha cambiato i file con 322 aggiunte e 93 eliminazioni
  1. +12
    -0
      src/app/api/shop/actions.ts
  2. +8
    -0
      src/app/api/shop/client.ts
  3. +36
    -18
      src/components/Shop/RouteBoard.tsx
  4. +226
    -33
      src/components/Shop/ScheduleTaskHistoryModal.tsx
  5. +0
    -3
      src/i18n/en/routeboard.json
  6. +5
    -3
      src/i18n/en/shop.json
  7. +15
    -18
      src/i18n/zh/routeboard.json
  8. +20
    -18
      src/i18n/zh/shop.json

+ 12
- 0
src/app/api/shop/actions.ts Vedi File

@@ -855,6 +855,18 @@ export const retryFailedTruckLaneScheduleAction = async (
});
};

export const reactivateCancelledTruckLaneScheduleAction = async (
id: number,
body?: RetryFailedTruckLaneScheduleRequest,
): Promise<TruckLaneScheduleResponse> => {
const endpoint = `${BASE_API_URL}/truckLaneSchedule/${id}/reactivate`;
return serverFetchJson<TruckLaneScheduleResponse>(endpoint, {
method: "POST",
body: JSON.stringify(body ?? {}),
headers: { "Content-Type": "application/json" },
});
};

export const ignoreTruckLaneScheduleAction = async (
id: number,
): Promise<TruckLaneScheduleResponse> => {


+ 8
- 0
src/app/api/shop/client.ts Vedi File

@@ -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<TruckLaneScheduleResponse> => {
return await reactivateCancelledTruckLaneScheduleAction(id, body);
};

export const ignoreTruckLaneScheduleClient = async (
id: number,
): Promise<TruckLaneScheduleResponse> => {


+ 36
- 18
src/components/Shop/RouteBoard.tsx Vedi File

@@ -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 }}
/>
</DialogContent>
@@ -5915,13 +5916,6 @@ const RouteBoard: React.FC = () => {
inputProps={{ step: 1 }}
sx={{ mt: 1 }}
/>
<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 1, display: "block" }}
>
{t("seqDialog_hint")}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={closeSeqEdit}>{t("cancel")}</Button>
@@ -7843,7 +7837,13 @@ const RouteBoard: React.FC = () => {
spacing={0.5}
alignItems="center"
>
<Tooltip title={t("tooltip_removeFromLane")}>
<Tooltip
title={
isScheduledMove
? t("schedule_shop_locked")
: t("tooltip_removeFromLane")
}
>
<span>
<IconButton
size="small"
@@ -7858,7 +7858,8 @@ const RouteBoard: React.FC = () => {
}}
disabled={
loading ||
dirtyDeletes.has(shop.id)
dirtyDeletes.has(shop.id) ||
isScheduledMove
}
>
<Trash2 size={16} />
@@ -7944,12 +7945,29 @@ const RouteBoard: React.FC = () => {
>
{t("btn_addShopToLane")}
</Button>
<Tooltip title={t("tooltip_clearLaneShops")}>
<Tooltip
title={
lane.shops.some(
(s) =>
s.id > 0 && scheduledShopIdSet.has(s.id),
)
? t("schedule_shop_locked")
: t("tooltip_clearLaneShops")
}
>
<span>
<IconButton
size="small"
onClick={() => handleClearLaneShops(lane)}
disabled={loading || lane.shops.length === 0}
disabled={
loading ||
lane.shops.length === 0 ||
lane.shops.some(
(s) =>
s.id > 0 &&
scheduledShopIdSet.has(s.id),
)
}
>
<Trash2 size={16} />
</IconButton>


+ 226
- 33
src/components/Shop/ScheduleTaskHistoryModal.tsx Vedi File

@@ -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<Props> = ({
const [actionId, setActionId] = useState<number | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const [actionNotice, setActionNotice] = useState<string | null>(null);
const [redoTarget, setRedoTarget] = useState<TruckLaneScheduleResponse | null>(
null,
);
const [redoDate, setRedoDate] = useState("");
const [redoTime, setRedoTime] = useState("");
const [redoSubmitting, setRedoSubmitting] = useState(false);

const laneById = useMemo(
() => new Map(lanes.map((l) => [l.id, l])),
@@ -218,6 +254,9 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({
setDetailById({});
setActionError(null);
setActionNotice(null);
setRedoTarget(null);
setRedoDate("");
setRedoTime("");
void loadTasks();
}, [open, loadTasks]);

@@ -238,15 +277,22 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({
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<Props> = ({
} 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<Props> = ({
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<Props> = ({
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<Props> = ({
}
};

const openRedoDialog = (task: TruckLaneScheduleResponse) => {
const { executeAt } = resolveRescheduleExecuteAt(task.executeAt);
const { date, time } = splitExecuteAt(executeAt);
setRedoTarget(task);
setRedoDate(date !== "-" ? date : "");
setRedoTime(time && time !== "-" ? time : "");
setActionError(null);
};

const closeRedoDialog = () => {
if (redoSubmitting) return;
setRedoTarget(null);
setRedoDate("");
setRedoTime("");
};

const handleConfirmRedo = async () => {
if (!redoTarget) return;
const executeAt = buildExecuteAtIso(redoDate, redoTime);
if (!executeAt) {
setActionError(t("schedule_err_execute_at_past"));
return;
}
if (isExecuteAtTooEarly(executeAt)) {
setActionError(t("schedule_err_execute_at_past"));
return;
}

setRedoSubmitting(true);
setActionId(redoTarget.id);
setActionError(null);
setActionNotice(null);
try {
const updated = await reactivateCancelledTruckLaneScheduleClient(
redoTarget.id,
{ executeAt },
);
setActionNotice(
t("schedule_reschedule_created", {
id: updated.id,
at: formatScheduleDisplayDateTime(executeAt),
}),
);
closeRedoDialog();
await reloadAfterMutation(redoTarget.id);
} catch (err: unknown) {
setActionError(extractApiErrorMessage(err) ?? t("schedule_err_generic"));
} finally {
setRedoSubmitting(false);
setActionId(null);
}
};

const handleReschedule = async (task: TruckLaneScheduleResponse) => {
if (String(task.status ?? "").toUpperCase() === "PARTIAL") {
setActionError(t("schedule_retry_rejects_partial"));
@@ -315,18 +435,7 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({
(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<Props> = ({
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<Props> = ({
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<Props> = ({
/>
);
}
if (ui === "cancelled") {
return (
<Chip
size="small"
icon={<Ban size={12} />}
label={t("schedule_history_status_cancelled")}
sx={{
fontWeight: 700,
bgcolor: "grey.100",
color: "text.secondary",
border: 1,
borderColor: "grey.400",
}}
/>
);
}
return (
<Chip
size="small"
@@ -966,6 +1089,24 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({
{t("schedule_history_status_ignored")}
</Typography>
)}
{ui === "cancelled" && (
<Button
size="small"
variant="contained"
disabled={busy}
startIcon={
busy ? (
<CircularProgress size={14} color="inherit" />
) : (
<RotateCcw size={14} />
)
}
onClick={() => openRedoDialog(task)}
sx={{ fontWeight: 700 }}
>
{t("schedule_history_reschedule")}
</Button>
)}
</Stack>
</Stack>
</Box>
@@ -988,6 +1129,58 @@ const ScheduleTaskHistoryModal: React.FC<Props> = ({
{t("schedule_history_close")}
</Button>
</DialogActions>

<Dialog
open={redoTarget != null}
onClose={closeRedoDialog}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ fontWeight: 800 }}>
{t("schedule_reschedule_dialog_title")}
</DialogTitle>
<DialogContent>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<TextField
label={t("schedule_exec_date")}
type="date"
size="small"
fullWidth
value={redoDate}
onChange={(e) => setRedoDate(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<TextField
label={t("schedule_exec_time")}
type="time"
size="small"
fullWidth
value={redoTime}
onChange={(e) => setRedoTime(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 2, pb: 2 }}>
<Button onClick={closeRedoDialog} disabled={redoSubmitting}>
{t("cancel")}
</Button>
<Button
variant="contained"
disabled={
redoSubmitting || !redoDate.trim() || !redoTime.trim()
}
onClick={() => void handleConfirmRedo()}
sx={{ fontWeight: 700 }}
>
{redoSubmitting ? (
<CircularProgress size={18} color="inherit" />
) : (
t("schedule_reschedule_dialog_confirm")
)}
</Button>
</DialogActions>
</Dialog>
</Dialog>
);
};


+ 0
- 3
src/i18n/en/routeboard.json Vedi File

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


+ 5
- 3
src/i18n/en/shop.json Vedi File

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


+ 15
- 18
src/i18n/zh/routeboard.json Vedi File

@@ -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": "有未儲存物流更改",


+ 20
- 18
src/i18n/zh/shop.json Vedi File

@@ -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": "部分完成",


Caricamento…
Annulla
Salva