Zum Hauptinhalt springen
daagwerk

daagwerk Changelog

Changelog

Changelog

Alle wesentlichen Änderungen an daagwerk — chronologisch, neueste zuerst. Format nach Keep a Changelog, Versionierung nach Semantic Versioning.

Changelog — daagwerk

Alle wesentlichen Änderungen an daagwerk werden in dieser Datei dokumentiert.
Format basiert auf Keep a Changelog.
Versionierung folgt Semantic Versioning.


Unreleased

  • Statistik-Charts + Profitabilitäts-Layer (v2.4) — aus v2.0 SR2 herausgelöst; Deckungsgrad = Umsatz ÷ (gebucht × Kunden-Satz), ohne Personalkosten; brauchen das v2.1-Fundament, landen mit dem Auswertungs-Re-Design. Enthält auch die Budget-Gauge-Geld-Modus-Korrektur (je-Modell statt hours×rate).

[2.6.4] — 2026-07-13

Fix der Serien-„Endet”-Zeile (Layout endgültig korrekt).

Fixed

  • Nach dem Umbau auf gestapelte Optionen (v2.6.3) standen die Texte („nach N Terminen” / „bis Datum”) am rechten Modal-Rand bzw. außerhalb, weil die <label>-Zeilen auf volle Breite gestreckt wurden. Jetzt sind die Zeilen inline-flex (inhaltsbreit) + linksbündig — Radio, Beschriftung und Feld sitzen eng zusammen, innerhalb des Fensters. Visuell in isolierter Vorschau verifiziert.

[2.6.3] — 2026-07-13

Politur: Serien-„Endet”-Zeile aufgeräumt.

Fixed

  • Im „Wiederholen”-Bereich war die Ende-Zeile (nach N Terminen / bis Datum) im Layout verrutscht — Radiobuttons, Zahlenfeld und Datumsauswahl liefen in einer umbrechenden Reihe durcheinander. Jetzt zwei saubere, gestapelte Optionen unter der Überschrift „Endet”.

[2.6.2] — 2026-07-13

Wiederkehrende (geplante) Buchungen — Serie (Scheibe 2).

Added

  • „Wiederholen” im Buchungs-/Duplizieren-Dialog: erzeugt aus einer Vorlage eine ganze Serie geplanter Buchungen (Outlook-artig).
    • Regel: täglich oder wöchentlich (mit Intervall „jede N. Woche” + Wochentagen Mo–So) und Ende wahlweise nach N Terminen oder bis Datum.
    • Live-Vorschau „erzeugt X Termine”, Obergrenze 200 pro Serie.
    • Uhrzeit und Dauer der Vorlage bleiben je Termin erhalten.
    • Alle Serien-Termine sind „geplant” → sie zählen nicht ins Budget; jeden aktivierst Du später einzeln, wenn er wirklich stattfindet. Ideal für Jour Fixes über lange Projektlaufzeiten, ohne das Budget-Bild zu verfälschen.
    • Neuer Endpoint POST /entries/recurring (legt die geplanten Buchungen in einer Transaktion an, jede über den planned-Pfad — kein Budget-Verbrauch).

Verifiziert mit einem Integrationstest (12-Termine-Serie berührt das PO-Budget nicht). Keine Migration.


[2.6.1] — 2026-07-13

Buchung verschieben: Von-Datum zieht das Bis-Datum mit (überall gleich).

Fixed

  • Beim Ändern des Von-Datums wird jetzt in allen Buchungs-Dialogen automatisch das Bis-Datum mitgezogen (die Bis-Uhrzeit bleibt) — kein „Ende darf nicht vor Anfang liegen”-Fehler mehr beim Verschieben/Duplizieren. Das Verhalten (syncEndDate, seit v1.6) fehlte bislang im Buchungs-Fenster (NewBookingModal, also auch beim Duplizieren) und im Job-Seiten-Inline-Edit; jetzt ist es unisono mit dem Bearbeiten-Modal, der Buchungsliste und den Reports.

[2.6.0] — 2026-07-13

Geplante Buchungen als eigener Status (Fundament, Scheibe 1).

Neues Konzept für zukünftige/wiederkehrende Termine: eine geplante Buchung ist eine ganz normale Buchung mit allen Feldern, nur im Status „geplant” – und wird aus allen Auswertungen ausgeklammert (Budget, Bestellnummern, Reports, PDF, Dashboard, Auslastung, Übersichten), bis sie aktiviert wird. So verfälschen z. B. Jour Fixes über 24 Monate nicht das aktuelle Budget-Bild.

Added

  • Status „geplant” (Migration 050, entry_status-Enum). Anlegen über eine auffällige Checkbox im Buchungs-Dialog (manuell und im Wochenkalender).
  • Anzeige: geplante Buchungen erscheinen in „Meine Woche” hell/durchscheinend mit gestricheltem Rahmen und „geplant”-Kennzeichnung; in Listen mit dem Status-Badge „Geplant”. Sie zählen nirgends ins Geld.
  • Aktivieren: ein ✓-Icon am Block wandelt „geplant → gebucht” – genau dann läuft der gehärtete Pfad (Bestellnummer auflösen, bei Überschreitung aufteilen, Erschöpfung prüfen). Erst hier wird aus „geplant” echtes Budget.
  • Geplante Buchungen lassen sich wie echte verschieben, in der Größe ziehen, bearbeiten, duplizieren und stornieren – ohne Budget-Wirkung.

Changed

  • Budget-/Auswertungs-Ausschluss an allen ~40 Aggregations-Stellen (der neue Status fällt überall raus, wo Geld/Stunden summiert werden).

Removed

  • Die alte Forecast-Funktion („+ Geplante Buchung” in der Buchungsliste, eigene planned_entries-Tabelle) wurde vollständig entfernt (Migration 051). Das neue Status-Konzept ersetzt sie sauberer.

Verifiziert mit neuen Integrationstests (geplant zählt nicht ins Budget; Aktivieren verbraucht es korrekt) und der vollen Test-Suite. Nächste Scheibe: wiederkehrendes Anlegen (Serie → geplante Buchungen).


[2.5.8] — 2026-07-13

„Meine Woche” — Duplizieren mit Bearbeiten (Phase 2).

Added

  • Zweites Kopieren-Icon in der Hover-Leiste (lila, mit „+”): öffnet das Anlege-Fenster vollständig vorbefüllt aus der Vorlage (Job, Start/Ende, Notiz; bei Managern der Nutzer). Der Aktions-Button heißt dort „Duplizieren” – Datum oder Werte lassen sich vorher anpassen, dann anlegen. So dupliziert man z. B. ein Meeting auf einen anderen Tag, ohne es hinterher zu verschieben.
    • NewBookingModal um Prefill (initialJob, initialNotes) + mode='duplicate' (Titel/Button/Toast) erweitert; getriggert über das bestehende daagwerk:open-new-booking-Event. Speicherpfad unverändert (POST /entries).
  • Die vier Overlay-Icons (Bearbeiten · Sofort-Duplikat · Duplizieren+Bearbeiten · Stornieren) wurden etwas kompakter gesetzt, damit sie auch auf schmalen Blöcken passen.

Nächste Phase: wiederkehrendes Duplizieren (Serie → konkrete Buchungen, mit Budget-Vorschau).


[2.5.7] — 2026-07-13

„Meine Woche” — Buchung duplizieren (Phase 1: Sofort-Duplikat).

Added

  • Kopieren-Icon in der Hover-Leiste jedes Blocks (zwischen Stift und Papierkorb): Ein Klick legt eine 1:1-Kopie der Buchung an – auf derselben Uhrzeit, wodurch das Überlappungs-Layout sie direkt daneben zeigt; danach per Drag verschieb- und bearbeitbar. Notiz und abrechenbare Zeit werden mitkopiert. Ideal für wiederkehrende Termine: einmal duplizieren, dann ziehen.
    • Erstellt über den gehärteten Buchungspfad (POST /entries); die Bestellnummer wird frisch aufgelöst (aktive PO des Jobs).

Nächste Phasen: Duplizieren mit Bearbeiten (Dialog vorbefüllt), danach wiederkehrendes Duplizieren (Serie → konkrete Buchungen, mit Budget-Vorschau).


[2.5.6] — 2026-07-12

„Meine Woche” — Bearbeiten und Stornieren per Hover-Overlay.

Added

  • Hover-Icons auf jedem Block: Bleistift (bearbeiten) und Mülleimer (stornieren) erscheinen, sobald die Maus über einer Buchung liegt.
    • Bleistift öffnet das bestehende „Erweitert bearbeiten”-Modal (EntryEditModal) — Account→Projekt→Job wechseln, Datum/Zeiten, Notiz, Abrechenbar, (Manager) Nutzer; speichert über den gehärteten Pfad, danach lädt die Woche neu.
    • Mülleimer storniert die Buchung (Status cancelled, mit Rückfrage) — sie verschwindet aus dem Kalender. daagwerk kennt kein hartes Löschen; Stornieren ist der reguläre Weg.
    • Klick auf ein Icon startet keinen Verschiebe-/Resize-Zug (sauber getrennt). Abgerechnete (invoiced) Buchungen und Timer haben keine Overlay-Icons.

[2.5.5] — 2026-07-12

„Meine Woche” — Buchungen verschieben und in der Größe ziehen (P3).

Added

  • Verschieben per Drag (auch auf einen anderen Tag) und Resize an beiden Kanten direkt im Wochenkalender. Block anfassen und ziehen verschiebt ihn (Uhrzeit + Tag), oben/unten anfassen ändert Start bzw. Ende. 15-Minuten-Raster, Live-Vorschau mit Uhrzeit-Label, Original während des Ziehens gedimmt. Gespeichert wird über PUT /entries/{id} — den gehärteten Pfad mit Neuberechnung von Gebucht/Abrechenbar und PO-Budget-Re-Split (v2.3.4–2.3.6); danach lädt die Woche neu (zeigt einen etwaigen Split sofort). Abgerechnete (invoiced) Buchungen und laufende Timer sind nicht ziehbar; scheitert das Speichern (z. B. gesperrter Zeitraum), wird der alte Stand wiederhergestellt.

Damit ist „Meine Woche” bis auf die geplanten Buchungen (P4) komplett bedienbar.


[2.5.4] — 2026-07-12

Bugfix: „Meine Woche” zeigte in vollen Wochen nicht alle Buchungen.

Fixed

  • Kalender lädt jetzt alle Buchungen der Woche. Die Wochenansicht rief /entries ohne limit auf → es griff der Default von 25 Einträgen bei ORDER BY started_at DESC. In Wochen mit vielen Buchungen fielen dadurch die frühesten Tage aus der Ansicht (in der Buchungsliste sichtbar, im Kalender nicht). Fix: limit=250 (Handler-Maximum) — eine Ein-Personen-Woche bleibt weit darunter.

Reines Frontend — keine Migration.


[2.5.3] — 2026-07-12

Bugfix: Timer-Erinnerung per Web Push wurde still verschluckt.

Fixed

  • Fehlgeschlagene Pushes gelten nicht mehr als zugestellt. webpush-go liefert bei einer Ablehnung durch den Push-Dienst (z. B. Apple/Safari mit 4xx) keinen Go-Fehler, sondern die HTTP-Antwort — der alte Code prüfte nur 404/410, sodass ein 400/403 fälschlich als Erfolg zählte: keine Push, aber auch keine Mail, und der stündliche Dedup-Slot blieb belegt (Erinnerung verstummte für die Stunde). Jetzt zählt nur ein 2xx als zugestellt; jeder andere Status wird geloggt (Host + Statuscode), der Dedup-Slot wird bei Fehlschlag zurückgegeben, und die E-Mail-Erinnerung feuert als Fallback, damit die Benachrichtigung ankommt. Ungültige Abos (404/410) werden weiterhin gelöscht. Neuer Integrationstest (Push-Ablehnung → Mail-Fallback + Slot-Freigabe).

Reines Backend — keine Migration.


[2.5.2] — 2026-07-12

„Meine Woche” — Buchungen im Kalender anlegen (P2).

Added

  • Anlegen per Klick oder Ziehen. Ein Klick auf einen freien Slot legt eine 60-Minuten-Buchung ab dieser Uhrzeit an; das Aufziehen einer Spanne übernimmt exakt Start und Ende (15-Minuten-Raster). Beides öffnet das bekannte NewBookingModal mit vorbefüllter Start-/Endzeit — Job, Notiz usw. wie gewohnt. Während des Ziehens zeigt eine gestrichelte Vorschau die Spanne mit Uhrzeit-Label. Nach dem Speichern erscheint der Block sofort (Refetch via daagwerk:entry-created).

Der Speicherpfad ist unverändert der gehärtete (POST /entries: Cross-Day-Split, PO-Auflösung/-Budget-Split). Kein Backend, keine Migration. Es folgen: Verschieben/Resize per Drag-&-Drop (P3), geplante Buchungen (P4).


[2.5.1] — 2026-07-12

„Meine Woche” — visuelle Politur der Tagesspalten.

Changed

  • Tagesspalten als erhabene Karten. Arbeitstage erhalten jetzt die Karten-Fläche (--bg-card), freie Tage (Wochenende/Feiertag) fallen auf die Seitenfarbe (--bg) zurück — die Tage heben sich klar voneinander ab. Weil --bg-card in beiden Themes heller ist als die Seite, funktioniert das im Light- wie im Dark-Mode ohne Sonderfall (weiße Karten auf Creme / erhabene dunkle Karten auf tieferem Schwarz).
  • Heute-Spalte dezent getönt (--bg-info), damit der aktuelle Tag auf einen Blick sichtbar ist.
  • Kräftigerer Trenner zwischen den Spalten (--border-strong).

[2.5.0] — 2026-07-12

„Meine Woche” — Kalender-Wochenansicht (P0 + P1: read-only). Start des großen Wochen-Features: alle eigenen Buchungen der Woche im Kalender-Grid.

Added

  • Neue Seite „Meine Woche” (unter Erfassen): Wochengrid mit Spalten je Tag, 0–24 h, Buchungs-Blöcke nach Start/Ende, Status-Farbe + PO-Indikator, überlappende Buchungen nebeneinander, laufender Timer als Live-Block. Toggle Arbeitswoche/Ganze Woche (Arbeitswoche aus den persönlichen Workdays), KW-Wechsel (‹ Heute ›), Auto-Scroll auf die Arbeitszeit.
  • Typische Arbeitszeit als neue persönliche Einstellung (Profil → Arbeitszeit-Modell): work_day_start/work_day_end (Migration 049). In der Wochenansicht werden Zeiten davor/danach sowie Wochenende + Feiertage ausgegraut — rein visuell, verhindert kein Buchen.

Read-only in dieser Runde. Es folgen: Anlegen per Klick/Ziehen (P2), Drag-&-Drop-Verschieben (P3), geplante Buchungen (P4).


[2.4.6] — 2026-07-12

Timer-Erinnerung als Browser-Push (Phase C). Store-unabhängiger Kanal für „Timer läuft noch?” auch bei geschlossenem Tab — ohne auf App-Store-Freigaben zu warten. Scope: Desktop-Browser (Chrome/Edge/Firefox, Windows/macOS).

Added

  • Web Push. In Profil → Timer-Stopps aktivierbar („Push auf diesem Gerät”). Nach Erlaubnis erscheint die Erinnerung als System-Benachrichtigung, auch wenn daagwerk nur im Hintergrund läuft. Migration 048 push_subscriptions, VAPID-signierter + Ende-zu-Ende-verschlüsselter Versand (webpush-go), Endpoints GET /push/public-key, POST/DELETE /push/subscribe, Service Worker sw.js.
  • Anti-Stau by design: nur laufende Timer werden gepusht (gestoppte nie), kurze TTL (30 Min) → veraltete Pushes verfallen statt sich anzustauen, und eine Sammel-Benachrichtigung pro Nutzer (fester tag → ersetzt sich selbst, nie ein Stapel).
  • Push ersetzt Mail: hat ein Nutzer ein aktives Push-Gerät, geht die Erinnerung als Push (kein E-Mail-Doppel); ohne Gerät weiterhin als E-Mail. Der persönliche Schwellwert (v2.4.5) steuert weiter das „wann”.

Sicherheit

  • HTTPS-only, Payload Ende-zu-Ende verschlüsselt (der Push-Dienst kann den Inhalt nicht lesen), Requests VAPID-signiert (nur daagwerk darf an seine Abonnenten pushen), Erlaubnis explizit und jederzeit widerrufbar.

Phase C = letzte Timer-Phase; echte Inaktivitäts-Erkennung (Tyme-Stil) bleibt den nativen Apps vorbehalten.


[2.4.5] — 2026-07-11

Timer-Erinnerung pro Nutzer einstellbar (Phase B+).

Added

  • Persönliche E-Mail-Erinnerung. Jeder Nutzer stellt in Profil → Timer-Stopps selbst ein, ab wann (oder ob) er die „Timer läuft noch?”-E-Mail bekommt: Aus / 1 h / 2 h (Standard) / 3 h / 4 h. Migration 047 (users.timer_reminder_minutes, Default 120 → bisheriges Verhalten bleibt). Der Server-Reminder (v2.4.4) nutzt jetzt den persönlichen Wert statt einer festen 2-h-Konstante; 0 schaltet die Mail komplett ab. GET/PUT /me erweitert.

[2.4.4] — 2026-07-11

Timer-Erinnerung per E-Mail (Phase B).

Added

  • Server-Mail-Reminder bei lange laufenden Timern. Läuft ein Timer länger als 2 Stunden, bekommt der Besitzer einmalig eine E-Mail („Läuft noch ein Timer?” mit Job, Startzeit und bisheriger Dauer) — auch bei geschlossenem Browser/Tab, weil serverseitig im Minuten-Cleanup geprüft (Dedup pro Timer via notification_log). Der Tenant-Auto-Stopp (Default 8 h) bleibt das harte Sicherheitsnetz. Testbarer Kern remindLongRunningTimer + Integrationstest.

Phase C (Web Push für „auch bei geschlossener Seite ohne E-Mail”) folgt als eigene Mini-Spec.


[2.4.3] — 2026-07-11

Fixed

  • Timer-Seite und globaler Dock jetzt synchron. Stoppte man einen Timer über den Dock unten rechts, blieb er in der „Läuft gerade”-Liste der Timer-Seite scheinbar weiter laufen (verschwand erst nach Reload). Beide Stellen reagieren jetzt auf ein gemeinsames timers-changed-Event — Start und Stopp an einer Stelle aktualisieren die andere sofort.

[2.4.2] — 2026-07-11

Multi-Timer-Dock + dynamische Toast-Position (Phase A).

Changed

  • Mehrere laufende Timer werden jetzt als gestapelter Dock unten rechts angezeigt (jeder mit eigener Dauer + Stopp-Button) — kein Überlagern mehr.
  • Toasts schweben dynamisch über dem Timer-Dock (statt oben rechts): der Dock meldet seine Höhe als CSS-Variable, die Toast-Leiste rutscht entsprechend nach oben — auch wenn Timer dazukommen oder wegfallen. Ohne laufenden Timer sitzen die Toasts wie gewohnt unten rechts.

[2.4.1] — 2026-07-11

Position-Politur (Phase A).

Changed

  • Läuft-Timer-Chip nach unten rechts verschoben (statt oben rechts) — dort kollidierte er mit den Kopf-Buttons und dem „?”-Icon.
  • Toasts nach oben rechts verschoben, damit sie sich mit dem Timer-Chip (unten rechts) nicht überlagern.

[2.4.0] — 2026-07-11

Timer-Bewusstsein & Auto-Stopp (Phase A). Reaktion auf den Test-Betrieb: Hintergrund-Timer liefen teils 24 h weiter. Jetzt gibt es einen Tenant-weiten Auto-Stopp-Default, eine überall sichtbare Läuft-Anzeige und In-App-Erinnerungen.

