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

update job order search and cacel job order

MergeProblem1
CANCERYS\kw093 пре 13 часа
родитељ
комит
081ccb9f8f
8 измењених фајлова са 205 додато и 44 уклоњено
  1. +11
    -0
      src/app/api/jo/actions.ts
  2. +83
    -6
      src/components/JoSearch/JoSearch.tsx
  3. +1
    -0
      src/components/JoSearch/JoSearchWrapper.tsx
  4. +9
    -2
      src/components/ProductionProcess/OverallTimeRemainingCard.tsx
  5. +79
    -17
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  6. +9
    -19
      src/components/ProductionProcess/ProductionProcessList.tsx
  7. +5
    -0
      src/i18n/zh/common.json
  8. +8
    -0
      src/i18n/zh/jo.json

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

@@ -29,6 +29,7 @@ export interface SearchJoResultRequest extends Pageable {
planStart?: string;
planStartTo?: string;
jobTypeName?: string;
joSearchStatus?: string;
}

export interface productProcessLineQtyRequest {
@@ -672,6 +673,16 @@ export const deleteJobOrder=cache(async (jobOrderId: number) => {
}
);
});

export const setJobOrderHidden = cache(async (jobOrderId: number, hidden: boolean) => {
const response = await serverFetchJson<any>(`${BASE_API_URL}/jo/set-hidden`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: jobOrderId, hidden }),
});
revalidateTag("jos");
return response;
});
export const fetchAllJobTypes = cache(async () => {
return serverFetchJson<JobTypeResponse[]>(
`${BASE_API_URL}/jo/jobTypes`,


+ 83
- 6
src/components/JoSearch/JoSearch.tsx Прегледај датотеку

@@ -1,5 +1,5 @@
"use client"
import { SearchJoResultRequest, fetchJos, releaseJo, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions";
import { SearchJoResultRequest, fetchJos, releaseJo, setJobOrderHidden, 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";
@@ -39,8 +39,7 @@ interface Props {
jobTypes: JobTypeResponse[];
}

type SearchQuery = Partial<Omit<JobOrder, "id">>;
type SearchParamNames = keyof SearchQuery;
type SearchParamNames = "code" | "itemName" | "planStart" | "planStartTo" | "jobTypeName" | "joSearchStatus";

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
const { t } = useTranslation("jo");
@@ -58,6 +57,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]);
const [releasingJoIds, setReleasingJoIds] = useState<Set<number>>(new Set());
const [isBatchReleasing, setIsBatchReleasing] = useState(false);
const [cancelConfirmJoId, setCancelConfirmJoId] = useState<number | null>(null);
const [cancelSubmitting, setCancelSubmitting] = useState(false);
const [cancelingJoIds, setCancelingJoIds] = useState<Set<number>>(new Set());
// 合并后的统一编辑 Dialog 状态
const [openEditDialog, setOpenEditDialog] = useState(false);
@@ -160,6 +162,19 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
type: "select",
options: jobTypes.map(jt => jt.name)
},
{
label: t("Status"),
paramName: "joSearchStatus",
type: "select-labelled",
options: [
{ label: t("Pending"), value: "pending" },
{ label: t("Packaging"), value: "packaging" },
{ label: t("Processing"), value: "processing" },
{ label: t("Storing"), value: "storing" },
{ label: t("Put Awayed"), value: "putAwayed" },
{ label: t("cancel"), value: "cancel" },
],
},
], [t, jobTypes])
const fetchBomForJo = useCallback(async (jo: JobOrder): Promise<BomCombo | null> => {
@@ -288,6 +303,29 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
});
}
}, [inputs, pagingController, t, newPageFetch]);

