Переглянути джерело

item trace , drink produce dashboard

production
tommy 1 день тому
джерело
коміт
f906a9e747
17 змінених файлів з 521 додано та 46 видалено
  1. +4
    -1
      src/app/api/itemTracing/index.ts
  2. +30
    -0
      src/app/api/jo/actions.ts
  3. +9
    -18
      src/components/ItemTracing/ItemTracingFlowGraphSearch.tsx
  4. +13
    -18
      src/components/ItemTracing/ItemTracingSections.tsx
  5. +36
    -2
      src/components/ItemTracing/TraceFlowNodes.tsx
  6. +1
    -0
      src/components/ItemTracing/buildExtendedTraceGraphNodes.ts
  7. +16
    -4
      src/components/ItemTracing/buildTraceGraphNodes.ts
  8. +13
    -0
      src/components/ItemTracing/traceLabelUtils.ts
  9. +11
    -2
      src/components/ItemTracing/traceStockTakeUtils.ts
  10. +349
    -0
      src/components/ProductionProcess/DrinkProductionQtyDashboard.tsx
  11. +7
    -1
      src/components/ProductionProcess/ProductionProcessPage.tsx
  12. +9
    -0
      src/i18n/en/itemTracing.json
  13. +2
    -0
      src/i18n/en/jo.json
  14. +5
    -0
      src/i18n/en/productionProcess.json
  15. +8
    -0
      src/i18n/zh/itemTracing.json
  16. +3
    -0
      src/i18n/zh/jo.json
  17. +5
    -0
      src/i18n/zh/productionProcess.json

+ 4
- 1
src/app/api/itemTracing/index.ts Переглянути файл

