From 3b8e5c2b0949fa6737e8fae3e3d4c332b6df0b11 Mon Sep 17 00:00:00 2001 From: DOMINIK SCHRADER Date: Mon, 1 Dec 2025 11:32:31 +0100 Subject: [PATCH] Initial Commit --- css/styles.css | 858 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 161 ++++++++++ js/api.js | 62 ++++ js/app.js | 506 +++++++++++++++++++++++++++++ js/config.js | 15 + 5 files changed, 1602 insertions(+) create mode 100644 css/styles.css create mode 100644 index.html create mode 100644 js/api.js create mode 100644 js/app.js create mode 100644 js/config.js diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..fd85c25 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,858 @@ +:root { + --primary: #2563eb; + --primary-hover: #1d4ed8; + --primary-light: #3b82f6; + + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-card: #ffffff; + --bg-hover: #f1f5f9; + + --text-primary: #0f172a; + --text-secondary: #475569; + --text-tertiary: #64748b; + + --border: #e2e8f0; + --border-light: #cbd5e1; + + --success: #10b981; + --danger: #ef4444; + --warning: #f59e0b; + + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} + +[data-theme="dark"] { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-card: #1e293b; + --bg-hover: #334155; + + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + + --border: #334155; + --border-light: #475569; + + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.5); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.6); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg-secondary); + color: var(--text-primary); + line-height: 1.6; + transition: background-color 0.3s ease, color 0.3s ease; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header */ +header { + background: var(--bg-primary); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 1000; + transition: background-color 0.3s ease; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 1.5rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.025em; +} + +.theme-toggle { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + font-size: 1.2rem; +} + +.theme-toggle:hover { + background: var(--bg-hover); + transform: scale(1.05); +} + +/* Navigation */ +nav { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem 1rem; + display: flex; + gap: 0.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.nav-btn { + background: transparent; + border: none; + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + transition: all 0.2s ease; + white-space: nowrap; +} + +.nav-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.nav-btn.active { + background: var(--primary); + color: white; +} + +/* Main Content */ +main { + flex: 1; + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + width: 100%; +} + +.view { + display: none; +} + +.view.active { + display: block; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +h2 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--text-primary); + letter-spacing: -0.025em; +} + +.section-subtitle { + color: var(--text-tertiary); + margin-bottom: 2rem; + font-size: 0.9375rem; +} + +h3 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-primary); +} + +/* Form Styles */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.tooltip { + position: relative; + cursor: help; + color: var(--primary); + margin-left: 0.25rem; +} + +.tooltip::after { + content: attr(data-tip); + position: absolute; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + padding: 0.5rem 0.75rem; + background: var(--text-primary); + color: var(--bg-primary); + border-radius: 0.375rem; + font-size: 0.75rem; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + z-index: 100; + box-shadow: var(--shadow-lg); +} + +.tooltip:hover::after { + opacity: 1; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + font-size: 1rem; + background: var(--bg-card); + color: var(--text-primary); + transition: all 0.2s ease; + font-family: inherit; +} + +.form-group input::placeholder { + color: var(--text-tertiary); +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.form-group input:hover:not(:focus), +.form-group select:hover:not(:focus) { + border-color: var(--border-light); +} + +.error-message { + display: block; + color: var(--danger); + font-size: 0.75rem; + margin-top: 0.25rem; + min-height: 1rem; +} + +/* Loading */ +.loading { + text-align: center; + padding: 2rem; + color: var(--text-tertiary); +} + +.loading::after { + content: ""; + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + margin-left: 0.5rem; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Kurse Container */ +.kurse-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.kurs-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +.kurs-card:hover:not(.voll) { + border-color: var(--primary); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.kurs-card.selected { + border-color: var(--primary); + background: rgba(37, 99, 235, 0.05); + box-shadow: var(--shadow-md); +} + +.kurs-card.voll { + opacity: 0.5; + cursor: not-allowed; +} + +.kurs-card label { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + margin-bottom: 0.75rem; +} + +.kurs-card input[type="checkbox"] { + width: 1.25rem; + height: 1.25rem; + cursor: pointer; + accent-color: var(--primary); +} + +.kurs-card input[type="checkbox"]:checked { + animation: checkboxPop 0.2s ease; +} + +@keyframes checkboxPop { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.15); + } +} + +.kurs-card strong { + font-size: 1.125rem; + color: var(--text-primary); + font-weight: 600; +} + +.kurs-info { + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.kurs-info p { + margin-bottom: 0.5rem; +} + +.kurs-info strong { + font-size: 0.875rem; + color: var(--text-primary); + font-weight: 600; +} + +/* Buttons */ +.actions { + margin-top: 1.5rem; + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.btn-primary { + background: var(--primary); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover:not(:disabled) { + background: var(--primary-hover); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-secondary { + background: transparent; + color: var(--primary); + border: 1px solid var(--primary); + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-secondary:hover { + background: var(--primary); + color: white; +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-ghost:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.btn-loader { + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-right: 0.5rem; +} + +/* Messages */ +.meldung { + margin-top: 2rem; + padding: 1rem 1.25rem; + border-radius: 0.75rem; + border-left: 4px solid; + animation: slideIn 0.3s ease; + box-shadow: var(--shadow-sm); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.meldung h3 { + margin-bottom: 0.5rem; + font-size: 1rem; + font-weight: 600; +} + +.meldung p { + font-size: 0.9375rem; + line-height: 1.6; +} + +.meldung.erfolg { + background: rgba(16, 185, 129, 0.1); + border-color: var(--success); + color: var(--text-primary); +} + +.meldung.fehler { + background: rgba(239, 68, 68, 0.1); + border-color: var(--danger); + color: var(--text-primary); +} + +.meldungs-actions { + margin-top: 1rem; + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.hidden { + display: none !important; +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--border); + padding-bottom: 0.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.tab-btn { + background: transparent; + border: none; + padding: 0.625rem 1.25rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + border-radius: 0.5rem; + transition: all 0.2s ease; + white-space: nowrap; +} + +.tab-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.tab-btn.active { + background: var(--primary); + color: white; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; + animation: fadeIn 0.2s ease; +} + +/* Tables */ +.bericht-table { + width: 100%; + border-collapse: collapse; + background: var(--bg-card); + border-radius: 0.75rem; + overflow: hidden; + box-shadow: var(--shadow-sm); + border: 1px solid var(--border); +} + +.bericht-table th, +.bericht-table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.bericht-table thead th { + background: var(--bg-secondary); + font-weight: 600; + font-size: 0.875rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.bericht-table tbody tr { + transition: background 0.15s ease; +} + +.bericht-table tbody tr:hover { + background: var(--bg-hover); +} + +.bericht-table tbody tr:last-child td { + border-bottom: none; +} + +.bericht-table td { + font-size: 0.9375rem; + color: var(--text-secondary); +} + +.bericht-table td strong { + color: var(--text-primary); + font-weight: 600; +} + +.bericht-table td small { + color: var(--text-tertiary); + font-size: 0.8125rem; +} + +.detail-liste { + list-style: none; + padding: 0; + margin: 0; +} + +.detail-liste li { + padding: 0.25rem 0; + font-size: 0.875rem; +} + +/* Rechnung */ +.rechnung { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 2rem; + margin-top: 1.5rem; + box-shadow: var(--shadow-sm); +} + +.rechnung-header { + display: flex; + justify-content: space-between; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); +} + +.rechnung-header h3 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + font-weight: 700; +} + +.rechnung-header p, +.rechnung p { + margin: 0.25rem 0; + color: var(--text-secondary); + font-size: 0.9375rem; +} + +.rechnung h4 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.rechnung-table { + width: 100%; + margin-top: 1.5rem; + border-collapse: collapse; + border: 1px solid var(--border); + border-radius: 0.5rem; + overflow: hidden; +} + +.rechnung-table th, +.rechnung-table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.rechnung-table th { + background: var(--bg-secondary); + font-weight: 600; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.rechnung-table tbody tr:last-child td { + border-bottom: none; +} + +.rechnung-table td { + font-size: 0.9375rem; + color: var(--text-secondary); +} + +.rechnung-summe { + margin-top: 1.5rem; + text-align: right; + border-top: 2px solid var(--border); + padding-top: 1rem; + font-variant-numeric: tabular-nums; +} + +.rechnung-summe div { + padding: 0.5rem 0; + font-size: 1rem; + color: var(--text-secondary); +} + +.rechnung-summe .gesamt { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary); + margin-top: 0.5rem; +} + +/* Footer */ +footer { + background: var(--bg-primary); + border-top: 1px solid var(--border); + padding: 1.5rem 2rem; + text-align: center; + color: var(--text-tertiary); + font-size: 0.875rem; + transition: background-color 0.3s ease; +} + +/* Inline helpers */ +.inline { + display: flex; + gap: 0.75rem; + align-items: center; +} + +/* Print styles */ +@media print { + body { + background: white; + } + .container { + background: white; + } + header, + nav, + .btn-primary, + footer { + display: none; + } + main { + padding: 20px; + background: white; + } + .rechnung { + border-color: #000; + background: white; + color: #000; + } + .rechnung * { + color: #000 !important; + } +} + +/* Responsive */ +@media (max-width: 1024px) { + main { + padding: 1.5rem; + } + .header-content, + nav { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +@media (max-width: 768px) { + main { + padding: 1rem; + } + + .header-content, + nav { + padding-left: 1rem; + padding-right: 1rem; + } + + h2 { + font-size: 1.5rem; + } + + .nav-btn { + padding: 0.5rem 1rem; + font-size: 0.8125rem; + } + + .kurse-container { + grid-template-columns: 1fr; + } + + .rechnung { + padding: 1.5rem 1rem; + } + + .rechnung-header { + flex-direction: column; + gap: 1rem; + } + + .meldungs-actions { + flex-direction: column; + } + + .inline { + flex-direction: column; + align-items: stretch; + } + + .bericht-table th, + .bericht-table td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } + + .form-group input, + .form-group select { + font-size: 16px; /* prevents iOS zoom */ + } +} + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +*:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..fcc84d5 --- /dev/null +++ b/index.html @@ -0,0 +1,161 @@ + + + + + + Sportarten-Anmeldung | Schulverwaltung + + + +
+
+
+

Sportarten-Anmeldung

+
+ + +
+
+ +
+ +
+
+

Kursanmeldung für Schüler

+

Wähle Betrieb und Sportarten aus. Mehrfachauswahl ist möglich.

+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ +
+
Kurse werden geladen...
+
+ +
+
+ +
+
+ + + + +
+ +
+

Anmeldung Schulleitung

+

Bitte melden Sie sich an, um Berichte und Rechnungen zu sehen.

+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+

Berichte für Schulleitung

+

Abruf der aktuellen Teilnehmerzahlen pro Betrieb und pro Kurs.

+
+ + +
+
+

Übersicht nach Betrieben

+
+
Bericht wird geladen...
+
+
+
+

Übersicht nach Kursen

+
+
Bericht wird geladen...
+
+
+
+ +
+

Rechnungen erstellen

+

Wählen Sie einen Betrieb und generieren Sie eine aktuelle Rechnung.

+
+ +
+ + +
+
+
+
+
+ +
+

© 2024 Schulverwaltung - Alle Rechte vorbehalten

+
+
+ + + + + + + \ No newline at end of file diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..e002169 --- /dev/null +++ b/js/api.js @@ -0,0 +1,62 @@ +const API = { + async request(endpoint, options = {}) { + const url = `${API_CONFIG.baseUrl}${endpoint}`; + try { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + credentials: 'include', + ...options, + }); + const text = await response.text(); + if (!response.ok) { + let error; + try { + error = JSON.parse(text); + } catch { + error = { message: text || 'Unbekannter Fehler' }; + } + throw new Error(error.message || 'API-Fehler'); + } + return text ? JSON.parse(text) : null; + } catch (err) { + console.error('API error:', err); + throw err; + } + }, + getKurse() { + return this.request(API_CONFIG.endpoints.kurse); + }, + getBetriebe() { + return this.request(API_CONFIG.endpoints.betriebe); + }, + anmelden(data) { + return this.request(API_CONFIG.endpoints.anmeldung, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + getBerichtBetrieb() { + return this.request(API_CONFIG.endpoints.berichtBetrieb); + }, + getBerichtKurs() { + return this.request(API_CONFIG.endpoints.berichtKurs); + }, + getRechnung(betriebId) { + return this.request(`${API_CONFIG.endpoints.rechnung}${betriebId}`); + }, + login(email, password) { + return this.request(API_CONFIG.endpoints.login, { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + }, + logout() { + return this.request(API_CONFIG.endpoints.logout, { method: 'POST' }); + }, + me() { + return this.request(API_CONFIG.endpoints.me); + }, +}; \ No newline at end of file diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..b370ed2 --- /dev/null +++ b/js/app.js @@ -0,0 +1,506 @@ +// Hauptanwendung +class SportAnmeldungApp { + constructor() { + this.kurse = []; + this.betriebe = []; + this.user = null; + this.init(); + } + + async init() { + this.setupThemeToggle(); + this.setupNavigation(); + this.setupTabs(); + this.setupLoginHandlers(); + this.setupLogoutHandler(); + + await this.checkAuthAndToggleUI(); + + await this.loadInitialData(); + this.setupFormHandlers(); + } + + setupThemeToggle() { + const toggle = document.getElementById('theme-toggle'); + const icon = toggle.querySelector('.theme-icon'); + + const savedTheme = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', savedTheme); + icon.textContent = savedTheme === 'dark' ? '☀️' : '🌙'; + toggle.setAttribute('aria-pressed', savedTheme === 'dark' ? 'true' : 'false'); + + toggle.addEventListener('click', () => { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + icon.textContent = newTheme === 'dark' ? '☀️' : '🌙'; + toggle.setAttribute('aria-pressed', newTheme === 'dark' ? 'true' : 'false'); + }); + } + + setupNavigation() { + const navButtons = document.querySelectorAll('.nav-btn'); + navButtons.forEach((btn) => { + btn.addEventListener('click', (e) => { + const view = e.target.dataset.view; + this.switchView(view); + }); + }); + } + + switchView(viewName) { + document.querySelectorAll('.view').forEach((v) => v.classList.remove('active')); + document.querySelectorAll('.nav-btn').forEach((b) => b.classList.remove('active')); + + const view = document.getElementById(`${viewName}-view`); + if (view) view.classList.add('active'); + document.querySelector(`[data-view="${viewName}"]`)?.classList.add('active'); + + if (viewName === 'berichte') { + this.loadBerichte(); + } else if (viewName === 'rechnungen') { + this.loadRechnungsOptionen(); + } + } + + setupTabs() { + const tabButtons = document.querySelectorAll('.tab-btn'); + tabButtons.forEach((btn) => { + btn.addEventListener('click', (e) => { + const tab = e.target.dataset.tab; + this.switchTab(tab); + }); + }); + } + + switchTab(tabName) { + document.querySelectorAll('.tab-content').forEach((t) => t.classList.remove('active')); + document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active')); + + document.getElementById(`${tabName}-tab`).classList.add('active'); + document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); + } + + async checkAuthAndToggleUI() { + try { + const me = await API.me(); + this.user = me.authenticated ? me.user : null; + + const isAdmin = !!this.user && this.user.role === 'admin'; + document.querySelectorAll('.admin-only').forEach((el) => { + el.classList.toggle('hidden', !isAdmin); + }); + + document.getElementById('logout-btn').classList.toggle('hidden', !this.user); + document.getElementById('login-tab-btn').classList.toggle('hidden', !!this.user); + + // Wenn aktuell Berichte/Rechnungen aktiv sind, aber nicht admin → auf Anmeldung umschalten + const activeView = document.querySelector('.view.active')?.id || ''; + if (!isAdmin && (activeView === 'berichte-view' || activeView === 'rechnungen-view')) { + this.switchView('login'); + } + } catch (e) { + this.user = null; + document.querySelectorAll('.admin-only').forEach((el) => el.classList.add('hidden')); + document.getElementById('logout-btn').classList.add('hidden'); + document.getElementById('login-tab-btn').classList.remove('hidden'); + } + } + + async loadInitialData() { + try { + this.kurse = await API.getKurse(); + this.betriebe = await API.getBetriebe(); + this.renderKurse(); + this.renderBetriebe(); + } catch (error) { + console.error('Fehler beim Laden der Daten:', error); + alert('Fehler beim Laden der Daten. Bitte überprüfen Sie die API-Verbindung.'); + } + } + + renderKurse() { + const container = document.getElementById('kurse-container'); + container.innerHTML = this.kurse + .map( + (kurs) => ` +
+ +
+

${kurs.beschreibung ?? ''}

+

Gebühr: ${kurs.gebuehr}€

+

Verfügbar: ${kurs.freie_plaetze} von ${ + kurs.max_teilnehmer + } Plätzen

+ ${ + !kurs.verfuegbar + ? '

Kurs ist voll!

' + : '' + } +
+
+ ` + ) + .join(''); + + document.querySelectorAll('.kurs-card').forEach((card) => { + const checkbox = card.querySelector('input[type="checkbox"]'); + if (checkbox && !checkbox.disabled) { + card.addEventListener('click', (e) => { + if (e.target.tagName !== 'INPUT') { + checkbox.checked = !checkbox.checked; + card.classList.toggle('selected', checkbox.checked); + } + }); + checkbox.addEventListener('change', () => { + card.classList.toggle('selected', checkbox.checked); + }); + } + }); + } + + renderBetriebe() { + const select = document.getElementById('betrieb'); + const rechnungSelect = document.getElementById('rechnung-betrieb'); + + const options = this.betriebe + .map((betrieb) => ``) + .join(''); + + select.innerHTML = '' + options; + rechnungSelect.innerHTML = '' + options; + } + + setupFormHandlers() { + const form = document.getElementById('anmelde-form'); + form.addEventListener('submit', (e) => { + e.preventDefault(); + this.handleAnmeldung(); + }); + + const rechnungBtn = document.getElementById('rechnung-erstellen'); + rechnungBtn.addEventListener('click', () => { + this.handleRechnungErstellen(); + }); + + document.getElementById('neu-anmelden')?.addEventListener('click', () => { + document.getElementById('erfolg-meldung').classList.add('hidden'); + form.reset(); + document.querySelectorAll('.kurs-card').forEach((c) => c.classList.remove('selected')); + }); + + document.getElementById('zur-uebersicht')?.addEventListener('click', () => { + this.switchView('berichte'); + }); + } + + setupLoginHandlers() { + const form = document.getElementById('login-form'); + if (!form) return; + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = document.getElementById('login-email').value.trim(); + const password = document.getElementById('login-password').value; + const err = document.getElementById('login-error'); + err.textContent = ''; + try { + await API.login(email, password); + await this.checkAuthAndToggleUI(); + this.switchView('berichte'); + } catch (ex) { + err.textContent = ex.message || 'Login fehlgeschlagen'; + } + }); + } + + setupLogoutHandler() { + const btn = document.getElementById('logout-btn'); + btn.addEventListener('click', async () => { + try { + await API.logout(); + this.user = null; + await this.checkAuthAndToggleUI(); + this.switchView('anmeldung'); + } catch (e) { + console.error(e); + } + }); + } + + async handleAnmeldung() { + const form = document.getElementById('anmelde-form'); + const formData = new FormData(form); + + const selectedKurse = Array.from(document.querySelectorAll('input[name="kurse"]:checked')).map( + (cb) => parseInt(cb.value, 10) + ); + + if (selectedKurse.length === 0) { + this.showFehler('Bitte wählen Sie mindestens eine Sportart aus.'); + return; + } + + const data = { + vorname: formData.get('vorname'), + nachname: formData.get('nachname'), + email: formData.get('email'), + geburtsdatum: formData.get('geburtsdatum') || null, + betrieb_id: parseInt(formData.get('betrieb'), 10), + kurs_ids: selectedKurse, + }; + + try { + const response = await API.anmelden(data); + this.showErfolg(response); + form.reset(); + document.querySelectorAll('.kurs-card').forEach((c) => c.classList.remove('selected')); + await this.loadInitialData(); + } catch (error) { + this.showFehler(error.message); + } + } + + showErfolg(response) { + const meldung = document.getElementById('erfolg-meldung'); + const text = document.getElementById('erfolg-text'); + text.innerHTML = ` + Vielen Dank für Ihre Anmeldung!

+ Angemeldete Kurse:
+ ${response.angemeldete_kurse.map((k) => `• ${k}`).join('
')} + `; + meldung.classList.remove('hidden'); + document.getElementById('fehler-meldung').classList.add('hidden'); + meldung.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + setTimeout(() => { + meldung.classList.add('hidden'); + }, 8000); + } + + showFehler(message) { + const meldung = document.getElementById('fehler-meldung'); + const text = document.getElementById('fehler-text'); + text.textContent = message; + meldung.classList.remove('hidden'); + document.getElementById('erfolg-meldung').classList.add('hidden'); + meldung.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + + async loadBerichte() { + await this.loadBerichtBetrieb(); + await this.loadBerichtKurs(); + } + + async loadBerichtBetrieb() { + try { + const bericht = await API.getBerichtBetrieb(); + const container = document.getElementById('betrieb-bericht'); + + container.innerHTML = ` + + + + + + + + + + + ${bericht + .map( + (b) => ` + + + + + + + ` + ) + .join('')} + +
BetriebAnzahl SchülerAnmeldungenDetails
+ ${b.betrieb_name}
+ ${b.adresse} +
${b.anzahl_schueler}${b.anzahl_anmeldungen} + ${ + b.schueler.length > 0 + ? ` +
    + ${b.schueler + .map((s) => `
  • ${s.vorname} ${s.nachname} - ${s.angemeldete_kurse}
  • `) + .join('')} +
+ ` + : 'Keine Anmeldungen' + } +
+ `; + } catch (error) { + console.error('Fehler beim Laden des Berichts:', error); + document.getElementById('betrieb-bericht').innerHTML = + '
Fehler beim Laden der Berichte
'; + if (error.message.includes('Nicht angemeldet') || error.message.includes('Zugriff')) { + this.switchView('login'); + } + } + } + + async loadBerichtKurs() { + try { + const bericht = await API.getBerichtKurs(); + const container = document.getElementById('kurs-bericht'); + + container.innerHTML = ` + + + + + + + + + + + ${bericht + .map( + (k) => ` + + + + + + + ` + ) + .join('')} + +
KursTeilnehmerAuslastungDetails
${k.kurs_name}${k.anzahl_teilnehmer} / ${k.max_teilnehmer} +
+
+ ${k.auslastung_prozent}% +
+
+
+ ${ + k.teilnehmer.length > 0 + ? ` +
    + ${k.teilnehmer + .map((t) => `
  • ${t.vorname} ${t.nachname} (${t.betrieb_name})
  • `) + .join('')} +
+ ` + : 'Keine Anmeldungen' + } +
+ `; + } catch (error) { + console.error('Fehler beim Laden des Berichts:', error); + document.getElementById('kurs-bericht').innerHTML = + '
Fehler beim Laden der Berichte
'; + if (error.message.includes('Nicht angemeldet') || error.message.includes('Zugriff')) { + this.switchView('login'); + } + } + } + + async loadRechnungsOptionen() { + // Betriebe werden bereits geladen + } + + async handleRechnungErstellen() { + const betriebId = document.getElementById('rechnung-betrieb').value; + if (!betriebId) { + alert('Bitte wählen Sie einen Betrieb aus.'); + return; + } + try { + const rechnung = await API.getRechnung(betriebId); + this.renderRechnung(rechnung); + } catch (error) { + console.error('Fehler beim Erstellen der Rechnung:', error); + alert('Fehler beim Erstellen der Rechnung: ' + error.message); + if (error.message.includes('Nicht angemeldet') || error.message.includes('Zugriff')) { + this.switchView('login'); + } + } + } + + renderRechnung(rechnung) { + const container = document.getElementById('rechnung-container'); + container.innerHTML = ` +
+
+
+

RECHNUNG

+

Rechnungsnummer: ${rechnung.rechnungsnummer}

+

Datum: ${new Date(rechnung.datum).toLocaleDateString('de-DE')}

+
+
+

Schulverwaltung

+

Musterstraße 123

+

12345 Musterstadt

+
+
+ +
+

Rechnungsempfänger:

+

${rechnung.betrieb.name}

+

${rechnung.betrieb.adresse}

+

${rechnung.betrieb.email}

+
+ +

Kursanmeldungen:

+ + + + + + + + + + + ${rechnung.zusammenfassung + .map( + (z) => ` + + + + + + + ` + ) + .join('')} + +
KursAnzahl TeilnehmerGebühr pro TeilnehmerGesamt
${z.kurs_name}${z.anzahl_teilnehmer}${z.gebuehr}€${z.gesamt_kurs}€
+ +
+
Netto: ${rechnung.netto.toFixed(2)}€
+
MwSt. (${rechnung.mwst_satz}%): ${rechnung.mwst_betrag.toFixed(2)}€
+
Gesamt: ${rechnung.gesamtsumme.toFixed(2)}€
+
+ +

+ Bitte überweisen Sie den Betrag innerhalb von 14 Tagen unter Angabe der Rechnungsnummer. +

+ + +
+ `; + } +} + +document.addEventListener('DOMContentLoaded', () => { + new SportAnmeldungApp(); +}); \ No newline at end of file diff --git a/js/config.js b/js/config.js new file mode 100644 index 0000000..c57b4ae --- /dev/null +++ b/js/config.js @@ -0,0 +1,15 @@ +const API_CONFIG = { + baseUrl: 'http://127.0.0.1:8000', + endpoints: { + kurse: '/api/kurse', + betriebe: '/api/betriebe', + anmeldung: '/api/anmeldung', + berichtBetrieb: '/api/berichte/teilnehmer-pro-betrieb', + berichtKurs: '/api/berichte/teilnehmer-pro-kurs', + rechnung: '/api/rechnungen/betrieb/', + login: '/api/auth/login', + logout: '/api/auth/logout', + me: '/api/auth/me', + }, +}; +