Quellcode durchsuchen

update

MergeProblem1
CANCERYS\kw093 vor 4 Tagen
Ursprung
Commit
d65e3db136
7 geänderte Dateien mit 267 neuen und 75 gelöschten Zeilen
  1. +7
    -4
      src/app/api/jo/actions.ts
  2. +158
    -39
      src/components/JoSearch/JoSearch.tsx
  3. +55
    -23
      src/components/Jodetail/JoPickOrderList.tsx
  4. +28
    -0
      src/components/Jodetail/newJobPickExecution.tsx
  5. +10
    -6
      src/components/Qc/QcComponent.tsx
  6. +7
    -3
      src/components/Qc/QcStockInModal.tsx
  7. +2
    -0
      src/i18n/zh/common.json

+ 7
- 4
src/app/api/jo/actions.ts Datei anzeigen

@@ -532,6 +532,8 @@ export interface AllJoPickOrderResponse {
jobOrderStatus: string;
finishedPickOLineCount: number;
floorPickCounts: FloorPickCount[];
noLotPickCount?: FloorPickCount | null;
suggestedFailCount?: number;
}
export interface UpdateJoPickOrderHandledByRequest {
pickOrderId: number;
@@ -689,10 +691,11 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder
},
);
});
export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null) => {
const query = isDrink !== undefined && isDrink !== null
? `?isDrink=${isDrink}`
: "";
export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null, floor?: string | null) => {
const params = new URLSearchParams();
if (isDrink !== undefined && isDrink !== null) params.set("isDrink", String(isDrink));
if (floor) params.set("floor", floor);
const query = params.toString() ? `?${params.toString()}` : "";
return serverFetchJson<AllJoPickOrderResponse[]>(
`${BASE_API_URL}/jo/AllJoPickOrder${query}`,
{ method: "GET" }


+ 158
- 39
src/components/JoSearch/JoSearch.tsx Datei anzeigen

@@ -1,5 +1,5 @@
"use client"
import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions";
import { SearchJoResultRequest, fetchJos, releaseJo, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Criterion } from "../SearchBox";
@@ -12,7 +12,7 @@ import { useRouter } from "next/navigation";
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { StockInLineInput } from "@/app/api/stockIn";
import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo";
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box } from "@mui/material";
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box, CircularProgress } from "@mui/material";
import { BomCombo } from "@/app/api/bom";
import JoCreateFormModal from "./JoCreateFormModal";
import AddIcon from '@mui/icons-material/Add';
@@ -55,6 +55,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]);
const [releasingJoIds, setReleasingJoIds] = useState<Set<number>>(new Set());
const [isBatchReleasing, setIsBatchReleasing] = useState(false);
// 合并后的统一编辑 Dialog 状态
const [openEditDialog, setOpenEditDialog] = useState(false);
@@ -229,9 +232,108 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
setEditProductionPriority(50);
setEditProductProcessId(null);
}, []);

const newPageFetch = useCallback(
async (
pagingController: { pageNum: number; pageSize: number },
filterArgs: SearchJoResultRequest,
) => {
const params: SearchJoResultRequest = {
...filterArgs,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
};
const response = await fetchJos(params);
console.log("newPageFetch params:", params)
console.log("newPageFetch response:", response)
if (response && response.records) {
console.log("newPageFetch - setting filteredJos with", response.records.length, "records");
setTotalCount(response.total);
setFilteredJos(response.records);
console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id);
} else {
console.warn("newPageFetch - no response or no records");
setFilteredJos([]);
}
},
[],
);

