FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

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