Преглед на файлове

Use new anonymous floor and bin labels

Workbench, delivery search, reports, and lot suggestion now match new db
debrand
Harry Groves преди 3 седмици
родител
ревизия
408086acc7
променени са 49 файла, в които са добавени 367 реда и са изтрити 286 реда
  1. +3
    -2
      src/app/(main)/chart/delivery/page.tsx
  2. +4
    -3
      src/app/api/chart/client.ts
  3. +2
    -1
      src/app/api/do/actions.tsx
  4. +3
    -2
      src/app/api/doworkbench/actions.ts
  5. +2
    -1
      src/app/api/jo/actions.ts
  6. +2
    -1
      src/app/api/pickOrder/actions.ts
  7. +3
    -2
      src/app/api/settings/deliveryOrderFloor/constants.ts
  8. +3
    -2
      src/app/utils/formatUtil.ts
  9. +4
    -3
      src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx
  10. +17
    -16
      src/components/DeliveryOrderFloorSettings/DeliveryOrderFloorSettings.tsx
  11. +9
    -8
      src/components/DoDetail/DoDetail.tsx
  12. +10
    -9
      src/components/DoSearch/DoSearch.tsx
  13. +3
    -2
      src/components/DoSearchWorkbench/DoSearchWorkbench.tsx
  14. +2
    -1
      src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx
  15. +4
    -3
      src/components/DoWorkbench/WorkbenchEtraMergeDialog.tsx
  16. +24
    -23
      src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx
  17. +4
    -3
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  18. +3
    -1
      src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx
  19. +3
    -2
      src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx
  20. +4
    -3
      src/components/DoWorkbench/workbenchLanePanelPrefs.ts
  21. +7
    -6
      src/components/FinishedGoodSearch/FGPickOrderCard.tsx
  22. +3
    -2
      src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx
  23. +20
    -19
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
  24. +27
    -26
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  25. +4
    -3
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  26. +6
    -5
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  27. +4
    -3
      src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx
  28. +2
    -1
      src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx
  29. +6
    -4
      src/components/InventorySearch/LotLabelPrintModal.tsx
  30. +6
    -5
      src/components/JoWorkbench/JoPickOrderList.tsx
  31. +19
    -13
      src/components/JoWorkbench/newJobPickExecution.tsx
  32. +10
    -9
      src/components/Jodetail/JoPickOrderList.tsx
  33. +2
    -1
      src/components/Jodetail/JodetailSearch.tsx
  34. +22
    -21
      src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx
  35. +4
    -3
      src/components/Jodetail/completeJobOrderRecord.tsx
  36. +16
    -8
      src/components/Jodetail/newJobPickExecution.tsx
  37. +5
    -4
      src/components/Jodetail/pickRecordHelpers.ts
  38. +9
    -8
      src/components/LotSuggestionRules/LotSuggestionRules.tsx
  39. +22
    -21
      src/components/Shop/RouteBoard.tsx
  40. +4
    -3
      src/components/Shop/Shop.tsx
  41. +3
    -2
      src/components/Shop/ShopDetail.tsx
  42. +6
    -5
      src/components/Shop/TruckLane.tsx
  43. +9
    -8
      src/components/Shop/TruckLaneDetail.tsx
  44. +6
    -5
      src/components/Shop/computeTruckLaneWarnings.ts
  45. +2
    -1
      src/components/Shop/routeBoardImportPreview.ts
  46. +4
    -3
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  47. +4
    -3
      src/components/StockTakeManagement/PickerCardList.tsx
  48. +19
    -0
      src/config/demoFloors.ts
  49. +7
    -6
      src/config/reportConfig.ts

