Add settings management with Bundesland selection and holiday calculations
This commit is contained in:
100
README.md
100
README.md
@@ -4,14 +4,58 @@ Eine Full-Stack-Zeiterfassungsanwendung, entwickelt mit Node.js, Express, SQLite
|
|||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
- ✅ Erfassung von Arbeitszeiten (Datum, Startzeit, Endzeit)
|
### Zeiterfassung
|
||||||
- ✅ Automatische Pausenberechnung nach deutschem Arbeitszeitgesetz
|
- ✅ **Start/Stop Timer**: Live-Timer mit automatischer Zeiterfassung für den aktuellen Tag
|
||||||
- ✅ Maximum von 10 Stunden Nettoarbeitszeit
|
- Pause nach 6 Stunden (30 Min) oder 9 Stunden (45 Min) gemäß deutschem Arbeitszeitgesetz
|
||||||
- ✅ Filterung nach Zeitraum
|
- Automatische Rundung auf 15-Minuten-Intervalle
|
||||||
- ✅ CSV-Export mit deutscher Formatierung
|
- Timer läuft auch nach Seiten-Reload weiter
|
||||||
- ✅ Responsive Benutzeroberfläche mit Tailwind CSS
|
- ✅ **Manuelle Eingabe**: Erfassung von Arbeitszeiten (Datum, Startzeit, Endzeit, Pause)
|
||||||
- ✅ Moderner Datums-/Zeitauswahl (Flatpickr)
|
- ✅ **Inline-Bearbeitung**: Schnelle Änderung von Zeiten durch Klick in die Tabelle
|
||||||
- ✅ Docker-Containerisierung
|
- ✅ **Standort-Tracking**: Home-Office oder Büro pro Eintrag
|
||||||
|
|
||||||
|
### Intelligente Berechnungen
|
||||||
|
- ✅ **Automatische Pausenberechnung** nach deutschem Arbeitszeitgesetz:
|
||||||
|
- \> 6 Stunden: 30 Minuten Pause
|
||||||
|
- \> 9 Stunden: 45 Minuten Pause
|
||||||
|
- ✅ **Maximum von 10 Stunden Nettoarbeitszeit** pro Tag
|
||||||
|
- ✅ **Monatliche Statistiken**: Soll-Stunden, Ist-Stunden, Saldo (Monat + Gesamt)
|
||||||
|
- ✅ **Arbeitstage-Berechnung**: Automatische Erkennung von Wochenenden und Feiertagen
|
||||||
|
|
||||||
|
### Bundesland-spezifische Feiertage
|
||||||
|
- ✅ **16 Bundesländer**: Auswahl des Bundeslandes für korrekte Feiertagsberechnung
|
||||||
|
- ✅ **Persistente Einstellung**: Bundesland-Auswahl wird gespeichert
|
||||||
|
- ✅ **Kollisionserkennung**: Warnung bei Feiertagen, die mit bestehenden Einträgen kollidieren
|
||||||
|
- ✅ **Alle regionalen Feiertage**: Heilige Drei Könige, Fronleichnam, Reformationstag, etc.
|
||||||
|
|
||||||
|
### Monatsansicht & Navigation
|
||||||
|
- ✅ **Monatskalender**: Vollständige Ansicht aller Tage des Monats
|
||||||
|
- ✅ **Farbcodierung**:
|
||||||
|
- Grün: Home-Office Tage
|
||||||
|
- Rot: Fehlende Einträge an Arbeitstagen
|
||||||
|
- Grau: Wochenenden
|
||||||
|
- Blau: Feiertage mit Namen
|
||||||
|
- ✅ **Vor/Zurück Navigation**: Einfaches Wechseln zwischen Monaten
|
||||||
|
- ✅ **Auto-Fill Funktion**: Automatisches Ausfüllen des gesamten Monats mit Standard-Arbeitszeiten
|
||||||
|
|
||||||
|
### Bulk-Operationen
|
||||||
|
- ✅ **Mehrfachauswahl**: Checkbox-Modus für schnelle Massenbearbeitung
|
||||||
|
- ✅ **Bulk-Standort setzen**: Mehrere Einträge auf einmal auf Home/Büro setzen
|
||||||
|
- ✅ **Bulk-Löschen**: Mehrere Einträge auf einmal löschen
|
||||||
|
|
||||||
|
### Filter & Export
|
||||||
|
- ✅ **Zeitraum-Filter**: Filterung nach Datum (Von/Bis)
|
||||||
|
- ✅ **CSV-Export (Alle)**: Export aller Einträge im gewählten Zeitraum
|
||||||
|
- ✅ **CSV-Export (Abweichungen)**: Export nur der Tage mit Abweichungen von 8,0 Stunden
|
||||||
|
- Ideal für Arbeitszeitnachweise bei Gleitzeit
|
||||||
|
- Zeigt nur relevante Über-/Unterschreitungen
|
||||||
|
- ✅ **Deutsches Format**: Komma als Dezimaltrennzeichen, DD.MM.YYYY Datumsformat
|
||||||
|
|
||||||
|
### Benutzerfreundlichkeit
|
||||||
|
- ✅ **Responsive Design**: Optimiert für Desktop, Tablet und Smartphone
|
||||||
|
- ✅ **Dark Mode**: Modernes dunkles Design für augenschonende Arbeit
|
||||||
|
- ✅ **Toast-Benachrichtigungen**: Visuelles Feedback bei Aktionen
|
||||||
|
- ✅ **Flatpickr**: Moderne Datums- und Zeitauswahl mit Touch-Support
|
||||||
|
- ✅ **Persistente Daten**: SQLite-Datenbank mit automatischer Migration
|
||||||
|
|
||||||
## Technologie-Stack
|
## Technologie-Stack
|
||||||
|
|
||||||
@@ -61,11 +105,20 @@ Die Anwendung berechnet automatisch die Pausenzeiten gemäß deutschem Arbeitsze
|
|||||||
|
|
||||||
## API-Endpunkte
|
## API-Endpunkte
|
||||||
|
|
||||||
|
### Zeiteinträge
|
||||||
- `GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD` - Alle Einträge im Zeitraum abrufen
|
- `GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD` - Alle Einträge im Zeitraum abrufen
|
||||||
- `POST /api/entries` - Neuen Eintrag erstellen
|
- `POST /api/entries` - Neuen Eintrag erstellen
|
||||||
- `PUT /api/entries/:id` - Bestehenden Eintrag aktualisieren
|
- `PUT /api/entries/:id` - Bestehenden Eintrag aktualisieren
|
||||||
- `DELETE /api/entries/:id` - Eintrag löschen
|
- `DELETE /api/entries/:id` - Eintrag löschen
|
||||||
- `GET /api/export?from=YYYY-MM-DD&to=YYYY-MM-DD` - Einträge als CSV exportieren
|
|
||||||
|
### Export
|
||||||
|
- `GET /api/export?from=YYYY-MM-DD&to=YYYY-MM-DD` - Alle Einträge als CSV exportieren
|
||||||
|
- `GET /api/export-deviations?from=YYYY-MM-DD&to=YYYY-MM-DD` - Nur Abweichungen als CSV exportieren
|
||||||
|
|
||||||
|
### Einstellungen
|
||||||
|
- `GET /api/settings/:key` - Einstellung abrufen
|
||||||
|
- `POST /api/settings` - Einstellung speichern (key, value)
|
||||||
|
- `GET /api/settings` - Alle Einstellungen abrufen
|
||||||
|
|
||||||
## Installation & Ausführung
|
## Installation & Ausführung
|
||||||
|
|
||||||
@@ -144,13 +197,32 @@ http://localhost:3000
|
|||||||
|
|
||||||
## CSV-Export-Format
|
## CSV-Export-Format
|
||||||
|
|
||||||
Die exportierte CSV-Datei enthält folgende Spalten:
|
Die Anwendung bietet zwei Export-Optionen:
|
||||||
- **Datum**: Datum im Format TT.MM.JJJJ
|
|
||||||
- **Startzeit**: Startzeit im Format HH:MM
|
### 1. Vollständiger Export (📥 Button)
|
||||||
- **Endzeit**: Endzeit im Format HH:MM
|
Exportiert alle Einträge im gewählten Zeitraum (oder alle, wenn kein Filter gesetzt).
|
||||||
- **Pause in Minuten**: Pausenzeit in Minuten
|
|
||||||
|
### 2. Export nur Abweichungen (⚠️ Button)
|
||||||
|
Exportiert **nur** Tage, die von der Standard-Arbeitszeit (8,0 Stunden) abweichen.
|
||||||
|
- **Zweck**: Ideal für Arbeitszeitnachweise bei Gleitzeit-Modellen
|
||||||
|
- **Inhalt**: Nur Über- und Unterschreitungen der 8-Stunden-Marke
|
||||||
|
- **Vorteil**: Übersichtlicher Nachweis für HR/Verwaltung ohne irrelevante Standard-Tage
|
||||||
|
|
||||||
|
### CSV-Spalten:
|
||||||
|
- **Datum**: TT.MM.JJJJ (z.B. 23.10.2025)
|
||||||
|
- **Startzeit**: HH:MM (z.B. 08:00)
|
||||||
|
- **Endzeit**: HH:MM (z.B. 17:00)
|
||||||
|
- **Pause in Minuten**: Ganzzahl (z.B. 30)
|
||||||
- **Gesamtstunden**: Nettostunden mit Komma als Dezimaltrennzeichen (z.B. 8,50)
|
- **Gesamtstunden**: Nettostunden mit Komma als Dezimaltrennzeichen (z.B. 8,50)
|
||||||
|
|
||||||
|
**Beispiel Abweichungs-Export:**
|
||||||
|
```csv
|
||||||
|
Datum,Startzeit,Endzeit,Pause in Minuten,Gesamtstunden
|
||||||
|
21.10.2025,08:00,18:30,45,9,75
|
||||||
|
22.10.2025,09:00,15:30,30,6,00
|
||||||
|
```
|
||||||
|
(Tage mit exakt 8,0h werden nicht exportiert)
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
Die Anwendung verwendet:
|
Die Anwendung verwendet:
|
||||||
|
|||||||
@@ -6,3 +6,9 @@ CREATE TABLE IF NOT EXISTS entries (
|
|||||||
pause_minutes INTEGER NOT NULL DEFAULT 0,
|
pause_minutes INTEGER NOT NULL DEFAULT 0,
|
||||||
location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home'))
|
location TEXT DEFAULT 'office' CHECK(location IN ('office', 'home'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|||||||
226
public/app.js
226
public/app.js
@@ -24,6 +24,9 @@ let displayMonth = new Date().getMonth(); // 0-11
|
|||||||
let bulkEditMode = false;
|
let bulkEditMode = false;
|
||||||
let selectedEntries = new Set();
|
let selectedEntries = new Set();
|
||||||
|
|
||||||
|
// Settings state
|
||||||
|
let currentBundesland = 'BW'; // Default: Baden-Württemberg
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// UTILITY FUNCTIONS
|
// UTILITY FUNCTIONS
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -169,58 +172,103 @@ function getEasterSunday(year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all public holidays for Baden-Württemberg for a given year
|
* Get all public holidays for a given year and Bundesland
|
||||||
*/
|
*/
|
||||||
function getPublicHolidays(year) {
|
function getPublicHolidays(year, bundesland = currentBundesland) {
|
||||||
const holidays = [];
|
const holidays = [];
|
||||||
|
|
||||||
// Fixed holidays
|
// Fixed holidays (all states)
|
||||||
holidays.push({ date: new Date(year, 0, 1), name: 'Neujahr' });
|
holidays.push({ date: new Date(year, 0, 1), name: 'Neujahr' });
|
||||||
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
|
|
||||||
holidays.push({ date: new Date(year, 4, 1), name: 'Tag der Arbeit' });
|
holidays.push({ date: new Date(year, 4, 1), name: 'Tag der Arbeit' });
|
||||||
holidays.push({ date: new Date(year, 9, 3), name: 'Tag der Deutschen Einheit' });
|
holidays.push({ date: new Date(year, 9, 3), name: 'Tag der Deutschen Einheit' });
|
||||||
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
|
|
||||||
holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' });
|
holidays.push({ date: new Date(year, 11, 25), name: '1. Weihnachtstag' });
|
||||||
holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' });
|
holidays.push({ date: new Date(year, 11, 26), name: '2. Weihnachtstag' });
|
||||||
|
|
||||||
|
// Heilige Drei Könige (BW, BY, ST)
|
||||||
|
if (['BW', 'BY', 'ST'].includes(bundesland)) {
|
||||||
|
holidays.push({ date: new Date(year, 0, 6), name: 'Heilige Drei Könige' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internationaler Frauentag (BE, MV since 2023)
|
||||||
|
if (['BE'].includes(bundesland) || (bundesland === 'MV' && year >= 2023)) {
|
||||||
|
holidays.push({ date: new Date(year, 2, 8), name: 'Internationaler Frauentag' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weltkindertag (TH since 2019)
|
||||||
|
if (bundesland === 'TH' && year >= 2019) {
|
||||||
|
holidays.push({ date: new Date(year, 8, 20), name: 'Weltkindertag' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reformationstag (BB, MV, SN, ST, TH, + HB, HH, NI, SH since 2018)
|
||||||
|
const reformationstagStates = ['BB', 'MV', 'SN', 'ST', 'TH'];
|
||||||
|
if (year >= 2018) {
|
||||||
|
reformationstagStates.push('HB', 'HH', 'NI', 'SH');
|
||||||
|
}
|
||||||
|
if (reformationstagStates.includes(bundesland)) {
|
||||||
|
holidays.push({ date: new Date(year, 9, 31), name: 'Reformationstag' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allerheiligen (BW, BY, NW, RP, SL)
|
||||||
|
if (['BW', 'BY', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||||
|
holidays.push({ date: new Date(year, 10, 1), name: 'Allerheiligen' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buß- und Bettag (only SN)
|
||||||
|
if (bundesland === 'SN') {
|
||||||
|
// Buß- und Bettag is the Wednesday before November 23
|
||||||
|
let bussbettag = new Date(year, 10, 23);
|
||||||
|
while (bussbettag.getDay() !== 3) { // 3 = Wednesday
|
||||||
|
bussbettag.setDate(bussbettag.getDate() - 1);
|
||||||
|
}
|
||||||
|
bussbettag.setDate(bussbettag.getDate() - 7); // One week before
|
||||||
|
holidays.push({ date: bussbettag, name: 'Buß- und Bettag' });
|
||||||
|
}
|
||||||
|
|
||||||
// Easter-dependent holidays
|
// Easter-dependent holidays
|
||||||
const easter = getEasterSunday(year);
|
const easter = getEasterSunday(year);
|
||||||
|
|
||||||
// Karfreitag (Good Friday) - 2 days before Easter
|
// Karfreitag (Good Friday) - 2 days before Easter (all states)
|
||||||
const goodFriday = new Date(easter);
|
const goodFriday = new Date(easter);
|
||||||
goodFriday.setDate(easter.getDate() - 2);
|
goodFriday.setDate(easter.getDate() - 2);
|
||||||
holidays.push({ date: goodFriday, name: 'Karfreitag' });
|
holidays.push({ date: goodFriday, name: 'Karfreitag' });
|
||||||
|
|
||||||
// Ostermontag (Easter Monday) - 1 day after Easter
|
// Ostermontag (Easter Monday) - 1 day after Easter (all states)
|
||||||
const easterMonday = new Date(easter);
|
const easterMonday = new Date(easter);
|
||||||
easterMonday.setDate(easter.getDate() + 1);
|
easterMonday.setDate(easter.getDate() + 1);
|
||||||
holidays.push({ date: easterMonday, name: 'Ostermontag' });
|
holidays.push({ date: easterMonday, name: 'Ostermontag' });
|
||||||
|
|
||||||
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter
|
// Christi Himmelfahrt (Ascension Day) - 39 days after Easter (all states)
|
||||||
const ascension = new Date(easter);
|
const ascension = new Date(easter);
|
||||||
ascension.setDate(easter.getDate() + 39);
|
ascension.setDate(easter.getDate() + 39);
|
||||||
holidays.push({ date: ascension, name: 'Christi Himmelfahrt' });
|
holidays.push({ date: ascension, name: 'Christi Himmelfahrt' });
|
||||||
|
|
||||||
// Pfingstmontag (Whit Monday) - 50 days after Easter
|
// Pfingstmontag (Whit Monday) - 50 days after Easter (all states)
|
||||||
const whitMonday = new Date(easter);
|
const whitMonday = new Date(easter);
|
||||||
whitMonday.setDate(easter.getDate() + 50);
|
whitMonday.setDate(easter.getDate() + 50);
|
||||||
holidays.push({ date: whitMonday, name: 'Pfingstmontag' });
|
holidays.push({ date: whitMonday, name: 'Pfingstmontag' });
|
||||||
|
|
||||||
// Fronleichnam (Corpus Christi) - 60 days after Easter
|
// Fronleichnam (Corpus Christi) - 60 days after Easter (BW, BY, HE, NW, RP, SL, + some communities in SN, TH)
|
||||||
|
if (['BW', 'BY', 'HE', 'NW', 'RP', 'SL'].includes(bundesland)) {
|
||||||
const corpusChristi = new Date(easter);
|
const corpusChristi = new Date(easter);
|
||||||
corpusChristi.setDate(easter.getDate() + 60);
|
corpusChristi.setDate(easter.getDate() + 60);
|
||||||
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
|
holidays.push({ date: corpusChristi, name: 'Fronleichnam' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mariä Himmelfahrt (Assumption of Mary) - August 15 (BY in some communities, SL)
|
||||||
|
if (['SL'].includes(bundesland)) {
|
||||||
|
holidays.push({ date: new Date(year, 7, 15), name: 'Mariä Himmelfahrt' });
|
||||||
|
}
|
||||||
|
|
||||||
return holidays;
|
return holidays;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if date is a public holiday in Baden-Württemberg
|
* Check if date is a public holiday
|
||||||
* Returns the holiday name or null
|
* Returns the holiday name or null
|
||||||
*/
|
*/
|
||||||
function getHolidayName(date) {
|
function getHolidayName(date) {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const holidays = getPublicHolidays(year);
|
const holidays = getPublicHolidays(year, currentBundesland);
|
||||||
|
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = date.toISOString().split('T')[0];
|
||||||
const holiday = holidays.find(h => {
|
const holiday = holidays.find(h => {
|
||||||
@@ -610,6 +658,53 @@ async function deleteEntry(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a setting by key
|
||||||
|
*/
|
||||||
|
async function getSetting(key) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/settings/${key}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null; // Setting doesn't exist yet
|
||||||
|
}
|
||||||
|
throw new Error('Failed to get setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting setting:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a setting
|
||||||
|
*/
|
||||||
|
async function setSetting(key, value) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key, value })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to set setting');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting setting:', error);
|
||||||
|
showNotification('Fehler beim Speichern der Einstellung', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export entries as CSV
|
* Export entries as CSV
|
||||||
*/
|
*/
|
||||||
@@ -1502,6 +1597,107 @@ async function bulkDeleteEntries() {
|
|||||||
showNotification(`✓ ${deleted} Eintrag/Einträge gelöscht`, 'success');
|
showNotification(`✓ ${deleted} Eintrag/Einträge gelöscht`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// BUNDESLAND / SETTINGS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Bundesland change
|
||||||
|
*/
|
||||||
|
async function handleBundeslandChange(event) {
|
||||||
|
const newBundesland = event.target.value;
|
||||||
|
const oldBundesland = currentBundesland;
|
||||||
|
|
||||||
|
// Check for conflicts with existing entries
|
||||||
|
const entries = await fetchEntries();
|
||||||
|
const conflicts = [];
|
||||||
|
|
||||||
|
// Get old and new holidays for comparison
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = [currentYear - 1, currentYear, currentYear + 1];
|
||||||
|
|
||||||
|
const oldHolidays = new Set();
|
||||||
|
const newHolidays = new Set();
|
||||||
|
|
||||||
|
years.forEach(year => {
|
||||||
|
getPublicHolidays(year, oldBundesland).forEach(h => {
|
||||||
|
oldHolidays.add(h.date.toISOString().split('T')[0]);
|
||||||
|
});
|
||||||
|
getPublicHolidays(year, newBundesland).forEach(h => {
|
||||||
|
newHolidays.add(h.date.toISOString().split('T')[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find dates that are holidays in new state but not in old state, and have entries
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (newHolidays.has(entry.date) && !oldHolidays.has(entry.date)) {
|
||||||
|
const dateObj = new Date(entry.date);
|
||||||
|
// Temporarily set to new bundesland to get holiday name
|
||||||
|
const tempBundesland = currentBundesland;
|
||||||
|
currentBundesland = newBundesland;
|
||||||
|
const holidayName = getHolidayName(dateObj);
|
||||||
|
currentBundesland = tempBundesland;
|
||||||
|
|
||||||
|
conflicts.push({
|
||||||
|
date: entry.date,
|
||||||
|
displayDate: formatDateDisplay(entry.date),
|
||||||
|
holidayName: holidayName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warn user if conflicts exist
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
const conflictList = conflicts.map(c => ` • ${c.displayDate} (${c.holidayName})`).join('\n');
|
||||||
|
const message = `⚠️ Achtung!\n\nDie folgenden Tage werden zu Feiertagen und haben bereits Einträge:\n\n${conflictList}\n\nMöchten Sie fortfahren? Die Einträge bleiben erhalten, aber die Tage werden als Feiertage markiert.`;
|
||||||
|
|
||||||
|
if (!confirm(message)) {
|
||||||
|
// Revert selection
|
||||||
|
event.target.value = oldBundesland;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state and save
|
||||||
|
currentBundesland = newBundesland;
|
||||||
|
await setSetting('bundesland', newBundesland);
|
||||||
|
|
||||||
|
// Reload view to show updated holidays
|
||||||
|
await loadMonthlyView();
|
||||||
|
|
||||||
|
const bundeslandNames = {
|
||||||
|
'BW': 'Baden-Württemberg',
|
||||||
|
'BY': 'Bayern',
|
||||||
|
'BE': 'Berlin',
|
||||||
|
'BB': 'Brandenburg',
|
||||||
|
'HB': 'Bremen',
|
||||||
|
'HH': 'Hamburg',
|
||||||
|
'HE': 'Hessen',
|
||||||
|
'MV': 'Mecklenburg-Vorpommern',
|
||||||
|
'NI': 'Niedersachsen',
|
||||||
|
'NW': 'Nordrhein-Westfalen',
|
||||||
|
'RP': 'Rheinland-Pfalz',
|
||||||
|
'SL': 'Saarland',
|
||||||
|
'SN': 'Sachsen',
|
||||||
|
'ST': 'Sachsen-Anhalt',
|
||||||
|
'SH': 'Schleswig-Holstein',
|
||||||
|
'TH': 'Thüringen'
|
||||||
|
};
|
||||||
|
|
||||||
|
showNotification(`✓ Bundesland auf ${bundeslandNames[newBundesland]} gesetzt`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved settings
|
||||||
|
*/
|
||||||
|
async function loadSettings() {
|
||||||
|
const savedBundesland = await getSetting('bundesland');
|
||||||
|
if (savedBundesland) {
|
||||||
|
currentBundesland = savedBundesland;
|
||||||
|
document.getElementById('bundeslandSelect').value = savedBundesland;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// INLINE EDITING
|
// INLINE EDITING
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1862,6 +2058,9 @@ function initializeEventListeners() {
|
|||||||
document.getElementById('btnStartWork').addEventListener('click', startWork);
|
document.getElementById('btnStartWork').addEventListener('click', startWork);
|
||||||
document.getElementById('btnStopWork').addEventListener('click', stopWork);
|
document.getElementById('btnStopWork').addEventListener('click', stopWork);
|
||||||
|
|
||||||
|
// Bundesland selection
|
||||||
|
document.getElementById('bundeslandSelect').addEventListener('change', handleBundeslandChange);
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Close modal when clicking outside
|
||||||
document.getElementById('entryModal').addEventListener('click', (e) => {
|
document.getElementById('entryModal').addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'entryModal') {
|
if (e.target.id === 'entryModal') {
|
||||||
@@ -1879,9 +2078,10 @@ function initializeEventListeners() {
|
|||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
initializeFlatpickr();
|
initializeFlatpickr();
|
||||||
initializeEventListeners();
|
initializeEventListeners();
|
||||||
|
await loadSettings(); // Load saved settings first
|
||||||
checkRunningTimer(); // Check if timer was running
|
checkRunningTimer(); // Check if timer was running
|
||||||
loadMonthlyView(); // Load monthly view by default
|
loadMonthlyView(); // Load monthly view by default
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -193,6 +193,38 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Section (Collapsible) -->
|
||||||
|
<details class="mt-4 pt-4 border-t border-gray-700">
|
||||||
|
<summary class="cursor-pointer text-gray-300 hover:text-gray-100 font-medium text-sm flex items-center gap-2">
|
||||||
|
<span class="text-lg">⚙️</span>
|
||||||
|
<span>Einstellungen</span>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-4 items-center">
|
||||||
|
<div class="flex-1 min-w-[200px]">
|
||||||
|
<label for="bundeslandSelect" class="block text-sm font-medium text-gray-300 mb-1">Bundesland (Feiertage)</label>
|
||||||
|
<select id="bundeslandSelect"
|
||||||
|
class="w-full px-4 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
<option value="BW">Baden-Württemberg</option>
|
||||||
|
<option value="BY">Bayern</option>
|
||||||
|
<option value="BE">Berlin</option>
|
||||||
|
<option value="BB">Brandenburg</option>
|
||||||
|
<option value="HB">Bremen</option>
|
||||||
|
<option value="HH">Hamburg</option>
|
||||||
|
<option value="HE">Hessen</option>
|
||||||
|
<option value="MV">Mecklenburg-Vorpommern</option>
|
||||||
|
<option value="NI">Niedersachsen</option>
|
||||||
|
<option value="NW">Nordrhein-Westfalen</option>
|
||||||
|
<option value="RP">Rheinland-Pfalz</option>
|
||||||
|
<option value="SL">Saarland</option>
|
||||||
|
<option value="SN">Sachsen</option>
|
||||||
|
<option value="ST">Sachsen-Anhalt</option>
|
||||||
|
<option value="SH">Schleswig-Holstein</option>
|
||||||
|
<option value="TH">Thüringen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistics -->
|
<!-- Statistics -->
|
||||||
|
|||||||
79
server.js
79
server.js
@@ -40,6 +40,20 @@ try {
|
|||||||
console.error('Error during migration:', error);
|
console.error('Error during migration:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create settings table if it doesn't exist
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
console.log('Settings table ready');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating settings table:', error);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Database initialized successfully');
|
console.log('Database initialized successfully');
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -334,6 +348,71 @@ app.get('/api/export', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SETTINGS ENDPOINTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Get a setting by key
|
||||||
|
app.get('/api/settings/:key', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params;
|
||||||
|
const stmt = db.prepare('SELECT value FROM settings WHERE key = ?');
|
||||||
|
const result = stmt.get(key);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return res.status(404).json({ error: 'Setting not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ key, value: result.value });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting setting:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get setting' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a setting
|
||||||
|
app.post('/api/settings', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { key, value } = req.body;
|
||||||
|
|
||||||
|
if (!key || value === undefined) {
|
||||||
|
return res.status(400).json({ error: 'Key and value are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO settings (key, value, updated_at)
|
||||||
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value = excluded.value,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(key, value);
|
||||||
|
res.json({ key, value });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting setting:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to set setting' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all settings
|
||||||
|
app.get('/api/settings', (req, res) => {
|
||||||
|
try {
|
||||||
|
const stmt = db.prepare('SELECT key, value FROM settings');
|
||||||
|
const settings = stmt.all();
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
settings.forEach(s => {
|
||||||
|
result[s.key] = s.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting settings:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get settings' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server is running on http://localhost:${PORT}`);
|
console.log(`Server is running on http://localhost:${PORT}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user