diff --git a/apps/core-admin/src/controllers/events.ts b/apps/core-admin/src/controllers/events.ts index 4d37b24f..6bb6f69d 100644 --- a/apps/core-admin/src/controllers/events.ts +++ b/apps/core-admin/src/controllers/events.ts @@ -28,6 +28,7 @@ export const getEvents = async (req: Request, res: Response) => { include: { Participant: true, attributes: true, + extras: true, ParticipantCheckin: true, }, }); @@ -40,6 +41,7 @@ export const getEvents = async (req: Request, res: Response) => { createdAt: event.createdAt, numberOfParticipants: event.Participant.length, numberOfAttributes: event.attributes.length, + numberOfExtras: event.extras.length, numberOfParticipantsCheckedIn: event.ParticipantCheckin.length, }; }); @@ -77,6 +79,7 @@ export const getEventStats = async (req: Request, res: Response) => { include: { Participant: true, attributes: true, + extras: true, ParticipantCheckin: true, }, }); @@ -92,6 +95,7 @@ export const getEventStats = async (req: Request, res: Response) => { createdAt: event.createdAt, numberOfParticipants: event.Participant.length, numberOfAttributes: event.attributes.length, + numberOfExtras: event.extras.length, numberOfParticipantsCheckedIn: event.ParticipantCheckin.length, }; diff --git a/apps/core-admin/src/controllers/extras.ts b/apps/core-admin/src/controllers/extras.ts new file mode 100644 index 00000000..9c3c5ac3 --- /dev/null +++ b/apps/core-admin/src/controllers/extras.ts @@ -0,0 +1,217 @@ +import { Request, Response } from 'express'; + +import prisma from '../utils/database'; + +export const addNewExtra = async (req: Request, res: Response) => { + try { + const { orgId, eventId } = req?.params; + const { name } = req?.body; + + const newExtra = await prisma.extras.create({ + data: { + name, + organizationId: orgId, + eventId: eventId, + }, + }); + + return res.status(200).json({ newExtra }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const getAllExtras = async (req: Request, res: Response) => { + try { + const { orgId, eventId } = req?.params; + + let extras = await prisma.extras.findMany({ + where: { + organizationId: orgId, + eventId: eventId, + }, + include: { + participantExtras: true, + participantExtrasCheckIn: true, + }, + }); + + if (!extras) { + return res.status(404).json({ error: 'No extras found' }); + } + + console.log(extras); + + extras = extras.map((extra: any) => { + return { + id: extra.id, + name: extra.name, + createdAt: extra.createdAt, + numberOfParticipantsWithExtrasAssigned: extra.participantExtras.length, + numberOfParticipantsWithExtrasCheckedIn: extra.participantExtrasCheckIn.length, + }; + }); + + return res.status(200).json({ extras }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const getExtraById = async (req: Request, res: Response) => { + try { + const { orgId, eventId, extraId } = req?.params; + + let extra = await prisma.extras.findFirst({ + where: { + organizationId: orgId, + eventId, + id: extraId, + }, + include: { + participantExtras: { + include: { + participant: true, + }, + }, + participantExtrasCheckIn: { + include: { + checkedInByUser: { + select: { + email: true, + firstName: true, + lastName: true, + }, + }, + participant: true, + }, + }, + }, + }); + + if (!extra) { + return res.status(404).json({ error: 'No extras found' }); + } + + extra = { + id: extra.id, + name: extra.name, + createdAt: extra.createdAt, + numberOfParticipantsWithExtrasAssigned: extra.participantExtras.length, + participantExtraDetails: extra.participantExtras?.map((participantExtra: any) => { + return { + id: participantExtra?.id, + addedAt: participantExtra?.createdAt, + firstName: participantExtra?.participant?.firstName, + lastName: participantExtra?.participant?.lastName, + checkedIn: { + status: extra.participantExtrasCheckIn.some( + (participantExtraCheckIn: any) => + participantExtraCheckIn.participantId === participantExtra.participantId, + ), + at: extra.participantExtrasCheckIn.find( + (participantExtraCheckIn: any) => + participantExtraCheckIn.participantId === participantExtra.participantId, + )?.checkedInAt, + by: { + email: extra.participantExtrasCheckIn.find( + (participantExtraCheckIn: any) => + participantExtraCheckIn.participantId === participantExtra.participantId, + )?.checkedInByUser?.email, + firstName: extra.participantExtrasCheckIn.find( + (participantExtraCheckIn: any) => + participantExtraCheckIn.participantId === participantExtra.participantId, + )?.checkedInByUser?.firstName, + lastName: extra.participantExtrasCheckIn.find( + (participantExtraCheckIn: any) => + participantExtraCheckIn.participantId === participantExtra.participantId, + )?.checkedInByUser?.lastName, + }, + }, + }; + }), + }; + + return res.status(200).json({ extra }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const checkInExtra = async (req: Request, res: Response) => { + try { + const userId = req?.auth?.payload?.sub; + + const { orgId, eventId, extraId } = req?.params; + + const { checkedInAt, participantId } = req?.body; + + if (!checkedInAt || !participantId) { + return res.status(400).json({ error: 'checkedInAt and participantId is required' }); + } + + const participantHasBeenAssignedExtra = await prisma.participantExtras.findFirst({ + where: { + participantId, + extraId, + }, + }); + + if (!participantHasBeenAssignedExtra) { + return res.status(400).json({ error: 'Participant has not been assigned this extra' }); + } + + const participantExtraAlreadyCheckedIn = await prisma.participantExtrasCheckIn.findFirst({ + where: { + participantId, + extraId, + }, + include: { + participant: true, + checkedInByUser: true, + extra: true, + }, + }); + + if (participantExtraAlreadyCheckedIn) { + return res.status(400).json({ + error: `${participantExtraAlreadyCheckedIn?.participant?.firstName} ${participantExtraAlreadyCheckedIn?.participant?.lastName} has already been checked-in at ${participantExtraAlreadyCheckedIn.checkedInAt} by ${participantExtraAlreadyCheckedIn.checkedInByUser.email}`, + }); + } + + let participantExtraCheckIn = await prisma.participantExtrasCheckIn.create({ + data: { + participantId, + extraId, + checkedInBy: userId, + checkedInAt, + }, + }); + + if (!participantExtraCheckIn) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + participantExtraCheckIn = await prisma.participantExtrasCheckIn.findFirst({ + where: { + participantId, + extraId, + }, + include: { + participant: true, + extra: true, + }, + }); + + return res.status(200).json({ + participantExtraCheckIn, + message: `${participantExtraCheckIn?.participant?.firstName} ${participantExtraCheckIn?.participant?.lastName} has been successfully checked-in for${participantExtraCheckIn.extra.name} at ${participantExtraCheckIn?.checkedInAt}`, + }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; diff --git a/apps/core-admin/src/controllers/participants.ts b/apps/core-admin/src/controllers/participants.ts index e9c6d0e5..036d4a29 100644 --- a/apps/core-admin/src/controllers/participants.ts +++ b/apps/core-admin/src/controllers/participants.ts @@ -19,10 +19,13 @@ export const addNewParticipant = async (req: Request, res: Response) => { } let attributesToBeAdded: string[] = []; + let extrasToBeAdded: string[] = []; for (const key in participants[0]) { if (key.startsWith('_')) { attributesToBeAdded.push(key.split('_')[1]); + } else if (key.startsWith('&')) { + extrasToBeAdded.push(key.split('&')[1]); } } @@ -34,10 +37,21 @@ export const addNewParticipant = async (req: Request, res: Response) => { }, }); + const extrasAlreadyPresent = await tx.extras.findMany({ + where: { + organizationId: orgId, + eventId, + }, + }); + const newAttributes = attributesToBeAdded.filter( (attribute) => !attributesAlreadyPresent.find((a: any) => a.name === attribute), ); + const newExtras = extrasToBeAdded.filter( + (extra) => !extrasAlreadyPresent.find((e: any) => e.name === extra), + ); + const newAttributesAdded = await tx.attributes.createMany({ data: newAttributes.map((attribute) => { return { @@ -48,6 +62,16 @@ export const addNewParticipant = async (req: Request, res: Response) => { }), }); + const newExtrasAdded = await tx.extras.createMany({ + data: newExtras.map((extra) => { + return { + name: extra, + organizationId: orgId, + eventId, + }; + }), + }); + const attributes = await tx.attributes.findMany({ where: { organizationId: orgId, @@ -55,6 +79,13 @@ export const addNewParticipant = async (req: Request, res: Response) => { }, }); + const extras = await tx.extras.findMany({ + where: { + organizationId: orgId, + eventId, + }, + }); + for (const p of participants) { const newParticipant = await tx.participant.create({ data: { @@ -72,6 +103,13 @@ export const addNewParticipant = async (req: Request, res: Response) => { (attribute: any) => attribute.value !== undefined && attribute.value !== null, ), }, + participantExtras: { + create: extras + .filter((extra: any) => p[`&${extra.name}`] === true) + .map((extra: any) => ({ + extraId: extra.id, + })), + }, }, }); @@ -165,6 +203,7 @@ export const getAllParticipants = async (req: Request, res: Response) => { include: { participantAttributes: true, participantCheckIn: true, + participantExtras: true, }, }); @@ -179,6 +218,7 @@ export const getAllParticipants = async (req: Request, res: Response) => { firstName: participant.firstName, lastName: participant.lastName, numberOfAttributesAssigned: participant.participantAttributes.length, + numnerOfExtrasAssigned: participant.participantExtras.length, checkedIn: participant.participantCheckIn.length > 0 ? true : false, }; }); @@ -271,6 +311,17 @@ export const getParticipantById = async (req: Request, res: Response) => { attribute: true, }, }, + participantExtras: { + include: { + extra: true, + }, + }, + participantExtrasCheckIn: { + select: { + checkedInAt: true, + checkedInByUser: true, + }, + }, }, }); @@ -317,13 +368,77 @@ export const getParticipantById = async (req: Request, res: Response) => { } }); + const extras = await prisma.extras.findMany({ + where: { + organizationId: orgId, + eventId, + }, + }); + + if (!extras) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + extras.forEach((extra: any) => { + const existingExtra = participant?.participantExtras?.find( + (pe: any) => pe.extraId === extra.id, + ); + + if (!existingExtra) { + if (!participant.extras) { + participant.extras = []; + } + + participant.extras.push({ + id: extra.id, + name: extra.name, + assigned: false, + checkedIn: false, + }); + } else { + if (!participant.extras) { + participant.extras = []; + } + + participant.extras.push({ + id: extra.id, + name: extra.name, + assigned: true, + checkIn: { + status: participant.participantExtrasCheckIn.length > 0 ? true : false, + at: + participant.participantExtrasCheckIn.length > 0 + ? participant.participantExtrasCheckIn[0].checkedInAt + : null, + by: { + email: + participant.participantExtrasCheckIn.length > 0 + ? participant.participantExtrasCheckIn[0].checkedInByUser.email + : null, + firstName: + participant.participantExtrasCheckIn.length > 0 + ? participant.participantExtrasCheckIn[0].checkedInByUser.firstName + : null, + lastName: + participant.participantExtrasCheckIn.length > 0 + ? participant.participantExtrasCheckIn[0].checkedInByUser.lastName + : null, + }, + }, + }); + } + }); + participant = { id: participant.id, addedAt: participant.createdAt, firstName: participant.firstName, lastName: participant.lastName, attributes: participant.attributes, + extras: participant.extras, numberOfAttributesAssigned: participant.participantAttributes.length, + numberOfExtrasAssigned: participant.participantExtras.length, + numberOfExtrasCheckedIn: participant.participantExtrasCheckIn.length, checkIn: { status: participant.participantCheckIn.length > 0 ? true : false, checkedInAt: @@ -523,23 +638,33 @@ export const checkOutParticipant = async (req: Request, res: Response) => { const { orgId, eventId, participantId } = req?.params; - const { checkedOutAt } = req?.body; - - const participantAlreadyCheckedOut = await prisma.participantCheckIn.findFirst({ + const isParticipantCheckedIn = await prisma.participantCheckIn.findFirst({ where: { participantId, organizationId: orgId, eventId, }, + include: { + participant: true, + checkedInByUser: true, + }, }); - if (!participantAlreadyCheckedOut) { - return res.status(400).json({ error: 'Participant already checked out' }); + const participant = await prisma.participant.findUnique({ + where: { + id: participantId, + }, + }); + + if (!isParticipantCheckedIn) { + return res.status(400).json({ + error: `${participant?.firstName} ${participant?.lastName} is not checked-in`, + }); } const participantCheckOut = await prisma.participantCheckIn.delete({ where: { - participantId, + id: isParticipantCheckedIn.id, }, }); @@ -547,7 +672,9 @@ export const checkOutParticipant = async (req: Request, res: Response) => { return res.status(500).json({ error: 'Something went wrong' }); } - return res.status(200).json({ participantCheckOut }); + return res.status(200).json({ + message: `${participant?.firstName} ${participant?.lastName} has been successfully checked-out`, + }); } catch (err: any) { console.error(err); return res.status(500).json({ error: 'Something went wrong' }); diff --git a/apps/core-admin/src/routes.ts b/apps/core-admin/src/routes.ts index 707ffe49..27ad9688 100644 --- a/apps/core-admin/src/routes.ts +++ b/apps/core-admin/src/routes.ts @@ -28,6 +28,7 @@ import { } from './controllers/attributes'; import { fetchAccountDetails, updateAccountDetails } from './controllers/users'; import { validateOrganizationUser, validateOrganizationAdmin } from './middlewares/authorization'; +import { addNewExtra, checkInExtra, getAllExtras, getExtraById } from './controllers/extras'; const router: Router = express.Router(); @@ -94,4 +95,9 @@ router.get( router.put('/organizations/:orgId/events/:eventId/attributes/:attributeId', editAttribute); router.post('/organizations/:orgId/events/:eventId/attributes', addNewAttribute); +router.get('/organizations/:orgId/events/:eventId/extras', getAllExtras); +router.get('/organizations/:orgId/events/:eventId/extras/:extraId', getExtraById); +router.post('/organizations/:orgId/events/:eventId/extras/:extraId/check-in', checkInExtra); +router.post('/organizations/:orgId/events/:eventId/extras', addNewExtra); + export default router; diff --git a/apps/web-admin/src/layouts/DashboardLayout.jsx b/apps/web-admin/src/layouts/DashboardLayout.jsx index 17f15b79..bdca9775 100644 --- a/apps/web-admin/src/layouts/DashboardLayout.jsx +++ b/apps/web-admin/src/layouts/DashboardLayout.jsx @@ -92,7 +92,7 @@ export default function DashboardLayout({ {!isMobile && ( - {debugInfo} + {JSON.stringify(debugInfo)} )} diff --git a/apps/web-admin/src/pages/onboarding/verify-email/index.jsx b/apps/web-admin/src/pages/onboarding/verify-email/index.jsx index e8c1740a..ed79f789 100644 --- a/apps/web-admin/src/pages/onboarding/verify-email/index.jsx +++ b/apps/web-admin/src/pages/onboarding/verify-email/index.jsx @@ -17,11 +17,7 @@ export default function VerifyEmail() { }, [user, isAuthenticated, isLoading]); return ( - + Please verify your email to continue using the application. Do not forget to check the diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/index.jsx new file mode 100644 index 00000000..51a71a88 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/index.jsx @@ -0,0 +1,127 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +import { Button, FormControl, FormLabel, Select } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; + +import { useAlert } from '@/hooks/useAlert'; +import { useFetch } from '@/hooks/useFetch'; + +export default function CheckInExtra() { + const { loading, post, get } = useFetch(); + const showAlert = useAlert(); + + const router = useRouter(); + const { orgId, eventId, extraId } = router.query; + + const [participantId, setParticipantId] = useState(null); + const [participants, setParticipants] = useState([]); + + const handleSubmit = async (e) => { + e.preventDefault(); + + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/extras/${extraId}/check-in`, + {}, + { + participantId, + checkedInAt: new Date().toISOString(), + }, + ); + if (status === 200) { + showAlert({ + title: 'Success', + description: 'Extra for participant has been checked in successfully.', + status: 'success', + }); + router.push(`/organizations/${orgId}/events/${eventId}/extras/${extraId}`); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + + useEffect(() => { + const fetchParticipants = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants`, + ); + if (status === 200) { + setParticipants(data.participants); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchParticipants(); + }, [orgId, eventId]); + + return ( + +
+ + Participant ID + + + + First Name + + + + Last Name + + + +
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/scanner/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/scanner/index.jsx new file mode 100644 index 00000000..eb942985 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/check-in/scanner/index.jsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +import { Button, FormControl, FormLabel, Select, Flex } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import Scanner from '@/components/Scanner'; + +import { useAlert } from '@/hooks/useAlert'; +import { useFetch } from '@/hooks/useFetch'; + +export default function CheckInParticipantWithScanner() { + const { loading, post, get } = useFetch(); + const showAlert = useAlert(); + + const router = useRouter(); + const { orgId, eventId, extraId } = router.query; + + const [previousPartiicpantId, setPreviousParticipantId] = useState(null); + const [participantId, setParticipantId] = useState(null); + const [participants, setParticipants] = useState([]); + + const handleSubmit = async () => { + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/extras/${extraId}/check-in`, + {}, + { + participantId, + checkedInAt: new Date().toISOString(), + }, + ); + if (status === 200) { + showAlert({ + title: 'Success', + description: data.message, + status: 'success', + }); + setPreviousParticipantId(participantId); + setParticipantId(null); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + setParticipantId(null); + setPreviousParticipantId(null); + } + }; + + useEffect(() => { + if (participantId && previousPartiicpantId !== participantId) handleSubmit(); + }, [participantId]); + + // + // Periodically clear the previous participant id + // + useEffect(() => { + const intervalId = setInterval(() => { + setPreviousParticipantId(null); + setParticipantId(null); + }, 10000); + + return () => clearInterval(intervalId); + }, [previousPartiicpantId]); + + useEffect(() => { + const fetchParticipants = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants`, + ); + if (status === 200) { + setParticipants(data.participants); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchParticipants(); + }, [orgId, eventId]); + + return ( + + {/*
+ + Participant ID + + + + First Name + + + + Last Name + + + +
*/} + + + +
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/index.jsx new file mode 100644 index 00000000..99363cce --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/[extraId]/index.jsx @@ -0,0 +1,114 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import { Button, Flex } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import DataDisplay from '@/components/DataDisplay'; + +import { useFetch } from '@/hooks/useFetch'; +import { useAlert } from '@/hooks/useAlert'; + +const columns = [ + { field: 'firstName', headerName: 'First Name', width: 200 }, + { field: 'lastName', headerName: 'Last Name', width: 200 }, + { field: 'addedAt', headerName: 'Added At', width: 200 }, + { + field: 'status', + headerName: 'Status', + width: 200, + valueGetter: (params) => params.row?.checkedIn?.status, + }, + { + field: 'at', + headerName: 'Checked In At', + width: 200, + valueGetter: (params) => params.row?.checkedIn?.at, + }, + { + field: 'email', + headerName: 'Checked In By Email', + width: 200, + valueGetter: (params) => params.row?.checkedIn?.by?.email, + }, +]; + +export default function ExtraById() { + const router = useRouter(); + const { orgId, eventId, extraId } = router.query; + const showAlert = useAlert(); + + const { loading, get } = useFetch(); + + const [extra, setExtra] = useState({}); + const [extraDetails, setExtraDetails] = useState([]); + + useEffect(() => { + const fetchExtra = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/extras/${extraId}`, + ); + if (status === 200) { + setExtra(data.extra || {}); + setExtraDetails(data.extra?.participantExtraDetails || []); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchExtra(); + }, []); + + return ( + + + + + + } + debugInfo={extra} + > + { + router.push(`/organizations/${orgId}/events/${eventId}/participants/${row.id}`); + }} + /> + + ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/index.jsx new file mode 100644 index 00000000..6b7e8d30 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/index.jsx @@ -0,0 +1,80 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + +import { Button } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; + +import { useFetch } from '@/hooks/useFetch'; +import { useAlert } from '@/hooks/useAlert'; + +import DataDisplay from '@/components/DataDisplay'; + +const columns = [ + { field: 'name', headerName: 'Name', width: 200 }, + { + field: 'numberOfParticipantsWithExtrasAssigned', + headerName: 'No of Participants Assigned', + width: 200, + }, + { + field: 'numberOfParticipantsWithExtrasCheckedIn', + headerName: 'No of Participants Checked In', + width: 200, + }, +]; + +export default function Extras() { + const router = useRouter(); + const { orgId, eventId } = router.query; + const showAlert = useAlert(); + + const { loading, get } = useFetch(); + + const [extras, setExtras] = useState([]); + + useEffect(() => { + const fetchExtras = async () => { + const { data, status } = await get(`/core/organizations/${orgId}/events/${eventId}/extras`); + if (status === 200) { + setExtras(data.extras || []); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchExtras(); + }, []); + + return ( + + + + } + debugInfo={extras} + > + { + router.push(`/organizations/${orgId}/events/${eventId}/extras/${row.id}`); + }} + /> + + ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/new/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/new/index.jsx new file mode 100644 index 00000000..96392913 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/extras/new/index.jsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +import { Button, FormControl, FormLabel, Input } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; + +import { useAlert } from '@/hooks/useAlert'; +import { useFetch } from '@/hooks/useFetch'; + +export default function NewExtra() { + const { loading, post } = useFetch(); + const showAlert = useAlert(); + + const router = useRouter(); + const { orgId, eventId } = router.query; + + const [name, setName] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/extras`, + {}, + { + name, + }, + ); + if (status === 200) { + showAlert({ + title: 'Success', + description: 'Extra has been added successfully.', + status: 'success', + }); + router.push(`/organizations/${orgId}/events/${eventId}/extras`); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + + return ( + +
+ + Name + { + setName(e.target.value); + }} + /> + + +
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/index.jsx index ed227224..8d22ffc1 100644 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/index.jsx +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/index.jsx @@ -77,6 +77,14 @@ export default function EventById() { > Attributes +
); diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/[participantId]/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/[participantId]/index.jsx index 7119b0c6..c067006b 100644 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/[participantId]/index.jsx +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/[participantId]/index.jsx @@ -10,12 +10,34 @@ import { useAlert } from '@/hooks/useAlert'; import DataDisplay from '@/components/DataDisplay'; -const columns = [ - { field: 'id', headerName: 'ID', width: 200 }, +const attributeColumns = [ { field: 'name', headerName: 'Name', width: 200 }, { field: 'value', headerName: 'Value', width: 200 }, ]; +const extraColumns = [ + { field: 'name', headerName: 'Name', width: 200 }, + { field: 'assigned', headerName: 'Assigned', width: 200 }, + { + field: 'status', + headerName: 'Checked In', + width: 200, + valueGetter: (params) => (params.row?.checkIn?.status ? 'true' : 'false'), + }, + { + field: 'at', + headerName: 'Checked In At', + width: 200, + valueGetter: (params) => params.row?.checkIn?.at, + }, + { + field: 'by', + headerName: 'Checked In By', + width: 200, + valueGetter: (params) => params.row?.checkIn?.by?.email, + }, +]; + export default function ParticipantById() { const router = useRouter(); const { orgId, eventId, participantId } = router.query; @@ -25,6 +47,7 @@ export default function ParticipantById() { const [participant, setParticipant] = useState([]); const [participantAttributes, setParticipantAttributes] = useState([]); + const [participantExtras, setParticipantExtras] = useState([]); const [participantCheckIn, setParticipantCheckIn] = useState({}); useEffect(() => { @@ -35,6 +58,7 @@ export default function ParticipantById() { if (status === 200) { setParticipant(data.participant || []); setParticipantAttributes(data.participant.attributes || []); + setParticipantExtras(data.participant.extras || []); setParticipantCheckIn(data.participant.checkIn || {}); } else { showAlert({ @@ -51,7 +75,7 @@ export default function ParticipantById() { @@ -68,14 +92,10 @@ export default function ParticipantById() { Attributes - {participant.numberOfAttributesAssigned} assigned - { - // router.push(`/organizations/${orgId}/events/${eventId}/attributes/${row.id}`); - // }} - /> + + Extras - {participant.numberOfExtrasAssigned} assigned + Extras - {participant.numberOfExtrasCheckedIn} checked in + ); diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/index.jsx index 106551f7..5cacc011 100644 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/index.jsx +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/index.jsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; -import { Button } from '@chakra-ui/react'; +import { Button, Flex } from '@chakra-ui/react'; import DashboardLayout from '@/layouts/DashboardLayout'; @@ -61,27 +61,53 @@ export default function ParticipantsCheckIn() { previousPage={`/organizations/${orgId}/events/${eventId}/participants`} headerButton={ <> - - + + + + + + + + } - debugInfo={JSON.stringify(participantsCheckIn)} + debugInfo={participantsCheckIn} > { + e.preventDefault(); + + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/participants/check-out/${participantId}`, + {}, + {}, + ); + if (status === 200) { + showAlert({ + title: 'Success', + description: 'Participant has been checked out successfully.', + status: 'success', + }); + router.push(`/organizations/${orgId}/events/${eventId}/participants/check-in`); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + + useEffect(() => { + const fetchParticipants = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants`, + ); + if (status === 200) { + setParticipants(data.participants); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchParticipants(); + }, [orgId, eventId]); + + return ( + +
+ + Participant ID + + + + First Name + + + + Last Name + + + +
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new-out/scanner/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new-out/scanner/index.jsx new file mode 100644 index 00000000..06c4bd10 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new-out/scanner/index.jsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +import { Button, FormControl, FormLabel, Select, Flex } from '@chakra-ui/react'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import Scanner from '@/components/Scanner'; + +import { useAlert } from '@/hooks/useAlert'; +import { useFetch } from '@/hooks/useFetch'; + +export default function CheckInParticipantWithScanner() { + const { loading, post, get } = useFetch(); + const showAlert = useAlert(); + + const router = useRouter(); + const { orgId, eventId } = router.query; + + const [previousPartiicpantId, setPreviousParticipantId] = useState(null); + const [participantId, setParticipantId] = useState(null); + const [participants, setParticipants] = useState([]); + + const handleSubmit = async () => { + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/participants/check-out/${participantId}`, + {}, + {}, + ); + if (status === 200) { + showAlert({ + title: 'Success', + description: data.message, + status: 'success', + }); + setPreviousParticipantId(participantId); + setParticipantId(null); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + setParticipantId(null); + setPreviousParticipantId(null); + } + }; + + useEffect(() => { + if (participantId && previousPartiicpantId !== participantId) handleSubmit(); + }, [participantId]); + + // + // Periodically clear the previous participant id + // + useEffect(() => { + const intervalId = setInterval(() => { + setPreviousParticipantId(null); + setParticipantId(null); + }, 10000); + + return () => clearInterval(intervalId); + }, [previousPartiicpantId]); + + useEffect(() => { + const fetchParticipants = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants`, + ); + if (status === 200) { + setParticipants(data.participants); + } else { + showAlert({ + title: 'Error', + description: data.error, + status: 'error', + }); + } + }; + fetchParticipants(); + }, [orgId, eventId]); + + return ( + + {/*
+ + Participant ID + + + + First Name + + + + Last Name + + + +
*/} + + + +
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/checkout/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/checkout/index.jsx deleted file mode 100644 index 6b294c73..00000000 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/checkout/index.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { useFetch } from '@/hooks/useFetch'; - -import { - Button, - Box, - Card, - CardBody, - FormControl, - FormLabel, - Input, - Flex, - Text, - Select, -} from '@chakra-ui/react'; - -import Scanner from '@/components/Scanner'; - -import { useRouter } from 'next/router'; -import DashboardLayout from '@/layouts/DashboardLayout'; - -export default function NewOrganization() { - const { loading, get, post } = useFetch(); - - const router = useRouter(); - - const { orgId, eventId } = router.query; - - const [uninterruptedScanMode, setUninterruptedScanMode] = useState(true); - const [scanResult, setScanResult] = useState(''); - - useEffect(() => { - if (scanResult) { - handleSubmit(); - } - }, [scanResult]); - - const handleSubmit = async () => { - const { data, status } = await post( - `/core/organizations/${orgId}/events/${eventId}/participants/check-out/${scanResult}`, - {}, - { - checkedInAt: new Date().toISOString(), - }, - ); - if (status === 200) { - if (uninterruptedScanMode) { - console.log(data.participant.firstname, status); - alert('Participant checked out successfully'); - setScanResult(''); - } else { - router.push(`/organizations/${orgId}/events/${eventId}/participants/${scanResult}`); - } - } else { - alert(data.error); - } - }; - - return ( - - - - - Check In Participant - - - - - - {JSON.stringify(scanResult)} - - - - - ); -} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/index.jsx index 9f379d3b..9043b268 100644 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/index.jsx +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/index.jsx @@ -15,6 +15,7 @@ const columns = [ { field: 'lastName', headerName: 'Last Name', width: 200 }, { field: 'checkedIn', headerName: 'CheckedIn', width: 200 }, { field: 'numberOfAttributesAssigned', headerName: 'Attributes Assigned', width: 200 }, + { field: 'numnerOfExtrasAssigned', headerName: 'Extras Assigned', width: 200 }, { field: 'addedAt', headerName: 'Added At', width: 200 }, ]; @@ -69,7 +70,7 @@ export default function Participants() { } - debugInfo={JSON.stringify(participants)} + debugInfo={participants} > column.field !== 'firstName' && column.field !== 'lastName' && - !column.field.startsWith('_'), + !(column.field.startsWith('_') || column.field.startsWith('&')), ) ) { showAlert({ title: 'Error', - description: 'Extra fields should be prefixed with an underscore (_)', + description: + 'Extra attributes should be prefixed with an underscore (_) and extras to be checked-in should be prefixed with an asterisk (&)', status: 'error', duration: 10000, }); @@ -62,7 +63,8 @@ export default function NewParticipantByCSVUpload() { if (columns.find((column) => column.field !== 'firstName' || column.field !== 'lastName')) { showAlert({ title: 'Info', - description: 'Extra fields marked with _ will be inserted as attributes', + description: + 'Extra columns marked with _ will be inserted as attributes and & will be inserted as extras to be checked-in.', status: 'info', duration: 10000, }); @@ -86,7 +88,7 @@ export default function NewParticipantByCSVUpload() { (column) => column.field !== 'firstName' && column.field !== 'lastName' && - !column.field.startsWith('_'), + !(column.field.startsWith('_') || column.field.startsWith('&')), ) ) { showAlert({ @@ -116,7 +118,7 @@ export default function NewParticipantByCSVUpload() { if (status === 200) { showAlert({ title: 'Success', - description: 'Participant has been added successfully.', + description: 'Participants have been added successfully.', status: 'success', }); router.push(`/organizations/${orgId}/events/${eventId}/participants`); @@ -133,7 +135,7 @@ export default function NewParticipantByCSVUpload() { Upload a CSV file of participants. The required columns are firstName, lastName. Extra - columns should be prefixed with an underscore (_). + attributes should be prefixed with an underscore (_) and extras to be checked-in should + be prefixed with and ampersand (&). )} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/index.jsx index 933b397a..f08ced01 100644 --- a/apps/web-admin/src/pages/organizations/[orgId]/events/index.jsx +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/index.jsx @@ -19,6 +19,8 @@ const columns = [ width: 200, }, { field: 'numberOfAttributes', headerName: 'No of Attributes', width: 200 }, + { field: 'numberOfExtras', headerName: 'No of Extras', width: 200 }, + { field: 'createdAt', headerName: 'Created At', width: 200 }, ]; export default function Events() { @@ -62,7 +64,7 @@ export default function Events() { } - debugInfo={JSON.stringify(events)} + debugInfo={events} >