From 5d836cdffcc43efdbd9133258aa41d78a69fcd4d Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sun, 11 Jan 2026 02:15:41 +0800 Subject: [PATCH] Adding new UI for production schedule --- src/app/(main)/ps/page.tsx | 316 ++++++++++++++++++ .../NavigationContent/NavigationContent.tsx | 7 + 2 files changed, 323 insertions(+) create mode 100644 src/app/(main)/ps/page.tsx diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx new file mode 100644 index 0000000..fc2a73e --- /dev/null +++ b/src/app/(main)/ps/page.tsx @@ -0,0 +1,316 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { + Box, Paper, Typography, Button, Dialog, DialogTitle, + DialogContent, DialogActions, TextField, Stack, Table, + TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, + CircularProgress, Tooltip +} from "@mui/material"; +import { + Search, Visibility, ListAlt, CalendarMonth, + OnlinePrediction, FileDownload, SettingsEthernet +} from "@mui/icons-material"; +import dayjs from "dayjs"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; + +export default function ProductionSchedulePage() { + // --- 1. States --- + const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD')); + const [schedules, setSchedules] = useState([]); + const [selectedLines, setSelectedLines] = useState([]); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [selectedPs, setSelectedPs] = useState(null); + const [loading, setLoading] = useState(false); + const [isGenerating, setIsGenerating] = useState(false); + + // --- 2. Auto-search on page entry --- + useEffect(() => { + handleSearch(); + }, []); + + // --- 3. Formatters & Helpers --- + + // Handles [YYYY, MM, DD] format from Kotlin/Java LocalDate + const formatBackendDate = (dateVal: any) => { + if (Array.isArray(dateVal)) { + const [year, month, day] = dateVal; + return dayjs(new Date(year, month - 1, day)).format('DD MMM (dddd)'); + } + return dayjs(dateVal).format('DD MMM (dddd)'); + }; + + // Adds commas as thousands separators + const formatNum = (num: any) => { + return new Intl.NumberFormat('en-US').format(Number(num) || 0); + }; + + // Logic to determine if the selected row's produceAt is TODAY + const isDateToday = useMemo(() => { + if (!selectedPs?.produceAt) return false; + const todayStr = dayjs().format('YYYY-MM-DD'); + let scheduleDateStr = ""; + + if (Array.isArray(selectedPs.produceAt)) { + const [y, m, d] = selectedPs.produceAt; + scheduleDateStr = dayjs(new Date(y, m - 1, d)).format('YYYY-MM-DD'); + } else { + scheduleDateStr = dayjs(selectedPs.produceAt).format('YYYY-MM-DD'); + } + + return todayStr === scheduleDateStr; + }, [selectedPs]); + + // --- 4. API Actions --- + + // Main Grid Query + const handleSearch = async () => { + const token = localStorage.getItem("accessToken"); + setLoading(true); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await response.json(); + setSchedules(Array.isArray(data) ? data : []); + } catch (e) { + console.error("Search Error:", e); + } finally { + setLoading(false); + } + }; + + // Forecast API + const handleForecast = async () => { + const token = localStorage.getItem("accessToken"); + setLoading(true); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (response.ok) { + await handleSearch(); // Refresh grid after successful forecast + } + } catch (e) { + console.error("Forecast Error:", e); + } finally { + setLoading(false); + } + }; + + // Export Excel API + const handleExport = async () => { + const token = localStorage.getItem("accessToken"); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/export-prod-schedule`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + if (!response.ok) throw new Error("Export failed"); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `production_schedule_${dayjs().format('YYYYMMDD')}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (e) { + console.error("Export Error:", e); + } + }; + + // Get Detail Lines + const handleViewDetail = async (ps: any) => { + const token = localStorage.getItem("accessToken"); + setSelectedPs(ps); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await response.json(); + setSelectedLines(data || []); + setIsDetailOpen(true); + } catch (e) { + console.error("Detail Error:", e); + } + }; + + // Auto Gen Job API (Only allowed for Today's date) + const handleAutoGenJob = async () => { + if (!isDateToday) return; + const token = localStorage.getItem("accessToken"); + setIsGenerating(true); + try { + const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: selectedPs.id }) + }); + + if (response.ok) { + alert("Job Orders generated successfully!"); + setIsDetailOpen(false); + } else { + alert("Failed to generate jobs."); + } + } catch (e) { + console.error("Release Error:", e); + } finally { + setIsGenerating(false); + } + }; + + return ( + + + {/* Top Header Buttons */} + + + + Production Planning + + + + + + + + + {/* Query Bar */} + + setSearchDate(e.target.value)} + /> + + + + {/* Main Grid Table */} + + + + + Action + ID + Production Date + Est. Prod Count + Total FG Types + + + + {schedules.map((ps) => ( + + + handleViewDetail(ps)}> + + + + #{ps.id} + {formatBackendDate(ps.produceAt)} + {formatNum(ps.totalEstProdCount)} + {formatNum(ps.totalFGType)} + + ))} + +
+
+ + {/* Detailed Lines Dialog */} + setIsDetailOpen(false)} maxWidth="lg" fullWidth> + + + + Schedule Details: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)}) + + + + + + + + Job Order + Item Code + Item Name + Avg Last Month + Stock + Days Left + Batch Need + Prod Qty + Priority + + + + {selectedLines.map((line: any) => ( + + {line.joCode || '-'} + {line.itemCode} + {line.itemName} + {formatNum(line.avgQtyLastMonth)} + {formatNum(line.stockQty)} + + {line.daysLeft} + + {formatNum(line.batchNeed)} + {formatNum(line.prodQty)} + {line.itemPriority} + + ))} + +
+
+
+ + {/* Footer Actions */} + + + + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 0abc585..7e33068 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -229,6 +229,13 @@ const NavigationContent: React.FC = () => { }, ], }, + { + icon: , + label: "PS", + path: "/ps", + requiredAbility: TESTING, + isHidden: false, + }, { icon: , label: "Printer Testing",