@@ -138,6 +138,7 @@ export interface ItemLotTraceMovement {
relationshipId?: number | null;
doOutboundIsExtra?: boolean;
doOutboundIsReplenish?: boolean;
doOutboundQtyChanged?: boolean;
processingStatus?: string;
matchStatus?: string;
}
@@ -199,7 +200,7 @@ export interface ItemLotTraceStockTakeEvent {
stockTakeRoundName?: string;
varianceQty: number;
beforeQty: number;
afterQty: number;
afterQty?: number | null;
approver: string;
timestamp: string | null;
recordDetail?: ItemLotTraceStockTakeRecordDetail | null;
@@ -409,6 +410,8 @@ export interface ItemLotTraceDoDelivery {
doPickOrderRecordId?: number | null;
isExtra?: boolean;
isReplenish?: boolean;
/** True when pick_order_line.qty ≠ SUM(stock_out_line.qty) for that line (改數). */
qtyChanged?: boolean;
}

export interface ItemLotTraceReturnEvent {


+ 30
- 0
src/app/api/jo/actions.ts Переглянути файл

@@ -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 =====

export interface EquipmentStatusProcessInfo {


+ 9
- 18
src/components/ItemTracing/ItemTracingFlowGraphSearch.tsx Переглянути файл

@@ -91,31 +91,22 @@ const ItemTracingFlowGraphSearch: React.FC<Props> = ({
}}
sx={{
"& .MuiOutlinedInput-root": {
justifyContent: hasQuery ? "flex-start" : "center",
alignItems: "center",
},
"& .MuiOutlinedInput-input": {
textAlign: hasQuery ? "left" : "center",
color: "text.secondary",
...(hasQuery
? {}
: {
flex: "0 1 auto",
width: "auto",
maxWidth: "90%",
}),
py: "8.5px",
lineHeight: 1.4375,
},
"& .MuiOutlinedInput-input::placeholder": {
color: "text.secondary",
color: "text.disabled",
opacity: 1,
textAlign: "center",
},
...(!hasQuery
? {
"& .MuiInputAdornment-positionStart": {
marginRight: 0.75,
},
}
: {}),
"& .MuiInputAdornment-root": {
height: "100%",
maxHeight: "none",
alignItems: "center",
},
}}
/>
{hasQuery && (


+ 13
- 18
src/components/ItemTracing/ItemTracingSections.tsx Переглянути файл

@@ -459,11 +459,6 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
align: "right",
value: (row) => formatQty(row.qty, stockUom),
},
{
key: "ref",
label: t("ref"),
value: (row) => row.transferCode,
},
{
key: "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} />
),
},
{ 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",
label: t("pickOrder"),
@@ -579,8 +574,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
<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",
label: t("materialQty"),
@@ -610,8 +605,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
<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",
label: t("fgQty"),
@@ -627,10 +622,10 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
], [t, stockUom]);

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",
label: `${t("material")} name`,
label: t("detailItemName"),
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",
label: t("lotNo"),
@@ -703,7 +698,7 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
const joPickLineCols = useMemo((): FilterableColumnDef<JoPickLine>[] => [
{
key: "material",
label: t("material"),
label: t("Item"),
value: (line) =>
line.itemName ? `${line.itemCode} · ${line.itemName}` : line.itemCode,
},
@@ -719,8 +714,8 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
align: "right",
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 (
<Paper variant="outlined">
@@ -878,7 +873,7 @@ const ItemTracingSections: React.FC<Props> = ({ data, compiledGraph, onTrace })
{" · "}
<Chip
size="small"
label={po.status}
label={tr.pickStatus(po.status)}
variant="outlined"
sx={{ ml: 0.5 }}
/>


+ 36
- 2
src/components/ItemTracing/TraceFlowNodes.tsx Переглянути файл

@@ -31,7 +31,7 @@ import { traceFlowHandleTopPercent } from "./traceFlowEdgeLayout";
import { kindColor, kindLabelKey } from "./traceFlowNodeUtils";
import { formatQty, formatSignedQty } from "./traceQtyUtils";
import { buildStockTakeLifecycleStages } from "./traceStockTakeUtils";
import { pickStatusValueColor } from "./traceLabelUtils";
import { pickStatusValueColor, resolveDoOutboundQtyColor } from "./traceLabelUtils";

const PHASE_LABEL_INNER = 96;
/** 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}
</Typography>
) : null}
{node.qty != null && (
{(node.qty != null || node.kind === "STOCK_TAKE") && (
<Typography
variant="body2"
fontWeight={600}
@@ -366,6 +366,15 @@ export const TraceFlowEventNode = memo(function TraceFlowEventNode({
display: "-webkit-box",
WebkitLineClamp: 2,
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"
@@ -385,6 +394,31 @@ export const TraceFlowEventNode = memo(function TraceFlowEventNode({
: formatQty(node.qty, node.uom)}
</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 ? (
<ItemTracingLotTraceLink
label={


+ 1
- 0
src/components/ItemTracing/buildExtendedTraceGraphNodes.ts Переглянути файл

@@ -193,6 +193,7 @@ export const buildExtendedTraceGraphNodes = (
categoryLabel: labels.categoryOutbound,
doOutboundIsExtra: chipFlags.isExtra,
doOutboundIsReplenish: chipFlags.isReplenish,
doOutboundQtyChanged: d.qtyChanged === true,
outboundKindLabel,
warehouseCode: scope?.defaultWarehouseCode,
details: [


+ 16
- 4
src/components/ItemTracing/buildTraceGraphNodes.ts Переглянути файл

@@ -123,6 +123,8 @@ export interface TraceGraphNode {
/** 加單 / 補貨 flags for DO_OUT title chips (detail uses outboundKindLabel). */
doOutboundIsExtra?: boolean;
doOutboundIsReplenish?: boolean;
/** 改數 — pick line qty ≠ sum of stock-out lines for that pick line. */
doOutboundQtyChanged?: boolean;
outboundKindLabel?: string;
/** Longer status explanation in detail panel (e.g. transfer inbound). */
putawayStatusDetail?: string;
@@ -156,6 +158,8 @@ export interface TraceGraphNode {
adjustmentDirection?: string;
/** Stocktake lifecycle record detail (for expandable STOCK_TAKE nodes). */
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. */
inventoryLotId?: number;
/** Translated pick-line statuses for JO pick cards. */
@@ -642,6 +646,7 @@ export const buildTraceGraphNodes = (
{
isExtra: boolean;
isReplenish: boolean;
qtyChanged: boolean;
consoCode?: string;
}
>();
@@ -660,6 +665,7 @@ export const buildTraceGraphNodes = (
doMetaByPickCode.set(code, {
isExtra: chipFlags.isExtra,
isReplenish: chipFlags.isReplenish,
qtyChanged: d.qtyChanged === true,
consoCode: d.consoCode?.trim() || code,
});
});
@@ -884,6 +890,9 @@ export const buildTraceGraphNodes = (
: undefined,
doOutboundIsExtra: chipFlags?.isExtra,
doOutboundIsReplenish: chipFlags?.isReplenish,
doOutboundQtyChanged: isDoOut
? (pickMeta?.qtyChanged ?? m.doOutboundQtyChanged === true)
: undefined,
warehouseCode: m.warehouseCode?.trim() || undefined,
categoryLabel: categoryForKind(kind, labels),
...(isJoOut
@@ -1022,10 +1031,12 @@ export const buildTraceGraphNodes = (
const bookQty = resolveStockTakeBookQty(detail, e.beforeQty);
const acceptedQty = resolveStockTakeAcceptedQty(detail, e.afterQty);
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({
id: lb
? `${idPfx}st-${i}-${e.stockTakeCode}`
@@ -1043,6 +1054,7 @@ export const buildTraceGraphNodes = (
traceLotNo: e.lotNo?.trim() || data.lot.lotNo?.trim() || undefined,
categoryLabel: labels.categoryStockTake,
stockTakeRecordDetail: detail,
stockTakeVarianceQty: varianceQty,
details: [
field(labels.detailStockTakeCode, e.stockTakeCode),
fieldIf(labels.detailStockTakeRound, roundLabel || undefined),


+ 13
- 0
src/components/ItemTracing/traceLabelUtils.ts Переглянути файл

@@ -176,3 +176,16 @@ export const resolveDoOutboundChipFlags = (
isExtra: resolveTraceDoOutboundIsExtra(source),
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";
};

+ 11
- 2
src/components/ItemTracing/traceStockTakeUtils.ts Переглянути файл

@@ -34,14 +34,16 @@ export const resolveStockTakeBookQty = (
/**
* Accepted physical qty used for variance / posting decision.
* 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 = (
detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
fallbackAfterQty?: number | null,
): number | null => {
const pick = (n: number | null | undefined) =>
n != null && !Number.isNaN(Number(n)) ? Number(n) : null;

if (detail) {
const pick = (n: number | null | undefined) =>
n != null && !Number.isNaN(Number(n)) ? Number(n) : null;
switch (detail.lastSelect) {
case 3: {
const q = pick(detail.approverQty);
@@ -67,6 +69,13 @@ export const resolveStockTakeAcceptedQty = (
if (fromSecond != null) return fromSecond;
const fromFirst = pick(detail.pickerFirstQty);
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))) {
return Number(fallbackAfterQty);


+ 349
- 0
src/components/ProductionProcess/DrinkProductionQtyDashboard.tsx Переглянути файл

@@ -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")}&nbsp;|&nbsp;
{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;

+ 7
- 1
src/components/ProductionProcess/ProductionProcessPage.tsx Переглянути файл

@@ -14,6 +14,7 @@ import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionse
import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus";
import OperatorKpiDashboard from "@/components/ProductionProcess/OperatorKpiDashboard";
import EquipmentStatusDashboard from "@/components/ProductionProcess/EquipmentStatusDashboard";
import DrinkProductionQtyDashboard from "@/components/ProductionProcess/DrinkProductionQtyDashboard";
import type { PrinterCombo } from "@/app/api/settings/printer";
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("Operator KPI Dashboard")} />
<Tab label={t("Production Equipment Status Dashboard")} />
<Tab label={t("Drink Production Qty Dashboard")} />
</Tabs>

{tabIndex === 0 && (
@@ -256,7 +258,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
}}
/>
)}
{tabIndex === 2 && (

{tabIndex === 2 && (
<ProductionProcessList
printerCombo={printerCombo}
qcReady={true}
@@ -288,6 +291,9 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
{tabIndex === 5 && (
<EquipmentStatusDashboard />
)}
{tabIndex === 6 && (
<DrinkProductionQtyDashboard />
)}
</Box>
<QcStockInModal
session={sessionToken}


+ 9
- 0
src/i18n/en/itemTracing.json Переглянути файл

@@ -6,6 +6,7 @@
"scanAgain": "Scan again",
"manualSearch": "Manual search",
"itemCode": "Item code",
"itemLot": "Item lot",
"lotNo": "Lot no.",
"search": "Trace",
"searching": "Tracing…",
@@ -197,6 +198,7 @@
"from": "From",
"to": "To",
"material": "Material",
"Item": "Item",
"detailItemCode": "Item code",
"detailItemName": "Item name",
"finishedItem": "Finished item",
@@ -258,6 +260,13 @@
"code.usageType.PRODUCTION": "Production use",
"code.usageType.FG_DELIVERY": "FG delivery",
"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.completed": "Completed",
"code.processingStatus.rejected": "Rejected",


+ 2
- 0
src/i18n/en/jo.json Переглянути файл

@@ -192,6 +192,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": "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",
@@ -353,6 +354,7 @@
"Production Output Data Entry": "Production Output Data Entry",
"Production Priority": "Production Priority",
"Production Priority required!": "Production Priority required!",
"Production Qty": "Production Qty",
"Production Process": "Production Process",
"Production Process Information": "Production Process Information",
"Production Process Line Remark": "Production Process Line Remark",


+ 5
- 0
src/i18n/en/productionProcess.json Переглянути файл

@@ -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": "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 process detail mode label": "Job process detail mode label",
"Job process detail: equipment": "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 Qty",
"Production Process": "Production Process",
"Production Process Line Remark": "Production Process Line Remark",
"Production Process Steps": "Production Process Steps",


+ 8
- 0
src/i18n/zh/itemTracing.json Переглянути файл

@@ -6,6 +6,7 @@
"scanAgain": "重新掃描",
"manualSearch": "手動查詢",
"itemCode": "貨品編號",
"itemLot": "貨品批號",
"lotNo": "批號",
"search": "追溯",
"searching": "查詢中…",
@@ -259,6 +260,13 @@
"code.usageType.PRODUCTION": "生產耗用",
"code.usageType.FG_DELIVERY": "成品出貨",
"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.completed": "已完成",
"code.processingStatus.rejected": "已拒收",


+ 3
- 0
src/i18n/zh/jo.json Переглянути файл

@@ -10,6 +10,7 @@
"Actual Pick Qty": "實際提料數量",
"Add Bag": "新增包裝袋",
"Add Record": "添加記錄",
"Just Pass": "通過",
"Add Selected Items to Created Items": "將已選擇的物品添加到創建的物品中",
"Add some entries!": "請添加條目",
"All": "全部",
@@ -194,6 +195,7 @@
"Job Order Type": "工單類型",
"Job Order not found or has no items": "工單不存在或沒有物品",
"Job Process Status Dashboard": "儀表板 - 工單狀態",
"Drink Production Qty Dashboard": "儀表板 - 飲料生產量數",
"Job Type": "工單類型",
"Job dashboard PP status: cancelled": "工序已取消",
"Job dashboard PP status: completed": "工序完成",
@@ -355,6 +357,7 @@
"Production Output Data Entry": "生產輸出數據輸入",
"Production Priority": "生產優先序",
"Production Priority required!": "生產優先度必填!",
"Production Qty": "生產數量",
"Production Process": "工藝流程",
"Production Process Information": "生產流程信息",
"Production Process Line Remark": "工藝明細",


+ 5
- 0
src/i18n/zh/productionProcess.json Переглянути файл

@@ -78,6 +78,10 @@
"Job Order and Product": "工單及貨品",
"Job Order Production Process": "工單生產流程",
"Job Process Status Dashboard": "儀表板 - 工單狀態",
"Drink Production Qty Dashboard": "儀表板 - 飲料生產量數",
"Expand job order details": "展開工單明細",
"Collapse job order details": "收合工單明細",
"Goods Name": "貨品名稱",
"Job Type": "工單類型",
"Job process detail mode label": "工序格顯示",
"Job process detail: equipment": "設備",
@@ -135,6 +139,7 @@
"Production Output Data": "生產輸出數據",
"Production Output Data Entry": "生產輸出數據輸入",
"Production Priority": "生產優先序",
"Production Qty": "生產數量",
"Production Process": "工藝流程",
"Production Process Line Remark": "工藝明細",
"Production Process Steps": "生產流程步驟",


Завантаження…
Відмінити
Зберегти