FPSMS-frontend
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

379 linhas
13 KiB

  1. "use client";
  2. import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel, Card, CardContent } 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. }
  13. const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => {
  14. const { t } = useTranslation("pickOrder");
  15. const { data: session } = useSession() as { data: SessionWithTokens | null };
  16. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  17. const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
  18. const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
  19. const [isLoadingSummary, setIsLoadingSummary] = useState(false);
  20. const [isAssigning, setIsAssigning] = useState(false);
  21. const [selectedDate, setSelectedDate] = useState<string>("today");
  22. const loadSummaries = useCallback(async () => {
  23. setIsLoadingSummary(true);
  24. try {
  25. const [s2, s4] = await Promise.all([
  26. fetchStoreLaneSummary("2/F"),
  27. fetchStoreLaneSummary("4/F")
  28. ]);
  29. setSummary2F(s2);
  30. setSummary4F(s4);
  31. } catch (error) {
  32. console.error("Error loading summaries:", error);
  33. } finally {
  34. setIsLoadingSummary(false);
  35. }
  36. }, []);
  37. useEffect(() => {
  38. loadSummaries();
  39. }, [loadSummaries]);
  40. const handleAssignByLane = useCallback(async (
  41. storeId: string,
  42. truckDepartureTime: string,
  43. truckLanceCode: string,
  44. requiredDate: string
  45. ) => {
  46. if (!currentUserId) {
  47. console.error("Missing user id in session");
  48. return;
  49. }
  50. setIsAssigning(true);
  51. try {
  52. const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, requiredDate);
  53. if (res.code === "SUCCESS") {
  54. console.log(" Successfully assigned pick order from lane", truckLanceCode);
  55. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  56. loadSummaries(); // 刷新按钮状态
  57. onPickOrderAssigned?.();
  58. } else if (res.code === "USER_BUSY") {
  59. Swal.fire({
  60. icon: "warning",
  61. title: t("Warning"),
  62. text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
  63. confirmButtonText: t("Confirm"),
  64. confirmButtonColor: "#8dba00"
  65. });
  66. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  67. } else if (res.code === "NO_ORDERS") {
  68. Swal.fire({
  69. icon: "info",
  70. title: t("Info"),
  71. text: t("No available pick order(s) for this lane."),
  72. confirmButtonText: t("Confirm"),
  73. confirmButtonColor: "#8dba00"
  74. });
  75. } else {
  76. console.log("ℹ️ Assignment result:", res.message);
  77. }
  78. } catch (error) {
  79. console.error("❌ Error assigning by lane:", error);
  80. Swal.fire({
  81. icon: "error",
  82. title: t("Error"),
  83. text: t("Error occurred during assignment."),
  84. confirmButtonText: t("Confirm"),
  85. confirmButtonColor: "#8dba00"
  86. });
  87. } finally {
  88. setIsAssigning(false);
  89. }
  90. }, [currentUserId, t, loadSummaries, onPickOrderAssigned]);
  91. const getDateLabel = (offset: number) => {
  92. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  93. };
  94. // Flatten rows to create one box per lane
  95. const flattenRows = (rows: any[]) => {
  96. const flattened: any[] = [];
  97. rows.forEach(row => {
  98. row.lanes.forEach((lane: any) => {
  99. flattened.push({
  100. truckDepartureTime: row.truckDepartureTime,
  101. lane: lane
  102. });
  103. });
  104. });
  105. return flattened;
  106. };
  107. return (
  108. <Card sx={{ mb: 2 }}>
  109. <CardContent>
  110. {/* Date Selector Dropdown */}
  111. <Box sx={{ maxWidth: 300, mb: 2 }}>
  112. <FormControl fullWidth size="small">
  113. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  114. <Select
  115. labelId="date-select-label"
  116. id="date-select"
  117. value={selectedDate}
  118. label={t("Select Date")}
  119. onChange={(e) => setSelectedDate(e.target.value)}
  120. >
  121. <MenuItem value="today">
  122. {t("Today")} ({getDateLabel(0)})
  123. </MenuItem>
  124. <MenuItem value="tomorrow">
  125. {t("Tomorrow")} ({getDateLabel(1)})
  126. </MenuItem>
  127. <MenuItem value="dayAfterTomorrow">
  128. {t("Day After Tomorrow")} ({getDateLabel(2)})
  129. </MenuItem>
  130. </Select>
  131. </FormControl>
  132. </Box>
  133. {/* Grid containing both floors */}
  134. <Grid container spacing={2}>
  135. {/* 2/F 楼层面板 */}
  136. <Grid item xs={12}>
  137. <Stack direction="row" spacing={2} alignItems="flex-start">
  138. {/* Floor Label */}
  139. <Box
  140. sx={{
  141. border: '2px solid #1976d2',
  142. borderRadius: 1,
  143. backgroundColor: '#e3f2fd',
  144. px: 2,
  145. py: 1,
  146. minWidth: 80,
  147. display: 'flex',
  148. alignItems: 'center',
  149. justifyContent: 'center'
  150. }}
  151. >
  152. <Typography
  153. variant="h6"
  154. sx={{
  155. fontWeight: 600,
  156. color: '#1976d2'
  157. }}
  158. >
  159. 2/F
  160. </Typography>
  161. </Box>
  162. {/* Content Box */}
  163. <Box
  164. sx={{
  165. border: '1px solid #e0e0e0',
  166. borderRadius: 1,
  167. p: 1,
  168. backgroundColor: '#fafafa',
  169. flex: 1
  170. }}
  171. >
  172. {isLoadingSummary ? (
  173. <Typography variant="caption">Loading...</Typography>
  174. ) : !summary2F?.rows || summary2F.rows.length === 0 ? (
  175. <Typography
  176. variant="body2"
  177. color="text.secondary"
  178. sx={{
  179. fontWeight: 600,
  180. fontSize: '1rem',
  181. textAlign: 'center',
  182. py: 1
  183. }}
  184. >
  185. {t("No entries available")}
  186. </Typography>
  187. ) : (
  188. <Grid container spacing={1}>
  189. {flattenRows(summary2F.rows).slice(0, 4).map((item, idx) => (
  190. <Grid item xs={12} sm={6} md={3} key={idx}>
  191. <Stack
  192. direction="row"
  193. spacing={1}
  194. alignItems="center"
  195. sx={{
  196. border: '1px solid #e0e0e0',
  197. borderRadius: 0.5,
  198. p: 1,
  199. backgroundColor: '#fff',
  200. height: '100%'
  201. }}
  202. >
  203. {/* Time on the left */}
  204. <Typography
  205. variant="body2"
  206. sx={{
  207. fontWeight: 600,
  208. fontSize: '1rem',
  209. minWidth: 50,
  210. whiteSpace: 'nowrap'
  211. }}
  212. >
  213. {item.truckDepartureTime}
  214. </Typography>
  215. {/* Single Button on the right */}
  216. <Button
  217. variant="outlined"
  218. size="medium"
  219. disabled={item.lane.unassigned === 0 || isAssigning}
  220. onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
  221. sx={{
  222. flex: 1,
  223. fontSize: '1.1rem',
  224. py: 1,
  225. px: 1.5,
  226. borderWidth: 1,
  227. borderColor: '#ccc',
  228. fontWeight: 500,
  229. '&:hover': {
  230. borderColor: '#999',
  231. backgroundColor: '#f5f5f5'
  232. }
  233. }}
  234. >
  235. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  236. </Button>
  237. </Stack>
  238. </Grid>
  239. ))}
  240. </Grid>
  241. )}
  242. </Box>
  243. </Stack>
  244. </Grid>
  245. {/* 4/F 楼层面板 */}
  246. <Grid item xs={12}>
  247. <Stack direction="row" spacing={2} alignItems="flex-start">
  248. {/* Floor Label */}
  249. <Box
  250. sx={{
  251. border: '2px solid #1976d2',
  252. borderRadius: 1,
  253. backgroundColor: '#e3f2fd',
  254. px: 2,
  255. py: 1,
  256. minWidth: 80,
  257. display: 'flex',
  258. alignItems: 'center',
  259. justifyContent: 'center'
  260. }}
  261. >
  262. <Typography
  263. variant="h6"
  264. sx={{
  265. fontWeight: 600,
  266. color: '#1976d2'
  267. }}
  268. >
  269. 4/F
  270. </Typography>
  271. </Box>
  272. {/* Content Box */}
  273. <Box
  274. sx={{
  275. border: '1px solid #e0e0e0',
  276. borderRadius: 1,
  277. p: 1,
  278. backgroundColor: '#fafafa',
  279. flex: 1
  280. }}
  281. >
  282. {isLoadingSummary ? (
  283. <Typography variant="caption">Loading...</Typography>
  284. ) : !summary4F?.rows || summary4F.rows.length === 0 ? (
  285. <Typography
  286. variant="body2"
  287. color="text.secondary"
  288. sx={{
  289. fontWeight: 600,
  290. fontSize: '1rem',
  291. textAlign: 'center',
  292. py: 1
  293. }}
  294. >
  295. {t("No entries available")}
  296. </Typography>
  297. ) : (
  298. <Grid container spacing={1}>
  299. {flattenRows(summary4F.rows).slice(0, 4).map((item, idx) => (
  300. <Grid item xs={12} sm={6} md={3} key={idx}>
  301. <Stack
  302. direction="row"
  303. spacing={1}
  304. alignItems="center"
  305. sx={{
  306. border: '1px solid #e0e0e0',
  307. borderRadius: 0.5,
  308. p: 1,
  309. backgroundColor: '#fff',
  310. height: '100%'
  311. }}
  312. >
  313. {/* Time on the left */}
  314. <Typography
  315. variant="body2"
  316. sx={{
  317. fontWeight: 600,
  318. fontSize: '1rem',
  319. minWidth: 50,
  320. whiteSpace: 'nowrap'
  321. }}
  322. >
  323. {item.truckDepartureTime}
  324. </Typography>
  325. {/* Single Button on the right */}
  326. <Button
  327. variant="outlined"
  328. size="medium"
  329. disabled={item.lane.unassigned === 0 || isAssigning}
  330. onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
  331. sx={{
  332. flex: 1,
  333. fontSize: '1.1rem',
  334. py: 1,
  335. px: 1.5,
  336. borderWidth: 1,
  337. borderColor: '#ccc',
  338. fontWeight: 500,
  339. '&:hover': {
  340. borderColor: '#999',
  341. backgroundColor: '#f5f5f5'
  342. }
  343. }}
  344. >
  345. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  346. </Button>
  347. </Stack>
  348. </Grid>
  349. ))}
  350. </Grid>
  351. )}
  352. </Box>
  353. </Stack>
  354. </Grid>
  355. </Grid>
  356. </CardContent>
  357. </Card>
  358. );
  359. };
  360. export default FinishedGoodFloorLanePanel;