|
- "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<any[]>([]);
- const [selectedLines, setSelectedLines] = useState<any[]>([]);
- const [isDetailOpen, setIsDetailOpen] = useState(false);
- const [selectedPs, setSelectedPs] = useState<any>(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<number | ''>(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 (
- <Box sx={{ p: 4, bgcolor: '#fbfbfb', minHeight: '100vh' }}>
-
- {/* Header */}
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
- <Stack direction="row" spacing={2} alignItems="center">
- <CalendarMonth color="primary" sx={{ fontSize: 32 }} />
- <Typography variant="h4" sx={{ fontWeight: 'bold' }}>排程</Typography>
- </Stack>
-
- <Stack direction="row" spacing={2}>
- <Button
- variant="outlined"
- color="success"
- startIcon={<FileDownload />}
- onClick={() => setIsExportDialogOpen(true)}
- sx={{ fontWeight: 'bold' }}
- >
- 匯出計劃/物料需求Excel
- </Button>
- <Button
- variant="contained"
- color="secondary"
- startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <OnlinePrediction />}
- onClick={() => setIsForecastDialogOpen(true)}
- disabled={loading}
- sx={{ fontWeight: 'bold' }}
- >
- 預測排期
- </Button>
- </Stack>
- </Stack>
-
- {/* Query Bar – unchanged */}
- <Paper sx={{ p: 2, mb: 3, display: 'flex', alignItems: 'center', gap: 2, borderLeft: '6px solid #1976d2' }}>
- <TextField
- label="生產日期"
- type="date"
- size="small"
- InputLabelProps={{ shrink: true }}
- value={searchDate}
- onChange={(e) => setSearchDate(e.target.value)}
- />
- <Button variant="contained" startIcon={<Search />} onClick={handleSearch}>
- 搜尋
- </Button>
- </Paper>
-
- {/* Main Table – unchanged */}
- <TableContainer component={Paper}>
- <Table stickyHeader size="small">
- <TableHead>
- <TableRow sx={{ bgcolor: '#f5f5f5' }}>
- <TableCell align="center" sx={{ fontWeight: 'bold', width: 100 }}>詳細</TableCell>
- <TableCell sx={{ fontWeight: 'bold' }}>生產日期</TableCell>
- <TableCell align="right" sx={{ fontWeight: 'bold' }}>預計生產數</TableCell>
- <TableCell align="right" sx={{ fontWeight: 'bold' }}>成品款數</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {schedules.map((ps) => (
- <TableRow key={ps.id} hover>
- <TableCell align="center">
- <IconButton color="primary" size="small" onClick={() => handleViewDetail(ps)}>
- <Visibility fontSize="small" />
- </IconButton>
- </TableCell>
- <TableCell>{formatBackendDate(ps.produceAt)}</TableCell>
- <TableCell align="right">{formatNum(ps.totalEstProdCount)}</TableCell>
- <TableCell align="right">{formatNum(ps.totalFGType)}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </TableContainer>
-
- {/* Detail Dialog – unchanged */}
- <Dialog open={isDetailOpen} onClose={() => setIsDetailOpen(false)} maxWidth="lg" fullWidth>
- <DialogTitle sx={{ bgcolor: '#1976d2', color: 'white' }}>
- <Stack direction="row" alignItems="center" spacing={1}>
- <ListAlt />
- <Typography variant="h6">排期詳細: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)})</Typography>
- </Stack>
- </DialogTitle>
- <DialogContent sx={{ p: 0 }}>
- <TableContainer sx={{ maxHeight: '65vh' }}>
- <Table size="small" stickyHeader>
- <TableHead>
- <TableRow>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>工單號</TableCell>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>物料編號</TableCell>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>物料名稱</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>每日平均出貨量</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>出貨前預計存貨量</TableCell>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>單位</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>可用日</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>生產量(批)</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>預計生產包數</TableCell>
- <TableCell align="center" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>優先度</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {selectedLines.map((line: any) => (
- <TableRow key={line.id} hover>
- <TableCell sx={{ color: 'primary.main', fontWeight: 'bold' }}>{line.joCode || '-'}</TableCell>
- <TableCell sx={{ fontWeight: 'bold' }}>{line.itemCode}</TableCell>
- <TableCell>{line.itemName}</TableCell>
- <TableCell align="right">{formatNum(line.avgQtyLastMonth)}</TableCell>
- <TableCell align="right">{formatNum(line.stockQty)}</TableCell>
- <TableCell>{line.stockUnit}</TableCell>
- <TableCell align="right" sx={{ color: line.daysLeft < 5 ? 'error.main' : 'inherit', fontWeight: line.daysLeft < 5 ? 'bold' : 'normal' }}>
- {line.daysLeft}
- </TableCell>
- <TableCell align="right">{formatNum(line.batchNeed)}</TableCell>
- <TableCell align="right"><strong>{formatNum(line.prodQty)}</strong></TableCell>
- <TableCell align="center">{line.itemPriority}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </TableContainer>
- </DialogContent>
-
- {/* Footer Actions */}
- <DialogActions sx={{ p: 2, bgcolor: '#f9f9f9' }}>
- <Stack direction="row" spacing={2}>
- {/*
- <Tooltip title={!isDateToday ? "Job Orders can only be generated for the current day's schedule." : ""}>
- */}
- <span>
- <Button
- variant="contained"
- color="primary"
- startIcon={isGenerating ? <CircularProgress size={20} color="inherit" /> : <SettingsEthernet />}
- onClick={handleAutoGenJob}
- disabled={isGenerating}
- //disabled={isGenerating || !isDateToday}
- >
- 自動生成工單
- </Button>
- </span>
- {/*
- </Tooltip>
- */}
- <Button
- onClick={() => setIsDetailOpen(false)}
- variant="outlined"
- color="inherit"
- disabled={isGenerating}
- >
- 關閉
- </Button>
- </Stack>
- </DialogActions>
- </Dialog>
-
- {/* ── Forecast Dialog ── */}
- <Dialog
- open={isForecastDialogOpen}
- onClose={() => setIsForecastDialogOpen(false)}
- maxWidth="sm"
- fullWidth
- >
- <DialogTitle>準備生成預計排期</DialogTitle>
- <DialogContent>
- <DialogContentText sx={{ mb: 3 }}>
-
- </DialogContentText>
-
- <Stack spacing={3} sx={{ mt: 2 }}>
- <TextField
- label="開始日期"
- type="date"
- fullWidth
- value={forecastStartDate}
- onChange={(e) => setForecastStartDate(e.target.value)}
- InputLabelProps={{ shrink: true }}
- inputProps={{
- min: dayjs().subtract(30, 'day').format('YYYY-MM-DD'), // optional
- }}
- />
- <TextField
- label="排期日數"
- type="number"
- fullWidth
- value={forecastDays}
- onChange={(e) => {
- 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,
- }}
- />
- </Stack>
- </DialogContent>
- <DialogActions>
- <Button onClick={() => setIsForecastDialogOpen(false)} color="inherit">
- 取消
- </Button>
- <Button
- variant="contained"
- color="secondary"
- onClick={handleConfirmForecast}
- disabled={!forecastStartDate || forecastDays === '' || loading}
- startIcon={loading ? <CircularProgress size={20} /> : <OnlinePrediction />}
- >
- 計算預測排期
- </Button>
- </DialogActions>
- </Dialog>
-
- {/* ── Export Dialog ── */}
- <Dialog
- open={isExportDialogOpen}
- onClose={() => setIsExportDialogOpen(false)}
- maxWidth="xs"
- fullWidth
- >
- <DialogTitle>匯出排期/物料用量預計</DialogTitle>
- <DialogContent>
- <DialogContentText sx={{ mb: 3 }}>
- 選擇要匯出的起始日期
- </DialogContentText>
-
- <TextField
- label="起始日期"
- type="date"
- fullWidth
- value={exportFromDate}
- onChange={(e) => setExportFromDate(e.target.value)}
- InputLabelProps={{ shrink: true }}
- inputProps={{
- min: dayjs().subtract(90, 'day').format('YYYY-MM-DD'), // optional limit
- }}
- sx={{ mt: 1 }}
- />
- </DialogContent>
- <DialogActions>
- <Button onClick={() => setIsExportDialogOpen(false)} color="inherit">
- 取消
- </Button>
- <Button
- variant="contained"
- color="success"
- onClick={handleConfirmExport}
- disabled={!exportFromDate || loading}
- startIcon={loading ? <CircularProgress size={20} /> : <FileDownload />}
- >
- 匯出
- </Button>
- </DialogActions>
- </Dialog>
-
- </Box>
- );
- }
|