20 Commits

Author SHA1 Message Date
9a45444db4 Version 1.3.10: Versionsanzeige im Footer hinzugefügt 2025-07-26 10:28:30 +02:00
386a3f688f Verbessere stats_dashboard.html: Schriftart angepasst und API-Counts entfernt 2025-07-26 10:25:11 +02:00
04dc301e5b CSS-Fixes: Scrollbars auf allen Geräten entfernt, responsive Box-Sizing und Breitenanpassung verbessert 2025-07-26 08:41:06 +02:00
aaf6dbdec0 API: Bundesland-Feiertage für REST API erweitert
- tage_werktage Endpunkt unterstützt jetzt bundesland Parameter
- Swagger-Dokumentation aktualisiert mit bundesland Parameter
- Alle 16 deutschen Bundesländer in der API-Dokumentation aufgelistet
2025-07-25 17:13:39 +02:00
e2a5c1a3fa Feat: Bundesland-Feiertage für Werktagsberechnung hinzugefügt
- Neue Funktion zur Abfrage bundeslandspezifischer Feiertage über feiertage-api.de
- Werktagsberechnung berücksichtigt jetzt optional Feiertage des gewählten Bundeslandes
- Frontend: Dropdown für Bundesland-Auswahl (nur aktiv wenn Werktage-Checkbox aktiviert)
- Anzeige der Anzahl Wochenendtage und Feiertage im Ergebnis
- REST API erweitert um bundesland-Parameter
- README.md aktualisiert mit Dokumentation der neuen Funktion
2025-07-25 17:10:57 +02:00
5009ec1085 Add lighthouse-score.pdf 2025-07-25 15:03:51 +02:00
feb179f7bc Update README 2025-07-25 14:51:15 +02:00
55a05ef2af Hilfe-Button mit Overlay hinzugefügt - Barrierefreier Tooltip und Modal-Dialog implementiert 2025-07-25 14:49:43 +02:00
97aa208bf2 Performance-Optimierungen: CSS inline eingebettet, Touch-Targets vergrößert, Layout-Shifts minimiert 2025-07-25 14:02:36 +02:00
ffa1af560c Code Statistik aktualisiert 2025-07-25 13:42:46 +02:00
5867e3eeb7 Fix 404 errors: Move favicon files to static directory and update all references 2025-07-25 13:19:12 +02:00
8f8c5f42ca Fix apple-mobile-web-app-status-bar-style 2025-07-25 13:13:59 +02:00
fec84005f4 UI: Submit-Buttons vergrößert und einheitlichen vertikalen Abstand hinzugefügt 2025-07-25 13:08:57 +02:00
f2391de8b4 UI: Checkbox-Alignment im Plus/Minus-Bereich verbessert 2025-07-25 12:54:28 +02:00
9361d11dff remove ) 2025-07-25 12:32:16 +02:00
c67df924c6 Update TOC 2025-07-25 12:31:48 +02:00
59145f9ea1 Add batch file 2025-07-25 12:30:05 +02:00
8c56cdeb55 Add cloc code stats 2025-07-25 12:25:06 +02:00
d24f93d039 Swagger/OpenAPI-Doku unter /api-docs, Link im Footer, keine Überschneidung mit API-Endpunkten 2025-07-25 12:14:22 +02:00
77a6b5c2c2 Change Codeberg link to README.md 2025-07-25 11:59:57 +02:00
16 changed files with 1027 additions and 258 deletions

View File

