diff --git a/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss b/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss deleted file mode 100644 index 53d96cdb..00000000 --- a/frontend/src/components/common/AttendeeCheckInTable/AttendeesCheckInTable.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -@import '../../../styles/mixins'; - -.header { - position: sticky; - top: 0; - z-index: 2; - display: flex; - flex-direction: column; - - .checkInCount { - font-size: 0.7em; - color: #999; - } - - .search { - flex: 1; - - .searchBar { - display: flex; - gap: 20px; - align-items: center; - - .searchInput { - flex: 1; - margin-bottom: 0 !important; - } - - .scanButton { - @include respond-below(sm) { - display: none; - } - } - - .scanIcon { - display: none; - @include respond-below(sm) { - display: flex; - } - } - } - } - - .stats { - flex: 1; - } -} - -.loading, .noResults { - display: flex; - justify-content: center; - align-items: center; - margin-top: 50px; -} - -.attendees { - .attendee { - display: flex; - align-items: center; - - .details { - flex: 1; - } - - .actions { - flex-grow: initial; - } - } -} diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss index c22c2e81..3cb3e6fa 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.module.scss @@ -62,7 +62,8 @@ position: absolute; animation: colorfulBorder 10s infinite; border-radius: 10px; - outline: solid 50vmax #472e7840; + outline: solid 50vmax rgb(71 46 120 / 50%); + transition: outline-color .2s ease-out; min-width: 200px; min-height: 200px; @@ -72,6 +73,18 @@ } } + .scannerOverlay.success { + outline: solid 50vmax rgb(80 148 80 / 75%); + } + + .scannerOverlay.failure { + outline: solid 50vmax rgb(193 72 72 / 75%); + } + + .scannerOverlay.checkingIn { + outline: solid 50vmax rgb(172 158 85 / 60%); + } + video { width: 100vw !important; height: 100vh !important; diff --git a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx index c9046517..6e309ed2 100644 --- a/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx +++ b/frontend/src/components/common/AttendeeCheckInTable/QrScanner.tsx @@ -8,7 +8,7 @@ import {showError} from "../../../utilites/notifications.tsx"; import {t, Trans} from "@lingui/macro"; interface QRScannerComponentProps { - onCheckIn: (attendeePublicId: string, onRequestComplete: () => void, onFailure: () => void) => void; + onCheckIn: (attendeePublicId: string, onRequestComplete: (didSucceed: boolean) => void, onFailure: () => void) => void; onClose: () => void; } @@ -25,7 +25,9 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const latestProcessedAttendeeIdsRef = useRef([]); const [currentAttendeeId, setCurrentAttendeeId] = useState(null); - const [debouncedAttendeeId] = useDebouncedValue(currentAttendeeId, 500); + const [debouncedAttendeeId] = useDebouncedValue(currentAttendeeId, 1000); + const [isScanFailed, setIsScanFailed] = useState(false); + const [isScanSucceeded, setIsScanSucceeded] = useState(false); useEffect(() => { latestProcessedAttendeeIdsRef.current = processedAttendeeIds; @@ -54,17 +56,39 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { const latestProcessedAttendeeIds = latestProcessedAttendeeIdsRef.current; const alreadyScanned = latestProcessedAttendeeIds.includes(debouncedAttendeeId); + if (isScanSucceeded || isScanFailed) { + return; + } + if (alreadyScanned) { showError(t`You already scanned this ticket`); + + setIsScanFailed(true); + setInterval(function() { + setIsScanFailed(false); + }, 500); + return; } if (!isCheckingIn && !alreadyScanned) { setIsCheckingIn(true); - props.onCheckIn(debouncedAttendeeId, () => { + props.onCheckIn(debouncedAttendeeId, (didSucceed) => { setIsCheckingIn(false); setProcessedAttendeeIds(prevIds => [...prevIds, debouncedAttendeeId]); setCurrentAttendeeId(null); + + if (didSucceed) { + setIsScanSucceeded(true); + setInterval(function() { + setIsScanSucceeded(false); + }, 500); + } else { + setIsScanFailed(true); + setInterval(function() { + setIsScanFailed(false); + }, 500); + } }, () => { setIsCheckingIn(false); setCurrentAttendeeId(null); @@ -178,7 +202,7 @@ export const QRScannerComponent = (props: QRScannerComponentProps) => { -
+
); }; diff --git a/frontend/src/components/common/AttendeeCheckInTable/index.tsx b/frontend/src/components/common/AttendeeCheckInTable/index.tsx deleted file mode 100644 index 37f5a004..00000000 --- a/frontend/src/components/common/AttendeeCheckInTable/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import {ActionIcon, Button, Loader, Modal, Switch} from "@mantine/core"; -import classes from './AttendeesCheckInTable.module.scss'; -import {useParams} from "react-router-dom"; -import {Card} from "../Card"; -import {PageTitle} from "../PageTitle"; -import {SearchBar} from "../SearchBar"; -import {useState} from "react"; -import {useDebouncedValue} from "@mantine/hooks"; -import {useGetAttendees} from "../../../queries/useGetAttendees.ts"; -import {useGetEvent} from "../../../queries/useGetEvent.ts"; -import {useCheckInAttendee} from "../../../mutations/useCheckInAttendee.ts"; -import {Attendee, QueryFilters} from "../../../types.ts"; -import {showError, showSuccess} from "../../../utilites/notifications.tsx"; -import {AxiosError} from "axios"; -import {IconQrcode} from "@tabler/icons-react"; -import {QRScannerComponent} from "./QrScanner.tsx"; -import {t, Trans} from "@lingui/macro"; -import {useGetEventCheckInStats} from "../../../queries/useGetEventCheckInStats.ts"; - -export const AttendeesCheckInTable = () => { - const {eventId} = useParams(); - const [searchQuery, setSearchQuery] = useState(''); - const [searchQueryDebounced] = useDebouncedValue(searchQuery, 200); - const [qrScannerOpen, setQrScannerOpen] = useState(false); - const {data: {tickets} = {}} = useGetEvent(eventId); - const queryFilters: QueryFilters = { - pageNumber: 1, - query: searchQueryDebounced, - perPage: 100, - filterFields: { - status: ['ACTIVE'], - }, - }; - const attendeesQuery = useGetAttendees(eventId, queryFilters); - const attendees = attendeesQuery?.data?.data; - const mutation = useCheckInAttendee(); - const {data: eventStats} = useGetEventCheckInStats(eventId); - - const handleCheckInToggle = (checked: boolean, attendee: Attendee) => { - mutation.mutate({ - eventId: eventId, - attendeePublicId: attendee.public_id, - action: checked ? 'check_in' : 'check_out', - pagination: queryFilters, - }, { - onSuccess: ({data: attendee}, variables) => { - showSuccess(Successfully - checked {attendee.first_name} {attendee.last_name} {variables.action === 'check_in' ? 'in' : 'out'} - ); - }, - onError: (error, variables) => { - if (error instanceof AxiosError) { - showError(error?.response?.data.message || - Unable to {variables.action ? t`check in` : t`check out`} attendee); - } - } - }) - } - - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete?: () => void) => { - mutation.mutate({ - eventId: eventId, - attendeePublicId: attendeePublicId, - action: 'check_in', - pagination: queryFilters, - }, { - onSuccess: ({data: attendee}, variables) => { - if (onRequestComplete) { - onRequestComplete() - } - showSuccess(Successfully - checked {attendee.first_name} {attendee.last_name} {variables.action === 'check_in' ? 'in' : 'out'} - ); - }, - onError: (error, variables) => { - if (onRequestComplete) { - onRequestComplete() - } - if (error instanceof AxiosError) { - showError(error?.response?.data.message || - Unable to {variables.action ? t`check in` : t`check out`} attendee); - } - } - }) - } - - const Attendees = () => { - const Container = () => { - if (attendeesQuery.isFetching || !attendees || !tickets) { - return ( -
- -
- ) - } - - if (attendees.length === 0) { - return ( -
- No attendees to show. -
- ); - } - - return ( -
- {attendees.map(attendee => { - return ( - -
-
- {attendee.first_name} {attendee.last_name} -
-
- {attendee.public_id} -
-
- {tickets.find(ticket => ticket.id === attendee.ticket_id)?.title} -
-
-
- handleCheckInToggle(event.target.checked, attendee)} - /> -
-
- ) - })} -
- ) - } - - return ( -
- -
- ); - } - - return ( - <> - -
- - {t`Check In`}{' '} - {eventStats && ( - - - {eventStats && `${eventStats.total_checked_in_attendees}/${eventStats.total_attendees}`} checked in - - - )} - - -
- setSearchQuery(event.target.value)} - onClear={() => setSearchQuery('')} - placeholder={t`Seach by name, order #, attendee # or email...`} - /> - - setQrScannerOpen(true)}> - - -
-
-
- - {qrScannerOpen && ( - setQrScannerOpen(false)} - fullScreen - radius={0} - transitionProps={{transition: 'fade', duration: 200}} - padding={'none'} - > - - - setQrScannerOpen(false)} - /> - - - )} - - ); -}; diff --git a/frontend/src/components/layouts/CheckIn/index.tsx b/frontend/src/components/layouts/CheckIn/index.tsx index 9b5e65e9..ed3ce836 100644 --- a/frontend/src/components/layouts/CheckIn/index.tsx +++ b/frontend/src/components/layouts/CheckIn/index.tsx @@ -100,14 +100,14 @@ const CheckIn = () => { ) } - const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: () => void, onFailure: () => void) => { + const handleQrCheckIn = (attendeePublicId: string, onRequestComplete: (didSucceed: boolean) => void, onFailure: () => void) => { checkInMutation.mutate({ checkInListShortId: checkInListShortId, attendeePublicId: attendeePublicId, }, { onSuccess: ({errors}) => { if (onRequestComplete) { - onRequestComplete() + onRequestComplete(!(errors && errors[attendeePublicId])) } // Show error if there is an error for this specific attendee // It's a bulk endpoint, so even if there's an error it returns a 200 @@ -211,7 +211,6 @@ const CheckIn = () => { />) } - if (checkInList?.is_expired) { return (