Darstellung
08 – Zeiterfassung
Reifegrad: 🟢 produktiv · Schicht: time_logs, src/lib/time-entries/*, src/app/zeiterfassung, src/app/api/time-entries/*, src/app/api/cron/personal-time-auto-close, src/components/time/*
Zweck
Erfassung von Arbeitszeit in zwei fachlichen Ausprägungen:
- Personalzeit (
entry_type='personal') – Anwesenheit/Kommen-Gehen je Mitarbeiter und Tag. - Projektzeit (
entry_type='project') – auf ein Projekt gebuchte Leistungsstunden, die in Rapporte und damit in die Abrechnung fließen.
time_logs ist die zentrale Zeitquelle für beide (siehe adr-0004-zeiterfassung-time-logs).
Datenmodell (time_logs, Migrationen 040 + 041)
Aufbauend auf der Ur-Tabelle (Migration 001) wurde time_logs erweitert zur zentralen Quelle:
| Spalte | Bedeutung |
|---|---|
company_id | Mandant (Tenancy) |
user_id | Mitarbeiter, dessen Zeit erfasst wird |
entry_type | personal | project (CHECK) |
project_id | nur bei project |
work_date | Arbeitstag (DATE) – Aggregationsschlüssel |
hours | Stunden NUMERIC(5,2) |
duration_minutes | Dauer in Minuten |
start_time / end_time | bei Live-Timer / Kommen-Gehen |
is_active | offener (laufender) Eintrag |
source | manual | whatsapp (CHECK) |
capture_mode | manual_hours | live_timer | manual_range | calendar_pair (CHECK) |
recorded_by_user_id | wer erfasst hat (z. B. Vorarbeiter für anderen) |
source_appointment_id / close_appointment_id | come/go-Termine bei calendar_pair |
rapport_entry_id | Verknüpfung zur Rapportzeile (Projektzeit) |
job_category | Tätigkeitskategorie (für Lohnsatz/Abrechnung) |
description | Notiz |
Wichtige Constraints/Indizes:
- Eindeutige Personalzeit pro Tag wurde durch eindeutigen aktiven Personal-Timer ersetzt:
idx_time_logs_active_personal→ max. ein laufender Personal-Timer pro(company_id, user_id). idx_time_logs_project_day– eine Projektzeit je(company, user, project, day).idx_time_logs_rapport_entry– 1:1 Zeile ↔ Rapport-Entry.- Idempotenter Backfill (Migration 040) erzeugt
time_logsaus bestehendenrapport_entries(Upgrade-Pfad für Altdaten).
Erfassungsmodi (capture_mode)
| Modus | Beschreibung | Erzeugung |
|---|---|---|
manual_hours | Stunden direkt eintragen (z. B. „3,5 h") | Standard-Eingabe |
live_timer | Start/Stop-Stoppuhr | startPersonalTimer / stopPersonalTimer |
manual_range | Kommen–Gehen-Zeitraum nachtragen | createManualPersonalRange |
calendar_pair | come/go-Termine aus Kalender koppeln | processCalendarCome / processCalendarGo |
UI-Labels (captureModeLabel): Live / Manuell / Kalender / Stunden.
Live in /zeiterfassung (Personalzeit) nutzt nur noch Kalender-Kommen/Gehen (calendar_pair) — kein separater live_timer-Button mehr in der UI.
Personalzeit – Geschäftslogik (personal-time-service.ts)
- Live-Timer:
startPersonalTimerlegt offenen Eintrag (is_active=true,hours=0) an; scheitert mitTIMER_ALREADY_ACTIVE, wenn schon einer läuft.stopPersonalTimerschließt exakt ab (resolvePersonalDuration– exakte Dauer, kein 0,25-Minimum). - 12-Stunden-Auto-Close:
PERSONAL_TIME_MAX_HOURS = 12. Offene Personalzeit, die älter als 12 h ist, wird automatisch begrenzt und beendet; der Mitarbeiter bekommt eine Notification („Personalzeit automatisch beendet"). Gilt für Live-Timer und offenes Kalender-Kommen. Bei Kalender-Kommen (source_appointment_id/calendar_pair) wird zusätzlich ein Gehen-Termin zum Cutoff-Zeitpunkt angelegt (Beschreibung inkl. Begründung + Zeitstempel,close_appointment_idgesetzt) — damit der Kalender nicht den ganzen Tag nur „Kommen“ zeigt. - Cron:
autoCloseExpiredPersonalTimers()schließt alle abgelaufenen Timer. EndpunktPOST /api/cron/personal-time-auto-close(geschützt per Cron-Secret) wird durch den lokalen/Helm-Runnerscripts/lib/personal-time-cron-runner.mjs(ausstart-all.js) getaktet. - Manueller Zeitraum:
createManualPersonalRange(come/go-Zeiten, Validierung „Gehen nach Kommen", 0,25-Schritte). - Kalender-Pairing:
processCalendarComeöffnet,processCalendarGoschließt einen Eintrag – ausgelöst überappointmentsvom Typcome/go(siehe 06-termine-kalender-plantafel). - Tätigkeits-Canonicalisierung (Projektzeit): Schreibweisen wie „Fliesen legen“ / „fliesenlegen“ gelten projektweit als dieselbe Tätigkeit (
activityMatchKey). Beim Buchen wird die etablierte Schreibweise wiederverwendet; gleicher Tag addiert die Stunden.
Projektzeit – Geschäftslogik (time-entries-service.ts)
listTimeEntries/summarizeTimeEntries– Liste & Summen mit Filtern (Zeitraum, User, Projekt, Typ). Sichtbarkeit: Nicht-Manager sehen nur eigene Einträge; Manager (Rolle 1/2) sehen alle bzw. gefiltert nachuserId(isManagerRole).- CRUD über
POST/PUT/DELETE /api/time-entries[/id]. - Import in Rapport:
importProjectTimesToRapport(nur Manager/Admin) übernimmt Projektzeiten eines Zeitraums als Rapportzeilen → Brücke zur Abrechnung (time-entries/import-to-rapport). Fehlerfälle:FORBIDDEN,RAPPORT_NOT_FOUND,RAPPORT_LOCKED,PROJECT_REQUIRED.
Validierungsregeln (time-entries-utils.ts)
- Stunden in 0,25-Schritten,
0 < h ≤ 24(validateHours); Parsing akzeptiert Komma (parseHours,"3,5" → 3.5). work_datestriktYYYY-MM-DD.- Monatsfilter (neu):
currentMonthValue,monthBounds,formatMonthLabel(Localede-AT) für Monats-Navigation in UI/URL. - Manager-Rollen:
isManagerRole(role)→ Rolle 1 oder 2.
API-Endpunkte (vollständig)
| Endpoint | Funktion |
|---|---|
GET/POST /api/time-entries | Liste (Filter) / Eintrag anlegen |
PUT/DELETE /api/time-entries/[id] | ändern / löschen |
GET /api/time-entries/summary | Summen (Filter Zeitraum/User/Projekt/Typ) |
GET /api/time-entries/personal/active | aktiven Personal-Timer abfragen |
POST /api/time-entries/personal/manual-range | Kommen–Gehen nachtragen |
POST /api/time-entries/personal/from-calendar-event | come/go aus Termin koppeln |
POST /api/time-entries/import-to-rapport | Projektzeiten → Rapport (Manager) |
POST /api/cron/personal-time-auto-close | 12 h-Auto-Close (Cron-Secret) |
UI
/zeiterfassung– Erfassung & Übersicht; Tab/Filter?type=personal|project, Monatsnavigation. Projektzeit: zusätzliche Filter Projekt und Tätigkeit (API:projectId,activity).- Komponenten:
src/components/time/*.
Tests
npm run test:time-entries → src/lib/time-entries/time-entries-utils.test.ts (Stunden-Parsing/-Validierung, Monatsgrenzen). Teil des Gesamt-npm test.
Ende-zu-Ende-Fluss (Beispiel)
Monteur tippt im WhatsApp "2,5h Elektrik Projekt Müller"
→ Agent-Tool schreibt time_logs(entry_type=project, source=whatsapp)
Vorarbeiter importiert Projektzeiten der Woche in den Rapport
→ rapport_entries + time_logs.rapport_entry_id gesetzt
Rapport wird abgeschlossen, Rechnung aus Rapport erzeugt
→ Stunden × labor_rates(job_category) = RechnungspostenBekannte Befunde / offene Punkte
- Zwei Zeitwelten in einer Tabelle (personal vs. project) – bewusst, aber Queries müssen immer
entry_typefiltern. time_logsenthält Altspalten aus Migration 001 (break_minutes,activity_type,approved_by_id,tags,position_id), die im neuen Flow kaum genutzt werden.- Auto-Close-Cron hängt am
start-all.js-Runner; in reinemnext startohne Runner läuft er nicht – im Helm-Setup über das App-Image abgedeckt.