@@ -29,6 +29,7 @@ Diese moderne Python-Webanwendung (Flask) ermöglicht verschiedene Datumsberechn
- [Vibe Coding](#vibe-coding) - [Vibe Coding](#vibe-coding)
- [Statistik-Erfassung, Logging](#statistik-erfassung-logging) - [Statistik-Erfassung, Logging](#statistik-erfassung-logging)
- [Barrierefreiheit (Accessibility)](#barrierefreiheit-accessibility) - [Barrierefreiheit (Accessibility)](#barrierefreiheit-accessibility)
- [Code Statistik](#code-statistik)
- [Lizenz](#lizenz) - [Lizenz](#lizenz)
## Demo ## Demo
@@ -40,7 +41,7 @@ Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
## Funktionen ## Funktionen
- Anzahl der Tage zwischen zwei Daten - Anzahl der Tage zwischen zwei Daten
- Anzahl der Werktage zwischen zwei Daten - Anzahl der Werktage zwischen zwei Daten (mit optionaler Berücksichtigung bundeslandspezifischer Feiertage)
- Anzeige des Wochentags eines Datums - Anzeige des Wochentags eines Datums
- Datum plus/minus X Tage - Datum plus/minus X Tage
- Datum plus/minus X Werktage - Datum plus/minus X Werktage
@@ -49,6 +50,30 @@ Datumsrechner Live: [https://date.elpatron.me](https://date.elpatron.me)
- Start-/Enddatum einer Kalenderwoche eines Jahres - Start-/Enddatum einer Kalenderwoche eines Jahres
- Statistik-Dashboard mit Passwortschutz unter `/stats` - Statistik-Dashboard mit Passwortschutz unter `/stats`
## Bundesland-Feiertage
Die Werktagsberechnung kann optional bundeslandspezifische Feiertage berücksichtigen. Dazu wird die kostenlose API von [feiertage-api.de](https://feiertage-api.de) verwendet.
**Verfügbare Bundesländer:**
- Baden-Württemberg (BW)
- Bayern (BY)
- Berlin (BE)
- Brandenburg (BB)
- Bremen (HB)
- Hamburg (HH)
- Hessen (HE)
- Mecklenburg-Vorpommern (MV)
- Niedersachsen (NI)
- Nordrhein-Westfalen (NW)
- Rheinland-Pfalz (RP)
- Saarland (SL)
- Sachsen (SN)
- Sachsen-Anhalt (ST)
- Schleswig-Holstein (SH)
- Thüringen (TH)
Die Feiertage werden automatisch für den gewählten Zeitraum abgerufen und bei der Werktagsberechnung als arbeitsfreie Tage behandelt. Im Ergebnis werden zusätzlich die Anzahl der Wochenendtage und Feiertage angezeigt.
## Installation (lokal) ## Installation (lokal)
1. Python 3.8+ installieren 1. Python 3.8+ installieren
@@ -147,7 +172,8 @@ Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptier
{ {
"start": "2024-06-01", "start": "2024-06-01",
"end": "2024-06-10", "end": "2024-06-10",
"werktage": true "werktage": true,
"bundesland": "BY"
} }
``` ```
@@ -155,7 +181,7 @@ Alle Datumsfunktionen stehen auch als REST-API zur Verfügung. Die API akzeptier
```bash ```bash
curl -X POST http://localhost:5000/api/tage_werktage \ curl -X POST http://localhost:5000/api/tage_werktage \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"start": "2024-06-01", "end": "2024-06-10", "werktage": true}' -d '{"start": "2024-06-01", "end": "2024-06-10", "werktage": true, "bundesland": "BY"}'
``` ```
**Antwort:** **Antwort:**
@@ -163,6 +189,8 @@ curl -X POST http://localhost:5000/api/tage_werktage \
{ "result": 7 } { "result": 7 }
``` ```
**Hinweis:** Der Parameter `bundesland` ist optional und wird nur bei `"werktage": true` berücksichtigt. Verfügbare Bundesland-Kürzel siehe oben.
#### 2. Wochentag zu einem Datum #### 2. Wochentag zu einem Datum
**POST** `/api/wochentag` **POST** `/api/wochentag`
@@ -385,6 +413,24 @@ Es werden keine IP-Adressen oder sonstigen persönlichen Daten gespeichert, ledi
Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Sehbehinderung, motorische Einschränkungen) gut nutzbar und entspricht modernen Webstandards. Damit ist die App für Menschen mit unterschiedlichen Einschränkungen (z.B. Sehbehinderung, motorische Einschränkungen) gut nutzbar und entspricht modernen Webstandards.
### Code Statistik
cloc|github.com/AlDanial/cloc v 2.06 T=0.06 s (263.5 files/s, 39193.4 lines/s)
--- | ---
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|4|21|6|927
Python|2|34|26|482
Markdown|2|116|0|328
JSON|2|0|0|237
JavaScript|1|0|0|20
SVG|2|0|0|14
Dockerfile|1|5|6|8
DOS Batch|1|0|0|1
--------|--------|--------|--------|--------
SUM:|15|176|38|2017
## Lizenz ## Lizenz
Dieses Projekt steht unter der [MIT-Lizenz](LICENSE). Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).

54
app.py
View File

@@ -4,16 +4,32 @@ import numpy as np
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
import os import os
import time import time
import requests
app_start_time = time.time() app_start_time = time.time()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev-key') app.secret_key = os.environ.get('SECRET_KEY', 'dev-key')
# Version der App
APP_VERSION = "1.3.10"
# HTML-Template wird jetzt aus templates/index.html geladen # HTML-Template wird jetzt aus templates/index.html geladen
WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] WOCHENTAGE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
def get_feiertage(year, bundesland):
"""Holt die Feiertage für ein Jahr und Bundesland von feiertage-api.de."""
url = f"https://feiertage-api.de/api/?jahr={year}&nur_land={bundesland}"
try:
resp = requests.get(url, timeout=5)
data = resp.json()
# Die API gibt ein Dict mit Feiertagsnamen als Key, jeweils mit 'datum' als Wert
return [v['datum'] for v in data.values() if 'datum' in v]
except Exception as e:
print(f"Fehler beim Abrufen der Feiertage: {e}")
return []
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def index(): def index():
# Rudimentäres Logging für Page Impressions # Rudimentäres Logging für Page Impressions
@@ -40,6 +56,7 @@ def index():
from datetime import datetime as dt from datetime import datetime as dt
f.write(f"{dt.now().isoformat()} PAGEVIEW\n") f.write(f"{dt.now().isoformat()} PAGEVIEW\n")
tage = werktage = wochentag = datumsrechnung = werktagsrechnung = kw_berechnen = kw_datum = wochen_monate = None tage = werktage = wochentag = datumsrechnung = werktagsrechnung = kw_berechnen = kw_datum = wochen_monate = None
feiertage_anzahl = wochenendtage_anzahl = None
active_idx = 0 active_idx = 0
plusminus_result = None plusminus_result = None
if request.method == 'POST': if request.method == 'POST':
@@ -53,13 +70,27 @@ def index():
start = request.form.get('start1') start = request.form.get('start1')
end = request.form.get('end1') end = request.form.get('end1')
is_werktage = request.form.get('werktage') in ('on', 'true', '1', True) is_werktage = request.form.get('werktage') in ('on', 'true', '1', True)
bundesland = request.form.get('bundesland')
try: try:
d1 = datetime.strptime(start, '%Y-%m-%d') d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d') d2 = datetime.strptime(end, '%Y-%m-%d')
if d1 > d2:
d1, d2 = d2, d1
# Feiertage bestimmen
holidays = []
if bundesland:
years = set([d1.year, d2.year])
for y in years:
holidays.extend(get_feiertage(y, bundesland))
# Alle Tage im Bereich
all_days = [(d1 + timedelta(days=i)).date() for i in range((d2 - d1).days + 1)]
# Wochenendtage zählen
wochenendtage_anzahl = sum(1 for d in all_days if d.weekday() >= 5)
# Feiertage zählen (nur die, die im Bereich liegen und nicht auf Wochenende fallen)
feiertage_im_zeitraum = [f for f in holidays if d1.date() <= datetime.strptime(f, '%Y-%m-%d').date() <= d2.date()]
feiertage_anzahl = sum(1 for f in feiertage_im_zeitraum if datetime.strptime(f, '%Y-%m-%d').date().weekday() < 5)
if is_werktage: if is_werktage:
if d1 > d2: tage = np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays)
d1, d2 = d2, d1
tage = np.busday_count(d1.date(), (d2 + timedelta(days=1)).date())
else: else:
tage = abs((d2 - d1).days) tage = abs((d2 - d1).days)
except Exception: except Exception:
@@ -129,7 +160,9 @@ def index():
plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}" plusminus_result = f"Datum {d.strftime('%d.%m.%Y')} {'plus' if anzahl_int>=0 else 'minus'} {abs(anzahl_int)} Monate: {result.strftime('%d.%m.%Y')}"
except Exception: except Exception:
plusminus_result = 'Ungültige Eingabe' plusminus_result = 'Ungültige Eingabe'
return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx) return render_template('index.html', tage=tage, werktage=werktage, wochentag=wochentag, plusminus_result=plusminus_result, kw_berechnen=kw_berechnen, kw_datum=kw_datum, active_idx=active_idx
, feiertage_anzahl=feiertage_anzahl, wochenendtage_anzahl=wochenendtage_anzahl, app_version=APP_VERSION
)
def parse_log_stats(log_path): def parse_log_stats(log_path):
@@ -187,13 +220,20 @@ def api_tage_werktage():
start = data.get('start') start = data.get('start')
end = data.get('end') end = data.get('end')
is_werktage = data.get('werktage', False) is_werktage = data.get('werktage', False)
bundesland = data.get('bundesland')
try: try:
d1 = datetime.strptime(start, '%Y-%m-%d') d1 = datetime.strptime(start, '%Y-%m-%d')
d2 = datetime.strptime(end, '%Y-%m-%d') d2 = datetime.strptime(end, '%Y-%m-%d')
if is_werktage: if is_werktage:
if d1 > d2: if d1 > d2:
d1, d2 = d2, d1 d1, d2 = d2, d1
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date())) holidays = []
if bundesland:
# Feiertage für alle Jahre im Bereich holen
years = set([d1.year, d2.year])
for y in years:
holidays.extend(get_feiertage(y, bundesland))
tage = int(np.busday_count(d1.date(), (d2 + timedelta(days=1)).date(), holidays=holidays))
else: else:
tage = abs((d2 - d1).days) tage = abs((d2 - d1).days)
return jsonify({'result': tage}) return jsonify({'result': tage})
@@ -302,6 +342,10 @@ def api_monitor():
"pageviews_last_7_days": pageviews "pageviews_last_7_days": pageviews
}) })
@app.route('/api-docs')
def api_docs():
return render_template('swagger.html')
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0") app.run(debug=True, host="0.0.0.0")

