|
|
|
@@ -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<any[]>([]); |
|
|
|
const [selectedLines, setSelectedLines] = useState([]); |
|
|
|
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); |
|
|
|
|
|
|
|
// --- 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<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(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
// --- 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 ( |
|
|
|
<Box sx={{ p: 4, bgcolor: '#fbfbfb', minHeight: '100vh' }}> |
|
|
|
|
|
|
|
{/* Top Header Buttons */} |
|
|
|
{/* 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 }} /> |
|
|
|
@@ -180,14 +230,20 @@ export default function ProductionSchedulePage() { |
|
|
|
</Stack> |
|
|
|
|
|
|
|
<Stack direction="row" spacing={2}> |
|
|
|
<Button variant="outlined" color="success" startIcon={<FileDownload />} onClick={handleExport} sx={{ fontWeight: 'bold' }}> |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
color="success" |
|
|
|
startIcon={<FileDownload />} |
|
|
|
onClick={() => setIsExportDialogOpen(true)} |
|
|
|
sx={{ fontWeight: 'bold' }} |
|
|
|
> |
|
|
|
Export Excel |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="secondary" |
|
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <OnlinePrediction />} |
|
|
|
onClick={handleForecast} |
|
|
|
onClick={() => setIsForecastDialogOpen(true)} |
|
|
|
disabled={loading} |
|
|
|
sx={{ fontWeight: 'bold' }} |
|
|
|
> |
|
|
|
@@ -196,7 +252,7 @@ export default function ProductionSchedulePage() { |
|
|
|
</Stack> |
|
|
|
</Stack> |
|
|
|
|
|
|
|
{/* Query Bar */} |
|
|
|
{/* Query Bar – unchanged */} |
|
|
|
<Paper sx={{ p: 2, mb: 3, display: 'flex', alignItems: 'center', gap: 2, borderLeft: '6px solid #1976d2' }}> |
|
|
|
<TextField |
|
|
|
label="Produce Date" |
|
|
|
@@ -206,10 +262,12 @@ export default function ProductionSchedulePage() { |
|
|
|
value={searchDate} |
|
|
|
onChange={(e) => setSearchDate(e.target.value)} |
|
|
|
/> |
|
|
|
<Button variant="contained" startIcon={<Search />} onClick={handleSearch}>Query</Button> |
|
|
|
<Button variant="contained" startIcon={<Search />} onClick={handleSearch}> |
|
|
|
Query |
|
|
|
</Button> |
|
|
|
</Paper> |
|
|
|
|
|
|
|
{/* Main Grid Table */} |
|
|
|
{/* Main Table – unchanged */} |
|
|
|
<TableContainer component={Paper}> |
|
|
|
<Table stickyHeader size="small"> |
|
|
|
<TableHead> |
|
|
|
@@ -239,7 +297,7 @@ export default function ProductionSchedulePage() { |
|
|
|
</Table> |
|
|
|
</TableContainer> |
|
|
|
|
|
|
|
{/* Detailed Lines Dialog */} |
|
|
|
{/* 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}> |
|
|
|
@@ -311,6 +369,110 @@ export default function ProductionSchedulePage() { |
|
|
|
</Stack> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
{/* ── Forecast Dialog ── */} |
|
|
|
<Dialog |
|
|
|
open={isForecastDialogOpen} |
|
|
|
onClose={() => setIsForecastDialogOpen(false)} |
|
|
|
maxWidth="sm" |
|
|
|
fullWidth |
|
|
|
> |
|
|
|
<DialogTitle>Generate Production Forecast</DialogTitle> |
|
|
|
<DialogContent> |
|
|
|
<DialogContentText sx={{ mb: 3 }}> |
|
|
|
Select the starting date and number of days to forecast. |
|
|
|
</DialogContentText> |
|
|
|
|
|
|
|
<Stack spacing={3} sx={{ mt: 2 }}> |
|
|
|
<TextField |
|
|
|
label="Start Date" |
|
|
|
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="No. of Dates (days)" |
|
|
|
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, |
|
|
|
}} |
|
|
|
helperText="Number of days to generate forecast for (1–365)" |
|
|
|
/> |
|
|
|
</Stack> |
|
|
|
</DialogContent> |
|
|
|
<DialogActions> |
|
|
|
<Button onClick={() => setIsForecastDialogOpen(false)} color="inherit"> |
|
|
|
Cancel |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="secondary" |
|
|
|
onClick={handleConfirmForecast} |
|
|
|
disabled={!forecastStartDate || forecastDays === '' || loading} |
|
|
|
startIcon={loading ? <CircularProgress size={20} /> : <OnlinePrediction />} |
|
|
|
> |
|
|
|
Generate Forecast |
|
|
|
</Button> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
{/* ── Export Dialog ── */} |
|
|
|
<Dialog |
|
|
|
open={isExportDialogOpen} |
|
|
|
onClose={() => setIsExportDialogOpen(false)} |
|
|
|
maxWidth="xs" |
|
|
|
fullWidth |
|
|
|
> |
|
|
|
<DialogTitle>Export Production Schedule</DialogTitle> |
|
|
|
<DialogContent> |
|
|
|
<DialogContentText sx={{ mb: 3 }}> |
|
|
|
Select the starting date for the export. |
|
|
|
</DialogContentText> |
|
|
|
|
|
|
|
<TextField |
|
|
|
label="From Date" |
|
|
|
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"> |
|
|
|
Cancel |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="success" |
|
|
|
onClick={handleConfirmExport} |
|
|
|
disabled={!exportFromDate || loading} |
|
|
|
startIcon={loading ? <CircularProgress size={20} /> : <FileDownload />} |
|
|
|
> |
|
|
|
Export |
|
|
|
</Button> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |