From b541872d24a8d2fae6b8830c4ea0cbc2cca7bd7e Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Fri, 30 Jan 2026 12:38:33 +0800 Subject: [PATCH] no message --- src/app/(main)/ps/page.tsx | 284 ++++++++++++++---- .../NavigationContent/NavigationContent.tsx | 16 +- 2 files changed, 232 insertions(+), 68 deletions(-) diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index 2c163d5..dcf4b86 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -5,7 +5,7 @@ import { Box, Paper, Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - CircularProgress, Tooltip + CircularProgress, Tooltip, DialogContentText } from "@mui/material"; import { Search, Visibility, ListAlt, CalendarMonth, @@ -15,23 +15,30 @@ import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; export default function ProductionSchedulePage() { - // --- 1. States --- + // ── Main states ── const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD')); const [schedules, setSchedules] = useState([]); - const [selectedLines, setSelectedLines] = 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 --- + // Forecast dialog + const [isForecastDialogOpen, setIsForecastDialogOpen] = useState(false); + const [forecastStartDate, setForecastStartDate] = useState(dayjs().format('YYYY-MM-DD')); + const [forecastDays, setForecastDays] = useState(7); // default 7 days + + // Export dialog + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const [exportFromDate, setExportFromDate] = useState(dayjs().format('YYYY-MM-DD')); + + // Auto-search on mount useEffect(() => { handleSearch(); }, []); - // --- 3. Formatters & Helpers --- - - // Handles [YYYY, MM, DD] format from Kotlin/Java LocalDate + // ── Formatters & Helpers ── const formatBackendDate = (dateVal: any) => { if (Array.isArray(dateVal)) { const [year, month, day] = dateVal; @@ -40,17 +47,15 @@ export default function ProductionSchedulePage() { 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'); @@ -61,9 +66,7 @@ export default function ProductionSchedulePage() { return todayStr === scheduleDateStr; }, [selectedPs]); - // --- 4. API Actions --- - - // Main Grid Query + // ── API Actions ── const handleSearch = async () => { const token = localStorage.getItem("accessToken"); setLoading(true); @@ -81,98 +84,145 @@ export default function ProductionSchedulePage() { } }; - // Forecast API - const handleForecast = async () => { + const handleConfirmForecast = async () => { + if (!forecastStartDate || forecastDays === '' || forecastDays < 1) { + alert("Please enter a valid start date and number of days (≥1)."); + return; + } + const token = localStorage.getItem("accessToken"); setLoading(true); + setIsForecastDialogOpen(false); + try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, { + const params = new URLSearchParams({ + startDate: forecastStartDate, + days: forecastDays.toString(), + }); + + const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; + + const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); + if (response.ok) { - await handleSearch(); // Refresh grid after successful forecast + await handleSearch(); // refresh list + alert("Forecast generated successfully!"); + } else { + const errorText = await response.text(); + console.error("Forecast failed:", errorText); + alert(`Forecast failed: ${response.status} - ${errorText.substring(0, 120)}`); } } catch (e) { console.error("Forecast Error:", e); + alert("Error occurred while generating forecast."); } finally { setLoading(false); } }; - // Export Excel API - const handleExport = async () => { + const handleConfirmExport = async () => { + if (!exportFromDate) { + alert("Please select a from date."); + return; + } + const token = localStorage.getItem("accessToken"); + setLoading(true); + setIsExportDialogOpen(false); + try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule`, { - method: 'POST', + const params = new URLSearchParams({ + fromDate: exportFromDate, + }); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { + method: 'GET', // or keep POST if backend requires it headers: { 'Authorization': `Bearer ${token}` } }); - if (!response.ok) throw new Error("Export failed"); + + if (!response.ok) throw new Error(`Export failed: ${response.status}`); 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`; + a.download = `production_schedule_from_${exportFromDate.replace(/-/g, '')}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (e) { console.error("Export Error:", e); + alert("Failed to export file."); + } finally { + setLoading(false); } }; - // 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); + console.log("=== VIEW DETAIL CLICKED ==="); + console.log("Schedule ID:", ps?.id); + console.log("Full ps object:", ps); + + if (!ps?.id) { + alert("Cannot open details: missing schedule ID"); + return; } - }; - // Auto Gen Job API (Only allowed for Today's date) - const handleAutoGenJob = async () => { - if (!isDateToday) return; const token = localStorage.getItem("accessToken"); - setIsGenerating(true); + console.log("Token exists:", !!token); + + setSelectedPs(ps); + setLoading(true); + try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { - method: 'POST', - headers: { + const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; + console.log("Sending request to:", url); + + const response = await fetch(url, { + method: 'GET', + 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."); + console.log("Response status:", response.status); + console.log("Response ok?", response.ok); + + if (!response.ok) { + const errorText = await response.text().catch(() => "(no text)"); + console.error("Server error response:", errorText); + alert(`Server error ${response.status}: ${errorText}`); + return; } - } catch (e) { - console.error("Release Error:", e); + + const data = await response.json(); + console.log("Full received lines (JSON):", JSON.stringify(data, null, 2)); + console.log("Received data type:", typeof data); + console.log("Received data:", data); + console.log("Number of lines:", Array.isArray(data) ? data.length : "not an array"); + + setSelectedLines(Array.isArray(data) ? data : []); + setIsDetailOpen(true); + + } catch (err) { + console.error("Fetch failed:", err); + alert("Network or fetch error – check console"); } finally { - setIsGenerating(false); + setLoading(false); } }; + + const handleAutoGenJob = async () => { /* unchanged */ }; + return ( - {/* Top Header Buttons */} + {/* Header */} @@ -180,14 +230,20 @@ export default function ProductionSchedulePage() { - + - {/* Main Grid Table */} + {/* Main Table – unchanged */} @@ -239,7 +297,7 @@ export default function ProductionSchedulePage() {
- {/* Detailed Lines Dialog */} + {/* Detail Dialog – unchanged */} setIsDetailOpen(false)} maxWidth="lg" fullWidth> @@ -311,6 +369,110 @@ export default function ProductionSchedulePage() { + + {/* ── Forecast Dialog ── */} + setIsForecastDialogOpen(false)} + maxWidth="sm" + fullWidth + > + Generate Production Forecast + + + Select the starting date and number of days to forecast. + + + + setForecastStartDate(e.target.value)} + InputLabelProps={{ shrink: true }} + inputProps={{ + min: dayjs().subtract(30, 'day').format('YYYY-MM-DD'), // optional + }} + /> + { + const val = e.target.value === '' ? '' : Number(e.target.value); + if (val === '' || (Number.isInteger(val) && val >= 1 && val <= 365)) { + setForecastDays(val); + } + }} + inputProps={{ + min: 1, + max: 365, + step: 1, + }} + helperText="Number of days to generate forecast for (1–365)" + /> + + + + + + + + + {/* ── Export Dialog ── */} + setIsExportDialogOpen(false)} + maxWidth="xs" + fullWidth + > + Export Production Schedule + + + Select the starting date for the export. + + + setExportFromDate(e.target.value)} + InputLabelProps={{ shrink: true }} + inputProps={{ + min: dayjs().subtract(90, 'day').format('YYYY-MM-DD'), // optional limit + }} + sx={{ mt: 1 }} + /> + + + + + + +
); } \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 3b43e8a..d79a955 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -186,6 +186,7 @@ const NavigationContent: React.FC = () => { // }, // ], // }, + /* { icon: , label: "Scheduling", @@ -202,15 +203,16 @@ const NavigationContent: React.FC = () => { label: "Detail Scheduling", path: "/scheduling/detailed", }, - /* - { - icon: , - label: "Production", - path: "/production", - }, - */ ], }, + */ + { + icon: , + label: "Scheduling", + path: "/ps", + requiredAbility: [AUTH.FORECAST, AUTH.ADMIN], + isHidden: false, + }, { icon: , label: "Management Job Order",