FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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