FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

366 lignes
12 KiB

  1. "use client";
  2. import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } 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. // Convert selectedDate to the format needed
  26. let dateParam: string | undefined;
  27. if (selectedDate === "today") {
  28. dateParam = dayjs().format('YYYY-MM-DD');
  29. } else if (selectedDate === "tomorrow") {
  30. dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
  31. } else if (selectedDate === "dayAfterTomorrow") {
  32. dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
  33. }
  34. const [s2, s4] = await Promise.all([
  35. fetchStoreLaneSummary("2/F", dateParam),
  36. fetchStoreLaneSummary("4/F", dateParam)
  37. ]);
  38. setSummary2F(s2);
  39. setSummary4F(s4);
  40. } catch (error) {
  41. console.error("Error loading summaries:", error);
  42. } finally {
  43. setIsLoadingSummary(false);
  44. }
  45. }, [selectedDate]);
  46. // 初始化
  47. useEffect(() => {
  48. loadSummaries();
  49. }, [loadSummaries]);
  50. const handleAssignByLane = useCallback(async (
  51. storeId: string,
  52. truckDepartureTime: string,
  53. truckLanceCode: string
  54. ) => {
  55. if (!currentUserId) {
  56. console.error("Missing user id in session");
  57. return;
  58. }
  59. setIsAssigning(true);
  60. try {
  61. const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
  62. if (res.code === "SUCCESS") {
  63. console.log("✅ Successfully assigned pick order from lane", truckLanceCode);
  64. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  65. loadSummaries(); // 刷新按钮状态
  66. onPickOrderAssigned?.();
  67. } else if (res.code === "USER_BUSY") {
  68. Swal.fire({
  69. icon: "warning",
  70. title: t("Warning"),
  71. text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
  72. confirmButtonText: t("Confirm"),
  73. confirmButtonColor: "#8dba00"
  74. });
  75. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  76. } else if (res.code === "NO_ORDERS") {
  77. Swal.fire({
  78. icon: "info",
  79. title: t("Info"),
  80. text: t("No available pick order(s) for this lane."),
  81. confirmButtonText: t("Confirm"),
  82. confirmButtonColor: "#8dba00"
  83. });
  84. } else {
  85. console.log("ℹ️ Assignment result:", res.message);
  86. }
  87. } catch (error) {
  88. console.error("❌ Error assigning by lane:", error);
  89. Swal.fire({
  90. icon: "error",
  91. title: t("Error"),
  92. text: t("Error occurred during assignment."),
  93. confirmButtonText: t("Confirm"),
  94. confirmButtonColor: "#8dba00"
  95. });
  96. } finally {
  97. setIsAssigning(false);
  98. }
  99. }, [currentUserId, t, selectedDate, onPickOrderAssigned]);
  100. const getDateLabel = (offset: number) => {
  101. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  102. };
  103. // Flatten rows to create one box per lane
  104. const flattenRows = (rows: any[]) => {
  105. const flattened: any[] = [];
  106. rows.forEach(row => {
  107. row.lanes.forEach((lane: any) => {
  108. flattened.push({
  109. truckDepartureTime: row.truckDepartureTime,
  110. lane: lane
  111. });
  112. });
  113. });
  114. return flattened;
  115. };
  116. return (
  117. <Box sx={{ mb: 2 }}>
  118. {/* Date Selector Dropdown */}
  119. <Box sx={{ maxWidth: 300, mb: 2 }}>
  120. <FormControl fullWidth size="small">
  121. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  122. <Select
  123. labelId="date-select-label"
  124. id="date-select"
  125. value={selectedDate}
  126. label={t("Select Date")}
  127. onChange={(e) => { {
  128. setSelectedDate(e.target.value);
  129. loadSummaries();
  130. }}}
  131. >
  132. <MenuItem value="today">
  133. {t("Today")} ({getDateLabel(0)})
  134. </MenuItem>
  135. <MenuItem value="tomorrow">
  136. {t("Tomorrow")} ({getDateLabel(1)})
  137. </MenuItem>
  138. <MenuItem value="dayAfterTomorrow">
  139. {t("Day After Tomorrow")} ({getDateLabel(2)})
  140. </MenuItem>
  141. </Select>
  142. </FormControl>
  143. </Box>
  144. {/* Grid containing both floors */}
  145. <Grid container spacing={2}>
  146. {/* 2/F 楼层面板 */}
  147. <Grid item xs={12}>
  148. <Stack direction="row" spacing={2} alignItems="flex-start">
  149. {/* Floor Label */}
  150. <Typography
  151. variant="h6"
  152. sx={{
  153. fontWeight: 600,
  154. minWidth: 60,
  155. pt: 1
  156. }}
  157. >
  158. 2/F
  159. </Typography>
  160. {/* Content Box */}
  161. <Box
  162. sx={{
  163. border: '1px solid #e0e0e0',
  164. borderRadius: 1,
  165. p: 1,
  166. backgroundColor: '#fafafa',
  167. flex: 1
  168. }}
  169. >
  170. {isLoadingSummary ? (
  171. <Typography variant="caption">Loading...</Typography>
  172. ) : !summary2F?.rows || summary2F.rows.length === 0 ? (
  173. <Typography
  174. variant="body2"
  175. color="text.secondary"
  176. sx={{
  177. fontWeight: 600,
  178. fontSize: '1rem',
  179. textAlign: 'center',
  180. py: 1
  181. }}
  182. >
  183. {t("No entries available")}
  184. </Typography>
  185. ) : (
  186. <Grid container spacing={1}>
  187. {flattenRows(summary2F.rows).map((item, idx) => (
  188. <Grid item xs={12} sm={6} md={3} key={idx}>
  189. <Stack
  190. direction="row"
  191. spacing={1}
  192. alignItems="center"
  193. sx={{
  194. border: '1px solid #e0e0e0',
  195. borderRadius: 0.5,
  196. p: 1,
  197. backgroundColor: '#fff',
  198. height: '100%'
  199. }}
  200. >
  201. {/* Time on the left */}
  202. <Typography
  203. variant="body2"
  204. sx={{
  205. fontWeight: 600,
  206. fontSize: '1rem',
  207. minWidth: 50,
  208. whiteSpace: 'nowrap'
  209. }}
  210. >
  211. {item.truckDepartureTime}
  212. </Typography>
  213. {/* Single Button on the right */}
  214. <Button
  215. variant="outlined"
  216. size="medium"
  217. disabled={item.lane.unassigned === 0 || isAssigning}
  218. onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)}
  219. sx={{
  220. flex: 1,
  221. fontSize: '1.1rem',
  222. py: 1,
  223. px: 1.5,
  224. borderWidth: 1,
  225. borderColor: '#ccc',
  226. fontWeight: 500,
  227. '&:hover': {
  228. borderColor: '#999',
  229. backgroundColor: '#f5f5f5'
  230. }
  231. }}
  232. >
  233. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  234. </Button>
  235. </Stack>
  236. </Grid>
  237. ))}
  238. </Grid>
  239. )}
  240. </Box>
  241. </Stack>
  242. </Grid>
  243. {/* 4/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. 4/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">Loading...</Typography>
  269. ) : !summary4F?.rows || summary4F.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(summary4F.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={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)}
  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. </Grid>
  341. </Box>
  342. );
  343. };
  344. export default FinishedGoodFloorLanePanel;