[email protected] пре 5 дана
родитељ
комит
b541872d24
2 измењених фајлова са 232 додато и 68 уклоњено
  1. +223
    -61
      src/app/(main)/ps/page.tsx
  2. +9
    -7
      src/components/NavigationContent/NavigationContent.tsx

+ 223
- 61
src/app/(main)/ps/page.tsx Прегледај датотеку

@@ -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>
);
}

+ 9
- 7
src/components/NavigationContent/NavigationContent.tsx Прегледај датотеку

@@ -186,6 +186,7 @@ const NavigationContent: React.FC = () => {
// },
// ],
// },
/*
{
icon: <RequestQuote />,
label: "Scheduling",
@@ -202,15 +203,16 @@ const NavigationContent: React.FC = () => {
label: "Detail Scheduling",
path: "/scheduling/detailed",
},
/*
{
icon: <RequestQuote />,
label: "Production",
path: "/production",
},
*/
],
},
*/
{
icon: <RequestQuote />,
label: "Scheduling",
path: "/ps",
requiredAbility: [AUTH.FORECAST, AUTH.ADMIN],
isHidden: false,
},
{
icon: <RequestQuote />,
label: "Management Job Order",


Loading…
Откажи
Сачувај