|
- "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<any[]>([]);
- const [selectedLines, setSelectedLines] = useState([]);
- const [isDetailOpen, setIsDetailOpen] = useState(false);
- const [selectedPs, setSelectedPs] = useState<any>(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 (
- <Box sx={{ p: 4, bgcolor: '#fbfbfb', minHeight: '100vh' }}>
-
- {/* Top Header Buttons */}
- <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' }}>Production Planning</Typography>
- </Stack>
-
- <Stack direction="row" spacing={2}>
- <Button variant="outlined" color="success" startIcon={<FileDownload />} onClick={handleExport} sx={{ fontWeight: 'bold' }}>
- Export Excel
- </Button>
- <Button
- variant="contained"
- color="secondary"
- startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <OnlinePrediction />}
- onClick={handleForecast}
- disabled={loading}
- sx={{ fontWeight: 'bold' }}
- >
- Forecast
- </Button>
- </Stack>
- </Stack>
-
- {/* Query Bar */}
- <Paper sx={{ p: 2, mb: 3, display: 'flex', alignItems: 'center', gap: 2, borderLeft: '6px solid #1976d2' }}>
- <TextField
- label="Produce Date"
- type="date"
- size="small"
- InputLabelProps={{ shrink: true }}
- value={searchDate}
- onChange={(e) => setSearchDate(e.target.value)}
- />
- <Button variant="contained" startIcon={<Search />} onClick={handleSearch}>Query</Button>
- </Paper>
-
- {/* Main Grid Table */}
- <TableContainer component={Paper}>
- <Table stickyHeader size="small">
- <TableHead>
- <TableRow sx={{ bgcolor: '#f5f5f5' }}>
- <TableCell align="center" sx={{ fontWeight: 'bold', width: 100 }}>Action</TableCell>
- <TableCell sx={{ fontWeight: 'bold' }}>ID</TableCell>
- <TableCell sx={{ fontWeight: 'bold' }}>Production Date</TableCell>
- <TableCell align="right" sx={{ fontWeight: 'bold' }}>Est. Prod Count</TableCell>
- <TableCell align="right" sx={{ fontWeight: 'bold' }}>Total FG Types</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>#{ps.id}</TableCell>
- <TableCell>{formatBackendDate(ps.produceAt)}</TableCell>
- <TableCell align="right">{formatNum(ps.totalEstProdCount)}</TableCell>
- <TableCell align="right">{formatNum(ps.totalFGType)}</TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </TableContainer>
-
- {/* Detailed Lines Dialog */}
- <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">Schedule Details: {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' }}>Job Order</TableCell>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Item Code</TableCell>
- <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Item Name</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Avg Last Month</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Stock</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Days Left</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Batch Need</TableCell>
- <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Prod Qty</TableCell>
- <TableCell align="center" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Priority</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 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 || !isDateToday}
- >
- Auto Gen Job
- </Button>
- </span>
- </Tooltip>
- <Button
- onClick={() => setIsDetailOpen(false)}
- variant="outlined"
- color="inherit"
- disabled={isGenerating}
- >
- Close
- </Button>
- </Stack>
- </DialogActions>
- </Dialog>
- </Box>
- );
- }
|