From b92b8d2aefd0dd7165e2a2c3547981beb3dfb321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aaron=20D=C3=B6tsch?= <aaron@fsmpi.rwth-aachen.de> Date: Sat, 17 Aug 2024 01:28:04 +0200 Subject: [PATCH] update --- src/lib/i18n/de.ts | 17 ++++- src/lib/i18n/en.ts | 17 ++++- src/lib/server/mail.ts | 39 +++++++++- src/routes/(non-admin)/upload/+page.svelte | 29 +++----- src/routes/admin/eswe/+page.svelte | 4 +- .../{[id] => [id=number]}/+page.server.ts | 0 .../{[id] => [id=number]}/+page.svelte | 2 +- .../rabatte/{[id] => [id=number]}/+server.ts | 0 .../OpeningHoursInput.svelte | 0 .../admin/rallye/points/+page.server.ts | 4 +- src/routes/admin/rallye/points/+page.svelte | 73 ++++++++++++++++++- src/routes/admin/schedule/+page.svelte | 6 +- .../{[id] => [id=number]}/+page.server.ts | 0 .../{[id] => [id=number]}/+page.svelte | 8 +- .../schedule/{[id] => [id=number]}/+server.ts | 0 .../[timeslot]/+server.ts | 0 src/routes/admin/schedule/fonts/+page.svelte | 2 +- .../{[id] => [id=number]}/+page.server.ts | 0 .../tutor/{[id] => [id=number]}/+page.svelte | 0 .../training/[id=number]/+page.server.ts | 30 ++++++++ 20 files changed, 190 insertions(+), 41 deletions(-) rename src/routes/admin/rabatte/{[id] => [id=number]}/+page.server.ts (100%) rename src/routes/admin/rabatte/{[id] => [id=number]}/+page.svelte (98%) rename src/routes/admin/rabatte/{[id] => [id=number]}/+server.ts (100%) rename src/routes/admin/rabatte/{[id] => [id=number]}/OpeningHoursInput.svelte (100%) rename src/routes/admin/schedule/{[id] => [id=number]}/+page.server.ts (100%) rename src/routes/admin/schedule/{[id] => [id=number]}/+page.svelte (95%) rename src/routes/admin/schedule/{[id] => [id=number]}/+server.ts (100%) rename src/routes/admin/schedule/{[id] => [id=number]}/[timeslot]/+server.ts (100%) rename src/routes/admin/tutor/{[id] => [id=number]}/+page.server.ts (100%) rename src/routes/admin/tutor/{[id] => [id=number]}/+page.svelte (100%) diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index 162e29c..9714251 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -15,19 +15,19 @@ export default { "/admin/eswe": "ESWE-Einstellungen", "/admin/flyer": "Flyer verwalten", "/admin/rabatte": "Rabatte verwalten", - "/admin/rabatte/[id]": "Rabatt bearbeiten: {discount.title}", + "/admin/rabatte/[id=number]": "Rabatt bearbeiten: {discount.title}", "/admin/rallye/points": "Punkte verwalten", "/admin/rallye/station": "Rallyestationen verwalten", "/admin/rallye/station/[id=uuid]": "Rallyestation bearbeiten: {rallyStation.name}", "/admin/rallye/station/new": "Neue Rallyestation erstellen", "/admin/schedule": "Stundenpläne verwalten", - "/admin/schedule/[id]": "Stundenplan bearbeiten: {schedule.studyProgram.name.de}", + "/admin/schedule/[id=number]": "Stundenplan bearbeiten: {schedule.studyProgram.name.de}", "/admin/schedule/fonts": "Schriftarten verwalten", "/admin/setup": "Instanz einrichten", "/admin/studyprogram": "Studiengänge verwalten", "/admin/templates": "E-Mail-Vorlagen verwalten", "/admin/tutor": "Tutor:innen verwalten", - "/admin/tutor/[id]": "Tutor:in bearbeiten: {tutor.firstname} {tutor.lastname}", + "/admin/tutor/[id=number]": "Tutor:in bearbeiten: {tutor.firstname} {tutor.lastname}", "/admin/tutor/mail": "E-Mail senden", "/admin/tutor/training": "Tutschulungen verwalten", "/admin/tutor/training/[id=number]": "Tutschulung bearbeiten: {training.date}", @@ -193,6 +193,17 @@ export default { Flyer: { Notice: "Einige Flyer haben mehr als eine Seite. Wenn du auf das Bild klickst, kommst du zum vollständigen PDF.", }, + ShareMoments: { // Text auf /upload + Headline: "Momente teilen", + Description: "Du hast Fotos von der Ersti-Woche gemacht und möchtest sie mit uns teilen? Dann bist du hier richtig! Lade deine Dateien hoch und wir sorgen dafür, dass sie an die richtige Stelle kommen.", + Disclaimer: "Mit dem Upload stimmst Du zu, dass die Bilder von der Fachschaft für Öffentlichkeitsarbeit verwendet und z.B. auf Instagram oder anderen Social Media-Plattformen veröffentlicht werden dürfen.", + Disclaimer2: "Bitte lade nur Bilder hoch, die Du selbst gemacht hast und für deren Veröffentlichung Du und alle erkennbaren Personen auf dem Bild einverstanden sind.", + LookingForwards: "Wir freuen uns auf deine Impressionen!", + Compliance: "Ich bestätige, dass ich die Rechte an diesem Bild oder diesen Bildern besitze und dass alle erkennbaren Personen der Veröffentlichung zugestimmt haben. Ich erlaube der Fachschaft I/1, das Bild für Öffentlichkeitsarbeit zu verwenden und auf Social Media zu veröffentlichen.", + Upload: "Hochladen", + UploadSuccess: "Deine Dateien wurden erfolgreich hochgeladen. Vielen Dank!", + UploadError: "Beim Hochladen deiner Dateien ist ein Fehler aufgetreten", + }, Legal: { Headline: "Impressum", Address: "Adresse", diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 9a8f48c..48d0b60 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -17,19 +17,19 @@ export default { "/admin/eswe": "ESWE Settings", "/admin/flyer": "Manage Flyers", "/admin/rabatte": "Manage Discounts", - "/admin/rabatte/[id]": "Edit Discount: {discount.title}", + "/admin/rabatte/[id=number]": "Edit Discount: {discount.title}", "/admin/rallye/points": "Manage Rally Points", "/admin/rallye/station": "Manage Rally Stations", "/admin/rallye/station/[id=uuid]": "Edit rally station: {rallyStation.name}", "/admin/rallye/station/new": "Create new rally station", "/admin/schedule": "Manage Schedules", - "/admin/schedule/[id]": "Edit Schedule: {schedule.studyProgram.name.en}", + "/admin/schedule/[id=number]": "Edit Schedule: {schedule.studyProgram.name.en}", "/admin/schedule/fonts": "Manage Fonts", "/admin/setup": "Setup Instance", "/admin/studyprogram": "Manage Study Programs", "/admin/templates": "Manage Mail Templates", "/admin/tutor": "Manage Tutors", - "/admin/tutor/[id]": "Edit Tutor: {tutor.firstname} {tutor.lastname}", + "/admin/tutor/[id=number]": "Edit Tutor: {tutor.firstname} {tutor.lastname}", "/admin/tutor/mail": "Send Email", "/admin/tutor/training": "Manage Tutor Trainings", "/admin/tutor/training/[id=number]": "Edit tutor training: {training.date}", @@ -195,6 +195,17 @@ export default { Flyer: { Notice: "Some flyers have more than one page. You can get to the full PDF by clicking on the image.", }, + ShareMoments: { // Text auf /upload + Headline: "Share your moments", + Description: "You have taken some great pictures during the freshers' week and want to share them with us? Then you are exactly right here! Upload your pictures here and we will share them on our social media channels.", + Disclaimer: "By uploading, you agree that the images may be used by the student council for public relations and published on social media platforms.", + Disclaimer2: "Please only upload pictures that you have taken yourself and for which you and all recognizable persons in the picture agree to publication.", + LookingForwards: "We are looking forward to your impressions!", + Compliance: "I confirm that I own the rights to this image or these images and that all recognizable persons have agreed to the publication. I allow the student council I/1 to use the image for public relations and to publish it on social media.", + Upload: "Upload", + UploadSuccess: "Your files have been uploaded successfully. Thank you!", + UploadError: "An error occurred while uploading your files", + }, Legal: { // Text auf /impressum Headline: "Legal Notice", Address: "Address", diff --git a/src/lib/server/mail.ts b/src/lib/server/mail.ts index 6c6ceb8..d0cab1d 100644 --- a/src/lib/server/mail.ts +++ b/src/lib/server/mail.ts @@ -6,6 +6,7 @@ import { compileTemplate, parseTemplate, renderTemplate, type MailTemplate } fro import { Config } from "./database/entities/Config.entity"; import ical from "ical-generator"; import type SMTPTransport from "nodemailer/lib/smtp-transport"; +import type { TutorTraining } from "./database/entities/TutorTraining.entity"; const transporter = nodemailer.createTransport({ host: env.MAIL_HOST, @@ -52,7 +53,7 @@ export async function sendTrainingMails(){ description: "Verpflichtende Schulung für alle Ersti-Tuts / Mandatory training for all fresher tutors", location: tutors[0].training!.location, url: "https://esa.fsmpi.rwth-aachen.de/", - } + }, ], }).toString(), }, @@ -65,6 +66,39 @@ export async function sendTrainingMails(){ } } +export async function sendTrainingMail(tutor: Tutor): Promise<SendMailResult|void> { + if(!tutor.training) return; + const { trainingsStart, trainingsEnd, mailTemplates: { trainingInformation: template } } = Config.get(); + const [startHour, startMinute] = trainingsStart.split(":").map(t => parseInt(t)); + const [endHour, endMinute] = trainingsEnd.split(":").map(t => parseInt(t)); + const start = new Date(tutor.training.date); + start.setHours(startHour, startMinute, 0, 0); + const end = new Date(start); + end.setHours(endHour, endMinute, 0, 0); + const result = await sendMail({ + template, + tutors: [tutor], + from: env.MAIL_FROM, + icalEvent: { + filename: "training.ics", + method: "request", + content: ical({ + events: [ + { + start, + end, + summary: "Tutschulung / Tutor training", + description: "Verpflichtende Schulung für alle Ersti-Tuts / Mandatory training for all fresher tutors", + location: tutor.training.location, + url: "https://esa.fsmpi.rwth-aachen.de/", + }, + ], + }).toString(), + }, + }); + if(result.successes.length > 0) await Tutor.update({id: tutor.id, sentTrainingMail: true}); +} + export async function sendTutorRegisteredMail(tutor: Tutor){ const { mailTemplates: { tutorRegistered: template } } = Config.get(); return sendMail({ @@ -77,13 +111,14 @@ export async function sendTutorRegisteredMail(tutor: Tutor){ type SuccessType = {status: "success", response: SMTPTransport.SentMessageInfo, tutorId: number}; type RejectedType = {status: "rejected", response: SMTPTransport.SentMessageInfo, tutorId: number}; type ErrorType = {status: "error", error: Error, tutorId: number}; +export type SendMailResult = {successes: SuccessType[], rejected: RejectedType[], errors: ErrorType[]}; export function sendMail({ template, tutors, attachments = [], from, icalEvent }: { template: MailTemplate, tutors: Tutor[], attachments?: Mail.Attachment[], from?: string | Mail.Address, icalEvent?: Mail.IcalAttachment, -}): Promise<{successes: SuccessType[], rejected: RejectedType[], errors: ErrorType[]}> { +}): Promise<SendMailResult> { const replyTo = template.replyTo === "-" ? undefined : template.replyTo; const textParts = parseTemplate(template.subject); const subjectParts = parseTemplate(template.subject); diff --git a/src/routes/(non-admin)/upload/+page.svelte b/src/routes/(non-admin)/upload/+page.svelte index 5687b55..fb04694 100644 --- a/src/routes/(non-admin)/upload/+page.svelte +++ b/src/routes/(non-admin)/upload/+page.svelte @@ -1,35 +1,28 @@ <script lang="ts"> import { enhance } from '$app/forms'; - import { addMessage } from '$lib/messages'; + import { addMessage } from '$lib/messages'; import { Heading, P, Fileupload, Button, Checkbox } from 'flowbite-svelte'; + import { LL } from "$lib/i18n/i18n"; </script> <!-- TODO i18n --> -<Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">Momente teilen</Heading> +<Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">{$LL.ShareMoments.Headline()}</Heading> -<P class="mb-3"> - Lade hier Deine Fotos von der Erstiwoche hoch und hilf uns, die besten Momente festzuhalten. - Mit dem Upload stimmst Du zu, dass die Bilder von der Fachschaft für Öffentlichkeitsarbeit verwendet - und z.B. auf Instagram oder anderen Social Media-Plattformen veröffentlicht werden dürfen. -</P> -<P class="mb-3"> - Bitte lade nur Bilder hoch, die Du selbst gemacht hast und für deren Veröffentlichung Du und alle - erkennbaren Personen auf dem Bild einverstanden sind. -</P> -<P class="mb-3"> - Wir freuen uns auf Deine Impressionen! -</P> +<P class="mb-3">{$LL.ShareMoments.Description()}</P> +<P class="mb-3">{$LL.ShareMoments.Disclaimer()}</P> +<P class="mb-3">{$LL.ShareMoments.Disclaimer2()}</P> +<P class="mb-3">{$LL.ShareMoments.LookingForwards()}</P> <form action="?/upload" method="post" enctype="multipart/form-data" use:enhance={()=>({result, update})=>{ if(result.type === "success"){ - addMessage({ type: "success", text: "Deine Bilder wurden erfolgreich hochgeladen. Vielen Dank!" }); + addMessage({ type: "success", text: $LL.ShareMoments.UploadSuccess() }); update(); }else{ - addMessage({ type: "error", text: "Beim Hochladen der Bilder ist ein Fehler aufgetreten. Bitte versuche es erneut." }); + addMessage({ type: "error", text: $LL.ShareMoments.UploadError() }); console.error(result); } }}> <Fileupload name="files" accept="image/*" multiple required class="mb-2" /> - <Checkbox class="mb-2" required>Ich bestätige, dass ich die Rechte an diesem Bild oder diesen Bildern besitze und dass alle erkennbaren Personen der Veröffentlichung zugestimmt haben. Ich erlaube der Fachschaft I/1, das Bild für Öffentlichkeitsarbeit zu verwenden und auf Social Media zu veröffentlichen.</Checkbox> - <Button type="submit">Hochladen</Button> + <Checkbox class="mb-2" required>{$LL.ShareMoments.Compliance()}</Checkbox> + <Button type="submit">{$LL.ShareMoments.Upload()}</Button> </form> diff --git a/src/routes/admin/eswe/+page.svelte b/src/routes/admin/eswe/+page.svelte index b76cfc9..bdd265a 100644 --- a/src/routes/admin/eswe/+page.svelte +++ b/src/routes/admin/eswe/+page.svelte @@ -74,7 +74,9 @@ </TableBodyRow> {/each} <TableBodyRow> - <TableBodyCell class="max-w-min"><Input type="file" name="image" form="new" class="max-w-min" accept={data.extensions.map(ext=>`.${ext}`).join(",")} /></TableBodyCell> + <TableBodyCell class="max-w-min"> + <Input type="file" name="image" form="new" class="w-30" accept={data.extensions.map(ext=>`.${ext}`).join(",")} /> + </TableBodyCell> <TableBodyCell> <Label> Sortierung diff --git a/src/routes/admin/rabatte/[id]/+page.server.ts b/src/routes/admin/rabatte/[id=number]/+page.server.ts similarity index 100% rename from src/routes/admin/rabatte/[id]/+page.server.ts rename to src/routes/admin/rabatte/[id=number]/+page.server.ts diff --git a/src/routes/admin/rabatte/[id]/+page.svelte b/src/routes/admin/rabatte/[id=number]/+page.svelte similarity index 98% rename from src/routes/admin/rabatte/[id]/+page.svelte rename to src/routes/admin/rabatte/[id=number]/+page.svelte index 81070ea..319a46b 100644 --- a/src/routes/admin/rabatte/[id]/+page.svelte +++ b/src/routes/admin/rabatte/[id=number]/+page.svelte @@ -96,7 +96,7 @@ <Breadcrumb class="mb-4"> <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> - <BreadcrumbItem href="/admin/rabatte">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rabatte">Rabatte</BreadcrumbItem> <BreadcrumbItem href="/admin/rallye/{data.discount.id}">{data.discount.title}</BreadcrumbItem> </Breadcrumb> diff --git a/src/routes/admin/rabatte/[id]/+server.ts b/src/routes/admin/rabatte/[id=number]/+server.ts similarity index 100% rename from src/routes/admin/rabatte/[id]/+server.ts rename to src/routes/admin/rabatte/[id=number]/+server.ts diff --git a/src/routes/admin/rabatte/[id]/OpeningHoursInput.svelte b/src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte similarity index 100% rename from src/routes/admin/rabatte/[id]/OpeningHoursInput.svelte rename to src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte diff --git a/src/routes/admin/rallye/points/+page.server.ts b/src/routes/admin/rallye/points/+page.server.ts index b558fc6..e10f142 100644 --- a/src/routes/admin/rallye/points/+page.server.ts +++ b/src/routes/admin/rallye/points/+page.server.ts @@ -8,8 +8,8 @@ export const load = (async () => { const stations = await RallyStation.getAll(); const points = await RallyPoints.getAll(); return { - tutorials: tutorials.map(t=>({id: t.id, name: t.name})), - stations: stations.map(s=>({id: s.id, name: s.name, pointsDescription: s.pointsDescription})), + tutorials: tutorials.map(t=>({id: t.id, name: t.name, rallyQuestionnairePoints: t.rallyQuestionnairePoints})), + stations: stations.map(s=>({id: s.id, name: s.name, pointsDescription: s.pointsDescription, shouldMaximize: s.shouldMaximize})), points: points.map(p=>({station: p.rallyStation.id, tutorial: p.tutorial, points: p.points, bribe: p.bribe})), }; }) satisfies PageServerLoad; diff --git a/src/routes/admin/rallye/points/+page.svelte b/src/routes/admin/rallye/points/+page.svelte index 34cdba3..601b18d 100644 --- a/src/routes/admin/rallye/points/+page.svelte +++ b/src/routes/admin/rallye/points/+page.svelte @@ -5,10 +5,54 @@ let tutorialIds = data.tutorials.map(tutorial => tutorial.id); let stationIds = data.stations.map(station => station.id); - let points = Array.from({ length: stationIds.length }).map(()=>Array.from({ length: tutorialIds.length }).map(()=>({points: 0, bribe: 0}))); + let points = Array.from({ length: stationIds.length }).map(()=>Array.from({ length: tutorialIds.length }).map(()=>null as {points: number, bribe: number}|null)); for(const point of data.points){ points[stationIds.indexOf(point.station)][tutorialIds.indexOf(point.tutorial)] = { points: point.points, bribe: point.bribe }; } + + function normalizeQuestionnaires(){ + const points = data.tutorials.map(tutorial => tutorial.rallyQuestionnairePoints ?? 0); + const max = Math.max(...points); + const maxPoints = 30; + return points.map(p => p / max * maxPoints); + } + + function normalize(){ + return points.map((_, stationIndex) => normalizeStation(stationIndex)); + } + + function normalizeStation(stationIndex: number){ + const normalized = points[stationIndex].map(p => ({points: 0, bribe: p?.bribe ?? 0})); + let station = data.stations[stationIndex]; + let [min, max] = points[stationIndex].reduce((acc, p) => p ? [Math.min(acc[0], p.points), Math.max(acc[1], p.points)] : acc, [Infinity, -Infinity]); + if(!Number.isFinite(min) || !Number.isFinite(max)) return normalized; + let range = max - min; + const maxPoints = 20; + for(let tutorialIndex = 0; tutorialIndex < normalized.length; tutorialIndex++){ + let point = points[stationIndex][tutorialIndex]; + if(!point) continue; + let normalizedPoints = (point.points - min) / range * (maxPoints - 1) + 1; + if(!station.shouldMaximize){ + normalizedPoints = maxPoints - normalizedPoints + 1; + } + point.points = normalizedPoints; + } + return normalized; + } + + function evaluate(){ + const normalized = normalize(); + const transposed = normalized[0].map((_, i) => normalized.map(row => row[i])); + const numberOfStationsToGrade = Math.min(6, stationIds.length); + for(let tutorialIndex = 0; tutorialIndex < transposed.length; tutorialIndex++){ + transposed[tutorialIndex].sort((a, b) => (b.points+b.bribe) - (a.points+a.bribe)).splice(numberOfStationsToGrade); + } + const normalizedQuestionnaires = normalizeQuestionnaires(); + return transposed.map((points, tutorialIndex) => ({ + tutorialId: tutorialIds[tutorialIndex], + points: points.reduce((acc, point) => acc + point.points + point.bribe + normalizedQuestionnaires[tutorialIndex], 0) + })).sort((a, b) => b.points - a.points); + } </script> <Breadcrumb class="mb-4"> @@ -17,6 +61,18 @@ <BreadcrumbItem href="/admin/rallye/points">Punkte</BreadcrumbItem> </Breadcrumb> +{#each data.tutorials as tutorial} +{#each data.stations as station} +<form id="form-{station.id}/{tutorial.id}" method="post" action="?/updateStation"> + <input type="hidden" name="stationId" value={station.id} /> + <input type="hidden" name="tutorialId" value={tutorial.id} /> +</form> +{/each} +<form id="form-{tutorial.id}" method="post" action="?/updateQuestionnaire"> + <input type="hidden" name="tutorialId" value={tutorial.id} /> +</form> +{/each} + <Table> <TableHead> <TableHeadCell></TableHeadCell> @@ -32,15 +88,26 @@ <TableBodyCell> <Label> {station.pointsDescription} - <Input type="number" bind:value={points[stationIndex][tutorialIndex].points} class="w-24" name="points" form="form-{station.id}/{tutorial.id}" /> + <Input type="number" value={points[stationIndex][tutorialIndex]?.points} class="w-24" name="points" form="form-{station.id}/{tutorial.id}" /> </Label> <Label> Bestechung - <Input type="number" bind:value={points[stationIndex][tutorialIndex].bribe} class="w-24" name="bribe" form="form-{station.id}/{tutorial.id}" /> + <Input type="number" value={points[stationIndex][tutorialIndex]?.bribe} class="w-24" name="bribe" form="form-{station.id}/{tutorial.id}" /> </Label> </TableBodyCell> {/each} </TableBodyRow> {/each} + <TableBodyRow> + <TableBodyCell>Laufzettel</TableBodyCell> + {#each data.tutorials as tutorial} + <TableBodyCell> + <Label> + Punkte + <Input type="number" value={tutorial.rallyQuestionnairePoints} class="w-24" name="rallyQuestionnairePoints" form="form-{tutorial.id}" /> + </Label> + </TableBodyCell> + {/each} + </TableBodyRow> </TableBody> </Table> diff --git a/src/routes/admin/schedule/+page.svelte b/src/routes/admin/schedule/+page.svelte index 48c0ac3..0d680d7 100644 --- a/src/routes/admin/schedule/+page.svelte +++ b/src/routes/admin/schedule/+page.svelte @@ -4,7 +4,7 @@ import { enhance } from "$app/forms"; import { Permission } from "$lib/perms"; import { invalidateAll } from "$app/navigation"; - import { locale, locales } from "$lib/i18n/i18n"; + import { locale, locales, LL } from "$lib/i18n/i18n"; import { addMessage } from "$lib/messages.js"; export let data; @@ -265,8 +265,8 @@ <TableBodyRow> <TableBodyCell>{schedule.studyProgram.name[$locale]}</TableBodyCell> <TableBodyCell> - <img src="/stundenplaene/{schedule.id}/{$locale}.dark.svg" class="dark:block hidden h-64" /> - <img src="/stundenplaene/{schedule.id}/{$locale}.light.svg" class="block dark:hidden h-64" /> + <img src="/stundenplaene/{schedule.id}/{$locale}.dark.svg" class="dark:block hidden h-64" alt={$LL.Information.ScheduleAlt({semester: data.semester, studyProgram: schedule.studyProgram.name[$locale]})} /> + <img src="/stundenplaene/{schedule.id}/{$locale}.light.svg" class="block dark:hidden h-64" alt={$LL.Information.ScheduleAlt({semester: data.semester, studyProgram: schedule.studyProgram.name[$locale]})} /> </TableBodyCell> {#if data.user.permissions.has(Permission.UPDATE_SCHEDULES)} <TableBodyCell class="max-w-min"> diff --git a/src/routes/admin/schedule/[id]/+page.server.ts b/src/routes/admin/schedule/[id=number]/+page.server.ts similarity index 100% rename from src/routes/admin/schedule/[id]/+page.server.ts rename to src/routes/admin/schedule/[id=number]/+page.server.ts diff --git a/src/routes/admin/schedule/[id]/+page.svelte b/src/routes/admin/schedule/[id=number]/+page.svelte similarity index 95% rename from src/routes/admin/schedule/[id]/+page.svelte rename to src/routes/admin/schedule/[id=number]/+page.svelte index b61f66b..b164378 100644 --- a/src/routes/admin/schedule/[id]/+page.svelte +++ b/src/routes/admin/schedule/[id=number]/+page.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import { Breadcrumb, BreadcrumbItem, Button, Heading, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from "flowbite-svelte"; import { enhance } from "$app/forms"; - import { locale, locales } from "$lib/i18n/i18n"; + import { locale, locales, LL } from "$lib/i18n/i18n"; import type { Timeslot } from "$lib/server/database/entities/Schedule.entity.js"; import { addMessage } from "$lib/messages.js"; @@ -28,7 +28,7 @@ <Breadcrumb class="mb-4"> <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> - <BreadcrumbItem href="/admin/schedule">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule">Stundenpläne</BreadcrumbItem> <BreadcrumbItem href="/admin/schedule/{data.schedule.id}">{data.schedule.studyProgram.name[$locale]}</BreadcrumbItem> </Breadcrumb> @@ -38,8 +38,8 @@ <Heading tag="h2" customSize="text-2xl font-bold" class="mb-2">Vorschau</Heading> <div> - <img src="/stundenplaene/{schedule.id}/{$locale}.dark.svg" class="hidden dark:block" /> - <img src="/stundenplaene/{schedule.id}/{$locale}.light.svg" class="block dark:hidden" /> + <img src="/stundenplaene/{schedule.id}/{$locale}.dark.svg" class="hidden dark:block" alt={$LL.Information.ScheduleAlt({semester: data.semester, studyProgram: schedule.studyProgram.name[$locale]})} /> + <img src="/stundenplaene/{schedule.id}/{$locale}.light.svg" class="block dark:hidden" alt={$LL.Information.ScheduleAlt({semester: data.semester, studyProgram: schedule.studyProgram.name[$locale]})} /> </div> <Button size="sm" on:click={rerender}>Neu rendern</Button> diff --git a/src/routes/admin/schedule/[id]/+server.ts b/src/routes/admin/schedule/[id=number]/+server.ts similarity index 100% rename from src/routes/admin/schedule/[id]/+server.ts rename to src/routes/admin/schedule/[id=number]/+server.ts diff --git a/src/routes/admin/schedule/[id]/[timeslot]/+server.ts b/src/routes/admin/schedule/[id=number]/[timeslot]/+server.ts similarity index 100% rename from src/routes/admin/schedule/[id]/[timeslot]/+server.ts rename to src/routes/admin/schedule/[id=number]/[timeslot]/+server.ts diff --git a/src/routes/admin/schedule/fonts/+page.svelte b/src/routes/admin/schedule/fonts/+page.svelte index 4a80503..051a881 100644 --- a/src/routes/admin/schedule/fonts/+page.svelte +++ b/src/routes/admin/schedule/fonts/+page.svelte @@ -22,7 +22,7 @@ <Breadcrumb class="mb-4"> <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> - <BreadcrumbItem href="/admin/schedule">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule">Stundenpläne</BreadcrumbItem> <BreadcrumbItem href="/admin/schedule/fonts">Schriftarten</BreadcrumbItem> </Breadcrumb> diff --git a/src/routes/admin/tutor/[id]/+page.server.ts b/src/routes/admin/tutor/[id=number]/+page.server.ts similarity index 100% rename from src/routes/admin/tutor/[id]/+page.server.ts rename to src/routes/admin/tutor/[id=number]/+page.server.ts diff --git a/src/routes/admin/tutor/[id]/+page.svelte b/src/routes/admin/tutor/[id=number]/+page.svelte similarity index 100% rename from src/routes/admin/tutor/[id]/+page.svelte rename to src/routes/admin/tutor/[id=number]/+page.svelte diff --git a/src/routes/admin/tutor/training/[id=number]/+page.server.ts b/src/routes/admin/tutor/training/[id=number]/+page.server.ts index ed0a0d8..a5fdb99 100644 --- a/src/routes/admin/tutor/training/[id=number]/+page.server.ts +++ b/src/routes/admin/tutor/training/[id=number]/+page.server.ts @@ -11,3 +11,33 @@ export const load = (async event => { training: {...training, participants: training.participants.length}, }; }) satisfies PageServerLoad; + +export const actions = { + update: async event => { + const id = Number(event.params.id); + if(!Number.isInteger(id) || id < 1) error(404, 'Not Found'); + const training = await TutorTraining.getById(id); + if(!training) error(404, 'Not Found'); + const data = await event.request.formData(); + const dateStr = data.get("date"); + const location = data.get("location"); + const maxParticipantsStr = data.get("maxParticipants"); + const notes = data.get("notes"); + const internal = data.get("internal") === "on"; + if(typeof dateStr !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) error(400, 'Invalid date'); + const dateNum = Date.parse(dateStr); + if(isNaN(dateNum)) error(400, 'Invalid date'); + if(typeof location !== "string" || location.length < 1) error(400, 'Invalid location'); + if(typeof maxParticipantsStr !== "string" || !/^\d+$/.test(maxParticipantsStr)) error(400, 'Invalid maxParticipants'); + const maxParticipants = Number(maxParticipantsStr); + if(!Number.isInteger(maxParticipants) || maxParticipants < 1) error(400, 'Invalid maxParticipants'); + if(typeof notes !== "string") error(400, 'Invalid notes'); + await TutorTraining.update(id, { + date: dateStr, + location, + maxParticipants, + notes, + internal, + }); + }, +}; -- GitLab