diff --git a/src/lib/components/AdminLayout.svelte b/src/lib/components/AdminLayout.svelte index c1c8ef8f95b0237a7c17a30d598a183387d931e3..5810cece1defe5b7fbf3166a07e06a2cdcf2659f 100644 --- a/src/lib/components/AdminLayout.svelte +++ b/src/lib/components/AdminLayout.svelte @@ -33,6 +33,9 @@ <Dropdown class="w-44 z-20"> <DropdownItem href="/admin/rallye/supervisor">Stationsbetreuer</DropdownItem> <DropdownItem href="/admin/rallye/mail">Mail senden</DropdownItem> + <DropdownDivider /> + <DropdownItem href="/admin/rallye/station">Stationen</DropdownItem> + <DropdownItem href="/admin/rallye/points">Punkte</DropdownItem> </Dropdown> <NavLi href="/admin/rabatte">Rabatte</NavLi> <NavLi href="/admin/flyer">Flyer</NavLi> diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index d7faa25b326169acdd5b12eeb5c21d80f613b697..162e29c28a2029f8e3bacda4065ddf98bfa1f969 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -7,13 +7,19 @@ export default { "/(non-admin)/impressum": "Impressum", "/(non-admin)/information": "Informationen", "/(non-admin)/rabatte": "Rabatte", + "/(non-admin)/rallye/[id=uuid]": "{rallyStation.name} bewerten", "/(non-admin)/tutor": "Tutor:in werden", "/(non-admin)/tutor/register": "Anmeldung als Tutor:in", + "/(non-admin)/upload": "Momente teilen", "/admin": "Einstellungen", "/admin/eswe": "ESWE-Einstellungen", "/admin/flyer": "Flyer verwalten", "/admin/rabatte": "Rabatte verwalten", "/admin/rabatte/[id]": "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/fonts": "Schriftarten verwalten", @@ -23,6 +29,9 @@ export default { "/admin/tutor": "Tutor:innen verwalten", "/admin/tutor/[id]": "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}", + "/admin/tutor/training/new": "Neue Tutschulung erstellen", "/admin/user": "Berechtigungen verwalten", }, Navbar: { diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 75a38f9990e6ab5eece3a040a859f00ed5b636b1..9a8f48c115e489833eee67b0ca774436129abd16 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -9,13 +9,19 @@ export default { "/(non-admin)/impressum": "Legal Notice", "/(non-admin)/information": "Information", "/(non-admin)/rabatte": "Discounts", + "/(non-admin)/rallye/[id=uuid]": "{station.name} grading", "/(non-admin)/tutor": "Become a Tutor", "/(non-admin)/tutor/register": "Tutor Registration", + "/(non-admin)/upload": "Share your moments", "/admin": "Settings", "/admin/eswe": "ESWE Settings", "/admin/flyer": "Manage Flyers", "/admin/rabatte": "Manage Discounts", "/admin/rabatte/[id]": "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/fonts": "Manage Fonts", @@ -25,6 +31,9 @@ export default { "/admin/tutor": "Manage Tutors", "/admin/tutor/[id]": "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}", + "/admin/tutor/training/new": "Create new tutor training", "/admin/user": "Manage Permissions", }, Navbar: { diff --git a/src/lib/perms.ts b/src/lib/perms.ts index 95f2fe8909640843e6af9bc3404e25c3503bd9e9..6bfda68d4327478707a11b572ada44492f4686be 100644 --- a/src/lib/perms.ts +++ b/src/lib/perms.ts @@ -12,6 +12,8 @@ export enum Permission { EDIT_TRAININGS = 1<<10, UPDATE_CONFIG = 1<<11, UPDATE_MAIL_TEMPLATES = 1<<12, + UPDATE_RALLY_STATIONS = 1<<13, + UPDATE_RALLY_POINTS = 1<<14, } export const PermissionDescription: {[permission in Permission]: string} = { [Permission.ADMIN]: "Berechtigungen aller Nutzer setzen", @@ -27,6 +29,8 @@ export const PermissionDescription: {[permission in Permission]: string} = { [Permission.EDIT_TRAININGS]: "Erstellen, Bearbeiten und Löschen von Schulungen", [Permission.UPDATE_CONFIG]: "Grundeinstellungen ändern", [Permission.UPDATE_MAIL_TEMPLATES]: "E-Mail-Vorlagen bearbeiten", + [Permission.UPDATE_RALLY_STATIONS]: "Rallye-Stationen erstellen/löschen/bearbeiten", + [Permission.UPDATE_RALLY_POINTS]: "Bewertungen für Rallye-Stationen bearbeiten und auswerten", }; export class PermissionSet { diff --git a/src/lib/server/database/entities/RallyStation.entity.ts b/src/lib/server/database/entities/RallyStation.entity.ts index 13d473bfc801d20d4d676f33e946fa144915e757..2a58a546111641e38824ec6d64d24e0b78149573 100644 --- a/src/lib/server/database/entities/RallyStation.entity.ts +++ b/src/lib/server/database/entities/RallyStation.entity.ts @@ -4,6 +4,8 @@ export class RallyStation { id!: string; name!: string; description!: string; + pointsDescription!: string; + shouldMaximize!: boolean; static parse(rallyStation: any): RallyStation|undefined { if(!rallyStation) return undefined; diff --git a/src/lib/server/database/entities/Tutor.entity.ts b/src/lib/server/database/entities/Tutor.entity.ts index 6723be0edaae326cdfd1c6a41a8f1246babdb5ad..03f6a67966f8677250f71cd6dbd202306573bcc4 100644 --- a/src/lib/server/database/entities/Tutor.entity.ts +++ b/src/lib/server/database/entities/Tutor.entity.ts @@ -28,6 +28,7 @@ export class Tutor { mentor!: boolean; tutorial?: Tutorial; notes!: string; + number?: string; former?: FormerTutor; static parse(tutor: any){ diff --git a/src/lib/server/database/migrations/0_init.sql b/src/lib/server/database/migrations/0_init.sql index fbaee01f3d4fc16b58c112144f4a16e614ba9000..75fbd5f64ad86cb79dc7b9813e0b76b4188b0863 100644 --- a/src/lib/server/database/migrations/0_init.sql +++ b/src/lib/server/database/migrations/0_init.sql @@ -73,6 +73,7 @@ CREATE TABLE IF NOT EXISTS tutors ( "co_tutor_wish" TEXT NOT NULL DEFAULT '', "mentor" BOOLEAN NOT NULL DEFAULT FALSE, "tutorial" INT DEFAULT NULL REFERENCES tutorials(id), + "number" TEXT, "notes" TEXT NOT NULL DEFAULT '' ); @@ -105,7 +106,9 @@ CREATE TABLE IF NOT EXISTS rally_station_supervisors ( CREATE TABLE IF NOT EXISTS rally_stations ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "name" TEXT NOT NULL, - "description" TEXT NOT NULL + "description" TEXT NOT NULL, + "points_description" TEXT NOT NULL, + "should_maximize" BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS rally_station_assignments ( diff --git a/src/params/number.ts b/src/params/number.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fa3e6839b7f6d14f97eb629437be3b4867a87c8 --- /dev/null +++ b/src/params/number.ts @@ -0,0 +1,2 @@ +const regex = /^[1-9]\d*$/; +export const match = (value: string) => regex.test(value); diff --git a/src/params/uuid.ts b/src/params/uuid.ts index 172d3813887427246b478c8a029ce9e53fcdb5c6..1a7db83f8033116f29b4de8778381934c614bcbd 100644 --- a/src/params/uuid.ts +++ b/src/params/uuid.ts @@ -1,3 +1,2 @@ -export function match(param: string): boolean { - return /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/.test(param); -} +const regex = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/; +export const match = (value: string) => regex.test(value); diff --git a/src/routes/(non-admin)/rallye/[id=uuid]/+page.server.ts b/src/routes/(non-admin)/rallye/[id=uuid]/+page.server.ts index 60d3cacf168e11eda3a0c71d1c0eca91bad748cd..4abe68528a02d32ae073bf5f6b3ac69b2f091d7e 100644 --- a/src/routes/(non-admin)/rallye/[id=uuid]/+page.server.ts +++ b/src/routes/(non-admin)/rallye/[id=uuid]/+page.server.ts @@ -31,7 +31,7 @@ export const actions = { if(!tutorial) error(404, "Tutorial nicht gefunden"); if(!pointsStr || typeof pointsStr !== "string") error(400, "Ungültige Punkte"); const points = Number(pointsStr); - if(!Number.isInteger(points) || points < 0 || points > 10) error(400, "Ungültige Punkte"); + if(!Number.isInteger(points) || points < 0) error(400, "Ungültige Punkte"); if(!bribeStr || typeof bribeStr !== "string") error(400, "Ungültige Bestechung"); const bribe = Number(bribeStr); if(!Number.isInteger(bribe) || bribe < 0 || bribe > 10) error(400, "Ungültige Bestechung"); diff --git a/src/routes/(non-admin)/rallye/[id=uuid]/+page.svelte b/src/routes/(non-admin)/rallye/[id=uuid]/+page.svelte index 66222062ce3ef868856e7e9227cc7f8e5e43b605..45c2a231ff085d0272cd6eb1eb3fe7019d0a8fdd 100644 --- a/src/routes/(non-admin)/rallye/[id=uuid]/+page.svelte +++ b/src/routes/(non-admin)/rallye/[id=uuid]/+page.svelte @@ -14,9 +14,15 @@ }); </script> +<style> + .description :global(p) { + margin-bottom: 0.7rem; + } +</style> + <Heading customSize="text-4xl font-bold" class="mb-6 text-center">{data.rallyStation.name}</Heading> -<P class="mb-4">{@html md.render(data.rallyStation.description)}</P> +<div class="description text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal mb-6">{@html md.render(data.rallyStation.description)}</div> <form method="post" action="?/setPoints" use:enhance={()=>({result, update})=>{ if(result.type === "success"){ @@ -27,8 +33,8 @@ console.error(result); } }}> - <div class="flex gap-2 mb-2"> - <Label class="w-full"> + <div class="flex flex-col gap-2 mb-4 md:flex-row"> + <Label class="flex-grow"> Tutorium <Select name="tutorial" required> {#each data.tutorials as tutorial} @@ -36,14 +42,16 @@ {/each} </Select> </Label> - <Label class="w-28"> - Punkte - <Input name="points" type="number" min="0" max="10" required /> - </Label> - <Label class="w-28"> - Bestechung - <Input name="bribe" type="number" min="0" max="10" required /> - </Label> + <div class="flex flex-col gap-2 xs:flex-row"> + <Label class="w-full md:w-44"> + {data.rallyStation.pointsDescription} + <Input name="points" type="number" min="0" required /> + </Label> + <Label class="w-full md:w-32"> + Bestechung + <Input name="bribe" type="number" min="0" max="10" required /> + </Label> + </div> </div> <Button type="submit">Punkte setzen</Button> </form> diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index dfe626fab078a04cb762ef405f5fe8e43ca977c4..651ad3a7d68c394d6d743ac410e4a64c901f6118 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -3,7 +3,7 @@ import { invalidateAll } from "$app/navigation"; import { addMessage } from "$lib/messages"; import { Permission } from "$lib/perms.js"; - import { Button, Checkbox, Heading, Input, Label, MultiSelect, Select } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Heading, Input, Label, MultiSelect, Select } from "flowbite-svelte"; let { data } = $props(); @@ -11,6 +11,10 @@ let disabled = !canEdit; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> +</Breadcrumb> + <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">Einstellungen</Heading> <Heading tag="h2" customSize="text-3xl font-bold" class="mt-6">Grundeinstellungen</Heading> <form method="post" action="?/updateBaseConfig" use:enhance={()=>({result})=>{ diff --git a/src/routes/admin/eswe/+page.svelte b/src/routes/admin/eswe/+page.svelte index f3187bfba4aab86628377d805f07acd3f274cf11..b76cfc91fcd6c266b0c91d3bcf7c6d4cc4ce0c96 100644 --- a/src/routes/admin/eswe/+page.svelte +++ b/src/routes/admin/eswe/+page.svelte @@ -3,11 +3,16 @@ import { invalidateAll } from "$app/navigation"; import Image from "$lib/components/Image.svelte"; import { addMessage } from "$lib/messages.js"; - import { Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; export let data; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/eswe">ESWE</BreadcrumbItem> +</Breadcrumb> + {#each data.images as image} <form method="post" action="?/updateImage" id={image.identifier} use:enhance={()=>({result})=>{ if(result.type === "success"){ diff --git a/src/routes/admin/flyer/+page.svelte b/src/routes/admin/flyer/+page.svelte index 2a8859e7b1c982f1d2678c7d5b089d0b33e66007..b21856e3916e0b50794a0e5a58f76bed52d084a2 100644 --- a/src/routes/admin/flyer/+page.svelte +++ b/src/routes/admin/flyer/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { Button, Input, Li, List } from 'flowbite-svelte'; + import { Breadcrumb, BreadcrumbItem, Button, Input, Li, List } from 'flowbite-svelte'; import { enhance } from '$app/forms'; import Image from '$lib/components/Image.svelte'; import { addMessage } from '$lib/messages.js'; @@ -13,6 +13,11 @@ } </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/flyer">Flyer</BreadcrumbItem> +</Breadcrumb> + <List list="none"> {#each data.flyer as flyer} <Li class="mb-2"> diff --git a/src/routes/admin/rabatte/+page.svelte b/src/routes/admin/rabatte/+page.svelte index 82a99e1400743669a4ed96d13cb42c960fe6d661..479b9b3ac8030ac2223acb3d65754bec6a6ec8e8 100644 --- a/src/routes/admin/rabatte/+page.svelte +++ b/src/routes/admin/rabatte/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { Button, Input, Label, P, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from 'flowbite-svelte'; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, P, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from 'flowbite-svelte'; import type { PageData } from './$types'; import { locales } from '$lib/i18n/i18n'; import { enhance } from '$app/forms'; @@ -30,6 +30,11 @@ } </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rabatte">Rabatte</BreadcrumbItem> +</Breadcrumb> + <form action="?/create" method="post" id="form-new" use:enhance={()=>({result})=>{ if(result.type === "success"){ goto(`/admin/rabatte/${result.data!.id}`); diff --git a/src/routes/admin/rabatte/[id]/+page.svelte b/src/routes/admin/rabatte/[id]/+page.svelte index 1d5643a28753b214b14479dd26117a50d05a88a3..81070ea9e826602e02a38694b3fd6638b7f28eb0 100644 --- a/src/routes/admin/rabatte/[id]/+page.svelte +++ b/src/routes/admin/rabatte/[id]/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { Button, Input, Label, MultiSelect, Textarea } from 'flowbite-svelte'; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, MultiSelect, Textarea } from 'flowbite-svelte'; import type { PageData } from './$types'; import { locale, locales } from '$lib/i18n/i18n'; import { onDestroy, onMount } from 'svelte'; @@ -94,6 +94,12 @@ } </style> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rabatte">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/{data.discount.id}">{data.discount.title}</BreadcrumbItem> +</Breadcrumb> + <form method="post" action="?/update" use:enhance={()=>({result})=>{ if(result.type === "success"){ addMessage({type: "success", text: "Daten gespeichert"}); diff --git a/src/routes/admin/rallye/points/+page.server.ts b/src/routes/admin/rallye/points/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..b558fc69831ec71d209df7d6b638a6bc1c34ce1d --- /dev/null +++ b/src/routes/admin/rallye/points/+page.server.ts @@ -0,0 +1,15 @@ +import { RallyPoints } from '$lib/server/database/entities/RallyPoints.entity'; +import { RallyStation } from '$lib/server/database/entities/RallyStation.entity'; +import { Tutorial } from '$lib/server/database/entities/Tutorial.entity'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const tutorials = await Tutorial.getAll(); + 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})), + 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 new file mode 100644 index 0000000000000000000000000000000000000000..34cdba3723746fce520148bc0af90e31de728fe1 --- /dev/null +++ b/src/routes/admin/rallye/points/+page.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { Breadcrumb, BreadcrumbItem, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + + let { data } = $props(); + + 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}))); + for(const point of data.points){ + points[stationIds.indexOf(point.station)][tutorialIds.indexOf(point.tutorial)] = { points: point.points, bribe: point.bribe }; + } +</script> + +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/points">Punkte</BreadcrumbItem> +</Breadcrumb> + +<Table> + <TableHead> + <TableHeadCell></TableHeadCell> + {#each data.tutorials as tutorial} + <TableHeadCell>{tutorial.name}</TableHeadCell> + {/each} + </TableHead> + <TableBody> + {#each data.stations as station, stationIndex} + <TableBodyRow> + <TableBodyCell>{station.name}</TableBodyCell> + {#each data.tutorials as tutorial, tutorialIndex} + <TableBodyCell> + <Label> + {station.pointsDescription} + <Input type="number" bind: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}" /> + </Label> + </TableBodyCell> + {/each} + </TableBodyRow> + {/each} + </TableBody> +</Table> diff --git a/src/routes/admin/rallye/station/+page.svelte b/src/routes/admin/rallye/station/+page.svelte index 95cdbf7a798707e9fe7c1de789ffc714ed72864c..9b8b376b193012baae723896327b4fd86ac9b68f 100644 --- a/src/routes/admin/rallye/station/+page.svelte +++ b/src/routes/admin/rallye/station/+page.svelte @@ -1,23 +1,30 @@ <script lang="ts"> import { enhance } from "$app/forms"; import { addMessage } from "$lib/messages"; - import { Button, Input, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from "flowbite-svelte"; + import { Permission } from "$lib/perms.js"; + import { A, Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import MarkdownIt from "markdown-it"; let { data } = $props(); + + const md = new MarkdownIt({ + breaks: true, + linkify: true, + html: true, + typographer: true, + }); + + let canUpdate = data.user.permissions.has(Permission.UPDATE_RALLY_STATIONS); </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/station">Stationen</BreadcrumbItem> +</Breadcrumb> + +{#if canUpdate} {#each data.stations as station} -<form id="form-{station.id}" method="post" action="?/update" use:enhance={()=>({result, update})=>{ - if(result.type === "success"){ - addMessage({type: "success", text: "Station wurde gespeichert"}); - update({ reset: false }); - }else{ - addMessage({type: "error", text: "Fehler beim Speichern der Station"}); - console.error(result); - } -}}> - <input type="hidden" name="id" value={station.id} /> -</form> <form id="delete-form-{station.id}" method="post" action="?/delete" use:enhance={()=>({result, update})=>{ if(result.type === "success"){ addMessage({type: "success", text: "Station wurde gelöscht"}); @@ -30,47 +37,39 @@ <input type="hidden" name="id" value={station.id} /> </form> {/each} -<form id="new-form" method="post" action="?/create" use:enhance={()=>({result, update})=>{ - if(result.type === "success"){ - addMessage({type: "success", text: "Station wurde erstellt"}); - update(); - }else{ - addMessage({type: "error", text: "Fehler beim Erstellen der Station"}); - console.error(result); - } -}}></form> +{/if} <Table> <TableHead> <TableHeadCell>Station</TableHeadCell> <TableHeadCell>Beschreibung</TableHeadCell> + {#if canUpdate} <TableHeadCell></TableHeadCell> + {/if} </TableHead> <TableBody> {#each data.stations as station (station.id)} <TableBodyRow> <TableBodyCell> - <Input name="name" type="text" value={station.name} required form="form-{station.id}" /> + <A href="/rallye/{station.id}">{station.name}</A> </TableBodyCell> <TableBodyCell> - <Textarea name="description" value={station.description} required form="form-{station.id}" /> + <div class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal">{@html md.render(station.description)}</div> </TableBodyCell> + {#if canUpdate} <TableBodyCell> - <Button type="submit" form="form-{station.id}">Speichern</Button> + <Button href="/admin/rallye/station/{station.id}">Bearbeiten</Button> <Button type="submit" form="delete-form-{station.id}" color="red">Löschen</Button> </TableBodyCell> + {/if} </TableBodyRow> {/each} + {#if canUpdate} <TableBodyRow> - <TableBodyCell> - <Input name="name" type="text" required form="new-form" /> - </TableBodyCell> - <TableBodyCell> - <Textarea name="description" required form="new-form" /> - </TableBodyCell> - <TableBodyCell> - <Button type="submit" form="new-form">Erstellen</Button> + <TableBodyCell colSpan="3"> + <Button href="/admin/rallye/station/new" class="w-20 block mx-auto">+</Button> </TableBodyCell> </TableBodyRow> + {/if} </TableBody> </Table> diff --git a/src/routes/admin/rallye/station/[id=uuid]/+page.server.ts b/src/routes/admin/rallye/station/[id=uuid]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c26880d7ce0b32a8326f44cd71bc2cc2d56dd46 --- /dev/null +++ b/src/routes/admin/rallye/station/[id=uuid]/+page.server.ts @@ -0,0 +1,29 @@ +import { RallyStation } from '$lib/server/database/entities/RallyStation.entity'; +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load = (async event => { + const id = event.params.id; + const station = await RallyStation.getById(id); + if(!station) error(404, "Rallye-Station nicht gefunden"); + return { + station: {...station}, + }; +}) satisfies PageServerLoad; + +export const actions = { + update: async event => { + const id = event.params.id; + const station = await RallyStation.getById(id); + if(!station) error(404, "Rallye-Station nicht gefunden"); + const data = await event.request.formData(); + const name = data.get("name"); + const description = data.get("description"); + const pointsDescription = data.get("pointsDescription"); + const shouldMaximize = data.get("shouldMaximize") === "on"; + if (!name || typeof name !== "string") error(400, "Ungültiger Name"); + if (!description || typeof description !== "string") error(400, "Ungültige Beschreibung"); + if (!pointsDescription || typeof pointsDescription !== "string") error(400, "Ungültige Punkte-Beschreibung"); + await RallyStation.update({ id, name, description, pointsDescription, shouldMaximize }); + }, +}; diff --git a/src/routes/admin/rallye/station/[id=uuid]/+page.svelte b/src/routes/admin/rallye/station/[id=uuid]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5a244c3a059186f2e69bb5ce33ae00db83ce8060 --- /dev/null +++ b/src/routes/admin/rallye/station/[id=uuid]/+page.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import { enhance } from "$app/forms"; + import { goto } from "$app/navigation"; + import { addMessage } from "$lib/messages.js"; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, P, Textarea, Toggle } from "flowbite-svelte"; + + let { data } = $props(); +</script> + +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/station">Stationen</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/station/{data.station.id}">{data.station.name}</BreadcrumbItem> +</Breadcrumb> + +<form method="post" action="?/update" use:enhance={()=>({result})=>{ + if(result.type === "success"){ + addMessage({type: "success", text: "Änderungen wurden gespeichert"}); + goto("/admin/rallye/station"); + }else{ + addMessage({type: "error", text: "Fehler beim Speichern der Station"}); + console.error(result); + } +}}> + <Label class="mb-2"> + Name + <Input type="text" name="name" value={data.station.name} required /> + </Label> + <Label class="mb-2"> + Beschreibung (unterstützt Markdown) + <Textarea name="description" value={data.station.description} required /> + </Label> + <div class="flex gap-3 mb-2"> + <Label class="w-full"> + Punktebeschreibung + <Input type="text" name="pointsDescription" value={data.station.pointsDescription} required /> + </Label> + <P class="content-end"> + <Toggle name="shouldMaximize" group={data.station.shouldMaximize ? ["on"] : []} choices={[{value: "on", label: "wenig"}]} classDiv="ms-3">viele</Toggle> + Punkte sind besser + </P> + </div> + <Button type="submit">Station speichern</Button> +</form> diff --git a/src/routes/admin/rallye/station/new/+page.server.ts b/src/routes/admin/rallye/station/new/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e544e2c1217320e1b58c1327304ee9dd918e608 --- /dev/null +++ b/src/routes/admin/rallye/station/new/+page.server.ts @@ -0,0 +1,17 @@ +import { RallyStation } from "$lib/server/database/entities/RallyStation.entity"; +import { error } from "@sveltejs/kit"; + +export const actions = { + create: async event => { + const data = await event.request.formData(); + const name = data.get("name"); + const description = data.get("description"); + const pointsDescription = data.get("pointsDescription"); + const shouldMaximize = data.get("shouldMaximize") === "on"; + if(!name || typeof name !== "string") error(400, "Ungültiger Name"); + if(!description || typeof description !== "string") error(400, "Ungültige Beschreibung"); + if(!pointsDescription || typeof pointsDescription !== "string") error(400, "Ungültige Punkte-Beschreibung"); + const id = await RallyStation.create({ name, description, pointsDescription, shouldMaximize }); + return { id }; + }, +}; diff --git a/src/routes/admin/rallye/station/new/+page.svelte b/src/routes/admin/rallye/station/new/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc2e59ad267b13a2ef2afc6d5a2e30a6890956a3 --- /dev/null +++ b/src/routes/admin/rallye/station/new/+page.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import { enhance } from "$app/forms"; + import { goto } from "$app/navigation"; + import { addMessage } from "$lib/messages.js"; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, P, Textarea, Toggle } from "flowbite-svelte"; + + let { data } = $props(); +</script> + +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/station">Stationen</BreadcrumbItem> + <BreadcrumbItem href="/admin/rallye/station/new">Neue Station</BreadcrumbItem> +</Breadcrumb> + +<form method="post" action="?/create" use:enhance={()=>({result})=>{ + if(result.type === "success"){ + addMessage({type: "success", text: "Station wurde erstellt"}); + goto("/admin/rallye/station"); + }else{ + addMessage({type: "error", text: "Fehler beim Erstellen der Station"}); + console.error(result); + } +}}> + <Label class="mb-2"> + Name + <Input type="text" name="name" required /> + </Label> + <Label class="mb-2"> + Beschreibung (unterstützt Markdown) + <Textarea name="description" required /> + </Label> + <div class="flex gap-3 mb-2"> + <Label class="w-full"> + Punktebeschreibung + <Input type="text" name="pointsDescription" required /> + </Label> + <P class="content-end"> + <Toggle name="shouldMaximize" group={[]} choices={[{value: "on", label: "wenig"}]} classDiv="ms-3">viele</Toggle> + Punkte sind besser + </P> + </div> + <Button type="submit">Station erstellen</Button> +</form> diff --git a/src/routes/admin/schedule/+page.svelte b/src/routes/admin/schedule/+page.svelte index 15f8e6c67be2b38223ad3192551f472a34d0d100..48c0ac3c38540fff2b0b2bd2383ac39a6390f4c9 100644 --- a/src/routes/admin/schedule/+page.svelte +++ b/src/routes/admin/schedule/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { Accordion, AccordionItem, Button, Heading, Input, Label, Modal, P, Select, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from "flowbite-svelte"; + import { Accordion, AccordionItem, Breadcrumb, BreadcrumbItem, Button, Heading, Input, Label, Modal, P, Select, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from "flowbite-svelte"; import type { Schedule } from "$lib/server/database/entities/Schedule.entity.js"; import { enhance } from "$app/forms"; import { Permission } from "$lib/perms"; @@ -28,6 +28,11 @@ let brandingImageElement: HTMLImageElement; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule">Stundenpläne</BreadcrumbItem> +</Breadcrumb> + {#if data.user.permissions.has(Permission.UPDATE_SCHEDULE_CONFIG)} <Accordion> <AccordionItem> diff --git a/src/routes/admin/schedule/[id]/+page.svelte b/src/routes/admin/schedule/[id]/+page.svelte index 9a0130941d0e37c73eda03993f329495770ed19b..b61f66b26c4f126d3e7f18ae135995102f367556 100644 --- a/src/routes/admin/schedule/[id]/+page.svelte +++ b/src/routes/admin/schedule/[id]/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { Button, Heading, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from "flowbite-svelte"; + 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 type { Timeslot } from "$lib/server/database/entities/Schedule.entity.js"; @@ -26,6 +26,12 @@ } </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule/{data.schedule.id}">{data.schedule.studyProgram.name[$locale]}</BreadcrumbItem> +</Breadcrumb> + <Button href="/admin/schedule">Zurück zur Übersicht</Button> <Heading tag="h1" customSize="text-3xl font-bold" class="mb-2">Stundenplan für {schedule.studyProgram.name[$locale]}</Heading> diff --git a/src/routes/admin/schedule/fonts/+page.svelte b/src/routes/admin/schedule/fonts/+page.svelte index 744d90f9ff4da11c01f346531e747cb62df57b17..4a805031dc019c50e2ae9e1bdf7014bd169c8057 100644 --- a/src/routes/admin/schedule/fonts/+page.svelte +++ b/src/routes/admin/schedule/fonts/+page.svelte @@ -2,7 +2,7 @@ import { enhance } from "$app/forms"; import { invalidateAll } from "$app/navigation"; import { addMessage } from "$lib/messages.js"; - import { Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; export let data; function transformFontName(filename: string): string{ @@ -20,6 +20,12 @@ let fontName = ""; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule">Rallye</BreadcrumbItem> + <BreadcrumbItem href="/admin/schedule/fonts">Schriftarten</BreadcrumbItem> +</Breadcrumb> + <Table> <TableHead> <TableHeadCell>Schriftart</TableHeadCell> diff --git a/src/routes/admin/studyprogram/+page.svelte b/src/routes/admin/studyprogram/+page.svelte index 18cad2512e627edab478b79c9a30c0570b9c63d8..63a21f64b824d139f8ef7479a84594ff746955aa 100644 --- a/src/routes/admin/studyprogram/+page.svelte +++ b/src/routes/admin/studyprogram/+page.svelte @@ -4,7 +4,7 @@ import { locales } from '$lib/i18n/i18n.js'; import { addMessage } from '$lib/messages.js'; import type { StudyProgram } from '$lib/server/database/entities/StudyProgram.entity.js'; - import { Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from 'flowbite-svelte'; + import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from 'flowbite-svelte'; export let data; @@ -20,6 +20,11 @@ } </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/studyprogram">Stundengänge</BreadcrumbItem> +</Breadcrumb> + {#each data.studyPrograms as studyProgram (studyProgram.id)} <form method="post" action="?/updateStudyProgram" id={String(studyProgram.id)} use:enhance={()=>({result, update})=>{ if(result.type === "success"){ diff --git a/src/routes/admin/templates/+page.svelte b/src/routes/admin/templates/+page.svelte index 69d746440e38ce7435cf406841465f8ffca44583..f760d6598b7e852d5d9bb482310bfbf0d06b3333 100644 --- a/src/routes/admin/templates/+page.svelte +++ b/src/routes/admin/templates/+page.svelte @@ -4,7 +4,7 @@ import MailInput from "$lib/components/MailInput.svelte"; import { addMessage } from "$lib/messages"; import { Permission } from "$lib/perms.js"; - import { Input, Label, P, TabItem, Tabs } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Input, Label, P, TabItem, Tabs } from "flowbite-svelte"; let { data } = $props(); @@ -12,6 +12,11 @@ let canEdit = data.user.permissions.has(Permission.UPDATE_MAIL_TEMPLATES); </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/templates">Email-Templates</BreadcrumbItem> +</Breadcrumb> + <Tabs> <TabItem title="Tutoranmeldung" open> <P class="mb-2">Die Email, die versendet wird, wenn sich jemand als Tutor anmeldet.</P> diff --git a/src/routes/admin/tutor/+page.svelte b/src/routes/admin/tutor/+page.svelte index 0f4e348643a5bab3c7eb97eb1a3e6c8edc2720a8..983573156ee5d6444bd8459cbd54acd2af63aec9 100644 --- a/src/routes/admin/tutor/+page.svelte +++ b/src/routes/admin/tutor/+page.svelte @@ -1,10 +1,10 @@ <script lang="ts"> - import { Button, Checkbox, Heading, Label, Modal, P, Radio, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Heading, Label, Modal, P, Radio, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; import { locale } from "$lib/i18n/i18n"; import type { ObjectToDotProp } from "$lib/utils"; import { Permission } from "$lib/perms.js"; import { toCSV } from "$lib/csv"; - import type { Tutor } from "$lib/server/database/entities/Tutor.entity"; + import type { Tutor } from "$lib/server/database/entities/Tutor.entity"; export let data; let sortedTutors = data.tutors; @@ -114,6 +114,11 @@ } </style> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> +</Breadcrumb> + {#if data.user.permissions.has(Permission.VIEW_TUTORS)} <Modal bind:open={showExportModal} autoclose outsideclose title="Tutoren exportieren" classBody="space-y-1"> <Heading tag="h2" customSize="text-2xl font-bold">Format</Heading> diff --git a/src/routes/admin/tutor/[id]/+page.svelte b/src/routes/admin/tutor/[id]/+page.svelte index a507e7d3305f8a4fbfbac814754a464eb1ffadaf..1250f1fccbbf24c90872b058939a77274c65e810 100644 --- a/src/routes/admin/tutor/[id]/+page.svelte +++ b/src/routes/admin/tutor/[id]/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { A, Button, Checkbox, Input, Label, P, Select, Textarea } from "flowbite-svelte"; + import { A, Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, P, Select, Textarea } from "flowbite-svelte"; import { locale } from "$lib/i18n/i18n"; import Gender from "$lib/genders"; import Degree from "$lib/degrees"; @@ -12,77 +12,78 @@ $: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" }); </script> -{#if data.tutor} - <form method="post" use:enhance={()=>({result})=>{ - if(result.type === "success"){ - addMessage({type: "success", text: "Tutor gespeichert"}); - invalidateAll(); - }else{ - addMessage({type: "error", text: "Fehler beim Speichern"}); - console.error(result); - } - }}> - <Label class="mb-2"> - Vorname - <Input name="firstname" value={data.tutor.firstname} /> - </Label> - <Label class="mb-2"> - Nachname - <Input name="lastname" value={data.tutor.lastname} /> - </Label> - <Label class="mb-2"> - Rufname - <Input name="nickname" value={data.tutor.nickname} /> - </Label> - <Label class="mb-2"> - Geburtstag - <Input name="birthday" value={data.tutor.birthday} type="date" /> - </Label> - <Label class="mb-2"> - Handynummer - <Input name="phone" value={data.tutor.phone} /> - </Label> - <Label class="mb-2"> - E-Mail - <Input name="email" value={data.tutor.email} /> - </Label> - <Label class="mb-2"> - Adresse - <Textarea name="address" value={data.tutor.address} /> - </Label> - <Label class="mb-2"> - Geschlecht - <Select name="gender" value={data.tutor.gender} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} /> - </Label> - <Label class="mb-2"> - Shirt-Größe - <Select name="shirtSize" value={data.tutor.shirtSize} items={data.shirtSizes.map(s=>({value:s,name:s}))} /> - </Label> - <Label class="mb-2"> - Studiengang - <Select name="studyProgram" value={data.tutor.studyProgram} items={data.studyPrograms.map(p=>({value:p.id,name:p.name[$locale]}))} /> - </Label> - <Label class="mb-2"> - Abschluss - <Select name="degree" value={data.tutor.degree} items={Object.entries(Degree).map(([value, name])=>({value, name: name[$locale]}))} /> - </Label> - <Checkbox class="mb-2" label="Geschult" name="trained" checked={data.tutor.trained}>Geschult</Checkbox> - <Label class="mb-2"> - Schulung - <Select name="training" value={data.tutor.training?.date ?? "-"} items={[{name:"Bereits geschult",value:"-"}, ...data.trainings.map(t=>({value:t.date,name:dateFormatter.format(new Date(t.date))}))]} /> - </Label> - <Label class="mb-2"> - Co-Tutor-Wunsch - <Input name="coTutorWish" value={data.tutor.coTutorWish} /> - </Label> - <Label class="mb-2"> - Notiz - <Textarea name="notes" value={data.tutor.notes} /> - </Label> - <Checkbox class="mb-2" name="mentor" checked={data.tutor.mentor}>Informatik Mentor</Checkbox> - <Button type="submit" color="primary">Speichern</Button> - </form> -{:else} - <P class="mb-6">Der Tutor konnte nicht gefunden werden.</P> - <P class="mb-6"><A href="/admin/tutor">Zurück zur Übersicht</A></P> -{/if} +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor/{data.tutor.id}">{data.tutor.firstname} {data.tutor.lastname}</BreadcrumbItem> +</Breadcrumb> + +<form method="post" use:enhance={()=>({result})=>{ + if(result.type === "success"){ + addMessage({type: "success", text: "Tutor gespeichert"}); + invalidateAll(); + }else{ + addMessage({type: "error", text: "Fehler beim Speichern"}); + console.error(result); + } +}}> + <Label class="mb-2"> + Vorname + <Input name="firstname" value={data.tutor.firstname} /> + </Label> + <Label class="mb-2"> + Nachname + <Input name="lastname" value={data.tutor.lastname} /> + </Label> + <Label class="mb-2"> + Rufname + <Input name="nickname" value={data.tutor.nickname} /> + </Label> + <Label class="mb-2"> + Geburtstag + <Input name="birthday" value={data.tutor.birthday} type="date" /> + </Label> + <Label class="mb-2"> + Handynummer + <Input name="phone" value={data.tutor.phone} /> + </Label> + <Label class="mb-2"> + E-Mail + <Input name="email" value={data.tutor.email} /> + </Label> + <Label class="mb-2"> + Adresse + <Textarea name="address" value={data.tutor.address} /> + </Label> + <Label class="mb-2"> + Geschlecht + <Select name="gender" value={data.tutor.gender} items={Object.entries(Gender).map(([value, name])=>({value, name: name[$locale]}))} /> + </Label> + <Label class="mb-2"> + Shirt-Größe + <Select name="shirtSize" value={data.tutor.shirtSize} items={data.shirtSizes.map(s=>({value:s,name:s}))} /> + </Label> + <Label class="mb-2"> + Studiengang + <Select name="studyProgram" value={data.tutor.studyProgram} items={data.studyPrograms.map(p=>({value:p.id,name:p.name[$locale]}))} /> + </Label> + <Label class="mb-2"> + Abschluss + <Select name="degree" value={data.tutor.degree} items={Object.entries(Degree).map(([value, name])=>({value, name: name[$locale]}))} /> + </Label> + <Checkbox class="mb-2" label="Geschult" name="trained" checked={data.tutor.trained}>Geschult</Checkbox> + <Label class="mb-2"> + Schulung + <Select name="training" value={data.tutor.training?.date ?? "-"} items={[{name:"Bereits geschult",value:"-"}, ...data.trainings.map(t=>({value:t.date,name:dateFormatter.format(new Date(t.date))}))]} /> + </Label> + <Label class="mb-2"> + Co-Tutor-Wunsch + <Input name="coTutorWish" value={data.tutor.coTutorWish} /> + </Label> + <Label class="mb-2"> + Notiz + <Textarea name="notes" value={data.tutor.notes} /> + </Label> + <Checkbox class="mb-2" name="mentor" checked={data.tutor.mentor}>Informatik Mentor</Checkbox> + <Button type="submit" color="primary">Speichern</Button> +</form> diff --git a/src/routes/admin/tutor/mail/+page.svelte b/src/routes/admin/tutor/mail/+page.svelte index 6665d9d2d34a04300850c2f5a070a54345456d89..17ef6d2c7ae0c76e7b1c445177d9fe7bebb15c3e 100644 --- a/src/routes/admin/tutor/mail/+page.svelte +++ b/src/routes/admin/tutor/mail/+page.svelte @@ -1,6 +1,6 @@ <script lang="ts"> import { Degree } from '$lib/degrees'; - import { Button, Heading, Label, MultiSelect, Select, Span } from 'flowbite-svelte'; + import { Breadcrumb, BreadcrumbItem, Button, Heading, Label, MultiSelect, Select, Span } from 'flowbite-svelte'; import MailInput from '$lib/components/MailInput.svelte'; import type { Filter } from './+server.js'; import { locales } from '$lib/i18n/i18n.js'; @@ -35,6 +35,12 @@ } </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor/mail">Email schreiben</BreadcrumbItem> +</Breadcrumb> + <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">E-Mail senden</Heading> {#each filters as filter, i (filter)} diff --git a/src/routes/admin/tutor/training/+page.svelte b/src/routes/admin/tutor/training/+page.svelte index bd6578c0695c7a49f30cafe8f02372fd1e719313..a8a75c01aec53f8f8527cbc13f43df7df375e66a 100644 --- a/src/routes/admin/tutor/training/+page.svelte +++ b/src/routes/admin/tutor/training/+page.svelte @@ -1,11 +1,17 @@ <script lang="ts"> import { locale } from "$lib/i18n/i18n"; - import { Permission } from "$lib/perms"; - import { Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import { Permission } from "$lib/perms"; + import { Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; let { data } = $props(); </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor/training">Schulungen</BreadcrumbItem> +</Breadcrumb> + <Table> <TableHead> <TableHeadCell>Datum</TableHeadCell> @@ -34,9 +40,7 @@ {#if data.user.permissions.has(Permission.EDIT_TRAININGS)} <TableBodyRow> <TableBodyCell colspan="5"> - <div class="flex justify-center"> - <Button href="/admin/tutor/training/new" class="w-20">+</Button> - </div> + <Button href="/admin/tutor/training/new" class="w-20 block mx-auto">+</Button> </TableBodyCell> </TableBodyRow> {/if} diff --git a/src/routes/admin/tutor/training/[id=number]/+page.server.ts b/src/routes/admin/tutor/training/[id=number]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed0a0d8d8f07d4438aeb415481bf9c5e68d3499b --- /dev/null +++ b/src/routes/admin/tutor/training/[id=number]/+page.server.ts @@ -0,0 +1,13 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity'; + +export const load = (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'); + return { + training: {...training, participants: training.participants.length}, + }; +}) satisfies PageServerLoad; diff --git a/src/routes/admin/tutor/training/[id=number]/+page.svelte b/src/routes/admin/tutor/training/[id=number]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..56457672418d35131efeae47f2469b0099ad8a22 --- /dev/null +++ b/src/routes/admin/tutor/training/[id=number]/+page.svelte @@ -0,0 +1,37 @@ +<script lang="ts"> + import { enhance } from '$app/forms'; + import { goto } from '$app/navigation'; + import { addMessage } from '$lib/messages.js'; + import { Label, Textarea, Checkbox, Button, Input } from 'flowbite-svelte'; + + let { data } = $props(); +</script> + +<form action="?/update" method="post" use:enhance={()=>({result})=>{ + if(result.type === "success"){ + addMessage({type: "success", text: "Schulung gespeichert"}); + goto("/admin/tutor/training"); + }else{ + addMessage({type: "error", text: "Fehler beim Speichern"}); + console.error(result); + } +}}> + <Label class="mb-2"> + Datum + <Input type="date" name="date" value={data.training.date} required /> + </Label> + <Label class="mb-2"> + Ort + <Input type="text" name="location" value={data.training.location} required /> + </Label> + <Label class="mb-2"> + Maximale Teilnehmer + <Input type="number" name="maxParticipants" min="1" value={data.training.maxParticipants} required /> + </Label> + <Label class="mb-2"> + Anmerkung + <Textarea type="text" name="notes" value={data.training.notes} /> + </Label> + <Checkbox name="internal" class="mb-2" value="on" checked={data.training.internal}>Intern (Tutoren sehen die Schulung nicht und können sich nicht selbst dafür anmelden)</Checkbox> + <Button type="submit" class="mb-2">Speichern</Button> +</form> diff --git a/src/routes/admin/tutor/training/new/+page.svelte b/src/routes/admin/tutor/training/new/+page.svelte index 3c9d6eee99766010a331bf737a75329d67ddbbd6..8058a464176c3ca7dfbebebab30f2b9b333ec789 100644 --- a/src/routes/admin/tutor/training/new/+page.svelte +++ b/src/routes/admin/tutor/training/new/+page.svelte @@ -2,9 +2,16 @@ import { enhance } from "$app/forms"; import { goto } from "$app/navigation"; import { addMessage } from "$lib/messages"; - import { Button, Checkbox, Input, Label, Textarea } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Checkbox, Input, Label, Textarea } from "flowbite-svelte"; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor/training">Schulungen</BreadcrumbItem> + <BreadcrumbItem href="/admin/tutor/training/new">Neue Schulung</BreadcrumbItem> +</Breadcrumb> + <form action="?/create" method="post" use:enhance={()=>({result})=>{ if(result.type === "success"){ addMessage({type: "success", text: "Schulung erstellt"}); diff --git a/src/routes/admin/user/+page.svelte b/src/routes/admin/user/+page.svelte index e56fbbcee0ff05e4924aa801f836e1f6845dd916..e9e80429e7e992b53829ba69aeec673440ea2640 100644 --- a/src/routes/admin/user/+page.svelte +++ b/src/routes/admin/user/+page.svelte @@ -1,9 +1,9 @@ <script lang="ts"> import { enhance } from "$app/forms"; import { invalidateAll } from "$app/navigation"; - import { addMessage } from "$lib/messages.js"; + import { addMessage } from "$lib/messages.js"; import { Permission, PermissionDescription } from "$lib/perms"; - import { Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; + import { Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; let { data } = $props(); @@ -11,6 +11,11 @@ const allPermissions: [string, Permission][] = Object.entries(Permission).filter(([, val])=>typeof val === "number") as [string, Permission][]; </script> +<Breadcrumb class="mb-4"> + <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> + <BreadcrumbItem href="/admin/user">Nutzer</BreadcrumbItem> +</Breadcrumb> + {#if canEdit} <form id="default-form" method="post" action="?/defaultPermissions" use:enhance={({formData})=>{ let perms = 0; diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 9c42179a9b00e637b06c7244b53ad247c902f19a..3251d2944d633a9b728a2380b6baa6df73c7af0e 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -20,6 +20,9 @@ const config = { minWidth: { "60": "15rem", }, + screens: { + "xs": "475px", + }, spacing: { "120": "30rem", "auto": "auto",