FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

FGPickOrderCard.tsx 13 KiB

6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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;