From e9da01b664e094d90b57df877afeae3b927fa03b Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 29 Dec 2025 00:16:32 +0800 Subject: [PATCH] Adding prod sche related --- src/app/api/scheduling/actions.ts | 19 +++++++ src/app/api/scheduling/index.ts | 2 + .../DetailedScheduleSearchView.tsx | 55 ++++++++++++++++++- .../DetailedScheduleDetailView.tsx | 5 +- .../DetailedScheduleDetailWrapper.tsx | 1 + .../ViewByFGDetails.tsx | 11 +++- .../ScheduleTable/ScheduleTable.tsx | 4 +- 7 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 0cc4b05..14c891d 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -177,6 +177,25 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => 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( `${BASE_API_URL}/productionSchedule/detail/detailed/save`, diff --git a/src/app/api/scheduling/index.ts b/src/app/api/scheduling/index.ts index 5ffa663..4171c56 100644 --- a/src/app/api/scheduling/index.ts +++ b/src/app/api/scheduling/index.ts @@ -105,6 +105,8 @@ export interface DetailedProdScheduleLineResult { stockQty: number; // Warehouse stock quantity daysLeft: number; // Days remaining before stockout needNoOfJobOrder: number; + prodQty: number; + outputQty: number; } export interface DetailedProdScheduleLineBomMaterialResult { diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index 6da92b5..12e17c3 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/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" @@ -298,21 +300,68 @@ const DSOverview: React.FC = ({ 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 ( <> + + = ({ } }, [scheduleId, setIsUploading, t, router]); // -------------------------------------------------------------------- - - + const [tempValue, setTempValue] = useState(null) const onEditClick = useCallback((rowId: number) => { const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) @@ -298,7 +297,7 @@ const DetailedScheduleDetailView: React.FC = ({ onClick={onGlobalReleaseClick} disabled={!scheduleId} // Disable if we don't have a schedule ID > - {t("放單(自動生成工單)")} + {t("生成工單")} {/* ------------------------------------------- */} diff --git a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx index 80a6472..9f3c6e1 100644 --- a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx +++ b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx @@ -32,6 +32,7 @@ const DetailedScheduleDetailWrapper: React.FC & SubComponents = async ({ stockQty: line.stockQty || 0, daysLeft: line.daysLeft || 0, needNoOfJobOrder: line.needNoOfJobOrder || 0, + outputQty: line.outputQty || 0, })).sort((a, b) => b.priority - a.priority); } diff --git a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx index a4fa909..151da2b 100644 --- a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx @@ -108,9 +108,16 @@ const ViewByFGDetails: React.FC = ({ 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("生產量"), + label: t("生產批次"), type: "read-only", style: { textAlign: "right", fontWeight: "bold" } as any, renderCell: (row) => <>{row.needNoOfJobOrder ?? 0}, @@ -137,7 +144,7 @@ const ViewByFGDetails: React.FC = ({ isEditable={true} isEdit={isEdit} hasCollapse={true} - // Note: onReleaseClick is NOT passed here to hide the row-level "Release" function + onReleaseClick={onReleaseClick} onEditClick={onEditClick} handleEditChange={handleEditChange} onSaveClick={onSaveClick} diff --git a/src/components/ScheduleTable/ScheduleTable.tsx b/src/components/ScheduleTable/ScheduleTable.tsx index d7d575e..db83752 100644 --- a/src/components/ScheduleTable/ScheduleTable.tsx +++ b/src/components/ScheduleTable/ScheduleTable.tsx @@ -227,7 +227,7 @@ function ScheduleTable({ return ( <> - {/*isDetailedType(type) && ( + {isDetailedType(type) && ( ({ - )*/} + )} {(isEditable || hasCollapse) && ( {editingRowId === row.id ? (