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

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/i18n/zh/common.json
master
LAPTOP-EU9T8H5F\User пре 13 часа
родитељ
комит
9c691a2494
30 измењених фајлова са 1834 додато и 712 уклоњено
  1. +5
    -2
      src/app/(main)/jo/edit/page.tsx
  2. +1
    -0
      src/app/api/bom/index.ts
  3. +2
    -2
      src/app/api/do/actions.tsx
  4. +67
    -7
      src/app/api/jo/actions.ts
  5. +45
    -0
      src/app/api/scheduling/actions.ts
  6. +7
    -0
      src/app/api/scheduling/index.ts
  7. +1
    -0
      src/app/api/stockIn/index.ts
  8. +77
    -28
      src/components/DetailedSchedule/DetailedScheduleSearchView.tsx
  9. +91
    -37
      src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx
  10. +19
    -13
      src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx
  11. +54
    -151
      src/components/DetailedScheduleDetail/ViewByFGDetails.tsx
  12. +149
    -29
      src/components/JoSearch/JoCreateFormModal.tsx
  13. +1
    -0
      src/components/JoSearch/JoSearch.tsx
  14. +9
    -2
      src/components/Jodetail/JoPickOrderList.tsx
  15. +15
    -10
      src/components/Jodetail/JobPickExecutionForm.tsx
  16. +39
    -10
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  17. +87
    -79
      src/components/Jodetail/JodetailSearch.tsx
  18. +32
    -68
      src/components/Jodetail/completeJobOrderRecord.tsx
  19. +105
    -35
      src/components/Jodetail/newJobPickExecution.tsx
  20. +255
    -0
      src/components/ProductionProcess/FinishedQcJobOrderList.tsx
  21. +60
    -45
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  22. +49
    -20
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  23. +7
    -5
      src/components/ProductionProcess/ProductionProcessList.tsx
  24. +97
    -15
      src/components/ProductionProcess/ProductionProcessPage.tsx
  25. +274
    -71
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx
  26. +3
    -2
      src/components/Qc/QcComponent.tsx
  27. +83
    -23
      src/components/Qc/QcStockInModal.tsx
  28. +113
    -25
      src/components/StockIn/FgStockInForm.tsx
  29. +67
    -21
      src/i18n/zh/common.json
  30. +20
    -12
      src/i18n/zh/jo.json

+ 5
- 2
src/app/(main)/jo/edit/page.tsx Прегледај датотеку

@@ -25,10 +25,13 @@ const JoEdit: React.FC<Props> = async ({ searchParams }) => {
try {
await fetchJoDetail(parseInt(id))
} catch (e) {

if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) {
console.log(e)
notFound();
console.log("Job Order not found:", e);
} else {
console.error("Error fetching Job Order detail:", e);
}
notFound();
}




+ 1
- 0
src/app/api/bom/index.ts Прегледај датотеку

@@ -8,6 +8,7 @@ export interface BomCombo {
label: string;
outputQty: number;
outputQtyUom: string;
description: string;
}

export const preloadBomCombo = (() => {


+ 2
- 2
src/app/api/do/actions.tsx Прегледај датотеку

@@ -173,9 +173,9 @@ export const fetchDoRecordByPage = cache(async (data?: SearchDeliveryOrderInfoRe

return response
})
export const fetchTicketReleaseTable = cache(async ()=> {
export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: string)=> {
return await serverFetchJson<getTicketReleaseTable[]>(
`${BASE_API_URL}/doPickOrder/ticket-release-table`,
`${BASE_API_URL}/doPickOrder/ticket-release-table/${startDate}&${endDate}`,
{
method: "GET",
}


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

@@ -1,11 +1,12 @@
"use server";

import { cache } from 'react';
import { Pageable, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { Pageable, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { JobOrder, JoStatus, Machine, Operator } from ".";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil";
import { FileResponse } from "@/app/api/pdf/actions";

export interface SaveJo {
bomId: number;
@@ -155,7 +156,7 @@ export const printFGStockInLabel = cache(async(data: PrintFGStockInLabelRequest)
}
return serverFetchWithNoContent(
`${BASE_API_URL}/jo/print-FGPickRecordLabel?${params.toString()}`,
`${BASE_API_URL}/jo/print-FGStockInLabel?${params.toString()}`,
{
method: "GET",
next: {
@@ -214,6 +215,7 @@ export interface ProductProcessLineResponse {
seqNo: number,
name: string,
description: string,
equipmentDetailId: number,
equipment_name: string,
equipmentDetailCode: string,
status: string,
@@ -259,6 +261,7 @@ export interface ProductProcessWithLinesResponse {
outputQtyUom: string;
productionPriority: number;
jobOrderLines: JobOrderLineInfo[];

productProcessLines: ProductProcessLineResponse[];
}
@@ -320,9 +323,12 @@ export interface AllJoborderProductProcessInfoResponse {
bomId?: number;
assignedTo: number;
pickOrderId: number;
pickOrderStatus: string;
itemCode: string;
itemName: string;
requiredQty: number;
jobOrderId: number;
uom: string;
stockInLineId: number;
jobOrderCode: string;
productProcessLineCount: number;
@@ -346,6 +352,11 @@ export interface ProductProcessLineQrscanUpadteRequest {
equipmentTypeSubTypeEquipmentNo?: string;
staffNo?: string;
}
export interface NewProductProcessLineQrscanUpadteRequest{
productProcessLineId: number;
equipmentCode?: string;
staffNo?: string;
}

export interface ProductProcessLineDetailResponse {
id: number,
@@ -398,7 +409,9 @@ export interface JobOrderProcessLineDetailResponse {
description: string;
equipmentId: number;
startTime: string | number[]; // API 返回的是数组格式
endTime: string | number[]; // API 返回的是数组格式
endTime: string | number[];
stopTime: string | number[];
totalPausedTimeMs?: number; // API 返回的是数组格式
status: string;
outputFromProcessQty: number;
outputFromProcessUom: string;
@@ -524,6 +537,7 @@ export interface PickOrderLineWithLotsResponse {
uomCode: string | null;
uomDesc: string | null;
status: string | null;
handler: string | null;
lots: LotDetailResponse[];
}

@@ -554,7 +568,16 @@ export interface LotDetailResponse {
matchQty?: number | null;
}


export interface JobOrderListForPrintQrCodeResponse {
id: number;
code: string;
name: string;
reqQty: number;
stockOutLineId: number;
stockOutLineQty: number;
stockOutLineStatus: string;
finihedTime: string;
}
export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/issue`,
@@ -656,6 +679,18 @@ export const updateProductProcessLineQrscan = cache(async (request: ProductProce
}
);
});


export const newUpdateProductProcessLineQrscan = cache(async (request: NewProductProcessLineQrscanUpadteRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/NewUpdate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}
);
});
export const fetchAllJoborderProductProcessInfo = cache(async () => {
return serverFetchJson<AllJoborderProductProcessInfoResponse[]>(
`${BASE_API_URL}/product-process/Demo/Process/all`,
@@ -868,16 +903,24 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) =>
);
});
// 获取已完成的 Job Order pick orders
export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => {
export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/completed-job-order-pick-orders-only/${userId}`,
`${BASE_API_URL}/jo/completed-job-order-pick-orders-only`,
{
method: "GET",
next: { tags: ["jo-completed"] },
},
);
});

export const fetchJoForPrintQrCode = cache(async (date: string) => {
return serverFetchJson<JobOrderListForPrintQrCodeResponse[]>(
`${BASE_API_URL}/jo/joForPrintQrCode/${date}`,
{
method: "GET",
next: { tags: ["jo-print-qr-code"] },
},
);
});
// 获取已完成的 Job Order pick order records
export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => {
return serverFetchJson<any[]>(
@@ -1027,3 +1070,20 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){

return { success: true, message: "Print job sent successfully (Pick Record)" } as PrintPickRecordResponse;
}

export interface ExportFGStockInLabelRequest {
stockInLineId: number;
}

export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Promise<FileResponse> => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/jo/FGStockInLabel`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

return reportBlob;
};

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

@@ -39,6 +39,10 @@ export interface ReleaseProdScheduleInputs {
demandQty: number;
}

export interface ReleaseProdScheduleReq {
id: number;
}

export interface ReleaseProdScheduleResponse {
id: number;
code: string;
@@ -48,6 +52,12 @@ export interface ReleaseProdScheduleResponse {
message: string;
}

export interface ReleaseProdScheduleRsp {
id: number;
code: string;
message: string;
}

export interface SaveProdScheduleResponse {
id: number;
code: string;
@@ -151,6 +161,41 @@ export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInp
return response;
})

export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => {
const response = serverFetchJson<ReleaseProdScheduleRsp>(
`${BASE_API_URL}/productionSchedule/detail/detailed/release`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
}
);

//revalidateTag("detailedProdSchedules");
//revalidateTag("prodSchedule");

return response;
})

export const exportProdSchedule = async (token: string | null) => {
if (!token) throw new Error("No access token found");

const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, {
method: "POST",
headers: {
"Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Authorization": `Bearer ${token}`
}
});

if (!response.ok) throw new Error(`Backend error: ${response.status}`);

const arrayBuffer = await response.arrayBuffer();
// Convert to Base64 so Next.js can send it safely over the wire
return Buffer.from(arrayBuffer).toString('base64');
};

export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => {
const response = serverFetchJson<SaveProdScheduleResponse>(
`${BASE_API_URL}/productionSchedule/detail/detailed/save`,


+ 7
- 0
src/app/api/scheduling/index.ts Прегледај датотеку

@@ -100,6 +100,13 @@ export interface DetailedProdScheduleLineResult {
priority: number;
approved: boolean;
proportion: number;
lastMonthAvgSales: number;
avgQtyLastMonth: number; // Average usage last month
stockQty: number; // Warehouse stock quantity
daysLeft: number; // Days remaining before stockout
needNoOfJobOrder: number;
prodQty: number;
outputQty: number;
}

export interface DetailedProdScheduleLineBomMaterialResult {


+ 1
- 0
src/app/api/stockIn/index.ts Прегледај датотеку

@@ -128,6 +128,7 @@ export interface StockInLine {
dnNo?: string;
dnDate?: number[];
stockQty?: number;
bomDescription?: string;
handlerId?: number;
putAwayLines?: PutAwayLine[];
qcResult?: QcResult[];


+ 77
- 28
src/components/DetailedSchedule/DetailedScheduleSearchView.tsx Прегледај датотеку

@@ -12,6 +12,7 @@ import {
SearchProdSchedule,
fetchDetailedProdSchedules,
fetchProdSchedules,
exportProdSchedule,
testDetailedSchedule,
} from "@/app/api/scheduling/actions";
import { defaultPagingController } from "../SearchResults/SearchResults";
@@ -21,6 +22,7 @@ import { orderBy, uniqBy, upperFirst } from "lodash";
import { Button, Stack } from "@mui/material";
import isToday from 'dayjs/plugin/isToday';
import useUploadContext from "../UploadProvider/useUploadContext";
import { FileDownload, CalendarMonth } from "@mui/icons-material";
dayjs.extend(isToday);

// may need move to "index" or "actions"
@@ -77,17 +79,17 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
// type: "dateRange",
// },
{ label: t("Production Date"), paramName: "scheduleAt", type: "date" },
{
label: t("Product Count"),
paramName: "totalEstProdCount",
type: "text",
},
{
label: t("Type"),
paramName: "types",
type: "autocomplete",
options: typeOptions,
},
//{
// label: t("Product Count"),
// paramName: "totalEstProdCount",
// type: "text",
//},
//{
// label: t("Type"),
// paramName: "types",
// type: "autocomplete",
// options: typeOptions,
//},
];
return searchCriteria;
}, [t]);
@@ -177,18 +179,18 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
) as ScheduleType[];

const params: SearchProdSchedule = {
scheduleAt: dayjs(query?.scheduleAt).isValid()
? query?.scheduleAt
: undefined,
schedulePeriod: dayjs(query?.schedulePeriod).isValid()
? query?.schedulePeriod
: undefined,
schedulePeriodTo: dayjs(query?.schedulePeriodTo).isValid()
? query?.schedulePeriodTo
: undefined,
totalEstProdCount: query?.totalEstProdCount
? Number(query?.totalEstProdCount)
: undefined,
//scheduleAt: dayjs(query?.scheduleAt).isValid()
// ? query?.scheduleAt
// : undefined,
//schedulePeriod: dayjs(query?.schedulePeriod).isValid()
// ? query?.schedulePeriod
// : undefined,
//schedulePeriodTo: dayjs(query?.schedulePeriodTo).isValid()
// ? query?.schedulePeriodTo
// : undefined,
//totalEstProdCount: query?.totalEstProdCount
// ? Number(query?.totalEstProdCount)
// : undefined,
types: convertedTypes,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
@@ -207,7 +209,7 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
setFilteredSchedules((fs) =>
orderBy(
uniqBy([...fs, ...response.records], "id"),
["id"], ["desc"]));
["id"], ["asc"]));
break;
}
}
@@ -298,20 +300,67 @@ const DSOverview: React.FC<Props> = ({ type, defaultInputs }) => {
}
}, [inputs])

const exportProdScheduleClick = async () => {
try {
const token = localStorage.getItem("accessToken");
// 1. Get Base64 string from server
const base64String = await exportProdSchedule(token);
// 2. Convert Base64 back to Blob
const byteCharacters = atob(base64String);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});

// 3. Trigger download (same as before)
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "production_schedule.xlsx";
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(error);
alert("Export failed. Check the console for details.");
}
};
return (
<>
<Stack
direction="row"
justifyContent="flex-end"
flexWrap="wrap"
rowGap={2}
spacing={2} // This provides consistent space between buttons
sx={{ mb: 3 }} // Adds some margin below the button group
>
<Button
variant="contained"
variant="outlined" // Outlined variant makes it look distinct from the primary action
color="primary"
startIcon={<CalendarMonth />}
onClick={testDetailedScheduleClick}
// disabled={filteredSchedules.some(ele => arrayToDayjs(ele.scheduleAt).isToday())}
>
{t("Test Detailed Schedule")}
{t("Detailed Schedule")}
</Button>
<Button
variant="contained" // Solid button for the "Export" action
color="success" // Green color often signifies a successful action/download
startIcon={<FileDownload />}
onClick={exportProdScheduleClick}
sx={{
boxShadow: 2,
'&:hover': { backgroundColor: 'success.dark', boxShadow: 4 }
}}
>
{t("Export Schedule")}
</Button>
</Stack>
<SearchBox


+ 91
- 37
src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx Прегледај датотеку

@@ -28,7 +28,9 @@ import ViewByFGDetails, {
// FGRecord,
} from "@/components/DetailedScheduleDetail/ViewByFGDetails";
import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling";
import { releaseProdScheduleLine, saveProdScheduleLine } from "@/app/api/scheduling/actions";
// NOTE: Assuming 'releaseProdSchedule' is the new action function
// you need to implement to call the '/productionSchedule/detail/detailed/release' API
import { releaseProdScheduleLine, saveProdScheduleLine, releaseProdSchedule } from "@/app/api/scheduling/actions";
import useUploadContext from "../UploadProvider/useUploadContext";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';

@@ -58,7 +60,8 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
// console.log(type)
const apiRef = useGridApiRef();
const params = useSearchParams();
console.log(params.get("id"));
const scheduleId = params.get("id"); // Get the schedule ID for the global release API
console.log(scheduleId);
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation("schedule");
@@ -72,6 +75,14 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
});
const errors = formProps.formState.errors;

const { reset, handleSubmit, setValue, getValues } = formProps

useEffect(() => {
if (defaultValues) {
reset(defaultValues);
}
}, [defaultValues, reset]);

const lineFormProps = useFieldArray<DetailedProdScheduleResult>({
control: formProps.control,
name: "prodScheduleLines"
@@ -138,32 +149,64 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
})

if (response) {
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id)
// console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`))
// formProps.setValue(`prodScheduleLines.${index}.approved`, true)
// formProps.setValue(`prodScheduleLines.${index}.jobNo`, response.code)
// Find index of the updated line to refresh its data
// const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id)
// Update the entire line array, assuming the backend returns the updated list
formProps.setValue(`prodScheduleLines`, response.entity.prodScheduleLines.sort((a, b) => b.priority - a.priority))
// console.log(index, formProps.getValues(`prodScheduleLines.${index}.approved`))
}
setIsUploading(false)
} catch (e) {
console.log(e)
setIsUploading(false)
}
}, [])
}, [formProps, setIsUploading])

// --- NEW FUNCTION: GLOBAL RELEASE FOR THE ENTIRE SCHEDULE ---
const onGlobalReleaseClick = useCallback(async () => {
if (!scheduleId) {
setServerError(t("Cannot release. Schedule ID is missing."));
return;
}

// Optional: Add a confirmation dialog here before proceeding

setIsUploading(true);
setServerError(""); // Clear previous errors

try {
// **IMPORTANT**: Ensure 'releaseProdSchedule' is implemented in your actions file
// to call the '/productionSchedule/detail/detailed/release' endpoint.
const response = await releaseProdSchedule({
id: Number(scheduleId),
})

if (response) {
router.refresh();
}

} catch (e) {
console.error(e);
setServerError(t("An unexpected error occurred during global schedule release."));
} finally {
setIsUploading(false);
}
}, [scheduleId, setIsUploading, t, router]);
// --------------------------------------------------------------------
const [tempValue, setTempValue] = useState<string | number | null>(null)
const onEditClick = useCallback((rowId: number) => {
const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId)
if (row) {
setTempValue(row.demandQty)
}
}, [])
}, [formProps])

const handleEditChange = useCallback((rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => {
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId)
formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(newValue))
}, [])
}, [formProps])

