This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2c2d2f" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="{{ app_title }}" />
|
||||
{% set __page_title %}{% block title %}{{ app_name }}{% endblock %}{% endset %}
|
||||
<title>
|
||||
{% if __page_title | trim == app_name %}{{ app_title }}{% else %}{{ app_title }} - {{ __page_title | trim }}{% endif %}
|
||||
</title>
|
||||
<link rel="manifest" href="/manifest.webmanifest?v={{ asset_version }}" />
|
||||
<link rel="icon" type="image/svg+xml" href="/img/Logo.svg" />
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/static/icons/favicon-32.png?v={{ asset_version }}" />
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/static/icons/favicon-16.png?v={{ asset_version }}" />
|
||||
<link rel="shortcut icon"
|
||||
href="/static/icons/favicon.ico?v={{ asset_version }}" />
|
||||
<link rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="{% if app_env != 'production' %}/static/icons/apple-touch-icon-stage.png{% else %}/static/icons/apple-touch-icon.png{% endif %}?v={{ asset_version }}" />
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{ asset_version }}" />
|
||||
{% block head_extra %}{% endblock %}
|
||||
</head>
|
||||
<body class="app-theme {% block body_class %}{% endblock %}">
|
||||
{% include "partials/header.html" %}
|
||||
<div class="container app-page-actions-wrap">{%- block page_actions -%}{%- endblock -%}</div>
|
||||
<main class="container page {% block page_class %}{% endblock %}">
|
||||
{% include "partials/flash.html" %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% include "partials/footer.html" %}
|
||||
{% include "partials/version_badge.html" %}
|
||||
<script type="module" src="/static/js/app.js?v={{ asset_version }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button, link_button %}
|
||||
{% from "ui/form_field.html" import input_field, select_field, textarea_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Mehrfacheingabe{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header('Mehrere Tage / Wochen bearbeiten', 'Zeitraum und Wochentage auswählen, dann Zeiten gesammelt für alle passenden Tage setzen.') }}
|
||||
{% call card('form-card full-width') %}
|
||||
<form method="post"
|
||||
action="/bulk-entry"
|
||||
class="stack"
|
||||
data-component="break-rules-form"
|
||||
data-auto-break-enabled="{{ 'true' if user.automatic_break_rules_enabled else 'false' }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="break_mode" value="{{ break_mode }}" data-break-mode />
|
||||
<div class="inline-grid">
|
||||
{{ input_field('Von', 'from_date', type='date', value=from_date, required=true) }}
|
||||
{{ input_field('Bis', 'to_date', type='date', value=to_date, required=true) }}
|
||||
</div>
|
||||
<fieldset class="weekday-fieldset">
|
||||
<legend>Wochentage</legend>
|
||||
<div class="weekday-grid">
|
||||
{% for option in weekday_options %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="weekdays_values"
|
||||
value="{{ option.value }}"
|
||||
{% if option.value in weekdays_selected %}checked{% endif %} />
|
||||
<span>{{ option.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="inline-grid">
|
||||
{{ input_field('Beginn', 'start_time', type='time', value=start_time, required=true, attrs='data-break-input=\"start\"') }}
|
||||
{{ input_field('Ende', 'end_time', type='time', value=end_time, required=true, attrs='data-break-input=\"end\"') }}
|
||||
</div>
|
||||
<div class="inline-grid">
|
||||
{{ input_field('Pause (Minuten)', 'break_minutes', type='number', value=break_minutes, required=true, attrs='min="0" step="1" data-break-input=\"minutes\"') }}
|
||||
{{ select_field('Modus', 'mode', [
|
||||
{'value': 'only_missing', 'label': 'Nur leere Tage anlegen'},
|
||||
{'value': 'upsert', 'label': 'Bestehende Einträge aktualisieren + fehlende anlegen'}
|
||||
], bulk_mode, required=true) }}
|
||||
</div>
|
||||
{% if user.automatic_break_rules_enabled %}
|
||||
<div class="form-field form-field--hint stack-xs" data-component="break-rules-status">
|
||||
<p class="muted" data-break-status>
|
||||
{% if break_mode == 'manual' %}
|
||||
Pause manuell gesetzt. Gesetzliche Mindestpause wird nicht automatisch überschrieben.
|
||||
{% else %}
|
||||
Gesetzliche Mindestpause nach deutschem Arbeitsrecht wird automatisch vorgeschlagen.
|
||||
{% endif %}
|
||||
</p>
|
||||
<button type="button" class="button ghost" data-action="break-reset-auto">Automatische Pause erneut anwenden</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ textarea_field('Notiz (optional)', 'notes', notes, 3) }}
|
||||
<div class="nav-row">
|
||||
{{ button('Mehrfacheingabe speichern', type='submit') }}
|
||||
{{ link_button('Zurück', '/dashboard', 'ghost') }}
|
||||
</div>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
|
||||
{% block title %}Kontakt{% endblock %}
|
||||
{% block page_class %}contact-page{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header("Kontakt", "Schicke eine Nachricht bei Problemen, Fehlermeldungen oder Funktionswünschen. Antworten erfolgen per E-Mail.") }}
|
||||
|
||||
<div class="contact-grid">
|
||||
{% call card('contact-card') %}
|
||||
<form method="post" action="/kontakt" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="started_at" value="{{ contact_started_at }}" />
|
||||
<label class="contact-honeypot" aria-hidden="true">
|
||||
<span>Bitte leer lassen</span>
|
||||
<input type="text" name="website" tabindex="-1" autocomplete="off" />
|
||||
</label>
|
||||
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
Name (optional)
|
||||
<input type="text" name="name" value="{{ contact_name }}" maxlength="120" />
|
||||
</label>
|
||||
<label>
|
||||
E-Mail-Adresse
|
||||
<input type="email" name="email" value="{{ contact_email }}" maxlength="254" required />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Kategorie
|
||||
<select name="category" required>
|
||||
{% for option in category_options %}
|
||||
<option value="{{ option.value }}"
|
||||
{% if option.value == contact_category %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Betreff
|
||||
<input type="text" name="subject" value="{{ contact_subject }}" maxlength="180" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="message" rows="9" maxlength="5000" required>{{ contact_message }}</textarea>
|
||||
</label>
|
||||
|
||||
<p class="muted">
|
||||
Bitte keine sensiblen Passwörter oder Zugangsdaten mitsenden. Anhänge sind in dieser ersten Version noch nicht möglich.
|
||||
</p>
|
||||
|
||||
{{ button("Nachricht senden", type="submit") }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
|
||||
{% call card('contact-card contact-info-card') %}
|
||||
<h2>Wofür ist das gedacht?</h2>
|
||||
<ul class="contact-info-list">
|
||||
<li>Fehler melden, wenn etwas nicht wie erwartet funktioniert</li>
|
||||
<li>Funktionswünsche einreichen</li>
|
||||
<li>Fragen zur Nutzung oder zu Einstellungen stellen</li>
|
||||
</ul>
|
||||
<p class="muted">
|
||||
Nachrichten werden intern als Ticket gespeichert. So gehen Rückmeldungen nicht verloren und können strukturiert bearbeitet werden.
|
||||
</p>
|
||||
<p class="muted">
|
||||
Hinweise zu Anbieter und Datenschutz findest du ebenfalls unten im Footer über <a href="/impressum">Impressum</a> und <a href="/datenschutz">Datenschutz</a>.
|
||||
</p>
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/day_row.html" import day_row with context %}
|
||||
{% from "ui/kpi_bar.html" import kpi_bar with context %}
|
||||
{% from "ui/week_header_bar.html" import week_header_bar with context %}
|
||||
{% from "ui/icon_button.html" import icon_link with context %}
|
||||
{% from "ui/warning_components.html" import workhours_target_warning_banner with context %}
|
||||
{% block title %}Wochenansicht{% endblock %}
|
||||
{% block body_class %}dashboard-theme{% endblock %}
|
||||
{% block page_class %}dashboard-page{% endblock %}
|
||||
{% block content %}
|
||||
{% set return_to = request.url.path ~ ('?' ~ request.url.query if request.url.query else '') %}
|
||||
|
||||
<div class="week-view-shell">
|
||||
{% call week_header_bar('/dashboard?date=' ~ previous_week.isoformat(), '/dashboard?date=' ~ next_week.isoformat(), 'KW ' ~ week.week_start.isocalendar()[1] ~ ' (' ~ week.week_start.strftime('%d.%m.') ~ ' - ' ~ week.week_end.strftime('%d.%m.%Y') ~ ')') %}
|
||||
<form method="post" action="/vacation/week/toggle" class="inline-form" data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="week_start" value="{{ week.week_start.isoformat() }}" />
|
||||
<input type="hidden" name="week_end" value="{{ week.week_end.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit"
|
||||
class="week-vacation-button {% if week.is_vacation_week %}is-active{% endif %}"
|
||||
title="Urlaub für ganze Woche umschalten"
|
||||
aria-label="Urlaub für ganze Woche umschalten">Urlaub</button>
|
||||
</form>
|
||||
{{ icon_link('/entry/new?date=' ~ selected_date.isoformat(), '/static/icons/add.svg', 'Tag hinzufügen') }}
|
||||
{{ icon_link('/bulk-entry?from=' ~ week.week_start.isoformat() ~ '&to=' ~ week.week_end.isoformat(), '/static/icons/batch.svg', 'Mehrere Tage bearbeiten') }}
|
||||
{{ icon_link('/export', '/static/icons/export.svg', 'Export') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ kpi_bar([
|
||||
{'label': 'IST', 'value': ('%.2f'|format(week.weekly_ist / 60) )|replace('.00', '')},
|
||||
{'label': 'SOLL', 'value': ('%.2f'|format(week.weekly_soll / 60))|replace('.00', '')},
|
||||
{'label': 'DELTA', 'value': ('%.2f'|format(week.weekly_delta / 60))|replace('.00', ''), 'value_class': 'negative' if week.weekly_delta < 0 else 'positive'},
|
||||
{'label': 'KUMULIERT', 'value': ('%.2f'|format(week.cumulative_delta / 60))|replace('.00', ''), 'value_class': 'negative' if week.cumulative_delta < 0 else 'positive'}
|
||||
], 'kpi-bar--week') }}
|
||||
|
||||
{{ workhours_target_warning_banner(workhours_target_warning) }}
|
||||
|
||||
<section class="day-list day-list--week">
|
||||
{% for day in week.days %}
|
||||
{{ day_row(day, csrf_token, weekday_name_de(day.date) ~ ', ' ~ day.date.strftime('%d.%m.%Y'), return_to, 'week') }}
|
||||
{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import link_button %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header(title, selected_date.strftime('%d.%m.%Y')) }}
|
||||
{% call card('form-card') %}
|
||||
<div class="stack">
|
||||
{% if has_entry %}
|
||||
<p class="muted">An diesem Tag ist bereits regulaere Arbeitszeit eingetragen. Bitte bearbeite zuerst den Zeiteintrag.</p>
|
||||
<div class="nav-row">
|
||||
{{ link_button('Zeiteintrag bearbeiten', '/entry/' ~ existing_entry_id ~ '/edit', 'primary') }}
|
||||
{{ link_button('Zurueck', return_to, 'ghost') }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">
|
||||
{% if is_active %}
|
||||
{{ title }} ist aktuell gesetzt. Mit dem Speichern entfernst du diesen Status wieder.
|
||||
{% else %}
|
||||
{% if current_status_label %}
|
||||
Aktuell ist {{ current_status_label }} gesetzt. Mit dem Speichern wird dieser Status ersetzt.
|
||||
{% else %}
|
||||
Hier kannst du diesen Status direkt für den ausgewählten Tag setzen oder wieder entfernen.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if day_overtime_adjustment_minutes %}
|
||||
<p class="muted">
|
||||
Zusaetzlicher Stundenausgleich aktiv:
|
||||
<strong>{{ '+' if day_overtime_adjustment_minutes > 0 else '' }}{{ minutes_to_hhmm(day_overtime_adjustment_minutes) }}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ action_url }}" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ selected_date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
{% if status_key != 'vacation' %}
|
||||
<input type="hidden" name="status" value="{{ status_key }}" />
|
||||
{% endif %}
|
||||
<div class="nav-row">
|
||||
<button type="submit" class="btn btn--primary">{{ title }} {% if is_active %}entfernen{% else %}speichern{% endif %}</button>
|
||||
{{ link_button('Abbrechen', return_to, 'ghost') }}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}E-Mail bestätigen{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("Bestätigungslink anfordern") }}
|
||||
{% call card('auth-card') %}
|
||||
<form action="/verify-email/resend" method="post" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
{{ input_field('E-Mail-Adresse', 'email', type='email', required=true, autocomplete='username') }}
|
||||
{{ button('Link senden', type='submit') }}
|
||||
</form>
|
||||
<p>
|
||||
<a href="/login">Zur Anmeldung</a>
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% from "ui/flash.html" import alert %}
|
||||
{% block title %}E-Mail bestätigen{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("E-Mail-Bestätigung") }}
|
||||
{% call card('auth-card') %}
|
||||
{% if success %}
|
||||
{{ alert(message, 'success') }}
|
||||
{% else %}
|
||||
{{ alert(message, 'error') }}
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="/verify-email/resend">Neuen Bestätigungslink anfordern</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/login">Zur Anmeldung</a>
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button, link_button %}
|
||||
{% from "ui/form_field.html" import input_field, textarea_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header(title, "Nur fuer regulaere Arbeitszeit." ~ (" Gesetzliche Pausen koennen automatisch beruecksichtigt werden." if user.automatic_break_rules_enabled else "")) }}
|
||||
{% call card('form-card') %}
|
||||
<form method="post"
|
||||
action="{{ action_url }}"
|
||||
class="stack"
|
||||
data-component="break-rules-form"
|
||||
data-auto-break-enabled="{{ 'true' if user.automatic_break_rules_enabled else 'false' }}"
|
||||
data-full-day-net-minutes="{{ full_day_net_minutes if full_day_net_minutes is not none else '' }}"
|
||||
data-default-break-minutes="{{ user.default_break_minutes }}"
|
||||
data-default-start-time="08:30">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<input type="hidden" name="break_mode" value="{{ entry.break_mode if entry else ('auto' if user.automatic_break_rules_enabled else 'manual') }}" data-break-mode />
|
||||
{{ input_field('Datum', 'date', type='date', value=(entry.date if entry and entry.date else selected_date.isoformat()), required=true) }}
|
||||
{{ input_field('Arbeitsbeginn', 'start_time', type='time', value=(entry.start_time if entry else ''), required=true, attrs='data-break-input=\"start\"') }}
|
||||
{{ input_field('Arbeitsende', 'end_time', type='time', value=(entry.end_time if entry else ''), required=true, attrs='data-break-input=\"end\"') }}
|
||||
{% if full_day_net_minutes is not none %}
|
||||
<div class="nav-row">
|
||||
<button type="button" class="button ghost" data-action="entry-apply-full-day">Ganzer Tag</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ input_field('Pause in Minuten', 'break_minutes', type='number', value=(entry.break_minutes if entry and entry.break_minutes is not none else 0), required=true, attrs='min=\"0\" step=\"1\" data-break-input=\"minutes\"') }}
|
||||
{% if user.automatic_break_rules_enabled %}
|
||||
<div class="form-field form-field--hint stack-xs" data-component="break-rules-status">
|
||||
<p class="muted" data-break-status>
|
||||
{% if entry and entry.break_mode == 'manual' %}
|
||||
Pause manuell gesetzt. Gesetzliche Mindestpause wird nicht automatisch überschrieben.
|
||||
{% else %}
|
||||
Gesetzliche Mindestpause nach deutschem Arbeitsrecht wird automatisch vorgeschlagen.
|
||||
{% endif %}
|
||||
</p>
|
||||
<button type="button" class="button ghost" data-action="break-reset-auto">Automatische Pause erneut anwenden</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ textarea_field('Notiz (optional)', 'notes', (entry.notes if entry else ''), 3) }}
|
||||
<p class="muted">Mit gespeicherter Arbeitszeit werden Urlaub, Feiertag, Krankheit und Stundenausgleich fuer diesen Tag entfernt.</p>
|
||||
<div class="nav-row">
|
||||
{{ button('Speichern', type='submit') }}
|
||||
{{ link_button('Abbrechen', return_to, 'ghost') }}
|
||||
</div>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button, link_button %}
|
||||
{% from "ui/form_field.html" import input_field, select_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Export{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header('Export', 'Zeitraum auf den Tag genau wählen und als Excel oder PDF herunterladen.') }}
|
||||
{% call card('form-card') %}
|
||||
<form method="post" action="/export" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="inline-grid">
|
||||
{{ input_field('Von', 'from_date', type='date', value=from_date, required=true) }}
|
||||
{{ input_field('Bis', 'to_date', type='date', value=to_date, required=true) }}
|
||||
</div>
|
||||
{{ select_field('Format', 'format', [
|
||||
{'value': 'xlsx', 'label': 'Excel (.xlsx)'},
|
||||
{'value': 'pdf', 'label': 'PDF (.pdf)'}
|
||||
], 'xlsx', required=true) }}
|
||||
<div class="nav-row">
|
||||
{{ button('Export starten', type='submit') }}
|
||||
{{ link_button('Zurück', '/dashboard', 'ghost') }}
|
||||
</div>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,206 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% from "ui/help_section.html" import help_section %}
|
||||
|
||||
{% block title %}Hilfe{% endblock %}
|
||||
{% block page_class %}help-page{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header("Hilfe", "Hier findest du alle wichtigen Funktionen des Stundenfuchs verständlich erklärt.") }}
|
||||
|
||||
<section class="help-intro">
|
||||
<p>
|
||||
Stundenfuchs hilft dir dabei, Arbeitszeiten, Urlaub, Feiertage, Krankheitstage und Überstunden an einem Ort zu verwalten.
|
||||
Die App ist so aufgebaut, dass du im Alltag schnell arbeiten kannst, ohne jede Berechnung selbst im Kopf machen zu müssen.
|
||||
</p>
|
||||
<p>
|
||||
Wenn du neu startest, beginne am besten mit der Wochenansicht. Dort kannst du Tage eintragen, bearbeiten und direkt sehen,
|
||||
wie sich deine Stunden verändern.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="help-guides">
|
||||
<header class="help-guides__header">
|
||||
<h2>Schritt-für-Schritt-Anleitungen</h2>
|
||||
<p class="muted">Die wichtigsten Abläufe einmal komplett erklärt. Du kannst diese Anleitungen direkt nacheinander durchgehen.</p>
|
||||
</header>
|
||||
<div class="help-guides__grid">
|
||||
<article class="help-guide-card">
|
||||
<h3>Einen normalen Arbeitstag eintragen</h3>
|
||||
<ol class="help-steps">
|
||||
<li>Gehe in die Wochen- oder Monatsansicht.</li>
|
||||
<li>Klicke beim gewünschten Tag auf <strong>+</strong> oder auf <strong>Bearbeiten</strong>.</li>
|
||||
<li>Wähle <strong>Zeit</strong>, wenn du einen normalen Arbeitstag erfassen möchtest.</li>
|
||||
<li>Trage Arbeitsbeginn und Arbeitsende ein. Wenn du die automatische Pausenregel aktiviert hast, schlägt Stundenfuchs die gesetzliche Mindestpause automatisch vor.</li>
|
||||
<li>Du kannst die Pause trotzdem jederzeit manuell ändern. Dann bleibt dein eigener Wert maßgeblich.</li>
|
||||
<li>Speichere den Eintrag. Die App berechnet Nettozeit, Ist-Stunden und Delta automatisch.</li>
|
||||
</ol>
|
||||
</article>
|
||||
<article class="help-guide-card">
|
||||
<h3>Urlaub, Feiertag oder Krankheit eintragen</h3>
|
||||
<ol class="help-steps">
|
||||
<li>Klicke beim gewünschten Tag auf <strong>+</strong>.</li>
|
||||
<li>Wähle <strong>Urlaub</strong>, <strong>Feiertag</strong> oder <strong>Krankheit</strong>.</li>
|
||||
<li>Der Status wird sofort gesetzt. Danach erscheint der Tag direkt in der Liste mit dem passenden Kürzel.</li>
|
||||
<li>Wenn nötig, kannst du den Status später wieder ändern oder entfernen.</li>
|
||||
</ol>
|
||||
</article>
|
||||
<article class="help-guide-card">
|
||||
<h3>Stundenausgleich buchen</h3>
|
||||
<ol class="help-steps">
|
||||
<li>Klicke am gewünschten Tag auf <strong>+</strong> und wähle <strong>Stundenausgleich</strong>.</li>
|
||||
<li>Entscheide dich für eine der drei Varianten: <strong>Stunden</strong>, <strong>Von-Bis Uhrzeit</strong> oder <strong>Ganzer Tag</strong>.</li>
|
||||
<li>Wähle, ob der Ausgleich positiv oder negativ sein soll.</li>
|
||||
<li>Speichere den Eintrag. In der Liste erscheint der Tag danach mit dem <strong>S</strong>-Symbol.</li>
|
||||
<li>Der Ausgleich verändert direkt deinen Überstundenstand, ohne als normale Arbeitszeit zu zählen.</li>
|
||||
</ol>
|
||||
</article>
|
||||
<article class="help-guide-card">
|
||||
<h3>Arbeitsstunden-Counter einrichten</h3>
|
||||
<ol class="help-steps">
|
||||
<li>Öffne die <strong>Einstellungen</strong>.</li>
|
||||
<li>Gehe zum Bereich <strong>Arbeitsstunden-Counter</strong>.</li>
|
||||
<li>Aktiviere den Counter und trage Start- und Enddatum ein.</li>
|
||||
<li>Optional kannst du Zusatzstunden, ein Ziel und die Anzeige im Header aktivieren.</li>
|
||||
<li>Speichere die Einstellungen. Danach siehst du deinen Stand direkt im Einstellungsbereich und auf Wunsch oben im Header.</li>
|
||||
</ol>
|
||||
</article>
|
||||
<article class="help-guide-card">
|
||||
<h3>Backup importieren</h3>
|
||||
<ol class="help-steps">
|
||||
<li>Öffne die <strong>Einstellungen</strong> oder nutze den Backup-Upload direkt in der Registrierung.</li>
|
||||
<li>Wähle deine Backup-Datei aus.</li>
|
||||
<li>Entscheide, ob du deine Daten <strong>zusammenführen</strong> oder <strong>vollständig ersetzen</strong> möchtest.</li>
|
||||
<li>Prüfe die Vorschau mit Datensatzanzahl und Konflikten.</li>
|
||||
<li>Starte erst danach den eigentlichen Import.</li>
|
||||
</ol>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="help-grid">
|
||||
{% call help_section("Schnellstart", "So kommst du am schnellsten zu einem sauberen Stundenstand.") %}
|
||||
<ol class="help-steps">
|
||||
<li>Lege in den Einstellungen deine Wochenstunden und relevanten Arbeitstage fest.</li>
|
||||
<li>Trage in der Wochenansicht deine regulären Arbeitstage ein oder markiere Urlaub, Feiertag oder Krankheit.</li>
|
||||
<li>Nutze den Stundenausgleich, wenn du Plus- oder Minusstunden ohne normale Arbeitszeit buchen möchtest.</li>
|
||||
<li>Kontrolliere oben in den Kacheln dein aktuelles Delta und den kumulierten Stand.</li>
|
||||
</ol>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Die Kopfzeile", "Die Leiste oben zeigt dir jederzeit die wichtigsten Werte.") %}
|
||||
<ul class="help-list">
|
||||
<li><strong>Urlaub</strong>: zeigt den verbleibenden Urlaub im Verhältnis zum Gesamturlaub.</li>
|
||||
<li><strong>Arbeitsstunden</strong>: erscheint nur, wenn du den Arbeitsstunden-Counter aktiviert hast.</li>
|
||||
<li><strong>Kumuliert</strong>: zeigt deinen gesamten Überstundenstand bis heute.</li>
|
||||
<li><strong>Woche / Monat</strong>: wechselt zwischen den beiden Hauptansichten.</li>
|
||||
<li><strong>Einstellungen</strong>: hier legst du alle persönlichen Regeln fest.</li>
|
||||
<li><strong>Hilfe</strong>: öffnet diese Erklärung.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Wochenansicht", "Die Wochenansicht ist der schnellste Weg für den Alltag.") %}
|
||||
<ul class="help-list">
|
||||
<li>Oben siehst du die ausgewählte Kalenderwoche mit Datumsspanne.</li>
|
||||
<li>Die Kacheln <strong>IST</strong>, <strong>SOLL</strong>, <strong>DELTA</strong> und <strong>KUMULIERT</strong> werden automatisch berechnet.</li>
|
||||
<li>Jeder Tag erscheint als eigene Zeile. Dort kannst du direkt sehen, was eingetragen ist.</li>
|
||||
<li>Leere Tage erkennst du an <strong>Keinen Eintrag</strong> und dem <strong>+</strong>-Button rechts.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Monatsansicht", "Die Monatsansicht eignet sich gut für Rückblicke und längere Zeiträume.") %}
|
||||
<ul class="help-list">
|
||||
<li>Der Monat ist in Kalenderwochen gegliedert, damit du längere Zeiträume übersichtlich prüfen kannst.</li>
|
||||
<li>Jede Woche zeigt eine eigene Zusammenfassung mit Ist, Soll, Delta und Urlaubstagen.</li>
|
||||
<li>Du kannst Tage auch hier direkt bearbeiten, ohne in die Wochenansicht zu wechseln.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Arbeitszeit eintragen", "Für normale Arbeitstage nutzt du immer den Zeiteintrag.") %}
|
||||
<ul class="help-list">
|
||||
<li>Gib <strong>Arbeitsbeginn</strong> und <strong>Arbeitsende</strong> an. Die Pause kannst du direkt mitpflegen.</li>
|
||||
<li>Wenn du in den Einstellungen die automatische Pausenregel aktiviert hast, setzt Stundenfuchs nach deutschem Arbeitsrecht automatisch mindestens 30 oder 45 Minuten Pause, sobald die Arbeitszeit lang genug ist.</li>
|
||||
<li>Manuelle Änderungen an der Pause haben immer Vorrang vor dem automatisch vorgeschlagenen Wert.</li>
|
||||
<li>Die App berechnet daraus automatisch deine Netto-Arbeitszeit.</li>
|
||||
<li>Reguläre Arbeitszeit und Stundenausgleich schließen sich aus. Ein Tag ist entweder Arbeitszeit oder Ausgleich.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Urlaub, Feiertag und Krankheit", "Diese Tagesarten beeinflussen deine Berechnungen anders als normale Arbeitszeit.") %}
|
||||
<ul class="help-list">
|
||||
<li><strong>Urlaub</strong>: reduziert normalerweise dein Soll und zählt als Urlaubstag.</li>
|
||||
<li><strong>Feiertag</strong>: markiert einen arbeitsfreien Feiertag ohne Urlaub zu verbrauchen.</li>
|
||||
<li><strong>Krankheit</strong>: markiert einen Krankheitstag ohne Urlaub zu verbrauchen.</li>
|
||||
<li>In den Einstellungen kannst du festlegen, ob diese Tage stundenmäßig wie reguläre Arbeitstage behandelt werden sollen.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Stundenausgleich (S)", "Damit kannst du Überstunden oder Minusstunden direkt verändern, ohne normale Arbeitszeit einzutragen.") %}
|
||||
<ul class="help-list">
|
||||
<li>Du kannst den Wert manuell als <strong>+/- Stunden</strong> eintragen.</li>
|
||||
<li>Alternativ kannst du eine <strong>Von-Bis Uhrzeit</strong> wählen, wenn du die Dauer nicht selbst ausrechnen möchtest.</li>
|
||||
<li>Außerdem gibt es <strong>Ganzer Tag +</strong> und <strong>Ganzer Tag -</strong>. Die App nutzt dafür dein Wochenziel und deine relevanten Arbeitstage.</li>
|
||||
<li>Stundenausgleich darf mit Urlaub, Feiertag oder Krankheit kombiniert werden, aber nicht mit normaler Arbeitszeit.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Arbeitsstunden-Counter", "Dieser Bereich ist unabhängig von deinem Überstundenkonto.") %}
|
||||
<ul class="help-list">
|
||||
<li>Der Counter ist für längere Zeiträume gedacht, zum Beispiel für Praxisstunden oder ein Anerkennungsjahr.</li>
|
||||
<li>Du legst Startdatum, Enddatum und optional ein Stundenziel fest.</li>
|
||||
<li>Zusätzliche bereits geleistete Stunden kannst du separat eintragen, zum Beispiel aus früheren Praktika.</li>
|
||||
<li>Wenn du es aktivierst, kann der Counter auch im Header angezeigt werden.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Automatischer Modus", "Wenn du nicht jeden Standard-Arbeitstag einzeln eintragen möchtest.") %}
|
||||
<ul class="help-list">
|
||||
<li>Im automatischen Modus füllt die App fehlende reguläre Arbeitstage bis einschließlich heute nach deinen Einstellungen automatisch aus.</li>
|
||||
<li>Zukünftige Tage werden dabei bewusst nicht vorausgefüllt.</li>
|
||||
<li>Du passt dann nur noch Abweichungen an, zum Beispiel Urlaub, Krankheit oder andere Zeiten.</li>
|
||||
<li>Wenn du in den Modus wechselst oder später zurück auf manuell gehst, werden automatisch erzeugte zukünftige Einträge entfernt.</li>
|
||||
<li>Zusätzlich gibt es einen serverseitigen Tagesabgleich, damit fehlende Tage auch ohne deine Anmeldung nachgezogen werden können.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Einstellungen", "Hier steuerst du die Regeln, nach denen Stunden berechnet werden.") %}
|
||||
<ul class="help-list">
|
||||
<li><strong>Urlaub</strong>: Gesamturlaubstage, Resturlaub im Header und Urlaubszeiträume.</li>
|
||||
<li><strong>Wochenstunden</strong>: dein allgemeines Wochenziel, das für Soll-Berechnungen genutzt wird.</li>
|
||||
<li><strong>Relevante Arbeitstage</strong>: bestimmt, an welchen Wochentagen deine Sollstunden verteilt werden.</li>
|
||||
<li><strong>Überstunden-Regeln</strong>: Startdatum, Verfall und andere Regeln für dein Delta.</li>
|
||||
<li><strong>Arbeitsstunden-Counter</strong>: langer Zeitraum, Ziel und optionale Anzeige im Header.</li>
|
||||
<li><strong>Datenexport</strong>: kompletter Export aller erfassten Daten als Excel, PDF oder Backup-Datei.</li>
|
||||
<li><strong>Backup importieren</strong>: Sichere Daten aus einer Backup-Datei wieder einspielen, entweder ergänzend oder als vollständigen Ersatz deiner bisherigen Arbeitsdaten.</li>
|
||||
<li><strong>Konto löschen</strong>: entfernt dein Konto und alle zugehörigen Daten dauerhaft nach Sicherheitsbestätigung.</li>
|
||||
<li><strong>Footer</strong>: Über Kontakt, Impressum und Datenschutz erreichst du die öffentlichen Service- und Rechtstexte.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Backup und Wiederherstellung", "So sicherst du deine Daten und spielst sie später wieder ein.") %}
|
||||
<ul class="help-list">
|
||||
<li>Die Backup-Datei enthält nur arbeitsbezogene Daten und fachliche Einstellungen, aber keine Passwörter, MFA-Daten oder deine E-Mail-Adresse.</li>
|
||||
<li>Ein Backup kannst du direkt bei der Registrierung importieren oder später in den Einstellungen einspielen.</li>
|
||||
<li><strong>Zusammenführen</strong> ergänzt nur konfliktfreie Inhalte. Bereits vorhandene Tagesdaten bleiben erhalten.</li>
|
||||
<li><strong>Alle bisherigen Daten ersetzen</strong> löscht zuerst deine importierbaren Arbeitsdaten und übernimmt dann den Inhalt des Backups.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
|
||||
{% call help_section("Praktische Tipps", "Diese Hinweise vermeiden typische Fehler im Alltag.") %}
|
||||
<ul class="help-list">
|
||||
<li>Trage Zeiten möglichst zeitnah ein, damit Wochen- und Monatswerte korrekt bleiben.</li>
|
||||
<li>Nutze Urlaub, Feiertag und Krankheit nur dann, wenn an dem Tag keine normale Arbeitszeit eingetragen wird.</li>
|
||||
<li>Verwende Stundenausgleich nur für direkte Saldo-Korrekturen, nicht für normale Arbeitstage.</li>
|
||||
<li>Wenn ein Wert unerwartet wirkt, prüfe zuerst Wochenstunden, relevante Arbeitstage und Sondertage in den Einstellungen.</li>
|
||||
</ul>
|
||||
{% endcall %}
|
||||
</section>
|
||||
|
||||
<section class="help-callout">
|
||||
<h2>Wenn etwas nicht passt</h2>
|
||||
<p>
|
||||
Die meisten Abweichungen entstehen durch falsche Wochenstunden, unpassende relevante Arbeitstage oder einen gesetzten Sonderstatus.
|
||||
Prüfe in diesem Fall zuerst die Tageszeile und danach die Einstellungen. Wenn der Fehler bleibt, kannst du über den Footer die Kontaktseite nutzen.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/button.html" import link_button %}
|
||||
{% block title %}Stundenfuchs{% endblock %}
|
||||
{% block page_class %}landing-page{% endblock %}
|
||||
{% block content %}
|
||||
<section class="landing-shell stack">
|
||||
<section class="landing-hero landing-hero--centered">
|
||||
<div class="landing-hero__logo-wrap" aria-hidden="true">
|
||||
<img class="landing-hero__logo" src="/img/fuchs.png" alt="" />
|
||||
</div>
|
||||
<div class="landing-hero__copy stack">
|
||||
<p class="landing-eyebrow">Arbeitszeit, Urlaub und Überstunden an einem Ort</p>
|
||||
<h1 class="landing-title">Stundenfuchs bringt Ordnung in deinen Arbeitsalltag.</h1>
|
||||
<p class="landing-lead">
|
||||
Dokumentiere deine Arbeitszeit übersichtlich, behalte Urlaub und Fehlzeiten im Blick und lass dir Sollstunden, Saldo und wichtige Auswertungen automatisch berechnen.
|
||||
</p>
|
||||
<div class="landing-cta-row">
|
||||
{{ link_button('Jetzt registrieren', '/register', extra_class='landing-cta-primary') }}
|
||||
{{ link_button('Einloggen', '/login', variant='ghost', extra_class='landing-cta-secondary') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block page_class %}legal-page{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ page_header(title, subtitle or '') }}
|
||||
{% call card('legal-card') %}
|
||||
<div class="legal-content">{{ content_html | safe }}</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Anmeldung{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("Anmeldung") }}
|
||||
{% call card('auth-card') %}
|
||||
<form action="/login" method="post" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
{{ input_field('E-Mail', 'email', type='email', required=true, autocomplete='username') }}
|
||||
{{ input_field('Passwort', 'password', type='password', required=true, autocomplete='current-password') }}
|
||||
{{ button('Einloggen', type='submit') }}
|
||||
</form>
|
||||
<div class="auth-links">
|
||||
<p>
|
||||
<a href="/verify-email/resend">Bestätigungslink erneut senden</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/password-reset/request">Passwort vergessen?</a>
|
||||
</p>
|
||||
<p>
|
||||
Noch kein Konto? <a href="/register">Jetzt registrieren</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Zwei-Faktor-Anmeldung{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("Zwei-Faktor-Anmeldung", "Methode: " ~ mfa_method_label) }}
|
||||
{% call card('auth-card') %}
|
||||
<form action="/login/mfa" method="post" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
{{ input_field('6-stelliger Code', 'code', type='text', required=true, attrs='inputmode="numeric" pattern="[0-9]{6}" minlength="6" maxlength="6"') }}
|
||||
{{ button('Code prüfen', type='submit') }}
|
||||
</form>
|
||||
{% if mfa_is_email %}
|
||||
<form action="/login/mfa/resend"
|
||||
method="post"
|
||||
class="stack mfa-resend-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
{{ button('Neuen Code senden', type='submit', variant='ghost') }}
|
||||
</form>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="/login">Zurück zur Anmeldung</a>
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/day_row.html" import day_row with context %}
|
||||
{% from "ui/kpi_bar.html" import kpi_bar with context %}
|
||||
{% from "ui/month_header_bar.html" import month_header_bar with context %}
|
||||
{% from "ui/week_group_header.html" import week_group_header with context %}
|
||||
{% from "ui/week_group_card_mobile.html" import week_group_card_mobile with context %}
|
||||
{% from "ui/icon_button.html" import icon_link with context %}
|
||||
{% from "ui/warning_components.html" import workhours_target_warning_banner with context %}
|
||||
{% block title %}Monatsansicht{% endblock %}
|
||||
{% block page_class %}month-page{% endblock %}
|
||||
{% block content %}
|
||||
{% set month_names = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"] %}
|
||||
{% set return_to = request.url.path ~ ('?' ~ request.url.query if request.url.query else '') %}
|
||||
{% set month_prev_url = '/month?month=' ~ previous_month.strftime('%Y-%m') ~ '&view=' ~ view_mode %}
|
||||
{% set month_next_url = '/month?month=' ~ next_month.strftime('%Y-%m') ~ '&view=' ~ view_mode %}
|
||||
|
||||
<div class="month-view-shell">
|
||||
{% call month_header_bar(month_prev_url, month_next_url, month_names[month_start.month - 1] ~ ' ' ~ month_start.year) %}
|
||||
{{ icon_link('/entry/new?date=' ~ month_start.isoformat(), '/static/icons/add.svg', 'Tag hinzufügen') }}
|
||||
{{ icon_link('/bulk-entry?from=' ~ month_start.isoformat() ~ '&to=' ~ month_end.isoformat(), '/static/icons/batch.svg', 'Mehrfacheingabe') }}
|
||||
{{ icon_link('/export?from=' ~ month_start.isoformat() ~ '&to=' ~ month_end.isoformat(), '/static/icons/export.svg', 'Export') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ kpi_bar([
|
||||
{'label': 'IST', 'value': ('%.2f'|format(month_ist / 60) )|replace('.00', '')},
|
||||
{'label': 'SOLL', 'value': ('%.2f'|format(month_soll / 60))|replace('.00', '')},
|
||||
{'label': 'DELTA', 'value': ('%.2f'|format(month_delta / 60))|replace('.00', ''), 'value_class': 'negative' if month_delta < 0 else 'positive'},
|
||||
{'label': 'KUMULIERT', 'value': ('%.2f'|format(header_cumulative_minutes / 60))|replace('.00', ''), 'value_class': 'negative' if header_cumulative_minutes < 0 else 'positive'}
|
||||
], 'kpi-bar--month') }}
|
||||
|
||||
{{ workhours_target_warning_banner(workhours_target_warning) }}
|
||||
|
||||
<section class="week-group-list">
|
||||
{% for week in weeks %}
|
||||
{% call week_group_card_mobile(week, csrf_token, return_to) %}
|
||||
{{ week_group_header(week, csrf_token, return_to) }}
|
||||
<div class="day-list day-list--month">
|
||||
{% for day in week.days %}
|
||||
{{ day_row(day, csrf_token, weekday_name_de(day.date) ~ ', ' ~ day.date.strftime('%d.%m.%Y'), return_to, 'month') }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button, link_button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header(title, selected_date.strftime('%d.%m.%Y')) }}
|
||||
{% call card('form-card') %}
|
||||
<div class="stack">
|
||||
{% if has_entry %}
|
||||
<p class="muted">An diesem Tag ist bereits regulaere Arbeitszeit eingetragen. Stundenausgleich ist dann nicht verfuegbar.</p>
|
||||
<div class="nav-row">
|
||||
{{ link_button('Zeiteintrag bearbeiten', '/entry/' ~ existing_entry_id ~ '/edit', 'primary') }}
|
||||
{{ link_button('Zurueck', return_to, 'ghost') }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">
|
||||
Baut Ueberstunden direkt auf oder ab. Der Eintrag wirkt nur auf den Saldo, nicht auf Ist-Stunden oder den Arbeitsstunden-Counter.
|
||||
</p>
|
||||
{% if day_is_vacation %}
|
||||
<p class="muted">Fuer diesen Tag ist zusaetzlich Urlaub gesetzt.</p>
|
||||
{% elif day_special_status == 'holiday' %}
|
||||
<p class="muted">Fuer diesen Tag ist zusaetzlich Feiertag gesetzt.</p>
|
||||
{% elif day_special_status == 'sick' %}
|
||||
<p class="muted">Fuer diesen Tag ist zusaetzlich Krankheit gesetzt.</p>
|
||||
{% endif %}
|
||||
{% if overtime_adjustment_error %}
|
||||
<p class="flash flash--error">{{ overtime_adjustment_error }}</p>
|
||||
{% endif %}
|
||||
{% if day_overtime_adjustment_minutes %}
|
||||
<p class="muted">
|
||||
Aktuell gesetzt:
|
||||
<strong>{{ '+' if day_overtime_adjustment_minutes > 0 else '' }}{{ minutes_to_hhmm(day_overtime_adjustment_minutes) }}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<hr />
|
||||
|
||||
<form method="post" action="/overtime-adjustment/set" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ selected_date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<input type="hidden" name="adjustment_mode" value="manual" />
|
||||
<h2>Stunden</h2>
|
||||
{{ input_field('Manuell (+HH:MM oder -HH:MM)', 'adjustment_value', type='text', value='', attrs='placeholder=\"+02:30\"') }}
|
||||
<div class="nav-row">
|
||||
{{ button('Stunden speichern', type='submit') }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<form method="post" action="/overtime-adjustment/set" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ selected_date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<input type="hidden" name="adjustment_mode" value="interval" />
|
||||
<h2>Von-Bis Uhrzeit</h2>
|
||||
<div class="inline-grid">
|
||||
{{ input_field('Von', 'interval_start_time', type='time', value='', required=true) }}
|
||||
{{ input_field('Bis', 'interval_end_time', type='time', value='', required=true) }}
|
||||
</div>
|
||||
<div class="nav-row">
|
||||
<button type="submit" name="interval_direction" value="positive" class="btn btn--primary">Als Plus speichern</button>
|
||||
<button type="submit" name="interval_direction" value="negative" class="btn btn--ghost">Als Minus speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<form method="post" action="/overtime-adjustment/set" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ selected_date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<input type="hidden" name="adjustment_mode" value="full_day" />
|
||||
<h2>Ganzer Tag</h2>
|
||||
<p class="muted">Der Tageswert wird aus Wochenstunden geteilt durch die relevanten Arbeitstage berechnet.</p>
|
||||
<div class="nav-row">
|
||||
<button type="submit" name="full_day_direction" value="positive" class="btn btn--primary">Ganzer Tag +</button>
|
||||
<button type="submit" name="full_day_direction" value="negative" class="btn btn--ghost">Ganzer Tag -</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="nav-row">
|
||||
{% if day_overtime_adjustment_minutes %}
|
||||
<form method="post" action="/overtime-adjustment/clear" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ selected_date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit" class="btn btn--ghost">Stundenausgleich entfernen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{{ link_button('Zurueck', return_to, 'ghost') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Neues Passwort setzen{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("Neues Passwort setzen") }}
|
||||
{% call card('auth-card') %}
|
||||
{% if token %}
|
||||
<form action="/password-reset/confirm" method="post" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="token" value="{{ token }}" />
|
||||
{{ input_field('Neues Passwort (mindestens 10 Zeichen)', 'new_password', type='password', required=true, autocomplete='new-password', attrs='minlength="10"') }}
|
||||
{{ input_field('Neues Passwort wiederholen', 'new_password_repeat', type='password', required=true, autocomplete='new-password', attrs='minlength="10"') }}
|
||||
{{ button('Passwort speichern', type='submit') }}
|
||||
</form>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="/login">Zur Anmeldung</a>
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/card.html" import card %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% from "ui/page_header.html" import page_header %}
|
||||
{% block title %}Passwort zurücksetzen{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header('Passwort zurücksetzen', 'Gib deine E-Mail ein. Du erhältst einen Link zum Setzen eines neuen Passworts.') }}
|
||||
{% call card('auth-card') %}
|
||||
<form action="/password-reset/request" method="post" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
{{ input_field('E-Mail', 'email', type='email', required=true, autocomplete='username') }}
|
||||
{{ button('Reset-Link senden', type='submit') }}
|
||||
</form>
|
||||
<p>
|
||||
<a href="/login">Zur Anmeldung</a>
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,171 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/button.html" import button %}
|
||||
{% from "ui/form_field.html" import input_field %}
|
||||
{% block title %}Registrierung{% endblock %}
|
||||
{% block body_class %}register-theme{% endblock %}
|
||||
{% block page_class %}register-page{% endblock %}
|
||||
{% block content %}
|
||||
<section class="register-shell">
|
||||
<h1 class="register-title">Registrierung</h1>
|
||||
|
||||
<form action="/register" method="post" enctype="multipart/form-data" class="register-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
|
||||
<section class="register-section register-section-auth">
|
||||
{{ input_field('E-Mail', 'email', type='email', required=true, autocomplete='username') }}
|
||||
{{ input_field('Passwort (mindestens 10 Zeichen)', 'password', type='password', required=true, autocomplete='new-password', attrs='minlength="10"') }}
|
||||
<p class="muted register-email-note">Nach dem Anlegen bestätigst du deine E-Mail-Adresse über einen Link.</p>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Backup importieren</h2>
|
||||
<p class="muted register-subtitle">Wenn du bereits eine Sicherung aus Stundenfuchs hast, kannst du sie direkt bei der Registrierung einspielen.</p>
|
||||
<label class="form-field">
|
||||
<span class="form-field__label">Backup-Datei (optional)</span>
|
||||
<input class="input" type="file" name="backup_file" accept=".json,application/json" />
|
||||
</label>
|
||||
<p class="muted register-subtitle">
|
||||
Deine E-Mail-Adresse, dein Passwort und deine gewählte Zwei-Faktor-Anmeldung bleiben erhalten.
|
||||
Arbeitsdaten und fachliche Einstellungen werden aus dem Backup übernommen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p class="muted register-section-note">
|
||||
Alle folgenden Einstellungen sind optional und können auch später in den Einstellungen geändert werden.
|
||||
</p>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Bundesland</h2>
|
||||
<p class="muted register-subtitle">für das automatische Festlegen von Feiertagen</p>
|
||||
<label>
|
||||
<select name="federal_state">
|
||||
<option value="">Bundesland auswählen...</option>
|
||||
{% for state in federal_state_options %}<option value="{{ state.code }}">{{ state.label }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Urlaub</h2>
|
||||
<p class="muted register-subtitle">Wieviele Urlaubstage pro Jahr stehen dir zur Verfügung</p>
|
||||
{{ input_field('', 'vacation_days_total', type='number', placeholder='z. B. 30', attrs='min="0" max="365" step="1"') }}
|
||||
{{ input_field('Wochenstunden (Standard)', 'weekly_target_hours', type='number', value='25', attrs='min="0.25" step="0.25"') }}
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Erfassungsmodus</h2>
|
||||
<p class="muted register-subtitle">Lege fest, ob du deine Arbeitstage komplett selbst pflegst oder ob Stundenfuchs fehlende Arbeitstage bis heute automatisch ergänzt.</p>
|
||||
<label>
|
||||
<select name="entry_mode">
|
||||
<option value="manual" selected>Manuell</option>
|
||||
<option value="auto_until_today">Automatisch bis heute</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Überstunden</h2>
|
||||
<div class="register-grid-2">
|
||||
{{ input_field('Startdatum für Überstundenberechnung', 'overtime_start_date', type='date', value=today_iso) }}
|
||||
{{ input_field('Nach welchem Zeitraum verfallen Überstunden (Tage)', 'overtime_expiry_days', type='number', placeholder='optional', attrs='min="1" step="1"') }}
|
||||
</div>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="expire_negative_overtime" checked />
|
||||
<span>Negative Stunden verfallen auch</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Gesamtarbeitsstunden</h2>
|
||||
<p class="muted register-subtitle">z. B. für die Übersicht zu geleisteten Praxisstunden im Anerkennungsjahr der Erzieherausbildung.</p>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="workhours_counter_enabled" />
|
||||
<span>Gesamtarbeitsstunden Counter aktivieren</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Gesamtarbeitsstunden</h2>
|
||||
<p class="muted register-subtitle">z. B. für die Übersicht zu geleisteten Praxisstunden im Anerkennungsjahr der Erzieherausbildung.</p>
|
||||
<div class="register-grid-2 register-grid-counter">
|
||||
{{ input_field('Counter Startdatum', 'workhours_counter_start_date', type='date') }}
|
||||
{{ input_field('Counter Enddatum', 'workhours_counter_end_date', type='date') }}
|
||||
</div>
|
||||
{{ input_field('Bereits geleistete Zusatzstunden (optional)', 'workhours_counter_manual_offset_hours', type='number', placeholder='z. B. 80', attrs='min="0" step="0.25"') }}
|
||||
<p class="muted register-subtitle">Zusätzlich geleistete Stunden, z.B. aus Praktika</p>
|
||||
{{ input_field('Gesamtstundenziel (in Stunden)', 'workhours_counter_target_hours', type='number', placeholder='z. B. 1200', attrs='min="0.25" step="0.25"') }}
|
||||
<div class="register-checkbox-row">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="workhours_counter_show_in_header" />
|
||||
<span>Counter im Header anzeigen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="workhours_counter_target_email_enabled" />
|
||||
<span>Zielwarnung per E-Mail (wenn das Ziel im angegebenen Zeitraum nicht erreicht werden sollte aufgrund von z. B. Krankheit)</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Relevante Arbeitstage</h2>
|
||||
<p class="muted register-subtitle">
|
||||
Diese Tage steuern Soll und Urlaubslogik. Beispiel: Wenn du nur Montag bis Donnerstag arbeitest, werden Sollstunden auf diese vier Tage verteilt.
|
||||
</p>
|
||||
<fieldset class="weekday-fieldset register-weekday-fieldset">
|
||||
<div class="register-weekday-grid">
|
||||
{% for weekday in weekday_options %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="working_days"
|
||||
value="{{ weekday.value }}"
|
||||
{% if weekday.value < 5 %}checked{% endif %} />
|
||||
<span>{{ weekday.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="register-checkbox-row">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="count_vacation_as_worktime" />
|
||||
<span>Urlaubstage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="count_holiday_as_worktime" />
|
||||
<span>Feiertage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="count_sick_as_worktime" />
|
||||
<span>Kranktage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="automatic_break_rules_enabled" />
|
||||
<span>Gesetzliche Pausen automatisch nach deutscher Arbeitszeit berechnen</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="register-section">
|
||||
<h2>Zwei-Faktor-Anmeldung</h2>
|
||||
<p class="muted register-subtitle">Zur Erhöhung der Sicherheit</p>
|
||||
<label>
|
||||
<select name="mfa_preference">
|
||||
<option value="none" selected>Keine 2FA</option>
|
||||
<option value="totp">Authenticator-App (6-stelliger Code)</option>
|
||||
<option value="email">E-Mail-Code</option>
|
||||
</select>
|
||||
</label>
|
||||
{% if not email_mfa_available %}
|
||||
<p class="muted">Hinweis: E-Mail-2FA ist aktuell nicht verfügbar, da kein Mailserver konfiguriert ist.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="register-actions">
|
||||
{{ button('Konto anlegen', type='submit', extra_class='register-submit') }}
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<p class="register-footer">
|
||||
Du hast bereits ein Konto? <a href="/login">Zur Anmeldung</a>
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,812 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "ui/segmented_toggle.html" import segmented_toggle %}
|
||||
{% from "ui/collapsible_section.html" import collapsible_section %}
|
||||
{% block title %}Einstellungen{% endblock %}
|
||||
{% block body_class %}settings-theme{% endblock %}
|
||||
{% block page_class %}settings-page{% endblock %}
|
||||
{% block content %}
|
||||
<section class="top-row">
|
||||
<h1>Einstellungen</h1>
|
||||
</section>
|
||||
{% if is_admin %}
|
||||
{{ segmented_toggle([
|
||||
{'href': '/settings?tab=settings', 'label': 'Einstellungen', 'active': active_settings_tab != 'admin'},
|
||||
{'href': '/settings?tab=admin', 'label': 'Admin', 'active': active_settings_tab == 'admin'}
|
||||
], 'Einstellungsbereiche', 'settings-tabs') }}
|
||||
{% endif %}
|
||||
<section class="settings-grid">
|
||||
{% if not is_admin or active_settings_tab != 'admin' %}
|
||||
{% call collapsible_section('Urlaub', 'settings-vacation') %}
|
||||
<p class="muted">Lege hier deine Gesamturlaubstage pro Kalenderjahr fest. Im Header siehst du danach Resturlaub/Gesamturlaub.</p>
|
||||
<form method="post" action="/settings/vacation-allowance" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="vacation_show_in_header_present" value="1" />
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
Gesamturlaubstage pro Jahr
|
||||
<input type="number"
|
||||
min="0"
|
||||
max="365"
|
||||
step="1"
|
||||
name="vacation_days_total"
|
||||
value="{{ user.vacation_days_total }}"
|
||||
required />
|
||||
</label>
|
||||
<label>
|
||||
Aktueller Stand ({{ header_vacation_year }})
|
||||
<input type="text"
|
||||
value="{{ header_vacation_days_remaining }} / {{ header_vacation_days_total }} (verplant/genutzt: {{ header_vacation_days_used }})"
|
||||
disabled />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="vacation_show_in_header"
|
||||
{% if user.vacation_show_in_header %}checked{% endif %} />
|
||||
<span>Resturlaub im Header anzeigen</span>
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
<p class="muted">Definierte Urlaubstage reduzieren automatisch das Wochen-Soll für die betroffenen Wochen.</p>
|
||||
<form method="post" action="/settings/vacations/add" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
Startdatum
|
||||
<input type="date" name="start_date" value="{{ vacation_start }}" required />
|
||||
</label>
|
||||
<label>
|
||||
Enddatum
|
||||
<input type="date" name="end_date" value="{{ vacation_end }}" required />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="include_weekends" />
|
||||
<span>Wochenenden mit einschließen</span>
|
||||
</label>
|
||||
<label>
|
||||
Notiz (optional)
|
||||
<input type="text" name="notes" />
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
<div class="vacation-list">
|
||||
{% for vacation in vacation_ranges %}
|
||||
<article class="vacation-item">
|
||||
<div>
|
||||
<strong>{{ vacation.start_date.strftime("%d.%m.%Y") }} - {{ vacation.end_date.strftime("%d.%m.%Y") }}</strong>
|
||||
<p class="muted">Effektive Urlaubstage unter Berücksichtigung deiner Arbeitstage.</p>
|
||||
</div>
|
||||
<form method="post"
|
||||
action="/settings/vacations/delete-range"
|
||||
class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden"
|
||||
name="start_date"
|
||||
value="{{ vacation.start_date.isoformat() }}" />
|
||||
<input type="hidden"
|
||||
name="end_date"
|
||||
value="{{ vacation.end_date.isoformat() }}" />
|
||||
<button type="submit" class="button danger">Löschen</button>
|
||||
</form>
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="muted">Noch keine Urlaubszeiträume angelegt.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Wochenstunden', 'settings-weekly-target') %}
|
||||
<p class="muted">Lege fest, wie viele Stunden du generell pro Woche arbeiten möchtest (Standard-Soll).</p>
|
||||
<form method="post" action="/settings/weekly-target" class="stack" data-component="break-settings-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Wochenstunden
|
||||
<input type="number"
|
||||
min="0.25"
|
||||
step="0.25"
|
||||
name="weekly_target_hours"
|
||||
value="{{ '%.2f'|format(settings_weekly_target_minutes / 60) |replace('.00', '') }}"
|
||||
required />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="automatic_break_rules_enabled"
|
||||
data-break-settings-toggle
|
||||
{% if user.automatic_break_rules_enabled %}checked{% endif %} />
|
||||
<span>Gesetzliche Pausen automatisch nach deutscher Arbeitszeit berechnen</span>
|
||||
</label>
|
||||
<label>
|
||||
Tägliche Pause in Minuten
|
||||
<input type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
name="default_break_minutes"
|
||||
value="{{ user.default_break_minutes }}"
|
||||
data-break-settings-minutes
|
||||
{% if user.automatic_break_rules_enabled %}disabled{% endif %} />
|
||||
</label>
|
||||
<p class="muted">
|
||||
Dieser Wert wird für neue reguläre Arbeitszeiteinträge und automatische Einträge verwendet, solange die gesetzliche Pausenregel nicht aktiv ist.
|
||||
</p>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Standardansicht', 'settings-preferences') %}
|
||||
<form method="post" action="/settings/preferences" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="preferred_month_view_mode" value="flat" />
|
||||
<label>
|
||||
Startansicht nach Anmeldung
|
||||
<select name="preferred_home_view" required>
|
||||
<option value="week"
|
||||
{% if user.preferred_home_view == 'week' %}selected{% endif %}>Wochenansicht</option>
|
||||
<option value="month"
|
||||
{% if user.preferred_home_view == 'month' %}selected{% endif %}>Monatsansicht</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Erfassungsmodus
|
||||
<select name="entry_mode" required>
|
||||
<option value="manual"
|
||||
{% if user.entry_mode == 'manual' %}selected{% endif %}>Manuell (jeden Tag selbst erfassen)</option>
|
||||
<option value="auto_until_today"
|
||||
{% if user.entry_mode == 'auto_until_today' %}selected{% endif %}>Automatisch bis heute</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="muted">
|
||||
Im automatischen Modus werden fehlende Einträge für deine Arbeitstage bis einschließlich heute automatisch angelegt. Abweichungen kannst du danach einzeln anpassen.
|
||||
</p>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Überstunden-Regeln', 'settings-overtime') %}
|
||||
<p class="muted">Optionales Startdatum und Verfall für die kumulierte Überstunden-Berechnung.</p>
|
||||
<form method="post" action="/settings/overtime" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
Startdatum (optional)
|
||||
<input type="date"
|
||||
name="overtime_start_date"
|
||||
value="{{ user.overtime_start_date.isoformat() if user.overtime_start_date else '' }}" />
|
||||
</label>
|
||||
<label>
|
||||
Verfall in Tagen (optional)
|
||||
<input type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
name="overtime_expiry_days"
|
||||
value="{{ user.overtime_expiry_days if user.overtime_expiry_days is not none else '' }}" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="expire_negative_overtime"
|
||||
{% if user.expire_negative_overtime %}checked{% endif %} />
|
||||
<span>Negative Stunden verfallen ebenfalls</span>
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
<div class="vacation-list">
|
||||
<article class="vacation-item">
|
||||
<div>
|
||||
<strong>Saldoaufbau gesamt</strong>
|
||||
<p class="muted">{{ minutes_to_hhmm(overtime_adjustment_total_positive) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Saldoabbau gesamt</strong>
|
||||
<p class="muted">{{ minutes_to_hhmm(overtime_adjustment_total_negative) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Ganze Tage</strong>
|
||||
<p class="muted">{{ overtime_adjustment_full_day_count }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% for adjustment in overtime_adjustments %}
|
||||
<article class="vacation-item">
|
||||
<div>
|
||||
<strong>{{ adjustment.date.strftime("%d.%m.%Y") }}</strong>
|
||||
<p class="muted">{{ adjustment.notes or "Stundenausgleich" }}</p>
|
||||
</div>
|
||||
<div class="settings-adjustment-meta">
|
||||
<strong class="{% if adjustment.minutes < 0 %}negative{% else %}positive{% endif %}">
|
||||
{{ '+' if adjustment.minutes > 0 else '' }}{{ minutes_to_hhmm(adjustment.minutes) }}
|
||||
</strong>
|
||||
<a href="/overtime-adjustment/edit?date={{ adjustment.date.isoformat() }}" class="button ghost">Bearbeiten</a>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="muted">Noch keine Ausgleichsstunden eingetragen.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Arbeitsstunden-Counter', 'settings-workhours-counter') %}
|
||||
<div class="settings-workhours-intro stack">
|
||||
<p class="muted">Hier kannst du einen Zeitraum festlegen und sehen, wie viele Stunden du darin insgesamt gesammelt hast. Das ist zum Beispiel hilfreich für Praxisstunden im Anerkennungsjahr.</p>
|
||||
<p class="muted">Urlaub, Feiertage und Krankheit werden nur dann mitgezählt, wenn du das unter „Relevante Arbeitstage“ aktiviert hast.</p>
|
||||
</div>
|
||||
<form method="post" action="/settings/workhours-counter" class="settings-workhours-form stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="workhours_counter_enabled"
|
||||
{% if user.workhours_counter_enabled %}checked{% endif %} />
|
||||
<span>Arbeitsstunden-Counter aktivieren</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="workhours_counter_show_in_header"
|
||||
{% if user.workhours_counter_show_in_header %}checked{% endif %} />
|
||||
<span>Counter im Header anzeigen</span>
|
||||
</label>
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
Zeitraum von
|
||||
<input type="date"
|
||||
name="workhours_counter_start_date"
|
||||
value="{{ user.workhours_counter_start_date.isoformat() if user.workhours_counter_start_date else '' }}" />
|
||||
</label>
|
||||
<label>
|
||||
Zeitraum bis
|
||||
<input type="date"
|
||||
name="workhours_counter_end_date"
|
||||
value="{{ user.workhours_counter_end_date.isoformat() if user.workhours_counter_end_date else '' }}" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-grid settings-workhours-inline-grid">
|
||||
<label class="settings-workhours-field">
|
||||
Zusätzliche Stunden (optional)
|
||||
<input type="number"
|
||||
min="0"
|
||||
step="0.25"
|
||||
name="workhours_counter_manual_offset_hours"
|
||||
value="{{ '%.2f'|format(user.workhours_counter_manual_offset_minutes / 60) |replace('.00', '') if user.workhours_counter_manual_offset_minutes else '' }}" />
|
||||
</label>
|
||||
<label class="settings-workhours-field">
|
||||
Zielstunden im Zeitraum (optional)
|
||||
<input type="number"
|
||||
min="0.25"
|
||||
step="0.25"
|
||||
name="workhours_counter_target_hours"
|
||||
value="{{ '%.2f'|format(user.workhours_counter_target_minutes / 60) |replace('.00', '') if user.workhours_counter_target_minutes is not none else '' }}" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="muted settings-workhours-field-hint">Zum Beispiel bereits geleistete Praxis- oder Praktikumsstunden, die nicht im Tracker erfasst wurden.</p>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="workhours_counter_target_email_enabled"
|
||||
{% if not mail_settings_available %}disabled{% endif %}
|
||||
{% if user.workhours_counter_target_email_enabled %}checked{% endif %} />
|
||||
<span>E-Mail senden, wenn das Ziel voraussichtlich nicht erreicht wird</span>
|
||||
</label>
|
||||
{% if not mail_settings_available %}
|
||||
<p class="muted">Diese Funktion ist erst verfügbar, wenn ein E-Mail-Server eingerichtet wurde.</p>
|
||||
{% endif %}
|
||||
<p class="muted">Beispiel: So kannst du deine Praxisstunden im Anerkennungsjahr im Blick behalten.</p>
|
||||
{% if user.workhours_counter_enabled %}
|
||||
<p class="muted">
|
||||
{% if workhours_counter_minutes is not none %}
|
||||
Aktueller Stand im gewählten Zeitraum:
|
||||
{% else %}
|
||||
Bitte gültigen Zeitraum setzen, um den Counter zu berechnen.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if workhours_counter_warning %}
|
||||
<div class="settings-counter-badges app-total-badges" aria-label="Ziel und Prognose">
|
||||
<span class="app-total-badge app-total-badge-workhours">
|
||||
<span class="app-total-badge__label">Bisher</span>
|
||||
<span class="app-total-badge__value">
|
||||
{% if workhours_counter_minutes is not none %}
|
||||
{{ minutes_to_hhmm(workhours_counter_minutes) }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
</span>
|
||||
</span>
|
||||
<span class="app-total-badge app-total-badge-target">
|
||||
<span class="app-total-badge__label">Ziel</span>
|
||||
<span class="app-total-badge__value">{{ minutes_to_hhmm(workhours_counter_warning.target_minutes) }}</span>
|
||||
</span>
|
||||
<span class="app-total-badge app-total-badge-projection {% if workhours_counter_warning.at_risk %}is-negative{% else %}is-positive{% endif %}">
|
||||
<span class="app-total-badge__label">Prognose</span>
|
||||
<span class="app-total-badge__value">{{ minutes_to_hhmm(workhours_counter_warning.projected_minutes) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
{% if workhours_counter_warning.at_risk %}
|
||||
<p class="muted">
|
||||
Bis zum Ziel fehlen voraussichtlich noch <strong class="negative">{{ minutes_to_hhmm(workhours_counter_warning.missing_minutes) }}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Relevante Arbeitstage', 'settings-workdays') %}
|
||||
<p class="muted">Diese Tage werden für Soll-/Delta-Berechnung verwendet (z. B. 4-Tage-Woche Mo-Do).</p>
|
||||
<form method="post" action="/settings/workdays" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<fieldset class="weekday-fieldset">
|
||||
<legend>Arbeitstage</legend>
|
||||
<div class="weekday-grid">
|
||||
{% for weekday in weekday_options %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="working_days"
|
||||
value="{{ weekday.value }}"
|
||||
{% if weekday.value in working_days_selected %}checked{% endif %} />
|
||||
<span>{{ weekday.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="stack">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="count_vacation_as_worktime"
|
||||
{% if user.count_vacation_as_worktime %}checked{% endif %} />
|
||||
<span>Urlaubstage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="count_holiday_as_worktime"
|
||||
{% if user.count_holiday_as_worktime %}checked{% endif %} />
|
||||
<span>Feiertage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="count_sick_as_worktime"
|
||||
{% if user.count_sick_as_worktime %}checked{% endif %} />
|
||||
<span>Kranktage wie reguläre Arbeitstage rechnen</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Sicherheit (2FA)', 'settings-mfa') %}
|
||||
{% set mfa_totp_pending = mfa_setup_secret and user.mfa_method == 'none' %}
|
||||
{% set mfa_selected_method = 'totp' if mfa_totp_pending else user.mfa_method %}
|
||||
<p class="muted">
|
||||
Status:
|
||||
<strong>
|
||||
{% if mfa_totp_pending %}
|
||||
TOTP-Einrichtung läuft
|
||||
{% else %}
|
||||
{{ mfa_method_labels.get(user.mfa_method, 'Unbekannt') }}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</p>
|
||||
{% if mfa_totp_pending %}
|
||||
<p class="muted">2FA wird aktiviert, sobald du den aktuellen 6-stelligen Code aus deiner Authenticator-App bestätigst.</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/settings/mfa" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Zwei-Faktor-Methode
|
||||
<select name="mfa_method" required>
|
||||
<option value="none" {% if mfa_selected_method == 'none' %}selected{% endif %}>Keine 2FA</option>
|
||||
<option value="totp" {% if mfa_selected_method == 'totp' %}selected{% endif %}>Authenticator-App (TOTP)</option>
|
||||
<option value="email" {% if mfa_selected_method == 'email' %}selected{% endif %}>E-Mail-Code</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Aktuelles Passwort bestätigen
|
||||
<input type="password" name="current_password" required />
|
||||
</label>
|
||||
<label>
|
||||
Setup-Code (nur für TOTP-Aktivierung)
|
||||
<input type="text"
|
||||
name="setup_code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6" />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="regenerate_totp" />
|
||||
<span>TOTP neu einrichten (alten Schlüssel verwerfen)</span>
|
||||
</label>
|
||||
{% if mfa_setup_secret %}
|
||||
<div class="settings-note">
|
||||
<p class="muted">
|
||||
<strong>TOTP-Setup aktiv:</strong> Hinterlege den folgenden Schlüssel oder die URI in deiner Authenticator-App und bestätige danach den Code.
|
||||
</p>
|
||||
<label>
|
||||
TOTP Secret
|
||||
<input type="text" value="{{ mfa_setup_secret }}" readonly />
|
||||
</label>
|
||||
<label>
|
||||
TOTP URI
|
||||
<input type="text" value="{{ mfa_setup_uri }}" readonly />
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
<div class="settings-auth-row">
|
||||
{% call collapsible_section('Account', 'settings-account', 'settings-auth-card', 'account-security') %}
|
||||
<form method="post" action="/settings/profile" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
E-Mail-Adresse
|
||||
<input type="email" name="email" value="{{ user.email }}" required />
|
||||
</label>
|
||||
<label>
|
||||
Bundesland
|
||||
<select name="federal_state">
|
||||
<option value="">Bitte auswählen</option>
|
||||
{% for state in federal_state_options %}
|
||||
<option value="{{ state.code }}"
|
||||
{% if user.federal_state == state.code %}selected{% endif %}>{{ state.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<p class="muted">
|
||||
Gesetzliche Feiertage deines Bundeslands werden automatisch als Feiertag markiert, sofern an diesen Tagen keine Arbeitszeit eingetragen ist.
|
||||
</p>
|
||||
<label>
|
||||
Aktuelles Passwort bestätigen
|
||||
<input type="password" name="current_password" required />
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Passwort ändern', 'settings-password', 'settings-auth-card', 'account-security') %}
|
||||
<form method="post" action="/settings/password" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Aktuelles Passwort
|
||||
<input type="password" name="current_password" required />
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort
|
||||
<input type="password" name="new_password" minlength="10" required />
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort wiederholen
|
||||
<input type="password" name="new_password_repeat" minlength="10" required />
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% call collapsible_section('Datenexport', 'settings-export') %}
|
||||
<p class="muted">Lade hier alle bisher eingetragenen Daten herunter. Für Excel und PDF wird dein kompletter erfasster Zeitraum exportiert. Die Backup-Datei ist für Sicherung und späteren Import gedacht.</p>
|
||||
<form method="post" action="/settings/export-all" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="settings-export-actions">
|
||||
<button type="submit" name="format" value="xlsx" class="button">Alles als Excel exportieren</button>
|
||||
<button type="submit" name="format" value="pdf" class="button ghost">Alles als PDF exportieren</button>
|
||||
<button type="submit" name="format" value="backup_json" class="button ghost">Backup-Datei herunterladen</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="muted">Die Backup-Datei enthält deine Einstellungen, Arbeitszeiteinträge, Urlaub, Sondertage, Soll-Historie und Stundenausgleich in einem strukturierten Format. Sicherheits- und Kontodaten sind nicht enthalten.</p>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Backup importieren', 'settings-import') %}
|
||||
<p class="muted">Du kannst eine zuvor exportierte Backup-Datei wieder einspielen. Dein Konto, dein Passwort und deine Sicherheitsdaten bleiben dabei unverändert.</p>
|
||||
<form method="post" action="/settings/import/preview" enctype="multipart/form-data" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Importmodus
|
||||
<select name="import_mode">
|
||||
<option value="merge" {% if import_mode_selected == 'merge' %}selected{% endif %}>Zusammenführen</option>
|
||||
<option value="replace_user_data"
|
||||
{% if import_mode_selected == 'replace_user_data' %}selected{% endif %}>Alle bisherigen Daten ersetzen</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="muted">
|
||||
Zusammenführen behält bestehende Tagesdaten bei und ergänzt nur konfliktfreie Inhalte. Ersetzen löscht zuerst alle importierbaren Arbeits- und Einstellungsdaten deines Kontos.
|
||||
</p>
|
||||
<label>
|
||||
Backup-Datei
|
||||
<input type="file" name="backup_file" accept=".json,application/json" required />
|
||||
</label>
|
||||
<button type="submit" class="button">Backup prüfen</button>
|
||||
</form>
|
||||
{% if import_preview %}
|
||||
<div class="settings-import-preview stack">
|
||||
<div class="settings-import-preview__header">
|
||||
<div>
|
||||
<h3>Importvorschau</h3>
|
||||
<p class="muted">
|
||||
Backup v{{ import_preview.backup_version }}
|
||||
{% if import_preview.source_app_version %}• exportiert mit {{ import_preview.source_app_version }}{% endif %}
|
||||
{% if import_preview.exported_at %}• {{ import_preview.exported_at }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<span class="settings-import-preview__mode">{{ import_preview.mode_label }}</span>
|
||||
</div>
|
||||
<div class="settings-import-grid">
|
||||
<div class="settings-import-stat">
|
||||
<strong>{{ import_preview.counts.time_entries }}</strong>
|
||||
<span>Arbeitszeiteinträge</span>
|
||||
</div>
|
||||
<div class="settings-import-stat">
|
||||
<strong>{{ import_preview.counts.weekly_target_rules }}</strong>
|
||||
<span>Wochenziele</span>
|
||||
</div>
|
||||
<div class="settings-import-stat">
|
||||
<strong>{{ import_preview.counts.vacation_periods }}</strong>
|
||||
<span>Urlaubszeiträume</span>
|
||||
</div>
|
||||
<div class="settings-import-stat">
|
||||
<strong>{{ import_preview.counts.special_day_statuses }}</strong>
|
||||
<span>Sondertage</span>
|
||||
</div>
|
||||
<div class="settings-import-stat">
|
||||
<strong>{{ import_preview.counts.overtime_adjustments }}</strong>
|
||||
<span>Stundenausgleich</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-import-summary">
|
||||
<p class="muted">
|
||||
Übernommen werden auch fachliche Einstellungen wie Wochenstunden, relevante Arbeitstage, Überstunden-Regeln, Arbeitsstunden-Counter und das Bundesland.
|
||||
</p>
|
||||
<ul class="settings-import-conflicts">
|
||||
<li>Konflikte Arbeitszeiteinträge: {{ import_preview.conflicts.time_entries }}</li>
|
||||
<li>Konflikte Wochenziele: {{ import_preview.conflicts.weekly_target_rules }}</li>
|
||||
<li>Konflikte Urlaubszeiträume: {{ import_preview.conflicts.vacation_periods }}</li>
|
||||
<li>Konflikte Sondertage: {{ import_preview.conflicts.special_day_statuses }}</li>
|
||||
<li>Konflikte Stundenausgleich: {{ import_preview.conflicts.overtime_adjustments }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form method="post" action="/settings/import/execute" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="preview_id" value="{{ import_preview.id }}" />
|
||||
{% if import_preview.mode == 'replace_user_data' %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="confirm_replace" />
|
||||
<span>Ich möchte meine bisherigen importierbaren Daten wirklich vollständig durch dieses Backup ersetzen.</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
<button type="submit" class="button">Import jetzt ausführen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Konto löschen', 'settings-delete-account', 'danger-card') %}
|
||||
<p class="muted">Wenn du dein Konto löschst, werden alle zugehörigen Daten dauerhaft entfernt: Arbeitszeiten, Urlaub, Sondertage, Stundenausgleich, Soll-Historie und persönliche Einstellungen.</p>
|
||||
<form method="post" action="/settings/account/delete" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Zur Bestätigung deine E-Mail-Adresse eingeben
|
||||
<input type="email" name="confirm_email" placeholder="{{ user.email }}" required />
|
||||
</label>
|
||||
<label>
|
||||
Aktuelles Passwort bestätigen
|
||||
<input type="password" name="current_password" required />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="confirm_delete" />
|
||||
<span>Ich möchte mein Konto und alle zugehörigen Daten dauerhaft löschen.</span>
|
||||
</label>
|
||||
<button type="submit" class="button danger">Konto dauerhaft löschen</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% if is_admin and active_settings_tab == 'admin' %}
|
||||
{% call collapsible_section('Benutzerverwaltung', 'settings-admin-users', 'admin-card') %}
|
||||
<p class="muted">Aktive Admins: {{ admin_user_count }}</p>
|
||||
<div class="admin-user-list">
|
||||
{% for managed in managed_users %}
|
||||
{% set disable_delete = managed.id == user.id or (managed.role == 'admin' and managed.is_active and admin_user_count <= 1) %}
|
||||
<form method="post"
|
||||
action="/settings/admin/users/{{ managed.id }}"
|
||||
class="admin-user-row">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="admin-user-meta">
|
||||
<strong>{{ managed.email }}</strong>
|
||||
<span class="muted">erstellt: {{ managed.created_at.strftime("%d.%m.%Y") }}</span>
|
||||
</div>
|
||||
<label>
|
||||
Rolle
|
||||
<select name="role" required>
|
||||
<option value="user" {% if managed.role == 'user' %}selected{% endif %}>User</option>
|
||||
<option value="admin" {% if managed.role == 'admin' %}selected{% endif %}>Admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="is_active"
|
||||
{% if managed.is_active %}checked{% endif %} />
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="reset_mfa" />
|
||||
<span>MFA zurücksetzen</span>
|
||||
</label>
|
||||
<div class="admin-user-actions">
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
<button type="submit"
|
||||
formaction="/settings/admin/users/{{ managed.id }}/delete"
|
||||
formmethod="post"
|
||||
class="button danger"
|
||||
{% if disable_delete %}disabled{% endif %}
|
||||
onclick="return confirm('Benutzer wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.');">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('E-Mail-Server', 'settings-admin-email', 'admin-card') %}
|
||||
<p class="muted">Wird für Passwort-Reset, E-Mail-MFA und Registrierungsmails verwendet.</p>
|
||||
<form method="post" action="/settings/admin/email-server" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="inline-grid">
|
||||
<label>
|
||||
SMTP Host
|
||||
<input type="text"
|
||||
name="smtp_host"
|
||||
value="{{ email_server.smtp_host }}"
|
||||
required />
|
||||
</label>
|
||||
<label>
|
||||
SMTP Port
|
||||
<input type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
name="smtp_port"
|
||||
value="{{ email_server.smtp_port }}"
|
||||
required />
|
||||
</label>
|
||||
<label>
|
||||
SMTP Username
|
||||
<input type="text"
|
||||
name="smtp_username"
|
||||
value="{{ email_server.smtp_username }}" />
|
||||
</label>
|
||||
<label>
|
||||
SMTP Passwort
|
||||
{% if email_server.has_password %}(leer lassen = unverändert){% endif %}
|
||||
<input type="password" name="smtp_password" />
|
||||
</label>
|
||||
<label>
|
||||
Absender E-Mail
|
||||
<input type="email"
|
||||
name="from_email"
|
||||
value="{{ email_server.from_email }}"
|
||||
required />
|
||||
</label>
|
||||
<label>
|
||||
Absender Name
|
||||
<input type="text"
|
||||
name="from_name"
|
||||
value="{{ email_server.from_name }}"
|
||||
required />
|
||||
</label>
|
||||
</div>
|
||||
<div class="inline-grid">
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="use_starttls"
|
||||
{% if email_server.use_starttls %}checked{% endif %} />
|
||||
<span>STARTTLS verwenden</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="use_ssl"
|
||||
{% if email_server.use_ssl %}checked{% endif %} />
|
||||
<span>SMTP SSL verwenden</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="verify_tls"
|
||||
{% if email_server.verify_tls %}checked{% endif %} />
|
||||
<span>TLS Zertifikat prüfen</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="registration_mails_enabled"
|
||||
{% if email_server.registration_mails_enabled %}checked{% endif %} />
|
||||
<span>Registrierungsmails aktiv</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="password_reset_mails_enabled"
|
||||
{% if email_server.password_reset_mails_enabled %}checked{% endif %} />
|
||||
<span>Passwort-Reset-Mails aktiv</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="registration_admin_notify_enabled"
|
||||
{% if email_server.registration_admin_notify_enabled %}checked{% endif %} />
|
||||
<span>Infomails bei neuer Registrierung aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-recipient-picker">
|
||||
<p class="muted">Empfänger für Registrierungs-Infomails (aktive Admins)</p>
|
||||
{% if admin_recipients %}
|
||||
<div class="admin-recipient-grid">
|
||||
{% for admin_recipient in admin_recipients %}
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox"
|
||||
name="registration_admin_notify_admin_ids"
|
||||
value="{{ admin_recipient.id }}"
|
||||
{% if admin_recipient.id in email_server.registration_admin_notify_admin_ids %}checked{% endif %} />
|
||||
<span>{{ admin_recipient.email }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Keine aktiven Admins gefunden.</p>
|
||||
{% endif %}
|
||||
{% if email_server.registration_notify_fallback_email %}
|
||||
<p class="muted">
|
||||
Wenn keine Admins ausgewählt sind, wird die Fallback-Adresse
|
||||
<strong>{{ email_server.registration_notify_fallback_email }}</strong> genutzt.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
<form method="post"
|
||||
action="/settings/admin/email-server/test"
|
||||
class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button type="submit" class="button ghost">Testmail an mich senden</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Rechtliches', 'settings-admin-legal', 'admin-card') %}
|
||||
<p class="muted">Diese Inhalte werden öffentlich über den Footer unter Impressum und Datenschutz angezeigt. Markdown ist erlaubt und wird beim Anzeigen sicher bereinigt.</p>
|
||||
<form method="post" action="/settings/admin/site-content" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<label>
|
||||
Impressum (Markdown)
|
||||
<textarea name="impressum_markdown" rows="14" required>{{ site_content_markdown['impressum'] }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
Datenschutz (Markdown)
|
||||
<textarea name="privacy_markdown" rows="16" required>{{ site_content_markdown['datenschutz'] }}</textarea>
|
||||
</label>
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% call collapsible_section('Kontakt & Tickets', 'settings-admin-tickets', 'admin-card') %}
|
||||
<p class="muted">Neue Nachrichten aus dem Kontaktformular werden hier als Tickets gesammelt. Für Benachrichtigungen werden dieselben Admin-Empfänger wie bei Registrierungs-Infomails verwendet.</p>
|
||||
<div class="support-ticket-list">
|
||||
{% for ticket in support_tickets %}
|
||||
<article class="support-ticket-card">
|
||||
<div class="support-ticket-card__header">
|
||||
<div class="support-ticket-card__title-wrap">
|
||||
<h3>{{ ticket.subject }}</h3>
|
||||
<p class="muted">
|
||||
{{ ticket_category_label(ticket.category) }} · {{ ticket_status_label(ticket.status) }} · {{ ticket.created_at.strftime("%d.%m.%Y %H:%M") }} UTC
|
||||
</p>
|
||||
</div>
|
||||
<div class="support-ticket-card__meta">
|
||||
<strong>{{ ticket.email }}</strong>
|
||||
<span class="muted">{{ ticket.name or 'Ohne Namen' }}</span>
|
||||
{% if ticket.user_id %}<span class="muted">Angemeldeter Nutzer</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="support-ticket-card__message">
|
||||
{{ ticket.message }}
|
||||
</div>
|
||||
<form method="post" action="/settings/admin/tickets/{{ ticket.id }}" class="stack">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<div class="inline-grid support-ticket-card__controls">
|
||||
<label>
|
||||
Status
|
||||
<select name="status" required>
|
||||
<option value="open" {% if ticket.status == 'open' %}selected{% endif %}>Offen</option>
|
||||
<option value="closed" {% if ticket.status == 'closed' %}selected{% endif %}>Geschlossen</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Interne Notiz
|
||||
<textarea name="admin_notes" rows="4">{{ ticket.admin_notes or '' }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="button">Ticket speichern</button>
|
||||
</form>
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="muted">Aktuell liegen keine Kontakt-Tickets vor.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% from "ui/flash.html" import alert %}
|
||||
{% set error_message = error if error is defined else None %}
|
||||
{% if success_message %}{{ alert(success_message, "success") }}{% endif %}
|
||||
{% if error_message %}{{ alert(error_message, "error") }}{% endif %}
|
||||
{% if info_message %}{{ alert(info_message, "info") }}{% endif %}
|
||||
{% if flash_messages %}
|
||||
{% for flash in flash_messages %}{{ alert(flash.message, flash.level or "info") }}{% endfor %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,10 @@
|
||||
<footer class="site-footer">
|
||||
<div class="container site-footer-inner">
|
||||
<small class="muted">{{ app_name }}</small>
|
||||
<nav class="site-footer-nav" aria-label="Footer">
|
||||
<a href="/kontakt">Kontakt</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,38 @@
|
||||
{% set is_auth_header = (not user) and (request.url.path.startswith('/login') or request.url.path.startswith('/register')) %}
|
||||
<header class="site-header app-topbar {% if is_auth_header %}is-auth-header{% endif %}">
|
||||
<div class="container app-topbar-inner {% if user %}is-user{% else %}is-guest{% endif %}">
|
||||
<div class="app-brand-wrap">
|
||||
<a class="brand app-brand"
|
||||
href="{% if user %}{% if user.preferred_home_view == 'month' %}/month?view={{ user.preferred_month_view_mode or 'flat' }}{% else %}/dashboard{% endif %}{% else %}/{% endif %}"
|
||||
aria-label="{{ app_name }} Startseite">
|
||||
<img class="app-logo" src="/img/Logo.svg" alt="{{ app_name }}" />
|
||||
</a>
|
||||
{% if user %}
|
||||
<div class="app-total-badges">
|
||||
{% if header_vacation_visible is not defined or header_vacation_visible %}
|
||||
<span class="app-total-badge app-total-badge-vacation"
|
||||
title="Resturlaub / Gesamturlaubstage ({{ header_vacation_year }})">
|
||||
<span class="app-total-badge__label">Urlaub</span>
|
||||
<span class="app-total-badge__value">{{ header_vacation_days_remaining }}/{{ header_vacation_days_total }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if header_workhours_counter_visible and header_workhours_counter_minutes is not none %}
|
||||
<span class="app-total-badge app-total-badge-workhours"
|
||||
title="Arbeitsstunden-Counter">
|
||||
<span class="app-total-badge__label">Gesamt</span>
|
||||
<span class="app-total-badge__value">{{ minutes_to_hhmm(header_workhours_counter_minutes) }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if header_cumulative_minutes is not none %}
|
||||
<span class="app-total-badge {% if header_cumulative_minutes < 0 %}is-negative{% else %}is-positive{% endif %}"
|
||||
title="Kumulierte Überstunden">
|
||||
<span class="app-total-badge__label">Stunden</span>
|
||||
<span class="app-total-badge__value">{{ ('%.2f'|format(header_cumulative_minutes / 60) )|replace('.00', '') }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "partials/nav.html" %}
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,38 @@
|
||||
{% from "ui/segmented_toggle.html" import segmented_toggle %}
|
||||
|
||||
{% if user %}
|
||||
<div class="app-user-nav">
|
||||
{{ segmented_toggle([
|
||||
{'href': main_nav_week_url, 'label': 'Woche', 'active': request.url.path.startswith('/dashboard')},
|
||||
{'href': main_nav_month_url, 'label': 'Monat', 'active': request.url.path.startswith('/month')}
|
||||
], 'Hauptnavigation', 'topbar-toggle app-main-nav') }}
|
||||
<div class="app-icon-nav" aria-label="Kopfzeilen-Aktionen">
|
||||
<a class="app-icon-btn"
|
||||
href="/settings"
|
||||
title="Einstellungen"
|
||||
aria-label="Einstellungen">
|
||||
<img class="dash-icon" src="/static/icons/settings.svg" alt="" />
|
||||
</a>
|
||||
<a class="app-icon-btn"
|
||||
href="/hilfe"
|
||||
title="Hilfe"
|
||||
aria-label="Hilfe">
|
||||
<img class="dash-icon" src="/img/Icon-Help.svg" alt="" />
|
||||
</a>
|
||||
<form action="/logout" method="post" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button type="submit"
|
||||
class="app-icon-btn"
|
||||
title="Abmelden"
|
||||
aria-label="Abmelden">
|
||||
<img class="dash-icon" src="/static/icons/logout.svg" alt="" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ segmented_toggle([
|
||||
{'href': '/login', 'label': 'Login', 'active': request.url.path.startswith('/login')},
|
||||
{'href': '/register', 'label': 'Registrierung', 'active': request.url.path.startswith('/register')}
|
||||
], 'Authentifizierung', 'auth-toggle app-auth-nav') }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% if user and user.role == 'admin' %}
|
||||
<span class="admin-version-badge" aria-label="Version {{ app_version }}{% if app_env != 'production' %} {{ app_env|capitalize }}{% endif %}">
|
||||
v{{ app_version }}{% if app_env != 'production' %} {{ app_env|capitalize }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% macro badge(text, tone='default', class_name='') -%}
|
||||
<span class="badge badge--{{ tone }} {{ class_name }}">{{ text }}</span>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% macro button(label, type='button', variant='primary', extra_class='', icon=None) -%}
|
||||
<button type="{{ type }}" class="btn btn--{{ variant }} {{ extra_class }}">
|
||||
{% if icon %}<span class="btn__icon" aria-hidden="true">{{ icon }}</span>{% endif %}
|
||||
<span>{{ label }}</span>
|
||||
</button>
|
||||
{%- endmacro %}
|
||||
{% macro link_button(label, href, variant='primary', extra_class='') -%}
|
||||
<a href="{{ href }}" class="btn btn--{{ variant }} {{ extra_class }}">{{ label }}</a>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% macro card(class_name='') -%}
|
||||
<section class="card {{ class_name }}">
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% macro chip(text, kind='default', extra_class='') -%}
|
||||
<span class="ui-chip ui-chip--{{ kind }} {{ extra_class }}">{{ text }}</span>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% macro collapsible_section(title, section_id, classes='', sync_group='') -%}
|
||||
<details id="{{ section_id }}"
|
||||
class="settings-section settings-section--collapsible form-card full-width {{ classes }}"
|
||||
data-component="settings-section"
|
||||
{% if sync_group %}data-sync-group="{{ sync_group }}"{% endif %}>
|
||||
<summary class="settings-section__summary">
|
||||
<span class="settings-section__heading">{{ title }}</span>
|
||||
<span class="settings-section__chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="settings-section__content">{{ caller() }}</div>
|
||||
</details>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,127 @@
|
||||
{% from "ui/chip.html" import chip %}
|
||||
|
||||
{% macro status_badge_form(csrf_token, day, return_to, kind) -%}
|
||||
{% if kind == 'vacation' %}
|
||||
<form method="post"
|
||||
action="/vacation/day/toggle"
|
||||
class="inline-form"
|
||||
data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ day.date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit" class="day-status-badge is-vacation" title="Urlaub entfernen" aria-label="Urlaub entfernen">U</button>
|
||||
</form>
|
||||
{% elif kind == 'holiday' %}
|
||||
<form method="post"
|
||||
action="/special-day/toggle"
|
||||
class="inline-form"
|
||||
data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ day.date.isoformat() }}" />
|
||||
<input type="hidden" name="status" value="holiday" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit" class="day-status-badge is-holiday" title="Feiertag entfernen" aria-label="Feiertag entfernen">F</button>
|
||||
</form>
|
||||
{% elif kind == 'sick' %}
|
||||
<form method="post"
|
||||
action="/special-day/toggle"
|
||||
class="inline-form"
|
||||
data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ day.date.isoformat() }}" />
|
||||
<input type="hidden" name="status" value="sick" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit" class="day-status-badge is-sick" title="Krankheitstag entfernen" aria-label="Krankheitstag entfernen">K</button>
|
||||
</form>
|
||||
{% elif kind == 'overtime' %}
|
||||
<form method="post" action="/overtime-adjustment/clear" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ day.date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit" class="day-status-badge is-overtime" title="Stundenausgleich entfernen" aria-label="Stundenausgleich entfernen">S</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro add_menu_status_form(action_url, csrf_token, day, return_to, label, status='') -%}
|
||||
<form method="post"
|
||||
action="{{ action_url }}"
|
||||
class="inline-form"
|
||||
data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="date" value="{{ day.date.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
{% if status %}<input type="hidden" name="status" value="{{ status }}" />{% endif %}
|
||||
<button type="submit" class="day-row__add-menu-item">{{ label }}</button>
|
||||
</form>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro day_row(day, csrf_token, date_label, return_to='/dashboard', mode='week') -%}
|
||||
{% set is_vacation = day.is_vacation if day.is_vacation is defined else false %}
|
||||
{% set special_status = day.special_status if day.special_status is defined else None %}
|
||||
{% set overtime_adjustment_minutes = day.overtime_adjustment_minutes if day.overtime_adjustment_minutes is defined else 0 %}
|
||||
{% set has_status = is_vacation or special_status in ['holiday', 'sick'] or overtime_adjustment_minutes %}
|
||||
{% set is_weekend = day.is_weekend if day.is_weekend is defined else day.date.weekday() >= 5 %}
|
||||
{% set is_today = today_date is defined and day.date == today_date %}
|
||||
{% set status_edit_url = None %}
|
||||
{% if overtime_adjustment_minutes %}
|
||||
{% set status_edit_url = '/overtime-adjustment/edit?date=' ~ day.date.isoformat() %}
|
||||
{% elif is_vacation %}
|
||||
{% set status_edit_url = '/day-status/edit?date=' ~ day.date.isoformat() ~ '&status=vacation' %}
|
||||
{% elif special_status == 'holiday' %}
|
||||
{% set status_edit_url = '/day-status/edit?date=' ~ day.date.isoformat() ~ '&status=holiday' %}
|
||||
{% elif special_status == 'sick' %}
|
||||
{% set status_edit_url = '/day-status/edit?date=' ~ day.date.isoformat() ~ '&status=sick' %}
|
||||
{% endif %}
|
||||
|
||||
<article class="day-row day-row--{{ mode }} {% if is_weekend %}day-row--weekend{% endif %} {% if is_today %}day-row--today{% endif %} {% if has_status %}day-row--has-status{% endif %}">
|
||||
<div class="day-row__label" title="{{ date_label }}">{{ date_label }}</div>
|
||||
|
||||
<div class="day-row__cells {% if not day.entry %}day-row__cells--empty{% endif %}">
|
||||
{% if day.entry %}
|
||||
{{ chip(minutes_to_hhmm(day.entry.start_minutes) ~ ' → ' ~ minutes_to_hhmm(day.entry.end_minutes), 'time') }}
|
||||
{{ chip('Pause: ' ~ day.entry.break_minutes ~ ' min', 'break') }}
|
||||
{{ chip('Netto: ' ~ minutes_to_hhmm(day.net_minutes), 'net') }}
|
||||
{% else %}
|
||||
{{ chip('Keinen Eintrag', 'empty') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="day-row__actions">
|
||||
{% if is_vacation %}{{ status_badge_form(csrf_token, day, return_to, 'vacation') }}{% endif %}
|
||||
{% if special_status == 'holiday' %}{{ status_badge_form(csrf_token, day, return_to, 'holiday') }}{% endif %}
|
||||
{% if special_status == 'sick' %}{{ status_badge_form(csrf_token, day, return_to, 'sick') }}{% endif %}
|
||||
{% if overtime_adjustment_minutes %}{{ status_badge_form(csrf_token, day, return_to, 'overtime') }}{% endif %}
|
||||
|
||||
{% if day.entry %}
|
||||
<a class="icon-button" href="/entry/{{ day.entry.id }}/edit" title="Eintrag bearbeiten" aria-label="Eintrag bearbeiten">
|
||||
<img class="dash-icon" src="/static/icons/edit.svg" alt="" />
|
||||
</a>
|
||||
<form method="post" action="/entry/{{ day.entry.id }}/delete" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<button type="submit" class="icon-button" title="Eintrag löschen" aria-label="Eintrag löschen">
|
||||
<img class="dash-icon" src="/static/icons/delete.svg" alt="" />
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% if has_status and status_edit_url %}
|
||||
<a class="icon-button" href="{{ status_edit_url }}" title="Tag bearbeiten" aria-label="Tag bearbeiten">
|
||||
<img class="dash-icon" src="/static/icons/edit.svg" alt="" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<details class="day-row__add-menu">
|
||||
<summary class="icon-button" title="Optionen hinzufügen" aria-label="Optionen hinzufügen">
|
||||
<img class="dash-icon" src="/static/icons/add.svg" alt="" />
|
||||
</summary>
|
||||
<div class="day-row__add-menu-panel">
|
||||
<a class="day-row__add-menu-item" href="/entry/new?date={{ day.date.isoformat() }}">Zeit</a>
|
||||
{{ add_menu_status_form('/vacation/day/toggle', csrf_token, day, return_to, 'Urlaub (U)') }}
|
||||
{{ add_menu_status_form('/special-day/toggle', csrf_token, day, return_to, 'Feiertag (F)', 'holiday') }}
|
||||
{{ add_menu_status_form('/special-day/toggle', csrf_token, day, return_to, 'Krankheit (K)', 'sick') }}
|
||||
<a class="day-row__add-menu-item" href="/overtime-adjustment/edit?date={{ day.date.isoformat() }}">Stundenausgleich (S)</a>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% macro empty_state(title, text='') -%}
|
||||
<section class="empty-state">
|
||||
<h2 class="empty-state__title">{{ title }}</h2>
|
||||
{% if text %}<p class="empty-state__text muted">{{ text }}</p>{% endif %}
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% macro alert(message, level='info') -%}
|
||||
<div class="alert alert--{{ level }}"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-component="flash">
|
||||
<span class="alert__message">{{ message }}</span>
|
||||
<button class="alert__close"
|
||||
type="button"
|
||||
aria-label="Hinweis schließen"
|
||||
data-action="flash-close">×</button>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% macro input_field(label, name, type='text', value='', required=false, placeholder='', autocomplete='', extra_class='', attrs='') -%}
|
||||
<label class="form-field {{ extra_class }}">
|
||||
<span class="form-field__label">{{ label }}</span>
|
||||
<input class="input"
|
||||
type="{{ type }}"
|
||||
name="{{ name }}"
|
||||
value="{{ value }}"
|
||||
{% if required %}required{% endif %}
|
||||
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %}
|
||||
{% if attrs %}{{ attrs|safe }}{% endif %} />
|
||||
</label>
|
||||
{%- endmacro %}
|
||||
{% macro textarea_field(label, name, value='', rows=3, placeholder='', extra_class='', attrs='') -%}
|
||||
<label class="form-field {{ extra_class }}">
|
||||
<span class="form-field__label">{{ label }}</span>
|
||||
<textarea class="input"
|
||||
name="{{ name }}"
|
||||
rows="{{ rows }}"
|
||||
{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if attrs %} {{ attrs|safe }}{% endif %}>{{ value }}</textarea>
|
||||
</label>
|
||||
{%- endmacro %}
|
||||
{% macro select_field(label, name, options, selected='', required=false, extra_class='', attrs='') -%}
|
||||
<label class="form-field {{ extra_class }}">
|
||||
<span class="form-field__label">{{ label }}</span>
|
||||
<select class="input"
|
||||
name="{{ name }}"
|
||||
{% if required %}required{% endif %}
|
||||
{% if attrs %}{{ attrs|safe }}{% endif %}>
|
||||
{% for option in options %}
|
||||
<option value="{{ option.value }}"
|
||||
{% if option.value == selected %}selected{% endif %}>{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% macro help_section(title, subtitle='') -%}
|
||||
<article class="help-section">
|
||||
<header class="help-section__header">
|
||||
<h2>{{ title }}</h2>
|
||||
{% if subtitle %}<p class="muted">{{ subtitle }}</p>{% endif %}
|
||||
</header>
|
||||
<div class="help-section__body">{{ caller() }}</div>
|
||||
</article>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% macro icon_link(href, icon, label, title='', extra_class='') -%}
|
||||
<a class="icon-button {{ extra_class }}"
|
||||
href="{{ href }}"
|
||||
title="{{ title if title else label }}"
|
||||
aria-label="{{ label }}">
|
||||
<img class="dash-icon" src="{{ icon }}" alt="" />
|
||||
</a>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro icon_submit(icon, label, title='', extra_class='') -%}
|
||||
<button type="submit"
|
||||
class="icon-button {{ extra_class }}"
|
||||
title="{{ title if title else label }}"
|
||||
aria-label="{{ label }}">
|
||||
<img class="dash-icon" src="{{ icon }}" alt="" />
|
||||
</button>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% macro kpi_bar(items, extra_class='') -%}
|
||||
<section class="kpi-bar {{ extra_class }}">
|
||||
{% for item in items %}
|
||||
<p class="kpi-bar__item {% if item.get('show_edit') %}kpi-bar__item--editable{% endif %}">
|
||||
<span class="kpi-bar__label">{{ item.label }}:</span>
|
||||
<strong class="kpi-bar__value {% if item.get('value_class') %}{{ item.get('value_class') }}{% endif %}">{{ item.value }}</strong>
|
||||
{% if item.get('show_edit') %}
|
||||
<button type="button"
|
||||
class="kpi-bar__edit js-toggle-weekly-target-editor"
|
||||
aria-label="Wochen-Soll bearbeiten"
|
||||
title="Wochen-Soll bearbeiten">
|
||||
<img class="dash-icon" src="/static/icons/edit.svg" alt="" />
|
||||
</button>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% macro modal(id, title, close_label='Schließen') -%}
|
||||
<div class="modal" id="{{ id }}" data-component="modal" hidden>
|
||||
<div class="modal__backdrop" data-action="modal-close"></div>
|
||||
<section class="modal__dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="{{ id }}-title">
|
||||
<header class="modal__header">
|
||||
<h2 id="{{ id }}-title">{{ title }}</h2>
|
||||
<button class="modal__close"
|
||||
type="button"
|
||||
data-action="modal-close"
|
||||
aria-label="{{ close_label }}">×</button>
|
||||
</header>
|
||||
<div class="modal__body">{{ caller() }}</div>
|
||||
</section>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% from "ui/week_group_header.html" import week_group_header %}
|
||||
|
||||
{% macro month_week_divider(week, csrf_token, return_to) -%}
|
||||
{{ week_group_header(week, csrf_token, return_to) }}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% macro month_header_bar(prev_url, next_url, title) -%}
|
||||
<section class="period-header"
|
||||
data-component="swipe-nav"
|
||||
data-prev-url="{{ prev_url }}"
|
||||
data-next-url="{{ next_url }}">
|
||||
<div class="period-header__nav" data-component="period-nav">
|
||||
<a class="icon-button"
|
||||
href="{{ prev_url }}"
|
||||
title="Vorheriger Monat"
|
||||
aria-label="Vorheriger Monat">
|
||||
<img class="dash-icon dash-icon-rotate-180" src="/static/icons/arrow.svg" alt="" />
|
||||
</a>
|
||||
<p class="period-header__title">{{ title }}</p>
|
||||
<a class="icon-button"
|
||||
href="{{ next_url }}"
|
||||
title="Nächster Monat"
|
||||
aria-label="Nächster Monat">
|
||||
<img class="dash-icon" src="/static/icons/arrow.svg" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="period-header__actions">{{ caller() }}</div>
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% macro page_header(title, subtitle='') -%}
|
||||
<header class="page-header">
|
||||
<h1 class="page-header__title">{{ title }}</h1>
|
||||
{% if subtitle %}<p class="page-header__subtitle muted">{{ subtitle }}</p>{% endif %}
|
||||
</header>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% macro pagination(prev_href='', next_href='', class_name='') -%}
|
||||
<nav class="pagination {{ class_name }}" aria-label="Seitennavigation">
|
||||
{% if prev_href %}<a class="btn btn--ghost" href="{{ prev_href }}">Zurück</a>{% endif %}
|
||||
{% if next_href %}<a class="btn btn--ghost" href="{{ next_href }}">Weiter</a>{% endif %}
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% macro segmented_toggle(items, aria_label='Umschalter', extra_class='') -%}
|
||||
<nav class="segmented-toggle {{ extra_class }}" aria-label="{{ aria_label }}">
|
||||
{% for item in items %}
|
||||
<a href="{{ item.href }}"
|
||||
class="segmented-toggle__item {% if item.active %}is-active{% endif %}">{{ item.label }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% from "ui/kpi_bar.html" import kpi_bar %}
|
||||
|
||||
{% macro status_strip(items, columns='4', extra_class='') -%}
|
||||
{{ kpi_bar(items, extra_class) }}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% macro table(headers, class_name='') -%}
|
||||
<div class="table-wrap {{ class_name }}">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for header in headers %}<th>{{ header }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ caller() }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% from "ui/day_row.html" import day_row %}
|
||||
|
||||
{% macro time_day_row(day, csrf_token, date_label, return_to='/dashboard') -%}
|
||||
{{ day_row(day, csrf_token, date_label, return_to, 'week') }}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% macro workhours_target_warning_banner(warning) -%}
|
||||
{% if warning and warning.at_risk %}
|
||||
<section class="workhours-warning-banner"
|
||||
data-component="workhours-warning"
|
||||
data-workhours-warning="{{ warning.start_date.isoformat() }}-{{ warning.end_date.isoformat() }}-{{ warning.target_minutes }}">
|
||||
<p class="workhours-warning-text">Achtung: Arbeitsstundenziel wird ggf. nicht erreicht</p>
|
||||
<button type="button"
|
||||
class="workhours-warning-close"
|
||||
aria-label="Hinweis ausblenden"
|
||||
data-action="warning-close">×</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% macro week_group_card_mobile(week, csrf_token, return_to) -%}
|
||||
<section class="week-group-card-mobile {% if week.is_vacation_week %}is-vacation-week{% endif %}">
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% macro week_group_header(week, csrf_token, return_to) -%}
|
||||
<header class="week-group-header {% if week.is_vacation_week %}is-vacation-week{% endif %}">
|
||||
<div class="week-group-header__left">
|
||||
<strong>KW{{ week.iso_week }}</strong>
|
||||
<span class="week-group-header__arrow">→</span>
|
||||
<span>{{ week.week_start.strftime("%d.%m.") }} - {{ week.week_end.strftime("%d.%m.") }}</span>
|
||||
</div>
|
||||
<p class="week-group-header__meta">
|
||||
Ist {{ minutes_to_hhmm(week.weekly_ist) }} | Soll {{ minutes_to_hhmm(week.weekly_soll) }} | Delta
|
||||
<span class="{% if week.weekly_delta < 0 %}negative{% else %}positive{% endif %}">{{ minutes_to_hhmm(week.weekly_delta) }}</span>
|
||||
| Urlaubstage {{ week.vacation_days }}
|
||||
</p>
|
||||
<form method="post"
|
||||
action="/vacation/week/toggle"
|
||||
class="inline-form week-group-header__action"
|
||||
data-async-refresh="view">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="week_start" value="{{ week.week_start.isoformat() }}" />
|
||||
<input type="hidden" name="week_end" value="{{ week.week_end.isoformat() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ return_to }}" />
|
||||
<button type="submit"
|
||||
class="week-vacation-button {% if week.is_vacation_week %}is-active{% endif %}"
|
||||
title="Urlaub für ganze Woche umschalten"
|
||||
aria-label="Urlaub für ganze Woche umschalten">Urlaub</button>
|
||||
</form>
|
||||
</header>
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% macro week_header_bar(prev_url, next_url, title) -%}
|
||||
<section class="period-header"
|
||||
data-component="swipe-nav"
|
||||
data-prev-url="{{ prev_url }}"
|
||||
data-next-url="{{ next_url }}">
|
||||
<div class="period-header__nav" data-component="period-nav">
|
||||
<a class="icon-button"
|
||||
href="{{ prev_url }}"
|
||||
title="Vorherige Woche"
|
||||
aria-label="Vorherige Woche">
|
||||
<img class="dash-icon dash-icon-rotate-180" src="/static/icons/arrow.svg" alt="" />
|
||||
</a>
|
||||
<p class="period-header__title">{{ title }}</p>
|
||||
<a class="icon-button"
|
||||
href="{{ next_url }}"
|
||||
title="Nächste Woche"
|
||||
aria-label="Nächste Woche">
|
||||
<img class="dash-icon" src="/static/icons/arrow.svg" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="period-header__actions">{{ caller() }}</div>
|
||||
</section>
|
||||
{%- endmacro %}
|
||||
Reference in New Issue
Block a user