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} + + {/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/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 + + + +
+
+ +
+
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 @@ + + +
+ + +
+ {@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

+
-
- - -
- -{#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/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} -
+ +
+
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 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} + + {/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/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
+ + +
{ 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/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/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/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

+
-
-
-
- - -
- -
-

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/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 @@ + + +
+ + +
+
+
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} +
+ +
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'} -
- - -
- {: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} + +