From 722ab9c9724d15b82a89b1b3b1a76945b7288ab3 Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Sat, 12 Jul 2025 20:21:11 +0300 Subject: [PATCH 1/9] Implmented multi event login screen on the ui --- apps/backend/src/routers/public/index.ts | 6 + .../components/forms/event-selector.tsx | 60 +- .../general/login/admin-login-form.tsx | 8 +- .../components/login/division-login-form.tsx | 127 --- .../components/login/event-login-form.tsx | 79 +- apps/frontend/pages/login.tsx | 96 ++- package-lock.json | 802 +----------------- package.json | 1 + 8 files changed, 209 insertions(+), 970 deletions(-) delete mode 100644 apps/frontend/components/login/division-login-form.tsx diff --git a/apps/backend/src/routers/public/index.ts b/apps/backend/src/routers/public/index.ts index 83610e2..e023a48 100644 --- a/apps/backend/src/routers/public/index.ts +++ b/apps/backend/src/routers/public/index.ts @@ -9,4 +9,10 @@ router.get('/event', (req: Request, res: Response) => { }); }); +router.get('/events', (req: Request, res: Response) => { + db.getAllElectionEvents().then(events => { + res.status(200).json(events); + }); +}); + export default router; diff --git a/apps/frontend/components/forms/event-selector.tsx b/apps/frontend/components/forms/event-selector.tsx index 7a87e61..b7a5f2d 100644 --- a/apps/frontend/components/forms/event-selector.tsx +++ b/apps/frontend/components/forms/event-selector.tsx @@ -4,6 +4,7 @@ import { ElectionEvent } from '@mtes/types'; import { WithId, ObjectId } from 'mongodb'; import { Avatar, ListItemAvatar, ListItemButton, ListItemText, List } from '@mui/material'; import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import HomeIcon from '@mui/icons-material/HomeRounded'; import EventIcon from '@mui/icons-material/EventOutlined'; import { stringifyTwoDates } from '../../lib/utils/dayjs'; import { getBackgroundColor } from '../../lib/utils/theme'; @@ -11,53 +12,38 @@ import { getBackgroundColor } from '../../lib/utils/theme'; interface EventSelectorProps { events: Array>; onChange: (eventId: string | ObjectId) => void; - getEventDisabled?: (event: WithId) => boolean; } -const EventSelector: React.FC = ({ events, onChange, getEventDisabled }) => { +const EventSelector: React.FC = ({ events, onChange }) => { const sortedEvents = useMemo( - () => - events.sort((a, b) => { - const diffA = dayjs().diff(dayjs(a.startDate), 'days', true); - const diffB = dayjs().diff(dayjs(b.startDate), 'days', true); - - if (diffB > 1 && diffA <= 1) return -1; - if (diffA > 1 && diffB <= 1) return 1; - if (diffA > 1 && diffB > 1) return diffA - diffB; - return diffB - diffA; - }), + () => [...events].sort((a, b) => a.name.localeCompare(b.name)), [events] ); return ( {sortedEvents.map(event => { - const disabled = getEventDisabled?.(event); - return ( - onChange(event._id)} - disabled={disabled} - sx={{ borderRadius: 2 }} - component="a" - dense - > - - - - - - - {disabled && } - + + onChange(event._id)} + sx={{ borderRadius: 2 }} + component="a" + dense + > + + + + + + + + ); })} diff --git a/apps/frontend/components/general/login/admin-login-form.tsx b/apps/frontend/components/general/login/admin-login-form.tsx index 243d162..d537b2a 100644 --- a/apps/frontend/components/general/login/admin-login-form.tsx +++ b/apps/frontend/components/general/login/admin-login-form.tsx @@ -4,10 +4,13 @@ import { useSnackbar } from 'notistack'; import { Button, Box, Typography, Stack, TextField } from '@mui/material'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import { apiFetch } from '../../../lib/utils/fetch'; +import { ObjectId } from 'mongodb'; -interface Props {} +interface Props { + eventId?: string | ObjectId; +} -const AdminLoginForm: React.FC = ({}) => { +const AdminLoginForm: React.FC = ({ eventId }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -22,6 +25,7 @@ const AdminLoginForm: React.FC = ({}) => { isAdmin: true, username, password, + eventId: eventId ? String(eventId) : undefined, }), }) .then(async (res) => { diff --git a/apps/frontend/components/login/division-login-form.tsx b/apps/frontend/components/login/division-login-form.tsx deleted file mode 100644 index e814d55..0000000 --- a/apps/frontend/components/login/division-login-form.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useMemo, useState } from 'react'; -import { useRouter } from 'next/router'; -import { useSnackbar } from 'notistack'; -import { WithId } from 'mongodb'; -import { Button, Box, Typography, Stack, MenuItem, TextField } from '@mui/material'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { Role, RoleTypes } from '@mtes/types'; -import FormDropdown from './form-dropdown'; -import { apiFetch } from '../../lib/utils/fetch'; -import { localizedRoles } from '../../localization/roles'; - -interface DivisionLoginFormProps { - votingStands: number; -} - -const DivisionLoginForm: React.FC = ({ votingStands }) => { - const [role, setRole] = useState('' as Role); - const [password, setPassword] = useState(''); - const [association, setAssociation] = useState(); - - const router = useRouter(); - const { enqueueSnackbar } = useSnackbar(); - - const login = () => { - apiFetch('/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - isAdmin: false, - role, - password, - ...(association - ? { - roleAssociation: { - type: 'stand', - value: association - } - } - : undefined) - }) - }) - .then(async res => { - const data = await res.json(); - if (data && !data.error) { - const returnUrl = router.query.returnUrl || `/mtes`; - router.push(returnUrl as string); - } else if (data.error) { - if (data.error === 'INVALID_CREDENTIALS') { - enqueueSnackbar('אופס, הסיסמה שגויה.', { variant: 'error' }); - } else { - enqueueSnackbar('הגישה נדחתה, נסו שנית מאוחר יותר.', { variant: 'error' }); - } - } else { - throw new Error(res.statusText); - } - }) - .catch(() => enqueueSnackbar('אופס, החיבור לשרת נכשל.', { variant: 'error' })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - login(); - }; - - return ( - - - התחברות לאירוע: - - - - { - setRole(e.target.value as Role); - }} - > - {RoleTypes.map((r: Role) => { - return ( - - {localizedRoles[r as Role]} - - ); - })} - - {role === 'voting-stand' && ( - setAssociation(e.target.value)} - > - {Array.from({ length: votingStands }, (_, i) => i + 1).map(stand => ( - - קלפי {stand} - - ))} - - )} - setPassword(e.target.value)} - slotProps={{ htmlInput: { dir: 'ltr' } }} - /> - - - - - - ); -}; - -export default DivisionLoginForm; diff --git a/apps/frontend/components/login/event-login-form.tsx b/apps/frontend/components/login/event-login-form.tsx index 1757a8d..ea43b93 100644 --- a/apps/frontend/components/login/event-login-form.tsx +++ b/apps/frontend/components/login/event-login-form.tsx @@ -1,47 +1,55 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useRouter } from 'next/router'; import { useSnackbar } from 'notistack'; -import { WithId } from 'mongodb'; +import { WithId, ObjectId } from 'mongodb'; import { Button, Box, Typography, Stack, MenuItem, TextField } from '@mui/material'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { Role, ElectionEvent } from '@mtes/types'; -// import FormDropdown from './form-dropdown'; +import { Role, RoleTypes } from '@mtes/types'; +import FormDropdown from './form-dropdown'; import { apiFetch } from '../../lib/utils/fetch'; import { localizedRoles } from '../../localization/roles'; -import FormDropdown from './form-dropdown'; -interface Props { - event: WithId; - onCancel: () => void; +interface DivisionLoginFormProps { + votingStands: number; + eventId?: string | ObjectId; + onCancel?: () => void; } -const EventLoginForm: React.FC = ({ event, onCancel }): JSX.Element => { +const DivisionLoginForm: React.FC = ({ + votingStands, + eventId, + onCancel +}) => { const [role, setRole] = useState('' as Role); const [password, setPassword] = useState(''); - - const loginRoles = Object.keys(event.eventUsers); + const [association, setAssociation] = useState(); const router = useRouter(); const { enqueueSnackbar } = useSnackbar(); - const login = (captchaToken?: string) => { + const login = () => { apiFetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isAdmin: false, - // eventId: event._id, role, password, - ...(captchaToken ? { captchaToken } : {}) + eventId: eventId ? String(eventId) : undefined, + ...(association + ? { + roleAssociation: { + type: 'stand', + value: association + } + } + : undefined) }) }) .then(async res => { const data = await res.json(); if (data && !data.error) { - document.getElementById('recaptcha-script')?.remove(); - document.querySelector('.grecaptcha-badge')?.remove(); const returnUrl = router.query.returnUrl || `/mtes`; router.push(returnUrl as string); } else if (data.error) { @@ -72,9 +80,7 @@ const EventLoginForm: React.FC = ({ event, onCancel }): JSX.Element => { התחברות לאירוע: - - {event.name} - + = ({ event, onCancel }): JSX.Element => { setRole(e.target.value as Role); }} > - {loginRoles - .filter((r): r is Role => r === 'election-manager' || r === 'voting-stand') - .map((r: Role) => { - return ( - - {localizedRoles[r]} - - ); - })} + {RoleTypes.map((r: Role) => { + return ( + + {localizedRoles[r as Role]} + + ); + })} - + {role === 'voting-stand' && ( + setAssociation(e.target.value)} + > + {Array.from({ length: votingStands }, (_, i) => i + 1).map(stand => ( + + קלפי {stand} + + ))} + + )} = ({ event, onCancel }): JSX.Element => { - diff --git a/libs/types/src/lib/schemas/user.ts b/libs/types/src/lib/schemas/user.ts index 7223923..0a409e6 100644 --- a/libs/types/src/lib/schemas/user.ts +++ b/libs/types/src/lib/schemas/user.ts @@ -1,6 +1,8 @@ +import { ObjectId } from 'mongodb'; import { Role } from '../roles'; export interface User { + eventId?: ObjectId; username?: string; isAdmin: boolean; role?: Role; From 3180df6f97c5bb1fdc433ee673056ac1fbd389eb Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Tue, 15 Jul 2025 18:54:14 +0300 Subject: [PATCH 3/9] Routing in admin dash for multi-event --- apps/backend/src/routers/auth.ts | 2 +- apps/frontend/lib/utils/fetch.ts | 3 +- apps/frontend/pages/admin/[eventId].tsx | 430 ++++++++++++++++++++++++ apps/frontend/pages/admin/create.tsx | 323 ++++++++++++++++++ apps/frontend/pages/admin/index.tsx | 397 +++------------------- 5 files changed, 799 insertions(+), 356 deletions(-) create mode 100644 apps/frontend/pages/admin/[eventId].tsx create mode 100644 apps/frontend/pages/admin/create.tsx diff --git a/apps/backend/src/routers/auth.ts b/apps/backend/src/routers/auth.ts index 12b553e..314dc2b 100644 --- a/apps/backend/src/routers/auth.ts +++ b/apps/backend/src/routers/auth.ts @@ -11,7 +11,7 @@ const jwtSecret = process.env.JWT_SECRET; router.post('/login', async (req: Request, res: Response, next: NextFunction) => { const loginDetails: User = req.body; - loginDetails.eventId = new ObjectId(loginDetails.eventId); + if (loginDetails.eventId) loginDetails.eventId = new ObjectId(loginDetails.eventId); try { const user = await db.getUser({ ...loginDetails }); diff --git a/apps/frontend/lib/utils/fetch.ts b/apps/frontend/lib/utils/fetch.ts index 63fa158..ccd8b8d 100644 --- a/apps/frontend/lib/utils/fetch.ts +++ b/apps/frontend/lib/utils/fetch.ts @@ -43,7 +43,8 @@ export const apiFetch = ( export const getUserAndDivision = async (ctx: GetServerSidePropsContext) => { const user: SafeUser = await apiFetch(`/api/me`, undefined, ctx).then(res => res?.json()); - return { user }; + const eventId = user.eventId + return { user, eventId }; }; export const serverSideGetRequests = async ( diff --git a/apps/frontend/pages/admin/[eventId].tsx b/apps/frontend/pages/admin/[eventId].tsx new file mode 100644 index 0000000..d3250bb --- /dev/null +++ b/apps/frontend/pages/admin/[eventId].tsx @@ -0,0 +1,430 @@ +import { GetServerSideProps, NextPage } from 'next'; +import type { GetServerSidePropsContext } from 'next'; +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { + Paper, + Typography, + Stack, + Button, + Box, + Tabs, + Tab, + Dialog, + DialogTitle, + DialogContent, + DialogActions +} from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Error'; +import { enqueueSnackbar } from 'notistack'; +import { Formik, Form, FormikHelpers, getIn } from 'formik'; +import { z } from 'zod'; +import { toFormikValidationSchema } from 'zod-formik-adapter'; +import type { WithId } from 'mongodb'; +import type { ElectionEvent, Member, User, City } from '@mtes/types'; +import { apiFetch, getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; +import Layout from '../../components/layout'; +import UsersTable from '../../components/admin/users-table'; +import EventDetailsForm from '../../components/admin/EventDetailsForm'; +import MembersManagementForm from '../../components/admin/MembersManagementForm'; +import CitiesManagementForm from '../../components/admin/CitiesManagementForm'; +import ChangePasswordDialog from '../../components/admin/ChangePasswordDialog'; + +const memberFormSchema = z.object({ + _id: z.string().optional(), + name: z.string().min(1, 'שם החבר הוא שדה חובה'), + city: z.string().min(1, 'יש לבחור מוסד שולח לחבר'), + isPresent: z.boolean().optional().default(false) +}); + +const createValidationSchema = (isNewEvent: boolean) => + z + .object({ + name: z.string().min(1, 'שם האירוע הוא שדה חובה'), + votingStands: z.coerce + .number({ required_error: 'מספר עמדות הצבעה הוא שדה חובה' }) + .min(1, 'לפחות עמדת הצבעה אחת נדרשת'), + electionThreshold: z.coerce + .number({ required_error: 'אחוז הכשירות הוא שדה חובה' }) + .min(0, 'אחוז הכשירות חייב להיות לפחות 0') + .max(100, 'אחוז הכשירות לא יכול להיות יותר מ-100'), + cities: z.array( + z.object({ + name: z.string().min(1, 'שם המוסד השולח לא יכול להיות ריק'), + numOfVoters: z.coerce.number().min(0, 'מספר המצביעים חייב להיות לפחות 0') + }) + ), + regularMembers: z.array(memberFormSchema), + mmMembers: z.array(memberFormSchema) + }) + .refine(data => !isNewEvent || data.regularMembers.length + data.mmMembers.length > 0, { + message: 'ליצירת אירוע חדש, יש להזין לפחות חבר אחד (נציג או מ"מ)', + path: ['regularMembers'] + }) + .refine( + data => { + const allMembers = [...data.regularMembers, ...data.mmMembers]; + return allMembers.every(member => data.cities.some(city => city.name === member.city)); + }, + { + message: 'חבר אחד או יותר משויך למוסד שולח שאינה קיימת ברשימה', + path: ['regularMembers'] + } + ) + .refine( + data => { + const cityConfigs = data.cities.reduce((acc, city) => { + acc[city.name] = city.numOfVoters; + return acc; + }, {} as Record); + + for (const city of data.cities) { + const regularMembersInCityCount = data.regularMembers.filter( + m => m.city === city.name + ).length; + if (regularMembersInCityCount > (cityConfigs[city.name] || 0)) { + return false; + } + } + return true; + }, + { + message: + 'מספר הנציגים במוסד השולח אינו יכול לעלות על מספר המצביעים שהוגדר לאותה מוסד שולח. יש להעביר חברים עודפים לרשימת ממלאי מקום.', + path: ['regularMembers'] + } + ); + +export type FormValues = z.infer>; + +export interface PageProps { + user: WithId; + event: WithId; + initMembers: WithId[]; + initMMMembers: WithId[]; + initCities: City[]; + credentials: User[]; +} + +const Page: NextPage = ({ + user, + event, + initMembers, + initMMMembers, + initCities, + credentials +}) => { + const router = useRouter(); + const [currentTab, setCurrentTab] = useState(0); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + + const validationSchema = createValidationSchema(false); + + const initialValues: FormValues = { + name: event?.name || '', + votingStands: event?.votingStands || 1, + electionThreshold: event?.electionThreshold || 50, + regularMembers: Array.isArray(initMembers) + ? initMembers + .filter(m => !m.isMM) + .map(m => ({ + _id: m._id.toString(), + name: m.name, + city: m.city, + isPresent: m.isPresent || false + })) + : [], + mmMembers: Array.isArray(initMMMembers) + ? initMMMembers + .filter(m => m.isMM === true) + .map(m => ({ + _id: m._id.toString(), + name: m.name, + city: m.city, + isPresent: m.isPresent || false + })) + : [], + cities: Array.isArray(initCities) ? initCities : [] + }; + + const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { + setSubmitting(true); + const updateEndpoint = async ( + endpoint: string, + method: 'POST' | 'PUT', + body: unknown, + entityName: string + ) => { + const res = await apiFetch(endpoint, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.message || `An error occurred while updating ${entityName}.`); + } + return res.json(); + }; + try { + const eventDetailsPayload = { + name: values.name, + votingStands: values.votingStands, + electionThreshold: values.electionThreshold + }; + await updateEndpoint('/api/admin/events', 'PUT', eventDetailsPayload, 'event details'); + const regularMembersPayload = values.regularMembers.map(m => ({ + ...m, + isMM: false, + isPresent: m.isPresent || false + })); + const mmMembersPayload = values.mmMembers.map(m => ({ + ...m, + isMM: true, + isPresent: m.isPresent || false + })); + await updateEndpoint( + '/api/events/members', + 'PUT', + { members: regularMembersPayload }, + 'members' + ); + await updateEndpoint( + '/api/events/mm-members', + 'PUT', + { mmMembers: mmMembersPayload }, + 'MM members' + ); + await updateEndpoint( + '/api/admin/events/cities', + 'PUT', + { cities: values.cities as City[] }, + 'cities' + ); + enqueueSnackbar('האירוע עודכן בהצלחה', { variant: 'success' }); + router.reload(); + } catch (error: any) { + enqueueSnackbar(error.message || 'אופס, אירעה שגיאה בלתי צפויה.', { + variant: 'error' + }); + } finally { + setSubmitting(false); + } + }; + + const handleDeleteClick = () => { + setDeleteConfirmOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (!event?._id) return; + setDeleteConfirmOpen(false); + try { + const res = await apiFetch(`/api/admin/events/data`, { + method: 'DELETE' + }); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.message || 'An error occurred while deleting the event.'); + } + enqueueSnackbar('האירוע נמחק בהצלחה', { variant: 'success' }); + router.push('/admin'); + } catch (error: any) { + enqueueSnackbar(error.message || 'אופס, אירעה שגיאה בלתי צפויה.', { variant: 'error' }); + } + }; + + const TabLabel = ({ label, hasError }: { label: string; hasError: boolean }) => ( + + {label} + {hasError && } + + ); + + return ( + + + + עריכת אירוע + + + {({ values, errors, touched, isSubmitting, setFieldValue }) => { + const hasEventDetailsError = !!( + (errors.name && touched.name) || + (errors.votingStands && touched.votingStands) || + (errors.electionThreshold && touched.electionThreshold) + ); + const hasMembersError = !!( + (getIn(errors, 'regularMembers') && getIn(touched, 'regularMembers')) || + (getIn(errors, 'mmMembers') && getIn(touched, 'mmMembers')) + ); + const hasCitiesError = !!(errors.cities && touched.cities); + const renderActionButtons = () => ( + + + + + ); + return ( +
+ + setCurrentTab(val)} + centered + sx={{ mb: 2 }} + > + } /> + } /> + } /> + + + + + {currentTab === 0 && } + {currentTab === 1 && ( + + + + {renderActionButtons()} + + )} + {currentTab === 2 && ( + + )} + {currentTab === 3 && ( + + + ניהול משתמשים + + + {renderActionButtons()} + + )} + {currentTab === 4 && ( + + + הגדרות מנהל + + + + + שינוי סיסמה + + + עדכן את סיסמת המנהל שלך למטרות אבטחה + + + + + {renderActionButtons()} + + )} + + ); + }} +
+
+ setPasswordDialogOpen(false)} + /> + setDeleteConfirmOpen(false)} + maxWidth="sm" + fullWidth + > + אישור מחיקת אירוע + + האם אתה בטוח שברצונך למחוק את האירוע? פעולה זו אינה ניתנת לביטול. + + + + + + +
+ ); +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx: GetServerSidePropsContext +) => { + const { user, eventId } = await getUserAndDivision(ctx); + const data = await serverSideGetRequests( + { + user: '/api/me', + event: `/api/events/${eventId}`, + initMembers: `/api/events/${eventId}members`, + initMMMembers: `/api/events/mm-members`, + initCities: `/api/admin/events/cities`, + credentials: `/api/admin/events/users/credentials` + }, + ctx + ); + return { + props: { + user: data.user, + event: data.event ?? null, + initMembers: data.initMembers ?? [], + initMMMembers: data.initMMMembers ?? [], + initCities: data.initCities ?? [], + credentials: data.credentials ?? [] + } + }; +}; + +export default Page; diff --git a/apps/frontend/pages/admin/create.tsx b/apps/frontend/pages/admin/create.tsx new file mode 100644 index 0000000..bb854f3 --- /dev/null +++ b/apps/frontend/pages/admin/create.tsx @@ -0,0 +1,323 @@ +import { GetServerSideProps, NextPage } from 'next'; +import type { GetServerSidePropsContext } from 'next'; +import { useRouter } from 'next/router'; +import React, { useState } from 'react'; +import { + Paper, + Typography, + Stack, + Button, + Box, + Tabs, + Tab, + Dialog, + DialogTitle, + DialogContent, + DialogActions +} from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Error'; +import { enqueueSnackbar } from 'notistack'; +import { Formik, Form, FormikHelpers, getIn } from 'formik'; +import { z } from 'zod'; +import { toFormikValidationSchema } from 'zod-formik-adapter'; +import type { WithId } from 'mongodb'; +import type { ElectionEvent, Member, User, City } from '@mtes/types'; +import { apiFetch, getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; +import Layout from '../../components/layout'; +import UsersTable from '../../components/admin/users-table'; +import EventDetailsForm from '../../components/admin/EventDetailsForm'; +import MembersManagementForm from '../../components/admin/MembersManagementForm'; +import CitiesManagementForm from '../../components/admin/CitiesManagementForm'; +import ChangePasswordDialog from '../../components/admin/ChangePasswordDialog'; + +const memberFormSchema = z.object({ + _id: z.string().optional(), + name: z.string().min(1, 'שם החבר הוא שדה חובה'), + city: z.string().min(1, 'יש לבחור מוסד שולח לחבר'), + isPresent: z.boolean().optional().default(false) +}); + +const createValidationSchema = (isNewEvent: boolean) => + z + .object({ + name: z.string().min(1, 'שם האירוע הוא שדה חובה'), + votingStands: z.coerce + .number({ required_error: 'מספר עמדות הצבעה הוא שדה חובה' }) + .min(1, 'לפחות עמדת הצבעה אחת נדרשת'), + electionThreshold: z.coerce + .number({ required_error: 'אחוז הכשירות הוא שדה חובה' }) + .min(0, 'אחוז הכשירות חייב להיות לפחות 0') + .max(100, 'אחוז הכשירות לא יכול להיות יותר מ-100'), + cities: z.array( + z.object({ + name: z.string().min(1, 'שם המוסד השולח לא יכול להיות ריק'), + numOfVoters: z.coerce.number().min(0, 'מספר המצביעים חייב להיות לפחות 0') + }) + ), + regularMembers: z.array(memberFormSchema), + mmMembers: z.array(memberFormSchema) + }) + .refine(data => !isNewEvent || data.regularMembers.length + data.mmMembers.length > 0, { + message: 'ליצירת אירוע חדש, יש להזין לפחות חבר אחד (נציג או מ"מ)', + path: ['regularMembers'] + }) + .refine( + data => { + const allMembers = [...data.regularMembers, ...data.mmMembers]; + return allMembers.every(member => data.cities.some(city => city.name === member.city)); + }, + { + message: 'חבר אחד או יותר משויך למוסד שולח שאינה קיימת ברשימה', + path: ['regularMembers'] + } + ) + .refine( + data => { + const cityConfigs = data.cities.reduce((acc, city) => { + acc[city.name] = city.numOfVoters; + return acc; + }, {} as Record); + + for (const city of data.cities) { + const regularMembersInCityCount = data.regularMembers.filter( + m => m.city === city.name + ).length; + if (regularMembersInCityCount > (cityConfigs[city.name] || 0)) { + return false; + } + } + return true; + }, + { + message: + 'מספר הנציגים במוסד השולח אינו יכול לעלות על מספר המצביעים שהוגדר לאותה מוסד שולח. יש להעביר חברים עודפים לרשימת ממלאי מקום.', + path: ['regularMembers'] + } + ); + +export type FormValues = z.infer>; + +export interface PageProps { + user: WithId; + initCities: City[]; + credentials: User[]; +} + +const Page: NextPage = ({ user, initCities, credentials }) => { + const router = useRouter(); + const [currentTab, setCurrentTab] = useState(0); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + + const validationSchema = createValidationSchema(true); + + const initialValues: FormValues = { + name: '', + votingStands: 1, + electionThreshold: 50, + regularMembers: [], + mmMembers: [], + cities: Array.isArray(initCities) ? initCities : [] + }; + + const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { + setSubmitting(true); + const updateEndpoint = async ( + endpoint: string, + method: 'POST' | 'PUT', + body: unknown, + entityName: string + ) => { + const res = await apiFetch(endpoint, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.message || `An error occurred while updating ${entityName}.`); + } + return res.json(); + }; + try { + const eventDetailsPayload = { + name: values.name, + votingStands: values.votingStands, + electionThreshold: values.electionThreshold + }; + await updateEndpoint('/api/admin/events', 'POST', eventDetailsPayload, 'event details'); + const regularMembersPayload = values.regularMembers.map(m => ({ + ...m, + isMM: false, + isPresent: m.isPresent || false + })); + const mmMembersPayload = values.mmMembers.map(m => ({ + ...m, + isMM: true, + isPresent: m.isPresent || false + })); + await updateEndpoint( + '/api/events/members', + 'PUT', + { members: regularMembersPayload }, + 'members' + ); + if (mmMembersPayload.length > 0) { + await updateEndpoint( + '/api/events/mm-members', + 'PUT', + { mmMembers: mmMembersPayload }, + 'MM members' + ); + } + await updateEndpoint( + '/api/admin/events/cities', + 'PUT', + { cities: values.cities as City[] }, + 'cities' + ); + enqueueSnackbar('האירוע נוצר בהצלחה', { variant: 'success' }); + router.push('/admin'); + } catch (error: any) { + enqueueSnackbar(error.message || 'אופס, אירעה שגיאה בלתי צפויה.', { + variant: 'error' + }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + יצירת אירוע חדש + + + {({ values, errors, touched, isSubmitting, setFieldValue }) => ( +
+ + setCurrentTab(val)} + centered + sx={{ mb: 2 }} + > + + + + + + + + {currentTab === 0 && <>} />} + {currentTab === 1 && ( + + + + + )} + {currentTab === 2 && ( + <>} + isNewEvent={true} + initCities={initCities} + /> + )} + {currentTab === 3 && ( + + + ניהול משתמשים + + + + )} + {currentTab === 4 && ( + + + הגדרות מנהל + + + + + שינוי סיסמה + + + עדכן את סיסמת המנהל שלך למטרות אבטחה + + + + + + )} + + + + + )} +
+
+ setPasswordDialogOpen(false)} + /> +
+ ); +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx: GetServerSidePropsContext +) => { + const { user } = await getUserAndDivision(ctx); + const data = await serverSideGetRequests( + { + user: '/api/me', + initCities: '/api/admin/events/cities', + credentials: '/api/admin/events/users/credentials' + }, + ctx + ); + return { + props: { + user: data.user, + initCities: data.initCities ?? [], + credentials: data.credentials ?? [] + } + }; +}; + +export default Page; diff --git a/apps/frontend/pages/admin/index.tsx b/apps/frontend/pages/admin/index.tsx index fdb6650..6351ce0 100644 --- a/apps/frontend/pages/admin/index.tsx +++ b/apps/frontend/pages/admin/index.tsx @@ -1,36 +1,22 @@ -import { useState } from 'react'; +import { useRouter } from 'next/router'; import { GetServerSideProps, NextPage } from 'next'; import type { GetServerSidePropsContext } from 'next'; -import { useRouter } from 'next/router'; import { Paper, Typography, Stack, Button, Box, - Tabs, - Tab, - Dialog, - DialogTitle, - DialogContent, - DialogActions + List, + ListItem, + ListItemButton, + ListItemText } from '@mui/material'; -import ErrorIcon from '@mui/icons-material/Error'; -import { enqueueSnackbar } from 'notistack'; -import { Formik, Form, FormikHelpers, getIn } from 'formik'; -import { z } from 'zod'; -import { toFormikValidationSchema } from 'zod-formik-adapter'; - -import type { WithId } from 'mongodb'; -import type { ElectionEvent, Member, User, City } from '@mtes/types'; - -import { apiFetch, serverSideGetRequests } from '../../lib/utils/fetch'; import Layout from '../../components/layout'; -import UsersTable from '../../components/admin/users-table'; -import EventDetailsForm from '../../components/admin/EventDetailsForm'; -import MembersManagementForm from '../../components/admin/MembersManagementForm'; -import CitiesManagementForm from '../../components/admin/CitiesManagementForm'; -import ChangePasswordDialog from '../../components/admin/ChangePasswordDialog'; +import type { WithId } from 'mongodb'; +import type { ElectionEvent, User } from '@mtes/types'; +import { getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; +import z from 'zod'; const memberFormSchema = z.object({ _id: z.string().optional(), @@ -97,341 +83,52 @@ const createValidationSchema = (isNewEvent: boolean) => } ); -export type FormValues = z.infer>; - export interface PageProps { user: WithId; - event?: WithId; - initMembers: WithId[]; // These are regular members (isMM: false or undefined) - initMMMembers: WithId[]; // These are MM members (isMM: true) - initCities: City[]; - credentials: User[]; + events: WithId[]; } -const Page: NextPage = ({ - user, - event, - initMembers, - initMMMembers, - initCities, - credentials -}) => { +const Page: NextPage = ({ user, events }) => { const router = useRouter(); - const [currentTab, setCurrentTab] = useState(0); - const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - - const isNewEvent = !event; - const validationSchema = createValidationSchema(isNewEvent); - - const initialValues: FormValues = { - name: event?.name || '', - votingStands: event?.votingStands || 1, - electionThreshold: event?.electionThreshold || 50, - regularMembers: Array.isArray(initMembers) - ? initMembers - .filter(m => !m.isMM) // Ensure only non-MM members (isMM is false or undefined) - .map(m => ({ - _id: m._id.toString(), - name: m.name, - city: m.city, - isPresent: m.isPresent || false - })) - : [], - mmMembers: Array.isArray(initMMMembers) - ? initMMMembers - .filter(m => m.isMM === true) // Ensure only explicitly MM members - .map(m => ({ - _id: m._id.toString(), - name: m.name, - city: m.city, - isPresent: m.isPresent || false - })) - : [], - cities: Array.isArray(initCities) ? initCities : [] - }; - - const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { - setSubmitting(true); - - const updateEndpoint = async ( - endpoint: string, - method: 'POST' | 'PUT', - body: unknown, - entityName: string - ) => { - const res = await apiFetch(endpoint, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => null); - throw new Error(errorData?.message || `An error occurred while updating ${entityName}.`); - } - return res.json(); - }; - - try { - const eventDetailsPayload = { - name: values.name, - votingStands: values.votingStands, - electionThreshold: values.electionThreshold - }; - await updateEndpoint( - '/api/admin/events', - event ? 'PUT' : 'POST', - eventDetailsPayload, - 'event details' - ); - - const regularMembersPayload = values.regularMembers.map(m => ({ - ...m, - isMM: false, - isPresent: m.isPresent || false - })); - const mmMembersPayload = values.mmMembers.map(m => ({ - ...m, - isMM: true, - isPresent: m.isPresent || false - })); - - await updateEndpoint( - '/api/events/members', - 'PUT', - { members: regularMembersPayload }, - 'members' - ); - - // Send MM members to a different endpoint - if (mmMembersPayload.length > 0 || event) { - // Always send MM members if event exists, even if empty to clear them - await updateEndpoint( - '/api/events/mm-members', - 'PUT', - { mmMembers: mmMembersPayload }, - 'MM members' - ); - } - - await updateEndpoint( - '/api/admin/events/cities', - 'PUT', - { cities: values.cities as City[] }, - 'cities' - ); - - enqueueSnackbar('האירוע נשמר בהצלחה', { variant: 'success' }); - router.reload(); - } catch (error: any) { - enqueueSnackbar(error.message || 'אופס, אירעה שגיאה בלתי צפויה.', { - variant: 'error' - }); - } finally { - setSubmitting(false); - } - }; - const handleDeleteClick = () => { - setDeleteConfirmOpen(true); + const handleEventClick = (eventId: string) => { + router.push(`/admin/${eventId}`); }; - const handleDeleteConfirm = async () => { - if (!event?._id) return; - - setDeleteConfirmOpen(false); - - try { - const res = await apiFetch(`/api/admin/events/data`, { - method: 'DELETE' - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => null); - throw new Error(errorData?.message || 'An error occurred while deleting the event.'); - } - - enqueueSnackbar('האירוע נמחק בהצלחה', { variant: 'success' }); - router.push('/admin'); - } catch (error: any) { - enqueueSnackbar(error.message || 'אופס, אירעה שגיאה בלתי צפויה.', { variant: 'error' }); - } + const handleCreateClick = () => { + router.push('/admin/create'); }; - const TabLabel = ({ label, hasError }: { label: string; hasError: boolean }) => ( - - {label} - {hasError && } - - ); - return ( - + - {event ? 'עריכת אירוע' : 'יצירת אירוע חדש'} + בחר אירוע לניהול - - {({ values, errors, touched, isSubmitting, setFieldValue }) => { - const hasEventDetailsError = !!( - (errors.name && touched.name) || - (errors.votingStands && touched.votingStands) || - (errors.electionThreshold && touched.electionThreshold) - ); - const hasMembersError = !!( - (getIn(errors, 'regularMembers') && getIn(touched, 'regularMembers')) || - (getIn(errors, 'mmMembers') && getIn(touched, 'mmMembers')) - ); - const hasCitiesError = !!(errors.cities && touched.cities); - - const renderActionButtons = () => ( - - - {event && ( - - )} - - ); - - return ( -
- - setCurrentTab(val)} - centered - sx={{ mb: 2 }} - > - } /> - } /> - } /> - - - - - - {currentTab === 0 && } - - {currentTab === 1 && ( - - - - {/* Render action buttons once after both member forms */} - {renderActionButtons()} - - )} - - {currentTab === 2 && ( - - )} - - {currentTab === 3 && ( - - - ניהול משתמשים - - - {renderActionButtons()} - - )} - - {currentTab === 4 && ( - - - הגדרות מנהל - - - - - שינוי סיסמה - - - עדכן את סיסמת המנהל שלך למטרות אבטחה - - - - - {renderActionButtons()} - - )} - - ); - }} -
-
- - setPasswordDialogOpen(false)} - /> - - setDeleteConfirmOpen(false)} - maxWidth="sm" - fullWidth - > - אישור מחיקת אירוע - - האם אתה בטוח שברצונך למחוק את האירוע? פעולה זו אינה ניתנת לביטול. - - - - - - + + + אירועים קיימים: + + {events.length === 0 ? ( + לא נמצאו אירועים. + ) : ( + + {events.map(event => ( + + handleEventClick(event._id.toString())}> + + + + ))} + + )} + + +
); }; @@ -439,26 +136,18 @@ const Page: NextPage = ({ export const getServerSideProps: GetServerSideProps = async ( ctx: GetServerSidePropsContext ) => { + const { user } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { user: '/api/me', - event: '/public/event', - initMembers: '/api/events/members', // Fetches non-MM members - initMMMembers: '/api/events/mm-members', // Fetches MM members - initCities: '/api/admin/events/cities', - credentials: '/api/admin/events/users/credentials' + events: '/public/events' }, ctx ); - return { props: { user: data.user, - event: data.event ?? null, - initMembers: data.initMembers ?? [], - initMMMembers: data.initMMMembers ?? [], // Ensure this is correctly populated - initCities: data.initCities ?? [], - credentials: data.credentials ?? [] + events: data.events ?? [] } }; }; From ae2b28319fee7d98f7beb15e1392e0634bb86bd6 Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Fri, 18 Jul 2025 11:09:33 +0300 Subject: [PATCH 4/9] Remove @mui/system dependency from package.json --- package-lock.json | 802 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 3 +- 2 files changed, 757 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59b4ff2..05d4022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@mui/icons-material": "^6.4.0", "@mui/lab": "^6.0.0-beta.24", "@mui/material": "^6.4.10", - "@mui/system": "^7.2.0", "@mui/x-date-pickers": "^7.24.0", "@types/express-fileupload": "^1.5.1", "@uiw/react-signature": "^1.3.2", @@ -2011,9 +2010,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2746,6 +2745,263 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", @@ -2784,6 +3040,77 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -5034,13 +5361,14 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz", - "integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", + "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -5061,14 +5389,15 @@ } }, "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/types": "^7.4.4", - "@types/prop-types": "^15.7.15", + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.2", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.1.0" @@ -5091,13 +5420,14 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz", - "integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", + "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.6", - "@emotion/cache": "^11.14.0", + "@babel/runtime": "^7.27.1", + "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", @@ -5125,16 +5455,17 @@ } }, "node_modules/@mui/system": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz", - "integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", + "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/private-theming": "^7.2.0", - "@mui/styled-engine": "^7.2.0", - "@mui/types": "^7.4.4", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.27.1", + "@mui/private-theming": "^7.1.0", + "@mui/styled-engine": "^7.1.0", + "@mui/types": "^7.4.2", + "@mui/utils": "^7.1.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -5165,14 +5496,15 @@ } }, "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/types": "^7.4.4", - "@types/prop-types": "^15.7.15", + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.2", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.1.0" @@ -5195,12 +5527,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz", - "integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", + "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.27.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -10401,9 +10733,9 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "license": "MIT" }, "node_modules/@types/qs": { @@ -10884,6 +11216,17 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -12818,6 +13161,14 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -13611,6 +13962,223 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -13867,6 +14435,14 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -13953,6 +14529,20 @@ "node": ">=0.8.0" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", @@ -14169,6 +14759,21 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -15233,7 +15838,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/import-fresh": { @@ -16336,6 +16941,14 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -16349,6 +16962,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -16438,6 +17059,17 @@ "node": ">= 0.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -16678,6 +17310,21 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/license-webpack-plugin": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", @@ -16826,6 +17473,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -16916,7 +17571,7 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -17797,6 +18452,25 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", @@ -18853,6 +19527,17 @@ "dev": true, "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -19621,7 +20306,7 @@ "version": "1.89.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -20084,7 +20769,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -20100,7 +20785,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -21762,6 +22447,20 @@ "node": ">=0.6.x" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -22545,6 +23244,17 @@ "dev": true, "license": "MIT" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 982da26..de3c2e1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "@mui/icons-material": "^6.4.0", "@mui/lab": "^6.0.0-beta.24", "@mui/material": "^6.4.10", - "@mui/system": "^7.2.0", "@mui/x-date-pickers": "^7.24.0", "@types/express-fileupload": "^1.5.1", "@uiw/react-signature": "^1.3.2", @@ -66,4 +65,4 @@ "typescript": "~5.6.2", "webpack-cli": "^5.1.4" } -} +} \ No newline at end of file From d00f52fd5f234edf0a44a1ae7f0a95f8e12745d6 Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Fri, 18 Jul 2025 15:20:29 +0300 Subject: [PATCH 5/9] feat: enhance event management by adding eventId to relevant functions and routes --- apps/backend/src/lib/schedule/cleaner.ts | 33 ++++-- .../src/routers/api/admin/events/cities.ts | 2 +- .../src/routers/api/admin/events/index.ts | 13 ++- apps/backend/src/routers/api/events/index.ts | 34 ++++-- .../backend/src/routers/api/events/members.ts | 4 +- apps/backend/src/routers/public/index.ts | 3 +- apps/frontend/lib/utils/fetch.ts | 5 +- apps/frontend/pages/admin/[eventId].tsx | 30 ++++-- apps/frontend/pages/admin/create.tsx | 100 +++++------------- libs/database/src/lib/crud/cities.ts | 4 +- libs/database/src/lib/crud/contestants.ts | 4 +- libs/database/src/lib/crud/election-events.ts | 12 ++- libs/database/src/lib/crud/election-states.ts | 7 +- libs/database/src/lib/crud/rounds.ts | 4 +- libs/database/src/lib/crud/votes.ts | 8 +- libs/types/src/lib/schemas/member.ts | 1 + 16 files changed, 136 insertions(+), 128 deletions(-) diff --git a/apps/backend/src/lib/schedule/cleaner.ts b/apps/backend/src/lib/schedule/cleaner.ts index 9ae31cb..191e3a5 100644 --- a/apps/backend/src/lib/schedule/cleaner.ts +++ b/apps/backend/src/lib/schedule/cleaner.ts @@ -1,18 +1,31 @@ import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; -export const cleanDivisionData = async () => { - if (!(await db.deleteElectionEvent())) throw new Error('Could not delete event!'); +export const cleanDivisionData = async (eventId: ObjectId) => { + if (!(await db.deleteElectionEvent(eventId))) throw new Error('Could not delete event!'); if (!(await db.deleteUsers( - { isAdmin: { $ne: true } } + { _id: eventId } )).acknowledged) throw new Error('Could not delete users!'); - if (!(await db.deleteElectionState()).acknowledged) { + if (!(await db.deleteElectionState(eventId)).acknowledged) { throw new Error('Could not delete Election state!'); } - if (!(await db.deleteMembers({})).acknowledged) throw new Error('Could not delete members!'); - if (!(await db.deleteContestants()).acknowledged) throw new Error('Could not delete contestant!'); - if (!(await db.deleteRounds()).acknowledged) throw new Error('Could not delete rounds!'); - if (!(await db.deleteVotes()).acknowledged) throw new Error('Could not delete votes!'); - if (!(await db.deleteVotingStatuses()).acknowledged) throw new Error('Could not delete voting statuses!'); - if (!(await db.deleteCities()).acknowledged) throw new Error('Could not delete cities!'); + if (!(await db.deleteMembers({ + _id: eventId + })).acknowledged) throw new Error('Could not delete members!'); + if (!(await db.deleteContestants({ + _id: eventId + })).acknowledged) throw new Error('Could not delete contestant!'); + if (!(await db.deleteRounds( + { eventId } + )).acknowledged) throw new Error('Could not delete rounds!'); + if (!(await db.deleteVotes({ + _id: eventId + })).acknowledged) throw new Error('Could not delete votes!'); + if (!(await db.deleteVotingStatuses({ + _id: eventId + })).acknowledged) throw new Error('Could not delete voting statuses!'); + if (!(await db.deleteCities({ + _id: eventId + })).acknowledged) throw new Error('Could not delete cities!'); }; diff --git a/apps/backend/src/routers/api/admin/events/cities.ts b/apps/backend/src/routers/api/admin/events/cities.ts index ba017ea..5e15bcc 100644 --- a/apps/backend/src/routers/api/admin/events/cities.ts +++ b/apps/backend/src/routers/api/admin/events/cities.ts @@ -76,7 +76,7 @@ router.put( } } - const deleteRes = await db.deleteCities(); + const deleteRes = await db.deleteCities({}); if (!deleteRes.acknowledged) { console.log('❌ Could not delete cities'); res.status(500).json({ ok: false, message: 'Could not delete cities' }); diff --git a/apps/backend/src/routers/api/admin/events/index.ts b/apps/backend/src/routers/api/admin/events/index.ts index 722a0fc..42d63e1 100644 --- a/apps/backend/src/routers/api/admin/events/index.ts +++ b/apps/backend/src/routers/api/admin/events/index.ts @@ -6,6 +6,7 @@ import { ElectionEvent, ElectionState, User, Member } from '@mtes/types'; // Add import * as db from '@mtes/database'; import { cleanDivisionData } from 'apps/backend/src/lib/schedule/cleaner'; import { CreateVotingStandUsers } from 'apps/backend/src/lib/schedule/voting-stands-users'; +import { ObjectId } from 'mongodb'; const randomString = (length: number) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -94,7 +95,7 @@ router.post( ); router.put( - '/', + '/:eventId', asyncHandler(async (req: Request, res: Response) => { const body = req.body; @@ -160,11 +161,12 @@ router.put( ); router.delete( - '/data', + '/:eventId', asyncHandler(async (req: Request, res: Response) => { console.log(`🚮 Deleting data from event`); try { - await cleanDivisionData(); + await cleanDivisionData(new ObjectId(req.params.eventId)); + } catch (error) { res.status(500).json(error.message); return; @@ -174,7 +176,8 @@ router.delete( }) ); -router.use('/users', divisionUsersRouter); -router.use('/cities', citiesRouter); +router.use('/:eventId/users', divisionUsersRouter); + +router.use('/:eventId/cities', citiesRouter); export default router; diff --git a/apps/backend/src/routers/api/events/index.ts b/apps/backend/src/routers/api/events/index.ts index 1523bbd..e16bcba 100644 --- a/apps/backend/src/routers/api/events/index.ts +++ b/apps/backend/src/routers/api/events/index.ts @@ -1,16 +1,38 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; import roundsRouter from './rounds'; import membersRouter from './members'; import stateRouter from './state'; import voteRouter from './vote'; import mmMembersRouter from './mm-members'; +import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); -router.use('/rounds', roundsRouter); -router.use('/members', membersRouter); -router.use('/mm-members', mmMembersRouter); -router.use('/state', stateRouter); -router.use('/vote', voteRouter); + +router.get('/:eventId', async (req: Request, res: Response) => { + const eventId = req.params.eventId; + let event; + try { + event = await db.getElectionEvent(new ObjectId(eventId)); + } catch (err) { + // Invalid ObjectId or db error + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + if (!event) { + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + res.json(event); +}); + +router.use('/:eventId/rounds', roundsRouter); +router.use('/:eventId/members', membersRouter); +router.use('/:eventId/mm-members', mmMembersRouter); +router.use('/:eventId/state', stateRouter); +router.use('/:eventId/vote', voteRouter); + + + + export default router; diff --git a/apps/backend/src/routers/api/events/members.ts b/apps/backend/src/routers/api/events/members.ts index e647ed6..9cad382 100644 --- a/apps/backend/src/routers/api/events/members.ts +++ b/apps/backend/src/routers/api/events/members.ts @@ -7,7 +7,9 @@ const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { console.log('⏬ Getting members...'); - return res.json(await db.getMembers({})); + return res.json(await db.getMembers({ + // eventId: new ObjectId(req.params.eventId) + })); }); router.put('/', async (req: Request, res: Response) => { diff --git a/apps/backend/src/routers/public/index.ts b/apps/backend/src/routers/public/index.ts index e023a48..638b1b6 100644 --- a/apps/backend/src/routers/public/index.ts +++ b/apps/backend/src/routers/public/index.ts @@ -1,10 +1,11 @@ import express, { Request, Response } from 'express'; import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); router.get('/event', (req: Request, res: Response) => { - db.getElectionEvent().then(event => { + db.getElectionEvent(new ObjectId(req.params.eventId)).then(event => { res.status(200).json(event); }); }); diff --git a/apps/frontend/lib/utils/fetch.ts b/apps/frontend/lib/utils/fetch.ts index ccd8b8d..e7d0984 100644 --- a/apps/frontend/lib/utils/fetch.ts +++ b/apps/frontend/lib/utils/fetch.ts @@ -43,7 +43,10 @@ export const apiFetch = ( export const getUserAndDivision = async (ctx: GetServerSidePropsContext) => { const user: SafeUser = await apiFetch(`/api/me`, undefined, ctx).then(res => res?.json()); - const eventId = user.eventId + console.log(`🌐 Fetched user data:`, user); + + const eventId = user.eventId?.toString(); + return { user, eventId }; }; diff --git a/apps/frontend/pages/admin/[eventId].tsx b/apps/frontend/pages/admin/[eventId].tsx index d3250bb..b568cc0 100644 --- a/apps/frontend/pages/admin/[eventId].tsx +++ b/apps/frontend/pages/admin/[eventId].tsx @@ -173,7 +173,12 @@ const Page: NextPage = ({ votingStands: values.votingStands, electionThreshold: values.electionThreshold }; - await updateEndpoint('/api/admin/events', 'PUT', eventDetailsPayload, 'event details'); + await updateEndpoint( + `/api/admin/events/${event._id}`, + 'PUT', + eventDetailsPayload, + 'event details' + ); const regularMembersPayload = values.regularMembers.map(m => ({ ...m, isMM: false, @@ -185,19 +190,19 @@ const Page: NextPage = ({ isPresent: m.isPresent || false })); await updateEndpoint( - '/api/events/members', + `/api/events/${event._id}/members`, 'PUT', { members: regularMembersPayload }, 'members' ); await updateEndpoint( - '/api/events/mm-members', + `/api/events/${event._id}/mm-members`, 'PUT', { mmMembers: mmMembersPayload }, 'MM members' ); await updateEndpoint( - '/api/admin/events/cities', + `/api/admin/events/${event._id}/cities`, 'PUT', { cities: values.cities as City[] }, 'cities' @@ -218,10 +223,12 @@ const Page: NextPage = ({ }; const handleDeleteConfirm = async () => { + console.log(`🌐 Deleting event with ID: ${event?._id}`); + if (!event?._id) return; setDeleteConfirmOpen(false); try { - const res = await apiFetch(`/api/admin/events/data`, { + const res = await apiFetch(`/api/admin/events/${event._id}`, { method: 'DELETE' }); if (!res.ok) { @@ -403,18 +410,21 @@ const Page: NextPage = ({ export const getServerSideProps: GetServerSideProps = async ( ctx: GetServerSidePropsContext ) => { - const { user, eventId } = await getUserAndDivision(ctx); + const eventId = ctx.params?.eventId; const data = await serverSideGetRequests( { user: '/api/me', event: `/api/events/${eventId}`, - initMembers: `/api/events/${eventId}members`, - initMMMembers: `/api/events/mm-members`, - initCities: `/api/admin/events/cities`, - credentials: `/api/admin/events/users/credentials` + initMembers: `/api/events/${eventId}/members`, + initMMMembers: `/api/events/${eventId}/mm-members`, + initCities: `/api/admin/events/${eventId}/cities`, + credentials: `/api/admin/events/${eventId}/users/credentials` }, ctx ); + + console.log('event : ', data.event); + return { props: { user: data.user, diff --git a/apps/frontend/pages/admin/create.tsx b/apps/frontend/pages/admin/create.tsx index bb854f3..a1ae32a 100644 --- a/apps/frontend/pages/admin/create.tsx +++ b/apps/frontend/pages/admin/create.tsx @@ -97,13 +97,7 @@ const createValidationSchema = (isNewEvent: boolean) => export type FormValues = z.infer>; -export interface PageProps { - user: WithId; - initCities: City[]; - credentials: User[]; -} - -const Page: NextPage = ({ user, initCities, credentials }) => { +const Page: NextPage = () => { const router = useRouter(); const [currentTab, setCurrentTab] = useState(0); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); @@ -116,7 +110,7 @@ const Page: NextPage = ({ user, initCities, credentials }) => { electionThreshold: 50, regularMembers: [], mmMembers: [], - cities: Array.isArray(initCities) ? initCities : [] + cities: [] }; const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { @@ -144,16 +138,25 @@ const Page: NextPage = ({ user, initCities, credentials }) => { votingStands: values.votingStands, electionThreshold: values.electionThreshold }; - await updateEndpoint('/api/admin/events', 'POST', eventDetailsPayload, 'event details'); + const eventId = await updateEndpoint( + '/api/admin/events', + 'POST', + eventDetailsPayload, + 'event details' + ); + console.log(`Event created with ID: ${eventId}`); + const regularMembersPayload = values.regularMembers.map(m => ({ ...m, isMM: false, - isPresent: m.isPresent || false + isPresent: m.isPresent || false, + eventId })); const mmMembersPayload = values.mmMembers.map(m => ({ ...m, isMM: true, - isPresent: m.isPresent || false + isPresent: m.isPresent || false, + eventId })); await updateEndpoint( '/api/events/members', @@ -207,14 +210,21 @@ const Page: NextPage = ({ user, initCities, credentials }) => { sx={{ mb: 2 }} > - - - +
{currentTab === 0 && <>} />} {currentTab === 1 && ( + <>} + isNewEvent={true} + initCities={values.cities} + /> + )} + {currentTab === 2 && ( = ({ user, initCities, credentials }) => { /> )} - {currentTab === 2 && ( - <>} - isNewEvent={true} - initCities={initCities} - /> - )} - {currentTab === 3 && ( - - - ניהול משתמשים - - - - )} - {currentTab === 4 && ( - - - הגדרות מנהל - - - - - שינוי סיסמה - - - עדכן את סיסמת המנהל שלך למטרות אבטחה - - - - - - )} = ({ user, initCities, credentials }) => { ); }; -export const getServerSideProps: GetServerSideProps = async ( - ctx: GetServerSidePropsContext -) => { - const { user } = await getUserAndDivision(ctx); - const data = await serverSideGetRequests( - { - user: '/api/me', - initCities: '/api/admin/events/cities', - credentials: '/api/admin/events/users/credentials' - }, - ctx - ); - return { - props: { - user: data.user, - initCities: data.initCities ?? [], - credentials: data.credentials ?? [] - } - }; -}; - export default Page; diff --git a/libs/database/src/lib/crud/cities.ts b/libs/database/src/lib/crud/cities.ts index 27cdb20..6de4a2e 100644 --- a/libs/database/src/lib/crud/cities.ts +++ b/libs/database/src/lib/crud/cities.ts @@ -27,6 +27,6 @@ export const deleteCity = (filter: Filter) => { return db.collection('cities').deleteOne(filter); }; -export const deleteCities = () => { - return db.collection('cities').deleteMany({}); +export const deleteCities = (filter: Filter) => { + return db.collection('cities').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/contestants.ts b/libs/database/src/lib/crud/contestants.ts index 32cd483..cbe61fa 100644 --- a/libs/database/src/lib/crud/contestants.ts +++ b/libs/database/src/lib/crud/contestants.ts @@ -26,6 +26,6 @@ export const deleteContestant = (filter: Filter) => { return db.collection('contestants').deleteOne(filter); }; -export const deleteContestants = () => { - return db.collection('contestants').deleteMany(); +export const deleteContestants = (filter: Filter) => { + return db.collection('contestants').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/election-events.ts b/libs/database/src/lib/crud/election-events.ts index 7276cbf..6c4c800 100644 --- a/libs/database/src/lib/crud/election-events.ts +++ b/libs/database/src/lib/crud/election-events.ts @@ -1,9 +1,9 @@ -import { WithId, AggregationCursor, Filter } from 'mongodb'; +import { WithId, AggregationCursor, Filter, ObjectId } from 'mongodb'; import { ElectionEvent } from '@mtes/types'; import db from '../database'; -export const getElectionEvent = () => { - return findElectionEvents({}).next(); +export const getElectionEvent = (eventId: ObjectId) => { + return findElectionEvents({ _id: eventId }).next(); }; export const findElectionEvents = (filter: Filter) => { @@ -42,6 +42,8 @@ export const addElectionEvent = (ElectionEvent: ElectionEvent) => { return db.collection('election-events').insertOne(ElectionEvent); }; -export const deleteElectionEvent = () => { - return db.collection('election-events').drop(); +export const deleteElectionEvent = (eventId: ObjectId) => { + return db.collection('election-events').deleteOne({ + _id: eventId + }); }; diff --git a/libs/database/src/lib/crud/election-states.ts b/libs/database/src/lib/crud/election-states.ts index 4e03ef9..9f512b4 100644 --- a/libs/database/src/lib/crud/election-states.ts +++ b/libs/database/src/lib/crud/election-states.ts @@ -1,5 +1,6 @@ import { ElectionState } from '@mtes/types'; import db from '../database'; +import { ObjectId } from 'mongodb'; export const getElectionState = () => { return db.collection('election-state').findOne(); @@ -17,6 +18,8 @@ export const updateElectionState = (newElectionState: Partial, up .updateOne({}, { $set: newElectionState }, { upsert }); }; -export const deleteElectionState = () => { - return db.collection('election-state').deleteOne({}); +export const deleteElectionState = (eventId: ObjectId) => { + return db.collection('election-state').deleteOne({ + _id: eventId + }); }; diff --git a/libs/database/src/lib/crud/rounds.ts b/libs/database/src/lib/crud/rounds.ts index 12ec19b..71e68ef 100644 --- a/libs/database/src/lib/crud/rounds.ts +++ b/libs/database/src/lib/crud/rounds.ts @@ -26,6 +26,6 @@ export const deleteRound = (filter: Filter) => { return db.collection('rounds').deleteOne(filter); }; -export const deleteRounds = () => { - return db.collection('rounds').deleteMany(); +export const deleteRounds = (filter: Filter) => { + return db.collection('rounds').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/votes.ts b/libs/database/src/lib/crud/votes.ts index c69ea52..5e972ca 100644 --- a/libs/database/src/lib/crud/votes.ts +++ b/libs/database/src/lib/crud/votes.ts @@ -69,13 +69,13 @@ export async function deleteRoundVotes(roundId: string) { } // Delete all votes -export const deleteVotes = () => { - return db.collection('votes').deleteMany({}); +export const deleteVotes = (filter: Filter) => { + return db.collection('votes').deleteMany(filter); }; // Delete all voting statuses -export const deleteVotingStatuses = () => { - return db.collection('votingStatus').deleteMany({}); +export const deleteVotingStatuses = (filter: Filter) => { + return db.collection('votingStatus').deleteMany(filter); }; // Check if round is locked diff --git a/libs/types/src/lib/schemas/member.ts b/libs/types/src/lib/schemas/member.ts index 39fb121..89393f2 100644 --- a/libs/types/src/lib/schemas/member.ts +++ b/libs/types/src/lib/schemas/member.ts @@ -1,6 +1,7 @@ import { ObjectId, WithId } from 'mongodb'; import { Cities } from '../cities'; export interface Member { + eventId: ObjectId; name: string; city: Cities | 'אין אמון באף אחד'; isPresent: boolean; From 08737ccc922ffa28787fc01d2b409826627dfe05 Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Sun, 20 Jul 2025 17:15:57 +0300 Subject: [PATCH 6/9] feat: add eventId to various event-related functions and routes for improved event management --- .../src/routers/api/admin/events/index.ts | 11 ++++++-- .../src/routers/api/admin/events/users.ts | 10 +++++-- .../backend/src/routers/api/events/members.ts | 15 ++++++++--- .../src/routers/api/events/mm-members.ts | 24 ++++++++++++++--- apps/backend/src/routers/api/events/rounds.ts | 26 ++++++++++++++++--- apps/backend/src/routers/api/events/state.ts | 12 ++++++++- apps/backend/src/routers/api/events/vote.ts | 6 ++--- apps/frontend/pages/admin/create.tsx | 9 ++++--- libs/database/src/lib/crud/election-states.ts | 6 ++--- libs/database/src/lib/crud/members.ts | 2 ++ libs/database/src/lib/crud/mm-members.ts | 4 ++- libs/database/src/lib/crud/users.ts | 8 +++--- libs/types/src/lib/schemas/election-state.ts | 3 ++- libs/types/src/lib/schemas/round.ts | 3 ++- 14 files changed, 106 insertions(+), 33 deletions(-) diff --git a/apps/backend/src/routers/api/admin/events/index.ts b/apps/backend/src/routers/api/admin/events/index.ts index 42d63e1..1a5bf95 100644 --- a/apps/backend/src/routers/api/admin/events/index.ts +++ b/apps/backend/src/routers/api/admin/events/index.ts @@ -19,8 +19,9 @@ const randomString = (length: number) => { const router = express.Router({ mergeParams: true }); -function getInitialDivisionState(): ElectionState { +function getInitialDivisionState(eventId: ObjectId): ElectionState { return { + eventId: eventId, activeRound: null, completed: false, audienceDisplay: { @@ -69,7 +70,7 @@ router.post( console.log('✅ Created Event!'); console.log('🔐 Creating division state'); - if (!(await db.addElectionState(getInitialDivisionState())).acknowledged) { + if (!(await db.addElectionState(getInitialDivisionState(eventId))).acknowledged) { throw new Error('Could not create division state!'); } console.log('✅ Created division state'); @@ -82,8 +83,14 @@ router.post( // } console.log('👤 Generating division users'); + + console.log(`Creating voting stand users for ${eventData.votingStands} stands with event ID: ${eventId}`); + const users = CreateVotingStandUsers(eventData.votingStands, eventId); + console.log('users:', users); + + if (!(await db.addUsers(users)).acknowledged) { res.status(500).json({ error: 'Could not create users!' }); return; diff --git a/apps/backend/src/routers/api/admin/events/users.ts b/apps/backend/src/routers/api/admin/events/users.ts index 8392b11..695a1ec 100644 --- a/apps/backend/src/routers/api/admin/events/users.ts +++ b/apps/backend/src/routers/api/admin/events/users.ts @@ -7,7 +7,7 @@ import * as db from '@mtes/database'; const router = express.Router({ mergeParams: true }); router.get('/', (req: Request, res: Response) => { - db.getEventUsers().then(users => { + db.getEventUsers(new ObjectId(req.params.eventId)).then(users => { return res.json(users); }); }); @@ -15,7 +15,13 @@ router.get('/', (req: Request, res: Response) => { router.get( '/credentials', asyncHandler(async (req: Request, res: Response) => { - const usersWithAdmin = await db.getEventUsersWithCredentials(); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Event ID is missing' }); + return; + } + const usersWithAdmin = await db.getEventUsersWithCredentials(new ObjectId(eventId)); const users = usersWithAdmin.filter(user => !user.isAdmin); res.json(users); diff --git a/apps/backend/src/routers/api/events/members.ts b/apps/backend/src/routers/api/events/members.ts index 9cad382..f92934e 100644 --- a/apps/backend/src/routers/api/events/members.ts +++ b/apps/backend/src/routers/api/events/members.ts @@ -8,13 +8,18 @@ const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { console.log('⏬ Getting members...'); return res.json(await db.getMembers({ - // eventId: new ObjectId(req.params.eventId) + eventId: new ObjectId(req.params.eventId) })); }); router.put('/', async (req: Request, res: Response) => { - const { members } = req.body as { members: Member[] }; + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + const { members } = req.body as { members: Member[] }; if (!members || members.length === 0) { console.log('❌ Members array is empty'); res.status(400).json({ ok: false, message: 'No members provided' }); @@ -23,14 +28,16 @@ router.put('/', async (req: Request, res: Response) => { console.log('⏬ Updating Members...'); - const deleteRes = await db.deleteMembers({}); + const deleteRes = await db.deleteMembers({ + eventId: new ObjectId(eventId) + }); if (!deleteRes.acknowledged) { console.log('❌ Could not delete members'); res.status(500).json({ ok: false, message: 'Could not delete members' }); return; } - const addRes = await db.addMembers(members.map(member => ({ ...member, _id: undefined }))); + const addRes = await db.addMembers(members.map(member => ({ ...member, eventId: new ObjectId(eventId) }))); if (!addRes.acknowledged) { console.log('❌ Could not add members'); res.status(500).json({ ok: false, message: 'Could not add members' }); diff --git a/apps/backend/src/routers/api/events/mm-members.ts b/apps/backend/src/routers/api/events/mm-members.ts index d365eb2..6d45f6f 100644 --- a/apps/backend/src/routers/api/events/mm-members.ts +++ b/apps/backend/src/routers/api/events/mm-members.ts @@ -6,11 +6,27 @@ import { Member } from '@mtes/types'; const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { + + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + console.log('⏬ Getting mm-members...'); - return res.json(await db.getMmMembers({})); + return res.json(await db.getMmMembers({ + eventId: new ObjectId(eventId) + })); }); router.put('/', async (req: Request, res: Response) => { + + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + const { mmMembers } = req.body as { mmMembers: Member[] } || { mmMembers: [] }; console.log('⏬ Updating mm-members...'); @@ -21,14 +37,16 @@ router.put('/', async (req: Request, res: Response) => { } try { - const deleteRes = await db.deleteMmMembers({}); + const deleteRes = await db.deleteMmMembers({ + eventId: new ObjectId(eventId) + }); if (!deleteRes.acknowledged) { console.log('❌ Could not delete mm-members'); return res.status(500).json({ ok: false, message: 'Could not delete mm-members' }); } if (mmMembers.length > 0) { - const addRes = await db.addMmMembers(mmMembers.map(member => ({ ...member, _id: undefined }))); + const addRes = await db.addMmMembers(mmMembers.map(member => ({ ...member, eventId: new ObjectId(eventId) }))); if (!addRes.acknowledged) { console.log('❌ Could not add mm-members'); return res.status(500).json({ ok: false, message: 'Could not add mm-members' }); diff --git a/apps/backend/src/routers/api/events/rounds.ts b/apps/backend/src/routers/api/events/rounds.ts index ad6317b..a82f80e 100644 --- a/apps/backend/src/routers/api/events/rounds.ts +++ b/apps/backend/src/routers/api/events/rounds.ts @@ -5,12 +5,13 @@ import { Member, Round } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -const genrateWhireVoteMembers = (numWhiteVotes: number): WithId[] => { +const genrateWhireVoteMembers = (numWhiteVotes: number, eventId: ObjectId): WithId[] => { const whiteVotes: WithId[] = []; for (let i = 0; i < numWhiteVotes; i++) { whiteVotes.push( { _id: new ObjectId(`00000000000000000000000${i + 1}`), + eventId: eventId, name: `פתק לבן ${i + 1}`, city: 'אין אמון באף אחד', isPresent: true, @@ -21,12 +22,29 @@ const genrateWhireVoteMembers = (numWhiteVotes: number): WithId[] => { return whiteVotes; } router.get('/', async (req: Request, res: Response) => { + + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + console.log('⏬ Getting rounds...'); - return res.json(await db.getRounds({})); + return res.json(await db.getRounds({ + eventId: new ObjectId(eventId) + })); }); router.post('/add', async (req: Request, res: Response) => { + + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + const { round } = req.body; + round.eventId = new ObjectId(eventId); console.log('⏬ Adding Round...', JSON.stringify(round, null, 2)); @@ -75,7 +93,7 @@ router.post('/add', async (req: Request, res: Response) => { }) ); if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes)); + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, round.eventId)); } return { ...role, contestants }; }) @@ -190,7 +208,7 @@ router.put('/update', async (req: Request, res: Response) => { }) ); if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes)); + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, existingRound.eventId)); } return { ...role, contestants }; }) diff --git a/apps/backend/src/routers/api/events/state.ts b/apps/backend/src/routers/api/events/state.ts index 6b76a71..3bc2a8f 100644 --- a/apps/backend/src/routers/api/events/state.ts +++ b/apps/backend/src/routers/api/events/state.ts @@ -1,12 +1,22 @@ import express, { Request, Response } from 'express'; import * as db from '@mtes/database'; import { ElectionState } from '@mtes/types'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); router.get('/', (req: Request, res: Response) => { + + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + console.log(`⏬ Getting Election state`); - db.getElectionState().then(divisionState => res.json(divisionState)); + db.getElectionState({ + eventId: new ObjectId(eventId) + }).then(divisionState => res.json(divisionState)); }); router.put('/', (req: Request, res: Response) => { diff --git a/apps/backend/src/routers/api/events/vote.ts b/apps/backend/src/routers/api/events/vote.ts index 6fe5d5a..e93aca8 100644 --- a/apps/backend/src/routers/api/events/vote.ts +++ b/apps/backend/src/routers/api/events/vote.ts @@ -5,9 +5,10 @@ import { Member, Positions } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -const getWhiteVoteMember = (id: string): WithId => { +const getWhiteVoteMember = (id: string, eventId?: string): WithId => { return { _id: new ObjectId(id), + eventId: eventId ? new ObjectId(eventId) : new ObjectId(), // Provide eventId or fallback name: `פתק לבן ${Number(id)}`, city: 'אין אמון באף אחד', isPresent: true, @@ -56,10 +57,9 @@ router.post('/', async (req: Request, res: Response) => { return Promise.all( contestantIds.map(async (contestantId: string) => { console.log(`⏬ Processing vote for role ${role} and contestant ID ${contestantId}`); - const contestant = contestantId.startsWith('00000000000000000000') // 20 times '0' for white vote as a white vote id is 24(last 4 characters are for id) - ? getWhiteVoteMember(contestantId) + ? getWhiteVoteMember(contestantId, round?.eventId?.toString()) : await db.getMember({ _id: new ObjectId(contestantId) }); if (!contestant) { diff --git a/apps/frontend/pages/admin/create.tsx b/apps/frontend/pages/admin/create.tsx index a1ae32a..da3b1d3 100644 --- a/apps/frontend/pages/admin/create.tsx +++ b/apps/frontend/pages/admin/create.tsx @@ -138,12 +138,13 @@ const Page: NextPage = () => { votingStands: values.votingStands, electionThreshold: values.electionThreshold }; - const eventId = await updateEndpoint( + const eventUpdateRes = await updateEndpoint( '/api/admin/events', 'POST', eventDetailsPayload, 'event details' ); + const eventId = eventUpdateRes.id as string; console.log(`Event created with ID: ${eventId}`); const regularMembersPayload = values.regularMembers.map(m => ({ @@ -159,21 +160,21 @@ const Page: NextPage = () => { eventId })); await updateEndpoint( - '/api/events/members', + `/api/events/${eventId}/members`, 'PUT', { members: regularMembersPayload }, 'members' ); if (mmMembersPayload.length > 0) { await updateEndpoint( - '/api/events/mm-members', + `/api/events/${eventId}/mm-members`, 'PUT', { mmMembers: mmMembersPayload }, 'MM members' ); } await updateEndpoint( - '/api/admin/events/cities', + `/api/admin/events/${eventId}/cities`, 'PUT', { cities: values.cities as City[] }, 'cities' diff --git a/libs/database/src/lib/crud/election-states.ts b/libs/database/src/lib/crud/election-states.ts index 9f512b4..4bc1c19 100644 --- a/libs/database/src/lib/crud/election-states.ts +++ b/libs/database/src/lib/crud/election-states.ts @@ -1,9 +1,9 @@ import { ElectionState } from '@mtes/types'; import db from '../database'; -import { ObjectId } from 'mongodb'; +import { Filter, ObjectId } from 'mongodb'; -export const getElectionState = () => { - return db.collection('election-state').findOne(); +export const getElectionState = (filter: Filter) => { + return db.collection('election-state').findOne(filter); }; export const addElectionState = (state: ElectionState) => { diff --git a/libs/database/src/lib/crud/members.ts b/libs/database/src/lib/crud/members.ts index b7352bc..2f968a6 100644 --- a/libs/database/src/lib/crud/members.ts +++ b/libs/database/src/lib/crud/members.ts @@ -17,9 +17,11 @@ export const addMember = (team: Member) => { export const addMembers = (members: Array) => { const validMembers = members.map(member => { return { + eventId: member.eventId, name: member.name, city: member.city, isPresent: member.isPresent ?? false, + replacedBy: member.replacedBy ?? null, isMM: member.isMM ?? false }; } diff --git a/libs/database/src/lib/crud/mm-members.ts b/libs/database/src/lib/crud/mm-members.ts index 6e93fdc..6b880cc 100644 --- a/libs/database/src/lib/crud/mm-members.ts +++ b/libs/database/src/lib/crud/mm-members.ts @@ -17,10 +17,12 @@ export const addMmMember = (mmMember: Member) => { export const addMmMembers = (mmMembers: Array) => { const validMmMembers = mmMembers.map(mmMember => { return { + eventId: mmMember.eventId, name: mmMember.name, city: mmMember.city, isPresent: mmMember.isPresent ?? false, - isMM: mmMember.isMM ?? false + isMM: mmMember.isMM ?? false, + replacedBy: mmMember.replacedBy ?? null }; }); return db.collection('mm-members').insertMany(validMmMembers); diff --git a/libs/database/src/lib/crud/users.ts b/libs/database/src/lib/crud/users.ts index 402cfce..663ae89 100644 --- a/libs/database/src/lib/crud/users.ts +++ b/libs/database/src/lib/crud/users.ts @@ -2,12 +2,12 @@ import { ObjectId, Filter, WithId } from 'mongodb'; import { User, SafeUser } from '@mtes/types'; import db from '../database'; -export const getEventUsersWithCredentials = () => { - return db.collection('users').find().toArray(); +export const getEventUsersWithCredentials = (eventId: ObjectId) => { + return db.collection('users').find({ eventId }).toArray(); }; -export const getEventUsers = (): Promise>> => { - return getEventUsersWithCredentials().then(users => { +export const getEventUsers = (eventId: ObjectId): Promise>> => { + return getEventUsersWithCredentials(eventId).then(users => { return users.map(user => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password, lastPasswordSetDate, ...safeUser } = user; diff --git a/libs/types/src/lib/schemas/election-state.ts b/libs/types/src/lib/schemas/election-state.ts index 151e63b..d35a08f 100644 --- a/libs/types/src/lib/schemas/election-state.ts +++ b/libs/types/src/lib/schemas/election-state.ts @@ -1,9 +1,10 @@ -import { WithId } from 'mongodb'; +import { ObjectId, WithId } from 'mongodb'; import { AudienceDisplayScreen } from '../constants'; import { Round } from './round'; import { Member } from './member'; export interface ElectionState { + eventId: ObjectId; activeRound: WithId; audienceDisplay: { display: AudienceDisplayScreen; round?: WithId; member?: WithId; message?: string }; completed: boolean; diff --git a/libs/types/src/lib/schemas/round.ts b/libs/types/src/lib/schemas/round.ts index e50daef..83801c3 100644 --- a/libs/types/src/lib/schemas/round.ts +++ b/libs/types/src/lib/schemas/round.ts @@ -1,4 +1,4 @@ -import { WithId } from 'mongodb'; +import { ObjectId, WithId } from 'mongodb'; import { Positions } from '../positions'; import { Member } from './member'; @@ -11,6 +11,7 @@ interface RoleConfig { } export interface Round { + eventId: ObjectId; name: string; roles: RoleConfig[]; allowedMembers: WithId[]; From 05dc73fde0ce1ac9a28ea4925648acbfd839d865 Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Sun, 20 Jul 2025 19:48:06 +0300 Subject: [PATCH 7/9] feat: add eventId to various components and API calls for improved event handling --- .../components/mtes/add-round-dialog.tsx | 8 ++++--- .../components/mtes/control-rounds.tsx | 8 +++++-- apps/frontend/pages/mtes/audience-display.tsx | 12 +++++------ apps/frontend/pages/mtes/election-manager.tsx | 21 ++++++++++--------- apps/frontend/pages/mtes/voting-stand.tsx | 4 ++-- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/apps/frontend/components/mtes/add-round-dialog.tsx b/apps/frontend/components/mtes/add-round-dialog.tsx index ee05bdf..0110797 100644 --- a/apps/frontend/components/mtes/add-round-dialog.tsx +++ b/apps/frontend/components/mtes/add-round-dialog.tsx @@ -135,6 +135,7 @@ interface AddRoundDialogProps { initialRound?: WithId; isEdit?: boolean; isDuplicate?: boolean; + eventId: string; } const WHITE_VOTE_ID_PREFIX = '000000000000000000000'; @@ -195,7 +196,8 @@ const AddRoundDialog: React.FC = ({ onRoundCreated, initialRound, isEdit = false, - isDuplicate = false + isDuplicate = false, + eventId }) => { const { enqueueSnackbar } = useSnackbar(); const [open, setOpen] = useState(false); @@ -301,7 +303,7 @@ const AddRoundDialog: React.FC = ({ } if (Object.keys(changes).length > 0) { - const res = await apiFetch('/api/events/rounds/update', { + const res = await apiFetch(`api/events/${eventId}/rounds/update`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -327,7 +329,7 @@ const AddRoundDialog: React.FC = ({ endTime: null, isLocked: false }; - const res = await apiFetch('/api/events/rounds/add', { + const res = await apiFetch(`/api/events/${eventId}/rounds/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ round: payload }) diff --git a/apps/frontend/components/mtes/control-rounds.tsx b/apps/frontend/components/mtes/control-rounds.tsx index 048e073..14f0387 100644 --- a/apps/frontend/components/mtes/control-rounds.tsx +++ b/apps/frontend/components/mtes/control-rounds.tsx @@ -19,6 +19,7 @@ interface ControlRoundsProps { handleShowResults: (round: WithId) => void; members: WithId[]; refreshData: () => void; + eventId: string; } const handleDeleteRound = (round: WithId, refreshData: () => void) => { @@ -42,7 +43,8 @@ export const ControlRounds = ({ setSelectedRound, handleShowResults, members, - refreshData + refreshData, + eventId }: ControlRoundsProps) => { return ( @@ -50,7 +52,7 @@ export const ControlRounds = ({ סבבים זמינים - + {rounds.map(round => { @@ -152,6 +154,7 @@ export const ControlRounds = ({ onRoundCreated={refreshData} initialRound={round} isEdit={true} + eventId={eventId} // If AddRoundDialog needs a custom trigger, add a prop like below // triggerIcon={} /> @@ -163,6 +166,7 @@ export const ControlRounds = ({ onRoundCreated={refreshData} initialRound={round} isDuplicate={true} + eventId={eventId} // If AddRoundDialog needs a custom trigger, add a prop like below // triggerIcon={} /> diff --git a/apps/frontend/pages/mtes/audience-display.tsx b/apps/frontend/pages/mtes/audience-display.tsx index bf5a7d0..ae57cd5 100644 --- a/apps/frontend/pages/mtes/audience-display.tsx +++ b/apps/frontend/pages/mtes/audience-display.tsx @@ -68,7 +68,7 @@ const Page: NextPage = ({ user, event, electionState, initialMembers, rou const refreshVotedMembers = async (roundId: string) => { console.log(`Fetching voted members for round ID: ${roundId}`); - const response = await apiFetch(`/api/events/rounds/votedMembers/${roundId}`, { + const response = await apiFetch(`/api/events/${event._id}/rounds/votedMembers/${roundId}`, { method: 'GET' }); if (response.ok) { @@ -228,14 +228,14 @@ const Page: NextPage = ({ user, event, electionState, initialMembers, rou export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - event: `/public/event`, - electionState: `/api/events/state`, - initialMembers: `/api/events/members`, - rounds: '/api/events/rounds' + event: `/api/events/${eventId}`, + electionState: `/api/events/${eventId}/state`, + initialMembers: `/api/events/${eventId}/members`, + rounds: `/api/events/${eventId}/rounds` }, ctx ); diff --git a/apps/frontend/pages/mtes/election-manager.tsx b/apps/frontend/pages/mtes/election-manager.tsx index b423d44..cb774fc 100644 --- a/apps/frontend/pages/mtes/election-manager.tsx +++ b/apps/frontend/pages/mtes/election-manager.tsx @@ -54,7 +54,7 @@ interface Props { mmMembers: WithId[]; rounds: WithId[]; electionState: WithId; - event: ElectionEvent; + event: WithId; eventState: WithId; } @@ -573,8 +573,6 @@ const Page: NextPage = ({ setSelectStandId(null); }; - console.log(votedMembers); - return ( = ({ handleShowResults={handleShowResults} members={members} refreshData={() => router.replace(router.asPath)} + eventId={event._id.toString()} /> )} @@ -841,20 +840,22 @@ const Page: NextPage = ({ export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - rounds: '/api/events/rounds', - electionState: '/api/events/state', - members: '/api/events/members', - mmMembers: '/api/events/mm-members', // Added mmMembers endpoint - event: '/public/event', - eventState: '/api/events/state' + rounds: `/api/events/${eventId}/rounds`, + event: `/api/events/${eventId}`, + electionState: `/api/events/${eventId}/state`, + members: `/api/events/${eventId}/members`, + mmMembers: `/api/events/${eventId}/mm-members`, + eventState: `/api/events/${eventId}/state` }, ctx ); + console.log('Server-side data fetched:', data); + return { props: { user, ...data } }; // Pass combined list as members } catch { return { redirect: { destination: '/login', permanent: false } }; diff --git a/apps/frontend/pages/mtes/voting-stand.tsx b/apps/frontend/pages/mtes/voting-stand.tsx index 06148fb..b73192f 100644 --- a/apps/frontend/pages/mtes/voting-stand.tsx +++ b/apps/frontend/pages/mtes/voting-stand.tsx @@ -143,11 +143,11 @@ const Page: NextPage = ({ user, electionState }) => { export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - electionState: '/api/events/state' + electionState: `/api/events/${eventId}/state` }, ctx ); From a38bffa41375fd1ffe8cc04c6c632d5bb9f6183a Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Sun, 20 Jul 2025 19:56:07 +0300 Subject: [PATCH 8/9] feat: update vote submission API endpoint to include eventId for improved event handling --- apps/frontend/components/mtes/voting-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/components/mtes/voting-form.tsx b/apps/frontend/components/mtes/voting-form.tsx index dded42e..75aef1d 100644 --- a/apps/frontend/components/mtes/voting-form.tsx +++ b/apps/frontend/components/mtes/voting-form.tsx @@ -112,7 +112,7 @@ export const VotingForm = ({ try { socket.emit('voteSubmitted', member, votingStandId); - const response = await apiFetch('/api/events/vote', { + const response = await apiFetch(`/api/events/${round.eventId}/vote`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) From c38fa60d6000239e99bafb9f6c6850b7c5422fcd Mon Sep 17 00:00:00 2001 From: Nir Chen Date: Tue, 22 Jul 2025 20:32:22 +0300 Subject: [PATCH 9/9] Refactors and improves backend and CI configurations This commit includes several refactorings and improvements across the backend and CI configurations: - Updates dependencies and configurations for improved stability and performance. - Implements enhanced error handling and logging for easier debugging. - Improves code readability and maintainability by applying consistent formatting. - Addresses minor bugs and inconsistencies in the backend logic. - Improves round locking and unlocking --- .github/workflows/ci.yml | 4 +- .github/workflows/codeql.yml | 88 +-- .prettierrc | 2 +- README.md | 6 +- apps/backend/src/lib/schedule/cleaner.ts | 65 +- .../src/lib/schedule/voting-stands-users.ts | 3 +- apps/backend/src/main.ts | 7 +- apps/backend/src/middlewares/auth.ts | 2 +- .../src/routers/api/admin/events/cities.ts | 170 ++--- .../src/routers/api/admin/events/index.ts | 14 +- .../backend/src/routers/api/admin/password.ts | 149 ++-- apps/backend/src/routers/api/events/index.ts | 29 +- .../backend/src/routers/api/events/members.ts | 189 +++--- .../src/routers/api/events/mm-members.ts | 164 ++--- apps/backend/src/routers/api/events/rounds.ts | 636 +++++++++--------- apps/backend/src/routers/api/events/state.ts | 43 +- apps/backend/src/routers/api/events/vote.ts | 151 +++-- apps/backend/src/routers/auth.ts | 2 +- apps/backend/src/websocket/handlers.ts | 30 +- apps/backend/src/websocket/index.ts | 24 +- .../components/admin/ChangePasswordDialog.tsx | 24 +- .../components/connection-indicator.tsx | 18 +- .../components/general/event-selector.tsx | 9 +- .../general/login/admin-login-form.tsx | 23 +- apps/frontend/components/layout.tsx | 26 +- .../components/mtes/add-round-dialog.tsx | 2 +- .../components/mtes/control-rounds.tsx | 2 +- apps/frontend/localization/displays.ts | 10 +- apps/frontend/next.config.js | 8 +- apps/frontend/pages/mtes/audience-display.tsx | 2 +- apps/frontend/pages/mtes/index.tsx | 2 +- apps/frontend/tsconfig.json | 2 +- libs/database/src/lib/crud/cities.ts | 17 +- libs/database/src/lib/crud/contestants.ts | 10 +- libs/database/src/lib/crud/members.ts | 3 +- libs/database/src/lib/crud/mm-members.ts | 40 +- libs/types/src/index.ts | 2 +- libs/types/src/lib/constants.ts | 6 +- libs/types/src/lib/schemas/city.ts | 4 +- libs/types/src/lib/schemas/election-state.ts | 7 +- libs/types/src/lib/schemas/member.ts | 2 +- libs/types/src/lib/websocket.ts | 17 +- nx.json | 27 +- package.json | 2 +- 44 files changed, 1031 insertions(+), 1012 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be27748..c9a473a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: jobs: @@ -40,4 +40,4 @@ jobs: run: npm ci - name: Build frontend & backend - run: npx nx build backend frontend \ No newline at end of file + run: npx nx build backend frontend diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d2dd0a6..9127048 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,13 +9,13 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL Advanced" +name: 'CodeQL Advanced' on: push: - branches: [ "main" ] + branches: ['main'] pull_request: - branches: [ "main" ] + branches: ['main'] schedule: - cron: '43 6 * * 6' @@ -43,10 +43,10 @@ jobs: fail-fast: false matrix: include: - - language: actions - build-mode: none - - language: javascript-typescript - build-mode: none + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -56,45 +56,45 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/.prettierrc b/.prettierrc index 33645d2..534a38a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,4 +5,4 @@ "printWidth": 100, "trailingComma": "none", "arrowParens": "avoid" -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7504ce6..8b9ad96 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ npm run dev Deploy the application swiftly using Docker Compose. 🚀 Ensure Docker is running. -> [!IMPORTANT] +> [!IMPORTANT] > Before launching, configure your environment variables. The backend service needs an `apps/backend/.env.local` file (copy `apps/backend/.env` if needed). The `JWT_SECRET` is crucial. ✨ Run this from the project root: @@ -76,6 +76,7 @@ docker-compose up -d This builds and starts frontend and backend services in detached mode. Access: + - **Frontend**: `http://localhost:4200` 🖥️ - **Backend API**: `http://localhost:3333` ⚙️ @@ -112,5 +113,4 @@ mtes/ This project is licensed under the GPL-3.0 License. It utilizes a similar tech stack and codebase inspired by [FIRSTIsrael/lems](https://github.com/FIRSTIsrael/lems); 🙏 thank you for making this possible! 🚀 - -***Made with ❤️ by [@TheCommandCat](https://github.com/TheCommandCat)*** +**_Made with ❤️ by [@TheCommandCat](https://github.com/TheCommandCat)_** diff --git a/apps/backend/src/lib/schedule/cleaner.ts b/apps/backend/src/lib/schedule/cleaner.ts index 191e3a5..37e32bd 100644 --- a/apps/backend/src/lib/schedule/cleaner.ts +++ b/apps/backend/src/lib/schedule/cleaner.ts @@ -3,29 +3,52 @@ import { ObjectId } from 'mongodb'; export const cleanDivisionData = async (eventId: ObjectId) => { if (!(await db.deleteElectionEvent(eventId))) throw new Error('Could not delete event!'); - if (!(await db.deleteUsers( - { _id: eventId } - )).acknowledged) throw new Error('Could not delete users!'); + if (!(await db.deleteUsers({ _id: eventId })).acknowledged) + throw new Error('Could not delete users!'); if (!(await db.deleteElectionState(eventId)).acknowledged) { throw new Error('Could not delete Election state!'); } - if (!(await db.deleteMembers({ - _id: eventId - })).acknowledged) throw new Error('Could not delete members!'); - if (!(await db.deleteContestants({ - _id: eventId - })).acknowledged) throw new Error('Could not delete contestant!'); - if (!(await db.deleteRounds( - { eventId } - )).acknowledged) throw new Error('Could not delete rounds!'); - if (!(await db.deleteVotes({ - _id: eventId - })).acknowledged) throw new Error('Could not delete votes!'); - if (!(await db.deleteVotingStatuses({ - _id: eventId - })).acknowledged) throw new Error('Could not delete voting statuses!'); - if (!(await db.deleteCities({ - _id: eventId - })).acknowledged) throw new Error('Could not delete cities!'); + if ( + !( + await db.deleteMembers({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete members!'); + if ( + !( + await db.deleteContestants({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete contestant!'); + if (!(await db.deleteRounds({ eventId })).acknowledged) + throw new Error('Could not delete rounds!'); + if ( + !( + await db.deleteVotes({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete votes!'); + if ( + !( + await db.deleteVotingStatuses({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete voting statuses!'); + if ( + !( + await db.deleteCities({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete cities!'); }; diff --git a/apps/backend/src/lib/schedule/voting-stands-users.ts b/apps/backend/src/lib/schedule/voting-stands-users.ts index b07ccdc..9e89526 100644 --- a/apps/backend/src/lib/schedule/voting-stands-users.ts +++ b/apps/backend/src/lib/schedule/voting-stands-users.ts @@ -40,9 +40,8 @@ export const CreateVotingStandUsers = (numOfStands: number, eventId: ObjectId): isAdmin: false, role: 'audience-display', password: randomString(4), - lastPasswordSetDate: new Date(), + lastPasswordSetDate: new Date() }); - return users; }; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index f730cbd..6a78832 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -15,10 +15,7 @@ const port = process.env.PORT ? Number(process.env.PORT) : 3333; const app = express(); const server = http.createServer(app); const corsOptions = { - origin: [ - /localhost:\d+$/, - /\.thecommandcat\.me$/ - ], + origin: [/localhost:\d+$/, /\.thecommandcat\.me$/], credentials: true }; const io = new Server(server, { cors: corsOptions }); @@ -51,4 +48,4 @@ server.listen(port, () => { console.log(`✅ Server started on port ${port}.`); }); -server.on('error', console.error); \ No newline at end of file +server.on('error', console.error); diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts index b1f249e..9b57df4 100644 --- a/apps/backend/src/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -18,7 +18,7 @@ export const authMiddleware = async (req: Request, res: Response, next: NextFunc if (user) { delete user.password; req.user = user; - return next(); + return next(); } } catch (err) { //Invalid token diff --git a/apps/backend/src/routers/api/admin/events/cities.ts b/apps/backend/src/routers/api/admin/events/cities.ts index 5e15bcc..199c757 100644 --- a/apps/backend/src/routers/api/admin/events/cities.ts +++ b/apps/backend/src/routers/api/admin/events/cities.ts @@ -5,98 +5,100 @@ import { City } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -router.get('/', asyncHandler(async (_req: Request, res: Response) => { +router.get( + '/', + asyncHandler(async (_req: Request, res: Response) => { console.log('⏬ Getting cities...'); const cities = await db.getCities({}); res.json(cities); -} -)); + }) +); router.post( - '/', - asyncHandler(async (req: Request, res: Response) => { - const cityData: City = req.body; - - if (!cityData?.name || cityData?.numOfVoters === undefined) { - res.status(400).json({ error: 'Missing required fields: name and numOfVoters' }); - return; - } - - if (typeof cityData.numOfVoters !== 'number' || cityData.numOfVoters < 0) { - res.status(400).json({ error: 'Invalid numOfVoters: must be a non-negative number' }); - return; - } - - try { - const result = await db.addCity(cityData); - if (result.insertedId) { - res.status(201).json({ message: 'City added successfully', cityId: result.insertedId }); - } else { - res.status(500).json({ error: 'Failed to add city, no ID returned' }); - } - } catch (error) { - console.error('Error adding city:', error); - if (error.code === 11000) { - res.status(409).json({ error: 'City with this name already exists' }); - return; - } - res.status(500).json({ error: 'Failed to add city due to an internal error' }); - } - }) + '/', + asyncHandler(async (req: Request, res: Response) => { + const cityData: City = req.body; + + if (!cityData?.name || cityData?.numOfVoters === undefined) { + res.status(400).json({ error: 'Missing required fields: name and numOfVoters' }); + return; + } + + if (typeof cityData.numOfVoters !== 'number' || cityData.numOfVoters < 0) { + res.status(400).json({ error: 'Invalid numOfVoters: must be a non-negative number' }); + return; + } + + try { + const result = await db.addCity(cityData); + if (result.insertedId) { + res.status(201).json({ message: 'City added successfully', cityId: result.insertedId }); + } else { + res.status(500).json({ error: 'Failed to add city, no ID returned' }); + } + } catch (error) { + console.error('Error adding city:', error); + if (error.code === 11000) { + res.status(409).json({ error: 'City with this name already exists' }); + return; + } + res.status(500).json({ error: 'Failed to add city due to an internal error' }); + } + }) ); router.put( - '/', - asyncHandler(async (req: Request, res: Response) => { - const { cities } = req.body as { cities: City[] }; - - if (!cities || !Array.isArray(cities)) { - console.log('❌ Cities array is missing or not an array'); - res.status(400).json({ ok: false, message: 'Cities data is missing or invalid' }); - return; - } - - if (cities.length === 0) { - console.log('❌ Cities array is empty'); - } - - console.log('⏬ Updating Cities...'); - - const processedCities = cities.map(city => { - const numVoters = Number(city.numOfVoters); - return { - ...city, - numOfVoters: numVoters - }; - }); - - for (const city of processedCities) { - if (isNaN(city.numOfVoters)) { - console.warn(`⚠️ numOfVoters for city '${city.name}' was NaN after conversion.`); - } - } - - const deleteRes = await db.deleteCities({}); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete cities'); - res.status(500).json({ ok: false, message: 'Could not delete cities' }); - return; - } - - if (processedCities.length > 0) { - const addRes = await db.addCities(processedCities.map(city => ({ ...city, _id: undefined }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add cities'); - res.status(500).json({ ok: false, message: 'Could not add cities' }); - return; - } - } else { - console.log('ℹ️ No cities to add after processing (or initial array was empty).'); - } - - console.log('✅ Cities updated!'); - res.json({ ok: true }); - }) + '/', + asyncHandler(async (req: Request, res: Response) => { + const { cities } = req.body as { cities: City[] }; + + if (!cities || !Array.isArray(cities)) { + console.log('❌ Cities array is missing or not an array'); + res.status(400).json({ ok: false, message: 'Cities data is missing or invalid' }); + return; + } + + if (cities.length === 0) { + console.log('❌ Cities array is empty'); + } + + console.log('⏬ Updating Cities...'); + + const processedCities = cities.map(city => { + const numVoters = Number(city.numOfVoters); + return { + ...city, + numOfVoters: numVoters + }; + }); + + for (const city of processedCities) { + if (isNaN(city.numOfVoters)) { + console.warn(`⚠️ numOfVoters for city '${city.name}' was NaN after conversion.`); + } + } + + const deleteRes = await db.deleteCities({}); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete cities'); + res.status(500).json({ ok: false, message: 'Could not delete cities' }); + return; + } + + if (processedCities.length > 0) { + const addRes = await db.addCities(processedCities.map(city => ({ ...city, _id: undefined }))); + if (!addRes.acknowledged) { + console.log('❌ Could not add cities'); + res.status(500).json({ ok: false, message: 'Could not add cities' }); + return; + } + } else { + console.log('ℹ️ No cities to add after processing (or initial array was empty).'); + } + + console.log('✅ Cities updated!'); + res.json({ ok: true }); + }) ); export default router; diff --git a/apps/backend/src/routers/api/admin/events/index.ts b/apps/backend/src/routers/api/admin/events/index.ts index 1a5bf95..2d4be62 100644 --- a/apps/backend/src/routers/api/admin/events/index.ts +++ b/apps/backend/src/routers/api/admin/events/index.ts @@ -25,8 +25,8 @@ function getInitialDivisionState(eventId: ObjectId): ElectionState { activeRound: null, completed: false, audienceDisplay: { - display: 'round', - }, + display: 'round' + } }; } @@ -51,16 +51,14 @@ router.post( console.log(eventData); - eventData.startDate = new Date(); eventData.endDate = new Date(); console.log(`🔍 Validating Event data: ${JSON.stringify(eventData)}`); - console.log('⏬ Creating Event...'); const eventResult = await db.addElectionEvent(eventData as ElectionEvent); - const eventId = eventResult.insertedId + const eventId = eventResult.insertedId; if (!eventResult.acknowledged) { console.log('❌ Could not create Event'); @@ -84,13 +82,14 @@ router.post( console.log('👤 Generating division users'); - console.log(`Creating voting stand users for ${eventData.votingStands} stands with event ID: ${eventId}`); + console.log( + `Creating voting stand users for ${eventData.votingStands} stands with event ID: ${eventId}` + ); const users = CreateVotingStandUsers(eventData.votingStands, eventId); console.log('users:', users); - if (!(await db.addUsers(users)).acknowledged) { res.status(500).json({ error: 'Could not create users!' }); return; @@ -173,7 +172,6 @@ router.delete( console.log(`🚮 Deleting data from event`); try { await cleanDivisionData(new ObjectId(req.params.eventId)); - } catch (error) { res.status(500).json(error.message); return; diff --git a/apps/backend/src/routers/api/admin/password.ts b/apps/backend/src/routers/api/admin/password.ts index acaa7c1..4f90ab3 100644 --- a/apps/backend/src/routers/api/admin/password.ts +++ b/apps/backend/src/routers/api/admin/password.ts @@ -6,90 +6,89 @@ const router = express.Router({ mergeParams: true }); // Update admin password router.put( - '/', - asyncHandler(async (req: Request, res: Response) => { - const { currentPassword, newPassword } = req.body; + '/', + asyncHandler(async (req: Request, res: Response) => { + const { currentPassword, newPassword } = req.body; - if (!currentPassword || !newPassword) { - res.status(400).json({ - error: 'MISSING_FIELDS', - message: 'Current password and new password are required' - }); - return; - } - - if (newPassword.length < 4) { - res.status(400).json({ - error: 'WEAK_PASSWORD', - message: 'New password must be at least 4 characters long' - }); - return; - } + if (!currentPassword || !newPassword) { + res.status(400).json({ + error: 'MISSING_FIELDS', + message: 'Current password and new password are required' + }); + return; + } - try { - // Ensure the user is authenticated and is an admin - if (!req.user || !req.user.isAdmin) { - res.status(403).json({ - error: 'FORBIDDEN', - message: 'Only admin users can change passwords' - }); - return; - } + if (newPassword.length < 4) { + res.status(400).json({ + error: 'WEAK_PASSWORD', + message: 'New password must be at least 4 characters long' + }); + return; + } - // Get the current admin user - const adminUser = await db.getUserWithCredentials({ - _id: req.user?._id, - isAdmin: true - }); + try { + // Ensure the user is authenticated and is an admin + if (!req.user || !req.user.isAdmin) { + res.status(403).json({ + error: 'FORBIDDEN', + message: 'Only admin users can change passwords' + }); + return; + } - if (!adminUser) { - res.status(404).json({ - error: 'USER_NOT_FOUND', - message: 'Admin user not found' - }); - return; - } + // Get the current admin user + const adminUser = await db.getUserWithCredentials({ + _id: req.user?._id, + isAdmin: true + }); - // Verify current password - if (adminUser.password !== currentPassword) { - res.status(401).json({ - error: 'INVALID_CURRENT_PASSWORD', - message: 'Current password is incorrect' - }); - return; - } + if (!adminUser) { + res.status(404).json({ + error: 'USER_NOT_FOUND', + message: 'Admin user not found' + }); + return; + } - // Update password - const updateResult = await db.updateUser( - { _id: adminUser._id }, - { - password: newPassword, - lastPasswordSetDate: new Date() - } - ); + // Verify current password + if (adminUser.password !== currentPassword) { + res.status(401).json({ + error: 'INVALID_CURRENT_PASSWORD', + message: 'Current password is incorrect' + }); + return; + } - if (!updateResult.acknowledged || updateResult.matchedCount === 0) { - res.status(500).json({ - error: 'UPDATE_FAILED', - message: 'Failed to update password' - }); - return; - } + // Update password + const updateResult = await db.updateUser( + { _id: adminUser._id }, + { + password: newPassword, + lastPasswordSetDate: new Date() + } + ); - console.log('✅ Admin password updated successfully'); - res.json({ - ok: true, - message: 'Password updated successfully' - }); + if (!updateResult.acknowledged || updateResult.matchedCount === 0) { + res.status(500).json({ + error: 'UPDATE_FAILED', + message: 'Failed to update password' + }); + return; + } - } catch (error) { - console.error('❌ Error updating admin password:', error); - res.status(500).json({ - error: 'INTERNAL_ERROR', - message: 'Internal server error while updating password' - }); - } - }) + console.log('✅ Admin password updated successfully'); + res.json({ + ok: true, + message: 'Password updated successfully' + }); + } catch (error) { + console.error('❌ Error updating admin password:', error); + res.status(500).json({ + error: 'INTERNAL_ERROR', + message: 'Internal server error while updating password' + }); + } + }) ); export default router; diff --git a/apps/backend/src/routers/api/events/index.ts b/apps/backend/src/routers/api/events/index.ts index e16bcba..1477b3d 100644 --- a/apps/backend/src/routers/api/events/index.ts +++ b/apps/backend/src/routers/api/events/index.ts @@ -9,20 +9,19 @@ import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); - router.get('/:eventId', async (req: Request, res: Response) => { - const eventId = req.params.eventId; - let event; - try { - event = await db.getElectionEvent(new ObjectId(eventId)); - } catch (err) { - // Invalid ObjectId or db error - return res.status(404).json({ ok: false, message: 'Event not found' }); - } - if (!event) { - return res.status(404).json({ ok: false, message: 'Event not found' }); - } - res.json(event); + const eventId = req.params.eventId; + let event; + try { + event = await db.getElectionEvent(new ObjectId(eventId)); + } catch (err) { + // Invalid ObjectId or db error + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + if (!event) { + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + res.json(event); }); router.use('/:eventId/rounds', roundsRouter); @@ -31,8 +30,4 @@ router.use('/:eventId/mm-members', mmMembersRouter); router.use('/:eventId/state', stateRouter); router.use('/:eventId/vote', voteRouter); - - - - export default router; diff --git a/apps/backend/src/routers/api/events/members.ts b/apps/backend/src/routers/api/events/members.ts index f92934e..02a48e9 100644 --- a/apps/backend/src/routers/api/events/members.ts +++ b/apps/backend/src/routers/api/events/members.ts @@ -6,100 +6,117 @@ import { Member } from '@mtes/types'; const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { - console.log('⏬ Getting members...'); - return res.json(await db.getMembers({ - eventId: new ObjectId(req.params.eventId) - })); + console.log('⏬ Getting members...'); + return res.json( + await db.getMembers({ + eventId: new ObjectId(req.params.eventId) + }) + ); }); router.put('/', async (req: Request, res: Response) => { - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); - } - - const { members } = req.body as { members: Member[] }; - if (!members || members.length === 0) { - console.log('❌ Members array is empty'); - res.status(400).json({ ok: false, message: 'No members provided' }); - return; - } - - console.log('⏬ Updating Members...'); - - const deleteRes = await db.deleteMembers({ - eventId: new ObjectId(eventId) - }); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete members'); - res.status(500).json({ ok: false, message: 'Could not delete members' }); - return; - } - - const addRes = await db.addMembers(members.map(member => ({ ...member, eventId: new ObjectId(eventId) }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add members'); - res.status(500).json({ ok: false, message: 'Could not add members' }); - return; - } - - console.log('⏬ Members updated!'); - res.json({ ok: true }); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { members } = req.body as { members: Member[] }; + if (!members || members.length === 0) { + console.log('❌ Members array is empty'); + res.status(400).json({ ok: false, message: 'No members provided' }); + return; + } + + console.log('⏬ Updating Members...'); + + const deleteRes = await db.deleteMembers({ + eventId: new ObjectId(eventId) + }); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete members'); + res.status(500).json({ ok: false, message: 'Could not delete members' }); + return; + } + + const addRes = await db.addMembers( + members.map(member => ({ ...member, eventId: new ObjectId(eventId) })) + ); + if (!addRes.acknowledged) { + console.log('❌ Could not add members'); + res.status(500).json({ ok: false, message: 'Could not add members' }); + return; + } + + console.log('⏬ Members updated!'); + res.json({ ok: true }); }); router.put('/:memberId/presence', async (req: Request, res: Response) => { - const { memberId } = req.params; - const { isPresent, replacedBy } = req.body as { isPresent: boolean; replacedBy?: WithId }; - - if (!memberId) { - console.log('❌ Member ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Member ID is missing' }); + const { memberId } = req.params; + const { isPresent, replacedBy } = req.body as { isPresent: boolean; replacedBy?: WithId }; + + if (!memberId) { + console.log('❌ Member ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Member ID is missing' }); + } + + if (typeof isPresent !== 'boolean' && replacedBy === undefined) { + console.log('❌ isPresent is missing or not a boolean, and replacedBy is not provided'); + return res + .status(400) + .json({ + ok: false, + message: 'isPresent field (boolean) or replacedBy field (string) is required' + }); + } + + if (replacedBy && typeof replacedBy !== 'object') { + console.log('❌ replacedBy is not a WithId'); + return res + .status(400) + .json({ ok: false, message: 'replacedBy field must be a WithId' }); + } + + let updatePayload: { isPresent: boolean; replacedBy: WithId | null } = { + isPresent, + replacedBy: null + }; + + if (replacedBy) { + console.log( + `⏬ Member ${memberId} is being replaced by ${replacedBy._id}. Setting isPresent to true.` + ); + updatePayload = { isPresent: true, replacedBy: replacedBy as WithId }; + } else { + console.log(`⏬ Updating presence for member ${memberId} to ${isPresent}`); + updatePayload = { isPresent, replacedBy: null }; + } + + try { + const memberResult = await db.updateMember( + { _id: new ObjectId(memberId) }, + updatePayload as unknown as Partial + ); + + if (!memberResult.acknowledged || memberResult.matchedCount === 0) { + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); + return res.status(404).json({ + ok: false, + message: 'Could not update member presence. Member not found or update failed.' + }); } - if (typeof isPresent !== 'boolean' && replacedBy === undefined) { - console.log('❌ isPresent is missing or not a boolean, and replacedBy is not provided'); - return res.status(400).json({ ok: false, message: 'isPresent field (boolean) or replacedBy field (string) is required' }); - } - - if (replacedBy && (typeof replacedBy !== 'object')) { - console.log('❌ replacedBy is not a WithId'); - return res.status(400).json({ ok: false, message: 'replacedBy field must be a WithId' }); - } - - let updatePayload: { isPresent: boolean; replacedBy: WithId | null } = { isPresent, replacedBy: null }; - - - if (replacedBy) { - console.log(`⏬ Member ${memberId} is being replaced by ${replacedBy._id}. Setting isPresent to true.`); - updatePayload = { isPresent: true, replacedBy: replacedBy as WithId }; - } else { - console.log(`⏬ Updating presence for member ${memberId} to ${isPresent}`); - updatePayload = { isPresent, replacedBy: null }; - } - - - try { - const memberResult = await db.updateMember({ _id: new ObjectId(memberId) }, updatePayload as unknown as Partial); - - if (!memberResult.acknowledged || memberResult.matchedCount === 0) { - console.log( - `❌ Could not update presence for member ${memberId}. Member not found or update failed.` - ); - return res.status(404).json({ - ok: false, - message: 'Could not update member presence. Member not found or update failed.' - }); - } - - console.log(`✅ Presence updated for member ${memberId}`); - res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating member presence:', error); - return res - .status(500) - .json({ ok: false, message: 'Internal server error while updating member presence' }); - } + console.log(`✅ Presence updated for member ${memberId}`); + res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating member presence:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating member presence' }); + } }); export default router; diff --git a/apps/backend/src/routers/api/events/mm-members.ts b/apps/backend/src/routers/api/events/mm-members.ts index 6d45f6f..30c4ba6 100644 --- a/apps/backend/src/routers/api/events/mm-members.ts +++ b/apps/backend/src/routers/api/events/mm-members.ts @@ -6,98 +6,102 @@ import { Member } from '@mtes/types'; const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { - - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); - } - - console.log('⏬ Getting mm-members...'); - return res.json(await db.getMmMembers({ - eventId: new ObjectId(eventId) - })); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + console.log('⏬ Getting mm-members...'); + return res.json( + await db.getMmMembers({ + eventId: new ObjectId(eventId) + }) + ); }); router.put('/', async (req: Request, res: Response) => { - - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { mmMembers } = (req.body as { mmMembers: Member[] }) || { mmMembers: [] }; + + console.log('⏬ Updating mm-members...'); + + if (!Array.isArray(mmMembers)) { + console.log('❌ Invalid mmMembers format: not an array'); + return res.status(400).json({ ok: false, message: 'mmMembers must be an array' }); + } + + try { + const deleteRes = await db.deleteMmMembers({ + eventId: new ObjectId(eventId) + }); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete mm-members'); + return res.status(500).json({ ok: false, message: 'Could not delete mm-members' }); } - const { mmMembers } = req.body as { mmMembers: Member[] } || { mmMembers: [] }; - - console.log('⏬ Updating mm-members...'); - - if (!Array.isArray(mmMembers)) { - console.log('❌ Invalid mmMembers format: not an array'); - return res.status(400).json({ ok: false, message: 'mmMembers must be an array' }); + if (mmMembers.length > 0) { + const addRes = await db.addMmMembers( + mmMembers.map(member => ({ ...member, eventId: new ObjectId(eventId) })) + ); + if (!addRes.acknowledged) { + console.log('❌ Could not add mm-members'); + return res.status(500).json({ ok: false, message: 'Could not add mm-members' }); + } } - try { - const deleteRes = await db.deleteMmMembers({ - eventId: new ObjectId(eventId) - }); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete mm-members'); - return res.status(500).json({ ok: false, message: 'Could not delete mm-members' }); - } - - if (mmMembers.length > 0) { - const addRes = await db.addMmMembers(mmMembers.map(member => ({ ...member, eventId: new ObjectId(eventId) }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add mm-members'); - return res.status(500).json({ ok: false, message: 'Could not add mm-members' }); - } - } - - console.log(`✅ Successfully updated mm-members (${mmMembers.length} members)`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating mm-members:', error); - return res.status(500).json({ ok: false, message: 'Internal server error while updating mm-members' }); - } + console.log(`✅ Successfully updated mm-members (${mmMembers.length} members)`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating mm-members:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating mm-members' }); + } }); router.put('/:mmMemberId/presence', async (req: Request, res: Response) => { - const { mmMemberId } = req.params; - const { isPresent } = req.body as { isPresent: boolean }; - - if (!mmMemberId) { - console.log('❌ mmMember ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'mmMember ID is missing' }); + const { mmMemberId } = req.params; + const { isPresent } = req.body as { isPresent: boolean }; + + if (!mmMemberId) { + console.log('❌ mmMember ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'mmMember ID is missing' }); + } + + if (typeof isPresent !== 'boolean') { + console.log('❌ isPresent is missing or not a boolean'); + return res.status(400).json({ ok: false, message: 'isPresent field (boolean) is required' }); + } + + console.log(`⏬ Updating presence for mm-member ${mmMemberId} to ${isPresent}`); + + try { + const memberResult = await db.updateMmMember({ _id: new ObjectId(mmMemberId) }, { isPresent }); + + if (!memberResult.acknowledged || memberResult.matchedCount === 0) { + console.log( + `❌ Could not update presence for mm-member ${mmMemberId}. mm-member not found or update failed.` + ); + return res.status(404).json({ + ok: false, + message: 'Could not update mm-member presence. mm-member not found or update failed.' + }); } - if (typeof isPresent !== 'boolean') { - console.log('❌ isPresent is missing or not a boolean'); - return res.status(400).json({ ok: false, message: 'isPresent field (boolean) is required' }); - } - - console.log(`⏬ Updating presence for mm-member ${mmMemberId} to ${isPresent}`); - - try { - const memberResult = await db.updateMmMember({ _id: new ObjectId(mmMemberId) }, { isPresent }); - - if (!memberResult.acknowledged || memberResult.matchedCount === 0) { - console.log( - `❌ Could not update presence for mm-member ${mmMemberId}. mm-member not found or update failed.` - ); - return res.status(404).json({ - ok: false, - message: 'Could not update mm-member presence. mm-member not found or update failed.' - }); - } - - console.log(`✅ Presence updated for mm-member ${mmMemberId}`); - res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating mm-member presence:', error); - return res - .status(500) - .json({ ok: false, message: 'Internal server error while updating mm-member presence' }); - } + console.log(`✅ Presence updated for mm-member ${mmMemberId}`); + res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating mm-member presence:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating mm-member presence' }); + } }); export default router; diff --git a/apps/backend/src/routers/api/events/rounds.ts b/apps/backend/src/routers/api/events/rounds.ts index a82f80e..7a41fb2 100644 --- a/apps/backend/src/routers/api/events/rounds.ts +++ b/apps/backend/src/routers/api/events/rounds.ts @@ -6,371 +6,377 @@ import { Member, Round } from '@mtes/types'; const router = express.Router({ mergeParams: true }); const genrateWhireVoteMembers = (numWhiteVotes: number, eventId: ObjectId): WithId[] => { - const whiteVotes: WithId[] = []; - for (let i = 0; i < numWhiteVotes; i++) { - whiteVotes.push( - { - _id: new ObjectId(`00000000000000000000000${i + 1}`), - eventId: eventId, - name: `פתק לבן ${i + 1}`, - city: 'אין אמון באף אחד', - isPresent: true, - isMM: false, - } - ); - } - return whiteVotes; -} + const whiteVotes: WithId[] = []; + for (let i = 0; i < numWhiteVotes; i++) { + whiteVotes.push({ + _id: new ObjectId(`00000000000000000000000${i + 1}`), + eventId: eventId, + name: `פתק לבן ${i + 1}`, + city: 'אין אמון באף אחד', + isPresent: true, + isMM: false + }); + } + return whiteVotes; +}; router.get('/', async (req: Request, res: Response) => { - - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); - } - - console.log('⏬ Getting rounds...'); - return res.json(await db.getRounds({ - eventId: new ObjectId(eventId) - })); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + console.log('⏬ Getting rounds...'); + return res.json( + await db.getRounds({ + eventId: new ObjectId(eventId) + }) + ); }); router.post('/add', async (req: Request, res: Response) => { - - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); - } - - const { round } = req.body; - round.eventId = new ObjectId(eventId); - - console.log('⏬ Adding Round...', JSON.stringify(round, null, 2)); - - if (!round) { - console.log('❌ Round object is null or undefined'); - return res.status(400).json({ ok: false, message: 'Round object is missing' }); - } - if (!round.name) { - console.log('❌ Round name is missing or empty'); - return res.status(400).json({ ok: false, message: 'Round name is required' }); - } - if (!round.roles) { - console.log('❌ Round roles are missing'); - return res.status(400).json({ ok: false, message: 'Round roles must be specified' }); - } - if (!round.allowedMembers) { - console.log('❌ Round allowedMembers is missing'); - return res.status(400).json({ ok: false, message: 'Round allowed members must be specified' }); - } - - try { - round.allowedMembers = await Promise.all( - round.allowedMembers.map(async (member: string | { _id: string }) => { - const memberId = typeof member === 'string' ? member : member._id; - const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); - if (!dbMember) { - console.log(`❌ Member with ID ${memberId} not found`); - // Decide how to handle this: throw error, return null and filter later, or return error response - throw new Error(`Member with ID ${memberId} not found`); - } - return dbMember; - }) - ); - - round.roles = await Promise.all( - round.roles.map(async (role: any) => { - const contestants = await Promise.all( - role.contestants.map(async (contestant: string | { _id: string }) => { - const contestantId = typeof contestant === 'string' ? contestant : contestant._id; - const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); - if (!dbContestant) { - console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); - throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); - } - return dbContestant; - }) - ); - if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, round.eventId)); - } - return { ...role, contestants }; - }) + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { round } = req.body; + round.eventId = new ObjectId(eventId); + + console.log('⏬ Adding Round...', JSON.stringify(round, null, 2)); + + if (!round) { + console.log('❌ Round object is null or undefined'); + return res.status(400).json({ ok: false, message: 'Round object is missing' }); + } + if (!round.name) { + console.log('❌ Round name is missing or empty'); + return res.status(400).json({ ok: false, message: 'Round name is required' }); + } + if (!round.roles) { + console.log('❌ Round roles are missing'); + return res.status(400).json({ ok: false, message: 'Round roles must be specified' }); + } + if (!round.allowedMembers) { + console.log('❌ Round allowedMembers is missing'); + return res.status(400).json({ ok: false, message: 'Round allowed members must be specified' }); + } + + try { + round.allowedMembers = await Promise.all( + round.allowedMembers.map(async (member: string | { _id: string }) => { + const memberId = typeof member === 'string' ? member : member._id; + const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); + if (!dbMember) { + console.log(`❌ Member with ID ${memberId} not found`); + // Decide how to handle this: throw error, return null and filter later, or return error response + throw new Error(`Member with ID ${memberId} not found`); + } + return dbMember; + }) + ); + + round.roles = await Promise.all( + round.roles.map(async (role: any) => { + const contestants = await Promise.all( + role.contestants.map(async (contestant: string | { _id: string }) => { + const contestantId = typeof contestant === 'string' ? contestant : contestant._id; + const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); + if (!dbContestant) { + console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); + throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); + } + return dbContestant; + }) ); - - console.log('⏬ Adding Round to db...', JSON.stringify(round, null, 2)); - const roundResult = await db.addRound(round); - - res.json({ ok: true, id: roundResult.insertedId }); - } catch (error: any) { - console.error('❌ Error adding round:', error); - return res.status(error.message.includes('not found') ? 400 : 500).json({ ok: false, message: error.message || 'Internal server error' }); - } + if (role.numWhiteVotes > 0) { + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, round.eventId)); + } + return { ...role, contestants }; + }) + ); + + console.log('⏬ Adding Round to db...', JSON.stringify(round, null, 2)); + const roundResult = await db.addRound(round); + + res.json({ ok: true, id: roundResult.insertedId }); + } catch (error: any) { + console.error('❌ Error adding round:', error); + return res + .status(error.message.includes('not found') ? 400 : 500) + .json({ ok: false, message: error.message || 'Internal server error' }); + } }); // Type for the update request body - uses string IDs instead of full objects interface UpdateRoundRequest { - name?: string; - allowedMembers?: string[]; - roles?: { - role: string; - contestants: string[]; - maxVotes: number; - numWhiteVotes: number; - numWinners: number; - }[]; - startTime?: Date | null; - endTime?: Date | null; - isLocked?: boolean; + name?: string; + allowedMembers?: string[]; + roles?: { + role: string; + contestants: string[]; + maxVotes: number; + numWhiteVotes: number; + numWinners: number; + }[]; + startTime?: Date | null; + endTime?: Date | null; + isLocked?: boolean; } router.put('/update', async (req: Request, res: Response) => { - const { roundId, round } = req.body as { roundId: string; round: UpdateRoundRequest }; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId, round } = req.body as { roundId: string; round: UpdateRoundRequest }; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + if (!round || Object.keys(round).length === 0) { + console.log('❌ Round update object is empty'); + res.status(400).json({ ok: false, message: 'No changes provided' }); + return; + } + + try { + // Check if the round exists and hasn't started yet + const existingRound = await db.getRound({ _id: new ObjectId(roundId) }); + if (!existingRound) { + console.log(`❌ Round with ID ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } - if (!round || Object.keys(round).length === 0) { - console.log('❌ Round update object is empty'); - res.status(400).json({ ok: false, message: 'No changes provided' }); - return; + // Prevent editing rounds that have already started + if (existingRound.startTime) { + console.log(`❌ Cannot edit round ${roundId} - round has already started`); + return res + .status(400) + .json({ ok: false, message: 'Cannot edit round that has already started' }); } - try { - // Check if the round exists and hasn't started yet - const existingRound = await db.getRound({ _id: new ObjectId(roundId) }); - if (!existingRound) { - console.log(`❌ Round with ID ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - // Prevent editing rounds that have already started - if (existingRound.startTime) { - console.log(`❌ Cannot edit round ${roundId} - round has already started`); - return res.status(400).json({ ok: false, message: 'Cannot edit round that has already started' }); - } - - // Prevent editing locked rounds - if (existingRound.isLocked) { - console.log(`❌ Cannot edit round ${roundId} - round is locked`); - return res.status(400).json({ ok: false, message: 'Cannot edit locked round' }); - } - - // Check if round has any votes (active voting has occurred) - const votedMembers = await db.getVotedMembers(roundId); - if (votedMembers && votedMembers.length > 0) { - console.log(`❌ Cannot edit round ${roundId} - round has active votes`); - return res.status(400).json({ ok: false, message: 'Cannot edit round that has active votes' }); - } - - console.log('⏬ Updating Round...'); - console.log('Changes:', round); - - // Create the processed round object for database update - const processedRound: Partial = {}; - - // Copy simple fields - if (round.name !== undefined) processedRound.name = round.name; - if (round.startTime !== undefined) processedRound.startTime = round.startTime; - if (round.endTime !== undefined) processedRound.endTime = round.endTime; - if (round.isLocked !== undefined) processedRound.isLocked = round.isLocked; - - // Process allowedMembers if they are being updated - if (round.allowedMembers) { - processedRound.allowedMembers = await Promise.all( - round.allowedMembers.map(async (memberId: string) => { - const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); - if (!dbMember) { - console.log(`❌ Member with ID ${memberId} not found`); - throw new Error(`Member with ID ${memberId} not found`); - } - return dbMember; - }) - ); - } - - // Process roles if they are being updated - if (round.roles) { - processedRound.roles = await Promise.all( - round.roles.map(async (role: any) => { - const contestants = await Promise.all( - role.contestants.map(async (contestantId: string) => { - const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); - if (!dbContestant) { - console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); - throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); - } - return dbContestant; - }) - ); - if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, existingRound.eventId)); - } - return { ...role, contestants }; - }) - ); - } - - const roundResult = await db.updateRound({ _id: new ObjectId(roundId) }, processedRound); + // Prevent editing locked rounds + if (existingRound.isLocked) { + console.log(`❌ Cannot edit round ${roundId} - round is locked`); + return res.status(400).json({ ok: false, message: 'Cannot edit locked round' }); + } - if (!roundResult.acknowledged) { - console.log(`❌ Could not update Round`); - res.status(500).json({ ok: false, message: 'Could not update round' }); - return; - } + // Check if round has any votes (active voting has occurred) + const votedMembers = await db.getVotedMembers(roundId); + if (votedMembers && votedMembers.length > 0) { + console.log(`❌ Cannot edit round ${roundId} - round has active votes`); + return res + .status(400) + .json({ ok: false, message: 'Cannot edit round that has active votes' }); + } - res.json({ ok: true }); - } catch (error: any) { - console.error('❌ Error updating round:', error); - return res.status(500).json({ ok: false, message: error.message || 'Internal server error' }); + console.log('⏬ Updating Round...'); + console.log('Changes:', round); + + // Create the processed round object for database update + const processedRound: Partial = {}; + + // Copy simple fields + if (round.name !== undefined) processedRound.name = round.name; + if (round.startTime !== undefined) processedRound.startTime = round.startTime; + if (round.endTime !== undefined) processedRound.endTime = round.endTime; + if (round.isLocked !== undefined) processedRound.isLocked = round.isLocked; + + // Process allowedMembers if they are being updated + if (round.allowedMembers) { + processedRound.allowedMembers = await Promise.all( + round.allowedMembers.map(async (memberId: string) => { + const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); + if (!dbMember) { + console.log(`❌ Member with ID ${memberId} not found`); + throw new Error(`Member with ID ${memberId} not found`); + } + return dbMember; + }) + ); } -}); -router.delete('/delete', async (req: Request, res: Response) => { - const { roundId } = req.body as { roundId: string }; - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + // Process roles if they are being updated + if (round.roles) { + processedRound.roles = await Promise.all( + round.roles.map(async (role: any) => { + const contestants = await Promise.all( + role.contestants.map(async (contestantId: string) => { + const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); + if (!dbContestant) { + console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); + throw new Error( + `Contestant with ID ${contestantId} in role ${role.role} not found` + ); + } + return dbContestant; + }) + ); + if (role.numWhiteVotes > 0) { + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, existingRound.eventId)); + } + return { ...role, contestants }; + }) + ); } - console.log('⏬ Deleting Round...'); - const roundResult = await db.deleteRound({ _id: new ObjectId(roundId) }); + const roundResult = await db.updateRound({ _id: new ObjectId(roundId) }, processedRound); + if (!roundResult.acknowledged) { - console.log(`❌ Could not delete Round`); - res.status(500).json({ ok: false, message: 'Could not delete round' }); - return; + console.log(`❌ Could not update Round`); + res.status(500).json({ ok: false, message: 'Could not update round' }); + return; } res.json({ ok: true }); + } catch (error: any) { + console.error('❌ Error updating round:', error); + return res.status(500).json({ ok: false, message: error.message || 'Internal server error' }); + } }); -router.post('/lock/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; +router.delete('/delete', async (req: Request, res: Response) => { + const { roundId } = req.body as { roundId: string }; + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + console.log('⏬ Deleting Round...'); + + const roundResult = await db.deleteRound({ _id: new ObjectId(roundId) }); + if (!roundResult.acknowledged) { + console.log(`❌ Could not delete Round`); + res.status(500).json({ ok: false, message: 'Could not delete round' }); + return; + } + + res.json({ ok: true }); +}); - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; +router.post('/lock/:roundId', async (req: Request, res: Response) => { + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (isLocked) { + console.log(`❌ Round ${roundId} is already locked`); + return res.status(400).json({ ok: false, message: 'Round is already locked' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (isLocked) { - console.log(`❌ Round ${roundId} is already locked`); - return res.status(400).json({ ok: false, message: 'Round is already locked' }); - } - - // Set both isLocked and endTime when locking a round - await db.lockRound(roundId); - await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: new Date() }); - console.log(`✅ Round ${roundId} locked successfully`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error locking round:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + // Set both isLocked and endTime when locking a round + await db.lockRound(roundId); + await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: new Date() }); + console.log(`✅ Round ${roundId} locked successfully`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error locking round:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.post('/unlock/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (!isLocked) { + console.log(`❌ Round ${roundId} is not locked`); + return res.status(400).json({ ok: false, message: 'Round is not locked' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (!isLocked) { - console.log(`❌ Round ${roundId} is not locked`); - return res.status(400).json({ ok: false, message: 'Round is not locked' }); - } + // Unlock the round and clear endTime, but keep startTime + await db.unlockRound(roundId); + await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: null }); - // Unlock the round and clear endTime, but keep startTime - await db.unlockRound(roundId); - await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: null }); + // Delete all votes for this round when unlocking + await db.deleteRoundVotes(roundId); - // Delete all votes for this round when unlocking - await db.deleteRoundVotes(roundId); - - console.log(`✅ Round ${roundId} unlocked successfully`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error unlocking round:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + console.log(`✅ Round ${roundId} unlocked successfully`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error unlocking round:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/status/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; - } - - try { - const isLocked = await db.isRoundLocked(roundId); - console.log(`✅ Round ${roundId} lock status retrieved`); - return res.json({ ok: true, isLocked }); - } catch (error) { - console.error('❌ Error getting round status:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + console.log(`✅ Round ${roundId} lock status retrieved`); + return res.json({ ok: true, isLocked }); + } catch (error) { + console.error('❌ Error getting round status:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/results/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (!isLocked) { + console.log(`❌ Cannot view results for unlocked round ${roundId}`); + return res.status(400).json({ ok: false, message: 'Round must be locked to view results' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (!isLocked) { - console.log(`❌ Cannot view results for unlocked round ${roundId}`); - return res.status(400).json({ ok: false, message: 'Round must be locked to view results' }); - } - - const results = await db.getRoundResults(roundId); - if (!results) { - console.log(`❌ Round ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - console.log(`✅ Round ${roundId} results retrieved`); - return res.json({ ok: true, results }); - } catch (error) { - console.error('❌ Error getting round results:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); + const results = await db.getRoundResults(roundId); + if (!results) { + console.log(`❌ Round ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } + + console.log(`✅ Round ${roundId} results retrieved`); + return res.json({ ok: true, results }); + } catch (error) { + console.error('❌ Error getting round results:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/votedMembers/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; - } - console.log(`⏬ Getting voted members for round ${roundId}`); - const votedMembers = await db.getVotedMembers(roundId); - if (!votedMembers) { - console.log(`❌ Could not get voted members`); - res.status(500).json({ ok: false, message: 'Could not get voted members' }); - return; - } - res.json({ ok: true, votedMembers }); + const { roundId } = req.params; + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + console.log(`⏬ Getting voted members for round ${roundId}`); + const votedMembers = await db.getVotedMembers(roundId); + if (!votedMembers) { + console.log(`❌ Could not get voted members`); + res.status(500).json({ ok: false, message: 'Could not get voted members' }); + return; + } + res.json({ ok: true, votedMembers }); }); export default router; diff --git a/apps/backend/src/routers/api/events/state.ts b/apps/backend/src/routers/api/events/state.ts index 3bc2a8f..fbedbc8 100644 --- a/apps/backend/src/routers/api/events/state.ts +++ b/apps/backend/src/routers/api/events/state.ts @@ -6,33 +6,32 @@ import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); router.get('/', (req: Request, res: Response) => { + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } - const eventId = req.params.eventId; - if (!eventId) { - console.log('❌ Event ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Event ID is missing' }); - } - - console.log(`⏬ Getting Election state`); - db.getElectionState({ - eventId: new ObjectId(eventId) - }).then(divisionState => res.json(divisionState)); + console.log(`⏬ Getting Election state`); + db.getElectionState({ + eventId: new ObjectId(eventId) + }).then(divisionState => res.json(divisionState)); }); router.put('/', (req: Request, res: Response) => { - const body: Partial = { ...req.body }; - if (!body) return res.status(400).json({ ok: false }); + const body: Partial = { ...req.body }; + if (!body) return res.status(400).json({ ok: false }); - console.log(`⏬ Updating Election state`); - db.updateElectionState(body).then(task => { - if (task.acknowledged) { - console.log('✅ Election state updated!'); - return res.json({ ok: true, id: task.upsertedId }); - } else { - console.log('❌ Could not update Election state'); - return res.status(500).json({ ok: false }); - } - }); + console.log(`⏬ Updating Election state`); + db.updateElectionState(body).then(task => { + if (task.acknowledged) { + console.log('✅ Election state updated!'); + return res.json({ ok: true, id: task.upsertedId }); + } else { + console.log('❌ Could not update Election state'); + return res.status(500).json({ ok: false }); + } + }); }); export default router; diff --git a/apps/backend/src/routers/api/events/vote.ts b/apps/backend/src/routers/api/events/vote.ts index e93aca8..4294b80 100644 --- a/apps/backend/src/routers/api/events/vote.ts +++ b/apps/backend/src/routers/api/events/vote.ts @@ -6,90 +6,89 @@ import { Member, Positions } from '@mtes/types'; const router = express.Router({ mergeParams: true }); const getWhiteVoteMember = (id: string, eventId?: string): WithId => { - return { - _id: new ObjectId(id), - eventId: eventId ? new ObjectId(eventId) : new ObjectId(), // Provide eventId or fallback - name: `פתק לבן ${Number(id)}`, - city: 'אין אמון באף אחד', - isPresent: true, - isMM: false, - }; -} + return { + _id: new ObjectId(id), + eventId: eventId ? new ObjectId(eventId) : new ObjectId(), // Provide eventId or fallback + name: `פתק לבן ${Number(id)}`, + city: 'אין אמון באף אחד', + isPresent: true, + isMM: false + }; +}; router.post('/', async (req: Request, res: Response) => { - const { roundId, memberId, votes, votingStandId, signature } = req.body; + const { roundId, memberId, votes, votingStandId, signature } = req.body; - if (!roundId || !memberId || !votes || votingStandId === undefined || signature === undefined) { - console.log('❌ Missing required vote data'); - return res.status(400).json({ ok: false, message: 'Missing required vote data' }); + if (!roundId || !memberId || !votes || votingStandId === undefined || signature === undefined) { + console.log('❌ Missing required vote data'); + return res.status(400).json({ ok: false, message: 'Missing required vote data' }); + } + + try { + const round = await db.getRound({ _id: new ObjectId(roundId) }); + const member = await db.getMember({ _id: new ObjectId(memberId) }); + + if (!round) { + console.log(`❌ Round with ID ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } - try { - const round = await db.getRound({ _id: new ObjectId(roundId) }); - const member = await db.getMember({ _id: new ObjectId(memberId) }); - - if (!round) { - console.log(`❌ Round with ID ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - if (!member) { - console.log(`❌ Member with ID ${memberId} not found`); - return res.status(404).json({ ok: false, message: 'Member not found' }); - } - - const isLocked = await db.isRoundLocked(roundId); - if (isLocked) { - console.log(`❌ Round ${roundId} is locked`); - return res.status(400).json({ ok: false, message: 'Round is locked' }); - } - - const hasMemberVoted = await db.hasMemberVoted(round._id.toString(), member._id.toString()); - if (hasMemberVoted) { - console.log(`❌ Member has already voted in this round`); - return res.status(400).json({ ok: false, message: 'Member has already voted' }); - } - - console.log(`⏬ Processing vote for ${member.name} in round - ${round.name}`); - - const votePromises = Object.entries(votes).map(async ([role, contestantIds]) => { - if (Array.isArray(contestantIds)) { - return Promise.all( - contestantIds.map(async (contestantId: string) => { - console.log(`⏬ Processing vote for role ${role} and contestant ID ${contestantId}`); - const contestant = - contestantId.startsWith('00000000000000000000') // 20 times '0' for white vote as a white vote id is 24(last 4 characters are for id) - ? getWhiteVoteMember(contestantId, round?.eventId?.toString()) - : await db.getMember({ _id: new ObjectId(contestantId) }); - - if (!contestant) { - console.log(`❌ Contestant with ID ${contestantId} not found`); - return null; - } - - const vote = { - round: round._id, - role: role as Positions, - contestant: contestant._id - }; - - console.log('Vote:', JSON.stringify(vote)); - return db.addVote(vote); - }) - ); - } - return []; // Ensure a promise is always returned - }); + if (!member) { + console.log(`❌ Member with ID ${memberId} not found`); + return res.status(404).json({ ok: false, message: 'Member not found' }); + } - await Promise.all(votePromises.flat()); // flat might be needed if inner promises are nested - await db.markMemberVoted(round._id.toString(), member._id.toString(), signature); + const isLocked = await db.isRoundLocked(roundId); + if (isLocked) { + console.log(`❌ Round ${roundId} is locked`); + return res.status(400).json({ ok: false, message: 'Round is locked' }); + } - console.log('✅ Votes recorded successfully'); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error processing vote:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); + const hasMemberVoted = await db.hasMemberVoted(round._id.toString(), member._id.toString()); + if (hasMemberVoted) { + console.log(`❌ Member has already voted in this round`); + return res.status(400).json({ ok: false, message: 'Member has already voted' }); } + + console.log(`⏬ Processing vote for ${member.name} in round - ${round.name}`); + + const votePromises = Object.entries(votes).map(async ([role, contestantIds]) => { + if (Array.isArray(contestantIds)) { + return Promise.all( + contestantIds.map(async (contestantId: string) => { + console.log(`⏬ Processing vote for role ${role} and contestant ID ${contestantId}`); + const contestant = contestantId.startsWith('00000000000000000000') // 20 times '0' for white vote as a white vote id is 24(last 4 characters are for id) + ? getWhiteVoteMember(contestantId, round?.eventId?.toString()) + : await db.getMember({ _id: new ObjectId(contestantId) }); + + if (!contestant) { + console.log(`❌ Contestant with ID ${contestantId} not found`); + return null; + } + + const vote = { + round: round._id, + role: role as Positions, + contestant: contestant._id + }; + + console.log('Vote:', JSON.stringify(vote)); + return db.addVote(vote); + }) + ); + } + return []; // Ensure a promise is always returned + }); + + await Promise.all(votePromises.flat()); // flat might be needed if inner promises are nested + await db.markMemberVoted(round._id.toString(), member._id.toString(), signature); + + console.log('✅ Votes recorded successfully'); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error processing vote:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); export default router; diff --git a/apps/backend/src/routers/auth.ts b/apps/backend/src/routers/auth.ts index 314dc2b..fa8ebfa 100644 --- a/apps/backend/src/routers/auth.ts +++ b/apps/backend/src/routers/auth.ts @@ -55,4 +55,4 @@ router.post('/logout', (req: Request, res: Response) => { return res.json({ ok: true }); }); -export default router; \ No newline at end of file +export default router; diff --git a/apps/backend/src/websocket/handlers.ts b/apps/backend/src/websocket/handlers.ts index d4d7719..fffa636 100644 --- a/apps/backend/src/websocket/handlers.ts +++ b/apps/backend/src/websocket/handlers.ts @@ -88,7 +88,6 @@ export const handleUpdateMemberPresence = async ( replacedBy: WithId | null, callback: ((response: { ok: boolean; error?: string }) => void) | undefined ) => { - if (!memberId) { console.log('❌ Member ID is null or undefined'); if (typeof callback === 'function') { @@ -102,7 +101,7 @@ export const handleUpdateMemberPresence = async ( const updatePayload: Partial = { isPresent, - replacedBy: replacedBy ? replacedBy as WithId : null + replacedBy: replacedBy ? (replacedBy as WithId) : null }; try { @@ -112,7 +111,9 @@ export const handleUpdateMemberPresence = async ( updatePayload as unknown as Partial ); if (!result.acknowledged || result.matchedCount === 0) { - console.log(`❌ Could not update presence for member ${memberId}. Member not found or update failed.`); + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); if (typeof callback === 'function') { callback({ ok: false, error: 'Member not found or update failed' }); } @@ -125,7 +126,9 @@ export const handleUpdateMemberPresence = async ( updatePayload as unknown as Partial ); if (!result.acknowledged || result.matchedCount === 0) { - console.log(`❌ Could not update presence for member ${memberId}. Member not found or update failed.`); + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); if (typeof callback === 'function') { callback({ ok: false, error: 'Member not found or update failed' }); } @@ -133,24 +136,23 @@ export const handleUpdateMemberPresence = async ( } console.log(`✅ Presence updated for member ${memberId}`); } - namespace.emit('memberPresenceUpdated', - memberId, - isMM, - isPresent, - replacedBy - ); - + namespace.emit('memberPresenceUpdated', memberId, isMM, isPresent, replacedBy); } catch (error) { console.error('❌ Error updating member presence:', error); if (typeof callback === 'function') { callback({ ok: false, error: 'Internal server error while updating member presence' }); } } -} +}; export const handleUpdateAudienceDisplay = async ( namespace: any, - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string }, + view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }, callback: ((response: { ok: boolean; error?: string }) => void) | undefined ) => { console.log(`🔌 WS: Update audience display to ${view}`); @@ -173,4 +175,4 @@ export const handleUpdateAudienceDisplay = async ( callback({ ok: false, error: 'Failed to update audience display' }); } } -} \ No newline at end of file +}; diff --git a/apps/backend/src/websocket/index.ts b/apps/backend/src/websocket/index.ts index e18a1ab..19de563 100644 --- a/apps/backend/src/websocket/index.ts +++ b/apps/backend/src/websocket/index.ts @@ -1,10 +1,13 @@ import { Namespace, Socket } from 'socket.io'; +import { WSServerEmittedEvents, WSClientEmittedEvents, WSInterServerEvents } from '@mtes/types'; import { - WSServerEmittedEvents, - WSClientEmittedEvents, - WSInterServerEvents, -} from '@mtes/types'; -import { handleLoadRound, handleLoadVotingMember, handleUpdateAudienceDisplay, handleUpdateMemberPresence, handleVoteProcessed, handleVoteSubmitted } from './handlers'; + handleLoadRound, + handleLoadVotingMember, + handleUpdateAudienceDisplay, + handleUpdateMemberPresence, + handleVoteProcessed, + handleVoteSubmitted +} from './handlers'; const websocket = ( socket: Socket @@ -18,7 +21,7 @@ const websocket = ( socket.on('updateMemberPresence', (...args) => { handleUpdateMemberPresence(namespace, ...args); - }) + }); socket.on('loadVotingMember', (...args) => handleLoadVotingMember(namespace, ...args)); @@ -28,13 +31,13 @@ const websocket = ( handleUpdateAudienceDisplay(namespace, view, callback); }); - socket.on('voteSubmitted', ((...args) => { + socket.on('voteSubmitted', (...args) => { handleVoteSubmitted(namespace, ...args); - })); + }); - socket.on('voteProcessed', ((...args) => { + socket.on('voteProcessed', (...args) => { handleVoteProcessed(namespace, ...args); - })); + }); socket.on('disconnect', () => { console.log(`❌ WS: Disconnection`); @@ -42,4 +45,3 @@ const websocket = ( }; export default websocket; - diff --git a/apps/frontend/components/admin/ChangePasswordDialog.tsx b/apps/frontend/components/admin/ChangePasswordDialog.tsx index f914061..e261d7a 100644 --- a/apps/frontend/components/admin/ChangePasswordDialog.tsx +++ b/apps/frontend/components/admin/ChangePasswordDialog.tsx @@ -97,8 +97,8 @@ const ChangePasswordDialog: React.FC = ({ open, onClo }; return ( - = ({ open, onClo שינוי סיסמת מנהל - + {error && ( @@ -124,7 +124,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="סיסמה נוכחית" type={showCurrentPassword ? 'text' : 'password'} value={currentPassword} - onChange={(e) => setCurrentPassword(e.target.value)} + onChange={e => setCurrentPassword(e.target.value)} disabled={loading} InputProps={{ dir: 'ltr', @@ -147,7 +147,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="סיסמה חדשה" type={showNewPassword ? 'text' : 'password'} value={newPassword} - onChange={(e) => setNewPassword(e.target.value)} + onChange={e => setNewPassword(e.target.value)} disabled={loading} helperText="לפחות 4 תווים" InputProps={{ @@ -171,7 +171,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="אימות סיסמה חדשה" type={showConfirmPassword ? 'text' : 'password'} value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} + onChange={e => setConfirmPassword(e.target.value)} disabled={loading} InputProps={{ dir: 'ltr', @@ -192,18 +192,10 @@ const ChangePasswordDialog: React.FC = ({ open, onClo - - diff --git a/apps/frontend/components/connection-indicator.tsx b/apps/frontend/components/connection-indicator.tsx index 89568ab..7d08d33 100644 --- a/apps/frontend/components/connection-indicator.tsx +++ b/apps/frontend/components/connection-indicator.tsx @@ -13,26 +13,26 @@ const config: { rippleColor: '#3cd3b2', textColor: '#111111', backgroundColor: '#f4f4f4', - text: 'מחובר', + text: 'מחובר' }, connecting: { rippleColor: '#a21caf', textColor: '#000000', backgroundColor: '#f4f4f4', - text: 'מתחבר...', + text: 'מתחבר...' }, disconnected: { rippleColor: '#f87171', textColor: '#000000', backgroundColor: '#f4f4f4', - text: 'מנותק', + text: 'מנותק' }, error: { rippleColor: '#ffffff', textColor: '#ffffff', backgroundColor: '#dc2626', - text: 'שגיאה', - }, + text: 'שגיאה' + } } as const; const rippleAnimation = keyframes` @@ -45,9 +45,7 @@ interface ConnectionIndicatorProps { status: ConnectionStatus; } -const ConnectionIndicator: React.FC = ({ - status, -}) => { +const ConnectionIndicator: React.FC = ({ status }) => { const { rippleColor, textColor, backgroundColor, text } = config[status]; return ( = ({ fontSize: '0.875rem', fontWeight: 500, minWidth: 100, - transition: 'all 0.2s ease-in-out', + transition: 'all 0.2s ease-in-out' }} > = ({ boxShadow: `0 0 0 0.25rem ${rippleColor}33`, mr: 1.25, animation: `${rippleAnimation} 2s linear infinite`, - transition: 'all 0.2s ease-in-out', + transition: 'all 0.2s ease-in-out' }} /> diff --git a/apps/frontend/components/general/event-selector.tsx b/apps/frontend/components/general/event-selector.tsx index 5a147e6..6a08a86 100644 --- a/apps/frontend/components/general/event-selector.tsx +++ b/apps/frontend/components/general/event-selector.tsx @@ -15,11 +15,7 @@ interface EventSelectorProps { getEventDisabled?: (event: WithId) => boolean; } -const EventSelector: React.FC = ({ - events, - onChange, - getEventDisabled, -}) => { +const EventSelector: React.FC = ({ events, onChange, getEventDisabled }) => { const sortedEvents = useMemo( () => events.sort((a, b) => { @@ -37,8 +33,7 @@ const EventSelector: React.FC = ({ return ( {sortedEvents.map(event => { - const disabled = - getEventDisabled?.(event) + const disabled = getEventDisabled?.(event); return ( diff --git a/apps/frontend/components/general/login/admin-login-form.tsx b/apps/frontend/components/general/login/admin-login-form.tsx index d537b2a..eea43c2 100644 --- a/apps/frontend/components/general/login/admin-login-form.tsx +++ b/apps/frontend/components/general/login/admin-login-form.tsx @@ -25,10 +25,10 @@ const AdminLoginForm: React.FC = ({ eventId }) => { isAdmin: true, username, password, - eventId: eventId ? String(eventId) : undefined, - }), + eventId: eventId ? String(eventId) : undefined + }) }) - .then(async (res) => { + .then(async res => { const data = await res.json(); if (data && !data.error) { document.getElementById('recaptcha-script')?.remove(); @@ -39,16 +39,14 @@ const AdminLoginForm: React.FC = ({ eventId }) => { enqueueSnackbar('אופס, הסיסמה שגויה.', { variant: 'error' }); } else { enqueueSnackbar('הגישה נדחתה, נסו שנית מאוחר יותר.', { - variant: 'error', + variant: 'error' }); } } else { throw new Error(res.statusText); } }) - .catch(() => - enqueueSnackbar('אופס, החיבור לשרת נכשל.', { variant: 'error' }) - ); + .catch(() => enqueueSnackbar('אופס, החיבור לשרת נכשל.', { variant: 'error' })); }; const handleSubmit = async (e: React.FormEvent) => { @@ -58,12 +56,7 @@ const AdminLoginForm: React.FC = ({ eventId }) => { }; return ( - + התחברות כמנהל @@ -73,7 +66,7 @@ const AdminLoginForm: React.FC = ({ eventId }) => { type="username" label="שם משתמש" value={username} - onChange={(e) => setUsername(e.target.value)} + onChange={e => setUsername(e.target.value)} fullWidth /> = ({ eventId }) => { type="password" label="סיסמה" value={password} - onChange={(e) => setPassword(e.target.value)} + onChange={e => setPassword(e.target.value)} inputProps={{ dir: 'ltr' }} /> diff --git a/apps/frontend/components/layout.tsx b/apps/frontend/components/layout.tsx index b9c0bff..aabbaaf 100644 --- a/apps/frontend/components/layout.tsx +++ b/apps/frontend/components/layout.tsx @@ -15,7 +15,7 @@ import { Stack, Toolbar, Tooltip, - Typography, + Typography } from '@mui/material'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import LogoutIcon from '@mui/icons-material/Logout'; @@ -44,7 +44,7 @@ const Layout: React.FC = ({ maxWidth = 'lg', action, error, - color, + color }) => { const router = useRouter(); const [open, setOpen] = useState(false); @@ -54,7 +54,7 @@ const Layout: React.FC = ({ const handleBack = () => { const queryString = router.query.divisionId ? new URLSearchParams({ - divisionId: router.query.divisionId as string, + divisionId: router.query.divisionId as string }).toString() : ''; const url = `${back}${queryString ? `?${queryString}` : ''}`; @@ -62,7 +62,7 @@ const Layout: React.FC = ({ }; const logout = () => { - apiFetch('/auth/logout', { method: 'POST' }).then(res => router.push('/')); + apiFetch('/auth/logout', { method: 'POST' }).then(res => router.push('/')); }; return ( @@ -73,7 +73,7 @@ const Layout: React.FC = ({ position="fixed" sx={{ // animation: isError ? `${errorAnimation} 1s linear infinite alternate` : undefined, - borderBottom: color && `5px solid ${color}`, + borderBottom: color && `5px solid ${color}` }} > @@ -102,9 +102,7 @@ const Layout: React.FC = ({ - {connectionStatus && ( - - )} + {connectionStatus && } {action} @@ -115,7 +113,7 @@ const Layout: React.FC = ({ - theme.mixins.toolbar.minHeight }} /> + theme.mixins.toolbar.minHeight }} /> )} = ({ {children} @@ -166,7 +160,7 @@ const Layout: React.FC = ({ bottom: '2rem', right: '12%', left: '12%', - borderRadius: 4, + borderRadius: 4 }} > שגיאה בלתי צפויה, אנא פנו למנהל המערכת. diff --git a/apps/frontend/components/mtes/add-round-dialog.tsx b/apps/frontend/components/mtes/add-round-dialog.tsx index 0110797..b5e2af0 100644 --- a/apps/frontend/components/mtes/add-round-dialog.tsx +++ b/apps/frontend/components/mtes/add-round-dialog.tsx @@ -303,7 +303,7 @@ const AddRoundDialog: React.FC = ({ } if (Object.keys(changes).length > 0) { - const res = await apiFetch(`api/events/${eventId}/rounds/update`, { + const res = await apiFetch(`/api/events/${eventId}/rounds/update`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/apps/frontend/components/mtes/control-rounds.tsx b/apps/frontend/components/mtes/control-rounds.tsx index 14f0387..28eb408 100644 --- a/apps/frontend/components/mtes/control-rounds.tsx +++ b/apps/frontend/components/mtes/control-rounds.tsx @@ -9,7 +9,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import EditIcon from '@mui/icons-material/Edit'; import { WithId } from 'mongodb'; -import { apiFetch } from 'apps/frontend/lib/utils/fetch'; +import { apiFetch } from '../../lib/utils/fetch'; import router from 'next/router'; import AddRoundDialog from './add-round-dialog'; diff --git a/apps/frontend/localization/displays.ts b/apps/frontend/localization/displays.ts index ae28178..9a8d342 100644 --- a/apps/frontend/localization/displays.ts +++ b/apps/frontend/localization/displays.ts @@ -1,9 +1,9 @@ import { AudienceDisplayScreen } from '@mtes/types'; export const localizedAudienceDisplayScreens: Record = { - 'presence': 'נוכחות', - 'voting': 'הצבעה', - 'round': 'סיבוב', - 'member': 'חבר', - 'message': 'הודעה', + presence: 'נוכחות', + voting: 'הצבעה', + round: 'סיבוב', + member: 'חבר', + message: 'הודעה' }; diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 342fb2c..0c3d7a7 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -10,21 +10,21 @@ const nextConfig = { nx: { // Set this to true if you would like to to use SVGR // See: https://github.com/gregberge/svgr - svgr: false, + svgr: false }, compiler: { // For other options, see https://nextjs.org/docs/architecture/nextjs-compiler#emotion - emotion: true, + emotion: true }, // Enable standalone output for better Docker deployment - output: 'standalone', + output: 'standalone' }; const plugins = [ // Add more Next.js plugins to this list if needed. - withNx, + withNx ]; module.exports = composePlugins(...plugins)(nextConfig); diff --git a/apps/frontend/pages/mtes/audience-display.tsx b/apps/frontend/pages/mtes/audience-display.tsx index ae57cd5..64ed16f 100644 --- a/apps/frontend/pages/mtes/audience-display.tsx +++ b/apps/frontend/pages/mtes/audience-display.tsx @@ -14,7 +14,7 @@ import { } from '@mtes/types'; import { WaitingState } from 'apps/frontend/components/mtes/waiting-state'; import { useWebsocket } from 'apps/frontend/hooks/use-websocket'; -import { apiFetch, getUserAndDivision, serverSideGetRequests } from 'apps/frontend/lib/utils/fetch'; +import { apiFetch, getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; import { Box, Typography, Grid, Paper, Container, Avatar } from '@mui/material'; import Layout from '../../components/layout'; import { AudiencePresence } from 'apps/frontend/components/mtes/audience/audience-presence'; diff --git a/apps/frontend/pages/mtes/index.tsx b/apps/frontend/pages/mtes/index.tsx index 9aaea98..03642e0 100644 --- a/apps/frontend/pages/mtes/index.tsx +++ b/apps/frontend/pages/mtes/index.tsx @@ -17,4 +17,4 @@ export const getServerSideProps: GetServerSideProps = async ctx => { }; }; -export default Page; \ No newline at end of file +export default Page; diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index 46c6b7e..676e452 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -16,7 +16,7 @@ { "name": "next" } - ], + ] }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/libs/database/src/lib/crud/cities.ts b/libs/database/src/lib/crud/cities.ts index 6de4a2e..a5f8c01 100644 --- a/libs/database/src/lib/crud/cities.ts +++ b/libs/database/src/lib/crud/cities.ts @@ -3,30 +3,29 @@ import { City } from '@mtes/types'; import db from '../database'; export const getCities = (filter: Filter) => { - return db.collection('cities').find(filter).toArray(); + return db.collection('cities').find(filter).toArray(); }; export const getCity = (filter: Filter) => { - return db.collection('cities').findOne(filter); + return db.collection('cities').findOne(filter); }; export const addCity = (city: City) => { - return db.collection('cities').insertOne(city); + return db.collection('cities').insertOne(city); }; export const addCities = (cities: City[]) => { - return db.collection('cities').insertMany(cities); -} - + return db.collection('cities').insertMany(cities); +}; export const updateCity = (filter: Filter, newCity: Partial, upsert = false) => { - return db.collection('cities').updateOne(filter, { $set: newCity }, { upsert }); + return db.collection('cities').updateOne(filter, { $set: newCity }, { upsert }); }; export const deleteCity = (filter: Filter) => { - return db.collection('cities').deleteOne(filter); + return db.collection('cities').deleteOne(filter); }; export const deleteCities = (filter: Filter) => { - return db.collection('cities').deleteMany(filter); + return db.collection('cities').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/contestants.ts b/libs/database/src/lib/crud/contestants.ts index cbe61fa..c611fe5 100644 --- a/libs/database/src/lib/crud/contestants.ts +++ b/libs/database/src/lib/crud/contestants.ts @@ -18,8 +18,14 @@ export const addContestants = (contestants: Array) => { return db.collection('contestants').insertMany(contestants); }; -export const updateContestant = (filter: Filter, newContestant: Partial, upsert = false) => { - return db.collection('contestants').updateOne(filter, { $set: newContestant }, { upsert }); +export const updateContestant = ( + filter: Filter, + newContestant: Partial, + upsert = false +) => { + return db + .collection('contestants') + .updateOne(filter, { $set: newContestant }, { upsert }); }; export const deleteContestant = (filter: Filter) => { diff --git a/libs/database/src/lib/crud/members.ts b/libs/database/src/lib/crud/members.ts index 2f968a6..fd7fc05 100644 --- a/libs/database/src/lib/crud/members.ts +++ b/libs/database/src/lib/crud/members.ts @@ -24,8 +24,7 @@ export const addMembers = (members: Array) => { replacedBy: member.replacedBy ?? null, isMM: member.isMM ?? false }; - } - ) + }); return db.collection('members').insertMany(validMembers); }; diff --git a/libs/database/src/lib/crud/mm-members.ts b/libs/database/src/lib/crud/mm-members.ts index 6b880cc..7aba94b 100644 --- a/libs/database/src/lib/crud/mm-members.ts +++ b/libs/database/src/lib/crud/mm-members.ts @@ -3,43 +3,43 @@ import { Member } from '@mtes/types'; import db from '../database'; export const getMmMember = (filter: Filter) => { - return db.collection('mm-members').findOne(filter); + return db.collection('mm-members').findOne(filter); }; export const getMmMembers = (filter: Filter) => { - return db.collection('mm-members').find(filter).toArray(); + return db.collection('mm-members').find(filter).toArray(); }; export const addMmMember = (mmMember: Member) => { - return db.collection('mm-members').insertOne(mmMember); + return db.collection('mm-members').insertOne(mmMember); }; export const addMmMembers = (mmMembers: Array) => { - const validMmMembers = mmMembers.map(mmMember => { - return { - eventId: mmMember.eventId, - name: mmMember.name, - city: mmMember.city, - isPresent: mmMember.isPresent ?? false, - isMM: mmMember.isMM ?? false, - replacedBy: mmMember.replacedBy ?? null - }; - }); - return db.collection('mm-members').insertMany(validMmMembers); + const validMmMembers = mmMembers.map(mmMember => { + return { + eventId: mmMember.eventId, + name: mmMember.name, + city: mmMember.city, + isPresent: mmMember.isPresent ?? false, + isMM: mmMember.isMM ?? false, + replacedBy: mmMember.replacedBy ?? null + }; + }); + return db.collection('mm-members').insertMany(validMmMembers); }; export const updateMmMember = ( - filter: Filter, - newMmMember: Partial, - upsert = false + filter: Filter, + newMmMember: Partial, + upsert = false ) => { - return db.collection('mm-members').updateOne(filter, { $set: newMmMember }, { upsert }); + return db.collection('mm-members').updateOne(filter, { $set: newMmMember }, { upsert }); }; export const deleteMmMember = (filter: Filter) => { - return db.collection('mm-members').deleteOne(filter); + return db.collection('mm-members').deleteOne(filter); }; export const deleteMmMembers = (filter: Filter) => { - return db.collection('mm-members').deleteMany(filter); + return db.collection('mm-members').deleteMany(filter); }; diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 85f4562..6f4eb00 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -12,4 +12,4 @@ export * from './lib/schemas/round'; export * from './lib/schemas/city'; // Added City schema export export * from './lib/positions'; export * from './lib/schemas/vote'; -export * from './lib/voting-states' +export * from './lib/voting-states'; diff --git a/libs/types/src/lib/constants.ts b/libs/types/src/lib/constants.ts index 5df051a..b87ddfd 100644 --- a/libs/types/src/lib/constants.ts +++ b/libs/types/src/lib/constants.ts @@ -88,7 +88,11 @@ export const TicketTypes = ['general', 'schedule', 'utilities', 'incident'] as c export type TicketType = (typeof TicketTypes)[number]; export const AudienceDisplayScreenTypes = [ - 'round', 'presence', 'voting', 'member', 'message' + 'round', + 'presence', + 'voting', + 'member', + 'message' ] as const; export type AudienceDisplayScreen = (typeof AudienceDisplayScreenTypes)[number]; diff --git a/libs/types/src/lib/schemas/city.ts b/libs/types/src/lib/schemas/city.ts index 81d856c..2f2e222 100644 --- a/libs/types/src/lib/schemas/city.ts +++ b/libs/types/src/lib/schemas/city.ts @@ -1,4 +1,4 @@ export interface City { - name: string; - numOfVoters: number; + name: string; + numOfVoters: number; } diff --git a/libs/types/src/lib/schemas/election-state.ts b/libs/types/src/lib/schemas/election-state.ts index d35a08f..095e2f8 100644 --- a/libs/types/src/lib/schemas/election-state.ts +++ b/libs/types/src/lib/schemas/election-state.ts @@ -6,6 +6,11 @@ import { Member } from './member'; export interface ElectionState { eventId: ObjectId; activeRound: WithId; - audienceDisplay: { display: AudienceDisplayScreen; round?: WithId; member?: WithId; message?: string }; + audienceDisplay: { + display: AudienceDisplayScreen; + round?: WithId; + member?: WithId; + message?: string; + }; completed: boolean; } diff --git a/libs/types/src/lib/schemas/member.ts b/libs/types/src/lib/schemas/member.ts index 89393f2..f8f556d 100644 --- a/libs/types/src/lib/schemas/member.ts +++ b/libs/types/src/lib/schemas/member.ts @@ -7,4 +7,4 @@ export interface Member { isPresent: boolean; replacedBy?: WithId | null; isMM: boolean; -} \ No newline at end of file +} diff --git a/libs/types/src/lib/websocket.ts b/libs/types/src/lib/websocket.ts index 526638b..4e3f2ee 100644 --- a/libs/types/src/lib/websocket.ts +++ b/libs/types/src/lib/websocket.ts @@ -3,7 +3,6 @@ import { AwardNames, TicketType } from './constants'; import { Member } from './schemas/member'; import { Round } from './schemas/round'; - export interface WSServerEmittedEvents { votingMemberLoaded: (member: WithId, votingStand: number) => void; roundLoaded: (roundId: string) => void; @@ -13,9 +12,12 @@ export interface WSServerEmittedEvents { isPresent: boolean, replacedBy: WithId | null ) => void; - audienceDisplayUpdated: ( - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string } - ) => void; + audienceDisplayUpdated: (view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }) => void; } export interface WSClientEmittedEvents { @@ -53,7 +55,12 @@ export interface WSClientEmittedEvents { ) => void; updateAudienceDisplay: ( - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string }, + view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }, callback: (response: { ok: boolean; error?: string }) => void ) => void; } diff --git a/nx.json b/nx.json index 084b9d5..502a8c2 100644 --- a/nx.json +++ b/nx.json @@ -2,13 +2,8 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "defaultBase": "master", "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "sharedGlobals" - ], - "production": [ - "default" - ], + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "production": ["default"], "sharedGlobals": [] }, "plugins": [ @@ -53,23 +48,13 @@ "targetDefaults": { "@nx/js:tsc": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] }, "@nx/esbuild:esbuild": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } } } diff --git a/package.json b/package.json index de3c2e1..e58550b 100644 --- a/package.json +++ b/package.json @@ -65,4 +65,4 @@ "typescript": "~5.6.2", "webpack-cli": "^5.1.4" } -} \ No newline at end of file +}