From 8c6621b5984cf01cac4977e841eb15df6c6446a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aaron=20D=C3=B6tsch?= <aaron@fsmpi.rwth-aachen.de>
Date: Fri, 16 Aug 2024 13:16:13 +0200
Subject: [PATCH] updated

---
 src/lib/components/AdminLayout.svelte         |   3 +
 src/lib/i18n/de.ts                            |   9 ++
 src/lib/i18n/en.ts                            |   9 ++
 src/lib/perms.ts                              |   4 +
 .../database/entities/RallyStation.entity.ts  |   2 +
 .../server/database/entities/Tutor.entity.ts  |   1 +
 src/lib/server/database/migrations/0_init.sql |   5 +-
 src/params/number.ts                          |   2 +
 src/params/uuid.ts                            |   5 +-
 .../rallye/[id=uuid]/+page.server.ts          |   2 +-
 .../(non-admin)/rallye/[id=uuid]/+page.svelte |  30 ++--
 src/routes/admin/+page.svelte                 |   6 +-
 src/routes/admin/eswe/+page.svelte            |   7 +-
 src/routes/admin/flyer/+page.svelte           |   7 +-
 src/routes/admin/rabatte/+page.svelte         |   7 +-
 src/routes/admin/rabatte/[id]/+page.svelte    |   8 +-
 .../admin/rallye/points/+page.server.ts       |  15 ++
 src/routes/admin/rallye/points/+page.svelte   |  46 ++++++
 src/routes/admin/rallye/station/+page.svelte  |  63 ++++----
 .../rallye/station/[id=uuid]/+page.server.ts  |  29 ++++
 .../rallye/station/[id=uuid]/+page.svelte     |  45 ++++++
 .../admin/rallye/station/new/+page.server.ts  |  17 ++
 .../admin/rallye/station/new/+page.svelte     |  45 ++++++
 src/routes/admin/schedule/+page.svelte        |   7 +-
 src/routes/admin/schedule/[id]/+page.svelte   |   8 +-
 src/routes/admin/schedule/fonts/+page.svelte  |   8 +-
 src/routes/admin/studyprogram/+page.svelte    |   7 +-
 src/routes/admin/templates/+page.svelte       |   7 +-
 src/routes/admin/tutor/+page.svelte           |   9 +-
 src/routes/admin/tutor/[id]/+page.svelte      | 151 +++++++++---------
 src/routes/admin/tutor/mail/+page.svelte      |   8 +-
 src/routes/admin/tutor/training/+page.svelte  |  14 +-
 .../training/[id=number]/+page.server.ts      |  13 ++
 .../tutor/training/[id=number]/+page.svelte   |  37 +++++
 .../admin/tutor/training/new/+page.svelte     |   9 +-
 src/routes/admin/user/+page.svelte            |   9 +-
 tailwind.config.cjs                           |   3 +
 37 files changed, 513 insertions(+), 144 deletions(-)
 create mode 100644 src/params/number.ts
 create mode 100644 src/routes/admin/rallye/points/+page.server.ts
 create mode 100644 src/routes/admin/rallye/points/+page.svelte
 create mode 100644 src/routes/admin/rallye/station/[id=uuid]/+page.server.ts
 create mode 100644 src/routes/admin/rallye/station/[id=uuid]/+page.svelte
 create mode 100644 src/routes/admin/rallye/station/new/+page.server.ts
 create mode 100644 src/routes/admin/rallye/station/new/+page.svelte
 create mode 100644 src/routes/admin/tutor/training/[id=number]/+page.server.ts
 create mode 100644 src/routes/admin/tutor/training/[id=number]/+page.svelte

diff --git a/src/lib/components/AdminLayout.svelte b/src/lib/components/AdminLayout.svelte
index c1c8ef8..5810cec 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 d7faa25..162e29c 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 75a38f9..9a8f48c 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 95f2fe8..6bfda68 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 13d473b..2a58a54 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 6723be0..03f6a67 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 fbaee01..75fbd5f 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 0000000..9fa3e68
--- /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 172d381..1a7db83 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 60d3cac..4abe685 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 6622206..45c2a23 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 dfe626f..651ad3a 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 f3187bf..b76cfc9 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 2a8859e..b21856e 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 82a99e1..479b9b3 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 1d5643a..81070ea 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 0000000..b558fc6
--- /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 0000000..34cdba3
--- /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 95cdbf7..9b8b376 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 0000000..5c26880
--- /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 0000000..5a244c3
--- /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 0000000..2e544e2
--- /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 0000000..fc2e59a
--- /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 15f8e6c..48c0ac3 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 9a01309..b61f66b 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 744d90f..4a80503 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 18cad25..63a21f6 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 69d7464..f760d65 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 0f4e348..9835731 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 a507e7d..1250f1f 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 6665d9d..17ef6d2 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 bd6578c..a8a75c0 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 0000000..ed0a0d8
--- /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 0000000..5645767
--- /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 3c9d6ee..8058a46 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 e56fbbc..e9e8042 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 9c42179..3251d29 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",
-- 
GitLab