1
code-stats.cmd Normal file
View File

@@ -0,0 +1 @@
@cloc . .\templates\ .\static\ --exclude-dir=.venv,.git,log,__pycache__,.pytest_cache --exclude-ext=txt,bak --md

31
info.md Normal file
View File

@@ -0,0 +1,31 @@
# Was ist Elpatrons Datumsrechner?
Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:
- Anzahl der Tage zwischen zwei Daten
- Anzahl der Werktage zwischen zwei Daten
- Anzeige des Wochentags eines Datums
- Datum plus/minus X Tage
- Datum plus/minus X Werktage
- Datum plus/minus X Wochen/Monate
- Kalenderwoche zu Datum
- Start-/Enddatum einer Kalenderwoche eines Jahres
## Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?
Aus zwei Gründen:
- Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!
- Das hat mich so geärgert, dass ich meinen eigenen programmiert habe.
- Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt ([Vibe Coding}(https://www.wikiwand.com/en/articles/Vibe_coding).)
## Was du noch wissen solltest
- Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.
- Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.
- Den Quellcode dieser App habe ich auf [Codeberg](https://codeberg.org/elpatron/datecalc) veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.
- Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.
- Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.
- Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)
Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche

BIN
lighthouse-score.pdf Normal file

Binary file not shown.

View File

@@ -2,3 +2,4 @@ Flask==3.0.3
numpy==1.26.4 numpy==1.26.4
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytest==8.2.2 pytest==8.2.2
requests

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -7,8 +7,8 @@
"theme_color": "#2563eb", "theme_color": "#2563eb",
"description": "Open Source Web-App für Kalender- und Datumsberechnungen.", "description": "Open Source Web-App für Kalender- und Datumsberechnungen.",
"icons": [ "icons": [
{ "src": "/favicon.png", "sizes": "32x32", "type": "image/png" }, { "src": "/static/favicon.png", "sizes": "32x32", "type": "image/png" },
{ "src": "/favicon.ico", "sizes": "48x48 64x64 128x128 256x256", "type": "image/x-icon" }, { "src": "/static/favicon.ico", "sizes": "48x48 64x64 128x128 256x256", "type": "image/x-icon" },
{ "src": "/static/logo.svg", "sizes": "any", "type": "image/svg+xml" } { "src": "/static/logo.svg", "sizes": "any", "type": "image/svg+xml" }
] ]
} }

View File

