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.
 
 

378 line
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. ) => {
  45. if (!currentUserId) {
  46. console.error("Missing user id in session");
  47. return;
  48. }
  49. setIsAssigning(true);
  50. try {
  51. const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
  52. if (res.code === "SUCCESS") {
  53. console.log("✅ Successfully assigned pick order from lane", truckLanceCode);
  54. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  55. loadSummaries(); // 刷新按钮状态
  56. onPickOrderAssigned?.();
  57. } else if (res.code === "USER_BUSY") {
  58. Swal.fire({
  59. icon: "warning",
  60. title: t("Warning"),
  61. text: t("You already have a pick order in progess. Please complete it first before taking next pick order."),
  62. confirmButtonText: t("Confirm"),
  63. confirmButtonColor: "#8dba00"
  64. });
  65. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  66. } else if (res.code === "NO_ORDERS") {
  67. Swal.fire({
  68. icon: "info",
  69. title: t("Info"),
  70. text: t("No available pick order(s) for this lane."),
  71. confirmButtonText: t("Confirm"),
  72. confirmButtonColor: "#8dba00"
  73. });
  74. } else {
  75. console.log("ℹ️ Assignment result:", res.message);
  76. }
  77. } catch (error) {
  78. console.error("❌ Error assigning by lane:", error);
  79. Swal.fire({
  80. icon: "error",
  81. title: t("Error"),
  82. text: t("Error occurred during assignment."),
  83. confirmButtonText: t("Confirm"),
  84. confirmButtonColor: "#8dba00"
  85. });
  86. } finally {
  87. setIsAssigning(false);
  88. }
  89. }, [currentUserId, t, loadSummaries, onPickOrderAssigned]);
  90. const getDateLabel = (offset: number) => {
  91. return dayjs().add(offset, 'day').format('YYYY-MM-DD');
  92. };
  93. // Flatten rows to create one box per lane
  94. const flattenRows = (rows: any[]) => {
  95. const flattened: any[] = [];
  96. rows.forEach(row => {
  97. row.lanes.forEach((lane: any) => {
  98. flattened.push({
  99. truckDepartureTime: row.truckDepartureTime,
  100. lane: lane
  101. });
  102. });
  103. });
  104. return flattened;
  105. };
  106. return (
  107. <Card sx={{ mb: 2 }}>
  108. <CardContent>
  109. {/* Date Selector Dropdown */}
  110. <Box sx={{ maxWidth: 300, mb: 2 }}>
  111. <FormControl fullWidth size="small">
  112. <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
  113. <Select
  114. labelId="date-select-label"
  115. id="date-select"
  116. value={selectedDate}
  117. label={t("Select Date")}
  118. onChange={(e) => setSelectedDate(e.target.value)}
  119. >
  120. <MenuItem value="today">
  121. {t("Today")} ({getDateLabel(0)})
  122. </MenuItem>
  123. <MenuItem value="tomorrow">
  124. {t("Tomorrow")} ({getDateLabel(1)})
  125. </MenuItem>
  126. <MenuItem value="dayAfterTomorrow">
  127. {t("Day After Tomorrow")} ({getDateLabel(2)})
  128. </MenuItem>
  129. </Select>
  130. </FormControl>
  131. </Box>
  132. {/* Grid containing both floors */}
  133. <Grid container spacing={2}>
  134. {/* 2/F 楼层面板 */}
  135. <Grid item xs={12}>
  136. <Stack direction="row" spacing={2} alignItems="flex-start">
  137. {/* Floor Label */}
  138. <Box
  139. sx={{
  140. border: '2px solid #1976d2',
  141. borderRadius: 1,
  142. backgroundColor: '#e3f2fd',
  143. px: 2,
  144. py: 1,
  145. minWidth: 80,
  146. display: 'flex',
  147. alignItems: 'center',
  148. justifyContent: 'center'
  149. }}
  150. >
  151. <Typography
  152. variant="h6"
  153. sx={{
  154. fontWeight: 600,
  155. color: '#1976d2'
  156. }}
  157. >
  158. 2/F
  159. </Typography>
  160. </Box>
  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).slice(0, 4).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. <Box
  249. sx={{
  250. border: '2px solid #1976d2',
  251. borderRadius: 1,
  252. backgroundColor: '#e3f2fd',
  253. px: 2,
  254. py: 1,
  255. minWidth: 80,
  256. display: 'flex',
  257. alignItems: 'center',
  258. justifyContent: 'center'
  259. }}
  260. >
  261. <Typography
  262. variant="h6"
  263. sx={{
  264. fontWeight: 600,
  265. color: '#1976d2'
  266. }}
  267. >
  268. 4/F
  269. </Typography>
  270. </Box>
  271. {/* Content Box */}
  272. <Box
  273. sx={{
  274. border: '1px solid #e0e0e0',
  275. borderRadius: 1,
  276. p: 1,
  277. backgroundColor: '#fafafa',
  278. flex: 1
  279. }}
  280. >
  281. {isLoadingSummary ? (
  282. <Typography variant="caption">Loading...</Typography>
  283. ) : !summary4F?.rows || summary4F.rows.length === 0 ? (
  284. <Typography
  285. variant="body2"
  286. color="text.secondary"
  287. sx={{
  288. fontWeight: 600,
  289. fontSize: '1rem',
  290. textAlign: 'center',
  291. py: 1
  292. }}
  293. >
  294. {t("No entries available")}
  295. </Typography>
  296. ) : (
  297. <Grid container spacing={1}>
  298. {flattenRows(summary4F.rows).slice(0, 4).map((item, idx) => (
  299. <Grid item xs={12} sm={6} md={3} key={idx}>
  300. <Stack
  301. direction="row"
  302. spacing={1}
  303. alignItems="center"
  304. sx={{
  305. border: '1px solid #e0e0e0',
  306. borderRadius: 0.5,
  307. p: 1,
  308. backgroundColor: '#fff',
  309. height: '100%'
  310. }}
  311. >
  312. {/* Time on the left */}
  313. <Typography
  314. variant="body2"
  315. sx={{
  316. fontWeight: 600,
  317. fontSize: '1rem',
  318. minWidth: 50,
  319. whiteSpace: 'nowrap'
  320. }}
  321. >
  322. {item.truckDepartureTime}
  323. </Typography>
  324. {/* Single Button on the right */}
  325. <Button
  326. variant="outlined"
  327. size="medium"
  328. disabled={item.lane.unassigned === 0 || isAssigning}
  329. onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)}
  330. sx={{
  331. flex: 1,
  332. fontSize: '1.1rem',
  333. py: 1,
  334. px: 1.5,
  335. borderWidth: 1,
  336. borderColor: '#ccc',
  337. fontWeight: 500,
  338. '&:hover': {
  339. borderColor: '#999',
  340. backgroundColor: '#f5f5f5'
  341. }
  342. }}
  343. >
  344. {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
  345. </Button>
  346. </Stack>
  347. </Grid>
  348. ))}
  349. </Grid>
  350. )}
  351. </Box>
  352. </Stack>
  353. </Grid>
  354. </Grid>
  355. </CardContent>
  356. </Card>
  357. );
  358. };
  359. export default FinishedGoodFloorLanePanel;