const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => {
setIsUploading(true)
@@ -175,6 +218,7 @@ const DetailedScheduleDetailView: React.FC<Props> = ({

if (response) {
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id)
// Update BOM materials for the line after saving demand quantity
formProps.setValue(`prodScheduleLines.${index}.bomMaterials`, response.entity.bomMaterials)
}
setIsUploading(false)
@@ -182,14 +226,15 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
console.log(e)
setIsUploading(false)
}
}, [])
}, [formProps, setIsUploading])

const onCancelClick = useCallback(async (rowId: number) => {
// if (tempValue) {
// Revert the demandQty to the temporary value stored on EditClick
if (tempValue !== null) {
const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId)
formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue))
// }
}, [tempValue])
}
}, [formProps, tempValue])

return (
<>
@@ -200,9 +245,9 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
{/*<Grid>*/}
{/* <Typography mb={2} variant="h4">*/}
{/* {t(`${mode} ${title}`)}*/}
{/* </Typography>*/}
{/*  <Typography mb={2} variant="h4">*/}
{/*    {t(`${mode} ${title}`)}*/}
{/*  </Typography>*/}
{/*</Grid>*/}
<DetailInfoCard
// recordDetails={formProps.formState.defaultValues}
@@ -210,26 +255,23 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
isEditing={false}
/>
{/* <Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Button
variant="contained"
onClick={onClickEdit}
// startIcon={<Add />}
//LinkComponent={Link}
//href="qcCategory/create"
>
{isEdit ? t("Save") : t("Edit")}
</Button>
</Stack> */}
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Button
variant="contained"
onClick={onClickEdit}
>
{isEdit ? t("Save") : t("Edit")}
</Button>
</Stack> */}

{/* <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" />
<Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" />
</Tabs> */}
<Tab label={t("View By FG") + (tabIndex === 0 ? " (Selected)" : "")} iconPosition="end" />
<Tab label={t("View By Material") + (tabIndex === 1 ? " (Selected)" : "")} iconPosition="end" />
</Tabs> */}
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
@@ -247,12 +289,24 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
type={type} />
{/* {tabIndex === 1 && <ViewByBomDetails isEdit={isEdit} apiRef={apiRef} isHideButton={true} />} */}
<Stack direction="row" justifyContent="flex-end" gap={1}>
{/* --- NEW BUTTON: Release Entire Schedule --- */}
<Button
variant="contained"
startIcon={<Check />}
onClick={onGlobalReleaseClick}
disabled={!scheduleId} // Disable if we don't have a schedule ID
>
{t("生成工單")}
</Button>
{/* ------------------------------------------- */}

{/* <Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
// disabled={submitDisabled}
//   disabled={submitDisabled}
>
{isEditMode ? t("Save") : t("Confirm")}
</Button> */}
@@ -269,4 +323,4 @@ const DetailedScheduleDetailView: React.FC<Props> = ({
</>
);
};
export default DetailedScheduleDetailView;
export default DetailedScheduleDetailView;

+ 19
- 13
src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx Прегледај датотеку

@@ -17,28 +17,34 @@ const DetailedScheduleDetailWrapper: React.FC<Props> & SubComponents = async ({
id,
type,
}) => {
// const defaultValues = {
// id: 1,
// productionDate: "2025-05-07",
// totalJobOrders: 13,
// totalProductionQty: 21000,
// };

const prodSchedule = id ? await fetchDetailedProdScheduleDetail(id) : undefined

if (prodSchedule) {
prodSchedule.prodScheduleLines = prodSchedule.prodScheduleLines.sort((a, b) => b.priority - a.priority)
const prodSchedule = id ? await fetchDetailedProdScheduleDetail(id) : undefined;
console.log("RAW API DATA:", prodSchedule?.prodScheduleLines[0]); // Check the actual keys here

if (prodSchedule && prodSchedule.prodScheduleLines) {
// 1. Map the lines to ensure the new fields are explicitly handled
prodSchedule.prodScheduleLines = prodSchedule.prodScheduleLines.map(line => ({
...line,
// If the API uses different names (e.g., 'stockQty'), map them here:
// avgQtyLastMonth: line.avgQtyLastMonth ?? 0,
// Ensure these keys match the 'field' property in your ViewByFGDetails.tsx columns
avgQtyLastMonth: line.avgQtyLastMonth || 0,
stockQty: line.stockQty || 0,
daysLeft: line.daysLeft || 0,
needNoOfJobOrder: line.needNoOfJobOrder || 0,
outputQty: line.outputQty || 0,
})).sort((a, b) => b.priority - a.priority);
}

return (
<DetailedScheduleDetailView
isEditMode={Boolean(id)}
defaultValues={prodSchedule}
type={type}
// qcChecks={qcChecks || []}
/>
);
};

DetailedScheduleDetailWrapper.Loading = GeneralLoading;

export default DetailedScheduleDetailWrapper;
export default DetailedScheduleDetailWrapper;

+ 54
- 151
src/components/DetailedScheduleDetail/ViewByFGDetails.tsx Прегледај датотеку

@@ -30,16 +30,16 @@ type Props = {
onCancelClick: (rowId: number) => void;
};

// export type FGRecord = {
// id: string | number;
// code: string;
// name: string;
// inStockQty: number;
// productionQty?: number;
// purchaseQty?: number;
// };
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => {
const ViewByFGDetails: React.FC<Props> = ({
apiRef,
isEdit,
type,
onReleaseClick,
onEditClick,
handleEditChange,
onSaveClick,
onCancelClick
}) => {
const {
t,
i18n: { language },
@@ -47,83 +47,20 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick

const {
getValues,
watch,
formState: { errors, defaultValues, touchedFields },
} = useFormContext<DetailedProdScheduleResult>();

// const apiRef = useGridApiRef();

// const [pagingController, setPagingController] = useState([
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// {
// pageNum: 1,
// pageSize: 10,
// totalCount: 0,
// },
// ]);

// const updatePagingController = (updatedObj) => {
// setPagingController((prevState) => {
// return prevState.map((item, index) => {
// if (index === updatedObj?.index) {
// return {
// ...item,
// pageNum: item.pageNum,
// pageSize: item.pageSize,
// totalCount: item.totalCount,
// };
// } else return item;
// });
// });
// };

const columns = useMemo<Column<DetailedProdScheduleLineResult>[]>(
() => [
{
field: "jobNo",
label: t("Job No."),
type: "read-only",
// editable: true,
},
{
field: "code",
label: t("code"),
type: "read-only",
// editable: true,
},
{
field: "name",
@@ -134,109 +71,75 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick
field: "type",
label: t("type"),
type: "read-only",
renderCell: (row) => {
return t(row.type);
},
// editable: true,
renderCell: (row) => <>{t(row.type)}</>,
},
// {
// field: "inStockQty",
// label: "Available Qty",
// type: 'read-only',
// style: {
// textAlign: "right",
// },
// // editable: true,
// renderCell: (row: FGRecord) => {
// if (typeof (row.inStockQty) == "number") {
// return decimalFormatter.format(row.inStockQty)
// }
// return row.inStockQty
// }
// },
{
field: "demandQty",
label: t("Demand Qty"),
type: "input-number",
style: {
textAlign: "right",
// width: "100px",
},
renderCell: (row) => {
if (typeof row.demandQty == "number") {
return integerFormatter.format(row.demandQty ?? 0);
}
return row.demandQty;
},
style: { textAlign: "right" } as any, // Use 'as any' to bypass strict CSS validation
renderCell: (row) => <>{integerFormatter.format(row.demandQty ?? 0)}</>,
},
{
field: "uomName",
label: t("UoM"),
type: "read-only",
style: {
textAlign: "left",
// width: "100px",
},
renderCell: (row) => {
return row.uomName;
},
renderCell: (row) => <>{row.uomName}</>,
},
// --- Added Avg Usage, Stock, Days Left, and Job Order Count ---
{
field: "avgQtyLastMonth", // This MUST match the key in the object
label: t("最近每日用量"),
type: "read-only",
// Ensure 'row' has the property 'avgQtyLastMonth'
renderCell: (row) => <>{decimalFormatter.format(row.avgQtyLastMonth ?? 0)}</>,
},
{
field: "stockQty",
label: t("存貨量"),
type: "read-only",
style: { textAlign: "right" } as any,
renderCell: (row) => <>{decimalFormatter.format(row.stockQty ?? 0)}</>,
},
{
field: "prodTimeInMinute",
label: t("Estimated Production Time"),
field: "daysLeft",
label: t("可用日"),
type: "read-only",
style: {
textAlign: "right",
// width: "100px",
},
renderCell: (row) => {
return <ProdTimeColumn prodTimeInMinute={row.prodTimeInMinute} />
}
style: { textAlign: "right" } as any,
renderCell: (row) => <>{row.daysLeft ?? 0}</>,
},
{
field: "outputQty",
label: t("每批次生產數"),
type: "read-only",
style: { textAlign: "right", fontWeight: "bold" } as any,
renderCell: (row) => <>{row.outputQty ?? 0}</>,
},
{
field: "needNoOfJobOrder",
label: t("生產批次"),
type: "read-only",
style: { textAlign: "right", fontWeight: "bold" } as any,
renderCell: (row) => <>{row.needNoOfJobOrder ?? 0}</>,
},
// -------------------------------------------------------------
{
field: "priority",
label: t("Production Priority"),
type: "read-only",
style: {
textAlign: "right",
// width: "100px",
},
// editable: true,
style: { textAlign: "right" } as any,
},
],
[],
[t]
);

return (
<Grid container spacing={2}>
{/* <Grid item xs={12} key={"all"}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("FG Demand List (7 Days)")}
</Typography>
<EditableSearchResults<FGRecord>
index={7}
items={fakeOverallRecords}
columns={overallColumns}
setPagingController={updatePagingController}
pagingController={pagingController[7]}
isAutoPaging={false}
isEditable={false}
isEdit={isEdit}
hasCollapse={true}
/>
</Grid> */}
{/* {dayPeriod.map((date, index) => ( */}
<Grid item xs={12}>
{/* <Typography variant="overline" display="block" marginBlockEnd={1}>
{`${t("FG Demand Date")}: ${date}`}
</Typography> */}
<ScheduleTable<DetailedProdScheduleLineResult>
type={type}
// items={fakeRecords[index]} // Use the corresponding records for the day
items={getValues("prodScheduleLines")} // Use the corresponding records for the day
items={getValues("prodScheduleLines")}
columns={columns}
// setPagingController={updatePagingController}
// pagingController={pagingController[index]}
isAutoPaging={false}
isEditable={true}
isEdit={isEdit}
@@ -248,8 +151,8 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick
onCancelClick={onCancelClick}
/>
</Grid>
{/* ))} */}
</Grid>
);
};
export default ViewByFGDetails;
}; // Added missing closing brace

export default ViewByFGDetails;

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

@@ -8,14 +8,16 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { isFinite } from "lodash";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react";
import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { msg } from "../Swal/CustomAlerts";
import { JobTypeResponse } from "@/app/api/jo/actions";

interface Props {
open: boolean;
bomCombo: BomCombo[];
jobTypes: JobTypeResponse[];
onClose: () => void;
onSearch: () => void;
}
@@ -23,6 +25,7 @@ interface Props {
const JoCreateFormModal: React.FC<Props> = ({
open,
bomCombo,
jobTypes,
onClose,
onSearch,
}) => {
@@ -30,19 +33,130 @@ const JoCreateFormModal: React.FC<Props> = ({
const formProps = useForm<SaveJo>({
mode: "onChange",
});
const { reset, trigger, watch, control, register, formState: { errors } } = formProps
const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps

// 监听 bomId 变化
const selectedBomId = watch("bomId");

const onModalClose = useCallback(() => {
reset()
onClose()
}, [])
}, [reset, onClose])

const handleAutoCompleteChange = useCallback(
(event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
console.log("BOM changed to:", value);
onChange(value.id);

// 1) 根据 BOM 设置数量
if (value.outputQty != null) {
formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true });
}

const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
onChange(value.id)
if (value.outputQty != null) {
formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true })
// 2) 选 BOM 时,把日期默认设为“今天”
const today = dayjs();
const todayStr = dayjsToDateString(today, "input"); // 你已经有的工具函数
formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true });
},
[formProps]
);