const isPlanningJo = useCallback((jo: JobOrder) => {
return String(jo.status ?? "").toLowerCase() === "planning";
}, []);
const handleReleaseJo = useCallback(async (joId: number) => {
if (!joId) return;
setReleasingJoIds((prev) => {
const next = new Set(prev);
next.add(joId);
return next;
});
try {
const response = await releaseJo({ id: joId });
if (response) {
msg(t("update success"));
setCheckboxIds((prev) => prev.filter((id) => Number(id) !== joId));
await newPageFetch(pagingController, inputs);
}
} catch (error) {
console.error("Error releasing JO:", error);
msg(t("update failed"));
} finally {
setReleasingJoIds((prev) => {
const next = new Set(prev);
next.delete(joId);
return next;
});
}
}, [inputs, pagingController, t, newPageFetch]);
const selectedPlanningJoIds = useMemo(() => {
const selectedIds = new Set(checkboxIds.map((id) => Number(id)));
return filteredJos
.filter((jo) => selectedIds.has(jo.id))
.filter((jo) => isPlanningJo(jo))
.map((jo) => jo.id);
}, [checkboxIds, filteredJos, isPlanningJo]);
const handleBatchRelease = useCallback(async () => {
if (selectedPlanningJoIds.length === 0) return;
setIsBatchReleasing(true);
try {
const results = await Promise.allSettled(
selectedPlanningJoIds.map((id) => releaseJo({ id })),
);
const successCount = results.filter((r) => r.status === "fulfilled").length;
const failedCount = results.length - successCount;
if (successCount > 0 && failedCount === 0) {
msg(t("update success"));
} else if (successCount > 0) {
msg(`${t("update success")} (${successCount}), ${t("update failed")} (${failedCount})`);
} else {
msg(t("update failed"));
}
setCheckboxIds((prev) => prev.filter((id) => !selectedPlanningJoIds.includes(Number(id))));
await newPageFetch(pagingController, inputs);
} catch (error) {
console.error("Error batch releasing JOs:", error);
msg(t("update failed"));
} finally {
setIsBatchReleasing(false);
}
}, [inputs, pagingController, selectedPlanningJoIds, t, newPageFetch]);
const columns = useMemo<Column<JobOrder>[]>(
() => [
{
name: "id",
label: "",
type: "checkbox",
disabled: (row) => {
const id = row.id;
return !isPlanningJo(row) || releasingJoIds.has(id) || isBatchReleasing;
}
},
{
name: "planStart",
@@ -340,49 +442,43 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
name: "id",
label: t("Actions"),
renderCell: (row) => {
const isPlanning = isPlanningJo(row);
const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing;
return (
<Button
id="emailSupplier"
type="button"
variant="contained"
color="primary"
sx={{ width: "150px" }}
onClick={() => onDetailClick(row)}
>
{t("View")}
</Button>
<Stack direction="row" spacing={1} alignItems="center">
<Button
type="button"
variant="contained"
color="primary"
sx={{ minWidth: 120 }}
onClick={(e) => {
e.stopPropagation();
onDetailClick(row);
}}
>
{t("View")}
</Button>
<Button
type="button"
variant="contained"
color="success"
disabled={!isPlanning || isReleasing}
sx={{ minWidth: 120 }}
onClick={(e) => {
e.stopPropagation();
handleReleaseJo(row.id);
}}
startIcon={isReleasing && isPlanning ? <CircularProgress size={16} color="inherit" /> : undefined}
>
{t("Release")}
</Button>
</Stack>
)
}
},
], [t, inventoryData, detailedJos, handleOpenEditDialog]
], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing]
)

const newPageFetch = useCallback(
async (
pagingController: { pageNum: number; pageSize: number },
filterArgs: SearchJoResultRequest,
) => {
const params: SearchJoResultRequest = {
...filterArgs,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
};
const response = await fetchJos(params);
console.log("newPageFetch params:", params)
console.log("newPageFetch response:", response)
if (response && response.records) {
console.log("newPageFetch - setting filteredJos with", response.records.length, "records");
setTotalCount(response.total);
setFilteredJos(response.records);
console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id);
} else {
console.warn("newPageFetch - no response or no records");
setFilteredJos([]);
}
},
[],
);
const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
try {
const response = await updateJoReqQty({
@@ -560,6 +656,27 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
spacing={2}
sx={{ mt: 2 }}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mr: "auto" }}>
<Typography variant="body2" color="text.secondary">
{t("Selected")}: {selectedPlanningJoIds.length}
</Typography>
<Button
variant="contained"
color="success"
disabled={selectedPlanningJoIds.length === 0 || isBatchReleasing}
onClick={handleBatchRelease}
startIcon={isBatchReleasing ? <CircularProgress size={16} color="inherit" /> : undefined}
>
{t("Release")} ({selectedPlanningJoIds.length})
</Button>
<Button
variant="outlined"
disabled={checkboxIds.length === 0 || isBatchReleasing}
onClick={() => setCheckboxIds([])}
>
{t("Reset")}
</Button>
</Stack>
<Button
variant="outlined"
startIcon={<AddIcon />}
@@ -580,6 +697,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
pagingController={pagingController}
totalCount={totalCount}
isAutoPaging={false}
checkboxIds={checkboxIds}
setCheckboxIds={setCheckboxIds}
/>
<JoCreateFormModal
open={isCreateJoModalOpen}


+ 55
- 23
src/components/Jodetail/JoPickOrderList.tsx Datei anzeigen

@@ -10,7 +10,6 @@ import {
Typography,
Chip,
CircularProgress,
TablePagination,
Grid,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
@@ -20,36 +19,35 @@ import JobPickExecution from "./newJobPickExecution";
interface Props {
onSwitchToRecordTab?: () => void;
}
const PER_PAGE = 6;

const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
const { t } = useTranslation(["common", "jo"]);
const [loading, setLoading] = useState(false);
const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]);
const [page, setPage] = useState(0);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined);
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
type PickOrderFilter = "all" | "drink" | "other";
const [filter, setFilter] = useState<PickOrderFilter>("all");
type FloorFilter = "ALL" | "2F" | "3F" | "4F";
const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL");
const fetchPickOrders = useCallback(async () => {
setLoading(true);
try {
const isDrinkParam =
filter === "all" ? undefined : filter === "drink" ? true : false;
const data = await fetchAllJoPickOrders(isDrinkParam);
const floorParam = floorFilter === "ALL" ? undefined : floorFilter;
const data = await fetchAllJoPickOrders(isDrinkParam, floorParam);
setPickOrders(Array.isArray(data) ? data : []);
setPage(0);
} catch (e) {
console.error(e);
setPickOrders([]);
} finally {
setLoading(false);
}
}, [filter]);
}, [filter, floorFilter]);

