| @@ -138,6 +138,7 @@ export interface ItemLotTraceMovement { | |||||
| relationshipId?: number | null; | relationshipId?: number | null; | ||||
| doOutboundIsExtra?: boolean; | doOutboundIsExtra?: boolean; | ||||
| doOutboundIsReplenish?: boolean; | doOutboundIsReplenish?: boolean; | ||||
| doOutboundQtyChanged?: boolean; | |||||
| processingStatus?: string; | processingStatus?: string; | ||||
| matchStatus?: string; | matchStatus?: string; | ||||
| } | } | ||||
| @@ -199,7 +200,7 @@ export interface ItemLotTraceStockTakeEvent { | |||||
| stockTakeRoundName?: string; | stockTakeRoundName?: string; | ||||
| varianceQty: number; | varianceQty: number; | ||||
| beforeQty: number; | beforeQty: number; | ||||
| afterQty: number; | |||||
| afterQty?: number | null; | |||||
| approver: string; | approver: string; | ||||
| timestamp: string | null; | timestamp: string | null; | ||||
| recordDetail?: ItemLotTraceStockTakeRecordDetail | null; | recordDetail?: ItemLotTraceStockTakeRecordDetail | null; | ||||
| @@ -409,6 +410,8 @@ export interface ItemLotTraceDoDelivery { | |||||
| doPickOrderRecordId?: number | null; | doPickOrderRecordId?: number | null; | ||||
| isExtra?: boolean; | isExtra?: boolean; | ||||
| isReplenish?: boolean; | isReplenish?: boolean; | ||||
| /** True when pick_order_line.qty ≠ SUM(stock_out_line.qty) for that line (改數). */ | |||||
| qtyChanged?: boolean; | |||||
| } | } | ||||
| export interface ItemLotTraceReturnEvent { | export interface ItemLotTraceReturnEvent { | ||||
| @@ -1643,6 +1643,36 @@ export const fetchOperatorKpi = cache(async (date?: string) => { | |||||
| }); | }); | ||||
| }); | }); | ||||
| // ===== Drink Production Qty Dashboard ===== | |||||
| export interface DrinkProductionQtyJobOrderDetail { | |||||
| jobOrderId: number; | |||||
| jobOrderCode?: string | null; | |||||
| productionDate?: string | null; | |||||
| reqQty: number; | |||||
| productionQty: number; | |||||
| } | |||||
| export interface DrinkProductionQtyResponse { | |||||
| itemCode?: string | null; | |||||
| itemName?: string | null; | |||||
| uom?: string | null; | |||||
| totalReqQty: number; | |||||
| totalQty: number; | |||||
| jobOrders?: DrinkProductionQtyJobOrderDetail[]; | |||||
| } | |||||
| export const fetchDrinkProductionQty = cache(async (date?: string) => { | |||||
| const params = new URLSearchParams(); | |||||
| if (date) params.set("date", date); | |||||
| const qs = params.toString(); | |||||
| const url = `${BASE_API_URL}/product-process/Demo/DrinkProductionQty${qs ? `?${qs}` : ""}`; | |||||
| return serverFetchJson<DrinkProductionQtyResponse[]>(url, { | |||||
| method: "GET", | |||||
| next: { tags: ["drinkProductionQty"] }, | |||||
| }); | |||||
| }); | |||||
| // ===== Equipment Status Dashboard ===== | // ===== Equipment Status Dashboard ===== | ||||
| export interface EquipmentStatusProcessInfo { | export interface EquipmentStatusProcessInfo { | ||||
| @@ -91,31 +91,22 @@ const ItemTracingFlowGraphSearch: React.FC<Props> = ({ | |||||
| }} | }} | ||||
| sx={{ | sx={{ | ||||
| "& .MuiOutlinedInput-root": { | "& .MuiOutlinedInput-root": { | ||||
| justifyContent: hasQuery ? "flex-start" : "center", | |||||
| alignItems: "center", | |||||
| }, | }, | ||||
| "& .MuiOutlinedInput-input": { | "& .MuiOutlinedInput-input": { | ||||
| textAlign: hasQuery ? "left" : "center", | |||||
| color: "text.secondary", | color: "text.secondary", | ||||
| ...(hasQuery | |||||
| ? {} | |||||
| : { | |||||
| flex: "0 1 auto", | |||||
| width: "auto", | |||||
| maxWidth: "90%", | |||||
| }), | |||||
| py: "8.5px", | |||||
| lineHeight: 1.4375, | |||||
| }, | }, | ||||
| "& .MuiOutlinedInput-input::placeholder": { | "& .MuiOutlinedInput-input::placeholder": { | ||||
| color: "text.secondary", | |||||
| color: "text.disabled", | |||||
| opacity: 1, | opacity: 1, | ||||
| textAlign: "center", | |||||
| }, | }, | ||||
| ...(!hasQuery | |||||
| ? { | |||||
| "& .MuiInputAdornment-positionStart": { | |||||
| marginRight: 0.75, | |||||
| }, | |||||
| } | |||||
| : {}), | |||||
| "& .MuiInputAdornment-root": { | |||||
| height: "100%", | |||||
| maxHeight: "none", | |||||
| alignItems: "center", | |||||
| }, | |||||
| }} | }} | ||||
| /> | /> | ||||
| {hasQuery && ( | {hasQuery && ( | ||||
| @@ -459,11 +459,6 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| align: "right", | align: "right", | ||||
| value: (row) => formatQty(row.qty, stockUom), | value: (row) => formatQty(row.qty, stockUom), | ||||
| }, | }, | ||||
| { | |||||
| key: "ref", | |||||
| label: t("ref"), | |||||
| value: (row) => row.transferCode, | |||||
| }, | |||||
| { | { | ||||
| key: "timestamp", | key: "timestamp", | ||||
| label: t("timestamp"), | label: t("timestamp"), | ||||
| @@ -485,8 +480,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| <ItemTracingDocLink kind="jo" code={m.jobOrderCode} id={m.jobOrderId} /> | <ItemTracingDocLink kind="jo" code={m.jobOrderCode} id={m.jobOrderId} /> | ||||
| ), | ), | ||||
| }, | }, | ||||
| { key: "material", label: t("material"), value: (m) => m.materialItemCode }, | |||||
| { key: "materialLot", label: t("materialLot"), value: (m) => m.materialLotNo }, | |||||
| { key: "material", label: t("itemCode"), value: (m) => m.materialItemCode }, | |||||
| { key: "materialLot", label: t("itemLot"), value: (m) => m.materialLotNo }, | |||||
| { | { | ||||
| key: "pickOrder", | key: "pickOrder", | ||||
| label: t("pickOrder"), | label: t("pickOrder"), | ||||
| @@ -579,8 +574,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| <ItemTracingDocLink kind="jo" code={u.jobOrderCode} id={u.jobOrderId} /> | <ItemTracingDocLink kind="jo" code={u.jobOrderCode} id={u.jobOrderId} /> | ||||
| ), | ), | ||||
| }, | }, | ||||
| { key: "material", label: t("material"), value: (u) => u.materialItemCode }, | |||||
| { key: "materialLot", label: t("materialLot"), value: (u) => u.materialLotNo }, | |||||
| { key: "material", label: t("itemCode"), value: (u) => u.materialItemCode }, | |||||
| { key: "materialLot", label: t("itemLot"), value: (u) => u.materialLotNo }, | |||||
| { | { | ||||
| key: "materialQty", | key: "materialQty", | ||||
| label: t("materialQty"), | label: t("materialQty"), | ||||
| @@ -610,8 +605,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| <ItemTracingDocLink kind="jo" code={d.jobOrderCode} id={d.jobOrderId} /> | <ItemTracingDocLink kind="jo" code={d.jobOrderCode} id={d.jobOrderId} /> | ||||
| ), | ), | ||||
| }, | }, | ||||
| { key: "finishedItem", label: t("finishedItem"), value: (d) => d.finishedItemCode }, | |||||
| { key: "finishedLot", label: t("finishedLot"), value: (d) => d.finishedLotNo }, | |||||
| { key: "finishedItem", label: t("itemCode"), value: (d) => d.finishedItemCode }, | |||||
| { key: "finishedLot", label: t("itemLot"), value: (d) => d.finishedLotNo }, | |||||
| { | { | ||||
| key: "fgQty", | key: "fgQty", | ||||
| label: t("fgQty"), | label: t("fgQty"), | ||||
| @@ -627,10 +622,10 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| ], [t, stockUom]); | ], [t, stockUom]); | ||||
| const bomRecipeCols = useMemo((): FilterableColumnDef<(typeof bomTrace.bomRecipe)[number]>[] => [ | const bomRecipeCols = useMemo((): FilterableColumnDef<(typeof bomTrace.bomRecipe)[number]>[] => [ | ||||
| { key: "material", label: t("material"), value: (r) => r.materialItemCode }, | |||||
| { key: "material", label: t("itemCode"), value: (r) => r.materialItemCode }, | |||||
| { | { | ||||
| key: "materialName", | key: "materialName", | ||||
| label: `${t("material")} name`, | |||||
| label: t("detailItemName"), | |||||
| value: (r) => r.materialItemName, | value: (r) => r.materialItemName, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -664,7 +659,7 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| "—" | "—" | ||||
| ), | ), | ||||
| }, | }, | ||||
| { key: "material", label: t("material"), value: (row) => row.materialItemCode }, | |||||
| { key: "material", label: t("Item"), value: (row) => row.materialItemCode }, | |||||
| { | { | ||||
| key: "lotNo", | key: "lotNo", | ||||
| label: t("lotNo"), | label: t("lotNo"), | ||||
| @@ -703,7 +698,7 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| const joPickLineCols = useMemo((): FilterableColumnDef<JoPickLine>[] => [ | const joPickLineCols = useMemo((): FilterableColumnDef<JoPickLine>[] => [ | ||||
| { | { | ||||
| key: "material", | key: "material", | ||||
| label: t("material"), | |||||
| label: t("Item"), | |||||
| value: (line) => | value: (line) => | ||||
| line.itemName ? `${line.itemCode} · ${line.itemName}` : line.itemCode, | line.itemName ? `${line.itemCode} · ${line.itemName}` : line.itemCode, | ||||
| }, | }, | ||||
| @@ -719,8 +714,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| align: "right", | align: "right", | ||||
| value: (line) => formatQty(Number(line.pickedQty), stockUom), | value: (line) => formatQty(Number(line.pickedQty), stockUom), | ||||
| }, | }, | ||||
| { key: "status", label: t("status"), value: (line) => line.status }, | |||||
| ], [t, stockUom]); | |||||
| { key: "status", label: t("status"), value: (line) => tr.pickStatus(line.status) }, | |||||
| ], [t, tr, stockUom]); | |||||
| return ( | return ( | ||||
| <Paper variant="outlined"> | <Paper variant="outlined"> | ||||
| @@ -878,7 +873,7 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace }) | |||||
| {" · "} | {" · "} | ||||
| <Chip | <Chip | ||||
| size="small" | size="small" | ||||
| label={po.status} | |||||
| label={tr.pickStatus(po.status)} | |||||
| variant="outlined" | variant="outlined" | ||||
| sx={{ ml: 0.5 }} | sx={{ ml: 0.5 }} | ||||
| /> | /> | ||||
| @@ -31,7 +31,7 @@ import { traceFlowHandleTopPercent } from "./traceFlowEdgeLayout"; | |||||
| import { kindColor, kindLabelKey } from "./traceFlowNodeUtils"; | import { kindColor, kindLabelKey } from "./traceFlowNodeUtils"; | ||||
| import { formatQty, formatSignedQty } from "./traceQtyUtils"; | import { formatQty, formatSignedQty } from "./traceQtyUtils"; | ||||
| import { buildStockTakeLifecycleStages } from "./traceStockTakeUtils"; | import { buildStockTakeLifecycleStages } from "./traceStockTakeUtils"; | ||||
| import { pickStatusValueColor } from "./traceLabelUtils"; | |||||
| import { pickStatusValueColor, resolveDoOutboundQtyColor } from "./traceLabelUtils"; | |||||
| const PHASE_LABEL_INNER = 96; | const PHASE_LABEL_INNER = 96; | ||||
| /** Expanded stock-take stage panel (mt + maxHeight + padding) below the fixed card. */ | /** Expanded stock-take stage panel (mt + maxHeight + padding) below the fixed card. */ | ||||
| @@ -348,7 +348,7 @@ export const TraceFlowEventNode = memo(function TraceFlowEventNode({ | |||||
| {t("Item")}: {node.meta?.trim() || node.traceItemCode} | {t("Item")}: {node.meta?.trim() || node.traceItemCode} | ||||
| </Typography> | </Typography> | ||||
| ) : null} | ) : null} | ||||
| {node.qty != null && ( | |||||
| {(node.qty != null || node.kind === "STOCK_TAKE") && ( | |||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| fontWeight={600} | fontWeight={600} | ||||
| @@ -366,6 +366,15 @@ export const TraceFlowEventNode = memo(function TraceFlowEventNode({ | |||||
| display: "-webkit-box", | display: "-webkit-box", | ||||
| WebkitLineClamp: 2, | WebkitLineClamp: 2, | ||||
| WebkitBoxOrient: "vertical", | WebkitBoxOrient: "vertical", | ||||
| color: | |||||
| node.kind === "DO_OUT" || | |||||
| node.kind === "JO_OUT" || | |||||
| node.kind === "MATERIAL_PICK" | |||||
| ? resolveDoOutboundQtyColor( | |||||
| node.qty, | |||||
| node.kind === "DO_OUT" ? node.doOutboundQtyChanged : false, | |||||
| ) | |||||
| : undefined, | |||||
| }} | }} | ||||
| > | > | ||||
| {node.kind === "PURCHASE" | {node.kind === "PURCHASE" | ||||
| @@ -385,6 +394,31 @@ export const TraceFlowEventNode = memo(function TraceFlowEventNode({ | |||||
| : formatQty(node.qty, node.uom)} | : formatQty(node.qty, node.uom)} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| {node.kind === "STOCK_TAKE" ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| fontWeight={600} | |||||
| title={formatQty(node.stockTakeVarianceQty, node.uom)} | |||||
| sx={{ | |||||
| mt: 0.25, | |||||
| fontSize: data.compact ? "0.8rem" : undefined, | |||||
| overflow: "hidden", | |||||
| overflowWrap: "anywhere", | |||||
| wordBreak: "break-word", | |||||
| display: "-webkit-box", | |||||
| WebkitLineClamp: 2, | |||||
| WebkitBoxOrient: "vertical", | |||||
| color: | |||||
| node.stockTakeVarianceQty != null && node.stockTakeVarianceQty > 0 | |||||
| ? "success.main" | |||||
| : node.stockTakeVarianceQty != null && node.stockTakeVarianceQty < 0 | |||||
| ? "error.main" | |||||
| : undefined, | |||||
| }} | |||||
| > | |||||
| {t("variance")}: {formatQty(node.stockTakeVarianceQty, node.uom)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {node.traceLotNo ? ( | {node.traceLotNo ? ( | ||||
| <ItemTracingLotTraceLink | <ItemTracingLotTraceLink | ||||
| label={ | label={ | ||||
| @@ -193,6 +193,7 @@ export const buildExtendedTraceGraphNodes = ( | |||||
| categoryLabel: labels.categoryOutbound, | categoryLabel: labels.categoryOutbound, | ||||
| doOutboundIsExtra: chipFlags.isExtra, | doOutboundIsExtra: chipFlags.isExtra, | ||||
| doOutboundIsReplenish: chipFlags.isReplenish, | doOutboundIsReplenish: chipFlags.isReplenish, | ||||
| doOutboundQtyChanged: d.qtyChanged === true, | |||||
| outboundKindLabel, | outboundKindLabel, | ||||
| warehouseCode: scope?.defaultWarehouseCode, | warehouseCode: scope?.defaultWarehouseCode, | ||||
| details: [ | details: [ | ||||
| @@ -123,6 +123,8 @@ export interface TraceGraphNode { | |||||
| /** 加單 / 補貨 flags for DO_OUT title chips (detail uses outboundKindLabel). */ | /** 加單 / 補貨 flags for DO_OUT title chips (detail uses outboundKindLabel). */ | ||||
| doOutboundIsExtra?: boolean; | doOutboundIsExtra?: boolean; | ||||
| doOutboundIsReplenish?: boolean; | doOutboundIsReplenish?: boolean; | ||||
| /** 改數 — pick line qty ≠ sum of stock-out lines for that pick line. */ | |||||
| doOutboundQtyChanged?: boolean; | |||||
| outboundKindLabel?: string; | outboundKindLabel?: string; | ||||
| /** Longer status explanation in detail panel (e.g. transfer inbound). */ | /** Longer status explanation in detail panel (e.g. transfer inbound). */ | ||||
| putawayStatusDetail?: string; | putawayStatusDetail?: string; | ||||
| @@ -156,6 +158,8 @@ export interface TraceGraphNode { | |||||
| adjustmentDirection?: string; | adjustmentDirection?: string; | ||||
| /** Stocktake lifecycle record detail (for expandable STOCK_TAKE nodes). */ | /** Stocktake lifecycle record detail (for expandable STOCK_TAKE nodes). */ | ||||
| stockTakeRecordDetail?: ItemLotTraceStockTakeRecordDetail; | stockTakeRecordDetail?: ItemLotTraceStockTakeRecordDetail; | ||||
| /** Stocktake variance (accepted − book); null when round has no accepted qty yet. */ | |||||
| stockTakeVarianceQty?: number | null; | |||||
| /** Owning inventory_lot for multi-location graph scopes. */ | /** Owning inventory_lot for multi-location graph scopes. */ | ||||
| inventoryLotId?: number; | inventoryLotId?: number; | ||||
| /** Translated pick-line statuses for JO pick cards. */ | /** Translated pick-line statuses for JO pick cards. */ | ||||
| @@ -642,6 +646,7 @@ export const buildTraceGraphNodes = ( | |||||
| { | { | ||||
| isExtra: boolean; | isExtra: boolean; | ||||
| isReplenish: boolean; | isReplenish: boolean; | ||||
| qtyChanged: boolean; | |||||
| consoCode?: string; | consoCode?: string; | ||||
| } | } | ||||
| >(); | >(); | ||||
| @@ -660,6 +665,7 @@ export const buildTraceGraphNodes = ( | |||||
| doMetaByPickCode.set(code, { | doMetaByPickCode.set(code, { | ||||
| isExtra: chipFlags.isExtra, | isExtra: chipFlags.isExtra, | ||||
| isReplenish: chipFlags.isReplenish, | isReplenish: chipFlags.isReplenish, | ||||
| qtyChanged: d.qtyChanged === true, | |||||
| consoCode: d.consoCode?.trim() || code, | consoCode: d.consoCode?.trim() || code, | ||||
| }); | }); | ||||
| }); | }); | ||||
| @@ -884,6 +890,9 @@ export const buildTraceGraphNodes = ( | |||||
| : undefined, | : undefined, | ||||
| doOutboundIsExtra: chipFlags?.isExtra, | doOutboundIsExtra: chipFlags?.isExtra, | ||||
| doOutboundIsReplenish: chipFlags?.isReplenish, | doOutboundIsReplenish: chipFlags?.isReplenish, | ||||
| doOutboundQtyChanged: isDoOut | |||||
| ? (pickMeta?.qtyChanged ?? m.doOutboundQtyChanged === true) | |||||
| : undefined, | |||||
| warehouseCode: m.warehouseCode?.trim() || undefined, | warehouseCode: m.warehouseCode?.trim() || undefined, | ||||
| categoryLabel: categoryForKind(kind, labels), | categoryLabel: categoryForKind(kind, labels), | ||||
| ...(isJoOut | ...(isJoOut | ||||
| @@ -1022,10 +1031,12 @@ export const buildTraceGraphNodes = ( | |||||
| const bookQty = resolveStockTakeBookQty(detail, e.beforeQty); | const bookQty = resolveStockTakeBookQty(detail, e.beforeQty); | ||||
| const acceptedQty = resolveStockTakeAcceptedQty(detail, e.afterQty); | const acceptedQty = resolveStockTakeAcceptedQty(detail, e.afterQty); | ||||
| const varianceQty = | const varianceQty = | ||||
| detail?.varianceQty != null ? Number(detail.varianceQty) : e.varianceQty; | |||||
| const meta = [e.approver, roundLabel, e.stockTakeSection, `Δ ${formatQty(varianceQty, stockUom)}`] | |||||
| .filter(Boolean) | |||||
| .join(" · "); | |||||
| detail?.varianceQty != null | |||||
| ? Number(detail.varianceQty) | |||||
| : acceptedQty != null && e.varianceQty != null | |||||
| ? Number(e.varianceQty) | |||||
| : null; | |||||
| const meta = [e.approver, roundLabel, e.stockTakeSection].filter(Boolean).join(" · "); | |||||
| nodes.push({ | nodes.push({ | ||||
| id: lb | id: lb | ||||
| ? `${idPfx}st-${i}-${e.stockTakeCode}` | ? `${idPfx}st-${i}-${e.stockTakeCode}` | ||||
| @@ -1043,6 +1054,7 @@ export const buildTraceGraphNodes = ( | |||||
| traceLotNo: e.lotNo?.trim() || data.lot.lotNo?.trim() || undefined, | traceLotNo: e.lotNo?.trim() || data.lot.lotNo?.trim() || undefined, | ||||
| categoryLabel: labels.categoryStockTake, | categoryLabel: labels.categoryStockTake, | ||||
| stockTakeRecordDetail: detail, | stockTakeRecordDetail: detail, | ||||
| stockTakeVarianceQty: varianceQty, | |||||
| details: [ | details: [ | ||||
| field(labels.detailStockTakeCode, e.stockTakeCode), | field(labels.detailStockTakeCode, e.stockTakeCode), | ||||
| fieldIf(labels.detailStockTakeRound, roundLabel || undefined), | fieldIf(labels.detailStockTakeRound, roundLabel || undefined), | ||||
| @@ -176,3 +176,16 @@ export const resolveDoOutboundChipFlags = ( | |||||
| isExtra: resolveTraceDoOutboundIsExtra(source), | isExtra: resolveTraceDoOutboundIsExtra(source), | ||||
| isReplenish: source.isReplenish === true, | isReplenish: source.isReplenish === true, | ||||
| }); | }); | ||||
| /** | |||||
| * Outbound qty color (DO_OUT / JO_OUT / MATERIAL_PICK): | |||||
| * 改數 → orange; else qty > 0 → green; qty ≤ 0 → red. | |||||
| */ | |||||
| export const resolveDoOutboundQtyColor = ( | |||||
| qty: number | null | undefined, | |||||
| qtyChanged?: boolean, | |||||
| ): "warning.main" | "success.main" | "error.main" | undefined => { | |||||
| if (qtyChanged) return "warning.main"; | |||||
| if (qty == null || !Number.isFinite(Number(qty))) return undefined; | |||||
| return Number(qty) > 0 ? "success.main" : "error.main"; | |||||
| }; | |||||
| @@ -34,14 +34,16 @@ export const resolveStockTakeBookQty = ( | |||||
| /** | /** | ||||
| * Accepted physical qty used for variance / posting decision. | * Accepted physical qty used for variance / posting decision. | ||||
| * Prefer lastSelect (1=first, 2=second, 3=approver), then fallback chain, then line finalQty. | * Prefer lastSelect (1=first, 2=second, 3=approver), then fallback chain, then line finalQty. | ||||
| * Incomplete rounds (no count yet) must not surface as 0 via COALESCE(finalQty, 0). | |||||
| */ | */ | ||||
| export const resolveStockTakeAcceptedQty = ( | export const resolveStockTakeAcceptedQty = ( | ||||
| detail: ItemLotTraceStockTakeRecordDetail | null | undefined, | detail: ItemLotTraceStockTakeRecordDetail | null | undefined, | ||||
| fallbackAfterQty?: number | null, | fallbackAfterQty?: number | null, | ||||
| ): number | null => { | ): number | null => { | ||||
| const pick = (n: number | null | undefined) => | |||||
| n != null && !Number.isNaN(Number(n)) ? Number(n) : null; | |||||
| if (detail) { | if (detail) { | ||||
| const pick = (n: number | null | undefined) => | |||||
| n != null && !Number.isNaN(Number(n)) ? Number(n) : null; | |||||
| switch (detail.lastSelect) { | switch (detail.lastSelect) { | ||||
| case 3: { | case 3: { | ||||
| const q = pick(detail.approverQty); | const q = pick(detail.approverQty); | ||||
| @@ -67,6 +69,13 @@ export const resolveStockTakeAcceptedQty = ( | |||||
| if (fromSecond != null) return fromSecond; | if (fromSecond != null) return fromSecond; | ||||
| const fromFirst = pick(detail.pickerFirstQty); | const fromFirst = pick(detail.pickerFirstQty); | ||||
| if (fromFirst != null) return fromFirst; | if (fromFirst != null) return fromFirst; | ||||
| const status = (detail.recordStatus ?? "").trim().toUpperCase(); | |||||
| const accepted = | |||||
| status === "ACCEPTED" || status === "COMPLETED" || status === "COMPLETE"; | |||||
| if (!accepted) { | |||||
| return null; | |||||
| } | |||||
| } | } | ||||
| if (fallbackAfterQty != null && !Number.isNaN(Number(fallbackAfterQty))) { | if (fallbackAfterQty != null && !Number.isNaN(Number(fallbackAfterQty))) { | ||||
| return Number(fallbackAfterQty); | return Number(fallbackAfterQty); | ||||
| @@ -0,0 +1,349 @@ | |||||
| "use client"; | |||||
| import React, { useState, useEffect, useCallback, useRef } from "react"; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| Card, | |||||
| CardContent, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| CircularProgress, | |||||
| Stack, | |||||
| IconButton, | |||||
| Collapse, | |||||
| Link as MuiLink, | |||||
| } from "@mui/material"; | |||||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | |||||
| import ExpandLessIcon from "@mui/icons-material/ExpandLess"; | |||||
| import NextLink from "next/link"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import dayjs from "dayjs"; | |||||
| import type { Dayjs } from "dayjs"; | |||||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import { | |||||
| fetchDrinkProductionQty, | |||||
| DrinkProductionQtyResponse, | |||||
| } from "@/app/api/jo/actions"; | |||||
| const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 分鐘 | |||||
| const formatQty = (qty: number | null | undefined): string => { | |||||
| if (qty === null || qty === undefined || Number.isNaN(qty)) return "-"; | |||||
| return qty.toLocaleString(undefined, { maximumFractionDigits: 2 }); | |||||
| }; | |||||
| const formatProductionDate = (value: string | null | undefined): string => { | |||||
| if (!value) return "-"; | |||||
| const parsed = dayjs(value); | |||||
| return parsed.isValid() ? parsed.format("YYYY-MM-DD") : value; | |||||
| }; | |||||
| const getRowKey = (row: DrinkProductionQtyResponse, idx: number): string => | |||||
| `${row.itemCode || "unknown"}-${idx}`; | |||||
| const DrinkProductionQtyDashboard: React.FC = () => { | |||||
| const { t } = useTranslation(["common", "jo", "productionProcess"]); | |||||
| const [data, setData] = useState<DrinkProductionQtyResponse[]>([]); | |||||
| const [loading, setLoading] = useState<boolean>(true); | |||||
| const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs()); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<Set<string>>( | |||||
| new Set(), | |||||
| ); | |||||
| const refreshCountRef = useRef<number>(0); | |||||
| const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>( | |||||
| null, | |||||
| ); | |||||
| const loadData = useCallback(async () => { | |||||
| setLoading(true); | |||||
| try { | |||||
| const result = await fetchDrinkProductionQty( | |||||
| selectedDate.format("YYYY-MM-DD"), | |||||
| ); | |||||
| setData(result || []); | |||||
| setExpandedRowKeys(new Set()); | |||||
| setLastDataRefreshTime(dayjs()); | |||||
| refreshCountRef.current += 1; | |||||
| } catch (error) { | |||||
| console.error("Error fetching drink production qty:", error); | |||||
| setData([]); | |||||
| setExpandedRowKeys(new Set()); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, [selectedDate]); | |||||
| useEffect(() => { | |||||
| loadData(); | |||||
| const interval = setInterval(() => { | |||||
| loadData(); | |||||
| }, REFRESH_INTERVAL); | |||||
| return () => clearInterval(interval); | |||||
| }, [loadData]); | |||||
| const toggleRowExpanded = (rowKey: string) => { | |||||
| setExpandedRowKeys((prev) => { | |||||
| const next = new Set(prev); | |||||
| if (next.has(rowKey)) { | |||||
| next.delete(rowKey); | |||||
| } else { | |||||
| next.add(rowKey); | |||||
| } | |||||
| return next; | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}> | |||||
| {t("Drink Production Qty Dashboard")} | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={2} sx={{ mb: 3, alignItems: "center" }}> | |||||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||||
| <DatePicker | |||||
| label={t("Date")} | |||||
| value={selectedDate} | |||||
| onChange={(newValue) => { | |||||
| if (newValue) setSelectedDate(newValue); | |||||
| }} | |||||
| format="YYYY-MM-DD" | |||||
| slotProps={{ | |||||
| textField: { size: "small", sx: { minWidth: 160 } }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| <Box sx={{ flexGrow: 1 }} /> | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ color: "text.secondary" }} | |||||
| suppressHydrationWarning | |||||
| > | |||||
| {t("Auto-refresh every 10 minutes")} | | |||||
| {t("Last updated")}:{" "} | |||||
| {lastDataRefreshTime | |||||
| ? lastDataRefreshTime.format("HH:mm:ss") | |||||
| : "--:--:--"} | |||||
| </Typography> | |||||
| </Stack> | |||||
| {loading ? ( | |||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer | |||||
| sx={{ | |||||
| border: "3px solid #135fed", | |||||
| overflowX: "auto", | |||||
| maxHeight: 440, | |||||
| overflow: "auto", | |||||
| }} | |||||
| component={Paper} | |||||
| > | |||||
| <Table size="small" sx={{ minWidth: 650 }}> | |||||
| <TableHead> | |||||
| <TableRow | |||||
| sx={{ | |||||
| bgcolor: "#424242", | |||||
| "& th": { | |||||
| borderBottom: "none", | |||||
| py: 1.5, | |||||
| position: "sticky", | |||||
| top: 0, | |||||
| zIndex: 1, | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <TableCell sx={{ width: 48 }} /> | |||||
| <TableCell sx={{ width: 160 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| {t("Item Code")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell sx={{ width: 260 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| {t("Goods Name")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell sx={{ width: 120 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| {t("Unit")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right" sx={{ width: 120 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| {t("Stock Req. Qty")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right" sx={{ width: 140 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| {t("Production Qty")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {data.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={6} align="center"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ py: 2, color: "text.secondary" }} | |||||
| > | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| data.map((row, idx) => { | |||||
| const rowKey = getRowKey(row, idx); | |||||
| const jobOrders = row.jobOrders ?? []; | |||||
| const isExpanded = expandedRowKeys.has(rowKey); | |||||
| const hasJobOrders = jobOrders.length > 0; | |||||
| return ( | |||||
| <React.Fragment key={rowKey}> | |||||
| <TableRow hover={hasJobOrders}> | |||||
| <TableCell padding="checkbox"> | |||||
| {hasJobOrders ? ( | |||||
| <IconButton | |||||
| size="small" | |||||
| aria-label={ | |||||
| isExpanded | |||||
| ? t("Collapse job order details") | |||||
| : t("Expand job order details") | |||||
| } | |||||
| onClick={() => toggleRowExpanded(rowKey)} | |||||
| > | |||||
| {isExpanded ? ( | |||||
| <ExpandLessIcon fontSize="small" /> | |||||
| ) : ( | |||||
| <ExpandMoreIcon fontSize="small" /> | |||||
| )} | |||||
| </IconButton> | |||||
| ) : null} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2"> | |||||
| {row.itemCode || "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2"> | |||||
| {row.itemName || "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2"> | |||||
| {row.uom || "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {formatQty(row.totalReqQty)} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {formatQty(row.totalQty)} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| {hasJobOrders && ( | |||||
| <TableRow> | |||||
| <TableCell | |||||
| colSpan={6} | |||||
| sx={{ py: 0, borderBottom: 0 }} | |||||
| > | |||||
| <Collapse | |||||
| in={isExpanded} | |||||
| timeout="auto" | |||||
| unmountOnExit | |||||
| > | |||||
| <Box sx={{ py: 1.5, pl: 6, pr: 2 }}> | |||||
| <Table size="small"> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell sx={{ fontWeight: 600 }}> | |||||
| {t("Job Order Code")} | |||||
| </TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}> | |||||
| {t("Production Date")} | |||||
| </TableCell> | |||||
| <TableCell | |||||
| align="right" | |||||
| sx={{ fontWeight: 600 }} | |||||
| > | |||||
| {t("Stock Req. Qty")} | |||||
| </TableCell> | |||||
| <TableCell | |||||
| align="right" | |||||
| sx={{ fontWeight: 600 }} | |||||
| > | |||||
| {t("Production Qty")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {jobOrders.map((jo) => ( | |||||
| <TableRow | |||||
| key={`${rowKey}-jo-${jo.jobOrderId}`} | |||||
| > | |||||
| <TableCell> | |||||
| {jo.jobOrderId > 0 ? ( | |||||
| <MuiLink | |||||
| component={NextLink} | |||||
| href={`/jo/edit?id=${jo.jobOrderId}`} | |||||
| underline="hover" | |||||
| > | |||||
| {jo.jobOrderCode || | |||||
| `JO-${jo.jobOrderId}`} | |||||
| </MuiLink> | |||||
| ) : ( | |||||
| jo.jobOrderCode || "-" | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {formatProductionDate( | |||||
| jo.productionDate, | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {formatQty(jo.reqQty)} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {formatQty(jo.productionQty)} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </Box> | |||||
| </Collapse> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )} | |||||
| </React.Fragment> | |||||
| ); | |||||
| }) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| )} | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default DrinkProductionQtyDashboard; | |||||
| @@ -14,6 +14,7 @@ import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionse | |||||
| import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; | import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; | ||||
| import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard"; | import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard"; | ||||
| import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard"; | import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard"; | ||||
| import DrinkProductionQtyDashboard from "@/components/ProductionProcess/DrinkProductionQtyDashboard"; | |||||
| import type { PrinterCombo } from "@/app/api/settings/printer"; | import type { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| @@ -208,6 +209,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| <Tab label={t("Job Process Status Dashboard")} /> | <Tab label={t("Job Process Status Dashboard")} /> | ||||
| <Tab label={t("Operator KPI Dashboard")} /> | <Tab label={t("Operator KPI Dashboard")} /> | ||||
| <Tab label={t("Production Equipment Status Dashboard")} /> | <Tab label={t("Production Equipment Status Dashboard")} /> | ||||
| <Tab label={t("Drink Production Qty Dashboard")} /> | |||||
| </Tabs> | </Tabs> | ||||
| {tabIndex === 0 && ( | {tabIndex === 0 && ( | ||||
| @@ -256,7 +258,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| }} | }} | ||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 2 && ( | |||||
| {tabIndex === 2 && ( | |||||
| <ProductionProcessList | <ProductionProcessList | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| qcReady={true} | qcReady={true} | ||||
| @@ -288,6 +291,9 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| {tabIndex === 5 && ( | {tabIndex === 5 && ( | ||||
| <EquipmentStatusDashboard /> | <EquipmentStatusDashboard /> | ||||
| )} | )} | ||||
| {tabIndex === 6 && ( | |||||
| <DrinkProductionQtyDashboard /> | |||||
| )} | |||||
| </Box> | </Box> | ||||
| <QcStockInModal | <QcStockInModal | ||||
| session={sessionToken} | session={sessionToken} | ||||
| @@ -6,6 +6,7 @@ | |||||
| "scanAgain": "Scan again", | "scanAgain": "Scan again", | ||||
| "manualSearch": "Manual search", | "manualSearch": "Manual search", | ||||
| "itemCode": "Item code", | "itemCode": "Item code", | ||||
| "itemLot": "Item lot", | |||||
| "lotNo": "Lot no.", | "lotNo": "Lot no.", | ||||
| "search": "Trace", | "search": "Trace", | ||||
| "searching": "Tracing…", | "searching": "Tracing…", | ||||
| @@ -197,6 +198,7 @@ | |||||
| "from": "From", | "from": "From", | ||||
| "to": "To", | "to": "To", | ||||
| "material": "Material", | "material": "Material", | ||||
| "Item": "Item", | |||||
| "detailItemCode": "Item code", | "detailItemCode": "Item code", | ||||
| "detailItemName": "Item name", | "detailItemName": "Item name", | ||||
| "finishedItem": "Finished item", | "finishedItem": "Finished item", | ||||
| @@ -258,6 +260,13 @@ | |||||
| "code.usageType.PRODUCTION": "Production use", | "code.usageType.PRODUCTION": "Production use", | ||||
| "code.usageType.FG_DELIVERY": "FG delivery", | "code.usageType.FG_DELIVERY": "FG delivery", | ||||
| "code.usageType.MATERIAL": "Material issue", | "code.usageType.MATERIAL": "Material issue", | ||||
| "code.pickStatus.pending": "Pending", | |||||
| "code.pickStatus.consolidated": "Consolidated", | |||||
| "code.pickStatus.assigned": "Assigned", | |||||
| "code.pickStatus.released": "Released", | |||||
| "code.pickStatus.picking": "Picking", | |||||
| "code.pickStatus.completed": "Completed", | |||||
| "code.pickStatus.partially_completed": "Partially completed", | |||||
| "code.processingStatus.pending": "Pending", | "code.processingStatus.pending": "Pending", | ||||
| "code.processingStatus.completed": "Completed", | "code.processingStatus.completed": "Completed", | ||||
| "code.processingStatus.rejected": "Rejected", | "code.processingStatus.rejected": "Rejected", | ||||
| @@ -192,6 +192,7 @@ | |||||
| "Job Order Type": "Job Order Type", | "Job Order Type": "Job Order Type", | ||||
| "Job Order not found or has no items": "Job Order not found or has no items", | "Job Order not found or has no items": "Job Order not found or has no items", | ||||
| "Job Process Status Dashboard": "Job Process Status Dashboard", | "Job Process Status Dashboard": "Job Process Status Dashboard", | ||||
| "Drink Production Qty Dashboard": "Drink Production Qty Dashboard", | |||||
| "Job Type": "Job Type", | "Job Type": "Job Type", | ||||
| "Job dashboard PP status: cancelled": "Job dashboard PP status: cancelled", | "Job dashboard PP status: cancelled": "Job dashboard PP status: cancelled", | ||||
| "Job dashboard PP status: completed": "Job dashboard PP status: completed", | "Job dashboard PP status: completed": "Job dashboard PP status: completed", | ||||
| @@ -353,6 +354,7 @@ | |||||
| "Production Output Data Entry": "Production Output Data Entry", | "Production Output Data Entry": "Production Output Data Entry", | ||||
| "Production Priority": "Production Priority", | "Production Priority": "Production Priority", | ||||
| "Production Priority required!": "Production Priority required!", | "Production Priority required!": "Production Priority required!", | ||||
| "Production Qty": "Production Qty", | |||||
| "Production Process": "Production Process", | "Production Process": "Production Process", | ||||
| "Production Process Information": "Production Process Information", | "Production Process Information": "Production Process Information", | ||||
| "Production Process Line Remark": "Production Process Line Remark", | "Production Process Line Remark": "Production Process Line Remark", | ||||
| @@ -78,6 +78,10 @@ | |||||
| "Job Order and Product": "Job Order and Product", | "Job Order and Product": "Job Order and Product", | ||||
| "Job Order Production Process": "Job Order Production Process", | "Job Order Production Process": "Job Order Production Process", | ||||
| "Job Process Status Dashboard": "Job Process Status Dashboard", | "Job Process Status Dashboard": "Job Process Status Dashboard", | ||||
| "Drink Production Qty Dashboard": "Drink Production Qty Dashboard", | |||||
| "Expand job order details": "Expand job order details", | |||||
| "Collapse job order details": "Collapse job order details", | |||||
| "Goods Name": "Goods Name", | |||||
| "Job Type": "Job Type", | "Job Type": "Job Type", | ||||
| "Job process detail mode label": "Job process detail mode label", | "Job process detail mode label": "Job process detail mode label", | ||||
| "Job process detail: equipment": "Equipment", | "Job process detail: equipment": "Equipment", | ||||
| @@ -135,6 +139,7 @@ | |||||
| "Production Output Data": "Production Output Data", | "Production Output Data": "Production Output Data", | ||||
| "Production Output Data Entry": "Production Output Data Entry", | "Production Output Data Entry": "Production Output Data Entry", | ||||
| "Production Priority": "Production Priority", | "Production Priority": "Production Priority", | ||||
| "Production Qty": "Production Qty", | |||||
| "Production Process": "Production Process", | "Production Process": "Production Process", | ||||
| "Production Process Line Remark": "Production Process Line Remark", | "Production Process Line Remark": "Production Process Line Remark", | ||||
| "Production Process Steps": "Production Process Steps", | "Production Process Steps": "Production Process Steps", | ||||
| @@ -6,6 +6,7 @@ | |||||
| "scanAgain": "重新掃描", | "scanAgain": "重新掃描", | ||||
| "manualSearch": "手動查詢", | "manualSearch": "手動查詢", | ||||
| "itemCode": "貨品編號", | "itemCode": "貨品編號", | ||||
| "itemLot": "貨品批號", | |||||
| "lotNo": "批號", | "lotNo": "批號", | ||||
| "search": "追溯", | "search": "追溯", | ||||
| "searching": "查詢中…", | "searching": "查詢中…", | ||||
| @@ -259,6 +260,13 @@ | |||||
| "code.usageType.PRODUCTION": "生產耗用", | "code.usageType.PRODUCTION": "生產耗用", | ||||
| "code.usageType.FG_DELIVERY": "成品出貨", | "code.usageType.FG_DELIVERY": "成品出貨", | ||||
| "code.usageType.MATERIAL": "原料提料", | "code.usageType.MATERIAL": "原料提料", | ||||
| "code.pickStatus.pending": "待處理", | |||||
| "code.pickStatus.consolidated": "已合併", | |||||
| "code.pickStatus.assigned": "已指派", | |||||
| "code.pickStatus.released": "已放單", | |||||
| "code.pickStatus.picking": "提料中", | |||||
| "code.pickStatus.completed": "已完成", | |||||
| "code.pickStatus.partially_completed": "部分完成", | |||||
| "code.processingStatus.pending": "待處理", | "code.processingStatus.pending": "待處理", | ||||
| "code.processingStatus.completed": "已完成", | "code.processingStatus.completed": "已完成", | ||||
| "code.processingStatus.rejected": "已拒收", | "code.processingStatus.rejected": "已拒收", | ||||
| @@ -10,6 +10,7 @@ | |||||
| "Actual Pick Qty": "實際提料數量", | "Actual Pick Qty": "實際提料數量", | ||||
| "Add Bag": "新增包裝袋", | "Add Bag": "新增包裝袋", | ||||
| "Add Record": "添加記錄", | "Add Record": "添加記錄", | ||||
| "Just Pass": "通過", | |||||
| "Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中", | "Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中", | ||||
| "Add some entries!": "請添加條目", | "Add some entries!": "請添加條目", | ||||
| "All": "全部", | "All": "全部", | ||||
| @@ -194,6 +195,7 @@ | |||||
| "Job Order Type": "工單類型", | "Job Order Type": "工單類型", | ||||
| "Job Order not found or has no items": "工單不存在或沒有物品", | "Job Order not found or has no items": "工單不存在或沒有物品", | ||||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | "Job Process Status Dashboard": "儀表板 - 工單狀態", | ||||
| "Drink Production Qty Dashboard": "儀表板 - 飲料生產量數", | |||||
| "Job Type": "工單類型", | "Job Type": "工單類型", | ||||
| "Job dashboard PP status: cancelled": "工序已取消", | "Job dashboard PP status: cancelled": "工序已取消", | ||||
| "Job dashboard PP status: completed": "工序完成", | "Job dashboard PP status: completed": "工序完成", | ||||
| @@ -355,6 +357,7 @@ | |||||
| "Production Output Data Entry": "生產輸出數據輸入", | "Production Output Data Entry": "生產輸出數據輸入", | ||||
| "Production Priority": "生產優先序", | "Production Priority": "生產優先序", | ||||
| "Production Priority required!": "生產優先度必填!", | "Production Priority required!": "生產優先度必填!", | ||||
| "Production Qty": "生產數量", | |||||
| "Production Process": "工藝流程", | "Production Process": "工藝流程", | ||||
| "Production Process Information": "生產流程信息", | "Production Process Information": "生產流程信息", | ||||
| "Production Process Line Remark": "工藝明細", | "Production Process Line Remark": "工藝明細", | ||||
| @@ -78,6 +78,10 @@ | |||||
| "Job Order and Product": "工單及貨品", | "Job Order and Product": "工單及貨品", | ||||
| "Job Order Production Process": "工單生產流程", | "Job Order Production Process": "工單生產流程", | ||||
| "Job Process Status Dashboard": "儀表板 - 工單狀態", | "Job Process Status Dashboard": "儀表板 - 工單狀態", | ||||
| "Drink Production Qty Dashboard": "儀表板 - 飲料生產量數", | |||||
| "Expand job order details": "展開工單明細", | |||||
| "Collapse job order details": "收合工單明細", | |||||
| "Goods Name": "貨品名稱", | |||||
| "Job Type": "工單類型", | "Job Type": "工單類型", | ||||
| "Job process detail mode label": "工序格顯示", | "Job process detail mode label": "工序格顯示", | ||||
| "Job process detail: equipment": "設備", | "Job process detail: equipment": "設備", | ||||
| @@ -135,6 +139,7 @@ | |||||
| "Production Output Data": "生產輸出數據", | "Production Output Data": "生產輸出數據", | ||||
| "Production Output Data Entry": "生產輸出數據輸入", | "Production Output Data Entry": "生產輸出數據輸入", | ||||
| "Production Priority": "生產優先序", | "Production Priority": "生產優先序", | ||||
| "Production Qty": "生產數量", | |||||
| "Production Process": "工藝流程", | "Production Process": "工藝流程", | ||||
| "Production Process Line Remark": "工藝明細", | "Production Process Line Remark": "工藝明細", | ||||
| "Production Process Steps": "生產流程步驟", | "Production Process Steps": "生產流程步驟", | ||||