// 使用 useMemo 来计算过滤后的 jobTypes,响应 selectedBomId 变化
/*
const filteredJobTypes = useMemo(() => {
console.log("getFilteredJobTypes called, selectedBomId:", selectedBomId);
if (!selectedBomId) {
console.log("No BOM selected, returning all jobTypes:", jobTypes);
return jobTypes;
}
}, [])
const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
console.log("Selected BOM:", selectedBom);
console.log("Selected BOM full object:", JSON.stringify(selectedBom, null, 2));
if (!selectedBom) {
console.log("BOM not found, returning all jobTypes");
return jobTypes;
}
// 检查 description 是否存在
const description = selectedBom.description;
console.log("BOM description (raw):", description);
console.log("BOM description type:", typeof description);
console.log("BOM description is undefined?", description === undefined);
console.log("BOM description is null?", description === null);
if (!description) {
console.log("BOM description is missing or empty, returning all jobTypes");
return jobTypes;
}
const descriptionUpper = description.toUpperCase();
console.log("BOM description (uppercase):", descriptionUpper);
console.log("All jobTypes:", jobTypes);
let filtered: JobTypeResponse[] = [];
if (descriptionUpper === "WIP") {
filtered = jobTypes.filter(jt => {
const jobTypeName = jt.name.toUpperCase();
const shouldInclude = jobTypeName !== "FG";
console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`);
return shouldInclude;
});
} else if (descriptionUpper === "FG") {
filtered = jobTypes.filter(jt => {
const jobTypeName = jt.name.toUpperCase();
const shouldInclude = jobTypeName !== "WIP";
console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`);
return shouldInclude;
});
} else {
filtered = jobTypes;
}
console.log("Filtered jobTypes:", filtered);
return filtered;
}, [bomCombo, jobTypes, selectedBomId]);
*/
// 当 BOM 改变时,自动选择匹配的 Job Type
useEffect(() => {
if (!selectedBomId) {
return;
}
const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
if (!selectedBom) {
return;
}
const description = selectedBom.description;
console.log("Auto-select effect - BOM description:", description);
if (!description) {
console.log("Auto-select effect - No description found, skipping auto-select");
return;
}
const descriptionUpper = description.toUpperCase();
console.log("Auto-selecting Job Type for BOM description:", descriptionUpper);
// 查找匹配的 Job Type
const matchingJobType = jobTypes.find(jt => {
const jobTypeName = jt.name.toUpperCase();
const matches = jobTypeName === descriptionUpper;
console.log(`Checking JobType ${jt.name} (${jobTypeName}) against ${descriptionUpper}: ${matches}`);
return matches;
});
if (matchingJobType) {
console.log("Found matching Job Type, setting jobTypeId to:", matchingJobType.id);
setValue("jobTypeId", matchingJobType.id, { shouldValidate: true, shouldDirty: true });
} else {
console.log("No matching Job Type found for description:", descriptionUpper);
}
}, [selectedBomId, bomCombo, jobTypes, setValue]);

const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
if (value != null) {
@@ -98,7 +212,7 @@ const JoCreateFormModal: React.FC<Props> = ({
<Box
sx={{
display: "flex",
"flex-direction": "column",
flexDirection: "column",
padding: "20px",
height: "100%", //'30rem',
width: "100%",
@@ -199,36 +313,42 @@ const JoCreateFormModal: React.FC<Props> = ({
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}
name="jobTypeId"
rules={{ required: t("Job Type required!") as string }}
render={({ field, fieldState: { error } }) => (
<FormControl fullWidth error={Boolean(error)}>
<InputLabel>{t("Job Type")}</InputLabel>
<Select
<Controller
control={control}
name="jobTypeId"
rules={{ required: t("Job Type required!") as string }}
render={({ field, fieldState: { error } }) => {
//console.log("Job Type Select render - filteredJobTypes:", filteredJobTypes);
//console.log("Current field.value:", field.value);
return (
<FormControl fullWidth error={Boolean(error)}>
<InputLabel>{t("Job Type")}</InputLabel>
<Select
{...field}
label={t("Job Type")}
value={field.value?.toString() ?? ""}
onChange={(event) => {
const value = event.target.value;
console.log("Job Type changed to:", value);
field.onChange(value === "" ? undefined : Number(value));
}}
>
>
<MenuItem value="">
<em>{t("Please select")}</em>
</MenuItem>
<MenuItem value="1">{t("FG")}</MenuItem>
<MenuItem value="2">{t("WIP")}</MenuItem>
<MenuItem value="3">{t("R&D")}</MenuItem>
<MenuItem value="4">{t("STF")}</MenuItem>
<MenuItem value="5">{t("Other")}</MenuItem>
</Select>
{/*{error && <FormHelperText>{error.message}</FormHelperText>}*/}
</FormControl>
)}
/>
</Grid>
{/* {filteredJobTypes.map((jobType) => (*/}
{jobTypes.map((jobType) => (
<MenuItem key={jobType.id} value={jobType.id.toString()}>
{t(jobType.name)}
</MenuItem>
))}
</Select>
</FormControl>
);
}}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}


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

@@ -398,6 +398,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
<JoCreateFormModal
open={isCreateJoModalOpen}
bomCombo={bomCombo}
jobTypes={jobTypes}
onClose={onCloseCreateJoModal}
onSearch={() => {
setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化


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

@@ -47,7 +47,10 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
useEffect(() => {
fetchPickOrders();
}, [fetchPickOrders]);

const handleBackToList = useCallback(() => {
setSelectedPickOrderId(undefined);
setSelectedJobOrderId(undefined);
}, []);
// If a pick order is selected, show JobPickExecution detail view
if (selectedPickOrderId !== undefined) {
return (
@@ -64,7 +67,11 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
{t("Back to List")}
</Button>
</Box>
<JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} />
<JobPickExecution
filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }}
//onSwitchToRecordTab={onSwitchToRecordTab}
onBackToList={handleBackToList} // 传递新的回调
/>
</Box>
);
}


+ 15
- 10
src/components/Jodetail/JobPickExecutionForm.tsx Прегледај датотеку

@@ -196,16 +196,19 @@ useEffect(() => {
if (verifiedQty === undefined || verifiedQty < 0) {
newErrors.actualPickQty = t('Qty is required');
}
const totalQty = verifiedQty + badItemQty + missQty;
const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0;
// ✅ 新增:必须至少有一个 > 0
if (!hasAnyValue) {
newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0');
}
if (hasAnyValue && totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -214,9 +217,10 @@ useEffect(() => {
return;
}
// Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0)
const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0))
&& formData.missQty == 0 && formData.badItemQty == 0;
// ✅ 只允许 Verified>0 且没有问题时,走 normal pick
const isNormalPick = verifiedQty > 0
&& formData.missQty == 0
&& formData.badItemQty == 0;
if (isNormalPick) {
if (onNormalPickSubmit) {
@@ -235,11 +239,12 @@ useEffect(() => {
}
return;
}

// ❌ 有问题(或全部为 0)才进入 Issue 提报流程
if (!validateForm() || !formData.pickOrderId) {
return;
}
setLoading(true);
try {
const submissionData = {


+ 39
- 10
src/components/Jodetail/JobPickExecutionsecondscan.tsx Прегледај датотеку

@@ -487,7 +487,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
matchStatus: lot.matchStatus,
routerArea: lot.routerArea,
routerRoute: lot.routerRoute,
uomShortDesc: lot.uomShortDesc
uomShortDesc: lot.uomShortDesc,
handler: lot.handler,
});
});
}
@@ -574,7 +575,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {

const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.matchStatus === 'scanned'
lot.matchStatus === 'scanned'||
lot.stockOutLineStatus === 'completed'
);
if (scannedLots.length === 0) {
@@ -614,7 +616,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
if (successCount > 0) {
setQrScanSuccess(true);
setTimeout(() => setQrScanSuccess(false), 2000);
setTimeout(() => {
setQrScanSuccess(false);
// 添加:提交成功后返回到列表
if (onBack) {
onBack();
}
}, 2000);
}
} catch (error: any) {
@@ -634,7 +642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
} finally {
setIsSubmittingAll(false);
}
}, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign]);
}, [combinedLotData, fetchJobOrderData, currentPickOrderId, handleUnassign, onBack]);

const scannedItemsCount = useMemo(() => {
return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length;
@@ -1112,7 +1120,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
)}

{/* Combined Lot Table */}
<Box>
<Button
variant="contained"
color="success"
onClick={handleSubmitAllScanned}
disabled={isSubmittingAll}
sx={{ minWidth: '160px' }}
>
{isSubmittingAll ? (
<>
<CircularProgress size={16} sx={{ mr: 1 }} />
{t("Submitting...")}
</>
) : (
t("Confirm All")
)}
</Button>
{/*
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
{!isManualScanning ? (
@@ -1166,18 +1192,19 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
{t("QR code verified.")}
</Alert>
)}
*/}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Index")}</TableCell>
<TableCell>{t("Route")}</TableCell>
<TableCell>{t("Handler")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell align="center">{t("Scan Result")}</TableCell>
{/* <TableCell align="center">{t("Scan Result")}</TableCell> */}
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
</TableRow>
</TableHead>
@@ -1212,6 +1239,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
{lot.routerRoute || '-'}
</Typography>
</TableCell>
<TableCell>{lot.handler || '-'}</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
<TableCell>
@@ -1232,7 +1260,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
})()}
</TableCell>
{/*
<TableCell align="center">
{lot.matchStatus?.toLowerCase() === 'scanned' ||
lot.matchStatus?.toLowerCase() === 'completed' ? (
@@ -1266,7 +1294,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
</Typography>
)}
</TableCell>
*/}
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Stack direction="row" spacing={1} alignItems="center">
@@ -1277,9 +1305,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty);
updateSecondQrScanStatus(lot.pickOrderLineId, lot.lotId, currentUserId || 0, submitQty);
}}
disabled={
lot.matchStatus !== 'scanned' ||
//lot.matchStatus !== 'scanned' ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'
@@ -1291,7 +1320,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
minWidth: '70px'
}}
>
{t("Submit")}
{t("Confirm")}
</Button>
<Button


+ 87
- 79
src/components/Jodetail/JodetailSearch.tsx Прегледај датотеку

@@ -15,7 +15,7 @@ import {
import {
arrayToDayjs,
} from "@/app/utils/formatUtil";
import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material";
import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box, TextField, Autocomplete } from "@mui/material";
import Jodetail from "./Jodetail"
import PickExecution from "./JobPickExecution";
import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
@@ -63,12 +63,18 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders, printerCombo }) => {
const [totalCount, setTotalCount] = useState<number>();
const [isAssigning, setIsAssigning] = useState(false);
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false);
const [hasDataTab0, setHasDataTab0] = useState(false);
const [hasDataTab1, setHasDataTab1] = useState(false);
const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
//const [printers, setPrinters] = useState<PrinterCombo[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false);
const [hasDataTab0, setHasDataTab0] = useState(false);
const [hasDataTab1, setHasDataTab1] = useState(false);
const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
// Add printer selection state
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
);
const [printQty, setPrintQty] = useState<number>(1);
const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
);
@@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener);
};
}, []);
/*
useEffect(() => {
const fetchPrinters = async () => {
try {
// 需要创建一个客户端版本的 fetchPrinterCombo
// 或者使用 API 路由
// const printersData = await fetch('/api/printers/combo').then(r => r.json());
// setPrinters(printersData);
} catch (error) {
console.error("Error fetching printers:", error);
}
};
fetchPrinters();
}, []);
*/
useEffect(() => {
const onAssigned = () => {
localStorage.removeItem('hideCompletedUntilNext');
@@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
window.addEventListener('pickOrderAssigned', onAssigned);
return () => window.removeEventListener('pickOrderAssigned', onAssigned);
}, []);
// ... existing code ...

useEffect(() => {
const handleCompletionStatusChange = (event: CustomEvent) => {
@@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
return () => {
window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
};
}, [tabIndex]); // 添加 tabIndex 依赖
}, [tabIndex]);

