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ů.
 
 

481 řádky
16 KiB

  1. "use client";
  2. import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material";
  3. import { useCallback, useEffect, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { useSession } from "next-auth/react";
  6. import { SessionWithTokens } from "@/config/authConfig";
  7. import { fetchStoreLaneSummary, assignByLane, type StoreLaneSummary } from "@/app/api/pickOrder/actions";
  8. import Swal from "sweetalert2";
  9. import dayjs from "dayjs";
  10. interface Props {
  11. onPickOrderAssigned?: () => void;
  12. onSwitchToDetailTab?: () => void;
  13. }
  14. const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitchToDetailTab }) => {
  15. const { t } = useTranslation("pickOrder");
  16. const { data: session } = useSession() as { data: SessionWithTokens | null };
  17. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  18. const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
  19. const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
  20. const [isLoadingSummary, setIsLoadingSummary] = useState(false);
  21. const [isAssigning, setIsAssigning] = useState(false);
  22. //const [selectedDate, setSelectedDate] = useState<string>("today");
  23. const [selectedDate, setSelectedDate] = useState<string>("today");
  24. const [releaseType, setReleaseType] = useState<string>("batch");
  25. const loadSummaries = useCallback(async () => {
  26. setIsLoadingSummary(true);
  27. try {
  28. // Convert selectedDate to the format needed
  29. let dateParam: string | undefined;
  30. if (selectedDate === "today") {
  31. dateParam = dayjs().format('YYYY-MM-DD');
  32. } else if (selectedDate === "tomorrow") {
  33. dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
  34. } else if (selectedDate === "dayAfterTomorrow") {
  35. dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
  36. }
  37. const [s2, s4] = await Promise.all([
  38. fetchStoreLaneSummary("2/F", dateParam, releaseType),
  39. fetchStoreLaneSummary("4/F", dateParam, releaseType)
  40. ]);
  41. setSummary2F(s2);
  42. setSummary4F(s4);
  43. } catch (error) {
  44. console.error("Error loading summaries:", error);
  45. } finally {
  46. setIsLoadingSummary(false);
  47. }
  48. }, [selectedDate, releaseType]);
  49. // 初始化
  50. useEffect(() => {
  51. loadSummaries();
  52. }, [loadSummaries]);
  53. const handleAssignByLane = useCallback(async (
  54. storeId: string,
  55. truckDepartureTime: string,
  56. truckLanceCode: string,
  57. requiredDate: string
  58. ) => {
  59. if (!currentUserId) {
  60. console.error("Missing user id in session");
  61. return;
  62. }
  63. let dateParam: string | undefined;
  64. if (requiredDate === "today") {
  65. dateParam = dayjs().format('YYYY-MM-DD');
  66. } else if (requiredDate === "tomorrow") {
  67. dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
  68. } else if (requiredDate === "dayAfterTomorrow") {
  69. dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
  70. }
  71. setIsAssigning(true);
  72. try {
  73. const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam);
  74. if (res.code === "SUCCESS") {
  75. console.log(" Successfully assigned pick order from lane", truckLanceCode);
  76. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  77. loadSummaries(); // 刷新按钮状态
  78. onPickOrderAssigned?.();
  79. onSwitchToDetailTab?.();
  80. } else if (res.code === "USER_BUSY") {
  81. Swal.fire({
  82. icon: "warning",
  83. title: t("Warning"),
  84. text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
  85. confirmButtonText: t("Confirm"),
  86. confirmButtonColor: "#8dba00"
  87. });
  88. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  89. } else if (res.code === "NO_ORDERS") {
  90. Swal.fire({
  91. icon: "info",
  92. title: t("Info"),
  93. text: t("No available pick order(s) for this lane."),
  94. confirmButtonText: t("Confirm"),
  95. confirmButtonColor: "#8dba00"
  96. });
  97. } else {
  98. console.log("ℹ️ Assignment result:", res.message);
  99. }
  100. } catch (error) {
  101. console.error("❌ Error assigning by lane:", error);
  102. Swal.fire({
  103. icon: "error",
  104. title: t("Error"),
  105. text: t("Error occurred during assignment."),
  106. confirmButtonText: t("Confirm"),
  107. confirmButtonColor: "#8dba00"
  108. });
  109. } finally {
  110. setIsAssigning(false);
  111. }
  112. }, [currentUserId, t, selectedDate, onPickOrderAssigned, onSwitchToDetailTab, loadSummaries]);
  113. const handleLaneButtonClick = useCallback(async (
  114. storeId: string,
  115. truckDepartureTime: string,
  116. truckLanceCode: string,
  117. requiredDate: string,
  118. unassigned: number,
  119. total: number
  120. ) => {
  121. // Format the date for display
  122. let dateDisplay: string;
  123. if (requiredDate === "today") {
  124. dateDisplay = dayjs().format('YYYY-MM-DD');
  125. } else if (requiredDate === "tomorrow") {
  126. dateDisplay = dayjs().add(1, 'day').format('YYYY-MM-DD');
  127. } else if (requiredDate === "dayAfterTomorrow") {
  128. dateDisplay = dayjs().add(2, 'day').format('YYYY-MM-DD');
  129. } else {
  130. dateDisplay = requiredDate;
  131. }
  132. // Show confirmation dialog
  133. const result = await Swal.fire({
  134. title: t("Confirm Assignment"),
  135. html: `
  136. <div style="text-align: left; padding: 10px 0;">
  137. <p><strong>${t("Store")}:</strong> ${storeId}</p>
  138. <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p>
  139. <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p>
  140. <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p>
  141. <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p>
  142. </div>
  143. `,
  144. icon: "question",
  145. showCancelButton: true,
  146. confirmButtonText: t("Confirm"),
  147. cancelButtonText: t("Cancel"),
  148. confirmButtonColor: "#8dba00",
  149. cancelButtonColor: "#F04438",
  150. reverseButtons: true
  151. });
  152. // Only proceed if user confirmed
  153. if (result.isConfirmed) {
  154. await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, requiredDate);
  155. }
  156. }, [handleAssignByLane, t]);
  157. const getDateLabel = (offset: number) => {
  158. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  159. };
  160. // Flatten rows to create one box per lane
  161. const flattenRows = (rows: any[]) => {
  162. const flattened: any[] = [];
  163. rows.forEach(row => {
  164. row.lanes.forEach((lane: any) => {
  165. flattened.push({
  166. truckDepartureTime: row.truckDepartureTime,
  167. lane: lane
  168. });
  169. });
  170. });
  171. return flattened;
  172. };
  173. return (
  174. <Box sx={{ mb: 2 }}>
  175. {/* Date Selector Dropdown and Legend */}
  176. <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>
  177. <Box sx={{ maxWidth: 300 }}>
  178. <FormControl fullWidth size="small">
  179. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  180. <Select
  181. labelId="date-select-label"
  182. id="date-select"
  183. value={selectedDate}
  184. label={t("Select Date")}
  185. onChange={(e) => { {
  186. setSelectedDate(e.target.value);
  187. loadSummaries();
  188. }}}
  189. >
  190. <MenuItem value="today">
  191. {t("Today")} ({getDateLabel(0)})
  192. </MenuItem>
  193. <MenuItem value="tomorrow">
  194. {t("Tomorrow")} ({getDateLabel(1)})
  195. </MenuItem>
  196. <MenuItem value="dayAfterTomorrow">
  197. {t("Day After Tomorrow")} ({getDateLabel(2)})
  198. </MenuItem>
  199. </Select>
  200. </FormControl>
  201. </Box>
  202. <Box sx={{minWidth: 140, maxWidth: 300 }}>
  203. <FormControl fullWidth size="small">
  204. <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
  205. <Select
  206. labelId="release-type-select-label"
  207. id="release-type-select"
  208. value={releaseType}
  209. label={t("Release Type")}
  210. onChange={(e) => { {
  211. setReleaseType(e.target.value);
  212. loadSummaries();
  213. }}}
  214. >
  215. <MenuItem value="batch">
  216. {t("Batch")}
  217. </MenuItem>
  218. <MenuItem value="single">
  219. {t("Single")}
  220. </MenuItem>
  221. </Select>
  222. </FormControl>
  223. </Box>
  224. <Box
  225. sx={{
  226. p: 1,
  227. backgroundColor: '#fafafa',
  228. borderRadius: 1,
  229. border: '1px solid #e0e0e0',
  230. flex: 1,
  231. maxWidth: 400
  232. }}
  233. >
  234. <Typography variant="body2" sx={{ display: 'block', color: 'text.secondary', fontWeight: 600 }}>
  235. {t("EDT - Lane Code (Unassigned/Total)")}
  236. </Typography>
  237. </Box>
  238. </Stack>
  239. <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>
  240. </Stack>
  241. {/* Grid containing both floors */}
  242. <Grid container spacing={2}>
  243. {/* 2/F 楼层面板 */}
  244. <Grid item xs={12}>
  245. <Stack direction="row" spacing={2} alignItems="flex-start">
  246. {/* Floor Label */}
  247. <Typography
  248. variant="h6"
  249. sx={{
  250. fontWeight: 600,
  251. minWidth: 60,
  252. pt: 1
  253. }}
  254. >
  255. 2/F
  256. </Typography>
  257. {/* Content Box */}
  258. <Box
  259. sx={{
  260. border: '1px solid #e0e0e0',
  261. borderRadius: 1,
  262. p: 1,
  263. backgroundColor: '#fafafa',
  264. flex: 1
  265. }}
  266. >
  267. {isLoadingSummary ? (
  268. <Typography variant="caption"> {t("Loading...")}</Typography>
  269. ) : !summary2F?.rows || summary2F.rows.length === 0 ? (
  270. <Typography
  271. variant="body2"
  272. color="text.secondary"
  273. sx={{
  274. fontWeight: 600,
  275. fontSize: '1rem',
  276. textAlign: 'center',
  277. py: 1
  278. }}
  279. >
  280. {t("No entries available")}
  281. </Typography>
  282. ) : (
  283. <Grid container spacing={1}>
  284. {flattenRows(summary2F.rows).map((item, idx) => (
  285. <Grid item xs={12} sm={6} md={3} key={idx}>
  286. <Stack
  287. direction="row"
  288. spacing={1}
  289. alignItems="center"
  290. sx={{
  291. border: '1px solid #e0e0e0',
  292. borderRadius: 0.5,
  293. p: 1,
  294. backgroundColor: '#fff',
  295. height: '100%'
  296. }}
  297. >
  298. {/* Time on the left */}
  299. <Typography
  300. variant="body2"
  301. sx={{
  302. fontWeight: 600,
  303. fontSize: '1rem',
  304. minWidth: 50,
  305. whiteSpace: 'nowrap'
  306. }}
  307. >
  308. {item.truckDepartureTime}
  309. </Typography>
  310. {/* Single Button on the right */}
  311. <Button
  312. variant="outlined"
  313. size="medium"
  314. disabled={item.lane.unassigned === 0 || isAssigning}
  315. onClick={() => handleLaneButtonClick("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)}
  316. sx={{
  317. flex: 1,
  318. fontSize: '1.1rem',
  319. py: 1,
  320. px: 1.5,
  321. borderWidth: 1,
  322. borderColor: '#ccc',
  323. fontWeight: 500,
  324. '&:hover': {
  325. borderColor: '#999',
  326. backgroundColor: '#f5f5f5'
  327. }
  328. }}
  329. >
  330. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  331. </Button>
  332. </Stack>
  333. </Grid>
  334. ))}
  335. </Grid>
  336. )}
  337. </Box>
  338. </Stack>
  339. </Grid>
  340. {/* 4/F 楼层面板 */}
  341. <Grid item xs={12}>
  342. <Stack direction="row" spacing={2} alignItems="flex-start">
  343. {/* Floor Label */}
  344. <Typography
  345. variant="h6"
  346. sx={{
  347. fontWeight: 600,
  348. minWidth: 60,
  349. pt: 1
  350. }}
  351. >
  352. 4/F
  353. </Typography>
  354. {/* Content Box */}
  355. <Box
  356. sx={{
  357. border: '1px solid #e0e0e0',
  358. borderRadius: 1,
  359. p: 1,
  360. backgroundColor: '#fafafa',
  361. flex: 1
  362. }}
  363. >
  364. {isLoadingSummary ? (
  365. <Typography variant="caption">{t("Loading...")}</Typography>
  366. ) : !summary4F?.rows || summary4F.rows.length === 0 ? (
  367. <Typography
  368. variant="body2"
  369. color="text.secondary"
  370. sx={{
  371. fontWeight: 600,
  372. fontSize: '1rem',
  373. textAlign: 'center',
  374. py: 1
  375. }}
  376. >
  377. {t("No entries available")}
  378. </Typography>
  379. ) : (
  380. <Grid container spacing={1}>
  381. {flattenRows(summary4F.rows).map((item, idx) => (
  382. <Grid item xs={12} sm={6} md={3} key={idx}>
  383. <Stack
  384. direction="row"
  385. spacing={1}
  386. alignItems="center"
  387. sx={{
  388. border: '1px solid #e0e0e0',
  389. borderRadius: 0.5,
  390. p: 1,
  391. backgroundColor: '#fff',
  392. height: '100%'
  393. }}
  394. >
  395. {/* Time on the left */}
  396. <Typography
  397. variant="body2"
  398. sx={{
  399. fontWeight: 600,
  400. fontSize: '1rem',
  401. minWidth: 50,
  402. whiteSpace: 'nowrap'
  403. }}
  404. >
  405. {item.truckDepartureTime}
  406. </Typography>
  407. {/* Single Button on the right */}
  408. <Button
  409. variant="outlined"
  410. size="medium"
  411. disabled={item.lane.unassigned === 0 || isAssigning}
  412. onClick={() => handleLaneButtonClick("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)}
  413. sx={{
  414. flex: 1,
  415. fontSize: '1.1rem',
  416. py: 1,
  417. px: 1.5,
  418. borderWidth: 1,
  419. borderColor: '#ccc',
  420. fontWeight: 500,
  421. '&:hover': {
  422. borderColor: '#999',
  423. backgroundColor: '#f5f5f5'
  424. }
  425. }}
  426. >
  427. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  428. </Button>
  429. </Stack>
  430. </Grid>
  431. ))}
  432. </Grid>
  433. )}
  434. </Box>
  435. </Stack>
  436. </Grid>
  437. </Grid>
  438. </Box>
  439. );
  440. };
  441. export default FinishedGoodFloorLanePanel;