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
|
||||
|
||||
- ✅ Erfassung von Arbeitszeiten (Datum, Startzeit, Endzeit)
|
||||
- ✅ Automatische Pausenberechnung nach deutschem Arbeitszeitgesetz
|
||||
- ✅ Maximum von 10 Stunden Nettoarbeitszeit
|
||||
- ✅ Filterung nach Zeitraum
|
||||
- ✅ CSV-Export mit deutscher Formatierung
|
||||
- ✅ Responsive Benutzeroberfläche mit Tailwind CSS
|
||||
- ✅ Moderner Datums-/Zeitauswahl (Flatpickr)
|
||||
- ✅ Docker-Containerisierung
|
||||
### Zeiterfassung
|
||||
- ✅ **Start/Stop Timer**: Live-Timer mit automatischer Zeiterfassung für den aktuellen Tag
|
||||
- Pause nach 6 Stunden (30 Min) oder 9 Stunden (45 Min) gemäß deutschem Arbeitszeitgesetz
|
||||
- Automatische Rundung auf 15-Minuten-Intervalle
|
||||
- Timer läuft auch nach Seiten-Reload weiter
|
||||
- ✅ **Manuelle Eingabe**: Erfassung von Arbeitszeiten (Datum, Startzeit, Endzeit, Pause)
|
||||
- ✅ **Inline-Bearbeitung**: Schnelle Änderung von Zeiten durch Klick in die Tabelle
|
||||
- ✅ **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
|
||||
|
||||
@@ -61,11 +105,20 @@ Die Anwendung berechnet automatisch die Pausenzeiten gemäß deutschem Arbeitsze
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
### Zeiteinträge
|
||||
- `GET /api/entries?from=YYYY-MM-DD&to=YYYY-MM-DD` - Alle Einträge im Zeitraum abrufen
|
||||
- `POST /api/entries` - Neuen Eintrag erstellen
|
||||
- `PUT /api/entries/:id` - Bestehenden Eintrag aktualisieren
|
||||
- `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
|
||||
|
||||
@@ -144,13 +197,32 @@ http://localhost:3000
|
||||
|
||||
## CSV-Export-Format
|
||||
|
||||
Die exportierte CSV-Datei enthält folgende Spalten:
|
||||
- **Datum**: Datum im Format TT.MM.JJJJ
|
||||
- **Startzeit**: Startzeit im Format HH:MM
|
||||
- **Endzeit**: Endzeit im Format HH:MM
|
||||
- **Pause in Minuten**: Pausenzeit in Minuten
|
||||
Die Anwendung bietet zwei Export-Optionen:
|
||||
|
||||
### 1. Vollständiger Export (📥 Button)
|
||||
Exportiert alle Einträge im gewählten Zeitraum (oder alle, wenn kein Filter gesetzt).
|
||||
|
||||
### 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)
|
||||
|
||||
**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
|
||||
|
||||
Die Anwendung verwendet:
|
||||
|
||||
@@ -6,3 +6,9 @@ CREATE TABLE IF NOT EXISTS entries (
|
||||
pause_minutes INTEGER NOT NULL DEFAULT 0,
|
||||
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 selectedEntries = new Set();
|
||||
|
||||
// Settings state
|
||||
let currentBundesland = 'BW'; // Default: Baden-Württemberg
|
||||
|
||||
// ============================================
|
||||
// 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 = [];
|
||||
|
||||
// Fixed holidays
|
||||
// Fixed holidays (all states)
|
||||
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, 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, 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
|
||||
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);
|
||||
goodFriday.setDate(easter.getDate() - 2);
|
||||
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);
|
||||
easterMonday.setDate(easter.getDate() + 1);
|
||||
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);
|
||||
ascension.setDate(easter.getDate() + 39);
|
||||
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);
|
||||
whitMonday.setDate(easter.getDate() + 50);
|
||||
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);
|
||||
corpusChristi.setDate(easter.getDate() + 60);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is a public holiday in Baden-Württemberg
|
||||
* Check if date is a public holiday
|
||||
* Returns the holiday name or null
|
||||
*/
|
||||
function getHolidayName(date) {
|
||||
const year = date.getFullYear();
|
||||
const holidays = getPublicHolidays(year);
|
||||
const holidays = getPublicHolidays(year, currentBundesland);
|
||||
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
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
|
||||
*/
|
||||
@@ -1502,6 +1597,107 @@ async function bulkDeleteEntries() {
|
||||
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
|
||||
// ============================================
|
||||
@@ -1862,6 +2058,9 @@ function initializeEventListeners() {
|
||||
document.getElementById('btnStartWork').addEventListener('click', startWork);
|
||||
document.getElementById('btnStopWork').addEventListener('click', stopWork);
|
||||
|
||||
// Bundesland selection
|
||||
document.getElementById('bundeslandSelect').addEventListener('change', handleBundeslandChange);
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('entryModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'entryModal') {
|
||||
@@ -1879,9 +2078,10 @@ function initializeEventListeners() {
|
||||
// INITIALIZATION
|
||||
// ============================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initializeFlatpickr();
|
||||
initializeEventListeners();
|
||||
await loadSettings(); // Load saved settings first
|
||||
checkRunningTimer(); // Check if timer was running
|
||||
loadMonthlyView(); // Load monthly view by default
|
||||
});
|
||||
|
||||
@@ -193,6 +193,38 @@
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- Statistics -->
|
||||
|
||||
79
server.js
79
server.js
@@ -40,6 +40,20 @@ try {
|
||||
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');
|
||||
|
||||
// ============================================
|
||||
@@ -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
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
|
||||
Reference in New Issue
Block a user