diff --git a/src/app/admin/hurtigutdeling/page.tsx b/src/app/admin/hurtigutdeling/page.tsx index 4b54d3e..333ea20 100644 --- a/src/app/admin/hurtigutdeling/page.tsx +++ b/src/app/admin/hurtigutdeling/page.tsx @@ -1,10 +1,8 @@ -import { Button, Card, Container, Typography } from "@mui/material"; +import { Card, Container, Typography } from "@mui/material"; import { Box } from "@mui/system"; import { Metadata } from "next"; -import DynamicLink from "@/components/DynamicLink"; import RapidHandout from "@/components/RapidHandout"; -import BL_CONFIG from "@/utils/bl-config"; export const metadata: Metadata = { title: "Hurtigutdeling", @@ -24,9 +22,6 @@ export default function HandoutPage() { }} > Hurtigutdeling - - - diff --git a/src/components/RapidHandoutDetails.tsx b/src/components/RapidHandoutDetails.tsx index e3eeff6..286c138 100644 --- a/src/components/RapidHandoutDetails.tsx +++ b/src/components/RapidHandoutDetails.tsx @@ -1,13 +1,17 @@ -import { Order, UserDetail } from "@boklisten/bl-model"; -import { Alert, Typography } from "@mui/material"; +import { Order, OrderItem, UserDetail } from "@boklisten/bl-model"; +import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner"; +import { Alert, Button, Typography } from "@mui/material"; +import React, { useEffect, useState } from "react"; import useSWR from "swr"; import BlFetcher from "@/api/blFetcher"; +import CountdownToRedirect from "@/components/CountdownToRedirect"; import { ItemStatus } from "@/components/matches/matches-helper"; import MatchItemTable from "@/components/matches/MatchItemTable"; +import ScannerModal from "@/components/matches/Scanner/ScannerModal"; import BL_CONFIG from "@/utils/bl-config"; -function mapOrdersToItemStatuses(orders: Order[]): ItemStatus[] { +function calculateUnfulfilledOrderItems(orders: Order[]): OrderItem[] { return orders .filter((order) => order.byCustomer && !order.handoutByDelivery) .flatMap((order) => order.orderItems) @@ -15,15 +19,16 @@ function mapOrdersToItemStatuses(orders: Order[]): ItemStatus[] { (orderItem) => !orderItem.movedToOrder && !orderItem.handout && - (orderItem.type === "rent" || - orderItem.type === "buy" || - orderItem.type === "partly-payment"), - ) - .map((oi) => ({ - id: oi.item, - title: oi.title, - fulfilled: false, - })); + orderItem.type === "rent", + ); +} + +function mapOrdersToItemStatuses(orders: Order[]): ItemStatus[] { + return calculateUnfulfilledOrderItems(orders).map((oi) => ({ + id: oi.item, + title: oi.title, + fulfilled: false, + })); } export default function RapidHandoutDetails({ @@ -31,12 +36,44 @@ export default function RapidHandoutDetails({ }: { customer: UserDetail; }) { - const { data: orders } = useSWR( + const { data: orders, mutate: updateOrders } = useSWR( `${BL_CONFIG.collection.order}?placed=true&customer=${customer.id}`, BlFetcher.get, { refreshInterval: 5000 }, ); - const itemStatuses = mapOrdersToItemStatuses(orders ?? []); + const [itemStatuses, setItemStatuses] = useState([]); + const [scanModalOpen, setScanModalOpen] = useState(false); + const [redirectCountdownStarted, setRedirectCountdownStarted] = + useState(false); + + const isFulfilled = itemStatuses.every((itemStatus) => itemStatus.fulfilled); + + useEffect(() => { + BlFetcher.get( + `${BL_CONFIG.collection.order}?placed=true&customer=${customer.id}`, + ) + .then((originalOrders) => { + return setItemStatuses(mapOrdersToItemStatuses(originalOrders)); + }) + .catch((error) => { + console.error("Failed to fetch original orders, error:", error); + }); + }, [customer.id]); + + useEffect(() => { + if (!orders) { + return; + } + const unfulfilledOrderItems = calculateUnfulfilledOrderItems(orders); + setItemStatuses((previousState) => + previousState.map((itemStatus) => ({ + ...itemStatus, + fulfilled: !unfulfilledOrderItems.some( + (orderItem) => orderItem.item === itemStatus.id, + ), + })), + ); + }, [orders]); return itemStatuses.length === 0 ? ( @@ -44,10 +81,40 @@ export default function RapidHandoutDetails({ ) : ( <> - + Plukkliste + + {redirectCountdownStarted && ( + + )} + + BlFetcher.post(BL_CONFIG.collection.order + "/rapid-handout", { + blid, + customerId: customer.id, + }) + } + open={scanModalOpen} + handleSuccessfulScan={updateOrders} + handleClose={() => { + setScanModalOpen(false); + setRedirectCountdownStarted(isFulfilled); + }} + itemStatuses={itemStatuses} + expectedItems={itemStatuses.map((itemStatus) => itemStatus.id)} + fulfilledItems={itemStatuses + .filter((itemStatus) => itemStatus.fulfilled) + .map((itemStatus) => itemStatus.id)} + /> ); } diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/matches/Scanner/ScannerModal.tsx index 4081104..0a4500f 100644 --- a/src/components/matches/Scanner/ScannerModal.tsx +++ b/src/components/matches/Scanner/ScannerModal.tsx @@ -4,13 +4,11 @@ import Typography from "@mui/material/Typography"; import { Scanner, IDetectedBarcode } from "@yudiel/react-qr-scanner"; import React, { useEffect, useState } from "react"; -import BlFetcher from "@/api/blFetcher"; import { ItemStatus } from "@/components/matches/matches-helper"; import ProgressBar from "@/components/matches/matchesList/ProgressBar"; import MatchItemTable from "@/components/matches/MatchItemTable"; import ManualRegistrationModal from "@/components/matches/Scanner/ManualRegistrationModal"; import ScannerFeedback from "@/components/matches/Scanner/ScannerFeedback"; -import BL_CONFIG from "@/utils/bl-config"; import { assertBlApiError, ScannedTextType, TextType } from "@/utils/types"; function determineScannedTextType(scannedText: string): ScannedTextType { @@ -30,16 +28,18 @@ type Feedback = { }; const ScannerModal = ({ + onScan, open, handleClose, - handleItemTransferred, + handleSuccessfulScan, itemStatuses, expectedItems, fulfilledItems, }: { + onScan: (blid: string) => Promise<[{ feedback: string }]>; open: boolean; handleClose: () => void; - handleItemTransferred?: (() => void) | undefined; + handleSuccessfulScan?: (() => void) | undefined; itemStatuses: ItemStatus[]; expectedItems: string[]; fulfilledItems: string[]; @@ -73,13 +73,7 @@ const ScannerModal = ({ } try { - const [{ feedback }] = await BlFetcher.post< - [ - { - feedback: string; - }, - ] - >(BL_CONFIG.collection.match + "/transfer-item", { blid: scannedText }); + const [{ feedback }] = await onScan(scannedText); try { navigator?.vibrate(100); } catch { @@ -90,7 +84,7 @@ const ScannerModal = ({ severity: feedback ? "info" : "success", visible: true, }); - handleItemTransferred?.(); + handleSuccessfulScan?.(); } catch (error) { if (assertBlApiError(error) && error.msg) { setFeedback({ diff --git a/src/components/matches/UserMatchDetail.tsx b/src/components/matches/UserMatchDetail.tsx index 393da74..5065648 100644 --- a/src/components/matches/UserMatchDetail.tsx +++ b/src/components/matches/UserMatchDetail.tsx @@ -2,6 +2,7 @@ import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner"; import { Alert, AlertTitle, Box, Button, Typography } from "@mui/material"; import React, { useState } from "react"; +import BlFetcher from "@/api/blFetcher"; import CountdownToRedirect from "@/components/CountdownToRedirect"; import { calculateFulfilledUserMatchItems, @@ -16,6 +17,7 @@ import MeetingInfo from "@/components/matches/MeetingInfo"; import OtherPersonContact from "@/components/matches/OtherPersonContact"; import ScannerModal from "@/components/matches/Scanner/ScannerModal"; import ScannerTutorial from "@/components/matches/Scanner/ScannerTutorial"; +import BL_CONFIG from "@/utils/bl-config"; import { UserMatchWithDetails } from "@/utils/types"; const UserMatchDetail = ({ @@ -147,8 +149,13 @@ const UserMatchDetail = ({ + BlFetcher.post(BL_CONFIG.collection.match + "/transfer-item", { + blid, + }) + } open={scanModalOpen} - handleItemTransferred={handleItemTransferred} + handleSuccessfulScan={handleItemTransferred} handleClose={() => { setScanModalOpen(false); setRedirectCountdownStarted(isFulfilled); diff --git a/src/components/search/UserDetailSearchField.tsx b/src/components/search/UserDetailSearchField.tsx index dd19c3f..ad35ab8 100644 --- a/src/components/search/UserDetailSearchField.tsx +++ b/src/components/search/UserDetailSearchField.tsx @@ -18,7 +18,7 @@ export default function UserDetailSearchField({ return ( - Søk etter en kunde for å se plukkliste + Søk etter en kunde for å starte utdeling