From 5e04854d6cd60404e03ef23331793e7d814b1e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Skjellevik?= <42360069+askjellevik@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:07:34 +0200 Subject: [PATCH 1/3] =?UTF-8?q?expand=20m=C3=B8tte=20ikke=20list=20to=20sh?= =?UTF-8?q?ow=20all=20absent=20members=20with=20reason=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/confirm-event-attendance.tsx | 127 ++++++++++++++---- .../controller/get-all-by-event.ts | 40 +++++- 2 files changed, 142 insertions(+), 25 deletions(-) diff --git a/src/app/(main)/lag/[id]/_components/confirm-event-attendance.tsx b/src/app/(main)/lag/[id]/_components/confirm-event-attendance.tsx index da6f8c6..b8909da 100644 --- a/src/app/(main)/lag/[id]/_components/confirm-event-attendance.tsx +++ b/src/app/(main)/lag/[id]/_components/confirm-event-attendance.tsx @@ -1,7 +1,7 @@ "use client"; import type { RegistrationType } from "@prisma/client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; import { Button } from "~/components/ui/button"; @@ -24,14 +24,61 @@ import { api } from "~/trpc/react"; type UserPreview = { id: string; name: string; image: string | null }; +type AbsenceReason = "overridden" | "not_attending" | "no_response"; + type RegistrationRow = { id: string; type: RegistrationType; confirmedAbsent: boolean; attendedWithoutRsvp: boolean; + absenceReason: AbsenceReason | null; + comment: string | null; user: UserPreview; }; +const absenceLabel: Record = { + overridden: "Møtte ikke likevel", + not_attending: "Meldte fravær", + no_response: "Svarte ikke", +}; + +function ExpandableComment({ + id, + comment, + expanded, + onToggle, +}: { + id: string; + comment: string; + expanded: boolean; + onToggle: () => void; +}) { + const ref = useRef(null); + const [isClamped, setIsClamped] = useState(false); + + useEffect(() => { + const el = ref.current; + if (el) setIsClamped(el.scrollHeight > el.clientHeight); + }, [id, comment]); + + return ( +
+

+ {comment} +

