diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a207b1f..029bc3c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,20 +10,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - run: | - docker build -t techno-event-core-admin . + - uses: actions/checkout@v2 + - run: | + docker build -t techno-event-core-admin -f apps/core-admin/Dockerfile . + + - run: | + docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }} + - run: | + docker tag techno-event-core-admin ${{ secrets.DOCKER_USER }}/techno-event-core-admin:latest + docker push ${{ secrets.DOCKER_USER }}/techno-event-core-admin:latest - - run: | - docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }} - - run: | - docker tag techno-event-core-admin ${{ secrets.DOCKER_USER }}/techno-event-core-admin:latest - docker push ${{ secrets.DOCKER_USER }}/techno-event-core-admin:latest - redeploy-backend: needs: build-and-push runs-on: ubuntu-latest steps: - - name: Call deploy hook - run: | - curl -X GET ${{ secrets.BACKEND_DEPLOY_HOOK }} + - name: Call deploy hook + run: | + curl -X GET ${{ secrets.BACKEND_DEPLOY_HOOK }} diff --git a/Dockerfile b/apps/core-admin/Dockerfile similarity index 92% rename from Dockerfile rename to apps/core-admin/Dockerfile index b3f068db..b15551de 100644 --- a/Dockerfile +++ b/apps/core-admin/Dockerfile @@ -6,14 +6,12 @@ WORKDIR /app RUN npm install -g pnpm turbo -COPY . . +COPY ../../. . RUN pnpm install RUN pnpm run build --filter=techno-event-core-admin... -RUN ls -la node_modules - FROM node:18-alpine COPY --from=builder /app/apps/core-admin/dist . diff --git a/apps/core-admin/src/controllers/participants.ts b/apps/core-admin/src/controllers/participants.ts new file mode 100644 index 00000000..cd4a9be1 --- /dev/null +++ b/apps/core-admin/src/controllers/participants.ts @@ -0,0 +1,148 @@ +import { Request, Response } from 'express'; + +import prisma from '../utils/database'; + +export const addNewParticipant = async (req: Request, res: Response) => { + try { + const { orgId, eventId } = req?.params; + const { firstName, lastName } = req?.body; + + const newParticipant = await prisma.participant.create({ + data: { + firstName, + lastName, + organizationId: orgId, + eventId, + }, + }); + + if (!newParticipant) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + return res.status(200).json({ newParticipant }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const getAllParticipants = async (req: Request, res: Response) => { + try { + const { orgId, eventId } = req?.params; + const participants = await prisma.participant.findMany({ + where: { + organizationId: orgId, + eventId, + }, + }); + + if (!participants) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + return res.status(200).json({ participants }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const getAllParticipantsCheckInDetails = async (req: Request, res: Response) => { + try { + const { orgId, eventId } = req?.params; + + const participantsCheckIn = await prisma.participant.findMany({ + where: { + organizationId: orgId, + eventId, + }, + include: { + participantCheckIn: { + select: { + checkedInAt: true, + checkedInByUser: true, + }, + }, + }, + }); + + if (!participantsCheckIn) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + return res.status(200).json({ participantsCheckIn }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const getParticipantById = async (req: Request, res: Response) => { + try { + const { orgId, eventId, participantId } = req?.params; + const participant = await prisma.participant.findUnique({ + where: { + id: participantId, + }, + include: { + participantCheckIn: { + select: { + checkedInAt: true, + checkedInByUser: true, + }, + }, + }, + }); + + if (!participant) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + return res.status(200).json({ participant }); + } catch (err: any) { + console.error(err); + return res.status(500).json({ error: 'Something went wrong' }); + } +}; + +export const checkInParticipant = async (req: Request, res: Response) => { + try { + const userId = req?.auth?.payload?.sub; + + const { orgId, eventId, participantId } = req?.params; + + const { checkedInAt } = req?.body; + + const participantAlreadyCheckedIn = await prisma.participantCheckIn.findFirst({ + where: { + participantId, + organizationId: orgId, + eventId, + }, + }); + + if (participantAlreadyCheckedIn) { + return res.status(400).json({ error: 'Participant already checked in' }); + } + + const participantCheckIn = await prisma.participantCheckIn.create({ + data: { + participantId, + organizationId: orgId, + eventId, + checkedInBy: userId, + checkedInAt, + }, + }); + + if (!participantCheckIn) { + return res.status(500).json({ error: 'Something went wrong' }); + } + + return res.status(200).json({ participantCheckIn }); + } 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 861f2d52..c83f1e71 100644 --- a/apps/core-admin/src/routes.ts +++ b/apps/core-admin/src/routes.ts @@ -1,6 +1,14 @@ import express, { Router } from 'express'; import { createNewOrganization, getUsersOrganizations } from './controllers/organizations'; import { createNewEvent, getEvents } from './controllers/events'; +import { + addNewParticipant, + getAllParticipants, + getParticipantById, + checkInParticipant, + getAllParticipantsCheckInDetails, +} from './controllers/participants'; + const router: Router = express.Router(); router.get('/', (req: any, res: any) => { @@ -18,4 +26,16 @@ router.post('/organizations', createNewOrganization); router.get('/organizations/:orgId/events', getEvents); router.post('/organizations/:orgId/events', createNewEvent); +router.get('/organizations/:orgId/events/:eventId/participants', getAllParticipants); +router.post('/organizations/:orgId/events/:eventId/participants', addNewParticipant); +router.get( + '/organizations/:orgId/events/:eventId/participants/check-in', + getAllParticipantsCheckInDetails, +); +router.get('/organizations/:orgId/events/:eventId/participants/:participantId', getParticipantById); +router.post( + '/organizations/:orgId/events/:eventId/participants/check-in/:participantId', + checkInParticipant, +); + export default router; diff --git a/apps/web-admin/src/components/Scanner/Scanner.jsx b/apps/web-admin/src/components/Scanner.jsx similarity index 76% rename from apps/web-admin/src/components/Scanner/Scanner.jsx rename to apps/web-admin/src/components/Scanner.jsx index 0369f9f3..d78d4b46 100644 --- a/apps/web-admin/src/components/Scanner/Scanner.jsx +++ b/apps/web-admin/src/components/Scanner.jsx @@ -1,11 +1,13 @@ import React, { useState } from 'react'; import { QrReader } from 'react-qr-reader'; import { Text } from '@chakra-ui/react'; + const Scanner = ({ result, setResult }) => { const handleScan = (result) => { - console.log(result); - setResult(result || ''); - //call checkin function + if (result) { + console.log(result); + setResult(result?.text); + } }; const handleError = (err) => { @@ -15,7 +17,6 @@ const Scanner = ({ result, setResult }) => { return (
- {JSON.stringify(result)}
); }; diff --git a/apps/web-admin/src/components/Sidebar.jsx b/apps/web-admin/src/components/Sidebar.jsx index 828419fb..ba8208e1 100644 --- a/apps/web-admin/src/components/Sidebar.jsx +++ b/apps/web-admin/src/components/Sidebar.jsx @@ -12,12 +12,14 @@ const Sidebar = () => { const { logout } = useAuth0(); - const router = useRouter(); - const handleLogout = (e) => { e.preventDefault(); setLoading(true); - logout(); + logout({ + logoutParams: { + returnTo: process.env.NEXT_PUBLIC_AUTH0_REDIRECT_URI, + }, + }); }; 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 new file mode 100644 index 00000000..1a642e5a --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/index.jsx @@ -0,0 +1,12 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +export default function Event() { + const router = useRouter(); + + const { orgId, eventId } = router.query; + + useEffect(() => { + router.push(`/organizations/${orgId}/events/${eventId}/participants`); + }, [orgId, eventId]); +} 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 new file mode 100644 index 00000000..59e22dcd --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/[participantId]/index.jsx @@ -0,0 +1,64 @@ +import { useRouter } from 'next/router'; + +import { + Box, + Flex, + Table, + TableCaption, + Tbody, + Td, + Tfoot, + Th, + Thead, + Tr, + TableContainer, + Text, +} from '@chakra-ui/react'; + +import { useFetch } from '@/hooks/useFetch'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import { useEffect, useState } from 'react'; + +export default function Events() { + const router = useRouter(); + + const { orgId, eventId, participantId } = router.query; + + const { loading, get } = useFetch(); + + const [participant, setParticipant] = useState({}); + + useEffect(() => { + const fetchParticipant = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants/${participantId}`, + ); + setParticipant(data.participant || []); + console.log(data); + }; + fetchParticipant(); + }, [orgId, eventId, participantId]); + + return ( + + + + + Participant Details + + + + {JSON.stringify(participant)} + + + + ); +} 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 new file mode 100644 index 00000000..12cb1789 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/index.jsx @@ -0,0 +1,95 @@ +import { useRouter } from 'next/router'; + +import { + Box, + Flex, + Table, + TableCaption, + Tbody, + Td, + Tfoot, + Th, + Thead, + Tr, + TableContainer, + Text, +} from '@chakra-ui/react'; + +import { useFetch } from '@/hooks/useFetch'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import { useEffect, useState } from 'react'; + +export default function Events() { + const router = useRouter(); + + const { orgId, eventId } = router.query; + + const { loading, get } = useFetch(); + + const [participantsCheckIn, setParticipantsCheckIn] = useState([]); + + useEffect(() => { + const fetchParticipantsCheckIn = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants/check-in`, + ); + setParticipantsCheckIn(data.participantsCheckIn || []); + console.log(data); + }; + fetchParticipantsCheckIn(); + }, [orgId, eventId]); + + return ( + + + + + Participants Check In + + + + + + Participants Check In + + + + + + + + + + + + {participantsCheckIn.map((participant) => ( + + + + + + + + + ))} + + + + + + +
IDFirst NameLast NameStatusCheck In AtChecked In By
{participant?.id}{participant?.firstName}{participant?.lastName}{participant?.participantCheckIn.length > 0 ? 'true' : 'false'}{participant?.participantCheckIn[0]?.checkedInAt}{participant?.participantCheckIn[0]?.checkedInByUser?.email}
{participantsCheckIn.length} participants
+
+
+
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/index.jsx new file mode 100644 index 00000000..0dc30fde --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/index.jsx @@ -0,0 +1,90 @@ +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 { 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 [participantId, setParticipantId] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/participants/check-in/${participantId}`, + {}, + { + checkedInAt: new Date().toISOString(), + }, + ); + if (status === 200) { + router.push(`/organizations/${orgId}/events/${eventId}/participants/${participantId}`); + } else { + alert(data.error); + } + }; + + return ( + + + + + Check In Participant + + + + +
+ + Participant ID + { + setParticipantId(e.target.value); + }} + /> + + +
+
+
+
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/index.jsx new file mode 100644 index 00000000..deb43635 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/check-in/new/scanner/index.jsx @@ -0,0 +1,83 @@ +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-in/${scanResult}`, + {}, + { + checkedInAt: new Date().toISOString(), + }, + ); + if (status === 200) { + if (uninterruptedScanMode) { + alert('Participant checked in 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 new file mode 100644 index 00000000..42f10b54 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/index.jsx @@ -0,0 +1,97 @@ +import { useRouter } from 'next/router'; + +import { + Box, + Flex, + Table, + TableCaption, + Tbody, + Td, + Tfoot, + Th, + Thead, + Tr, + TableContainer, + Text, +} from '@chakra-ui/react'; + +import { useFetch } from '@/hooks/useFetch'; + +import DashboardLayout from '@/layouts/DashboardLayout'; +import { useEffect, useState } from 'react'; + +export default function Events() { + const router = useRouter(); + + const { orgId, eventId } = router.query; + + const { loading, get } = useFetch(); + + const [participants, setParticipants] = useState([]); + + useEffect(() => { + const fetchParticipants = async () => { + const { data, status } = await get( + `/core/organizations/${orgId}/events/${eventId}/participants`, + ); + setParticipants(data.participants || []); + console.log(data); + }; + fetchParticipants(); + }, [orgId, eventId]); + + return ( + + + + + Participants + + + + + + Participants + + + + + + + + + {participants.map((participant) => ( + { + router.push( + `/organizations/${orgId}/events/${eventId}/participants/${participant?.id}`, + ); + }} + cursor="pointer" + > + + + + + ))} + + + + + + +
IDFirst NameLast Name
{participant?.id}{participant?.firstName}{participant?.lastName}
{participants.length} participants
+
+
+
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/new/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/new/index.jsx new file mode 100644 index 00000000..614c22a6 --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/events/[eventId]/participants/new/index.jsx @@ -0,0 +1,103 @@ +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 { 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 [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + const { data, status } = await post( + `/core/organizations/${orgId}/events/${eventId}/participants`, + {}, + { + firstName, + lastName, + }, + ); + if (status === 200) { + router.push(`/organizations/${orgId}/events/${eventId}/participants`); + } else { + alert(data.error); + } + }; + + return ( + + + + + Add new participant + + + + +
+ + Name + { + setFirstName(e.target.value); + }} + /> + + + Last Name + { + setLastName(e.target.value); + }} + /> + + +
+
+
+
+
+ ); +} diff --git a/apps/web-admin/src/pages/organizations/[orgId]/index.jsx b/apps/web-admin/src/pages/organizations/[orgId]/index.jsx new file mode 100644 index 00000000..6936ec1c --- /dev/null +++ b/apps/web-admin/src/pages/organizations/[orgId]/index.jsx @@ -0,0 +1,12 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +export default function Organization() { + const router = useRouter(); + + const { orgId } = router.query; + + useEffect(() => { + router.push(`/organizations/${orgId}/events`); + }, [orgId]); +} diff --git a/apps/web-admin/src/pages/scanner/scanner.jsx b/apps/web-admin/src/pages/scanner/scanner.jsx deleted file mode 100644 index 97f686e2..00000000 --- a/apps/web-admin/src/pages/scanner/scanner.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import Scanner from '@/components/Scanner/Scanner'; -import DashboardLayout from '@/layouts/DashboardLayout'; - -import { useState } from 'react'; - -import { Box, Text, Button, Flex } from '@chakra-ui/react'; - -export default function ScannerPage() { - const [result, setResult] = useState('No result'); - return ( - - - - - - ); -} diff --git a/packages/database/prisma/migrations/20240131154020_y/migration.sql b/packages/database/prisma/migrations/20240201163536_/migration.sql similarity index 100% rename from packages/database/prisma/migrations/20240131154020_y/migration.sql rename to packages/database/prisma/migrations/20240201163536_/migration.sql diff --git a/packages/database/prisma/migrations/20240201165019_/migration.sql b/packages/database/prisma/migrations/20240201165019_/migration.sql new file mode 100644 index 00000000..c3b99015 --- /dev/null +++ b/packages/database/prisma/migrations/20240201165019_/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `firstnName` on the `Participant` table. All the data in the column will be lost. + - Added the required column `firstName` to the `Participant` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Participant" DROP COLUMN "firstnName", +ADD COLUMN "firstName" TEXT NOT NULL; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 9682c216..4268a42b 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -64,13 +64,13 @@ model Participant { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - firstnName String + firstName String lastName String? eventId String @db.Uuid event Event @relation(fields: [eventId], references: [id]) organizationId String organization Organization @relation(fields: [organizationId], references: [id]) - ParticipantCheckin ParticipantCheckIn[] + participantCheckIn ParticipantCheckIn[] } model ParticipantCheckIn {