| @@ -1,115 +1,378 @@ | |||||
| "use client"; | "use client"; | ||||
| import { FGPickOrderResponse } from "@/app/api/pickOrder/actions"; | |||||
| import { Box, Card, CardContent, Grid, Stack, TextField, Button } from "@mui/material"; | |||||
| import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel, Card, CardContent } from "@mui/material"; | |||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | |||||
| import { fetchStoreLaneSummary, assignByLane, type StoreLaneSummary } from "@/app/api/pickOrder/actions"; | |||||
| import Swal from "sweetalert2"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import FinishedGoodFloorLanePanel from "./FinishedGoodFloorLanePanel"; | |||||
| type Props = { | |||||
| fgOrder: FGPickOrderResponse; | |||||
| onQrCodeClick: (pickOrderId: number) => void; | |||||
| interface Props { | |||||
| onPickOrderAssigned?: () => void; | onPickOrderAssigned?: () => void; | ||||
| }; | |||||
| } | |||||
| const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick, onPickOrderAssigned }) => { | |||||
| const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => { | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||||
| return ( | |||||
| <> | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <FinishedGoodFloorLanePanel onPickOrderAssigned={onPickOrderAssigned} /> | |||||
| </Box> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Delivery Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.deliveryNo} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Pick Order Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.pickOrderCode} | |||||
| //helperText={fgOrder.pickOrderConsoCode} | |||||
| /> | |||||
| </Grid> | |||||
| const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null); | |||||
| const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null); | |||||
| const [isLoadingSummary, setIsLoadingSummary] = useState(false); | |||||
| const [isAssigning, setIsAssigning] = useState(false); | |||||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Store ID")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.storeId} | |||||
| /> | |||||
| </Grid> | |||||
| const loadSummaries = useCallback(async () => { | |||||
| setIsLoadingSummary(true); | |||||
| try { | |||||
| const [s2, s4] = await Promise.all([ | |||||
| fetchStoreLaneSummary("2/F"), | |||||
| fetchStoreLaneSummary("4/F") | |||||
| ]); | |||||
| setSummary2F(s2); | |||||
| setSummary4F(s4); | |||||
| } catch (error) { | |||||
| console.error("Error loading summaries:", error); | |||||
| } finally { | |||||
| setIsLoadingSummary(false); | |||||
| } | |||||
| }, []); | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Shop Name")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopName} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Delivery Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={dayjs(fgOrder.deliveryDate).format(OUTPUT_DATE_FORMAT)} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("Shop Address")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.shopAddress} | |||||
| multiline | |||||
| rows={2} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Departure Time")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.DepartureTime} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Truck No.")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.truckLanceCode} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Ticket No.")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={fgOrder.ticketNo} | |||||
| /> | |||||
| </Grid> | |||||
| useEffect(() => { | |||||
| loadSummaries(); | |||||
| }, [loadSummaries]); | |||||
| </Grid> | |||||
| const handleAssignByLane = useCallback(async ( | |||||
| storeId: string, | |||||
| truckDepartureTime: string, | |||||
| truckLanceCode: string | |||||
| ) => { | |||||
| if (!currentUserId) { | |||||
| console.error("Missing user id in session"); | |||||
| return; | |||||
| } | |||||
| setIsAssigning(true); | |||||
| try { | |||||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime); | |||||
| if (res.code === "SUCCESS") { | |||||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | |||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||||
| loadSummaries(); // 刷新按钮状态 | |||||
| onPickOrderAssigned?.(); | |||||
| } else if (res.code === "USER_BUSY") { | |||||
| Swal.fire({ | |||||
| icon: "warning", | |||||
| title: t("Warning"), | |||||
| text: t("You already have a pick order in progess. Please complete it first before taking next pick order."), | |||||
| confirmButtonText: t("Confirm"), | |||||
| confirmButtonColor: "#8dba00" | |||||
| }); | |||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||||
| } else if (res.code === "NO_ORDERS") { | |||||
| Swal.fire({ | |||||
| icon: "info", | |||||
| title: t("Info"), | |||||
| text: t("No available pick order(s) for this lane."), | |||||
| confirmButtonText: t("Confirm"), | |||||
| confirmButtonColor: "#8dba00" | |||||
| }); | |||||
| } else { | |||||
| console.log("ℹ️ Assignment result:", res.message); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("❌ Error assigning by lane:", error); | |||||
| Swal.fire({ | |||||
| icon: "error", | |||||
| title: t("Error"), | |||||
| text: t("Error occurred during assignment."), | |||||
| confirmButtonText: t("Confirm"), | |||||
| confirmButtonColor: "#8dba00" | |||||
| }); | |||||
| } finally { | |||||
| setIsAssigning(false); | |||||
| } | |||||
| }, [currentUserId, t, loadSummaries, onPickOrderAssigned]); | |||||
| const getDateLabel = (offset: number) => { | |||||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | |||||
| }; | |||||
| // Flatten rows to create one box per lane | |||||
| const flattenRows = (rows: any[]) => { | |||||
| const flattened: any[] = []; | |||||
| rows.forEach(row => { | |||||
| row.lanes.forEach((lane: any) => { | |||||
| flattened.push({ | |||||
| truckDepartureTime: row.truckDepartureTime, | |||||
| lane: lane | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| return flattened; | |||||
| }; | |||||
| return ( | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| {/* Date Selector Dropdown */} | |||||
| <Box sx={{ maxWidth: 300, mb: 2 }}> | |||||
| <FormControl fullWidth size="small"> | |||||
| <InputLabel id="date-select-label">{t("Select Date")}</InputLabel> | |||||
| <Select | |||||
| labelId="date-select-label" | |||||
| id="date-select" | |||||
| value={selectedDate} | |||||
| label={t("Select Date")} | |||||
| onChange={(e) => setSelectedDate(e.target.value)} | |||||
| > | |||||
| <MenuItem value="today"> | |||||
| {t("Today")} ({getDateLabel(0)}) | |||||
| </MenuItem> | |||||
| <MenuItem value="tomorrow"> | |||||
| {t("Tomorrow")} ({getDateLabel(1)}) | |||||
| </MenuItem> | |||||
| <MenuItem value="dayAfterTomorrow"> | |||||
| {t("Day After Tomorrow")} ({getDateLabel(2)}) | |||||
| </MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Box> | </Box> | ||||
| {/* Grid containing both floors */} | |||||
| <Grid container spacing={2}> | |||||
| {/* 2/F 楼层面板 */} | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||||
| {/* Floor Label */} | |||||
| <Box | |||||
| sx={{ | |||||
| border: '2px solid #1976d2', | |||||
| borderRadius: 1, | |||||
| backgroundColor: '#e3f2fd', | |||||
| px: 2, | |||||
| py: 1, | |||||
| minWidth: 80, | |||||
| display: 'flex', | |||||
| alignItems: 'center', | |||||
| justifyContent: 'center' | |||||
| }} | |||||
| > | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| color: '#1976d2' | |||||
| }} | |||||
| > | |||||
| 2/F | |||||
| </Typography> | |||||
| </Box> | |||||
| {/* Content Box */} | |||||
| <Box | |||||
| sx={{ | |||||
| border: '1px solid #e0e0e0', | |||||
| borderRadius: 1, | |||||
| p: 1, | |||||
| backgroundColor: '#fafafa', | |||||
| flex: 1 | |||||
| }} | |||||
| > | |||||
| {isLoadingSummary ? ( | |||||
| <Typography variant="caption">Loading...</Typography> | |||||
| ) : !summary2F?.rows || summary2F.rows.length === 0 ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: '1rem', | |||||
| textAlign: 'center', | |||||
| py: 1 | |||||
| }} | |||||
| > | |||||
| {t("No entries available")} | |||||
| </Typography> | |||||
| ) : ( | |||||
| <Grid container spacing={1}> | |||||
| {flattenRows(summary2F.rows).slice(0, 4).map((item, idx) => ( | |||||
| <Grid item xs={12} sm={6} md={3} key={idx}> | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={1} | |||||
| alignItems="center" | |||||
| sx={{ | |||||
| border: '1px solid #e0e0e0', | |||||
| borderRadius: 0.5, | |||||
| p: 1, | |||||
| backgroundColor: '#fff', | |||||
| height: '100%' | |||||
| }} | |||||
| > | |||||
| {/* Time on the left */} | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: '1rem', | |||||
| minWidth: 50, | |||||
| whiteSpace: 'nowrap' | |||||
| }} | |||||
| > | |||||
| {item.truckDepartureTime} | |||||
| </Typography> | |||||
| {/* Single Button on the right */} | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="medium" | |||||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| fontSize: '1.1rem', | |||||
| py: 1, | |||||
| px: 1.5, | |||||
| borderWidth: 1, | |||||
| borderColor: '#ccc', | |||||
| fontWeight: 500, | |||||
| '&:hover': { | |||||
| borderColor: '#999', | |||||
| backgroundColor: '#f5f5f5' | |||||
| } | |||||
| }} | |||||
| > | |||||
| {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| </Grid> | |||||
| {/* 4/F 楼层面板 */} | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||||
| {/* Floor Label */} | |||||
| <Box | |||||
| sx={{ | |||||
| border: '2px solid #1976d2', | |||||
| borderRadius: 1, | |||||
| backgroundColor: '#e3f2fd', | |||||
| px: 2, | |||||
| py: 1, | |||||
| minWidth: 80, | |||||
| display: 'flex', | |||||
| alignItems: 'center', | |||||
| justifyContent: 'center' | |||||
| }} | |||||
| > | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| color: '#1976d2' | |||||
| }} | |||||
| > | |||||
| 4/F | |||||
| </Typography> | |||||
| </Box> | |||||
| {/* Content Box */} | |||||
| <Box | |||||
| sx={{ | |||||
| border: '1px solid #e0e0e0', | |||||
| borderRadius: 1, | |||||
| p: 1, | |||||
| backgroundColor: '#fafafa', | |||||
| flex: 1 | |||||
| }} | |||||
| > | |||||
| {isLoadingSummary ? ( | |||||
| <Typography variant="caption">Loading...</Typography> | |||||
| ) : !summary4F?.rows || summary4F.rows.length === 0 ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: '1rem', | |||||
| textAlign: 'center', | |||||
| py: 1 | |||||
| }} | |||||
| > | |||||
| {t("No entries available")} | |||||
| </Typography> | |||||
| ) : ( | |||||
| <Grid container spacing={1}> | |||||
| {flattenRows(summary4F.rows).slice(0, 4).map((item, idx) => ( | |||||
| <Grid item xs={12} sm={6} md={3} key={idx}> | |||||
| <Stack | |||||
| direction="row" | |||||
| spacing={1} | |||||
| alignItems="center" | |||||
| sx={{ | |||||
| border: '1px solid #e0e0e0', | |||||
| borderRadius: 0.5, | |||||
| p: 1, | |||||
| backgroundColor: '#fff', | |||||
| height: '100%' | |||||
| }} | |||||
| > | |||||
| {/* Time on the left */} | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: '1rem', | |||||
| minWidth: 50, | |||||
| whiteSpace: 'nowrap' | |||||
| }} | |||||
| > | |||||
| {item.truckDepartureTime} | |||||
| </Typography> | |||||
| {/* Single Button on the right */} | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="medium" | |||||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| fontSize: '1.1rem', | |||||
| py: 1, | |||||
| px: 1.5, | |||||
| borderWidth: 1, | |||||
| borderColor: '#ccc', | |||||
| fontWeight: 500, | |||||
| '&:hover': { | |||||
| borderColor: '#999', | |||||
| backgroundColor: '#f5f5f5' | |||||
| } | |||||
| }} | |||||
| > | |||||
| {`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default FGPickOrderCard; | |||||
| export default FinishedGoodFloorLanePanel; | |||||