Compare commits

..

6 Commits

@ -1,161 +1,11 @@
# PIOMINT # PIOMINT
Track every hours with minty Fresh Efficiency!
Track every hour with minty fresh efficiency! 🌿 ## AppIdee
Es gibt einige Zeit-Tracker Apps für Pioniere. Leide sind die guten in der Regel nur auf Englisch erhältlicht und viel zu Umständlich.
Eine schlanke Progressive Web App für Pioniere, um Dienststunden, Bibelstudien und ## Das Ziel
Bemerkungen unkompliziert zu erfassen und den Monatsbericht mit einem Klick fertig Eine einfache App mit einem freshen UI mit der es leicht fällt die Stunden aufzuschreiben und pünktlich abzugeben.
formatiert für WhatsApp zu teilen.
---
## Inhalt
- [Über das Projekt](#über-das-projekt)
- [Aktueller Stand](#aktueller-stand)
- [Features](#features)
- [Tech-Stack](#tech-stack)
- [Projektstruktur](#projektstruktur)
- [Lokale Entwicklung](#lokale-entwicklung)
- [Daten & Datenschutz](#daten--datenschutz)
- [Versionierung & Deployment](#versionierung--deployment)
- [Roadmap / offene Punkte](#roadmap--offene-punkte)
- [Mitwirken](#mitwirken)
- [Lizenz](#lizenz)
---
## Über das Projekt
Es gibt einige Zeit-Tracker-Apps für Pioniere. Leider sind die guten in der Regel nur
auf Englisch erhältlich und viel zu umständlich.
**Das Ziel:** eine einfache App mit einem frischen UI, mit der es leicht fällt, die
Stunden aufzuschreiben und pünktlich abzugeben als installierbare PWA, komplett auf
Deutsch, ohne Account-Zwang.
## Aktueller Stand ## Aktueller Stand
GANZ AM ANFANG :D
**Version 26.0** in aktivem Einsatz. Aus dem "GANZ AM ANFANG"-Prototyp ist eine
funktionsreiche PWA geworden (siehe [Features](#features)). Läuft produktiv unter
[app.piomint.de](https://app.piomint.de).
## Features
- **Dienstjahr statt Kalenderjahr** rechnet korrekt von September bis August, inkl.
Navigation beliebig weit vor und zurück über Jahresgrenzen hinweg
- **Monats- & Jahresübersicht** mit Fortschrittsbalken (600h-Ziel) und individuellem
Monatsziel
- **55h-Regel** (optional, in den Einstellungen aktivierbar): deckelt Monate mit
LDC-/Sonstiges-Anteil bei der Jahressumme automatisch auf 55h
- **Schnellwahl-Feld** (früher fix "LDC") Name frei in den Einstellungen anpassbar
- **Aktivitäten-Tabelle** mit Sortierung (Datum/Dauer/Typ, auf-/absteigend),
ein-/ausblendbaren Filteroptionen und Maximieren-Ansicht für mehr Überblick
- **Bericht teilen** ein Klick kopiert den Monatsbericht fertig für WhatsApp
formatiert in die Zwischenablage (inkl. Vorschau-Dialog)
- **Timer** zur direkten Zeiterfassung
- **Installierbare PWA** (Homescreen-Icon, Standalone-Modus, Splashscreen) mit
automatischem Update-Hinweis bei neuer Version
- Alle Daten liegen **lokal auf dem Gerät** (IndexedDB) siehe
[Daten & Datenschutz](#daten--datenschutz)
## Tech-Stack
| Bereich | Technologie |
|---|---|
| Backend | Django 5.2 (`django-pwa` für Manifest/Service-Worker-Einbindung) |
| Datenhaltung (Nutzerdaten) | IndexedDB im Browser (kein serverseitiges Datenmodell) |
| Datenbank (Backend) | SQLite lokal / MySQL in Produktion |
| Frontend | Vanilla JavaScript, Bootstrap (CSS + JS-Bundle) |
| Deployment | Gunicorn |
| Sprache | Deutsch (`LANGUAGE_CODE = 'de'`) |
Es gibt bewusst kein Frontend-Build-Setup (kein npm/webpack) die JS-Dateien unter
`app/static/app/js/` werden direkt vom Browser geladen.
## Projektstruktur
```
piomint/
├── piomint/ # Django-Projekt (Settings, URLs, PWA-Konfiguration)
│ ├── settings.py
│ ├── pwa_settings.py # App-Name, Icons, Start-URL, Theme-Farben
│ └── urls.py
├── app/ # Hauptanwendung (Dashboard, Tracking, Einstellungen)
│ ├── views.py # Versionierte Routen (siehe Versionierung & Deployment)
│ ├── urls.py
│ ├── templates/app/ # base.html (Header/Settings), home.html (Dashboard), ...
│ └── static/app/
│ ├── js/ # dbcontrol.js (IndexedDB), home01.js (UI-Logik), ...
│ ├── css/
│ └── images/
├── web/ # Öffentliche Landingpage (piomint.de)
├── doc/ # Platzhalter-App, aktuell ungenutzt
└── requirements.txt
```
## Lokale Entwicklung
Voraussetzung: Python 3.12+
```bash
git clone https://git.samuelzielke.de/samuelzielke/piomint.git
cd piomint
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver
```
Die App läuft dann unter `http://127.0.0.1:8000/app/` (aktuelle Version, siehe unten).
Für die Backend-Datenbank reicht lokal SQLite (Standardeinstellung in
`piomint/settings.py`) die eigentlichen Nutzerdaten (Stunden, Einstellungen) landen
ohnehin im Browser, nicht in dieser DB.
## Daten & Datenschutz
PioMint speichert alle erfassten Stunden, Einstellungen und Bevorzugungen **lokal im
Browser** (IndexedDB + `localStorage`) es gibt aktuell keinen Account und keine
Server-Synchronisation. Das bedeutet:
- Kein Tracking, keine personenbezogenen Daten auf dem Server
- **Löscht man die App/den Browser-Speicher, sind alle Einträge unwiderruflich weg**
ein manuelles Backup (Export-Funktion in den Einstellungen) ist bis zu einer
Server-Synchronisation die einzige Absicherung
## Versionierung & Deployment
Jede Version bekommt ihre eigene URL nach dem Muster `/app/v<version>/`
(z. B. `/app/v260/` für 26.0), definiert in `app/urls.py` / `app/views.py`. Der Grund:
die PWA merkt sich beim Hinzufügen zum Homescreen eine feste `start_url`
(`PWA_APP_START_URL` in `piomint/pwa_settings.py`). Damit Nutzer:innen nach einem
Update nicht auf einer eingefrorenen alten Version hängen bleiben, leiten **alle**
bisherigen Versions-URLs auf die aktuellste weiter.
**Checkliste für ein neues Release:**
1. Neue View + URL `v<neue Version>` in `app/views.py` / `app/urls.py` anlegen
2. Alle bisherigen Versions-Views auf die neue Route umbiegen (`redirect(...)`)
3. `PWA_APP_START_URL` in `piomint/pwa_settings.py` auf die neue Route setzen
4. `versionnumber` in `app/templates/app/updateinfo.html` hochzählen
5. Changelog-Text in `updateinfo.html` schreiben (erscheint als Update-Hinweis beim
ersten Öffnen der neuen Version)
## Roadmap / offene Punkte
- Server-seitige Synchronisation / Account-System (aktuell rein lokale Speicherung)
- Einstellbares Rundungsverhalten (aktuell fix: Aufrundung ab 15 Minuten)
- `doc`-App ist als Platzhalter angelegt, aber noch ohne Inhalt/Routing
## Mitwirken
Feedback, Bug-Reports und Ideen gerne über die
[WhatsApp-Community](https://chat.whatsapp.com/Cy4laGaEdLP1DZbhN04zRQ) oder direkt an
Samuel.
## Lizenz
Bisher keine Lizenz festgelegt alle Rechte liegen beim Autor.

@ -38,29 +38,45 @@
@keyframes openSettingsbg { @keyframes openSettingsbg {
0% { 0% {
height: 8vh; height: 7vh;
} }
100% { 100% {
height: 100vh; height: 100vh;
} }
} }
/* rem statt vh: die Monatskarte soll auf jedem Gerät gleich groß aussehen ("fix"), nur die
Aktivitäten-Tabelle (flex:1 1 auto, siehe unten) gleicht unterschiedliche Displayhöhen aus. */
@keyframes openMonthArea { @keyframes openMonthArea {
0% { 0% {
height: 6.5rem; height: 15vh;
margin-top: 1.5rem; margin-top: 1.5rem;
} }
100% { 100% {
height: 9.5rem; height: 20vh;
margin-top: 1.5rem; margin-top: 1.5rem;
} }
} }
@keyframes shrinkActivitys {
0% {
height: 40vh;
}
100% {
height: 35vh;
}
}
@keyframes growActivitys {
0% {
height: 35vh;
}
100% {
height: 40vh;
}
}
@keyframes openSettingscolor { @keyframes openSettingscolor {
0% { 0% {
height: 7vh; height: 6vh;
} }
100% { 100% {
height: 95vh; height: 95vh;
@ -72,7 +88,7 @@
height: 95vh; height: 95vh;
} }
100% { 100% {
height: 7vh; height: 6vh;
} }
} }
@ -81,27 +97,27 @@
height: 100vh; height: 100vh;
} }
100% { 100% {
height: 8vh; height: 7vh;
} }
} }
@keyframes closeMonthArea { @keyframes closeMonthArea {
0% { 0% {
height: 9.5rem; height: 20vh;
margin-top: 1.5rem; margin-top: 1.5rem;
} }
100% { 100% {
height: 6.5rem; height: 15vh;
margin-top: 1.5rem; margin-top: 1.5rem;
} }
} }
.HeaderDefaultHeightbg { .HeaderDefaultHeightbg {
height: 8vh; height: 7vh;
} }
.HeaderDefaultHeightcolor { .HeaderDefaultHeightcolor {
height: 7vh; height: 6vh;
} }
.slide-out { .slide-out {
@ -129,6 +145,14 @@
animation: closeMonthArea 1s forwards; animation: closeMonthArea 1s forwards;
} }
.shrinkActivitys {
animation: shrinkActivitys 1s forwards;
}
.growActivitys {
animation: growActivitys 1s forwards;
}
.closeSettingsbg { .closeSettingsbg {
animation: closeSettingsbg 1s forwards; animation: closeSettingsbg 1s forwards;
} }
@ -146,66 +170,7 @@
} }
#dashbord_month_area { #dashbord_month_area {
height: 6.5rem; height: 15vh;
}
/* Neu hinzugefügte Icons (Filter-Toggle, Maximieren) in Grau statt Schwarz/Weiß.
invert(50%) bildet jede Ausgangsfarbe (schwarz, weiß, hell-grau) auf denselben
neutralen Grauton ab - unabhängig davon, welches der beiden Icons gerade angezeigt wird. */
#toggleSortOptionsIcon,
#openViewOfEntryIcon {
filter: invert(50%);
}
/* Tabelle maximieren: Timer-Karte klappt weg, Aktivitäten-Tabelle wächst.
overflow bleibt im normalen Zustand "visible", damit Rand/Schatten der Timer-Karte
nicht abgeschnitten werden - nur beim Einklappen selbst wird geclippt. */
#timerCard {
max-height: 20vh;
opacity: 1;
overflow: visible;
transition: max-height 0.4s ease-in-out, opacity 0.3s ease-in-out, padding 0.4s ease-in-out;
}
#timerCard.timerCollapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
padding-top: 0 !important;
padding-bottom: 0 !important;
pointer-events: none;
}
/* Kein fester height-Override beim Maximieren mehr nötig: ActivitysTable ist flex:1 1 auto
(siehe #dashboard) und wächst automatisch in den Platz, den die kollabierende Timer-Karte
freigibt - inklusive sauberer Animation, weil beides gleichzeitig neu layoutet wird. */
/* Filter/Sortierung ein- und ausblenden.
overflow bleibt im geöffneten Zustand "visible", damit das Dropdown-Menü (sortFieldMenu)
unterhalb der Zeile nicht abgeschnitten wird - nur beim Einklappen selbst wird geclippt. */
#sortOptionsRow {
max-height: 3rem;
opacity: 1;
overflow: visible;
transition: max-height 0.3s ease-in-out, opacity 0.25s ease-in-out, margin-top 0.3s ease-in-out;
}
#sortOptionsRow.sortOptionsCollapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
margin-top: 0 !important;
pointer-events: none;
}
.sortFieldOption {
cursor: pointer;
}
.sortFieldOption:hover,
.sortFieldOption:active {
background-color: rgb(120,170,86);
color: white;
} }
.header-bg-block { .header-bg-block {
@ -220,58 +185,9 @@
} }
.maincontainer { .maincontainer {
height: 92vh; height: 93vh;
transition: height 1s ease, transform 1s ease; transition: height 1s ease, transform 1s ease;
margin: 0; margin: 0;
/* Grundsätzlicher Fix für kleinere Displays (z.B. iPhone 12 mini vs. 15 Pro): die feste
vh-Höhe reicht auf kürzeren Viewports nicht für den ganzen Karten-Stapel, und body hat
overflow:hidden (verhindert Bounce/Pull-to-refresh) - ohne dieses overflow-y wäre alles
unterhalb des sichtbaren Bereichs dauerhaft unerreichbar statt nur eng. */
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
/* align-self statt der geerbten align-items-center des Elternelements: sonst würde
überlaufender Inhalt (auf kleinen Displays) mittig "geclippt" und wäre weder oben
noch unten per Scroll erreichbar. update_content bekommt das ebenfalls, da die
Update-Infos (siehe unten) jetzt deutlich mehr Inhalt haben. welcome_content bleibt
kurz genug, um weiterhin mittig zentriert zu sein. */
#dashboard,
#update_content {
align-self: flex-start;
width: 100%;
}
/* Jahr/Monat/Timer bleiben "fix" (rem-/inhaltsbasiert, gerätunabhängig gleich groß).
Die Aktivitäten-Tabelle (flex:1 1 auto, siehe ihre eigene Regel) gleicht als einziges
Element unterschiedliche Displayhöhen aus, statt dass wir jede Karte einzeln pro
Gerät neu austarieren müssen. */
#dashboard {
display: flex;
flex-direction: column;
height: 100%;
}
/* "Wow-Moment" beim Öffnen der Update-Infos: sanftes Herein-Poppen statt einfach da zu sein. */
@keyframes updateWow {
0% { opacity: 0; transform: scale(0.85) translateY(20px); }
60% { opacity: 1; transform: scale(1.03) translateY(-4px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
#update_content {
animation: updateWow 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes emojiPulse {
0%, 100% { transform: scale(1) rotate(0deg); }
50% { transform: scale(1.15) rotate(-8deg); }
}
.wow-emoji {
display: inline-block;
animation: emojiPulse 1.6s ease-in-out infinite;
} }
.bg-white-color { .bg-white-color {

@ -4,7 +4,6 @@ function setUpdateOk() {
document.getElementById('update_content').style.display = 'none'; document.getElementById('update_content').style.display = 'none';
document.getElementById('dashboard').classList.remove('d-none'); document.getElementById('dashboard').classList.remove('d-none');
document.getElementById('refresh_icon').classList.remove('d-none') document.getElementById('refresh_icon').classList.remove('d-none')
document.getElementById('share_icon').classList.remove('d-none')
document.getElementById('settings_icon').classList.remove('d-none') document.getElementById('settings_icon').classList.remove('d-none')
} }

@ -226,35 +226,29 @@ function formatDate(dateString) {
return `${day}.${month}.${year}`; // Format tt.mm.yy return `${day}.${month}.${year}`; // Format tt.mm.yy
} }
// Dienstjahr-Label für ein Datum ermitteln. // Einträge nach Monat und/oder Jahr filtern
// Dienstjahr läuft September(Jahr X) bis August(Jahr X+1) und wird mit dem Label X+1 bezeichnet.
// Beispiel: 15. September 2025 -> Dienstjahr-Label 2026; 15. Mai 2026 -> Dienstjahr-Label 2026.
function getServiceYear(date) {
const d = (date instanceof Date) ? date : new Date(date);
const calendarYear = d.getFullYear();
const calendarMonth = d.getMonth() + 1; // 1-basiert
return (calendarMonth >= 9) ? calendarYear + 1 : calendarYear;
}
// Einträge nach Monat und/oder Kalenderjahr filtern.
// WICHTIG: 'year' ist hier immer das ECHTE Kalenderjahr, KEIN Dienstjahr-Label.
// Für Dienstjahr-Filterung siehe filterEntriesByServiceYear().
function filterEntriesByMonthYear(month, year) { function filterEntriesByMonthYear(month, year) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction(['hours'], 'readonly'); const transaction = db.transaction(['hours'], 'readonly');
const store = transaction.objectStore('hours'); const store = transaction.objectStore('hours');
const request = store.getAll(); const request = store.getAll();
request.onsuccess = function () { request.onsuccess = function () {
const allEntries = request.result; const allEntries = request.result;
// Filter nach Monat und Jahr // Filter nach Monat und Jahr
const filteredEntries = allEntries.filter(entry => { const filteredEntries = allEntries.filter(entry => {
const entryDate = new Date(entry.date); // Konvertiere das date-Feld in ein Date-Objekt const entryDate = new Date(entry.date); // Konvertiere das date-Feld in ein Date-Objekt
var actualYear = year;
const matchesMonth = month ? entryDate.getMonth() + 1 === month : true; // +1 da getMonth() 0-basiert ist const matchesMonth = month ? entryDate.getMonth() + 1 === month : true; // +1 da getMonth() 0-basiert ist
const matchesYear = year ? entryDate.getFullYear() === year : true; if (entryDate.getMonth() + 1 >= 9){
actualYear = actualYear - 1;
} else {
actualYear = actualYear;
};
const matchesYear = actualYear ? entryDate.getFullYear() === actualYear : true;
return matchesMonth && matchesYear; return matchesMonth && matchesYear;
}); });
@ -268,49 +262,17 @@ function filterEntriesByMonthYear(month, year) {
}); });
} }
// Einträge für ein komplettes Dienstjahr (Sept. Vorjahr - Aug. Label-Jahr) filtern.
// serviceYearLabel entspricht dem Rückgabewert von getServiceYear(), z.B. 2026 für Sept 2025 - Aug 2026.
function filterEntriesByServiceYear(serviceYearLabel) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['hours'], 'readonly');
const store = transaction.objectStore('hours');
const request = store.getAll();
request.onsuccess = function () {
const allEntries = request.result;
const filteredEntries = allEntries.filter(entry => {
return getServiceYear(new Date(entry.date)) === serviceYearLabel;
});
resolve(filteredEntries);
};
request.onerror = function (event) {
reject('Fehler beim Abrufen der Einträge:', event.target.error);
};
});
}
// Tabelle auf dem Dashboard mit Infos füllen // Tabelle auf dem Dashboard mit Infos füllen
// sortField: 'date' | 'duration' | 'type' - sortDirection: 'asc' | 'desc' function fillTableWithEntries(entries) {
function fillTableWithEntries(entries, sortField = 'date', sortDirection = 'asc', ldcLabel = 'LDC') {
const tableBody = document.getElementById('entriesTableBody'); const tableBody = document.getElementById('entriesTableBody');
const sortedEntries = entries.slice().sort((a, b) => { entries.sort((a, b) => new Date(a.date) - new Date(b.date));
const valA = (sortField === 'date') ? new Date(a.date) : a[sortField];
const valB = (sortField === 'date') ? new Date(b.date) : b[sortField];
if (valA < valB) return (sortDirection === 'asc') ? -1 : 1;
if (valA > valB) return (sortDirection === 'asc') ? 1 : -1;
return 0;
});
// Zuerst den Inhalt der Tabelle löschen (falls schon Daten da sind) // Zuerst den Inhalt der Tabelle löschen (falls schon Daten da sind)
tableBody.innerHTML = ''; tableBody.innerHTML = '';
// Iteriere durch die Einträge und füge Zeilen hinzu // Iteriere durch die Einträge und füge Zeilen hinzu
sortedEntries.forEach(entry => { entries.forEach(entry => {
const row = document.createElement('tr'); const row = document.createElement('tr');
//EVENTUELL MAL IN DER ZUKUNFT - UM EINTRÄGE ZU SEITE WISCHEN //EVENTUELL MAL IN DER ZUKUNFT - UM EINTRÄGE ZU SEITE WISCHEN
@ -357,7 +319,7 @@ function fillTableWithEntries(entries, sortField = 'date', sortDirection = 'asc'
if (entry.type === 3) { if (entry.type === 3) {
typeCell.textContent = entry.sondertext; // Bei Type 3 zeige "Beschreibung" an typeCell.textContent = entry.sondertext; // Bei Type 3 zeige "Beschreibung" an
} else if (entry.type === 2) { } else if (entry.type === 2) {
typeCell.textContent = ldcLabel; // Ansonsten zeige den Type-Wert (individuell umbenennbar, siehe Einstellungen) typeCell.textContent = 'LDC'; // Ansonsten zeige den Type-Wert
} else if (entry.type === 1) { } else if (entry.type === 1) {
typeCell.textContent = 'Dienst'; // Ansonsten zeige den Type-Wert typeCell.textContent = 'Dienst'; // Ansonsten zeige den Type-Wert
} }
@ -416,43 +378,4 @@ function sumDurations(data, type = null, roundToHalfHour = false) {
} }
return totalDuration; return totalDuration;
}
// Gesamtstunden (in Minuten) für ein Dienstjahr berechnen, optional mit "55h-Regel".
// Wenn rule55Active true ist: jeder Kalendermonat, der LDC- oder Sonstiges-Stunden (type 2/3)
// enthält, wird für die Summe auf maximal 55h gedeckelt. Reine Dienststunden (type 1) zählen
// in jedem Monat immer voll, auch wenn sie allein schon über 55h liegen - der Deckel greift
// nur, sobald LDC/Sonstiges dazukommt.
function calculateServiceYearTotal(entries, rule55Active = false) {
if (!rule55Active) {
return sumDurations(entries, null, true);
}
const CAP_MINUTES = 55 * 60;
// Einträge nach Kalendermonat gruppieren (Jahr+Monat, damit Dez/Jan nicht vermischt werden)
const monthBuckets = {};
entries.forEach(entry => {
const entryDate = new Date(entry.date);
const key = `${entryDate.getFullYear()}-${entryDate.getMonth()}`;
if (!monthBuckets[key]) {
monthBuckets[key] = [];
}
monthBuckets[key].push(entry);
});
let total = 0;
Object.values(monthBuckets).forEach(bucketEntries => {
const normalMinutes = sumDurations(bucketEntries, 1);
const capRuleMinutes = sumDurations(bucketEntries, 2) + sumDurations(bucketEntries, 3);
let monthTotal = normalMinutes + capRuleMinutes;
if (capRuleMinutes > 0) {
monthTotal = Math.min(monthTotal, CAP_MINUTES);
}
total += Math.round(monthTotal / 30) * 30;
});
return total;
} }

@ -4,49 +4,10 @@ const mins = document.getElementById('minuten');
const NewEntryButton = document.getElementById('newEntrySubmitButton') const NewEntryButton = document.getElementById('newEntrySubmitButton')
const SETStudySaveButton = document.getElementById('SETStudySaveButton') const SETStudySaveButton = document.getElementById('SETStudySaveButton')
const SETMonthGoalSaveButton = document.getElementById('SETMonthGoalSaveButton') const SETMonthGoalSaveButton = document.getElementById('SETMonthGoalSaveButton')
const SETLdcNameSaveButton = document.getElementById('SETLdcNameSaveButton')
const rule55hCheck = document.getElementById('rule55hCheck')
const openViewOfEntry = document.getElementById('openViewOfEntry')
const openViewOfEntryIcon = document.getElementById('openViewOfEntryIcon')
const ActivitysTable = document.getElementById('ActivitysTable')
const timerCard = document.getElementById('timerCard')
const toggleSortOptions = document.getElementById('toggleSortOptions')
const sortOptionsRow = document.getElementById('sortOptionsRow')
const sortFieldButton = document.getElementById('sortFieldButton')
const sortFieldLabel = document.getElementById('sortFieldLabel')
const sortFieldMenu = document.getElementById('sortFieldMenu')
const sortFieldOptions = document.querySelectorAll('.sortFieldOption')
const sortDirectionToggle = document.getElementById('sortDirectionToggle')
const sortDirectionIcon = document.getElementById('sortDirectionIcon')
const shareIcon = document.getElementById('share_icon')
const shareReportModalEl = document.getElementById('shareReportModal')
const shareReportStatus = document.getElementById('shareReportStatus')
const shareReportPreview = document.getElementById('shareReportPreview')
var allowToSend = false; var allowToSend = false;
const monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]; const monate = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
const currentDate = new Date(); const currentDate = new Date();
// 55h-Regel (Jahresansicht): wird beim Start aus den Settings geladen und beim Umschalten
// live aktualisiert, damit getActualDataAndFillView() sie synchron verwenden kann.
var rule55Active = false;
// Individuell vergebbarer Name für "LDC" (SettingsID 4), überall im UI verwendet statt fest "LDC".
var ldcLabel = 'LDC';
// Sortierung der Aktivitäten-Tabelle: wird aus dem Cache (localStorage) geladen/gespeichert.
var sortField = 'date';
var sortDirection = 'asc';
// Ob die Filter/Sortier-Optionen gerade sichtbar sind (wird ebenfalls im Cache gespeichert).
// Standardmäßig ausgeblendet, bis der Nutzer sie einmal selbst einblendet.
var isFilterVisible = false;
// Ob die Aktivitäten-Tabelle gerade maximiert ist (Timer-Karte ist dann ausgeblendet).
var isTableMaximized = false;
// Einträge des gerade angezeigten Monats, für den WhatsApp-Bericht (share_icon) wiederverwendet.
var currentMonthEntries = [];
// //
// Trigger // Trigger
@ -54,189 +15,19 @@ var currentMonthEntries = [];
// Wenn das Dokument komplett geladen ist // Wenn das Dokument komplett geladen ist
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
initDB().then(async () => { initDB().then(() => {
console.log('IndexedDB erfolgreich initialisiert.'); console.log('IndexedDB erfolgreich initialisiert.');
// Hier kannst du die App weiter initialisieren, z.B. Daten laden oder die UI aufbauen // Hier kannst du die App weiter initialisieren, z.B. Daten laden oder die UI aufbauen
// 55h-Regel Einstellung laden, bevor die erste Jahresansicht berechnet wird
await loadRule55Setting();
// LDC-Bezeichnung laden, bevor die Tabelle erstmalig gefüllt wird
await loadLdcLabel();
// Gespeicherte Sortierung der Aktivitäten-Tabelle aus dem Cache laden
loadSortPreference();
// Gespeicherte Sichtbarkeit der Filter/Sortier-Optionen aus dem Cache laden
loadFilterVisibility();
// Bildschirm mit dem Inhalt für den aktuellen Tag füllen // Bildschirm mit dem Inhalt für den aktuellen Tag füllen
getActualDataAndFillView(); getActualDataAndFillView();
//loadInitialData(); //loadInitialData();
}).catch((error) => { }).catch((error) => {
console.error('Fehler bei der Initialisierung der IndexedDB:', error); console.error('Fehler bei der Initialisierung der IndexedDB:', error);
}); });
}); });
// Behebt die Bootstrap-Konsolenwarnung "Blocked aria-hidden on an element because its
// descendant retained focus": beim Schließen eines Modals (z.B. per "X"-Button) setzt
// Bootstrap aria-hidden="true", obwohl der geklickte Button noch fokussiert ist. Fix: Fokus
// vor dem Ausblenden gezielt vom Modal wegnehmen. Gilt für alle Modals der Seite.
document.querySelectorAll('.modal').forEach(modalEl => {
modalEl.addEventListener('hide.bs.modal', () => {
if (document.activeElement && modalEl.contains(document.activeElement)) {
document.activeElement.blur()
}
})
})
// Lädt die 55h-Regel Einstellung (SettingsID 3) aus der DB, setzt rule55Active
// und synchronisiert die Checkbox sowie den Badge in der Jahres-Card.
async function loadRule55Setting() {
const result = await getAllEntries('settings')
const setting = result.find(element => element.SettingsID === 3)
rule55Active = (setting == undefined) ? false : !!parseInt(setting.SettingsValue)
rule55hCheck.checked = rule55Active
updateRule55Badge()
}
// Zeigt/versteckt den "55h-Regel aktiv"-Hinweis an der Jahres-Card
function updateRule55Badge() {
document.getElementById('rule55Badge').classList.toggle('d-none', !rule55Active)
}
// Einstellungen wenn die 55h-Regel umgeschaltet wird
rule55hCheck.addEventListener('change', () => {
rule55Active = rule55hCheck.checked
updateRule55Badge()
const Rule55SettingsData = {
SettingsID: 3,
SettingsValue: rule55Active ? 1 : 0,
}
getAllEntries('settings')
.then(function(result) {
const SETDBData = result.find(element => element.SettingsID === 3)
if (SETDBData != undefined) {
updateEntry(SETDBData.id, 'settings', Rule55SettingsData)
} else {
addEntry(Rule55SettingsData, 'settings')
}
// Jahresansicht mit/ohne Deckel neu berechnen
getActualDataAndFillView(cursorMonth, cursorYear)
})
})
//
// TABELLE MAXIMIEREN
//
// Aktivitäten-Tabelle vergrößern/verkleinern: Timer-Karte klappt beim Maximieren
// animiert weg, um Platz zu schaffen; die Tabelle wächst gleichzeitig.
openViewOfEntry.addEventListener('click', () => {
isTableMaximized = !isTableMaximized
ActivitysTable.classList.toggle('ActivitysTableMaximized', isTableMaximized)
timerCard.classList.toggle('timerCollapsed', isTableMaximized)
openViewOfEntryIcon.src = isTableMaximized
? openViewOfEntryIcon.src.replace('open.png', 'close.png')
: openViewOfEntryIcon.src.replace('close.png', 'open.png')
openViewOfEntry.title = isTableMaximized ? 'Tabelle minimieren' : 'Tabelle maximieren'
})
//
// SORTIERUNG DER AKTIVITÄTEN-TABELLE
//
// Lädt die gespeicherte Sortierung aus dem Cache (localStorage) und synchronisiert die Steuerelemente
function loadSortPreference() {
try {
const saved = JSON.parse(localStorage.getItem('piomintTableSort'))
if (saved && saved.field) {
sortField = saved.field
sortDirection = (saved.direction === 'desc') ? 'desc' : 'asc'
}
} catch (error) {
console.error('Sortierung konnte nicht aus dem Cache geladen werden:', error)
}
const selectedOption = Array.from(sortFieldOptions).find(option => option.dataset.value === sortField)
sortFieldLabel.innerText = selectedOption ? selectedOption.dataset.label : 'Datum'
sortDirectionIcon.innerText = (sortDirection === 'asc') ? 'A → Z' : 'Z → A'
}
// Speichert die aktuelle Sortierung im Cache (localStorage)
function saveSortPreference() {
try {
localStorage.setItem('piomintTableSort', JSON.stringify({ field: sortField, direction: sortDirection }))
} catch (error) {
console.error('Sortierung konnte nicht im Cache gespeichert werden:', error)
}
}
// Eigener Dropdown für die Sortier-Eigenschaft (statt <select>) öffnen/schließen
sortFieldButton.addEventListener('click', (event) => {
event.stopPropagation()
sortFieldMenu.classList.toggle('d-none')
})
// Außerhalb des Menüs klicken schließt es wieder
document.addEventListener('click', (event) => {
if (!sortFieldMenu.classList.contains('d-none') && !sortFieldMenu.contains(event.target) && event.target !== sortFieldButton) {
sortFieldMenu.classList.add('d-none')
}
})
sortFieldOptions.forEach(option => {
option.addEventListener('click', () => {
sortField = option.dataset.value
sortFieldLabel.innerText = option.dataset.label
sortFieldMenu.classList.add('d-none')
saveSortPreference()
getActualDataAndFillView(cursorMonth, cursorYear)
})
})
sortDirectionToggle.addEventListener('click', () => {
sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc'
sortDirectionIcon.innerText = (sortDirection === 'asc') ? 'A → Z' : 'Z → A'
saveSortPreference()
getActualDataAndFillView(cursorMonth, cursorYear)
})
//
// FILTER/SORTIER-OPTIONEN EIN-/AUSBLENDEN
//
// Lädt die gespeicherte Sichtbarkeit der Filter-Optionen aus dem Cache (localStorage)
function loadFilterVisibility() {
try {
const saved = localStorage.getItem('piomintFilterVisible')
if (saved !== null) {
isFilterVisible = (saved === 'true')
}
} catch (error) {
console.error('Filter-Sichtbarkeit konnte nicht aus dem Cache geladen werden:', error)
}
sortOptionsRow.classList.toggle('sortOptionsCollapsed', !isFilterVisible)
}
toggleSortOptions.addEventListener('click', () => {
isFilterVisible = !isFilterVisible
sortOptionsRow.classList.toggle('sortOptionsCollapsed', !isFilterVisible)
// Offenes Dropdown-Menü beim Einklappen der Filterzeile mit schließen
if (!isFilterVisible) {
sortFieldMenu.classList.add('d-none')
}
try {
localStorage.setItem('piomintFilterVisible', isFilterVisible)
} catch (error) {
console.error('Filter-Sichtbarkeit konnte nicht im Cache gespeichert werden:', error)
}
})
// Änderungen überwachen damit die das Formular nicht zu früh abgesendet wird // Änderungen überwachen damit die das Formular nicht zu früh abgesendet wird
@ -248,13 +39,7 @@ mins.addEventListener('input', () => {
}) })
document.getElementById('sonstigesInput').addEventListener('input', () => { document.getElementById('sonstigesInput').addEventListener('input', () => {
checkInput(); checkInput();
}) })
// Button-Status beim Öffnen des Modals einmal anhand der Standardwerte (z.B. Minuten = 0) setzen,
// da sonst ohne ein tatsächliches 'input'-Event der Button trotz gültiger Vorbelegung deaktiviert bleibt
document.getElementById('newEntryModal').addEventListener('show.bs.modal', () => {
checkInput();
})
// Einstellungen wenn Neuer Eintrag erstellt wird // Einstellungen wenn Neuer Eintrag erstellt wird
NewEntryButton.addEventListener('click', () => { NewEntryButton.addEventListener('click', () => {
@ -273,8 +58,8 @@ NewEntryButton.addEventListener('click', () => {
sonstigesText.style.display = 'none'; sonstigesText.style.display = 'none';
} }
// Berechnen der Duration (Stunden sind optional -> ohne Eingabe wird 0 gespeichert) // Berechnen der Duration
const duration = ((parseInt(hours.value) || 0)*60) + parseInt(mins.value) const duration = (parseInt(hours.value)*60) + parseInt(mins.value)
const newEntryData = { const newEntryData = {
date: document.getElementById('datum').value, date: document.getElementById('datum').value,
@ -369,127 +154,6 @@ SETMonthGoalSaveButton.addEventListener('click', () => {
SETMonthGoalSaveButton.classList.add('d-none') SETMonthGoalSaveButton.classList.add('d-none')
}) })
// Lädt die individuelle LDC-Bezeichnung (SettingsID 4) aus der DB, setzt ldcLabel,
// füllt das Einstellungen-Feld vor und wendet den Namen überall im UI an.
async function loadLdcLabel() {
const result = await getAllEntries('settings')
const setting = result.find(element => element.SettingsID === 4)
ldcLabel = (setting == undefined || !setting.SettingsValue) ? 'LDC' : setting.SettingsValue
document.getElementById('ldcNameInput').value = ldcLabel
applyLdcLabel()
}
// Setzt die aktuelle LDC-Bezeichnung überall dort, wo sie im UI (außerhalb der Tabelle) auftaucht.
// Die Tabelle selbst bekommt den Namen bei jedem Render über fillTableWithEntries(...).
function applyLdcLabel() {
document.getElementById('ldcCheckLabel').innerText = ldcLabel
document.getElementById('editldcCheckLabel').innerText = ldcLabel
document.getElementById('monthHourLDCLabel').innerText = ldcLabel
}
SETLdcNameSaveButton.addEventListener('click', () => {
getAllEntries('settings')
.then(function(result) {
const SETDBData = result.find(element => element.SettingsID === 4)
actualSettingsEntryID = SETDBData ? SETDBData.id : undefined;
actualSettings = SETDBData ? SETDBData.SettingsValue : undefined;
// AKTUELLE DATEN AUS FELD ÜBERNHEMEN - bei leerem Feld auf "LDC" zurückfallen
var ActInput = document.getElementById('ldcNameInput').value.trim() || 'LDC'
document.getElementById('ldcNameInput').value = ActInput
const LdcNameSettingsData = {
SettingsID: 4,
SettingsValue: ActInput,
}
// ÜBERPRÜFEN OB EIN EINTRAG MTI DER SETTINGS ID VORHANDEN IST - WENN JA DANN UPDATE WENN NEIN DANN NEUEN ERSTELLEN
if ( actualSettings != undefined ) {
updateEntry(actualSettingsEntryID, 'settings', LdcNameSettingsData)
} else {
addEntry(LdcNameSettingsData, 'settings')
}
ldcLabel = ActInput
applyLdcLabel()
// Tabelle neu füllen, damit die Typ-Spalte den neuen Namen sofort übernimmt
getActualDataAndFillView(cursorMonth, cursorYear)
})
SETLdcNameSaveButton.classList.add('d-none')
})
//
// BERICHT FÜR WHATSAPP TEILEN
//
// Baut den vorformatierten Bericht für den gerade angezeigten Monat (WhatsApp-Formatierung: *fett*)
function buildMonthReportText() {
const monthLabel = `${monate[cursorMonth]} ${cursorYear}`
const dienstHours = document.getElementById('monthHourService').innerText
const bibelstudien = document.getElementById('studysinput').value
const ldcHours = document.getElementById('monthHourLDC').innerText
// Sonstige Einträge nach ihrem individuellen Bemerkungstext gruppieren und Stunden je Gruppe summieren
const sonstigeGroups = {}
currentMonthEntries
.filter(entry => entry.type === 3)
.forEach(entry => {
const label = entry.sondertext || 'Sonstiges'
sonstigeGroups[label] = (sonstigeGroups[label] || 0) + entry.duration
})
var message = `Bericht: *${monthLabel}*\n`
message += `Dienst: ${dienstHours}h\n`
message += `Bibelstudien: ${bibelstudien}\n`
message += `Bemerkungen:\n`
message += `${ldcLabel}: ${ldcHours}h`
Object.keys(sonstigeGroups).forEach(label => {
message += `\n${label}: ${formatDuration(sonstigeGroups[label], true)}h`
})
return message
}
// Kopiert Text in die Zwischenablage - mit Fallback für Umgebungen ohne Clipboard-API
async function copyTextToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
// Bestätigung als Vorschau-Modal statt kurzem Icon-Blitz: so sieht man den Bericht direkt
// (auch praktisch, wenn man ihn gar nicht kopieren, sondern nur ansehen möchte) und kann
// in Ruhe wieder schließen.
shareIcon.addEventListener('click', () => {
const message = buildMonthReportText()
shareReportPreview.innerText = message
copyTextToClipboard(message)
.then(() => {
shareReportStatus.innerText = '✓ In die Zwischenablage kopiert'
shareReportStatus.style.color = 'rgb(120,170,86)'
})
.catch((error) => {
console.error('Bericht konnte nicht in die Zwischenablage kopiert werden:', error)
shareReportStatus.innerText = 'Kopieren nicht möglich - Text unten manuell markieren'
shareReportStatus.style.color = '#dc3545'
})
.finally(() => {
new bootstrap.Modal(shareReportModalEl).show()
})
})
async function getActualDataAndFillView(monat = currentDate.getMonth(), jahr = currentDate.getFullYear()) { async function getActualDataAndFillView(monat = currentDate.getMonth(), jahr = currentDate.getFullYear()) {
try { try {
@ -514,13 +178,9 @@ async function getActualDataAndFillView(monat = currentDate.getMonth(), jahr = c
// Filter funktion aufrufen und entsprechend mit dem aktuellen Datum füllen // Filter funktion aufrufen und entsprechend mit dem aktuellen Datum füllen
const filteredEntries = await filterEntriesByMonthYear(monat === 12 ? 1 : monat + 1, jahr); const filteredEntries = await filterEntriesByMonthYear(monat === 12 ? 1 : monat + 1, jahr);
currentMonthEntries = filteredEntries
console.log("update-complete")
console.log(jahr)
// Tabelle mit den aktuellen informationen füllen (funktion triggern und Daten übergeben) // Tabelle mit den aktuellen informationen füllen (funktion triggern und Daten übergeben)
fillTableWithEntries(filteredEntries, sortField, sortDirection, ldcLabel) fillTableWithEntries(filteredEntries)
// Ausgabe der Daten in den Log für DEV möglich // Ausgabe der Daten in den Log für DEV möglich
// console.log('Gefilterte Einträge für September 2024:', filteredEntries); // console.log('Gefilterte Einträge für September 2024:', filteredEntries);
@ -546,16 +206,16 @@ async function getActualDataAndFillView(monat = currentDate.getMonth(), jahr = c
document.getElementById('monthHourLDC').innerText = formatDuration(sumDurations(filteredEntries,2,true), true) document.getElementById('monthHourLDC').innerText = formatDuration(sumDurations(filteredEntries,2,true), true)
document.getElementById('monthHourOther').innerText = formatDuration(sumDurations(filteredEntries,3,true), true) document.getElementById('monthHourOther').innerText = formatDuration(sumDurations(filteredEntries,3,true), true)
// Jahres Ansicht füllen (Dienstjahr, gekoppelt an den gerade angezeigten Monat/Jahr) // Jahres Ansicht füllen
// Bei aktiver 55h-Regel wird jeder Monat mit LDC/Sonstiges-Anteil auf max. 55h gedeckelt, var requestYear = currentDate.getFullYear()
// reine Dienststunden zählen dabei immer voll (siehe calculateServiceYearTotal). if (currentDate.getMonth() +1 >= 9) {
var requestServiceYear = getServiceYear(new Date(jahr, monat, 1)); requestYear += 1;
var yearEntries = await filterEntriesByServiceYear(requestServiceYear) }
var yearData = formatDuration(calculateServiceYearTotal(yearEntries, rule55Active), true) var yearData = await filterEntriesByMonthYear(null, requestYear)
yearData = formatDuration(sumDurations(yearData, null, true), true)
document.getElementById('yearHours').innerText = yearData document.getElementById('yearHours').innerText = yearData
document.getElementById('yearRest').innerText = 600 - parseInt(yearData) document.getElementById('yearRest').innerText = 600 - parseInt(yearData)
document.getElementById('prBarYear').style.width = `${Math.round((100/600)*parseInt(yearData))}%` document.getElementById('prBarYear').style.width = `${Math.round((100/600)*parseInt(yearData))}%`
updateRule55Badge()
} catch (error) { } catch (error) {
console.error('Fehler bei der Initialisierung oder Filterung:', error); console.error('Fehler bei der Initialisierung oder Filterung:', error);
@ -663,7 +323,7 @@ document.getElementById('editEntrySubmitButton').addEventListener('click', () =>
updateEntry(parseInt(ToEditID), 'hours', { updateEntry(parseInt(ToEditID), 'hours', {
date: document.getElementById('editDatum').value, date: document.getElementById('editDatum').value,
duration: ((parseInt(edithours.value) || 0)*60) + parseInt(editMins.value), duration: (parseInt(edithours.value)*60) + parseInt(editMins.value),
type: type, type: type,
status: 1, status: 1,
sondertext: document.getElementById('editsonstigesInput').value, sondertext: document.getElementById('editsonstigesInput').value,
@ -689,22 +349,16 @@ document.getElementById('DeleteEntryButton').addEventListener('click', () => {
// //
const monthEntrys = document.querySelectorAll('.monthEntrys'); const monthEntrys = document.querySelectorAll('.monthEntrys');
var diffMonth = 0
// Cursor für die Monatsnavigation: echtes Kalenderjahr + 0-basierter Monat (JS-Konvention).
// Startet auf dem aktuellen Monat/Jahr und wird bei Vor/Zurück direkt fortgeschrieben,
// unabhängig vom Dienstjahr-Label - dadurch keine künstliche Sperre mehr bei Sept/Aug.
var cursorMonth = new Date().getMonth();
var cursorYear = new Date().getFullYear();
document.getElementById('dashbord_month_area').addEventListener('click', () =>{ document.getElementById('dashbord_month_area').addEventListener('click', () =>{
if (!document.getElementById('dashbord_month_area').classList.contains('ActiveMonth')) { if (!document.getElementById('dashbord_month_area').classList.contains('ActiveMonth')) {
document.getElementById('dashbord_month_area').classList.replace('closeMonthArea', 'openMonthArea') document.getElementById('dashbord_month_area').classList.replace('closeMonthArea', 'openMonthArea')
document.getElementById('dashbord_month_area').classList.add(['openMonthArea']) document.getElementById('dashbord_month_area').classList.add(['openMonthArea'])
document.getElementById('dashbord_month_area').classList.add(['ActiveMonth']) document.getElementById('dashbord_month_area').classList.add(['ActiveMonth'])
// ActivitysTable ist jetzt flex:1 1 auto (siehe custom.css) und macht der wachsenden document.getElementById('ActivitysTable').classList.add(['shrinkActivitys'])
// Monatskarte automatisch Platz - kein manuelles shrinkActivitys/growActivitys mehr nötig, document.getElementById('ActivitysTable').classList.replace('growActivitys', 'shrinkActivitys')
// das würde der Flex-Berechnung sonst mit einer festen vh-Höhe in die Quere kommen. document.getElementById('dashbord_month_area').classList.remove('mt-4')
document.getElementById('dashbord_month_area').classList.remove('mt-3')
// document.getElementById('dashbord_year_area').classList.add('fade-out') // document.getElementById('dashbord_year_area').classList.add('fade-out')
// Für jedes ausgewählte Element die neue Klasse hinzufügen // Für jedes ausgewählte Element die neue Klasse hinzufügen
setTimeout(function() { setTimeout(function() {
@ -720,37 +374,43 @@ document.getElementById('dashbord_month_area').addEventListener('click', () =>{
} else { } else {
document.getElementById('dashbord_month_area').classList.remove('ActiveMonth') document.getElementById('dashbord_month_area').classList.remove('ActiveMonth')
document.getElementById('dashbord_month_area').classList.replace('openMonthArea', 'closeMonthArea') document.getElementById('dashbord_month_area').classList.replace('openMonthArea', 'closeMonthArea')
document.getElementById('dashbord_month_area').classList.add('mt-3') document.getElementById('ActivitysTable').classList.replace('shrinkActivitys', 'growActivitys')
monthEntrys.forEach(element => { monthEntrys.forEach(element => {
element.classList.replace('fade-in', 'fade-out'); element.classList.replace('fade-in', 'fade-out');
}); });
document.getElementById('MonthNameOfActual').innerHTML = "Monat:" document.getElementById('MonthNameOfActual').innerHTML = "Monat:"
cursorMonth = new Date().getMonth(); // document.getElementById('actualMonthName').innerHTML = monate[new Date().getMonth()];
cursorYear = new Date().getFullYear(); diffMonth = 0;
getActualDataAndFillView(cursorMonth, cursorYear) getActualDataAndFillView(new Date().getMonth() + diffMonth)
} }
}) })
document.getElementById('btn-month-before').addEventListener('click', () =>{ document.getElementById('btn-month-before').addEventListener('click', () =>{
if (cursorMonth === 0) { console.log("CLICK-BEFORE: ", diffMonth)
// Januar -> Dezember des Vorjahres if (new Date().getMonth() + diffMonth == 0) {
cursorMonth = 11; diffMonth = 11 - new Date().getMonth();
cursorYear -= 1; getActualDataAndFillView(new Date().getMonth() + diffMonth)
console.log("CLICK-BEFRORE-0TO11: ", diffMonth)
} else { } else {
cursorMonth -= 1; diffMonth = ((monate[new Date().getMonth() + diffMonth]) != 'September') ? diffMonth - 1 : diffMonth;
} // document.getElementById('actualMonthName').innerHTML = monate[new Date().getMonth() + diffMonth];
getActualDataAndFillView(cursorMonth, cursorYear) getActualDataAndFillView(new Date().getMonth() + diffMonth)
};
console.log("CLICK-BEFORE-AFTER: ", diffMonth)
}); });
document.getElementById('btn-month-next').addEventListener('click', () =>{ document.getElementById('btn-month-next').addEventListener('click', () =>{
if (cursorMonth === 11) { console.log("CLICK-NEXT: ", diffMonth)
// Dezember -> Januar des Folgejahres if (new Date().getMonth() + diffMonth == 11) {
cursorMonth = 0; diffMonth = 0 - new Date().getMonth();
cursorYear += 1; getActualDataAndFillView(new Date().getMonth() + diffMonth)
console.log("CLICK-NEXT-11TO0: ", diffMonth)
} else { } else {
cursorMonth += 1; diffMonth = ((monate[new Date().getMonth() + diffMonth]) != 'August') ? diffMonth + 1 : diffMonth;
} // document.getElementById('actualMonthName').innerHTML = monate[new Date().getMonth() + diffMonth];
getActualDataAndFillView(cursorMonth, cursorYear) getActualDataAndFillView(new Date().getMonth() + diffMonth)
};
console.log("CLICK-NEXT-AFTER: ", diffMonth)
}) })
function setNewCalsOfMonth(TheMonth) { function setNewCalsOfMonth(TheMonth) {
@ -775,7 +435,7 @@ function checkInput() {
if ((sonstigesCheck.checked && sonstigesInput.value !== "") || !(sonstigesCheck.checked)) { if ((sonstigesCheck.checked && sonstigesInput.value !== "") || !(sonstigesCheck.checked)) {
// Debug Ausgabe für Bedingungsstufen // Debug Ausgabe für Bedingungsstufen
// console.log("Erste Bedingung: TRUE") // console.log("Erste Bedingung: TRUE")
if (mins.value) { // Stunden sind optional, Minuten bleiben Pflicht if (hours.value && mins.value) {
// Debug Ausgabe für Bedingungsstufen // Debug Ausgabe für Bedingungsstufen
// console.log("Zweite Bedingung: TRUE") // console.log("Zweite Bedingung: TRUE")
if (!allowToSend) { if (!allowToSend) {
@ -815,7 +475,7 @@ function checkEditInput() {
if ((editsonstigesCheck.checked && editsonstigesInput.value !== "") || !(editsonstigesCheck.checked)) { if ((editsonstigesCheck.checked && editsonstigesInput.value !== "") || !(editsonstigesCheck.checked)) {
// Debug Ausgabe für Bedingungsstufen // Debug Ausgabe für Bedingungsstufen
// console.log("Erste Bedingung: TRUE") // console.log("Erste Bedingung: TRUE")
if (editMins.value) { // Stunden sind optional, Minuten bleiben Pflicht if (edithours.value && editMins.value) {
// Debug Ausgabe für Bedingungsstufen // Debug Ausgabe für Bedingungsstufen
// console.log("Zweite Bedingung: TRUE") // console.log("Zweite Bedingung: TRUE")
if (!editallowToSend) { if (!editallowToSend) {

@ -7,6 +7,7 @@ self.addEventListener("install", event => {
'/static/app/css/bootstrap.css', '/static/app/css/bootstrap.css',
'/static/app/css/custom.css', '/static/app/css/custom.css',
'/static/app/js/dbcontrol.js', '/static/app/js/dbcontrol.js',
'/static/app/js/home.js',
'/static/app/js/timer.js', '/static/app/js/timer.js',
'/static/app/js/welcomeControl.js', '/static/app/js/welcomeControl.js',
'/static/app/js/exportDB.js', '/static/app/js/exportDB.js',

@ -37,7 +37,8 @@ function timerIsReset() {
function timerIsPause() { function timerIsPause() {
document.getElementById('startTimer').classList.remove('d-none') document.getElementById('startTimer').classList.remove('d-none')
document.getElementById('pauseTimer').classList.add('d-none') document.getElementById('pauseTimer').classList.add('d-none')
document.getElementById('timerDisplay').style.color = 'rdb(43,100,110)' document.getElementById('resetTimer').classList.remove('d-none')
document.getElementById('timerDisplay').style.color = 'rgb(43,100,110)'
} }
@ -64,6 +65,8 @@ pauseButton.addEventListener('click', () => {
elapsedTime = Math.floor((Date.now() - savedStartTime) / 1000); // Berechne die vergangene Zeit elapsedTime = Math.floor((Date.now() - savedStartTime) / 1000); // Berechne die vergangene Zeit
} }
localStorage.setItem('elapsedTime', elapsedTime); // Speichere die vergangene Zeit localStorage.setItem('elapsedTime', elapsedTime); // Speichere die vergangene Zeit
// localStorage.removeItem('startTime'); // wichtig, damit beim Reload nicht weitergezählt wird
console.log("Gespeicherte-Vergange Zeit: " + localStorage.getItem('elapsedTime'))
} }
}); });
@ -113,11 +116,24 @@ function startTimer(startTime) {
// Überprüfen, ob ein Timer beim Laden der Seite bereits läuft // Überprüfen, ob ein Timer beim Laden der Seite bereits läuft
window.addEventListener('load', () => { window.addEventListener('load', () => {
const savedElapsedTime = parseInt(localStorage.getItem('elapsedTime')) || 0;
const savedStartTime = localStorage.getItem('startTime'); const savedStartTime = localStorage.getItem('startTime');
elapsedTime = parseInt(localStorage.getItem('elapsedTime')) || 0;
if (savedStartTime && !isRunning) { console.log("Vorher Gespeicherte Zeit vergangen: " + savedElapsedTime)
if (savedElapsedTime > 0) {
// Timer war pausiert → Nicht starten, nur Zeit anzeigen
elapsedTime = savedElapsedTime;
// updateTimerDisplay(elapsedTime);
displayTime(savedElapsedTime)
timerIsPause()
console.log("Vergangene Zeit > 0 && Timer wurde auf Pause gesetzt")
isRunning = false;
} else if (savedStartTime && !isRunning) {
// Timer war aktiv → weiterlaufen lassen
startTimer(parseInt(savedStartTime)); startTimer(parseInt(savedStartTime));
timerIsRunning();
isRunning = true; isRunning = true;
} }
}); });

@ -5,7 +5,6 @@ function setAllOk() {
if (localStorage.getItem('UpdateInfoRead') === document.getElementById('versionnumber').value) { if (localStorage.getItem('UpdateInfoRead') === document.getElementById('versionnumber').value) {
document.getElementById('dashboard').classList.remove('d-none'); document.getElementById('dashboard').classList.remove('d-none');
document.getElementById('refresh_icon').classList.remove('d-none') document.getElementById('refresh_icon').classList.remove('d-none')
document.getElementById('share_icon').classList.remove('d-none')
document.getElementById('settings_icon').classList.remove('d-none') document.getElementById('settings_icon').classList.remove('d-none')
} }
} }

@ -20,23 +20,14 @@
<div class="container-fluid header-bg-block bg-white-color HeaderDefaultHeightbg" id="header-bg-block"> <div class="container-fluid header-bg-block bg-white-color HeaderDefaultHeightbg" id="header-bg-block">
<div class="container-fluid header-color-block text-center bg-white-color HeaderDefaultHeightcolor" id="header-color-block" style="color: white; width: 100vw;"> <div class="container-fluid header-color-block text-center bg-white-color HeaderDefaultHeightcolor" id="header-color-block" style="color: white; width: 100vw;">
<!-- Flexbox statt Bootstrap-Grid: 4 Icons (refresh, share, settings, settings-close) als <div class="row headerline justify-content-around">
col-3 hätten zusammen mit dem Titel die 12 Spalten gesprengt und wären umgebrochen - <span class="col-3 d-none" id="refresh_icon"><img onclick="window.location.reload();" src="{% static 'app/images/refresh.png' %}" alt="Settings" class="img-fluid" id="image" style="width: 40%; opacity: 0.75; padding-top: 0.65rem;"></span>
dadurch wurde der Settings-Button vom overflow:hidden des Headers abgeschnitten.
HeaderText ist absolut mittig zur vollen Breite positioniert, damit "PIOMINT" mittig <h3 id="HeaderText" class="pt-2" style="display: none;"><b>PIO</b><b style="color: rgb(120,170,86);">MINT</b></h3>
bleibt, egal wie breit die Icon-Gruppen links/rechts gerade sind. -->
<div class="d-flex align-items-center headerline" style="position: relative; width: 100%; height: 7vh; flex: 0 0 auto; padding: 0 0.75rem;"> <!-- <div class="col-3"></div> -->
<span class="d-none" id="refresh_icon" style="width: 28px; flex: 0 0 auto;"><img onclick="window.location.reload();" src="{% static 'app/images/refresh.png' %}" alt="Settings" class="img-fluid" id="image" style="width: 100%; opacity: 0.75;"></span> <span class="col-3 d-none" id="settings_icon"><img src="{% static 'app/images/settings.png' %}" alt="Settings" class="img-fluid" id="image" style="width: 40%; opacity: 0.75; padding-top: 0.65rem;"></span>
<span class="col-3 d-none" id="settings_close_icon"><img src="{% static 'app/images/close.png' %}" alt="SettingsClose" class="img-fluid" id="image" style="width: 40%; opacity: 0.75; padding-top: 0.65rem;"></span>
<h3 id="HeaderText" style="display: none; margin: 0; padding: 0; line-height: 1; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);"><b>PIO</b><b style="color: rgb(120,170,86);">MINT</b></h3>
<div class="d-flex align-items-center" style="gap: 1rem; margin-left: auto;">
<span class="d-none" id="share_icon" style="width: 28px; flex: 0 0 auto;" title="Bericht für WhatsApp kopieren">
<img id="share_icon_img" src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='rgb(120,170,86)'><path d='M18,16.08c-0.76,0-1.44,0.3-1.96,0.77L8.91,12.7c0.05-0.23,0.09-0.46,0.09-0.7s-0.04-0.47-0.09-0.7l7.05-4.11c0.54,0.5,1.25,0.81,2.04,0.81c1.66,0,3-1.34,3-3c0-1.66-1.34-3-3-3s-3,1.34-3,3c0,0.24,0.04,0.47,0.09,0.7L8.04,9.81C7.5,9.31,6.79,9,6,9c-1.66,0-3,1.34-3,3s1.34,3,3,3c0.79,0,1.5-0.31,2.04-0.81l7.12,4.16c-0.05,0.21-0.08,0.43-0.08,0.65c0,1.61,1.31,2.92,2.92,2.92c1.61,0,2.92-1.31,2.92-2.92C20.92,17.39,19.61,16.08,18,16.08z'/></svg>" alt="Teilen" class="img-fluid" style="width: 100%; opacity: 0.9;">
</span>
<span class="d-none" id="settings_icon" style="width: 28px; flex: 0 0 auto;"><img src="{% static 'app/images/settings.png' %}" alt="Settings" class="img-fluid" id="image" style="width: 100%; opacity: 0.75;"></span>
<span class="d-none" id="settings_close_icon" style="width: 28px; flex: 0 0 auto;"><img src="{% static 'app/images/close.png' %}" alt="SettingsClose" class="img-fluid" id="image" style="width: 100%; opacity: 0.75;"></span>
</div>
</div> </div>
<div class="settings text-left"> <div class="settings text-left">
@ -53,14 +44,11 @@
<input type="number" class="w-25 btn btn-secondary" name="MonthGoalInput" id="MonthGoalInput" value="50" onfocus="document.getElementById('SETMonthGoalSaveButton').classList.remove('d-none')"> <input type="number" class="w-25 btn btn-secondary" name="MonthGoalInput" id="MonthGoalInput" value="50" onfocus="document.getElementById('SETMonthGoalSaveButton').classList.remove('d-none')">
<button class="btn d-none" id="SETMonthGoalSaveButton">Sichern</button> <button class="btn d-none" id="SETMonthGoalSaveButton">Sichern</button>
</li> </li>
<h5 class="mt-5 text-white text-center">Bericht</h5>
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">
55h-Regel (Jahr): Aktuellen Monat
<input type="checkbox" name="rule55hCheck" id="rule55hCheck" style="position: static; width: 1.5rem; height: 1.5rem; margin: 0;"> <a class="btn btn-secondary w-25" href="https://chat.whatsapp.com/Cy4laGaEdLP1DZbhN04zRQ" target="_blank" rel="noopener noreferrer">Sehen</a>
</li> <a class="btn btn-secondary w-25" href="https://chat.whatsapp.com/Cy4laGaEdLP1DZbhN04zRQ" target="_blank" rel="noopener noreferrer">Copy</a>
<li class="list-group-item d-flex justify-content-between align-items-center">
Schnellwahl-Titel:
<input type="text" class="w-25 btn btn-secondary" name="ldcNameInput" id="ldcNameInput" value="LDC" maxlength="20" onfocus="document.getElementById('SETLdcNameSaveButton').classList.remove('d-none')">
<button class="btn d-none" id="SETLdcNameSaveButton">Sichern</button>
</li> </li>
<h5 class="mt-5 text-white text-center">Kontakt</h5> <h5 class="mt-5 text-white text-center">Kontakt</h5>
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">

@ -10,11 +10,11 @@
document.getElementById('maincontainer').classList.replace('align-items-center', 'align-items-top') document.getElementById('maincontainer').classList.replace('align-items-center', 'align-items-top')
</script> </script>
<div class="container" id="welcome_content" style="padding-bottom: 3rem;"> <div class="container" id="welcome_content">
<br> <br>
<h1>Willkommen! 👋</h1> <h1>Wilkommen!</h1>
<p> <p>
Schön, dass du hier bist! Schön das du hier bist!
<br>Ich wünsche dir viel Freude mit der App. <br>Ich wünsche dir viel Freude mit der App.
<br>Diese ist noch ganz neu und wächst immer weiter! <br>Diese ist noch ganz neu und wächst immer weiter!
<br> <br>
@ -34,59 +34,54 @@
<button class="btn btn-primary" id="allOk">Alles klar</button> <button class="btn btn-primary" id="allOk">Alles klar</button>
</div> </div>
<div class="container mt-5" id="update_content" style="padding-bottom: 3rem;"> <div class="container mt-5" id="update_content">
{% include 'app/updateinfo.html' %} {% include 'app/updateinfo.html' %}
<button class="btn btn-primary" id="allUpdateOk">Alles klar</button> <button class="btn btn-primary" id="allUpdateOk">Alles klar</button>
</div> </div>
<div class="d-none" id="dashboard"> <div class="d-none" id="dashboard">
<div class="row justify-content-around mt-3" style="width: 100vw; color: black; flex-shrink: 0;"> <div class="row justify-content-around mt-3" style="width: 100vw; color: black;">
<div class="col-11" id="dashbord_year_area" style="padding: 0.5rem 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray; display: flex; flex-direction: column; justify-content: center; gap: 0.35rem;"> <div class="col-11" id="dashbord_year_area" style="height: 5vh; padding: 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray">
<!-- flex-shrink:0 auf beiden Kindern: sonst quetscht Flexbox in der knappen Höhe <div class="row justify-content-between">
den 5px-Balken dünner als den (nicht-flex) Balken der Monatskarte. --> <p class="col-6" style="font-size: 1rem; margin-top: -0.75rem;"><b>Jahr:</b> <span id="yearHours"></span>h/600h</p>
<div class="row justify-content-between" style="margin: 0; flex-shrink: 0;"> <p class="offset-1 col-5" style="font-size: 1rem; margin-top: -0.75rem;"><b>Offen:</b> <span id="yearRest"></span>h</p>
<p class="col-6" style="font-size: 1rem; margin: 0; white-space: nowrap;"><b>Jahr:</b> <span id="yearHours"></span>h/600h<span id="rule55Badge" class="badge badge-warning d-none" title="55h-Regel aktiv: Monate mit LDC/Sonstiges werden auf 55h gedeckelt" style="font-size: 0.55rem; vertical-align: middle; margin-left: 0.25rem;">55h</span></p>
<p class="offset-1 col-5" style="font-size: 1rem; margin: 0;"><b>Offen:</b> <span id="yearRest"></span>h</p>
</div> </div>
<div class="progress" style="height: 5px; width: 100%; border-radius: 2rem; background-color: lightgray; margin: 0; flex-shrink: 0;"> <div class="progress" style="height: 5px; width: 100%; border-radius: 2rem; background-color: lightgray; margin-top: -1rem;">
<div class="progress-bar" role="progressbar" style="background-color:rgb(120,170,86); width: 0.5%; height: 5px;" aria-valuenow="10" aria-valuemin="0" aria-valuemax="50" id="prBarYear"></div> <div class="progress-bar" role="progressbar" style="background-color:rgb(120,170,86); width: 0.5%; height: 5px;" aria-valuenow="10" aria-valuemin="0" aria-valuemax="50" id="prBarYear"></div>
</div> </div>
</div> </div>
<div class="col-11 mt-3" id="dashbord_month_area" style="padding: 0.75rem 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray"> <div class="col-11 mt-4" id="dashbord_month_area" style="padding: 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray">
<div class="row justify-content-between" style="margin: 0 0 0.4rem 0;"> <div class="row justify-content-between">
<p class="col-7" style="font-size: 1.25rem; margin: 0;"><b id="MonthNameOfActual">Monat:</b> <span id="monthHours"></span>h</p> <p class="col-7" style="font-size: 1.25rem;"><b id="MonthNameOfActual">Monat:</b> <span id="monthHours"></span>h</p>
<input type="number" class="d-none" id="MonthSumHours"> <input type="number" class="d-none" id="MonthSumHours">
<p class="col-5" style="font-size: 1.25rem; margin: 0;"><b>Offen:</b> <span id="monthRest"></span>h</p> <p class="col-5" style="font-size: 1.25rem;"><b>Offen:</b> <span id="monthRest"></span>h</p>
</div> </div>
<div class="progress" style="height: 5px; width: 100%; border-radius: 2rem; background-color: lightgray; margin: 0;"> <div class="progress" style="height: 5px; width: 100%; border-radius: 2rem; background-color: lightgray; margin-top: -0.75rem">
<div class="progress-bar" role="progressbar" style="background-color:rgb(120,170,86); height: 5px; width: 1%;" aria-valuenow="10" aria-valuemin="0" aria-valuemax="50" id="prBarMonth"></div> <div class="progress-bar" role="progressbar" style="background-color:rgb(120,170,86); height: 5px; width: 1%;" aria-valuenow="10" aria-valuemin="0" aria-valuemax="50" id="prBarMonth"></div>
</div> </div>
<!-- justify-content-around verteilt die Badges wie bisher auf die verfügbare Breite, <div class="row justify-content-around">
solange sie passen; erst wenn der individuell vergebene Schnellwahl-Titel (siehe <button class="col-3 mt-3 badge badge-primary" style="height: 25px; border: none;">Dienst: <span id="monthHourService"></span>h</button>
Einstellungen) zu lang wird, greift overflow-x: auto statt eines Umbruchs. --> <button class="col-3 mt-3 badge badge-primary" style="height: 25px; border: none;">LDC: <span id="monthHourLDC"></span>h</button>
<div class="d-flex justify-content-around" style="overflow-x: auto; gap: 0.5rem; padding-bottom: 2px; margin-top: 0.5rem;"> <button class="col-3 mt-3 badge badge-primary" style="height: 25px; border: none;">Übrig: <span id="monthHourOther"></span>h</button>
<button class="badge badge-primary" style="height: 25px; border: none; white-space: nowrap; flex: 0 0 auto;">Dienst: <span id="monthHourService"></span>h</button>
<button class="badge badge-primary" style="height: 25px; border: none; white-space: nowrap; flex: 0 0 auto;"><span id="monthHourLDCLabel">LDC</span>: <span id="monthHourLDC"></span>h</button>
<button class="badge badge-primary" style="height: 25px; border: none; white-space: nowrap; flex: 0 0 auto;">Übrig: <span id="monthHourOther"></span>h</button>
</div> </div>
<div class="row justify-content-around align-middle monthEntrys" style="max-height: 2.5rem; width: 99%; margin-left: 1%; margin-top: 0.5rem;"> <div class="row justify-content-around align-middle monthEntrys mt-3" style="max-height: 2.5rem; width: 99%; margin-left: 1%;">
<span class="col-2 btn btn-secondary" id="btn-month-before" onclick="event.stopPropagation();"><</span> <span class="col-2 btn btn-secondary" id="btn-month-before" onclick="event.stopPropagation();"><</span>
<p class="col-6" style="text-align:center; width: 100%; font-size: 1.75rem; margin: 0;"><b id="actualMonthName"></b></p> <p class="col-6" style="text-align:center; width: 100%; font-size: 1.75rem; margin: 0;"><b id="actualMonthName"></b></p>
<span class="col-2 btn btn-secondary" id="btn-month-next" onclick="event.stopPropagation();">></span> <span class="col-2 btn btn-secondary" id="btn-month-next" onclick="event.stopPropagation();">></span>
</div> </div>
</div> </div>
</div> </div>
<div class="row justify-content-around" id="timerCard" style="width: 100vw; color: black; padding-top: 1.5rem; flex-shrink: 0;"> <div class="row justify-content-around" style="width: 100vw; color: black; padding-top: 2rem;">
<div class="col-11" style="padding: 0.75rem 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray; display: flex; flex-direction: column; justify-content: center; gap: 0.35rem;"> <div class="col-11" style="height: 15vh; padding: 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray">
<p style="font-size: 1.2rem; margin: 0; flex-shrink: 0;"><b>Timer</b></p> <p style="font-size: 1.2rem;"><b>Timer</b></p>
<div class="row justify-content-between" style="margin: 0; flex-shrink: 0;"> <div class="row justify-content-between" style="margin-top: -1rem;">
<p class="col-5"><b id="timerDisplay" style="font-size: 2.5rem;">00:00</b><i id="secondsDisplay" style="font-size: 1rem;">:00</i></p> <p class="col-5"><b id="timerDisplay" style="font-size: 2.5rem;">00:00</b><i id="secondsDisplay" style="font-size: 1rem;">:00</i></p>
<div class="col-7 row justify-content-center"> <div class="col-7 row justify-content-center">
<img id="startTimer" src="{% static 'app/images/play.png' %}" alt="play" class="img-fluid col-6" style="height: 55px;"> <img id="startTimer" src="{% static 'app/images/play.png' %}" alt="play" class="img-fluid col-6" style="height: 55px;">
@ -96,39 +91,19 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 mt-3" id="ActivitysTable" style="flex: 1 1 auto; min-height: 220px; padding: 0rem 1rem 1rem 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray; overflow-y: auto;"> <div class="row justify-content-around" style="width: 95vw; color: black; padding-top: 2rem;"></div>
<div style="position: sticky; top: 0; background-color: white; padding-top: 1rem; padding-bottom: 0.5rem; z-index: 100; width: 100%;"> <div class="col-12" id="ActivitysTable" style="height: 40vh; padding: 0rem 1rem 1rem 1rem; border-radius:1rem; box-shadow: 0 0 0.5rem gray; overflow-y: auto;">
<div class="d-flex justify-content-between align-items-center" style="width: 100%;"> <div class="row justify-content-between align-items-center m-0" style="width: 100%;position: sticky; top: 0; background-color: white; padding-top: 1rem; z-index: 100;">
<p style="font-size: 1.2rem; margin: 0;" ><b>Aktivitäten</b></p> <p class="col-5" style="font-size: 1.2rem; margin: 0 1rem 0 -1rem;" ><b>Aktivitäten</b></p>
<div class="d-flex align-items-center" style="gap: 1rem;"> <!-- <span class="col-2" style="margin-left: 32%; padding: 0;"><img id="resetTimer" src="{% static 'app/images/addcircle.png' %}" alt="add" class="img-fluid" style="height: 35px"></span> -->
<button type="button" id="openNewEntryForm" class="btn btn-light" data-bs-toggle="modal" data-bs-target="#newEntryModal" style="width: 28px; height: 28px; padding: 0;"><img id="openNewEntryFormIcon" src="{% static 'app/images/addcircle.png' %}" alt="add" class="img-fluid" style="width: 100%;"></button> <button type="button" id="openNewEntryForm" class="offset-2 col-2 btn btn-light" data-bs-toggle="modal" data-bs-target="#newEntryModal"><img id="openNewEntryFormIcon" src="{% static 'app/images/addcircle.png' %}" alt="add" class="img-fluid"></button>
<button type="button" id="toggleSortOptions" class="btn btn-light" title="Filter/Sortierung ein-/ausblenden" style="width: 28px; height: 28px; padding: 0;"><img id="toggleSortOptionsIcon" src="{% static 'app/images/more.png' %}" alt="filter" class="img-fluid" style="width: 100%;"></button> <button type="button" id="openViewOfEntry" class="offset-1 col-2 btn btn-light d-none"><img id="openViewOfEntryIcon" src="{% static 'app/images/open.png' %}" alt="open" class="img-fluid"></button>
<button type="button" id="openViewOfEntry" class="btn btn-light" title="Tabelle maximieren" style="width: 28px; height: 28px; padding: 0;"><img id="openViewOfEntryIcon" src="{% static 'app/images/open.png' %}" alt="open" class="img-fluid" style="width: 100%;"></button>
</div>
</div>
<div class="d-flex align-items-center sortOptionsCollapsed" id="sortOptionsRow" style="width: 100%; margin-top: 0.5rem; gap: 0.5rem;">
<!-- Eigener Dropdown statt <select>, damit das Auswahlmenü nicht als winziges natives
Popup irgendwo auf dem Bildschirm erscheint, sondern direkt unter dem Button. -->
<div style="position: relative; flex: 1;">
<button type="button" id="sortFieldButton" class="btn btn-light btn-sm" style="font-size: 0.85rem; width: 100%; text-align: left;">
<span id="sortFieldLabel">Datum</span>
</button>
<div id="sortFieldMenu" class="d-none" style="position: absolute; top: 100%; left: 0; right: 0; margin-top: 0.25rem; background-color: white; border-radius: 0.5rem; box-shadow: 0 0 0.5rem gray; z-index: 200; overflow: hidden;">
<div class="sortFieldOption" data-value="date" data-label="Datum" style="padding: 0.5rem 0.75rem; font-size: 0.85rem;">Datum</div>
<div class="sortFieldOption" data-value="duration" data-label="Dauer" style="padding: 0.5rem 0.75rem; font-size: 0.85rem;">Dauer</div>
<div class="sortFieldOption" data-value="type" data-label="Typ" style="padding: 0.5rem 0.75rem; font-size: 0.85rem;">Typ</div>
</div>
</div>
<button type="button" id="sortDirectionToggle" class="btn btn-light btn-sm" style="font-size: 0.85rem; flex: 1;" title="Sortierrichtung umkehren">
<span id="sortDirectionIcon">A → Z</span>
</button>
</div>
</div> </div>
<table class="table table-striped" style="z-index: 1;"> <table class="table table-striped" style="z-index: 1;">
<tbody id="entriesTableBody" style="overflow-y: auto; max-height: 70%;"></tbody> <tbody id="entriesTableBody" style="overflow-y: auto; max-height: 70%;"></tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
@ -173,7 +148,7 @@
<div class="form-group col-7"> <div class="form-group col-7">
<p><b>Dauer</b></p> <p><b>Dauer</b></p>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" id="stunden" placeholder="00" min="0" max="99" maxlength="2"> <input type="number" class="form-control" id="stunden" placeholder="00" min="0" max="99" maxlength="2" required>
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text">h</span> <span class="input-group-text">h</span>
</div> </div>
@ -197,7 +172,7 @@
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="ldcCheck" name="optionen" value="LDC"> <input class="form-check-input" type="checkbox" id="ldcCheck" name="optionen" value="LDC">
<label class="form-check-label" for="ldcCheck" id="ldcCheckLabel">LDC</label> <label class="form-check-label" for="ldcCheck">LDC</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="sonstigesCheck" name="optionen" value="Sonstiges"> <input class="form-check-input" type="checkbox" id="sonstigesCheck" name="optionen" value="Sonstiges">
@ -240,7 +215,7 @@
<div class="form-group col-7"> <div class="form-group col-7">
<p><b>Dauer</b></p> <p><b>Dauer</b></p>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control editItem" id="editStunden" placeholder="00" min="0" max="99" maxlength="2"> <input type="number" class="form-control editItem" id="editStunden" placeholder="00" min="0" max="99" maxlength="2" required>
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text">h</span> <span class="input-group-text">h</span>
</div> </div>
@ -264,7 +239,7 @@
<div> <div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input editItem" type="checkbox" id="editldcCheck" name="editldcCheck" value="LDC"> <input class="form-check-input editItem" type="checkbox" id="editldcCheck" name="editldcCheck" value="LDC">
<label class="form-check-label" for="editldcCheck" id="editldcCheckLabel">LDC</label> <label class="form-check-label" for="editldcCheck">LDC</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<input class="form-check-input editItem" type="checkbox" id="editsonstigesCheck" name="editsonstigesCheck" value="Sonstiges"> <input class="form-check-input editItem" type="checkbox" id="editsonstigesCheck" name="editsonstigesCheck" value="Sonstiges">
@ -288,22 +263,6 @@
</div> </div>
</div> </div>
<!-- BERICHT TEILEN: Vorschau des kopierten Textes, auch als reine Übersicht ohne Kopieren nutzbar -->
<div class="modal fade" id="shareReportModal" tabindex="-1" aria-labelledby="shareReportModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="shareReportModalLabel">Bericht</h5>
<button type="button" class="btn btn-sm btn-danger" data-bs-dismiss="modal" aria-label="Schließen">X</button>
</div>
<div class="modal-body">
<p id="shareReportStatus" class="mb-2" style="font-weight: bold;"></p>
<pre id="shareReportPreview" style="white-space: pre-wrap; font-family: inherit; margin: 0; background-color: #f8f9fa; padding: 0.75rem; border-radius: 0.5rem;"></pre>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
@ -320,7 +279,6 @@
document.getElementById('header-color-block').classList.replace('HeaderDefaultHeightcolor', 'openSettingscolor') document.getElementById('header-color-block').classList.replace('HeaderDefaultHeightcolor', 'openSettingscolor')
document.getElementById('header-color-block').classList.replace('closeSettingscolor', 'openSettingscolor') document.getElementById('header-color-block').classList.replace('closeSettingscolor', 'openSettingscolor')
document.getElementById('refresh_icon').style.opacity = 0; document.getElementById('refresh_icon').style.opacity = 0;
document.getElementById('share_icon').style.opacity = 0;
document.getElementById('settings_icon').classList.add('d-none'); document.getElementById('settings_icon').classList.add('d-none');
document.getElementById('settings_close_icon').classList.remove('d-none'); document.getElementById('settings_close_icon').classList.remove('d-none');
}) })
@ -330,11 +288,10 @@
document.getElementById('settings_icon').classList.remove('d-none'); document.getElementById('settings_icon').classList.remove('d-none');
document.getElementById('settings_close_icon').classList.add('d-none'); document.getElementById('settings_close_icon').classList.add('d-none');
document.getElementById('refresh_icon').style.opacity = 1; document.getElementById('refresh_icon').style.opacity = 1;
document.getElementById('share_icon').style.opacity = 1;
}) })
</script> </script>
<script src="{% static 'app/js/home01.js' %}"></script> <script src="{% static 'app/js/home.js' %}"></script>
<script src="{% static 'app/js/timer.js' %}"></script> <script src="{% static 'app/js/timer.js' %}"></script>
<script src="{% static 'app/js/welcomeControl.js' %}"></script> <script src="{% static 'app/js/welcomeControl.js' %}"></script>
<script src="{% static 'app/js/UpdateInfoControl.js' %}"></script> <script src="{% static 'app/js/UpdateInfoControl.js' %}"></script>

@ -1,36 +1,22 @@
<br> <br>
<div class="text-center"> <h1>Update Time!</h1>
<span class="wow-emoji" style="font-size: 3rem;">🎉</span> <br>
<h1 style="margin-bottom: 0;">Großes Update <span style="color: rgb(120,170,86);">26.0</span></h1> <h5>v1.3.2-2</h5>
<h5 class="text-muted">ist da!</h5> <p>
</div> <b style="color: red">Fehler Behebung! 🔥</b>
<br> <br>Monatswechsel funktioniert!
<p> <br>
Hey! 👋 <br><b>Was ist neu?</b>
<br>Dieses Update ist eines der größten bisher ich habe richtig viel Zeit reingesteckt, <br> Ich habe einen tief sitzenden Denkfehler behoben,
damit sich PioMint für dich noch besser anfühlt. Ich hoffe, es macht dir genauso viel <br> so das nun das nicht wieder auftreten sollte.
Freude wie mir beim Bauen. 💚 <br>
</p> <!-- <br><b>Hinweise:</b>
<br> 1. Backup muss Manuell gemacht werden (aktuell)!
<p><b>✨ Was ist neu?</b></p> <br> 2. Backup muss Gespeichert werden! */}}
<ul style="padding-left: 1.25rem;"> <br> -->
<li>📅 <b>Dienstjahr-Navigation komplett überarbeitet</b> blättere jetzt beliebig weit vor <br>Danke für die Geduld und das du Piomint nutzt!
&amp; zurück, auch über den September hinweg. Kein Ärger mehr im Herbst!</li> <br><b>Gruß Samuel</b>
<li>⏱️ <b>Neu: 55h-Regel</b> (optional in den Einstellungen aktivierbar) deine
Jahresübersicht rechnet jetzt automatisch korrekt, sobald LDC/Sonstiges dazukommt.</li>
<li>🔍 <b>Sortierung &amp; Filter</b> für deine Aktivitäten-Tabelle nach Datum, Dauer oder
Typ, auf- oder absteigend. Deine Wahl wird gemerkt.</li>
<li>📈 <b>Tabelle maximieren</b> mit einem Klick mehr Einträge auf einen Blick.</li>
<li>✏️ <b>Schnellwahl-Titel frei benennbar</b> "LDC" heißt jetzt so, wie du möchtest.</li>
<li>📤 <b>Neu: Teilen-Button!</b> Ein Klick, und dein Monatsbericht steht fertig formatiert
für WhatsApp in deiner Zwischenablage perfekt für den nächsten Bericht.</li>
<li>🐛 Dazu jede Menge kleiner Verbesserungen &amp; Bugfixes unter der Haube.</li>
</ul>
<p> </p>
Danke, dass du PioMint nutzt und mir dein Feedback gibst genau das macht die App
besser! 🙏
<br><b>Gruß Samuel</b>
</p>
<br> <br>
<input type="text" name="versionnumber" id="versionnumber" class="d-none" value="26.0"> <input type="text" name="versionnumber" id="versionnumber" class="d-none" value="1.3.2_2">

@ -10,13 +10,6 @@ urlpatterns = [
path("home/", views.home), path("home/", views.home),
path('offline/', TemplateView.as_view(template_name="offline.html")), path('offline/', TemplateView.as_view(template_name="offline.html")),
path("install/", views.install), path("install/", views.install),
path("v1321/", views.v1321), path("v1321/", views.v1322),
path("v1322/", views.v1322), path("v1322/", views.v1322),
path("v133/", views.v133),
path("v1332/", views.v1332),
path("v1333/", views.v1333),
path("v1334/", views.v1334),
path("v1335/", views.v1335),
path("v1340/", views.v1340),
path("v260/", views.v260),
] ]

@ -8,34 +8,13 @@ def install(request):
return render(request, "app/install.html") return render(request, "app/install.html")
def home(request): def home(request):
return redirect("/app/v260") return redirect("/app/v1322")
def login(request): def login(request):
return render(request, "app/login.html") return render(request, "app/login.html")
def v1321(request): def v1321(request):
return redirect("/app/v260") return redirect("/app/v1322")
def v1322(request): def v1322(request):
return redirect("/app/v260") return render(request, "app/home.html")
def v133(request):
return redirect("/app/v260")
def v1332(request):
return redirect("/app/v260")
def v1333(request):
return redirect("/app/v260")
def v1334(request):
return redirect("/app/v260")
def v1335(request):
return redirect("/app/v260")
def v1340(request):
return redirect("/app/v260")
def v260(request):
return render(request, "app/home.html")

@ -9,12 +9,13 @@ PWA_APP_BACKGROUND_COLOR = '#9FEDD7'
PWA_APP_DISPLAY = 'standalone' PWA_APP_DISPLAY = 'standalone'
PWA_APP_SCOPE = '/' PWA_APP_SCOPE = '/'
PWA_APP_ORIENTATION = 'portrait' PWA_APP_ORIENTATION = 'portrait'
PWA_APP_START_URL = '/app/v260/' PWA_APP_START_URL = '/app/v1322/'
PWA_APP_STATUS_BAR_COLOR = 'black-translucent' PWA_APP_STATUS_BAR_COLOR = 'black-translucent'
PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'app/static/app/js', 'serviceworker.js') PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'app/static/app/js', 'serviceworker.js')
PWA_APP_ICONS = [ PWA_APP_ICONS = [
{ {
'src': '/static/app/images/appleicon_whitebg.png', 'src': '/static/app/images/appleicon_whitebg.png',
'sizes': '160x160' 'sizes': '160x160'
} }
] ]
@ -31,4 +32,4 @@ PWA_APP_SPLASH_SCREEN = [
} }
] ]
PWA_APP_DIR = 'ltr' PWA_APP_DIR = 'ltr'
PWA_APP_LANG = 'de' PWA_APP_LANG = 'de'

@ -27,7 +27,7 @@ SECRET_KEY = 'django-insecure-h^$jdg0n_yarg*l#r3fd_7=7_)-!0bspac70f$1#b7w8gmkc-*
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['*'] if DEBUG else ['localhost', 'app.piomint.de'] ALLOWED_HOSTS = ['*'] if DEBUG else ['localhost']
# Application definition # Application definition

@ -1,5 +1,5 @@
asgiref==3.8.1 asgiref==3.8.1
Django==5.2.17 Django==5.1.1
django-pwa==2.0.0 django-pwa==2.0.0
gunicorn==23.0.0 gunicorn==23.0.0
mysqlclient==2.2.4 mysqlclient==2.2.4

Loading…
Cancel
Save

Powered by TurnKey Linux.