Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-03-30 22:06:01 +02:00
parent 2fc49e3dfa
commit 9b43865415
11 changed files with 315 additions and 523 deletions
+117 -375
View File
@@ -1,235 +1,99 @@
{% extends "base.html" %}
{% block title %}Buchungsoption{% endblock %}
{% block title %}Buchungsoptionen{% endblock %}
{% block head %}
{{ super() }}
<style>
.appointments-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 1rem;
align-items: start;
.packages-hero {
background: linear-gradient(140deg, #f6f2e6 0%, #e8efe4 55%, #dfe9ef 100%);
border: 1px solid #ced8d1;
border-radius: 18px;
padding: 1.4rem;
box-shadow: 0 14px 34px rgba(16, 36, 47, 0.08);
margin-bottom: 1rem;
}
.panel {
.packages-hero h1 {
color: #173f57;
font-size: clamp(1.65rem, 3.2vw, 2.45rem);
margin-bottom: 0.35rem;
}
.packages-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.9rem;
}
.package-card {
background: #ffffff;
border: 1px solid #d5e0e8;
border-radius: 16px;
padding: 1rem;
box-shadow: 0 10px 24px rgba(23, 44, 57, 0.05);
display: grid;
gap: 0.7rem;
}
.package-name {
margin: 0;
color: #184560;
font-size: 1.35rem;
}
.package-headline {
color: #3a6177;
font-weight: 600;
font-size: 0.95rem;
}
.feature-list {
margin: 0;
padding-left: 1.1rem;
color: #456274;
display: grid;
gap: 0.35rem;
}
.package-actions {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
}
.btn {
border: 1px solid #0d4f77;
background: linear-gradient(120deg, #0d6294 0%, #0b476a 100%);
color: #ffffff;
border-radius: 999px;
padding: 0.45rem 0.88rem;
font-weight: 700;
cursor: pointer;
font: inherit;
}
.btn.secondary {
border-color: #9ab5c8;
background: #f2f8fc;
color: #275a78;
}
.notice {
margin-top: 1rem;
background: #ffffff;
border: 1px solid #d4e0e9;
border-radius: 16px;
padding: 1.2rem;
box-shadow: 0 14px 32px rgba(20, 39, 55, 0.06);
border-radius: 14px;
padding: 0.9rem;
color: #274f65;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
margin-bottom: 0.9rem;
.notice strong {
color: #173f57;
}
.calendar-header h1 {
font-size: 1.5rem;
color: #133f5d;
}
.month-nav {
display: inline-flex;
gap: 0.4rem;
}
.month-nav a {
border-radius: 999px;
border: 1px solid #bfd1de;
padding: 0.38rem 0.7rem;
color: #185170;
font-weight: 700;
background: #ffffff;
}
.weekday-row,
.day-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 0.3rem;
}
.weekday {
text-align: center;
font-size: 0.82rem;
font-weight: 700;
color: #587689;
padding-bottom: 0.2rem;
}
.day {
min-height: 74px;
border: 1px solid #d8e3ec;
border-radius: 10px;
background: #f9fcff;
color: #0f3f5d;
cursor: pointer;
font-weight: 700;
transition: all 0.15s ease;
}
.day:hover {
border-color: #9ec0d6;
transform: translateY(-1px);
}
.day.empty {
background: #f2f6f9;
border-style: dashed;
cursor: default;
}
.day.today {
border-color: #2d7fab;
}
.day.selected {
background: linear-gradient(140deg, #0d5c88 0%, #0a4464 100%);
border-color: #0d5c88;
color: #ffffff;
}
.day.blocked {
background: #f3f4f7;
color: #8b96a1;
border-color: #d2d8de;
cursor: not-allowed;
}
.day.blocked:hover {
transform: none;
border-color: #d2d8de;
}
.booking-form h2,
.appointment-list h2 {
font-size: 1.2rem;
color: #143f5e;
margin: 0 0 0.8rem;
}
.selected-date-badge {
display: inline-block;
margin-bottom: 0.8rem;
border-radius: 999px;
padding: 0.28rem 0.6rem;
border: 1px solid #bfd2df;
color: #23526c;
font-weight: 700;
font-size: 0.9rem;
}
.field {
margin-bottom: 0.8rem;
}
.field label {
display: block;
margin-bottom: 0.3rem;
font-weight: 700;
color: #1f4b65;
}
.field input,
.field textarea,
.field select {
width: 100%;
border: 1px solid #c4d4e0;
border-radius: 10px;
padding: 0.65rem 0.72rem;
font: inherit;
color: #143f5d;
background: #ffffff;
}
.field textarea {
min-height: 86px;
resize: vertical;
}
.submit-btn {
width: 100%;
border: none;
border-radius: 999px;
padding: 0.74rem;
font-size: 1rem;
font-weight: 700;
color: #ffffff;
background: linear-gradient(120deg, #0a5f8f 0%, #0b4567 100%);
cursor: pointer;
}
.entry {
border: 1px solid #d7e2ea;
border-radius: 12px;
padding: 0.72rem;
margin-bottom: 0.55rem;
background: #f9fbfd;
}
.entry strong {
color: #113d59;
}
.entry p {
margin-top: 0.18rem;
font-size: 0.92rem;
}
.status {
display: inline-block;
margin-top: 0.45rem;
border-radius: 999px;
border: 1px solid #c9d7e2;
padding: 0.15rem 0.55rem;
font-size: 0.8rem;
color: #2f5f79;
font-weight: 700;
background: #eef4f8;
}
.meeting-note {
margin: 0 0 0.75rem;
color: #27526d;
font-weight: 600;
}
.helper-line {
margin-top: 0.35rem;
font-size: 0.84rem;
color: #4f6f83;
}
.maps-tools {
display: flex;
gap: 0.45rem;
margin-top: 0.45rem;
}
.maps-btn {
border: 1px solid #8fb1c8;
background: #f6fbff;
color: #1d5575;
border-radius: 999px;
padding: 0.35rem 0.7rem;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
}
.entry-meta {
margin-top: 0.2rem;
color: #345e78;
font-size: 0.9rem;
}
@media (max-width: 960px) {
.appointments-grid {
@media (max-width: 980px) {
.packages-grid {
grid-template-columns: 1fr;
}
}
@@ -237,167 +101,45 @@
{% endblock %}
{% block content %}
<section class="appointments-grid">
<article class="panel">
<div class="calendar-header">
<h1>{{ month_name }} {{ year }}</h1>
<div class="month-nav">
<a href="{{ url_for('appointments', month=previous_month, year=previous_year) }}">Zurück</a>
<a href="{{ url_for('appointments', month=next_month, year=next_year) }}">Weiter</a>
</div>
</div>
<div class="weekday-row" aria-hidden="true">
<div class="weekday">Mo</div>
<div class="weekday">Di</div>
<div class="weekday">Mi</div>
<div class="weekday">Do</div>
<div class="weekday">Fr</div>
<div class="weekday">Sa</div>
<div class="weekday">So</div>
</div>
<div class="day-grid" id="dayGrid">
{% for week in month_grid %}
{% for day in week %}
{% if day == 0 %}
<button type="button" class="day empty" disabled></button>
{% else %}
{% set day_iso = "%04d-%02d-%02d"|format(year, month, day) %}
{% set blocked_day = blocked_day_map.get(day_iso) %}
<button
type="button"
class="day {% if day_iso == today_iso %}today{% endif %} {% if blocked_day %}blocked{% endif %}"
data-date="{{ day_iso }}"
{% if blocked_day %}
disabled
title="Gesperrt{% if blocked_day.reason %}: {{ blocked_day.reason }}{% endif %}"
{% endif %}
>
{{ day }}
</button>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</article>
<aside class="panel booking-form">
<h2>Buchung anfragen</h2>
<p class="meeting-note">Persoenliche Buchungsoption: Digital oder Vor Ort.</p>
<span class="selected-date-badge" id="selectedDateBadge">Kein Datum gewählt</span>
<form method="POST" action="{{ url_for('appointments', month=month, year=year) }}">
<input type="hidden" id="selectedDateInput" name="selected_date" required>
<div class="field">
<label for="appointment_time">Uhrzeit</label>
<input id="appointment_time" name="appointment_time" type="time" required>
</div>
<div class="field">
<label for="meeting_type">Buchungsart</label>
<select id="meeting_type" name="meeting_type" required>
<option value="digital">Digital</option>
<option value="vor_ort">Vor Ort</option>
</select>
</div>
<div class="field">
<label for="subject">Betreff</label>
<input id="subject" name="subject" type="text" placeholder="z. B. Projekt Kickoff" required>
</div>
<div class="field" id="locationField" style="display: none;">
<label for="location_name">Ort (bei Vor-Ort Pflicht)</label>
<input id="location_name" name="location_name" type="text" placeholder="Adresse oder Ort">
<div class="maps-tools">
<button type="button" id="mapsSearchBtn" class="maps-btn">In Google Maps suchen</button>
</div>
<div class="helper-line">Optional: Google-Maps-Link eintragen.</div>
<input id="location_maps_url" name="location_maps_url" type="url" placeholder="https://maps.google.com/..." style="margin-top: 0.4rem;">
</div>
<div class="field">
<label for="note">Notiz (optional)</label>
<textarea id="note" name="note" placeholder="Kurzbeschreibung der Anfrage"></textarea>
</div>
<button class="submit-btn" type="submit">Buchung anfragen</button>
</form>
</aside>
<section class="packages-hero">
<h1>Buchungsoptionen als Softwarepakete</h1>
<p>Alle Pakete sind frei einsehbar, auch ohne Admin. Die Testversion kann nur mit aktivem Konto gestartet werden.</p>
<p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschuetzten Sessions und nachvollziehbaren Aktionen.</p>
</section>
<section class="panel appointment-list" style="margin-top: 1rem;">
<h2>Meine Buchungsanfragen</h2>
{% if user_appointments %}
{% for item in user_appointments %}
<article class="entry">
<strong>{{ item.date }} um {{ item.time }} - {{ item.subject }}</strong>
{% if item.note %}
<p>{{ item.note }}</p>
<section class="packages-grid">
{% for package in software_packages %}
<article class="package-card">
<h2 class="package-name">{{ package.name }}</h2>
<p class="package-headline">{{ package.headline }}</p>
<ul class="feature-list">
{% for feature in package.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
<div class="package-actions">
{% if 'username' in session %}
<form method="POST" action="{{ url_for('start_test_package') }}">
<input type="hidden" name="package" value="{{ package.slug }}">
<button class="btn" type="submit">Testversion starten</button>
</form>
{% else %}
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto starten</a>
{% endif %}
<p class="entry-meta"><strong>Buchungsart:</strong> {{ item.meeting_label or ('Vor Ort' if item.meeting_type == 'vor_ort' else 'Digital') }}</p>
{% if item.location_name %}
<p class="entry-meta"><strong>Ort:</strong> {{ item.location_name }}</p>
{% endif %}
{% if item.location_maps_url %}
<p class="entry-meta"><a href="{{ item.location_maps_url }}" target="_blank" rel="noopener noreferrer">Google Maps anzeigen</a></p>
{% endif %}
{% if item.response %}
<p><strong>Admin-Antwort:</strong> {{ item.response }}</p>
{% endif %}
<span class="status">{{ item.status }}</span>
</article>
{% endfor %}
</div>
</article>
{% endfor %}
</section>
<section class="notice">
{% if 'username' in session %}
{% if active_test_license %}
<p><strong>Aktive Testversion:</strong> {{ active_test_license.plan }} | Key: {{ active_test_license.license_key }} | Gueltig bis: {{ active_test_license.valid_until }}</p>
{% else %}
<p><strong>Hinweis:</strong> Fuer Ihr Konto ist noch keine Testversion aktiv. Starten Sie oben eine Testversion fuer Normal, Pro oder Buecherei.</p>
{% endif %}
{% else %}
<p>Noch keine Anfragen vorhanden.</p>
<p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um die Testversion zu starten.</p>
{% endif %}
</section>
<script>
(function () {
const dayGrid = document.getElementById("dayGrid");
const selectedDateInput = document.getElementById("selectedDateInput");
const selectedDateBadge = document.getElementById("selectedDateBadge");
const meetingType = document.getElementById("meeting_type");
const locationField = document.getElementById("locationField");
const locationNameInput = document.getElementById("location_name");
const mapsSearchBtn = document.getElementById("mapsSearchBtn");
function setSelection(dateValue, button) {
selectedDateInput.value = dateValue;
selectedDateBadge.textContent = "Ausgewählt: " + dateValue;
dayGrid.querySelectorAll(".day").forEach(function (node) {
node.classList.remove("selected");
});
if (button) {
button.classList.add("selected");
}
}
function updateLocationRequirement() {
const isOnsite = meetingType.value === "vor_ort";
locationField.style.display = isOnsite ? "block" : "none";
locationNameInput.required = isOnsite;
if (!isOnsite) {
locationNameInput.value = "";
document.getElementById("location_maps_url").value = "";
}
}
dayGrid.addEventListener("click", function (event) {
const target = event.target.closest(".day[data-date]");
if (!target) {
return;
}
setSelection(target.dataset.date, target);
});
meetingType.addEventListener("change", updateLocationRequirement);
mapsSearchBtn.addEventListener("click", function () {
const query = (locationNameInput.value || "").trim();
const base = "https://www.google.com/maps/search/?api=1&query=";
window.open(base + encodeURIComponent(query || "meeting location"), "_blank", "noopener");
});
updateLocationRequirement();
})();
</script>
{% endblock %}
+85 -30
View File
@@ -268,6 +268,41 @@
color: #ffffff;
}
.logo-wall {
margin-top: 2rem;
padding: 1.5rem;
border-radius: 18px;
border: 1px solid #d4ddd4;
background: #ffffff;
box-shadow: 0 14px 30px rgba(28, 45, 41, 0.05);
}
.logo-wall h2 {
color: #21444a;
margin-bottom: 0.4rem;
}
.logo-grid {
margin-top: 1rem;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.8rem;
}
.logo-item {
border: 1px solid #d8e0d8;
border-radius: 12px;
background: #f8faf7;
padding: 0.65rem;
}
.logo-item img {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 8px;
}
@keyframes rise {
from {
opacity: 0;
@@ -313,6 +348,10 @@
align-items: flex-start;
padding: 1.5rem;
}
.logo-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
@@ -320,10 +359,10 @@
{% block content %}
<section class="hero" id="top">
<div>
<span class="eyebrow">Für moderne Schulverwaltung</span>
<h1>Mehr Ruhe im Schulalltag durch klare digitale Verwaltung.</h1>
<span class="eyebrow">Lehrmittelverwaltungssoftware für Schulen</span>
<h1>Lehrmittel, Schul-IT und Ausleihen in einem sicheren System.</h1>
<p>
Invario unterstützt Bildungseinrichtungen in Deutschland dabei, Inventar, Lizenzen und Anfragen nachvollziehbar zu organisieren. Die Bedienung bleibt bewusst einfach, während zentrale Sicherheitsfunktionen wie Rollenrechte, Protokollierung und geschützte Verbindungen standardmäßig integriert sind.
Invario ist eine Lehrmittelverwaltungssoftware, die Schulbibliothek, Inventar und digitale Ausleihe zusammenfuehrt. Prozesse bleiben bewusst schlank und schnell, waehrend Sicherheitsfunktionen wie rollenbasierte Rechte, Sitzungsabsicherung und nachvollziehbare Protokolle im Hintergrund dauerhaft aktiv sind.
</p>
<div class="cta-row">
{% if 'username' in session %}
@@ -348,71 +387,87 @@
</section>
<section class="section" id="nutzen" style="animation-delay: 100ms;">
<h2>Vorteile für Ihr Sekretariat</h2>
<p>Alle Informationen sind gebündelt, Vorgänge sind standardisiert, und Rückfragen lassen sich schneller beantworten.</p>
<h2>Vorteile fuer Verwaltung und Bibliothek</h2>
<p>Alle Daten zu Lehrmitteln, Geraeten und Ausleihen sind gebuendelt, sodass Rückfragen schnell geklaert werden koennen.</p>
<div class="section-grid">
<article class="card">
<h3>Inventar auf einen Blick</h3>
<p>Geräte, Standorte und Zuständigkeiten sind zentral erfasst. Das reduziert Suchzeiten und doppelte Rückfragen.</p>
<h3>Lehrmittel pro Klasse steuern</h3>
<p>Buecher und Unterrichtsmaterialien lassen sich klassenweise ausgeben und zum Schuljahresende sauber rueckfuehren.</p>
</article>
<article class="card">
<h3>Lizenzfristen im Blick</h3>
<p>Auslaufende Lizenzen werden rechtzeitig sichtbar, damit keine Unterrichtsausfälle durch fehlende Software entstehen.</p>
<h3>Scanner-gestuetzte Ausleihe</h3>
<p>Barcode- oder QR-Workflows beschleunigen Ausgabe und Ruecknahme deutlich, auch bei grossen Bestandsmengen.</p>
</article>
<article class="card">
<h3>Verständliche Prozesse</h3>
<p>Statt technischer Fachsprache zeigt die Plattform klare Schritte und eindeutige Statusmeldungen für jede Aufgabe.</p>
<h3>IT- und Medienbestand kombiniert</h3>
<p>Tablets, Laptops, Webcams und Print-Medien werden gemeinsam verwaltet statt in voneinander getrennten Listen.</p>
</article>
</div>
</section>
<section class="section" id="alltag" style="animation-delay: 180ms;">
<h2>Typische Situationen aus dem Schulalltag</h2>
<p>Die folgenden Beispiele zeigen, wie die Plattform bei wiederkehrenden Verwaltungsaufgaben hilft.</p>
<h2>Typische Lehrmittel-Szenarien</h2>
<p>Die folgenden Beispiele orientieren sich an realen Schulablaeufen mit Fokus auf Tempo und Nachvollziehbarkeit.</p>
<div class="routine-list">
<div class="routine">
<div>
<b>Neues Gerät kommt an</b>
Seriennummer, Raum und Ansprechperson werden in wenigen Schritten eingetragen und sind sofort nachvollziehbar.
<b>Neue Klassensaetze treffen ein</b>
Lehrmittel werden etikettiert, Klassen zugeordnet und mit Ausgabehistorie gespeichert.
</div>
<span>Inventar</span>
<span>Lehrmittel</span>
</div>
<div class="routine">
<div>
<b>Rückfrage zur Gerätehistorie</b>
Status zu Bestellung, Nutzung oder Defekt ist direkt sichtbar, ohne mehrere Listen durchsuchen zu müssen.
<b>Rueckfrage zu ausgeliehenen Medien</b>
Verfuegbarkeit, Entleiher und Rueckgabedatum sind sofort sichtbar, ohne manuelle Listenpflege.
</div>
<span>Transparenz</span>
<span>Ausleihe</span>
</div>
<div class="routine">
<div>
<b>Supportfall im Unterricht</b>
Tickets werden dokumentiert und priorisiert. Das Team sieht jederzeit, wer bereits daran arbeitet.
<b>Defekt bei einem Endgeraet</b>
Zustand, Schaden und Verantwortlichkeiten werden dokumentiert, inklusive klarer Eskalationswege.
</div>
<span>Support</span>
<span>Sicherheit</span>
</div>
</div>
</section>
<section class="section" id="sicherheit" style="animation-delay: 220ms;">
<h2>Allgemeine Sicherheit des Programms</h2>
<p>Das Sicherheitskonzept verbindet technische Schutzmaßnahmen mit klaren Arbeitsabläufen, damit Daten im laufenden Betrieb zuverlässig geschützt bleiben.</p>
<h2>Erweiterte Sicherheit fuer den Schulbetrieb</h2>
<p>Die Plattform setzt auf Datenschutz und Betriebssicherheit, damit personenbezogene Daten und Inventardaten langfristig geschuetzt bleiben.</p>
<div class="section-grid">
<article class="card">
<h3>Rollenbasierte Zugriffe</h3>
<p>Nutzerkonten sehen nur Funktionen, die für die jeweilige Aufgabe benötigt werden. Das reduziert Fehlbedienungen und ungewollte Änderungen.</p>
<h3>Rollen- und Bereichstrennung</h3>
<p>Bibliothek, Verwaltung und IT erhalten abgestufte Rechte, sodass jede Rolle nur ihre benoetigten Funktionen sieht.</p>
</article>
<article class="card">
<h3>Dokumentierte Änderungen</h3>
<p>Wichtige Aktionen werden nachvollziehbar protokolliert. Dadurch lassen sich Abläufe prüfen und Sicherheitsereignisse schneller einordnen.</p>
<h3>Protokollierung und Nachweis</h3>
<p>Ausgaben, Ruecknahmen und kritische Admin-Aktionen werden dokumentiert und sind jederzeit auditierbar.</p>
</article>
<article class="card">
<h3>Geschützte Übertragung</h3>
<p>Verbindungen laufen über sichere Protokolle, ergänzt durch Sicherheits-Header und strikte Session-Regeln für den täglichen Webbetrieb.</p>
<h3>Gesicherte Web-Session</h3>
<p>Sicherheits-Header, HttpOnly-Cookies und klare Session-Richtlinien schuetzen den laufenden Betrieb vor typischen Webangriffen.</p>
</article>
</div>
</section>
<section class="logo-wall" id="logos">
<h2>Einblicke und Logo-Varianten</h2>
<p>Die folgenden Grafiken sind direkt aus dem statischen Bereich Ihrer Website eingebunden.</p>
<div class="logo-grid">
<figure class="logo-item">
<img src="{{ url_for('static', filename='images/logo-1.jpeg') }}" alt="Invario Logo Variante 1">
</figure>
<figure class="logo-item">
<img src="{{ url_for('static', filename='images/logo-2.jpeg') }}" alt="Invario Logo Variante 2">
</figure>
<figure class="logo-item">
<img src="{{ url_for('static', filename='images/logo-3.jpeg') }}" alt="Invario Logo Variante 3">
</figure>
</div>
</section>
<section class="section" id="ablauf" style="animation-delay: 260ms;">
<h2>So verläuft die Zusammenarbeit</h2>
<p>Ein klarer Prozess sorgt für Sicherheit, besonders bei begrenzten Zeitfenstern im Schulbetrieb.</p>
+4
View File
@@ -105,6 +105,10 @@
<label for="username">Benutzername</label>
<input id="username" name="username" type="text" autocomplete="username" required>
</div>
<div class="field">
<label for="email">E-Mail</label>
<input id="email" name="email" type="email" autocomplete="email" required>
</div>
<div class="field">
<label for="password">Passwort (mind. 8 Zeichen)</label>
<input id="password" name="password" type="password" autocomplete="new-password" required>