Files
2026-04-29 04:38:26 +02:00

490 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// admin.jsx — Tutor admin shell + supporting screens (dashboard, attendance, rooms, students, login)
const TutorShell = ({ active, onNav, children }) => {
const navItems = [
{ id: "dashboard", label: "Dashboard" },
{ id: "live", label: "Live · Sitzplan" },
{ id: "attendance", label: "Anwesenheit" },
{ id: "rooms", label: "Räume" },
{ id: "students", label: "Studierende" },
];
return (
<div className="paper-bg" style={{ width: "100%", height: "100%", display: "grid", gridTemplateColumns: "220px 1fr", overflow: "hidden" }}>
{/* Sidebar */}
<aside style={{ borderRight: "1px solid var(--rule)", padding: "20px 18px", background: "rgba(0,0,0,0.015)", display: "flex", flexDirection: "column", gap: 18 }}>
<div>
<div className="serif" style={{ fontSize: 22, fontWeight: 500, letterSpacing: "-0.01em" }}>
Tutor<span style={{ color: "var(--accent)" }}>·</span>manager
</div>
<div className="tiny" style={{ marginTop: 2 }}>v0.1 · Puchstein</div>
</div>
<div>
<div className="eyebrow" style={{ marginBottom: 8 }}>Kurs</div>
<div style={{ padding: "10px 12px", background: "#fbf7ee", border: "1px solid var(--rule)", borderRadius: 4 }}>
<div className="serif" style={{ fontSize: 14, fontWeight: 500 }}>{COURSE.name}</div>
<div className="tiny" style={{ marginTop: 2 }}>{COURSE.semester} · {COURSE.weekday}s</div>
</div>
</div>
<nav style={{ display: "flex", flexDirection: "column", gap: 1 }}>
<div className="eyebrow" style={{ marginBottom: 6 }}>Navigation</div>
{navItems.map((item) => (
<button key={item.id}
onClick={() => onNav && onNav(item.id)}
style={{
textAlign: "left", border: "none", background: active === item.id ? "rgba(31,27,22,0.08)" : "transparent",
padding: "8px 10px", borderRadius: 4, cursor: "pointer",
fontFamily: "var(--sans)", fontSize: 13,
color: active === item.id ? "var(--ink)" : "var(--ink-2)",
fontWeight: active === item.id ? 500 : 400,
display: "flex", alignItems: "center", gap: 8,
}}>
<span style={{ width: 4, height: 4, borderRadius: "50%", background: active === item.id ? "var(--accent)" : "var(--ink-4)" }} />
{item.label}
</button>
))}
</nav>
<div style={{ flex: 1 }} />
<div style={{ borderTop: "1px solid var(--rule)", paddingTop: 14, display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ width: 28, height: 28, borderRadius: "50%", background: "var(--ink)", color: "var(--paper)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 600 }}>LP</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 500 }}>{COURSE.tutorin}</div>
<div className="tiny" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>lina@puchstein.lu</div>
</div>
</div>
</aside>
<main style={{ overflow: "auto" }}>{children}</main>
</div>
);
};
// Dashboard ─────────────────────────────────────────────────────────────
const Dashboard = () => {
const slots = [
{ id: 1, week: 4, day: "Do, 30. April", time: "14:00 15:00", room: "BC2 1.103", status: "open", code: "K7QJMX2P", checkedIn: 14, total: 19 },
{ id: 2, week: 4, day: "Do, 30. April", time: "17:00 18:00", room: "BC2 1.207", status: "closed", code: null, checkedIn: 0, total: 19 },
{ id: 3, week: 3, day: "Do, 23. April", time: "14:00 15:00", room: "BC2 1.103", status: "locked", code: "RX48ZF2K", checkedIn: 17, total: 19 },
{ id: 4, week: 3, day: "Do, 23. April", time: "17:00 18:00", room: "BC2 1.207", status: "locked", code: "QM9WJ3VC", checkedIn: 12, total: 19 },
{ id: 5, week: 2, day: "Do, 16. April", time: "14:00 15:00", room: "BC2 1.103", status: "locked", code: "B2HFNX9P", checkedIn: 18, total: 19 },
{ id: 6, week: 2, day: "Do, 16. April", time: "17:00 18:00", room: "BC2 1.207", status: "locked", code: "VJ5KQM7R", checkedIn: 15, total: 19 },
{ id: 7, week: 1, day: "Do, 09. April", time: "14:00 15:00", room: "— (Papier)", status: "locked", code: null, checkedIn: 16, total: 19 },
];
return (
<div style={{ padding: "28px 36px", display: "flex", flexDirection: "column", gap: 22 }}>
<header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 16 }}>
<div>
<div className="eyebrow">Dashboard</div>
<div className="serif" style={{ fontSize: 36, fontWeight: 500, letterSpacing: "-0.015em", marginTop: 2 }}>
Diese Woche, <span className="marker">Woche 04</span>
</div>
<div className="small" style={{ marginTop: 6 }}>2 Slots geplant · 1 läuft gerade · 14 von 19 sind eingecheckt.</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn ghost"><Icon.download /> Export</button>
<button className="btn"><Icon.plus /> Neuer Slot</button>
</div>
</header>
{/* Stat row */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14 }}>
<StatCard label="Anwesend gerade" value="14" suffix="/ 19" accent="var(--green)" hint="Slot K7QJ-MX2P" />
<StatCard label="Ø Anwesenheit" value="84%" hint="Über 4 Wochen" />
<StatCard label="Bonus vergeben" value="186" suffix="Punkte" hint="Saison gesamt" />
<StatCard label="Offene Notizen" value="7" hint="ohne Eintrag diese Woche" accent="var(--accent)" />
</div>
{/* Slots table */}
<section className="card" style={{ overflow: "hidden" }}>
<div style={{ padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: "1px solid var(--rule)" }}>
<div>
<div className="serif" style={{ fontSize: 18, fontWeight: 500 }}>Slots</div>
<UnderlineStroke width={50} />
</div>
<div className="small">Sortiert: neueste zuerst</div>
</div>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ background: "rgba(0,0,0,0.02)", color: "var(--ink-3)", textAlign: "left" }}>
<Th>Woche</Th>
<Th>Datum</Th>
<Th>Zeit</Th>
<Th>Raum</Th>
<Th>Status</Th>
<Th>Code</Th>
<Th>Eingecheckt</Th>
<Th style={{ textAlign: "right" }}>Aktionen</Th>
</tr>
</thead>
<tbody>
{slots.map((s, i) => (
<tr key={s.id} className="row-hover" style={{ borderTop: i === 0 ? "none" : "1px solid var(--rule)" }}>
<Td><span className="mono" style={{ fontSize: 12 }}>W{String(s.week).padStart(2, "0")}</span></Td>
<Td>{s.day}</Td>
<Td className="mono" style={{ fontSize: 12 }}>{s.time}</Td>
<Td>{s.room}</Td>
<Td><StatusPill status={s.status} /></Td>
<Td>{s.code ? <span className="mono" style={{ fontSize: 12, letterSpacing: "0.08em" }}>{s.code}</span> : <span className="tiny"></span>}</Td>
<Td>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontVariantNumeric: "tabular-nums" }}>{s.checkedIn} / {s.total}</span>
<div style={{ width: 48, height: 4, background: "var(--paper-2)", borderRadius: 2, overflow: "hidden" }}>
<div style={{ width: `${(s.checkedIn / s.total) * 100}%`, height: "100%", background: s.status === "locked" ? "var(--ink-3)" : "var(--accent)" }} />
</div>
</div>
</Td>
<Td style={{ textAlign: "right" }}>
{s.status === "open" && <button className="btn sm"><Icon.lock /> Sperren</button>}
{s.status === "closed" && <button className="btn sm"><Icon.open /> Öffnen</button>}
{s.status === "locked" && <button className="btn ghost sm">Anzeigen</button>}
</Td>
</tr>
))}
</tbody>
</table>
</section>
</div>
);
};
const Th = ({ children, style }) => <th style={{ padding: "10px 14px", fontFamily: "var(--mono)", fontSize: 10.5, letterSpacing: "0.1em", textTransform: "uppercase", fontWeight: 500, ...style }}>{children}</th>;
const Td = ({ children, style, className }) => <td className={className} style={{ padding: "12px 14px", ...style }}>{children}</td>;
const StatCard = ({ label, value, suffix, hint, accent }) => (
<div className="card" style={{ padding: "14px 16px" }}>
<div className="eyebrow">{label}</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 6, marginTop: 4 }}>
<span className="serif" style={{ fontSize: 32, fontWeight: 500, color: accent || "var(--ink)", letterSpacing: "-0.01em" }}>{value}</span>
{suffix && <span className="small">{suffix}</span>}
</div>
{hint && <div className="tiny" style={{ marginTop: 4 }}>{hint}</div>}
</div>
);
// Attendance matrix ─────────────────────────────────────────────────────────────
const AttendanceMatrix = () => {
const weeks = [1, 2, 3, 4];
// deterministic random-ish presence
const presence = STUDENTS.map((s) => ({
student: s,
weeks: weeks.map((w) => {
const hash = (s.id * 31 + w * 7) % 11;
return hash > 2; // ~80%
}),
}));
return (
<div style={{ padding: "28px 36px", display: "flex", flexDirection: "column", gap: 18 }}>
<header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between" }}>
<div>
<div className="eyebrow">Anwesenheit</div>
<div className="serif" style={{ fontSize: 32, fontWeight: 500, letterSpacing: "-0.015em", marginTop: 2 }}>
Kursmatrix · <span className="marker">SS 2026</span>
</div>
<div className="small" style={{ marginTop: 6 }}>Bonus = Anwesenheiten × 3 Punkte</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn ghost sm"><Icon.download /> CSV</button>
<button className="btn ghost sm"><Icon.download /> Markdown</button>
<button className="btn ghost sm"><Icon.download /> SQLite Backup</button>
</div>
</header>
<div className="card" style={{ overflow: "hidden" }}>
<div className="tabs" style={{ padding: "0 18px" }}>
<div className="tab active">Pro Studierende:r</div>
<div className="tab">Pro Woche</div>
<div className="tab">Notizen</div>
</div>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ background: "rgba(0,0,0,0.02)", color: "var(--ink-3)" }}>
<Th style={{ width: 40 }}>#</Th>
<Th>Studierende:r</Th>
{weeks.map((w) => (
<Th key={w} style={{ textAlign: "center" }}>W{String(w).padStart(2, "0")}</Th>
))}
<Th style={{ textAlign: "right" }}>Anwesend</Th>
<Th style={{ textAlign: "center", width: 60 }}>Bonus</Th>
<Th style={{ width: 32 }}></Th>
</tr>
</thead>
<tbody>
{presence.map((row, i) => {
const count = row.weeks.filter(Boolean).length;
return (
<tr key={row.student.id} className="row-hover" style={{ borderTop: "1px solid var(--rule-soft)" }}>
<Td className="mono tiny">{String(i + 1).padStart(2, "0")}</Td>
<Td>
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
<span style={{ width: 22, height: 22, borderRadius: "50%", background: "var(--paper-2)", color: "var(--ink-2)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 9, fontWeight: 600 }}>{row.student.initials}</span>
<span>{row.student.name}</span>
</div>
</Td>
{row.weeks.map((p, wi) => (
<Td key={wi} style={{ textAlign: "center" }}>
{p ? (
<span style={{ display: "inline-flex", width: 22, height: 22, borderRadius: 3, background: "rgba(74,107,58,0.14)", color: "var(--green)", alignItems: "center", justifyContent: "center" }}>
<Icon.check />
</span>
) : (
<span style={{ display: "inline-flex", width: 22, height: 22, borderRadius: 3, color: "var(--ink-4)", alignItems: "center", justifyContent: "center", fontSize: 13 }}></span>
)}
</Td>
))}
<Td style={{ textAlign: "right", fontVariantNumeric: "tabular-nums" }}>{count} / {weeks.length}</Td>
<Td style={{ textAlign: "center" }}>
{count > 0 ? (
<span style={{ display: "inline-flex", width: 24, height: 24, borderRadius: "50%", background: "rgba(138,44,31,0.08)", color: "var(--accent)", alignItems: "center", justifyContent: "center" }}>
<Icon.check />
</span>
) : (
<span className="tiny"></span>
)}
</Td>
<Td><Icon.arrow style={{ color: "var(--ink-4)" }} /></Td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
// Rooms / layout editor ─────────────────────────────────────────────────────────────
const RoomsScreen = () => {
return (
<div style={{ padding: "28px 36px", display: "flex", flexDirection: "column", gap: 18 }}>
<header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between" }}>
<div>
<div className="eyebrow">Räume</div>
<div className="serif" style={{ fontSize: 32, fontWeight: 500, letterSpacing: "-0.015em", marginTop: 2 }}>
Layout-Editor · <span className="serif" style={{ fontStyle: "italic" }}>{ROOM.name}</span>
</div>
<div className="small" style={{ marginTop: 6 }}>Räume sind kursunabhängig einmal anlegen, mehrere Semester nutzen.</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button className="btn ghost sm">Als Vorlage speichern</button>
<button className="btn"><Icon.check /> Speichern</button>
</div>
</header>
<div style={{ display: "grid", gridTemplateColumns: "240px 1fr", gap: 18 }}>
{/* Tool palette */}
<div className="card" style={{ padding: 16, display: "flex", flexDirection: "column", gap: 12, height: "fit-content" }}>
<div>
<div className="eyebrow">Werkzeuge</div>
<div style={{ marginTop: 8, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
<ToolBtn label="Sitz" active />
<ToolBtn label="Tisch" />
<ToolBtn label="Tür" />
<ToolBtn label="Fenster" />
<ToolBtn label="Lücke" />
<ToolBtn label="Beamer" />
</div>
</div>
<div className="div-h" />
<div>
<div className="eyebrow">Eigenschaften</div>
<div style={{ marginTop: 8, display: "flex", flexDirection: "column", gap: 8 }}>
<Field label="Bezeichnung" value="T2-3" />
<Field label="Tisch" value="T2" />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
<Field label="X" value="540" mono />
<Field label="Y" value="194" mono />
</div>
</div>
</div>
<div className="div-h" />
<div>
<div className="eyebrow">Räume</div>
<div style={{ marginTop: 8, display: "flex", flexDirection: "column", gap: 4 }}>
<RoomItem label="BC2 1.103" sub="20 Sitze · 4 Tische" active />
<RoomItem label="BC2 1.207" sub="16 Sitze · 4 Tische" />
<RoomItem label="Lib 0.04" sub="12 Sitze · 3 Tische" />
<button className="btn ghost sm" style={{ marginTop: 6, justifyContent: "center" }}><Icon.plus /> Neuer Raum</button>
</div>
</div>
</div>
{/* Editor canvas with seat being dragged */}
<div className="card" style={{ padding: 18, position: "relative" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
<div className="serif" style={{ fontSize: 16, fontWeight: 500 }}>Bearbeiten</div>
<div className="tiny mono">760 × 460 · 1× Zoom</div>
</div>
<div style={{ display: "flex", justifyContent: "center" }}>
<SeatMap variant="tutor" assignments={{}} scale={0.78} />
</div>
<div className="handwritten" style={{ position: "absolute", top: 90, right: 90, transform: "rotate(-6deg)", maxWidth: 130, lineHeight: 1.1 }}>
ziehen um Sitz zu verschieben
</div>
</div>
</div>
</div>
);
};
const ToolBtn = ({ label, active }) => (
<button style={{
padding: "10px 8px", borderRadius: 4, cursor: "pointer",
border: `1px solid ${active ? "var(--ink)" : "var(--rule)"}`,
background: active ? "var(--ink)" : "#fbf7ee",
color: active ? "var(--paper)" : "var(--ink-2)",
fontFamily: "var(--sans)", fontSize: 12, fontWeight: 500,
}}>{label}</button>
);
const Field = ({ label, value, mono }) => (
<label style={{ display: "flex", flexDirection: "column", gap: 3 }}>
<span className="tiny">{label}</span>
<input className="input" defaultValue={value} style={{ fontFamily: mono ? "var(--mono)" : "var(--sans)", fontSize: 12 }} />
</label>
);
const RoomItem = ({ label, sub, active }) => (
<button style={{
border: "none", background: active ? "rgba(31,27,22,0.06)" : "transparent",
textAlign: "left", padding: "7px 9px", borderRadius: 4, cursor: "pointer",
borderLeft: `3px solid ${active ? "var(--accent)" : "transparent"}`,
}}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{label}</div>
<div className="tiny">{sub}</div>
</button>
);
// Students CRUD ─────────────────────────────────────────────────────────────
const StudentsScreen = () => {
return (
<div style={{ padding: "28px 36px", display: "flex", flexDirection: "column", gap: 18 }}>
<header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between" }}>
<div>
<div className="eyebrow">Studierende</div>
<div className="serif" style={{ fontSize: 32, fontWeight: 500, letterSpacing: "-0.015em", marginTop: 2 }}>
{STUDENTS.length} Studierende · {COURSE.name}
</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", border: "1px solid var(--rule)", borderRadius: 4, background: "#fbf7ee" }}>
<Icon.search style={{ color: "var(--ink-3)" }} />
<input placeholder="Suchen…" style={{ border: "none", background: "transparent", outline: "none", fontSize: 13, width: 160 }} />
</div>
<button className="btn ghost sm">CSV importieren</button>
<button className="btn"><Icon.plus /> Hinzufügen</button>
</div>
</header>
<div className="card" style={{ overflow: "hidden" }}>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ background: "rgba(0,0,0,0.02)", color: "var(--ink-3)" }}>
<Th style={{ width: 40 }}>#</Th>
<Th>Name</Th>
<Th>Anwesend</Th>
<Th>Bonus</Th>
<Th>Notizen</Th>
<Th>Letzte Sitzung</Th>
<Th></Th>
</tr>
</thead>
<tbody>
{STUDENTS.map((s, i) => {
const count = (s.id * 7) % 5;
const noteCount = (s.id * 3) % 4;
return (
<tr key={s.id} className="row-hover" style={{ borderTop: "1px solid var(--rule-soft)" }}>
<Td className="mono tiny">{String(i + 1).padStart(2, "0")}</Td>
<Td>
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
<span style={{ width: 24, height: 24, borderRadius: "50%", background: "var(--paper-2)", color: "var(--ink-2)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 600 }}>{s.initials}</span>
<span>{s.name}</span>
</div>
</Td>
<Td><span style={{ fontVariantNumeric: "tabular-nums" }}>{count} / 4</span></Td>
<Td>
{count > 0 ? (
<span style={{ display: "inline-flex", width: 22, height: 22, borderRadius: "50%", background: "rgba(138,44,31,0.08)", color: "var(--accent)", alignItems: "center", justifyContent: "center" }}>
<Icon.check />
</span>
) : (
<span className="tiny"></span>
)}
</Td>
<Td>
{noteCount > 0 ? (
<span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--accent)" }} />
<span>{noteCount} Notizen</span>
</span>
) : (
<span className="tiny"></span>
)}
</Td>
<Td className="tiny">Do, 23. April · T{(s.id % 4) + 1}-{(s.id % 5) + 1}</Td>
<Td><Icon.arrow style={{ color: "var(--ink-4)" }} /></Td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
// Login ─────────────────────────────────────────────────────────────
const LoginScreen = () => {
return (
<div className="paper-bg" style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", padding: 40 }}>
<div style={{ width: 420 }}>
<div className="serif" style={{ fontSize: 38, fontWeight: 500, letterSpacing: "-0.015em" }}>
Tutor<span style={{ color: "var(--accent)" }}>·</span>manager
</div>
<div className="body" style={{ marginTop: 6, color: "var(--ink-3)" }}>
Anwesenheit & Notizen für Tutorien.
</div>
<div className="card" style={{ marginTop: 28, padding: 26 }}>
<div className="eyebrow">Anmeldung</div>
<div className="serif" style={{ fontSize: 22, fontWeight: 500, marginTop: 4 }}>Willkommen zurück</div>
<UnderlineStroke width={120} />
<label style={{ display: "flex", flexDirection: "column", gap: 5, marginTop: 22 }}>
<span className="tiny">E-Mail</span>
<input className="input" defaultValue="lina@puchstein.lu" />
</label>
<label style={{ display: "flex", flexDirection: "column", gap: 5, marginTop: 14 }}>
<span className="tiny">Passwort</span>
<input type="password" className="input" defaultValue="••••••••••" />
</label>
<button className="btn" style={{ width: "100%", justifyContent: "center", marginTop: 22, padding: "11px 14px" }}>
Anmelden
</button>
<div className="tiny" style={{ marginTop: 16, textAlign: "center", color: "var(--ink-3)" }}>
Nur für Tutor:innen. Studierende nutzen den vom Beamer projizierten Code.
</div>
</div>
<div className="handwritten" style={{ marginTop: 18, textAlign: "center" }}>
~ Donnerstags ab 14 Uhr ~
</div>
</div>
</div>
);
};
Object.assign(window, { TutorShell, Dashboard, AttendanceMatrix, RoomsScreen, StudentsScreen, LoginScreen });