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/lib/api.ts b/frontend/src/lib/api.ts
index 15924f4..ba6b19a 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -125,10 +125,10 @@ export const api = {
}
},
checkin: {
- getInfo: (code: string) => request(`/api/checkin/${code}`),
- getStudents: (code: string) => request(`/api/checkin/${code}/students`),
- post: (code: string, student_id: number, seat_id?: string) =>
- request('/api/checkin', {
+ getInfo: (code: string) => request(`/checkin/${code}`),
+ getStudents: (code: string) => request(`/checkin/${code}/students`),
+ post: (code: string, student_id: number, seat_id?: string) =>
+ request('/checkin', {
method: 'POST',
body: JSON.stringify({ code, student_id, seat_id })
}),
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/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}
+ onStudentSelect?.(s.id)}
+ >
+
+ {initials(s.name)}
+
+ {s.name}
+ {#if hasNote(s.id)}
+
+ {/if}
+ {checkinTime(s.id)}
+
+ {/each}
+ {#each absent as s}
+ onStudentSelect?.(s.id)}
+ >
+
+ {initials(s.name)}
+
+ {s.name}
+ —
+
+ {/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}
+ appendTag(tag)}
+ >+ {tag}
+ {/each}
+
+
+
+
+
+
+ {:else}
+
+ Sitz anklicken um Notiz zu schreiben
+
+ {/if}
+
+
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)}
+
onSeatClick?.(seat)}
+ >
+ {s.label}
+
+ {/each}
+
+
+
+
+
+
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/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 @@
+
+
+
+
+
+
+
+
+ Tutor· manager
+
+
v0.1 · Puchstein
+
+
+
+ {#if courseName}
+
+
Kurs
+
+
{courseName}
+
{semester}{weekday ? ` · ${weekday}s` : ''}
+
+
+ {/if}
+
+
+
+ Navigation
+ {#each navItems as item}
+ {@const active = isActive(item)}
+
+
+ {item.label}
+
+ {/each}
+
+
+
+
+
+
+
+ {tutorName ? initials(tutorName) : 'T'}
+
+
+
{tutorName || 'Tutor:in'}
+
{tutorEmail}
+
+
+
+
+
+
+ {@render children()}
+
+
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 @@
+
+
+
+
+
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 @@
+
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..120ba36 100644
--- a/frontend/src/routes/admin/+layout.svelte
+++ b/frontend/src/routes/admin/+layout.svelte
@@ -1,65 +1,50 @@
{#if $token}
-
-
-
-
-
-
-
+
+ {#snippet children()}
+ {@render children()}
+ {/snippet}
+
+{:else}
+ {@render children()}
{/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
+
-
- Select Course:
-
- {#each courses as course}
- {course.name} ({course.semester})
- {/each}
-
-
-
-{#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'}
- updateStatus(slot.id, 'open')}>Open
- {:else if slot.status === 'open'}
- copyLink(slot.code!)}>Copy Link
- updateStatus(slot.id, 'locked')}>Lock
- updateStatus(slot.id, 'closed')}>Close
- {:else if slot.status === 'locked'}
- updateStatus(slot.id, 'open')}>Unlock
- updateStatus(slot.id, 'closed')}>Close
- {/if}
-
-
- {/each}
- {:else}
-
No slots scheduled.
- {/if}
-
-
- {/each}
+
+
-
+
+
+ 0 ? `Code: ${openSlots[0].code ?? '—'}` : 'Kein Slot offen'}
+ accent={openSlots.length > 0 ? 'var(--green)' : undefined}
+ />
+
+
+
+
+
+
+
+
+
+ {#if loading && slotRows.length === 0}
+
+ Wird geladen…
+
+ {:else if slotRows.length === 0}
+
+ Noch keine Slots geplant.
+
+ {:else}
+
+
+
+ Woche
+ Datum
+ Zeit
+ Status
+ Code
+ Aktionen
+
+
+
+ {#each slotRows as { slot, session }, i}
+
+
+ {weekLabel(session.week_nr)}
+
+ {session.date}
+
+ {slot.start_time}–{slot.end_time}
+
+
+
+ {#if slot.code}
+ {slot.code}
+ {:else}
+ —
+ {/if}
+
+
+
+ {#if slot.status === 'closed'}
+
updateStatus(slot.id, 'open')}>
+ Öffnen
+
+ {:else if slot.status === 'open'}
+ {#if slot.code}
+
copyLink(slot.code!)}>
+ Kopieren
+
+ {/if}
+
Anzeigen
+
updateStatus(slot.id, 'locked')}>
+ Sperren
+
+ {:else if slot.status === 'locked'}
+
Anzeigen
+
updateStatus(slot.id, 'open')}>Öffnen
+ {/if}
+
+
+
+ {/each}
+
+
+ {/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
+
-
-
- {#each courses as course}
- {course.name}
- {/each}
-
-
- {#if sessions.length > 0}
-
- {#each sessions as session}
- Week {session.week_nr} ({session.date})
- {/each}
-
- {/if}
-
-
-{#if data}
-
-
-
-
- Student
- {#each data.slots as slot}
- {slot.start_time}
- {/each}
-
-
-
- {#each data.students as student}
-
- {student.name}
- {#each data.slots as slot}
- {@const present = data.attendances.some(a => a.slot_id === slot.id && a.student_id === student.id)}
- toggleAttendance(slot.id, student.id)}>
- {present ? '✓' : ''}
-
- {/each}
-
- {/each}
-
-
+
+
-
+
+
+
+
+ {#if loading}
+
Wird geladen…
+ {/if}
+
+
+ {#if students.length === 0}
+
+ Keine Studierenden gefunden.
+
+ {:else}
+
+
+
+
+ #
+ Studierende:r
+ {#each sessions as session, i}
+
+ W{String(session.week_nr).padStart(2, '0')}
+
+ {/each}
+ Anwesend
+ Bonus
+
+
+
+ {#each students as student, i}
+
+
+ {i + 1}
+
+
+
+
+ {initials(student.name)}
+
+ {student.name}
+
+
+ {#each sessions as session}
+ {@const slotIds = (session.slots ?? []).map((sl: any) => sl.id)}
+ {@const sessionPresent = slotIds.some((sid: number) => isPresent(sid, student.id))}
+
+ {#if slotIds.length > 0}
+ toggleAttendance(slotIds[0], student.id)}
+ title={sessionPresent ? 'Anwesend – klicken zum Entfernen' : 'Abwesend – klicken zum Eintragen'}
+ >
+ {#if sessionPresent}
+
+ {:else}
+ —
+ {/if}
+
+ {:else}
+ —
+ {/if}
+
+ {/each}
+
+ {presentCount(student.id)} / {allSlotIds.length}
+
+
+ {#if allSlotIds.length > 0}
+
+ {#if bonusEligible(student.id)}
+
+ {:else}
+ —
+ {/if}
+
+ {:else}
+ —
+ {/if}
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
diff --git a/frontend/src/routes/admin/courses/+page.svelte b/frontend/src/routes/admin/courses/+page.svelte
index ed9f149..d54fbb1 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}
-
+
+
-
- {#if selectedCourseId}
- {@const selectedCourse = courses.find(c => c.id === selectedCourseId)}
-
Students in {selectedCourse?.name}
-
-
-
-
-
- Import CSV (name header):
-
-
-
-
-
-
-
- ID
- Name
- Actions
-
-
-
- {#each students as student}
-
- {student.id}
- {student.name}
-
- deleteStudent(student.id)}>Delete
-
-
- {/each}
-
-
- {:else}
-
Select a course to manage students.
- {/if}
+
+
+
+
+
+ {#if courses.length === 0}
+
+ Noch keine Kurse vorhanden.
+
+ {:else}
+
+
+
+ #
+ Name
+ Semester
+ Aktionen
+
+
+
+ {#each courses as course, i}
+
+
+ {i + 1}
+
+ {course.name}
+
+ {course.semester}
+
+
+
+
+
+ {/each}
+
+
+ {/if}
+
+
-
-
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..3283fe5
--- /dev/null
+++ b/frontend/src/routes/admin/live/[slotId]/+page.svelte
@@ -0,0 +1,234 @@
+
+
+
+
+ {#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}
+ Kopieren
+ {/if}
+ {#if slot.status === 'closed'}
+ updateStatus('open')}> Öffnen
+ {:else if slot.status === 'open'}
+ updateStatus('locked')}> Sperren
+ {:else if slot.status === 'locked'}
+ updateStatus('open')}>Öffnen
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ selectedStudentId = null}>
+ Manuell eintragen
+
+
+
+
+
+ {#if students.length > 0}
+
+
+ Anwesenheit manuell
+
+
+
+ {#each students as student, i}
+ {@const present = attendances.some((a: Attendance) => a.student_id === student.id)}
+
+
+
+
+ {student.name.split(' ').map((w: string) => w[0]).slice(0, 2).join('').toUpperCase()}
+
+ {student.name}
+
+
+
+ toggleAttendance(student.id)}
+ >
+ {#if present}
+ Anwesend
+ {:else}
+ — Abwesend
+ {/if}
+
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+ { selectedStudentId = id; }}
+ weekNr={session.week_nr}
+ />
+
+
+ {/if}
+
+
diff --git a/frontend/src/routes/admin/login/+page.svelte b/frontend/src/routes/admin/login/+page.svelte
new file mode 100644
index 0000000..ecea79a
--- /dev/null
+++ b/frontend/src/routes/admin/login/+page.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+ Tutor· manager
+
+
Anwesenheit & Notizen für Tutorien.
+
+
+
+
+
Anmeldung
+
Willkommen zurück
+
+
+
+
+
+ Nur für Tutor:innen. Studierende nutzen den Link der Tutor:in.
+
+
+
+
~ Donnerstags ab 14 Uhr ~
+
+
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
-
-
-
- {#each courses as course}
- {course.name}
- {/each}
-
-
-
- {#each sessions as session}
- Week {session.week_nr}
- {/each}
-
-
-
- {#each slots as slot}
- {slot.start_time} - {slot.end_time}
- {/each}
-
-
-
-
-
-
-
-
-
- {#if activeSeatId}
- {@const studentName = studentNames[activeSeatId]}
-
Note for {studentName || 'Empty Seat'}
- {#if studentName}
-
-
Save Note
- {:else}
-
Select a seat occupied by a student to leave a note.
- {/if}
-
activeSeatId = null}>Cancel
- {: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/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}
-
+
+
-
- {#if selectedRoom}
-
-
-
-
selectedElementId = el.id}
- />
-
-
-
Properties
- {#if selectedElement}
-
- Label
-
-
-
- Width
-
-
-
- Height
-
-
-
Delete Element
- {: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}
+
+
+
+ Name
+ Aktionen
+
+
+
+ {#each rooms as room, i}
+
+ {room.name}
+
+ Bearbeiten
+
+
+ {/each}
+
+
+ {/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}
+
+
+ addElement('seat')}>+ Sitz
+ addElement('table')}>+ Tisch
+ addElement('door')}>+ Tür
+ Speichern
+
+
+
+
+
+ { selectedElementId = el.id; }}
+ />
+
+
+
+
Auswahl
+ {#if selectedElement}
+
+ {:else}
+
Element auswählen
+ {/if}
+
+
+
+{:else}
+
+ Raum wird geladen…
+
+{/if}
diff --git a/frontend/src/routes/admin/sessions/+page.svelte b/frontend/src/routes/admin/sessions/+page.svelte
index bf4316a..43e573d 100644
--- a/frontend/src/routes/admin/sessions/+page.svelte
+++ b/frontend/src/routes/admin/sessions/+page.svelte
@@ -1,241 +1,214 @@
-
Schedule Sessions & Slots
+
-
-
-
- Course:
-
- {#each courses as course}
- {course.name}
- {/each}
-
-
-
-
-
-
- {#each sessions as session}
-
-
-
- {#each session.slots || [] as slot}
-
- {slot.start_time}-{slot.end_time}
- deleteSlot(slot.id)}>×
-
- {/each}
-
-
- {/each}
-
+
+
+
+
+
+
+
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}
+
+
selectedSessionId = session.id}>
+ Slot hinzufügen
+
+
+
+ {#if (session.slots ?? []).length === 0}
+
+ Noch kein Slot für diese Sitzung.
+
+ {:else}
+
+
+ {#each session.slots ?? [] as slot, i}
+
+
+ {slot.start_time}–{slot.end_time}
+
+
+
+ {#if slot.code}
+ {slot.code}
+ {:else}
+ —
+ {/if}
+
+
+
+ {#if slot.status === 'open' || slot.status === 'locked'}
+
Anzeigen
+ {/if}
+
deleteSlot(slot.id)}
+ >
+
+
+
+ {/each}
+
+
+ {/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/admin/students/+page.svelte b/frontend/src/routes/admin/students/+page.svelte
new file mode 100644
index 0000000..50a0f40
--- /dev/null
+++ b/frontend/src/routes/admin/students/+page.svelte
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+ {#if filtered.length === 0}
+
+
+ {search ? 'Keine Treffer.' : 'Noch keine Studierenden.'}
+
+
+ {:else}
+
+
+
+ #
+ Name
+ Aktionen
+
+
+
+ {#each filtered as student, i}
+
+
+ {i + 1}
+
+
+
+
+ {initials(student.name)}
+
+ {student.name}
+
+
+
+ deleteStudent(student.id)}
+ >Entfernen
+
+
+ {/each}
+
+
+ {/if}
+
+
+
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 @@
-
-
-
-
-
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'}
-
- I am:
-
- Select your name...
- {#each students as student}
- {student.name}
- {/each}
-
-
- {: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'}
+
+
+ {: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}
+
+
+
+
+
+ {#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
+
+
+
+
{ step = 'name'; selectedStudent = null; }}>
+ ← Zurück
+
+
+
+ {:else if step === 'confirmed' && (myAttendance || selectedStudent)}
+
+
+
+ Du sitzt auf Platz {myAttendance?.seat_id ?? '—'}
+
+
+
+
+
✓ ANWESEND
+
Eingecheckt um {myAttendance?.checked_in_at?.slice(11, 16) ?? '—'} Uhr
+
+
+
+
+
+
+
+ Sitz wechseln
+
+
+ {/if}
+
+ {:else}
+
+
+ {#if step === 'name'}
+
+
+
Check-in
+
Wer bist du?
+
Wähle deinen Namen aus der Liste.
+
+
+ {#if errorMsg}
+
+ {errorMsg}
+
+ {/if}
+
+
+
+
+
+
+ {: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
+
+
+
{ step = 'name'; selectedStudent = null; }}>
+ ← Zurück
+
+
+
+
+ {:else if step === 'confirmed'}
+
+
+
+
+
Eingecheckt
+
+ Du sitzt auf Platz {myAttendance?.seat_id ?? '—'}
+
+
+
+
+
+
+ Sitz wechseln
+ window.print()}>Drucken
+
+
+
+
+
+
+
✓ 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}
+
+