diff --git a/.gitignore b/.gitignore index 9cc9553..c8686b9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ next-env.d.ts .vscode +# Cursor (local-only rules) +.cursor/rules/local/ + #fpsms.zip fpsms.zip diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index f62ca9b..cc020d8 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -9,8 +9,6 @@ import { SetupAxiosInterceptors } from "@/app/(main)/axios/axiosInstance"; import { UploadProvider } from "@/components/UploadProvider/UploadProvider"; import SessionProviderWrapper from "@/components/SessionProviderWrapper/SessionProviderWrapper"; import QrCodeScannerProvider from "@/components/QrCodeScannerProvider/QrCodeScannerProvider"; -import DevicePresenceReporterHost from "@/components/DevicePresence/DevicePresenceReporterHost"; -import { isMonitoringEnabled } from "@/config/monitoring"; import { I18nProvider } from "@/i18n"; import "src/app/global.css"; export default async function MainLayout({ @@ -35,7 +33,6 @@ export default async function MainLayout({ return ( - {isMonitoringEnabled && } {/* */} diff --git a/src/components/PoWorkbench/PoWorkbenchDetailsPlaceholder.tsx b/src/components/PoWorkbench/PoWorkbenchDetailsPlaceholder.tsx deleted file mode 100644 index ae3bd98..0000000 --- a/src/components/PoWorkbench/PoWorkbenchDetailsPlaceholder.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client"; - -import Typography from "@mui/material/Typography"; -import { useTranslation } from "react-i18next"; - -/** Right-column body placeholder until PO detail is wired into the workbench. */ -export default function PoWorkbenchDetailsPlaceholder() { - const { t } = useTranslation("poWorkbench"); - - return ( - - {t("detailsPlaceholder.content")} - - ); -} diff --git a/src/components/PoWorkbench/PoWorkbenchShell.tsx b/src/components/PoWorkbench/PoWorkbenchShell.tsx index d05488f..e97f1ba 100644 --- a/src/components/PoWorkbench/PoWorkbenchShell.tsx +++ b/src/components/PoWorkbench/PoWorkbenchShell.tsx @@ -1,5 +1,19 @@ "use client"; +import PoWorkbenchDetailsGrid from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGrid"; +import PoWorkbenchDetailsHeader from "@/components/PoWorkbench/details/PoWorkbenchDetailsHeader"; +import { + PO_WORKBENCH_GRID_TEMPLATE_COLUMNS, + PO_WORKBENCH_GRID_TEMPLATE_ROWS, +} from "@/components/PoWorkbench/layout/poWorkbenchShellLayout"; +import PoWorkbenchRegion from "@/components/PoWorkbench/layout/PoWorkbenchRegion"; +import PoWorkbenchLeftPane from "@/components/PoWorkbench/search/PoWorkbenchLeftPane"; +import PoWorkbenchSearchCriteriaBar from "@/components/PoWorkbench/search/PoWorkbenchSearchCriteriaBar"; +import { + createDefaultAdvancedFilters, + type PoWorkbenchAdvancedFilters, +} from "@/components/PoWorkbench/types"; +import { usePoWorkbenchListSearch } from "@/components/PoWorkbench/search/usePoWorkbenchListSearch"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; @@ -8,20 +22,6 @@ import { useMediaQuery, useTheme } from "@mui/material"; import type { Theme } from "@mui/material/styles"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - WORKBENCH_GRID_TEMPLATE_COLUMNS, - WORKBENCH_GRID_TEMPLATE_ROWS, -} from "@/components/PoWorkbench/mock/workbenchMockData"; -import PoWorkbenchDetailsHeader from "@/components/PoWorkbench/PoWorkbenchDetailsHeader"; -import PoWorkbenchDetailsPlaceholder from "@/components/PoWorkbench/PoWorkbenchDetailsPlaceholder"; -import PoWorkbenchRegion from "@/components/PoWorkbench/PoWorkbenchRegion"; -import PoWorkbenchSearchCriteriaBar from "@/components/PoWorkbench/PoWorkbenchSearchCriteriaBar"; -import PoWorkbenchLeftPane from "@/components/PoWorkbench/PoWorkbenchLeftPane"; -import { - createDefaultAdvancedFilters, - type PoWorkbenchAdvancedFilters, -} from "@/components/PoWorkbench/types"; -import { usePoWorkbenchListSearch } from "@/components/PoWorkbench/usePoWorkbenchListSearch"; const ROOT_SHELL_SX = { alignSelf: "stretch", @@ -49,8 +49,8 @@ const compactStackSx = { const gridInnerSx = { display: "grid", - gridTemplateColumns: WORKBENCH_GRID_TEMPLATE_COLUMNS, - gridTemplateRows: WORKBENCH_GRID_TEMPLATE_ROWS, + gridTemplateColumns: PO_WORKBENCH_GRID_TEMPLATE_COLUMNS, + gridTemplateRows: PO_WORKBENCH_GRID_TEMPLATE_ROWS, gap: "1px", width: "100%", height: "100%", @@ -72,8 +72,8 @@ const compactBackBarSx = { } as const; /** - * Root layout for PO Workbench: a 2×2 CSS Grid (md+) or compact list/detail stack (below md). - * List data comes from `/po/list` (same API as legacy Po search); logic lives in `usePoWorkbenchListSearch`. + * Root layout for PO Workbench: 2×2 CSS grid (md+) or compact list/detail stack. + * List data from `/po/list` via `usePoWorkbenchListSearch`. */ export default function PoWorkbenchShell() { const theme = useTheme(); @@ -176,7 +176,7 @@ export default function PoWorkbenchShell() { const detailsHeader = ( ); - const detailsBody = ; + const detailsBody = ; return ( diff --git a/src/components/PoWorkbench/README.md b/src/components/PoWorkbench/README.md new file mode 100644 index 0000000..a258620 --- /dev/null +++ b/src/components/PoWorkbench/README.md @@ -0,0 +1,41 @@ +# PO Workbench module + +Purchase order receiving workbench at `/po/workbench`. + +## Public API + +Only **`PoWorkbenchShell`** is imported from outside this folder (via `PoWorkbenchPageClient`). Do not import subfolders from other routes. + +## Layout (desktop) + +``` +searchCriteria | detailsHeader +searchResults | details (form + line grid) +``` + +- **layout/** — `PoWorkbenchShell` 2×2 grid tokens and `PoWorkbenchRegion` pane chrome. +- **search/** — Criteria bar, advanced filters, PO list, `/po/list` hook and mappers. +- **details/** — Selected PO summary header. +- **detailsGrid/** — Receipt form and four-column line grid (mock lines until detail API). +- **shared/** — Result summary, receive status chip, shared icon/typography tokens. +- **mock/** — Optional local list fixtures (`PO_WORKBENCH_LIST_MOCK_ROWS`); not used by production shell. + +## Naming + +| Kind | Pattern | Example | +|------|---------|---------| +| Component | `PoWorkbench` + PascalCase | `PoWorkbenchDetailsGrid.tsx` | +| Hook | `usePoWorkbench` + camelCase | `usePoWorkbenchListSearch.ts` | +| Helper / layout | `poWorkbench` + camelCase file | `poWorkbenchDetailsGridLayout.ts` | +| Type | `PoWorkbench` + domain | `PoWorkbenchListRow` | +| Constant | `PO_WORKBENCH_*` / `DETAILS_GRID_*` | `PO_WORKBENCH_GRID_TEMPLATE_COLUMNS` | + +## Data flow + +1. User filters → `usePoWorkbenchListSearch` → `GET /po/list` → `PoWorkbenchListRow[]`. +2. Selection → `PoWorkbenchDetailsHeader` + `PoWorkbenchDetailsGrid`. +3. Detail lines: `PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS` (replace with API later). + +## Comments + +Source comments are in **English**. UI strings use i18n (`poWorkbench`, `purchaseOrder` for status labels). diff --git a/src/components/PoWorkbench/PoWorkbenchDetailsHeader.tsx b/src/components/PoWorkbench/details/PoWorkbenchDetailsHeader.tsx similarity index 86% rename from src/components/PoWorkbench/PoWorkbenchDetailsHeader.tsx rename to src/components/PoWorkbench/details/PoWorkbenchDetailsHeader.tsx index 8b9d540..135fb4c 100644 --- a/src/components/PoWorkbench/PoWorkbenchDetailsHeader.tsx +++ b/src/components/PoWorkbench/details/PoWorkbenchDetailsHeader.tsx @@ -2,8 +2,8 @@ import type { PoWorkbenchListRow } from "@/components/PoWorkbench/types"; import Box from "@mui/material/Box"; -import PoWorkbenchDetailsHeaderSkeleton from "@/components/PoWorkbench/PoWorkbenchDetailsHeaderSkeleton"; -import WorkbenchResultSummary from "@/components/PoWorkbench/WorkbenchResultSummary"; +import PoWorkbenchDetailsHeaderSkeleton from "@/components/PoWorkbench/details/PoWorkbenchDetailsHeaderSkeleton"; +import PoWorkbenchResultSummary from "@/components/PoWorkbench/shared/PoWorkbenchResultSummary"; const DETAILS_HEADER_ROOT_SX = { flexShrink: 0, @@ -48,7 +48,7 @@ export default function PoWorkbenchDetailsHeader({ return ( - + ); diff --git a/src/components/PoWorkbench/PoWorkbenchDetailsHeaderSkeleton.tsx b/src/components/PoWorkbench/details/PoWorkbenchDetailsHeaderSkeleton.tsx similarity index 96% rename from src/components/PoWorkbench/PoWorkbenchDetailsHeaderSkeleton.tsx rename to src/components/PoWorkbench/details/PoWorkbenchDetailsHeaderSkeleton.tsx index 0b3c8f7..b59a321 100644 --- a/src/components/PoWorkbench/PoWorkbenchDetailsHeaderSkeleton.tsx +++ b/src/components/PoWorkbench/details/PoWorkbenchDetailsHeaderSkeleton.tsx @@ -6,7 +6,7 @@ import { useMediaQuery, useTheme } from "@mui/material"; import type { SxProps, Theme } from "@mui/material/styles"; /** - * Placeholder for {@link WorkbenchResultSummary} `layout="header"`: left PO (body1) + supplier (body2); + * Placeholder for {@link PoWorkbenchResultSummary} `layout="header"`: left PO (body1) + supplier (body2); * right chips + two date rows (icon + body2, matching the live header). */ export default function PoWorkbenchDetailsHeaderSkeleton() { diff --git a/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGrid.tsx b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGrid.tsx new file mode 100644 index 0000000..92055cb --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGrid.tsx @@ -0,0 +1,191 @@ +"use client"; + +import PoWorkbenchDetailsGridExpectedQtyCell from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridExpectedQtyCell"; +import PoWorkbenchDetailsGridReceivedQtyCell from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridReceivedQtyCell"; +import PoWorkbenchDetailsGridItemCell from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridItemCell"; +import { PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS } from "@/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridMock"; +import { + DETAILS_GRID_ROOT_SX, + DETAILS_GRID_ROW_SX, + DETAILS_GRID_SCROLL_HOST_SX, + detailsGridBodyCellSx, + detailsGridHeaderCellSx, +} from "@/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridLayout"; +import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; +import LocalShippingIcon from "@mui/icons-material/LocalShipping"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import dayjs, { Dayjs } from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +/** Matches other PoWorkbench inputs: secondary gray hint text. */ +const FORM_TEXT_FIELD_PLACEHOLDER_SX = { + "& .MuiInputBase-input::placeholder": { + color: "text.secondary", + opacity: 1, + }, +} as const; + +export default function PoWorkbenchDetailsGrid() { + const { t } = useTranslation("poWorkbench"); + const [receiptDate, setReceiptDate] = useState(dayjs()); + + const pendingCell = t("detailsGrid.columnPending"); + const lastRowIndex = PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS.length - 1; + + return ( + + + + + + + + {t("detailsGrid.form.deliveryNoteNo")} + + + + + + + + + {t("detailsGrid.form.receiptDate")} + + + + setReceiptDate(next)} + slotProps={{ + textField: { + size: "small", + fullWidth: true, + variant: "outlined", + }, + }} + /> + + + + + + + + + + {t("detailsGrid.columns.itemInfo")} + + + {t("detailsGrid.columns.expectedQty")} + + + {t("detailsGrid.columns.receivedQty")} + + + {t("detailsGrid.columns.inputArea")} + + + + {PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS.map((row, index) => { + const isLastRow = index === lastRowIndex; + + return ( + + + + + + + + + + + + {pendingCell} + + + ); + })} + + + + + ); +} diff --git a/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridExpectedQtyCell.tsx b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridExpectedQtyCell.tsx new file mode 100644 index 0000000..cd5f351 --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridExpectedQtyCell.tsx @@ -0,0 +1,20 @@ +"use client"; + +import PoWorkbenchDetailsGridQtyUnitBlock from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridQtyUnitBlock"; +import type { PoWorkbenchDetailsGridLineRow } from "@/components/PoWorkbench/types"; + +interface PoWorkbenchDetailsGridExpectedQtyCellProps { + row: PoWorkbenchDetailsGridLineRow; +} + +/** Expected qty column: large centered qty + purchase-unit badge. */ +export default function PoWorkbenchDetailsGridExpectedQtyCell({ + row, +}: PoWorkbenchDetailsGridExpectedQtyCellProps) { + return ( + + ); +} diff --git a/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridItemCell.tsx b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridItemCell.tsx new file mode 100644 index 0000000..0bd8420 --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridItemCell.tsx @@ -0,0 +1,129 @@ +"use client"; + +import type { PoWorkbenchDetailsGridLineRow } from "@/components/PoWorkbench/types"; +import { PO_WORKBENCH_META_ICON_SX } from "@/components/PoWorkbench/shared/poWorkbenchSharedStyles"; +import PoWorkbenchReceiveStatusChip from "@/components/PoWorkbench/shared/PoWorkbenchReceiveStatusChip"; +import Inventory2Icon from "@mui/icons-material/Inventory2"; +import LocationOnIcon from "@mui/icons-material/LocationOn"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +const SINGLE_LINE_CLAMP_SX = { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + minWidth: 0, +} as const; + +const WRAP_TEXT_SX = { + minWidth: 0, + overflowWrap: "break-word", + wordBreak: "break-word", +} as const; + +const META_LINE_TEXT_SX = { + fontSize: "0.8125rem", + lineHeight: 1.4, +} as const; + +const META_ICON_SLOT_SX = { + ...META_LINE_TEXT_SX, + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + width: 16, + height: "1.45em", +} as const; + +const ITEM_CELL_ROOT_SX = { + display: "flex", + flexDirection: "column", + gap: 0.375, + width: "100%", + minWidth: 0, + boxSizing: "border-box", +} as const; + +interface ItemMetaIconRowProps { + icon: ReactNode; + children: ReactNode; +} + +function ItemMetaIconRow({ icon, children }: ItemMetaIconRowProps) { + return ( + + {icon} + {children} + + ); +} + +interface PoWorkbenchDetailsGridItemCellProps { + row: PoWorkbenchDetailsGridLineRow; +} + +/** Row 1: name. Rows 2–3: code/category, location. Row 4: status chip. */ +export default function PoWorkbenchDetailsGridItemCell({ + row, +}: PoWorkbenchDetailsGridItemCellProps) { + const { t } = useTranslation("poWorkbench"); + + const categoryLabel = t(`detailsGrid.itemCategory.${row.category}`); + + return ( + + + {row.itemName} + + + }> + + {`${row.itemCode} · ${categoryLabel}`} + + + + }> + + {row.storageLocation} + + + + + + + + ); +} diff --git a/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridQtyUnitBlock.tsx b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridQtyUnitBlock.tsx new file mode 100644 index 0000000..09d1a9c --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridQtyUnitBlock.tsx @@ -0,0 +1,141 @@ +"use client"; + +import type { PoWorkbenchReceivedQtyTone } from "@/components/PoWorkbench/detailsGrid/poWorkbenchReceivedQtyTone"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import type { SxProps, Theme } from "@mui/material/styles"; + +/** Shared row height so qty digits align across expected / received columns. */ +export const DETAILS_GRID_QTY_VALUE_ROW_MIN_HEIGHT = "1.65rem"; + +export const DETAILS_GRID_QTY_VALUE_FONT_SIZE = "1.375rem"; + +const QTY_VALUE_ROW_SX = { + minHeight: DETAILS_GRID_QTY_VALUE_ROW_MIN_HEIGHT, + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", +} as const; + +const QTY_VALUE_SX = { + fontSize: DETAILS_GRID_QTY_VALUE_FONT_SIZE, + fontWeight: 700, + lineHeight: 1.2, + textAlign: "center", + color: "text.primary", + fontVariantNumeric: "tabular-nums", +} as const; + +function qtyValueColorSx( + tone?: PoWorkbenchReceivedQtyTone, +): SxProps { + if (!tone) return {}; + if (tone === "complete") return { color: "success.main" }; + if (tone === "variance") return { color: "warning.main" }; + return { color: "error.main" }; +} + +const UNIT_BADGE_SX = (theme: import("@mui/material/styles").Theme) => ({ + width: "100%", + maxWidth: "100%", + minWidth: 0, + px: 0.875, + py: 0.625, + borderRadius: 0.75, + textAlign: "center", + bgcolor: theme.palette.mode === "dark" ? "grey.800" : "grey.100", + border: 1, + borderColor: "divider", +}); + +const UNIT_LABEL_WRAP_SX = { + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + columnGap: 0, + rowGap: 0.25, + width: "100%", + minWidth: 0, +} as const; + +const UNIT_SEGMENT_SX = { + fontSize: "0.8125rem", + lineHeight: 1.4, + color: "text.primary", + whiteSpace: "nowrap", +} as const; + +const UNIT_LABEL_PLAIN_SX = { + ...UNIT_SEGMENT_SX, + whiteSpace: "normal", + textAlign: "center", + overflowWrap: "break-word", + wordBreak: "break-word", + width: "100%", +} as const; + +/** Split at `X` / `×` so tiers stay together (e.g. `150克`, `100包`). */ +function splitPurchaseUnitLabel(label: string): string[] { + return label.split(/X|×/i).filter((part) => part.length > 0); +} + +function UnitLabelContent({ unitLabel }: { unitLabel: string }) { + const segments = splitPurchaseUnitLabel(unitLabel); + + if (segments.length <= 1) { + return ( + + {unitLabel} + + ); + } + + return ( + + {segments.map((segment, index) => ( + + {index === 0 ? segment : `X${segment}`} + + ))} + + ); +} + +function formatQty(qty: number): string { + return qty.toLocaleString(); +} + +export interface PoWorkbenchDetailsGridQtyUnitBlockProps { + qty: number; + unitLabel: string; + /** Purchase-side qty color in received column (green = met/over, yellow = under, red = none). */ + purchaseQtyTone?: PoWorkbenchReceivedQtyTone; +} + +/** Centered qty with purchase / inventory unit badge below. */ +export default function PoWorkbenchDetailsGridQtyUnitBlock({ + qty, + unitLabel, + purchaseQtyTone, +}: PoWorkbenchDetailsGridQtyUnitBlockProps) { + return ( + + + + {formatQty(qty)} + + + + + + + ); +} diff --git a/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridReceivedQtyCell.tsx b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridReceivedQtyCell.tsx new file mode 100644 index 0000000..748bcaf --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridReceivedQtyCell.tsx @@ -0,0 +1,65 @@ +"use client"; + +import PoWorkbenchDetailsGridQtyUnitBlock, { + DETAILS_GRID_QTY_VALUE_FONT_SIZE, + DETAILS_GRID_QTY_VALUE_ROW_MIN_HEIGHT, +} from "@/components/PoWorkbench/detailsGrid/PoWorkbenchDetailsGridQtyUnitBlock"; +import { getPoWorkbenchReceivedQtyTone } from "@/components/PoWorkbench/detailsGrid/poWorkbenchReceivedQtyTone"; +import type { PoWorkbenchDetailsGridLineRow } from "@/components/PoWorkbench/types"; +import CompareArrowsIcon from "@mui/icons-material/CompareArrows"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; + +const QTY_ARROW_SLOT_SX = { + flexShrink: 0, + minHeight: DETAILS_GRID_QTY_VALUE_ROW_MIN_HEIGHT, + display: "flex", + alignItems: "center", + justifyContent: "center", +} as const; + +interface PoWorkbenchDetailsGridReceivedQtyCellProps { + row: PoWorkbenchDetailsGridLineRow; +} + +/** Received qty: purchase unit (left) ↔ inventory unit (right). */ +export default function PoWorkbenchDetailsGridReceivedQtyCell({ + row, +}: PoWorkbenchDetailsGridReceivedQtyCellProps) { + const purchaseQtyTone = getPoWorkbenchReceivedQtyTone( + row.receivedQtyPurchase, + row.expectedQty, + ); + + return ( + + + + + + + + + + + + ); +} diff --git a/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridLayout.ts b/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridLayout.ts new file mode 100644 index 0000000..06aeb89 --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridLayout.ts @@ -0,0 +1,118 @@ +import type { SxProps, Theme } from "@mui/material/styles"; + +/** Rounded corners for the grid frame and end cells. */ +export const DETAILS_GRID_BORDER_RADIUS = 1.5; + +/** Column width ratio 30 : 16 : 34 : 20 (15fr : 8fr : 17fr : 10fr). */ +export const DETAILS_GRID_COLUMN_TEMPLATE = + "minmax(0, 15fr) minmax(0, 8fr) minmax(0, 17fr) minmax(0, 10fr)"; + +export const DETAILS_GRID_SCROLL_HOST_SX = { + border: 1, + borderColor: "divider", + borderRadius: DETAILS_GRID_BORDER_RADIUS, + bgcolor: "background.paper", + flex: 1, + minHeight: 0, + overflow: "auto", + width: "100%", +} as const; + +export const DETAILS_GRID_ROOT_SX = { + display: "grid", + gridTemplateColumns: DETAILS_GRID_COLUMN_TEMPLATE, + width: "100%", + minWidth: 0, +} as const; + +/** Row children participate in the parent column grid. */ +export const DETAILS_GRID_ROW_SX = { + display: "contents", + "&:hover > [role=cell]": { + bgcolor: "action.hover", + }, +} as const; + +type CornerPosition = "first" | "middle" | "last"; + +const HEADER_CELL_BASE_SX = { + typography: "body2", + fontWeight: 700, + fontSize: "0.8125rem", + whiteSpace: "nowrap", + color: "text.primary", + bgcolor: (theme: Theme) => + theme.palette.mode === "dark" ? "grey.800" : "grey.100", + borderBottom: 1, + borderColor: "divider", + px: 1.25, + py: 0.875, + position: "sticky", + top: 0, + zIndex: 2, +} as const; + +const BODY_CELL_BASE_SX = { + px: 1.25, + py: 1, + fontSize: "0.8125rem", + borderBottom: 1, + borderColor: "divider", + color: "text.secondary", + minWidth: 0, +} as const; + +export function detailsGridHeaderCellSx( + position: CornerPosition, + options?: { alignCenter?: boolean }, +): SxProps { + const { alignCenter } = options ?? {}; + + return { + ...HEADER_CELL_BASE_SX, + ...(alignCenter ? { textAlign: "center" } : {}), + ...(position === "first" && { + borderTopLeftRadius: DETAILS_GRID_BORDER_RADIUS, + }), + ...(position === "last" && { + borderTopRightRadius: DETAILS_GRID_BORDER_RADIUS, + }), + }; +} + +export function detailsGridBodyCellSx(options: { + position: CornerPosition; + isLastRow?: boolean; + primaryText?: boolean; + alignCenter?: boolean; + dense?: boolean; +}): SxProps { + const { position, isLastRow, primaryText, alignCenter, dense } = options; + + return { + ...BODY_CELL_BASE_SX, + ...(dense ? { px: 1, py: 0.875 } : {}), + ...(alignCenter + ? { + display: "flex", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + "& > *": { + width: "100%", + minWidth: 0, + }, + } + : {}), + ...(primaryText ? { color: "text.primary" } : {}), + ...(isLastRow && { + borderBottom: 0, + ...(position === "first" && { + borderBottomLeftRadius: DETAILS_GRID_BORDER_RADIUS, + }), + ...(position === "last" && { + borderBottomRightRadius: DETAILS_GRID_BORDER_RADIUS, + }), + }), + }; +} diff --git a/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridMock.ts b/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridMock.ts new file mode 100644 index 0000000..09ddbdd --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/poWorkbenchDetailsGridMock.ts @@ -0,0 +1,76 @@ +import type { PoWorkbenchDetailsGridLineRow } from "@/components/PoWorkbench/types"; + +/** Temporary detail lines until the workbench detail API is wired. */ +export const PO_WORKBENCH_DETAILS_GRID_MOCK_ROWS: readonly PoWorkbenchDetailsGridLineRow[] = + [ + { + id: "mock-line-1", + itemName: + "澳洲和牛腱(冷凍)· A5級去筋切片裝 2.5kg×4包/箱(供應商批次:AU-WG-2026-Q1)", + itemCode: "AB1234", + category: "material", + storageLocation: "YF-W201-#A-12", + receiveStatus: "pending", + expectedQty: 100, + purchaseUnitLabel: "1箱X100包X150克", + receivedQtyPurchase: 0, + receivedQtyInventory: 0, + inventoryUnitLabel: "克", + }, + { + id: "mock-line-2", + itemName: + "耐高温真空膠袋(食品級)· 300×450mm 厚度80μm 每卷500個(雙層密封條)", + itemCode: "BC5678", + category: "consumable", + storageLocation: "YF-C305-#B-04", + receiveStatus: "receiving", + expectedQty: 24, + purchaseUnitLabel: "1箱X24卷X500個", + receivedQtyPurchase: 26, + receivedQtyInventory: 312000, + inventoryUnitLabel: "個", + }, + { + id: "mock-line-3", + itemName: + "照燒汁(商用濃縮裝)· 日式醬油基底 5L/桶(開封後需冷藏,效期見標籤)", + itemCode: "FG9012", + category: "finishedGoods", + storageLocation: "YF-D102-#C-08", + receiveStatus: "completed", + expectedQty: 48, + purchaseUnitLabel: "1板X4桶X5升", + receivedQtyPurchase: 48, + receivedQtyInventory: 960, + inventoryUnitLabel: "升", + }, + { + id: "mock-line-4", + itemName: + "高筋麵粉預拌漿(半成品)· 中央廚房標準配方 20kg/袋(含酵母改良劑)", + itemCode: "DE3456", + category: "semiFinished", + storageLocation: "YF-W118-#D-22", + receiveStatus: "receiving", + expectedQty: 60, + purchaseUnitLabel: "1板X3袋X20公斤", + receivedQtyPurchase: 55, + receivedQtyInventory: 3300, + inventoryUnitLabel: "公斤", + }, + { + id: "mock-line-5", + itemName: + "廚房多功能清潔劑(濃縮原液)· 無磷配方 1L×12瓶/箱(勿與漂白水混用)", + itemCode: "EF7890", + category: "miscNonConsumable", + storageLocation: "YF-M402-#E-01", + receiveStatus: "pending", + expectedQty: 120, + purchaseUnitLabel: "1扎X3箱X12包X350克", + receivedQtyPurchase: 0, + receivedQtyInventory: 0, + inventoryUnitLabel: "瓶", + }, + ] as const; diff --git a/src/components/PoWorkbench/detailsGrid/poWorkbenchReceivedQtyTone.ts b/src/components/PoWorkbench/detailsGrid/poWorkbenchReceivedQtyTone.ts new file mode 100644 index 0000000..18387a5 --- /dev/null +++ b/src/components/PoWorkbench/detailsGrid/poWorkbenchReceivedQtyTone.ts @@ -0,0 +1,12 @@ +/** Received vs expected qty display tone for the received-qty column. */ +export type PoWorkbenchReceivedQtyTone = "complete" | "variance" | "none"; + +/** complete = on target or over; variance = under only; none = not received (purchase UoM). */ +export function getPoWorkbenchReceivedQtyTone( + receivedQtyPurchase: number, + expectedQty: number, +): PoWorkbenchReceivedQtyTone { + if (receivedQtyPurchase <= 0) return "none"; + if (receivedQtyPurchase >= expectedQty) return "complete"; + return "variance"; +} diff --git a/src/components/PoWorkbench/PoWorkbenchRegion.tsx b/src/components/PoWorkbench/layout/PoWorkbenchRegion.tsx similarity index 83% rename from src/components/PoWorkbench/PoWorkbenchRegion.tsx rename to src/components/PoWorkbench/layout/PoWorkbenchRegion.tsx index 2c64323..36128a4 100644 --- a/src/components/PoWorkbench/PoWorkbenchRegion.tsx +++ b/src/components/PoWorkbench/layout/PoWorkbenchRegion.tsx @@ -2,7 +2,7 @@ import Box from "@mui/material/Box"; import type { ReactNode } from "react"; -import type { WorkbenchGridRegionId } from "@/components/PoWorkbench/mock/workbenchMockData"; +import type { PoWorkbenchGridRegionId } from "@/components/PoWorkbench/layout/poWorkbenchShellLayout"; /** `gridCell`: fill CSS grid area. `compactHug` / `compactGrow`: flex children in the narrow layout. */ export type PoWorkbenchRegionHeightMode = @@ -11,8 +11,8 @@ export type PoWorkbenchRegionHeightMode = | "compactGrow"; export interface PoWorkbenchRegionProps { - /** Which pane to render; must match {@link WorkbenchGridRegionId}. */ - region: WorkbenchGridRegionId; + /** Which pane to render; must match {@link PoWorkbenchGridRegionId}. */ + region: PoWorkbenchGridRegionId; children?: ReactNode; /** Default `gridCell` for the 2×2 desktop grid. */ heightMode?: PoWorkbenchRegionHeightMode; @@ -44,7 +44,7 @@ const heightModeOuterSx: Record = { * * @remarks * The root sets `data-workbench-region` to the `region` value for automated tests and debugging. - * Values are stable and correspond to {@link WorkbenchGridRegionId}. + * Values are stable and correspond to {@link PoWorkbenchGridRegionId}. */ export default function PoWorkbenchRegion({ region, @@ -66,11 +66,11 @@ export default function PoWorkbenchRegion({ ...heightModeOuterSx[heightMode], overflow: "hidden", /** - * Top grid row height = max(left search strip, right header). With default stretch, the - * shorter cell’s pane grew to the row height, leaving a blank band under the header text. + * Fill the top grid row so divider-colored grid gaps do not show as a grey + * band under the header when the left search strip is taller. */ ...(isDetailsHeaderHug - ? { alignSelf: "start", height: "auto", width: "100%" } + ? { alignSelf: "stretch", height: "100%", width: "100%" } : {}), ...(isDetailsHeader ? { borderBottom: 1, borderColor: "divider" } @@ -81,7 +81,11 @@ export default function PoWorkbenchRegion({ - + ); } diff --git a/src/components/PoWorkbench/PoWorkbenchSearchResultsListSkeleton.tsx b/src/components/PoWorkbench/search/PoWorkbenchSearchResultsListSkeleton.tsx similarity index 100% rename from src/components/PoWorkbench/PoWorkbenchSearchResultsListSkeleton.tsx rename to src/components/PoWorkbench/search/PoWorkbenchSearchResultsListSkeleton.tsx diff --git a/src/components/PoWorkbench/poWorkbenchMapPoResult.ts b/src/components/PoWorkbench/search/poWorkbenchMapPoResult.ts similarity index 100% rename from src/components/PoWorkbench/poWorkbenchMapPoResult.ts rename to src/components/PoWorkbench/search/poWorkbenchMapPoResult.ts diff --git a/src/components/PoWorkbench/poWorkbenchPoListQuery.ts b/src/components/PoWorkbench/search/poWorkbenchPoListQuery.ts similarity index 95% rename from src/components/PoWorkbench/poWorkbenchPoListQuery.ts rename to src/components/PoWorkbench/search/poWorkbenchPoListQuery.ts index cd36f7e..beef6b2 100644 --- a/src/components/PoWorkbench/poWorkbenchPoListQuery.ts +++ b/src/components/PoWorkbench/search/poWorkbenchPoListQuery.ts @@ -1,5 +1,5 @@ import type { PoWorkbenchAdvancedFilters } from "@/components/PoWorkbench/types"; -import { trimString } from "@/components/PoWorkbench/workbenchUtils"; +import { trimString } from "@/components/PoWorkbench/search/workbenchUtils"; /** Rows per request; keeps the results list DOM small until the user scrolls. */ export const PO_WORKBENCH_LIST_PAGE_SIZE = 50; diff --git a/src/components/PoWorkbench/usePoWorkbenchListSearch.ts b/src/components/PoWorkbench/search/usePoWorkbenchListSearch.ts similarity index 97% rename from src/components/PoWorkbench/usePoWorkbenchListSearch.ts rename to src/components/PoWorkbench/search/usePoWorkbenchListSearch.ts index 5ff6d5b..e5f00ab 100644 --- a/src/components/PoWorkbench/usePoWorkbenchListSearch.ts +++ b/src/components/PoWorkbench/search/usePoWorkbenchListSearch.ts @@ -4,11 +4,11 @@ import type { PoResult } from "@/app/api/po"; import type { RecordsRes } from "@/app/api/utils"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; -import { mapPoResultToListRow } from "@/components/PoWorkbench/poWorkbenchMapPoResult"; +import { mapPoResultToListRow } from "@/components/PoWorkbench/search/poWorkbenchMapPoResult"; import { buildWorkbenchPoListSearchParams, PO_WORKBENCH_LIST_PAGE_SIZE, -} from "@/components/PoWorkbench/poWorkbenchPoListQuery"; +} from "@/components/PoWorkbench/search/poWorkbenchPoListQuery"; import type { PoWorkbenchAdvancedFilters, PoWorkbenchListRow, diff --git a/src/components/PoWorkbench/workbenchUtils.ts b/src/components/PoWorkbench/search/workbenchUtils.ts similarity index 100% rename from src/components/PoWorkbench/workbenchUtils.ts rename to src/components/PoWorkbench/search/workbenchUtils.ts diff --git a/src/components/PoWorkbench/shared/PoWorkbenchReceiveStatusChip.tsx b/src/components/PoWorkbench/shared/PoWorkbenchReceiveStatusChip.tsx new file mode 100644 index 0000000..d6e7900 --- /dev/null +++ b/src/components/PoWorkbench/shared/PoWorkbenchReceiveStatusChip.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { PO_WORKBENCH_DETAILS_GRID_META_FONT_SIZE } from "@/components/PoWorkbench/shared/poWorkbenchSharedStyles"; +import type { PoWorkbenchListRow } from "@/components/PoWorkbench/types"; +import Chip from "@mui/material/Chip"; +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +/** Status labels use `purchaseOrder` i18n namespace (shared with legacy PO screens). */ + +const STATUS_CHIP_SX = { + height: "auto", + minHeight: 26, + fontWeight: 600, + lineHeight: 1.25, + "& .MuiChip-label": { + px: 1, + py: 0.25, + display: "block", + }, +} as const; + +const STATUS_CHIP_DENSE_SX = { + height: "auto", + minHeight: "unset", + fontWeight: 600, + lineHeight: 1.4, + fontSize: PO_WORKBENCH_DETAILS_GRID_META_FONT_SIZE, + "& .MuiChip-label": { + px: 0.75, + py: 0.125, + fontSize: PO_WORKBENCH_DETAILS_GRID_META_FONT_SIZE, + lineHeight: 1.4, + display: "block", + }, +} as const; + +export function receiveStatusChipColor( + status: PoWorkbenchListRow["status"], +): "default" | "primary" | "success" | "warning" { + if (status === "completed") return "success"; + if (status === "receiving") return "primary"; + return "warning"; +} + +interface PoWorkbenchReceiveStatusChipProps { + status: PoWorkbenchListRow["status"]; + /** Use details-grid meta font size (0.8125rem). */ + dense?: boolean; +} + +/** Receive workflow chip; same styling as PO list / details header. */ +export default function PoWorkbenchReceiveStatusChip({ + status, + dense = false, +}: PoWorkbenchReceiveStatusChipProps) { + const { t } = useTranslation("purchaseOrder"); + const theme = useTheme(); + + return ( + + ); +} diff --git a/src/components/PoWorkbench/WorkbenchResultSummary.tsx b/src/components/PoWorkbench/shared/PoWorkbenchResultSummary.tsx similarity index 84% rename from src/components/PoWorkbench/WorkbenchResultSummary.tsx rename to src/components/PoWorkbench/shared/PoWorkbenchResultSummary.tsx index a9a34eb..5a1d43b 100644 --- a/src/components/PoWorkbench/WorkbenchResultSummary.tsx +++ b/src/components/PoWorkbench/shared/PoWorkbenchResultSummary.tsx @@ -1,5 +1,10 @@ "use client"; +import PoWorkbenchReceiveStatusChip from "@/components/PoWorkbench/shared/PoWorkbenchReceiveStatusChip"; +import { + PO_WORKBENCH_META_ICON_SX, + PO_WORKBENCH_RESULT_LINE_SX, +} from "@/components/PoWorkbench/shared/poWorkbenchSharedStyles"; import type { PoWorkbenchListRow } from "@/components/PoWorkbench/types"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import LocalShippingIcon from "@mui/icons-material/LocalShipping"; @@ -9,30 +14,6 @@ import Typography from "@mui/material/Typography"; import { useMediaQuery, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; -/** Match list row text: wrapping long PO / supplier lines. */ -export const RESULT_LINE_SX = { - overflowWrap: "break-word", - wordBreak: "break-word", -} as const; - -export const RESULT_DATE_ICON_SX = { - fontSize: 16, - color: "text.secondary", - flexShrink: 0, -} as const; - -const STATUS_CHIP_SX = { - height: "auto", - minHeight: 26, - fontWeight: 600, - lineHeight: 1.25, - "& .MuiChip-label": { - px: 1, - py: 0.25, - display: "block", - }, -} as const; - const DATE_ROW_SX = { direction: "row" as const, spacing: 2, @@ -66,26 +47,29 @@ interface DateSegmentProps { function WorkbenchResultDateSegment({ kind, dateYmd }: DateSegmentProps) { const icon = kind === "order" ? ( - + ) : ( - + ); return ( {icon} - + {dateYmd} ); } -export interface WorkbenchResultSummaryProps { +export interface PoWorkbenchResultSummaryProps { row: PoWorkbenchListRow; /** `header`: PO + supplier left; status chips then dates on the right. `list`: list row. */ layout?: "list" | "header"; } +/** @deprecated Use {@link PoWorkbenchResultSummaryProps}. */ +export type WorkbenchResultSummaryProps = PoWorkbenchResultSummaryProps; + interface PoSupplierBlockProps { row: PoWorkbenchListRow; } @@ -102,13 +86,17 @@ const HEADER_LINE_CLAMP_SX = { minWidth: 0, } as const; -function receiveStatusChipColor( - status: PoWorkbenchListRow["status"], -): "default" | "primary" | "success" | "warning" { - if (status === "completed") return "success"; - if (status === "receiving") return "primary"; - return "warning"; -} +const ETA_VARIANCE_CHIP_SX = { + height: "auto", + minHeight: 26, + fontWeight: 600, + lineHeight: 1.25, + "& .MuiChip-label": { + px: 1, + py: 0.25, + display: "block", + }, +} as const; function startOfLocalDay(value: Date): Date { return new Date(value.getFullYear(), value.getMonth(), value.getDate()); @@ -132,15 +120,12 @@ function parseYmdToLocalDate(ymd: string): Date | null { return startOfLocalDay(date); } -/** 未上架:尚未完成收貨(`completed` 視為可視為已上架/結案,不顯示此提醒)。 */ +/** Not yet shelf-listed: still pending or receiving (completed POs skip ETA reminder). */ function isNotYetListedOnShelf(row: PoWorkbenchListRow): boolean { return row.status === "pending" || row.status === "receiving"; } -/** - * 未上架 PO:比較「今天」與 ETA 的日曆天差,提醒不要提早處理非當天到貨的單、或關注已過 ETA 仍未完成。 - * `diffDays` = 今天(本地) − 預計到貨日(本地)(整天)。 - */ +/** ETA vs today (local calendar days) for unlisted POs; labels from poWorkbench i18n. */ function buildEtaVarianceReminder( row: PoWorkbenchListRow, t: (key: string, options?: Record) => string, @@ -184,7 +169,7 @@ function WorkbenchResultStatusChips({ const { t: tPo } = useTranslation("purchaseOrder"); const theme = useTheme(); const chipSx = { - ...STATUS_CHIP_SX, + ...ETA_VARIANCE_CHIP_SX, fontSize: theme.typography.body2.fontSize, }; const etaVarianceReminder = showEtaVsTodayChip @@ -210,13 +195,7 @@ function WorkbenchResultStatusChips({ sx={chipSx} /> ) : null} - + {row.escalated ? ( ; } @@ -355,13 +334,13 @@ export default function WorkbenchResultSummary({ variant="body1" color="text.primary" fontWeight={600} - sx={RESULT_LINE_SX} + sx={PO_WORKBENCH_RESULT_LINE_SX} > {row.poNumber} - + {row.supplierName} diff --git a/src/components/PoWorkbench/shared/poWorkbenchSharedStyles.ts b/src/components/PoWorkbench/shared/poWorkbenchSharedStyles.ts new file mode 100644 index 0000000..1792f3d --- /dev/null +++ b/src/components/PoWorkbench/shared/poWorkbenchSharedStyles.ts @@ -0,0 +1,22 @@ +/** Shared typography / icon styles for list, header, and detail grid meta rows. */ + +export const PO_WORKBENCH_META_ICON_SX = { + fontSize: 16, + color: "text.secondary", + flexShrink: 0, +} as const; + +/** Matches detail grid meta lines (code, location, dense status chip). */ +export const PO_WORKBENCH_DETAILS_GRID_META_FONT_SIZE = "0.8125rem"; + +/** Long PO / supplier lines in list and header. */ +export const PO_WORKBENCH_RESULT_LINE_SX = { + overflowWrap: "break-word", + wordBreak: "break-word", +} as const; + +/** @deprecated Use {@link PO_WORKBENCH_META_ICON_SX}. */ +export const RESULT_DATE_ICON_SX = PO_WORKBENCH_META_ICON_SX; + +/** @deprecated Use {@link PO_WORKBENCH_RESULT_LINE_SX}. */ +export const RESULT_LINE_SX = PO_WORKBENCH_RESULT_LINE_SX; diff --git a/src/components/PoWorkbench/types.ts b/src/components/PoWorkbench/types.ts index d8e42f9..048d057 100644 --- a/src/components/PoWorkbench/types.ts +++ b/src/components/PoWorkbench/types.ts @@ -1,3 +1,14 @@ +/** `PoResult.status` receive workflow (pending / receiving / completed). */ +export type PoWorkbenchReceiveStatus = "pending" | "receiving" | "completed"; + +/** Item category shown in the workbench detail grid (mock / future API). */ +export type PoWorkbenchItemCategory = + | "material" + | "finishedGoods" + | "semiFinished" + | "consumable" + | "miscNonConsumable"; + /** One row in the workbench list UI; populated from `mapPoResultToListRow` and `/po/list`. */ export interface PoWorkbenchListRow { id: string; @@ -9,12 +20,30 @@ export interface PoWorkbenchListRow { estimatedArrivalDate: string; /** Same as `PoResult.escalated`. */ escalated: boolean; - /** `PoResult.status` receive workflow (pending / receiving / completed). */ - status: "pending" | "receiving" | "completed"; + status: PoWorkbenchReceiveStatus; } -/** @deprecated Use {@link PoWorkbenchListRow}. */ -export type WorkbenchMockSearchResult = PoWorkbenchListRow; +/** One line in the workbench detail grid (item column wired first). */ +export interface PoWorkbenchDetailsGridLineRow { + id: string; + itemName: string; + /** Two letters + four digits, e.g. `AB1234`. */ + itemCode: string; + category: PoWorkbenchItemCategory; + /** Format `YF-WYYY-#X-YY` (X = letter, Y = digit). */ + storageLocation: string; + receiveStatus: PoWorkbenchReceiveStatus; + /** Expected receive quantity for the line (display in qty column). */ + expectedQty: number; + /** Purchase UoM breakdown, e.g. `1箱X100包X150克`. */ + purchaseUnitLabel: string; + /** Received qty in purchase UoM (實收 · 採購單位). */ + receivedQtyPurchase: number; + /** Received qty converted to inventory UoM (實收 · 庫存單位). */ + receivedQtyInventory: number; + /** Inventory UoM label, e.g. `克` or `個`. */ + inventoryUnitLabel: string; +} /** Matches `PoResult.escalated` filtering on the PO search screen. */ export type ReportStatusFilter = "ALL" | "ESCALATED" | "NOT_ESCALATED"; @@ -54,7 +83,7 @@ export function getLocalDateYmd(date: Date = new Date()): string { )}`; } -/** Default: no supplier/order filters; 預計送貨/到貨日期 = 當地今天(起訖同天)。 */ +/** Default: no supplier/order filters; ETA range = local today (from–to). */ export function createDefaultAdvancedFilters(): PoWorkbenchAdvancedFilters { const today = getLocalDateYmd(); return { diff --git a/src/i18n/en/poWorkbench.json b/src/i18n/en/poWorkbench.json index 53b6603..d054a2f 100644 --- a/src/i18n/en/poWorkbench.json +++ b/src/i18n/en/poWorkbench.json @@ -36,6 +36,28 @@ "detailsPlaceholder": { "content": "Detail content (placeholder)" }, + "detailsGrid": { + "ariaLabel": "PO details table", + "form": { + "deliveryNoteNo": "Delivery note no. (DN No.)", + "deliveryNoteNoPlaceholder": "Enter delivery note number", + "receiptDate": "Receipt date" + }, + "columns": { + "itemInfo": "Item info", + "expectedQty": "Expected qty", + "receivedQty": "Received qty", + "inputArea": "Input area" + }, + "columnPending": "—", + "itemCategory": { + "material": "Material", + "finishedGoods": "Finished goods", + "semiFinished": "Semi-finished goods", + "consumable": "Consumable", + "miscNonConsumable": "Misc. & non-consumable" + } + }, "breadcrumb": { "segment": "PO Workbench" }, diff --git a/src/i18n/zh/poWorkbench.json b/src/i18n/zh/poWorkbench.json index d4426e1..7c656ae 100644 --- a/src/i18n/zh/poWorkbench.json +++ b/src/i18n/zh/poWorkbench.json @@ -30,12 +30,34 @@ "detailsHeader": { "orderDateLabel": "訂單日期", "etaLabel": "預計送貨日期", - "etaUnlistedBeforeEta": "比預計到貨早 {{days}} 天", - "etaUnlistedAfterEta": "比預計到貨晚 {{days}} 天" + "etaUnlistedBeforeEta": "比預計到貨日早 {{days}} 天", + "etaUnlistedAfterEta": "比預計到貨日晚 {{days}} 天" }, "detailsPlaceholder": { "content": "明細內容(預留)" }, + "detailsGrid": { + "ariaLabel": "採購單明細表格", + "form": { + "deliveryNoteNo": "送貨單編號(DN No.)", + "deliveryNoteNoPlaceholder": "請輸入送貨單編號", + "receiptDate": "收貨日期" + }, + "columns": { + "itemInfo": "貨品資料", + "expectedQty": "應收數量", + "receivedQty": "實收數量", + "inputArea": "資料輸入區" + }, + "columnPending": "—", + "itemCategory": { + "material": "材料", + "finishedGoods": "成品", + "semiFinished": "半成品", + "consumable": "消耗品", + "miscNonConsumable": "雜項及非消耗品" + } + }, "breadcrumb": { "segment": "採購單工作台" },