From cf4155b61271b4563705583e48b064a4194a0455 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 15 Jul 2025 18:09:59 +0800 Subject: [PATCH] [Prod Schedule] Update Detailed Prod Schedule --- src/app/api/scheduling/actions.ts | 53 ++++++-- src/app/api/scheduling/index.ts | 3 + src/app/utils/formatUtil.ts | 1 + .../DetailedScheduleDetailView.tsx | 118 +++++++++++++++--- .../ViewByFGDetails.tsx | 50 +++++--- .../ScheduleTable/ScheduleTable.tsx | 104 ++++++++++++--- src/i18n/zh/schedule.json | 3 +- 7 files changed, 271 insertions(+), 61 deletions(-) diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 4798c1b..030533e 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -4,7 +4,8 @@ import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; -import { ScheduleType } from "."; +import { DetailedProdScheduleLineBomMaterialResult, DetailedProdScheduleLineResult, ScheduleType } from "."; +import { revalidateTag } from "next/cache"; export interface SearchProdSchedule { scheduleAt?: string; @@ -31,11 +32,29 @@ export interface ProdScheduleResultByPage { records: ProdScheduleResult[]; } -export interface ReleaseDetailProdScheduleInputs { +export interface ReleaseProdScheduleInputs { id: number; demandQty: number; } +export interface ReleaseProdScheduleResponse { + id: number; + code: string; + entity: { + prodScheduleLines: DetailedProdScheduleLineResult[]; + }; + message: string; +} + +export interface SaveProdScheduleResponse { + id: number; + code: string; + entity: { + bomMaterials: DetailedProdScheduleLineBomMaterialResult[] + }; + message: string; +} + export const fetchProdSchedules = cache( async (data: SearchProdSchedule | null) => { const params = convertObjToURLSearchParams(data); @@ -79,16 +98,32 @@ export const testDetailedSchedule = cache(async () => { ); }); -export const releaseProdScheduleLine = cache(async (data: ReleaseDetailProdScheduleInputs) => { - return serverFetchJson( - `${BASE_API_URL}/productionSchedule/releaseLine`, +export const releaseProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { + const response = serverFetchJson( + `${BASE_API_URL}/productionSchedule/detail/detailed/releaseLine`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ); + + revalidateTag("prodSchedules"); + + return response; +}) + +export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { + const response = serverFetchJson( + `${BASE_API_URL}/productionSchedule/detail/detailed/save`, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, - next: { - tags: ["prodSchedules"], - }, } - ) + ); + + revalidateTag("prodSchedules"); + + return response; }) \ No newline at end of file diff --git a/src/app/api/scheduling/index.ts b/src/app/api/scheduling/index.ts index 5f55b86..9002573 100644 --- a/src/app/api/scheduling/index.ts +++ b/src/app/api/scheduling/index.ts @@ -93,8 +93,11 @@ export interface DetailedProdScheduleLineResult { name: string; type: string; demandQty: number; + bomOutputQty: number; prodTimeInMinute: DetailedProdScheduleLineProdTimeResult[]; priority: number; + approved: boolean; + proportion: number; } export interface DetailedProdScheduleLineBomMaterialResult { diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index b5f7084..b665297 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -23,6 +23,7 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", { export const decimalFormatter = new Intl.NumberFormat("en-HK", { minimumFractionDigits: 2, + maximumFractionDigits: 2, }); export const integerFormatter = new Intl.NumberFormat("en-HK", {}); diff --git a/src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx b/src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx index 7b03a57..61d2e86 100644 --- a/src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx +++ b/src/components/DetailedScheduleDetail/DetailedScheduleDetailView.tsx @@ -7,6 +7,7 @@ import { FormProvider, SubmitErrorHandler, SubmitHandler, + useFieldArray, useForm, } from "react-hook-form"; import { @@ -26,7 +27,10 @@ import DetailInfoCard from "@/components/DetailedScheduleDetail/DetailInfoCard"; import ViewByFGDetails, { // FGRecord, } from "@/components/DetailedScheduleDetail/ViewByFGDetails"; -import { DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; +import { DetailedProdScheduleLineResult, DetailedProdScheduleResult, ScheduleType } from "@/app/api/scheduling"; +import { releaseProdScheduleLine, saveProdScheduleLine } from "@/app/api/scheduling/actions"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; // temp interface input // export interface SaveDetailedSchedule { @@ -59,14 +63,20 @@ const DetailedScheduleDetailView: React.FC = ({ const [tabIndex, setTabIndex] = useState(0); const { t } = useTranslation("schedule"); const router = useRouter(); -const [isEdit, setIsEdit] = useState(false); - + const [isEdit, setIsEdit] = useState(false); + const { setIsUploading } = useUploadContext() + // console.log(typeId) const formProps = useForm({ defaultValues: defaultValues }); const errors = formProps.formState.errors; + const lineFormProps = useFieldArray({ + control: formProps.control, + name: "prodScheduleLines" + }) + const handleTabChange = useCallback>( (_e, newValue) => { setTabIndex(newValue); @@ -74,6 +84,10 @@ const [isEdit, setIsEdit] = useState(false); [], ); + // const calNewProportion = useCallback((demandQty: number, bomOutputQty: number) => { + // return ((demandQty ?? 0) / (bomOutputQty ?? 1)).toFixed(2) + // }, []) + // const [pagingController, setPagingController] = useState({ // pageNum: 1, // pageSize: 10, @@ -81,7 +95,7 @@ const [isEdit, setIsEdit] = useState(false); // }); const handleCancel = () => { - router.replace(`/scheduling/Detail`); + router.replace(`/scheduling/detailed`); }; const onSubmit = useCallback>( @@ -106,7 +120,7 @@ const [isEdit, setIsEdit] = useState(false); // multiple tabs const onSubmitError = useCallback>( - (errors) => {}, + (errors) => { }, [], ); @@ -114,10 +128,69 @@ const [isEdit, setIsEdit] = useState(false); setIsEdit(!isEdit); }; - const onReleaseClick = useCallback(() => { + const onReleaseClick = useCallback(async (row: DetailedProdScheduleLineResult) => { + setIsUploading(true) + + try { + const response = await releaseProdScheduleLine({ + id: row.id, + demandQty: row.demandQty + }) + + 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) + 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) + } + }, []) + + const [tempValue, setTempValue] = useState(null) + const onEditClick = useCallback((rowId: number) => { + const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) + if (row) { + setTempValue(row.demandQty) + } + }, []) + 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)) }, []) + const onSaveClick = useCallback(async (row: DetailedProdScheduleLineResult) => { + setIsUploading(true) + try { + const response = await saveProdScheduleLine({ + id: row.id, + demandQty: row.demandQty + }) + + if (response) { + const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == row.id) + formProps.setValue(`prodScheduleLines.${index}.bomMaterials`, response.entity.bomMaterials) + } + setIsUploading(false) + } catch (e) { + console.log(e) + setIsUploading(false) + } + }, []) + + const onCancelClick = useCallback(async (rowId: number) => { + // if (tempValue) { + const index = formProps.getValues("prodScheduleLines").findIndex(ele => ele.id == rowId) + formProps.setValue(`prodScheduleLines.${index}.demandQty`, Number(tempValue)) + // } + }, [tempValue]) + return ( <> @@ -133,9 +206,10 @@ const [isEdit, setIsEdit] = useState(false); {/**/} - } - //LinkComponent={Link} - //href="qcCategory/create" + // startIcon={} + //LinkComponent={Link} + //href="qcCategory/create" > {isEdit ? t("Save") : t("Edit")} - + */} {/* @@ -162,24 +236,32 @@ const [isEdit, setIsEdit] = useState(false); )} {/* {tabIndex === 0 && } */} - + {/* {tabIndex === 1 && } */} - + */} diff --git a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx index c157968..9e860dd 100644 --- a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx @@ -23,7 +23,11 @@ type Props = { apiRef: MutableRefObject; isEdit: boolean; type: ScheduleType; - onReleaseClick: () => void; + onReleaseClick: (item: DetailedProdScheduleLineResult) => void; + onEditClick: (rowId: number) => void; + handleEditChange: (rowId: number, fieldName: keyof DetailedProdScheduleLineResult, newValue: number | string) => void; + onSaveClick: (item: DetailedProdScheduleLineResult) => void; + onCancelClick: (rowId: number) => void; }; // export type FGRecord = { @@ -35,7 +39,7 @@ type Props = { // purchaseQty?: number; // }; -const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick }) => { +const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick, onEditClick, handleEditChange, onSaveClick, onCancelClick }) => { const { t, i18n: { language }, @@ -43,8 +47,10 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick const { getValues, + watch, formState: { errors, defaultValues, touchedFields }, } = useFormContext(); + // const apiRef = useGridApiRef(); // const [pagingController, setPagingController] = useState([ @@ -128,6 +134,9 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick field: "type", label: t("type"), type: "read-only", + renderCell: (row) => { + return t(row.type); + }, // editable: true, }, // { @@ -148,7 +157,7 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick { field: "demandQty", label: t("Demand Qty"), - type: "input", + type: "input-number", style: { textAlign: "right", }, @@ -202,23 +211,28 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick /> */} {/* {dayPeriod.map((date, index) => ( */} - - {/* + + {/* {`${t("FG Demand Date")}: ${date}`} */} - - type={type} - // items={fakeRecords[index]} // Use the corresponding records for the day - items={getValues("prodScheduleLines")} // Use the corresponding records for the day - columns={columns} - // setPagingController={updatePagingController} - // pagingController={pagingController[index]} - isAutoPaging={false} - isEditable={true} - isEdit={isEdit} - hasCollapse={true} - /> - + + type={type} + // items={fakeRecords[index]} // Use the corresponding records for the day + items={getValues("prodScheduleLines")} // Use the corresponding records for the day + columns={columns} + // setPagingController={updatePagingController} + // pagingController={pagingController[index]} + isAutoPaging={false} + isEditable={true} + isEdit={isEdit} + hasCollapse={true} + onReleaseClick={onReleaseClick} + onEditClick={onEditClick} + handleEditChange={handleEditChange} + onSaveClick={onSaveClick} + onCancelClick={onCancelClick} + /> + {/* ))} */} ); diff --git a/src/components/ScheduleTable/ScheduleTable.tsx b/src/components/ScheduleTable/ScheduleTable.tsx index 75a5acc..db83752 100644 --- a/src/components/ScheduleTable/ScheduleTable.tsx +++ b/src/components/ScheduleTable/ScheduleTable.tsx @@ -5,6 +5,7 @@ import React, { DetailedHTMLProps, HTMLAttributes, useEffect, + useRef, useState, } from "react"; import Paper from "@mui/material/Paper"; @@ -37,6 +38,7 @@ import { ScheduleType, } from "@/app/api/scheduling"; import { defaultPagingController } from "../SearchResults/SearchResults"; +import { useFormContext } from "react-hook-form"; export interface ResultWithId { id: string | number; @@ -81,6 +83,11 @@ interface Props { isEditable: boolean; hasCollapse: boolean; type: ScheduleType; + onReleaseClick?: (item: T) => void; + onEditClick?: (rowId: number) => void; + handleEditChange?: (rowId: number, fieldName: keyof T, newValue: number | string) => void; + onSaveClick?: (item: T) => void; + onCancelClick?: (rowId: number) => void; } function ScheduleTable({ @@ -95,15 +102,23 @@ function ScheduleTable({ isEdit = false, isEditable = true, hasCollapse = false, + onReleaseClick = undefined, + onEditClick = undefined, + handleEditChange = undefined, + onSaveClick = undefined, + onCancelClick = undefined, }: Props) { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [editingRowId, setEditingRowId] = useState(null); const [editedItems, setEditedItems] = useState(items); const { t } = useTranslation("schedule"); + console.log(items) + useEffect(() => { setEditedItems(items); }, [items]); + const handleChangePage = (_event: unknown, newPage: number) => { setPage(newPage); if (setPagingController && pagingController) { @@ -132,24 +147,42 @@ function ScheduleTable({ const handleEditClick = (id: number) => { setEditingRowId(id); + if (onEditClick) { + onEditClick(id) + } }; const handleSaveClick = (item: T) => { setEditingRowId(null); // Call API or any save logic here - setEditedItems((prev) => - prev.map((row) => (row.id === item.id ? { ...row } : row)), - ); + if (onSaveClick) { + onSaveClick(item) + } else { + setEditedItems((prev) => + prev.map((row) => (row.id === item.id ? { ...row } : row)), + ); + } }; + const handleReleaseClick = (item: T) => { + if (onReleaseClick) { + onReleaseClick(item) + } + } + const handleInputChange = ( id: number, field: keyof T, - value: string | number[], + // value: string | number[], + value: string | number, ) => { - setEditedItems((prev) => - prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), - ); + if (handleEditChange) { + handleEditChange(id, field, value) + } else { + setEditedItems((prev) => + prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)), + ); + } }; const handleDeleteClick = (id: number) => { @@ -157,6 +190,14 @@ function ScheduleTable({ setEditedItems((prev) => prev.filter((item) => item.id !== id)); }; + const handleCancelClick = (id: number) => { + if (onCancelClick) { + onCancelClick(id) + } + + setEditingRowId(null) + } + useEffect(() => { console.log("[debug] isEdit in table", isEdit); //TODO: switch all record to not in edit mode and save the changes @@ -188,7 +229,15 @@ function ScheduleTable({ {isDetailedType(type) && ( - + (ele.availableQty ?? 0) >= (ele.demandQty ?? 0)) + // || + editingRowId === row.id + || (row as unknown as DetailedProdScheduleLineResult).approved} + onClick={() => handleReleaseClick(row)} + > @@ -208,7 +257,7 @@ function ScheduleTable({ {isDetailedType(type) && isEditable && ( setEditingRowId(null)} + onClick={() => handleCancelClick(row.id as number)} > @@ -232,20 +281,20 @@ function ScheduleTable({ <> {isDetailedType(type) && isEditable && ( handleEditClick(row.id as number)} > )} - {isDetailedType(type) && isEditable && ( + {/* {isDetailedType(type) && isEditable && ( handleDeleteClick(row.id as number)} > - )} + )} */} {hasCollapse && ( ({ } /> ); + case "input-number": + return ( + { + handleInputChange( + row.id as number, + columnName, + e.target.value, + ) + }} + // onChange={(e) => + // handleInputChange( + // row.id as number, + // columnName, + // e.target.value, + // ) + // } + /> + ); // case 'multi-select': // //TODO: May need update if use // return ( @@ -297,7 +369,9 @@ function ScheduleTable({ // /> // ); case "read-only": - return {row[columnName] as string}; + return column.renderCell ? ( +
{column.renderCell(row)}
+ ) : {row[columnName] as string}; default: return null; // Handle any default case if needed } @@ -330,8 +404,8 @@ function ScheduleTable({ diff --git a/src/i18n/zh/schedule.json b/src/i18n/zh/schedule.json index 4892d68..415011d 100644 --- a/src/i18n/zh/schedule.json +++ b/src/i18n/zh/schedule.json @@ -82,5 +82,6 @@ "mat": "物料", "Product Count(s)": "產品數量", "Schedule Period To": "排程期間至", - "Overall": "總計" + "Overall": "總計", + "Back": "返回" }