+ 3
- 2
src/app/(main)/chart/delivery/page.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import React, { useCallback, useMemo, useState } from "react";
import {
Box,
@@ -37,8 +38,8 @@ const STAFF_PERF_STORE_FILTER_OPTIONS: {
label: string;
}[] = [
{ value: "all", label: "全部" },
{ value: "2/F", label: "2/F" },
{ value: "4/F", label: "4/F" },
{ value: FLOOR_A, label: FLOOR_A },
{ value: FLOOR_B, label: FLOOR_B },
{ value: "null_only", label: "車線-X" },
];



+ 4
- 3
src/app/api/chart/client.ts Целия файл

@@ -1,3 +1,4 @@
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

@@ -577,7 +578,7 @@ export async function fetchPlannedOutputByDateAndItem(
}

/** Warehouse / lane filter for staff delivery performance chart (delivery_order_pick_order.store_id). */
export type StaffDeliveryPerformanceStoreFilter = "all" | "2/F" | "4/F" | "null_only";
export type StaffDeliveryPerformanceStoreFilter = "all" | typeof FLOOR_A | typeof FLOOR_B | "null_only";

/** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */
export async function fetchStaffDeliveryPerformance(
@@ -591,8 +592,8 @@ export async function fetchStaffDeliveryPerformance(
if (endDate) p.set("endDate", endDate);
(staffNos ?? []).forEach((no) => p.append("staffNo", no));
if (storeFilter === "null_only") p.set("storeIdNull", "true");
else if (storeFilter === "2/F") p.set("storeId", "2/F");
else if (storeFilter === "4/F") p.set("storeId", "4/F");
else if (storeFilter === FLOOR_A) p.set("storeId", FLOOR_A);
else if (storeFilter === FLOOR_B) p.set("storeId", FLOOR_B);
const q = p.toString();
const res = await clientAuthFetch(
q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance`


+ 2
- 1
src/app/api/do/actions.tsx Целия файл

@@ -1,4 +1,5 @@
"use server";
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { BASE_API_URL } from "@/config/api";
// import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { revalidateTag } from "next/cache";
@@ -85,7 +86,7 @@ export interface ReleaseDoResponse {


export interface AssignByStoreRequest {
storeId: string; // "2/F" or "4/F"
storeId: string; // FLOOR_A or FLOOR_B
assignTo: number;
}



+ 3
- 2
src/app/api/doworkbench/actions.ts Целия файл

@@ -1,5 +1,6 @@
"use server";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { revalidateTag } from "next/cache";
import { BASE_API_URL } from "@/config/api";
import { serverFetchJson } from "@/app/utils/fetchUtil";
@@ -244,7 +245,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelection(
storeId?: string,
truck?: string,
releaseType?: string,
/** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */
/** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}`: Truck X split by DO supplier preferred floor (storeId stays null). */
floor?: string
): Promise<ReleasedDoPickOrderListItem[]> {
const params = new URLSearchParams();
@@ -266,7 +267,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday(
truck?: string,
requiredDeliveryDate?: string,
releaseType?: string,
/** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */
/** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}`: Truck X split by DO supplier preferred floor (storeId stays null). */
floor?: string
): Promise<ReleasedDoPickOrderListItem[]> {
const params = new URLSearchParams();


+ 2
- 1
src/app/api/jo/actions.ts Целия файл

@@ -1,5 +1,6 @@
"use server";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import { cache } from 'react';
import { Pageable, ServerFetchError, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { JobOrder, JoStatus, Machine, Operator } from ".";
@@ -133,7 +134,7 @@ export interface PrintPickRecordRequest{
pickOrderId: number;
printerId: number;
printQty: number;
floor?: "2F" | "3F" | "4F" | "ALL";
floor?: typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "ALL";
plasticBoxCartonQty?: number;
plasticBoxCartonQty2f?: number;
plasticBoxCartonQty3f?: number;


+ 2
- 1
src/app/api/pickOrder/actions.ts Целия файл

@@ -1,4 +1,5 @@
"use server";
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { BASE_API_URL } from "@/config/api";
// import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { revalidateTag } from "next/cache";
@@ -333,7 +334,7 @@ export interface DoPickOrderDetail {
}
export interface AutoAssignReleaseByStoreRequest {
userId: number;
storeId: string; // "2/F" | "4/F"
storeId: string; // FLOOR_A | FLOOR_B
}
export interface UpdateDoPickOrderHideStatusRequest {
id: number;


+ 3
- 2
src/app/api/settings/deliveryOrderFloor/constants.ts Целия файл

@@ -1,5 +1,6 @@
import { SETTING_SUPPLIERS_A, SETTING_SUPPLIERS_B } from "@/config/demoFloors";
/** `settings.name`:逗號分隔 supplier **code**(須與後端讀取邏輯一致)。 */
export const SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F";
export const SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F";
export const SETTING_DO_FLOOR_SUPPLIERS_2F = SETTING_SUPPLIERS_A;
export const SETTING_DO_FLOOR_SUPPLIERS_4F = SETTING_SUPPLIERS_B;

export const SETTING_DO_FLOOR_CATEGORY = "DO_FLOOR";

+ 3
- 2
src/app/utils/formatUtil.ts Целия файл

@@ -1,3 +1,4 @@
import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import dayjs, { ConfigType, Dayjs } from "dayjs";
import { Uom } from "../api/settings/uom";
import {
@@ -249,7 +250,7 @@ export const normalizeStoreId = (storeId: string | number | String | Number | nu
const storeIdStr = typeof storeId === 'string' || storeId instanceof String
? String(storeId)
: String(storeId);
if (storeIdStr === "2" || storeIdStr === "2F") return "2F";
if (storeIdStr === "4" || storeIdStr === "4F") return "4F";
if (storeIdStr === "2" || storeIdStr === FLOOR_A_KEY) return FLOOR_A_KEY;
if (storeIdStr === "4" || storeIdStr === FLOOR_B_KEY) return FLOOR_B_KEY;
return storeIdStr;
};

+ 4
- 3
src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Box,
@@ -449,8 +450,8 @@ const TruckScheduleDashboard: React.FC = () => {
displayEmpty
>
<MenuItem value="">{t("All Stores")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
<MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem>
</Select>
</FormControl>

@@ -535,7 +536,7 @@ const TruckScheduleDashboard: React.FC = () => {
<Chip
label={row.storeId || '-'}
size="small"
color={row.storeId === '2/F' ? 'primary' : 'secondary'}
color={row.storeId === FLOOR_A ? 'primary' : 'secondary'}
/>
</TableCell>
<TableCell>


+ 17
- 16
src/components/DeliveryOrderFloorSettings/DeliveryOrderFloorSettings.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import EditOutlined from "@mui/icons-material/EditOutlined";
@@ -57,7 +58,7 @@ function formatBracketList(codesCsv: string): string {
return `[${n.split(",").join(", ")}]`;
}

type EditFloor = "2F" | "4F";
type EditFloor = typeof FLOOR_A_KEY | typeof FLOOR_B_KEY;

type FloorRow = { code: string; name: string };

@@ -97,7 +98,7 @@ const DeliveryOrderFloorSettings: React.FC = () => {
const [codes4F, setCodes4F] = useState("");

const [editOpen, setEditOpen] = useState(false);
const [editFloor, setEditFloor] = useState<EditFloor>("2F");
const [editFloor, setEditFloor] = useState<EditFloor>(FLOOR_A_KEY);
const [dialogSaving, setDialogSaving] = useState(false);
const [comboLoading, setComboLoading] = useState(false);
const [supplierCombo, setSupplierCombo] = useState<ShopComboRow[] | null>(null);
@@ -130,8 +131,8 @@ const DeliveryOrderFloorSettings: React.FC = () => {
const display2F = useMemo(() => formatBracketList(codes2F), [codes2F]);
const display4F = useMemo(() => formatBracketList(codes4F), [codes4F]);

const currentDraftRows = editFloor === "2F" ? draftRows2F : draftRows4F;
const setCurrentDraftRows = editFloor === "2F" ? setDraftRows2F : setDraftRows4F;
const currentDraftRows = editFloor === FLOOR_A_KEY ? draftRows2F : draftRows4F;
const setCurrentDraftRows = editFloor === FLOOR_A_KEY ? setDraftRows2F : setDraftRows4F;

const openEdit = async (floor: EditFloor) => {
setEditFloor(floor);
@@ -172,12 +173,12 @@ const DeliveryOrderFloorSettings: React.FC = () => {
setError(null);
setSuccess(null);
try {
const rows = editFloor === "2F" ? draftRows2F : draftRows4F;
const rows = editFloor === FLOOR_A_KEY ? draftRows2F : draftRows4F;
const normalized = normalizeCodesCsv(floorRowsToCsv(rows));
const key =
editFloor === "2F" ? SETTING_DO_FLOOR_SUPPLIERS_2F : SETTING_DO_FLOOR_SUPPLIERS_4F;
editFloor === FLOOR_A_KEY ? SETTING_DO_FLOOR_SUPPLIERS_2F : SETTING_DO_FLOOR_SUPPLIERS_4F;
await postSettingClient(key, normalized);
if (editFloor === "2F") setCodes2F(normalized);
if (editFloor === FLOOR_A_KEY) setCodes2F(normalized);
else setCodes4F(normalized);
setSuccess(t("Saved"));
setEditOpen(false);
@@ -237,7 +238,7 @@ const DeliveryOrderFloorSettings: React.FC = () => {
return;
}
const canonical = hit.code.trim();
const otherRows = editFloor === "2F" ? draftRows4F : draftRows2F;
const otherRows = editFloor === FLOOR_A_KEY ? draftRows4F : draftRows2F;
if (currentDraftRows.some((r) => r.code.toLowerCase() === canonical.toLowerCase())) {
setAddError(t("Duplicate in floor"));
return;
@@ -286,7 +287,7 @@ const DeliveryOrderFloorSettings: React.FC = () => {
}}
>
<Typography component="span" variant="subtitle1" sx={{ minWidth: 120, fontWeight: 600 }}>
{t("2F supplier")}
{t(FLOOR_A_KEY + " supplier")}
</Typography>
<Typography
component="span"
@@ -296,10 +297,10 @@ const DeliveryOrderFloorSettings: React.FC = () => {
{display2F}
</Typography>
<IconButton
aria-label={t("Edit 2F")}
aria-label={t("Edit " + FLOOR_A_KEY)}
color="primary"
edge="end"
onClick={() => void openEdit("2F")}
onClick={() => void openEdit(FLOOR_A_KEY)}
size="small"
>
<EditOutlined />
@@ -319,7 +320,7 @@ const DeliveryOrderFloorSettings: React.FC = () => {
}}
>
<Typography component="span" variant="subtitle1" sx={{ minWidth: 120, fontWeight: 600 }}>
{t("4F supplier")}
{t(FLOOR_B_KEY + " supplier")}
</Typography>
<Typography
component="span"
@@ -329,10 +330,10 @@ const DeliveryOrderFloorSettings: React.FC = () => {
{display4F}
</Typography>
<IconButton
aria-label={t("Edit 4F")}
aria-label={t("Edit " + FLOOR_B_KEY)}
color="primary"
edge="end"
onClick={() => void openEdit("4F")}
onClick={() => void openEdit(FLOOR_B_KEY)}
size="small"
>
<EditOutlined />
@@ -361,8 +362,8 @@ const DeliveryOrderFloorSettings: React.FC = () => {
onChange={onFloorSelectChange}
disabled={dialogSaving || comboLoading}
>
<MenuItem value="2F">2/F</MenuItem>
<MenuItem value="4F">4/F</MenuItem>
<MenuItem value={FLOOR_A_KEY}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_B_KEY}>{FLOOR_B}</MenuItem>
</Select>
</FormControl>
<Button


+ 9
- 8
src/components/DoDetail/DoDetail.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import type { DoDetail as DoDetailType } from "@/app/api/do/actions";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
@@ -203,20 +204,20 @@ const DoDetail: React.FC<Props> = ({
<Button
variant="contained"
color="primary"
onClick={() => handleAssignByStore("2/F")}
onClick={() => handleAssignByStore(FLOOR_A)}
disabled={isAssigning}
sx={{ minWidth: 120 }}
>
{t("Assign 2/F")}
{t("Assign " + FLOOR_A)}
</Button>
<Button
variant="contained"
color="secondary"
onClick={() => handleAssignByStore("4/F")}
onClick={() => handleAssignByStore(FLOOR_B)}
disabled={isAssigning}
sx={{ minWidth: 120 }}
>
{t("Assign 4/F")}
{t("Assign " + FLOOR_B)}
</Button>
</Stack>
@@ -224,20 +225,20 @@ const DoDetail: React.FC<Props> = ({
<Button
variant="outlined"
color="primary"
onClick={() => handleReleaseByStore("2/F")}
onClick={() => handleReleaseByStore(FLOOR_A)}
disabled={isAssigning}
sx={{ minWidth: 120 }}
>
{t("Release 2/F")}
{t("Release " + FLOOR_A)}
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => handleReleaseByStore("4/F")}
onClick={() => handleReleaseByStore(FLOOR_B)}
disabled={isAssigning}
sx={{ minWidth: 120 }}
>
{t("Release 4/F")}
{t("Release " + FLOOR_B)}
</Button>
</Stack>
</Box>


+ 10
- 9
src/components/DoSearch/DoSearch.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import { DoResult } from "@/app/api/do";
import {
DoSearchAll,
@@ -64,8 +65,8 @@ type Props = {
};
type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>;
type SearchParamNames = keyof SearchBoxInputs;
type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA" | "REPLENISH";
type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string };
type DoSearchTab = typeof FLOOR_A_KEY | typeof FLOOR_B_KEY | "TRUCK_X" | "ETRA" | "REPLENISH";
type TabFilter = { floor: typeof FLOOR_A_KEY | typeof FLOOR_B_KEY | null; isExtra: boolean; forceTruckKeyword?: string };

// put all this into a new component
// ConsoDoForm
@@ -103,7 +104,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [isWorkbench, setIsWorkbench] = useState(false);
const [activeTab, setActiveTab] = useState<DoSearchTab>("2F");
const [activeTab, setActiveTab] = useState<DoSearchTab>(FLOOR_A_KEY);
const [searchBoxResetKey, setSearchBoxResetKey] = useState(0);
const [pagingController, setPagingController] = useState({
pageNum: 1,
@@ -326,10 +327,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea

const resolveTabFilter = useCallback((tab: DoSearchTab): TabFilter => {
switch (tab) {
case "2F":
return { floor: "2F", isExtra: false };
case "4F":
return { floor: "4F", isExtra: false };
case FLOOR_A_KEY:
return { floor: FLOOR_A_KEY, isExtra: false };
case FLOOR_B_KEY:
return { floor: FLOOR_B_KEY, isExtra: false };
case "TRUCK_X":
return { floor: null, isExtra: false, forceTruckKeyword: "x" };
case "ETRA":
@@ -802,8 +803,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
onChange={handleTabChange}
variant="scrollable"
>
<Tab value="2F" label="2/F" />
<Tab value="4F" label="4/F" />
<Tab value={FLOOR_A_KEY} label={FLOOR_A} />
<Tab value={FLOOR_B_KEY} label={FLOOR_B} />
<Tab value="TRUCK_X" label={t("Truck X")} />
<Tab value="ETRA" label={t("Etra")} />
<Tab value="REPLENISH" label={t("Replenishment")} />


+ 3
- 2
src/components/DoSearchWorkbench/DoSearchWorkbench.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { DoResult } from "@/app/api/do";
import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo } from "@/app/api/do/actions";
import {
@@ -150,8 +151,8 @@ const DoSearchWorkbench: React.FC<Props> = ({
paramName: "floor",
type: "select-labelled",
options: [
{ label: "2F", value: "2F" },
{ label: "4F", value: "4F" },
{ label: FLOOR_A_KEY, value: FLOOR_A_KEY },
{ label: FLOOR_B_KEY, value: FLOOR_B_KEY },
],
},
{


+ 2
- 1
src/components/DoWorkbench/TruckRoutingSummaryTabWorkbench.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { useEffect, useState } from "react";
import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
@@ -114,7 +115,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => {
<TextField
select
fullWidth
label={t("2/F or 4/F")}
label={t(FLOOR_A + " or " + FLOOR_B)}
value={storeId}
onChange={(e) => onStoreChange(e.target.value)}
>


+ 4
- 3
src/components/DoWorkbench/WorkbenchEtraMergeDialog.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -41,8 +42,8 @@ type Props = {

function floorLabel(storeId: string | null | undefined, t: (k: string) => string): string {
const s = (storeId ?? "").trim().replace(/\//g, "").toUpperCase();
if (s === "2F") return "2/F";
if (s === "4F") return "4/F";
if (s === FLOOR_A_KEY) return FLOOR_A;
if (s === FLOOR_B_KEY) return FLOOR_B;
if (!s) return t("Truck X");
return storeId ?? "";
}
@@ -58,7 +59,7 @@ function laneSecondary(c: WorkbenchMergeTicketCandidate, t: (k: string) => strin
const dep = (c.truckDepartureTime ?? "").trim();
const lane = (c.truckLanceCode ?? "").trim();
const floor = floorLabel(c.storeId, t);
const is4F = (c.storeId ?? "").trim().replace(/\//g, "").toUpperCase() === "4F";
const is4F = (c.storeId ?? "").trim().replace(/\//g, "").toUpperCase() === FLOOR_B_KEY;
const seq =
is4F && c.loadingSequence != null
? `${t("Loading sequence")} ${c.loadingSequence} · `


+ 24
- 23
src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -53,7 +54,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const [selectedStore, setSelectedStore] = useState<string>("2/F");
const [selectedStore, setSelectedStore] = useState<string>(FLOOR_A);
const [selectedTruck, setSelectedTruck] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false);
const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
@@ -92,7 +93,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
[ticketFloor],
);
const defaultTruckCount =
(ticketFloor === "2/F" ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0;
(ticketFloor === FLOOR_A ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0;
const etraEnterInFlightRef = useRef(false);
const [etraMergeDialogOpen, setEtraMergeDialogOpen] = useState(false);

@@ -167,8 +168,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
const [s2, s4] = await Promise.all([
fetchWorkbenchStoreLaneSummary("2/F", dateParam, releaseType),
fetchWorkbenchStoreLaneSummary("4/F", dateParam, releaseType),
fetchWorkbenchStoreLaneSummary(FLOOR_A, dateParam, releaseType),
fetchWorkbenchStoreLaneSummary(FLOOR_B, dateParam, releaseType),
]);
setSummary2F(s2);
setSummary4F(s4);
@@ -217,8 +218,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
startFullTimer();
try {
const [list2F, list4F] = await Promise.all([
fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "2/F"),
fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "4/F"),
fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, FLOOR_A),
fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, FLOOR_B),
]);
const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {};
@@ -441,8 +442,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
<FormControl fullWidth size="small">
<InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel>
<Select labelId="ticket-floor-select-label" value={ticketFloor} label={t("Floor ticket")} onChange={(e) => patchLanePanelPrefs({ ticketFloor: e.target.value as WorkbenchLanePanelPrefs["ticketFloor"] })}>
<MenuItem value="2/F">{t("2F ticket")}</MenuItem>
<MenuItem value="4/F">{t("4F ticket")}</MenuItem>
<MenuItem value={FLOOR_A}>{t(FLOOR_A_KEY + " ticket")}</MenuItem>
<MenuItem value={FLOOR_B}>{t(FLOOR_B_KEY + " ticket")}</MenuItem>
</Select>
</FormControl>
</Box>
@@ -481,10 +482,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({

{!inEtraUi ? (
<Grid container spacing={2}>
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_A}</Typography>
<Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
{isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : (
<Grid container spacing={1}>
@@ -494,7 +495,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
<Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography>
<Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
{row.lanes.map((lane) => (
<Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("2/F", row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}>
<Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick(FLOOR_A, row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}>
{`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
</Button>
))}
@@ -509,10 +510,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
</Grid>
)}

{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_B}</Typography>
<Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
{isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !truckGroups4F.length ? renderNoEntry() : (
<Grid container spacing={1}>
@@ -524,7 +525,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
{slots.map((slot) => {
const handlerName = (slot.lane.handlerName ?? "").trim();
return (
<Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("4/F", slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}>
<Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick(FLOOR_B, slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}>
{`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})${handlerName ? ` ${handlerName}` : ""}`}
</Button>
);
@@ -585,21 +586,21 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
</Box>
</Grid>

{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_A}</Typography>
<Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
{truckCounts2F.length === 0 ? renderNoEntry() : (
<Grid container spacing={1}>
{truckCounts2F.map(({ truck, count }) => (
<Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
<Grid item xs={6} sm={4} md={3} key={`${FLOOR_A_KEY}-${truck}`} sx={{ display: "flex" }}>
<Button
variant="outlined"
onClick={() => {
clearModalEtraContext();
setIsDefaultTruck(false);
setSelectedStore("2/F");
setSelectedStore(FLOOR_A);
setSelectedTruck(truck);
setModalOpen(true);
}}
@@ -616,21 +617,21 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
</Grid>
)}

{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_B}</Typography>
<Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
{truckCounts4F.length === 0 ? renderNoEntry() : (
<Grid container spacing={1}>
{truckCounts4F.map(({ truck, count }) => (
<Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
<Grid item xs={6} sm={4} md={3} key={`${FLOOR_B_KEY}-${truck}`} sx={{ display: "flex" }}>
<Button
variant="outlined"
onClick={() => {
clearModalEtraContext();
setIsDefaultTruck(false);
setSelectedStore("4/F");
setSelectedStore(FLOOR_B);
setSelectedTruck(truck);
setModalOpen(true);
}}
@@ -694,7 +695,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
(lane.truckLanceCode ?? "").trim() === "車線-X" ||
(lane.truckLanceCode ?? "").trim() === "Truck X";
const floorKey = sid.replace(/\//g, "").toUpperCase();
const is4F = floorKey === "4F";
const is4F = floorKey === FLOOR_B_KEY;
const labelCore =
isTruckX
? `${t("車線-X")}${sid ? ` · ${sid}` : ""} (${lane.unassigned}/${lane.total})`


+ 4
- 3
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import {
Box,
Button,
@@ -742,8 +743,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
.toUpperCase()
.replace(/\s/g, "");
const floorKey = storeId.replace(/\//g, "");
if (floorKey === "2F") return "Label機 2F A+B";
if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
if (floorKey === FLOOR_A_KEY) return LABEL_A;
if (floorKey === FLOOR_B_KEY) return LABEL_B;
return undefined;
}, [fgPickOrders]);

@@ -1037,7 +1038,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
.replace(/\//g, '')
.replace(/\s/g, '');
const pickOrderLinesForDisplay =
doFloorKey === '2F'
doFloorKey === FLOOR_A_KEY
? [...(mergedPickOrder.pickOrderLines || [])].sort((a: any, b: any) => {
const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999;
const bo = b.itemOrder != null ? Number(b.itemOrder) : 999999;


+ 3
- 1
src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx Целия файл

@@ -5,6 +5,7 @@
* 「掃碼提貨」 per listed lot row (parent calls `workbenchScanPick` with `inventoryLotLineId`).
*/

import { LOC_PREFIX } from "@/config/demoFloors";
import React, {
useCallback,
useEffect,
@@ -455,7 +456,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
return availableLots.filter((lot) => {
// 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號
if (lot._scanned) return true;
return String(lot.warehouseCode ?? "").startsWith(prefix);
const code = String(lot.warehouseCode ?? "");
return code.startsWith(prefix) || code.startsWith(LOC_PREFIX);
});
}, [availableLots, warehouseCodePrefixFilter]);



+ 3
- 2
src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
Box,
@@ -278,8 +279,8 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => {
displayEmpty
>
<MenuItem value="">{t("All Floors")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
<MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ minWidth: 150 }} size="small">


+ 4
- 3
src/components/DoWorkbench/workbenchLanePanelPrefs.ts Целия файл

@@ -1,5 +1,6 @@
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
export type WorkbenchLaneDateKey = "today" | "tomorrow" | "dayAfterTomorrow";
export type WorkbenchLaneFloor = "2/F" | "4/F";
export type WorkbenchLaneFloor = typeof FLOOR_A | typeof FLOOR_B;

/** Tab 0 lane assignment filters — lifted to DoWorkbenchTabs so they survive tab switches. */
export type WorkbenchLanePanelPrefs = {
@@ -10,7 +11,7 @@ export type WorkbenchLanePanelPrefs = {

export const DEFAULT_WORKBENCH_LANE_PANEL_PREFS: WorkbenchLanePanelPrefs = {
selectedDate: "today",
ticketFloor: "2/F",
ticketFloor: FLOOR_A,
releaseType: "batch",
};

@@ -19,5 +20,5 @@ export function isWorkbenchLaneDateKey(v: string): v is WorkbenchLaneDateKey {
}

export function isWorkbenchLaneFloor(v: string): v is WorkbenchLaneFloor {
return v === "2/F" || v === "4/F";
return v === FLOOR_A || v === FLOOR_B;
}

+ 7
- 6
src/components/FinishedGoodSearch/FGPickOrderCard.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel, Card, CardContent } from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -28,8 +29,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
setIsLoadingSummary(true);
try {
const [s2, s4] = await Promise.all([
fetchStoreLaneSummary("2/F"),
fetchStoreLaneSummary("4/F")
fetchStoreLaneSummary(FLOOR_A),
fetchStoreLaneSummary(FLOOR_B)
]);
setSummary2F(s2);
setSummary4F(s4);
@@ -169,7 +170,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
color: '#1976d2'
}}
>
2/F
{FLOOR_A}
</Typography>
</Box>
@@ -232,7 +233,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
onClick={() => handleAssignByLane(FLOOR_A, item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',
@@ -282,7 +283,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
color: '#1976d2'
}}
>
4/F
{FLOOR_B}
</Typography>
</Box>
@@ -345,7 +346,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
onClick={() => handleAssignByLane(FLOOR_B, item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',


+ 3
- 2
src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
/**
* 權限說明(與全站一致):
* - 登入後 JWT / session 帶有 `abilities: string[]`(見 config/authConfig、authorities.ts)。
@@ -303,8 +304,8 @@ const FGPickOrderTicketReleaseTable: React.FC = () => {
displayEmpty
>
<MenuItem value="">{t("All Floors")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
<MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem>
</Select>
</FormControl>



+ 20
- 19
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Alert,
@@ -29,7 +30,7 @@ import {
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { useTranslation } from "react-i18next";

type FloorFilter = "all" | "2/F" | "4/F";
type FloorFilter = "all" | typeof FLOOR_A | typeof FLOOR_B;
type BreakdownDimension = "lane" | "shop" | "floor";

type DailySummaryRow = {
@@ -361,10 +362,10 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
total: 0,
};

if (record.storeId === "2/F") {
if (record.storeId === FLOOR_A) {
current.floor2F += cartonQty;
}
if (record.storeId === "4/F") {
if (record.storeId === FLOOR_B) {
current.floor4F += cartonQty;
}
if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) {
@@ -390,18 +391,18 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
let truckX = 0;
filteredRecords.forEach((record) => {
const cartonQty = Number(record.numberOfCartons ?? 0);
if (record.storeId === "2/F") floor2F += cartonQty;
if (record.storeId === "4/F") floor4F += cartonQty;
if (record.storeId === FLOOR_A) floor2F += cartonQty;
if (record.storeId === FLOOR_B) floor4F += cartonQty;
if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) truckX += cartonQty;
});
return toSorted(
[
{ key: "2/F", label: "2/F", qty: floor2F },
{ key: "4/F", label: "4/F", qty: floor4F },
{ key: FLOOR_A, label: FLOOR_A, qty: floor2F },
{ key: FLOOR_B, label: FLOOR_B, qty: floor4F },
{ key: TRUCK_X_LANE, label: shortLaneLabel(TRUCK_X_LANE), qty: truckX },
].filter((row) => {
if (row.qty > 0) return true;
if (row.key === "2/F" || row.key === "4/F") return floor === row.key;
if (row.key === FLOOR_A || row.key === FLOOR_B) return floor === row.key;
return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE;
}),
);
@@ -490,7 +491,7 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
(row: BreakdownRow) => {
if (breakdownDimension === "lane") return lane === row.key;
if (breakdownDimension === "shop") return shop === row.key;
if (row.key === "2/F" || row.key === "4/F") return floor === row.key;
if (row.key === FLOOR_A || row.key === FLOOR_B) return floor === row.key;
return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE;
},
[breakdownDimension, floor, lane, shop],
@@ -550,8 +551,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
};
if (!current.shopName && shopName) current.shopName = shopName;

if (record.storeId === "2/F") current.floor2F += cartonQty;
if (record.storeId === "4/F") current.floor4F += cartonQty;
if (record.storeId === FLOOR_A) current.floor2F += cartonQty;
if (record.storeId === FLOOR_B) current.floor4F += cartonQty;
if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) current.truckX += cartonQty;

current.total += cartonQty;
@@ -677,8 +678,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
t("Date"),
t("Shop code"),
t("Shop Name"),
t("2/F carton qty"),
t("4/F carton qty"),
t(FLOOR_A + " carton qty"),
t(FLOOR_B + " carton qty"),
t("Truck X carton qty"),
t("Total carton qty"),
],
@@ -693,8 +694,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
]),
[...blank],
[t("Summary"), ...blank.slice(1)],
[t("2/F carton qty"), "", "", reportSummary.floor2F, "", "", ""],
[t("4/F carton qty"), "", "", "", reportSummary.floor4F, "", ""],
[t(FLOOR_A + " carton qty"), "", "", reportSummary.floor2F, "", "", ""],
[t(FLOOR_B + " carton qty"), "", "", "", reportSummary.floor4F, "", ""],
[t("Truck X carton qty"), "", "", "", "", reportSummary.truckX, ""],
[t("Total carton qty"), "", "", "", "", "", reportSummary.total],
];
@@ -990,8 +991,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
onChange={(event) => setFloor(event.target.value as FloorFilter)}
>
<MenuItem value={ALL}>{t("All")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
<MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={3}>
@@ -1041,8 +1042,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
<Paper sx={{ px: 1, py: 1 }}>
<Grid container>
{[
{ label: t("2/F carton qty"), value: summary.floor2F },
{ label: t("4/F carton qty"), value: summary.floor4F },
{ label: t(FLOOR_A + " carton qty"), value: summary.floor2F },
{ label: t(FLOOR_B + " carton qty"), value: summary.floor4F },
{ label: t("Truck X carton qty"), value: summary.truckX },
{ label: t("Total carton qty"), value: summary.total },
].map((kpi, index) => (


+ 27
- 26
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
@@ -22,7 +23,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const [selectedStore, setSelectedStore] = useState<string>("2/F");
const [selectedStore, setSelectedStore] = useState<string>(FLOOR_A);
const [selectedTruck, setSelectedTruck] = useState<string>("");
const [selectedDefaultTruck, setSelectedDefaultTruck] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false);
@@ -44,7 +45,7 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to

const [selectedDate, setSelectedDate] = useState<string>("today");
const [releaseType, setReleaseType] = useState<string>("batch");
const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F");
const [ticketFloor, setTicketFloor] = useState<typeof FLOOR_A | typeof FLOOR_B>(FLOOR_A);
const startFullTimer = () => {
if (typeof window === "undefined") return;
const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
@@ -79,8 +80,8 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to
}
const [s2, s4] = await Promise.all([
fetchStoreLaneSummary("2/F", dateParam, releaseType),
fetchStoreLaneSummary("4/F", dateParam, releaseType)
fetchStoreLaneSummary(FLOOR_A, dateParam, releaseType),
fetchStoreLaneSummary(FLOOR_B, dateParam, releaseType)
]);
setSummary2F(s2);
setSummary4F(s4);
@@ -117,8 +118,8 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to
startFullTimer();
try {
const [list2F, list4F] = await Promise.all([
fetchReleasedDoPickOrdersForSelection(undefined, "2/F"),
fetchReleasedDoPickOrdersForSelection(undefined, "4/F"),
fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_A),
fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_B),
]);
const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {};
@@ -388,10 +389,10 @@ const getDateLabel = (offset: number) => {
id="ticket-floor-select"
value={ticketFloor}
label={t("Floor ticket")}
onChange={(e) => setTicketFloor(e.target.value as "2/F" | "4/F")}
onChange={(e) => setTicketFloor(e.target.value as typeof FLOOR_A | typeof FLOOR_B)}
>
<MenuItem value="2/F">{t("2F ticket")}</MenuItem>
<MenuItem value="4/F">{t("4F ticket")}</MenuItem>
<MenuItem value={FLOOR_A}>{t(FLOOR_A_KEY + " ticket")}</MenuItem>
<MenuItem value={FLOOR_B}>{t(FLOOR_B_KEY + " ticket")}</MenuItem>
</Select>
</FormControl>
</Box>
@@ -414,7 +415,7 @@ const getDateLabel = (offset: number) => {
{/* Grid containing both floors */}
<Grid container spacing={2}>
{/* 2/F 楼层面板 */}
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */}
@@ -426,7 +427,7 @@ const getDateLabel = (offset: number) => {
pt: 1
}}
>
2/F
{FLOOR_A}
</Typography>
{/* Content Box */}
@@ -495,7 +496,7 @@ const getDateLabel = (offset: number) => {
disabled={lane.unassigned === 0 || isAssigning}
onClick={() =>
handleLaneButtonClick(
"2/F",
FLOOR_A,
row.truckDepartureTime,
lane.truckLanceCode,
null,
@@ -530,7 +531,7 @@ const getDateLabel = (offset: number) => {
</Stack>
</Grid>
)}
{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */}
@@ -542,7 +543,7 @@ const getDateLabel = (offset: number) => {
pt: 1
}}
>
4/F
{FLOOR_B}
</Typography>
{/* Content Box */}
@@ -613,7 +614,7 @@ const getDateLabel = (offset: number) => {
disabled={slot.lane.unassigned === 0 || isAssigning}
onClick={() =>
handleLaneButtonClick(
"4/F",
FLOOR_B,
slot.truckDepartureTime,
slot.lane.truckLanceCode,
slot.lane.loadingSequence ?? null,
@@ -720,11 +721,11 @@ const getDateLabel = (offset: number) => {
</Box>
</Grid>
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
2/F
{FLOOR_A}
</Typography>
<Box
sx={{
@@ -751,7 +752,7 @@ const getDateLabel = (offset: number) => {
) : (
<Grid container spacing={1}>
{truckCounts2F.map(({ truck, count }) => (
<Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
<Grid item xs={6} sm={4} md={3} key={`${FLOOR_A_KEY}-${truck}`} sx={{ display: "flex" }}>

<Button
@@ -759,7 +760,7 @@ const getDateLabel = (offset: number) => {
size="medium"
onClick={() => {
setIsDefaultTruck(false);
setSelectedStore("2/F");
setSelectedStore(FLOOR_A);
setSelectedTruck(truck);
setModalOpen(true);
}}
@@ -788,11 +789,11 @@ const getDateLabel = (offset: number) => {
</Stack>
</Grid>
)}
{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
4/F
{FLOOR_B}
</Typography>
<Box
sx={{
@@ -819,7 +820,7 @@ const getDateLabel = (offset: number) => {
) : (
<Grid container spacing={1}>
{truckCounts4F.map(({ truck, count }) => (
<Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
<Grid item xs={6} sm={4} md={3} key={`${FLOOR_B_KEY}-${truck}`} sx={{ display: "flex" }}>
<Button
@@ -827,7 +828,7 @@ const getDateLabel = (offset: number) => {
size="medium"
onClick={() => {
setIsDefaultTruck(false);
setSelectedStore("4/F");
setSelectedStore(FLOOR_B);
setSelectedTruck(truck);
setModalOpen(true);
}}
@@ -877,7 +878,7 @@ const getDateLabel = (offset: number) => {
variant="outlined"
size="medium"
onClick={() => {
setSelectedStore("4/F"); // 或用專門標示 Truck X
setSelectedStore(FLOOR_B); // 或用專門標示 Truck X
setSelectedTruck("車線-X");
setIsDefaultTruck(true);
setDefaultDateScope("before"); // 類似上一輪說的 dateScope
@@ -902,8 +903,8 @@ const getDateLabel = (offset: number) => {
const loadCounts = async () => {
try {
const [list2F, list4F] = await Promise.all([
fetchReleasedDoPickOrdersForSelection(undefined, "2/F"),
fetchReleasedDoPickOrdersForSelection(undefined, "4/F"),
fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_A),
fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_B),
]);
const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {};


+ 4
- 3
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Целия файл

@@ -1,4 +1,5 @@
"use client";
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { PickOrderResult } from "@/app/api/pickOrder";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -134,8 +135,8 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo
setIsLoadingSummary(true);
try {
const [s2, s4] = await Promise.all([
fetchStoreLaneSummary("2/F"),
fetchStoreLaneSummary("4/F")
fetchStoreLaneSummary(FLOOR_A),
fetchStoreLaneSummary(FLOOR_B)
]);
setSummary2F(s2);
setSummary4F(s4);
@@ -300,7 +301,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo
Swal.fire({
position: "bottom-end",
icon: "warning",
text: "請先選擇 2/F 或 4/F、車線和日期",
text: "請先選擇 " + FLOOR_A + " 或 " + FLOOR_B + "、車線和日期",
showConfirmButton: false,
timer: 1800,
});


+ 6
- 5
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Целия файл

@@ -1,6 +1,7 @@
/*
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import {
Box,
Button,
@@ -786,7 +787,7 @@ const PickExecution: React.FC<Props> = ({
.trim()
.toUpperCase()
.replace(/\s/g, "");
// e.g. "2/F" -> "2F-", "4/F" -> "4F-"
// e.g. FLOOR_A -> FLOOR_A_KEY + "-", FLOOR_B -> FLOOR_B_KEY + "-"
const floorKey = storeId.replace(/\//g, "");
return floorKey ? `${floorKey}-` : "";
}, [fgPickOrders]);
@@ -796,8 +797,8 @@ const PickExecution: React.FC<Props> = ({
.toUpperCase()
.replace(/\s/g, "");
const floorKey = storeId.replace(/\//g, "");
if (floorKey === "2F") return "Label機 2F A+B";
if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
if (floorKey === FLOOR_A_KEY) return LABEL_A;
if (floorKey === FLOOR_B_KEY) return LABEL_B;
return undefined;
}, [fgPickOrders]);
// Add these missing state variables after line 352
@@ -1212,14 +1213,14 @@ const PickExecution: React.FC<Props> = ({
// 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = [];

// 2/F 與後端 store_id 一致時需按 itemOrder;避免 API 未走 2F 分支時畫面仍亂序
// ${FLOOR_A} 與後端 store_id 一致時需按 itemOrder;避免 API 未走 ${FLOOR_A_KEY} 分支時畫面仍亂序
const doFloorKey = String(hierarchicalData.fgInfo.storeId ?? "")
.trim()
.toUpperCase()
.replace(/\//g, "")
.replace(/\s/g, "");
const pickOrderLinesForDisplay =
doFloorKey === "2F"
doFloorKey === FLOOR_A_KEY
? [...(mergedPickOrder.pickOrderLines || [])].sort(
(a: any, b: any) => {
const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999;


+ 4
- 3
src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx Целия файл

@@ -1,6 +1,7 @@
// 新建檔案
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Modal,
Box,
@@ -37,7 +38,7 @@ export type ReleasedDoPickListBridge = {
storeId?: string,
truck?: string,
releaseType?: string,
/** Optional `2F`/`4F` for Truck X supplier-floor split */
/** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` for Truck X supplier-floor split */
floor?: string
) => Promise<ReleasedDoPickOrderListItem[]>;
/** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */
@@ -47,7 +48,7 @@ export type ReleasedDoPickListBridge = {
truck?: string,
requiredDeliveryDate?: string,
releaseType?: string,
/** Optional `2F`/`4F` for Truck X supplier-floor split */
/** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` for Truck X supplier-floor split */
floor?: string
) => Promise<ReleasedDoPickOrderListItem[]>;
assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>;
@@ -75,7 +76,7 @@ interface Props {
*/
filterRequiredDeliveryDate?: string;
/**
* Truck X only: `2F`/`4F` (or `2/F`/`4/F`) — filter by DO supplier preferred floor; storeId stays null.
* Truck X only: `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` (or `${FLOOR_A}`/`${FLOOR_B}`) — filter by DO supplier preferred floor; storeId stays null.
*/
truckXFloor?: string;
}


+ 2
- 1
src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { useEffect, useState } from "react";
import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
@@ -121,7 +122,7 @@ const TruckRoutingSummaryTab: React.FC<Props> = ({ onFiltersChange }) => {
<TextField
select
fullWidth
label="2/F 或 4/F"
label={FLOOR_A + " 或 " + FLOOR_B}
value={storeId}
onChange={(e) => onStoreChange(e.target.value)}
>


+ 6
- 4
src/components/InventorySearch/LotLabelPrintModal.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, LOC_PREFIX } from "@/config/demoFloors";
import React, {
useCallback,
useEffect,
@@ -77,7 +78,7 @@ export interface LotLabelPrintModalProps {
reminderText?: string;
/** 額外標題(顯示在最上方,reminderText 之下) */
statusTitleText?: string;
/** 只顯示特定倉位前綴(例如 "2F-") */
/** 只顯示特定倉位前綴(例如 FLOOR_A_KEY + "-") */
warehouseCodePrefixFilter?: string;
/** 不顯示觸發視窗的批號(analysis.scanned) */
hideTriggeredLot?: boolean;
@@ -346,9 +347,10 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({
const filteredLots = useMemo(() => {
const prefix = String(warehouseCodePrefixFilter ?? "").trim();
if (!prefix) return availableLots;
return availableLots.filter((lot) =>
String(lot.warehouseCode ?? "").startsWith(prefix),
);
return availableLots.filter((lot) => {
const code = String(lot.warehouseCode ?? "");
return code.startsWith(prefix) || code.startsWith(LOC_PREFIX);
});
}, [availableLots, warehouseCodePrefixFilter]);

const selectedPrinter = useMemo(() => {


+ 6
- 5
src/components/JoWorkbench/JoPickOrderList.tsx Целия файл

@@ -1,4 +1,5 @@
"use client";
import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
@@ -45,7 +46,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ planStart: today });

type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT";
type FloorFilter = "ALL" | typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "NO_LOT";

const searchCriteria: Criterion<any>[] = [
{ label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" },
@@ -76,9 +77,9 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
paramName: "floor",
type: "select-labelled",
options: [
{ label: "2F", value: "2F" },
{ label: "3F", value: "3F" },
{ label: "4F", value: "4F" },
{ label: FLOOR_A_KEY, value: FLOOR_A_KEY },
{ label: FLOOR_D_KEY, value: FLOOR_D_KEY },
{ label: FLOOR_B_KEY, value: FLOOR_B_KEY },
{ label: t("No Lot"), value: "NO_LOT" },
],
},
@@ -86,7 +87,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {

const selectedFloor: FloorFilter = (() => {
const floor = String(searchQuery.floor || "ALL");
if (floor === "2F" || floor === "3F" || floor === "4F" || floor === "NO_LOT") {
if (floor === FLOOR_A_KEY || floor === FLOOR_D_KEY || floor === FLOOR_B_KEY || floor === "NO_LOT") {
return floor;
}
return "ALL";


+ 19
- 13
src/components/JoWorkbench/newJobPickExecution.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import {
Box,
Button,
@@ -1017,17 +1018,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
[],
);
const extractFloor = (lot: any): string => {
const raw = lot.routerRoute || lot.routerArea || lot.location || "";
const match = raw.match(/^(\d+F?)/i) || raw.split("-")[0];
return (match?.[1] || match || raw || "")
.toUpperCase()
.replace(/(\d)F?/i, "$1F");
const raw = String(lot.routerRoute || lot.routerArea || lot.location || "");
const head = raw.trim().toUpperCase().replace(/\//g, "").split("-")[0];
if (head === "2" || head === FLOOR_A_KEY) return FLOOR_A_KEY;
if (head === "4" || head === FLOOR_B_KEY) return FLOOR_B_KEY;
if (head === "3" || head === FLOOR_D_KEY) return FLOOR_D_KEY;
if (head === "1" || head === FLOOR_C_KEY) return FLOOR_C_KEY;
return head;
};

// 楼层排序权重:4F > 3F > 2F(数字越大越靠前)
// B/F, then D/F, then A/F.
const floorSortOrder = (floor: string): number => {
const n = parseInt(floor.replace(/\D/g, ""), 10);
return isNaN(n) ? 0 : n;
if (floor === FLOOR_B_KEY) return 4;
if (floor === FLOOR_D_KEY) return 3;
if (floor === FLOOR_A_KEY) return 2;
if (floor === FLOOR_C_KEY) return 1;
return 0;
};
const combinedLotData = useMemo(() => {
const lots = getAllLotsFromHierarchical(jobOrderData);
@@ -1156,8 +1162,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
.toUpperCase()
.replace(/\s/g, "");
const floorKey = storeId.replace(/\//g, "");
if (floorKey === "2F") return "Label機 2F A+B";
if (floorKey === "4F") return "Label機 4F 乾貨 C, D";
if (floorKey === FLOOR_A_KEY) return LABEL_A;
if (floorKey === FLOOR_B_KEY) return LABEL_B;
return undefined;
}, [workbenchStoreId]);

@@ -3905,7 +3911,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("2F")}
onClick={() => handlePickRecord(FLOOR_A_KEY)}
>
{t("Print Pick Record")} 2F
</Button>
@@ -3913,7 +3919,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("3F")}
onClick={() => handlePickRecord(FLOOR_D_KEY)}
>
{t("Print Pick Record")} 3F
</Button>
@@ -3921,7 +3927,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("4F")}
onClick={() => handlePickRecord(FLOOR_B_KEY)}
>
{t("Print Pick Record")} 4F
</Button>


+ 10
- 9
src/components/Jodetail/JoPickOrderList.tsx Целия файл

@@ -1,5 +1,6 @@
/*
"use client";
import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
@@ -28,7 +29,7 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
type PickOrderFilter = "all" | "drink" | "Powder_Mixture" | "other";
const [filter, setFilter] = useState<PickOrderFilter>("all");
type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT";
type FloorFilter = "ALL" | typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "NO_LOT";
const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL");
const fetchPickOrders = useCallback(async () => {
setLoading(true);
@@ -124,25 +125,25 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
{t("Select All")}
</Button>
<Button
variant={floorFilter === "2F" ? "contained" : "outlined"}
variant={floorFilter === FLOOR_A_KEY ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("2F")}
onClick={() => setFloorFilter(FLOOR_A_KEY)}
>
2F
{FLOOR_A_KEY}
</Button>
<Button
variant={floorFilter === "3F" ? "contained" : "outlined"}
variant={floorFilter === FLOOR_D_KEY ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("3F")}
onClick={() => setFloorFilter(FLOOR_D_KEY)}
>
3F
</Button>
<Button
variant={floorFilter === "4F" ? "contained" : "outlined"}
variant={floorFilter === FLOOR_B_KEY ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("4F")}
onClick={() => setFloorFilter(FLOOR_B_KEY)}
>
4F
{FLOOR_B_KEY}
</Button>
<Button
variant={floorFilter === "NO_LOT" ? "contained" : "outlined"}


+ 2
- 1
src/components/Jodetail/JodetailSearch.tsx Целия файл

@@ -1,4 +1,5 @@
"use client";
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { PickOrderResult } from "@/app/api/pickOrder";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
@@ -165,7 +166,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => {
}
}, [tabIndex]);

const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
const handleAssignByStore = async (storeId: typeof FLOOR_A | typeof FLOOR_B) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;


+ 22
- 21
src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A, FLOOR_B, FLOOR_D } from "@/config/demoFloors";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Alert,
@@ -31,7 +32,7 @@ import SafeApexCharts from "@/components/charts/SafeApexCharts";
/** Day.js has no typed `dayjs.invalid()` factory; Invalid Date parses to an invalid Dayjs instance. */
const DAYJS_INVALID = dayjs(new Date(Number.NaN));

type FloorFilter = "all" | "2/F" | "3/F" | "4/F";
type FloorFilter = "all" | typeof FLOOR_A | typeof FLOOR_D | typeof FLOOR_B;

type DailySummaryRow = {
date: string;
@@ -81,21 +82,21 @@ const addRecordQty = (
const q3 = Number(record.plasticBoxCartonQty3f ?? 0);
const q4 = Number(record.plasticBoxCartonQty4f ?? 0);

if (selectedFloor === "all" || selectedFloor === "2/F") {
if (selectedFloor === "all" || selectedFloor === FLOOR_A) {
current.floor2F += q2;
}
if (selectedFloor === "all" || selectedFloor === "3/F") {
if (selectedFloor === "all" || selectedFloor === FLOOR_D) {
current.floor3F += q3;
}
if (selectedFloor === "all" || selectedFloor === "4/F") {
if (selectedFloor === "all" || selectedFloor === FLOOR_B) {
current.floor4F += q4;
}

if (selectedFloor === "all") {
current.total += q2 + q3 + q4;
} else if (selectedFloor === "2/F") {
} else if (selectedFloor === FLOOR_A) {
current.total += q2;
} else if (selectedFloor === "3/F") {
} else if (selectedFloor === FLOOR_D) {
current.total += q3;
} else {
current.total += q4;
@@ -198,9 +199,9 @@ const PlasticBoxCartonDashboardTab: React.FC = () => {

const chartSeries = useMemo(
() => [
{ name: "2/F", data: rows.map((row) => row.floor2F) },
{ name: "3/F", data: rows.map((row) => row.floor3F) },
{ name: "4/F", data: rows.map((row) => row.floor4F) },
{ name: FLOOR_A, data: rows.map((row) => row.floor2F) },
{ name: FLOOR_D, data: rows.map((row) => row.floor3F) },
{ name: FLOOR_B, data: rows.map((row) => row.floor4F) },
{ name: t("Total plastic box carton qty"), data: rows.map((row) => row.total) },
],
[rows, t],
@@ -351,17 +352,17 @@ const PlasticBoxCartonDashboardTab: React.FC = () => {
["", "", "", "", ""],
[
t("Date"),
t("2/F plastic box carton qty count"),
t("3/F plastic box carton qty count"),
t("4/F plastic box carton qty count"),
t(FLOOR_A + " plastic box carton qty count"),
t(FLOOR_D + " plastic box carton qty count"),
t(FLOOR_B + " plastic box carton qty count"),
t("Total plastic box carton qty"),
],
...dailyRows.map((row) => [row.date, row.floor2F, row.floor3F, row.floor4F, row.total]),
["", "", "", "", ""],
[t("Summary"), "", "", "", ""],
[t("2/F plastic box carton qty count"), reportSummary.floor2F, "", "", ""],
[t("3/F plastic box carton qty count"), reportSummary.floor3F, "", "", ""],
[t("4/F plastic box carton qty count"), reportSummary.floor4F, "", "", ""],
[t(FLOOR_A + " plastic box carton qty count"), reportSummary.floor2F, "", "", ""],
[t(FLOOR_D + " plastic box carton qty count"), reportSummary.floor3F, "", "", ""],
[t(FLOOR_B + " plastic box carton qty count"), reportSummary.floor4F, "", "", ""],
[t("Total plastic box carton qty"), reportSummary.total, "", "", ""],
];

@@ -469,9 +470,9 @@ const PlasticBoxCartonDashboardTab: React.FC = () => {
onChange={(event) => setFloor(event.target.value as FloorFilter)}
>
<MenuItem value="all">{t("All")}</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="3/F">3/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
<MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem>
<MenuItem value={FLOOR_D}>{FLOOR_D}</MenuItem>
<MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} md={6}>
@@ -492,19 +493,19 @@ const PlasticBoxCartonDashboardTab: React.FC = () => {
<Table size="small">
<TableBody>
<TableRow>
<TableCell>{t("2/F plastic box carton qty count")}</TableCell>
<TableCell>{t(FLOOR_A + " plastic box carton qty count")}</TableCell>
<TableCell align="right">
{summary.floor2F.toLocaleString("zh-HK")}
</TableCell>
</TableRow>
<TableRow>
<TableCell>{t("3/F plastic box carton qty count")}</TableCell>
<TableCell>{t(FLOOR_D + " plastic box carton qty count")}</TableCell>
<TableCell align="right">
{summary.floor3F.toLocaleString("zh-HK")}
</TableCell>
</TableRow>
<TableRow>
<TableCell>{t("4/F plastic box carton qty count")}</TableCell>
<TableCell>{t(FLOOR_B + " plastic box carton qty count")}</TableCell>
<TableCell align="right">
{summary.floor4F.toLocaleString("zh-HK")}
</TableCell>


+ 4
- 3
src/components/Jodetail/completeJobOrderRecord.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -774,21 +775,21 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
<Button
variant="contained"
color="primary"
onClick={() => handlePickRecord(jobOrderPickOrder, "2F")}
onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_A_KEY)}
>
{t("Print Pick Record")} 2F
</Button>
<Button
variant="contained"
color="primary"
onClick={() => handlePickRecord(jobOrderPickOrder, "3F")}
onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_D_KEY)}
>
{t("Print Pick Record")} 3F
</Button>
<Button
variant="contained"
color="primary"
onClick={() => handlePickRecord(jobOrderPickOrder, "4F")}
onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_B_KEY)}
>
{t("Print Pick Record")} 4F
</Button>


+ 16
- 8
src/components/Jodetail/newJobPickExecution.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -684,15 +685,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return allLots;
}, []);
const extractFloor = (lot: any): string => {
const raw = lot.routerRoute || lot.routerArea || lot.location || '';
const match = raw.match(/^(\d+F?)/i) || raw.split('-')[0];
return (match?.[1] || match || raw || '').toUpperCase().replace(/(\d)F?/i, '$1F');
const raw = String(lot.routerRoute || lot.routerArea || lot.location || "");
const head = raw.trim().toUpperCase().replace(/\//g, "").split("-")[0];
if (head === "2" || head === FLOOR_A_KEY) return FLOOR_A_KEY;
if (head === "4" || head === FLOOR_B_KEY) return FLOOR_B_KEY;
if (head === "3" || head === FLOOR_D_KEY) return FLOOR_D_KEY;
if (head === "1" || head === FLOOR_C_KEY) return FLOOR_C_KEY;
return head;
};
// 楼层排序权重:4F > 3F > 2F(数字越大越靠前)
// B/F, then D/F, then A/F.
const floorSortOrder = (floor: string): number => {
const n = parseInt(floor.replace(/\D/g, ''), 10);
return isNaN(n) ? 0 : n;
if (floor === FLOOR_B_KEY) return 4;
if (floor === FLOOR_D_KEY) return 3;
if (floor === FLOOR_A_KEY) return 2;
if (floor === FLOOR_C_KEY) return 1;
return 0;
};
const combinedLotData = useMemo(() => {
const lots = getAllLotsFromHierarchical(jobOrderData);
@@ -2613,7 +2621,7 @@ const sortedData = [...sourceData].sort((a, b) => {
const floorB = extractFloor(b);
const orderA = floorSortOrder(floorA);
const orderB = floorSortOrder(floorB);
if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F
if (orderA !== orderB) return orderB - orderA; // ${FLOOR_B_KEY}, ${FLOOR_D_KEY}, ${FLOOR_A_KEY}
// 同楼层再按 routerIndex、pickOrderCode、lotNo
const aIndex = a.routerIndex ?? 0;
const bIndex = b.routerIndex ?? 0;


+ 5
- 4
src/components/Jodetail/pickRecordHelpers.ts Целия файл

@@ -1,10 +1,11 @@
"use client";

import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, FLOOR_D, FLOOR_D_KEY } from "@/config/demoFloors";
import Swal from "sweetalert2";
import type { TFunction } from "i18next";
import { fetchPickRecordPlasticBoxCartonQty } from "@/app/api/jo/actions";

export type PickRecordFloor = "2F" | "3F" | "4F" | "ALL";
export type PickRecordFloor = typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "ALL";

export interface AllFloorsPlasticBoxCartonQtyResult {
qty2f: number;
@@ -115,9 +116,9 @@ export async function promptAllFloorsPlasticBoxCartonQty(
title: t("Enter all floors plastic box carton qty"),
html: `
<div style="display:flex;flex-direction:column;gap:10px;text-align:left;">
${buildFloorQtyInputRow("swal-qty-2f", t("2/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty2f)}
${buildFloorQtyInputRow("swal-qty-3f", t("3/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty3f)}
${buildFloorQtyInputRow("swal-qty-4f", t("4/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty4f)}
${buildFloorQtyInputRow("swal-qty-2f", t(FLOOR_A + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty2f)}
${buildFloorQtyInputRow("swal-qty-3f", t(FLOOR_D + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty3f)}
${buildFloorQtyInputRow("swal-qty-4f", t(FLOOR_B + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty4f)}
</div>
`,
showCancelButton: true,


+ 9
- 8
src/components/LotSuggestionRules/LotSuggestionRules.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY, LOC_HOLD, LOC_WIP } from "@/config/demoFloors";
import React, { useState } from "react";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import {
@@ -33,17 +34,17 @@ const sections: { title: string; lines: string[] }[] = [
{
title: "送貨揀貨的樓層",
lines: [
"票上已指定 2F 或 4F:只建議該樓層的批次。",
"沒有指定樓層:供應商在「4F 供應商」清單就用 4F,否則用 2F。這兩份清單可以在「送貨單樓層(供應商)」修改。",
"放單時若供應商在 2F 或 4F 清單內,只建議該樓層;1F 的批次仍然可以建議。不在清單內的供應商不限制 2F / 4F。",
"送貨單不建議 3F。",
"票上已指定 " + FLOOR_A_KEY + " 或 " + FLOOR_B_KEY + ":只建議該樓層的批次。",
"沒有指定樓層:供應商在「" + FLOOR_B_KEY + " 供應商」清單就用 " + FLOOR_B_KEY + ",否則用 " + FLOOR_A_KEY + "。這兩份清單可以在「送貨單樓層(供應商)」修改。",
"放單時若供應商在 " + FLOOR_A_KEY + " 或 " + FLOOR_B_KEY + " 清單內,只建議該樓層;" + FLOOR_C_KEY + " 的批次仍然可以建議。不在清單內的供應商不限制 " + FLOOR_A_KEY + " / " + FLOOR_B_KEY + "。",
"送貨單不建議 " + FLOOR_D_KEY + "。",
],
},
{
title: "目前不建議的倉位",
lines: [
"2F-W202-01-00",
"2F-W200-#A-00",
LOC_HOLD,
LOC_WIP,
"這兩個倉位寫在程式裡,畫面還不能改。",
],
},
@@ -51,8 +52,8 @@ const sections: { title: string; lines: string[] }[] = [
title: "工單提料(和送貨揀貨不同)",
lines: [
"同樣是先到期先出。",
"不建議 2F-W202-01-00。半成品會先試 2F-W200-#A-00。",
"工單可以使用 3F。",
"不建議 " + LOC_HOLD + "。半成品會先試 " + LOC_WIP + "。",
"工單可以使用 " + FLOOR_D_KEY + "。",
],
},
];


+ 22
- 21
src/components/Shop/RouteBoard.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import React, {
useCallback,
useEffect,
@@ -260,7 +261,7 @@ function summarizeLogisticsColumnStats(lanes: Lane[]): {
let count4F = 0;
for (const l of lanes) {
shopCount += l.shops.length;
if (normalizeStoreId(l.storeId) === "4F") count4F++;
if (normalizeStoreId(l.storeId) === FLOOR_B_KEY) count4F++;
else count2F++;
}
return {
@@ -516,7 +517,7 @@ function buildLaneFromPendingNewLane(p: PendingNewLane): Lane {
const code = String(payload.truckLanceCode || "").trim();
const storeNorm = normalizeStoreId(payload.store_id);
const remarkRaw =
storeNorm === "4F" &&
storeNorm === FLOOR_B_KEY &&
payload.remark != null &&
String(payload.remark).trim() !== ""
? String(payload.remark).trim()
@@ -690,7 +691,7 @@ function buildLaneFromTruckRows(
phone: lanePhone,
plate: lanePlate,
startTime: startTime || "00:00:00",
storeId: storeId || "2F",
storeId: storeId || FLOOR_A_KEY,
remark,
shops,
};
@@ -853,7 +854,7 @@ const RouteBoard: React.FC = () => {
"board",
);
const [laneFilter, setLaneFilter] = useState<{
floor: "all" | "2F" | "4F";
floor: "all" | typeof FLOOR_A_KEY | typeof FLOOR_B_KEY;
query: string;
}>({
floor: "all",
@@ -1100,7 +1101,7 @@ const RouteBoard: React.FC = () => {
/** 物流主檔 id;null = 未指定(至物流商管理指派) */
logisticId: number | null;
startTime: string;
storeId: "2F" | "4F";
storeId: typeof FLOOR_A_KEY | typeof FLOOR_B_KEY;
remark: string;
};

@@ -1108,7 +1109,7 @@ const RouteBoard: React.FC = () => {
truckLanceCode: "",
logisticId: null,
startTime: "07:30",
storeId: "2F",
storeId: FLOOR_A_KEY,
remark: "",
});

@@ -1201,7 +1202,7 @@ const RouteBoard: React.FC = () => {
const code = String(form.truckLanceCode || "").trim();
const storeNorm = normalizeStoreId(form.storeId);
const remarkRaw =
storeNorm === "4F" && String(form.remark || "").trim() !== ""
storeNorm === FLOOR_B_KEY && String(form.remark || "").trim() !== ""
? String(form.remark).trim()
: null;
const dep = parseTimeForBackend(form.startTime || "") || "00:00:00";
@@ -1244,7 +1245,7 @@ const RouteBoard: React.FC = () => {
const code = String(form.truckLanceCode || "").trim();
const storeNorm = normalizeStoreId(form.storeId);
const remarkRaw =
storeNorm === "4F" && String(form.remark || "").trim() !== ""
storeNorm === FLOOR_B_KEY && String(form.remark || "").trim() !== ""
? String(form.remark).trim()
: null;
return {
@@ -2296,7 +2297,7 @@ const RouteBoard: React.FC = () => {
const code = String(newRouteForm.truckLanceCode || "").trim();
const storeNorm = normalizeStoreId(newRouteForm.storeId);
const remarkRaw =
storeNorm === "4F" && String(newRouteForm.remark || "").trim() !== ""
storeNorm === FLOOR_B_KEY && String(newRouteForm.remark || "").trim() !== ""
? String(newRouteForm.remark).trim()
: null;
const laneKey = encodeLaneId(code, remarkRaw);
@@ -2370,7 +2371,7 @@ const RouteBoard: React.FC = () => {
const seq = Number(newSeq) || 0;
const storeId = normalizeStoreId(lane.storeId);
const remark =
storeId === "4F" &&
storeId === FLOOR_B_KEY &&
lane.remark != null &&
String(lane.remark).trim() !== ""
? String(lane.remark).trim()
@@ -2678,7 +2679,7 @@ const RouteBoard: React.FC = () => {
departureTime: to.startTime,
storeId: to.storeId,
remark:
normalizeStoreId(to.storeId) === "4F"
normalizeStoreId(to.storeId) === FLOOR_B_KEY
? to.remark != null && String(to.remark).trim() !== ""
? String(to.remark).trim()
: null
@@ -2713,7 +2714,7 @@ const RouteBoard: React.FC = () => {
to.shops = inserted;

// IMPORTANT: Do NOT renumber all loadingSequence.
// loadingSequence can be duplicated intentionally (e.g. 4F grouping).
// loadingSequence can be duplicated intentionally (e.g. ${FLOOR_B_KEY} grouping).
// On drag/drop, only update the moved shop's loadingSequence so it joins the target group.
const newSeq = computeMovedLoadingSequence(
inserted,
@@ -3356,7 +3357,7 @@ const RouteBoard: React.FC = () => {
if (!lane) continue;
const sid = normalizeStoreId(lane.storeId);
const remark =
sid === "4F" &&
sid === FLOOR_B_KEY &&
lane.remark != null &&
String(lane.remark).trim() !== ""
? String(lane.remark).trim()
@@ -6085,15 +6086,15 @@ const RouteBoard: React.FC = () => {
onChange={(e) =>
setNewRouteForm((p) => ({
...p,
storeId: e.target.value as "2F" | "4F",
storeId: e.target.value as typeof FLOOR_A_KEY | typeof FLOOR_B_KEY,
}))
}
>
<MenuItem value="2F">2F</MenuItem>
<MenuItem value="4F">4F</MenuItem>
<MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem>
<MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem>
</Select>
</FormControl>
{newRouteForm.storeId === "4F" && (
{newRouteForm.storeId === FLOOR_B_KEY && (
<TextField
size="small"
fullWidth
@@ -6683,8 +6684,8 @@ const RouteBoard: React.FC = () => {
}
>
<MenuItem value="all">{t("floor_all")}</MenuItem>
<MenuItem value="2F">2F</MenuItem>
<MenuItem value="4F">4F</MenuItem>
<MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem>
<MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem>
</Select>
</FormControl>
<Stack
@@ -7263,7 +7264,7 @@ const RouteBoard: React.FC = () => {
/>
<Chip
size="small"
label={`2F · ${colStats.count2F}`}
label={`${FLOOR_A_KEY} · ${colStats.count2F}`}
sx={{
height: 24,
fontWeight: 700,
@@ -7272,7 +7273,7 @@ const RouteBoard: React.FC = () => {
/>
<Chip
size="small"
label={`4F · ${colStats.count4F}`}
label={`${FLOOR_B_KEY} · ${colStats.count4F}`}
sx={{
height: 24,
fontWeight: 700,


+ 4
- 3
src/components/Shop/Shop.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -134,14 +135,14 @@ const Shop: React.FC = () => {
const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null;
const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0;
// Check storeId: must exist and not be 0 (can be string "2F"/"4F" or number)
// Check storeId: must exist and not be 0 (can be string FLOOR_A_KEY/FLOOR_B_KEY or number)
// Actual field name in JSON is store_id (underscore, lowercase)
const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id;
let storeIdValid = false;
if (storeId != null && storeId !== undefined && storeId !== "") {
const storeIdStr = String(storeId).trim();
// If it's "2F" or "4F", it's valid (not 0)
if (storeIdStr === "2F" || storeIdStr === "4F") {
// If it's FLOOR_A_KEY or FLOOR_B_KEY, it's valid (not 0)
if (storeIdStr === FLOOR_A_KEY || storeIdStr === FLOOR_B_KEY) {
storeIdValid = true;
} else {
const storeIdNum = Number(storeId);


+ 3
- 2
src/components/Shop/ShopDetail.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Card,
@@ -274,9 +275,9 @@ const ShopDetail: React.FC = () => {
}

const lane = selectedTruckLane!;
const storeIdStr = normalizeStoreId(lane.storeId) || "2F";
const storeIdStr = normalizeStoreId(lane.storeId) || FLOOR_A_KEY;
const remarkValue =
storeIdStr === "4F" && lane.remark != null && String(lane.remark).trim() !== ""
storeIdStr === FLOOR_B_KEY && lane.remark != null && String(lane.remark).trim() !== ""
? String(lane.remark).trim()
: null;



+ 6
- 5
src/components/Shop/TruckLane.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Card,
@@ -56,7 +57,7 @@ const TruckLane: React.FC = () => {
const [newTruck, setNewTruck] = useState({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
storeId: FLOOR_A_KEY,
});
const [saving, setSaving] = useState<boolean>(false);
const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false);
@@ -192,7 +193,7 @@ const TruckLane: React.FC = () => {
setNewTruck({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
storeId: FLOOR_A_KEY,
});
setAddDialogOpen(true);
setError(null);
@@ -203,7 +204,7 @@ const TruckLane: React.FC = () => {
setNewTruck({
truckLanceCode: "",
departureTime: "",
storeId: "2F",
storeId: FLOOR_A_KEY,
});
};

@@ -373,8 +374,8 @@ const TruckLane: React.FC = () => {
}}
disabled={saving}
>
<MenuItem value="2F">2F</MenuItem>
<MenuItem value="4F">4F</MenuItem>
<MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem>
<MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem>
</Select>
</FormControl>
</Grid>


+ 9
- 8
src/components/Shop/TruckLaneDetail.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Card,
@@ -291,9 +292,9 @@ const TruckLaneDetail: React.FC = () => {

await Promise.all(
withIds.map((row) => {
const storeIdStr = normalizeStoreId(row.storeId) || "2F";
const storeIdStr = normalizeStoreId(row.storeId) || FLOOR_A_KEY;
let remarkValue: string | null = null;
if (storeIdStr === "4F" && row.remark != null && String(row.remark).trim() !== "") {
if (storeIdStr === FLOOR_B_KEY && row.remark != null && String(row.remark).trim() !== "") {
remarkValue = String(row.remark).trim();
}
return updateTruckLaneClient({
@@ -579,8 +580,8 @@ const TruckLaneDetail: React.FC = () => {
try {
const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String
? String(truckData.storeId)
: String(truckData.storeId)) : "2F";
const displayStoreId = normalizeStoreId(storeIdValue) || "2F";
: String(truckData.storeId)) : FLOOR_A_KEY;
const displayStoreId = normalizeStoreId(storeIdValue) || FLOOR_A_KEY;

let departureTimeStr = "";
if (truckData.departureTime) {
@@ -597,8 +598,8 @@ const TruckLaneDetail: React.FC = () => {
);
const shopIdValue = foundShop ? foundShop.id : null;

// Get remark - only if storeId is "4F"
const remarkValue = displayStoreId === "4F" ? (newShop.remark?.trim() || null) : null;
// Get remark - only if storeId is FLOOR_B_KEY
const remarkValue = displayStoreId === FLOOR_B_KEY ? (newShop.remark?.trim() || null) : null;

// Check if there's an "Unassign" row for this truck lane that should be replaced
let unassignTruck: Truck | null = null;
@@ -909,7 +910,7 @@ const TruckLaneDetail: React.FC = () => {
{(editingRowIndex === index || massEditing) ? (
(() => {
const storeIdValue = truckData.storeId ? String(truckData.storeId) : null;
const isEditable = normalizeStoreId(storeIdValue) === "4F";
const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY;
return (
<Autocomplete
freeSolo
@@ -1096,7 +1097,7 @@ const TruckLaneDetail: React.FC = () => {
const storeIdValue = truckData?.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String
? String(truckData.storeId)
: String(truckData.storeId)) : null;
const isEditable = normalizeStoreId(storeIdValue) === "4F";
const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY;
return isEditable ? (
<Grid item xs={12}>
<Autocomplete


+ 6
- 5
src/components/Shop/computeTruckLaneWarnings.ts Целия файл

@@ -2,6 +2,7 @@
* 車線看板警示:Rule1(4F 同店跨線 weekday)、Rule2(非 4F 同店跨線發車時分)。
* 規則細節見 `README-ROUTE-BOARD-WARNINGS.md`。
*/
import { FLOOR_B_KEY } from "@/config/demoFloors";
import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil";

const LANE_KEY_SEP = "|";
@@ -168,7 +169,7 @@ function rowToLaneRef(row: TruckLaneWarningInputRow): TruckLaneWarningLaneRef {
const storeNorm = normalizeStoreId(row.storeId);
const depDisp = departureTimeToHHmmKey(row.departureTime);
const weekday =
storeNorm === "4F"
storeNorm === FLOOR_B_KEY
? parseWeekdayFromTruckLanceCode(row.truckLanceCode)
: null;
return {
@@ -227,7 +228,7 @@ export function computeTruckLaneWarnings(
>();
for (const row of active) {
const store = normalizeStoreId(row.storeId);
if (store !== "4F") continue;
if (store !== FLOOR_B_KEY) continue;
const wd = parseWeekdayFromTruckLanceCode(row.truckLanceCode);
if (!wd) {
weekdayParseFailures.push({
@@ -258,7 +259,7 @@ export function computeTruckLaneWarnings(
const sampleRow = active.find(
(r) =>
shopKOf(r) === shopK &&
normalizeStoreId(r.storeId) === "4F" &&
normalizeStoreId(r.storeId) === FLOOR_B_KEY &&
parseWeekdayFromTruckLanceCode(r.truckLanceCode) === wd,
);
const laneKeyList: string[] = Array.from(laneKeys);
@@ -278,7 +279,7 @@ export function computeTruckLaneWarnings(
const r2Map = new Map<string, Map<string, Set<string>>>();
for (const row of active) {
const store = normalizeStoreId(row.storeId);
if (store === "4F") continue;
if (store === FLOOR_B_KEY) continue;
const hhmm = departureTimeToHHmmKey(row.departureTime);
if (!hhmm) continue;
const shopK = shopKOf(row);
@@ -302,7 +303,7 @@ export function computeTruckLaneWarnings(
const sampleRow = active.find(
(r) =>
shopKOf(r) === shopK &&
normalizeStoreId(r.storeId) !== "4F" &&
normalizeStoreId(r.storeId) !== FLOOR_B_KEY &&
departureTimeToHHmmKey(r.departureTime) === hhmm,
);
const laneKeyList: string[] = Array.from(laneKeys);


+ 2
- 1
src/components/Shop/routeBoardImportPreview.ts Целия файл

@@ -1,3 +1,4 @@
import { FLOOR_A_KEY } from "@/config/demoFloors";
import type { RouteLaneImportPreviewRow } from "@/app/api/shop/client";
import type { Truck } from "@/app/api/shop/actions";
import { normalizeStoreId } from "@/app/utils/formatUtil";
@@ -118,7 +119,7 @@ export function mergeImportPreviewIntoLanes<
String(previewRows[0].remark).trim() !== ""
? String(previewRows[0].remark).trim()
: null;
const storeId = normalizeStoreId(previewRows[0]?.storeId ?? "2F");
const storeId = normalizeStoreId(previewRows[0]?.storeId ?? FLOOR_A_KEY);
const startTime = parseTimeForPreview(previewRows[0]?.departureTime ?? "");

const existing = laneMap.get(laneId);


+ 4
- 3
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -252,7 +253,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc");
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>([FLOOR_A_KEY, FLOOR_B_KEY]);
const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All");
const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>("");
const [searchItemKeyword, setSearchItemKeyword] = useState<string>("");
@@ -373,7 +374,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
.then((sections) => {
const descSet = new Set<string>();
const sectionSet = new Set<string>();
const storeSet = new Set<string>(["2F", "4F"]);
const storeSet = new Set<string>([FLOOR_A_KEY, FLOOR_B_KEY]);
sections.forEach((s) => {
const section = s.stockTakeSection?.trim();
if (section) sectionSet.add(section);
@@ -1375,7 +1376,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
return Array.from(sectionSet).sort((a, b) => a.localeCompare(b));
});
setStoreIdOptions((prev) => {
const storeSet = new Set<string>([...prev, "2F", "4F"]);
const storeSet = new Set<string>([...prev, FLOOR_A_KEY, FLOOR_B_KEY]);
inventoryLotDetails.forEach((item) => {
const storeId = item.storeId?.trim();
if (storeId) storeSet.add(storeId);


+ 4
- 3
src/components/StockTakeManagement/PickerCardList.tsx Целия файл

@@ -1,5 +1,6 @@
"use client";

import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import {
Box,
Button,
@@ -173,7 +174,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set());
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]);
const [storeIdOptions, setStoreIdOptions] = useState<string[]>([FLOOR_A_KEY, FLOOR_B_KEY]);

const statusOptions = ["pending", "stockTaking", "approving", "completed"];

@@ -605,7 +606,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
setStockTakeSectionRows(Array.isArray(sections) ? sections : []);
const descSet = new Set<string>();
const sectionSet = new Set<string>();
const storeIdSet = new Set<string>(["2F", "4F"]);
const storeIdSet = new Set<string>([FLOOR_A_KEY, FLOOR_B_KEY]);
sections.forEach((s) => {
const section = s.stockTakeSection?.trim();
if (section) sectionSet.add(section);
@@ -637,7 +638,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}, [stockTakeSessions]);
useEffect(() => {
setStoreIdOptions((prev) => {
const storeIdSet = new Set<string>([...prev, "2F", "4F"]);
const storeIdSet = new Set<string>([...prev, FLOOR_A_KEY, FLOOR_B_KEY]);
stockTakeSessions.forEach((item) => {
const storeId = item.storeId?.trim();
if (storeId) storeIdSet.add(storeId);


+ 19
- 0
src/config/demoFloors.ts Целия файл

@@ -0,0 +1,19 @@
/** Floor and bin labels stored in the anonymized demo database. */
export const FLOOR_A = "A/F";
export const FLOOR_A_KEY = "AF";
export const FLOOR_B = "B/F";
export const FLOOR_B_KEY = "BF";
export const FLOOR_C = "C/F";
export const FLOOR_C_KEY = "CF";
export const FLOOR_D = "D/F";
export const FLOOR_D_KEY = "DF";

export const LOC_HOLD = "LOC-HOLD";
export const LOC_WIP = "LOC-WIP";
export const LOC_PREFIX = "LOC-";

export const LABEL_A = "LABEL-A";
export const LABEL_B = "LABEL-B";

export const SETTING_SUPPLIERS_A = "DO.floor.suppliers.AF";
export const SETTING_SUPPLIERS_B = "DO.floor.suppliers.BF";

+ 7
- 6
src/config/reportConfig.ts Целия файл

@@ -1,8 +1,9 @@
/** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */
export type FieldType = 'date' | 'text' | 'select' | 'number' | 'checkbox';

import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { NEXT_PUBLIC_API_URL } from "@/config/api";

export type FieldType = 'date' | 'text' | 'select' | 'number' | 'checkbox';

export interface ReportField {
label: string;
name: string;
@@ -62,13 +63,13 @@ const asyncItemCodeField = (

const floorOptionsAll: ReportField["options"] = [
{ label: "全部", value: "All" },
{ label: "2/F", value: "2/F" },
{ label: "4/F", value: "4/F" },
{ label: FLOOR_A, value: FLOOR_A },
{ label: FLOOR_B, value: FLOOR_B },
];

const floorOptions: ReportField["options"] = [
{ label: "2/F", value: "2/F" },
{ label: "4/F", value: "4/F" },
{ label: FLOOR_A, value: FLOOR_A },
{ label: FLOOR_B, value: FLOOR_B },
];

const pasteItemCodeField = (): ReportField => ({


Зареждане…
Отказ
Запис