Workbench, delivery search, reports, and lot suggestion now match new dbdebrand
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import { | |||
| Box, | |||
| @@ -37,8 +38,8 @@ const STAFF_PERF_STORE_FILTER_OPTIONS: { | |||
| label: string; | |||
| }[] = [ | |||
| { value: "all", label: "全部" }, | |||
| { value: "2/F", label: "2/F" }, | |||
| { value: "4/F", label: "4/F" }, | |||
| { value: FLOOR_A, label: FLOOR_A }, | |||
| { value: FLOOR_B, label: FLOOR_B }, | |||
| { value: "null_only", label: "車線-X" }, | |||
| ]; | |||
| @@ -1,3 +1,4 @@ | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| @@ -577,7 +578,7 @@ export async function fetchPlannedOutputByDateAndItem( | |||
| } | |||
| /** Warehouse / lane filter for staff delivery performance chart (delivery_order_pick_order.store_id). */ | |||
| export type StaffDeliveryPerformanceStoreFilter = "all" | "2/F" | "4/F" | "null_only"; | |||
| export type StaffDeliveryPerformanceStoreFilter = "all" | typeof FLOOR_A | typeof FLOOR_B | "null_only"; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10 */ | |||
| export async function fetchStaffDeliveryPerformance( | |||
| @@ -591,8 +592,8 @@ export async function fetchStaffDeliveryPerformance( | |||
| if (endDate) p.set("endDate", endDate); | |||
| (staffNos ?? []).forEach((no) => p.append("staffNo", no)); | |||
| if (storeFilter === "null_only") p.set("storeIdNull", "true"); | |||
| else if (storeFilter === "2/F") p.set("storeId", "2/F"); | |||
| else if (storeFilter === "4/F") p.set("storeId", "4/F"); | |||
| else if (storeFilter === FLOOR_A) p.set("storeId", FLOOR_A); | |||
| else if (storeFilter === FLOOR_B) p.set("storeId", FLOOR_B); | |||
| const q = p.toString(); | |||
| const res = await clientAuthFetch( | |||
| q ? `${BASE}/staff-delivery-performance?${q}` : `${BASE}/staff-delivery-performance` | |||
| @@ -1,4 +1,5 @@ | |||
| "use server"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| @@ -85,7 +86,7 @@ export interface ReleaseDoResponse { | |||
| export interface AssignByStoreRequest { | |||
| storeId: string; // "2/F" or "4/F" | |||
| storeId: string; // FLOOR_A or FLOOR_B | |||
| assignTo: number; | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| "use server"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| @@ -244,7 +245,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelection( | |||
| storeId?: string, | |||
| truck?: string, | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| /** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| floor?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> { | |||
| const params = new URLSearchParams(); | |||
| @@ -266,7 +267,7 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday( | |||
| truck?: string, | |||
| requiredDeliveryDate?: string, | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| /** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}`: Truck X split by DO supplier preferred floor (storeId stays null). */ | |||
| floor?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> { | |||
| const params = new URLSearchParams(); | |||
| @@ -1,5 +1,6 @@ | |||
| "use server"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import { cache } from 'react'; | |||
| import { Pageable, ServerFetchError, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { JobOrder, JoStatus, Machine, Operator } from "."; | |||
| @@ -133,7 +134,7 @@ export interface PrintPickRecordRequest{ | |||
| pickOrderId: number; | |||
| printerId: number; | |||
| printQty: number; | |||
| floor?: "2F" | "3F" | "4F" | "ALL"; | |||
| floor?: typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "ALL"; | |||
| plasticBoxCartonQty?: number; | |||
| plasticBoxCartonQty2f?: number; | |||
| plasticBoxCartonQty3f?: number; | |||
| @@ -1,4 +1,5 @@ | |||
| "use server"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| @@ -333,7 +334,7 @@ export interface DoPickOrderDetail { | |||
| } | |||
| export interface AutoAssignReleaseByStoreRequest { | |||
| userId: number; | |||
| storeId: string; // "2/F" | "4/F" | |||
| storeId: string; // FLOOR_A | FLOOR_B | |||
| } | |||
| export interface UpdateDoPickOrderHideStatusRequest { | |||
| id: number; | |||
| @@ -1,5 +1,6 @@ | |||
| import { SETTING_SUPPLIERS_A, SETTING_SUPPLIERS_B } from "@/config/demoFloors"; | |||
| /** `settings.name`:逗號分隔 supplier **code**(須與後端讀取邏輯一致)。 */ | |||
| export const SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F"; | |||
| export const SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F"; | |||
| export const SETTING_DO_FLOOR_SUPPLIERS_2F = SETTING_SUPPLIERS_A; | |||
| export const SETTING_DO_FLOOR_SUPPLIERS_4F = SETTING_SUPPLIERS_B; | |||
| export const SETTING_DO_FLOOR_CATEGORY = "DO_FLOOR"; | |||
| @@ -1,3 +1,4 @@ | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import dayjs, { ConfigType, Dayjs } from "dayjs"; | |||
| import { Uom } from "../api/settings/uom"; | |||
| import { | |||
| @@ -249,7 +250,7 @@ export const normalizeStoreId = (storeId: string | number | String | Number | nu | |||
| const storeIdStr = typeof storeId === 'string' || storeId instanceof String | |||
| ? String(storeId) | |||
| : String(storeId); | |||
| if (storeIdStr === "2" || storeIdStr === "2F") return "2F"; | |||
| if (storeIdStr === "4" || storeIdStr === "4F") return "4F"; | |||
| if (storeIdStr === "2" || storeIdStr === FLOOR_A_KEY) return FLOOR_A_KEY; | |||
| if (storeIdStr === "4" || storeIdStr === FLOOR_B_KEY) return FLOOR_B_KEY; | |||
| return storeIdStr; | |||
| }; | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; | |||
| import { | |||
| Box, | |||
| @@ -449,8 +450,8 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| displayEmpty | |||
| > | |||
| <MenuItem value="">{t("All Stores")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| @@ -535,7 +536,7 @@ const TruckScheduleDashboard: React.FC = () => { | |||
| <Chip | |||
| label={row.storeId || '-'} | |||
| size="small" | |||
| color={row.storeId === '2/F' ? 'primary' : 'secondary'} | |||
| color={row.storeId === FLOOR_A ? 'primary' : 'secondary'} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import EditOutlined from "@mui/icons-material/EditOutlined"; | |||
| @@ -57,7 +58,7 @@ function formatBracketList(codesCsv: string): string { | |||
| return `[${n.split(",").join(", ")}]`; | |||
| } | |||
| type EditFloor = "2F" | "4F"; | |||
| type EditFloor = typeof FLOOR_A_KEY | typeof FLOOR_B_KEY; | |||
| type FloorRow = { code: string; name: string }; | |||
| @@ -97,7 +98,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| const [codes4F, setCodes4F] = useState(""); | |||
| const [editOpen, setEditOpen] = useState(false); | |||
| const [editFloor, setEditFloor] = useState<EditFloor>("2F"); | |||
| const [editFloor, setEditFloor] = useState<EditFloor>(FLOOR_A_KEY); | |||
| const [dialogSaving, setDialogSaving] = useState(false); | |||
| const [comboLoading, setComboLoading] = useState(false); | |||
| const [supplierCombo, setSupplierCombo] = useState<ShopComboRow[] | null>(null); | |||
| @@ -130,8 +131,8 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| const display2F = useMemo(() => formatBracketList(codes2F), [codes2F]); | |||
| const display4F = useMemo(() => formatBracketList(codes4F), [codes4F]); | |||
| const currentDraftRows = editFloor === "2F" ? draftRows2F : draftRows4F; | |||
| const setCurrentDraftRows = editFloor === "2F" ? setDraftRows2F : setDraftRows4F; | |||
| const currentDraftRows = editFloor === FLOOR_A_KEY ? draftRows2F : draftRows4F; | |||
| const setCurrentDraftRows = editFloor === FLOOR_A_KEY ? setDraftRows2F : setDraftRows4F; | |||
| const openEdit = async (floor: EditFloor) => { | |||
| setEditFloor(floor); | |||
| @@ -172,12 +173,12 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| setError(null); | |||
| setSuccess(null); | |||
| try { | |||
| const rows = editFloor === "2F" ? draftRows2F : draftRows4F; | |||
| const rows = editFloor === FLOOR_A_KEY ? draftRows2F : draftRows4F; | |||
| const normalized = normalizeCodesCsv(floorRowsToCsv(rows)); | |||
| const key = | |||
| editFloor === "2F" ? SETTING_DO_FLOOR_SUPPLIERS_2F : SETTING_DO_FLOOR_SUPPLIERS_4F; | |||
| editFloor === FLOOR_A_KEY ? SETTING_DO_FLOOR_SUPPLIERS_2F : SETTING_DO_FLOOR_SUPPLIERS_4F; | |||
| await postSettingClient(key, normalized); | |||
| if (editFloor === "2F") setCodes2F(normalized); | |||
| if (editFloor === FLOOR_A_KEY) setCodes2F(normalized); | |||
| else setCodes4F(normalized); | |||
| setSuccess(t("Saved")); | |||
| setEditOpen(false); | |||
| @@ -237,7 +238,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| return; | |||
| } | |||
| const canonical = hit.code.trim(); | |||
| const otherRows = editFloor === "2F" ? draftRows4F : draftRows2F; | |||
| const otherRows = editFloor === FLOOR_A_KEY ? draftRows4F : draftRows2F; | |||
| if (currentDraftRows.some((r) => r.code.toLowerCase() === canonical.toLowerCase())) { | |||
| setAddError(t("Duplicate in floor")); | |||
| return; | |||
| @@ -286,7 +287,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| }} | |||
| > | |||
| <Typography component="span" variant="subtitle1" sx={{ minWidth: 120, fontWeight: 600 }}> | |||
| {t("2F supplier")} | |||
| {t(FLOOR_A_KEY + " supplier")} | |||
| </Typography> | |||
| <Typography | |||
| component="span" | |||
| @@ -296,10 +297,10 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| {display2F} | |||
| </Typography> | |||
| <IconButton | |||
| aria-label={t("Edit 2F")} | |||
| aria-label={t("Edit " + FLOOR_A_KEY)} | |||
| color="primary" | |||
| edge="end" | |||
| onClick={() => void openEdit("2F")} | |||
| onClick={() => void openEdit(FLOOR_A_KEY)} | |||
| size="small" | |||
| > | |||
| <EditOutlined /> | |||
| @@ -319,7 +320,7 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| }} | |||
| > | |||
| <Typography component="span" variant="subtitle1" sx={{ minWidth: 120, fontWeight: 600 }}> | |||
| {t("4F supplier")} | |||
| {t(FLOOR_B_KEY + " supplier")} | |||
| </Typography> | |||
| <Typography | |||
| component="span" | |||
| @@ -329,10 +330,10 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| {display4F} | |||
| </Typography> | |||
| <IconButton | |||
| aria-label={t("Edit 4F")} | |||
| aria-label={t("Edit " + FLOOR_B_KEY)} | |||
| color="primary" | |||
| edge="end" | |||
| onClick={() => void openEdit("4F")} | |||
| onClick={() => void openEdit(FLOOR_B_KEY)} | |||
| size="small" | |||
| > | |||
| <EditOutlined /> | |||
| @@ -361,8 +362,8 @@ const DeliveryOrderFloorSettings: React.FC = () => { | |||
| onChange={onFloorSelectChange} | |||
| disabled={dialogSaving || comboLoading} | |||
| > | |||
| <MenuItem value="2F">2/F</MenuItem> | |||
| <MenuItem value="4F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A_KEY}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_B_KEY}>{FLOOR_B}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Button | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import type { DoDetail as DoDetailType } from "@/app/api/do/actions"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -203,20 +204,20 @@ const DoDetail: React.FC<Props> = ({ | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handleAssignByStore("2/F")} | |||
| onClick={() => handleAssignByStore(FLOOR_A)} | |||
| disabled={isAssigning} | |||
| sx={{ minWidth: 120 }} | |||
| > | |||
| {t("Assign 2/F")} | |||
| {t("Assign " + FLOOR_A)} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="secondary" | |||
| onClick={() => handleAssignByStore("4/F")} | |||
| onClick={() => handleAssignByStore(FLOOR_B)} | |||
| disabled={isAssigning} | |||
| sx={{ minWidth: 120 }} | |||
| > | |||
| {t("Assign 4/F")} | |||
| {t("Assign " + FLOOR_B)} | |||
| </Button> | |||
| </Stack> | |||
| @@ -224,20 +225,20 @@ const DoDetail: React.FC<Props> = ({ | |||
| <Button | |||
| variant="outlined" | |||
| color="primary" | |||
| onClick={() => handleReleaseByStore("2/F")} | |||
| onClick={() => handleReleaseByStore(FLOOR_A)} | |||
| disabled={isAssigning} | |||
| sx={{ minWidth: 120 }} | |||
| > | |||
| {t("Release 2/F")} | |||
| {t("Release " + FLOOR_A)} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| color="secondary" | |||
| onClick={() => handleReleaseByStore("4/F")} | |||
| onClick={() => handleReleaseByStore(FLOOR_B)} | |||
| disabled={isAssigning} | |||
| sx={{ minWidth: 120 }} | |||
| > | |||
| {t("Release 4/F")} | |||
| {t("Release " + FLOOR_B)} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { DoResult } from "@/app/api/do"; | |||
| import { | |||
| DoSearchAll, | |||
| @@ -64,8 +65,8 @@ type Props = { | |||
| }; | |||
| type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>; | |||
| type SearchParamNames = keyof SearchBoxInputs; | |||
| type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA" | "REPLENISH"; | |||
| type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string }; | |||
| type DoSearchTab = typeof FLOOR_A_KEY | typeof FLOOR_B_KEY | "TRUCK_X" | "ETRA" | "REPLENISH"; | |||
| type TabFilter = { floor: typeof FLOOR_A_KEY | typeof FLOOR_B_KEY | null; isExtra: boolean; forceTruckKeyword?: string }; | |||
| // put all this into a new component | |||
| // ConsoDoForm | |||
| @@ -103,7 +104,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| const [searchAllDos, setSearchAllDos] = useState<DoSearchAll[]>([]); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| const [isWorkbench, setIsWorkbench] = useState(false); | |||
| const [activeTab, setActiveTab] = useState<DoSearchTab>("2F"); | |||
| const [activeTab, setActiveTab] = useState<DoSearchTab>(FLOOR_A_KEY); | |||
| const [searchBoxResetKey, setSearchBoxResetKey] = useState(0); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 1, | |||
| @@ -326,10 +327,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| const resolveTabFilter = useCallback((tab: DoSearchTab): TabFilter => { | |||
| switch (tab) { | |||
| case "2F": | |||
| return { floor: "2F", isExtra: false }; | |||
| case "4F": | |||
| return { floor: "4F", isExtra: false }; | |||
| case FLOOR_A_KEY: | |||
| return { floor: FLOOR_A_KEY, isExtra: false }; | |||
| case FLOOR_B_KEY: | |||
| return { floor: FLOOR_B_KEY, isExtra: false }; | |||
| case "TRUCK_X": | |||
| return { floor: null, isExtra: false, forceTruckKeyword: "x" }; | |||
| case "ETRA": | |||
| @@ -802,8 +803,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| > | |||
| <Tab value="2F" label="2/F" /> | |||
| <Tab value="4F" label="4/F" /> | |||
| <Tab value={FLOOR_A_KEY} label={FLOOR_A} /> | |||
| <Tab value={FLOOR_B_KEY} label={FLOOR_B} /> | |||
| <Tab value="TRUCK_X" label={t("Truck X")} /> | |||
| <Tab value="ETRA" label={t("Etra")} /> | |||
| <Tab value="REPLENISH" label={t("Replenishment")} /> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { DoResult } from "@/app/api/do"; | |||
| import { DoSearchAll, DoSearchLiteResponse, fetchDoSearch, fetchAllDoSearch, fetchDoSearchList, releaseDo } from "@/app/api/do/actions"; | |||
| import { | |||
| @@ -150,8 +151,8 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| paramName: "floor", | |||
| type: "select-labelled", | |||
| options: [ | |||
| { label: "2F", value: "2F" }, | |||
| { label: "4F", value: "4F" }, | |||
| { label: FLOOR_A_KEY, value: FLOOR_A_KEY }, | |||
| { label: FLOOR_B_KEY, value: FLOOR_B_KEY }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { useEffect, useState } from "react"; | |||
| import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; | |||
| import DownloadIcon from "@mui/icons-material/Download"; | |||
| @@ -114,7 +115,7 @@ const TruckRoutingSummaryTabWorkbench: React.FC = () => { | |||
| <TextField | |||
| select | |||
| fullWidth | |||
| label={t("2/F or 4/F")} | |||
| label={t(FLOOR_A + " or " + FLOOR_B)} | |||
| value={storeId} | |||
| onChange={(e) => onStoreChange(e.target.value)} | |||
| > | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -41,8 +42,8 @@ type Props = { | |||
| function floorLabel(storeId: string | null | undefined, t: (k: string) => string): string { | |||
| const s = (storeId ?? "").trim().replace(/\//g, "").toUpperCase(); | |||
| if (s === "2F") return "2/F"; | |||
| if (s === "4F") return "4/F"; | |||
| if (s === FLOOR_A_KEY) return FLOOR_A; | |||
| if (s === FLOOR_B_KEY) return FLOOR_B; | |||
| if (!s) return t("Truck X"); | |||
| return storeId ?? ""; | |||
| } | |||
| @@ -58,7 +59,7 @@ function laneSecondary(c: WorkbenchMergeTicketCandidate, t: (k: string) => strin | |||
| const dep = (c.truckDepartureTime ?? "").trim(); | |||
| const lane = (c.truckLanceCode ?? "").trim(); | |||
| const floor = floorLabel(c.storeId, t); | |||
| const is4F = (c.storeId ?? "").trim().replace(/\//g, "").toUpperCase() === "4F"; | |||
| const is4F = (c.storeId ?? "").trim().replace(/\//g, "").toUpperCase() === FLOOR_B_KEY; | |||
| const seq = | |||
| is4F && c.loadingSequence != null | |||
| ? `${t("Loading sequence")} ${c.loadingSequence} · ` | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material"; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -53,7 +54,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [selectedStore, setSelectedStore] = useState<string>("2/F"); | |||
| const [selectedStore, setSelectedStore] = useState<string>(FLOOR_A); | |||
| const [selectedTruck, setSelectedTruck] = useState<string>(""); | |||
| const [modalOpen, setModalOpen] = useState(false); | |||
| const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]); | |||
| @@ -92,7 +93,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| [ticketFloor], | |||
| ); | |||
| const defaultTruckCount = | |||
| (ticketFloor === "2/F" ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0; | |||
| (ticketFloor === FLOOR_A ? summary2F?.defaultTruckCount : summary4F?.defaultTruckCount) ?? 0; | |||
| const etraEnterInFlightRef = useRef(false); | |||
| const [etraMergeDialogOpen, setEtraMergeDialogOpen] = useState(false); | |||
| @@ -167,8 +168,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD"); | |||
| else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD"); | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchWorkbenchStoreLaneSummary("2/F", dateParam, releaseType), | |||
| fetchWorkbenchStoreLaneSummary("4/F", dateParam, releaseType), | |||
| fetchWorkbenchStoreLaneSummary(FLOOR_A, dateParam, releaseType), | |||
| fetchWorkbenchStoreLaneSummary(FLOOR_B, dateParam, releaseType), | |||
| ]); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| @@ -217,8 +218,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| startFullTimer(); | |||
| try { | |||
| const [list2F, list4F] = await Promise.all([ | |||
| fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "2/F"), | |||
| fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "4/F"), | |||
| fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, FLOOR_A), | |||
| fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, FLOOR_B), | |||
| ]); | |||
| const groupByTruck = (list: { truckLanceCode?: string | null }[]) => { | |||
| const map: Record<string, number> = {}; | |||
| @@ -441,8 +442,8 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| <FormControl fullWidth size="small"> | |||
| <InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel> | |||
| <Select labelId="ticket-floor-select-label" value={ticketFloor} label={t("Floor ticket")} onChange={(e) => patchLanePanelPrefs({ ticketFloor: e.target.value as WorkbenchLanePanelPrefs["ticketFloor"] })}> | |||
| <MenuItem value="2/F">{t("2F ticket")}</MenuItem> | |||
| <MenuItem value="4/F">{t("4F ticket")}</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{t(FLOOR_A_KEY + " ticket")}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{t(FLOOR_B_KEY + " ticket")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| @@ -481,10 +482,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| {!inEtraUi ? ( | |||
| <Grid container spacing={2}> | |||
| {ticketFloor === "2/F" && ( | |||
| {ticketFloor === FLOOR_A && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_A}</Typography> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : ( | |||
| <Grid container spacing={1}> | |||
| @@ -494,7 +495,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| <Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography> | |||
| <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}> | |||
| {row.lanes.map((lane) => ( | |||
| <Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("2/F", row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}> | |||
| <Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick(FLOOR_A, row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}> | |||
| {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`} | |||
| </Button> | |||
| ))} | |||
| @@ -509,10 +510,10 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| </Grid> | |||
| )} | |||
| {ticketFloor === "4/F" && ( | |||
| {ticketFloor === FLOOR_B && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_B}</Typography> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !truckGroups4F.length ? renderNoEntry() : ( | |||
| <Grid container spacing={1}> | |||
| @@ -524,7 +525,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| {slots.map((slot) => { | |||
| const handlerName = (slot.lane.handlerName ?? "").trim(); | |||
| return ( | |||
| <Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("4/F", slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}> | |||
| <Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick(FLOOR_B, slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}> | |||
| {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})${handlerName ? ` ${handlerName}` : ""}`} | |||
| </Button> | |||
| ); | |||
| @@ -585,21 +586,21 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| </Box> | |||
| </Grid> | |||
| {ticketFloor === "2/F" && ( | |||
| {ticketFloor === FLOOR_A && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_A}</Typography> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {truckCounts2F.length === 0 ? renderNoEntry() : ( | |||
| <Grid container spacing={1}> | |||
| {truckCounts2F.map(({ truck, count }) => ( | |||
| <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}> | |||
| <Grid item xs={6} sm={4} md={3} key={`${FLOOR_A_KEY}-${truck}`} sx={{ display: "flex" }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| clearModalEtraContext(); | |||
| setIsDefaultTruck(false); | |||
| setSelectedStore("2/F"); | |||
| setSelectedStore(FLOOR_A); | |||
| setSelectedTruck(truck); | |||
| setModalOpen(true); | |||
| }} | |||
| @@ -616,21 +617,21 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| </Grid> | |||
| )} | |||
| {ticketFloor === "4/F" && ( | |||
| {ticketFloor === FLOOR_B && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{FLOOR_B}</Typography> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {truckCounts4F.length === 0 ? renderNoEntry() : ( | |||
| <Grid container spacing={1}> | |||
| {truckCounts4F.map(({ truck, count }) => ( | |||
| <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}> | |||
| <Grid item xs={6} sm={4} md={3} key={`${FLOOR_B_KEY}-${truck}`} sx={{ display: "flex" }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| clearModalEtraContext(); | |||
| setIsDefaultTruck(false); | |||
| setSelectedStore("4/F"); | |||
| setSelectedStore(FLOOR_B); | |||
| setSelectedTruck(truck); | |||
| setModalOpen(true); | |||
| }} | |||
| @@ -694,7 +695,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ | |||
| (lane.truckLanceCode ?? "").trim() === "車線-X" || | |||
| (lane.truckLanceCode ?? "").trim() === "Truck X"; | |||
| const floorKey = sid.replace(/\//g, "").toUpperCase(); | |||
| const is4F = floorKey === "4F"; | |||
| const is4F = floorKey === FLOOR_B_KEY; | |||
| const labelCore = | |||
| isTruckX | |||
| ? `${t("車線-X")}${sid ? ` · ${sid}` : ""} (${lane.unassigned}/${lane.total})` | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -742,8 +743,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| if (floorKey === "2F") return "Label機 2F A+B"; | |||
| if (floorKey === "4F") return "Label機 4F 乾貨 C, D"; | |||
| if (floorKey === FLOOR_A_KEY) return LABEL_A; | |||
| if (floorKey === FLOOR_B_KEY) return LABEL_B; | |||
| return undefined; | |||
| }, [fgPickOrders]); | |||
| @@ -1037,7 +1038,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| .replace(/\//g, '') | |||
| .replace(/\s/g, ''); | |||
| const pickOrderLinesForDisplay = | |||
| doFloorKey === '2F' | |||
| doFloorKey === FLOOR_A_KEY | |||
| ? [...(mergedPickOrder.pickOrderLines || [])].sort((a: any, b: any) => { | |||
| const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999; | |||
| const bo = b.itemOrder != null ? Number(b.itemOrder) : 999999; | |||
| @@ -5,6 +5,7 @@ | |||
| * 「掃碼提貨」 per listed lot row (parent calls `workbenchScanPick` with `inventoryLotLineId`). | |||
| */ | |||
| import { LOC_PREFIX } from "@/config/demoFloors"; | |||
| import React, { | |||
| useCallback, | |||
| useEffect, | |||
| @@ -455,7 +456,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||
| return availableLots.filter((lot) => { | |||
| // 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號 | |||
| if (lot._scanned) return true; | |||
| return String(lot.warehouseCode ?? "").startsWith(prefix); | |||
| const code = String(lot.warehouseCode ?? ""); | |||
| return code.startsWith(prefix) || code.startsWith(LOC_PREFIX); | |||
| }); | |||
| }, [availableLots, warehouseCodePrefixFilter]); | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { | |||
| Box, | |||
| @@ -278,8 +279,8 @@ const WorkbenchTicketReleaseTableTab: React.FC = () => { | |||
| displayEmpty | |||
| > | |||
| <MenuItem value="">{t("All Floors")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <FormControl sx={{ minWidth: 150 }} size="small"> | |||
| @@ -1,5 +1,6 @@ | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| export type WorkbenchLaneDateKey = "today" | "tomorrow" | "dayAfterTomorrow"; | |||
| export type WorkbenchLaneFloor = "2/F" | "4/F"; | |||
| export type WorkbenchLaneFloor = typeof FLOOR_A | typeof FLOOR_B; | |||
| /** Tab 0 lane assignment filters — lifted to DoWorkbenchTabs so they survive tab switches. */ | |||
| export type WorkbenchLanePanelPrefs = { | |||
| @@ -10,7 +11,7 @@ export type WorkbenchLanePanelPrefs = { | |||
| export const DEFAULT_WORKBENCH_LANE_PANEL_PREFS: WorkbenchLanePanelPrefs = { | |||
| selectedDate: "today", | |||
| ticketFloor: "2/F", | |||
| ticketFloor: FLOOR_A, | |||
| releaseType: "batch", | |||
| }; | |||
| @@ -19,5 +20,5 @@ export function isWorkbenchLaneDateKey(v: string): v is WorkbenchLaneDateKey { | |||
| } | |||
| export function isWorkbenchLaneFloor(v: string): v is WorkbenchLaneFloor { | |||
| return v === "2/F" || v === "4/F"; | |||
| return v === FLOOR_A || v === FLOOR_B; | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel, Card, CardContent } from "@mui/material"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -28,8 +29,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| setIsLoadingSummary(true); | |||
| try { | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchStoreLaneSummary("2/F"), | |||
| fetchStoreLaneSummary("4/F") | |||
| fetchStoreLaneSummary(FLOOR_A), | |||
| fetchStoreLaneSummary(FLOOR_B) | |||
| ]); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| @@ -169,7 +170,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| color: '#1976d2' | |||
| }} | |||
| > | |||
| 2/F | |||
| {FLOOR_A} | |||
| </Typography> | |||
| </Box> | |||
| @@ -232,7 +233,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| onClick={() => handleAssignByLane(FLOOR_A, item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -282,7 +283,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| color: '#1976d2' | |||
| }} | |||
| > | |||
| 4/F | |||
| {FLOOR_B} | |||
| </Typography> | |||
| </Box> | |||
| @@ -345,7 +346,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| onClick={() => handleAssignByLane(FLOOR_B, item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| /** | |||
| * 權限說明(與全站一致): | |||
| * - 登入後 JWT / session 帶有 `abilities: string[]`(見 config/authConfig、authorities.ts)。 | |||
| @@ -303,8 +304,8 @@ const FGPickOrderTicketReleaseTable: React.FC = () => { | |||
| displayEmpty | |||
| > | |||
| <MenuItem value="">{t("All Floors")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||
| import { | |||
| Alert, | |||
| @@ -29,7 +30,7 @@ import { | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type FloorFilter = "all" | "2/F" | "4/F"; | |||
| type FloorFilter = "all" | typeof FLOOR_A | typeof FLOOR_B; | |||
| type BreakdownDimension = "lane" | "shop" | "floor"; | |||
| type DailySummaryRow = { | |||
| @@ -361,10 +362,10 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| total: 0, | |||
| }; | |||
| if (record.storeId === "2/F") { | |||
| if (record.storeId === FLOOR_A) { | |||
| current.floor2F += cartonQty; | |||
| } | |||
| if (record.storeId === "4/F") { | |||
| if (record.storeId === FLOOR_B) { | |||
| current.floor4F += cartonQty; | |||
| } | |||
| if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) { | |||
| @@ -390,18 +391,18 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| let truckX = 0; | |||
| filteredRecords.forEach((record) => { | |||
| const cartonQty = Number(record.numberOfCartons ?? 0); | |||
| if (record.storeId === "2/F") floor2F += cartonQty; | |||
| if (record.storeId === "4/F") floor4F += cartonQty; | |||
| if (record.storeId === FLOOR_A) floor2F += cartonQty; | |||
| if (record.storeId === FLOOR_B) floor4F += cartonQty; | |||
| if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) truckX += cartonQty; | |||
| }); | |||
| return toSorted( | |||
| [ | |||
| { key: "2/F", label: "2/F", qty: floor2F }, | |||
| { key: "4/F", label: "4/F", qty: floor4F }, | |||
| { key: FLOOR_A, label: FLOOR_A, qty: floor2F }, | |||
| { key: FLOOR_B, label: FLOOR_B, qty: floor4F }, | |||
| { key: TRUCK_X_LANE, label: shortLaneLabel(TRUCK_X_LANE), qty: truckX }, | |||
| ].filter((row) => { | |||
| if (row.qty > 0) return true; | |||
| if (row.key === "2/F" || row.key === "4/F") return floor === row.key; | |||
| if (row.key === FLOOR_A || row.key === FLOOR_B) return floor === row.key; | |||
| return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE; | |||
| }), | |||
| ); | |||
| @@ -490,7 +491,7 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| (row: BreakdownRow) => { | |||
| if (breakdownDimension === "lane") return lane === row.key; | |||
| if (breakdownDimension === "shop") return shop === row.key; | |||
| if (row.key === "2/F" || row.key === "4/F") return floor === row.key; | |||
| if (row.key === FLOOR_A || row.key === FLOOR_B) return floor === row.key; | |||
| return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE; | |||
| }, | |||
| [breakdownDimension, floor, lane, shop], | |||
| @@ -550,8 +551,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| }; | |||
| if (!current.shopName && shopName) current.shopName = shopName; | |||
| if (record.storeId === "2/F") current.floor2F += cartonQty; | |||
| if (record.storeId === "4/F") current.floor4F += cartonQty; | |||
| if (record.storeId === FLOOR_A) current.floor2F += cartonQty; | |||
| if (record.storeId === FLOOR_B) current.floor4F += cartonQty; | |||
| if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) current.truckX += cartonQty; | |||
| current.total += cartonQty; | |||
| @@ -677,8 +678,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| t("Date"), | |||
| t("Shop code"), | |||
| t("Shop Name"), | |||
| t("2/F carton qty"), | |||
| t("4/F carton qty"), | |||
| t(FLOOR_A + " carton qty"), | |||
| t(FLOOR_B + " carton qty"), | |||
| t("Truck X carton qty"), | |||
| t("Total carton qty"), | |||
| ], | |||
| @@ -693,8 +694,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| ]), | |||
| [...blank], | |||
| [t("Summary"), ...blank.slice(1)], | |||
| [t("2/F carton qty"), "", "", reportSummary.floor2F, "", "", ""], | |||
| [t("4/F carton qty"), "", "", "", reportSummary.floor4F, "", ""], | |||
| [t(FLOOR_A + " carton qty"), "", "", reportSummary.floor2F, "", "", ""], | |||
| [t(FLOOR_B + " carton qty"), "", "", "", reportSummary.floor4F, "", ""], | |||
| [t("Truck X carton qty"), "", "", "", "", reportSummary.truckX, ""], | |||
| [t("Total carton qty"), "", "", "", "", "", reportSummary.total], | |||
| ]; | |||
| @@ -990,8 +991,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | |||
| > | |||
| <MenuItem value={ALL}>{t("All")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem> | |||
| </TextField> | |||
| </Grid> | |||
| <Grid item xs={12} sm={6} md={3}> | |||
| @@ -1041,8 +1042,8 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) => | |||
| <Paper sx={{ px: 1, py: 1 }}> | |||
| <Grid container> | |||
| {[ | |||
| { label: t("2/F carton qty"), value: summary.floor2F }, | |||
| { label: t("4/F carton qty"), value: summary.floor4F }, | |||
| { label: t(FLOOR_A + " carton qty"), value: summary.floor2F }, | |||
| { label: t(FLOOR_B + " carton qty"), value: summary.floor4F }, | |||
| { label: t("Truck X carton qty"), value: summary.truckX }, | |||
| { label: t("Total carton qty"), value: summary.total }, | |||
| ].map((kpi, index) => ( | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material"; | |||
| import { useCallback, useEffect, useMemo, useState, useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -22,7 +23,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [selectedStore, setSelectedStore] = useState<string>("2/F"); | |||
| const [selectedStore, setSelectedStore] = useState<string>(FLOOR_A); | |||
| const [selectedTruck, setSelectedTruck] = useState<string>(""); | |||
| const [selectedDefaultTruck, setSelectedDefaultTruck] = useState<string>(""); | |||
| const [modalOpen, setModalOpen] = useState(false); | |||
| @@ -44,7 +45,7 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to | |||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const [releaseType, setReleaseType] = useState<string>("batch"); | |||
| const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F"); | |||
| const [ticketFloor, setTicketFloor] = useState<typeof FLOOR_A | typeof FLOOR_B>(FLOOR_A); | |||
| const startFullTimer = () => { | |||
| if (typeof window === "undefined") return; | |||
| const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const; | |||
| @@ -79,8 +80,8 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to | |||
| } | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchStoreLaneSummary("2/F", dateParam, releaseType), | |||
| fetchStoreLaneSummary("4/F", dateParam, releaseType) | |||
| fetchStoreLaneSummary(FLOOR_A, dateParam, releaseType), | |||
| fetchStoreLaneSummary(FLOOR_B, dateParam, releaseType) | |||
| ]); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| @@ -117,8 +118,8 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to | |||
| startFullTimer(); | |||
| try { | |||
| const [list2F, list4F] = await Promise.all([ | |||
| fetchReleasedDoPickOrdersForSelection(undefined, "2/F"), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, "4/F"), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_A), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_B), | |||
| ]); | |||
| const groupByTruck = (list: { truckLanceCode?: string | null }[]) => { | |||
| const map: Record<string, number> = {}; | |||
| @@ -388,10 +389,10 @@ const getDateLabel = (offset: number) => { | |||
| id="ticket-floor-select" | |||
| value={ticketFloor} | |||
| label={t("Floor ticket")} | |||
| onChange={(e) => setTicketFloor(e.target.value as "2/F" | "4/F")} | |||
| onChange={(e) => setTicketFloor(e.target.value as typeof FLOOR_A | typeof FLOOR_B)} | |||
| > | |||
| <MenuItem value="2/F">{t("2F ticket")}</MenuItem> | |||
| <MenuItem value="4/F">{t("4F ticket")}</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{t(FLOOR_A_KEY + " ticket")}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{t(FLOOR_B_KEY + " ticket")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| @@ -414,7 +415,7 @@ const getDateLabel = (offset: number) => { | |||
| {/* Grid containing both floors */} | |||
| <Grid container spacing={2}> | |||
| {/* 2/F 楼层面板 */} | |||
| {ticketFloor === "2/F" && ( | |||
| {ticketFloor === FLOOR_A && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| {/* Floor Label */} | |||
| @@ -426,7 +427,7 @@ const getDateLabel = (offset: number) => { | |||
| pt: 1 | |||
| }} | |||
| > | |||
| 2/F | |||
| {FLOOR_A} | |||
| </Typography> | |||
| {/* Content Box */} | |||
| @@ -495,7 +496,7 @@ const getDateLabel = (offset: number) => { | |||
| disabled={lane.unassigned === 0 || isAssigning} | |||
| onClick={() => | |||
| handleLaneButtonClick( | |||
| "2/F", | |||
| FLOOR_A, | |||
| row.truckDepartureTime, | |||
| lane.truckLanceCode, | |||
| null, | |||
| @@ -530,7 +531,7 @@ const getDateLabel = (offset: number) => { | |||
| </Stack> | |||
| </Grid> | |||
| )} | |||
| {ticketFloor === "4/F" && ( | |||
| {ticketFloor === FLOOR_B && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| {/* Floor Label */} | |||
| @@ -542,7 +543,7 @@ const getDateLabel = (offset: number) => { | |||
| pt: 1 | |||
| }} | |||
| > | |||
| 4/F | |||
| {FLOOR_B} | |||
| </Typography> | |||
| {/* Content Box */} | |||
| @@ -613,7 +614,7 @@ const getDateLabel = (offset: number) => { | |||
| disabled={slot.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => | |||
| handleLaneButtonClick( | |||
| "4/F", | |||
| FLOOR_B, | |||
| slot.truckDepartureTime, | |||
| slot.lane.truckLanceCode, | |||
| slot.lane.loadingSequence ?? null, | |||
| @@ -720,11 +721,11 @@ const getDateLabel = (offset: number) => { | |||
| </Box> | |||
| </Grid> | |||
| {ticketFloor === "2/F" && ( | |||
| {ticketFloor === FLOOR_A && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> | |||
| 2/F | |||
| {FLOOR_A} | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| @@ -751,7 +752,7 @@ const getDateLabel = (offset: number) => { | |||
| ) : ( | |||
| <Grid container spacing={1}> | |||
| {truckCounts2F.map(({ truck, count }) => ( | |||
| <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}> | |||
| <Grid item xs={6} sm={4} md={3} key={`${FLOOR_A_KEY}-${truck}`} sx={{ display: "flex" }}> | |||
| <Button | |||
| @@ -759,7 +760,7 @@ const getDateLabel = (offset: number) => { | |||
| size="medium" | |||
| onClick={() => { | |||
| setIsDefaultTruck(false); | |||
| setSelectedStore("2/F"); | |||
| setSelectedStore(FLOOR_A); | |||
| setSelectedTruck(truck); | |||
| setModalOpen(true); | |||
| }} | |||
| @@ -788,11 +789,11 @@ const getDateLabel = (offset: number) => { | |||
| </Stack> | |||
| </Grid> | |||
| )} | |||
| {ticketFloor === "4/F" && ( | |||
| {ticketFloor === FLOOR_B && ( | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> | |||
| 4/F | |||
| {FLOOR_B} | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| @@ -819,7 +820,7 @@ const getDateLabel = (offset: number) => { | |||
| ) : ( | |||
| <Grid container spacing={1}> | |||
| {truckCounts4F.map(({ truck, count }) => ( | |||
| <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}> | |||
| <Grid item xs={6} sm={4} md={3} key={`${FLOOR_B_KEY}-${truck}`} sx={{ display: "flex" }}> | |||
| <Button | |||
| @@ -827,7 +828,7 @@ const getDateLabel = (offset: number) => { | |||
| size="medium" | |||
| onClick={() => { | |||
| setIsDefaultTruck(false); | |||
| setSelectedStore("4/F"); | |||
| setSelectedStore(FLOOR_B); | |||
| setSelectedTruck(truck); | |||
| setModalOpen(true); | |||
| }} | |||
| @@ -877,7 +878,7 @@ const getDateLabel = (offset: number) => { | |||
| variant="outlined" | |||
| size="medium" | |||
| onClick={() => { | |||
| setSelectedStore("4/F"); // 或用專門標示 Truck X | |||
| setSelectedStore(FLOOR_B); // 或用專門標示 Truck X | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("before"); // 類似上一輪說的 dateScope | |||
| @@ -902,8 +903,8 @@ const getDateLabel = (offset: number) => { | |||
| const loadCounts = async () => { | |||
| try { | |||
| const [list2F, list4F] = await Promise.all([ | |||
| fetchReleasedDoPickOrdersForSelection(undefined, "2/F"), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, "4/F"), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_A), | |||
| fetchReleasedDoPickOrdersForSelection(undefined, FLOOR_B), | |||
| ]); | |||
| const groupByTruck = (list: { truckLanceCode?: string | null }[]) => { | |||
| const map: Record<string, number> = {}; | |||
| @@ -1,4 +1,5 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -134,8 +135,8 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo | |||
| setIsLoadingSummary(true); | |||
| try { | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchStoreLaneSummary("2/F"), | |||
| fetchStoreLaneSummary("4/F") | |||
| fetchStoreLaneSummary(FLOOR_A), | |||
| fetchStoreLaneSummary(FLOOR_B) | |||
| ]); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| @@ -300,7 +301,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo | |||
| Swal.fire({ | |||
| position: "bottom-end", | |||
| icon: "warning", | |||
| text: "請先選擇 2/F 或 4/F、車線和日期", | |||
| text: "請先選擇 " + FLOOR_A + " 或 " + FLOOR_B + "、車線和日期", | |||
| showConfirmButton: false, | |||
| timer: 1800, | |||
| }); | |||
| @@ -1,6 +1,7 @@ | |||
| /* | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -786,7 +787,7 @@ const PickExecution: React.FC<Props> = ({ | |||
| .trim() | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| // e.g. "2/F" -> "2F-", "4/F" -> "4F-" | |||
| // e.g. FLOOR_A -> FLOOR_A_KEY + "-", FLOOR_B -> FLOOR_B_KEY + "-" | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| return floorKey ? `${floorKey}-` : ""; | |||
| }, [fgPickOrders]); | |||
| @@ -796,8 +797,8 @@ const PickExecution: React.FC<Props> = ({ | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| if (floorKey === "2F") return "Label機 2F A+B"; | |||
| if (floorKey === "4F") return "Label機 4F 乾貨 C, D"; | |||
| if (floorKey === FLOOR_A_KEY) return LABEL_A; | |||
| if (floorKey === FLOOR_B_KEY) return LABEL_B; | |||
| return undefined; | |||
| }, [fgPickOrders]); | |||
| // Add these missing state variables after line 352 | |||
| @@ -1212,14 +1213,14 @@ const PickExecution: React.FC<Props> = ({ | |||
| // 将层级数据转换为平铺格式(用于表格显示) | |||
| const flatLotData: any[] = []; | |||
| // 2/F 與後端 store_id 一致時需按 itemOrder;避免 API 未走 2F 分支時畫面仍亂序 | |||
| // ${FLOOR_A} 與後端 store_id 一致時需按 itemOrder;避免 API 未走 ${FLOOR_A_KEY} 分支時畫面仍亂序 | |||
| const doFloorKey = String(hierarchicalData.fgInfo.storeId ?? "") | |||
| .trim() | |||
| .toUpperCase() | |||
| .replace(/\//g, "") | |||
| .replace(/\s/g, ""); | |||
| const pickOrderLinesForDisplay = | |||
| doFloorKey === "2F" | |||
| doFloorKey === FLOOR_A_KEY | |||
| ? [...(mergedPickOrder.pickOrderLines || [])].sort( | |||
| (a: any, b: any) => { | |||
| const ao = a.itemOrder != null ? Number(a.itemOrder) : 999999; | |||
| @@ -1,6 +1,7 @@ | |||
| // 新建檔案 | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Modal, | |||
| Box, | |||
| @@ -37,7 +38,7 @@ export type ReleasedDoPickListBridge = { | |||
| storeId?: string, | |||
| truck?: string, | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F` for Truck X supplier-floor split */ | |||
| /** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` for Truck X supplier-floor split */ | |||
| floor?: string | |||
| ) => Promise<ReleasedDoPickOrderListItem[]>; | |||
| /** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */ | |||
| @@ -47,7 +48,7 @@ export type ReleasedDoPickListBridge = { | |||
| truck?: string, | |||
| requiredDeliveryDate?: string, | |||
| releaseType?: string, | |||
| /** Optional `2F`/`4F` for Truck X supplier-floor split */ | |||
| /** Optional `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` for Truck X supplier-floor split */ | |||
| floor?: string | |||
| ) => Promise<ReleasedDoPickOrderListItem[]>; | |||
| assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>; | |||
| @@ -75,7 +76,7 @@ interface Props { | |||
| */ | |||
| filterRequiredDeliveryDate?: string; | |||
| /** | |||
| * Truck X only: `2F`/`4F` (or `2/F`/`4/F`) — filter by DO supplier preferred floor; storeId stays null. | |||
| * Truck X only: `${FLOOR_A_KEY}`/`${FLOOR_B_KEY}` (or `${FLOOR_A}`/`${FLOOR_B}`) — filter by DO supplier preferred floor; storeId stays null. | |||
| */ | |||
| truckXFloor?: string; | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { useEffect, useState } from "react"; | |||
| import { Box, Button, MenuItem, Stack, TextField, Typography } from "@mui/material"; | |||
| import DownloadIcon from "@mui/icons-material/Download"; | |||
| @@ -121,7 +122,7 @@ const TruckRoutingSummaryTab: React.FC<Props> = ({ onFiltersChange }) => { | |||
| <TextField | |||
| select | |||
| fullWidth | |||
| label="2/F 或 4/F" | |||
| label={FLOOR_A + " 或 " + FLOOR_B} | |||
| value={storeId} | |||
| onChange={(e) => onStoreChange(e.target.value)} | |||
| > | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, LOC_PREFIX } from "@/config/demoFloors"; | |||
| import React, { | |||
| useCallback, | |||
| useEffect, | |||
| @@ -77,7 +78,7 @@ export interface LotLabelPrintModalProps { | |||
| reminderText?: string; | |||
| /** 額外標題(顯示在最上方,reminderText 之下) */ | |||
| statusTitleText?: string; | |||
| /** 只顯示特定倉位前綴(例如 "2F-") */ | |||
| /** 只顯示特定倉位前綴(例如 FLOOR_A_KEY + "-") */ | |||
| warehouseCodePrefixFilter?: string; | |||
| /** 不顯示觸發視窗的批號(analysis.scanned) */ | |||
| hideTriggeredLot?: boolean; | |||
| @@ -346,9 +347,10 @@ const LotLabelPrintModal: React.FC<LotLabelPrintModalProps> = ({ | |||
| const filteredLots = useMemo(() => { | |||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | |||
| if (!prefix) return availableLots; | |||
| return availableLots.filter((lot) => | |||
| String(lot.warehouseCode ?? "").startsWith(prefix), | |||
| ); | |||
| return availableLots.filter((lot) => { | |||
| const code = String(lot.warehouseCode ?? ""); | |||
| return code.startsWith(prefix) || code.startsWith(LOC_PREFIX); | |||
| }); | |||
| }, [availableLots, warehouseCodePrefixFilter]); | |||
| const selectedPrinter = useMemo(() => { | |||
| @@ -1,4 +1,5 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import { | |||
| Box, | |||
| @@ -45,7 +46,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => { | |||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ planStart: today }); | |||
| type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT"; | |||
| type FloorFilter = "ALL" | typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "NO_LOT"; | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" }, | |||
| @@ -76,9 +77,9 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => { | |||
| paramName: "floor", | |||
| type: "select-labelled", | |||
| options: [ | |||
| { label: "2F", value: "2F" }, | |||
| { label: "3F", value: "3F" }, | |||
| { label: "4F", value: "4F" }, | |||
| { label: FLOOR_A_KEY, value: FLOOR_A_KEY }, | |||
| { label: FLOOR_D_KEY, value: FLOOR_D_KEY }, | |||
| { label: FLOOR_B_KEY, value: FLOOR_B_KEY }, | |||
| { label: t("No Lot"), value: "NO_LOT" }, | |||
| ], | |||
| }, | |||
| @@ -86,7 +87,7 @@ const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => { | |||
| const selectedFloor: FloorFilter = (() => { | |||
| const floor = String(searchQuery.floor || "ALL"); | |||
| if (floor === "2F" || floor === "3F" || floor === "4F" || floor === "NO_LOT") { | |||
| if (floor === FLOOR_A_KEY || floor === FLOOR_D_KEY || floor === FLOOR_B_KEY || floor === "NO_LOT") { | |||
| return floor; | |||
| } | |||
| return "ALL"; | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY, LABEL_A, LABEL_B } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -1017,17 +1018,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| [], | |||
| ); | |||
| const extractFloor = (lot: any): string => { | |||
| const raw = lot.routerRoute || lot.routerArea || lot.location || ""; | |||
| const match = raw.match(/^(\d+F?)/i) || raw.split("-")[0]; | |||
| return (match?.[1] || match || raw || "") | |||
| .toUpperCase() | |||
| .replace(/(\d)F?/i, "$1F"); | |||
| const raw = String(lot.routerRoute || lot.routerArea || lot.location || ""); | |||
| const head = raw.trim().toUpperCase().replace(/\//g, "").split("-")[0]; | |||
| if (head === "2" || head === FLOOR_A_KEY) return FLOOR_A_KEY; | |||
| if (head === "4" || head === FLOOR_B_KEY) return FLOOR_B_KEY; | |||
| if (head === "3" || head === FLOOR_D_KEY) return FLOOR_D_KEY; | |||
| if (head === "1" || head === FLOOR_C_KEY) return FLOOR_C_KEY; | |||
| return head; | |||
| }; | |||
| // 楼层排序权重:4F > 3F > 2F(数字越大越靠前) | |||
| // B/F, then D/F, then A/F. | |||
| const floorSortOrder = (floor: string): number => { | |||
| const n = parseInt(floor.replace(/\D/g, ""), 10); | |||
| return isNaN(n) ? 0 : n; | |||
| if (floor === FLOOR_B_KEY) return 4; | |||
| if (floor === FLOOR_D_KEY) return 3; | |||
| if (floor === FLOOR_A_KEY) return 2; | |||
| if (floor === FLOOR_C_KEY) return 1; | |||
| return 0; | |||
| }; | |||
| const combinedLotData = useMemo(() => { | |||
| const lots = getAllLotsFromHierarchical(jobOrderData); | |||
| @@ -1156,8 +1162,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| .toUpperCase() | |||
| .replace(/\s/g, ""); | |||
| const floorKey = storeId.replace(/\//g, ""); | |||
| if (floorKey === "2F") return "Label機 2F A+B"; | |||
| if (floorKey === "4F") return "Label機 4F 乾貨 C, D"; | |||
| if (floorKey === FLOOR_A_KEY) return LABEL_A; | |||
| if (floorKey === FLOOR_B_KEY) return LABEL_B; | |||
| return undefined; | |||
| }, [workbenchStoreId]); | |||
| @@ -3905,7 +3911,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("2F")} | |||
| onClick={() => handlePickRecord(FLOOR_A_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 2F | |||
| </Button> | |||
| @@ -3913,7 +3919,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("3F")} | |||
| onClick={() => handlePickRecord(FLOOR_D_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 3F | |||
| </Button> | |||
| @@ -3921,7 +3927,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("4F")} | |||
| onClick={() => handlePickRecord(FLOOR_B_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 4F | |||
| </Button> | |||
| @@ -1,5 +1,6 @@ | |||
| /* | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import { | |||
| Box, | |||
| @@ -28,7 +29,7 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | |||
| type PickOrderFilter = "all" | "drink" | "Powder_Mixture" | "other"; | |||
| const [filter, setFilter] = useState<PickOrderFilter>("all"); | |||
| type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT"; | |||
| type FloorFilter = "ALL" | typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "NO_LOT"; | |||
| const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL"); | |||
| const fetchPickOrders = useCallback(async () => { | |||
| setLoading(true); | |||
| @@ -124,25 +125,25 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| {t("Select All")} | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "2F" ? "contained" : "outlined"} | |||
| variant={floorFilter === FLOOR_A_KEY ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("2F")} | |||
| onClick={() => setFloorFilter(FLOOR_A_KEY)} | |||
| > | |||
| 2F | |||
| {FLOOR_A_KEY} | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "3F" ? "contained" : "outlined"} | |||
| variant={floorFilter === FLOOR_D_KEY ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("3F")} | |||
| onClick={() => setFloorFilter(FLOOR_D_KEY)} | |||
| > | |||
| 3F | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "4F" ? "contained" : "outlined"} | |||
| variant={floorFilter === FLOOR_B_KEY ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("4F")} | |||
| onClick={() => setFloorFilter(FLOOR_B_KEY)} | |||
| > | |||
| 4F | |||
| {FLOOR_B_KEY} | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "NO_LOT" ? "contained" : "outlined"} | |||
| @@ -1,4 +1,5 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { PickOrderResult } from "@/app/api/pickOrder"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useSearchParams } from "next/navigation"; | |||
| @@ -165,7 +166,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||
| } | |||
| }, [tabIndex]); | |||
| const handleAssignByStore = async (storeId: "2/F" | "4/F") => { | |||
| const handleAssignByStore = async (storeId: typeof FLOOR_A | typeof FLOOR_B) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_B, FLOOR_D } from "@/config/demoFloors"; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||
| import { | |||
| Alert, | |||
| @@ -31,7 +32,7 @@ import SafeApexCharts from "@/components/charts/SafeApexCharts"; | |||
| /** Day.js has no typed `dayjs.invalid()` factory; Invalid Date parses to an invalid Dayjs instance. */ | |||
| const DAYJS_INVALID = dayjs(new Date(Number.NaN)); | |||
| type FloorFilter = "all" | "2/F" | "3/F" | "4/F"; | |||
| type FloorFilter = "all" | typeof FLOOR_A | typeof FLOOR_D | typeof FLOOR_B; | |||
| type DailySummaryRow = { | |||
| date: string; | |||
| @@ -81,21 +82,21 @@ const addRecordQty = ( | |||
| const q3 = Number(record.plasticBoxCartonQty3f ?? 0); | |||
| const q4 = Number(record.plasticBoxCartonQty4f ?? 0); | |||
| if (selectedFloor === "all" || selectedFloor === "2/F") { | |||
| if (selectedFloor === "all" || selectedFloor === FLOOR_A) { | |||
| current.floor2F += q2; | |||
| } | |||
| if (selectedFloor === "all" || selectedFloor === "3/F") { | |||
| if (selectedFloor === "all" || selectedFloor === FLOOR_D) { | |||
| current.floor3F += q3; | |||
| } | |||
| if (selectedFloor === "all" || selectedFloor === "4/F") { | |||
| if (selectedFloor === "all" || selectedFloor === FLOOR_B) { | |||
| current.floor4F += q4; | |||
| } | |||
| if (selectedFloor === "all") { | |||
| current.total += q2 + q3 + q4; | |||
| } else if (selectedFloor === "2/F") { | |||
| } else if (selectedFloor === FLOOR_A) { | |||
| current.total += q2; | |||
| } else if (selectedFloor === "3/F") { | |||
| } else if (selectedFloor === FLOOR_D) { | |||
| current.total += q3; | |||
| } else { | |||
| current.total += q4; | |||
| @@ -198,9 +199,9 @@ const PlasticBoxCartonDashboardTab: React.FC = () => { | |||
| const chartSeries = useMemo( | |||
| () => [ | |||
| { name: "2/F", data: rows.map((row) => row.floor2F) }, | |||
| { name: "3/F", data: rows.map((row) => row.floor3F) }, | |||
| { name: "4/F", data: rows.map((row) => row.floor4F) }, | |||
| { name: FLOOR_A, data: rows.map((row) => row.floor2F) }, | |||
| { name: FLOOR_D, data: rows.map((row) => row.floor3F) }, | |||
| { name: FLOOR_B, data: rows.map((row) => row.floor4F) }, | |||
| { name: t("Total plastic box carton qty"), data: rows.map((row) => row.total) }, | |||
| ], | |||
| [rows, t], | |||
| @@ -351,17 +352,17 @@ const PlasticBoxCartonDashboardTab: React.FC = () => { | |||
| ["", "", "", "", ""], | |||
| [ | |||
| t("Date"), | |||
| t("2/F plastic box carton qty count"), | |||
| t("3/F plastic box carton qty count"), | |||
| t("4/F plastic box carton qty count"), | |||
| t(FLOOR_A + " plastic box carton qty count"), | |||
| t(FLOOR_D + " plastic box carton qty count"), | |||
| t(FLOOR_B + " plastic box carton qty count"), | |||
| t("Total plastic box carton qty"), | |||
| ], | |||
| ...dailyRows.map((row) => [row.date, row.floor2F, row.floor3F, row.floor4F, row.total]), | |||
| ["", "", "", "", ""], | |||
| [t("Summary"), "", "", "", ""], | |||
| [t("2/F plastic box carton qty count"), reportSummary.floor2F, "", "", ""], | |||
| [t("3/F plastic box carton qty count"), reportSummary.floor3F, "", "", ""], | |||
| [t("4/F plastic box carton qty count"), reportSummary.floor4F, "", "", ""], | |||
| [t(FLOOR_A + " plastic box carton qty count"), reportSummary.floor2F, "", "", ""], | |||
| [t(FLOOR_D + " plastic box carton qty count"), reportSummary.floor3F, "", "", ""], | |||
| [t(FLOOR_B + " plastic box carton qty count"), reportSummary.floor4F, "", "", ""], | |||
| [t("Total plastic box carton qty"), reportSummary.total, "", "", ""], | |||
| ]; | |||
| @@ -469,9 +470,9 @@ const PlasticBoxCartonDashboardTab: React.FC = () => { | |||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | |||
| > | |||
| <MenuItem value="all">{t("All")}</MenuItem> | |||
| <MenuItem value="2/F">2/F</MenuItem> | |||
| <MenuItem value="3/F">3/F</MenuItem> | |||
| <MenuItem value="4/F">4/F</MenuItem> | |||
| <MenuItem value={FLOOR_A}>{FLOOR_A}</MenuItem> | |||
| <MenuItem value={FLOOR_D}>{FLOOR_D}</MenuItem> | |||
| <MenuItem value={FLOOR_B}>{FLOOR_B}</MenuItem> | |||
| </TextField> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| @@ -492,19 +493,19 @@ const PlasticBoxCartonDashboardTab: React.FC = () => { | |||
| <Table size="small"> | |||
| <TableBody> | |||
| <TableRow> | |||
| <TableCell>{t("2/F plastic box carton qty count")}</TableCell> | |||
| <TableCell>{t(FLOOR_A + " plastic box carton qty count")}</TableCell> | |||
| <TableCell align="right"> | |||
| {summary.floor2F.toLocaleString("zh-HK")} | |||
| </TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell>{t("3/F plastic box carton qty count")}</TableCell> | |||
| <TableCell>{t(FLOOR_D + " plastic box carton qty count")}</TableCell> | |||
| <TableCell align="right"> | |||
| {summary.floor3F.toLocaleString("zh-HK")} | |||
| </TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell>{t("4/F plastic box carton qty count")}</TableCell> | |||
| <TableCell>{t(FLOOR_B + " plastic box carton qty count")}</TableCell> | |||
| <TableCell align="right"> | |||
| {summary.floor4F.toLocaleString("zh-HK")} | |||
| </TableCell> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -774,21 +775,21 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, "2F")} | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_A_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 2F | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, "3F")} | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_D_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 3F | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, "4F")} | |||
| onClick={() => handlePickRecord(jobOrderPickOrder, FLOOR_B_KEY)} | |||
| > | |||
| {t("Print Pick Record")} 4F | |||
| </Button> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -684,15 +685,22 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| return allLots; | |||
| }, []); | |||
| const extractFloor = (lot: any): string => { | |||
| const raw = lot.routerRoute || lot.routerArea || lot.location || ''; | |||
| const match = raw.match(/^(\d+F?)/i) || raw.split('-')[0]; | |||
| return (match?.[1] || match || raw || '').toUpperCase().replace(/(\d)F?/i, '$1F'); | |||
| const raw = String(lot.routerRoute || lot.routerArea || lot.location || ""); | |||
| const head = raw.trim().toUpperCase().replace(/\//g, "").split("-")[0]; | |||
| if (head === "2" || head === FLOOR_A_KEY) return FLOOR_A_KEY; | |||
| if (head === "4" || head === FLOOR_B_KEY) return FLOOR_B_KEY; | |||
| if (head === "3" || head === FLOOR_D_KEY) return FLOOR_D_KEY; | |||
| if (head === "1" || head === FLOOR_C_KEY) return FLOOR_C_KEY; | |||
| return head; | |||
| }; | |||
| // 楼层排序权重:4F > 3F > 2F(数字越大越靠前) | |||
| // B/F, then D/F, then A/F. | |||
| const floorSortOrder = (floor: string): number => { | |||
| const n = parseInt(floor.replace(/\D/g, ''), 10); | |||
| return isNaN(n) ? 0 : n; | |||
| if (floor === FLOOR_B_KEY) return 4; | |||
| if (floor === FLOOR_D_KEY) return 3; | |||
| if (floor === FLOOR_A_KEY) return 2; | |||
| if (floor === FLOOR_C_KEY) return 1; | |||
| return 0; | |||
| }; | |||
| const combinedLotData = useMemo(() => { | |||
| const lots = getAllLotsFromHierarchical(jobOrderData); | |||
| @@ -2613,7 +2621,7 @@ const sortedData = [...sourceData].sort((a, b) => { | |||
| const floorB = extractFloor(b); | |||
| const orderA = floorSortOrder(floorA); | |||
| const orderB = floorSortOrder(floorB); | |||
| if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F | |||
| if (orderA !== orderB) return orderB - orderA; // ${FLOOR_B_KEY}, ${FLOOR_D_KEY}, ${FLOOR_A_KEY} | |||
| // 同楼层再按 routerIndex、pickOrderCode、lotNo | |||
| const aIndex = a.routerIndex ?? 0; | |||
| const bIndex = b.routerIndex ?? 0; | |||
| @@ -1,10 +1,11 @@ | |||
| "use client"; | |||
| import { FLOOR_A, FLOOR_A_KEY, FLOOR_B, FLOOR_B_KEY, FLOOR_D, FLOOR_D_KEY } from "@/config/demoFloors"; | |||
| import Swal from "sweetalert2"; | |||
| import type { TFunction } from "i18next"; | |||
| import { fetchPickRecordPlasticBoxCartonQty } from "@/app/api/jo/actions"; | |||
| export type PickRecordFloor = "2F" | "3F" | "4F" | "ALL"; | |||
| export type PickRecordFloor = typeof FLOOR_A_KEY | typeof FLOOR_D_KEY | typeof FLOOR_B_KEY | "ALL"; | |||
| export interface AllFloorsPlasticBoxCartonQtyResult { | |||
| qty2f: number; | |||
| @@ -115,9 +116,9 @@ export async function promptAllFloorsPlasticBoxCartonQty( | |||
| title: t("Enter all floors plastic box carton qty"), | |||
| html: ` | |||
| <div style="display:flex;flex-direction:column;gap:10px;text-align:left;"> | |||
| ${buildFloorQtyInputRow("swal-qty-2f", t("2/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty2f)} | |||
| ${buildFloorQtyInputRow("swal-qty-3f", t("3/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty3f)} | |||
| ${buildFloorQtyInputRow("swal-qty-4f", t("4/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty4f)} | |||
| ${buildFloorQtyInputRow("swal-qty-2f", t(FLOOR_A + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty2f)} | |||
| ${buildFloorQtyInputRow("swal-qty-3f", t(FLOOR_D + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty3f)} | |||
| ${buildFloorQtyInputRow("swal-qty-4f", t(FLOOR_B + " plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty4f)} | |||
| </div> | |||
| `, | |||
| showCancelButton: true, | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY, FLOOR_C_KEY, FLOOR_D_KEY, LOC_HOLD, LOC_WIP } from "@/config/demoFloors"; | |||
| import React, { useState } from "react"; | |||
| import InfoOutlined from "@mui/icons-material/InfoOutlined"; | |||
| import { | |||
| @@ -33,17 +34,17 @@ const sections: { title: string; lines: string[] }[] = [ | |||
| { | |||
| title: "送貨揀貨的樓層", | |||
| lines: [ | |||
| "票上已指定 2F 或 4F:只建議該樓層的批次。", | |||
| "沒有指定樓層:供應商在「4F 供應商」清單就用 4F,否則用 2F。這兩份清單可以在「送貨單樓層(供應商)」修改。", | |||
| "放單時若供應商在 2F 或 4F 清單內,只建議該樓層;1F 的批次仍然可以建議。不在清單內的供應商不限制 2F / 4F。", | |||
| "送貨單不建議 3F。", | |||
| "票上已指定 " + FLOOR_A_KEY + " 或 " + FLOOR_B_KEY + ":只建議該樓層的批次。", | |||
| "沒有指定樓層:供應商在「" + FLOOR_B_KEY + " 供應商」清單就用 " + FLOOR_B_KEY + ",否則用 " + FLOOR_A_KEY + "。這兩份清單可以在「送貨單樓層(供應商)」修改。", | |||
| "放單時若供應商在 " + FLOOR_A_KEY + " 或 " + FLOOR_B_KEY + " 清單內,只建議該樓層;" + FLOOR_C_KEY + " 的批次仍然可以建議。不在清單內的供應商不限制 " + FLOOR_A_KEY + " / " + FLOOR_B_KEY + "。", | |||
| "送貨單不建議 " + FLOOR_D_KEY + "。", | |||
| ], | |||
| }, | |||
| { | |||
| title: "目前不建議的倉位", | |||
| lines: [ | |||
| "2F-W202-01-00", | |||
| "2F-W200-#A-00", | |||
| LOC_HOLD, | |||
| LOC_WIP, | |||
| "這兩個倉位寫在程式裡,畫面還不能改。", | |||
| ], | |||
| }, | |||
| @@ -51,8 +52,8 @@ const sections: { title: string; lines: string[] }[] = [ | |||
| title: "工單提料(和送貨揀貨不同)", | |||
| lines: [ | |||
| "同樣是先到期先出。", | |||
| "不建議 2F-W202-01-00。半成品會先試 2F-W200-#A-00。", | |||
| "工單可以使用 3F。", | |||
| "不建議 " + LOC_HOLD + "。半成品會先試 " + LOC_WIP + "。", | |||
| "工單可以使用 " + FLOOR_D_KEY + "。", | |||
| ], | |||
| }, | |||
| ]; | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import React, { | |||
| useCallback, | |||
| useEffect, | |||
| @@ -260,7 +261,7 @@ function summarizeLogisticsColumnStats(lanes: Lane[]): { | |||
| let count4F = 0; | |||
| for (const l of lanes) { | |||
| shopCount += l.shops.length; | |||
| if (normalizeStoreId(l.storeId) === "4F") count4F++; | |||
| if (normalizeStoreId(l.storeId) === FLOOR_B_KEY) count4F++; | |||
| else count2F++; | |||
| } | |||
| return { | |||
| @@ -516,7 +517,7 @@ function buildLaneFromPendingNewLane(p: PendingNewLane): Lane { | |||
| const code = String(payload.truckLanceCode || "").trim(); | |||
| const storeNorm = normalizeStoreId(payload.store_id); | |||
| const remarkRaw = | |||
| storeNorm === "4F" && | |||
| storeNorm === FLOOR_B_KEY && | |||
| payload.remark != null && | |||
| String(payload.remark).trim() !== "" | |||
| ? String(payload.remark).trim() | |||
| @@ -690,7 +691,7 @@ function buildLaneFromTruckRows( | |||
| phone: lanePhone, | |||
| plate: lanePlate, | |||
| startTime: startTime || "00:00:00", | |||
| storeId: storeId || "2F", | |||
| storeId: storeId || FLOOR_A_KEY, | |||
| remark, | |||
| shops, | |||
| }; | |||
| @@ -853,7 +854,7 @@ const RouteBoard: React.FC = () => { | |||
| "board", | |||
| ); | |||
| const [laneFilter, setLaneFilter] = useState<{ | |||
| floor: "all" | "2F" | "4F"; | |||
| floor: "all" | typeof FLOOR_A_KEY | typeof FLOOR_B_KEY; | |||
| query: string; | |||
| }>({ | |||
| floor: "all", | |||
| @@ -1100,7 +1101,7 @@ const RouteBoard: React.FC = () => { | |||
| /** 物流主檔 id;null = 未指定(至物流商管理指派) */ | |||
| logisticId: number | null; | |||
| startTime: string; | |||
| storeId: "2F" | "4F"; | |||
| storeId: typeof FLOOR_A_KEY | typeof FLOOR_B_KEY; | |||
| remark: string; | |||
| }; | |||
| @@ -1108,7 +1109,7 @@ const RouteBoard: React.FC = () => { | |||
| truckLanceCode: "", | |||
| logisticId: null, | |||
| startTime: "07:30", | |||
| storeId: "2F", | |||
| storeId: FLOOR_A_KEY, | |||
| remark: "", | |||
| }); | |||
| @@ -1201,7 +1202,7 @@ const RouteBoard: React.FC = () => { | |||
| const code = String(form.truckLanceCode || "").trim(); | |||
| const storeNorm = normalizeStoreId(form.storeId); | |||
| const remarkRaw = | |||
| storeNorm === "4F" && String(form.remark || "").trim() !== "" | |||
| storeNorm === FLOOR_B_KEY && String(form.remark || "").trim() !== "" | |||
| ? String(form.remark).trim() | |||
| : null; | |||
| const dep = parseTimeForBackend(form.startTime || "") || "00:00:00"; | |||
| @@ -1244,7 +1245,7 @@ const RouteBoard: React.FC = () => { | |||
| const code = String(form.truckLanceCode || "").trim(); | |||
| const storeNorm = normalizeStoreId(form.storeId); | |||
| const remarkRaw = | |||
| storeNorm === "4F" && String(form.remark || "").trim() !== "" | |||
| storeNorm === FLOOR_B_KEY && String(form.remark || "").trim() !== "" | |||
| ? String(form.remark).trim() | |||
| : null; | |||
| return { | |||
| @@ -2296,7 +2297,7 @@ const RouteBoard: React.FC = () => { | |||
| const code = String(newRouteForm.truckLanceCode || "").trim(); | |||
| const storeNorm = normalizeStoreId(newRouteForm.storeId); | |||
| const remarkRaw = | |||
| storeNorm === "4F" && String(newRouteForm.remark || "").trim() !== "" | |||
| storeNorm === FLOOR_B_KEY && String(newRouteForm.remark || "").trim() !== "" | |||
| ? String(newRouteForm.remark).trim() | |||
| : null; | |||
| const laneKey = encodeLaneId(code, remarkRaw); | |||
| @@ -2370,7 +2371,7 @@ const RouteBoard: React.FC = () => { | |||
| const seq = Number(newSeq) || 0; | |||
| const storeId = normalizeStoreId(lane.storeId); | |||
| const remark = | |||
| storeId === "4F" && | |||
| storeId === FLOOR_B_KEY && | |||
| lane.remark != null && | |||
| String(lane.remark).trim() !== "" | |||
| ? String(lane.remark).trim() | |||
| @@ -2678,7 +2679,7 @@ const RouteBoard: React.FC = () => { | |||
| departureTime: to.startTime, | |||
| storeId: to.storeId, | |||
| remark: | |||
| normalizeStoreId(to.storeId) === "4F" | |||
| normalizeStoreId(to.storeId) === FLOOR_B_KEY | |||
| ? to.remark != null && String(to.remark).trim() !== "" | |||
| ? String(to.remark).trim() | |||
| : null | |||
| @@ -2713,7 +2714,7 @@ const RouteBoard: React.FC = () => { | |||
| to.shops = inserted; | |||
| // IMPORTANT: Do NOT renumber all loadingSequence. | |||
| // loadingSequence can be duplicated intentionally (e.g. 4F grouping). | |||
| // loadingSequence can be duplicated intentionally (e.g. ${FLOOR_B_KEY} grouping). | |||
| // On drag/drop, only update the moved shop's loadingSequence so it joins the target group. | |||
| const newSeq = computeMovedLoadingSequence( | |||
| inserted, | |||
| @@ -3356,7 +3357,7 @@ const RouteBoard: React.FC = () => { | |||
| if (!lane) continue; | |||
| const sid = normalizeStoreId(lane.storeId); | |||
| const remark = | |||
| sid === "4F" && | |||
| sid === FLOOR_B_KEY && | |||
| lane.remark != null && | |||
| String(lane.remark).trim() !== "" | |||
| ? String(lane.remark).trim() | |||
| @@ -6085,15 +6086,15 @@ const RouteBoard: React.FC = () => { | |||
| onChange={(e) => | |||
| setNewRouteForm((p) => ({ | |||
| ...p, | |||
| storeId: e.target.value as "2F" | "4F", | |||
| storeId: e.target.value as typeof FLOOR_A_KEY | typeof FLOOR_B_KEY, | |||
| })) | |||
| } | |||
| > | |||
| <MenuItem value="2F">2F</MenuItem> | |||
| <MenuItem value="4F">4F</MenuItem> | |||
| <MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem> | |||
| <MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| {newRouteForm.storeId === "4F" && ( | |||
| {newRouteForm.storeId === FLOOR_B_KEY && ( | |||
| <TextField | |||
| size="small" | |||
| fullWidth | |||
| @@ -6683,8 +6684,8 @@ const RouteBoard: React.FC = () => { | |||
| } | |||
| > | |||
| <MenuItem value="all">{t("floor_all")}</MenuItem> | |||
| <MenuItem value="2F">2F</MenuItem> | |||
| <MenuItem value="4F">4F</MenuItem> | |||
| <MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem> | |||
| <MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <Stack | |||
| @@ -7263,7 +7264,7 @@ const RouteBoard: React.FC = () => { | |||
| /> | |||
| <Chip | |||
| size="small" | |||
| label={`2F · ${colStats.count2F}`} | |||
| label={`${FLOOR_A_KEY} · ${colStats.count2F}`} | |||
| sx={{ | |||
| height: 24, | |||
| fontWeight: 700, | |||
| @@ -7272,7 +7273,7 @@ const RouteBoard: React.FC = () => { | |||
| /> | |||
| <Chip | |||
| size="small" | |||
| label={`4F · ${colStats.count4F}`} | |||
| label={`${FLOOR_B_KEY} · ${colStats.count4F}`} | |||
| sx={{ | |||
| height: 24, | |||
| fontWeight: 700, | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -134,14 +135,14 @@ const Shop: React.FC = () => { | |||
| const loadingSeqNum = loadingSeq != null && loadingSeq !== undefined ? Number(loadingSeq) : null; | |||
| const hasLoadingSequence = loadingSeqNum !== null && !isNaN(loadingSeqNum) && loadingSeqNum !== 0; | |||
| // Check storeId: must exist and not be 0 (can be string "2F"/"4F" or number) | |||
| // Check storeId: must exist and not be 0 (can be string FLOOR_A_KEY/FLOOR_B_KEY or number) | |||
| // Actual field name in JSON is store_id (underscore, lowercase) | |||
| const storeId = (truck as any).store_id || (truck as any).storeId || (truck as any).Store_id; | |||
| let storeIdValid = false; | |||
| if (storeId != null && storeId !== undefined && storeId !== "") { | |||
| const storeIdStr = String(storeId).trim(); | |||
| // If it's "2F" or "4F", it's valid (not 0) | |||
| if (storeIdStr === "2F" || storeIdStr === "4F") { | |||
| // If it's FLOOR_A_KEY or FLOOR_B_KEY, it's valid (not 0) | |||
| if (storeIdStr === FLOOR_A_KEY || storeIdStr === FLOOR_B_KEY) { | |||
| storeIdValid = true; | |||
| } else { | |||
| const storeIdNum = Number(storeId); | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -274,9 +275,9 @@ const ShopDetail: React.FC = () => { | |||
| } | |||
| const lane = selectedTruckLane!; | |||
| const storeIdStr = normalizeStoreId(lane.storeId) || "2F"; | |||
| const storeIdStr = normalizeStoreId(lane.storeId) || FLOOR_A_KEY; | |||
| const remarkValue = | |||
| storeIdStr === "4F" && lane.remark != null && String(lane.remark).trim() !== "" | |||
| storeIdStr === FLOOR_B_KEY && lane.remark != null && String(lane.remark).trim() !== "" | |||
| ? String(lane.remark).trim() | |||
| : null; | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -56,7 +57,7 @@ const TruckLane: React.FC = () => { | |||
| const [newTruck, setNewTruck] = useState({ | |||
| truckLanceCode: "", | |||
| departureTime: "", | |||
| storeId: "2F", | |||
| storeId: FLOOR_A_KEY, | |||
| }); | |||
| const [saving, setSaving] = useState<boolean>(false); | |||
| const [snackbarOpen, setSnackbarOpen] = useState<boolean>(false); | |||
| @@ -192,7 +193,7 @@ const TruckLane: React.FC = () => { | |||
| setNewTruck({ | |||
| truckLanceCode: "", | |||
| departureTime: "", | |||
| storeId: "2F", | |||
| storeId: FLOOR_A_KEY, | |||
| }); | |||
| setAddDialogOpen(true); | |||
| setError(null); | |||
| @@ -203,7 +204,7 @@ const TruckLane: React.FC = () => { | |||
| setNewTruck({ | |||
| truckLanceCode: "", | |||
| departureTime: "", | |||
| storeId: "2F", | |||
| storeId: FLOOR_A_KEY, | |||
| }); | |||
| }; | |||
| @@ -373,8 +374,8 @@ const TruckLane: React.FC = () => { | |||
| }} | |||
| disabled={saving} | |||
| > | |||
| <MenuItem value="2F">2F</MenuItem> | |||
| <MenuItem value="4F">4F</MenuItem> | |||
| <MenuItem value={FLOOR_A_KEY}>{FLOOR_A_KEY}</MenuItem> | |||
| <MenuItem value={FLOOR_B_KEY}>{FLOOR_B_KEY}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -291,9 +292,9 @@ const TruckLaneDetail: React.FC = () => { | |||
| await Promise.all( | |||
| withIds.map((row) => { | |||
| const storeIdStr = normalizeStoreId(row.storeId) || "2F"; | |||
| const storeIdStr = normalizeStoreId(row.storeId) || FLOOR_A_KEY; | |||
| let remarkValue: string | null = null; | |||
| if (storeIdStr === "4F" && row.remark != null && String(row.remark).trim() !== "") { | |||
| if (storeIdStr === FLOOR_B_KEY && row.remark != null && String(row.remark).trim() !== "") { | |||
| remarkValue = String(row.remark).trim(); | |||
| } | |||
| return updateTruckLaneClient({ | |||
| @@ -579,8 +580,8 @@ const TruckLaneDetail: React.FC = () => { | |||
| try { | |||
| const storeIdValue = truckData.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : "2F"; | |||
| const displayStoreId = normalizeStoreId(storeIdValue) || "2F"; | |||
| : String(truckData.storeId)) : FLOOR_A_KEY; | |||
| const displayStoreId = normalizeStoreId(storeIdValue) || FLOOR_A_KEY; | |||
| let departureTimeStr = ""; | |||
| if (truckData.departureTime) { | |||
| @@ -597,8 +598,8 @@ const TruckLaneDetail: React.FC = () => { | |||
| ); | |||
| const shopIdValue = foundShop ? foundShop.id : null; | |||
| // Get remark - only if storeId is "4F" | |||
| const remarkValue = displayStoreId === "4F" ? (newShop.remark?.trim() || null) : null; | |||
| // Get remark - only if storeId is FLOOR_B_KEY | |||
| const remarkValue = displayStoreId === FLOOR_B_KEY ? (newShop.remark?.trim() || null) : null; | |||
| // Check if there's an "Unassign" row for this truck lane that should be replaced | |||
| let unassignTruck: Truck | null = null; | |||
| @@ -909,7 +910,7 @@ const TruckLaneDetail: React.FC = () => { | |||
| {(editingRowIndex === index || massEditing) ? ( | |||
| (() => { | |||
| const storeIdValue = truckData.storeId ? String(truckData.storeId) : null; | |||
| const isEditable = normalizeStoreId(storeIdValue) === "4F"; | |||
| const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY; | |||
| return ( | |||
| <Autocomplete | |||
| freeSolo | |||
| @@ -1096,7 +1097,7 @@ const TruckLaneDetail: React.FC = () => { | |||
| const storeIdValue = truckData?.storeId ? (typeof truckData.storeId === 'string' || truckData.storeId instanceof String | |||
| ? String(truckData.storeId) | |||
| : String(truckData.storeId)) : null; | |||
| const isEditable = normalizeStoreId(storeIdValue) === "4F"; | |||
| const isEditable = normalizeStoreId(storeIdValue) === FLOOR_B_KEY; | |||
| return isEditable ? ( | |||
| <Grid item xs={12}> | |||
| <Autocomplete | |||
| @@ -2,6 +2,7 @@ | |||
| * 車線看板警示:Rule1(4F 同店跨線 weekday)、Rule2(非 4F 同店跨線發車時分)。 | |||
| * 規則細節見 `README-ROUTE-BOARD-WARNINGS.md`。 | |||
| */ | |||
| import { FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| const LANE_KEY_SEP = "|"; | |||
| @@ -168,7 +169,7 @@ function rowToLaneRef(row: TruckLaneWarningInputRow): TruckLaneWarningLaneRef { | |||
| const storeNorm = normalizeStoreId(row.storeId); | |||
| const depDisp = departureTimeToHHmmKey(row.departureTime); | |||
| const weekday = | |||
| storeNorm === "4F" | |||
| storeNorm === FLOOR_B_KEY | |||
| ? parseWeekdayFromTruckLanceCode(row.truckLanceCode) | |||
| : null; | |||
| return { | |||
| @@ -227,7 +228,7 @@ export function computeTruckLaneWarnings( | |||
| >(); | |||
| for (const row of active) { | |||
| const store = normalizeStoreId(row.storeId); | |||
| if (store !== "4F") continue; | |||
| if (store !== FLOOR_B_KEY) continue; | |||
| const wd = parseWeekdayFromTruckLanceCode(row.truckLanceCode); | |||
| if (!wd) { | |||
| weekdayParseFailures.push({ | |||
| @@ -258,7 +259,7 @@ export function computeTruckLaneWarnings( | |||
| const sampleRow = active.find( | |||
| (r) => | |||
| shopKOf(r) === shopK && | |||
| normalizeStoreId(r.storeId) === "4F" && | |||
| normalizeStoreId(r.storeId) === FLOOR_B_KEY && | |||
| parseWeekdayFromTruckLanceCode(r.truckLanceCode) === wd, | |||
| ); | |||
| const laneKeyList: string[] = Array.from(laneKeys); | |||
| @@ -278,7 +279,7 @@ export function computeTruckLaneWarnings( | |||
| const r2Map = new Map<string, Map<string, Set<string>>>(); | |||
| for (const row of active) { | |||
| const store = normalizeStoreId(row.storeId); | |||
| if (store === "4F") continue; | |||
| if (store === FLOOR_B_KEY) continue; | |||
| const hhmm = departureTimeToHHmmKey(row.departureTime); | |||
| if (!hhmm) continue; | |||
| const shopK = shopKOf(row); | |||
| @@ -302,7 +303,7 @@ export function computeTruckLaneWarnings( | |||
| const sampleRow = active.find( | |||
| (r) => | |||
| shopKOf(r) === shopK && | |||
| normalizeStoreId(r.storeId) !== "4F" && | |||
| normalizeStoreId(r.storeId) !== FLOOR_B_KEY && | |||
| departureTimeToHHmmKey(r.departureTime) === hhmm, | |||
| ); | |||
| const laneKeyList: string[] = Array.from(laneKeys); | |||
| @@ -1,3 +1,4 @@ | |||
| import { FLOOR_A_KEY } from "@/config/demoFloors"; | |||
| import type { RouteLaneImportPreviewRow } from "@/app/api/shop/client"; | |||
| import type { Truck } from "@/app/api/shop/actions"; | |||
| import { normalizeStoreId } from "@/app/utils/formatUtil"; | |||
| @@ -118,7 +119,7 @@ export function mergeImportPreviewIntoLanes< | |||
| String(previewRows[0].remark).trim() !== "" | |||
| ? String(previewRows[0].remark).trim() | |||
| : null; | |||
| const storeId = normalizeStoreId(previewRows[0]?.storeId ?? "2F"); | |||
| const storeId = normalizeStoreId(previewRows[0]?.storeId ?? FLOOR_A_KEY); | |||
| const startTime = parseTimeForPreview(previewRows[0]?.departureTime ?? ""); | |||
| const existing = laneMap.get(laneId); | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -252,7 +253,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); | |||
| const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); | |||
| const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>([FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| const [searchSectionDescription, setSearchSectionDescription] = useState<string>("All"); | |||
| const [searchStockTakeSession, setSearchStockTakeSession] = useState<string>(""); | |||
| const [searchItemKeyword, setSearchItemKeyword] = useState<string>(""); | |||
| @@ -373,7 +374,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| .then((sections) => { | |||
| const descSet = new Set<string>(); | |||
| const sectionSet = new Set<string>(); | |||
| const storeSet = new Set<string>(["2F", "4F"]); | |||
| const storeSet = new Set<string>([FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| sections.forEach((s) => { | |||
| const section = s.stockTakeSection?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| @@ -1375,7 +1376,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| setStoreIdOptions((prev) => { | |||
| const storeSet = new Set<string>([...prev, "2F", "4F"]); | |||
| const storeSet = new Set<string>([...prev, FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| inventoryLotDetails.forEach((item) => { | |||
| const storeId = item.storeId?.trim(); | |||
| if (storeId) storeSet.add(storeId); | |||
| @@ -1,5 +1,6 @@ | |||
| "use client"; | |||
| import { FLOOR_A_KEY, FLOOR_B_KEY } from "@/config/demoFloors"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| @@ -173,7 +174,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set()); | |||
| const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]); | |||
| const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["2F", "4F"]); | |||
| const [storeIdOptions, setStoreIdOptions] = useState<string[]>([FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| const statusOptions = ["pending", "stockTaking", "approving", "completed"]; | |||
| @@ -605,7 +606,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| setStockTakeSectionRows(Array.isArray(sections) ? sections : []); | |||
| const descSet = new Set<string>(); | |||
| const sectionSet = new Set<string>(); | |||
| const storeIdSet = new Set<string>(["2F", "4F"]); | |||
| const storeIdSet = new Set<string>([FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| sections.forEach((s) => { | |||
| const section = s.stockTakeSection?.trim(); | |||
| if (section) sectionSet.add(section); | |||
| @@ -637,7 +638,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| }, [stockTakeSessions]); | |||
| useEffect(() => { | |||
| setStoreIdOptions((prev) => { | |||
| const storeIdSet = new Set<string>([...prev, "2F", "4F"]); | |||
| const storeIdSet = new Set<string>([...prev, FLOOR_A_KEY, FLOOR_B_KEY]); | |||
| stockTakeSessions.forEach((item) => { | |||
| const storeId = item.storeId?.trim(); | |||
| if (storeId) storeIdSet.add(storeId); | |||
| @@ -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"; | |||
| @@ -1,8 +1,9 @@ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.0 | 2026-08-03 */ | |||
| export type FieldType = 'date' | 'text' | 'select' | 'number' | 'checkbox'; | |||
| import { FLOOR_A, FLOOR_B } from "@/config/demoFloors"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| export type FieldType = 'date' | 'text' | 'select' | 'number' | 'checkbox'; | |||
| export interface ReportField { | |||
| label: string; | |||
| name: string; | |||
| @@ -62,13 +63,13 @@ const asyncItemCodeField = ( | |||
| const floorOptionsAll: ReportField["options"] = [ | |||
| { label: "全部", value: "All" }, | |||
| { label: "2/F", value: "2/F" }, | |||
| { label: "4/F", value: "4/F" }, | |||
| { label: FLOOR_A, value: FLOOR_A }, | |||
| { label: FLOOR_B, value: FLOOR_B }, | |||
| ]; | |||
| const floorOptions: ReportField["options"] = [ | |||
| { label: "2/F", value: "2/F" }, | |||
| { label: "4/F", value: "4/F" }, | |||
| { label: FLOOR_A, value: FLOOR_A }, | |||
| { label: FLOOR_B, value: FLOOR_B }, | |||
| ]; | |||
| const pasteItemCodeField = (): ReportField => ({ | |||