// 新增:处理标签页切换时的打印按钮状态重置
useEffect(() => {
@@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
}
}, [tabIndex]);

// ... existing code ...
const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
if (!currentUserId) {
console.error("Missing user id in session");
@@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;

return (
<Box sx={{
height: '100vh', // Full viewport height
overflow: 'auto' // Single scrollbar for the whole page
height: '100vh',
overflow: 'auto'
}}>
{/* Header section */}
<Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
<Stack rowGap={2}>
<Grid container alignItems="center">
<Grid item xs={8}>

</Grid>
{/* Last 2 buttons aligned right

<Grid item xs={6} >
{!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && (
<Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
{t("Unassigned Job Orders")} ({unassignedOrders.length})
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap">
{unassignedOrders.map((order) => (
<Button
key={order.pickOrderId}
variant="outlined"
size="small"
onClick={() => handleAssignOrder(order.pickOrderId)}
disabled={isLoadingUnassigned}
>
{order.pickOrderCode} - {order.jobOrderName}
</Button>
))}
</Stack>
</Box>
)}
</Grid>
*/}
{/* Header section with printer selection */}
<Box sx={{
p: 1,
borderBottom: '1px solid #e0e0e0',
minHeight: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
flexWrap: 'wrap',
}}>
{/* Left side - Title */}

</Grid>
</Stack>
</Box>
{/* Right side - Printer selection (only show on tab 1) */}
{tabIndex === 1 && (
<Stack
direction="row"
spacing={2}
sx={{
alignItems: 'center',
flexWrap: 'wrap',
rowGap: 1,
}}
>
<Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}>
{t("Select Printer")}:
</Typography>
<Autocomplete
options={printerCombo || []}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter}
onChange={(_, newValue) => setSelectedPrinter(newValue)}
sx={{ minWidth: 200 }}
size="small"
renderInput={(params) => (
<TextField {...params} placeholder={t("Printer")} />
)}
/>
<Typography variant="body2" sx={{ minWidth: 'fit-content', ml: 1 }}>
{t("Print Quantity")}:
</Typography>
<TextField
type="number"
label={t("Print Quantity")}
value={printQty}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setPrintQty(Math.max(1, value));
}}
inputProps={{ min: 1, step: 1 }}
sx={{ width: 120 }}
size="small"
/>
</Stack>
)}
</Box>

{/* Tabs section - Move the click handler here */}
{/* Tabs section */}
<Box sx={{
borderBottom: '1px solid #e0e0e0'
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
{/* <Tab label={t("Pick Order Detail")} iconPosition="end" /> */}
<Tab label={t("Jo Pick Order Detail")} iconPosition="end" />
<Tab label={t("Complete Job Order Record")} iconPosition="end" />

{/* <Tab label={t("Job Order Match")} iconPosition="end" /> */}
{/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */}
<Tab label={t("Jo Pick Order Detail")} iconPosition="end" />
<Tab label={t("Complete Job Order Record")} iconPosition="end" />
</Tabs>
</Box>

{/* Content section - NO overflow: 'auto' here */}
<Box sx={{
p: 2
}}>
{/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */}
{tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />}
{/* Content section */}
<Box sx={{ p: 2 }}>
{tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />}
{/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */}
{/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */}
{tabIndex === 1 && (
<CompleteJobOrderRecord
filterArgs={filterArgs}
printerCombo={printerCombo}
selectedPrinter={selectedPrinter}
printQty={printQty}
/>
)}
</Box>
</Box>
);


+ 32
- 68
src/components/Jodetail/completeJobOrderRecord.tsx Прегледај датотеку

@@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer";
interface Props {
filterArgs: Record<string, any>;
printerCombo: PrinterCombo[];
selectedPrinter?: PrinterCombo | null;
printQty?: number;
}

// 修改:已完成的 Job Order Pick Order 接口
@@ -99,9 +101,15 @@ interface LotDetail {
itemName: string;
uomCode: string;
uomDesc: string;
match_status: string;
}

const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) => {
const CompleteJobOrderRecord: React.FC<Props> = ({
filterArgs,
printerCombo,
selectedPrinter: selectedPrinterProp,
printQty: printQtyProp
}) => {
const { t } = useTranslation("jo");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -121,25 +129,11 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
// 修改:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
//const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]);
const defaultDemoPrinter: PrinterCombo = {
id: 2,
value: 2,
name: "2fi",
label: "2fi",
code: "2fi"
};
const availablePrinters = useMemo(() => {
if (printerCombo.length === 0) {
console.log("No printers available, using default demo printer");
return [defaultDemoPrinter];
}
return printerCombo;
}, [printerCombo]);
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
);
const [printQty, setPrintQty] = useState<number>(1);
// Use props with fallback
const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null);
const printQty = printQtyProp ?? 1;
// 修改:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
@@ -157,7 +151,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
try {
console.log("🔍 Fetching completed Job Order pick orders (pick completed only)...");
const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId);
const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords();
// Fix: Ensure the data is always an array
const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : [];
@@ -226,7 +220,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
setFilteredJobOrderPickOrders(filtered);
console.log("Filtered Job Order pick orders count:", filtered.length);
}, [completedJobOrderPickOrders]);

const formatDateTime = (value: any) => {
if (!value) return "-";
// 后端发来的是 [yyyy, MM, dd, HH, mm, ss]
if (Array.isArray(value)) {
const [year, month, day, hour = 0, minute = 0, second = 0] = value;
return new Date(year, month - 1, day, hour, minute, second).toLocaleString();
}
// 如果以后改成字符串/ISO,也兼容
const d = new Date(value);
return isNaN(d.getTime()) ? "-" : d.toLocaleString();
};
// 修改:重置搜索
const handleSearchReset = useCallback(() => {
setSearchQuery({});
@@ -433,18 +439,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
<strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom}
</Typography>
</Stack>
{/*
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}>
<Button
variant="contained"
color="primary"
onClick={() => handlePickRecord(selectedJobOrderPickOrder)}
sx={{ mt: 1 }}
>
{t("Print Pick Record")}
</Button>
</Stack>
*/}
</CardContent>
</Card>

@@ -545,12 +539,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
height: '100%'
}}>
<Checkbox
checked={lot.secondQrScanStatus === 'completed'}
checked={lot.match_status === 'completed'}
disabled={true}
readOnly={true}
size="large"
sx={{
color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400',
color: lot.match_status === 'completed' ? 'success.main' : 'grey.400',
'&.Mui-checked': {
color: 'success.main',
},
@@ -600,37 +594,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")}
</Typography>
<Box sx={{ mb: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1, bgcolor: 'background.paper' }}>
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
<Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}>
{t("Select Printer")}:
</Typography>
<Autocomplete
options={availablePrinters}
getOptionLabel={(option) => option.name || option.label || option.code || `Printer ${option.id}`}
value={selectedPrinter}
onChange={(_, newValue) => setSelectedPrinter(newValue)}
sx={{ minWidth: 250 }}
size="small"
renderInput={(params) => <TextField {...params} label={t("Printer")} />}
/>
<Typography variant="subtitle1" sx={{ minWidth: 'fit-content' }}>
{t("Print Quantity")}:
</Typography>
<TextField
type="number"
label={t("Print Quantity")}
value={printQty}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setPrintQty(Math.max(1, value));
}}
inputProps={{ min: 1, step: 1 }}
sx={{ width: 120 }}
size="small"
/>
</Stack>
</Box>
{/* 列表 */}
{filteredJobOrderPickOrders.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
@@ -652,7 +616,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs ,printerCombo}) =>
{jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()}
{t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate}


+ 105
- 35
src/components/Jodetail/newJobPickExecution.tsx Прегледај датотеку

@@ -42,8 +42,6 @@ import {
} from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API
import {
//fetchJobOrderLotsHierarchical,
//fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder,
fetchJobOrderLotsHierarchicalByPickOrderId,
updateJoPickOrderHandledBy,
@@ -67,7 +65,8 @@ import FGPickOrderCard from "./FGPickOrderCard";
import LotConfirmationModal from "./LotConfirmationModal";
interface Props {
filterArgs: Record<string, any>;
onSwitchToRecordTab: () => void;
//onSwitchToRecordTab: () => void;
onBackToList?: () => void;
}

// QR Code Modal Component (from GoodPickExecution)
@@ -324,7 +323,7 @@ const QrCodeModal: React.FC<{
);
};

const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) => {
const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const { t } = useTranslation("jo");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -412,6 +411,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
pickOrderType: data.pickOrder.type,
pickOrderStatus: data.pickOrder.status,
pickOrderAssignTo: data.pickOrder.assignTo,
handler: line.handler,
});
});
}
@@ -537,6 +537,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
setCombinedDataLoading(false);
}
}, [getAllLotsFromHierarchical]);