Added

  • Standard-Auto-Stopp pro Mandant. Neues Tenant-Setting „Max. Timer-Laufzeit (Standard)” (Default 8 h). Greift als Fallback im Timer-Cleanup, wenn ein Nutzer keine eigene Maximal-Laufzeit gesetzt hat — so kann kein Timer mehr endlos (24 h) laufen. Migration 046. Persönliche Einstellung geht weiterhin vor.
  • Globaler Läuft-Timer-Chip oben rechts auf jeder Seite: live mitzählende Dauer, Job/Account und ein Stopp-Button. Zeigt bei mehreren Timern „+N”.
  • Tab-Titel-Ticker: solange ein Timer läuft, steht die Dauer im Browser-Tab (⏱ 2:14 · KI QA) — passiver Dauer-Blick auch im Hintergrund-Tab.
  • Browser-Erinnerung (Notifications API, opt-in per 🔔): „Timer läuft noch — aktiv?” ab 2 h Dauerlauf, danach stündlich (solange der Tab offen ist).

Phase B (Server-Mail-Reminder) und Phase C (Web Push für geschlossene Seite) folgen separat.


[2.3.7] — 2026-07-11

Zwei UI-Fixes aus dem Test-Betrieb.

Fixed

  • Textbausteine-Dropdown wurde im Modal abgeschnitten. Das Auswahl-Dropdown (📋) öffnete vom rechts sitzenden Button nach rechts über den Modal-Rand hinaus und wurde vom overflow:hidden des Modals gekappt. Es öffnet jetzt nach links ins Modal hinein und ist vollständig sichtbar (alle Erfassungs-Masken).
  • Literaler „{{shortcut}}” im Job-Suchfeld. Der Job-Picker gab den Platzhalter ohne den Tastenkürzel-Parameter weiter, sodass die i18n-Variable unaufgelöst als Text erschien. Jetzt wird das erkannte Kürzel eingesetzt.

[2.3.6] — 2026-07-11

PO-Budget-Split beim Bearbeiten (Phase 2). Bisher wurde beim Bearbeiten einer Buchung (PUT /entries/{id}, alle Edit-Oberflächen) das PO-Budget ignoriert: verlängerte man eine Buchung über das Budget einer Bestellnummer hinaus, wurde die PO still überbucht. Jetzt greift derselbe Split-Mechanismus wie beim Anlegen.

Fixed

  • Bearbeiten überschreitet PO-Budget → Split statt stiller Überbuchung. Sprengt eine Änderung (Zeit, Gebucht, Abrechenbar) das Budget der Bestellnummer, wird die Buchung automatisch geteilt: der passende Teil bleibt auf der PO, der Überlauf wandert auf die Nachfolge-PO oder wird als „ohne PO” markiert. Inklusive Budget-Erschöpfungs-Prüfung und Benachrichtigung. Gilt auch für die Sammel-Bearbeitung.

Details (intern)

  • Anker = die bestehende PO des Eintrags; nur bei Job-Wechsel/ohne-PO wird die aktive PO frisch aufgelöst.
  • Eigen-Verbrauch wird ausgeklammert — ein Eintrag, der schon auf der PO liegt, zählt beim Neurechnen nicht doppelt (sonst Fehl-Split).
  • Ein Eintrag auf einer erschöpften PO bleibt darauf, solange er hineinpasst; kein Auto-Reopen und kein Umschichten (bewusst schlank gehalten).
  • Re-Split nur bei budgetrelevanter Änderung; reine Notiz-/User-Edits bleiben ein schlankes Update.
  • Sauber in einer Transaktion mit FOR UPDATE (race-frei); Sammel-Bearbeitung verarbeitet sequenziell (Kreuz-Budget korrekt, Teilerfolg bleibt erhalten).
  • Zentraler Helfer resplitEntryInTx; 3 neue geld-kritische Integrationstests.
  • Frontend: Inline-Editoren in „Meine Buchungen”/„Auswertungen” laden nach dem Speichern neu, damit Split-Überlauf-Einträge sofort erscheinen.

[2.3.5] — 2026-07-10

Zeitänderung berechnet „Gebucht”/„Abrechenbar” neu (statt zu schützen). Verhaltens-Korrektur zu v2.3.4: Da eine Änderung von Start/Ende die Grundlage der Buchung ändert, werden „Gebucht” und „Abrechenbar” jetzt immer frisch aus der neuen Zeitspanne abgeleitet — auch zuvor manuell/abweichend gesetzte Werte werden verworfen. Ein sichtbarer Hinweis (Marken-Pink) erklärt die Neuberechnung. Wer danach wieder einen Sonderwert braucht, setzt ihn erneut.

Unverändert: eine Änderung nur an „Abrechenbar” (ohne Start/Ende anzufassen) bleibt bestehen und setzt „Gebucht” nicht zurück. Gilt an allen vier Edit-Oberflächen (Modal + Inline in „Meine Buchungen”/„Auswertungen”/JobPage).


[2.3.4] — 2026-07-10

Einheitliche Buchungs-Bearbeitung: „Gebucht”/„Abrechenbar” rechnen jetzt überall gleich. Phase 1 der Edit-Konsistenz (Phase 2 = PO-Split beim Bearbeiten folgt separat).

Fixed

  • Start/Ende ändern berechnete „Gebucht”/„Abrechenbar” nicht neu. An allen Edit-Oberflächen (großes Modal + Inline-Editoren in „Meine Buchungen”, „Auswertungen” und JobPage) wird „Gebucht” jetzt live aus der neuen Zeitspanne berechnet (mit Tenant-Rundung, symmetrisch für Start und Ende). „Abrechenbar” folgt gekoppelt, solange es „Gebucht” entspricht; ist es bewusst abweichend gesetzt, bleibt es unangetastet.
  • Reiner „Abrechenbar”-Edit in „Auswertungen” war unsichtbar. Die Auswertungen-Tabelle hatte gar keine „Abrechenbar”-Spalte — der Wert wurde gespeichert, aber nirgends angezeigt. Spalte nachgerüstet (mit Warnfarbe, wenn Abrechenbar < Gebucht).
  • Schutz vor stillem Überschreiben von „Gebucht”: Buchungen mit bewusst abweichendem „Gebucht” (≠ Zeitspanne) werden beim Speichern nicht mehr versehentlich auf die Spanne zurückgesetzt.

Changed

  • „Abrechenbar” bei Festpreis/Pauschale/nicht-abrechenbar-Jobs deaktiviert — mit deutlichem Hinweis in Marken-Pink. Das Backend erzwingt bei diesen Job-Typen ohnehin den Wert (= Gebucht bzw. 0); das Feld tut nicht mehr so, als wäre es editierbar.
  • Zentrale, getestete Rechenlogik (lib/bookingCalc.ts) als eine Quelle für alle Edit-Oberflächen. Additive Backend-Felder: billing_type in /jobs, rounding_minutes in /tenant.

[2.3.3] — 2026-07-10

Globaler Zugang zu Handbuch + API. Im Sidebar-Footer (über der Versionszeile) stehen jetzt zwei dezente Links „Handbuch” (sprachabhängig DE/EN → Online-Handbuch) und „API” (→ api.daagwerk.de/docs, Swagger), beide in neuem Tab. Auf jeder Seite und für jede Rolle sichtbar — ergänzt die (admin-only) „Hilfe & Ressourcen”-Karte aus v2.3.1.


[2.3.2] — 2026-07-10

