Ver código fonte

Use new anonymous floor and bin labels

Workbench, delivery search, reports, and lot suggestion now match new db
debrand
Harry Groves 3 semanas atrás
pai
commit
408086acc7
49 arquivos alterados com 367 adições e 286 exclusões
  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 Ver arquivo

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


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




+ 4
- 3
src/app/api/chart/client.ts Ver arquivo

@@ -1,3 +1,4 @@
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api"; 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). */ /** 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 */ /** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */
export async function fetchStaffDeliveryPerformance( export async function fetchStaffDeliveryPerformance(
@@ -591,8 +592,8 @@ export async function fetchStaffDeliveryPerformance(
if (endDate) p.set("endDate", endDate); if (endDate) p.set("endDate", endDate);
(staffNos ?? []).forEach((no) => p.append("staffNo", no)); (staffNos ?? []).forEach((no) => p.append("staffNo", no));
if (storeFilter === "null_only") p.set("storeIdNull", "true"); 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 q = p.toString();
const res = await clientAuthFetch( const res = await clientAuthFetch(
q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance` q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance`


+ 2
- 1
src/app/api/do/actions.tsx Ver arquivo

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




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




+ 3
- 2
src/app/api/doworkbench/actions.ts Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { revalidateTag } from "next/cache"; import { revalidateTag } from "next/cache";
import { BASE_API_URL } from "@/config/api"; import { BASE_API_URL } from "@/config/api";
import { serverFetchJson } from "@/app/utils/fetchUtil"; import { serverFetchJson } from "@/app/utils/fetchUtil";
@@ -244,7 +245,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelection(
storeId?: string, storeId?: string,
truck?: string, truck?: string,
releaseType?: 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 floor?: string
): Promise<ReleasedDoPickOrderListItem[]> { ): Promise<ReleasedDoPickOrderListItem[]> {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -266,7 +267,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday(
truck?: string, truck?: string,
requiredDeliveryDate?: string, requiredDeliveryDate?: string,
releaseType?: 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 floor?: string
): Promise<ReleasedDoPickOrderListItem[]> { ): Promise<ReleasedDoPickOrderListItem[]> {
const params = new URLSearchParams(); const params = new URLSearchParams();


+ 2
- 1
src/app/api/jo/actions.ts Ver arquivo

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


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


+ 2
- 1
src/app/api/pickOrder/actions.ts Ver arquivo

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


+ 3
- 2
src/app/api/settings/deliveryOrderFloor/constants.ts Ver arquivo

@@ -1,5 +1,6 @@
import { SETTING_SUPPLIERS_A, SETTING_SUPPLIERS_B } from "@/config/demoFloors";
/** `settings.name`:逗號分隔 supplier **code**(須與後端讀取邏輯一致)。 */ /** `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"; export const SETTING_DO_FLOOR_CATEGORY = "DO_FLOOR";

+ 3
- 2
src/app/utils/formatUtil.ts Ver arquivo

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

+ 4
- 3
src/components/DashboardPage/truckSchedule/TruckScheduleDashboard.tsx Ver arquivo

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


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


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


+ 17
- 16
src/components/DeliveryOrderFloorSettings/DeliveryOrderFloorSettings.tsx Ver arquivo

@@ -1,5 +1,6 @@
"use client"; "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 React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EditOutlined from "@mui/icons-material/EditOutlined"; import EditOutlined from "@mui/icons-material/EditOutlined";
@@ -57,7 +58,7 @@ function formatBracketList(codesCsv: string): string {
return `[${n.split(",").join(", ")}]`; return `[${n.split(",").join(", ")}]`;
} }


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


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


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


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


+ 9
- 8
src/components/DoDetail/DoDetail.tsx Ver arquivo

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


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


+ 10
- 9
src/components/DoSearch/DoSearch.tsx Ver arquivo

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


import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import { DoResult } from "@/app/api/do"; import { DoResult } from "@/app/api/do";
import { import {
DoSearchAll, 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 SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>;
type SearchParamNames = keyof SearchBoxInputs; 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 // put all this into a new component
// ConsoDoForm // ConsoDoForm
@@ -103,7 +104,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [isWorkbench, setIsWorkbench] = useState(false); 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 [searchBoxResetKey, setSearchBoxResetKey] = useState(0);
const [pagingController, setPagingController] = useState({ const [pagingController, setPagingController] = useState({
pageNum: 1, pageNum: 1,
@@ -326,10 +327,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea


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


+ 3
- 2
src/components/DoSearchWorkbench/DoSearchWorkbench.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { DoResult } from "@/app/api/do"; import { DoResult } from "@/app/api/do";
import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo } from "@/app/api/do/actions"; import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo } from "@/app/api/do/actions";
import { import {
@@ -150,8 +151,8 @@ const DoSearchWorkbench: React.FC<Props> = ({
paramName: "floor", paramName: "floor",
type: "select-labelled", type: "select-labelled",
options: [ 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 Ver arquivo

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


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


+ 4
- 3
src/components/DoWorkbench/WorkbenchEtraMergeDialog.tsx Ver arquivo

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


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


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


+ 24
- 23
src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx Ver arquivo

@@ -1,5 +1,6 @@
"use client"; "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 { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -53,7 +54,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined; 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 [selectedTruck, setSelectedTruck] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]); const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
@@ -92,7 +93,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
[ticketFloor], [ticketFloor],
); );
const defaultTruckCount = const defaultTruckCount =
(ticketFloor === "2/F" ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0;
(ticketFloor === FLOOR_A ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0;
const etraEnterInFlightRef = useRef(false); const etraEnterInFlightRef = useRef(false);
const [etraMergeDialogOpen, setEtraMergeDialogOpen] = useState(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 === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD"); else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
const [s2, s4] = await Promise.all([ 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); setSummary2F(s2);
setSummary4F(s4); setSummary4F(s4);
@@ -217,8 +218,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
startFullTimer(); startFullTimer();
try { try {
const [list2F, list4F] = await Promise.all([ 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 groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {}; const map: Record<string, number> = {};
@@ -441,8 +442,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
<InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel> <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"] })}> <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> </Select>
</FormControl> </FormControl>
</Box> </Box>
@@ -481,10 +482,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({


{!inEtraUi ? ( {!inEtraUi ? (
<Grid container spacing={2}> <Grid container spacing={2}>
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <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 }}> <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() : ( {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : (
<Grid container spacing={1}> <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> <Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography>
<Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}> <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
{row.lanes.map((lane) => ( {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})`} {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
</Button> </Button>
))} ))}
@@ -509,10 +510,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
</Grid> </Grid>
)} )}


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


{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <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 }}> <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
{truckCounts2F.length === 0 ? renderNoEntry() : ( {truckCounts2F.length === 0 ? renderNoEntry() : (
<Grid container spacing={1}> <Grid container spacing={1}>
{truckCounts2F.map(({ truck, count }) => ( {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 <Button
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
clearModalEtraContext(); clearModalEtraContext();
setIsDefaultTruck(false); setIsDefaultTruck(false);
setSelectedStore("2/F");
setSelectedStore(FLOOR_A);
setSelectedTruck(truck); setSelectedTruck(truck);
setModalOpen(true); setModalOpen(true);
}} }}
@@ -616,21 +617,21 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({
</Grid> </Grid>
)} )}


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


+ 4
- 3
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -742,8 +743,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
.toUpperCase() .toUpperCase()
.replace(/\s/g, ""); .replace(/\s/g, "");
const floorKey = storeId.replace(/\//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; return undefined;
}, [fgPickOrders]); }, [fgPickOrders]);


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


+ 3
- 1
src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx Ver arquivo

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


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




+ 3
- 2
src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx Ver arquivo

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


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


+ 4
- 3
src/components/DoWorkbench/workbenchLanePanelPrefs.ts Ver arquivo

@@ -1,5 +1,6 @@
import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
export type WorkbenchLaneDateKey = "today" | "tomorrow" | "dayAfterTomorrow"; 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. */ /** Tab 0 lane assignment filters — lifted to DoWorkbenchTabs so they survive tab switches. */
export type WorkbenchLanePanelPrefs = { export type WorkbenchLanePanelPrefs = {
@@ -10,7 +11,7 @@ export type WorkbenchLanePanelPrefs = {


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


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


export function isWorkbenchLaneFloor(v: string): v is WorkbenchLaneFloor { 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 Ver arquivo

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


+ 3
- 2
src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx Ver arquivo

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


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




+ 20
- 19
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx Ver arquivo

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


import { FLOOR_A, FLOOR_B } from "@/config/demoFloors";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
Alert, Alert,
@@ -29,7 +30,7 @@ import {
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { useTranslation } from "react-i18next"; 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 BreakdownDimension = "lane" | "shop" | "floor";


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


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


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


+ 27
- 26
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx Ver arquivo

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


const [selectedDate, setSelectedDate] = useState<string>("today"); const [selectedDate, setSelectedDate] = useState<string>("today");
const [releaseType, setReleaseType] = useState<string>("batch"); 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 = () => { const startFullTimer = () => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const; 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([ 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); setSummary2F(s2);
setSummary4F(s4); setSummary4F(s4);
@@ -117,8 +118,8 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to
startFullTimer(); startFullTimer();
try { try {
const [list2F, list4F] = await Promise.all([ 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 groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {}; const map: Record<string, number> = {};
@@ -388,10 +389,10 @@ const getDateLabel = (offset: number) => {
id="ticket-floor-select" id="ticket-floor-select"
value={ticketFloor} value={ticketFloor}
label={t("Floor ticket")} 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> </Select>
</FormControl> </FormControl>
</Box> </Box>
@@ -414,7 +415,7 @@ const getDateLabel = (offset: number) => {
{/* Grid containing both floors */} {/* Grid containing both floors */}
<Grid container spacing={2}> <Grid container spacing={2}>
{/* 2/F 楼层面板 */} {/* 2/F 楼层面板 */}
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */} {/* Floor Label */}
@@ -426,7 +427,7 @@ const getDateLabel = (offset: number) => {
pt: 1 pt: 1
}} }}
> >
2/F
{FLOOR_A}
</Typography> </Typography>
{/* Content Box */} {/* Content Box */}
@@ -495,7 +496,7 @@ const getDateLabel = (offset: number) => {
disabled={lane.unassigned === 0 || isAssigning} disabled={lane.unassigned === 0 || isAssigning}
onClick={() => onClick={() =>
handleLaneButtonClick( handleLaneButtonClick(
"2/F",
FLOOR_A,
row.truckDepartureTime, row.truckDepartureTime,
lane.truckLanceCode, lane.truckLanceCode,
null, null,
@@ -530,7 +531,7 @@ const getDateLabel = (offset: number) => {
</Stack> </Stack>
</Grid> </Grid>
)} )}
{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */} {/* Floor Label */}
@@ -542,7 +543,7 @@ const getDateLabel = (offset: number) => {
pt: 1 pt: 1
}} }}
> >
4/F
{FLOOR_B}
</Typography> </Typography>
{/* Content Box */} {/* Content Box */}
@@ -613,7 +614,7 @@ const getDateLabel = (offset: number) => {
disabled={slot.lane.unassigned === 0 || isAssigning} disabled={slot.lane.unassigned === 0 || isAssigning}
onClick={() => onClick={() =>
handleLaneButtonClick( handleLaneButtonClick(
"4/F",
FLOOR_B,
slot.truckDepartureTime, slot.truckDepartureTime,
slot.lane.truckLanceCode, slot.lane.truckLanceCode,
slot.lane.loadingSequence ?? null, slot.lane.loadingSequence ?? null,
@@ -720,11 +721,11 @@ const getDateLabel = (offset: number) => {
</Box> </Box>
</Grid> </Grid>
{ticketFloor === "2/F" && (
{ticketFloor === FLOOR_A && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
2/F
{FLOOR_A}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@@ -751,7 +752,7 @@ const getDateLabel = (offset: number) => {
) : ( ) : (
<Grid container spacing={1}> <Grid container spacing={1}>
{truckCounts2F.map(({ truck, count }) => ( {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 <Button
@@ -759,7 +760,7 @@ const getDateLabel = (offset: number) => {
size="medium" size="medium"
onClick={() => { onClick={() => {
setIsDefaultTruck(false); setIsDefaultTruck(false);
setSelectedStore("2/F");
setSelectedStore(FLOOR_A);
setSelectedTruck(truck); setSelectedTruck(truck);
setModalOpen(true); setModalOpen(true);
}} }}
@@ -788,11 +789,11 @@ const getDateLabel = (offset: number) => {
</Stack> </Stack>
</Grid> </Grid>
)} )}
{ticketFloor === "4/F" && (
{ticketFloor === FLOOR_B && (
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start"> <Stack direction="row" spacing={2} alignItems="flex-start">
<Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>
4/F
{FLOOR_B}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@@ -819,7 +820,7 @@ const getDateLabel = (offset: number) => {
) : ( ) : (
<Grid container spacing={1}> <Grid container spacing={1}>
{truckCounts4F.map(({ truck, count }) => ( {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 <Button
@@ -827,7 +828,7 @@ const getDateLabel = (offset: number) => {
size="medium" size="medium"
onClick={() => { onClick={() => {
setIsDefaultTruck(false); setIsDefaultTruck(false);
setSelectedStore("4/F");
setSelectedStore(FLOOR_B);
setSelectedTruck(truck); setSelectedTruck(truck);
setModalOpen(true); setModalOpen(true);
}} }}
@@ -877,7 +878,7 @@ const getDateLabel = (offset: number) => {
variant="outlined" variant="outlined"
size="medium" size="medium"
onClick={() => { onClick={() => {
setSelectedStore("4/F"); // 或用專門標示 Truck X
setSelectedStore(FLOOR_B); // 或用專門標示 Truck X
setSelectedTruck("車線-X"); setSelectedTruck("車線-X");
setIsDefaultTruck(true); setIsDefaultTruck(true);
setDefaultDateScope("before"); // 類似上一輪說的 dateScope setDefaultDateScope("before"); // 類似上一輪說的 dateScope
@@ -902,8 +903,8 @@ const getDateLabel = (offset: number) => {
const loadCounts = async () => { const loadCounts = async () => {
try { try {
const [list2F, list4F] = await Promise.all([ 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 groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
const map: Record<string, number> = {}; const map: Record<string, number> = {};


+ 4
- 3
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Ver arquivo

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


+ 6
- 5
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Ver arquivo

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


import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -786,7 +787,7 @@ const PickExecution: React.FC<Props> = ({
.trim() .trim()
.toUpperCase() .toUpperCase()
.replace(/\s/g, ""); .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, ""); const floorKey = storeId.replace(/\//g, "");
return floorKey ? `${floorKey}-` : ""; return floorKey ? `${floorKey}-` : "";
}, [fgPickOrders]); }, [fgPickOrders]);
@@ -796,8 +797,8 @@ const PickExecution: React.FC<Props> = ({
.toUpperCase() .toUpperCase()
.replace(/\s/g, ""); .replace(/\s/g, "");
const floorKey = storeId.replace(/\//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; return undefined;
}, [fgPickOrders]); }, [fgPickOrders]);
// Add these missing state variables after line 352 // Add these missing state variables after line 352
@@ -1212,14 +1213,14 @@ const PickExecution: React.FC<Props> = ({
// 将层级数据转换为平铺格式(用于表格显示) // 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = []; 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 ?? "") const doFloorKey = String(hierarchicalData.fgInfo.storeId ?? "")
.trim() .trim()
.toUpperCase() .toUpperCase()
.replace(/\//g, "") .replace(/\//g, "")
.replace(/\s/g, ""); .replace(/\s/g, "");
const pickOrderLinesForDisplay = const pickOrderLinesForDisplay =
doFloorKey === "2F"
doFloorKey === FLOOR_A_KEY
? [...(mergedPickOrder.pickOrderLines || [])].sort( ? [...(mergedPickOrder.pickOrderLines || [])].sort(
(a: any, b: any) => { (a: any, b: any) => {
const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999; const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999;


+ 4
- 3
src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx Ver arquivo

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


import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors";
import { import {
Modal, Modal,
Box, Box,
@@ -37,7 +38,7 @@ export type ReleasedDoPickListBridge = {
storeId?: string, storeId?: string,
truck?: string, truck?: string,
releaseType?: 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 floor?: string
) => Promise<ReleasedDoPickOrderListItem[]>; ) => Promise<ReleasedDoPickOrderListItem[]>;
/** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */ /** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */
@@ -47,7 +48,7 @@ export type ReleasedDoPickListBridge = {
truck?: string, truck?: string,
requiredDeliveryDate?: string, requiredDeliveryDate?: string,
releaseType?: 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 floor?: string
) => Promise<ReleasedDoPickOrderListItem[]>; ) => Promise<ReleasedDoPickOrderListItem[]>;
assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>; assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>;
@@ -75,7 +76,7 @@ interface Props {
*/ */
filterRequiredDeliveryDate?: string; 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; truckXFloor?: string;
} }


+ 2
- 1
src/components/FinishedGoodSearch/TruckRoutingSummaryTab.tsx Ver arquivo

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


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


+ 6
- 4
src/components/InventorySearch/LotLabelPrintModal.tsx Ver arquivo

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


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


const selectedPrinter = useMemo(() => { const selectedPrinter = useMemo(() => {


+ 6
- 5
src/components/JoWorkbench/JoPickOrderList.tsx Ver arquivo

@@ -1,4 +1,5 @@
"use client"; "use client";
import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { import {
Box, Box,
@@ -45,7 +46,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ planStart: today }); 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>[] = [ const searchCriteria: Criterion<any>[] = [
{ label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" }, { label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" },
@@ -76,9 +77,9 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
paramName: "floor", paramName: "floor",
type: "select-labelled", type: "select-labelled",
options: [ 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" }, { label: t("No Lot"), value: "NO_LOT" },
], ],
}, },
@@ -86,7 +87,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {


const selectedFloor: FloorFilter = (() => { const selectedFloor: FloorFilter = (() => {
const floor = String(searchQuery.floor || "ALL"); 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 floor;
} }
return "ALL"; return "ALL";


+ 19
- 13
src/components/JoWorkbench/newJobPickExecution.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -1017,17 +1018,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
[], [],
); );
const extractFloor = (lot: any): string => { 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 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 combinedLotData = useMemo(() => {
const lots = getAllLotsFromHierarchical(jobOrderData); const lots = getAllLotsFromHierarchical(jobOrderData);
@@ -1156,8 +1162,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
.toUpperCase() .toUpperCase()
.replace(/\s/g, ""); .replace(/\s/g, "");
const floorKey = storeId.replace(/\//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; return undefined;
}, [workbenchStoreId]); }, [workbenchStoreId]);


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


+ 10
- 9
src/components/Jodetail/JoPickOrderList.tsx Ver arquivo

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


+ 2
- 1
src/components/Jodetail/JodetailSearch.tsx Ver arquivo

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


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


+ 22
- 21
src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx Ver arquivo

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


import { FLOOR_A, FLOOR_B, FLOOR_D } from "@/config/demoFloors";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
Alert, 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. */ /** Day.js has no typed `dayjs.invalid()` factory; Invalid Date parses to an invalid Dayjs instance. */
const DAYJS_INVALID = dayjs(new Date(Number.NaN)); 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 = { type DailySummaryRow = {
date: string; date: string;
@@ -81,21 +82,21 @@ const addRecordQty = (
const q3 = Number(record.plasticBoxCartonQty3f ?? 0); const q3 = Number(record.plasticBoxCartonQty3f ?? 0);
const q4 = Number(record.plasticBoxCartonQty4f ?? 0); const q4 = Number(record.plasticBoxCartonQty4f ?? 0);


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


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


const chartSeries = useMemo( 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) }, { name: t("Total plastic box carton qty"), data: rows.map((row) => row.total) },
], ],
[rows, t], [rows, t],
@@ -351,17 +352,17 @@ const PlasticBoxCartonDashboardTab: React.FC = () => {
["", "", "", "", ""], ["", "", "", "", ""],
[ [
t("Date"), 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"), t("Total plastic box carton qty"),
], ],
...dailyRows.map((row) => [row.date, row.floor2F, row.floor3F, row.floor4F, row.total]), ...dailyRows.map((row) => [row.date, row.floor2F, row.floor3F, row.floor4F, row.total]),
["", "", "", "", ""], ["", "", "", "", ""],
[t("Summary"), "", "", "", ""], [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, "", "", ""], [t("Total plastic box carton qty"), reportSummary.total, "", "", ""],
]; ];


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


+ 4
- 3
src/components/Jodetail/completeJobOrderRecord.tsx Ver arquivo

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


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


+ 16
- 8
src/components/Jodetail/newJobPickExecution.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -684,15 +685,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return allLots; return allLots;
}, []); }, []);
const extractFloor = (lot: any): string => { 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 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 combinedLotData = useMemo(() => {
const lots = getAllLotsFromHierarchical(jobOrderData); const lots = getAllLotsFromHierarchical(jobOrderData);
@@ -2613,7 +2621,7 @@ const sortedData = [...sourceData].sort((a, b) => {
const floorB = extractFloor(b); const floorB = extractFloor(b);
const orderA = floorSortOrder(floorA); const orderA = floorSortOrder(floorA);
const orderB = floorSortOrder(floorB); 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 // 同楼层再按 routerIndex、pickOrderCode、lotNo
const aIndex = a.routerIndex ?? 0; const aIndex = a.routerIndex ?? 0;
const bIndex = b.routerIndex ?? 0; const bIndex = b.routerIndex ?? 0;


+ 5
- 4
src/components/Jodetail/pickRecordHelpers.ts Ver arquivo

@@ -1,10 +1,11 @@
"use client"; "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 Swal from "sweetalert2";
import type { TFunction } from "i18next"; import type { TFunction } from "i18next";
import { fetchPickRecordPlasticBoxCartonQty } from "@/app/api/jo/actions"; 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 { export interface AllFloorsPlasticBoxCartonQtyResult {
qty2f: number; qty2f: number;
@@ -115,9 +116,9 @@ export async function promptAllFloorsPlasticBoxCartonQty(
title: t("Enter all floors plastic box carton qty"), title: t("Enter all floors plastic box carton qty"),
html: ` html: `
<div style="display:flex;flex-direction:column;gap:10px;text-align:left;"> <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> </div>
`, `,
showCancelButton: true, showCancelButton: true,


+ 9
- 8
src/components/LotSuggestionRules/LotSuggestionRules.tsx Ver arquivo

@@ -1,5 +1,6 @@
"use client"; "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 React, { useState } from "react";
import InfoOutlined from "@mui/icons-material/InfoOutlined"; import InfoOutlined from "@mui/icons-material/InfoOutlined";
import { import {
@@ -33,17 +34,17 @@ const sections: { title: string; lines: string[] }[] = [
{ {
title: "送貨揀貨的樓層", title: "送貨揀貨的樓層",
lines: [ 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: "目前不建議的倉位", title: "目前不建議的倉位",
lines: [ lines: [
"2F-W202-01-00",
"2F-W200-#A-00",
LOC_HOLD,
LOC_WIP,
"這兩個倉位寫在程式裡,畫面還不能改。", "這兩個倉位寫在程式裡,畫面還不能改。",
], ],
}, },
@@ -51,8 +52,8 @@ const sections: { title: string; lines: string[] }[] = [
title: "工單提料(和送貨揀貨不同)", title: "工單提料(和送貨揀貨不同)",
lines: [ lines: [
"同樣是先到期先出。", "同樣是先到期先出。",
"不建議 2F-W202-01-00。半成品會先試 2F-W200-#A-00。",
"工單可以使用 3F。",
"不建議 " + LOC_HOLD + "。半成品會先試 " + LOC_WIP + "。",
"工單可以使用 " + FLOOR_D_KEY + "。",
], ],
}, },
]; ];


+ 22
- 21
src/components/Shop/RouteBoard.tsx Ver arquivo

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


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


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


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


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


+ 4
- 3
src/components/Shop/Shop.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -134,14 +135,14 @@ const Shop: React.FC = () => {
const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null; const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null;
const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0; 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) // 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; const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id;
let storeIdValid = false; let storeIdValid = false;
if (storeId != null && storeId !== undefined && storeId !== "") { if (storeId != null && storeId !== undefined && storeId !== "") {
const storeIdStr = String(storeId).trim(); 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; storeIdValid = true;
} else { } else {
const storeIdNum = Number(storeId); const storeIdNum = Number(storeId);


+ 3
- 2
src/components/Shop/ShopDetail.tsx Ver arquivo

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


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


const lane = selectedTruckLane!; const lane = selectedTruckLane!;
const storeIdStr = normalizeStoreId(lane.storeId) || "2F";
const storeIdStr = normalizeStoreId(lane.storeId) || FLOOR_A_KEY;
const remarkValue = 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() ? String(lane.remark).trim()
: null; : null;




+ 6
- 5
src/components/Shop/TruckLane.tsx Ver arquivo

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


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


@@ -373,8 +374,8 @@ const TruckLane: React.FC = () => {
}} }}
disabled={saving} 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> </Select>
</FormControl> </FormControl>
</Grid> </Grid>


+ 9
- 8
src/components/Shop/TruckLaneDetail.tsx Ver arquivo

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


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


await Promise.all( await Promise.all(
withIds.map((row) => { withIds.map((row) => {
const storeIdStr = normalizeStoreId(row.storeId) || "2F";
const storeIdStr = normalizeStoreId(row.storeId) || FLOOR_A_KEY;
let remarkValue: string | null = null; 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(); remarkValue = String(row.remark).trim();
} }
return updateTruckLaneClient({ return updateTruckLaneClient({
@@ -579,8 +580,8 @@ const TruckLaneDetail: React.FC = () => {
try { try {
const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String
? String(truckData.storeId) ? 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 = ""; let departureTimeStr = "";
if (truckData.departureTime) { if (truckData.departureTime) {
@@ -597,8 +598,8 @@ const TruckLaneDetail: React.FC = () => {
); );
const shopIdValue = foundShop ? foundShop.id : null; 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 // Check if there's an "Unassign" row for this truck lane that should be replaced
let unassignTruck: Truck | null = null; let unassignTruck: Truck | null = null;
@@ -909,7 +910,7 @@ const TruckLaneDetail: React.FC = () => {
{(editingRowIndex === index || massEditing) ? ( {(editingRowIndex === index || massEditing) ? (
(() => { (() => {
const storeIdValue = truckData.storeId ? String(truckData.storeId) : null; const storeIdValue = truckData.storeId ? String(truckData.storeId) : null;
const isEditable = normalizeStoreId(storeIdValue) === "4F";
const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY;
return ( return (
<Autocomplete <Autocomplete
freeSolo freeSolo
@@ -1096,7 +1097,7 @@ const TruckLaneDetail: React.FC = () => {
const storeIdValue = truckData?.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String const storeIdValue = truckData?.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String
? String(truckData.storeId) ? String(truckData.storeId)
: String(truckData.storeId)) : null; : String(truckData.storeId)) : null;
const isEditable = normalizeStoreId(storeIdValue) === "4F";
const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY;
return isEditable ? ( return isEditable ? (
<Grid item xs={12}> <Grid item xs={12}>
<Autocomplete <Autocomplete


+ 6
- 5
src/components/Shop/computeTruckLaneWarnings.ts Ver arquivo

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


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


+ 2
- 1
src/components/Shop/routeBoardImportPreview.ts Ver arquivo

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


const existing = laneMap.get(laneId); const existing = laneMap.get(laneId);


+ 4
- 3
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Ver arquivo

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


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


+ 4
- 3
src/components/StockTakeManagement/PickerCardList.tsx Ver arquivo

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


import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors";
import { import {
Box, Box,
Button, Button,
@@ -173,7 +174,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set()); const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set());
const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
const [stockTakeSectionOptions, setStockTakeSectionOptions] = 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"]; const statusOptions = ["pending", "stockTaking", "approving", "completed"];


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


+ 19
- 0
src/config/demoFloors.ts Ver arquivo

@@ -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 Ver arquivo

@@ -1,8 +1,9 @@
/** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */ /** 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"; import { NEXT_PUBLIC_API_URL } from "@/config/api";


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

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


const floorOptionsAll: ReportField["options"] = [ const floorOptionsAll: ReportField["options"] = [
{ label: "全部", value: "All" }, { 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"] = [ 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 => ({ const pasteItemCodeField = (): ReportField => ({


Carregando…
Cancelar
Salvar