const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
if (!currentUserId || !pickOrderId || !itemId) {
return;
@@ -901,11 +902,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
// Use the first active suggested lot as the "expected" lot
const expectedLot = activeSuggestedLots[0];
// 2) Check if the scanned lot matches exactly
if (scanned?.lotNo === expectedLot.lotNo) {
// ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快)
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`);
if (!expectedLot.stockOutLineId) {
console.warn("No stockOutLineId on expectedLot, cannot update status by QR.");
setQrScanError(true);
@@ -922,24 +921,33 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
status: "checked",
});
if (res.code === "checked" || res.code === "SUCCESS") {
setQrScanError(false);
setQrScanSuccess(true);
const updateOk =
res?.type === "checked" ||
typeof res?.id === "number" ||
(res?.message && res.message.includes("success"));
if (updateOk) {
setQrScanError(false);
setQrScanSuccess(true);
// ✅ 刷新数据而不是直接更新 state
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
console.log("✅ Status updated, data refreshed");
} else if (res.code === "LOT_NUMBER_MISMATCH") {
console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else if (res.code === "ITEM_MISMATCH") {
console.warn("Backend reported ITEM_MISMATCH:", res.message);

if (
expectedLot.pickOrderId &&
expectedLot.itemId &&
(expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" ||
!expectedLot.stockOutLineStatus) &&
!expectedLot.handler
) {
await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId);
}
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
} else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") {
setQrScanError(true);
setQrScanSuccess(false);
} else {
console.warn("Unexpected response code from backend:", res.code);
console.warn("Unexpected response from backend:", res);
setQrScanError(true);
setQrScanSuccess(false);
}
@@ -949,7 +957,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
setQrScanSuccess(false);
}
return; // ✅ 直接返回,不再调用 handleQrCodeSubmit
return; // ✅ 直接返回,不再调用后面的分支
}
// Case 2: Same item, different lot - show confirmation modal
@@ -977,7 +985,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
setQrScanSuccess(false);
return;
}
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]);
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]);


const handleManualInputSubmit = useCallback(() => {
@@ -1310,6 +1318,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
console.error("Error submitting pick quantity:", error);
}
}, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
const handleSkip = useCallback(async (lot: any) => {
try {
console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
await handleSubmitPickQtyWithQty(lot, 0);
} catch (err) {
console.error("Error in Skip:", err);
}
}, [handleSubmitPickQtyWithQty]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.stockOutLineStatus === 'checked'
@@ -1365,8 +1381,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
setTimeout(() => {
setQrScanSuccess(false);
checkAndAutoAssignNext();
if (onSwitchToRecordTab) {
onSwitchToRecordTab();
if (onBackToList) {
onBackToList();
}
}, 2000);
} else {
@@ -1380,7 +1396,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
} finally {
setIsSubmittingAll(false);
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab])
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList])

// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
@@ -1544,7 +1560,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
}, [startScan]);

const handleStopScan = useCallback(() => {
console.log("⏹️ Stopping manual QR scan...");
console.log(" Stopping manual QR scan...");
setIsManualScanning(false);
setQrScanError(false);
setQrScanSuccess(false);
@@ -1563,7 +1579,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
}, [isManualScanning, stopScan, resetScan]);
useEffect(() => {
if (isManualScanning && combinedLotData.length === 0) {
console.log("⏹️ No data available, auto-stopping QR scan...");
console.log(" No data available, auto-stopping QR scan...");
handleStopScan();
}
}, [combinedLotData.length, isManualScanning, handleStopScan]);
@@ -1677,16 +1693,59 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
</Box>
</Box>

{qrScanError && !qrScanSuccess && (
<Alert severity="error" sx={{ mb: 2 }}>
{qrScanError && !qrScanSuccess && (
<Alert
severity="error"
sx={{
mb: 2,
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
fontSize: "1rem",
color: "error.main", // ✅ 整个 Alert 文字用错误红
"& .MuiAlert-message": {
width: "100%",
textAlign: "center",
// color: "error.main", // ✅ 明确指定 message 文字颜色
},
"& .MuiSvgIcon-root": {
color: "error.main", // 图标继续红色(可选)
},
backgroundColor: "error.light",
}}
>
{t("QR code does not match any item in current orders.")}
</Alert>
)}
{qrScanSuccess && (
<Alert severity="success" sx={{ mb: 2 }}>
{t("QR code verified.")}
</Alert>
)}
{qrScanSuccess && (
<Alert
severity="success"
sx={{
mb: 2,
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
fontSize: "1rem",
// 背景用很浅的绿色
bgcolor: "rgba(76, 175, 80, 0.08)",
// 文字用主题 success 绿
color: "success.main",
// 去掉默认强烈的色块感
"& .MuiAlert-icon": {
color: "success.main",
},
"& .MuiAlert-message": {
width: "100%",
textAlign: "center",
color: "success.main",
},
}}
>
{t("QR code verified.")}
</Alert>
)}
<TableContainer component={Paper}>
<Table>
@@ -1694,6 +1753,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
<TableRow>
<TableCell>{t("Index")}</TableCell>
<TableCell>{t("Route")}</TableCell>
<TableCell>{t("Handler")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
@@ -1733,6 +1793,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
{lot.routerRoute || '-'}
</Typography>
</TableCell>
<TableCell>{lot.handler || '-'}</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
<TableCell>
@@ -1837,6 +1898,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab })
>
{t("Issue")}
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handleSkip(lot)}
disabled={lot.stockOutLineStatus === 'completed'}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
>
{t("Skip")}
</Button>
</Stack>
</Box>
</TableCell>


+ 255
- 0
src/components/ProductionProcess/FinishedQcJobOrderList.tsx Прегледај датотеку

@@ -0,0 +1,255 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Button,
Stack,
Typography,
Chip,
CircularProgress,
TablePagination,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Tooltip,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { useTranslation } from "react-i18next";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchJoForPrintQrCode,
JobOrderListForPrintQrCodeResponse,
printFGStockInLabel,
PrintFGStockInLabelRequest,
} from "@/app/api/jo/actions";
import { PrinterCombo } from "@/app/api/settings/printer";

interface FinishedQcJobOrderListProps {
printerCombo: PrinterCombo[];
selectedPrinter: PrinterCombo | null;
}

const PER_PAGE = 10;

const FinishedQcJobOrderList: React.FC<FinishedQcJobOrderListProps> = ({
printerCombo,
selectedPrinter,
}) => {
const { t } = useTranslation(["common"]);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const [loading, setLoading] = useState(false);
const [jobOrders, setJobOrders] = useState<JobOrderListForPrintQrCodeResponse[]>([]);
const [page, setPage] = useState(0);
const [isPrinting, setIsPrinting] = useState(false);
const [printingId, setPrintingId] = useState<number | null>(null);
const [selectedDate, setSelectedDate] = useState<string>("today");
const getDateLabel = (offset: number) => {
return dayjs().subtract(offset, 'day').format('YYYY-MM-DD');
};

// 根据选择的日期获取实际日期字符串
const getDateParam = (dateOption: string): string => {
if (dateOption === "today") {
return dayjs().format('YYYY-MM-DD');
} else if (dateOption === "yesterday") {
return dayjs().subtract(1, 'day').format('YYYY-MM-DD');
} else if (dateOption === "dayBeforeYesterday") {
return dayjs().subtract(2, 'day').format('YYYY-MM-DD');
}
return dayjs().format('YYYY-MM-DD');
};

const fetchJobOrders = useCallback(async () => {
setLoading(true);
try {
const dateParam = getDateParam(selectedDate);
const data = await fetchJoForPrintQrCode(dateParam);
setJobOrders(data || []);
setPage(0);
} catch (e) {
console.error(e);
setJobOrders([]);
} finally {
setLoading(false);
}
}, [selectedDate]);



useEffect(() => {
fetchJobOrders();
}, [fetchJobOrders]);

const handlePrint = useCallback(async (jobOrder: JobOrderListForPrintQrCodeResponse) => {
if (!selectedPrinter) {
alert(t("Please select a printer"));
return;
}

// Use stockInLineId from the response (assuming backend returns it)
// If the backend still returns stockOutLineId, you may need to update the interface
const stockInLineId = (jobOrder as any).stockInLineId || jobOrder.stockOutLineId;
if (!stockInLineId) {
alert(t("Invalid Stock In Line Id"));
return;
}

try {
setIsPrinting(true);
setPrintingId(jobOrder.id);
const data: PrintFGStockInLabelRequest = {
stockInLineId: stockInLineId,
printerId: selectedPrinter.id,
printQty: 1 // Default to 1
};
const response = await printFGStockInLabel(data);
if (response) {
console.log("Print response:", response);
alert(t("Print job sent successfully"));
}
} catch (error: any) {
console.error("Error printing:", error);
alert(t(`Print failed: ${error?.message || "Unknown error"}`));
} finally {
setIsPrinting(false);
setPrintingId(null);
}
}, [selectedPrinter, t]);

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

return (
<Box>
{/* Date Selector */}
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>
<Box sx={{ maxWidth: 300 }}>
<FormControl fullWidth size="small">
<InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
<Select
labelId="date-select-label"
id="date-select"
value={selectedDate}
// label={t("Select Date")}
onChange={(e) => {
setSelectedDate(e.target.value);
}}
>
<MenuItem value="today">
{t("Today")} ({getDateLabel(0)})
</MenuItem>
<MenuItem value="yesterday">
{t("Yesterday")} ({getDateLabel(1)})
</MenuItem>
<MenuItem value="dayBeforeYesterday">
{t("Day Before Yesterday")} ({getDateLabel(2)})
</MenuItem>
</Select>
</FormControl>
</Box>
</Stack>

{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total finished QC job orders")}: {jobOrders.length}
</Typography>

<TableContainer component={Paper} sx={{ boxShadow: 2 }}>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow sx={{ bgcolor: "grey.50" }}>
<TableCell sx={{ fontWeight: "bold" }}>{t("Code")}</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>{t("Name")}</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>{t("Required Qty")}</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>{t("Finished Time")}</TableCell>
<TableCell sx={{ fontWeight: "bold" }} align="center">{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((jobOrder) => {
const statusColor = jobOrder.stockOutLineStatus === "completed"
? "success"
: "default";

const finishedTimeDisplay = jobOrder.finihedTime
? dayjs(jobOrder.finihedTime).format(OUTPUT_DATE_FORMAT)
: "-";

const isCurrentlyPrinting = isPrinting && printingId === jobOrder.id;

return (
<TableRow
key={jobOrder.id}
sx={{
"&:last-child td, &:last-child th": { border: 0 },
"&:hover": { bgcolor: "grey.50" },
}}
>
<TableCell component="th" scope="row">
{jobOrder.code}
</TableCell>
<TableCell>{jobOrder.name}</TableCell>
<TableCell>{jobOrder.reqQty}</TableCell>

<TableCell>{finishedTimeDisplay}</TableCell>
<TableCell align="center">
<Tooltip title={t("Print QR Code")}>
<IconButton
color="primary"
onClick={() => handlePrint(jobOrder)}
disabled={isPrinting || printerCombo.length <= 0 || !selectedPrinter}
size="small"
>
{isCurrentlyPrinting ? (
<CircularProgress size={20} />
) : (
<QrCodeIcon />
)}
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>

{jobOrders.length > 0 && (
<TablePagination
component="div"
count={jobOrders.length}
page={page}
rowsPerPage={PER_PAGE}
onPageChange={(e, p) => setPage(p)}
rowsPerPageOptions={[PER_PAGE]}
/>
)}
</Box>
)}
</Box>
);
};

export default FinishedQcJobOrderList;

+ 60
- 45
src/components/ProductionProcess/ProductionProcessDetail.tsx Прегледај датотеку

@@ -33,16 +33,11 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchProductProcessById,
updateProductProcessLineQrscan,
// updateProductProcessLineQrscan,
newUpdateProductProcessLineQrscan,
fetchProductProcessLineDetail,
ProductProcessLineDetailResponse,
JobOrderProcessLineDetailResponse,
updateLineOutput,
ProductProcessLineInfoResponse,
ProductProcessResponse,
ProductProcessLineResponse,
completeProductProcessLine,
startProductProcessLine,
fetchProductProcessesByJobOrderId
} from "@/app/api/jo/actions";
@@ -61,7 +56,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
onBack,
fromJosave,
}) => {
const { t } = useTranslation();
const { t } = useTranslation("common");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
@@ -80,8 +75,10 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [scannedOperatorId, setScannedOperatorId] = useState<number | null>(null);
const [scannedEquipmentId, setScannedEquipmentId] = useState<number | null>(null);
const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null);
// const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState<string | null>(null);
const [scannedStaffNo, setScannedStaffNo] = useState<string | null>(null);
// const [scannedEquipmentDetailId, setScannedEquipmentDetailId] = useState<number | null>(null);
const [scannedEquipmentCode, setScannedEquipmentCode] = useState<string | null>(null);
const [scanningLineId, setScanningLineId] = useState<number | null>(null);
const [lineDetailForScan, setLineDetailForScan] = useState<JobOrderProcessLineDetailResponse | null>(null);
const [showScanDialog, setShowScanDialog] = useState(false);
@@ -224,7 +221,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
const currentLine = lines.find(l => l.id === lineId);
if (currentLine && currentLine.equipment_name) {
const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`;
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo);
setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
// 如果找不到 line,尝试从 API 获取 line detail
@@ -232,11 +229,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
fetchProductProcessLineDetail(lineId)
.then((lineDetail) => {
// 从 lineDetail 中获取 equipment_name
// 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整
const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || "";
if (equipmentName) {
const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`;
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentTypeSubTypeEquipmentNo);
setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
console.warn(`Equipment name not found in line detail for lineId: ${lineId}`);
@@ -249,7 +245,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
return;
}
// 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo
// 例如:{2fitestu123} = staffNo: "123"
// 例如:{2fitestustaff001} = staffNo: "staff001"
@@ -271,11 +266,11 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
return;
}
// 检查 equipmentTypeSubTypeEquipmentNo 格式
const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo):\s*(.+)$/i);
// 检查 equipmentCode 格式
const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo|equipmentCode):\s*(.+)$/i);
if (equipmentCodeMatch) {
const equipmentCode = equipmentCodeMatch[1].trim();
setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode);
setScannedEquipmentCode(equipmentCode);
return;
}
@@ -286,11 +281,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
setScannedStaffNo(String(qrData.staffNo));
}
if (qrData.equipmentTypeSubTypeEquipmentNo || qrData.equipmentCode) {
setScannedEquipmentTypeSubTypeEquipmentNo(
setScannedEquipmentCode(
String(qrData.equipmentTypeSubTypeEquipmentNo ?? qrData.equipmentCode)
);
}
// TODO: 处理 JSON 格式的 QR 码
} catch {
// 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode
if (trimmedValue.length > 0) {
@@ -299,7 +293,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
setScannedStaffNo(trimmedValue);
} else if (trimmedValue.includes("-")) {
// 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號")
setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue);
setScannedEquipmentCode(trimmedValue);
}
}
}
@@ -323,36 +317,41 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
console.log("submitScanAndStart called with:", {
lineId,
scannedStaffNo,
scannedEquipmentTypeSubTypeEquipmentNo,
// scannedEquipmentTypeSubTypeEquipmentNo,
scannedEquipmentCode,
});
if (!scannedStaffNo) {
console.log("No staffNo, cannot submit");
setIsAutoSubmitting(false);
return false; // 没有 staffNo,不能提交
return false;
}
try {
// 获取 line detail 以检查 bomProcessEquipmentId
const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId);
// 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo
console.log("Submitting scan data:", {
// ✅ 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo)
const effectiveEquipmentCode =
scannedEquipmentCode ?? null;
console.log("Submitting scan data with equipmentCode:", {
productProcessLineId: lineId,
staffNo: scannedStaffNo,
equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo,
equipmentCode: effectiveEquipmentCode,
});
const response = await updateProductProcessLineQrscan({
const response = await newUpdateProductProcessLineQrscan({
productProcessLineId: lineId,
equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined,
staffNo: scannedStaffNo || undefined,
equipmentCode: effectiveEquipmentCode ?? "",
staffNo: scannedStaffNo,
});
console.log("Scan submit response:", response);
// 检查响应中的 message 字段来判断是否成功
if (response && response.message) {
if (response && response.type === "error") {
console.error("Scan validation failed:", response.message);
alert(t(response.message) || t("Validation failed. Please check your input."));
setIsAutoSubmitting(false);
if (autoSubmitTimerRef.current) {
clearTimeout(autoSubmitTimerRef.current);
@@ -360,25 +359,31 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
}
return false;
}
// 验证通过,继续执行后续步骤
console.log("Validation passed, starting line...");
handleStopScan();
setShowScanDialog(false);
setIsAutoSubmitting(false);
await handleStartLine(lineId);
setSelectedLineId(lineId);
setIsExecutingLine(true);
await fetchProcessDetail();
return true;
} catch (error) {
console.error("Error submitting scan:", error);
alert("Failed to submit scan data. Please try again.");
setIsAutoSubmitting(false);
return false;
}
}, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]);
}, [
scannedStaffNo,
scannedEquipmentCode,
lineDetailForScan,
t,
fetchProcessDetail,
]);
const handleSubmitScanAndStart = useCallback(async (lineId: number) => {
console.log("handleSubmitScanAndStart called with lineId:", lineId);
@@ -408,6 +413,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
setProcessedQrCodes(new Set());
setScannedOperatorId(null);
setScannedEquipmentId(null);
setScannedStaffNo(null); // ✅ Add this
setScannedEquipmentCode(null);
setIsAutoSubmitting(false); // 添加:重置自动提交状态
setLineDetailForScan(null);
// 获取 line detail 以获取 bomProcessEquipmentId
@@ -431,7 +438,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
}
setIsManualScanning(false);
setIsAutoSubmitting(false); // 添加:重置自动提交状态
setIsAutoSubmitting(false);
setScannedStaffNo(null); // ✅ Add this
setScannedEquipmentCode(null);
stopScan();
resetScan();
}, [stopScan, resetScan]);
@@ -446,20 +455,21 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
}
};
// 提交扫描结果并验证
/*
useEffect(() => {
console.log("Auto-submit check:", {
scanningLineId,
scannedStaffNo,
scannedEquipmentTypeSubTypeEquipmentNo,
scannedEquipmentCode,
isAutoSubmitting,
isManualScanning,
});

// ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId
if (
scanningLineId &&
scannedStaffNo !== null &&
scannedEquipmentTypeSubTypeEquipmentNo !== null &&
(scannedEquipmentCode !== null) &&
!isAutoSubmitting &&
isManualScanning
) {
@@ -484,7 +494,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
// 注意:这里不立即清除定时器,因为我们需要它执行
// 只在组件卸载时清除
};
}, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]);
}, [scanningLineId, scannedStaffNo, scannedEquipmentCode, isAutoSubmitting, isManualScanning, submitScanAndStart]);
*/
useEffect(() => {
return () => {
if (autoSubmitTimerRef.current) {
@@ -502,6 +513,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
setScannedEquipmentId(null);
setProcessedQrCodes(new Set());
setScannedStaffNo(null);
setScannedEquipmentCode(null);
setProcessedQrCodes(new Set());
// 清除之前的定时器
if (autoSubmitTimerRef.current) {
clearTimeout(autoSubmitTimerRef.current);
@@ -764,9 +778,10 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<Box>
<Typography variant="body2" color="text.secondary">
{scannedEquipmentTypeSubTypeEquipmentNo
? `${t("Equipment Type/Code")}: ${scannedEquipmentTypeSubTypeEquipmentNo}`
: t("Please scan equipment code (optional if not required)")
{/* ✅ Show both options */}
{scannedEquipmentCode
? `${t("Equipment Code")}: ${scannedEquipmentCode}`
: t("Please scan equipment code")
}
</Typography>
</Box>
@@ -792,7 +807,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<Button
variant="contained"
onClick={() => scanningLineId && handleSubmitScanAndStart(scanningLineId)}
disabled={!scannedStaffNo}
disabled={!scannedStaffNo }
>
{t("Submit & Start")}
</Button>


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

@@ -115,7 +115,7 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp
}, [fetchData]);
// PickTable 组件内容
const getStockAvailable = (line: JobOrderLine) => {
if (line.type?.toLowerCase() === "consumables") {
if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") {
return null;
}
const inventory = inventoryData.find(inv =>
@@ -158,7 +158,7 @@ const isStockSufficient = (line: JobOrderLine) => {
const stockCounts = useMemo(() => {
// 过滤掉 consumables 类型的 lines
const nonConsumablesLines = jobOrderLines.filter(
line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb"
line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm"
);
const total = nonConsumablesLines.length;
const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
@@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
const response = await deleteJobOrder(jobOrderId)
if (response) {
//setProcessData(response.entity);
await fetchData();
//await fetchData();
onBack();
}
}, [jobOrderId]);
const handleRelease = useCallback(async ( jobOrderId: number) => {
@@ -315,7 +316,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
headerAlign: "left",
type: "number",
renderCell: (params) => {
return <Typography sx={{ fontSize: "18px" }}>{params.value}</Typography>;
return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
},
},
{
@@ -325,16 +326,28 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
align: "left",
headerAlign: "left",
renderCell: (params) => {
return <Typography sx={{ fontSize: "18px" }}>{params.value || ""}</Typography>;
return(
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
<Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
<Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
</Box>
)
},
},

];
const productionProcessesLineRemarkTableRows =
processData?.productProcessLines?.map((line: any) => ({
id: line.seqNo,
seqNo: line.seqNo,

description: line.description ?? "",



})) ?? [];


@@ -486,21 +499,37 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
/>
</Box>
);
const ProductionProcessesLineRemarkTableContent = () => (
<Box sx={{ mt: 2 }}>
<ProcessSummaryHeader processData={processData} />
<StyledDataGrid
sx={{
"--DataGrid-overlayHeight": "100px",
}}
disableColumnMenu
rows={productionProcessesLineRemarkTableRows ?? []}
columns={productionProcessesLineRemarkTableColumns}
getRowHeight={() => 'auto'}
/>
</Box>
);
const ProductionProcessesLineRemarkTableContent = () => (
<Box sx={{ mt: 2 }}>
<ProcessSummaryHeader processData={processData} />
<StyledDataGrid
sx={{
"--DataGrid-overlayHeight": "100px",
// ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
"& .MuiDataGrid-cell": {
fontSize: "0.875rem", // ✅ Match default body2 size
fontWeight: 500,
},
"& .MuiDataGrid-columnHeader": {
fontSize: "0.875rem", // ✅ Match header size
fontWeight: 600,
},
// ✅ Ensure empty columns are visible
"& .MuiDataGrid-columnHeaders": {
display: "flex",
},
"& .MuiDataGrid-row": {
display: "flex",
},
}}
disableColumnMenu
rows={productionProcessesLineRemarkTableRows ?? []}
columns={productionProcessesLineRemarkTableColumns}
getRowHeight={() => 'auto'}
hideFooter={false} // ✅ Ensure footer is visible
/>
</Box>
);

return (


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

@@ -94,7 +94,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess

setModalInfo({
id: process.stockInLineId,
expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT),
//expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT),
// 视需要补 itemId、jobOrderId 等
});
setOpenModal(true);
@@ -155,9 +156,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first
fetchProcesses();
// setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect
}, []);
}, [fetchProcesses]);

const startIdx = page * PER_PAGE;
const paged = processes.slice(startIdx, startIdx + PER_PAGE);
@@ -233,10 +235,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
</Stack>

<Typography variant="body2" color="text.secondary">
{t("Item Name")}: {process.itemName}
{t("Item Name")}: {process.itemCode} {process.itemName}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Required Qty")}: {process.requiredQty}
{t("Required Qty")}: {process.requiredQty} {process.uom}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"}
@@ -268,7 +270,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
<Button
variant="contained"
size="small"
disabled={process.assignedTo != null || process.matchStatus == "completed"}
disabled={process.assignedTo != null || process.matchStatus == "completed"|| process.pickOrderStatus != "completed"}
onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)}
>
{t("Matching Stock")}


+ 97
- 15
src/components/ProductionProcess/ProductionProcessPage.tsx Прегледај датотеку

@@ -2,15 +2,19 @@
import React, { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material";
import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList";
import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail";
import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail";
import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan";
import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList";
import {
fetchProductProcesses,
fetchProductProcessesByJobOrderId,
ProductProcessLineResponse
} from "@/app/api/jo/actions";
import { useTranslation } from "react-i18next";

type PrinterCombo = {
id: number;
value: number;
@@ -25,17 +29,25 @@ type PrinterCombo = {
interface ProductionProcessPageProps {
printerCombo: PrinterCombo[];
}

const STORAGE_KEY = 'productionProcess_selectedMatchingStock';

const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCombo }) => {
const { t } = useTranslation(["common"]);
const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
const [selectedMatchingStock, setSelectedMatchingStock] = useState<{
jobOrderId: number;
productProcessId: number;
} | null>(null);
const [tabIndex, setTabIndex] = useState(0);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;

// Add printer selection state
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
);

// 从 sessionStorage 恢复状态(仅在客户端)
useEffect(() => {
if (typeof window !== 'undefined') {
@@ -76,6 +88,10 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
}
}, []);

const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => {
setTabIndex(newValue);
}, []);

if (selectedMatchingStock) {
return (
<JobPickExecutionsecondscan
@@ -84,6 +100,7 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
/>
);
}
if (selectedProcessId !== null) {
return (
<ProductionProcessJobOrderDetail
@@ -94,21 +111,86 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
}

return (
<ProductionProcessList
printerCombo={printerCombo}
onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null;
if (id !== null) {
setSelectedProcessId(id);
}
}}
onSelectMatchingStock={(jobOrderId, productProcessId) => {
setSelectedMatchingStock({
jobOrderId: jobOrderId || 0,
productProcessId: productProcessId || 0
});
}}
/>
<Box>
{/* Header section with printer selection */}
{tabIndex === 1 && (
<Box sx={{
p: 1,
borderBottom: '1px solid #e0e0e0',
minHeight: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 2,
flexWrap: 'wrap',
}}>
<Stack
direction="row"
spacing={2}
sx={{
alignItems: 'center',
flexWrap: 'wrap',
rowGap: 1,
}}
>
<Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}>
{t("Select Printer")}:
</Typography>
<Autocomplete
disableClearable
options={printerCombo || []}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter || undefined}
onChange={(_, newValue) => setSelectedPrinter(newValue)}
sx={{ minWidth: 200 }}
size="small"
renderInput={(params) => (
<TextField
{...params}
placeholder={t("Printer")}
inputProps={{
...params.inputProps,
readOnly: true,
}}
/>
)}
/>
</Stack>
</Box>
)}

<Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}>
<Tab label={t("Production Process")} />
<Tab label={t("Finished QC Job Orders")} />
</Tabs>

{tabIndex === 0 && (
<ProductionProcessList
printerCombo={printerCombo}
onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null;
if (id !== null) {
setSelectedProcessId(id);
}
}}
onSelectMatchingStock={(jobOrderId, productProcessId) => {
setSelectedMatchingStock({
jobOrderId: jobOrderId || 0,
productProcessId: productProcessId || 0
});
}}
/>
)}

{tabIndex === 1 && (
<FinishedQcJobOrderList
printerCombo={printerCombo}
selectedPrinter={selectedPrinter}
/>
)}
</Box>
);
};


+ 274
- 71
src/components/ProductionProcess/ProductionProcessStepExecution.tsx Прегледај датотеку

@@ -19,6 +19,8 @@ import {
CardContent,
Grid,
} from "@mui/material";
import { Alert } from "@mui/material";

import QrCodeIcon from '@mui/icons-material/QrCode';
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import StopIcon from "@mui/icons-material/Stop";
@@ -75,6 +77,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-";
const [remainingTime, setRemainingTime] = useState<string | null>(null);
const [isOverTime, setIsOverTime] = useState(false);
const [frozenRemainingTime, setFrozenRemainingTime] = useState<string | null>(null);
const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null);
const[isOpenReasonModel, setIsOpenReasonModel] = useState(false);
const [pauseReason, setPauseReason] = useState("");
// 检查是否两个都已扫描
@@ -91,7 +96,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
fetchProductProcessLineDetail(lineId)
.then((detail) => {
setLineDetail(detail as any);
// 初始化 outputData 从 lineDetail
console.log("📋 Line Detail loaded:", {
id: detail.id,
status: detail.status,
durationInMinutes: detail.durationInMinutes,
startTime: detail.startTime,
startTimeType: typeof detail.startTime,
hasDuration: !!detail.durationInMinutes,
hasStartTime: !!detail.startTime,
});
setOutputData(prev => ({
...prev,
productProcessLineId: detail.id,
@@ -112,27 +125,192 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
});
}, [lineId]);
useEffect(() => {
// Don't show time remaining if completed
if (lineDetail?.status === "Completed") {
console.log("Line is completed");
setRemainingTime(null);
setIsOverTime(false);
return;
}

// ✅ 问题1:添加详细的调试打印
console.log("🔍 Time Remaining Debug:", {
lineId: lineDetail?.id,
equipmentId: lineDetail?.equipmentId,
equipmentType: lineDetail?.equipmentType,
durationInMinutes: lineDetail?.durationInMinutes,
startTime: lineDetail?.startTime,
startTimeType: typeof lineDetail?.startTime,
isStartTimeArray: Array.isArray(lineDetail?.startTime),
status: lineDetail?.status,
hasDuration: !!lineDetail?.durationInMinutes,
hasStartTime: !!lineDetail?.startTime,
});

if (!lineDetail?.durationInMinutes || !lineDetail?.startTime) {
console.log("❌ Line duration or start time is not valid", {
durationInMinutes: lineDetail?.durationInMinutes,
startTime: lineDetail?.startTime,
equipmentId: lineDetail?.equipmentId,
equipmentType: lineDetail?.equipmentType,
});
setRemainingTime(null);
setIsOverTime(false);
return;
}
const start = new Date(lineDetail.startTime as any);
const end = new Date(start.getTime() + lineDetail.durationInMinutes * 60_000);

// Handle startTime format - it can be string or number array
let start: Date;
if (Array.isArray(lineDetail.startTime)) {
console.log("Line start time is an array:", lineDetail.startTime);
// If it's an array like [2025, 12, 15, 10, 30, 0], convert to Date
const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
start = new Date(year, month - 1, day, hour, minute, second);
} else {
start = new Date(lineDetail.startTime);
console.log("Line start time is a string:", lineDetail.startTime);
}

// Check if date is valid
if (isNaN(start.getTime())) {
console.error("Invalid startTime:", lineDetail.startTime);
setRemainingTime(null);
setIsOverTime(false);
return;
}

const durationMs = lineDetail.durationInMinutes * 60_000;
// Check if line is paused
const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused";
// ✅ 问题2:修复 stopTime 类型处理,像 startTime 一样处理
const parseStopTime = (stopTime: string | number[] | undefined): Date | null => {
if (!stopTime) return null;
if (Array.isArray(stopTime)) {
const [year, month, day, hour = 0, minute = 0, second = 0] = stopTime;
return new Date(year, month - 1, day, hour, minute, second);
} else {
return new Date(stopTime);
}
};
const update = () => {
const diff = end.getTime() - Date.now();
if (diff <= 0) {
setRemainingTime("00:00");
if (isPaused) {
// If paused, freeze the time at the last calculated value
// If we don't have a frozen value yet, calculate it based on stopTime
if (!frozenRemainingTime) {
// ✅ 修复问题2:正确处理 stopTime 的类型(string | number[])
const pauseTime = lineDetail.stopTime
? parseStopTime(lineDetail.stopTime)
: null;
// 如果没有 stopTime,使用当前时间(首次暂停时)
const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime())
? pauseTime
: new Date();
// ✅ 计算总暂停时间(所有已恢复的暂停记录)
const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
console.log("⏸️ Paused - calculating frozen time:", {
stopTime: lineDetail.stopTime,
pauseTime: pauseTimeToUse,
startTime: start,
totalPausedTimeMs: totalPausedTimeMs,
});
// ✅ 实际工作时间 = 暂停时间 - 开始时间 - 已恢复的暂停时间
const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs;
const remaining = durationMs - elapsed;
if (remaining <= 0) {
const overTime = Math.abs(remaining);
const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0");
const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0");
const frozenValue = `-${minutes}:${seconds}`;
setFrozenRemainingTime(frozenValue);
setRemainingTime(frozenValue);
setIsOverTime(true);
console.log("⏸️ Frozen time (overtime):", frozenValue);
} else {
const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0");
const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0");
const frozenValue = `${minutes}:${seconds}`;
setFrozenRemainingTime(frozenValue);
setRemainingTime(frozenValue);
setIsOverTime(false);
console.log("⏸️ Frozen time:", frozenValue);
}
} else {
// ✅ 关键修复:暂停时始终使用冻结的值,不重新计算
setRemainingTime(frozenRemainingTime);
console.log("⏸️ Using frozen time:", frozenRemainingTime);
}
return;
}
const minutes = Math.floor(diff / 60000).toString().padStart(2, "0");
const seconds = Math.floor((diff % 60000) / 1000).toString().padStart(2, "0");
setRemainingTime(`${minutes}:${seconds}`);

// If resumed or in progress, clear frozen time and continue counting
if (frozenRemainingTime && !isPaused) {
console.log("▶️ Resumed - clearing frozen time");
setFrozenRemainingTime(null);
setLastPauseTime(null);
}

// ✅ 关键修复:计算剩余时间时,需要减去所有已恢复的暂停时间
const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
const now = new Date();
// ✅ 实际工作时间 = 当前时间 - 开始时间 - 所有已恢复的暂停时间
const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs;
const remaining = durationMs - elapsed;

console.log("⏱️ Time calculation:", {
now: now,
start: start,
totalPausedTimeMs: totalPausedTimeMs,
elapsed: elapsed,
remaining: remaining,
durationMs: durationMs,
});

if (remaining <= 0) {
// Over time - show negative time in red
const overTime = Math.abs(remaining);
const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0");
const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0");
setRemainingTime(`-${minutes}:${seconds}`);
setIsOverTime(true);
} else {
const minutes = Math.floor(remaining / 60000).toString().padStart(2, "0");
const seconds = Math.floor((remaining % 60000) / 1000).toString().padStart(2, "0");
setRemainingTime(`${minutes}:${seconds}`);
setIsOverTime(false);
}
};

update();
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}, [lineDetail?.durationInMinutes, lineDetail?.startTime]);
// Only set interval if not paused
if (!isPaused) {
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}
}, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]);

// Reset frozen time when status changes from paused to in progress
useEffect(() => {
const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused";
const isNowInProgress = lineDetail?.status === "InProgress";
if (wasPaused && isNowInProgress && frozenRemainingTime) {
// When resuming, we need to account for the pause duration
// For now, we'll continue from the frozen time
// In a more accurate implementation, you'd fetch the issue details to get exact pause duration
setFrozenRemainingTime(null);
}
}, [lineDetail?.status, lineDetail?.productProcessIssueStatus]);
const handleSubmitOutput = async () => {
if (!lineDetail?.id) return;

@@ -164,6 +342,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
fetchProductProcessLineDetail(lineDetail.id)
.then((detail) => {
console.log("Line Detail loaded:", {
id: detail.id,
status: detail.status,
startTime: detail.startTime,
durationInMinutes: detail.durationInMinutes,
productProcessIssueStatus: detail.productProcessIssueStatus
});
setLineDetail(detail as any);
// 初始化 outputData 从 lineDetail
setOutputData(prev => ({
@@ -249,6 +434,37 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
alert(t("Failed to pause. Please try again."));
}
};

// ✅ Add this new handler for resume
const handleResume = async () => {
if (!lineDetail?.productProcessIssueId) {
console.error("No productProcessIssueId found");
return;
}
try {
await saveProductProcessResumeTime(lineDetail.productProcessIssueId);
console.log("✅ Resume API called successfully");
// ✅ Refresh line detail after resume
if (lineDetail?.id) {
fetchProductProcessLineDetail(lineDetail.id)
.then((detail) => {
console.log("✅ Line detail refreshed after resume:", detail);
setLineDetail(detail as any);
// Clear frozen time when resuming
setFrozenRemainingTime(null);
setLastPauseTime(null);
})
.catch(err => {
console.error("❌ Failed to load line detail after resume", err);
});
}
} catch (error) {
console.error("❌ Error resuming:", error);
alert(t("Failed to resume. Please try again."));
}
};
return (
<Box>
<Box sx={{ mb: 2 }}>
@@ -256,13 +472,13 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
{t("Back to List")}
</Button>
</Box>
{/* 如果已完成,显示合并的视图 */}
{isCompleted ? (
<Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}>
<CardContent>
<Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
{t("Completed Step")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
{t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
</Typography>
{/*<Divider sx={{ my: 2 }} />*/}
@@ -272,27 +488,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
{t("Step Information")}
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">
<strong>{t("Description")}:</strong> {lineDetail?.description || "-"}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">
<strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">
<strong>{t("Equipment")}:</strong> {equipmentName}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary">
<strong>{t("Status")}:</strong> {lineDetail?.status || "-"}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
<strong>{t("Description")}:</strong> {lineDetail?.description || "-"}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
<strong>{t("Operator")}:</strong> {lineDetail?.operatorName || "-"}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
<strong>{t("Equipment")}:</strong> {equipmentName}
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '1.25rem' }}>
<strong>{t("Status")}:</strong> {t(lineDetail?.status || "-")}
</Typography>
</Grid>
</Grid>

{/*<Divider sx={{ my: 2 }} />*/}

@@ -415,7 +631,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<Card sx={{ bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main', height: '100%' }}>
<CardContent>
<Typography variant="h6" color="primary.main" gutterBottom>
{t("Executing")}: {lineDetail?.name} (Seq: {lineDetail?.seqNo})
{t("Executing")}: {lineDetail?.name} ({t("Seq")}:{lineDetail?.seqNo})
</Typography>
<Typography variant="body2" color="text.secondary">
{lineDetail?.description}
@@ -426,7 +642,25 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<Typography variant="body2" color="text.secondary">
{t("Equipment")}: {equipmentName}
</Typography>

{!isCompleted && remainingTime !== null && (
<Box sx={{ mt: 2, mb: 2, p: 2, bgcolor: isOverTime ? 'error.50' : 'info.50', borderRadius: 1, border: '1px solid', borderColor: isOverTime ? 'error.main' : 'info.main' }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
{t("Time Remaining")}
</Typography>
<Typography
variant="h5"
fontWeight="bold"
color={isOverTime ? 'error.main' : 'info.main'}
>
{isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime}
</Typography>
{lineDetail?.status === "Paused" && (
<Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}>
{t("Timer Paused")}
</Typography>
)}
</Box>
)}
<Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
{/*
<Button
@@ -453,7 +687,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained"
color="success"
startIcon={<PlayArrowIcon />}
onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)}
onClick={handleResume} // ✅ Change from inline call to handler
>
{t("Continue")}
</Button>
@@ -462,6 +696,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<Button
sx={{ mt: 2, alignSelf: "flex-end" }}
variant="outlined"
disabled={lineDetail?.status === 'Paused'}
onClick={() => setShowOutputTable(true)}
>
{t("Order Complete")}
@@ -521,39 +756,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableCell>
</TableRow>

{/* byproduct */}
{/*
<TableRow>
<TableCell>
<Stack>
<Typography fontWeight={500}>{t("By-product")}</Typography>
</Stack>
</TableCell>
<TableCell>
<TextField
type="number"
fullWidth
size="small"
value={outputData.byproductQty}
onChange={(e) => setOutputData({
...outputData,
byproductQty: parseInt(e.target.value) || 0
})}
/>
</TableCell>
<TableCell>
<TextField
fullWidth
size="small"
value={outputData.byproductUom}
onChange={(e) => setOutputData({
...outputData,
byproductUom: e.target.value
})}
/>
</TableCell>
</TableRow>
*/}
{/* defect 1 */}
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>


+ 3
- 2
src/components/Qc/QcComponent.tsx Прегледај датотеку

@@ -413,10 +413,11 @@ useEffect(() => {
} else { return 60}
};

const formattedDesc = (content: string = "") => {
const formattedDesc = (content: string | null | undefined = "") => {
const safeContent = content || "";
return (
<>
{content.split("\\n").map((line, index) => (
{safeContent.split("\\n").map((line, index) => (
<span key={index}> {line} <br/></span>
))}
</>


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

@@ -40,7 +40,7 @@ import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForS
import { fetchStockInLineInfo } from "@/app/api/stockIn/actions";
import FgStockInForm from "../StockIn/FgStockInForm";
import LoadingComponent from "../General/LoadingComponent";
import { printFGStockInLabel, PrintFGStockInLabelRequest } from "@/app/api/jo/actions";
import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions";

const style = {
position: "absolute",
@@ -119,7 +119,7 @@ const QcStockInModal: React.FC<Props> = ({
const res = await fetchStockInLineInfo(stockInLineId);
if (res) {
console.log("%c Fetched Stock In Line: ", "color:orange", res);
setStockInLineInfo({...inputDetail, ...res, expiryDate: inputDetail?.expiryDate}); // TODO review to overwrite res with inputDetail instead (revise PO fetching data)
setStockInLineInfo({...inputDetail, ...res, expiryDate: res.expiryDate});
// fetchQcResultData(stockInLineId);
} else throw("Result is undefined");
@@ -168,8 +168,8 @@ const QcStockInModal: React.FC<Props> = ({
{
...d,
// status: d.status ?? "pending",
productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : undefined,
expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined,
productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : dayjs().format(INPUT_DATE_FORMAT),
expiryDate: d.expiryDate ? (Array.isArray(d.expiryDate) ? arrayToDateString(d.expiryDate, "input") : d.expiryDate) : undefined,
receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input")
: dayjs().add(0, "month").format(INPUT_DATE_FORMAT),
acceptQty: d.status != StockInStatus.REJECTED ? (d.demandQty?? d.acceptedQty) : 0,
@@ -350,9 +350,9 @@ const QcStockInModal: React.FC<Props> = ({
const qcData = {
dnNo : data.dnNo? data.dnNo : "DN00000",
// dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()),
productionDate : arrayToDateString(data.productionDate, "input"),
expiryDate : arrayToDateString(data.expiryDate, "input"),
receiptDate : arrayToDateString(data.receiptDate, "input"),
productionDate : data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined,
expiryDate : data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined,
receiptDate : data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined,
qcAccept: qcAccept? qcAccept : false,
acceptQty: acceptQty? acceptQty : 0,
@@ -396,6 +396,52 @@ const QcStockInModal: React.FC<Props> = ({
// submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?",
// confirmButtonText: t("confirm putaway"), html: ""});
// onOpenPutaway();
const isJobOrderBom = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess")
&& stockInLineInfo?.bomDescription === "WIP";
if (isJobOrderBom) {
// Auto putaway to default warehouse
const defaultWarehouseId = stockInLineInfo?.defaultWarehouseId ?? 1;
// Get warehouse name from warehouse prop or use default
let defaultWarehouseName = "2F-W201-#A-01"; // Default warehouse name
if (warehouse && warehouse.length > 0) {
const defaultWarehouse = warehouse.find(w => w.id === defaultWarehouseId);
if (defaultWarehouse) {
defaultWarehouseName = `${defaultWarehouse.code} - ${defaultWarehouse.name}`;
}
}
// Create putaway data
const putawayData = {
id: stockInLineInfo?.id, // Include ID
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
warehouseId: defaultWarehouseId,
status: "received", // Use string like PutAwayModal
productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined,
expiryDate: data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined,
receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined,
inventoryLotLines: [{
warehouseId: defaultWarehouseId,
qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal
}],
} as StockInLineEntry & ModalFormInput;
try {
// Use updateStockInLine directly like PutAwayModal does
const res = await updateStockInLine(putawayData);
if (Boolean(res.id)) {
console.log("Auto putaway completed for job order bom");
}
} catch (error) {
console.error("Error during auto putaway:", error);
alert(t("Auto putaway failed. Please complete putaway manually."));
}

}
closeHandler({}, "backdropClick");
// setTabIndex(1); // Need to go Putaway tab?
} else {
@@ -540,24 +586,38 @@ const QcStockInModal: React.FC<Props> = ({
// return isPassed
// }, [acceptQty, formProps])

const printQrcode = useCallback(
async () => {
setIsPrinting(true);
try {
const postData = { stockInLineIds: [stockInLineInfo?.id] };
const response = await fetchPoQrcode(postData);
if (response) {
console.log(response);
downloadFile(new Uint8Array(response.blobValue), response.filename!);
}
} catch (e) {
console.log("%c Error downloading QR Code", "color:red", e);
} finally {
const printQrcode = useCallback(
async () => {
setIsPrinting(true);
try {
let response;
if (printSource === "productionProcess") {
// Use FG Stock In Label download API for production process
if (!stockInLineInfo?.id) {
console.error("Stock In Line ID is required for download");
setIsPrinting(false);
return;
}
},
[stockInLineInfo],
);
const postData = { stockInLineId: stockInLineInfo.id };
response = await fetchFGStockInLabel(postData);
} else {
const postData = { stockInLineIds: [stockInLineInfo?.id] };
response = await fetchPoQrcode(postData);
}
if (response) {
console.log(response);
downloadFile(new Uint8Array(response.blobValue), response.filename!);
}
} catch (e) {
console.log("%c Error downloading QR Code", "color:red", e);
} finally {
setIsPrinting(false);
}
},
[stockInLineInfo, printSource],
);

return (
<>


+ 113
- 25
src/components/StockIn/FgStockInForm.tsx Прегледај датотеку

@@ -12,11 +12,12 @@ import {
TextField,
Tooltip,
Typography,
Button,
} from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
GridColDef,
GridRowIdGetter,
@@ -35,6 +36,11 @@ import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import dayjs from "dayjs";
import CalculateExpiryDateModal from "./CalculateExpiryDateModal";
import { InputAdornment } from "@mui/material";
import { dayjsToDateString } from "@/app/utils/formatUtil";


// change PurchaseQcResult to stock in entry props
interface Props {
itemDetail: StockInLine;
@@ -115,6 +121,8 @@ const FgStockInForm: React.FC<Props> = ({
console.log(errors);
}, [errors]);
const [openModal, setOpenModal] = useState<boolean>(false);
const [openExpDatePicker, setOpenExpDatePicker] = useState<boolean>(false);
const productionDate = watch("productionDate");
const expiryDate = watch("expiryDate");
const uom = watch("uom");
@@ -140,7 +148,23 @@ const FgStockInForm: React.FC<Props> = ({
console.log("%c StockInForm itemDetail update: ", "color: brown", itemDetail);
}, [itemDetail]);

return (
const handleOpenModal = useCallback(() => {
setOpenModal(true);
}, []);

const handleOnModalClose = useCallback(() => {
setOpenExpDatePicker(false);
setOpenModal(false);
}, []);

const handleReturnExpiryDate = useCallback((result: dayjs.Dayjs) => {
if (result) {
setValue("expiryDate", dayjsToDateString(result));
}
}, [setValue]);

return (
<>
<Grid container justifyContent="flex-start" alignItems="flex-start">
{/* <Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}>
@@ -250,6 +274,44 @@ const FgStockInForm: React.FC<Props> = ({
/>)
}
</Grid>
<Grid item xs={6}>
<Controller
control={control}
name="productionDate"
render={({ field }) => {
return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={textfieldSx}
label={t("productionDate")}
value={productionDate ? dayjs(productionDate) : undefined}
format={OUTPUT_DATE_FORMAT}
disabled={disabled}
onChange={(date) => {
if (!date) return;
setValue(
"productionDate",
date.format(INPUT_DATE_FORMAT),
);
}}
inputRef={field.ref}
slotProps={{
textField: {
error: Boolean(errors.productionDate?.message),
helperText: errors.productionDate?.message,
},
}}
/>
</LocalizationProvider>
);
}}
/>
</Grid>
{/* {putawayMode || (<>
{/* {putawayMode || (<>
<Grid item xs={6}>
<Controller
@@ -313,28 +375,47 @@ const FgStockInForm: React.FC<Props> = ({
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DatePicker
{...field}
sx={textfieldSx}
label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined}
format={OUTPUT_DATE_FORMAT}
disabled={disabled}
onChange={(date) => {
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
// field.onChange(date);
}}
inputRef={field.ref}
slotProps={{
textField: {
// required: true,
error: Boolean(errors.expiryDate?.message),
helperText: errors.expiryDate?.message,
},
}}
/>
<DatePicker
{...field}
sx={textfieldSx}
label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined}
format={OUTPUT_DATE_FORMAT}
disabled={disabled}
onChange={(date) => {
if (!date) return;
console.log(date.format(INPUT_DATE_FORMAT));
setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
}}
inputRef={field.ref}
open={openExpDatePicker && !openModal}
onOpen={() => setOpenExpDatePicker(true)}
onClose={() => setOpenExpDatePicker(false)}
slotProps={{
textField: {
InputProps: {
...(!disabled && {
endAdornment: (
<InputAdornment position='end'>
<Button
type="button"
variant="contained"
color="primary"
sx={{ fontSize: '24px' }}
onClick={handleOpenModal}
>
{t("Calculate Expiry Date")}
</Button>
</InputAdornment>
),
})
},
error: Boolean(errors.expiryDate?.message),
helperText: errors.expiryDate?.message,
onClick: () => setOpenExpDatePicker(true),
},
}}
/>
</LocalizationProvider>
);
}}
@@ -442,6 +523,13 @@ const FgStockInForm: React.FC<Props> = ({
</Grid> */}
</Grid>
</Grid>
);
<CalculateExpiryDateModal
open={openModal}
onClose={handleOnModalClose}
onSubmit={handleReturnExpiryDate}
textfieldSx={textfieldSx}
/>
</>
);
};
export default FgStockInForm;

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

@@ -1,5 +1,4 @@
{

"dashboard": "資訊展示面板",
"Edit": "編輯",
"Job Order Production Process": "工單生產流程",
@@ -7,12 +6,28 @@
"Search Criteria": "搜尋條件",
"All": "全部",
"No options": "沒有選項",
"Finished QC Job Orders": "完成QC工單",
"Reset": "重置",
"Search": "搜尋",
"Staff No Required": "員工編號必填",
"User Not Found": "用戶不存在",
"Time Remaining": "剩餘時間",
"Select Printer": "選擇打印機",
"Finished Time": "完成時間",
"Printer": "打印機",
"Finished Qc Job Order List": "完成QC工單列表",
"Total finished Qc Job Order": "總完成QC工單數量",
"Timer Paused": "計時器已暫停",
"User not found with staffNo:": "用戶不存在",
"Total finished QC job orders": "總完成QC工單數量",
"Over Time": "超時",
"Code": "編號",
"Staff No": "員工編號",
"code": "編號",
"Name": "名稱",
"Assignment successful": "分配成功",
"Pass": "通過",
"Unable to get user ID": "無法獲取用戶ID",
"Unknown error: ": "未知錯誤: ",
"Please try again later.": "請稍後重試。",
@@ -25,7 +40,6 @@
"R&D": "研發",
"STF": "樣品",
"Other": "其他",

"Add some entries!": "添加條目",
"Add Record": "新增",
"Clean Record": "重置",
@@ -49,19 +63,42 @@
"Changeover Time": "生產後轉換時間",
"Warehouse": "倉庫",
"Supplier": "供應商",
"Purchase Order":"採購單",
"Demand Forecast":"需求預測",
"Purchase Order": "採購單",
"Demand Forecast": "需求預測",
"Pick Order": "提料單",
"Deliver Order":"送貨訂單",
"Project":"專案",
"Product":"產品",
"Material":"材料",
"mat":"原料",
"Deliver Order": "送貨訂單",
"Project": "專案",
"Product": "產品",
"Material": "材料",
"mat": "原料",
"consumables": "消耗品",
"non-consumables": "非消耗品",
"fg": "成品",
"sfg": "半成品",
"item": "貨品",
"FG": "成品",
"Qty": "數量",
"FG & Material Demand Forecast Detail": "成品及材料需求預測詳情",
"View item In-out And inventory Ledger": "查看物料出入庫及庫存日誌",
"Delivery Order": "送貨訂單",
"Detail Scheduling": "詳細排程",
"Customer": "客戶",
"qcItem": "品檢項目",
"Item": "成品/半成品",
"Today": "今天",
"Yesterday": "昨天",
"Input Equipment is not match with process": "輸入的設備與流程不匹配",
"Staff No is required": "員工編號必填",
"Day Before Yesterday": "前天",
"Select Date": "選擇日期",
"Production Date": "生產日期",
"QC Check Item": "QC品檢項目",
"QC Category": "QC品檢模板",
"qcCategory": "品檢模板",
"QC Check Template": "QC檢查模板",
"Mail": "郵件",
"Import Testing": "匯入測試",
"FG":"成品",
"Qty":"數量",
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情",
@@ -87,7 +124,7 @@
"Qc Item": "QC 項目",
"FG Production Schedule": "FG 生產排程",
"Inventory": "庫存",
"scheduling":"排程",
"scheduling": "排程",
"settings": "設定",
"items": "物料",
"edit":"編輯",
@@ -95,6 +132,11 @@
"Edit Equipment":"設備詳情",
"equipmentType":"設備種類",
"Description":"描述",
"edit": "編輯",
"Edit Equipment Type": "設備類型詳情",
"Edit Equipment": "設備詳情",
"equipmentType": "設備類型",
"Description": "描述",
"Details": "詳情",
"Equipment Type Details":"設備類型詳情",
"Equipment Type":"設備類型",
@@ -103,6 +145,12 @@
"Equipment Details":"設備詳情",
"Exclude Date":"排除日期",
"Finished Goods Name":"成品名稱",
"Equipment Type Details": "設備類型詳情",
"Save": "儲存",
"Cancel": "取消",
"Equipment Details": "設備詳情",
"Exclude Date": "排除日期",
"Finished Goods Name": "成品名稱",
"create": "新增",
"hr": "小時",
"hrs": "小時",
@@ -125,7 +173,6 @@
"Stop Scan": "停止掃碼",
"Scan Result": "掃碼結果",
"Expiry Date": "有效期",

"Pick Order Code": "提料單編號",
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數量",
@@ -135,8 +182,6 @@
"No data available": "沒有資料",
"jodetail": "工單細節",
"Sign out": "登出",


"By-product": "副產品",
"Complete Step": "完成步驟",
"Defect": "不良品",
@@ -163,7 +208,6 @@
"Output Qty": "輸出數量",
"Pending": "待處理",
"pending": "待處理",

"Please scan equipment code (optional if not required)": "請掃描設備編號(可選)",
"Please scan operator code": "請掃描操作員編號",
"Please scan operator code first": "請先掃描操作員編號",
@@ -171,15 +215,13 @@
"Production Process Information": "生產流程信息",
"Production Process Steps": "生產流程步驟",
"Scan Operator & Equipment": "掃描操作員和設備",
"Seq": "序號",
"Setup Time (mins)": "生產前預備時間(分鐘)",
"Start": "開始",
"Start QR Scan": "開始掃碼",
"Status": "狀態",
"Status": "狀態",
"in_progress": "進行中",
"In_Progress": "進行中",
"inProgress": "進行中",
"Step Name": "名稱",
"Stop QR Scan": "停止掃碼",
"Submit & Start": "提交並開始",
@@ -188,10 +230,13 @@
"Validation failed. Please check operator and equipment.": "驗證失敗. 請檢查操作員和設備.",
"View": "查看",
"Back": "返回",
"BoM Material": "物料清單",
"BoM Material": "成品/半成品清單",
"N/A": "不適用",
"Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度",
"Item Code": "物料編號",
"Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度",
"Item Code": "成品/半成品名稱",
"Please scan equipment code": "請掃描設備編號",
"Equipment Code": "設備編號",
"Seq": "步驟",
"Item Name": "物料名稱",
"Job Order Info": "工單信息",
"Matching Stock": "工單對料",
@@ -223,6 +268,7 @@
"View Details": "查看詳情",
"view stockin": "品檢",
"No completed Job Order pick orders with matching found": "沒有相關記錄",
"Handler": "提料員",
"Completed Step": "完成步驟",
"Continue": "繼續",
"Executing": "執行中",
@@ -235,4 +281,4 @@
"Lines with sufficient stock: ": "可提料項目數量: ",
"Lines with insufficient stock: ": "未能提料項目數量: ",
"Total lines: ": "總數量:"
}
}

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

@@ -8,11 +8,19 @@
"Code": "工單編號",
"Name": "成品/半成品名稱",
"Picked Qty": "已提料數量",
"Req. Qty": "需求數量",
"Confirm All": "確認所有提料",
"UoM": "銷售單位",
"No": "沒有",
"User not found with staffNo:": "用戶不存在",
"Time Remaining": "剩餘時間",
"Over Time": "超時",
"Staff No:": "員工編號:",
"Timer Paused": "計時器已暫停",
"Staff No Required": "員工編號必填",
"Staff No": "員工編號",
"Status": "工單狀態",
"Lot No.": "批號",
"Pass": "通過",
"Delete Job Order": "刪除工單",
"Bom": "半成品/成品編號",
"Release": "放單",
@@ -86,6 +94,8 @@
"Job Order Item Name": "工單物料名稱",
"Job Order Code": "工單編號",
"View Details": "查看詳情",
"Skip": "跳過",
"Handler": "提料員",
"Required Qty": "需求數量",
"completed Job Order pick orders with Matching": "工單已完成提料和對料",
"No completed Job Order pick orders with matching found": "沒有相關記錄",
@@ -134,7 +144,7 @@
"Confirm Lot Substitution": "確認批號替換",
"Processing...": "處理中",
"Complete Job Order Record": "已完成工單記錄",
"Back": "返回",
"Lot Details": "批號細節",
"No lot details available": "沒有批號細節",
"Second Scan Completed": "對料已完成",
@@ -146,7 +156,9 @@
"Reject": "拒絕",
"Stock Unit": "庫存單位",
"Group": "組",
"Item": "物料",
"Input Equipment is not match with process": "輸入的設備與流程不匹配",
"Item": "成品/半成品",
"Select Date": "選擇日期",
"No Group": "沒有組",
"No created items": "沒有創建物料",
"Order Quantity": "需求數量",
@@ -274,7 +286,6 @@
"acceptQty must not greater than": "接受數量不能大於",
"escalation": "升級",
"failedQty": "失敗數量",
"qcItem": "QC物料",
"qcResult": "QC結果",
"remarks": "備註",
"supervisor": "主管",
@@ -324,13 +335,15 @@
"pending": "待處理",

"Please scan equipment code (optional if not required)": "請掃描設備編號(可選)",
"Please scan equipment code": "請掃描設備編號",
"Equipment Code": "設備編號",
"Please scan operator code": "請掃描操作員編號",
"Please scan operator code first": "請先掃描操作員編號",
"Processing Time (mins)": "步驟時間(分鐘)",
"Production Process Information": "生產流程信息",
"Production Process Steps": "生產流程步驟",
"Scan Operator & Equipment": "掃描操作員和設備",
"Seq": "序號",
"Seq:": "步驟",
"Setup Time (mins)": "生產前預備時間(分鐘)",
"Start": "開始",
"Start QR Scan": "開始掃碼",
@@ -356,18 +369,13 @@
"View": "查看",
"Back": "返回",
"N/A": "不適用",
"BoM Material": "物料清單",
"BoM Material": "成品/半成品清單",
"Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間順序 | 複雜度",
"Item Code": "物料編號",
"Item Name": "物料名稱",
"Enter the number of cartons: ": "請輸入箱數:",
"Number of cartons": "箱數",
"You need to enter a number": "您需要輸入一個數字",
"Number must be at least 1": "數字必須至少為1",
"Confirm": "確認",
"Cancel": "取消",
"Print Pick Record": "打印板頭紙",
"Printed Successfully.": "成功列印",

"Job Order Info": "工單信息",
"Matching Stock": "工單對料",
"No data found": "沒有找到資料",


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