From 97a3c2196b2107336417246d39ea26c336a72754 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 15:01:22 +0200 Subject: [PATCH 01/11] feat(frontend): add paper design tokens and Google Fonts --- frontend/src/app.css | 151 +++++++++++++++++++++++++++++ frontend/src/app.html | 3 + frontend/src/routes/+layout.svelte | 3 + 3 files changed, 157 insertions(+) create mode 100644 frontend/src/app.css diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..9aa2a9a --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,151 @@ +/* Academic / paper-inspired design system for Tutormanager */ + +:root { + --paper: #f4efe6; + --paper-2: #ebe4d6; + --paper-3: #ded4c0; + --rule: #c9bfa9; + --rule-soft: #d9d0bb; + --ink: #1f1b16; + --ink-2: #3a342b; + --ink-3: #6b6356; + --ink-4: #968b7a; + --accent: #8a2c1f; /* oxblood */ + --accent-soft: #c66a5b; + --highlight: #f1d36a; /* highlighter yellow */ + --highlight-soft: #f5e3a4; + --green: #4a6b3a; /* present */ + --red: #8a2c1f; /* absent / lock */ + --amber: #b07d2a; /* open */ + + --serif: "Source Serif 4", "Source Serif Pro", "EB Garamond", Georgia, serif; + --sans: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif; + --mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; +} + +/* Paper grain — subtle SVG noise overlay */ +.paper-bg { + background-color: var(--paper); + background-image: + radial-gradient(circle at 25% 15%, rgba(160,140,110,0.05) 0, transparent 40%), + radial-gradient(circle at 80% 70%, rgba(160,140,110,0.04) 0, transparent 50%), + url("data:image/svg+xml;utf8,"); +} + +/* Ruled lines — like a notebook */ +.ruled { + background-image: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 27px, + var(--rule-soft) 27px, + var(--rule-soft) 28px + ); +} + +* { box-sizing: border-box; } + +body, .ui { + font-family: var(--sans); + color: var(--ink); + font-feature-settings: "ss01", "cv11"; +} + +.serif { font-family: var(--serif); font-weight: 400; letter-spacing: -0.01em; } +.mono { font-family: var(--mono); } + +.h1 { font-family: var(--serif); font-weight: 500; font-size: 44px; line-height: 1.05; letter-spacing: -0.02em; color: var(--ink); } +.h2 { font-family: var(--serif); font-weight: 500; font-size: 28px; line-height: 1.15; letter-spacing: -0.01em; color: var(--ink); } +.h3 { font-family: var(--serif); font-weight: 500; font-size: 20px; line-height: 1.2; color: var(--ink); } +.eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); } +.body { font-family: var(--sans); font-size: 14px; line-height: 1.5; color: var(--ink-2); } +.small { font-family: var(--sans); font-size: 12px; color: var(--ink-3); } +.tiny { font-family: var(--sans); font-size: 11px; color: var(--ink-3); } + +/* Underline marker — looks like a hand drawn highlight stroke */ +.marker { + background: linear-gradient(180deg, transparent 60%, var(--highlight-soft) 60%, var(--highlight-soft) 92%, transparent 92%); + padding: 0 2px; +} + +/* Status pills */ +.pill { + display: inline-flex; align-items: center; gap: 6px; + font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; + padding: 3px 9px; border-radius: 999px; + border: 1px solid currentColor; + background: transparent; +} +.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } + +.pill.open { color: var(--amber); background: rgba(176,125,42,0.08); } +.pill.closed { color: var(--ink-3); } +.pill.locked { color: var(--accent); background: rgba(138,44,31,0.06); } +.pill.present { color: var(--green); background: rgba(74,107,58,0.08); } +.pill.absent { color: var(--accent); } + +/* Buttons */ +.btn { + font-family: var(--sans); font-size: 13px; font-weight: 500; + padding: 8px 14px; border-radius: 6px; + border: 1px solid var(--ink); background: var(--ink); color: var(--paper); + cursor: pointer; display: inline-flex; align-items: center; gap: 6px; +} +.btn.ghost { background: transparent; color: var(--ink); border-color: var(--rule); } +.btn.ghost:hover { background: rgba(0,0,0,0.04); } +.btn.accent { background: var(--accent); border-color: var(--accent); color: #f7eedc; } +.btn.sm { padding: 5px 10px; font-size: 12px; } + +/* Card */ +.card { + background: #fbf7ee; + border: 1px solid var(--rule); + border-radius: 4px; + box-shadow: 0 1px 0 rgba(0,0,0,0.03); +} + +/* Marginalia — handwritten look. Use Caveat as marginal handwriting. */ +.handwritten { + font-family: "Caveat", "Kalam", cursive; + color: var(--accent); + font-size: 18px; + line-height: 1; +} + +/* Custom scrollbars */ +.scroll::-webkit-scrollbar { width: 8px; height: 8px; } +.scroll::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 8px; } +.scroll::-webkit-scrollbar-track { background: transparent; } + +/* hand-drawn underline svg accent under section titles */ +.underline-stroke { display: inline-block; margin-top: 2px; } + +/* Stamp — for "PRÄSENT" mark */ +.stamp { + display: inline-block; font-family: var(--mono); font-weight: 700; font-size: 11px; + letter-spacing: 0.15em; text-transform: uppercase; + color: var(--accent); border: 2px solid var(--accent); + padding: 3px 8px; border-radius: 3px; + transform: rotate(-4deg); + background: rgba(138,44,31,0.04); +} + +/* Subtle hover row */ +.row-hover:hover { background: rgba(0,0,0,0.025); } + +/* Tab bar */ +.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--rule); } +.tab { font-family: var(--sans); font-size: 13px; padding: 10px 14px; color: var(--ink-3); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; } +.tab.active { color: var(--ink); border-bottom-color: var(--ink); } + +/* Input */ +.input { + font-family: var(--sans); font-size: 13px; + padding: 8px 11px; border: 1px solid var(--rule); + background: #fbf7ee; border-radius: 4px; + color: var(--ink); +} +.input:focus { outline: none; border-color: var(--ink); } + +/* Inline divider */ +.div-h { height: 1px; background: var(--rule); width: 100%; } diff --git a/frontend/src/app.html b/frontend/src/app.html index 6769ed5..9ec39e9 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -4,6 +4,9 @@ + + + %sveltekit.head% diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 4fa864c..aa75f7e 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1 +1,4 @@ + From 04155c182a205246fdc6222c69b16cb803da91b0 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 15:08:50 +0200 Subject: [PATCH 02/11] feat(frontend): add primitive UI components (Icon, StatusPill, UnderlineStroke, StatCard, Tally, Field) --- frontend/src/lib/components/Field.svelte | 24 +++++++++ frontend/src/lib/components/Icon.svelte | 52 +++++++++++++++++++ frontend/src/lib/components/StatCard.svelte | 24 +++++++++ frontend/src/lib/components/StatusPill.svelte | 15 ++++++ frontend/src/lib/components/Tally.svelte | 21 ++++++++ .../src/lib/components/UnderlineStroke.svelte | 21 ++++++++ 6 files changed, 157 insertions(+) create mode 100644 frontend/src/lib/components/Field.svelte create mode 100644 frontend/src/lib/components/Icon.svelte create mode 100644 frontend/src/lib/components/StatCard.svelte create mode 100644 frontend/src/lib/components/StatusPill.svelte create mode 100644 frontend/src/lib/components/Tally.svelte create mode 100644 frontend/src/lib/components/UnderlineStroke.svelte diff --git a/frontend/src/lib/components/Field.svelte b/frontend/src/lib/components/Field.svelte new file mode 100644 index 0000000..a29fdd6 --- /dev/null +++ b/frontend/src/lib/components/Field.svelte @@ -0,0 +1,24 @@ + + +
+ {label} +
+ + {#if suffix} + {suffix} + {/if} +
+
diff --git a/frontend/src/lib/components/Icon.svelte b/frontend/src/lib/components/Icon.svelte new file mode 100644 index 0000000..a1f4625 --- /dev/null +++ b/frontend/src/lib/components/Icon.svelte @@ -0,0 +1,52 @@ + + +{#if name === 'check'} + + + +{:else if name === 'x'} + + + +{:else if name === 'lock'} + + + + +{:else if name === 'open'} + + + + +{:else if name === 'copy'} + + + + +{:else if name === 'edit'} + + + +{:else if name === 'download'} + + + +{:else if name === 'arrow'} + + + +{:else if name === 'search'} + + + + +{:else if name === 'plus'} + + + +{/if} diff --git a/frontend/src/lib/components/StatCard.svelte b/frontend/src/lib/components/StatCard.svelte new file mode 100644 index 0000000..88ed7f6 --- /dev/null +++ b/frontend/src/lib/components/StatCard.svelte @@ -0,0 +1,24 @@ + + +
+ {label} +
+ + {value} + + {#if suffix} + {suffix} + {/if} +
+ {#if hint} +
{hint}
+ {/if} +
diff --git a/frontend/src/lib/components/StatusPill.svelte b/frontend/src/lib/components/StatusPill.svelte new file mode 100644 index 0000000..b17ea8c --- /dev/null +++ b/frontend/src/lib/components/StatusPill.svelte @@ -0,0 +1,15 @@ + + +{labels[status]} diff --git a/frontend/src/lib/components/Tally.svelte b/frontend/src/lib/components/Tally.svelte new file mode 100644 index 0000000..5d7c57e --- /dev/null +++ b/frontend/src/lib/components/Tally.svelte @@ -0,0 +1,21 @@ + + +
+ {label} + + {value} + {#if total !== undefined} + / {total} + {:else if suffix} + {suffix} + {/if} + +
diff --git a/frontend/src/lib/components/UnderlineStroke.svelte b/frontend/src/lib/components/UnderlineStroke.svelte new file mode 100644 index 0000000..dcd318e --- /dev/null +++ b/frontend/src/lib/components/UnderlineStroke.svelte @@ -0,0 +1,21 @@ + + + + + From 7da7c1e1d0021fbaa070d0c135b52eb9328e3d51 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 15:13:35 +0200 Subject: [PATCH 03/11] feat(frontend): add SeatMap component (tutor/student/student-self variants) --- frontend/src/lib/components/SeatMap.svelte | 169 +++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 frontend/src/lib/components/SeatMap.svelte diff --git a/frontend/src/lib/components/SeatMap.svelte b/frontend/src/lib/components/SeatMap.svelte new file mode 100644 index 0000000..d5a62ed --- /dev/null +++ b/frontend/src/lib/components/SeatMap.svelte @@ -0,0 +1,169 @@ + + +
+ + +
+ + +
+ + +
+ + +
+
+
+
Fenster
+ + +
+ + + + +
Tür
+ + +
+
Beamer
+ + +
+ Pult · Tutor:in +
+ + + {#each TABLES as t} +
+ {t.label} +
+ {/each} + + + {#each SEATS as seat} + {@const s = seatStyle(seat)} + + {/each} + + +
+
+ N + + + +
+
+ +
+
From 7e326153a864a98d37a6f3821bc3053140219482 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 15:39:28 +0200 Subject: [PATCH 04/11] feat(frontend): add TutorShell sidebar layout and NoteEditor with auto-save --- frontend/src/lib/components/NoteEditor.svelte | 187 ++++++++++++++++++ frontend/src/lib/components/TutorShell.svelte | 96 +++++++++ 2 files changed, 283 insertions(+) create mode 100644 frontend/src/lib/components/NoteEditor.svelte create mode 100644 frontend/src/lib/components/TutorShell.svelte diff --git a/frontend/src/lib/components/NoteEditor.svelte b/frontend/src/lib/components/NoteEditor.svelte new file mode 100644 index 0000000..62601fd --- /dev/null +++ b/frontend/src/lib/components/NoteEditor.svelte @@ -0,0 +1,187 @@ + + +
+ + +
+
Studierende
+
{present.length} anwesend · {absent.length} fehlen
+
+ + +
+ {#each present as s} + {@const isSel = s.id === selectedStudentId} + + {/each} + {#each absent as s} + + {/each} +
+ + + {#if selected} +
+ + +
+ + {initials(selected.name)} + +
+
{selected.name}
+
+ Sitzplatz {selectedAttendance?.seat_id ?? '—'} · seit {checkinTime(selected.id)} +
+
+ Präsent +
+ +
+ Notiz · Woche {String(weekNr).padStart(2, '0')} +
+ + + + +
+ {#each TAGS as tag} + + {/each} +
+ + +
+ {savedAt ? `Auto-gespeichert · ${savedAt}` : 'Noch nicht gespeichert'} + Notizen vergangener Wochen ↗ +
+ +
+ {:else} +
+ Sitz anklicken um Notiz zu schreiben +
+ {/if} + +
diff --git a/frontend/src/lib/components/TutorShell.svelte b/frontend/src/lib/components/TutorShell.svelte new file mode 100644 index 0000000..a2784c0 --- /dev/null +++ b/frontend/src/lib/components/TutorShell.svelte @@ -0,0 +1,96 @@ + + +
+ + +
+ {@render children()} +
+
From bbccef4436c825c4a56c19054926f6a0b98dfbed Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 17:46:50 +0200 Subject: [PATCH 05/11] feat(frontend): route migration - add /admin/login, /admin/students, /admin/live/[slotId], rooms/[roomId] --- frontend/.svelte-kit/ambient.d.ts | 22 +- frontend/.svelte-kit/generated/client/app.js | 20 +- .../.svelte-kit/generated/client/nodes/10.js | 2 +- .../.svelte-kit/generated/client/nodes/11.js | 2 +- .../.svelte-kit/generated/client/nodes/12.js | 2 +- .../.svelte-kit/generated/client/nodes/13.js | 1 + .../.svelte-kit/generated/client/nodes/14.js | 1 + .../.svelte-kit/generated/client/nodes/15.js | 1 + .../.svelte-kit/generated/client/nodes/16.js | 1 + .../.svelte-kit/generated/client/nodes/8.js | 2 +- .../.svelte-kit/generated/client/nodes/9.js | 2 +- .../.svelte-kit/generated/server/internal.js | 4 +- frontend/.svelte-kit/non-ambient.d.ts | 17 +- .../.svelte-kit/types/route_meta_data.json | 4 + .../.svelte-kit/types/src/routes/$types.d.ts | 4 +- .../types/src/routes/admin/$types.d.ts | 4 +- .../routes/admin/live/[slotId]/$types.d.ts | 18 ++ .../types/src/routes/admin/login/$types.d.ts | 17 ++ .../routes/admin/rooms/[roomId]/$types.d.ts | 18 ++ .../src/routes/admin/students/$types.d.ts | 17 ++ frontend/src/routes/+page.svelte | 50 +--- frontend/src/routes/admin/+layout.svelte | 4 +- .../routes/admin/live/[slotId]/+page.svelte | 10 + frontend/src/routes/admin/login/+page.svelte | 82 ++++++ frontend/src/routes/admin/rooms/+page.svelte | 248 ++++-------------- .../routes/admin/rooms/[roomId]/+page.svelte | 111 ++++++++ .../src/routes/admin/students/+page.svelte | 8 + 27 files changed, 403 insertions(+), 269 deletions(-) create mode 100644 frontend/.svelte-kit/generated/client/nodes/13.js create mode 100644 frontend/.svelte-kit/generated/client/nodes/14.js create mode 100644 frontend/.svelte-kit/generated/client/nodes/15.js create mode 100644 frontend/.svelte-kit/generated/client/nodes/16.js create mode 100644 frontend/.svelte-kit/types/src/routes/admin/live/[slotId]/$types.d.ts create mode 100644 frontend/.svelte-kit/types/src/routes/admin/login/$types.d.ts create mode 100644 frontend/.svelte-kit/types/src/routes/admin/rooms/[roomId]/$types.d.ts create mode 100644 frontend/.svelte-kit/types/src/routes/admin/students/$types.d.ts create mode 100644 frontend/src/routes/admin/live/[slotId]/+page.svelte create mode 100644 frontend/src/routes/admin/login/+page.svelte create mode 100644 frontend/src/routes/admin/rooms/[roomId]/+page.svelte create mode 100644 frontend/src/routes/admin/students/+page.svelte diff --git a/frontend/.svelte-kit/ambient.d.ts b/frontend/.svelte-kit/ambient.d.ts index 38f289b..5e21f33 100644 --- a/frontend/.svelte-kit/ambient.d.ts +++ b/frontend/.svelte-kit/ambient.d.ts @@ -40,6 +40,7 @@ declare module '$env/static/private' { export const SHELL: string; export const npm_command: string; + export const COREPACK_ENABLE_AUTO_PIN: string; export const UV_CACHE_DIR: string; export const npm_config_userconfig: string; export const COLORTERM: string; @@ -81,6 +82,8 @@ declare module '$env/static/private' { export const SYSTEMD_EXEC_PID: string; export const _: string; export const KITTY_PUBLIC_KEY: string; + export const NoDefaultCurrentDirectoryInExePath: string; + export const CLAUDECODE: string; export const MOTD_SHOWN: string; export const HOME: string; export const LC_PAPER: string; @@ -111,7 +114,6 @@ declare module '$env/static/private' { export const npm_config_prefix: string; export const ZDOTDIR: string; export const USER: string; - export const GIT_PAGER: string; export const SDL_VIDEODRIVER: string; export const HYPRLAND_INSTANCE_SIGNATURE: string; export const MANPAGER: string; @@ -122,6 +124,7 @@ declare module '$env/static/private' { export const LESS_TERMCAP_ue: string; export const MOZ_ENABLE_WAYLAND: string; export const LESS_TERMCAP_us: string; + export const GIT_EDITOR: string; export const PAGER: string; export const LC_TELEPHONE: string; export const ANDROID_SDK_ROOT: string; @@ -132,24 +135,26 @@ declare module '$env/static/private' { export const MANAGERPIDFDID: string; export const npm_config_user_agent: string; export const ROCM_PATH: string; + export const OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: string; export const XDG_STATE_HOME: string; export const npm_execpath: string; export const LD_LIBRARY_PATH: string; + export const DISABLE_AUTOUPDATER: string; export const LC_CTYPE: string; export const XDG_RUNTIME_DIR: string; export const KITTY_LISTEN_ON: string; export const GRIMBLAST_EDITOR: string; - export const GEMINI_CLI: string; + export const CLAUDE_CODE_ENTRYPOINT: string; export const DEBUGINFOD_URLS: string; export const npm_package_json: string; export const LC_TIME: string; export const HYPRCURSOR_THEME: string; - export const GEMINI_CLI_NO_RELAUNCH: string; export const JOURNAL_STREAM: string; export const LC_COLLATE: string; export const XCURSOR_THEME: string; export const XDG_DATA_DIRS: string; export const GDK_BACKEND: string; + export const CLAUDE_CODE_EXECPATH: string; export const npm_config_noproxy: string; export const PATH: string; export const npm_config_node_gyp: string; @@ -254,6 +259,7 @@ declare module '$env/dynamic/private' { export const env: { SHELL: string; npm_command: string; + COREPACK_ENABLE_AUTO_PIN: string; UV_CACHE_DIR: string; npm_config_userconfig: string; COLORTERM: string; @@ -295,6 +301,8 @@ declare module '$env/dynamic/private' { SYSTEMD_EXEC_PID: string; _: string; KITTY_PUBLIC_KEY: string; + NoDefaultCurrentDirectoryInExePath: string; + CLAUDECODE: string; MOTD_SHOWN: string; HOME: string; LC_PAPER: string; @@ -325,7 +333,6 @@ declare module '$env/dynamic/private' { npm_config_prefix: string; ZDOTDIR: string; USER: string; - GIT_PAGER: string; SDL_VIDEODRIVER: string; HYPRLAND_INSTANCE_SIGNATURE: string; MANPAGER: string; @@ -336,6 +343,7 @@ declare module '$env/dynamic/private' { LESS_TERMCAP_ue: string; MOZ_ENABLE_WAYLAND: string; LESS_TERMCAP_us: string; + GIT_EDITOR: string; PAGER: string; LC_TELEPHONE: string; ANDROID_SDK_ROOT: string; @@ -346,24 +354,26 @@ declare module '$env/dynamic/private' { MANAGERPIDFDID: string; npm_config_user_agent: string; ROCM_PATH: string; + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: string; XDG_STATE_HOME: string; npm_execpath: string; LD_LIBRARY_PATH: string; + DISABLE_AUTOUPDATER: string; LC_CTYPE: string; XDG_RUNTIME_DIR: string; KITTY_LISTEN_ON: string; GRIMBLAST_EDITOR: string; - GEMINI_CLI: string; + CLAUDE_CODE_ENTRYPOINT: string; DEBUGINFOD_URLS: string; npm_package_json: string; LC_TIME: string; HYPRCURSOR_THEME: string; - GEMINI_CLI_NO_RELAUNCH: string; JOURNAL_STREAM: string; LC_COLLATE: string; XCURSOR_THEME: string; XDG_DATA_DIRS: string; GDK_BACKEND: string; + CLAUDE_CODE_EXECPATH: string; npm_config_noproxy: string; PATH: string; npm_config_node_gyp: string; diff --git a/frontend/.svelte-kit/generated/client/app.js b/frontend/.svelte-kit/generated/client/app.js index 42e9286..d3bb8c7 100644 --- a/frontend/.svelte-kit/generated/client/app.js +++ b/frontend/.svelte-kit/generated/client/app.js @@ -13,7 +13,11 @@ export const nodes = [ () => import('./nodes/9'), () => import('./nodes/10'), () => import('./nodes/11'), - () => import('./nodes/12') + () => import('./nodes/12'), + () => import('./nodes/13'), + () => import('./nodes/14'), + () => import('./nodes/15'), + () => import('./nodes/16') ]; export const server_loads = []; @@ -24,11 +28,15 @@ export const dictionary = { "/admin/attendance": [5,[2]], "/admin/courses": [6,[2]], "/admin/export": [7,[2]], - "/admin/notes": [8,[2]], - "/admin/rooms": [9,[2]], - "/admin/sessions": [10,[2]], - "/login": [11], - "/s/[code]": [12] + "/admin/live/[slotId]": [8,[2]], + "/admin/login": [9,[2]], + "/admin/notes": [10,[2]], + "/admin/rooms": [11,[2]], + "/admin/rooms/[roomId]": [12,[2]], + "/admin/sessions": [13,[2]], + "/admin/students": [14,[2]], + "/login": [15], + "/s/[code]": [16] }; export const hooks = { diff --git a/frontend/.svelte-kit/generated/client/nodes/10.js b/frontend/.svelte-kit/generated/client/nodes/10.js index 82fa30b..6e870a3 100644 --- a/frontend/.svelte-kit/generated/client/nodes/10.js +++ b/frontend/.svelte-kit/generated/client/nodes/10.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/admin/sessions/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/admin/notes/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/11.js b/frontend/.svelte-kit/generated/client/nodes/11.js index f2b26cd..210de29 100644 --- a/frontend/.svelte-kit/generated/client/nodes/11.js +++ b/frontend/.svelte-kit/generated/client/nodes/11.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/login/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/admin/rooms/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/12.js b/frontend/.svelte-kit/generated/client/nodes/12.js index 37be5c2..f206cf4 100644 --- a/frontend/.svelte-kit/generated/client/nodes/12.js +++ b/frontend/.svelte-kit/generated/client/nodes/12.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/s/[code]/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/admin/rooms/[roomId]/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/13.js b/frontend/.svelte-kit/generated/client/nodes/13.js new file mode 100644 index 0000000..82fa30b --- /dev/null +++ b/frontend/.svelte-kit/generated/client/nodes/13.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/admin/sessions/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/14.js b/frontend/.svelte-kit/generated/client/nodes/14.js new file mode 100644 index 0000000..30a9a59 --- /dev/null +++ b/frontend/.svelte-kit/generated/client/nodes/14.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/admin/students/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/15.js b/frontend/.svelte-kit/generated/client/nodes/15.js new file mode 100644 index 0000000..f2b26cd --- /dev/null +++ b/frontend/.svelte-kit/generated/client/nodes/15.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/login/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/16.js b/frontend/.svelte-kit/generated/client/nodes/16.js new file mode 100644 index 0000000..37be5c2 --- /dev/null +++ b/frontend/.svelte-kit/generated/client/nodes/16.js @@ -0,0 +1 @@ +export { default as component } from "../../../../src/routes/s/[code]/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/8.js b/frontend/.svelte-kit/generated/client/nodes/8.js index 6e870a3..42577b6 100644 --- a/frontend/.svelte-kit/generated/client/nodes/8.js +++ b/frontend/.svelte-kit/generated/client/nodes/8.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/admin/notes/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/admin/live/[slotId]/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/client/nodes/9.js b/frontend/.svelte-kit/generated/client/nodes/9.js index 210de29..973ae05 100644 --- a/frontend/.svelte-kit/generated/client/nodes/9.js +++ b/frontend/.svelte-kit/generated/client/nodes/9.js @@ -1 +1 @@ -export { default as component } from "../../../../src/routes/admin/rooms/+page.svelte"; \ No newline at end of file +export { default as component } from "../../../../src/routes/admin/login/+page.svelte"; \ No newline at end of file diff --git a/frontend/.svelte-kit/generated/server/internal.js b/frontend/.svelte-kit/generated/server/internal.js index 0ee50f9..89e97b5 100644 --- a/frontend/.svelte-kit/generated/server/internal.js +++ b/frontend/.svelte-kit/generated/server/internal.js @@ -22,10 +22,10 @@ export const options = { service_worker_options: undefined, server_error_boundaries: false, templates: { - app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\n\n", + app: ({ head, body, assets, nonce, env }) => "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t" + head + "\n\t\n\t\n\t\t
" + body + "
\n\t\n\n", error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t
\n\t\t\t" + status + "\n\t\t\t
\n\t\t\t\t

" + message + "

\n\t\t\t
\n\t\t
\n\t\n\n" }, - version_hash: "eq7x7d" + version_hash: "2cu8qm" }; export async function get_hooks() { diff --git a/frontend/.svelte-kit/non-ambient.d.ts b/frontend/.svelte-kit/non-ambient.d.ts index 49acb9d..fee462b 100644 --- a/frontend/.svelte-kit/non-ambient.d.ts +++ b/frontend/.svelte-kit/non-ambient.d.ts @@ -29,24 +29,31 @@ declare module "$app/types" { type MatcherParam = M extends (param : string) => param is (infer U extends string) ? U : string; export interface AppTypes { - RouteId(): "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/notes" | "/admin/rooms" | "/admin/sessions" | "/login" | "/s" | "/s/[code]"; + RouteId(): "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/live" | "/admin/live/[slotId]" | "/admin/login" | "/admin/notes" | "/admin/rooms" | "/admin/rooms/[roomId]" | "/admin/sessions" | "/admin/students" | "/login" | "/s" | "/s/[code]"; RouteParams(): { + "/admin/live/[slotId]": { slotId: string }; + "/admin/rooms/[roomId]": { roomId: string }; "/s/[code]": { code: string } }; LayoutParams(): { - "/": { code?: string }; - "/admin": Record; + "/": { slotId?: string; roomId?: string; code?: string }; + "/admin": { slotId?: string; roomId?: string }; "/admin/attendance": Record; "/admin/courses": Record; "/admin/export": Record; + "/admin/live": { slotId?: string }; + "/admin/live/[slotId]": { slotId: string }; + "/admin/login": Record; "/admin/notes": Record; - "/admin/rooms": Record; + "/admin/rooms": { roomId?: string }; + "/admin/rooms/[roomId]": { roomId: string }; "/admin/sessions": Record; + "/admin/students": Record; "/login": Record; "/s": { code?: string }; "/s/[code]": { code: string } }; - Pathname(): "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/notes" | "/admin/rooms" | "/admin/sessions" | "/login" | `/s/${string}` & {}; + Pathname(): "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | `/admin/live/${string}` & {} | "/admin/login" | "/admin/notes" | "/admin/rooms" | `/admin/rooms/${string}` & {} | "/admin/sessions" | "/admin/students" | "/login" | `/s/${string}` & {}; ResolvedPathname(): `${"" | `/${string}`}${ReturnType}`; Asset(): string & {}; } diff --git a/frontend/.svelte-kit/types/route_meta_data.json b/frontend/.svelte-kit/types/route_meta_data.json index 0358f33..9713c89 100644 --- a/frontend/.svelte-kit/types/route_meta_data.json +++ b/frontend/.svelte-kit/types/route_meta_data.json @@ -4,9 +4,13 @@ "/admin/attendance": [], "/admin/courses": [], "/admin/export": [], + "/admin/live/[slotId]": [], + "/admin/login": [], "/admin/notes": [], "/admin/rooms": [], + "/admin/rooms/[roomId]": [], "/admin/sessions": [], + "/admin/students": [], "/login": [], "/s/[code]": [] } \ No newline at end of file diff --git a/frontend/.svelte-kit/types/src/routes/$types.d.ts b/frontend/.svelte-kit/types/src/routes/$types.d.ts index 127b8cc..be4b408 100644 --- a/frontend/.svelte-kit/types/src/routes/$types.d.ts +++ b/frontend/.svelte-kit/types/src/routes/$types.d.ts @@ -11,8 +11,8 @@ type EnsureDefined = T extends null | undefined ? {} : T; type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; export type Snapshot = Kit.Snapshot; type PageParentData = EnsureDefined; -type LayoutRouteId = RouteId | "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/notes" | "/admin/rooms" | "/admin/sessions" | "/login" | "/s/[code]" | null -type LayoutParams = RouteParams & { code?: string } +type LayoutRouteId = RouteId | "/" | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/live/[slotId]" | "/admin/login" | "/admin/notes" | "/admin/rooms" | "/admin/rooms/[roomId]" | "/admin/sessions" | "/admin/students" | "/login" | "/s/[code]" | null +type LayoutParams = RouteParams & { slotId?: string; roomId?: string; code?: string } type LayoutParentData = EnsureDefined<{}>; export type PageServerData = null; diff --git a/frontend/.svelte-kit/types/src/routes/admin/$types.d.ts b/frontend/.svelte-kit/types/src/routes/admin/$types.d.ts index a103dff..1ba9cf0 100644 --- a/frontend/.svelte-kit/types/src/routes/admin/$types.d.ts +++ b/frontend/.svelte-kit/types/src/routes/admin/$types.d.ts @@ -11,8 +11,8 @@ type EnsureDefined = T extends null | undefined ? {} : T; type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; export type Snapshot = Kit.Snapshot; type PageParentData = Omit, keyof LayoutData> & EnsureDefined; -type LayoutRouteId = RouteId | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/notes" | "/admin/rooms" | "/admin/sessions" -type LayoutParams = RouteParams & { } +type LayoutRouteId = RouteId | "/admin" | "/admin/attendance" | "/admin/courses" | "/admin/export" | "/admin/live/[slotId]" | "/admin/login" | "/admin/notes" | "/admin/rooms" | "/admin/rooms/[roomId]" | "/admin/sessions" | "/admin/students" +type LayoutParams = RouteParams & { slotId?: string; roomId?: string } type LayoutParentData = EnsureDefined; export type PageServerData = null; diff --git a/frontend/.svelte-kit/types/src/routes/admin/live/[slotId]/$types.d.ts b/frontend/.svelte-kit/types/src/routes/admin/live/[slotId]/$types.d.ts new file mode 100644 index 0000000..fefc3a5 --- /dev/null +++ b/frontend/.svelte-kit/types/src/routes/admin/live/[slotId]/$types.d.ts @@ -0,0 +1,18 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +type MatcherParam = M extends (param : string) => param is (infer U extends string) ? U : string; +type RouteParams = { slotId: string }; +type RouteId = '/admin/live/[slotId]'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = Omit, keyof import('../../$types.js').LayoutData> & EnsureDefined; + +export type EntryGenerator = () => Promise> | Array; +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/frontend/.svelte-kit/types/src/routes/admin/login/$types.d.ts b/frontend/.svelte-kit/types/src/routes/admin/login/$types.d.ts new file mode 100644 index 0000000..7769d46 --- /dev/null +++ b/frontend/.svelte-kit/types/src/routes/admin/login/$types.d.ts @@ -0,0 +1,17 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +type MatcherParam = M extends (param : string) => param is (infer U extends string) ? U : string; +type RouteParams = { }; +type RouteId = '/admin/login'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = Omit, keyof import('../$types.js').LayoutData> & EnsureDefined; + +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/frontend/.svelte-kit/types/src/routes/admin/rooms/[roomId]/$types.d.ts b/frontend/.svelte-kit/types/src/routes/admin/rooms/[roomId]/$types.d.ts new file mode 100644 index 0000000..229e71f --- /dev/null +++ b/frontend/.svelte-kit/types/src/routes/admin/rooms/[roomId]/$types.d.ts @@ -0,0 +1,18 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +type MatcherParam = M extends (param : string) => param is (infer U extends string) ? U : string; +type RouteParams = { roomId: string }; +type RouteId = '/admin/rooms/[roomId]'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = Omit, keyof import('../../$types.js').LayoutData> & EnsureDefined; + +export type EntryGenerator = () => Promise> | Array; +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/frontend/.svelte-kit/types/src/routes/admin/students/$types.d.ts b/frontend/.svelte-kit/types/src/routes/admin/students/$types.d.ts new file mode 100644 index 0000000..32a7c13 --- /dev/null +++ b/frontend/.svelte-kit/types/src/routes/admin/students/$types.d.ts @@ -0,0 +1,17 @@ +import type * as Kit from '@sveltejs/kit'; + +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; +type MatcherParam = M extends (param : string) => param is (infer U extends string) ? U : string; +type RouteParams = { }; +type RouteId = '/admin/students'; +type MaybeWithVoid = {} extends T ? T | void : T; +export type RequiredKeys = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T]; +type OutputDataShape = MaybeWithVoid> & Partial> & Record> +type EnsureDefined = T extends null | undefined ? {} : T; +type OptionalUnion, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude]?: never } & U : never; +export type Snapshot = Kit.Snapshot; +type PageParentData = Omit, keyof import('../$types.js').LayoutData> & EnsureDefined; + +export type PageServerData = null; +export type PageData = Expand; +export type PageProps = { params: RouteParams; data: PageData } \ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index a9a128f..1d25b60 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,47 +1,9 @@ - -
-

FPTutor Attendance

-

Efficiently tracking attendance and student observations.

- - - - -
- - diff --git a/frontend/src/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte index 9f5d1c8..7bf33ac 100644 --- a/frontend/src/routes/admin/+layout.svelte +++ b/frontend/src/routes/admin/+layout.svelte @@ -5,13 +5,13 @@ onMount(() => { if (!$token) { - goto('/login'); + goto('/admin/login'); } }); function handleLogout() { logout(); - goto('/login'); + goto('/admin/login'); } diff --git a/frontend/src/routes/admin/live/[slotId]/+page.svelte b/frontend/src/routes/admin/live/[slotId]/+page.svelte new file mode 100644 index 0000000..e2bc8c9 --- /dev/null +++ b/frontend/src/routes/admin/live/[slotId]/+page.svelte @@ -0,0 +1,10 @@ + + +
+ Tutor:innen-Ansicht · Live +

Slot {slotId}

+
diff --git a/frontend/src/routes/admin/login/+page.svelte b/frontend/src/routes/admin/login/+page.svelte new file mode 100644 index 0000000..9fcc2fa --- /dev/null +++ b/frontend/src/routes/admin/login/+page.svelte @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/routes/admin/rooms/+page.svelte b/frontend/src/routes/admin/rooms/+page.svelte index b1481d9..0f70aad 100644 --- a/frontend/src/routes/admin/rooms/+page.svelte +++ b/frontend/src/routes/admin/rooms/+page.svelte @@ -1,205 +1,63 @@ -

Room Layouts

+
+
+ Räume +

Raumlayout-Editor

+
-
-
-

Rooms

-
- - -
- -
- {#each rooms as room} -
selectedRoomId = room.id} - > - {room.name} -
- {/each} -
+
+
+
Räume
+
{ e.preventDefault(); createRoom(); }} style="display:flex;gap:8px"> + + +
-
- {#if selectedRoom} -
-

Editing: {selectedRoom.name}

-
- - - - -
-
- -
- selectedElementId = el.id} - /> - -
-

Properties

- {#if selectedElement} -
- - -
-
- - -
-
- - -
- - {:else} -

Select an element to edit properties.

- {/if} -
-
- {:else} -

Select a room to edit its layout.

- {/if} -
+ {#if rooms.length === 0} +
+ Noch keine Räume angelegt. +
+ {:else} + + + + + + + + + {#each rooms as room, i} + + + + + {/each} + +
NameAktionen
{room.name} + Bearbeiten +
+ {/if} +
- - diff --git a/frontend/src/routes/admin/rooms/[roomId]/+page.svelte b/frontend/src/routes/admin/rooms/[roomId]/+page.svelte new file mode 100644 index 0000000..61ee361 --- /dev/null +++ b/frontend/src/routes/admin/rooms/[roomId]/+page.svelte @@ -0,0 +1,111 @@ + + +{#if room} +
+
+
+ Räume +

{room.name}

+
+
+ + + + +
+
+ +
+
+ { selectedElementId = el.id; }} + /> +
+ +
+
Auswahl
+ {#if selectedElement} +
+
+
Bezeichnung
+ +
+
+
+
Breite
+ +
+
+
Höhe
+ +
+
+ +
+ {:else} + Element auswählen + {/if} +
+
+
+{:else} +
+ Raum wird geladen… +
+{/if} diff --git a/frontend/src/routes/admin/students/+page.svelte b/frontend/src/routes/admin/students/+page.svelte new file mode 100644 index 0000000..b49f46f --- /dev/null +++ b/frontend/src/routes/admin/students/+page.svelte @@ -0,0 +1,8 @@ + + +
+ Studierende +

Studierende

+
From 60c871dec036b7c6fa8c9724ee54db3abfd23aea Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 18:55:44 +0200 Subject: [PATCH 06/11] feat(frontend): redesign dashboard, attendance, students, login with paper aesthetic --- frontend/src/routes/admin/+layout.svelte | 94 ++--- frontend/src/routes/admin/+page.svelte | 337 +++++++++++------- .../src/routes/admin/attendance/+page.svelte | 305 +++++++++++----- frontend/src/routes/admin/login/+page.svelte | 140 ++++---- .../src/routes/admin/students/+page.svelte | 158 +++++++- 5 files changed, 665 insertions(+), 369 deletions(-) diff --git a/frontend/src/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte index 7bf33ac..d141dbf 100644 --- a/frontend/src/routes/admin/+layout.svelte +++ b/frontend/src/routes/admin/+layout.svelte @@ -1,65 +1,45 @@ {#if $token} - - -
- -
+ + {#snippet children()} + + {/snippet} + {/if} - - diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 98791d3..801a9a3 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1,147 +1,214 @@ -

Admin Dashboard

+
-
- - -
- -{#if loading && sessions.length === 0} -

Loading sessions...

-{:else} -
- {#each sessions as session} -
-

Week {session.week_nr} - {session.date}

-
- {#if session.slots && session.slots.length > 0} - {#each session.slots as slot} -
-
- {slot.status} - {slot.start_time} - {slot.end_time} - {#if slot.code} - {slot.code} - {/if} -
-
- {#if slot.status === 'closed'} - - {:else if slot.status === 'open'} - - - - {:else if slot.status === 'locked'} - - - {/if} -
-
- {/each} - {:else} -

No slots scheduled.

- {/if} -
-
- {/each} + +
+
+
Dashboard
+
+ Diese Woche, Woche {String(currentWeekNr).padStart(2, '0')} +
+ {#if selectedCourse} +
+ {selectedCourse.name} · {selectedCourse.semester} +
+ {/if}
-{/if} +
+ {#if courses.length > 1} + + {/if} + + Export + +
+
- + +
+ 0 ? `Code: ${openSlots[0].code ?? '—'}` : 'Kein Slot offen'} + accent={openSlots.length > 0 ? 'var(--green)' : undefined} + /> + + + +
+ + +
+
+
+
Slots
+ +
+
Neueste zuerst
+
+ + {#if loading && slotRows.length === 0} +
+ Wird geladen… +
+ {:else if slotRows.length === 0} +
+ Noch keine Slots geplant. +
+ {:else} + + + + + + + + + + + + + {#each slotRows as { slot, session }, i} + + + + + + + + + {/each} + +
WocheDatumZeitStatusCodeAktionen
+ {weekLabel(session.week_nr)} + {session.date} + {slot.start_time}–{slot.end_time} + + {#if slot.code} + {slot.code} + {:else} + + {/if} + +
+ {#if slot.status === 'closed'} + + {:else if slot.status === 'open'} + {#if slot.code} + + {/if} + Anzeigen + + {:else if slot.status === 'locked'} + Anzeigen + + {/if} +
+
+ {/if} +
+ +
diff --git a/frontend/src/routes/admin/attendance/+page.svelte b/frontend/src/routes/admin/attendance/+page.svelte index f3a24e4..f6235f2 100644 --- a/frontend/src/routes/admin/attendance/+page.svelte +++ b/frontend/src/routes/admin/attendance/+page.svelte @@ -1,111 +1,220 @@ -

Attendance Matrix

+
-
- - - {#if sessions.length > 0} - - {/if} -
- -{#if data} -
- - - - - {#each data.slots as slot} - - {/each} - - - - {#each data.students as student} - - - {#each data.slots as slot} - {@const present = data.attendances.some(a => a.slot_id === slot.id && a.student_id === student.id)} - - {/each} - - {/each} - -
Student{slot.start_time}
{student.name} toggleAttendance(slot.id, student.id)}> - {present ? '✓' : ''} -
+ +
+
+
Anwesenheit
+
+ Kursmatrix · {selectedCourse?.semester ?? '—'} +
-{:else} -

Loading matrix...

-{/if} +
+ {#if courses.length > 1} + + {/if} + {#if selectedCourseId} + + CSV + + + Markdown + + + SQLite Backup + + {/if} +
+
- + +
+
+
+
Pro Studierende:r
+ +
+ {#if loading} + Wird geladen… + {/if} +
+ + {#if students.length === 0} +
+ Keine Studierenden gefunden. +
+ {:else} +
+ + + + + + {#each sessions as session, i} + + {/each} + + + + + + {#each students as student, i} + + + + {#each sessions as session} + {@const slotIds = (session.slots ?? []).map((sl: any) => sl.id)} + {@const sessionPresent = slotIds.some((sid: number) => isPresent(sid, student.id))} + + {/each} + + + + {/each} + +
#Studierende:r + W{String(session.week_nr).padStart(2, '0')} + AnwesendBonus
+ {i + 1} + +
+ + {initials(student.name)} + + {student.name} +
+
+ {#if slotIds.length > 0} + + {:else} + + {/if} + + {presentCount(student.id)} / {allSlotIds.length} + + {#if allSlotIds.length > 0} + + {#if bonusEligible(student.id)} + + {:else} + + {/if} + + {:else} + + {/if} +
+
+ {/if} +
+ +
diff --git a/frontend/src/routes/admin/login/+page.svelte b/frontend/src/routes/admin/login/+page.svelte index 9fcc2fa..ecea79a 100644 --- a/frontend/src/routes/admin/login/+page.svelte +++ b/frontend/src/routes/admin/login/+page.svelte @@ -1,82 +1,72 @@ - +
- + +
+
+ Tutor·manager +
+
Anwesenheit & Notizen für Tutorien.
+
+ + +
+
Anmeldung
+
Willkommen zurück
+ + +
{ e.preventDefault(); login(); }} style="margin-top:20px;display:flex;flex-direction:column;gap:12px"> +
+ + +
+ +
+ + +
+ + {#if error} +
+ {error} +
+ {/if} + + +
+ +
+ Nur für Tutor:innen. Studierende nutzen den Link der Tutor:in. +
+
+ +
~ Donnerstags ab 14 Uhr ~
+ +
diff --git a/frontend/src/routes/admin/students/+page.svelte b/frontend/src/routes/admin/students/+page.svelte index b49f46f..50a0f40 100644 --- a/frontend/src/routes/admin/students/+page.svelte +++ b/frontend/src/routes/admin/students/+page.svelte @@ -1,8 +1,158 @@ -
- Studierende -

Studierende

+
+ + +
+
+
Studierende
+
+ {selectedCourse?.name ?? 'Studierende'} + · {filtered.length} +
+
+
+ {#if courses.length > 1} + + {/if} + + +
+ + + + +
+ + + + + +
+ + +
+
+
+ + +
+ {#if filtered.length === 0} +
+ + {search ? 'Keine Treffer.' : 'Noch keine Studierenden.'} + +
+ {:else} + + + + + + + + + + {#each filtered as student, i} + + + + + + {/each} + +
#NameAktionen
+ {i + 1} + +
+ + {initials(student.name)} + + {student.name} +
+
+ +
+ {/if} +
+
From 0298e03781a8a406252ce0d8b2f69246108500df Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 19:15:39 +0200 Subject: [PATCH 07/11] feat(frontend): redesign sessions, courses, live view, and student check-in --- .../src/routes/admin/courses/+page.svelte | 263 ++++------ .../routes/admin/live/[slotId]/+page.svelte | 232 ++++++++- .../src/routes/admin/sessions/+page.svelte | 423 +++++++-------- frontend/src/routes/s/[code]/+page.svelte | 481 ++++++++++++++---- 4 files changed, 883 insertions(+), 516 deletions(-) diff --git a/frontend/src/routes/admin/courses/+page.svelte b/frontend/src/routes/admin/courses/+page.svelte index ed9f149..6d20a89 100644 --- a/frontend/src/routes/admin/courses/+page.svelte +++ b/frontend/src/routes/admin/courses/+page.svelte @@ -1,188 +1,97 @@ -

Courses & Students

+
-
-
-

Manage Courses

-
- - - -
- -
- {#each courses as course} -
selectedCourseId = course.id} - > - {course.name} ({course.semester}) -
- {/each} -
+ +
+
Verwaltung
+
+ Kurse + · {courses.length}
+
-
- {#if selectedCourseId} - {@const selectedCourse = courses.find(c => c.id === selectedCourseId)} -

Students in {selectedCourse?.name}

- -
-
- - -
- -
- Import CSV (name header): - -
-
- - - - - - - - - - - {#each students as student} - - - - - - {/each} - -
IDNameActions
{student.id}{student.name} - -
- {:else} -

Select a course to manage students.

- {/if} + +
+
+
Neuen Kurs anlegen
+
+
+
+ + +
+
+ + +
+ +
+
+ + +
+ {#if courses.length === 0} +
+ Noch keine Kurse vorhanden. +
+ {:else} + + + + + + + + + + + {#each courses as course, i} + + + + + + + {/each} + +
#NameSemesterAktionen
+ {i + 1} + {course.name} + {course.semester} + + +
+ {/if} +
+
- - diff --git a/frontend/src/routes/admin/live/[slotId]/+page.svelte b/frontend/src/routes/admin/live/[slotId]/+page.svelte index e2bc8c9..3283fe5 100644 --- a/frontend/src/routes/admin/live/[slotId]/+page.svelte +++ b/frontend/src/routes/admin/live/[slotId]/+page.svelte @@ -1,10 +1,234 @@ -
- Tutor:innen-Ansicht · Live -

Slot {slotId}

+
+ + {#if loading} +
+ Wird geladen… +
+ {:else if !slot || !session} +
+ Slot nicht gefunden. +
+ {:else} + +
+
+
Tutor:innen-Ansicht · Live
+
+ {weekLabel(session.week_nr)} · {session.date} +
+
+ {slot.start_time}–{slot.end_time} +
+
+
+ {#if slot.code} + {slot.code} + {/if} + + {#if slot.code} + + {/if} + {#if slot.status === 'closed'} + + {:else if slot.status === 'open'} + + {:else if slot.status === 'locked'} + + {/if} +
+
+ + +
+ + +
+
+
Sitzplan
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ + + {#if students.length > 0} +
+
+ Anwesenheit manuell +
+ + + {#each students as student, i} + {@const present = attendances.some((a: Attendance) => a.student_id === student.id)} + + + + + {/each} + +
+
+ + {student.name.split(' ').map((w: string) => w[0]).slice(0, 2).join('').toUpperCase()} + + {student.name} +
+
+ +
+
+ {/if} +
+ + +
+ { selectedStudentId = id; }} + weekNr={session.week_nr} + /> +
+
+ {/if} +
diff --git a/frontend/src/routes/admin/sessions/+page.svelte b/frontend/src/routes/admin/sessions/+page.svelte index bf4316a..ca1d714 100644 --- a/frontend/src/routes/admin/sessions/+page.svelte +++ b/frontend/src/routes/admin/sessions/+page.svelte @@ -1,241 +1,214 @@ -

Schedule Sessions & Slots

+
-
-
-
- - -
- -
-

Add Session

-
- - - -
-
- -
- {#each sessions as session} -
-
- Week {session.week_nr} ({session.date}) - -
-
- {#each session.slots || [] as slot} -
- {slot.start_time}-{slot.end_time} - -
- {/each} -
-
- {/each} -
+ +
+
+
Sitzungen
+
+ Planung + {#if selectedCourse} + · {selectedCourse.name} + {/if} +
- - {#if selectedSessionId} - + {#if courses.length > 1} + {/if} +
+ + +
+
+
Neue Sitzung anlegen
+ +
+
+
+ + +
+
+ + +
+ +
+
+ + + {#if sessions.length === 0} +
+ Noch keine Sitzungen angelegt. +
+ {:else} +
+ {#each sessions as session} +
+
+
+ W{String(session.week_nr).padStart(2, '0')} + {session.date} +
+ +
+ + {#if (session.slots ?? []).length === 0} +
+ Noch kein Slot für diese Sitzung. +
+ {:else} + + + {#each session.slots ?? [] as slot, i} + + + + + + + {/each} + +
+ {slot.start_time}–{slot.end_time} + + {#if slot.code} + {slot.code} + {:else} + + {/if} + +
+ {#if slot.status === 'open' || slot.status === 'locked'} + Anzeigen + {/if} + +
+
+ {/if} +
+ {/each} +
+ {/if}
- + +{#if selectedSessionId !== null} + {@const sess = sessions.find((s: Session) => s.id === selectedSessionId)} +
+
+
Neuer Slot
+
+ Woche {sess?.week_nr} · {sess?.date} +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+{/if} diff --git a/frontend/src/routes/s/[code]/+page.svelte b/frontend/src/routes/s/[code]/+page.svelte index 84e778e..80458a0 100644 --- a/frontend/src/routes/s/[code]/+page.svelte +++ b/frontend/src/routes/s/[code]/+page.svelte @@ -1,124 +1,385 @@ -
- {#if loading} -

Loading...

- {:else if error} -

{error}

- {:else if slot} -

Check-in: {slot.start_time} - {slot.end_time}

- - {#if slot.status === 'locked'} -

Check-in is currently locked by the tutor.

- {/if} +
- {#if !myAttendance && slot.status === 'open'} -
- - -
- {:else if myAttendance} -

You are checked in as {students.find(s => s.id === myAttendance.student_id)?.name || 'Student'}

- {/if} + {#if step === 'loading'} +
+ Wird geladen… +
-
-

Select a seat to check in:

- + {:else if step === 'error'} +
+
+
Fehler
+
{errorMsg}
+
+
+ + {:else if step === 'locked'} +
+
+
Anwesenheit
+
+ Erfassung abgeschlossen
- {/if} -
+
+
+ {#if myAttendance} +
✓ ANWESEND
+
Du wurdest als anwesend eingetragen.
+ {:else} +
Der Check-in-Link ist nicht mehr aktiv.
+
Wende dich an deine:n Tutor:in.
+ {/if} +
+ {#if slot} +
{slot.start_time}–{slot.end_time}
+ {/if} +
- + {:else if !isDesktop} + + + {#if step === 'name'} +
+
+
Check-in
+
Wer bist du?
+
Wähle deinen Namen aus der Liste.
+
+ + {#if errorMsg} +
+ {errorMsg} +
+ {/if} + + + +
+ {#each filteredStudents as student} + + {:else} +
+ Keine Treffer. +
+ {/each} +
+ + {#if slot} +
{slot.start_time}–{slot.end_time}
+ {/if} +
+ + {:else if step === 'seat' && selectedStudent} +
+
+
Hallo, {selectedStudent.name.split(' ')[0]} 👋
+
Wähle deinen Sitz
+
Tippe auf einen freien Platz.
+
+ + {#if errorMsg} +
+ {errorMsg} +
+ {/if} + +
+ checkin(seat.id)} + /> +
+ +
+ + Dein Platz + + + Belegt + + + Frei + +
+ + +
+ + {:else if step === 'confirmed' && (myAttendance || selectedStudent)} +
+
+
+ Du sitzt auf Platz {myAttendance?.seat_id ?? '—'} +
+
+ +
+
✓ ANWESEND
+
Eingecheckt um {myAttendance?.checked_in_at?.slice(11, 16) ?? '—'} Uhr
+
+ +
+ +
+ + +
+ {/if} + + {:else} + + + {#if step === 'name'} +
+
+
Check-in
+
Wer bist du?
+
Wähle deinen Namen aus der Liste.
+
+ + {#if errorMsg} +
+ {errorMsg} +
+ {/if} + + + +
+ {#each filteredStudents as student} + + {:else} +
+ Keine Treffer. +
+ {/each} +
+
+ + {:else if step === 'seat' && selectedStudent} +
+ +
+
+
Hallo, {selectedStudent.name.split(' ')[0]} 👋
+
Wähle deinen Sitz
+
+ + {#if errorMsg} +
+ {errorMsg} +
+ {/if} + + checkin(seat.id)} + /> + +
+ + Dein Platz + + + Belegt + + + Frei + +
+
+ + +
+
+
Sitzung
+ {#if slot} +
{slot.start_time}–{slot.end_time}
+ {/if} +
+
+ +
+
Anwesend
+
{attendances.length}
+
von {students.length} Studierenden
+
+ + +
+
+ + {:else if step === 'confirmed'} +
+ +
+
+
Eingecheckt
+
+ Du sitzt auf Platz {myAttendance?.seat_id ?? '—'} +
+
+ + + +
+ + +
+
+ + +
+
+
✓ ANWESEND
+
+ {myAttendance?.checked_in_at?.slice(11, 16) ?? '—'} Uhr +
+
+ + {#if slot} +
+
Sitzung
+
{slot.start_time}–{slot.end_time}
+
+ {/if} + +
+
Anwesende
+
{attendances.length} / {students.length}
+
+
+
+ {/if} + {/if} + +
From 74255f23acd457240d9a0cf2e53b9bfe653ccf17 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 19:16:06 +0200 Subject: [PATCH 08/11] chore(frontend): delete obsolete login and notes routes --- frontend/src/routes/admin/notes/+page.svelte | 172 ------------------- frontend/src/routes/login/+page.svelte | 82 --------- 2 files changed, 254 deletions(-) delete mode 100644 frontend/src/routes/admin/notes/+page.svelte delete mode 100644 frontend/src/routes/login/+page.svelte diff --git a/frontend/src/routes/admin/notes/+page.svelte b/frontend/src/routes/admin/notes/+page.svelte deleted file mode 100644 index 4787f71..0000000 --- a/frontend/src/routes/admin/notes/+page.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -

Seat Notes

- -
- - - - - -
- -
-
- -
- -
- {#if activeSeatId} - {@const studentName = studentNames[activeSeatId]} -

Note for {studentName || 'Empty Seat'}

- {#if studentName} - - - {:else} -

Select a seat occupied by a student to leave a note.

- {/if} - - {:else} -

Click a seat on the map to add or view notes.

- {/if} - -
-

Recent Notes

- {#each notes as note} -
- {students.find(s => s.id === note.student_id)?.name}: -

{note.content}

-
- {/each} -
-
-
- - diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte deleted file mode 100644 index 9fcc2fa..0000000 --- a/frontend/src/routes/login/+page.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - - From 19f2b5ae7ff80f01f0a6736c4008bb233fc6fe02 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 28 Apr 2026 19:22:30 +0200 Subject: [PATCH 09/11] fix(frontend): fix slot deprecation and a11y label warnings --- frontend/src/routes/admin/+layout.svelte | 5 +++- .../src/routes/admin/courses/+page.svelte | 8 +++---- .../src/routes/admin/sessions/+page.svelte | 24 +++++++++---------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/frontend/src/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte index d141dbf..1700bc5 100644 --- a/frontend/src/routes/admin/+layout.svelte +++ b/frontend/src/routes/admin/+layout.svelte @@ -1,4 +1,5 @@