@@ -2,9 +2,9 @@ const CACHE_NAME = 'datumsrechner-cache-v1';
const urlsToCache = [ const urlsToCache = [
'/', '/',
'/static/style.css', '/static/style.css',
'/favicon.ico', '/static/favicon.ico',
'/favicon.png', '/static/favicon.png',
'/logo.svg', '/static/logo.svg',
]; ];
self.addEventListener('install', event => { self.addEventListener('install', event => {
event.waitUntil( event.waitUntil(

View File

@@ -1,229 +0,0 @@
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--background: #f8fafc;
--surface: #fff;
--border: #e5e7eb;
--text: #1e293b;
--shadow: 0 2px 8px rgba(30,41,59,0.07);
}
body {
background: var(--background);
color: var(--text);
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 480px;
margin: 3em auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 2.5em 2em 2em 2em;
border: 1px solid var(--border);
}
h1 {
text-align: center;
margin-bottom: 2em;
font-size: 2.1em;
letter-spacing: 0.01em;
}
form {
margin-bottom: 2.2em;
padding-bottom: 1.2em;
border-bottom: 1px solid var(--border);
}
form:last-of-type {
border-bottom: none;
margin-bottom: 0;
}
h2 {
font-size: 1.15em;
margin-bottom: 0.7em;
color: var(--primary-dark);
}
label {
display: block;
margin-bottom: 0.7em;
font-weight: 500;
}
.date-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
.date-calc-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
input[type="date"] {
padding: 0.45em 0.7em;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1em;
background: #f1f5f9;
color: var(--text);
}
.today-btn {
padding: 0.35em 0.9em;
background: var(--primary-dark);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.today-btn:hover {
background: var(--primary);
}
button, .accordion-header {
background: var(--primary);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
transition: background 0.2s;
}
button:hover, .accordion-header:hover {
background: var(--primary-dark);
}
button:focus, .accordion-header:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
z-index: 2;
}
.result {
margin-top: 1em;
font-weight: bold;
background: #e0e7ff;
color: #1e293b;
border-radius: 6px;
padding: 0.7em 1em;
box-shadow: 0 1px 2px rgba(30,41,59,0.04);
border: 2px solid #2563eb;
}
.accordion {
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--surface);
margin-bottom: 2em;
}
.accordion-item + .accordion-item {
border-top: 1px solid var(--border);
}
.accordion-header {
background: var(--primary-dark);
color: #fff;
cursor: pointer;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: 600;
border: none;
outline: none;
width: 100%;
text-align: left;
transition: background 0.2s;
}
.accordion-header.active, .accordion-header:hover {
background: var(--primary);
}
.accordion-content {
display: none;
padding: 1.2em 1.2em 1em 1.2em;
background: var(--surface);
}
.accordion-content.active {
display: block;
}
.header-tage {
background: #2563eb;
color: #fff;
}
.header-tage.active, .header-tage:hover {
background: #1e40af;
color: #fff;
}
.header-werktage {
background: #059669;
color: #fff;
}
.header-werktage.active, .header-werktage:hover {
background: #047857;
color: #fff;
}
.header-wochentag {
background: #f59e42;
color: #1e293b;
}
.header-wochentag.active, .header-wochentag:hover {
background: #d97706;
color: #fff;
}
.header-plusminus-tage {
background: #a21caf;
color: #fff;
}
.header-plusminus-tage.active, .header-plusminus-tage:hover {
background: #701a75;
color: #fff;
}
.header-plusminus-werktage {
background: #0ea5e9;
color: #fff;
}
.header-plusminus-werktage.active, .header-plusminus-werktage:hover {
background: #0369a1;
color: #fff;
}
.header-plusminus-wochenmonate {
background: #f43f5e;
color: #fff;
}
.header-plusminus-wochenmonate.active, .header-plusminus-wochenmonate:hover {
background: #be123c;
color: #fff;
}
.header-kw {
background: #a78bfa;
color: #1e293b;
}
.header-kw.active, .header-kw:hover {
background: #7c3aed;
color: #fff;
}
.header-kw-datum {
background: #facc15;
color: #1e293b;
}
.header-kw-datum.active, .header-kw-datum:hover {
background: #ca8a04;
color: #fff;
}
.header-plusminus {
background: #be123c;
color: #fff;
}
.header-plusminus.active, .header-plusminus:hover {
background: #7f1d1d;
color: #fff;
}
@media (max-width: 600px) {
.container {
margin: 1em;
padding: 1.2em 0.7em 1em 0.7em;
}
h1 {
font-size: 1.3em;
}
}

228
static/swagger.json Normal file
View File

@@ -0,0 +1,228 @@
{
"openapi": "3.0.3",
"info": {
"title": "Elpatrons Datumsrechner API",
"version": "1.0.0",
"description": "REST-API für Datumsberechnungen."
},
"servers": [
{ "url": "/api" }
],
"paths": {
"/tage_werktage": {
"post": {
"summary": "Berechnet die Anzahl der Tage oder Werktage zwischen zwei Daten.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" },
"werktage": { "type": "boolean", "default": false },
"bundesland": {
"type": "string",
"description": "Bundesland-Kürzel für Feiertagsberücksichtigung (nur bei werktage=true)",
"enum": ["BW", "BY", "BE", "BB", "HB", "HH", "HE", "MV", "NI", "NW", "RP", "SL", "SN", "ST", "SH", "TH"]
}
},
"required": ["start", "end"]
}
}
}
},
"responses": {
"200": {
"description": "Ergebnis der Berechnung.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "integer" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/wochentag": {
"post": {
"summary": "Gibt den Wochentag zu einem Datum zurück.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" }
},
"required": ["datum"]
}
}
}
},
"responses": {
"200": {
"description": "Wochentag als Text.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/kw_berechnen": {
"post": {
"summary": "Berechnet die Kalenderwoche zu einem Datum.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" }
},
"required": ["datum"]
}
}
}
},
"responses": {
"200": {
"description": "Kalenderwoche und Jahr.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"kw": { "type": "integer" },
"jahr": { "type": "integer" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/kw_datum": {
"post": {
"summary": "Berechnet Start- und Enddatum einer Kalenderwoche.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jahr": { "type": "integer" },
"kw": { "type": "integer" }
},
"required": ["jahr", "kw"]
}
}
}
},
"responses": {
"200": {
"description": "Start- und Enddatum der Kalenderwoche.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/plusminus": {
"post": {
"summary": "Berechnet ein Datum plus/minus X Tage, Wochen oder Monate.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"datum": { "type": "string", "format": "date" },
"anzahl": { "type": "integer" },
"einheit": { "type": "string", "enum": ["tage", "wochen", "monate"] },
"richtung": { "type": "string", "enum": ["add", "sub"], "default": "add" },
"werktage": { "type": "boolean", "default": false }
},
"required": ["datum", "anzahl", "einheit"]
}
}
}
},
"responses": {
"200": {
"description": "Berechnetes Datum.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": { "type": "string", "format": "date" }
}
}
}
}
},
"400": { "description": "Ungültige Eingabe" }
}
}
},
"/monitor": {
"get": {
"summary": "Status- und Monitoring-Informationen zur App.",
"responses": {
"200": {
"description": "Status-Objekt mit Uptime und Pageviews.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": { "type": "string" },
"message": { "type": "string" },
"time": { "type": "string", "format": "date-time" },
"uptime_seconds": { "type": "integer" },
"pageviews_last_7_days": { "type": "integer" }
}
}
}
}
}
}
}
}
}
}

View File