Politur des Handbuch-„?”-Icons. Das kontextuelle Handbuch-Symbol (DocsLink) ist jetzt ein gefüllter Marken-Pink-Vollkreis (#F72A7C) mit kräftigem weißem Fragezeichen (30 px) statt des vorherigen dünnen Outline-„?↗”. Damit ist es deutlich sichtbarer und klar abgesetzt vom grauen Tooltip- HelpIcon. Rein visuell, keine Verhaltensänderung.


[2.3.1] — 2026-07-10

Bugfixes aus dem Test-Betrieb + kontextuelle Handbuch-Hilfe.

Fixed

  • CSV-Zeitbuchungs-Import: „User not found” bei jeder Zeile. Die User-Auflösung im Import filterte auf eine Spalte users.active, die es nicht gibt (Nutzer-Status liegt in users.status, Enum-Wert active). Der PostgreSQL-Fehler column "active" does not exist wurde im Code verschluckt → jede Zeile scheiterte mit „User not found”, obwohl die Nutzer existierten und aktiv waren. Query in Vorschau und Import auf status='active' korrigiert und den E-Mail-Vergleich case-robust gemacht (LOWER(email)). Konsistent mit den übrigen User-Lookups (Auth, Admin, Passwort-Reset), die längst status='active' nutzen.
  • Eigene Super-Admin-Zeile war nicht bearbeitbar. In der Nutzerverwaltung waren Super-Admin-Zeilen komplett vom Bearbeiten ausgeschlossen (Frontend canEdit = !isSuperAdmin; Backend WHERE role != 'super_admin' + nicht erlaubte Rolle beim Update). Damit kam ein Super-Admin nicht an die eigene E-Mail/Name. Jetzt darf ausschließlich der Super-Admin selbst die eigene Zeile bearbeiten (andere Admins bekommen 403); die Rolle wird dabei fix auf super_admin gepinnt (kein versehentliches Degradieren, keine Rechte-Eskalation für normale Admins).

Changed

  • Import-Vorschau: Spaltenlabel „Nutzer” → „Nutzer (E-Mail)” bzw. „User (e-mail)” — die Spalte enthält die E-Mail-Adresse, passend zur CSV-Spalte user_email.

Added

  • Kontextuelle Handbuch-Verlinkung im Frontend. Jede Hauptseite hat oben rechts ein „?↗“-Symbol, das direkt das passende Kapitel des Online-Handbuchs (daagwerk.de/dokumentation bzw. daagwerk.com/en/documentation) in einem neuen Tab öffnet; bei mehrdeutigen Seiten das Inhaltsverzeichnis. Sprachabhängig (DE/EN) über eine zentrale Zuordnung (lib/docs.ts). Bewusst abgesetzt vom bestehenden Tooltip-Hilfe-Icon durch den Außenpfeil (führt nach draußen).
  • Sammelseite „Hilfe & Ressourcen” in Profil → Unternehmens-Einstellungen (Manager/Admin): komplettes Handbuch, FAQ und „Für Entwickler (API/Swagger)” gebündelt.

[2.3.0] — 2026-07-04

Public-Facing: vollständige API-Referenz, Produkt-Doku und Online-Changelog.

Neu

  • Vollständige interaktive API-Referenz (Swagger). Alle Endpunkte der daagwerk-API sind jetzt dokumentiert (145 Operationen, zuvor 111) – mit ausführlicher Beschreibung, Parametern, Antworttypen und Fehlercodes. Live unter api.daagwerk.de/docs.

Vorbereitung (separates Website-Projekt)

  • Produkt-Dokumentation (15 nummerierte Endnutzer-Kapitel) und ein web-fertiger Online-Changelog als saubere Markdown-Artefakte unter website/ – zur Einbindung in daagwerk.com.

[2.2.0] — 2026-07-04

Großes Sammel-Release: der ganze v2.0-Zyklus (HTML/PDF-Plattform), das Billing-Fundament (v2.1) und die PO-/Budget-Benachrichtigungen (v2.2) — gemeinsam auf main entwickelt und in einem Rutsch getaggt.

v2.0 — HTML/PDF-Plattform + Plans + Branding + Multi-Currency

SR1 — Foundation & Premium-PDF (feat(v2.0-sr1):, Render via Gotenberg verifiziert)

  • Premium-PDF scharf: entries.pdf rendert das Premium-Editorial (customer_page.html.tmpl), nur echte Daten. Plan-System scharf (requireFeature(FeaturePDFExport)), Gotenberg-Pipeline.
  • Tenant-Branding (FeaturePDFBranding): Logo (base64-Data-URI) + Primärfarbe (--primary-Override) + Briefkopf (Cover) + Footer (jede Seite via @page); GET/PUT /tenant/branding + Logo-Upload/Delete; Branding-Reiter im Profil.
  • Multi-Currency: Per-Account-Währung serverseitig gegated (Business+), „Summen pro Währung”-Tabelle im PDF (keine Umrechnung).
  • Bulk + Role-Gating: /export/entries-bulk.zip (Business+, ein PDF je Account); Umsatz-Tabelle nur für Manager+.

SR2 — Dynamic Columns (feat(v2.0-sr2):)

  • Konfigurierbare PDF-Export-Spalten: zentrale Registry (15 Spalten) mit serverseitigem Role-Gating (rate nur Manager+, still gedroppt); die Premium-PDF rendert die Buchungstabelle dynamisch über die gewählten Spalten (?preset=/?columns=).
  • Presets: 3 Built-in (Kundenrechnung/Intern/Compliance) + benutzerdefinierte Presets (Migration 045 tenant_export_presets, /me/export-presets, privat oder tenant-weit geteilt).
  • PDF-Modal: Preset-Dropdown + Spalten-Checkboxen + Drag-Reorder + „Als Preset speichern” + Live-Vorschau der ersten 3 Buchungen.
  • Cleanup: alter fpdf-Handler (~345 Z.) + fpdf-exklusive Helfer + go-pdf/fpdf-Dependency entfernt.

v2.1 — Billing-Model-Fundament

  • Neuer Abrechnungs-Typ Pauschale/Fee (recurring_fee) neben stündlich, Festpreis, nicht-abrechenbar; im Tool setzbar (Betrag/Rhythmus/Laufzeit).
  • Arrangement-Resolver: Stundensatz feldweise Account→Project→Job; Modell + Beträge als Einheit. Umsatz je Modell (Festpreis/Fee zählen einmal, keine Blanket-×Rate-Fehlrechnung; PDF-„Summen pro Währung” rechnen korrekt).
  • Migrationen 040–042 (recurring_fee-Enum, fee-Felder, Altlast-Bereinigung).

v2.2 — PO-/Budget-Empfänger-Benachrichtigung

  • Interne Account-Rollen (Account-Manager / Customer-Success / Kundenkontakt, freie Name/E-Mail-Felder). Operative Alerts gehen nur an AM+CS — nicht mehr fälschlich an den externen Nachweis-Empfänger.
  • Vier Ereignisse: PO erschöpft, PO läuft ab, Budget-Schwelle (80/100 %, PO + Projekt), verwaiste Buchungen (wöchentlicher Digest). Dedup je Ereignis über notification_log. Periodischer Endpoint POST /internal/notification-run (systemd-Timer).
  • Migrationen 043–044 (Account-Rollen, notification_log).

Migrationen

040–045.


[1.13.3] — 2026-05-31

HelpIcon-System (Sparring Session 20).

Erstes Iterations-Release: kontextuelle Hilfe-Tooltips, eingebaut an 6 strategischen Stellen. Tester können künftig bei Unklarheiten direkt am Ort des Geschehens nachschauen, ohne in eine separate Doku zu springen.

<HelpIcon helpKey> Komponente:

  • Neue Datei components/ui/HelpIcon.tsx
  • Nutzt @radix-ui/react-tooltip (war schon Dependency, A11y kommt out-of-the-box: ARIA, Tastatur-Navigation, Focus-Management)
  • Tooltip.Provider global im Root (App.tsx)
  • Props: helpKey, optional size (Default 14px), side (Default 'top')
  • Mehrzeilige Texte: \n im i18n-String wird zu separaten Absätzen
  • Auf Hover und Tastatur-Focus erscheint der Tooltip (200 ms Delay)
  • Dunkler Background (#383838), helle Schrift, dezenter Schatten
  • Hover-Animation: graues ? wird zu lila bei Hover

i18n-Registry (help.* Namespace):

  • 8 initiale Einträge in de.json + en.json:
    • help.po.tab — Was sind Bestellnummern + Auto-Switch-Erklärung
    • help.po.orphan — Warum gibt es verwaiste Buchungen
    • help.table_settings.po_column — Tri-State Spalten-Modi
    • help.blank_timer.what — Schnell-Timer + Inbox-Workflow
    • help.inbox.privacy — Inbox ist privat, auch für Manager
    • help.sidebar.quick_timer — Was macht der Schnell-Timer-Button
    • help.assign_job.what — Was passiert beim Job-Zuordnen (Rounding, Job-Typ-Billing, PO-Resolve)
    • help.po.budget_type — Stunden- vs. Geld-Budget für POs

Initial platziert in 6 Spots:

  • JobPage „Bestellnummern”-Header (help.po.tab)
  • TableColumnSettings Dropdown-Header (help.table_settings.po_column)
  • BlankTimerModal Header (help.blank_timer.what)
  • Inbox-Page Header (help.inbox.privacy)
  • AssignJobModal Header (help.assign_job.what)
  • NewPurchaseOrderModal Budget-Type-Label (help.po.budget_type)

Bewusst NICHT platziert:

  • POCell „ohne PO”-Pille: hat schon einen Native-HTML-Tooltip, ein zusätzliches HelpIcon würde Tabellen-Zellen überfüllen
  • Sidebar Schnell-Timer-Button: das HelpIcon im Button selbst würde den Klick-Handler fummeln; das ist der erste Schritt — weitere Spots kommen in nachfolgenden Iterationen.

PageHeader-Refactor (Mini-Breaking):

  • title: stringtitle: React.ReactNode damit die Inbox-Page ein HelpIcon neben dem Titel rendern kann.

[1.13.2] — 2026-05-31

Security & Tenant-Scope-Sweep + Code-Hygiene.

Dritte Runde des Code-Review-Refactorings — schließt eine latente IDOR- Falle, fügt fehlende DB-Indizes hinzu, und ersetzt 25+ kopierte Roles- Checks durch zentrale Helper. Keine User-sichtbare Funktionalität, aber das Fundament wird vor v2.0 PDF nochmal um eine Schicht sicherer.

resolveRecipientForTimeEntry mit Tenant-Scope (Befund #4 — KRITISCH latent):

  • Die 5-stufige Empfänger-Cascade-Funktion (PO → Job → Project → Account → Tenant) hatte ein WHERE te.id = $1 ohne tenant_id-Filter. Aktuell Dead Code — wäre aber SOFORT ein theoretischer IDOR-Pfad geworden, sobald v2.0-PDF die Funktion aufgerufen hätte: bei bekannter time_entry-ID hätte ein Aufrufer Empfänger-Daten anderer Tenants extrahieren können.
  • Funktion behalten (gut gebaut für v2.0 PDF: ein Query mit allen LEFT JOINs, kein N+1), aber tenantID als ZWINGENDEN zweiten Parameter ergänzt — WHERE te.id = $1 AND te.tenant_id = $2.

Migration 039 — FK-Indizes (Befund #12):

  • Sechs Foreign-Key-Spalten hatten bisher keinen Index: accounts.recipient_user_id, projects.recipient_user_id, jobs.recipient_user_id, purchase_orders.recipient_user_id, purchase_orders.created_by, purchase_order_history.changed_by
  • User-Löschung triggerte Seq-Scans über die referenzierenden Tabellen. Aktuell unkritisch (< 10k Buchungen pro Tenant), wird aber Pain bei großen Tenants.
  • Partial-Indizes mit WHERE x IS NOT NULL — kein Storage-Overhead für die meist leeren Spalten.

Roles-Helper-Migration (Befund #19):

  • Neue Helper in auth.go:
    • isManagerPlus(c) — manager, admin, super_admin
    • isAdminPlus(c) — admin, super_admin
    • isSuperAdmin(c) — nur super_admin
    • isBucher(c) — nur bucher
    • Alle nil-tolerant (c == nil → false)
  • 0 rohe claims.Role == "..."-Checks mehr im gesamten Backend. ~30 Aufrufer migriert über 14 Handler-Dateien.
  • Wenn das Role-Modell sich künftig erweitert (neue Rolle, andere Hierarchie), muss nur EINE Stelle geändert werden statt 30.

Dead-Code-Sweep (Befund #17):

  • _ = durationSecs in handlers_entries.go entfernt — Variable inline berechnet, Marker war überflüssig.
  • _ = accountID in po_lifecycle.go (Backfill-Pfad) entfernt — Spalte gar nicht erst gescannt, Logik ist topologisch implizit.
  • _ = tr in pdf_premium.go entfernt — der defensive Marker war irreführend (behauptete „weiter unten verwendet”, was stimmt) und wurde durch sauberes Lassen der tr-Zuweisung ersetzt.

Was Befund #7 angeht (assignJob Tenant-Scope + Scan-Errors):

  • War bereits in v1.12.1 mit erledigt — die billing_type-Query in handlers_inbox.go assignJob bekam dort den tenant_id-Filter. Daher nicht erneut angefasst.

[1.13.1] — 2026-05-31

Frontend-Hygiene: Modal-Shell + i18n + react-query-Entscheidung.

Zweite Runde des Code-Review-Refactorings — alles im Frontend. Keine User-sichtbare Funktionalitätsänderung, aber Code wird konsistenter und englischsprachige Tester sehen jetzt durchgängig Englisch.

Modal-Shell-Konsolidierung (Befund #9):

  • Neue zentrale <Modal>-Primitive unter components/ui/Modal.tsx — Overlay + ESC + Click-outside + Body-Scroll-Lock + Auto-Focus in einer Komponente. Props: isOpen, onClose, title, subtitle, headerLeft (für custom Icon-Boxen wie im Schnell-Timer-Modal), maxWidth, disableEscClose, disableClickOutsideClose.
  • Alle 9 Modals migriert: NewTimerModal, NewBookingModal, BlankTimerModal, AssignJobModal, EntryEditModal, BulkEditModal, NewPurchaseOrderModal, PurchaseOrderDetailModal, POBackfillModal.
  • UX-Bug-Fix dabei: EntryEditModal und BulkEditModal hatten zuvor KEIN ESC-Handling — jetzt schließt ESC konsistent in allen 9 Modals.

i18n-Konsolidierung (Befund #8):

  • 44 inline defaultValue-Strings aus 9 Komponenten in de.json und en.json migriert (~30 neue Keys; einige existierten bereits)
  • Englische Übersetzungen für alle neu migrierten Keys ergänzt — Englisch-sprachige Tester sehen jetzt keine deutschen Defaults mehr
  • 3 verbliebene defaultValue-Aufrufe sind bewusst dynamisch (balance.holiday.${key}, balance.absence.${kind}, import.col.${col} — Fallback auf den Variablen-Wert wenn kein Translate-Eintrag da ist)

react-query rausgeworfen (Befund #11):

  • @tanstack/react-query war als Dependency installiert aber nirgends genutzt — die App hat keinen QueryClientProvider im Root. Der erste Versuch in v1.11.1 mit useQuery ist beim Mount abgestürzt und ist seit Hotfix 1eef284 auf Modul-Level-Cache + Subscriber-Pattern umgestellt. Paket entfernt → kleinere Bundle, weniger Verwirrung für Reviewer.
  • Wenn künftig Caching gewünscht ist, kommt das Paket bewusst rein mit Provider-Setup.

Doku-Lüge korrigiert (Befund #11):

  • CHANGELOG-Eintrag zu v1.11.1 sagte fälschlich „react-query + optimistic update”. Tatsächlich war es vom ersten Live-Test an ein Modul-Level-Cache. Korrigiert mit Hinweis auf den Hotfix-Commit.

[1.13.0] — 2026-05-31

Refactoring-Release: Geld-Pfade konsolidiert + Test-Infrastruktur.

Aus dem unabhängigen Code-Review nach v1.12.0 kam als HOCH priorisierter Refactoring-Auftrag: das in 6 Pfaden duplizierte Booking-Insert-Pattern sauber konsolidieren und mit Tests absichern, bevor v2.0 PDF die nächste Welle an Features bringt. v1.13.0 erledigt genau das — keine User-sichtbare Funktionalität, aber das Fundament für künftige Features ist jetzt belastbar.

Neues booking_insert.go mit InsertTimeEntryWithPOSplit():

  • Zentraler Eintrittspunkt für 5 von 6 Booking-Insert-Pfaden
  • Eine Funktion macht: Cross-Day-Split + Tenant-Rounding + Job-Typ-Billable + PO-Resolution + Budget-Split + Insert + Exhaustion-Check + Notification
  • Öffnet eigene Tx mit SELECT … FOR UPDATE auf der aktiven PO-Row → vollständiger Race-Fix für Code-Review-Befund #1
  • Akzeptiert optionalen RoundingOverride (Importer schickt vorgerundet) und BillableSecondsRequest (Manager-Wunsch für Hourly-Jobs)

5 Aufrufer migriert (~260 Zeilen dupliziertem Code eliminiert):

  • handlers_planned.go activate — −55 Zeilen
  • handlers_timer_cleanup.go convertTimer (non-blank) — −50 Zeilen
  • handlers_timers.go stopTimer (non-blank) — −65 Zeilen
  • handlers_timers.go createEntry — −55 Zeilen
  • handlers_time_import.go importRow — −35 Zeilen

Bewusst NICHT migriert:

  • Blanko-Fast-Paths in stopTimer/convertTimer (kein PO, kein Billable, eigene 10-Zeilen-Logik)
  • handlers_inbox.go assignJob — UPDATE-existing-Pattern statt INSERT-all, strukturell anders. Logik aus v1.12.1 bereits korrekt.

Bonus-Fix beim Konsolidieren entdeckt:

  • Re-Resolve-Bug: nach Budget-Split-Segment 0 (das die alte PO erschöpft hat) hat der Code naiv resolveActivePOForJob für Segment 1 aufgerufen. Da die alte PO im selben Tx noch status='active' ist (Flip auf exhausted läuft erst nach Commit), bekam Segment 1 wieder die alte PO zugewiesen. Resultat: Budget-Split war nur formal — Buchung landete effektiv komplett auf der überlaufenden PO. Dieser Bug existierte in ALLEN 6 alten Pfaden (auch v1.12.1-Codestand). Jetzt: der Helper trackt in touchedPOs welche POs in diesem Aufruf bereits benutzt wurden, und Re-Resolve skippt eine PO die schon erschöpft wurde. Race-Test mit 10 Goroutinen auf 2h-Budget liefert jetzt exakt 2h auf PO + Rest NULL.

Erste Go-Tests im Repo (~37 Tests, alle grün):

  • Unit: cross_day_split_test.go (Cross-Day, DST-Sprünge, Edge Cases) + rounding_test.go (15/30/1-Min-Rundung, Boundary-Werte)
  • Integration gegen daagwerk_test-DB:
    • billing_helpers_test.go (Job-Typ-Logik)
    • po_lifecycle_test.go (resolveActivePOForJob, computeBudgetSplit, checkAndApplyBudgetExhaustion mit 10-Goroutine-Race-Test)
    • booking_insert_test.go (Helper End-to-End mit 5-Goroutine-Race-Test der Befund #1 beweist)
  • Neue Test-Infrastruktur: internal/testutil/ mit DB-Setup, Migrations- Loader, Fixtures (NewFixture, AddPOHours, InsertBookedSeconds)
  • Makefile-Targets: make test-db-create, make test-db-reset, make test, make test-short
  • Test-DB läuft im bestehenden Dev-Postgres-Container (Datenbank daagwerk_test) — kein zweiter Container nötig

Architektur-Notizen:

  • Helper akzeptiert *pgxpool.Pool, öffnet eigene Tx intern. Caller müssen selbst keine Tx wrappen. Ausnahme: Cleanup hat seine eigene Tx für den Blanko-Pfad behalten — Job-Pfad nutzt den Helper.
  • FOR UPDATE auf der PO-Row ist die Wurzel des Race-Fixes. Postgres serialisiert konkurrierende Tx auf derselben Row. Nach G1’s Commit sieht G2 die aktuelle time_entries-Sicht und splitter korrekt.
  • Die touchedPOs-Map im Helper deckt den (zuvor unentdeckten) Re-Resolve-Bug ab — Bonusgewinn durch das Refactoring.

Was NICHT in v1.13.0 ist (kommt mit v1.13.1):

  • Modal-Shell-Konsolidierung (~9 Modals mit kopierter ESC/Click-Outside- Logik) — Code-Review Befund #9
  • i18n defaultValue inline → JSON-Migration (45 Vorkommen) — Befund #8
  • react-query-Entscheidung: Provider einbauen oder Paket raus — Befund #11
  • CHANGELOG v1.11.1 Doku-Lüge korrigieren — Befund #11

[1.12.1] — 2026-05-31

Hotfix: vier KRITISCHE Bugs aus dem unabhängigen Code-Review.

Aus dem Code-Audit nach v1.12.0 kamen vier Findings die nicht „nur” Refactoring sind, sondern in Produktion still falsche Daten erzeugen. Vor weiteren Refactorings (geplant in v1.13.x) zuerst die Bugs raus.

Befund #2 — Planned-Aktivierung vergaß Budget-Exhaustion-Check

  • handlers_planned.go activate rief nach dem INSERT von time_entries kein checkAndApplyBudgetExhaustion auf. Eine geplante Buchung konnte eine PO erschöpfen ohne dass status='exhausted' gesetzt oder die Notification an den PO-Empfänger rausging.
  • plannedHandler bekommt einen Mailer für die Notification-Cascade.

Befund #3 — Auto-Stop Cleanup macht jetzt Cross-Day-Split

  • handlers_timer_cleanup.go convertTimer hat bisher die gesamte Dauer eines über-Nacht-laufenden Timers auf den Start-Tag gebucht. Das ist genau der Pfad, der nachts greift wenn jemand den Timer vergessen hat — typischer Fall: Timer 18:00 gestartet, Max-Dauer 8h, Auto-Stop 02:00 am Folgetag.
  • Folge im Auslastungs-Tab: 25h+ Werte für den Vortag.
  • Jetzt: splitAtMidnight greift auch im Cleanup-Pfad. Sowohl der Blanko-Fast-Path als auch der reguläre Job-Pfad bekommen die Cross-Day-Logik. Pro Cross-Day-Segment: eigenes Rounding, eigene Budget-Split-Berechnung, eigene PO-Resolution. Touched POs werden nach Commit alle einzeln auf Exhaustion geprüft.

Befund #4/#15 — assignJob macht jetzt Budget-Split

  • handlers_inbox.go assignJob hat bisher einen einfachen UPDATE mit Job + PO gemacht. Sprengt der zugeordnete Job die PO, wurde die Buchung trotzdem komplett auf die alte PO geschrieben — keine Aufteilung in „erstes Segment alte PO + Restsegment Nachfolge/NULL”.
  • Jetzt: computeBudgetSplit läuft auch hier. Erstes Segment landet per UPDATE im existierenden Blanko-Eintrag (ID bleibt erhalten), Folgesegmente per INSERT als neue Einträge. Nachfolge-PO wird zwischen Segmenten neu resolved.
  • Plus Tenant-Scope-Fix in der billing_type-Abfrage (Befund #7): vorher gab es zwei separate Queries, die zweite ohne tenant_id-Filter. Theoretischer IDOR — jetzt eine kombinierte Query mit Tenant-Scope.

Befund #16 — checkAndApplyBudgetExhaustion hat Race-Condition gefixt

  • Vorher: snapshot → bedingter UPDATE (mit WHERE status='active') → History-INSERT → Mail. Bei parallelen Booking-Inserts haben mehrere Aufrufer alle den status-Wechsel beobachtet, alle ihre History-Zeilen geschrieben und alle Mails verschickt.
  • Jetzt: nur wenn tag.RowsAffected() == 1 (= unser UPDATE hat die Zeile wirklich von active auf exhausted geflippt) folgen History + Mail. Andere parallele Aufrufer fallen still aus.
  • Postgres-MVCC garantiert dass exakt eine Tx den Switch gewinnt.

Was Hotfix NICHT abdeckt (kommt in v1.13.0):

  • Befund #1 (Race-Condition in resolveActivePOForJob) — der vollständige Fix braucht eine Tx-Klammer um Resolve+Insert in allen 6 Booking-Pfaden. Macht erst Sinn mit dem geplanten insertTimeEntryWithPOSplit-Helper, sonst dupliziert man die Tx-Logik 6x. Mitigation in #16 fängt zumindest die Mehrfach-Notifications ab.
  • Die anderen v1.13-Befunde (Modal-Shell, react-query-Entscheidung, i18n-Konsolidierung, Doku-Drift, Tests) — Mittwoch.

Doku-Korrektur:

  • CHANGELOG v1.11.1 hatte fälschlicherweise „react-query + optimistic update” für useViewPreferences behauptet. Die echte Implementierung nutzt einen Modul-Level-Cache + Subscriber-Pattern (bewusst, weil die App keinen QueryClientProvider im Root hat). Korrektur kommt mit v1.13.1.

[1.12.0] — 2026-05-31

Blanko-Buchungen / Speed-Timer + Inbox.

Neue Bucher-UX: einfach loslegen, klassifizieren später. Pro User ein „Schnellstart”-Button in der Sidebar (#F7C619, daagwerk-Amber). Klick öffnet ein minimales Modal mit optionalem Notizfeld — kein Job-Picker, kein Project-Picker. Beim Klick auf „Jetzt starten” läuft sofort ein Blanko- Timer. Stopp → Buchung landet in der Inbox mit Badge-Zahl in der Sidebar. User klickt einen Eintrag an → AssignJobModal mit JobPicker (wiederverwendet aus NewTimerModal). Speichern → Job + PO + Billable wird gesetzt, die Buchung wandert in die regulären Reports.

Backend:

  • Migration 038: time_entries.job_id + active_timers.job_id nullable. Partieller Unique-Index active_timers_user_blank_unique erzwingt max. einen Blanko-Timer pro User gleichzeitig. Index time_entries_inbox_idx für die Inbox-Abfrage.
  • Neuer Endpoint POST /timers/start-blank — startet Blanko-Timer mit optionaler Notiz. Auto-Stop + Start-Snap aus Tenant-Settings greifen identisch zum normalen Timer.
  • Neuer Endpoint GET /inbox — Liste eigener Blanko-Buchungen (strict user-scoped, Manager sehen NICHT die fremden — DSGVO).
  • Neuer Endpoint GET /inbox/count — schmaler Count für Sidebar-Badge.
  • Neuer Endpoint POST /entries/{id}/assign-job — zuordnen. Berechnet booked_seconds mit Tenant-Rounding neu (Blanko hatte rohe Sekunden), wendet Job-Typ-Billing-Logik an, resolved aktive PO, prüft Budget-Erschöpfung.
  • stopTimer und Auto-Stop (handlers_timer_cleanup.go) bekommen einen Blanko-Fast-Path: kein Rounding (rohe Sekunden), kein Billable, kein PO-Split — alles wird beim Assign nachgerechnet.
  • listTimers von INNER JOIN auf LEFT JOIN umgestellt, damit laufende Blanko-Timer sichtbar bleiben. Antwort enthält neues Flag is_blank.
  • ActiveTimer.JobID bleibt string, leerer String markiert Blanko (vermeidet Breaking Change in 30+ Aufrufstellen).

Frontend:

  • BlankTimerModal mit optionalem Notizfeld, Enter-Submit, ESC-Close, Auto-Focus. Trigger via Window-Event daagwerk:open-blank-timer — von überall in der App aus aktivierbar.
  • Dritter Sidebar-Button „Schnellstart” in #F7C619 mit ⚡-Symbol, positioniert unter „Timer starten” (lime) und „Buchung eintragen” (lila).
  • Neuer Sidebar-Nav-Eintrag „Inbox Blanko Buchungen” unter „Erfassen” (zwischen Timer und Meine Buchungen) mit dynamischer Badge-Zahl. Badge in #F7C619, polled jede Minute + Event-getriggert bei Timer-Start/Entry-Created/Inbox-Changed.
  • Neue Seite /inbox (pages/Inbox.tsx) — Tabelle mit Start/Ende/Dauer/ Notiz pro Blanko-Buchung. Klick auf Zeile öffnet AssignJobModal. „Verwerfen”-Button storniert die Buchung (status=‘cancelled’) — taucht dann auch nicht mehr in der Inbox auf.
  • AssignJobModal zeigt Read-Only Start/Ende/Dauer der Blanko-Buchung oben, dann JobPicker + Notiz (übernommen aus Blanko-Notiz, editierbar).
  • Timer-Seite zeigt laufenden Blanko-Timer als kleine Amber-Pill statt Account/Projekt/Job — Symbol ⚡ + „Schnellstart (Blanko)”.
  • i18n de+en: nav.inbox und defaultValue-Inline-Strings in allen neuen Komponenten (kein blocker für Tester).

Architektur-Entscheidungen:

  • Datenmodell A (mit User abgestimmt): time_entries.job_id wird nullable, KEINE separate inbox_entries-Tabelle. Vorteil: Stop-Logik bleibt eine, Restore/Backup ohne Sonderfall. Bestehende Reports/ Dashboard-Joins (JOIN jobs) filtern Blanko-Buchungen stillschweigend raus — gewollt, weil sie bis zur Zuordnung nicht abrechnungsrelevant sind.
  • Max. 1 Blanko-Timer pro User: partieller Unique-Index. Mehrere parallele Blanko-Timer wären UX-mäßig verwirrend (alle hießen gleich).
  • Inbox strict user-scoped: Notizen können persönlich sein („Telefonat Anwalt”). Manager dürfen NICHT die Inbox anderer sehen.
  • Verwerfen statt Löschen: Stornieren statt DELETE — bleibt im Audit-Trail, ist mit existierender Status-Logik konsistent.

Offen / kommt mit v1.12.1 oder später:

  • iOS / macOS / Watch: Schnellstart + Inbox. Tester nutzen v1.12 erstmal nur Web.
  • Bulk-Zuordnung mehrerer Inbox-Einträge in einem Schwung.
  • Reminder-Mail „Du hast N unzugeordnete Buchungen seit X Tagen”.
  • Inbox-Sortierung nach Datum-Filter (aktuell immer DESC).

[1.11.1] — 2026-05-31

Verwaiste Buchungen sichtbar machen + dynamische Bestellnummer-Spalte.

Nach den ersten Live-Tests mit v1.11.0 fiel auf: bei Budget-Überschreitung gesplittete Buchungen (zweiter Teil ohne PO) und alle Buchungen in einem Job mit historischen POs zeigten denselben Status „Gebucht” wie reguläre Buchungen — der „Verhandlungs-Lücken”-Zustand war für den Manager nicht sichtbar. v1.11.1 löst das ohne neuen Status-Enum-Wert (Status bleibt der Abrechnungs-Lebenszyklus) und führt die PO-Zugehörigkeit als zweite, parallele Dimension visuell ein.

Backend:

  • Migration 037: users.view_preferences JSONB DEFAULT '{}' für persistente Tabellen-Sichteinstellungen pro User
  • handlers_view_preferences.go: GET + PATCH /me/view-preferences (Partial-Merge), strict user-scoped, fehlende Keys werden serverseitig als 'auto' interpretiert, unbekannte Werte (z. B. „invalid”) fallen auf 'auto' zurück
  • /entries, /reports, JobPage-Overview liefern jetzt pro Buchung: purchase_order_id, purchase_order_number (LEFT JOIN), is_orphaned (computed: NULL po_id UND Job/Projekt hat min. eine PO)
  • Response-Envelope um has_purchase_orders Flag — wird einmal pro Request über die gesamte gefilterte Menge berechnet (EXISTS), damit die Spalte beim Paginieren nicht flackert
  • JobDetail um has_purchase_orders ergänzt (Job-Level: Job oder sein Projekt hat min. eine PO)

Frontend Web:

  • POCell-Komponente unter features/purchase-orders/: drei Zustände
    • PO zugeordnet → Lila Pill #9D2AC6 mit PO-Nummer, klickbar
    • Verwaist (NULL pid + Job/Projekt hatte POs) → Amber Pill #F7C619 „ohne PO” mit Tooltip
    • Sonst (keine PO + Job hat nie POs gehabt) → leer (—)
  • TableColumnSettings-Komponente unter components/ui/: kleines Säulen- Icon oben rechts an jeder Tabelle, öffnet Dropdown mit Tri-State Radio „Dynamisch / Immer an / Aus”
  • useViewPreferences-Hook mit Modul-Level-Cache + Subscriber-Pattern (NICHT mit react-query — die App hat keinen QueryClientProvider; die erste Implementierung mit useQuery ist beim Mount abgestürzt und wurde direkt im Hotfix 1eef284 auf plain useState umgestellt; optimistic update + Rollback bei Server-Fehler bleiben erhalten)
  • shouldShowPOColumn()-Helper: on → immer, off → nie, auto → nur wenn Backend has_purchase_orders=true meldet
  • Integration in Entries.tsx, Reports.tsx, JobPage.tsx — jeweils eigene Preference (entries.po_column, reports.po_column, job_page.po_column)
  • Neue Spalte „Bestellnummer” zwischen „Abrechenbar” und „Status”
  • i18n de+en: ~12 neue Keys unter table_settings.*, po.cell.*, entries.col.po, error.po_column_mode_invalid

Architektur-Entscheidung (Sparring Session 21):

  • Status-Enum bleibt unverändert — time_entries.status ist der Abrechnungs-Lebenszyklus (bookedreviewedinvoiced/cancelled)
  • PO-Zugehörigkeit ist eine parallele Dimension — keine Vermischung über einen neuen Status-Wert wie z. B. „orphaned” (würde zu kombinierten Werten wie „orphaned_invoiced” führen)
  • View-Preferences sind erweiterbar — heute nur po_column, künftig problemlos user_column, notes_full_width, amount_diff etc. möglich ohne Schema-Änderung
  • Pro Tabelle einzeln konfigurierbar — JobPage kann „Immer an” sein (Kontext „Job hat POs” ist sowieso bekannt), Entries kann „Dynamisch” bleiben

Offen nach v1.11.1 (kommt mit v2.0 oder eigener Patch):

  • Backend-Endpoint für die View-Preferences anderer optionaler Spalten (User-Spalte in Entries, Notiz-Vollbreite, Differenz-Spalte etc.)
  • OrphansBanner im Dashboard mit Direkt-Link „N verwaiste Buchungen, jetzt zuordnen”
  • PDF Premium Layout 2: PO-Nummer als optionale Spalte / Reihen-Hinweis
  • PO-Detail-Modal: CTA „Verwaiste Buchungen anzeigen” (vorgefilterter Reports-View)

[1.11.0] — 2026-05-31

Bestellnummern als First-Class-Budget + Empfänger-Cascade.

Spec: specs/v1.11-purchase-orders.md (Session 20). Reaktion auf Tester- Praxis: Standard-Aufträge wie Projektmanagement, Beratungsmandate oder Controlling-Oberhäupter laufen am gleichen Job über Monate/Jahre weiter, bekommen aber regelmäßig neue Bestellnummern mit neuem Budget. Bisheriges Modell (PO-Nummer als String an Job/Projekt + Budget-Felder) konnte das nicht abbilden. Außerdem: Verhandlungs-Lücke (altes Budget auf, neue PO noch nicht da) ist Realität und musste ohne Workaround machbar sein.

Neu — Datenmodell (Migration 036)

Neue Entität purchase_orders:

  • Hängt entweder am Projekt ODER am Job (XOR-Constraint).
  • Genau eine aktive PO pro Träger gleichzeitig (partial unique index).
  • Budget in Stunden ODER Geld (CHECK: mind. eine Dimension).
  • 5 Status-Werte: active, exhausted, expired, cancelled, awaiting_replacement.
  • PO-Nummer eindeutig pro (tenant_id, account_id).
  • Recipient-XOR-Felder: interner User (FK auf users) ODER externe Mail-Adresse, plus Name + Postanschrift.
  • created_by-Tracking, created_at, updated_at.

Plus:

  • time_entries.purchase_order_id als nullable FK (NULL = Verhandlungs- Lücke oder Account ohne PO-Modell).
  • purchase_order_history für Status-Wechsel-Audit (manual, budget_exhausted, date_expired, backfill_overflow).
  • Recipient-Override-Felder an tenants/accounts/projects/jobs mit XOR-Constraint pro Ebene (außer Tenants: nur Default-Felder).
  • accounts.po_enforcement (off/warn/strict, Default off).
  • tenants.default_po_enforcement_for_new_accounts (Tenant-weiter Default für neue Accounts).

Neu — Bestandsmapping

Migration 036 wandelt aktiv um: bestehende order_number + budget_* an Projects/Jobs werden zu POs (1:1, status='active', start_date = budget_start ?? created_at). Bestehende time_entries werden auf die passende PO verlinkt (Job-PO gewinnt — XOR-Regel). Pre-Migration-Backup-Hook (erweitert in main.go) sichert alle Tenants vorher mit Filename-Prefix pre-v1.11-036_*.json.gz.

Neu — Backend

recipient_helpers.go: Cascade-Resolver resolveRecipientForTime Entry läuft die Reihenfolge PO → Job → Project → Account → Tenant durch und liefert ein normalisiertes ResolvedRecipient-Struct (Source, Type internal/external, UserID, DisplayName, Email, PostalAddress).

handlers_purchase_orders.go: CRUD-Endpoints.

  • GET /purchase-orders (Filter: account/project/job/status)
  • GET /purchase-orders/{id} und /budget
  • POST /purchase-orders (Manager+): legt PO an; wenn am Träger schon eine active-PO existiert, schaltet sie auf expired (Serializable-Tx).
  • PUT /purchase-orders/{id} (Manager+): dynamisches UPDATE; Status- Wechsel landet in History.
  • DELETE /purchase-orders/{id} (Admin+): nur ohne hängende Buchungen.
  • POST /purchase-orders/{id}/exhaust (manueller Status-Switch + async Notification).
  • POST /purchase-orders/{id}/backfill mit Dry-Run-Preview + Apply: ordnet alle Buchungen ohne PO im gewählten Zeitraum der Ziel-PO zu (topologisch matchend). Bei Überlauf auto-switch auf exhausted.

po_lifecycle.go:

  • PoBudgetSnapshot mit Live-Verbrauch (SUM(billable_seconds)/3600)
    • Cascade-Rate (Job → Project → Account) für Geld-Budgets.
  • resolveActivePOForJob: Job-PO gewinnt, sonst Project-PO, sonst NULL.
  • checkAndApplyBudgetExhaustion: idempotent, läuft nach jedem Booking-Insert; setzt PO auf exhausted + History + Notification.
  • notifyPOExhausted: 2-stufige Cascade (PO-Recipient → Account- Recipient → leer), async.
  • computeBudgetSplit: immer-splitten wenn eine Buchung das PO- Budget überschreitet. Linear interpoliert; erstes Sub-Segment an die alte PO (die genau dabei erschöpft wird), zweites an die Nachfolge-PO falls schon eingetragen, sonst NULL (Backfill-Pool). Cross-Day-Komposition: erst Mitternachtsschnitt, dann Budget-Split pro Day-Segment.

Booking-Insert-Pfade umgestellt:

  • handlers_timers.go stopTimer + createEntry
  • handlers_timer_cleanup.go autoStopTimer
  • handlers_time_import.go importRow
  • handlers_planned.go activate
  • handlers_entries.go PUT Cross-Day-Folge-Segmente Jeder Pfad resolved die aktive PO, schreibt sie in purchase_order_id und ruft nach dem Insert checkAndApplyBudgetExhaustion.

Restore-Pfad in handlers_backup.go erweitert: restoreDeleteOrder, restoreInsertOrder und backupTableList kennen jetzt purchase_orders und purchase_order_history in FK-sicherer Reihenfolge.

Neu — Frontend Web

Neue Komponenten unter frontend/src/features/purchase-orders/:

  • PurchaseOrdersTab — wiederverwendbar in JobPage + ProjectPage. Zeigt oben die aktive PO als prominente Karte mit Budget-Gauge (Web-Farblogik grün < 51 % / gelb 51–75 % / rot 76–99 % / pink ≥ 100 %), darunter historische POs als kompakte Tabelle. Lädt POs + Budget- Snapshots parallel.
  • NewPurchaseOrderModal — Toggle Stunden/Geld, 11 Währungen, Start- und End-Datum, Notizen.
  • PurchaseOrderDetailModal — Edit (end_date, notes), Budget- Status, Lifecycle-Aktionen (manuell exhausten bei active, Backfill-CTA bei exhausted/expired).
  • POBackfillModal — Zwei-Stufen-Wizard mit Zeitraum-Auswahl, Dry-Run-Vorschau (Anzahl, Stunden, would_exceed-Warnung), Übernehmen.

Types in frontend/src/types/index.ts: PurchaseOrder, PoBudgetSnapshot, PurchaseOrderStatus, POEnforcementMode, POBackfillDryRunResponse, POBackfillApplyResponse.

i18n: ~60 neue Keys unter po.* (de + en) inkl. ausführlicher Inline-Help-Texte zu Konzepten (Auto-Switch, Backfill, Verhandlungs- Lücke).

JobPage + ProjectPage zeigen den PurchaseOrdersTab unter den Eigenschaften, vor Buchungen/Jobs.

Neu — iOS v1.4.0 + macOS v1.2.0 (Read-Only)

  • TimeEntry.purchaseOrderID: String? decoded aus purchase_order_id.
  • iOS EntriesView: kleiner lila „PO”-Badge unter der Projekt-Zeile.
  • macOS EntriesView: lila Zahlen-Icon hinter dem Job-Namen.
  • PO-Management läuft bewusst nur über Web — Apps sind nur Read.

Verhalten

  • Ab v1.11 trägt jede neue Buchung automatisch die zum Zeitpunkt aktive PO (Job-PO gewinnt > Project-PO > NULL).
  • Budget-Erreichen schaltet die PO auto auf exhausted, schickt Notification an PO-Empfänger (via Cascade), nachfolgende Buchungen laufen mit purchase_order_id = NULL.
  • Manager räumt die Lücke nach Eintrag der neuen PO über das Backfill- Modal auf (Dry-Run zeigt vor Apply was passieren würde).

Offen für nachfolgende Iterationen

  • Recipient-XOR-Forms in Account/Project/Job-Edit (internal/external)
  • OrphansBanner im Dashboard („X Buchungen ohne PO seit Y Tagen”)
  • PDF-Modal-Erweiterung „Pro Bestellnummer” (Gruppierung + Header)
  • JobPage/ProjectPage: bei po_enforcement aktiv die alten order_number/budget_*-Felder ausblenden
  • Tenant-Settings: default_po_enforcement_for_new_accounts-Switch
  • iOS/macOS: PO-Liste + Anlegen
  • Bulk-PDF pro (Account × PO) — kommt mit v2.0 SR2

[1.10.0] — 2026-05-30

Datenmodell-Schnitt: „Gebucht / Abrechenbar” statt „Dauer / Abr. %” / „Brutto / Netto”.

Strukturelle Vereinfachung über alle Komponenten (Backend, Web, iOS, macOS, PDF). Tester-Feedback: das alte billable_percent wurde in der Praxis nie verwendet; die Trennung in „Brutto / Netto” über Rundungsregel war konzeptionell nicht belastbar (Netto war nur eine Funktion vom Brutto, nicht eigenständig editierbar).

Geändert — Datenmodell

time_entries wird per Migration 035 umgebaut:

  • Neu: booked_seconds (gearbeitete Zeit, vorher duration_rounded_seconds)
  • Neu/Konsolidiert: billable_seconds (abrechenbare Zeit, war seit Migration 009 da aber nie konsistent befüllt — wird beim Schnitt aus Bestand neu berechnet)
  • Raus: duration_seconds, duration_rounded_seconds, billable_percent

Befüllungs-Logik beim Schnitt:

booked_seconds   = duration_rounded_seconds ?? duration_seconds ?? 0
billable_seconds = booked_seconds × billable_percent / 100

Plus Job-Typ-Overrides (greift auf jobs.billing_type zu):

  • not_billablebillable_seconds = 0
  • billable_fixedbillable_seconds = booked_seconds (Aufwand zur Information; Pauschale wird separat verrechnet)
  • billable_hourly → unverändert (Wert aus Formel oben)

NOT NULL + CHECK ≥ 0 Constraints, neuer Index auf billable_seconds > 0.

Neu — Pre-Migration-Backup-Hook + Restore-Adapter

main.go ruft vor runMigrations() einen neuen Hook preMigration BackupIfNeeded(ctx, db, cfg): wenn schema_migrations.version < 35 und Tenants existieren, läuft pro Tenant ein vollständiger JSON-Snapshot mit Dateiname-Marker pre-v1.10-035_<timestamp>.json.gz. Bei Fehler → harter Abbruch, Migration läuft NICHT ohne Sicherung.

handlers_backup.go performRestore erkennt Backups VOR Migration 035 (am Vorhandensein von duration_seconds / duration_rounded_seconds / billable_percent im Record-JSON) und rechnet sie über transformLegacyTimeEntriesForRestore ins neue Schema um. Adapter bleibt bis einschließlich v2.0 stehen.

Geändert — Backend-Handler

  • Neuer Helper billing_helpers.go: zentralisiert die Job-Typ-Logik (billableSecondsForJob, billableSecondsForBookingChange, clampBillable).
  • Schreibpfade: Timer-Stop, Auto-Stop (systemd), PUT /entries (inkl. Audit-Insert in time_entry_history.old_data), Bulk-Edit, Plan- Aktivierung, CSV-Import — alle schreiben jetzt booked_seconds + billable_seconds mit Job-Typ-Override.
  • Read-/Aggregat-Pfade: Export-Handler (CSV/PDF/JSON/XML), ZIP-Export, AccountsDetail (JobEntry-Amount = billable_seconds × Stundensatz / 3600), Auslastungs-Tab, Reports, Dashboard, Projects, Admin-Stats — alle SQLs umgestellt.
  • API-Typen (swagger_types.go): UpdateEntryRequest + BulkUpdateChanges nutzen BookedSeconds + BillableSeconds statt BillablePercent.

Geändert — Frontend Web

  • Types: TimeEntry (types/index.ts) + JobEntry (features/accounts/types.ts) auf booked_seconds + billable_seconds.
  • 4 Inline-Edit-Pfade (EntryEditModal, Entries, Reports, JobPage) haben zwei HH:MM-Eingabefelder „Gebucht” + „Abrechenbar” statt einem „Abr.%“-Number-Field. Neuer Helper secondsToHHMM / parseHHMM.
  • Tabellen (Entries, Reports, JobPage): „Dauer” zeigt booked_seconds, neue Spalte „Abrechenbar” zeigt billable_seconds in HH:MM mit Amber-Warnfarbe wenn billable < booked.
  • NewBookingModal: „Abrechnung in %“-Field komplett raus (Backend rechnet billable per Job-Typ beim Insert; Override später via Edit-Pfad).
  • BulkEditModal: „Abrechnung in %” → „Abrechenbare Zeit (HH:MM)”.
  • Dashboard: DashEntry.duration_secondsbooked_seconds.
  • PDF-Modal: showBillablePct-Option entfernt.
  • PDF-Template (customer_page.html.tmpl): „Brutto/Netto” → „Gebucht/ Abrechenbar” über alle Stellen (KPI-Labels, Tabellenkopf, Job-Total-Text).
  • i18n (de + en): ~10 Keys umbenannt (entry_edit.billable_percententry_edit.booked + .billable; entries.col.billable_pctentries.col.billable; analog Reports, Bulk-Edit).

Geändert — iOS, macOS

  • iOS v1.3.0 (MARKETING_VERSION 1.2.1 → 1.3.0) + macOS v1.1.0 (MARKETING_VERSION 1.0.0 → 1.1.0).
  • TimeEntry-Struct auf bookedSeconds + billableSeconds, CodingKeys auf booked_seconds / billable_seconds.
  • EntriesView Property-Refs aktualisiert (Display + Summen).
  • NewBookingSheet: Abrechnung-%-Section + State entfernt; Backend setzt billable_seconds per Job-Typ.
  • iOS SettingsView releaseDate-Switch um v1.2.1 + v1.3.0 erweitert.
  • watchOS unverändert (keine Buchungs-Schemata in WatchModels).

Audit-Trail

Bei jedem PUT /entries werden die alten Werte in time_entry_history.old_data (JSONB) gespeichert — inkl. booked_seconds, billable_seconds, started_at, ended_at, notes, job_id. Tisch existiert seit Migration 005, wird ab v1.10 für booked/billable- Änderungen verpflichtend befüllt.

Rollen-Logik

Edit-Pfad (PUT /entries) respektiert die bekannten Rollen-Regeln:

  • Bucher: darf eigene Buchungen in beide Richtungen anpassen (booked
    • billable). Audit-Eintrag Pflicht.
  • Manager/Admin/Super-Admin: alle Buchungen im Tenant.
  • Period-Locks: greifen für alle Rollen einheitlich.

Job-Typ-Constraints werden im Backend unabhängig vom UI durchgesetzt: not_billable zwingt billable_seconds = 0; billable_fixed zwingt billable_seconds = booked_seconds.

Big-Bang-Deploy

Datenmodell-Schnitt läuft ohne v-1-Kompatibilität. Vorbereitungen mitgeliefert:

  1. Pre-Migration-Backup pro Tenant (siehe oben) — Sicherungsnetz gegen Migrations-Fehler.
  2. Restore-Adapter für historische Backups — kein stiller Datenverlust beim Wiederherstellen alter Stände.
  3. iOS-App war zum Schnitt nicht im Store / Beta-Review-Status. macOS-App lief nur als DMG-Distribution.
  4. Browser-Cache: Tester werden per Mail über den Schnitt informiert, Hard-Refresh-Anweisung.

Offen für nachfolgende Iterationen

  • iOS / macOS Edit-Pfad für Buchungen (HH:MM Inputs analog zur Web-Inline- Edit) — derzeit nur Anzeige.
  • Job-Typ-Wechsel-Modal in JobPage (Web) mit Empfehlung „neuen Job anlegen” bei vorhandenen Buchungen.
  • Audit-UI Hover-Icon „letzte Änderung” in den Inline-Edit-Zeilen.
  • docs/docs.go per swag init regenerieren (Swagger-UI zeigt sonst noch die alten Request-Schemas).
  • History & Logs als eigenes Konzept (Spec-Workshop vor v1.11).

[1.9.2] — 2026-05-29

KPI-Reihe ruhig gestellt — Accounts / Projekte / Jobs nur als reine Zahl, letzte KPI rechtsbündig.

Kleiner Polish direkt nach v1.9.1: das Ratio-Format X / Y war zu informativ für eine KPI-Reihe und brachte ein zweites Farbsignal (lila) in Spalten, die eigentlich ruhig laufen sollen. Außerdem lief die rechte KPI-Kante optisch nicht mit der Page-Margin mit.

Geändert — Accounts / Projekte / Jobs nur als reine Zahl, schwarz

Das <span class="hi">X</span> / Y-Konstrukt + .kpi.ratio .value .hi { color: var(--primary); }-Regel ist raus. Die drei KPIs zeigen jetzt nur {{.Customers}}, {{.Projects}}, {{.Jobs}} — gleicher Geist ExtraLight Schwarz-Stil wie Buchungen / Werktage. Die Sub-Zeile „mit Buchungen” bleibt.

Die Backend-Felder AccountsTotal / ProjectsTotal / JobsTotal und loadTenantTotals bleiben im Code stehen — werden später für eine optionale „X / Y”-Detailansicht oder einen Tenant-Insights-Block wiederverwendbar sein.

Geändert — Letzte KPI rechtsbündig

.kpi-row > .kpi:last-child { text-align: right; } — Label, Value und Sub der letzten Kachel („Ø pro Tag”) fluchten jetzt mit der rechten Seitenmarge, genauso wie der Mandanten-Name im Page-Header und der Page-Counter im Footer. Macht die Reihe optisch ruhiger.


[1.9.1] — 2026-05-29

Polish auf dem Premium-PDF — echte Projekt-Budgets aus der DB, KPI-Split, Account-Spalte dynamisch.

Patch direkt nach v1.9.0, nachdem die erste Live-Render-Iteration auf Produktiv- Daten von ATAMYA GmbH gezeigt hat: lange Account-Namen brachen mehrzeilig um, keine Budget-Gauges erschienen (Sample-Overlay matchte natürlich nicht auf die echten Firmen-Namen), und die Cover-KPIs hatten „Kunden” mit Projekte · Jobs als Sub — der Sinn dahinter geht in der dichten Darstellung verloren.

Geändert — Echte Projekt-Budgets aus der DB

applyRealProjectBudgets (in pdf_premium.go) lädt nach applySampleOverlay alle aktiven Projekte des Tenants mit budget_hours OR budget_amount gesetzt und stellt sie pro projectSection ein. Used = HoursNet bei Stunden-Budget, HoursNet × effektive Stundenrate bei Betrags-Budget (Cascade COALESCE(p.hourly_rate, a.hourly_rate, 0)). Währungs-Glyphen über currencyGlyph() (€/$/£/CHF/Fallback Code).

Das Sample-Overlay liefert ab v1.9.1 nur noch Demo-Adressen + Demo-Rollen für die vier hardcoded Demo-Accounts (bis Migration 034 die echten DB- Felder einführt). Budget-Logik dort komplett gestrichen — produktive Tenants sehen jetzt ihre real hinterlegten Budgets, statt nichts.

Geändert — Cover-KPIs: 5 Tiles → 7 Tiles, „Kunden” → 3 Ratio-KPIs

Aus der einen „Kunden”-Kachel werden drei separate „Accounts” / „Projekte” / „Jobs”-Kacheln im Format X / Y mit X = mit Buchungen im Berichtszeitraum (= Customers / Projects / Jobs aus dem Builder), Y = aktiv insgesamt im Tenant (neu via loadTenantTotals). Erste Zahl in #9D2AC6 (.ratio .hi), zweite normal (Default-Body-Text). Sub „mit Buchungen”.

Grid-Layout im KPI-Row schaltet auf 7 Spalten mit kleinerem Gap (10pt statt 18pt) um, sobald .kpi-row-7 gesetzt ist.

Globales Wording: „Kunden” → „Accounts” in der Cover-Übersicht (siehe v1.9.0 Eyebrow „Übersicht nach Account”) — die KPI folgt jetzt.

Geändert — Account-Spalte in Cover-Übersicht dynamisch

<th>Account</th> hat keine feste Breite mehr, und .cover-overview .ku-name bekommt white-space: nowrap — die Account-Spalte wächst mit dem längsten Namen, die Anmerkung-Spalte gibt nach (selten lang befüllt). Lange Firmennamen wie „Christ Juweliere und Uhrmacherseit 1863 GmbH” oder „RUD Ketten Rieger & Dietz GmbH u. Co. KG” stehen jetzt einzeilig.

Geändert — Account-Detail-Header expandiert ohne Rechnungsadresse

Wenn weder ContactPerson noch InvoiceAddress gepflegt sind, schaltet .kunde-head.no-invoice das Grid von 1.2fr 1fr 0.9fr auf 1fr auto um und versteckt den Invoice-Block. Der Account-Name nimmt die freie Mitte ein — Namen wie „Christ Juweliere und Uhrmacherseit 1863 GmbH” brechen nicht mehr drei Zeilen um.

Geändert — pdfFmtHHMM-Nachzug

AvgPerWorkDayLbl enthält jetzt nur die Zahl ohne „ h”; das Template hängt die Einheit als <span class="unit"> an, damit die Cover-KPI „Ø pro Tag” konsistent mit den anderen Stunden-KPIs aussieht.

Roadmap-Notiz

Sortierung der Übersicht als künftige Option ins v2.0-Modal-Backlog geschrieben (Default „nach Stunden netto”, optional alphabetisch nach Account-Name + perspektivisch in den Tenant-Defaults).


[1.9.0] — 2026-05-29

Premium-PDF Layout 2 — editorial Cover + per-Account-Detailseiten (Vorschau im Test-Endpoint).

Iteration mit @kaiwarmus über mehrere Sessions (24.–28.05.): aus dem Design-Entwurf „Stundennachweis 2.pdf” entsteht eine vollständige neue PDF-Vorlage im editorial Bank-Statement-Stil. In v1.9 ist sie über /export/test-customer.pdf erreichbar (Layout-2-Test-Button im PDF-Export-Modal). Die produktive entries.pdf bleibt vorerst Layout 1, bis v2.0 die Umstellung mitnimmt.

Neu — Premium-PDF Layout 2

  • Cover-Seite: Mandanten-Name (Geist Thin 100, 75 % Schwarz, links bündig)
    • Mandanten-Logo-Platzhalter rechts, darunter 0,25 pt Trennlinie in daagwerk-Lila #9D2AC6.
  • Eyebrow-Zeile: „Auswertung · Periode · Vertraulichkeit (pink) · Erzeugt von Owner · Übersicht nach Account” in Geist Thin Letter-Spacing 0.18em uppercase, 100 % Schwarz auf der Cover-Seite, 75 % Schwarz auf den Folgeseiten als Running Header.
  • Headline „Aufwands- und Stundennachweis” in Geist ExtraLight 28 pt.
  • Fünf KPI-Kacheln: Stunden netto als Leit-KPI in Geist Light lila, die übrigen vier (Buchungen / Kunden / Werktage / Ø pro Tag) in Geist ExtraLight Schwarz; Labels und Sub-Zeilen in Geist Thin, 75 % Schwarz.
  • Übersichts-Tabelle „Account · Anmerkung · Projekte/Jobs · Buchungen · Stunden netto” mit Spaltenbreiten 22 / 34 / 16 / 12 / 16 %.
  • Pro Account eine Detailseite mit Page-Header (gespiegelt zur Cover- Eyebrow), Account-Name in Geist Light 22 pt lila, Rechnungsadresse + Ansprechpartner, Stunden-netto-Box rechts mit Brutto/Differenz.
  • Pro Projekt ein Header mit Halbkreis-Budget-Gauge (Inline-SVG, dynamic stroke-dasharray), Web-Farblogik grün < 51 % / gelb 51–75 % / rot 76–99 % / pink ≥ 100 % + „Überzogen”-Pille; Soll/Ist-Box mit Stunden- oder Betrags-Budget; Projekt-Netto rechts.
  • Pro Job ein Header mit Netto-/Brutto-Total und Buchungs-Tabelle (Datum · Von—Bis · Brutto · Netto · Nutzer · Abr.% · Status · Notiz) in Spaltenverteilung 7 / 11 / 7 / 7 / 14 / 6 / 9 / 39 %.
  • Tabellen brechen sauber über Seiten: thead wiederholt sich, einzelne Buchungszeilen bleiben atomar, Projekt- und Job-Header kleben am ersten Inhalt darunter.
  • Footer komplett über @page @bottom-*-Margin-Boxes (counter(page) / counter(pages) sind in position: fixed in Chromium unzuverlässig); durchgehende 0,25 pt Linie über alle drei Bottom-Boxes in 75 % Lila, „SEITE n VON m” rechtsbündig in Geist Thin.

Neu — Schriften lokal eingebettet

  • Geist als WOFF2: Thin (100) und ExtraLight (200) frisch aus fontsource ergänzt, plus die bereits vorhandenen Light (300), Regular (400), Medium (500), SemiBold (600). Geist Mono Regular/Medium als Fallback.
  • Instrument Serif Italic — nur noch als Akzent für den Tenant-Brand- Schriftzug, alle anderen Headlines auf Geist umgestellt.
  • buildFontFaceBlock in pdf_templates.go um Thin- und ExtraLight- Mapping erweitert.

Neu — Design-Tokens in premium.css

--primary: #9D2AC6  (daagwerk Lila, Hauptakzent + Hervorhebungen)
--ok:      #A1E929  (Lime, Budget < 51 %)
--warn:    #F7C619  (Gelb, Budget 51–75 %)
--danger:  #E03B3B  (Rot, Budget 76–99 %)
--over:    #F72A7C  (Magenta, Budget ≥ 100 % + Vertraulichkeits-Marker)

Alle Zahlen mit font-feature-settings: "tnum" 1 (tabular-nums), damit Stunden- und Geldspalten sauber untereinander bleiben.

Neu — Sample-Overlay für Demo-Daten

pdf_premium_sample.go befüllt vier Demo-Accounts (ACME Corp., ATAMYA GmbH, Muster GmbH, Mainufer Bank AG) mit Rechnungsadressen, Ansprechpartnern und Projekt-Budgets. Wird in v2.0 entfernt, sobald Migration 034 die Felder auf accounts trägt und der /export/entries.pdf?layout=2-Datenadapter direkt aus der DB liest.

Neu — Frontend: „Layout 2 (Test)“-Button im PDF-Export-Modal

In Reports.tsx unten links im PDF-Modal ein gestrichelter Mini-Button. Klick zieht die aktuell gesetzten Filter heran, ruft /export/test-customer.pdf via Axios (inkl. JWT) und öffnet das PDF in einem neuen Tab. Bewusst kein Download — beim Iterieren am Layout würde sonst der Downloads-Ordner geflutet.

Geändert — pdfFmtHHMM

Liefert ab jetzt nur „HH:MM” (vorher „HH:MM h”). Die Einheit hängt das Template separat an, damit sie stylebar wird (z. B. dünner als die Zahl, oder lila als Akzent) und nicht doppelt rendert. Nur das Premium-Layout nutzt die Funktion — der bestehende report.html.tmpl- Pfad (Layout 1) ist nicht betroffen.

Geändert — Test-Endpoint zieht Tenant-Namen aus der DB

/export/test-customer.pdf liest tenants.name statt des hartem „daagwerk”-Defaults, sodass im Brand-Spot der echte Mandantenname steht. Fallback bleibt „daagwerk” bei leerer Spalte.

Offen für v2.0

  • Migration 034: accounts.account_manager + customer_success + contact_person (löst Sample-Overlay ab)
  • AccountPage Edit-Form um die drei Felder erweitern
  • /export/entries.pdf?layout=2 mit echten DB-Daten
  • PDF-Modal: Layout-Switcher prominent statt Test-Button, Chronologisch aus den Gruppierungen entfernen, neue Optionen (Brutto/Netto-Anzeige, Brutto/Netto-Differenz, Vertraulichkeits- Free-Text)
  • Bulk-Export pro Account als ZIP (/export/entries-bulk.zip)
  • Echte Werktags-Logik mit Feiertags-Abzug aus holidays_de.go (aktuell nur Mo–Fr-Count ohne Feiertage)
  • Running Header auf Folgeseiten innerhalb desselben Accounts („… Forts.”) — derzeit nur auf erster Seite des Accounts sichtbar
  • Tenant-Custom-Branding (Logo) — Migration 031 (tenant_branding) ist da, scharf schalten in v2.0 SR2

[1.8.0] — 2026-05-23

Manuelle Buchungen ohne Timer-Umweg — neuer Sidebar-Button + Modal.

Reaktion auf konvergierendes Tester-Feedback (Asana-Tickets 1215050122488119 „Neue Buchung mit Start/Ende erweitern” + 1215042790826420 „Unter Timer starten zusätzlicher Button”): Wer Zeiten nachträglich einträgt — etwa nach einem Kunden- Termin oder offline — musste bisher den Umweg „Timer starten → sofort stoppen → in Meine Buchungen wechseln → bearbeiten” gehen. Mit v1.8.0 gibt es einen direkten Pfad.

Neu — „Buchung eintragen”-Button in der Sidebar

Direkt unter dem lime-grünen „Timer starten”-Button sitzt jetzt ein zweiter Button mit lila Outline (#9D2AC6, daagwerk-Purple) und Diskette-Icon. Hover füllt sich solid lila, Klick öffnet das neue NewBookingModal — analog zum Timer-Modal über ein Window-Event (daagwerk:open-new-booking), damit die Aktion von jeder Seite aus erreichbar ist.

Neu — NewBookingModal mit voller Buchungs-Mechanik

Inhalt analog zum Mockup aus Ticket 1215050122488119:

  • Job-Picker mit Volltextsuche (gleiche JobPicker-Komponente wie Timer-Modal)
  • Start + Ende als zwei DateTime-Picker nebeneinander (flatpickr, deutsche Lokalisierung, Default: jetzt minus eine Stunde bis jetzt)
  • Notiz mit Template-Picker
  • Abrechnung in % (Default 100, 0–100 erlaubt)
  • Nutzer-Dropdown — nur sichtbar für Manager/Admin/Super-Admin, lädt /users und erlaubt die Vertretungs-Buchung für andere Team-Mitglieder. Bucher sehen das Feld nicht und buchen immer für sich selbst.
  • Hinweis-Zeile „Es gibt kein Undo — die Buchung wird direkt gespeichert.”
  • Inline-Validierung: Ende vor Start zeigt sofort einen roten Hinweis, Speichern-Button bleibt disabled. Server gibt zusätzlich error.ended_after_started als Fallback.

Auf erfolgreiches Speichern feuert das Modal daagwerk:entry-createdEntries.tsx lauscht und refetched die Liste, der neue Eintrag erscheint sofort.

Geändert — Backend POST /entries erweitert

handlers_timers.go createEntry akzeptiert jetzt zwei zusätzliche optionale Felder im Request-Body:

  • user_id — Manager/Admin/Super-Admin können Buchungen für andere User anlegen. Bucher die das Feld trotzdem schicken werden ignoriert. Der angegebene User wird gegen den eigenen Tenant verifiziert, fremde User führen zu error.user_not_found.
  • billable_percent — Abrechnungs-Prozentsatz für die manuelle Buchung (Default 100, Clamping auf 0–100). Wandert direkt in die time_entries.billable_percent-Spalte.

Cross-Day-Split (seit v1.7.3) greift unverändert — mehrtägige manuelle Buchungen werden weiterhin an der Mitternachtsgrenze der Tenant-Timezone in tagesweise time_entries zerlegt, beide neuen Felder kommen pro Segment in denselben INSERT.

Hinweis für iOS- und macOS-App-Tester

Die zwei Apps bekommen das gleiche Pattern in der nächsten Iteration. Für v1.8.0 ist die Funktion zunächst nur im Web verfügbar.


[1.7.3] — 2026-05-21

Polish-Tag direkt nach v1.7.2. Drei voneinander unabhängige Aufräumarbeiten, die schon länger auf der Liste standen und in dem Augenblick erledigt waren wo man sie zusammen anfasst.

Behoben — Cross-Day-Buchungen werden jetzt automatisch gesplittet

Wenn ein Timer über Mitternacht lief (oder eine manuell angelegte Buchung über Mitternacht reichte), landete die volle Dauer am Start-Tag. Der Auslastungs-Tab zeigte dadurch unmögliche Werte wie 25,8 h an einem einzelnen Kalendertag.

Backend zerlegt diese Bereiche jetzt an der Mitternachts-Grenze der Tenant- Timezone (tenants.timezone, Default Europe/Berlin) in tagesweise time_entries-Segmente. Pro Segment wird die Rundungsregel sauber neu angewendet. Betroffen sind drei Pfade:

  • Timer stoppen (POST /timers/{id}/stop): wer den Timer um 23:50 startet und am nächsten Morgen 09:00 stoppt, bekommt zwei Buchungen — eine von 23:50 bis 24:00 am Vortag, eine von 00:00 bis 09:00 am Folgetag.
  • Buchung anlegen (POST /entries): manuelles Anlegen einer Cross-Day- Buchung über das Bearbeiten-Modal wird gleich gesplittet.
  • Buchung bearbeiten (PUT /entries/{id}): das erste Tages-Segment ersetzt die ursprüngliche Buchung (Original-ID bleibt erhalten), zusätzliche Tages-Segmente werden als neue Einträge angelegt. Frontend refetched seine Liste und sieht die zusätzlichen Tage automatisch.

Helper-Funktionen in der neuen Datei cross_day_split.go. CSV-Zeitbuchungs- Import erzeugt strukturell keine Cross-Day-Einträge (Start- und End-Time teilen sich eine date-Spalte), daher dort kein Eingriff nötig.

Neu — Budget-CSV-Export auf der Budgets-Seite

Der Backend-Endpoint GET /export/budget.csv existierte seit Längerem, hatte aber keinen UI-Button. Neuer „CSV-Export”-Button rechts neben der Speichern- Suche in der Budgets-Filterleiste. Lädt für alle Projekte mit hinterlegtem Budget den aktuellen Stand als Semikolon-CSV (BOM für Excel) — Kunde, Projekt, Budget-/Ist-Stunden, Auslastung, Beträge, Zeitraum.

Entfernt — ChartLab-Playground

Die Datei frontend/src/pages/ChartLab.tsx (~900 Z. reines SVG) wurde mit dem v1.4-Re-Design obsolet. Die Budgets-Seite löst die Gauge-/Balken-Visualisierung abschließend, der Auslastungs-Tab deckt Wochen-/Monats-/Jahres-Sicht ab. Datei + verbleibende Notizen in CLAUDE.md aufgeräumt. Spart ~900 Zeilen toten Code, keine Verhaltens-Änderung.


[1.7.2] — 2026-05-21

Globaler „Timer starten”-Button in der Sidebar + Schnellstart aus Job-Listen.

Quick-Patch direkt nach v1.7.1: ein Timer ließ sich bisher nur über die Timer-Seite oder Favoriten starten. Wer gerade in einem Account, Projekt oder Bericht arbeitete, musste erst dort hin navigieren. v1.7.2 macht den Timer-Start von jeder Seite aus erreichbar.

Neu — „Timer starten”-Button in der Sidebar

Direkt unter dem daagwerk-Logo, oberhalb der Navigation: ein lime-grüner Button (#A1E929) mit Play-Icon und der Beschriftung „Timer starten”. Klick öffnet ein Modal mit der gewohnten Job-Suche (Volltext über Account · Projekt · Job, mit Cascade-Fallback), Notiz-Feld und Template-Picker.

  • Funktioniert von jeder Seite aus (Dashboard, Berichte, Buchungen, Einstellungen, etc.) — kein Seitenwechsel mehr nötig.
  • Architektur: Eine zentrale NewTimerModal-Komponente lebt im Layout-Mount, wird über das Window-Event daagwerk:open-new-timer getriggert. Timer-Seite nutzt dasselbe Modal — keine Code-Duplizierung.
  • Nach erfolgreichem Start feuert ein daagwerk:timer-started-Event, damit offene Listen (z. B. Timer-Seite) sich refetchen.

Neu — Play-Button (▶) in Job-Listen

In der Accounts-Übersicht (Default-Account-Jobs) und in jedem Projekt-Detail (Job-Tabelle) steht in jeder Zeile vorne ein kleiner lime-grüner Play-Button. Klick startet den Timer für genau diesen Job ohne Umweg über Modal oder Picker — ideal wenn man weiß welchen Job man buchen will.

  • Inaktive Jobs zeigen den Button ausgegraut (nicht klickbar).
  • Bei bereits laufendem Timer auf dem Job: Toast „Für diesen Job läuft bereits ein Timer.”
  • Bestehende Zeilen-Aktionen (→ Projekt, ↑ Account, Öffnen) bleiben unverändert.

Geändert — Timer-Seite nutzt das neue Modal

Die Timer-Seite hatte bisher einen eigenen Picker-Dialog mit gestapelten Account/Projekt/Job-Dropdowns. Dieser wurde entfernt — der „+ Neuer Timer”-Button im PageHeader öffnet jetzt das gleiche NewTimerModal wie der Sidebar-Button. Eine Quelle, ein Verhalten.


[1.7.1] — 2026-05-20

Edit-Buttons: Layout-Fix + visuelle Differenzierung.

Kleiner Patch direkt nach v1.7.0 — in der „Meine Buchungen”-Tabelle waren die beiden Edit-Buttons (Inline + Erweitert) untereinander gestapelt statt nebeneinander. Außerdem optisch nicht klar unterscheidbar.

Behoben — Edit-Buttons in „Meine Buchungen” nicht mehr gestapelt

Die Aktionen-Spalte der Entries-Tabelle hatte eine fixe Breite von 48 px, zusammen mit table-layout: fixed. Der zweite Button überlief die Zelle und wurde abgeschnitten — beim Rendern in mehreren Browsern (Firefox, Safari) sahen User entweder gar nichts oder die Buttons vertikal gestapelt.

  • Aktionen-Spalte auf 100 px verbreitert, paddingRight auf 16 px erhöht für etwas Luft zum Tabellenrand.
  • Die zwei Buttons in einen inline-flex-Container mit gap: 4px gepackt, Zelle bekommt whiteSpace: nowrap damit der Browser nicht mehr versucht zu wrappen.

Verbessert — Edit-Buttons visuell differenziert

Beide Edit-Buttons sahen vorher identisch aus (graue Outline, Bleistift- Icon). User sollten auf einen Blick erkennen, welcher der „schnelle” und welcher der „erweiterte” Edit ist.

  • Inline-Edit (✏): weißer Hintergrund, 1,5 px Outline in #10A7BD (Teal), Bleistift in #10A7BD. Wirkt wie ein „leichter” Button für den schnellen Eingriff.
  • Erweitert-Edit (✏+): vollflächig #10A7BD-Hintergrund, weißer Bleistift + weißes „+”. Wirkt wie ein „voller” Button für den umfangreicheren Eingriff.

Gleiche Behandlung in den drei Stellen wo Buchungen editierbar sind: Entries.tsx (Meine Buchungen), Reports.tsx (Berichte) und JobPage.tsx (Job-Detail-Buchungstabelle).

Sonstiges

  • JobPage.tsx nutzt jetzt ebenfalls das IconPencil-SVG statt des Unicode-Bleistifts (✏) — konsistent zum Rest der App.

[1.7.0] — 2026-05-20

Volltext-Suche, Default-Account-Jobs, JobPicker mit Toggle, Eigenschaften-Collapse.

Großes UX-Release nach Rückmeldungen aus den ersten Echt-Tenants (109 aktive Accounts, 225 aktive Projekte, 1109 aktive Jobs). Vier voneinander unabhängige Bausteine — alle zusammen drehen einen Kernschmerz: Navigation in einer großen Hierarchie soll leicht sein, und die starre dreistufige Account→Projekt→Job-Pflicht passt nicht zu Bestandskunden ohne Projekt.

Neu — Volltext-Suche auf der Timer-Seite

Ganz oben über Laufende Timer / Favoriten / Letzte Jobs steht jetzt eine Schnellsuche, die direkt einen Timer startet wenn Du einen Treffer auswählst.

  • AND-Suche mit Substring, case-insensitive über Account · Projekt · Job (z. B. „müllermilch wartung” findet den eindeutigen Job auch ohne genaues Erinnern an den Projekt-Namen).
  • Globale Tastatur-Shortcuts: ⌘K (Mac) / Ctrl+K (Win) und / als Quick-Shortcut. Beide springen jederzeit ins Suchfeld, außer wenn Du gerade in einem anderen Input tippst (kein Fokus-Klau).
  • Auto-Focus beim Page-Load: tippe einfach drauflos.
  • Pfeil-Navigation hoch/runter, Enter startet den Timer für den aktiven Treffer. Mit der Maus: Hover färbt grün, ein Klick startet.
  • Highlighting der Suchbegriffe in den Treffern. Empty-State zeigt „Keine Treffer für „xyz”.”
  • Backend: neuer Endpoint GET /jobs/search?q=… mit ILIKE-Tokenisierung, alphabetisch sortiert, hart auf 30 Treffer gecapped. Filter: nur aktive Accounts/Projekte/Jobs.

Neu — Default-Account-Jobs (Jobs direkt am Account)

60–70 % unserer Accounts sind Bestandskunden ohne klassisches Projekt- geschäft. Für die musste man bisher ein Pseudo-Projekt anlegen, in dem der Job liegt. Mit v1.7 kann ein Job direkt am Account hängen.

  • AccountPage: neue hellgraue Sektion „Default Account Jobs” zwischen Eigenschaften und Projekte. Tabelle mit Job · Abrechnung · Stundensatz · Gebucht · Buchungen, plus Button „+ Job direkt am Account”.
  • Anlegen-Modal: Name + Abrechnungsart (stündlich / Festpreis / nicht abr.) + optionaler eigener Stundensatz oder Festpreis. Cascade-Logik für die Stundensatz-Berechnung (Job → Default-Projekt → Account) funktioniert unverändert weiter.
  • Migration 029: projects.is_default BOOLEAN NOT NULL DEFAULT FALSE
    • partieller Unique-Index, der genau ein Default-Projekt pro Account erlaubt. Race-safe Erzeugung via SELECT-then-INSERT-mit-Recovery.
  • Lazy-Create: das Default-Projekt wird erst erzeugt, wenn der erste Job direkt am Account angelegt oder dorthin verschoben wird. Bestehende Accounts ohne Default-Bedarf bleiben unverändert.
  • Move-Aktionen in beide Richtungen:
    • In der ProjectPage-Jobs-Tabelle: pro Zeile Button „↑ Account” → Confirm-Dialog → POST /jobs/{id}/move-to-account.
    • In der AccountPage-Default-Sektion: pro Zeile Button „→ Projekt” → Modal mit Projekt-Dropdown → POST /jobs/{id}/move-to-project.
  • Konsistente Anzeige in Listen (Entries-Tab + Reports-Tab): die Projekt-Spalte zeigt für Default-Jobs „(Default)” in grau-kursiv, Tooltip „(Default Account Jobs)”. Im CSV/JSON/XML-Export bleibt der Literal- String „(Default)” stehen damit Excel-Filter sauber funktionieren.
  • PDF-Export blendet (Default) aus: PDFs gehen zur visuellen Kontrolle an Kunden — der interne Marker hat da nichts zu suchen. Chronologische Ansicht zeigt „Account / Job” statt „Account / (Default) / Job”, hierarchische Ansicht skippt den Default-Sub-Header.

Neu — JobPicker mit Toggle Suche ↔ Auswahl

Bei 400 Kunden (Wachstumsschätzung +50–100 % auf die aktuellen 109) ist eine reine Suche frustrierend wenn der User den Namen nicht im Kopf hat. Der neue JobPicker bietet beides:

  • Default-Modus: Suche (nutzt die gleiche JobSearchBox wie auf der Timer-Seite, ohne Hotkey-Capture).
  • Toggle-Link „→ über Auswahl wählen” wechselt zu klassischen Cascading- Dropdowns (Account → Projekt → Job). Modus wird pro User in localStorage gemerkt — Power-User landen immer in der Suche, Klick- User immer im Cascade.
  • Default-Projekte erscheinen im Cascade-Modus als „(Default Account Jobs)” und werden im Projekt-Dropdown nach oben einsortiert.
  • Aktuell gewählter Job wird oben als Pille angezeigt mit ×-Button zum Löschen.
  • Eingesetzt im EntryEditModal („Erweitert bearbeiten”) und in der „Hierarchie verschieben”-FieldGroup des BulkEditModal. Auf der Timer-Seite bleibt es bei der reinen Suche, weil dort eh genug Browse- Sektionen (Favoriten, Letzte Jobs) für den Namen-Vergesser da sind.

Verbessert — Eigenschaften-Sektion zugeklappt

Auf den Detail-Seiten (Account, Projekt, Job) startet die Eigenschaften- Sektion jetzt zugeklappt, zeigt nur einen schmalen Streifen mit ▶-Caret, Titel und Bearbeiten-Button rechts.

  • Klick auf den Streifen → klappt zum Lesen auf.
  • „Bearbeiten” klickt → klappt auf + Edit-Mode mit Speichern/Abbrechen auf dem Streifen.
  • Speichern/Abbrechen → zurück in den vorherigen Zustand (offen wenn vorher zum Lesen geöffnet, sonst zu).
  • PageHeader-Bearbeiten-Button (oben rechts) entfernt — Edit ist jetzt nur noch auf dem Streifen.

Backend — neue Endpoints + Schema-Änderungen

  • GET /jobs/search?q=… — Volltext-Suche
  • POST /accounts/{accountID}/jobs/direct — Job direkt am Account
  • POST /jobs/{id}/move-to-account — Job in Default verschieben
  • POST /jobs/{id}/move-to-project — Job in Projekt verschieben
  • Migration 029 — projects.is_default
  • is_default-Felder in den Responses von /jobs, /jobs/recent, /jobs/search, /entries, /reports/entries
  • AccountDetail.default_jobs in /accounts/{id}/overview
  • swag init regeneriert für alle neuen Endpoints

Sonstige Änderungen

  • ROADMAP: User-Feedback-Punkte aus Session 14 in v1.7 abgeschlossen. Tagesansicht/Wochenansicht bleibt offen für eine spätere Iteration — ist als eigener Spec-Schritt zu planen.
  • .claude/launch.json lokal: cwd auf relativen Pfad geändert (nicht im Git, weil unter .claude/ gitignored).
  • i18n-Keys unter search.* und job_picker.* in DE+EN.

[1.6.0] — 2026-05-19

User-Feedback-Release: Polish + Letzte Jobs + Bulk-Edit Iteration 3.

Sammelt sechs Verbesserungen, die nach den ersten Rückmeldungen echter Nutzer entstanden sind. Bewusst als Minor-Release (v1.6.0) statt Patch, weil zwei neue Features dazukommen (Bulk-Datum/Uhrzeit + Letzte Jobs). Die Tagesansicht — ursprünglich als Highlight von v1.6 geplant — wandert in eine spätere Version (v1.6.1 oder v1.7).

Neu — „Letzte Jobs”-Sektion auf Timer-Seite

Dynamische Liste der zuletzt verwendeten Jobs unter den Favoriten, zum schnellen Neustart. Pro Nutzer im Profil konfigurierbar.

  • Migration 028: users.recent_jobs_limit SMALLINT NOT NULL DEFAULT 5 CHECK (>=0 AND <=25). 0 = Sektion komplett ausblenden.
  • Backend: neuer Endpoint GET /jobs/recent (favoriteHandler.recentJobs). Liefert die N zuletzt verwendeten distinkten Jobs, sortiert nach letzter Buchung, inactive/cancelled gefiltert. Limit aus users.recent_jobs_limit, defensiv geklemmt auf [0, 25].
  • Frontend: dritte Sektion auf /timer mit neutraler Farbe (var(--text-muted)), nutzt dieselbe FavCard-Komponente wie die bestehenden Favoriten. Nach Timer-Stop wird die Liste neu geladen.
  • Profil-Seite: neues Feld „Letzte Jobs auf der Timer-Seite” mit Hinweistext und Range-Limit.
  • GET /me + PUT /me um recent_jobs_limit erweitert.

Neu — Bulk-Edit Iteration 3: Datum + Uhrzeit im Bulk

In der Reports-Multi-Select-Toolbar lassen sich jetzt auch Datum und Uhrzeit (Start + Ende) auf mehrere Buchungen gleichzeitig anwenden — deckt den häufigen Fall „Meeting mit 5 Personen war auf dem falschen Tag oder mit falschen Uhrzeiten” sauber ab.

  • Backend (POST /entries/bulk-update): akzeptiert jetzt date (YYYY-MM-DD), start_time (HH:MM), end_time (HH:MM). Die Felder sind unabhängig kombinierbar: nur Datum schiebt Buchungen auf einen neuen Tag (Uhrzeiten bleiben), nur Uhrzeit vereinheitlicht die Zeiten (Datum bleibt), beides zusammen setzt alles fest.
  • Skip-Logik: bei Mehrtages-Buchungen, deren neues Ende vor dem neuen Start läge, wird der Eintrag mit error.end_before_start übersprungen — kein Update mit kaputter Validierung.
  • Datum/Zeit-Berechnung in der Tenant-Timezone (aus tenants.timezone, Default Europe/Berlin). PostgreSQL TIMESTAMPTZ konvertiert beim Schreiben automatisch nach UTC.
  • Dauer wird neu berechnet mit der Tenant-spezifischen Rundungsregel (tenants.rounding_minutes).
  • Frontend: zwei neue FieldGroups („Datum verschieben (Uhrzeit bleibt)” und „Uhrzeit setzen (Start + Ende)”) im BulkEditModal mit Checkbox-pro-Feld-Pattern, Validation und Preview im Confirmation-Step.
  • Fix: tzdata-Paket ins Alpine-Docker-Image installiert. Ohne das gab time.LoadLocation("Europe/Berlin") nil zurück und nachfolgende .In(nil)-Aufrufe haben paniciert. Defensive Fallback-Kaskade im Go-Code: tenant.timezone → Europe/Berlin → UTC.

Verbessert — DateTime-Eingabe „Harvest-Stil”

Die Eingabe für Datum/Uhrzeit-Felder war umständlich, weil der flatpickr-Popup automatisch bei Klick ins Feld aufging und den Tippe- Bereich verdeckte. Jetzt:

  • clickOpens: false + wrap: true in der DateInput/DateTimeInput- Komponente: Picker öffnet sich nicht mehr automatisch, dafür gibt es einen expliziten Calendar-Icon-Button rechts im Feld. Tippen direkt im Input bleibt aktiv (allowInput: true).
  • Neues IconCalendar (14×14 SVG) konsistent zur Icon-Bibliothek.
  • CSS-Wrapper mit Padding-Right für den Icon-Button, Dark-Mode-Hover via var(--c-primary).

Neu — Auto-Sync Ende-Datum bei Datum-Wechsel im Start

Wenn man im Bearbeiten-Modal das Datum des Starts wechselt (z. B. weil die Buchung am falschen Tag landete), zieht das Ende-Datum automatisch mit — Ende-Uhrzeit bleibt.

  • Neuer Helper lib/syncEndDate.ts mit Sicherheits-Regeln:
    • Sync nur wenn das Datum geändert wurde (nicht reine Uhrzeit).
    • Sync nur bei Eintages-Buchungen (vorher Start.date == Ende.date) — Mehrtages-Buchungen werden in Ruhe gelassen, damit wir nicht versehentlich Ende < Start erzwingen.
  • Eingesetzt im EntryEditModal („Erweitert bearbeiten”) und in den Inline-Edit-Forms von Entries.tsx und Reports.tsx.

Behoben — „Meine Buchungen” + „Berichte”: Default-Monat und Stale-Closure

Zwei Bugs zusammen gefixt, weil sie denselben Code-Bereich betrafen.

  • Default-Monat: /entries und /reports defaulten jetzt auf den aktuellen Monat statt mit leeren Filtern zu starten. Vorher: Seite initial leer, User musste erst „Aktueller Monat” + „Auswerten” klicken, was nicht selbsterklärend war.
  • Stale-Closure: buildQuery/buildParams waren useCallback-Closures über den Filter-State. Bei synchronem setFrom(...) + load() im selben React-Tick lasen sie noch die alten Werte → „Auswerten” funktionierte nicht zuverlässig direkt nach einer Filter-Änderung. Workaround war „vor jeder Änderung Zurücksetzen klicken”.
  • Saubere Lösung: ein einziges useEffect([reloadTick]) führt jetzt den Fetch durch und liest Filter-Werte aus dem commit-frischen State. Trigger-Funktionen (Auswerten, Pagination, Modal-Saves) bumpen nur noch den Tick. Identisches Pattern in beiden Seiten.
  • UI-Folge: Hinweistext „Bitte vor Veränderung der Filter immer zurücksetzen” raus, Reset-Button auf invertierten Auswerten-Stil umgestellt (weiß + #9D2AC6 Rahmen + Schrift).

Layout — Laufende Timer ganz oben

Auf der Timer-Seite steht die „Läuft gerade”-Tabelle jetzt vor den Favoriten-/Letzte-Jobs-Karten. Wer einen laufenden Timer hat, sieht ihn sofort und kann ihn direkt stoppen, ohne zu scrollen.

  • Empty-State-Bedingung um recentJobs.length === 0 erweitert, damit „Kein Timer läuft. Wähle einen Favoriten oder starte einen neuen Timer” nicht fälschlich erscheint wenn der User schon gebucht hat aber keine Favoriten gepinnt sind.

Sonstiges

  • ROADMAP umsortiert: User-Feedback-Punkte zu v1.6 gebündelt, Monitoring auf v1.7, Budget-PDF auf v1.8, Public Signup auf v1.9. „Bulk-Edit Iteration 3” als „Offen” notiert (jetzt erledigt mit diesem Release).
  • Swagger-Doku via swag init für die neuen Endpoints/Felder regeneriert.
  • i18n-Keys (DE + EN) für bulk_edit.field.{date,time}, bulk_edit.{date,time}_hint, bulk_edit.error.{date_required, time_required, time_order}, timer.recent.section, settings.profile.recent_jobs_limit_*, common.back.

[1.5.1] — 2026-05-19

Bulk-Edit für Buchungen, JWT-Claim-Fix, Build-Performance-Fix.

Patch-Release mit drei voneinander unabhängigen Verbesserungen, die zusammen beim Test mit den ersten echten Tenants aufgefallen sind.

Neu — Bulk-Edit (Iteration 2) in Reports

In der Berichte-Seite gibt es bisher eine Multi-Select-Toolbar, die nur den Status (Gebucht/Geprüft/Verrechnet/Storniert) für mehrere Buchungen umstellen konnte. Mit v1.5.1 kommt ein zweiter Button „Erweitert bearbeiten…” dazu, der ein Modal öffnet, in dem mehrere Buchungen auf einmal in andere Account/Project/Job-Kombinationen verschoben, mit einer neuen Notiz überschrieben, auf einen anderen Abrechnungs-Prozentwert gesetzt oder einem anderen Nutzer zugewiesen werden können.

Frontend (components/BulkEditModal.tsx, pages/Reports.tsx):

  • Checkbox-pro-Feld-Pattern: jedes der vier Bulk-Felder hat einen eigenen „auf alle anwenden”-Schalter. Nur aktivierte Felder werden ans Backend geschickt — Felder ohne Häkchen bleiben unverändert.
  • Cascading-Dropdowns für Account → Projekt → Job (identische Logik wie im Einzel-Edit-Modal).
  • Confirmation-Step vor dem Absenden mit Preview-Liste der ersten 8 betroffenen Buchungen + Warnhinweis „nicht rückgängig zu machen”.
  • Bewusst ohne Zeit-Felder — siehe ROADMAP, „Bulk-Edit Iteration 3”.
  • User-Reassign nur für Manager/Admin sichtbar.
  • i18n-Keys unter bulk_edit.* in DE + EN.

Backend (handlers_entries.go, main.go, swagger_types.go):

  • Neue Route POST /entries/bulk-update mit Body { ids, changes }.
  • Pro Eintrag: Tenant-Check, Period-Lock-Check, Status-Filter (invoiced und cancelled werden für Manager übersprungen, nicht abgewiesen — Admin darf alles).
  • Job- und User-Reassign werden einmalig vor der Schleife gegen den Tenant geprüft (nicht pro Eintrag).
  • billable_percent wird auf 0–100 geklemmt.
  • Pro tatsächlich geänderter Buchung wird ein Audit-Log-Eintrag in time_entry_history mit den alten Werten geschrieben.
  • Response: { updated, skipped, errors } — der Toast im Frontend zeigt beides an.
  • Volle Swagger-Annotation mit BulkUpdateRequest/BulkUpdateResponse-Typen.

Behoben — JWT-Claim-Namen (Frontend ↔ Backend Drift)

Versteckter Bug, der bei zwei realen Tenants dazu führte, dass Nutzer im „Meine Buchungen”-Tab fremde Einträge ihres Tenants gesehen haben (kein Cross-Tenant-Leck — der Filter „nur eigene” griff einfach nicht).

Das Backend (auth.go) schreibt die JWT-Claims als uid und tid (Custom-Felder), nicht als die JWT-Standard-Felder sub und tenant_id. Das Frontend (AuthContext.tsx) las aber die Standard-Felder, was als undefined zurückkam — beim API-Call /entries?user_id=... wurde dann der Parameter leer gelassen und das Backend lieferte die Default-Sicht (alle Buchungen des Tenants, nicht nur die eigenen).

Fix: Frontend liest jetzt uid/tid aus dem dekodierten Token. Bestandsnutzer müssen sich einmal aus- und einloggen, damit der neu parsende Code greift — der Token selbst ändert sich nicht.

Behoben — Vite-Build von 12 min auf 42 s

Der make deploy-Build hing seit Tagen bei ~12 min mit 0 % CPU — reine I/O-Wartezeit. Ursache: das Projekt lag unter ~/Documents/..., und iCloud Drive (fileproviderd, cloudd, bird) plus Spotlight (mdworker_shared) blockierten den Disk-I/O. Ohne den ~/Documents-Zwang schreibt Vite die Build-Artefakte mit normaler Geschwindigkeit raus.

Fix: Projekt wurde nach ~/dev/daagwerkCode verschoben (als Kopie, das Original unter ~/Documents/development/daagwerkCode bleibt unangetastet als historisches Backup). Deploy-Zeit ist von 12 min auf 42 s runter; davon sind 29 s der go build im Docker-Container auf dem Server (nicht mehr unser Rechner). CLAUDE.md-Pfade entsprechend aktualisiert.

Neu — make sync-backup-Target

Neues Makefile-Target, das das Arbeitsverzeichnis nach ~/Documents/... zurück-rsynct (einseitig, ohne node_modules/dist/.git/worktrees), damit iCloud eine zweite Kopie hat. Vor jedem git tag ausführen (Policy in CLAUDE.md festgehalten).

Sonstige Änderungen

  • time_entry_history-Einträge enthalten jetzt auch das alte job_id, damit eine spätere History-Ansicht das Verschieben zwischen Jobs nachvollziehen kann.
  • common.back-i18n-Key (DE + EN) für den „Zurück”-Button im BulkEditModal-Confirmation-Step.

1.5.0 — 2026-05-18

Auslastungs-Tab, Feiertage, Abwesenheiten, JobPage-Betrag.

Großes Release mit dem komplett neuen persönlichen Auslastungs-Bereich (MVP-1 bis MVP-4) und mehreren Backend-Verbesserungen.

Neu — JobPage-Buchungstabelle: billable_percent + amount

Die Spalten „Betrag” und „Abr. %” in der Buchungstabelle von JobPage zeigten bisher immer „–” / „100 %”, weil das Backend die Werte nicht mitlieferte. Behoben durch Erweiterung des JobEntry-Structs und einer CASE-Expression im SQL, die den Betrag nur bei billing_type='billable_hourly' berechnet (Cascade Job→Project→Account für den Stundensatz, Basis ist duration_rounded_seconds mit Fallback auf duration_seconds). Für billable_fixed und not_billable bleibt amount korrekt NULL.

Neu — Auslastungs-Tab MVP-1 (persönliches Arbeitszeit-Modell)

Persönliche Soll/Ist-Übersicht der Arbeitszeit für jeden Nutzer.

Backend:

  • Migration 026: neue Spalten weekly_hours, workdays, country_code, state_code in users
  • 3 neue Endpoints, alle strict user-scoped (DSGVO) — keine Manager-/Admin-Sicht, kein user_id-Parameter:
    • GET /me/work-schedule — eigenes Arbeitszeit-Modell lesen
    • PUT /me/work-schedule — Wochenstunden / Arbeitstage / Land / Bundesland setzen
    • GET /me/work-balance?from=&to= — Soll/Ist pro Tag im Zeitraum (max. 366 Tage)
  • Soll = weekly_hours / count(workdays) an Arbeitstagen, sonst 0
  • Ist = Summe duration_rounded_seconds (Fallback duration_seconds) aller time_entries außer cancelledbillable_percent ignoriert (reine Arbeitszeit)
  • Buchungen an Wochenenden/Frei-Tagen werden weiter akzeptiert; sie tauchen im grauen Frei-Tag-Balken auf (Phase 2 + 3 ergänzen Feiertage + Abwesenheiten)

Frontend:

  • Profil-Tab 1: neuer Abschnitt Arbeitszeit-Modell — Wochenstunden, 7 Workday-Pills (Mo–So), Land (MVP: DE), Bundesland (16 DE-Subdivisionen — MVP-2 nutzt sie für Feiertage)
  • Buchungen-Seite (/entries): neue Tab-Bar (Cleanup-Pill-Stil) mit Tab Meine Buchungen (Bestand) und Auslastung (neu)
  • Auslastungs-Tab: Wochenansicht mit 7 vertikalen Balken (Mo–So), Soll-Hintergrund + Ist-Füllung
  • Farb-Codierung: 0–33 % #F72A7C · 34–50 % #F7C619 · ≥ 51 % #A1E929 · Frei-Tage grau (Soll-Bg #E5E5E5, Ist-Fill #383838)
  • Footer mit Wochensumme (Soll / Ist / Differenz, Differenz farbig grün oder pink)
  • Leer-State mit Link zur Profil-Seite wenn weekly_hours noch nicht gesetzt
  • i18n: 41 neue Keys (DE + EN), inkl. 16 Bundesland-Bezeichnungen

Neu — Auslastungs-Tab MVP-4 (Wochen-/Monats-/Jahres-Ansicht + Saldo)

Drei Zeit-Granularitäten in einem Tab, mit konsistenter Optik und Navigation.

Frontend (Entries.tsx):

  • View-Switcher (Pill): Woche · Monat · Jahr
  • Navigation: ‹ / Heute / › verschiebt um eine View-Einheit (Woche / Monat / Jahr)
  • Zeitraum-Label rechts: „KW 20 · 11.05.–17. Mai 2026” / „Mai 2026” / „2026”
  • getBalanceRange(mode, cursor, locale) baut from/to + Anzeige-Label
  • buildBalanceBuckets() aggregiert je View:
    • Woche: 7 Buckets (Mo–So, ein Tag pro Bucket)
    • Monat: 4–6 Buckets nach ISO-Kalenderwoche; nur Tage des Monats summiert
    • Jahr: 12 Buckets pro Kalendermonat
  • Y-Skala-Logik einheitlich: referenceTarget = max(bucket.target), das ist die 100 %-Marke. Bucket-Soll = bucket.target, Ist relativ dazu; Cap bei 100 % + ↑-Indikator bei Überbuchung.
  • Gridline-Step je Modus: 1h / 8h / 40h. Top-Linie immer am referenceTarget mit Stunden-Label.
  • ISO-Wochen-Berechnung via Standard-Algorithmus (getIsoWeek()).
  • Saldo-Footer kommt aus data.sums.diff_hours über den jeweiligen Zeitraum.
  • i18n: 6 neue Keys DE+EN (balance.view.*, balance.nav.*).

Backend unverändert:

  • /me/work-balance?from=&to= ist generisch genug, bereits in MVP-1 implementiert.
  • 366-Tage-Limit deckt Jahres-View ab.

Neu — Auslastungs-Tab MVP-2 (Feiertage DE, 16 Bundesländer)

Aufbauend auf MVP-1: an gesetzlichen Feiertagen ist das Soll automatisch 0, der Balken erscheint im Frei-Tag-Look (hellgrau, dunkle Outline), unter dem Datum steht der Feiertagsname.

Backend (holidays_de.go):

  • Gauß-Osterformel (Anonymous Gregorian Algorithm) für Ostersonntag
  • Davon abgeleitet: Karfreitag, Ostermontag, Christi Himmelfahrt, Pfingstmontag, Fronleichnam
  • 9 bundesweite + 8 bundeslandspezifische Feiertage
  • Buß- und Bettag (nur SN): letzter Mittwoch vor dem 23.11.
  • In-Memory-Cache via sync.Map, Key "DE-BY-2026" — pro (Jahr, Land, Bundesland) einmal berechnet
  • Bundesländer korrekt belegt:
    • Heilige Drei Könige: BW, BY, ST
    • Frauentag (08.03.): BE, MV
    • Fronleichnam: BW, BY, HE, NW, RP, SL
    • Mariä Himmelfahrt: SL (BY-Gemeinde-Differenzierung bewusst ausgelassen)
    • Weltkindertag: TH
    • Reformationstag: BB, HB, HH, MV, NI, SN, ST, SH, TH
    • Allerheiligen: BW, BY, NW, RP, SL
    • Buß- und Bettag: SN
  • holidayLookup(from, to, country, state) baut Map YYYY-MM-DD → key über alle Jahre im Zeitraum
  • Erweiterbar für AT/CH durch eigene Dateien + Country-Switch in holidaysFor()

API-Änderung:

  • WorkBalanceDay.HolidayKey (statt bisher leerem HolidayName) — stabiler i18n-Key wie "good_friday"
  • balance() lädt zusätzlich country_code, state_code aus users; Feiertage werden auf das Soll des Tags angewendet (Soll = 0)

Frontend:

  • TS-Type WorkBalanceDay.holiday_key (war holiday_name)
  • WorkBalanceTab zeigt unter dem Datum den Feiertagsnamen kursiv, mit Tooltip + Ellipsis bei zu langem Namen
  • i18n: 17 neue Keys DE+EN (balance.holiday.*)
  • Hauntung: alle Frei-Tage (Wochenende, Feiertag, später Absenz) teilen dieselbe Optik — hellgrau mit dunkler Outline

Neu — Auslastungs-Tab MVP-3 (Abwesenheiten: Urlaub, Krankheit, Freizeitausgleich)

Erfassbar im Profil, automatisch im Auslastungs-Tab als Frei-Tag-Optik mit Label.

Backend (handlers_absences.go, Migration 027):

  • Neue Enums absence_kind (‘vacation’, ‘comp_time’, ‘sick’) und day_portion (‘full’, ‘half’)
  • Tabelle user_absences (id, tenant_id, user_id, kind, start_date, end_date, portion, notes)
  • Constraints: end_date >= start_date und portion='half' nur bei start_date = end_date
  • 4 CRUD-Endpoints, alle strict user-scoped (DSGVO):
    • GET /me/absences — Liste sortiert nach start_date DESC
    • POST /me/absences
    • PUT /me/absences/{id}
    • DELETE /me/absences/{id}
  • absenceLookup() baut Map YYYY-MM-DD → {kind, portion} über alle Absenzen die in [from, to] fallen
  • balance() bezieht Absenzen als Soll-Reduzierer ein:
    • portion='full' → Soll an dem Tag = 0
    • portion='half' → Soll an dem Tag = dailyTarget / 2
  • Feiertag hat Vorrang vor Abwesenheit (semantisch klarer: Feiertag ist kein Urlaub)

Frontend:

  • Profile.tsx: neue Section „Abwesenheiten” mit Tabelle + Modal
    • Modal-Felder: Art (Urlaub/Freizeitausgleich/Krankheit), Von, Bis, Umfang-Pills (Ganzer Tag / Halber Tag, nur sichtbar wenn Von = Bis), Notiz
    • Tabelle: Art, Von, Bis, Umfang, Notiz, Bearbeiten/Löschen — sortiert nach start_date
    • Bearbeiten ruft dasselbe Modal mit vorbefülltem State auf
  • WorkBalanceTab: Label unter dem Tagesdatum zeigt jetzt entweder Feiertag oder Abwesenheit
    • Bei halber Abwesenheit: Label hat „½”-Suffix (z.B. „Urlaub (½)”)
  • i18n: 26 neue Keys DE+EN (balance.absence.*, profile.section.absences, profile.absences.*)

1.4.0 — 2026-05-16

Re-Design & Navigation-Restrukturierung + Verwaltungs-Drill-Down.

Neu — Verwaltungsbereich: Account → Projekt → Job Drill-Down

Vollständig neue Verwaltungsoberfläche für die Hierarchie-Ebenen. Ersetzt die alte Accounts.tsx-Seite durch vier dedizierte Feature-Seiten unter src/features/accounts/.

AccountsListPage (/accounts)

  • Suchfeld durchsucht Accounts, Projekte und Jobs gleichzeitig
  • Segmentierter Filter: Aktiv · Mit Budget · Nahe Limit
  • Sortierung A–Z / Z–A
  • Toggle „Deaktivierte anzeigen”
  • Tabelle mit dunklem Header (#383838), Spalten: Avatar · Name · Budget-Bar · Gebuchte Stunden · Buchungen · Aktionen
  • Budget-Balken: grün < 50 % · gelb 51–75 % · rot ≥ 76 %
  • Pro-Zeile: + (neues Projekt anlegen) + ··· (Account öffnen)
  • Header-Buttons: „Importieren” + „+ Neuer Account” (#9D2AC6)
  • Modal zum Anlegen eines neuen Accounts (Name + Enter)

AccountPage (/accounts/:id)

  • Breadcrumb: Verwalten / Accounts → klickbar
  • 5 Metric-Tiles: Projekte · Jobs · Buchungen · Gebuchte Stunden · Budget (% + „Xh von Yh” Sub-Label)
  • Eigenschaften: Beschreibung, Stammdaten, Stundensatz, Rundungsregel, Status, Erstellt
  • Bearbeiten-Button oben rechts und am Ende der Eigenschaften (beide #9D2AC6)
  • Projektübersicht als Tabelle (wie AccountsListPage): Avatar · Projektname · Jobs · Buchungen · Stunden · Budget-Balken · Aktionen
  • „+ Neues Projekt”-Button (#9D2AC6)

ProjectPage (/accounts/:id/:projectId)

  • Breadcrumb: Account-Name → klickbar
  • 4 Metric-Tiles: Jobs · Buchungen · Gebuchte Stunden · Budget (% + „Xh von Yh”)
  • Eigenschaften: Stundensatz (mit Vererbungsanzeige), Rundungsregel, Bestellnummer, Budget (Stunden + Betrag + Zeitraum), Limit, Status
  • PATCH-Endpunkt korrigiert: /projects/:id statt /accounts/:aid/projects/:pid
  • Bearbeiten-Button oben rechts und am Ende der Eigenschaften
  • Job-Übersicht als Tabelle: Farbpunkt · Job-Name · Typ-Badge · Buchungen · Stunden · Limit-Balken · Aktionen
  • „+ Neuer Job”-Button (#9D2AC6)

JobPage (/accounts/:id/:projectId/:jobId)

  • Breadcrumb: Account → Projekt → klickbar
  • 5 Metric-Tiles: Buchungen · Gebuchte Stunden · Stundensatz (effektiv) · Limit (h + % Sub) · Budget (% + „Xh von Yh”)
  • Eigenschaften: Billing-Typ · Stundensatz · Festpreis · Rundungsregel · Bestellnummer · Budget-Stunden · Limit · Status
  • Bearbeiten-Button oben rechts und am Ende der Eigenschaften
  • Buchungstabelle mit dunklem Header: Datum · Start · Ende · Dauer · Benutzer · Notiz-Icon · Status · Betrag · Abr.% · Aktionen
  • Notiz-Icon: grün (#A1E929) wenn Notiz vorhanden, grau wenn nicht
  • Inline-Edit-Zeile: Von / Bis / Notiz / Abr.% — Speichern via PUT /entries/:id

Neu — Backend: Aggregations-Endpunkte

Neue Handler-Datei handlers_accounts_detail.go:

RouteBeschreibung
GET /accounts/listAccount-Liste mit Aggregaten (projects_count, jobs_count, booked_hours, bookings_count, budget_total_hours, budget_used_hours, budget_percent)
GET /accounts/{id}/overviewAccount-Detail + Projektliste mit Aggregaten
PATCH /accounts/{id}Account-Felder patchen (name, description, master_data, hourly_rate, rounding_rule, active)
GET /accounts/{aid}/projects/{pid}/overviewProjekt-Detail + Jobliste mit Aggregaten
PATCH /projects/{id}Projekt-Felder patchen
GET /accounts/{aid}/projects/{pid}/jobs/{jid}/overviewJob-Detail + letzte 20 Buchungen
PATCH /jobs/{id}Job-Felder patchen
  • SQL-Aggregationen via Subqueries (budget aus Projekten hochaggregiert auf Account-Ebene)
  • sqlPatcher: dynamischer PATCH-Builder mit map[string]json.RawMessage — absent = skip, "null" = DB NULL
  • Chi-Router: statische Route /accounts/list hat Vorrang vor parametrisierter /accounts/{id}

Neu — Unternehmens-Favoriten (Company Favorites)

  • Neue Tabelle company_favorites (Migration 025): tenant-weite Favoriten mit UNIQUE(tenant_id, job_id)
  • Backend: GET/POST /favorites/company, DELETE /favorites/company/{id} (bucher-Rolle blockiert)
  • Timer-Seite: zwei Sektionen — Eigene Favoriten (lila #9D2AC6) + Unternehmens-Favoriten (#10A7BD)
  • FavManagerModal mit Tab-Switcher (Persönlich / Unternehmen), FavAdder-Komponente (cascading Dropdowns Account→Project→Job)
  • „+ Favoriten”-Button im PageHeader (teal #10A7BD)
  • CompanyFavorite Interface in types/index.ts

Neu — Profil-Seite (/profile)

  • Neue dedizierte Seite (pages/Profile.tsx) — Avatar-Klick in der Sidebar navigiert direkt dorthin
  • Tab 1 „Profil-Einstellungen” (alle Nutzer): Vorname, Nachname, E-Mail, Passwort ändern, Automatische Timer-Stopps
  • Tab 2 „Unternehmens-Einstellungen” (Manager/Admin): Unternehmenseinstellungen (Name, Rundungsregel, Snap, Timezone, Feierabendstopp) + Daten Exportieren
  • Tab-Bar im Cleanup-CI: Pill-Design, #9D2AC6 aktiver Tab, var(—bg-card) Container
  • Tab 2 nur sichtbar für manager/admin/super_admin — normale Bucher sehen gar keine Tabs

Neu — WebDAV-Konfiguration in Datensicherung (/backup)

  • WebDAV-Backup-Ziel Konfiguration direkt auf der Backup-Seite (oben, vor der Backup-Tabelle)
  • URL, Benutzer, Passwort, Test-Verbindung, Speichern

Geändert — Cleanup-Seite

    1. Register „Favoriten” (nur Manager/Admin): kombinierte Tabelle aller Favoriten
  • Favoriten-Typ-Spalte: „Persönlich” (lila #9D2AC6) / „Unternehmen” (#10A7BD) — Badge + Job-Farbe
  • Alle Tab-Labels, Suche, Filter, Typ-Filter über i18n (DE + EN)

Geändert — Timer-Stopps Hinweis

  • Info-Box über volle Kartenbreite (außerhalb des 480px-Form-Containers)
  • Neuer 3-Absatz-Text: Prioritätsreihenfolge (Max. Laufdauer → Mittagsstopp → Feierabendstopp) + konkretes Beispiel
  • Dark Mode: weiße Schrift + #F72A7C Rahmen + rgba(247,42,124,0.08) Hintergrund

Geändert — Navigation-Restrukturierung

  • Avatar + Name in der Sidebar → klickbar → navigiert zu /profile
  • Logout-Button bleibt separat daneben
  • Bisherige Settings-Seite (/settings): Profil/Passwort/Timer-Stopps → /profile Tab 1; Unternehmenseinstellungen/Export → /profile Tab 2; WebDAV → /backup

Geändert — i18n Vervollständigung

  • Cleanup-Seite: alle Tab-Labels, Suche, Filter, Typfilter, Leer-Texte
  • Import-Seite: Tab-Labels, InfoCard-Texte, Radio-Labels, Konflikt-Modi
  • Favoriten-Tab: Labels

Geändert — Status-Farben (global)

Neue einheitliche Status-Farben in Entries.tsx, Reports.tsx und JobPage.tsx:

StatusVorherNachher
GebuchtCSS-Variable (blau)#10A7BD
VerrechnetViolett #7c3aed#383838
StorniertCSS-Variable (rot)#F72A7C

Geändert — Buchungsliste (Meine Buchungen)

  • Spalte „Kunde / Account / Job” aufgeteilt in drei separate Spalten: Account · Projekt · Job
  • Ellipsis-Truncation bei langen Texten: Account max. 160 px, Projekt max. 180 px, Job max. 150 px
  • Hover zeigt vollen Text via native title-Tooltip
  • Neue i18n-Keys: entries.col.account, entries.col.project, entries.col.job (DE + EN)
  • colSpan in Inline-Edit-Zeile und SkeletonRow cols auf 11 aktualisiert

Neu — TypeScript Feature-Typen

Neue Datei src/features/accounts/types.ts:

  • AccountListItem, ProjectListItem, JobListItem
  • AccountDetail, ProjectDetail, JobDetail
  • AccountRef, ProjectRef
  • JobEntry (inkl. billable_percent?: number, amount?: number)

Design-System / CI

  • Primär-Aktionsfarbe für Account-Verwaltung: #9D2AC6 (Lila/Violett)
  • Alle Bearbeiten- und Erstellen-Buttons im Verwaltungsbereich einheitlich auf #9D2AC6
  • PageHeader-Komponente: breadcrumb-Prop von string auf React.ReactNode erweitert
  • Profil-Seite Tab-Bar: Pill-Design identisch zu Cleanup (bg-card Container, radius-lg, active #9D2AC6)
  • Unternehmens-Favoriten Akzentfarbe: #10A7BD (teal/cyan) — „+ Favoriten” Button, Company FavCard, Badges
  • Dark Mode Info-Box: #F72A7C Rahmen, weiße Schrift

1.3.0 — 2026-05-07

Backup-Restore, WebDAV-Backup-Ziel und Self-Service Daten-Export.

Neu — Backup-Restore

  • Restore-Dialog auf der Backup-Seite: beliebiges Backup aus der Liste auswählen und wiederherstellen
  • Zwei Restore-Modi: Vollständig (alles zurücksetzen) oder Bis zu einem Datum (transaktionale Daten werden auf Stichtag gefiltert)
  • Vor jedem Restore: automatische Sicherung des Ist-Zustands (Safety-Backup)
  • Bestätigungs-Checkbox + visueller Warnbanner im Dialog — fehlklick-sicher
  • Tenant-isoliert: Restore eines Tenants hat null Einfluss auf andere Tenants
  • Migration 021 (webdav_url, webdav_user, webdav_password in tenants-Tabelle)

Neu — WebDAV-Backup-Ziel

  • Tenant-Admins können in den Einstellungen einen WebDAV-Server als externes Backup-Ziel konfigurieren (z.B. Nextcloud, ownCloud)
  • Tägliche Backups werden automatisch zusätzlich auf den WebDAV-Server hochgeladen
  • Passwort wird serverseitig gespeichert, nie im Frontend angezeigt (webdav_configured: bool)
  • „Verbindung testen”-Button sendet PROPFIND-Request und zeigt Ergebnis sofort an
  • Neuer Einstellungs-Abschnitt „WebDAV-Backup-Ziel” (nur für Admin/Manager sichtbar)

Neu — Self-Service Daten-Export

  • Neuer Einstellungs-Abschnitt „Daten exportieren” für alle Nutzer
  • ZIP-Download mit: accounts.json, projects.json, jobs.json, time_entries.json, time_entries.csv, users.json, README.txt
  • CSV enthält alle Zeitbuchungen mit vollständigen Kontext-Spalten (Account, Projekt, Job, Nutzer)
  • Erfüllt DSGVO Art. 20 (Datenportabilität)

Backend / Infrastruktur

  • POST /backups/{id}/restore — neuer Restore-Endpoint mit optionalem restore_until-Parameter
  • POST /backups/webdav-test — Test-Endpoint für WebDAV-Verbindungsprüfung
  • GET /export/tenant.zip — ZIP-Streaming-Export (archive/zip + encoding/csv)
  • handlers_backup.go erweitert: restore(), readBackupFile(), performRestore(), filterRecordsByDate(), uploadWebDAVIfConfigured(), uploadToWebDAV(), testWebDAV()
  • handlers_export_zip.go neu: tenantZip() auf exportHandler
  • handlers_tenant.go erweitert: WebDAV-Felder in TenantSettings, GET + PUT entsprechend aktualisiert

1.2.0 — 2026-05-07

SMTP-Integration, Passwort-Reset-Seiten, automatische Timer-Stopps und Datensicherung.

Neu — E-Mail & Passwort-Reset

  • SMTP-Versand vollständig konfiguriert und getestet (Migadu, STARTTLS Port 587)
  • Neue Seite „Passwort vergessen” (/forgot-password): E-Mail eingeben → Link mit Reset-Token per Mail
  • Neue Seite „Passwort zurücksetzen” (/reset-password?token=...): Neues Passwort setzen mit Bestätigung
  • Rate-Limiting: max. 3 Reset-Anfragen pro E-Mail-Adresse pro Stunde (Redis-basiert)
  • Link „Passwort vergessen?” auf der Login-Seite ergänzt

Neu — Automatische Timer-Stopps

  • Nutzer können in den Einstellungen drei automatische Stopp-Regeln konfigurieren:
    • Max. Laufdauer — Timer stoppt nach Ablauf der maximalen Laufzeit (Eingabe in h:mm)
    • Mittagszeit — Timer stoppt täglich um eine definierte Uhrzeit (Mittagspause)
    • Feierabend (Nutzer) — Timer stoppt täglich um die persönliche Feierabendzeit
  • Tenant-Admins können zusätzlich eine unternehmensweite Feierabendzeit konfigurieren
  • Tenant-Admins können die Zeitzone des Unternehmens festlegen (Europa, US, UTC)
  • Alle zeitbasierten Stopps berücksichtigen die konfigurierte Tenant-Zeitzone korrekt
  • Nach einem automatischen Stopp wird die Tenant-Aufrundungsregel angewendet
  • Info-Banner in den Einstellungen erklärt: Frontend-Timer zeigt weiter (rein kosmetisch) — der tatsächliche Stopp und die Rundung wurden server-seitig bereits durchgeführt

Neu — Datensicherung

  • Täglicher automatischer Backup-Cron um 02:00 Uhr (systemd-Timer daagwerk-backup.timer)
  • Vollständiger gzip-JSON-Snapshot aller Tenant-Daten (Accounts, Projekte, Jobs, Buchungen, Nutzer, …)
  • 28-Tage Rolling Retention: ältere Backups werden automatisch gelöscht
  • Admin-UI „Datensicherung” in der Navigation: Backup-Liste, Download-Button, „Jetzt sichern”-Button
  • Migration 020: tenant_backups-Tabelle für Backup-Metadaten

Neu — Backend / Infrastruktur

  • POST /internal/timer-cleanup — interner Endpoint, abgesichert via X-Internal-Secret-Header
  • POST /internal/backup-run — interner Endpoint für tägliche Backups
  • systemd-Timer (daagwerk-timer-cleanup.timer) auf dem Server: Aufruf jede Minute, AccuracySec=10s
  • systemd-Timer (daagwerk-backup.timer) auf dem Server: täglich 02:00 Uhr
  • Docker-Volume backup_data für persistente Backup-Speicherung
  • Migration 019: neue Spalten stop_max_minutes, stop_lunch_time, stop_eod_time in users; timezone, stop_eod_time in tenants

Technisch

  • pgx v5: PostgreSQL TIME-Spalten werden als *string gescannt + parseTimeStr() konvertiert sie
  • Hilfsfunktionen minsToHMM() / hmmToMins() in Settings.tsx für h:mm-Darstellung
  • type="text" mit inputMode="numeric" statt type="number" für Zeitdauer-Inputs (Browser-Spinner überdeckten sonst die letzte Ziffer)

1.1.0 — 2026-05-05

Swagger API-Dokumentation und CSV-Import für Zeitbuchungen.

Neu — API-Dokumentation

  • Vollständige OpenAPI 2.0 Spezifikation über swaggo/swag generiert
  • Swagger UI öffentlich erreichbar unter https://api.daagwerk.de/docs
  • Alle Endpoints dokumentiert: Summary, ausführliche Description, Parameter, Response-Typen, Auth-Hinweise
  • Sprache: Englisch (internationaler Standard)

Neu — CSV-Import für Zeitbuchungen

  • Neuer Tab „Zeitbuchungen” auf der Import-Seite neben dem bestehenden Hierarchie-Import
  • Vorlage: vorausgefüllte CSV mit allen aktiven Jobs des Tenants (Auth-required, pro Nutzer angepasst)
  • Format: account_name, project_name, job_name, date, start_time, end_time, notes, billable[, user_email] — separate Datum- und Zeitspalten für maximale Excel-Kompatibilität
  • Konfliktmodus (ganzdateiweise konfigurierbar vor dem Import):
    • Überspringen — bestehende Buchungen bleiben unberührt
    • Ersetzen — bestehende Buchungen werden überschrieben
  • Vorschau: Zeilenweise Validierung vor dem Import (kein DB-Write) mit Status ok / Konflikt / Fehler
  • Rollen: Bucher importieren nur für sich selbst; Manager und Admins können per user_email-Spalte für andere Nutzer importieren
  • Import-Protokoll: farbcodierte Ergebnistabelle (grün = importiert, orange = übersprungen, rot = Fehler); abrufbar für 7 Tage
  • Frühere Imports: Liste der letzten 10 Import-Protokolle mit aufklappbarer Zeilenansicht
  • Tenant-Aufrundungsregel wird beim Import automatisch angewendet

Technisch

  • Neue Datenbanktabelle import_reports (Migration 018) mit JSONB-Zeilenspeicherung und automatischem Ablauf nach 7 Tagen
  • 5 neue API-Endpoints: GET /import/time-template, POST /import/time/preview, POST /import/time, GET /import/time/reports, GET /import/time/reports/{id}
  • Swagger-Dokumentation für alle neuen Endpoints

1.0.0 — 2026-04-20

Erster öffentlicher Release. Vollständige Grundfunktionalität für professionelles Zeiterfassungs-SaaS.

Neu — Kern-Features

Authentifizierung & Mandantenfähigkeit

  • Mandantenfähige (Multi-Tenant) Architektur mit vollständiger Datentrennung per Row-Level-Security
  • Eigenes JWT-basiertes Auth-System (kein externer Provider erforderlich)
  • Rollen-System: Bucher · Manager · Admin · Super-Admin
  • Passwort-Reset per E-Mail-Link
  • Sichere Session-Verwaltung mit Refresh-Token-Rotation

Hierarchie: Account → Project → Job

  • Account — oberste Ebene; steht für Kunden, Auftraggeber, Abteilungen oder Kostenstellen
  • Project — mittlere Ebene; Kampagnen, Initiativen, Aufträge eines Accounts
  • Job — buchbare Tätigkeit/Position innerhalb eines Projekts
  • Stundensatz-Vererbung (Tenant → Account → Project → Job) mit individuellem Override
  • Aufrundungsregel je Ebene (none / 5min / 10min / 15min / 30min / 60min)
  • Bestellnummern für Accounts und Jobs
  • Abrechnungstypen je Job: stündlich abrechenbar · Festpreis · nicht abrechenbar
  • Budgets (Stunden + Betrag) mit Limit und Auslastungsanzeige in Echtzeit
  • Inaktiv-/Aktiv-Verwaltung auf allen Ebenen

Timer

  • Beliebig viele Timer gleichzeitig pro Nutzer
  • Timer starten mit Freitextsuche (Account / Project / Job)
  • Favoriten: häufig genutzte Account-Job-Kombinationen mit einem Klick starten
  • Automatischer Timer-Stopp konfigurierbar

Zeiterfassung & Buchungen

  • Manuelle Buchungen (von/bis oder Dauer)
  • Teilweise Abrechenbarkeit je Buchung (0–100 % billable)
  • Notizen pro Buchung mit Freitexteingabe
  • Textbausteine/Vorlagen: vordefinierte Notizvorlagen global, pro Account und pro Job
  • Geplante Buchungen (Forecast): erstellen → aktivieren → bestätigen
  • Status-Workflow: Gebucht → Geprüft → Abgerechnet · Storniert
  • Perioden-Sperre: Wochen und Monate abschließen (Manager-Funktion)
  • Buchungen bearbeiten: Inline-Bearbeitung in Tabellenansicht
  • Massenauswahl und Statusänderung für mehrere Buchungen gleichzeitig

Favoriten

  • Favoriten-Verwaltung: Kombinationen aus Account + Job speichern
  • Individuelle Sortierung, Umbenennung, Deaktivierung
  • Direkt im Timer nutzbar

Reports & Auswertungen

  • Vollständige Buchungsliste mit Filterung nach: Account, Project, Job, Nutzer, Abrechnungstyp, Status, Datumsbereich, Freitext
  • Inline-Bearbeitung direkt in der Reportansicht
  • Budget-Auslastung je Account/Project als Fortschrittsbalken
  • Dashboard: Tagesstunden, Wochenstunden, laufende Timer, Top-Accounts

Export & Import

  • Export als PDF, CSV, JSON, XML
  • CSV-Import für Accounts, Projects und Jobs (Bulk-Anlage)
  • Konfigurierbarer Export: Spaltenauswahl, Kundenstammdaten-Option

Einstellungen & Verwaltung

  • Nutzerprofil: Name, E-Mail, Passwort ändern
  • Favoriten-Verwaltung in den Einstellungen
  • Cleanup-Seite: inaktive Accounts/Projects/Jobs einsehen und bereinigen
  • Tenant-weite Einstellungen für Admins (Stundensatz, Rundungsregel)

Neu — Oberfläche

  • Dark Mode / Light Mode — manuell umschaltbar, System-Präferenz als Standard
  • Internationalisierung — vollständig auf Deutsch und Englisch übersetzt (i18n via react-i18next)
  • Ladeanimationen — Skeleton-Loader und Spinner überall wo Daten geladen werden
  • Toast-Benachrichtigungen — nicht-blockierende Erfolgs-, Fehler- und Infomeldungen
  • Datepicker — browserübergreifend konsistenter Kalender (flatpickr, deutsch, Dark-Mode-fähig)

Neu — Plattformen

  • Web (primär) — React + Vite, auslieferbar als statisches Build hinter Nginx
  • iOS App v0.1.0 — SwiftUI, Timer starten/stoppen, Buchungsliste, Einstellungen, Dark Mode
  • watchOS App v0.1.0 — Timer sehen, starten, stoppen; Complications (Circular, Rectangular, Corner, Inline)

Neu — Backend & Infrastruktur

  • Go-Monolith mit PostgreSQL und Redis
  • 17 Datenbankmigrationen (vollständig versioniert)
  • REST-API mit vollständiger Rollenprüfung
  • Docker-basiertes Deployment (VPS: IONOS, Deutschland, DSGVO-konform)
  • Nginx Reverse Proxy + Let’s Encrypt SSL
  • make deploy — ein Befehl für vollständiges Deployment

Behoben

  • Dashboard: Schwarzer Bildschirm nach Login wenn API-Antwort leer war
  • Dashboard: Absturz in Top-Accounts-Karte bei fehlendem Array-Wert
  • Accounts-Seite: Alle aufgeklappten Projekte wurden nach dem Speichern eines Accounts zugeklappt
  • Accounts-Seite: Variablen-Referenzfehler im ProjectBlock nach Komponenten-Umbenennung
  • Reports / Buchungen: Spinner-Pfeile bei Number-Inputs im Dark Mode nicht sichtbar
  • Reports: Filter-Aktiv-Punkt zu klein und kaum sichtbar
  • Checkboxen: Im Dark Mode zu klein und unsichtbar dargestellt
  • Datepicker: Safari-Bug beim Eingeben der Jahreszahl in nativen Date-Inputs

0.9.0 — 2026-04-10

Feature-Complete-Stand vor dem 1.0-Release. Primär interne Entwicklungsversion.

Neu

  • Vollständige Umbenennung der Hierarchie: Client → Account → Project → Job (Backend + Frontend + Datenbank)
  • Migration 017: clients-Tabelle → accounts, alle Fremdschlüssel und Indices angepasst
  • Neue API-Routen: /accounts, /accounts/:id/projects (ersetzt /clients, /clients/:id/accounts)
  • Alle Go-Handler, TypeScript-Typen und React-Komponenten auf neue Namensgebung umgestellt
  • i18n-Keys vollständig auf neue Hierarchie-Begriffe aktualisiert (DE + EN)

Behoben

  • Templates-Seite: Scope-Level accountproject in Anzeige und API-Parametern
  • Cleanup-Seite: Zähler und Purge-Parameter auf neue Feldnamen aktualisiert
  • Einstellungen: Favoritenpicker zeigte falsche Auswahlebene

[0.8.0] — 2026-04-05

Reports, Export und Buchungs-Workflow.

Neu

  • Reports-Seite: Buchungsliste mit vollständiger Filterzeile
  • Inline-Bearbeitung von Buchungen direkt in Reports und Buchungsliste
  • Massenauswahl + Statuswechsel für mehrere Einträge
  • Export als PDF (Go-seitig generiert), CSV, JSON, XML
  • Konfigurierbare Export-Optionen (Spaltenauswahl, Kundenstammdaten)
  • Budget-Auslastungsbalken in Reports und Account-Übersicht
  • Textbausteine/Vorlagen: Verwaltungsseite und Auswahl beim Bearbeiten einer Buchung

Behoben

  • Timer: Laufende Timer wurden nach Seiten-Reload nicht sofort angezeigt
  • Buchungen: Doppelklick auf Inline-Edit öffnete zwei Editoren gleichzeitig

[0.7.0] — 2026-03-28

Dashboard, Favoriten und Perioden-Sperre.

Neu

  • Dashboard: Tagesstunden, Wochenstunden, monatliche Auslastung, laufende Timer
  • Dashboard: Admin-Ansicht mit Top-Accounts und Teamauslastung
  • Favoriten: Erstellen, benennen, sortieren, direkt aus Timer nutzen
  • Perioden-Sperre: Manager können Wochen/Monate für weitere Buchungen sperren
  • Geplante Buchungen (Forecast): Anlegen, aktivieren, bestätigen

[0.6.0] — 2026-03-15

Internationalisierung und Dark Mode.

Neu

  • Vollständige i18n-Infrastruktur (react-i18next): alle Seiten auf DE und EN übersetzt
  • Dark Mode / Light Mode mit System-Präferenz und manuellem Toggle
  • Sprachumschalter (DE / EN) in der TopBar

[0.5.0] — 2026-03-01

Zeiterfassung: manuell, Status-Workflow, Teilabrechenbarkeit.

Neu

  • Manuelle Buchungen (von/bis oder Dauer)
  • Status-Workflow: Gebucht → Geprüft → Abgerechnet · Storniert
  • Teilweise Abrechenbarkeit (billable_percent 0–100 %) pro Buchung
  • Notizen mit Freitexteingabe je Buchung
  • Abrechnungstypen je Job: stündlich · Festpreis · nicht abrechenbar

[0.4.0] — 2026-02-15

Account-Hierarchie und Stundensatz-System.

Neu

  • Dreistufige Hierarchie: Account → Project → Job (damals noch: Client → Account → Job)
  • Stundensatz-Vererbung über alle Ebenen mit individuellem Override
  • Aufrundungsregel (none / 5 / 10 / 15 / 30 / 60 Minuten) je Ebene
  • Budgets (Stunden + Betrag) mit Limit-Anzeige
  • Bestellnummern für Accounts und Jobs
  • CSV-Import für Accounts und Jobs

[0.3.0] — 2026-02-01

Timer-Kern.

Neu

  • Mehrere Timer gleichzeitig pro Nutzer
  • Suche beim Timer-Start (Account / Project / Job)
  • Timer automatisch stoppen

[0.2.0] — 2026-01-20

Multi-Tenant-Architektur und Rollen.

Neu

  • Vollständige Mandantenfähigkeit mit Row-Level-Security in PostgreSQL
  • Rollen: Bucher · Manager · Admin · Super-Admin
  • Passwort-Reset per E-Mail

[0.1.0] — 2026-01-10

Erste lauffähige Version — Fundament.

Neu

  • JWT-Authentifizierung (Login, Logout, Token-Refresh)
  • Go-Backend mit PostgreSQL und Redis
  • React-Frontend (Vite + TypeScript)
  • Grundlegendes Design-System: Farben, Typografie, Toast, Dark Mode vorbereitet
  • Erste Datenbankmigrationen (Tenants, Users, Rollen)