1968 lines
84 KiB
HTML
1968 lines
84 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>IFB · INFO1 Dashboard</title>
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--blue: #2563eb; --blue-d: #1d4ed8;
|
||
--blue-50: #eff6ff; --blue-100: #dbeafe;
|
||
--n-0: #ffffff;
|
||
--n-50: #f9fafb; --n-100: #f3f4f6; --n-150: #eaecf0;
|
||
--n-200: #e5e7eb; --n-300: #d1d5db; --n-400: #9ca3af;
|
||
--n-500: #6b7280; --n-600: #4b5563; --n-700: #374151;
|
||
--n-800: #1f2937; --n-900: #111827;
|
||
--green: #16a34a; --green-50: #f0fdf4;
|
||
--red: #dc2626; --red-50: #fef2f2;
|
||
--amber: #d97706; --amber-50: #fffbeb;
|
||
--bg: #f4f6f9;
|
||
--surface: #ffffff;
|
||
--surface-2: #f9fafb;
|
||
--border: #e5e7eb;
|
||
--border-subtle: #f0f2f5;
|
||
--text: #111827;
|
||
--text-2: #374151;
|
||
--text-muted: #6b7280;
|
||
--text-subtle: #9ca3af;
|
||
--shadow-xs: 0 1px 2px rgba(0,0,0,.04);
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
||
--shadow: 0 2px 8px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
||
--shadow-lg: 0 8px 24px rgba(0,0,0,.08), 0 2px 6px rgba(0,0,0,.04);
|
||
--shadow-xl: 0 20px 40px rgba(0,0,0,.1), 0 4px 8px rgba(0,0,0,.04);
|
||
--r-xs: 4px; --r-sm: 6px; --r: 10px; --r-lg: 12px; --r-xl: 16px;
|
||
}
|
||
|
||
body.dark {
|
||
--blue-50: #172554; --blue-100: #1e3a8a;
|
||
--n-50: #1a1f2e; --n-100: #202637; --n-150: #262d3f;
|
||
--n-200: #2d3548; --n-300: #3a4459; --n-400: #5d6d84;
|
||
--n-500: #7e90a8; --n-600: #9dafC5; --n-700: #b8cad9;
|
||
--n-800: #d4e0ec; --n-900: #eaf0f8;
|
||
--green-50: #052e16; --red-50: #300; --amber-50: #2d1a00;
|
||
--bg: #141820;
|
||
--surface: #1a1f2e;
|
||
--surface-2: #202637;
|
||
--border: #2d3548;
|
||
--border-subtle: #262d3f;
|
||
--text: #eaf0f8; --text-2: #b8cad9;
|
||
--text-muted: #7e90a8; --text-subtle: #5d6d84;
|
||
--shadow-xs: 0 1px 2px rgba(0,0,0,.25);
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,.3), 0 1px 2px rgba(0,0,0,.2);
|
||
--shadow: 0 2px 8px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.2);
|
||
--shadow-lg: 0 8px 24px rgba(0,0,0,.45), 0 2px 6px rgba(0,0,0,.25);
|
||
--shadow-xl: 0 20px 40px rgba(0,0,0,.55), 0 4px 8px rgba(0,0,0,.3);
|
||
}
|
||
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
/* ── HEADER ──────────────────────────────────────────────── */
|
||
|
||
header {
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 0 20px;
|
||
height: 54px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
text-decoration: none;
|
||
user-select: none;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand-mark {
|
||
width: 30px;
|
||
height: 30px;
|
||
background: var(--blue);
|
||
border-radius: 7px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
letter-spacing: -.5px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
|
||
.brand-sub { font-size: 10px; color: var(--text-subtle); font-weight: 500; letter-spacing: .2px; }
|
||
.brand-name { font-size: 14px; font-weight: 700; color: var(--text); letter-spacing: -.3px; }
|
||
|
||
.h-sep { width: 1px; height: 18px; background: var(--border); margin: 0 14px; flex-shrink: 0; }
|
||
.h-spacer { flex: 1; }
|
||
.h-right { display: flex; align-items: center; gap: 8px; }
|
||
|
||
#weather {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
padding: 3px 9px;
|
||
height: 28px;
|
||
}
|
||
|
||
#clock {
|
||
font-size: 12px;
|
||
color: var(--text-subtle);
|
||
font-weight: 500;
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.h-icon-btn {
|
||
width: 30px; height: 30px;
|
||
border-radius: var(--r-sm);
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 15px;
|
||
color: var(--text-muted);
|
||
transition: background .12s;
|
||
font-family: inherit;
|
||
}
|
||
.h-icon-btn:hover { background: var(--n-100); }
|
||
body.dark #btn-dark { color: #f1c40f; }
|
||
|
||
.h-btn {
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
||
height: 30px; padding: 0 12px;
|
||
border-radius: var(--r-sm);
|
||
font-size: 12px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer;
|
||
transition: background .1s, color .1s, border-color .1s;
|
||
text-decoration: none; white-space: nowrap;
|
||
border: 1px solid var(--border);
|
||
background: none; color: var(--text-muted);
|
||
}
|
||
.h-btn:hover { background: var(--n-100); color: var(--text); }
|
||
.h-btn-primary { background: var(--blue); color: #fff; border-color: var(--blue); }
|
||
.h-btn-primary:hover { background: var(--blue-d); border-color: var(--blue-d); color: #fff; }
|
||
|
||
/* Legacy class aliases */
|
||
.btn-sm { display: inline-flex; align-items: center; gap: 4px; height: 30px; padding: 0 12px; border-radius: var(--r-sm); font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer; transition: background .1s; text-decoration: none; white-space: nowrap; border: 1px solid var(--border); background: none; color: var(--text-muted); }
|
||
.btn-sm:hover { background: var(--n-100); color: var(--text); }
|
||
.btn-primary-sm { background: var(--blue); color: #fff; border-color: var(--blue); }
|
||
.btn-primary-sm:hover { background: var(--blue-d); border-color: var(--blue-d); }
|
||
|
||
.avatar {
|
||
width: 30px; height: 30px;
|
||
border-radius: 50%;
|
||
background: var(--blue);
|
||
color: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-weight: 600; font-size: 12px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
user-select: none;
|
||
transition: opacity .12s;
|
||
}
|
||
.avatar:hover { opacity: .85; }
|
||
|
||
.dropdown {
|
||
position: absolute;
|
||
top: calc(100% + 6px);
|
||
right: 0;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-lg);
|
||
box-shadow: var(--shadow-lg);
|
||
min-width: 190px;
|
||
overflow: hidden;
|
||
display: none;
|
||
z-index: 200;
|
||
padding: 5px;
|
||
}
|
||
.dropdown.open { display: block; }
|
||
.dd-item {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 7px 10px;
|
||
font-size: 13px; font-weight: 500;
|
||
cursor: pointer;
|
||
color: var(--text-2);
|
||
border-radius: var(--r-sm);
|
||
transition: background .1s;
|
||
text-decoration: none;
|
||
user-select: none;
|
||
}
|
||
.dd-item:hover { background: var(--n-100); }
|
||
.dd-item.danger { color: var(--red); }
|
||
.dd-item.danger:hover { background: var(--red-50); }
|
||
.dd-item.meta { color: var(--text-subtle); font-size: 11px; cursor: default; }
|
||
.dd-item.meta:hover { background: none; }
|
||
.dd-sep { height: 1px; background: var(--border-subtle); margin: 4px 0; }
|
||
|
||
/* ── BANNER ──────────────────────────────────────────────── */
|
||
|
||
#banner {
|
||
background: var(--blue);
|
||
padding: 0 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
flex-wrap: wrap;
|
||
min-height: 44px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.banner-label { font-size: 13px; font-weight: 500; color: rgba(255,255,255,.92); white-space: nowrap; }
|
||
.banner-chips { display: flex; gap: 5px; flex-wrap: wrap; flex: 1; }
|
||
.chip {
|
||
background: rgba(255,255,255,.15);
|
||
border: 1px solid rgba(255,255,255,.2);
|
||
border-radius: var(--r-sm);
|
||
padding: 2px 8px;
|
||
font-size: 11px; font-weight: 500;
|
||
color: rgba(255,255,255,.9);
|
||
}
|
||
.banner-cta {
|
||
background: rgba(255,255,255,.95);
|
||
color: var(--blue);
|
||
font-size: 12px; font-weight: 600;
|
||
border: none;
|
||
padding: 6px 14px;
|
||
border-radius: var(--r-sm);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
font-family: inherit;
|
||
flex-shrink: 0;
|
||
transition: background .12s;
|
||
}
|
||
.banner-cta:hover { background: #fff; }
|
||
|
||
/* ── MAIN LAYOUT ─────────────────────────────────────────── */
|
||
|
||
main {
|
||
flex: 1;
|
||
width: 100%;
|
||
max-width: 1380px;
|
||
margin: 0 auto;
|
||
padding: 20px 20px 28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 296px;
|
||
gap: 16px;
|
||
align-items: start;
|
||
}
|
||
|
||
.col-primary {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.col-secondary {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-pair {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
align-items: start;
|
||
}
|
||
|
||
/* ── CARD ────────────────────────────────────────────────── */
|
||
|
||
.card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-lg);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-xs);
|
||
}
|
||
|
||
.card-head {
|
||
padding: 12px 16px 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
display: flex; align-items: center; gap: 6px;
|
||
line-height: 1;
|
||
text-transform: uppercase;
|
||
letter-spacing: .4px;
|
||
}
|
||
.card-title .ci { font-size: 13px; }
|
||
|
||
.card-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||
|
||
.card-body { padding: 14px 16px; }
|
||
.card-body-flush { padding: 0; }
|
||
|
||
/* Card action buttons */
|
||
.add-btn {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
height: 26px; padding: 0 10px;
|
||
border-radius: var(--r-sm);
|
||
font-size: 11px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer;
|
||
background: var(--blue); color: #fff;
|
||
border: 1px solid var(--blue);
|
||
transition: background .1s;
|
||
white-space: nowrap;
|
||
}
|
||
.add-btn:hover { background: var(--blue-d); border-color: var(--blue-d); }
|
||
|
||
.print-btn {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
height: 26px; padding: 0 10px;
|
||
border-radius: var(--r-sm);
|
||
font-size: 11px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer;
|
||
background: none; color: var(--text-muted);
|
||
border: 1px solid var(--border);
|
||
transition: background .1s;
|
||
}
|
||
.print-btn:hover { background: var(--n-100); color: var(--text); }
|
||
|
||
/* ── SKELETON ────────────────────────────────────────────── */
|
||
|
||
.sk {
|
||
background: linear-gradient(90deg, var(--n-100) 25%, var(--n-150) 50%, var(--n-100) 75%);
|
||
background-size: 200%;
|
||
animation: shimmer 1.6s infinite;
|
||
border-radius: var(--r-xs);
|
||
}
|
||
@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
|
||
.sk-line { height: 12px; margin-bottom: 8px; }
|
||
.sk-line.short { width: 55%; }
|
||
|
||
/* ── TIMETABLE ───────────────────────────────────────────── */
|
||
|
||
.tt-wrap { overflow-x: auto; }
|
||
.tt-grid {
|
||
display: grid;
|
||
grid-template-columns: 52px repeat(5, 1fr);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r-sm);
|
||
overflow: hidden;
|
||
min-width: 380px;
|
||
}
|
||
.tt-cell {
|
||
border-right: 1px solid var(--border-subtle);
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
min-height: 52px;
|
||
position: relative;
|
||
}
|
||
.tt-cell:last-child, .tt-cell.no-right { border-right: none; }
|
||
.tt-cell.no-bot { border-bottom: none; }
|
||
.tt-head {
|
||
background: var(--surface-2);
|
||
padding: 7px 5px;
|
||
font-size: 10px; font-weight: 700;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
letter-spacing: .5px; text-transform: uppercase;
|
||
min-height: auto;
|
||
}
|
||
.tt-head.today-col { color: var(--blue); background: var(--blue-50); }
|
||
.tt-time {
|
||
background: var(--surface-2);
|
||
padding: 5px 7px;
|
||
display: flex; flex-direction: column; justify-content: center; gap: 1px;
|
||
}
|
||
.tt-time-num { font-size: 11px; font-weight: 600; color: var(--text-muted); }
|
||
.tt-time-val { font-size: 9px; color: var(--text-subtle); }
|
||
.tt-lesson {
|
||
position: absolute; inset: 3px;
|
||
border-radius: 5px; padding: 5px 7px;
|
||
overflow: hidden;
|
||
border-left: 2.5px solid transparent;
|
||
}
|
||
.tt-lesson-subj { font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.tt-lesson-meta { font-size: 9px; opacity: .7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
|
||
.tt-del {
|
||
position: absolute; top: 3px; right: 4px;
|
||
font-size: 11px; cursor: pointer;
|
||
opacity: 0;
|
||
border: none; background: none; font-family: inherit; color: inherit; padding: 0; line-height: 1;
|
||
}
|
||
.tt-lesson:hover .tt-del { opacity: .5; }
|
||
.tt-lesson:hover .tt-del:hover { opacity: 1; }
|
||
.tt-pending { text-align: center; font-size: 13px; color: var(--text-subtle); padding: 28px 0; font-weight: 500; }
|
||
|
||
/* Placeholder TT */
|
||
.ph-tt { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
||
.ph-col { display: flex; flex-direction: column; gap: 4px; }
|
||
.ph-head { text-align: center; font-size: 10px; font-weight: 700; color: var(--text-muted); padding: 4px 0; text-transform: uppercase; letter-spacing: .5px; }
|
||
.ph-head.today { color: var(--blue); }
|
||
.ph-slot { height: 52px; border-radius: 7px; }
|
||
|
||
/* ── EVENTS ──────────────────────────────────────────────── */
|
||
|
||
.ev-item {
|
||
display: flex; align-items: center; gap: 11px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.ev-item:last-child { border-bottom: none; }
|
||
.ev-days {
|
||
font-size: 18px; font-weight: 800; min-width: 36px; text-align: center;
|
||
letter-spacing: -1px; font-variant-numeric: tabular-nums; line-height: 1;
|
||
}
|
||
.ev-info { flex: 1; min-width: 0; }
|
||
.ev-title { font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; color: var(--text); }
|
||
.ev-date { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
||
.ev-del {
|
||
color: var(--text-subtle); cursor: pointer;
|
||
font-size: 12px; border: none; background: none;
|
||
padding: 4px; border-radius: var(--r-sm); flex-shrink: 0;
|
||
opacity: 0; transition: opacity .1s, color .1s;
|
||
}
|
||
.ev-item:hover .ev-del { opacity: 1; }
|
||
.ev-del:hover { color: var(--red); }
|
||
|
||
/* ── BADGES ──────────────────────────────────────────────── */
|
||
|
||
.badge {
|
||
font-size: 10px; font-weight: 600;
|
||
padding: 1px 5px; border-radius: var(--r-xs);
|
||
letter-spacing: .1px; white-space: nowrap;
|
||
}
|
||
.badge-red { background: #fee2e2; color: #dc2626; }
|
||
.badge-orange { background: #fef3c7; color: #b45309; }
|
||
.badge-blue { background: var(--blue-100); color: #1d4ed8; }
|
||
.badge-green { background: #dcfce7; color: #15803d; }
|
||
|
||
/* ── HOMEWORK ────────────────────────────────────────────── */
|
||
|
||
.hw-item {
|
||
display: flex; align-items: flex-start; gap: 9px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.hw-item:last-child { border-bottom: none; }
|
||
.check {
|
||
width: 16px; height: 16px;
|
||
border-radius: 4px;
|
||
border: 1.5px solid var(--n-300);
|
||
cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0; margin-top: 2px;
|
||
transition: all .14s; font-size: 9px; color: transparent;
|
||
}
|
||
.check.done { background: var(--green); border-color: var(--green); color: #fff; }
|
||
.hw-body { flex: 1; min-width: 0; }
|
||
.hw-title { font-size: 13px; font-weight: 500; color: var(--text); line-height: 1.35; }
|
||
.hw-title.crossed { text-decoration: line-through; color: var(--text-muted); }
|
||
.hw-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||
|
||
.del-btn {
|
||
color: var(--text-subtle); cursor: pointer;
|
||
font-size: 12px; border: none; background: none;
|
||
flex-shrink: 0; padding: 4px; border-radius: var(--r-sm);
|
||
opacity: 0; transition: opacity .1s, color .1s;
|
||
font-family: inherit;
|
||
}
|
||
.hw-item:hover .del-btn,
|
||
.grade-row:hover .del-btn,
|
||
.ab-item:hover .del-btn,
|
||
.todo-item:hover .del-btn,
|
||
.cd-item:hover .del-btn,
|
||
.file-item:hover .del-btn { opacity: 1; }
|
||
.del-btn:hover { color: var(--red); }
|
||
|
||
/* ── GRADES ──────────────────────────────────────────────── */
|
||
|
||
.grade-row {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.grade-row:last-child { border-bottom: none; }
|
||
.grade-info { flex: 1; min-width: 0; }
|
||
.grade-subj { font-size: 13px; font-weight: 500; color: var(--text); }
|
||
.grade-type { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
||
.grade-note { font-size: 11px; color: var(--text-muted); }
|
||
.grade-val {
|
||
font-size: 13px; font-weight: 700;
|
||
min-width: 30px; text-align: center;
|
||
padding: 3px 6px; border-radius: 5px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.g1,.g2 { background: #dcfce7; color: #166534; }
|
||
.g3 { background: #fef9c3; color: #854d0e; }
|
||
.g4 { background: #ffedd5; color: #c2410c; }
|
||
.g5,.g6 { background: #fee2e2; color: #991b1b; }
|
||
.grade-avg {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding-top: 12px; margin-top: 2px;
|
||
border-top: 1px solid var(--border-subtle);
|
||
}
|
||
.grade-avg-label { font-size: 11px; color: var(--text-muted); font-weight: 500; }
|
||
.grade-avg-val { font-size: 24px; font-weight: 800; color: var(--blue); letter-spacing: -1px; }
|
||
.grade-avg-sub { font-size: 10px; color: var(--text-subtle); margin-top: 2px; }
|
||
|
||
/* ── ABSENCES ────────────────────────────────────────────── */
|
||
|
||
.ab-item {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 7px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.ab-item:last-child { border-bottom: none; }
|
||
.ab-date { font-size: 11px; font-weight: 600; min-width: 70px; color: var(--text-muted); padding-top: 1px; }
|
||
.ab-info { flex: 1; }
|
||
.ab-subj { font-size: 13px; font-weight: 500; color: var(--text); }
|
||
.ab-reason { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
||
.ab-total { font-size: 11px; color: var(--text-muted); font-weight: 500; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-subtle); }
|
||
|
||
/* ── TODOS ───────────────────────────────────────────────── */
|
||
|
||
.todo-item {
|
||
display: flex; align-items: center; gap: 9px;
|
||
padding: 7px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.todo-item:last-child { border-bottom: none; }
|
||
.todo-check {
|
||
width: 16px; height: 16px;
|
||
border-radius: 50%;
|
||
border: 1.5px solid var(--n-300);
|
||
cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0; font-size: 9px; color: transparent;
|
||
transition: all .14s;
|
||
}
|
||
.todo-check.done { background: var(--blue); border-color: var(--blue); color: #fff; }
|
||
.todo-title { flex: 1; font-size: 13px; font-weight: 500; color: var(--text); }
|
||
.todo-title.crossed { text-decoration: line-through; color: var(--text-muted); font-weight: 400; }
|
||
|
||
/* ── COUNTDOWNS ──────────────────────────────────────────── */
|
||
|
||
.cd-item {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
}
|
||
.cd-item:last-child { border-bottom: none; }
|
||
.cd-days { font-size: 18px; font-weight: 800; color: var(--blue); min-width: 36px; text-align: center; letter-spacing: -1px; font-variant-numeric: tabular-nums; line-height: 1; }
|
||
.cd-days.past { color: var(--text-subtle); font-size: 14px; }
|
||
.cd-info { flex: 1; }
|
||
.cd-title { font-size: 13px; font-weight: 500; color: var(--text); }
|
||
.cd-date { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
||
|
||
/* ── QUICKLINKS ──────────────────────────────────────────── */
|
||
|
||
.ql-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 6px; }
|
||
.ql-item {
|
||
display: flex; flex-direction: column; align-items: center; gap: 5px;
|
||
padding: 10px 6px;
|
||
background: var(--surface-2);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
text-decoration: none;
|
||
color: var(--text-muted);
|
||
font-size: 11px; font-weight: 500;
|
||
text-align: center;
|
||
transition: border-color .12s, background .12s, color .12s;
|
||
position: relative; cursor: pointer;
|
||
line-height: 1.3;
|
||
}
|
||
.ql-item:hover { background: var(--blue-50); border-color: var(--blue); color: var(--blue); }
|
||
.ql-icon { font-size: 18px; line-height: 1; }
|
||
.ql-del { position: absolute; top: 3px; right: 4px; font-size: 10px; color: var(--text-subtle); display: none; border: none; background: none; cursor: pointer; line-height: 1; }
|
||
.ql-item:hover .ql-del { display: block; }
|
||
.ql-del:hover { color: var(--red); }
|
||
|
||
/* ── EMPTY STATE ─────────────────────────────────────────── */
|
||
|
||
.empty { text-align: center; color: var(--text-subtle); font-size: 13px; padding: 24px 0; font-weight: 500; }
|
||
|
||
/* ── CHAT ────────────────────────────────────────────────── */
|
||
|
||
.chat-msgs {
|
||
height: 240px; overflow-y: auto;
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
padding-right: 4px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--n-200) transparent;
|
||
}
|
||
.chat-msg { display: flex; flex-direction: column; gap: 1px; }
|
||
.chat-msg-meta { display: flex; align-items: baseline; gap: 6px; }
|
||
.chat-msg-user { font-size: 12px; font-weight: 600; color: var(--blue); }
|
||
.chat-msg-user.own { color: var(--green); }
|
||
.chat-msg-time { font-size: 10px; color: var(--text-subtle); }
|
||
.chat-msg-body { font-size: 13px; color: var(--text); line-height: 1.45; word-break: break-word; padding-left: 2px; }
|
||
.chat-msg-del { font-size: 11px; color: var(--text-subtle); border: none; background: none; cursor: pointer; margin-left: auto; flex-shrink: 0; visibility: hidden; }
|
||
.chat-msg:hover .chat-msg-del { visibility: visible; }
|
||
.chat-msg-del:hover { color: var(--red); }
|
||
.chat-input-row { display: flex; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
|
||
.chat-input {
|
||
flex: 1; padding: 7px 11px;
|
||
border: 1px solid var(--border); border-radius: var(--r-sm);
|
||
font-size: 13px; font-family: inherit;
|
||
background: var(--surface); color: var(--text);
|
||
outline: none; transition: border-color .12s, box-shadow .12s;
|
||
}
|
||
.chat-input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.08); }
|
||
|
||
/* ── FILE STORAGE ────────────────────────────────────────── */
|
||
|
||
.files-quota { margin-bottom: 12px; }
|
||
.files-quota-bar { height: 5px; background: var(--n-100); border-radius: 99px; overflow: hidden; margin-top: 5px; }
|
||
.files-quota-fill { height: 100%; background: var(--blue); border-radius: 99px; transition: width .3s; }
|
||
.files-quota-fill.warn { background: var(--amber); }
|
||
.files-quota-fill.danger { background: var(--red); }
|
||
.files-quota-text { font-size: 11px; color: var(--text-subtle); display: flex; justify-content: space-between; margin-top: 3px; }
|
||
.file-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border-subtle); }
|
||
.file-item:last-child { border-bottom: none; }
|
||
.file-icon { font-size: 15px; flex-shrink: 0; line-height: 1; }
|
||
.file-info { flex: 1; min-width: 0; }
|
||
.file-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
|
||
.file-meta { font-size: 11px; color: var(--text-muted); margin-top: 1px; }
|
||
.file-dl { font-size: 11px; font-weight: 600; color: var(--blue); background: var(--blue-50); border: none; padding: 4px 9px; border-radius: var(--r-sm); cursor: pointer; font-family: inherit; white-space: nowrap; transition: background .1s; }
|
||
.file-dl:hover { background: var(--blue-100); }
|
||
.upload-drop {
|
||
border: 1.5px dashed var(--n-300); border-radius: var(--r-sm);
|
||
padding: 16px; text-align: center;
|
||
color: var(--text-muted); font-size: 12px;
|
||
margin-bottom: 12px;
|
||
transition: border-color .12s, background .12s;
|
||
}
|
||
.upload-drop.drag { border-color: var(--blue); background: var(--blue-50); color: var(--blue); }
|
||
|
||
/* ── MODAL ───────────────────────────────────────────────── */
|
||
|
||
.overlay {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0,0,0,.45);
|
||
z-index: 300;
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 16px;
|
||
backdrop-filter: blur(1px);
|
||
}
|
||
.modal {
|
||
background: var(--surface);
|
||
border-radius: var(--r-lg);
|
||
padding: 22px;
|
||
width: 100%; max-width: 400px;
|
||
box-shadow: var(--shadow-xl);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.modal h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; color: var(--text); }
|
||
.modal-fields { display: flex; flex-direction: column; gap: 8px; }
|
||
.modal-fields input,
|
||
.modal-fields select,
|
||
.modal-fields textarea {
|
||
padding: 8px 11px;
|
||
border: 1px solid var(--border); border-radius: var(--r-sm);
|
||
font-size: 13px; font-family: inherit;
|
||
color: var(--text); background: var(--surface);
|
||
outline: none;
|
||
transition: border-color .12s, box-shadow .12s;
|
||
}
|
||
.modal-fields input:focus,
|
||
.modal-fields select:focus,
|
||
.modal-fields textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.08); }
|
||
.modal-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.modal-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
||
.btn-cancel {
|
||
padding: 7px 15px;
|
||
border: 1px solid var(--border); border-radius: var(--r-sm);
|
||
background: none; cursor: pointer;
|
||
font-size: 13px; font-weight: 500; font-family: inherit;
|
||
color: var(--text-muted); transition: background .1s;
|
||
}
|
||
.btn-cancel:hover { background: var(--n-100); color: var(--text); }
|
||
.btn-save {
|
||
padding: 7px 15px;
|
||
background: var(--blue); color: #fff;
|
||
border: 1px solid var(--blue); border-radius: var(--r-sm);
|
||
font-size: 13px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer; transition: background .1s;
|
||
}
|
||
.btn-save:hover { background: var(--blue-d); }
|
||
.btn-danger {
|
||
padding: 7px 15px;
|
||
background: var(--red); color: #fff;
|
||
border: 1px solid var(--red); border-radius: var(--r-sm);
|
||
font-size: 13px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* ── SETTINGS ────────────────────────────────────────────── */
|
||
|
||
.settings-section { margin-bottom: 18px; padding-bottom: 18px; border-bottom: 1px solid var(--border-subtle); }
|
||
.settings-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
||
.settings-label { font-size: 13px; font-weight: 600; margin-bottom: 10px; color: var(--text); }
|
||
.settings-fields { display: flex; flex-direction: column; gap: 8px; }
|
||
.settings-fields input {
|
||
padding: 8px 11px;
|
||
border: 1px solid var(--border); border-radius: var(--r-sm);
|
||
font-size: 13px; font-family: inherit;
|
||
background: var(--surface); color: var(--text); outline: none;
|
||
}
|
||
.settings-fields input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.08); }
|
||
.danger-zone { background: var(--red-50); border: 1px solid #fecaca; border-radius: var(--r-sm); padding: 14px; }
|
||
.danger-zone p { font-size: 13px; color: #7f1d1d; margin-bottom: 10px; line-height: 1.5; }
|
||
body.dark .danger-zone { background: #300; border-color: #5a1a1a; }
|
||
body.dark .danger-zone p { color: #fca5a5; }
|
||
|
||
/* ── TOAST ───────────────────────────────────────────────── */
|
||
|
||
#toasts { position: fixed; bottom: 20px; right: 20px; z-index: 400; display: flex; flex-direction: column; gap: 6px; pointer-events: none; }
|
||
.toast {
|
||
background: var(--n-900);
|
||
color: #fff;
|
||
font-size: 13px; font-weight: 500;
|
||
padding: 9px 14px; border-radius: var(--r);
|
||
box-shadow: var(--shadow-lg);
|
||
opacity: 0; transform: translateY(6px) scale(.98);
|
||
transition: opacity .18s, transform .18s;
|
||
pointer-events: none; max-width: 300px;
|
||
border: 1px solid rgba(255,255,255,.06);
|
||
}
|
||
.toast.show { opacity: 1; transform: none; }
|
||
.toast.success { background: #166534; }
|
||
.toast.error { background: #991b1b; }
|
||
|
||
/* ── FOOTER ──────────────────────────────────────────────── */
|
||
|
||
footer {
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
padding: 13px 20px;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
flex-wrap: wrap; gap: 8px; flex-shrink: 0;
|
||
}
|
||
.footer-left { font-size: 11px; color: var(--text-subtle); }
|
||
.footer-left strong { color: var(--text-muted); font-weight: 500; }
|
||
.footer-links { display: flex; gap: 14px; }
|
||
.footer-links a { font-size: 11px; color: var(--text-subtle); text-decoration: none; transition: color .12s; }
|
||
.footer-links a:hover { color: var(--blue); }
|
||
|
||
/* ── PRINT ───────────────────────────────────────────────── */
|
||
|
||
@media print {
|
||
body > *:not(main) { display: none !important; }
|
||
.main-grid { display: block !important; }
|
||
.col-secondary { display: none !important; }
|
||
#card-tt { box-shadow: none !important; border: 1px solid #ddd !important; }
|
||
.col-primary > *:not(#card-tt) { display: none !important; }
|
||
}
|
||
|
||
/* ── SIDEBAR ─────────────────────────────────────────────── */
|
||
|
||
.sidebar-backdrop {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0,0,0,.32);
|
||
z-index: 150;
|
||
opacity: 0; pointer-events: none;
|
||
transition: opacity .2s;
|
||
}
|
||
.sidebar-backdrop.open { opacity: 1; pointer-events: all; }
|
||
|
||
.sidebar {
|
||
position: fixed; top: 0; right: 0;
|
||
width: 360px; max-width: 100vw; height: 100%;
|
||
background: var(--bg);
|
||
border-left: 1px solid var(--border);
|
||
box-shadow: var(--shadow-lg);
|
||
z-index: 160;
|
||
transform: translateX(100%);
|
||
transition: transform .22s cubic-bezier(.4,0,.2,1);
|
||
display: flex; flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.sidebar.open { transform: translateX(0); }
|
||
|
||
.sidebar-head {
|
||
height: 54px;
|
||
padding: 0 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
||
background: var(--surface);
|
||
flex-shrink: 0;
|
||
}
|
||
.sidebar-title { font-size: 13px; font-weight: 600; color: var(--text); }
|
||
.sidebar-close {
|
||
width: 28px; height: 28px;
|
||
border: none; background: none; cursor: pointer;
|
||
border-radius: var(--r-sm);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 18px; line-height: 1; color: var(--text-muted);
|
||
transition: background .1s; font-family: inherit;
|
||
}
|
||
.sidebar-close:hover { background: var(--n-100); color: var(--text); }
|
||
|
||
.sidebar-body {
|
||
flex: 1; overflow-y: auto;
|
||
padding: 14px;
|
||
display: flex; flex-direction: column; gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--n-200) transparent;
|
||
}
|
||
|
||
/* burger button badge dot (optional active state) */
|
||
#sidebar-btn { position: relative; }
|
||
|
||
/* ── RESPONSIVE ──────────────────────────────────────────── */
|
||
|
||
/* ── TICKET THREAD ────────────────────────────── */
|
||
.ticket-list-item { display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid var(--border-subtle); cursor:pointer; transition:background .1s; }
|
||
.ticket-list-item:last-child { border-bottom:none; }
|
||
.ticket-list-item:hover { background:var(--n-50); margin:0 -14px; padding-left:14px; padding-right:14px; border-radius:var(--r-sm); }
|
||
.ticket-list-subj { flex:1; font-size:13px; font-weight:600; color:var(--text); }
|
||
.ticket-list-meta { font-size:11px; color:var(--text-muted); margin-top:1px; }
|
||
.thread-wrap { display:flex; flex-direction:column; gap:8px; max-height:360px; overflow-y:auto; margin:12px 0; padding-right:2px; scrollbar-width:thin; }
|
||
.thread-bubble { padding:8px 12px; border-radius:var(--r-lg); font-size:13px; line-height:1.55; word-break:break-word; white-space:pre-wrap; max-width:85%; }
|
||
.thread-bubble.mine { background:var(--blue); color:#fff; align-self:flex-end; border-bottom-right-radius:4px; }
|
||
.thread-bubble.other { background:var(--n-100); color:var(--text); align-self:flex-start; border-bottom-left-radius:4px; }
|
||
.thread-bubble.admin-bubble { background:var(--amber-50); color:var(--text); border:1px solid var(--border); }
|
||
.thread-bubble-meta { font-size:10px; opacity:.65; margin-top:3px; }
|
||
.thread-reply-row { display:flex; gap:8px; margin-top:6px; }
|
||
.thread-reply-row textarea { flex:1; border:1.5px solid var(--border); border-radius:var(--r-sm); padding:8px 10px; font-size:13px; font-family:inherit; resize:none; outline:none; background:var(--surface); color:var(--text); transition:border-color .12s; }
|
||
.thread-reply-row textarea:focus { border-color:var(--blue); }
|
||
|
||
@media (max-width: 860px) {
|
||
.main-grid { grid-template-columns: 1fr; }
|
||
}
|
||
@media (max-width: 600px) {
|
||
main { padding: 12px 12px 20px; gap: 14px; }
|
||
.main-grid { gap: 12px; }
|
||
.card-pair { grid-template-columns: 1fr; }
|
||
header { padding: 0 12px; height: 50px; }
|
||
#clock { display: none; }
|
||
#weather { display: none; }
|
||
.banner-chips { display: none; }
|
||
.h-sep { display: none; }
|
||
footer { padding: 11px 14px; }
|
||
}
|
||
|
||
/* ── ICONS ───────────────────────────────────────────────── */
|
||
.lucide {
|
||
display: inline-block;
|
||
vertical-align: -0.125em;
|
||
flex-shrink: 0;
|
||
width: 1em; height: 1em;
|
||
stroke-width: 2;
|
||
}
|
||
.card-title .lucide { width: 14px; height: 14px; }
|
||
.chip .lucide { width: 11px; height: 11px; stroke-width: 2.5; }
|
||
.h-icon-btn .lucide { width: 16px; height: 16px; }
|
||
.ql-icon .lucide { width: 20px; height: 20px; }
|
||
.file-icon .lucide { width: 16px; height: 16px; }
|
||
.dd-item .lucide { width: 15px; height: 15px; }
|
||
.del-btn .lucide, .ev-del .lucide { width: 13px; height: 13px; }
|
||
.tt-del .lucide, .chat-msg-del .lucide, .ql-del .lucide { width: 11px; height: 11px; stroke-width: 2.5; }
|
||
.check .lucide, .todo-check .lucide { width: 9px; height: 9px; stroke-width: 3; }
|
||
.cd-days.past .lucide, .ev-days .lucide { width: 18px; height: 18px; }
|
||
#weather .lucide { width: 14px; height: 14px; stroke-width: 1.5; }
|
||
.print-btn .lucide { width: 13px; height: 13px; }
|
||
.btn-sm .lucide { width: 13px; height: 13px; }
|
||
.sidebar-close .lucide { width: 16px; height: 16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="brand" onclick="location.href='/'">
|
||
<div class="brand-mark">i1</div>
|
||
<div class="brand-text">
|
||
<span class="brand-sub">IFB-Berufsfachschule Rosenheim</span>
|
||
<span class="brand-name">INFO1</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="h-sep"></div>
|
||
<div class="h-spacer"></div>
|
||
|
||
<div class="h-right">
|
||
<div id="weather">–</div>
|
||
<div id="clock"></div>
|
||
<button id="btn-dark" class="h-icon-btn" onclick="toggleDark()" title="Dark Mode"><i data-lucide="moon" aria-label="Dark Mode"></i></button>
|
||
<button id="sidebar-btn" class="h-icon-btn" style="display:none" onclick="openSidebar()" title="Noten, Dateien & Fehlzeiten"><i data-lucide="menu" aria-label="Seitenleiste öffnen"></i></button>
|
||
<div id="h-user" style="display:flex;align-items:center;gap:7px">
|
||
<a href="/login" class="h-btn">Anmelden</a>
|
||
<a href="/login?tab=register" class="h-btn h-btn-primary">Registrieren</a>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="banner">
|
||
<div class="banner-label">Mit Account freischalten:</div>
|
||
<div class="banner-chips">
|
||
<span class="chip"><i data-lucide="calendar" aria-hidden="true"></i> Stundenplan</span>
|
||
<span class="chip"><i data-lucide="pencil" aria-hidden="true"></i> Hausaufgaben</span>
|
||
<span class="chip"><i data-lucide="graduation-cap" aria-hidden="true"></i> Noten & Ø</span>
|
||
<span class="chip"><i data-lucide="user-x" aria-hidden="true"></i> Fehlzeiten</span>
|
||
<span class="chip"><i data-lucide="check-square" aria-hidden="true"></i> To-Do</span>
|
||
<span class="chip"><i data-lucide="timer" aria-hidden="true"></i> Countdowns</span>
|
||
<span class="chip"><i data-lucide="link" aria-hidden="true"></i> Links</span>
|
||
</div>
|
||
<button class="banner-cta" onclick="location.href='/login?tab=register'">Jetzt registrieren →</button>
|
||
</div>
|
||
|
||
<main>
|
||
<div class="main-grid">
|
||
|
||
<!-- ── PRIMARY COLUMN ──────────────────────────── -->
|
||
<div class="col-primary">
|
||
|
||
<!-- Timetable (logged-out placeholder) -->
|
||
<div class="card" id="card-tt-public">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="calendar" aria-hidden="true"></i> Stundenplan</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="ph-tt">
|
||
<div class="ph-col"><div class="ph-head" id="ph0">Mo</div><div class="ph-slot sk"></div><div class="ph-slot sk" style="height:40px;opacity:.6"></div><div class="ph-slot sk" style="height:40px;opacity:.4"></div></div>
|
||
<div class="ph-col"><div class="ph-head" id="ph1">Di</div><div class="ph-slot sk"></div><div class="ph-slot sk" style="height:40px;opacity:.6"></div><div class="ph-slot sk" style="height:40px;opacity:.4"></div></div>
|
||
<div class="ph-col"><div class="ph-head" id="ph2">Mi</div><div class="ph-slot sk"></div><div class="ph-slot sk" style="height:40px;opacity:.6"></div><div class="ph-slot sk" style="height:40px;opacity:.4"></div></div>
|
||
<div class="ph-col"><div class="ph-head" id="ph3">Do</div><div class="ph-slot sk"></div><div class="ph-slot sk" style="height:40px;opacity:.6"></div><div class="ph-slot sk" style="height:40px;opacity:.4"></div></div>
|
||
<div class="ph-col"><div class="ph-head" id="ph4">Fr</div><div class="ph-slot sk"></div><div class="ph-slot sk" style="height:40px;opacity:.6"></div><div class="ph-slot sk" style="height:40px;opacity:.4"></div></div>
|
||
</div>
|
||
<div class="tt-pending"><i data-lucide="clock" aria-hidden="true"></i> Wird noch eingetragen</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Timetable (logged-in) -->
|
||
<div class="card" id="card-tt" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="calendar" aria-hidden="true"></i> Stundenplan</div>
|
||
<div class="card-actions">
|
||
<button class="print-btn" onclick="window.print()"><i data-lucide="printer" aria-hidden="true"></i> Drucken</button>
|
||
<button class="add-btn" onclick="openModal('timetable')">+ Stunde</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="tt-wrap"><div class="tt-grid" id="tt-grid"></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Homework + Todos (pair) -->
|
||
<div class="card-pair" id="pair-hw-todo" style="display:none">
|
||
<div class="card" id="card-hw">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="pencil" aria-hidden="true"></i> Hausaufgaben</div>
|
||
<button class="add-btn" onclick="openModal('homework')">+ Aufgabe</button>
|
||
</div>
|
||
<div class="card-body" id="list-hw"></div>
|
||
</div>
|
||
|
||
<div class="card" id="card-todo">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="check-square" aria-hidden="true"></i> To-Do</div>
|
||
<button class="add-btn" onclick="openModal('todos')">+ Aufgabe</button>
|
||
</div>
|
||
<div class="card-body" id="list-todo"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /col-primary -->
|
||
|
||
<!-- ── SECONDARY COLUMN ─────────────────────────── -->
|
||
<div class="col-secondary">
|
||
|
||
<!-- Exams (shared) -->
|
||
<div class="card" id="card-pruefungen">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="clipboard-list" aria-hidden="true"></i> Prüfungen & Klausuren</div>
|
||
<button class="add-btn" id="btn-add-pruefung" style="display:none" onclick="openModal('class-events','pruefung')">+ Prüfung</button>
|
||
</div>
|
||
<div class="card-body" id="list-pruefungen"><div class="sk-line sk"></div><div class="sk-line sk short"></div></div>
|
||
</div>
|
||
|
||
<!-- Holidays (shared) -->
|
||
<div class="card" id="card-ferien">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="calendar-range" aria-hidden="true"></i> Ferien & Feiertage</div>
|
||
<button class="add-btn" id="btn-add-ferien" style="display:none" onclick="openModal('class-events','ferien')">+ Termin</button>
|
||
</div>
|
||
<div class="card-body" id="list-ferien"><div class="sk-line sk"></div><div class="sk-line sk short"></div></div>
|
||
</div>
|
||
|
||
<!-- Countdowns -->
|
||
<div class="card" id="card-cd" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="timer" aria-hidden="true"></i> Countdowns</div>
|
||
<button class="add-btn" onclick="openModal('countdowns')">+ Countdown</button>
|
||
</div>
|
||
<div class="card-body" id="list-cd"></div>
|
||
</div>
|
||
|
||
<!-- Quick Links -->
|
||
<div class="card" id="card-ql">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="link" aria-hidden="true"></i> Schnelllinks</div>
|
||
<button class="add-btn" id="btn-add-ql" style="display:none" onclick="openModal('quicklinks')">+ Link</button>
|
||
</div>
|
||
<div class="card-body"><div class="ql-grid" id="ql-grid">
|
||
<a class="ql-item" href="https://www.ifb-schulen.de" target="_blank" rel="noopener"><span class="ql-icon"><i data-lucide="building-2" aria-hidden="true"></i></span>IFB Website</a>
|
||
<a class="ql-item" href="https://www.bib-info.de" target="_blank" rel="noopener"><span class="ql-icon"><i data-lucide="book-open" aria-hidden="true"></i></span>Bibliothek</a>
|
||
</div></div>
|
||
</div>
|
||
|
||
</div><!-- /col-secondary -->
|
||
</div><!-- /main-grid -->
|
||
|
||
<!-- Chat (full width) -->
|
||
<div class="card" id="card-chat" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="message-square" aria-hidden="true"></i> Klassen-Chat · INFO1</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="chat-msgs" id="chat-msgs"></div>
|
||
<div class="chat-input-row">
|
||
<input class="chat-input" id="chat-input" type="text" placeholder="Nachricht schreiben…" maxlength="500" autocomplete="off">
|
||
<button class="add-btn" onclick="sendChatMsg()">Senden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Support Tickets (full width, logged-in) -->
|
||
<div class="card" id="card-tickets" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><span class="ci">🎫</span> Support-Tickets</div>
|
||
<button class="add-btn" onclick="openNewTicket()">+ Ticket</button>
|
||
</div>
|
||
<div class="card-body" id="list-tickets"><div class="empty">Keine Tickets</div></div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<!-- ── SIDEBAR ──────────────────────────────────────────── -->
|
||
<div class="sidebar-backdrop" id="sidebar-backdrop" onclick="closeSidebar()"></div>
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-head">
|
||
<span class="sidebar-title">Noten, Dateien & Fehlzeiten</span>
|
||
<button class="sidebar-close" onclick="closeSidebar()" aria-label="Schließen"><i data-lucide="x" aria-hidden="true"></i></button>
|
||
</div>
|
||
<div class="sidebar-body">
|
||
|
||
<div class="card" id="card-grades" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="graduation-cap" aria-hidden="true"></i> Noten</div>
|
||
<button class="add-btn" onclick="openModal('grades')">+ Note</button>
|
||
</div>
|
||
<div class="card-body" id="list-grades"></div>
|
||
</div>
|
||
|
||
<div class="card" id="card-files" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="folder" aria-hidden="true"></i> Dateispeicher</div>
|
||
<div class="card-actions">
|
||
<input type="file" id="file-input" style="display:none" multiple onchange="uploadFiles(event)">
|
||
<button class="add-btn" onclick="document.getElementById('file-input').click()">↑ Hochladen</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body" id="files-body">
|
||
<div class="upload-drop" id="files-drop">Dateien hier ablegen oder oben hochladen</div>
|
||
<div class="sk-line sk"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="card-ab" style="display:none">
|
||
<div class="card-head">
|
||
<div class="card-title"><i data-lucide="user-x" aria-hidden="true"></i> Fehlzeiten</div>
|
||
<button class="add-btn" onclick="openModal('absences')">+ Eintragen</button>
|
||
</div>
|
||
<div class="card-body" id="list-ab"></div>
|
||
</div>
|
||
|
||
</div>
|
||
</aside>
|
||
|
||
<footer>
|
||
<div class="footer-left">Daten auf <strong>Hetzner-Server, Nürnberg, Deutschland</strong> · EU-DSGVO konform</div>
|
||
<div class="footer-links">
|
||
<a href="/datenschutz">Datenschutzerklärung</a>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- NEW TICKET MODAL -->
|
||
<div class="overlay" id="new-ticket-overlay" style="display:none" onclick="if(event.target===this)closeNewTicket()">
|
||
<div class="modal" style="max-width:460px">
|
||
<h3>Neues Support-Ticket</h3>
|
||
<div style="display:flex;flex-direction:column;gap:10px;margin:14px 0">
|
||
<input type="text" id="new-ticket-subject" placeholder="Betreff (max. 200 Zeichen)" maxlength="200"
|
||
style="padding:8px 10px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-size:13px;font-family:inherit;outline:none;background:var(--surface);color:var(--text)">
|
||
<textarea id="new-ticket-message" placeholder="Beschreibe dein Problem…" rows="5" maxlength="5000"
|
||
style="padding:8px 10px;border:1.5px solid var(--border);border-radius:var(--r-sm);font-size:13px;font-family:inherit;resize:vertical;outline:none;background:var(--surface);color:var(--text)"></textarea>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn-cancel" onclick="closeNewTicket()">Abbrechen</button>
|
||
<button class="btn-save" onclick="submitNewTicket()">Absenden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TICKET THREAD MODAL -->
|
||
<div class="overlay" id="thread-overlay" style="display:none" onclick="if(event.target===this)closeThread()">
|
||
<div class="modal" style="max-width:520px">
|
||
<h3 id="thread-title" style="word-break:break-word"></h3>
|
||
<p id="thread-meta" style="font-size:12px;color:var(--text-muted);margin-top:3px"></p>
|
||
<div class="thread-wrap" id="thread-msgs"></div>
|
||
<div class="thread-reply-row" id="thread-reply-area">
|
||
<textarea id="thread-input" placeholder="Antwort schreiben…" rows="2" maxlength="5000"></textarea>
|
||
<button class="btn-save" onclick="sendThreadReply()">Senden</button>
|
||
</div>
|
||
<div class="modal-actions" style="margin-top:8px">
|
||
<button class="btn-cancel" onclick="closeThread()">Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DATA MODAL -->
|
||
<div class="overlay" id="modal-overlay" style="display:none" onclick="if(event.target===this)closeModal()">
|
||
<div class="modal">
|
||
<h3 id="modal-title"></h3>
|
||
<div class="modal-fields" id="modal-fields"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn-cancel" onclick="closeModal()">Abbrechen</button>
|
||
<button class="btn-save" onclick="saveModal()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SETTINGS MODAL -->
|
||
<div class="overlay" id="settings-overlay" style="display:none" onclick="if(event.target===this)closeSettings()">
|
||
<div class="modal" style="max-width:420px">
|
||
<h3>Einstellungen</h3>
|
||
|
||
<div class="settings-section">
|
||
<div class="settings-label">Passwort ändern</div>
|
||
<div class="settings-fields">
|
||
<input type="password" id="pw-current" placeholder="Aktuelles Passwort">
|
||
<input type="password" id="pw-new" placeholder="Neues Passwort (min. 6 Zeichen)">
|
||
<button class="btn-save" style="align-self:flex-start" onclick="changePassword()">Ändern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-section">
|
||
<div class="danger-zone">
|
||
<p>Account und alle gespeicherten Daten werden <strong>unwiderruflich gelöscht</strong>.</p>
|
||
<div class="settings-fields">
|
||
<input type="password" id="pw-delete" placeholder="Passwort zur Bestätigung">
|
||
<button class="btn-danger" style="align-self:flex-start" onclick="deleteAccount()">Account löschen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top:10px">
|
||
<button class="btn-cancel" onclick="closeSettings()">Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toasts"></div>
|
||
|
||
<script>
|
||
// ── CONSTANTS ────────────────────────────────────────────────
|
||
const DAYS = ['Montag','Dienstag','Mittwoch','Donnerstag','Freitag'];
|
||
const DAY_S = ['Mo','Di','Mi','Do','Fr'];
|
||
const TIME_SLOTS = [
|
||
{ key:'07:45', label:'1.', time:'07:45' },
|
||
{ key:'08:30', label:'2.', time:'08:30' },
|
||
{ key:'09:30', label:'3.', time:'09:30' },
|
||
{ key:'10:15', label:'4.', time:'10:15' },
|
||
{ key:'11:15', label:'5.', time:'11:15' },
|
||
{ key:'12:00', label:'6.', time:'12:00' },
|
||
{ key:'13:30', label:'7.', time:'13:30' },
|
||
{ key:'14:15', label:'8.', time:'14:15' },
|
||
];
|
||
const GRADE_WEIGHTS = { schulaufgabe:2, kurzarbeit:1.5, stegreifaufgabe:1, muendlich:1, sonstiges:1 };
|
||
const GRADE_TYPES = { schulaufgabe:'Schulaufgabe', kurzarbeit:'Kurzarbeit', stegreifaufgabe:'Stegreifaufgabe', muendlich:'Mündlich', sonstiges:'Sonstiges' };
|
||
const SUBJECT_COLORS = [
|
||
{ bg:'#eff6ff', border:'#2563eb', text:'#1e40af' },
|
||
{ bg:'#f0fdf4', border:'#16a34a', text:'#166534' },
|
||
{ bg:'#fef3c7', border:'#d97706', text:'#92400e' },
|
||
{ bg:'#fdf4ff', border:'#9333ea', text:'#7e22ce' },
|
||
{ bg:'#fff1f2', border:'#e11d48', text:'#9f1239' },
|
||
{ bg:'#ecfdf5', border:'#059669', text:'#065f46' },
|
||
{ bg:'#f0f9ff', border:'#0ea5e9', text:'#0c4a6e' },
|
||
{ bg:'#fefce8', border:'#ca8a04', text:'#713f12' },
|
||
];
|
||
|
||
let currentModal = null;
|
||
let currentModalSub = null;
|
||
|
||
// ── UTILITIES ─────────────────────────────────────────────────
|
||
function esc(s){ return String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
|
||
function subjectColor(s){
|
||
let h=0; for(const c of String(s)) h=(h*31+c.charCodeAt(0))&0xffffffff;
|
||
return SUBJECT_COLORS[Math.abs(h)%SUBJECT_COLORS.length];
|
||
}
|
||
|
||
function daysUntil(dateStr){
|
||
const t=new Date(); t.setHours(0,0,0,0);
|
||
const d=new Date(dateStr); d.setHours(0,0,0,0);
|
||
return Math.round((d-t)/86400000);
|
||
}
|
||
|
||
function fmtDate(s){ return s ? new Date(s).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}) : ''; }
|
||
|
||
// ── TOAST ─────────────────────────────────────────────────────
|
||
function toast(msg, type='success'){
|
||
const el=document.createElement('div');
|
||
el.className=`toast ${type}`; el.textContent=msg;
|
||
document.getElementById('toasts').appendChild(el);
|
||
requestAnimationFrame(()=>{ requestAnimationFrame(()=>el.classList.add('show')); });
|
||
setTimeout(()=>{ el.classList.remove('show'); setTimeout(()=>el.remove(),300); },3000);
|
||
}
|
||
|
||
// ── CLOCK ─────────────────────────────────────────────────────
|
||
function tick(){
|
||
const n=new Date();
|
||
document.getElementById('clock').textContent=
|
||
n.toLocaleDateString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit',year:'numeric'})+
|
||
' · '+n.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
|
||
}
|
||
tick(); setInterval(tick,10000);
|
||
|
||
// ── WEATHER ───────────────────────────────────────────────────
|
||
async function loadWeather(){
|
||
try{
|
||
const d=await(await fetch('https://api.open-meteo.com/v1/forecast?latitude=47.857&longitude=12.128¤t_weather=true')).json();
|
||
const w=d.current_weather;
|
||
const ic={0:'☀️',1:'🌤️',2:'⛅',3:'☁️',45:'🌫️',51:'🌦️',55:'🌧️',61:'🌧️',65:'🌧️',71:'🌨️',75:'❄️',80:'🌧️',95:'⛈️'};
|
||
document.getElementById('weather').innerHTML=`${ic[w.weathercode]||'🌡️'} <strong>${Math.round(w.temperature)}°C</strong> <span style="color:var(--text-subtle);font-size:10px">RO</span>`;
|
||
}catch{}
|
||
}
|
||
loadWeather();
|
||
|
||
// ── API ────────────────────────────────────────────────────────
|
||
async function api(method,path,body){
|
||
const r=await fetch('/api/'+path,{method,headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
|
||
return r.json();
|
||
}
|
||
|
||
// ── SESSION ────────────────────────────────────────────────────
|
||
async function init(){
|
||
// Highlight today on public timetable
|
||
const ti=(new Date().getDay()+6)%7;
|
||
if(ti<5) document.getElementById('ph'+ti)?.classList.add('today');
|
||
|
||
// Load shared class events immediately
|
||
loadClassEvents();
|
||
|
||
const r=await fetch('/api/me');
|
||
if(r.ok){ const d=await r.json(); loginUI(d.username,d.id,d.role); }
|
||
}
|
||
|
||
function loginUI(username,id,role){
|
||
document.getElementById('banner').style.display='none';
|
||
document.getElementById('card-tt-public').style.display='none';
|
||
document.getElementById('card-tt').style.display='';
|
||
document.getElementById('pair-hw-todo').style.display='grid';
|
||
document.getElementById('sidebar-btn').style.display='flex';
|
||
document.getElementById('card-tickets').style.display='';
|
||
['card-hw','card-grades','card-ab','card-todo','card-cd','card-files'].forEach(id=>document.getElementById(id).style.display='');
|
||
['btn-add-pruefung','btn-add-ferien','btn-add-ql'].forEach(id=>document.getElementById(id).style.display='');
|
||
|
||
const adminLink = role === 'admin' ? `<div class="dd-sep"></div><a class="dd-item" href="/admin">🛡️ Admin</a>` : '';
|
||
const adminBtn = role === 'admin' ? `<a href="/admin" class="btn-sm" style="background:#fef3c7;color:#92400e;border-color:#fde68a;font-weight:700">🛡️ Admin</a>` : '';
|
||
document.getElementById('h-user').innerHTML=`
|
||
${adminBtn}
|
||
<div class="avatar" onclick="toggleDropdown(this)">
|
||
${esc(username[0].toUpperCase())}
|
||
<div class="dropdown" id="user-dropdown">
|
||
<div class="dd-item meta">${esc(username)}</div>
|
||
<div class="dd-sep"></div>
|
||
<span class="dd-item" onclick="openSettings()">⚙️ Einstellungen</span>
|
||
${adminLink}
|
||
<div class="dd-sep"></div>
|
||
<span class="dd-item danger" onclick="doLogout()">Abmelden</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
loadAll();
|
||
initChat(username);
|
||
}
|
||
|
||
function toggleDropdown(el){
|
||
event.stopPropagation();
|
||
document.getElementById('user-dropdown').classList.toggle('open');
|
||
}
|
||
document.addEventListener('click',()=>document.getElementById('user-dropdown')?.classList.remove('open'));
|
||
|
||
async function doLogout(){
|
||
await fetch('/api/logout',{method:'POST'});
|
||
location.reload();
|
||
}
|
||
|
||
// ── LOAD ALL ──────────────────────────────────────────────────
|
||
async function loadAll(){
|
||
const [tt,hw,gr,ab,td,cd,ql]=await Promise.all([
|
||
api('GET','timetable'),api('GET','homework'),api('GET','grades'),
|
||
api('GET','absences'),api('GET','todos'),api('GET','countdowns'),api('GET','quicklinks')
|
||
]);
|
||
renderTT(tt); renderHW(hw); renderGrades(gr);
|
||
renderAbsences(ab); renderTodos(td); renderCountdowns(cd); renderQL(ql);
|
||
loadFiles();
|
||
loadTickets();
|
||
}
|
||
|
||
async function loadClassEvents(){
|
||
const events=await api('GET','class-events');
|
||
renderPruefungen(events.filter(e=>e.type==='pruefung'));
|
||
renderFerien(events.filter(e=>e.type==='ferien'));
|
||
}
|
||
|
||
// ── TIMETABLE ─────────────────────────────────────────────────
|
||
function renderTT(data){
|
||
const todayIdx=(new Date().getDay()+6)%7;
|
||
const grid=document.getElementById('tt-grid');
|
||
grid.innerHTML='';
|
||
const cols=6; // time col + 5 days
|
||
const rows=TIME_SLOTS.length+1; // header + slots
|
||
|
||
// Header row
|
||
const corner=document.createElement('div');
|
||
corner.className='tt-cell tt-head no-right'; corner.textContent='';
|
||
grid.appendChild(corner);
|
||
DAYS.forEach((d,i)=>{
|
||
const h=document.createElement('div');
|
||
h.className='tt-cell tt-head'+(i===todayIdx?' today-col':'')+(i===4?' no-right':'');
|
||
h.textContent=DAY_S[i]; grid.appendChild(h);
|
||
});
|
||
|
||
// Slot rows
|
||
TIME_SLOTS.forEach((slot,si)=>{
|
||
const isLast=si===TIME_SLOTS.length-1;
|
||
// Time label
|
||
const tc=document.createElement('div');
|
||
tc.className='tt-cell tt-time'+(isLast?' no-bot':'');
|
||
tc.innerHTML=`<div class="tt-time-num">${slot.label}</div><div class="tt-time-val">${slot.time}</div>`;
|
||
grid.appendChild(tc);
|
||
|
||
DAYS.forEach((day,di)=>{
|
||
const cell=document.createElement('div');
|
||
cell.className='tt-cell'+(isLast?' no-bot':'')+(di===4?' no-right':'');
|
||
const lesson=data.find(l=>l.day===day && l.time_start===slot.key);
|
||
if(lesson){
|
||
const col=subjectColor(lesson.subject||'');
|
||
const lel=document.createElement('div');
|
||
lel.className='tt-lesson';
|
||
lel.style.cssText=`background:${col.bg};border-left:2.5px solid ${col.border};color:${col.text}`;
|
||
lel.innerHTML=`<div class="tt-lesson-subj">${esc(lesson.subject||'')}</div><div class="tt-lesson-meta">${esc(lesson.room||'')}${lesson.teacher?' · '+esc(lesson.teacher):''}</div><button class="tt-del" onclick="delItem('timetable',${lesson.id})">✕</button>`;
|
||
cell.appendChild(lel);
|
||
}
|
||
grid.appendChild(cell);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── CLASS EVENTS ──────────────────────────────────────────────
|
||
function renderPruefungen(data){
|
||
const el=document.getElementById('list-pruefungen');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Prüfungen eingetragen</div>';return;}
|
||
el.innerHTML=data.map(e=>{
|
||
const diff=e.date?daysUntil(e.date):null;
|
||
let badge='', col='var(--blue)';
|
||
if(diff!==null){
|
||
if(diff<0){badge=`<span class="badge badge-green">Vorbei</span>`;col='var(--text-subtle)';}
|
||
else if(diff===0){badge=`<span class="badge badge-red">Heute</span>`;col='var(--red)';}
|
||
else if(diff<=7){badge=`<span class="badge badge-orange">in ${diff}d</span>`;col='var(--amber)';}
|
||
else{badge=`<span class="badge badge-blue">in ${diff}d</span>`;}
|
||
}
|
||
return `<div class="ev-item">
|
||
<div class="ev-days" style="color:${col}">${diff===null?'–':diff<0?'✓':diff}</div>
|
||
<div class="ev-info"><div class="ev-title">${esc(e.title)} ${badge}</div><div class="ev-date">${e.date?fmtDate(e.date):''}${e.description?' · '+esc(e.description):''}</div></div>
|
||
<button class="ev-del" onclick="delClassEvent(${e.id})">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderFerien(data){
|
||
const el=document.getElementById('list-ferien');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Termine eingetragen</div>';return;}
|
||
el.innerHTML=data.map(e=>{
|
||
const diff=e.date?daysUntil(e.date):null;
|
||
const col=diff!==null&&diff<0?'var(--text-subtle)':'var(--green)';
|
||
return `<div class="ev-item">
|
||
<div class="ev-days" style="color:${col}">${diff===null?'–':diff<0?'✓':diff}</div>
|
||
<div class="ev-info"><div class="ev-title">${esc(e.title)}</div><div class="ev-date">${e.date?fmtDate(e.date):''}${e.date_end?' – '+fmtDate(e.date_end):''}</div></div>
|
||
<button class="ev-del" onclick="delClassEvent(${e.id})">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function delClassEvent(id){
|
||
await api('DELETE','class-events/'+id);
|
||
loadClassEvents();
|
||
toast('Gelöscht');
|
||
}
|
||
|
||
// ── HOMEWORK ──────────────────────────────────────────────────
|
||
function renderHW(data){
|
||
const el=document.getElementById('list-hw');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Hausaufgaben</div>';return;}
|
||
const today=new Date(); today.setHours(0,0,0,0);
|
||
el.innerHTML=[...data].sort((a,b)=>(a.due_date||'9999').localeCompare(b.due_date||'9999')).map(h=>{
|
||
const due=h.due_date?new Date(h.due_date):null;
|
||
let badge='';
|
||
if(due&&!h.done){due.setHours(0,0,0,0);const d=Math.round((due-today)/86400000);
|
||
badge=d<0?'<span class="badge badge-red">Überfällig</span>':d===0?'<span class="badge badge-red">Heute</span>':d<=7?`<span class="badge badge-orange">in ${d}d</span>`:`<span class="badge badge-blue">in ${d}d</span>`;}
|
||
return `<div class="hw-item">
|
||
<div class="check ${h.done?'done':''}" onclick="toggle('homework',${h.id},${h.done})">${h.done?'✓':''}</div>
|
||
<div class="hw-body">
|
||
<div class="hw-title ${h.done?'crossed':''}">${esc(h.title)}</div>
|
||
<div class="hw-meta">${esc(h.subject||'')}${h.due_date?' · '+fmtDate(h.due_date):''} ${badge}</div>
|
||
</div>
|
||
<button class="del-btn" onclick="delItem('homework',${h.id})">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── GRADES ────────────────────────────────────────────────────
|
||
function renderGrades(data){
|
||
const el=document.getElementById('list-grades');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Noten eingetragen</div>';return;}
|
||
const rows=data.map(g=>{
|
||
const gn=Math.round(g.grade||0);
|
||
const typeName=GRADE_TYPES[g.type]||'Sonstiges';
|
||
return `<div class="grade-row">
|
||
<div class="grade-info">
|
||
<div class="grade-subj">${esc(g.subject)}</div>
|
||
<div class="grade-type">${typeName}${g.note?' · '+esc(g.note):''}</div>
|
||
</div>
|
||
<div class="grade-val g${Math.min(gn,6)}">${g.grade}</div>
|
||
<button class="del-btn" onclick="delItem('grades',${g.id})">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
// Weighted average
|
||
const valid=data.filter(g=>g.grade!=null);
|
||
const wSum=valid.reduce((s,g)=>s+(GRADE_WEIGHTS[g.type]||1),0);
|
||
const wGrades=valid.reduce((s,g)=>s+g.grade*(GRADE_WEIGHTS[g.type]||1),0);
|
||
const wavg=wSum?wGrades/wSum:null;
|
||
const avg=valid.length?(valid.reduce((s,g)=>s+g.grade,0)/valid.length).toFixed(2):null;
|
||
const avgBlock=avg?`<div class="grade-avg">
|
||
<div><div class="grade-avg-label">Ø gewichtet</div><div class="grade-avg-sub">ungewichtet: ${avg}</div></div>
|
||
<div class="grade-avg-val">${wavg.toFixed(2)}</div>
|
||
</div>`:'';
|
||
el.innerHTML=rows+avgBlock;
|
||
}
|
||
|
||
// ── ABSENCES ──────────────────────────────────────────────────
|
||
function renderAbsences(data){
|
||
const el=document.getElementById('list-ab');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Fehlzeiten</div>';return;}
|
||
el.innerHTML=[...data].sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(a=>
|
||
`<div class="ab-item">
|
||
<div class="ab-date">${a.date?fmtDate(a.date):'–'}</div>
|
||
<div class="ab-info"><div class="ab-subj">${esc(a.subject||'')}</div><div class="ab-reason">${esc(a.reason||'')}</div></div>
|
||
<button class="del-btn" onclick="delItem('absences',${a.id})">🗑</button>
|
||
</div>`).join('')+`<div class="ab-total">${data.length} Fehlzeit${data.length!==1?'en':''} gesamt</div>`;
|
||
}
|
||
|
||
// ── TODOS ─────────────────────────────────────────────────────
|
||
function renderTodos(data){
|
||
const el=document.getElementById('list-todo');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Aufgaben</div>';return;}
|
||
el.innerHTML=data.map(t=>`<div class="todo-item">
|
||
<div class="todo-check ${t.done?'done':''}" onclick="toggle('todos',${t.id},${t.done})">${t.done?'✓':''}</div>
|
||
<div class="todo-title ${t.done?'crossed':''}">${esc(t.title)}</div>
|
||
<button class="del-btn" onclick="delItem('todos',${t.id})">🗑</button>
|
||
</div>`).join('');
|
||
}
|
||
|
||
// ── COUNTDOWNS ────────────────────────────────────────────────
|
||
function renderCountdowns(data){
|
||
const el=document.getElementById('list-cd');
|
||
if(!data.length){el.innerHTML='<div class="empty">Keine Countdowns</div>';return;}
|
||
el.innerHTML=[...data].sort((a,b)=>(a.target_date||'').localeCompare(b.target_date||'')).map(c=>{
|
||
const d=daysUntil(c.target_date);
|
||
return `<div class="cd-item">
|
||
<div class="cd-days ${d<0?'past':''}">${d<0?'✓':d}</div>
|
||
<div class="cd-info"><div class="cd-title">${esc(c.title)}</div><div class="cd-date">${fmtDate(c.target_date)}</div></div>
|
||
<button class="del-btn" onclick="delItem('countdowns',${c.id})">🗑</button>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── QUICKLINKS ────────────────────────────────────────────────
|
||
function renderQL(data){
|
||
if(!data.length)return;
|
||
const grid=document.getElementById('ql-grid');
|
||
data.forEach(l=>{
|
||
const a=document.createElement('a');
|
||
a.className='ql-item'; a.href=l.url; a.target='_blank'; a.rel='noopener';
|
||
a.innerHTML=`<span class="ql-icon">🔗</span>${esc(l.label)}<button class="ql-del" onclick="event.preventDefault();delItem('quicklinks',${l.id})">✕</button>`;
|
||
grid.appendChild(a);
|
||
});
|
||
}
|
||
|
||
// ── CRUD HELPERS ──────────────────────────────────────────────
|
||
async function delItem(type,id){
|
||
await api('DELETE',`${type}/${id}`);
|
||
loadAll(); toast('Gelöscht');
|
||
}
|
||
async function toggle(type,id,current){
|
||
await api('PUT',`${type}/${id}`,{done:current?0:1});
|
||
loadAll();
|
||
}
|
||
|
||
// ── MODAL CONFIG ──────────────────────────────────────────────
|
||
const MODALS={
|
||
timetable:{title:'Stunde hinzufügen',fields:[
|
||
{n:'day',l:'Tag',t:'select',opts:DAYS},
|
||
{n:'time_start',l:'Uhrzeit',t:'select',opts:TIME_SLOTS.map(s=>s.key),labels:TIME_SLOTS.map(s=>`${s.label} Std. – ${s.time}`)},
|
||
{n:'subject',l:'Fach',t:'text'},{n:'room',l:'Raum',t:'text'},{n:'teacher',l:'Lehrkraft',t:'text'}
|
||
]},
|
||
homework:{title:'Hausaufgabe hinzufügen',fields:[
|
||
{n:'title',l:'Titel',t:'text'},{n:'subject',l:'Fach',t:'text'},{n:'due_date',l:'Fällig am',t:'date'}
|
||
]},
|
||
grades:{title:'Note hinzufügen',fields:[
|
||
{n:'subject',l:'Fach',t:'text'},
|
||
{n:'grade',l:'Note (1–6)',t:'number',min:1,max:6,step:0.1},
|
||
{n:'type',l:'Typ',t:'select',opts:Object.keys(GRADE_TYPES),labels:Object.values(GRADE_TYPES)},
|
||
{n:'note',l:'Anmerkung (optional)',t:'text'}
|
||
]},
|
||
absences:{title:'Fehlzeit eintragen',fields:[
|
||
{n:'date',l:'Datum',t:'date'},{n:'subject',l:'Fach / Stunde',t:'text'},{n:'reason',l:'Grund',t:'text'}
|
||
]},
|
||
todos:{title:'Aufgabe hinzufügen',fields:[{n:'title',l:'Aufgabe',t:'text'}]},
|
||
countdowns:{title:'Countdown hinzufügen',fields:[
|
||
{n:'title',l:'Bezeichnung',t:'text'},{n:'target_date',l:'Datum',t:'date'}
|
||
]},
|
||
quicklinks:{title:'Link hinzufügen',fields:[
|
||
{n:'label',l:'Bezeichnung',t:'text'},{n:'url',l:'URL',t:'url'}
|
||
]},
|
||
'class-events-pruefung':{title:'Prüfung eintragen',fields:[
|
||
{n:'title',l:'Bezeichnung',t:'text'},{n:'date',l:'Datum',t:'date'},{n:'description',l:'Anmerkung (optional)',t:'text'}
|
||
]},
|
||
'class-events-ferien':{title:'Ferienblock eintragen',fields:[
|
||
{n:'title',l:'Bezeichnung',t:'text'},{n:'date',l:'Von',t:'date'},{n:'date_end',l:'Bis (optional)',t:'date'}
|
||
]},
|
||
};
|
||
|
||
function openModal(type, sub){
|
||
const key = sub ? `${type}-${sub}` : type;
|
||
currentModal=type; currentModalSub=sub||null;
|
||
const cfg=MODALS[key];
|
||
document.getElementById('modal-title').textContent=cfg.title;
|
||
document.getElementById('modal-fields').innerHTML=cfg.fields.map(f=>{
|
||
if(f.t==='select'){
|
||
const opts=(f.opts||[]).map((o,i)=>`<option value="${esc(o)}">${esc(f.labels?f.labels[i]:o)}</option>`).join('');
|
||
return `<select name="${f.n}"><option value="">– ${f.l} –</option>${opts}</select>`;
|
||
}
|
||
const ex=f.min!=null?` min="${f.min}" max="${f.max}" step="${f.step}"`:'';
|
||
return `<input type="${f.t}" name="${f.n}" placeholder="${f.l}"${ex}>`;
|
||
}).join('');
|
||
document.getElementById('modal-overlay').style.display='flex';
|
||
document.querySelector('#modal-fields input, #modal-fields select')?.focus();
|
||
}
|
||
|
||
function closeModal(){document.getElementById('modal-overlay').style.display='none';currentModal=null;currentModalSub=null;}
|
||
|
||
async function saveModal(){
|
||
if(!currentModal)return;
|
||
const key=currentModalSub?`${currentModal}-${currentModalSub}`:currentModal;
|
||
const cfg=MODALS[key];
|
||
const body={};
|
||
if(currentModalSub) body.type=currentModalSub;
|
||
cfg.fields.forEach(f=>{body[f.n]=document.querySelector(`#modal-fields [name="${f.n}"]`)?.value||null;});
|
||
await api('POST', currentModal==='class-events'?'class-events':currentModal, body);
|
||
closeModal();
|
||
if(currentModal==='class-events'||['pruefung','ferien'].includes(currentModalSub)) loadClassEvents();
|
||
else loadAll();
|
||
toast('Gespeichert ✓','success');
|
||
}
|
||
|
||
// ── SETTINGS ──────────────────────────────────────────────────
|
||
function openSettings(){
|
||
document.getElementById('settings-overlay').style.display='flex';
|
||
document.getElementById('user-dropdown')?.classList.remove('open');
|
||
}
|
||
function closeSettings(){document.getElementById('settings-overlay').style.display='none';}
|
||
|
||
async function changePassword(){
|
||
const cp=document.getElementById('pw-current').value;
|
||
const np=document.getElementById('pw-new').value;
|
||
const r=await api('PUT','me/password',{currentPassword:cp,newPassword:np});
|
||
if(r.ok){toast('Passwort geändert','success');document.getElementById('pw-current').value='';document.getElementById('pw-new').value='';}
|
||
else toast(r.error,'error');
|
||
}
|
||
|
||
async function deleteAccount(){
|
||
const pw=document.getElementById('pw-delete').value;
|
||
if(!confirm('Account und alle Daten wirklich löschen? Dies kann nicht rückgängig gemacht werden.'))return;
|
||
const r=await api('DELETE','me',{password:pw});
|
||
if(r.ok){location.reload();}
|
||
else toast(r.error,'error');
|
||
}
|
||
|
||
// ── CHAT ──────────────────────────────────────────────────────
|
||
let chatLastId = 0;
|
||
let chatPollTimer = null;
|
||
let chatMyUsername = '';
|
||
|
||
function chatFmtTime(ts) {
|
||
if (!ts) return '';
|
||
const d = new Date(ts + 'Z');
|
||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
|
||
function renderChatMsg(m, append) {
|
||
const el = document.getElementById('chat-msgs');
|
||
const isOwn = m.username === chatMyUsername;
|
||
const div = document.createElement('div');
|
||
div.className = 'chat-msg';
|
||
div.dataset.id = m.id;
|
||
div.innerHTML = `<div class="chat-msg-meta">
|
||
<span class="chat-msg-user${isOwn ? ' own' : ''}">${esc(m.username)}</span>
|
||
<span class="chat-msg-time">${chatFmtTime(m.created_at)}</span>
|
||
<button class="chat-msg-del" onclick="delChatMsg(${m.id})" title="Löschen">✕</button>
|
||
</div>
|
||
<div class="chat-msg-body">${esc(m.content)}</div>`;
|
||
if (append) {
|
||
el.appendChild(div);
|
||
el.scrollTop = el.scrollHeight;
|
||
} else {
|
||
el.prepend(div);
|
||
}
|
||
}
|
||
|
||
async function loadChat() {
|
||
const msgs = await api('GET', 'chat');
|
||
const el = document.getElementById('chat-msgs');
|
||
el.innerHTML = '';
|
||
msgs.forEach(m => renderChatMsg(m, true));
|
||
if (msgs.length) chatLastId = msgs[msgs.length - 1].id;
|
||
}
|
||
|
||
async function pollChat() {
|
||
try {
|
||
const msgs = await api('GET', 'chat?after=' + chatLastId);
|
||
msgs.forEach(m => {
|
||
renderChatMsg(m, true);
|
||
chatLastId = Math.max(chatLastId, m.id);
|
||
});
|
||
} catch {}
|
||
chatPollTimer = setTimeout(pollChat, 3000);
|
||
}
|
||
|
||
async function sendChatMsg() {
|
||
const inp = document.getElementById('chat-input');
|
||
const content = inp.value.trim();
|
||
if (!content) return;
|
||
inp.value = '';
|
||
const r = await api('POST', 'chat', { content });
|
||
if (r.error) { toast(r.error, 'error'); return; }
|
||
renderChatMsg(r, true);
|
||
chatLastId = Math.max(chatLastId, r.id);
|
||
}
|
||
|
||
async function delChatMsg(id) {
|
||
if (!confirm('Nachricht wirklich löschen?')) return;
|
||
const r = await api('DELETE', 'chat/' + id);
|
||
if (r.error) { toast(r.error, 'error'); return; }
|
||
document.querySelector(`.chat-msg[data-id="${id}"]`)?.remove();
|
||
}
|
||
|
||
function initChat(username) {
|
||
chatMyUsername = username;
|
||
document.getElementById('card-chat').style.display = '';
|
||
loadChat().then(() => pollChat());
|
||
document.getElementById('chat-input').addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMsg(); }
|
||
});
|
||
}
|
||
|
||
// ── FILE STORAGE ──────────────────────────────────────────────
|
||
let filesData = { files: [], used: 0, quota: 2 * 1024 * 1024 * 1024 };
|
||
|
||
function fmtBytes(b) {
|
||
if (b >= 1024 * 1024 * 1024) return (b / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
||
if (b >= 1024 * 1024) return (b / 1024 / 1024).toFixed(1) + ' MB';
|
||
if (b >= 1024) return (b / 1024).toFixed(1) + ' KB';
|
||
return b + ' B';
|
||
}
|
||
|
||
function fileIcon(mime) {
|
||
if (mime.startsWith('image/')) return '🖼️';
|
||
if (mime === 'application/pdf') return '📕';
|
||
if (mime.includes('spreadsheet') || mime.includes('excel') || mime === 'text/csv') return '📊';
|
||
if (mime.includes('presentation') || mime.includes('powerpoint')) return '📋';
|
||
if (mime.includes('word') || mime.includes('document')) return '📝';
|
||
if (['application/zip','application/vnd.rar','application/x-7z-compressed','application/x-tar','application/gzip'].includes(mime)) return '🗜️';
|
||
if (mime === 'application/json' || mime === 'application/xml' || mime === 'text/xml') return '🔧';
|
||
return '📄';
|
||
}
|
||
|
||
async function loadFiles() {
|
||
try {
|
||
const r = await fetch('/api/files');
|
||
if (!r.ok) return;
|
||
filesData = await r.json();
|
||
renderFiles();
|
||
} catch {}
|
||
}
|
||
|
||
function renderFiles() {
|
||
const el = document.getElementById('files-body');
|
||
const pct = filesData.quota > 0 ? Math.min(100, filesData.used / filesData.quota * 100) : 0;
|
||
const fillClass = pct >= 90 ? 'danger' : pct >= 70 ? 'warn' : '';
|
||
const quotaHtml = `<div class="files-quota">
|
||
<div class="files-quota-bar"><div class="files-quota-fill ${fillClass}" style="width:${pct.toFixed(1)}%"></div></div>
|
||
<div class="files-quota-text"><span>${fmtBytes(filesData.used)} genutzt</span><span>${fmtBytes(filesData.quota)} Speicher</span></div>
|
||
</div>`;
|
||
const dropZone = `<div class="upload-drop" id="files-drop">Dateien hier ablegen oder oben hochladen</div>`;
|
||
if (!filesData.files.length) {
|
||
el.innerHTML = quotaHtml + dropZone + '<div class="empty">Noch keine Dateien hochgeladen</div>';
|
||
} else {
|
||
el.innerHTML = quotaHtml + dropZone + filesData.files.map(f => `<div class="file-item">
|
||
<div class="file-icon">${fileIcon(f.mime_type)}</div>
|
||
<div class="file-info">
|
||
<div class="file-name" title="${esc(f.original_name)}">${esc(f.original_name)}</div>
|
||
<div class="file-meta">${fmtBytes(f.size)} · ${fmtDate(f.created_at ? f.created_at.slice(0,10) : '')}</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-shrink:0">
|
||
<button class="file-dl" onclick="downloadFile(${f.id})">↓ Laden</button>
|
||
<button class="del-btn" onclick="deleteFile(${f.id})">🗑</button>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
initDropZone();
|
||
}
|
||
|
||
function initDropZone() {
|
||
const zone = document.getElementById('files-drop');
|
||
if (!zone) return;
|
||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag'); });
|
||
zone.addEventListener('dragleave', () => zone.classList.remove('drag'));
|
||
zone.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
zone.classList.remove('drag');
|
||
const files = Array.from(e.dataTransfer.files);
|
||
if (files.length) uploadFileList(files);
|
||
});
|
||
}
|
||
|
||
function downloadFile(id) {
|
||
window.location.href = '/api/files/' + id + '/download';
|
||
}
|
||
|
||
async function deleteFile(id) {
|
||
if (!confirm('Datei wirklich löschen?')) return;
|
||
const r = await fetch('/api/files/' + id, { method: 'DELETE' });
|
||
const d = await r.json();
|
||
if (d.error) { toast(d.error, 'error'); return; }
|
||
toast('Datei gelöscht');
|
||
loadFiles();
|
||
}
|
||
|
||
async function uploadFiles(event) {
|
||
const files = Array.from(event.target.files);
|
||
document.getElementById('file-input').value = '';
|
||
if (!files.length) return;
|
||
await uploadFileList(files);
|
||
}
|
||
|
||
async function uploadFileList(files) {
|
||
for (const file of files) {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
try {
|
||
const r = await fetch('/api/files', { method: 'POST', body: fd });
|
||
const d = await r.json();
|
||
if (d.error) toast(d.error, 'error');
|
||
else toast(`${esc(file.name)} hochgeladen ✓`);
|
||
} catch {
|
||
toast('Upload fehlgeschlagen', 'error');
|
||
}
|
||
}
|
||
loadFiles();
|
||
}
|
||
|
||
// ── SUPPORT TICKETS ───────────────────────────────────────────
|
||
let ticketsData = [];
|
||
let activeTicket = null;
|
||
let myUserId = null;
|
||
|
||
function fmtDateTime(s){
|
||
if(!s)return'';
|
||
const d=new Date(s);
|
||
return d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'})+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'});
|
||
}
|
||
|
||
async function loadTickets(){
|
||
const r = await api('GET','tickets');
|
||
if(r.error) return;
|
||
ticketsData = r;
|
||
renderTickets();
|
||
}
|
||
|
||
function ticketStatusBadge(s){
|
||
const m={open:'badge-red',in_progress:'badge-orange',closed:'badge-green'};
|
||
const l={open:'Offen',in_progress:'In Bearbeitung',closed:'Geschlossen'};
|
||
return `<span class="badge ${m[s]||'badge-gray'}">${l[s]||esc(s)}</span>`;
|
||
}
|
||
|
||
function renderTickets(){
|
||
const el=document.getElementById('list-tickets');
|
||
if(!ticketsData.length){ el.innerHTML='<div class="empty">Noch keine Tickets erstellt</div>'; return; }
|
||
el.innerHTML=ticketsData.map(t=>`
|
||
<div class="ticket-list-item" onclick="openThread(${t.id})">
|
||
<div style="flex:1;min-width:0">
|
||
<div class="ticket-list-subj">${esc(t.subject)}</div>
|
||
<div class="ticket-list-meta">${fmtDateTime(t.created_at)}</div>
|
||
</div>
|
||
${ticketStatusBadge(t.status)}
|
||
</div>`).join('');
|
||
}
|
||
|
||
function openNewTicket(){
|
||
document.getElementById('new-ticket-subject').value='';
|
||
document.getElementById('new-ticket-message').value='';
|
||
document.getElementById('new-ticket-overlay').style.display='flex';
|
||
setTimeout(()=>document.getElementById('new-ticket-subject').focus(),50);
|
||
}
|
||
function closeNewTicket(){ document.getElementById('new-ticket-overlay').style.display='none'; }
|
||
|
||
async function submitNewTicket(){
|
||
const subject=document.getElementById('new-ticket-subject').value.trim();
|
||
const message=document.getElementById('new-ticket-message').value.trim();
|
||
if(!subject||!message){ toast('Betreff und Nachricht erforderlich','error'); return; }
|
||
const r=await api('POST','tickets',{subject,message});
|
||
if(r.error){ toast(r.error,'error'); return; }
|
||
closeNewTicket();
|
||
toast('Ticket erstellt');
|
||
await loadTickets();
|
||
openThread(r.id);
|
||
}
|
||
|
||
async function openThread(ticketId){
|
||
const t=ticketsData.find(x=>x.id===ticketId); if(!t) return;
|
||
activeTicket=t;
|
||
document.getElementById('thread-title').textContent=t.subject;
|
||
document.getElementById('thread-meta').textContent=`Status: ${({open:'Offen',in_progress:'In Bearbeitung',closed:'Geschlossen'}[t.status]||t.status)} · Erstellt: ${fmtDateTime(t.created_at)}`;
|
||
document.getElementById('thread-msgs').innerHTML='<div class="empty" style="padding:8px 0">Laden…</div>';
|
||
const ra=document.getElementById('thread-reply-area');
|
||
ra.style.display=t.status==='closed'?'none':'flex';
|
||
document.getElementById('thread-overlay').style.display='flex';
|
||
|
||
const msgs=await api('GET',`tickets/${ticketId}/messages`);
|
||
renderThread(t, msgs.error?[]:msgs);
|
||
}
|
||
|
||
function renderThread(ticket, messages){
|
||
const wrap=document.getElementById('thread-msgs');
|
||
const rows=[];
|
||
// Initial message from ticket itself
|
||
rows.push(`<div style="display:flex;flex-direction:column;align-items:flex-end">
|
||
<div class="thread-bubble mine">${esc(ticket.message)}</div>
|
||
<div class="thread-bubble-meta" style="text-align:right">${fmtDateTime(ticket.created_at)}</div>
|
||
</div>`);
|
||
messages.forEach(m=>{
|
||
const isAdmin=m.role==='admin';
|
||
const isMine=!isAdmin;
|
||
rows.push(`<div style="display:flex;flex-direction:column;align-items:${isMine?'flex-end':'flex-start'}">
|
||
<div class="thread-bubble ${isMine?'mine':'other'}${isAdmin?' admin-bubble':''}">${esc(m.message)}</div>
|
||
<div class="thread-bubble-meta" style="text-align:${isMine?'right':'left'}">${isAdmin?'🛡️ Admin · ':''}${fmtDateTime(m.created_at)}</div>
|
||
</div>`);
|
||
});
|
||
if(!rows.length){ wrap.innerHTML='<div class="empty" style="padding:8px 0">Noch keine Nachrichten</div>'; return; }
|
||
wrap.innerHTML=rows.join('');
|
||
wrap.scrollTop=wrap.scrollHeight;
|
||
}
|
||
|
||
async function sendThreadReply(){
|
||
if(!activeTicket) return;
|
||
const inp=document.getElementById('thread-input');
|
||
const message=inp.value.trim();
|
||
if(!message){ toast('Nachricht darf nicht leer sein','error'); return; }
|
||
const r=await api('POST',`tickets/${activeTicket.id}/messages`,{message});
|
||
if(r.error){ toast(r.error,'error'); return; }
|
||
inp.value='';
|
||
await loadTickets();
|
||
const t=ticketsData.find(x=>x.id===activeTicket.id);
|
||
if(t){ activeTicket=t; document.getElementById('thread-meta').textContent=`Status: ${({open:'Offen',in_progress:'In Bearbeitung',closed:'Geschlossen'}[t.status]||t.status)} · Erstellt: ${fmtDateTime(t.created_at)}`; }
|
||
const msgs=await api('GET',`tickets/${activeTicket.id}/messages`);
|
||
renderThread(activeTicket, msgs.error?[]:msgs);
|
||
}
|
||
|
||
function closeThread(){
|
||
document.getElementById('thread-overlay').style.display='none';
|
||
activeTicket=null;
|
||
}
|
||
|
||
// ── DARK MODE ─────────────────────────────────────────────────
|
||
function toggleDark(){
|
||
const dark=document.body.classList.toggle('dark');
|
||
localStorage.setItem('dark',dark?'1':'');
|
||
document.getElementById('btn-dark').textContent=dark?'☀️':'🌙';
|
||
}
|
||
if(localStorage.getItem('dark')){
|
||
document.body.classList.add('dark');
|
||
document.getElementById('btn-dark').textContent='☀️';
|
||
}
|
||
|
||
// ── SIDEBAR ───────────────────────────────────────────────────
|
||
function openSidebar(){
|
||
document.getElementById('sidebar').classList.add('open');
|
||
document.getElementById('sidebar-backdrop').classList.add('open');
|
||
document.body.style.overflow='hidden';
|
||
}
|
||
function closeSidebar(){
|
||
document.getElementById('sidebar').classList.remove('open');
|
||
document.getElementById('sidebar-backdrop').classList.remove('open');
|
||
document.body.style.overflow='';
|
||
}
|
||
|
||
// ── KEYBOARD ──────────────────────────────────────────────────
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key==='Escape'){closeModal();closeSettings();closeSidebar();closeThread();closeNewTicket();}
|
||
});
|
||
|
||
// ── START ─────────────────────────────────────────────────────
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|