| @@ -1,115 +1,378 @@ | |||
| "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 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 FinishedGoodFloorLanePanel from "./FinishedGoodFloorLanePanel"; | |||
| type Props = { | |||
| fgOrder: FGPickOrderResponse; | |||
| onQrCodeClick: (pickOrderId: number) => void; | |||
| interface Props { | |||
| onPickOrderAssigned?: () => void; | |||
| }; | |||
| } | |||
| const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick, onPickOrderAssigned }) => { | |||
| const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => { | |||
| 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> | |||
| {/* 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> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default FGPickOrderCard; | |||
| export default FinishedGoodFloorLanePanel; | |||