FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

316 řádky
12 KiB

  1. "use client";
  2. import React, { useState, useEffect, useMemo } from "react";
  3. import {
  4. Box, Paper, Typography, Button, Dialog, DialogTitle,
  5. DialogContent, DialogActions, TextField, Stack, Table,
  6. TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
  7. CircularProgress, Tooltip
  8. } from "@mui/material";
  9. import {
  10. Search, Visibility, ListAlt, CalendarMonth,
  11. OnlinePrediction, FileDownload, SettingsEthernet
  12. } from "@mui/icons-material";
  13. import dayjs from "dayjs";
  14. import { NEXT_PUBLIC_API_URL } from "@/config/api";
  15. export default function ProductionSchedulePage() {
  16. // --- 1. States ---
  17. const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD'));
  18. const [schedules, setSchedules] = useState<any[]>([]);
  19. const [selectedLines, setSelectedLines] = useState([]);
  20. const [isDetailOpen, setIsDetailOpen] = useState(false);
  21. const [selectedPs, setSelectedPs] = useState<any>(null);
  22. const [loading, setLoading] = useState(false);
  23. const [isGenerating, setIsGenerating] = useState(false);
  24. // --- 2. Auto-search on page entry ---
  25. useEffect(() => {
  26. handleSearch();
  27. }, []);
  28. // --- 3. Formatters & Helpers ---
  29. // Handles [YYYY, MM, DD] format from Kotlin/Java LocalDate
  30. const formatBackendDate = (dateVal: any) => {
  31. if (Array.isArray(dateVal)) {
  32. const [year, month, day] = dateVal;
  33. return dayjs(new Date(year, month - 1, day)).format('DD MMM (dddd)');
  34. }
  35. return dayjs(dateVal).format('DD MMM (dddd)');
  36. };
  37. // Adds commas as thousands separators
  38. const formatNum = (num: any) => {
  39. return new Intl.NumberFormat('en-US').format(Number(num) || 0);
  40. };
  41. // Logic to determine if the selected row's produceAt is TODAY
  42. const isDateToday = useMemo(() => {
  43. if (!selectedPs?.produceAt) return false;
  44. const todayStr = dayjs().format('YYYY-MM-DD');
  45. let scheduleDateStr = "";
  46. if (Array.isArray(selectedPs.produceAt)) {
  47. const [y, m, d] = selectedPs.produceAt;
  48. scheduleDateStr = dayjs(new Date(y, m - 1, d)).format('YYYY-MM-DD');
  49. } else {
  50. scheduleDateStr = dayjs(selectedPs.produceAt).format('YYYY-MM-DD');
  51. }
  52. return todayStr === scheduleDateStr;
  53. }, [selectedPs]);
  54. // --- 4. API Actions ---
  55. // Main Grid Query
  56. const handleSearch = async () => {
  57. const token = localStorage.getItem("accessToken");
  58. setLoading(true);
  59. try {
  60. const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, {
  61. method: 'GET',
  62. headers: { 'Authorization': `Bearer ${token}` }
  63. });
  64. const data = await response.json();
  65. setSchedules(Array.isArray(data) ? data : []);
  66. } catch (e) {
  67. console.error("Search Error:", e);
  68. } finally {
  69. setLoading(false);
  70. }
  71. };
  72. // Forecast API
  73. const handleForecast = async () => {
  74. const token = localStorage.getItem("accessToken");
  75. setLoading(true);
  76. try {
  77. const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule`, {
  78. method: 'POST',
  79. headers: { 'Authorization': `Bearer ${token}` }
  80. });
  81. if (response.ok) {
  82. await handleSearch(); // Refresh grid after successful forecast
  83. }
  84. } catch (e) {
  85. console.error("Forecast Error:", e);
  86. } finally {
  87. setLoading(false);
  88. }
  89. };
  90. // Export Excel API
  91. const handleExport = async () => {
  92. const token = localStorage.getItem("accessToken");
  93. try {
  94. const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/export-prod-schedule`, {
  95. method: 'POST',
  96. headers: { 'Authorization': `Bearer ${token}` }
  97. });
  98. if (!response.ok) throw new Error("Export failed");
  99. const blob = await response.blob();
  100. const url = window.URL.createObjectURL(blob);
  101. const a = document.createElement('a');
  102. a.href = url;
  103. a.download = `production_schedule_${dayjs().format('YYYYMMDD')}.xlsx`;
  104. document.body.appendChild(a);
  105. a.click();
  106. window.URL.revokeObjectURL(url);
  107. document.body.removeChild(a);
  108. } catch (e) {
  109. console.error("Export Error:", e);
  110. }
  111. };
  112. // Get Detail Lines
  113. const handleViewDetail = async (ps: any) => {
  114. const token = localStorage.getItem("accessToken");
  115. setSelectedPs(ps);
  116. try {
  117. const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`, {
  118. method: 'GET',
  119. headers: { 'Authorization': `Bearer ${token}` }
  120. });
  121. const data = await response.json();
  122. setSelectedLines(data || []);
  123. setIsDetailOpen(true);
  124. } catch (e) {
  125. console.error("Detail Error:", e);
  126. }
  127. };
  128. // Auto Gen Job API (Only allowed for Today's date)
  129. const handleAutoGenJob = async () => {
  130. if (!isDateToday) return;
  131. const token = localStorage.getItem("accessToken");
  132. setIsGenerating(true);
  133. try {
  134. const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, {
  135. method: 'POST',
  136. headers: {
  137. 'Authorization': `Bearer ${token}`,
  138. 'Content-Type': 'application/json'
  139. },
  140. body: JSON.stringify({ id: selectedPs.id })
  141. });
  142. if (response.ok) {
  143. alert("Job Orders generated successfully!");
  144. setIsDetailOpen(false);
  145. } else {
  146. alert("Failed to generate jobs.");
  147. }
  148. } catch (e) {
  149. console.error("Release Error:", e);
  150. } finally {
  151. setIsGenerating(false);
  152. }
  153. };
  154. return (
  155. <Box sx={{ p: 4, bgcolor: '#fbfbfb', minHeight: '100vh' }}>
  156. {/* Top Header Buttons */}
  157. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
  158. <Stack direction="row" spacing={2} alignItems="center">
  159. <CalendarMonth color="primary" sx={{ fontSize: 32 }} />
  160. <Typography variant="h4" sx={{ fontWeight: 'bold' }}>Production Planning</Typography>
  161. </Stack>
  162. <Stack direction="row" spacing={2}>
  163. <Button variant="outlined" color="success" startIcon={<FileDownload />} onClick={handleExport} sx={{ fontWeight: 'bold' }}>
  164. Export Excel
  165. </Button>
  166. <Button
  167. variant="contained"
  168. color="secondary"
  169. startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <OnlinePrediction />}
  170. onClick={handleForecast}
  171. disabled={loading}
  172. sx={{ fontWeight: 'bold' }}
  173. >
  174. Forecast
  175. </Button>
  176. </Stack>
  177. </Stack>
  178. {/* Query Bar */}
  179. <Paper sx={{ p: 2, mb: 3, display: 'flex', alignItems: 'center', gap: 2, borderLeft: '6px solid #1976d2' }}>
  180. <TextField
  181. label="Produce Date"
  182. type="date"
  183. size="small"
  184. InputLabelProps={{ shrink: true }}
  185. value={searchDate}
  186. onChange={(e) => setSearchDate(e.target.value)}
  187. />
  188. <Button variant="contained" startIcon={<Search />} onClick={handleSearch}>Query</Button>
  189. </Paper>
  190. {/* Main Grid Table */}
  191. <TableContainer component={Paper}>
  192. <Table stickyHeader size="small">
  193. <TableHead>
  194. <TableRow sx={{ bgcolor: '#f5f5f5' }}>
  195. <TableCell align="center" sx={{ fontWeight: 'bold', width: 100 }}>Action</TableCell>
  196. <TableCell sx={{ fontWeight: 'bold' }}>ID</TableCell>
  197. <TableCell sx={{ fontWeight: 'bold' }}>Production Date</TableCell>
  198. <TableCell align="right" sx={{ fontWeight: 'bold' }}>Est. Prod Count</TableCell>
  199. <TableCell align="right" sx={{ fontWeight: 'bold' }}>Total FG Types</TableCell>
  200. </TableRow>
  201. </TableHead>
  202. <TableBody>
  203. {schedules.map((ps) => (
  204. <TableRow key={ps.id} hover>
  205. <TableCell align="center">
  206. <IconButton color="primary" size="small" onClick={() => handleViewDetail(ps)}>
  207. <Visibility fontSize="small" />
  208. </IconButton>
  209. </TableCell>
  210. <TableCell>#{ps.id}</TableCell>
  211. <TableCell>{formatBackendDate(ps.produceAt)}</TableCell>
  212. <TableCell align="right">{formatNum(ps.totalEstProdCount)}</TableCell>
  213. <TableCell align="right">{formatNum(ps.totalFGType)}</TableCell>
  214. </TableRow>
  215. ))}
  216. </TableBody>
  217. </Table>
  218. </TableContainer>
  219. {/* Detailed Lines Dialog */}
  220. <Dialog open={isDetailOpen} onClose={() => setIsDetailOpen(false)} maxWidth="lg" fullWidth>
  221. <DialogTitle sx={{ bgcolor: '#1976d2', color: 'white' }}>
  222. <Stack direction="row" alignItems="center" spacing={1}>
  223. <ListAlt />
  224. <Typography variant="h6">Schedule Details: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)})</Typography>
  225. </Stack>
  226. </DialogTitle>
  227. <DialogContent sx={{ p: 0 }}>
  228. <TableContainer sx={{ maxHeight: '65vh' }}>
  229. <Table size="small" stickyHeader>
  230. <TableHead>
  231. <TableRow>
  232. <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Job Order</TableCell>
  233. <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Item Code</TableCell>
  234. <TableCell sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Item Name</TableCell>
  235. <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Avg Last Month</TableCell>
  236. <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Stock</TableCell>
  237. <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Days Left</TableCell>
  238. <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Batch Need</TableCell>
  239. <TableCell align="right" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Prod Qty</TableCell>
  240. <TableCell align="center" sx={{ bgcolor: '#eee', fontWeight: 'bold' }}>Priority</TableCell>
  241. </TableRow>
  242. </TableHead>
  243. <TableBody>
  244. {selectedLines.map((line: any) => (
  245. <TableRow key={line.id} hover>
  246. <TableCell sx={{ color: 'primary.main', fontWeight: 'bold' }}>{line.joCode || '-'}</TableCell>
  247. <TableCell sx={{ fontWeight: 'bold' }}>{line.itemCode}</TableCell>
  248. <TableCell>{line.itemName}</TableCell>
  249. <TableCell align="right">{formatNum(line.avgQtyLastMonth)}</TableCell>
  250. <TableCell align="right">{formatNum(line.stockQty)}</TableCell>
  251. <TableCell align="right" sx={{ color: line.daysLeft < 5 ? 'error.main' : 'inherit', fontWeight: line.daysLeft < 5 ? 'bold' : 'normal' }}>
  252. {line.daysLeft}
  253. </TableCell>
  254. <TableCell align="right">{formatNum(line.batchNeed)}</TableCell>
  255. <TableCell align="right"><strong>{formatNum(line.prodQty)}</strong></TableCell>
  256. <TableCell align="center">{line.itemPriority}</TableCell>
  257. </TableRow>
  258. ))}
  259. </TableBody>
  260. </Table>
  261. </TableContainer>
  262. </DialogContent>
  263. {/* Footer Actions */}
  264. <DialogActions sx={{ p: 2, bgcolor: '#f9f9f9' }}>
  265. <Stack direction="row" spacing={2}>
  266. <Tooltip title={!isDateToday ? "Job Orders can only be generated for the current day's schedule." : ""}>
  267. <span>
  268. <Button
  269. variant="contained"
  270. color="primary"
  271. startIcon={isGenerating ? <CircularProgress size={20} color="inherit" /> : <SettingsEthernet />}
  272. onClick={handleAutoGenJob}
  273. disabled={isGenerating || !isDateToday}
  274. >
  275. Auto Gen Job
  276. </Button>
  277. </span>
  278. </Tooltip>
  279. <Button
  280. onClick={() => setIsDetailOpen(false)}
  281. variant="outlined"
  282. color="inherit"
  283. disabled={isGenerating}
  284. >
  285. Close
  286. </Button>
  287. </Stack>
  288. </DialogActions>
  289. </Dialog>
  290. </Box>
  291. );
  292. }