Преглед изворни кода

drink output qty

fix負數倉
tommy пре 2 недеља
родитељ
комит
087a940c40
6 измењених фајлова са 337 додато и 44 уклоњено
  1. +1
    -1
      src/app/(main)/production/page.tsx
  2. +1
    -1
      src/app/(main)/productionProcess/page.tsx
  3. +32
    -0
      src/app/api/jo/actions.ts
  4. +284
    -37
      src/components/ProductionProcess/DrinkProductionQtyDashboard.tsx
  5. +9
    -2
      src/i18n/en/productionProcess.json
  6. +10
    -3
      src/i18n/zh/productionProcess.json

+ 1
- 1
src/app/(main)/production/page.tsx Прегледај датотеку

@@ -38,7 +38,7 @@ const production: React.FC = async () => {
{t("Create Process")}
</Button> */}
</Stack>
<I18nProvider namespaces={["production","productionProcess","navigation","common","purchaseOrder","jo","dashboard"]}>
<I18nProvider namespaces={["production","productionProcess","navigation","common","purchaseOrder","jo","do","dashboard"]}>
<ProductionProcessPage printerCombo={printerCombo} /> {/* Use new component */}
</I18nProvider>
</>


+ 1
- 1
src/app/(main)/productionProcess/page.tsx Прегледај датотеку

@@ -36,7 +36,7 @@ const productionProcess: React.FC = async () => {
{t("Create Process")}
</Button> */}
</Stack>
<I18nProvider namespaces={["productionProcess","navigation","common","purchaseOrder","jo","dashboard"]}>
<I18nProvider namespaces={["productionProcess","navigation","common","purchaseOrder","jo","do","dashboard"]}>
<Suspense fallback={<ProductionProcessLoading />}>
<ProductionProcessPage printerCombo={printerCombo} />
</Suspense>


+ 32
- 0
src/app/api/jo/actions.ts Прегледај датотеку

@@ -1741,6 +1741,38 @@ export const fetchDrinkProductionQty = cache(
},
);

export interface DrinkShipmentQtyDeliveryDetail {
deliveryOrderId: number;
deliveryOrderCode?: string | null;
deliveryDate?: string | null;
shopCode?: string | null;
shopName?: string | null;
deliveryOrderStatus?: string | null;
orderQty: number;
shippedQty: number;
}

export interface DrinkShipmentQtyResponse {
itemCode?: string | null;
itemName?: string | null;
uom?: string | null;
totalOrderQty: number;
totalShippedQty: number;
deliveries?: DrinkShipmentQtyDeliveryDetail[];
}

