Initiale Arbeitszeiterfassung (Next.js 16 + SQLite)

Web-App zur Erfassung der Arbeitszeit, Layout/Architektur an Projekt logbuch
angelehnt, mit grüner Grundfarbe und SQLite3 statt MySQL.

- Eingabemaske: Datum (mit Wochentag), Ort (Kunde FFM/Homeoffice/Andrena),
  Beginn/Ende, Reisezeit, Vertriebsunterstützung (default 2h), Kommentar (max 500)
- Berechnung: Arbeitszeit = Ende-Beginn; Mehrarbeit = Arbeitszeit - 8h - Pause
- Pause als globale Einstellung (Einstellungen-Tab)
- Tabs Eingabe/Liste/Einstellungen, letzte 10 Einträge unter der Maske
- Single-User-Login (Benutzer Birgit + Passwort fest in .env), JWT-Cookie via jose
- better-sqlite3, Docker-Setup für docker.citysensor.de

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:53:44 +02:00
commit 01a7ac8e0b
38 changed files with 9062 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
'use client';
import { useState } from 'react';
interface Props {
pause: number;
onPauseChange: (pause: number) => void;
}
export default function Einstellungen({ pause, onPauseChange }: Props) {
const [raw, setRaw] = useState(String(pause).replace('.', ','));
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<'idle' | 'ok' | 'error'>('idle');
async function handleSave(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setStatus('idle');
const value = parseFloat(raw.replace(',', '.'));
try {
const res = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pause: value }),
});
if (!res.ok) throw new Error();
const data = await res.json();
onPauseChange(data.pause);
setRaw(String(data.pause).replace('.', ','));
setStatus('ok');
setTimeout(() => setStatus('idle'), 3000);
} catch {
setStatus('error');
} finally {
setSaving(false);
}
}
return (
<form onSubmit={handleSave} className="max-w-md space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Pause (dezimale Stunden)
</label>
<input
type="text"
inputMode="decimal"
value={raw}
onChange={(e) => setRaw(e.target.value)}
className="w-32 px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-green-600 focus:outline-none"
/>
<p className="mt-1 text-xs text-gray-500">
Wird in der Mehrarbeit-Berechnung abgezogen: Mehrarbeit = Arbeitszeit 8 h Pause.
</p>
</div>
<div className="flex items-center gap-3">
<button
type="submit"
disabled={saving}
className="px-6 py-1.5 bg-[#86C28B] hover:bg-[#6FB075] text-black font-medium rounded-lg transition-colors disabled:opacity-50 text-sm"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
{status === 'ok' && <span className="text-green-700 text-sm">Gespeichert.</span>}
{status === 'error' && <span className="text-red-600 text-sm">Fehler beim Speichern.</span>}
</div>
</form>
);
}