diff --git a/src/app/sjekk/page.tsx b/src/app/sjekk/page.tsx
new file mode 100644
index 0000000..8201119
--- /dev/null
+++ b/src/app/sjekk/page.tsx
@@ -0,0 +1,26 @@
+import { Card, Container, Stack, Typography } from "@mui/material";
+import { Box } from "@mui/system";
+import { Metadata } from "next";
+import React from "react";
+
+import PublicBlidSearch from "@/components/search/PublicBlidSearch";
+
+export const metadata: Metadata = {
+ title: "Boksøk",
+ description: "Sjekk hvem bøker utdelt fra Boklisten tilhører",
+};
+
+export default function PublicBlidSearchPage() {
+ return (
+
+
+
+ Boksøk
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/RapidHandoutDetails.tsx b/src/components/RapidHandoutDetails.tsx
index 6fc0403..d57ef5b 100644
--- a/src/components/RapidHandoutDetails.tsx
+++ b/src/components/RapidHandoutDetails.tsx
@@ -7,7 +7,8 @@ import useSWR from "swr";
import BlFetcher from "@/api/blFetcher";
import { ItemStatus } from "@/components/matches/matches-helper";
import MatchItemTable from "@/components/matches/MatchItemTable";
-import ScannerModal from "@/components/matches/Scanner/ScannerModal";
+import MatchScannerContent from "@/components/matches/MatchScannerContent";
+import ScannerModal from "@/components/scanner/ScannerModal";
import BL_CONFIG from "@/utils/bl-config";
function calculateUnfulfilledOrderItems(orders: Order[]): OrderItem[] {
@@ -101,12 +102,19 @@ export default function RapidHandoutDetails({
handleClose={() => {
setScanModalOpen(false);
}}
- itemStatuses={itemStatuses}
- expectedItems={itemStatuses.map((itemStatus) => itemStatus.id)}
- fulfilledItems={itemStatuses
- .filter((itemStatus) => itemStatus.fulfilled)
- .map((itemStatus) => itemStatus.id)}
- />
+ >
+ {
+ setScanModalOpen(false);
+ }}
+ itemStatuses={itemStatuses}
+ expectedItems={itemStatuses.map((itemStatus) => itemStatus.id)}
+ fulfilledItems={itemStatuses
+ .filter((itemStatus) => itemStatus.fulfilled)
+ .map((itemStatus) => itemStatus.id)}
+ />
+
>
);
}
diff --git a/src/components/matches/MatchScannerContent.tsx b/src/components/matches/MatchScannerContent.tsx
new file mode 100644
index 0000000..a4be9f4
--- /dev/null
+++ b/src/components/matches/MatchScannerContent.tsx
@@ -0,0 +1,50 @@
+import { Box } from "@mui/material";
+import Typography from "@mui/material/Typography";
+import React, { useEffect } from "react";
+
+import { ItemStatus } from "@/components/matches/matches-helper";
+import ProgressBar from "@/components/matches/matchesList/ProgressBar";
+import MatchItemTable from "@/components/matches/MatchItemTable";
+
+export default function MatchScannerContent({
+ expectedItems,
+ fulfilledItems,
+ itemStatuses,
+ scannerOpen,
+ handleClose,
+}: {
+ itemStatuses: ItemStatus[];
+ expectedItems: string[];
+ fulfilledItems: string[];
+ scannerOpen: boolean;
+ handleClose: () => void;
+}) {
+ useEffect(() => {
+ if (scannerOpen && expectedItems.length === fulfilledItems.length) {
+ handleClose();
+ }
+ }, [expectedItems.length, fulfilledItems.length, handleClose, scannerOpen]);
+ return (
+ <>
+
+
+ {fulfilledItems.length} av {expectedItems.length} bøker mottatt
+
+ }
+ />
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/matches/UserMatchDetail.tsx b/src/components/matches/UserMatchDetail.tsx
index 5065648..3943e19 100644
--- a/src/components/matches/UserMatchDetail.tsx
+++ b/src/components/matches/UserMatchDetail.tsx
@@ -13,10 +13,11 @@ import {
import { UserMatchTitle } from "@/components/matches/matchesList/helper";
import ProgressBar from "@/components/matches/matchesList/ProgressBar";
import MatchItemTable from "@/components/matches/MatchItemTable";
+import MatchScannerContent from "@/components/matches/MatchScannerContent";
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 ScannerModal from "@/components/scanner/ScannerModal";
+import ScannerTutorial from "@/components/scanner/ScannerTutorial";
import BL_CONFIG from "@/utils/bl-config";
import { UserMatchWithDetails } from "@/utils/types";
@@ -160,10 +161,18 @@ const UserMatchDetail = ({
setScanModalOpen(false);
setRedirectCountdownStarted(isFulfilled);
}}
- itemStatuses={itemStatuses}
- expectedItems={match.expectedItems}
- fulfilledItems={fulfilledItems}
- />
+ >
+ {
+ setScanModalOpen(false);
+ setRedirectCountdownStarted(isFulfilled);
+ }}
+ scannerOpen={scanModalOpen}
+ itemStatuses={itemStatuses}
+ expectedItems={match.expectedItems}
+ fulfilledItems={fulfilledItems}
+ />
+
>
);
};
diff --git a/src/components/scanner/BlidScanner.tsx b/src/components/scanner/BlidScanner.tsx
new file mode 100644
index 0000000..ce43bdb
--- /dev/null
+++ b/src/components/scanner/BlidScanner.tsx
@@ -0,0 +1,53 @@
+import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
+
+import { ScannedTextType, TextType } from "@/utils/types";
+
+export function determineScannedTextType(scannedText: string): ScannedTextType {
+ if (/^[\dA-Za-z]{12}$|^\d{8}$/.test(scannedText)) {
+ return TextType.BLID;
+ } else if (/^\d{13}$/.test(scannedText)) {
+ return TextType.ISBN;
+ }
+
+ return TextType.UNKNOWN;
+}
+
+export default function BlidScanner({
+ onResult,
+}: {
+ onResult: (scannedText: string) => Promise;
+}) {
+ const handleCodeDetection = async (
+ detectedCodes: IDetectedBarcode[],
+ ): Promise => {
+ const didFindBlid = detectedCodes.some(
+ (code) => determineScannedTextType(code.rawValue) === TextType.BLID,
+ );
+ const codesToProcess = didFindBlid
+ ? detectedCodes.filter(
+ (code) => determineScannedTextType(code.rawValue) === TextType.BLID,
+ )
+ : detectedCodes;
+
+ for (const code of codesToProcess) {
+ try {
+ await onResult(code.rawValue);
+ } catch (error) {
+ console.error("Failed to handle scan", error);
+ }
+ // Arbitrary delay to somewhat avoid races the backend isn't smart enough to handle
+ await new Promise((resolve) => {
+ window.setTimeout(resolve, 250);
+ });
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/matches/Scanner/ManualRegistrationModal.tsx b/src/components/scanner/ManualBlidSearchModal.tsx
similarity index 95%
rename from src/components/matches/Scanner/ManualRegistrationModal.tsx
rename to src/components/scanner/ManualBlidSearchModal.tsx
index 7c7cc7a..c741d6e 100644
--- a/src/components/matches/Scanner/ManualRegistrationModal.tsx
+++ b/src/components/scanner/ManualBlidSearchModal.tsx
@@ -12,7 +12,7 @@ import {
} from "@mui/material";
import React, { useState } from "react";
-const ManualRegistrationModal = ({
+const ManualBlidSearchModal = ({
open,
handleClose,
handleSubmit,
@@ -69,4 +69,4 @@ const ManualRegistrationModal = ({
);
};
-export default ManualRegistrationModal;
+export default ManualBlidSearchModal;
diff --git a/src/components/matches/Scanner/ScannerFeedback.tsx b/src/components/scanner/ScannerFeedback.tsx
similarity index 100%
rename from src/components/matches/Scanner/ScannerFeedback.tsx
rename to src/components/scanner/ScannerFeedback.tsx
diff --git a/src/components/matches/Scanner/ScannerModal.tsx b/src/components/scanner/ScannerModal.tsx
similarity index 51%
rename from src/components/matches/Scanner/ScannerModal.tsx
rename to src/components/scanner/ScannerModal.tsx
index 0a4500f..77c9b67 100644
--- a/src/components/matches/Scanner/ScannerModal.tsx
+++ b/src/components/scanner/ScannerModal.tsx
@@ -1,25 +1,13 @@
import { Close, InputRounded } from "@mui/icons-material";
import { AlertColor, Box, Button, Card, Modal, Stack } from "@mui/material";
-import Typography from "@mui/material/Typography";
-import { Scanner, IDetectedBarcode } from "@yudiel/react-qr-scanner";
-import React, { useEffect, useState } from "react";
+import React, { ReactNode, useState } from "react";
-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 { assertBlApiError, ScannedTextType, TextType } from "@/utils/types";
-
-function determineScannedTextType(scannedText: string): ScannedTextType {
- if (/^[\dA-Za-z]{12}$|^\d{8}$/.test(scannedText)) {
- return TextType.BLID;
- } else if (/^\d{13}$/.test(scannedText)) {
- return TextType.ISBN;
- }
-
- return TextType.UNKNOWN;
-}
+import BlidScanner, {
+ determineScannedTextType,
+} from "@/components/scanner/BlidScanner";
+import ManualBlidSearchModal from "@/components/scanner/ManualBlidSearchModal";
+import ScannerFeedback from "@/components/scanner/ScannerFeedback";
+import { assertBlApiError, TextType } from "@/utils/types";
type Feedback = {
text: string;
@@ -32,17 +20,15 @@ const ScannerModal = ({
open,
handleClose,
handleSuccessfulScan,
- itemStatuses,
- expectedItems,
- fulfilledItems,
+ allowManualRegistration,
+ children,
}: {
onScan: (blid: string) => Promise<[{ feedback: string }]>;
open: boolean;
handleClose: () => void;
handleSuccessfulScan?: (() => void) | undefined;
- itemStatuses: ItemStatus[];
- expectedItems: string[];
- fulfilledItems: string[];
+ allowManualRegistration?: boolean;
+ children?: ReactNode;
}) => {
const [manualRegistrationModalOpen, setManualRegistrationModalOpen] =
useState(false);
@@ -96,46 +82,6 @@ const ScannerModal = ({
}
};
- useEffect(() => {
- if (
- open &&
- expectedItems.length === fulfilledItems.length &&
- !(feedback.visible && feedback.severity === "info")
- ) {
- handleClose();
- }
- }, [
- expectedItems.length,
- fulfilledItems.length,
- handleClose,
- open,
- feedback.visible,
- feedback.severity,
- ]);
-
- const handleCodeDetection = async (
- detectedCodes: IDetectedBarcode[],
- ): Promise => {
- const didFindBlid = detectedCodes.some(
- (code) => determineScannedTextType(code.rawValue) === TextType.BLID,
- );
- const codesToProcess = didFindBlid
- ? detectedCodes.filter(
- (code) => determineScannedTextType(code.rawValue) === TextType.BLID,
- )
- : detectedCodes;
-
- for (const code of codesToProcess) {
- await handleRegistration(code.rawValue).catch((error) =>
- console.error("Failed to handle scan", error),
- );
- // Arbitrary delay to somewhat avoid races the backend isn't smart enough to handle
- await new Promise((resolve) => {
- window.setTimeout(resolve, 250);
- });
- }
- };
-
return (
-
-
-
-
- {fulfilledItems.length} av {expectedItems.length} bøker mottatt
-
- }
- />
-
-
-
+
+ {children}
- }
- onClick={() => setManualRegistrationModalOpen(true)}
- >
- Manuell registrering
-
+ {allowManualRegistration && (
+ }
+ onClick={() => setManualRegistrationModalOpen(true)}
+ >
+ Skriv inn blid manuelt
+
+ )}
}
@@ -208,7 +131,7 @@ const ScannerModal = ({
Lukk
- {
setManualRegistrationModalOpen(false);
diff --git a/src/components/matches/Scanner/ScannerTutorial.tsx b/src/components/scanner/ScannerTutorial.tsx
similarity index 100%
rename from src/components/matches/Scanner/ScannerTutorial.tsx
rename to src/components/scanner/ScannerTutorial.tsx
diff --git a/src/components/search/PublicBlidSearch.tsx b/src/components/search/PublicBlidSearch.tsx
new file mode 100644
index 0000000..ca3afff
--- /dev/null
+++ b/src/components/search/PublicBlidSearch.tsx
@@ -0,0 +1,228 @@
+"use client";
+import { Phone } from "@mui/icons-material";
+import EmailIcon from "@mui/icons-material/Email";
+import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner";
+import {
+ Alert,
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+ Typography,
+} from "@mui/material";
+import Button from "@mui/material/Button";
+import TextField from "@mui/material/TextField";
+import { Box } from "@mui/system";
+import moment from "moment";
+import { useState } from "react";
+
+import { isLoggedIn } from "@/api/auth";
+import BlFetcher from "@/api/blFetcher";
+import DynamicLink from "@/components/DynamicLink";
+import ScannerModal from "@/components/scanner/ScannerModal";
+import BL_CONFIG from "@/utils/bl-config";
+import useIsHydrated from "@/utils/useIsHydrated";
+
+interface PublicBlidLookupResult {
+ handoutBranch: string;
+ handoutTime: string;
+ deadline: string;
+ title: string;
+ isbn: number;
+ name: string;
+ email: string;
+ phone: string;
+}
+
+export default function PublicBlidSearch() {
+ const hydrated = useIsHydrated();
+ const [scannerModalOpen, setScannerModalOpen] = useState(false);
+ const [blidSearch, setBlidSearch] = useState("");
+ const [searchResult, setSearchResult] = useState<
+ PublicBlidLookupResult | "inactive" | null
+ >(null);
+
+ async function onBlidSearch(blid: string) {
+ setSearchResult(null);
+ setScannerModalOpen(false);
+ setBlidSearch(blid);
+ if (blid.length !== 8 && blid.length !== 12) {
+ // Only lookup for valid blids
+ return;
+ }
+ try {
+ const result = await BlFetcher.post<[PublicBlidLookupResult] | []>(
+ BL_CONFIG.collection.customerItem + "/public-blid-lookup",
+ {
+ blid,
+ },
+ );
+ if (result.length === 1) {
+ setSearchResult(result[0]);
+ } else {
+ setSearchResult("inactive");
+ }
+ } catch (error) {
+ console.error(error);
+ setSearchResult("inactive");
+ }
+ }
+
+ return (
+ hydrated &&
+ (isLoggedIn() ? (
+
+
+ Skriv inn en bok sin unike ID (8 eller 12 siffer) for å se hvem den
+ tilhører. Du kan også trykke på{" "}
+ for å
+ scanne bokas unike ID med kamera.
+
+ onBlidSearch(event.target.value)}
+ InputProps={{
+ endAdornment: (
+
+ ),
+ }}
+ />
+
+ {searchResult === "inactive" &&
+ "Denne boken er ikke registrert som utdelt."}
+ {((searchResult === null &&
+ blidSearch.length !== 8 &&
+ blidSearch.length !== 12) ||
+ blidSearch.length === 0) &&
+ "Venter på unik ID"}
+
+
+ {searchResult !== "inactive" && searchResult !== null && (
+ <>
+
+ Denne boken tilhører
+
+
+
+ {searchResult.name}
+
+
+
+
+
+ {searchResult.phone}
+
+
+
+ {searchResult.email}
+
+
+
+
+
+
+
+ Tittel
+
+ {searchResult.title}
+
+
+
+ ISBN
+
+ {searchResult.isbn}
+
+
+
+ Utdelt hos
+
+ {searchResult.handoutBranch}
+
+
+
+ Utdelt den
+
+
+ {moment(searchResult.handoutTime).format("DD/MM/YYYY")}
+
+
+
+
+ Frist
+
+
+ {moment(searchResult.deadline).format("DD/MM/YYYY")}
+
+
+
+
+
+ >
+ )}
+
+ {
+ onBlidSearch(blid);
+ return [{ feedback: "" }] as [{ feedback: string }];
+ }}
+ open={scannerModalOpen}
+ handleClose={() => setScannerModalOpen(false)}
+ />
+
+ ) : (
+ <>
+ Du må logge inn for å kunne bruke boksøk
+
+
+
+ >
+ ))
+ );
+}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 21bc256..2b25c1c 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -76,11 +76,8 @@ export interface GroupedMatches {
}
export enum TextType {
- // eslint-disable-next-line no-unused-vars
BLID,
- // eslint-disable-next-line no-unused-vars
ISBN,
- // eslint-disable-next-line no-unused-vars
UNKNOWN,
}