Kaynağa Gözat

Adding new UI for production schedule

master
ebeveyn
işleme
5d836cdffc
2 değiştirilmiş dosya ile 323 ekleme ve 0 silme
  1. +316
    -0
      src/app/(main)/ps/page.tsx
  2. +7
    -0
      src/components/NavigationContent/NavigationContent.tsx

+ 316
- 0
src/app/(main)/ps/page.tsx Dosyayı Görüntüle

@@ -0,0 +1,316 @@
"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>
);
}

+ 7
- 0
src/components/NavigationContent/NavigationContent.tsx Dosyayı Görüntüle

@@ -229,6 +229,13 @@ const NavigationContent: React.FC = () => {
},
],
},
{
icon: <BugReportIcon />,
label: "PS",
path: "/ps",
requiredAbility: TESTING,
isHidden: false,
},
{
icon: <BugReportIcon />,
label: "Printer Testing",


Yükleniyor…
İptal
Kaydet