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
+93
View File
@@ -0,0 +1,93 @@
'use client';
import { useEffect, useState } from 'react';
interface Props {
value: string; // "HH:MM"
onChange: (value: string) => void;
className?: string;
clearOnFocus?: boolean;
autoFocus?: boolean;
}
function isValid(t: string): boolean {
if (!/^\d{1,2}:\d{2}$/.test(t)) return false;
const [h, m] = t.split(':').map(Number);
return h >= 0 && h <= 23 && m >= 0 && m <= 59;
}
function normalize(t: string): string {
const [h, m] = t.split(':').map(Number);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
export default function TimeInput({ value, onChange, className = '', clearOnFocus = false, autoFocus = false }: Props) {
const [local, setLocal] = useState(value);
const [error, setError] = useState(false);
// Lokalen Bearbeitungstext zurücksetzen, wenn sich der Wert von außen ändert.
// Bewusster Prop→State-Sync (Hybrid aus controlled/uncontrolled Eingabe).
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocal(value);
setError(false);
}, [value]);
function handleChange(raw: string) {
setError(false);
// Auto-insert colon after the 2nd digit (only when adding, not deleting)
if (/^\d{2}$/.test(raw) && /^\d$/.test(local)) {
setLocal(raw + ':');
return;
}
setLocal(raw);
}
function handleFocus() {
if (clearOnFocus) {
setLocal('');
setError(false);
}
}
function handleBlur() {
if (local === '') {
setLocal(value);
setError(false);
return;
}
const expanded = /^\d{1,2}:?$/.test(local) ? local.replace(/:$/, '') + ':00' : local;
if (isValid(expanded)) {
const norm = normalize(expanded);
setLocal(norm);
setError(false);
onChange(norm);
} else {
setError(true);
}
}
return (
<div className={`relative ${className}`}>
<input
type="text"
inputMode="numeric"
value={local}
onChange={(e) => handleChange(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={autoFocus}
placeholder="HH:MM"
maxLength={5}
className={`w-full px-2 py-1 border-2 rounded-lg bg-white text-sm text-gray-900 font-mono text-center focus:outline-none ${
error ? 'border-red-500 focus:border-red-500' : 'border-gray-400 focus:border-green-600'
}`}
/>
{error && (
<p className="absolute left-0 top-full mt-0.5 text-xs text-red-600 whitespace-nowrap z-10">
Ungültig (00:00 23:59)
</p>
)}
</div>
);
}