const handleConfirmCancelJobOrder = useCallback(async () => {
if (cancelConfirmJoId == null) return;
const id = cancelConfirmJoId;
setCancelSubmitting(true);
setCancelingJoIds((prev) => new Set(prev).add(id));
try {
await setJobOrderHidden(id, true);
msg(t("update success"));
setCancelConfirmJoId(null);
await newPageFetch(pagingController, inputs);
} catch (error) {
console.error("Error cancelling job order:", error);
msg(t("update failed"));
} finally {
setCancelSubmitting(false);
setCancelingJoIds((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
}
}, [cancelConfirmJoId, newPageFetch, pagingController, inputs, t]);
const selectedPlanningJoIds = useMemo(() => {
const selectedIds = new Set(checkboxIds.map((id) => Number(id)));
@@ -444,6 +482,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
renderCell: (row) => {
const isPlanning = isPlanningJo(row);
const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing;
const isCancelingRow = cancelingJoIds.has(row.id);
const isPutAwayed = row.stockInLineStatus?.toLowerCase() === "completed";
return (
<Stack direction="row" spacing={1} alignItems="center">
<Button
@@ -458,6 +498,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
>
{t("View")}
</Button>
{isPlanning ? (
<Button
type="button"
variant="contained"
@@ -472,11 +513,27 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
>
{t("Release")}
</Button>
) : (
<Button
type="button"
variant="contained"
color="warning"
disabled={isPutAwayed || isCancelingRow || isBatchReleasing || cancelSubmitting}
sx={{ minWidth: 120 }}
onClick={(e) => {
e.stopPropagation();
setCancelConfirmJoId(row.id);
}}
startIcon={isCancelingRow ? <CircularProgress size={16} color="inherit" /> : undefined}
>
{t("Cancel Job Order")}
</Button>
)}
</Stack>
)
}
},
], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing]
], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing, cancelingJoIds, cancelSubmitting]
)

const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
@@ -622,7 +679,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
...query,
planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart,
planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo,
jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : ""
jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "",
joSearchStatus: query.joSearchStatus && query.joSearchStatus !== "All" ? query.joSearchStatus : "all",
};
setInputs({
@@ -630,7 +688,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
itemName: transformedQuery.itemName,
planStart: transformedQuery.planStart,
planStartTo: transformedQuery.planStartTo,
jobTypeName: transformedQuery.jobTypeName
jobTypeName: transformedQuery.jobTypeName,
joSearchStatus: transformedQuery.joSearchStatus
});
setPagingController(defaultPagingController);
@@ -839,6 +898,24 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
</Button>
</DialogActions>
</Dialog>

<Dialog
open={cancelConfirmJoId !== null}
onClose={() => !cancelSubmitting && setCancelConfirmJoId(null)}
maxWidth="xs"
fullWidth
>
<DialogTitle>{t("Confirm cancel job order")}</DialogTitle>
<DialogContent>
<Typography variant="body2">{t("Cancel job order confirm message")}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setCancelConfirmJoId(null)} disabled={cancelSubmitting}>{t("Cancel")}</Button>
<Button variant="contained" color="warning" onClick={() => void handleConfirmCancelJobOrder()} disabled={cancelSubmitting}>
{cancelSubmitting ? <CircularProgress size={20} /> : t("Cancel Job Order")}
</Button>
</DialogActions>
</Dialog>
</>
}


+ 1
- 0
src/components/JoSearch/JoSearchWrapper.tsx Прегледај датотеку

@@ -18,6 +18,7 @@ const JoSearchWrapper: React.FC & SubComponents = async () => {
itemName: "",
planStart: `${todayStr}T00:00`,
planStartTo: `${todayStr}T23:59:59`,
joSearchStatus: "all",

}



+ 9
- 2
src/components/ProductionProcess/OverallTimeRemainingCard.tsx Прегледај датотеку