useEffect(() => {
fetchPickOrders( );
}, [fetchPickOrders, filter]);
fetchPickOrders();
}, [fetchPickOrders, filter, floorFilter]);
const handleBackToList = useCallback(() => {
setSelectedPickOrderId(undefined);
setSelectedJobOrderId(undefined);
@@ -80,9 +78,6 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
);
}

const startIdx = page * PER_PAGE;
const paged = pickOrders.slice(startIdx, startIdx + PER_PAGE);

return (
<Box>
{loading ? (
@@ -115,12 +110,43 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
{t("Other")}
</Button>
</Box>

<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap', mb: 2 }}>
<Button
variant={floorFilter === "ALL" ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("ALL")}
>
{t("Select All")}
</Button>
<Button
variant={floorFilter === "2F" ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("2F")}
>
2F
</Button>
<Button
variant={floorFilter === "3F" ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("3F")}
>
3F
</Button>
<Button
variant={floorFilter === "4F" ? "contained" : "outlined"}
size="small"
onClick={() => setFloorFilter("4F")}
>
4F
</Button>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total pick orders")}: {pickOrders.length}
</Typography>

<Grid container spacing={2}>
{paged.map((pickOrder) => {
{pickOrders.map((pickOrder) => {
const status = String(pickOrder.jobOrderStatus || "");
const statusLower = status.toLowerCase();
const statusColor =
@@ -175,6 +201,22 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
{floor}: {finishedCount}/{totalCount}
</Typography>
))}
{!!pickOrder.noLotPickCount && (
<Typography
key="NO_LOT"
variant="body2"
color="text.secondary"
component="span"
sx={{ mr: 1 }}
>
{t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount}
</Typography>
)}
{typeof pickOrder.suggestedFailCount === "number" && pickOrder.suggestedFailCount > 0 && (
<Typography variant="body2" color="error" sx={{ mt: 0.5 }}>
{t("Suggested Fail")}: {pickOrder.suggestedFailCount}
</Typography>
)}
{statusLower !== "pending" && finishedCount > 0 && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600}>
@@ -202,16 +244,6 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
);
})}
</Grid>
{pickOrders.length > 0 && (
<TablePagination
component="div"
count={pickOrders.length}
page={page}
rowsPerPage={PER_PAGE}
onPageChange={(e, p) => setPage(p)}
rowsPerPageOptions={[PER_PAGE]}
/>
)}
</Box>
)}
</Box>


+ 28
- 0
src/components/Jodetail/newJobPickExecution.tsx Datei anzeigen

@@ -2003,6 +2003,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// ✅ 使用 batchSubmitList API
const result = await batchSubmitList(request);
console.log(`📥 Batch submit result:`, result);

// ✅ After batch submit, explicitly trigger completion check per consoCode.
// Otherwise pick_order/job_order may stay RELEASED even when all lines are completed.
try {
const consoCodes = Array.from(
new Set(
lines
.map((l) => (l.pickOrderConsoCode || "").trim())
.filter((c) => c.length > 0),
),
);
if (consoCodes.length > 0) {
await Promise.all(
consoCodes.map(async (code) => {
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(code);
console.log(`✅ Pick order completion check (${code}):`, completionResponse);
} catch (e) {
console.error(`❌ Error checking completion for ${code}:`, e);
}
}),
);
}
} catch (e) {
console.error("❌ Error triggering completion checks after batch submit:", e);
}
// 刷新数据
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
@@ -2118,6 +2144,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const issueData = {
...data,
type: "Jo", // Delivery Order Record 类型
pickerName: session?.user?.name || undefined,
handledBy: currentUserId || undefined,
};
const result = await recordPickExecutionIssue(issueData);


