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 (
+
+
+
+ );
+}
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 (
+
+ {/* */}
+
+
+
+
+ );
+}
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 (
+
+
+
+ );
+}
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 (
+
+
+
+ );
+}
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 (
+
+ {/* */}
+
+
+
+
+ );
+}
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}
>