@@ -19,14 +19,478 @@
<meta property="og:image" content="/static/logo.svg"> <meta property="og:image" content="/static/logo.svg">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="HandheldFriendly" content="true"> <meta name="HandheldFriendly" content="true">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta http-equiv="x-ua-compatible" content="IE=edge"> <meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="application-name" content="Elpatrons Datumsrechner"> <meta name="application-name" content="Elpatrons Datumsrechner">
<meta name="msapplication-TileColor" content="#2563eb"> <meta name="msapplication-TileColor" content="#2563eb">
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/style.css"> <style>
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--background: #f8fafc;
--surface: #fff;
--border: #e5e7eb;
--text: #1e293b;
--shadow: 0 2px 8px rgba(30,41,59,0.07);
}
* {
box-sizing: border-box;
}
body {
background: var(--background);
color: var(--text);
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
box-sizing: border-box;
}
.container {
max-width: 480px;
width: 100%;
margin: 3em auto;
background: var(--surface);
border-radius: 16px;
box-shadow: var(--shadow);
padding: 2.5em 2em 2em 2em;
border: 1px solid var(--border);
position: relative;
box-sizing: border-box;
}
.help-button-container {
position: absolute;
top: 2.5em;
right: 2em;
z-index: 10;
}
.help-button {
width: 2.2em;
height: 2.2em;
border-radius: 50%;
background: rgba(37, 99, 235, 0.1);
color: var(--primary);
border: 1px solid var(--border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1em;
font-weight: 500;
transition: all 0.2s;
min-width: 44px;
min-height: 44px;
}
.help-button:hover {
background: rgba(37, 99, 235, 0.2);
border-color: var(--primary);
}
.help-button:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
}
.help-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: white;
padding: 0.5em 0.8em;
border-radius: 6px;
font-size: 0.85em;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
margin-top: 0.5em;
z-index: 20;
}
.help-tooltip::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-bottom-color: var(--text);
}
.help-button:hover + .help-tooltip,
.help-button:focus + .help-tooltip {
opacity: 1;
visibility: visible;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: var(--surface);
border-radius: 12px;
padding: 2em;
max-width: 90%;
width: 90%;
max-height: 90%;
overflow-y: auto;
position: relative;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
box-sizing: border-box;
}
.modal-close {
position: absolute;
top: 1em;
right: 1em;
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
color: var(--text);
padding: 0.5em;
border-radius: 50%;
transition: background 0.2s;
width: 2.5em;
height: 2.5em;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
}
.modal-close:hover {
background: var(--border);
}
.modal-close:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
}
.modal-content h1 {
margin-top: 0;
color: var(--primary-dark);
}
.modal-content h2 {
color: var(--primary-dark);
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.modal-content p {
line-height: 1.6;
margin-bottom: 1em;
}
.modal-content ul {
margin-bottom: 1em;
padding-left: 1.5em;
}
.modal-content li {
margin-bottom: 0.5em;
}
h1 {
text-align: center;
margin-bottom: 2em;
font-size: 2.1em;
letter-spacing: 0.01em;
}
form {
margin-bottom: 2.2em;
padding-bottom: 1.2em;
border-bottom: 1px solid var(--border);
}
form:last-of-type {
border-bottom: none;
margin-bottom: 0;
}
h2 {
font-size: 1.15em;
margin-bottom: 0.7em;
color: var(--primary-dark);
}
label {
display: block;
margin-bottom: 0.7em;
font-weight: 500;
}
.date-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
.date-calc-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.2em;
}
input[type="date"] {
padding: 0.45em 0.7em;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1em;
background: #f1f5f9;
color: var(--text);
}
.today-btn {
padding: 0.35em 0.9em;
background: var(--primary-dark);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.95em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.today-btn:hover {
background: var(--primary);
}
button, .accordion-header {
background: var(--primary);
color: #fff;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
box-shadow: 0 1px 3px rgba(30,41,59,0.05);
transition: background 0.2s;
}
/* Berechnungs-Buttons vergrößern */
button[type="submit"] {
font-size: 1.1em;
padding: 0.8em 1.5em;
min-width: 140px;
margin-top: 1.2em;
}
button:hover, .accordion-header:hover {
background: var(--primary-dark);
}
button:focus, .accordion-header:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
box-shadow: 0 0 0 4px #1e293b;
z-index: 2;
}
.result {
margin-top: 1em;
font-weight: bold;
background: #e0e7ff;
color: #1e293b;
border-radius: 6px;
padding: 0.7em 1em;
box-shadow: 0 1px 2px rgba(30,41,59,0.04);
border: 2px solid #2563eb;
}
.accordion {
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--surface);
margin-bottom: 2em;
/* Layout-Shift-Prävention */
min-height: 200px;
contain: layout style paint;
}
.accordion-item + .accordion-item {
border-top: 1px solid var(--border);
margin-top: 0.5em;
}
.accordion-header {
background: var(--primary-dark);
color: #fff;
cursor: pointer;
padding: 1em 1.2em;
font-size: 1.1em;
font-weight: 600;
border: none;
outline: none;
width: 100%;
text-align: left;
transition: background 0.2s;
}
.accordion-header.active, .accordion-header:hover {
background: var(--primary);
}
.accordion-content {
display: none;
padding: 1.2em 1.2em 1em 1.2em;
background: var(--surface);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out;
opacity: 0;
}
.accordion-content.active {
display: block;
max-height: 500px;
opacity: 1;
transition: max-height 0.3s ease-in, padding 0.3s ease-in, opacity 0.3s ease-in;
}
.header-tage {
background: #2563eb;
color: #fff;
}
.header-tage.active, .header-tage:hover {
background: #1e40af;
color: #fff;
}
.header-werktage {
background: #059669;
color: #fff;
}
.header-werktage.active, .header-werktage:hover {
background: #047857;
color: #fff;
}
.header-wochentag {
background: #f59e42;
color: #1e293b;
}
.header-wochentag.active, .header-wochentag:hover {
background: #d97706;
color: #fff;
}
.header-plusminus-tage {
background: #a21caf;
color: #fff;
}
.header-plusminus-tage.active, .header-plusminus-tage:hover {
background: #701a75;
color: #fff;
}
.header-plusminus-werktage {
background: #0ea5e9;
color: #fff;
}
.header-plusminus-werktage.active, .header-plusminus-werktage:hover {
background: #0369a1;
color: #fff;
}
.header-plusminus-wochenmonate {
background: #f43f5e;
color: #fff;
}
.header-plusminus-wochenmonate.active, .header-plusminus-wochenmonate:hover {
background: #be123c;
color: #fff;
}
.header-kw {
background: #a78bfa;
color: #1e293b;
}
.header-kw.active, .header-kw:hover {
background: #7c3aed;
color: #fff;
}
.header-kw-datum {
background: #facc15;
color: #1e293b;
}
.header-kw-datum.active, .header-kw-datum:hover {
background: #ca8a04;
color: #fff;
}
.header-plusminus {
background: #be123c;
color: #fff;
}
.header-plusminus.active, .header-plusminus:hover {
background: #7f1d1d;
color: #fff;
}
@media (max-width: 600px) {
.container {
margin: 1em;
padding: 1.2em 0.7em 1em 0.7em;
width: calc(100% - 2em);
max-width: none;
}
h1 {
font-size: 1.3em;
}
.help-button-container {
top: 1.5em;
right: 1.2em;
}
.help-button {
width: 2em;
height: 2em;
font-size: 0.9em;
min-width: 48px;
min-height: 48px;
}
.help-tooltip {
font-size: 0.8em;
padding: 0.4em 0.6em;
}
.modal-content {
padding: 1.5em;
margin: 1em;
width: calc(100% - 2em);
max-width: none;
left: 0;
transform: none;
}
}
/* Touch-Target Optimierungen für Footer-Links */
footer a {
display: inline-block;
padding: 0.5em 0.8em;
margin: 0.3em 0.2em;
min-height: 44px;
min-width: 44px;
line-height: 1.4;
text-decoration: none;
border-radius: 6px;
transition: background-color 0.2s, color 0.2s;
position: relative;
}
footer a:hover {
background-color: rgba(37, 99, 235, 0.1);
text-decoration: underline;
}
footer a:focus {
outline: 3px solid #facc15;
outline-offset: 2px;
background-color: rgba(37, 99, 235, 0.1);
}
/* Zusätzlicher Abstand zwischen Footer-Links */
footer br + a {
margin-top: 0.5em;
}
/* Responsive Anpassungen für Footer */
@media (max-width: 600px) {
footer a {
padding: 0.6em 1em;
margin: 0.4em 0.3em;
min-height: 48px;
min-width: 48px;
}
}
</style>
<link rel="manifest" href="/static/manifest.json"> <link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#2563eb"> <meta name="theme-color" content="#2563eb">
<script> <script>
@@ -45,10 +509,38 @@
el.classList.toggle('active', i === idx); el.classList.toggle('active', i === idx);
}); });
} }
function showHelp() {
const modal = document.getElementById('helpModal');
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Fokus auf den Schließen-Button setzen
setTimeout(() => {
document.querySelector('.modal-close').focus();
}, 100);
}
function hideHelp() {
const modal = document.getElementById('helpModal');
modal.classList.remove('active');
document.body.style.overflow = '';
// Fokus zurück auf den Hilfe-Button setzen
document.querySelector('.help-button').focus();
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
openAccordion(parseInt("{{ active_idx|default(0) }}")); // Sofortige Aktivierung der ersten Accordion-Sektion um Layout-Shifts zu vermeiden
// Tastatursteuerung für Accordion const activeIdx = parseInt("{{ active_idx|default(0) }}");
const headers = document.querySelectorAll('.accordion-header'); const headers = document.querySelectorAll('.accordion-header');
const panels = document.querySelectorAll('.accordion-content');
// Sofort den aktiven Zustand setzen
headers.forEach((btn, i) => {
btn.classList.toggle('active', i === activeIdx);
btn.setAttribute('aria-expanded', i === activeIdx ? 'true' : 'false');
});
panels.forEach((el, i) => {
el.classList.toggle('active', i === activeIdx);
});
// Tastatursteuerung für Accordion
headers.forEach((header, idx) => { headers.forEach((header, idx) => {
header.addEventListener('keydown', function(e) { header.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
@@ -66,6 +558,20 @@
} }
}); });
}); });
// ESC-Taste zum Schließen des Modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideHelp();
}
});
// Klick außerhalb des Modals zum Schließen
document.getElementById('helpModal').addEventListener('click', function(e) {
if (e.target === this) {
hideHelp();
}
});
}); });
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', function() { window.addEventListener('load', function() {
@@ -76,6 +582,10 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="help-button-container">
<button type="button" class="help-button" onclick="showHelp()" aria-label="Hilfe anzeigen" title="Hilfe anzeigen" aria-describedby="help-tooltip">?</button>
<div id="help-tooltip" class="help-tooltip" role="tooltip">Öffnet ein Hilfefenster mit Informationen über den Datumsrechner</div>
</div>
<div style="text-align:center; margin-bottom:1.2em;"> <div style="text-align:center; margin-bottom:1.2em;">
<div style="font-size:1.1em; font-style:italic; color:#64748b;">Elpatrons</div> <div style="font-size:1.1em; font-style:italic; color:#64748b;">Elpatrons</div>
<h1 style="margin:0;">Datumsrechner</h1> <h1 style="margin:0;">Datumsrechner</h1>
@@ -85,14 +595,14 @@
</div> </div>
<div class="accordion"> <div class="accordion">
<div class="accordion-item"> <div class="accordion-item">
<button type="button" class="accordion-header header-tage" id="accordion-header-0" aria-expanded="true" aria-controls="accordion-panel-0" role="button" tabindex="0" onclick="openAccordion(0)"> <button type="button" class="accordion-header header-tage active" id="accordion-header-0" aria-expanded="true" aria-controls="accordion-panel-0" role="button" tabindex="0" onclick="openAccordion(0)">
<span style="vertical-align:middle;display:inline-block;width:1.5em;"> <span style="vertical-align:middle;display:inline-block;width:1.5em;">
<!-- Kalender mit Doppelpfeil --> <!-- Kalender mit Doppelpfeil -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M8 15h8M8 15l2-2M8 15l2 2M16 15l-2-2M16 15l-2 2" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/></svg> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="5" width="18" height="16" rx="4" fill="#fff" stroke="#2563eb" stroke-width="2"/><rect x="3" y="5" width="18" height="4" rx="2" fill="#2563eb"/><rect x="6" y="2" width="2" height="4" rx="1" fill="#2563eb"/><rect x="16" y="2" width="2" height="4" rx="1" fill="#2563eb"/><path d="M8 15h8M8 15l2-2M8 15l2 2M16 15l-2-2M16 15l-2 2" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/></svg>
</span> </span>
Anzahl der Tage/Werktage zwischen zwei Daten Anzahl der Tage/Werktage zwischen zwei Daten
</button> </button>
<div class="accordion-content" id="accordion-panel-0" role="region" aria-labelledby="accordion-header-0"> <div class="accordion-content active" id="accordion-panel-0" role="region" aria-labelledby="accordion-header-0">
<form method="post"> <form method="post">
<label for="start1">Startdatum:<br> <label for="start1">Startdatum:<br>
<span class="date-row"> <span class="date-row">
@@ -110,16 +620,49 @@
<legend class="sr-only">Optionen</legend> <legend class="sr-only">Optionen</legend>
<input type="checkbox" name="werktage" id="werktage" {% if request.form.get('werktage') %}checked{% endif %} aria-checked="{{ 'true' if request.form.get('werktage') else 'false' }}"> <input type="checkbox" name="werktage" id="werktage" {% if request.form.get('werktage') %}checked{% endif %} aria-checked="{{ 'true' if request.form.get('werktage') else 'false' }}">
<label for="werktage" style="margin:0;">Nur Werktage</label> <label for="werktage" style="margin:0;">Nur Werktage</label>
<label for="bundesland" style="margin-left:1em;">Feiertage berücksichtigen für:
<select name="bundesland" id="bundesland" {% if not request.form.get('werktage') %}disabled{% endif %}>
<option value="">(kein Bundesland)</option>
<option value="BW" {% if request.form.get('bundesland') == 'BW' %}selected{% endif %}>Baden-Württemberg</option>
<option value="BY" {% if request.form.get('bundesland') == 'BY' %}selected{% endif %}>Bayern</option>
<option value="BE" {% if request.form.get('bundesland') == 'BE' %}selected{% endif %}>Berlin</option>
<option value="BB" {% if request.form.get('bundesland') == 'BB' %}selected{% endif %}>Brandenburg</option>
<option value="HB" {% if request.form.get('bundesland') == 'HB' %}selected{% endif %}>Bremen</option>
<option value="HH" {% if request.form.get('bundesland') == 'HH' %}selected{% endif %}>Hamburg</option>
<option value="HE" {% if request.form.get('bundesland') == 'HE' %}selected{% endif %}>Hessen</option>
<option value="MV" {% if request.form.get('bundesland') == 'MV' %}selected{% endif %}>Mecklenburg-Vorpommern</option>
<option value="NI" {% if request.form.get('bundesland') == 'NI' %}selected{% endif %}>Niedersachsen</option>
<option value="NW" {% if request.form.get('bundesland') == 'NW' %}selected{% endif %}>Nordrhein-Westfalen</option>
<option value="RP" {% if request.form.get('bundesland') == 'RP' %}selected{% endif %}>Rheinland-Pfalz</option>
<option value="SL" {% if request.form.get('bundesland') == 'SL' %}selected{% endif %}>Saarland</option>
<option value="SN" {% if request.form.get('bundesland') == 'SN' %}selected{% endif %}>Sachsen</option>
<option value="ST" {% if request.form.get('bundesland') == 'ST' %}selected{% endif %}>Sachsen-Anhalt</option>
<option value="SH" {% if request.form.get('bundesland') == 'SH' %}selected{% endif %}>Schleswig-Holstein</option>
<option value="TH" {% if request.form.get('bundesland') == 'TH' %}selected{% endif %}>Thüringen</option>
</select>
</label>
</fieldset> </fieldset>
<button name="action" value="tage_werktage" type="submit">Berechnen</button> <button name="action" value="tage_werktage" type="submit">Berechnen</button>
</form> </form>
{% if tage is not none %} {% if tage is not none %}
<div class="result" aria-live="polite"> <div class="result" aria-live="polite">
{% if request.form.get('werktage') %} {% if request.form.get('werktage') %}
Anzahl der Werktage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }} Anzahl der Werktage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>{% if request.form.get('bundesland') %} (Feiertage {{ request.form.get('bundesland') }}){% endif %}: {{ tage }}
{% else %} {% else %}
Anzahl der Tage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }} Anzahl der Tage zwischen <b>{{ format_date(request.form.get('start1', '')) }}</b> und <b>{{ format_date(request.form.get('end1', '')) }}</b>: {{ tage }}
{% endif %} {% endif %}
{% if wochenendtage_anzahl is not none or feiertage_anzahl is not none %}
<br>
<span style="font-size:0.98em; color:#1e293b;">
{% if wochenendtage_anzahl is not none %}
<b>{{ wochenendtage_anzahl }}</b> Wochenendtage
{% endif %}
{% if feiertage_anzahl is not none %}
{% if wochenendtage_anzahl is not none %} | {% endif %}
<b>{{ feiertage_anzahl }}</b> Feiertage (Mo-Fr{% if request.form.get('bundesland') %}, {{ request.form.get('bundesland') }}{% endif %})
{% endif %}
</span>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -217,9 +760,9 @@
<label for="richtung_pm_add"><input type="radio" name="richtung_pm" id="richtung_pm_add" value="add" checked> addieren</label> <label for="richtung_pm_add"><input type="radio" name="richtung_pm" id="richtung_pm_add" value="add" checked> addieren</label>
<label for="richtung_pm_sub"><input type="radio" name="richtung_pm" id="richtung_pm_sub" value="sub"> subtrahieren</label> <label for="richtung_pm_sub"><input type="radio" name="richtung_pm" id="richtung_pm_sub" value="sub"> subtrahieren</label>
</fieldset> </fieldset>
<fieldset class="date-calc-row" style="border:none; padding:0;"> <fieldset style="display:flex; align-items:center; gap:0.5em; margin-top:0.7em; border:none; padding:0;">
<legend class="sr-only">Einheit und Werktage</legend> <legend class="sr-only">Einheit und Werktage</legend>
<label for="einheit_pm">Einheit: <label for="einheit_pm" style="margin:0;">Einheit:
<select name="einheit_pm" id="einheit_pm"> <select name="einheit_pm" id="einheit_pm">
<option value="tage">Tage</option> <option value="tage">Tage</option>
<option value="wochen">Wochen</option> <option value="wochen">Wochen</option>
@@ -227,7 +770,7 @@
</select> </select>
</label> </label>
<input type="checkbox" name="werktage_pm" id="werktage_pm"> <input type="checkbox" name="werktage_pm" id="werktage_pm">
<label for="werktage_pm" style="margin-left:1em;">Nur Werktage</label> <label for="werktage_pm" style="margin:0;">Nur Werktage</label>
</fieldset> </fieldset>
<button name="action" value="plusminus" type="submit">Berechnen</button> <button name="action" value="plusminus" type="submit">Berechnen</button>
</form> </form>
@@ -238,9 +781,76 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Help Modal Overlay -->
<div id="helpModal" class="modal-overlay" role="dialog" aria-labelledby="help-title" aria-describedby="help-content">
<div class="modal-content">
<button type="button" class="modal-close" onclick="hideHelp()" aria-label="Hilfe schließen">&times;</button>
<h1 id="help-title">Was ist Elpatrons Datumsrechner?</h1>
<p>Der Datumsrechner kann verschiedene Datumsberechnungen durchführen:</p>
<ul>
<li>Anzahl der Tage zwischen zwei Daten</li>
<li>Anzahl der Werktage zwischen zwei Daten</li>
<li>Anzeige des Wochentags eines Datums</li>
<li>Datum plus/minus X Tage</li>
<li>Datum plus/minus X Werktage</li>
<li>Datum plus/minus X Wochen/Monate</li>
<li>Kalenderwoche zu Datum</li>
<li>Start-/Enddatum einer Kalenderwoche eines Jahres</li>
</ul>
<h2>Online Datumsrechner gibt es bereits in einer Vielzahl, warum also noch einer?</h2>
<p>Aus zwei Gründen:</p>
<ul>
<li>Finde mal einen Datumsrechner, der nicht vollkommen verseucht mit Werbung, Tracking und Cookies ist!</li>
<li>Das hat mich so geärgert, dass ich meinen eigenen programmiert habe.
<ul>
<li>Genau genommen nicht ich selbst. Diese App wurde zum überwiegenden Teil von KI nach meinen Anweisungen entwickelt (Vibe Coding).</li>
</ul>
</li>
</ul>
<h2>Was du noch wissen solltest</h2>
<ul>
<li>Ich habe versucht, die App möglichst barrierefrei zu gestalten, um Menschen mit Einschränkungen die Benutzung zu erleichtern.</li>
<li>Diese App schnüffelt dir nicht hinterher, sammelt keine persönlichen Daten und geht dir auch sonst (hoffentlich!) in keiner Weise auf die Nerven.</li>
<li>Den Quellcode dieser App habe ich auf <a href="https://codeberg.org/elpatron/datecalc" target="_blank">Codeberg</a> veröffentlicht, du kannst ihn einsehen, verändern oder damit deinen eigenen kleinen Datumsrechner betreiben.</li>
<li>Die App läuft auf meinem kleinen Home-Server und ist derzeit nicht für große Besucherzahlen ausgelegt.</li>
<li>Ich übernehme keine Gewähr für die Funktionalität und die Rechenergebnisse. Die KI, die das programmiert hat, übrigens auch nicht.</li>
<li>Falls du einen Fehler findest oder eine weitere Funktion wünschst, kannst du mir eine Mail schreiben (siehe Mailto Link in der Fußzeile)</li>
</ul>
<p><strong>Hab Spaß mit Elpatrons Datumsrechner! Dein M. Busche</strong></p>
</div>
<div id="help-content" class="sr-only">
Hilfe-Informationen für den Datumsrechner mit Erklärungen zu allen Funktionen
</div>
</div>
<footer style="text-align:center; margin-top:2em; color:#64748b; font-size:0.98em; padding-bottom:1.5em;"> <footer style="text-align:center; margin-top:2em; color:#64748b; font-size:0.98em; padding-bottom:1.5em;">
Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc" target="_blank" style="color:#2563eb; text-decoration:underline;">Open Source Datumsrechner</a><br> Dies ist ein werbe- und trackingfreier <a href="https://codeberg.org/elpatron/datecalc/src/branch/main/README.md" target="_blank" style="color:#2563eb; text-decoration:underline;">Open Source Datumsrechner</a><br>
<a href="/api-docs" target="_blank" style="color:#2563eb; text-decoration:underline;">REST API Dokumentation (Swagger)</a><br>
© 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#2563eb; text-decoration:underline;">M. Busche</a> © 2025 <a href="mailto:elpatron@mailbox.org?subject=Datumsrechner" style="color:#2563eb; text-decoration:underline;">M. Busche</a>
<div style="margin-top:0.5em; font-size:0.85em; color:#94a3b8;">v{{ app_version }}</div>
</footer> </footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
var werktageCheckbox = document.getElementById('werktage');
var bundeslandSelect = document.getElementById('bundesland');
if (werktageCheckbox && bundeslandSelect) {
function toggleBundesland() {
bundeslandSelect.disabled = !werktageCheckbox.checked;
}
werktageCheckbox.addEventListener('change', toggleBundesland);
// Initial setzen
toggleBundesland();
}
});
</script>
</body> </body>
</html> </html>