+ 10
- 6
src/components/Qc/QcComponent.tsx Datei anzeigen

@@ -126,6 +126,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay
}
return "IQC"; // Default
}, [itemDetail]);
const isJobOrder = useMemo(() => {
return isExist(itemDetail?.jobOrderId);
}, [itemDetail?.jobOrderId]);

const detailMode = useMemo(() => {
const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId);
@@ -146,7 +150,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay
if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") {
setError("acceptQty", { message: t("value must be a number") });
} else
if (accQty > itemDetail.acceptedQty) {
if (!isJobOrder && accQty > itemDetail.acceptedQty) {
setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty}` });
} else
@@ -156,10 +160,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay
console.log("%c Validated accQty:", "color:yellow", accQty);
}

},[setError, qcDecision, accQty, itemDetail])
},[setError, qcDecision, accQty, itemDetail, isJobOrder])
useEffect(() => { // W I P // -----
if (qcDecision == 1) {
if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${
if (!isJobOrder && validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty}`)) return;
if (validateFieldFail("acceptQty", accQty <= 0, t("minimal value is 1"))) return;
@@ -188,7 +192,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay
}

// console.log("Validated without errors");
}, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult]);
}, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult, isJobOrder]);

useEffect(() => {
clearErrors();
@@ -612,7 +616,7 @@ useEffect(() => {
}
e.target.value = r;
}}
inputProps={{ min: 0.01, max:itemDetail.acceptedQty, step: 0.01 }}
inputProps={isJobOrder ? { min: 0.01, step: 0.01 } : { min: 0.01, max: itemDetail.acceptedQty, step: 0.01 }}
// onChange={(e) => {
// const inputValue = e.target.value;
// if (inputValue === '' || /^[0-9]*$/.test(inputValue)) {
@@ -632,7 +636,7 @@ useEffect(() => {
sx={{ width: '150px' }}
value={
(!Boolean(errors.acceptQty) && qcDecision !== undefined && qcDecision != 3) ?
(qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty)
(qcDecision == 1 ? Math.max(0, itemDetail.acceptedQty - accQty) : itemDetail.acceptedQty)
: ""
}
error={Boolean(errors.acceptQty)}


+ 7
- 3
src/components/Qc/QcStockInModal.tsx Datei anzeigen

@@ -404,6 +404,7 @@ const QcStockInModal: React.FC<Props> = ({
return;
}

const isJobOrderSource = Boolean(stockInLineInfo?.jobOrderId) || printSource === "productionProcess";
const qcData = {
dnNo : data.dnNo? data.dnNo : "DN00000",
// dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()),
@@ -413,6 +414,9 @@ const QcStockInModal: React.FC<Props> = ({
qcAccept: qcAccept? qcAccept : false,
acceptQty: acceptQty? acceptQty : 0,
// For Job Order QC, allow updating received qty beyond demand/accepted.
// Backend uses request.acceptedQty in QC flow, so we must send it explicitly.
acceptedQty: (qcAccept && isJobOrderSource) ? (acceptQty ? acceptQty : 0) : stockInLineInfo?.acceptedQty,
// qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({
qcResult: qcResults.map(item => ({
// id: item.id,
@@ -481,8 +485,8 @@ const QcStockInModal: React.FC<Props> = ({
itemId: stockInLineInfo?.itemId, // Include Item ID
purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists
purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL ID if exists
acceptedQty:acceptQty, // Include acceptedQty
acceptQty: stockInLineInfo?.acceptedQty, // Putaway quantity
acceptedQty: acceptQty, // Keep in sync with QC acceptQty
acceptQty: acceptQty, // Putaway quantity
warehouseId: defaultWarehouseId,
status: "received", // Use string like PutAwayModal
productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined,
@@ -490,7 +494,7 @@ const QcStockInModal: React.FC<Props> = ({
receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined,
inventoryLotLines: [{
warehouseId: defaultWarehouseId,
qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal
qty: acceptQty, // Putaway qty
}],
} as StockInLineEntry & ModalFormInput;


+ 2
- 0
src/i18n/zh/common.json Datei anzeigen

@@ -10,6 +10,8 @@
"Issue BOM List": "問題 BOM 列表",
"File Name": "檔案名稱",
"Please Select BOM": "請選擇 BOM",
"No Lot": "沒有批號",
"Select All": "全選",
"Loading BOM Detail...": "正在載入 BOM 明細…",
"Output Quantity": "使用數量",
"Process & Equipment": "製程與設備",


Laden…
Abbrechen
Speichern