"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, DialogContentText } from "@mui/material"; import { Search, Visibility, ListAlt, CalendarMonth, OnlinePrediction, FileDownload, SettingsEthernet } from "@mui/icons-material"; import dayjs from "dayjs"; import { redirect } from "next/navigation"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; export default function ProductionSchedulePage() { // ── Main 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); // 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(); }, []); // ── Formatters & Helpers ── 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)'); }; const formatNum = (num: any) => { return new Intl.NumberFormat('en-US').format(Number(num) || 0); }; 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]); // ── API Actions ── 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}` } }); if (response.status === 401 || response.status === 403) { console.warn(`Auth error ${response.status} → clearing token & redirecting`); window.location.href = "/login?session=expired"; return; // ← stops execution here } const data = await response.json(); setSchedules(Array.isArray(data) ? data : []); } catch (e) { console.error("Search Error:", e); } finally { setLoading(false); } }; 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 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 list alert("成功計算排期!"); } else { const errorText = await response.text(); console.error("Forecast failed:", errorText); alert(`計算錯誤: ${response.status} - ${errorText.substring(0, 120)}`); } } catch (e) { console.error("Forecast Error:", e); alert("發生不明狀況."); } finally { setLoading(false); } }; const handleConfirmExport = async () => { if (!exportFromDate) { alert("Please select a from date."); return; } const token = localStorage.getItem("accessToken"); setLoading(true); setIsExportDialogOpen(false); try { 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: ${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_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); } }; const handleViewDetail = async (ps: any) => { 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; } const token = localStorage.getItem("accessToken"); console.log("Token exists:", !!token); setSelectedPs(ps); setLoading(true); try { 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}`, }, }); 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; } 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 { setLoading(false); } }; 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) { const data = await response.json(); const displayMessage = data.message || "Operation completed."; alert(displayMessage); //alert("Job Orders generated successfully!"); setIsDetailOpen(false); } else { alert("Failed to generate jobs."); } } catch (e) { console.error("Release Error:", e); } finally { setIsGenerating(false); } }; return ( {/* Header */} 排程 {/* Query Bar – unchanged */} setSearchDate(e.target.value)} /> {/* Main Table – unchanged */} 詳細 生產日期 預計生產數 成品款數 {schedules.map((ps) => ( handleViewDetail(ps)}> {formatBackendDate(ps.produceAt)} {formatNum(ps.totalEstProdCount)} {formatNum(ps.totalFGType)} ))}
{/* Detail Dialog – unchanged */} setIsDetailOpen(false)} maxWidth="lg" fullWidth> 排期詳細: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)}) 工單號 物料編號 物料名稱 每日平均出貨量 出貨前預計存貨量 單位 可用日 生產量(批) 預計生產包數 優先度 {selectedLines.map((line: any) => ( {line.joCode || '-'} {line.itemCode} {line.itemName} {formatNum(line.avgQtyLastMonth)} {formatNum(line.stockQty)} {line.stockUnit} {line.daysLeft} {formatNum(line.batchNeed)} {formatNum(line.prodQty)} {line.itemPriority} ))}
{/* Footer Actions */} {/* */} {/* */}
{/* ── Forecast Dialog ── */} setIsForecastDialogOpen(false)} maxWidth="sm" fullWidth > 準備生成預計排期 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, }} /> {/* ── Export Dialog ── */} setIsExportDialogOpen(false)} maxWidth="xs" fullWidth > 匯出排期/物料用量預計 選擇要匯出的起始日期 setExportFromDate(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: dayjs().subtract(90, 'day').format('YYYY-MM-DD'), // optional limit }} sx={{ mt: 1 }} />
); }