+ {(isClamped || expanded) && ( + + )} +
+ ); +} + interface ConfirmEventAttendanceProps { eventId: string; eventName: string; @@ -52,6 +99,9 @@ export default function ConfirmEventAttendance({ const [tab, setTab] = useState("present"); const [selectedWithoutRsvpUserId, setSelectedWithoutRsvpUserId] = useState(""); + const [expandedComments, setExpandedComments] = useState>( + new Set(), + ); const utils = api.useUtils(); const { data: registrations, isLoading } = @@ -94,14 +144,27 @@ export default function ConfirmEventAttendance({ }, }); - const attending = (registrations ?? []).filter( - (r: RegistrationRow) => r.type === "ATTENDING", + const present = (registrations ?? []).filter( + (r: RegistrationRow) => r.absenceReason === null, + ); + const absent = (registrations ?? []).filter( + (r: RegistrationRow) => r.absenceReason !== null, ); - const present = attending.filter((r: RegistrationRow) => !r.confirmedAbsent); - const absent = attending.filter((r: RegistrationRow) => r.confirmedAbsent); const list = tab === "present" ? present : absent; + function toggleComment(id: string) { + setExpandedComments((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + } + return ( @@ -166,7 +230,7 @@ export default function ConfirmEventAttendance({ key={row.id} className="flex flex-col gap-2 rounded-lg border bg-card p-3" > -
+
)} + {row.absenceReason && ( +
+ {absenceLabel[row.absenceReason]} +
+ )} + {tab === "absent" && row.comment && ( + toggleComment(row.id)} + /> + )}
{isAdmin && tab === "present" && ( @@ -203,24 +280,26 @@ export default function ConfirmEventAttendance({ Møtte ikke )} - {isAdmin && tab === "absent" && ( - - )} + {isAdmin && + tab === "absent" && + row.absenceReason === "overridden" && ( + + )} ))} diff --git a/src/server/api/registration/controller/get-all-by-event.ts b/src/server/api/registration/controller/get-all-by-event.ts index fdbbf39..a433c5e 100644 --- a/src/server/api/registration/controller/get-all-by-event.ts +++ b/src/server/api/registration/controller/get-all-by-event.ts @@ -28,7 +28,7 @@ const handler: Controller< "USER", ]); - const [registrations, attendances] = await Promise.all([ + const [registrations, attendances, teamMembers] = await Promise.all([ db.registration.findMany({ where: { eventId: input.eventId }, include: { @@ -42,6 +42,12 @@ const handler: Controller< user: { select: { id: true, name: true, image: true } }, }, }), + db.teamMember.findMany({ + where: { teamId: event.teamId }, + include: { + user: { select: { id: true, name: true, image: true } }, + }, + }), ]); const absentUserIds = new Set( @@ -50,12 +56,24 @@ const handler: Controller< const attendingUserIds = new Set( registrations.filter((r) => r.type === "ATTENDING").map((r) => r.userId), ); + const registeredUserIds = new Set(registrations.map((r) => r.userId)); + const manualAttendanceUserIds = new Set( + attendances.filter((a) => a.source === "MANUAL").map((a) => a.userId), + ); + + const now = new Date(); return [ ...registrations.map((reg) => ({ ...reg, confirmedAbsent: absentUserIds.has(reg.userId), attendedWithoutRsvp: false, + absenceReason: + reg.type === "NOT_ATTENDING" + ? ("not_attending" as const) + : absentUserIds.has(reg.userId) + ? ("overridden" as const) + : null, })), ...attendances .filter((a) => a.source === "MANUAL" && !attendingUserIds.has(a.userId)) @@ -64,6 +82,7 @@ const handler: Controller< type: "ATTENDING" as const, confirmedAbsent: a.status === "ABSENT", attendedWithoutRsvp: true, + absenceReason: null as null, userId: a.userId, eventId: a.eventId, comment: null, @@ -71,6 +90,25 @@ const handler: Controller< createdAt: a.createdAt, updatedAt: a.updatedAt, })), + ...teamMembers + .filter( + (m) => + !registeredUserIds.has(m.userId) && + !manualAttendanceUserIds.has(m.userId), + ) + .map((m) => ({ + id: m.id, + type: "NOT_ATTENDING" as const, + confirmedAbsent: false, + attendedWithoutRsvp: false, + absenceReason: "no_response" as const, + userId: m.userId, + eventId: input.eventId, + comment: null, + user: m.user, + createdAt: now, + updatedAt: now, + })), ].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); }; From 8761524911efcb21fffaf3a9e16dd642ebcd81ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Skjellevik?= <42360069+askjellevik@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:10:17 +0200 Subject: [PATCH 2/3] update footer email address from hs to idkom --- src/components/navigation/footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/navigation/footer.tsx b/src/components/navigation/footer.tsx index de32b39..4fce33c 100644 --- a/src/components/navigation/footer.tsx +++ b/src/components/navigation/footer.tsx @@ -5,7 +5,7 @@ import TihldeLogo from "../logo"; const Footer = () => { const attributes = [ - { id: "email", key: "E-post", value: "hs@tihlde.org" }, + { id: "email", key: "E-post", value: "idkom@tihlde.org" }, { id: "orgNumber", key: "Organisasjonsnummer", value: "989 684 183" }, { id: "location", key: "Lokasjon", value: "c/o IDI NTNU" }, ]; From ea6c3ee364056b568eadbdd08b26d97480f6117f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Skjellevik?= <42360069+askjellevik@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:10:43 +0200 Subject: [PATCH 3/3] fix absent marking for manually added members --- src/server/api/registration/controller/get-all-by-event.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server/api/registration/controller/get-all-by-event.ts b/src/server/api/registration/controller/get-all-by-event.ts index a433c5e..f7d5e92 100644 --- a/src/server/api/registration/controller/get-all-by-event.ts +++ b/src/server/api/registration/controller/get-all-by-event.ts @@ -64,7 +64,9 @@ const handler: Controller< const now = new Date(); return [ - ...registrations.map((reg) => ({ + ...registrations + .filter((reg) => !manualAttendanceUserIds.has(reg.userId)) + .map((reg) => ({ ...reg, confirmedAbsent: absentUserIds.has(reg.userId), attendedWithoutRsvp: false, @@ -82,7 +84,7 @@ const handler: Controller< type: "ATTENDING" as const, confirmedAbsent: a.status === "ABSENT", attendedWithoutRsvp: true, - absenceReason: null as null, + absenceReason: a.status === "ABSENT" ? ("overridden" as const) : (null as null), userId: a.userId, eventId: a.eventId, comment: null,