View File

@@ -6,6 +6,13 @@
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f8fafc;
color: #1e293b;
margin: 0;
padding: 0;
}
.dashboard-box { max-width: 600px; margin: 3em auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #cbd5e1; padding: 2em 2em 1.5em 2em; border: 1px solid #e5e7eb; } .dashboard-box { max-width: 600px; margin: 3em auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #cbd5e1; padding: 2em 2em 1.5em 2em; border: 1px solid #e5e7eb; }
.dashboard-box h2 { text-align: center; margin-bottom: 1.2em; } .dashboard-box h2 { text-align: center; margin-bottom: 1.2em; }
.stats-row { display: flex; justify-content: space-between; margin-bottom: 2em; } .stats-row { display: flex; justify-content: space-between; margin-bottom: 2em; }
@@ -16,9 +23,9 @@
</head> </head>
<body> <body>
<div class="dashboard-box"> <div class="dashboard-box">
<h2>Statistik-Dashboard</h2> <h1>Statistik-Dashboard</h1>
<div class="stats-row"> <div class="stats-row">
<div class="stats-label">Gesamt-Pageviews:</div> <div class="stats-label">Gesamt-Pageviews (7 Tage):</div>
<div class="stats-value">{{ pageviews }}</div> <div class="stats-value">{{ pageviews }}</div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@@ -32,12 +39,12 @@
<canvas id="apiChart" width="400" height="220"></canvas> <canvas id="apiChart" width="400" height="220"></canvas>
</div> </div>
{% endif %} {% endif %}
<pre style="background:#f3f4f6; color:#334155; padding:0.5em; border-radius:6px; font-size:0.9em;">API-Counts: {{ api_counts|tojson }}</pre>
<a href="/" style="color:#2563eb;">Zurück zur App</a> <a href="/" style="color:#2563eb;">Zurück zur App</a>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Impressions pro Tag // Impressions pro Tag
// eslint-disable-next-line
const imprData = {{ impressions_per_day|tojson }}; const imprData = {{ impressions_per_day|tojson }};
const imprLabels = Object.keys(imprData); const imprLabels = Object.keys(imprData);
const imprCounts = Object.values(imprData); const imprCounts = Object.values(imprData);
@@ -62,6 +69,7 @@
} }
}); });
// Funktionsaufrufe // Funktionsaufrufe
// eslint-disable-next-line
const funcCounts = {{ func_counts|tojson }}; const funcCounts = {{ func_counts|tojson }};
const labels = Object.keys(funcCounts); const labels = Object.keys(funcCounts);
const data = Object.values(funcCounts); const data = Object.values(funcCounts);
@@ -83,6 +91,7 @@
} }
}); });
// API-Nutzung // API-Nutzung
// eslint-disable-next-line
const apiCounts = {{ api_counts|tojson }}; const apiCounts = {{ api_counts|tojson }};
if (Object.keys(apiCounts).length > 0 && document.getElementById('apiChart')) { if (Object.keys(apiCounts).length > 0 && document.getElementById('apiChart')) {
new Chart(document.getElementById('apiChart').getContext('2d'), { new Chart(document.getElementById('apiChart').getContext('2d'), {

28
templates/swagger.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>API-Dokumentation Swagger UI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css">
<style>
body { margin: 0; background: #f8fafc; }
#swagger-ui { max-width: 900px; margin: 2em auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(30,41,59,0.07); }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
SwaggerUIBundle({
url: '/static/swagger.json',
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis],
layout: "BaseLayout",
docExpansion: 'list',
defaultModelsExpandDepth: 1
});
};
</script>
</body>
</html>