export const fetchDrinkShipmentQty = 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/DrinkShipmentQty${qs ? `?${qs}` : ""}`;

return serverFetchJson<DrinkShipmentQtyResponse[]>(url, {
method: "GET",
next: { tags: ["drinkShipmentQty"] },
});
});

// ===== Equipment Status Dashboard =====

export interface EquipmentStatusProcessInfo {


+ 284
- 37
src/components/ProductionProcess/DrinkProductionQtyDashboard.tsx Прегледај датотеку

@@ -37,8 +37,11 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import {
fetchDrinkProductionQty,
fetchDrinkShipmentQty,
DrinkProductionQtyResponse,
DrinkProductionQtyJobOrderDetail,
DrinkShipmentQtyResponse,
DrinkShipmentQtyDeliveryDetail,
} from "@/app/api/jo/actions";
import { arrayToDayjs } from "@/app/utils/formatUtil";
import { exportDrinkProductionQtyXlsx } from "@/components/ProductionProcess/exportDrinkProductionQtyXlsx";
@@ -55,7 +58,7 @@ const JO_STATUS_FILTER_VALUES = [
"completed",
] as const;

type DrinkViewMode = "actual" | "planned";
type DrinkViewMode = "actual" | "planned" | "shipment";

const formatQty = (qty: number | null | undefined): string => {
if (qty === null || qty === undefined || Number.isNaN(qty)) return "-";
@@ -100,10 +103,16 @@ const ProcessSummaryTimeText: React.FC<{ value: unknown }> = ({ value }) => {
const getRowKey = (row: DrinkProductionQtyResponse, idx: number): string =>
`${row.itemCode || "unknown"}-${idx}`;

const getShipmentRowKey = (row: DrinkShipmentQtyResponse, idx: number): string =>
`ship-${row.itemCode || "unknown"}-${idx}`;

/** FP-MTMS Version Checklist | Functions Ref. No. 26 | v1.0.0 | 2026-07-20 */
const DrinkProductionQtyDashboard: React.FC = () => {
const { t } = useTranslation(["common", "jo", "productionProcess"]);
const { t } = useTranslation(["common", "jo", "do", "productionProcess"]);
const [data, setData] = useState<DrinkProductionQtyResponse[]>([]);
const [shipmentData, setShipmentData] = useState<DrinkShipmentQtyResponse[]>(
[],
);
const [loading, setLoading] = useState<boolean>(true);
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const [joStatusFilter, setJoStatusFilter] = useState<string>("");
@@ -117,21 +126,62 @@ const DrinkProductionQtyDashboard: React.FC = () => {
);

const isPlanned = viewMode === "planned";
const isShipment = viewMode === "shipment";

const qtyHeaders = ((): { left: string; right: string } => {
switch (viewMode) {
case "shipment":
return {
left: t("Shipment Order Qty"),
right: t("Shipped Qty"),
};
case "planned":
return {
left: t("Planned Output Qty"),
right: t("Actual Output Qty"),
};
case "actual":
return {
left: t("Stock Req. Qty"),
right: t("Production Qty"),
};
default: {
const _exhaustive: never = viewMode;
return _exhaustive;
}
}
})();

const loadData = useCallback(async () => {
setLoading(true);
const dateStr = selectedDate.format("YYYY-MM-DD");
try {
const result = await fetchDrinkProductionQty(
selectedDate.format("YYYY-MM-DD"),
viewMode,
);
setData(result || []);
switch (viewMode) {
case "shipment": {
const result = await fetchDrinkShipmentQty(dateStr);
setShipmentData(result || []);
setData([]);
break;
}
case "actual":
case "planned": {
const result = await fetchDrinkProductionQty(dateStr, viewMode);
setData(result || []);
setShipmentData([]);
break;
}
default: {
const _exhaustive: never = viewMode;
return _exhaustive;
}
}
setExpandedRowKeys(new Set());
setLastDataRefreshTime(dayjs());
refreshCountRef.current += 1;
} catch (error) {
console.error("Error fetching drink production qty:", error);
setData([]);
setShipmentData([]);
setExpandedRowKeys(new Set());
} finally {
setLoading(false);
@@ -190,6 +240,33 @@ const DrinkProductionQtyDashboard: React.FC = () => {
);
};

const renderDoStatusChip = (status: string | null | undefined) => {
if (!status) return <>—</>;
const normalized = status.toLowerCase();
let label: string;
switch (normalized) {
case "pending":
label = t("Drink do status pending");
break;
case "receiving":
label = t("Drink do status receiving");
break;
case "completed":
label = t("Drink do status completed");
break;
default:
label = t(status, { ns: "do", defaultValue: status });
break;
}
return (
<Chip
size="small"
label={label}
sx={{ height: 22 }}
/>
);
};

return (
<Card sx={{ mb: 2 }}>
<CardContent>
@@ -216,29 +293,31 @@ const DrinkProductionQtyDashboard: React.FC = () => {
/>
</LocalizationProvider>

<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel id="drink-jo-status-filter-label">
{t("Job Order Status")}
</InputLabel>
<Select
labelId="drink-jo-status-filter-label"
id="drink-jo-status-filter"
label={t("Job Order Status")}
value={joStatusFilter}
onChange={(e) => {
setJoStatusFilter(String(e.target.value));
}}
>
<MenuItem value="">
<em>{t("All")}</em>
</MenuItem>
{JO_STATUS_FILTER_VALUES.map((v) => (
<MenuItem key={v} value={v}>
{t(v, { ns: "jo" })}
{!isShipment && (
<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel id="drink-jo-status-filter-label">
{t("Job Order Status")}
</InputLabel>
<Select
labelId="drink-jo-status-filter-label"
id="drink-jo-status-filter"
label={t("Job Order Status")}
value={joStatusFilter}
onChange={(e) => {
setJoStatusFilter(String(e.target.value));
}}
>
<MenuItem value="">
<em>{t("All")}</em>
</MenuItem>
))}
</Select>
</FormControl>
{JO_STATUS_FILTER_VALUES.map((v) => (
<MenuItem key={v} value={v}>
{t(v, { ns: "jo" })}
</MenuItem>
))}
</Select>
</FormControl>
)}

<Box sx={{ flexGrow: 1 }} />

@@ -246,7 +325,9 @@ const DrinkProductionQtyDashboard: React.FC = () => {
variant="outlined"
size="small"
startIcon={<FileDownloadIcon />}
disabled={loading || data.length === 0}
disabled={
loading || (isShipment ? shipmentData.length === 0 : data.length === 0)
}
sx={{ display: "none" }}
onClick={() => {
exportDrinkProductionQtyXlsx({
@@ -298,6 +379,9 @@ const DrinkProductionQtyDashboard: React.FC = () => {
<ToggleButton value="planned">
{t("Drink detail mode: planned")}
</ToggleButton>
<ToggleButton value="shipment">
{t("Drink detail mode: shipment")}
</ToggleButton>
</ToggleButtonGroup>
</Stack>

@@ -347,22 +431,185 @@ const DrinkProductionQtyDashboard: React.FC = () => {
</TableCell>
<TableCell align="right" sx={{ width: 140 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{isPlanned
? t("Planned Output Qty")
: t("Stock Req. Qty")}
{qtyHeaders.left}
</Typography>
</TableCell>
<TableCell align="right" sx={{ width: 140 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{isPlanned
? t("Actual Output Qty")
: t("Production Qty")}
{qtyHeaders.right}
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.length === 0 ? (
{isShipment ? (
shipmentData.length === 0 ? (
<TableRow>
<TableCell colSpan={6} align="center">
<Typography
variant="body2"
sx={{ py: 2, color: "text.secondary" }}
>
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
shipmentData.map((row, idx) => {
const rowKey = getShipmentRowKey(row, idx);
const deliveries: DrinkShipmentQtyDeliveryDetail[] =
row.deliveries ?? [];
const isExpanded = expandedRowKeys.has(rowKey);
const hasExpandable = deliveries.length > 0;

return (
<React.Fragment key={rowKey}>
<TableRow hover={hasExpandable}>
<TableCell padding="checkbox">
{hasExpandable ? (
<IconButton
size="small"
aria-label={
isExpanded
? t("Collapse delivery order details")
: t("Expand delivery 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.totalOrderQty)}
</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="body2">
{formatQty(row.totalShippedQty)}
</Typography>
</TableCell>
</TableRow>
{hasExpandable && (
<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("Delivery Order Code", {
ns: "do",
})}
</TableCell>
<TableCell sx={{ fontWeight: 600 }}>
{t("Delivery Order Status", {
ns: "do",
})}
</TableCell>
<TableCell sx={{ fontWeight: 600 }}>
{t("Shop Name", { ns: "do" })}
</TableCell>
<TableCell sx={{ fontWeight: 600 }}>
{t("Delivery Date", { ns: "do" })}
</TableCell>
<TableCell
align="right"
sx={{ fontWeight: 600 }}
>
{t("Shipment Order Qty")}
</TableCell>
<TableCell
align="right"
sx={{ fontWeight: 600 }}
>
{t("Shipped Qty")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{deliveries.map((delivery) => (
<TableRow
key={`${rowKey}-do-${delivery.deliveryOrderId}`}
>
<TableCell>
{delivery.deliveryOrderId > 0 ? (
<MuiLink
component={NextLink}
href={`/do/edit?id=${delivery.deliveryOrderId}`}
underline="hover"
>
{delivery.deliveryOrderCode ||
`DO-${delivery.deliveryOrderId}`}
</MuiLink>
) : (
delivery.deliveryOrderCode ||
"-"
)}
</TableCell>
<TableCell>
{renderDoStatusChip(
delivery.deliveryOrderStatus,
)}
</TableCell>
<TableCell>
{delivery.shopName ||
delivery.shopCode ||
"-"}
</TableCell>
<TableCell>
{formatProductionDate(
delivery.deliveryDate,
)}
</TableCell>
<TableCell align="right">
{formatQty(delivery.orderQty)}
</TableCell>
<TableCell align="right">
{formatQty(delivery.shippedQty)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})
)
) : data.length === 0 ? (
<TableRow>
<TableCell colSpan={6} align="center">
<Typography


+ 9
- 2
src/i18n/en/productionProcess.json Прегледај датотеку

@@ -98,6 +98,14 @@
"Drink detail mode label": "Detail display",
"Drink detail mode: actual": "Actual production",
"Drink detail mode: planned": "Planned production",
"Drink detail mode: shipment": "Shipment qty",
"Shipment Order Qty": "Order qty",
"Shipped Qty": "Shipped today",
"Drink do status pending": "Pending",
"Drink do status receiving": "Released",
"Drink do status completed": "Completed",
"Expand delivery order details": "Expand delivery order details",
"Collapse delivery order details": "Collapse delivery order details",
"Planned Output Qty": "Planned output",
"Actual Output Qty": "Actual output",
"Latest Start By": "Latest start by",
@@ -123,8 +131,7 @@
"Job process detail: handler": "Handler",
"Job process detail: process name": "Process Name",
"Job process detail: time": "Time",
"Just Pass": "Skip",
"Auto Pass": "Auto Skipped",
"Just Pass": "Just Pass",
"Last updated": "Last updated",
"Lines with insufficient stock: ": "Lines with insufficient stock: ",
"Lines with sufficient stock: ": "Lines with sufficient stock: ",


+ 10
- 3
src/i18n/zh/productionProcess.json Прегледај датотеку

@@ -32,7 +32,7 @@
"Confirm": "確認",
"Confirm cancel job order": "確認取消工單",
"Confirm delete job order": "確認刪除工單",
"Confirm to Pass this Process?": "確認要過此工序嗎?",
"Confirm to Pass this Process?": "確認要過此工序嗎?",
"Confirm to update this Job Order?": "確認要完成此工單嗎?",
"Consumed Qty": "消耗數量",
"Continue": "繼續",
@@ -98,6 +98,14 @@
"Drink detail mode label": "明細顯示",
"Drink detail mode: actual": "實際生產",
"Drink detail mode: planned": "預計生產",
"Drink detail mode: shipment": "出貨數量",
"Shipment Order Qty": "預期出貨數量",
"Shipped Qty": "當前出貨數量",
"Drink do status pending": "待處理",
"Drink do status receiving": "已放單",
"Drink do status completed": "已完成",
"Expand delivery order details": "展開出貨明細",
"Collapse delivery order details": "收合出貨明細",
"Planned Output Qty": "預計生產數量",
"Actual Output Qty": "實際生產數量",
"Latest Start By": "最晚開工時間",
@@ -123,8 +131,7 @@
"Job process detail: handler": "員工",
"Job process detail: process name": "工序",
"Job process detail: time": "時間",
"Just Pass": "跳過",
"Auto Pass": "已自動跳過",
"Just Pass": "已完成",
"Last updated": "最後更新",
"Lines with insufficient stock: ": "未能提料項目數量: ",
"Lines with sufficient stock: ": "可提料項目數量: ",


Loading…
Откажи
Сачувај