From 8c353169b3f4761fd76a4567b941fd4a5d7d3a65 Mon Sep 17 00:00:00 2001 From: Daniel Heidemann Date: Sat, 23 Nov 2024 13:54:26 +0100 Subject: [PATCH] fixed registration & client side: add tutorials --- .../app/(registration)/register/layout.tsx | 5 +- frontend/app/(registration)/register/page.tsx | 303 ++++++------- .../components/event-dialog/event-dialog.tsx | 2 + .../event-dialog/room-selection.tsx | 95 +--- .../event-dialog/tutor-selection.tsx | 96 ++--- .../event-dialog/tutorials-table.tsx | 404 ++++++++++++------ frontend/components/header.tsx | 23 +- frontend/components/planner.tsx | 8 +- frontend/components/sign-in-dialog.tsx | 1 - server/graph/schema.resolvers.go | 2 +- 10 files changed, 478 insertions(+), 461 deletions(-) diff --git a/frontend/app/(registration)/register/layout.tsx b/frontend/app/(registration)/register/layout.tsx index fc5e979..5cbbd03 100644 --- a/frontend/app/(registration)/register/layout.tsx +++ b/frontend/app/(registration)/register/layout.tsx @@ -7,9 +7,10 @@ interface RegistrationLayoutProps { export default function RegistrationLayout({ children }: RegistrationLayoutProps) { return ( -
+
-
{children}
+
+
{children}
); diff --git a/frontend/app/(registration)/register/page.tsx b/frontend/app/(registration)/register/page.tsx index 8e68460..a639cfe 100644 --- a/frontend/app/(registration)/register/page.tsx +++ b/frontend/app/(registration)/register/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { AddStudentApplicationForEventDocument, AddStudentApplicationForEventMutation, @@ -38,12 +38,10 @@ import { FormMessage, } from "@/components/ui/form"; import { toast } from "sonner"; - -type Props = { - searchParams: { - e: string; - }; -}; +import { CardSkeleton } from "@/components/card-skeleton"; +import {Dialog} from "@/components/ui/dialog"; +import {useUmbrella, useUser} from "@/components/providers"; +import {SignInDialog} from "@/components/sign-in-dialog"; const SingleChoiceFormSchema = (required: boolean) => z.object({ @@ -63,7 +61,13 @@ const MultipleChoiceFormSchema = (required: boolean) => : z.array(z.number()).optional(), }); -const Home = ({ searchParams }: Props) => { +export default function Registration() { + const searchParams = useSearchParams() + const router = useRouter(); + + const { user } = useUser(); + const { setUmbrellaID } = useUmbrella() + const [regForm, setForm] = useState( null ); @@ -71,7 +75,6 @@ const Home = ({ searchParams }: Props) => { const [sliderValue, setSliderValue] = useState(0); const [index, setIndex] = useState(0); const [loading, setLoading] = useState(true); - const router = useRouter(); const [responses, setResponses] = useState([]); useEffect(() => { @@ -80,11 +83,12 @@ const Home = ({ searchParams }: Props) => { } }, [responses]); - const eventID = searchParams.e; + const eventID = parseInt(searchParams.get("e") ?? "0") useEffect(() => { + setUmbrellaID(eventID) const fetchData = async () => { const vars: RegistrationFormQueryVariables = { - eventID: parseInt(eventID), + eventID: eventID }; const data = await client.request( @@ -139,8 +143,7 @@ const Home = ({ searchParams }: Props) => { } const application: NewUserToEventApplication = { - // TODO - userMail: "tutor1@example.de", + userMail: user?.mail ?? "", eventID: +eventID, answers: responses, }; @@ -158,7 +161,7 @@ const Home = ({ searchParams }: Props) => { handleQuit(); } catch (err) { toast("Ein Fehler ist aufgetreten"); - console.error(err) + console.error(err); } }; @@ -203,114 +206,149 @@ const Home = ({ searchParams }: Props) => {
); - if (loading) { - return
Loading...
; - } - return ( -
-
-
-

{regForm?.title}

-

- {regForm?.description} -

-
- - - - {regForm?.questions[index].title} - - {regForm?.questions[index].type === QuestionType.MultipleChoice && ( -
- - -
- ( - - {regForm?.questions[index].answers.map((answer) => ( - ( - + <> + + + + {loading ? ( + + ) : ( +
+
+

{regForm?.title}

+

+ {regForm?.description} +

+
+ + + + {regForm?.questions[index].title} + + {regForm?.questions[index].type === QuestionType.MultipleChoice && ( + + + +
+ ( + + {regForm?.questions[index].answers.map((answer) => ( + ( + +
+ + { + return checked + ? field.onChange([ + ...(field.value || []), + answer.ID, + ]) + : field.onChange( + field.value?.filter( + (value) => + value !== answer.ID + ) + ); + }} + /> + + +
+
+ )} + /> + ))} + +
+ )} + /> +
+
+ + + + + + )} + + {regForm?.questions[index].type === QuestionType.SingleChoice && ( +
+ + +
+ ( + + + field.onChange(parseInt(value, 10)) + } + > + {regForm.questions[index].answers.map( + (answer) => (
- - { - return checked - ? field.onChange([ - ...(field.value || []), - answer.ID, - ]) - : field.onChange( - field.value?.filter( - (value) => value !== answer.ID - ) - ); - }} - /> - - + +
-
+ ) )} - /> - ))} - - - )} - /> -
-
- - - -
- - )} + + + + )} + /> +
+ + + + + + + )} - {regForm?.questions[index].type === QuestionType.SingleChoice && ( -
- + {regForm?.questions[index].type === QuestionType.Scale && ( + -
- ( - - - field.onChange(parseInt(value, 10)) - } - > - {regForm.questions[index].answers.map((answer) => ( -
- - -
- ))} -
- -
- )} +
+
+ + {regForm.questions[index].answers[1].title} + + + {regForm.questions[index].answers[0].title} + +
+ setSliderValue(value[0])} + min={regForm.questions[index].answers[1].points} + max={regForm.questions[index].answers[0].points} + step={1} />
@@ -318,39 +356,10 @@ const Home = ({ searchParams }: Props) => { - - )} - - {regForm?.questions[index].type === QuestionType.Scale && ( -
- -
-
- - {regForm.questions[index].answers[1].title} - - - {regForm.questions[index].answers[0].title} - -
- setSliderValue(value[0])} - min={regForm.questions[index].answers[1].points} - max={regForm.questions[index].answers[0].points} - step={1} - /> -
-
- - - -
- )} - -
-
+ )} +
+
+ )} + ); }; - -export default Home; diff --git a/frontend/components/event-dialog/event-dialog.tsx b/frontend/components/event-dialog/event-dialog.tsx index bccc8c2..d90a31b 100644 --- a/frontend/components/event-dialog/event-dialog.tsx +++ b/frontend/components/event-dialog/event-dialog.tsx @@ -243,7 +243,9 @@ export default function EventDialog() { } capacities={event?.tutorsAssigned?.map(t => t.room.capacity ?? 1) || []} edit={edit} + newAssignments={newAssignments} setNewAssignments={setNewAssignments} + deleteAssignments={deleteAssignments} setDeleteAssignments={setDeleteAssignments} />
diff --git a/frontend/components/event-dialog/room-selection.tsx b/frontend/components/event-dialog/room-selection.tsx index dc9d4be..6072e62 100644 --- a/frontend/components/event-dialog/room-selection.tsx +++ b/frontend/components/event-dialog/room-selection.tsx @@ -1,11 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { - EventTutorRoomPair, - MutationUpdateRoomForTutorialArgs, - Room, -} from "@/lib/gql/generated/graphql"; +import { Room } from "@/lib/gql/generated/graphql"; import { ArrowDownToDot, Check, ChevronsUpDown, Move } from "lucide-react"; import React, { useState } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; @@ -18,33 +14,19 @@ import { CommandList, } from "../ui/command"; import { cn } from "@/lib/utils"; -import { useUmbrella } from "../providers"; interface RoomSelectionProps { - i?: number; - tutorial: EventTutorRoomPair | null; + selectedRoom: Room | undefined; + onSelectedRoomChange: (room: Room | undefined) => void; groupedRooms: { [key: string]: Room[] }; - setAvailableRooms: React.Dispatch>; - setCapacities?: React.Dispatch>; - updateRooms: MutationUpdateRoomForTutorialArgs[]; - setUpdateRooms: React.Dispatch< - React.SetStateAction - >; } export function RoomSelection({ - i, - tutorial, + selectedRoom, + onSelectedRoomChange, groupedRooms, - setAvailableRooms, - setUpdateRooms, - updateRooms, - setCapacities, }: RoomSelectionProps) { - const { closeupID } = useUmbrella(); - const [open, setOpen] = useState(false); - const [selectedRoom, setSelectedRoom] = useState(tutorial?.room); const rooms = structuredClone(groupedRooms); if (selectedRoom) { @@ -101,72 +83,7 @@ export function RoomSelection({ room.building.number } onSelect={() => { - const u = updateRooms.find( - (r) => - r.oldBuildingID === tutorial?.room.building.ID && - r.oldRoomNumber === tutorial.room.number - ); - if (room !== selectedRoom) { - if (setCapacities && i != undefined) { - setCapacities((prev) => { - prev[i] = room.capacity ?? 1; - return prev; - }); - } - setAvailableRooms((prev) => { - const newRooms = prev.filter( - (r) => - !( - r.number === room.number && - r.building.ID === room.building.ID - ) - ); - if (selectedRoom) { - newRooms.push(selectedRoom); - } - return newRooms; - }); - setSelectedRoom(room); - if ( - room.number !== tutorial?.room.number && - room.building.ID !== tutorial?.room.building.ID - ) { - if (u) { - u.newRoomNumber = room.number; - u.newBuildingID = room.building.ID; - } else { - setUpdateRooms((prev) => [ - ...prev, - { - eventID: closeupID ?? 0, - oldBuildingID: - tutorial?.room.building.ID ?? 0, - oldRoomNumber: tutorial?.room.number ?? "", - newBuildingID: room.building.ID, - newRoomNumber: room.number, - }, - ]); - } - } else { - setUpdateRooms((prev) => - prev.filter((r) => r !== u) - ); - } - } else { - if (setCapacities && i !== undefined) { - setCapacities((prev) => { - prev[i] = 0; - return prev; - }); - } - if (selectedRoom) { - setAvailableRooms((prev) => [ - ...prev, - selectedRoom, - ]); - } - setSelectedRoom(undefined); - } + onSelectedRoomChange(room); setOpen(false); }} > diff --git a/frontend/components/event-dialog/tutor-selection.tsx b/frontend/components/event-dialog/tutor-selection.tsx index 10a8743..d577a49 100644 --- a/frontend/components/event-dialog/tutor-selection.tsx +++ b/frontend/components/event-dialog/tutor-selection.tsx @@ -1,16 +1,9 @@ "use client"; import { Button } from "@/components/ui/button"; -import { - EventToUserAssignment, - EventTutorRoomPair, - User, -} from "@/lib/gql/generated/graphql"; -import { - ChevronsUpDown, -} from "lucide-react"; -import { useUmbrella } from "../providers"; -import React, { useState } from "react"; +import { User } from "@/lib/gql/generated/graphql"; +import { ChevronsUpDown } from "lucide-react"; +import React, { useEffect, useState } from "react"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Command, @@ -23,26 +16,22 @@ import { import { Checkbox } from "../ui/checkbox"; interface TutorSelectionProps { - tutorial: EventTutorRoomPair | null; + selectedTutors?: User[]; + onSelectedTutorsChange: (tutor: User) => void; availableTutors: User[]; - setDeleteAssignments: React.Dispatch< - React.SetStateAction - >; - setNewAssignments: React.Dispatch< - React.SetStateAction - >; } export function TutorSelection({ - tutorial, + selectedTutors, + onSelectedTutorsChange, availableTutors, - setDeleteAssignments, - setNewAssignments, }: TutorSelectionProps) { - const { closeupID } = useUmbrella(); - const [open, setOpen] = useState(false); - const [selectedTutors, setSelectedTutors] = useState(tutorial?.tutors); + const [selected, setSelected] = useState(selectedTutors ?? []); + + useEffect(() => { + setSelected(selectedTutors ?? []) + }, [selectedTutors]) return ( @@ -53,17 +42,17 @@ export function TutorSelection({ aria-expanded={open} className="w-fit h-fit space-x-2" > - {selectedTutors && selectedTutors.length > 0 ? ( -
- {selectedTutors?.map((t) => ( -

- {t.fn} {t.sn} -

- ))} -
- ) : ( -

Tutor wählen...

- )} + {selected.length > 0 ? ( +
+ {selected.map((t) => ( +

+ {t.fn} {t.sn} +

+ ))} +
+ ) : ( +

Tutor wählen...

+ )} @@ -74,49 +63,22 @@ export function TutorSelection({ Keine Ergebnisse. {availableTutors.map((tutor) => { - const isSelected = selectedTutors?.find( - (t) => t.mail === tutor.mail - ) + const isSelected = selected.find((t) => t.mail === tutor.mail) ? true : false; - const isOriginal = tutorial?.tutors.find( - (t) => t.mail === tutor.mail - ) - ? true - : false; - const assignment: EventToUserAssignment = { - eventID: closeupID ?? 0, - userMail: tutor.mail, - roomNumber: tutorial?.room.number ?? "", - buildingID: tutorial?.room.building.ID ?? 0, - }; return ( { + onSelectedTutorsChange(tutor); + if (isSelected) { - setSelectedTutors((prev) => - prev?.filter((t) => t.mail !== tutor.mail) + setSelected((prev) => + prev.filter((t) => t.mail !== tutor.mail) ); - - if (isOriginal) { - setDeleteAssignments((prev) => [...prev, assignment]); - } else { - setNewAssignments((prev) => - prev.filter((a) => a !== assignment) - ); - } } else { - setSelectedTutors((prev) => [...prev ?? [], tutor]); - - if (isOriginal) { - setDeleteAssignments((prev) => - prev.filter((a) => a !== assignment) - ); - } else { - setNewAssignments((prev) => [...prev, assignment]); - } + setSelected((prev) => [...prev, tutor]); } }} > diff --git a/frontend/components/event-dialog/tutorials-table.tsx b/frontend/components/event-dialog/tutorials-table.tsx index 6084bfe..19eef95 100644 --- a/frontend/components/event-dialog/tutorials-table.tsx +++ b/frontend/components/event-dialog/tutorials-table.tsx @@ -16,6 +16,7 @@ import { DeleteStudentRegistrationForEventDocument, DeleteStudentRegistrationForEventMutation, DeleteStudentRegistrationForEventMutationVariables, + EventRegistration, EventToUserAssignment, EventTutorRoomPair, MutationUpdateRoomForTutorialArgs, @@ -26,38 +27,18 @@ import { TutorialAvailabilitysQueryVariables, User, } from "@/lib/gql/generated/graphql"; -import { - ArrowDownToDot, - Building2, - Check, - ChevronsUpDown, - Loader2, - MoreVertical, - Plus, - Trash2, -} from "lucide-react"; +import { Loader2, MoreVertical, Plus, Trash2 } from "lucide-react"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "../ui/hover-card"; -import MapPreview from "../map-preview"; import { MailLinkWithLabel } from "../links/email"; import { useUmbrella, useUser } from "../providers"; import { client } from "@/lib/graphql"; import React, { useEffect, useState } from "react"; import { Table, TableBody, TableCell, TableRow } from "../ui/table"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "../ui/command"; import { TutorSelection } from "./tutor-selection"; -import { cn } from "@/lib/utils"; import { RoomSelection } from "./room-selection"; import { RoomHoverCard } from "../room-hover-card"; @@ -66,9 +47,11 @@ interface TutorialsTableProps { registrationCounts: number[]; capacities: number[]; edit: boolean; + deleteAssignments: EventToUserAssignment[]; setDeleteAssignments: React.Dispatch< React.SetStateAction >; + newAssignments: EventToUserAssignment[]; setNewAssignments: React.Dispatch< React.SetStateAction >; @@ -79,15 +62,17 @@ export function TutorialsTable({ registrationCounts, capacities, edit, + newAssignments, setNewAssignments, + deleteAssignments, setDeleteAssignments, }: TutorialsTableProps) { const { user, registrations, setRegistrations } = useUser(); const { closeupID } = useUmbrella(); const [loading, setLoading] = useState(false); - const [registration, setRegistration] = useState( - registrations.find((r) => r.event.ID === closeupID) - ); + const [registration, setRegistration] = useState< + EventRegistration | undefined + >(); const [regCounts, setRegCounts] = useState(registrationCounts); const [cap, setCap] = useState(capacities); const [availableTutors, setAvailableTutors] = useState([]); @@ -95,6 +80,15 @@ export function TutorialsTable({ const [updateRooms, setUpdateRooms] = useState< MutationUpdateRoomForTutorialArgs[] >([]); + const [selectedRooms, setSelectedRooms] = useState<(Room | undefined)[]>([]); + const [tuts, setTuts] = useState(tutorials); + const [newTutorialTutors, setNewTutorialTutors] = useState([]); + const [newTutorialRoom, setNewTutorialRoom] = useState(); + + useEffect(() => { + if (!user) return; + setRegistration(registrations.find((r) => r.event.ID === closeupID)); + }, [user]); const groupRoomsByBuildingID = () => { return availableRooms.reduce((acc, room) => { @@ -160,6 +154,8 @@ export function TutorialsTable({ })) ?? [] ); + setSelectedRooms(tutorials.map((t) => t.room)); + setAvailableRooms( eventData.events[0].roomsAvailable ?.map((r) => ({ @@ -207,6 +203,29 @@ export function TutorialsTable({ ); }; + const handleAvailableRoomsChange = ( + newRoom: Room | undefined, + oldRoom: Room | undefined + ) => { + if (newRoom !== oldRoom) { + setAvailableRooms((prev) => { + const newRooms = prev.filter( + (r) => + !( + r.number === newRoom?.number && + r.building.ID === newRoom?.building.ID + ) + ); + if (oldRoom) { + newRooms.push(oldRoom); + } + return newRooms; + }); + } else if (oldRoom) { + setAvailableRooms((prev) => [...prev, oldRoom]); + } + }; + const handleRegistrationChange = async (room: Room, i: number) => { setLoading(true); @@ -256,137 +275,248 @@ export function TutorialsTable({
- {tutorials.map((e, i) => { - const capacity = cap[i]; - const utilization = (regCounts[i] / capacity) * 100; - const isRegisteredEvent = - e.room?.number === registration?.room.number && - e.room?.building.ID === registration?.room.building.ID; + {tuts.length ? ( + <> + {tuts.map((e, i) => { + const capacity = cap[i]; + const utilization = (regCounts[i] / capacity) * 100; + const isRegisteredEvent = + e.room?.number === registration?.room.number && + e.room?.building.ID === registration?.room.building.ID; - return ( - -
- - {edit ? ( - - ) : ( - <> - {e.tutors?.map((t) => ( - - -

- {t.fn + " " + t.sn[0] + "."} -

-
- - - -
- ))} - - )} -
- - {edit ? ( - +
- ) : ( - - )} - - - {regCounts[i]}/{capacity !== 0 ? capacity : "?"} - - - {edit ? ( - - - + + + Optionen + + + Löschen + + + + ) : ( + - - - Optionen - Bearbeiten - - - Löschen - - - - ) : ( - - )} - - - ); - })} + + + ); + })} + + ) : ( + + + Noch keine Tutorien verfügbar. + + + )} {edit && (
{ + const isSelected = newTutorialTutors.find( + (t) => t.mail === tutor.mail + ) + ? true + : false; + + if (isSelected) { + setNewTutorialTutors((prev) => + prev.filter((t) => t.mail !== tutor.mail) + ); + } else { + setNewTutorialTutors((prev) => [...prev, tutor]); + } + }} /> { + const oldRoom = newTutorialRoom; + + handleAvailableRoomsChange(room, oldRoom); + + if (room !== oldRoom) { + if (oldRoom) { + setAvailableRooms((prev) => [...prev, oldRoom]); + } + setNewTutorialRoom(room); + } else { + setNewTutorialRoom(undefined); + } + }} /> - diff --git a/frontend/components/header.tsx b/frontend/components/header.tsx index 65ac816..44a345d 100644 --- a/frontend/components/header.tsx +++ b/frontend/components/header.tsx @@ -1,6 +1,6 @@ "use client"; -import { LogIn, Moon, Sun } from "lucide-react"; +import { LogIn, Moon, SquareCheckBig, Sun } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; @@ -33,7 +33,6 @@ import { useUmbrella, useUser } from "./providers"; import { client } from "@/lib/graphql"; export default function Header() { - const [isClient, setIsClient] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [events, setEvents] = useState( null @@ -41,7 +40,7 @@ export default function Header() { const { setCloseupID } = useUmbrella(); const { setTheme } = useTheme(); - const { user, setUser } = useUser(); + const { user, setUser, registrations } = useUser(); useEffect(() => { const fetchData = async () => { @@ -67,14 +66,6 @@ export default function Header() { return () => document.removeEventListener("keydown", down); }, []); - useEffect(() => { - setIsClient(true); - }, []); - - if (!isClient) { - return null; - } - const groupEventsByUmbrellaId = () => { return events?.reduce((acc, event) => { const umbrellaId = event.umbrella?.ID; @@ -122,6 +113,7 @@ export default function Header() { {groupedEvents ? groupedEvents[uID].map((e) => ( { setSearchOpen(false); @@ -129,6 +121,12 @@ export default function Header() { }} > {e.title} + {user && + registrations.find((r) => r.event.ID === e.ID) + ? true + : false && ( + + )} )) : ""} @@ -175,9 +173,6 @@ export default function Header() {

{user.mail}

- - Dashboard - Einstellungen setUser(null)}> diff --git a/frontend/components/planner.tsx b/frontend/components/planner.tsx index e837f3b..6eabc8b 100644 --- a/frontend/components/planner.tsx +++ b/frontend/components/planner.tsx @@ -16,6 +16,7 @@ import { import { useUmbrella, useUser } from "./providers"; import { SquareCheckBig } from "lucide-react"; import {RoomHoverCard} from "./room-hover-card"; +import {calculateFontColor} from "@/lib/utils/colorUtils"; interface PlannerProps { events: Event[]; @@ -109,8 +110,9 @@ export function Planner({ events }: PlannerProps) { >
  • {registration && ( -
    - +
    +
    )} diff --git a/frontend/components/sign-in-dialog.tsx b/frontend/components/sign-in-dialog.tsx index 7b7d2be..c319a9a 100644 --- a/frontend/components/sign-in-dialog.tsx +++ b/frontend/components/sign-in-dialog.tsx @@ -16,7 +16,6 @@ import { EmailPasswordLoginDocument, EmailPasswordLoginQuery, EmailPasswordLoginQueryVariables, - EventRegistration, RegistrationDocument, RegistrationMutation, RegistrationMutationVariables, diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 047882c..2117450 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -831,7 +831,7 @@ func (r *queryResolver) Events(ctx context.Context, id []int, umbrellaID []int, } if onlyFuture != nil && *onlyFuture == true { - query = query.Where(`"e"."from" >= ?`, time.Now()) + query = query.Where(`"umbrella"."to" >= ?`, time.Now()) } if userMail != nil {