@@ -22,7 +22,14 @@ const OverallTimeRemainingCard: React.FC<OverallTimeRemainingCardProps> = ({
console.log("🕐 OverallTimeRemainingCard - processData?.startTime type:", typeof processData?.startTime);
console.log("🕐 OverallTimeRemainingCard - processData?.startTime isArray:", Array.isArray(processData?.startTime));
if (!processData?.startTime) {
const jobOrderStatus = String((processData as any)?.jobOrderStatus ?? "").trim().toLowerCase();
const shouldStopCount =
jobOrderStatus === "storing" ||
jobOrderStatus === "completed" ||
jobOrderStatus === "pendingqc" ||
jobOrderStatus === "pending_qc";

if (shouldStopCount || !processData?.startTime) {
console.log("❌ OverallTimeRemainingCard - No startTime found");
setOverallRemainingTime(null);
setIsOverTime(false);
@@ -176,7 +183,7 @@ const OverallTimeRemainingCard: React.FC<OverallTimeRemainingCardProps> = ({
update();
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}, [processData?.startTime, processData?.productProcessLines]);
}, [processData?.startTime, processData?.productProcessLines, (processData as any)?.jobOrderStatus]);

if (!processData?.startTime || overallRemainingTime === null) {
return null;


+ 79
- 17
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Прегледај датотеку

@@ -23,7 +23,7 @@ import {
} from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from "react-i18next";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder, setJobOrderHidden, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions";
import ProductionProcessDetail from "./ProductionProcessDetail";
import { BomCombo } from "@/app/api/bom";
import { fetchBomCombo } from "@/app/api/bom/index";
@@ -265,15 +265,42 @@ const stockCounts = useMemo(() => {
insufficient: total - sufficient,
};
}, [jobOrderLines, inventoryData]);
const status = processData?.status?.toLowerCase?.() ?? "";
const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
const response = await deleteJobOrder(jobOrderId)
if (response) {
//setProcessData(response.entity);
//await fetchData();
const jobOrderPlanning = useMemo(
() => (processData?.jobOrderStatus ?? "").toLowerCase() === "planning",
[processData?.jobOrderStatus]
);
const isPutAwayed = useMemo(
() => (processData?.jobOrderStatus ?? "").toLowerCase() === "completed",
[processData?.jobOrderStatus]
);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [cancelLoading, setCancelLoading] = useState(false);

const handleConfirmDeleteJobOrder = useCallback(async () => {
setDeleteLoading(true);
try {
const response = await deleteJobOrder(jobOrderId);
if (response) {
setDeleteConfirmOpen(false);
onBack();
}
} finally {
setDeleteLoading(false);
}
}, [jobOrderId, onBack]);

const handleConfirmCancelJobOrder = useCallback(async () => {
setCancelLoading(true);
try {
await setJobOrderHidden(jobOrderId, true);
setCancelConfirmOpen(false);
onBack();
} finally {
setCancelLoading(false);
}
}, [jobOrderId]);
}, [jobOrderId, onBack]);
const handleRelease = useCallback(async ( jobOrderId: number) => {
// TODO: 替换为实际的 release 调用
console.log("Release clicked for jobOrderId:", jobOrderId);
@@ -675,15 +702,24 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
</Typography>
{fromJosave && (
{fromJosave && jobOrderPlanning && (
<Button
variant="contained"
color="error"
onClick={() => handleDeleteJobOrder(jobOrderId)}
disabled={processData?.jobOrderStatus !== "planning"}
>
{t("Delete Job Order")}
</Button>
variant="contained"
color="error"
onClick={() => setDeleteConfirmOpen(true)}
>
{t("Delete Job Order")}
</Button>
)}
{fromJosave && !jobOrderPlanning && (
<Button
variant="contained"
color="warning"
onClick={() => setCancelConfirmOpen(true)}
disabled={isPutAwayed}
>
{t("Cancel Job Order")}
</Button>
)}
{fromJosave && (
<Button
@@ -781,7 +817,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
setTabIndex(0);

}}
fromJosave={fromJosave}
fromJosave={Boolean(fromJosave && !isPutAwayed)}
/>
)}
{tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
@@ -928,6 +964,32 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
</DialogActions>
</Dialog>

<Dialog open={deleteConfirmOpen} onClose={() => !deleteLoading && setDeleteConfirmOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>{t("Confirm delete job order")}</DialogTitle>
<DialogContent>
<Typography variant="body2">{t("Delete job order confirm message")}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)} disabled={deleteLoading}>{t("Cancel")}</Button>
<Button variant="contained" color="error" onClick={() => void handleConfirmDeleteJobOrder()} disabled={deleteLoading}>
{deleteLoading ? <CircularProgress size={20} /> : t("Delete Job Order")}
</Button>
</DialogActions>
</Dialog>

<Dialog open={cancelConfirmOpen} onClose={() => !cancelLoading && setCancelConfirmOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>{t("Confirm cancel job order")}</DialogTitle>
<DialogContent>
<Typography variant="body2">{t("Cancel job order confirm message")}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setCancelConfirmOpen(false)} disabled={cancelLoading}>{t("Cancel")}</Button>
<Button variant="contained" color="warning" onClick={() => void handleConfirmCancelJobOrder()} disabled={cancelLoading}>
{cancelLoading ? <CircularProgress size={20} /> : t("Cancel Job Order")}
</Button>
</DialogActions>
</Dialog>

</Box>
</Box>


+ 9
- 19
src/components/ProductionProcess/ProductionProcessList.tsx Прегледај датотеку

@@ -217,16 +217,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
(inputs: Record<SearchParam | `${SearchParam}To`, string>) => {
const selectedProcessType = (inputs.processType || "all") as ProcessFilter;
const fallback = defaultPlanStartRange();
let from = (inputs.date || "").trim() || fallback.from;
let to = (inputs.dateTo || "").trim() || fallback.to;
if (dayjs(from).isAfter(dayjs(to), "day")) {
[from, to] = [to, from];
}
const selectedDate = (inputs.date || "").trim() || fallback.from;
onListPersistedStateChange((prev) => ({
...prev,
filter: selectedProcessType,
planStartFrom: from,
planStartTo: to,
date: selectedDate,
itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null,
jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null,
selectedItemCodes: [],
@@ -241,8 +236,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
onListPersistedStateChange((prev) => ({
...prev,
filter: "all",
planStartFrom: r.from,
planStartTo: r.to,
date: r.from,
itemCode: null,
jobOrderCode: null,
selectedItemCodes: [],
@@ -368,16 +362,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
const r = defaultPlanStartRange();
return [
{
type: "dateRange",
label: t("Plan start (from)"),
label2: t("Plan start (to)"),
type: "date",
label: t("Search date"),
paramName: "date",
defaultValue: r.from,
defaultValueTo: r.to,
preFilledValue: {
from: appliedSearch.date,
to: appliedSearch.date,
},
defaultValue: appliedSearch.date,
preFilledValue: appliedSearch.date,
},
{
type: "text",
@@ -471,6 +460,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
{paged.map((process) => {
const status = String(process.status || "");
const statusLower = status.toLowerCase();
const displayStatus = statusLower === "in_progress" ? "processing" : status;
const statusColor =
statusLower === "completed"
? "success"
@@ -529,7 +519,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({
</Typography>
</Box>
<Chip size="small" label={t(status)} color={statusColor as any} />
<Chip size="small" label={t(displayStatus)} color={statusColor as any} />
</Stack>
<Typography variant="body2" color="text.secondary">
{t("Lot No")}: {process.lotNo ?? "-"}


+ 5
- 0
src/i18n/zh/common.json Прегледај датотеку

@@ -24,6 +24,11 @@
"Confirm to Pass this Process?": "確認要通過此工序嗎?",
"Equipment Name": "設備",
"Confirm to update this Job Order?": "確認要完成此工單嗎?",
"Cancel Job Order": "取消工單",
"Confirm delete job order": "確認刪除工單",
"Delete job order confirm message": "確定要刪除此工單嗎?此操作無法復原。",
"Confirm cancel job order": "確認取消工單",
"Cancel job order confirm message": "確定要取消此工單嗎?工單將從列表中隱藏。",
"all": "全部",
"Bom Uom": "BOM 單位",
"Searched Item": "已搜索物料",


+ 8
- 0
src/i18n/zh/jo.json Прегледај датотеку

@@ -25,6 +25,7 @@
"UoM": "銷售單位",
"Select Another Bag Lot":"選擇另一個包裝袋",
"No": "沒有",
"Packaging":"提料中",
"Overall Time Remaining": "總剩餘時間",
"User not found with staffNo:": "用戶不存在",
"Time Remaining": "剩餘時間",
@@ -41,9 +42,16 @@
"Lot No.": "批號",
"Pass": "通過",
"Delete Job Order": "刪除工單",
"Cancel Job Order": "取消工單",
"Confirm delete job order": "確認刪除工單",
"Delete job order confirm message": "確定要刪除此工單嗎?此操作無法復原。",
"Confirm cancel job order": "確認取消工單",
"Cancel job order confirm message": "確定要取消此工單嗎?工單將從列表中隱藏。",
"Bom": "半成品/成品編號",
"Release": "放單",
"Pending": "待掃碼",
"Put Awayed": "已上架",
"cancel": "已取消",
"Pending for pick": "待提料",
"Planning": "計劃中",
"Processing": "已開始工序",


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