# Status — Rakku Website V2

**Dashboard (für den Nutzer, immer aktuell):** https://claude.ai/code/artifact/3a293143-0ebf-4c8e-bcb4-866ddd803ec4
Bei jedem Fortschritt hier in STATUS.md UND im Dashboard-Artifact aktualisieren (gleiche Datei erneut per Artifact-Tool veröffentlichen, URL bleibt gleich).

Zuletzt aktualisiert: 12.08.2026

## Neu: Einheitliche Seitentitel überall (12.08.2026) ✅
Titel-Format zentral fest verankert: „Rakku Photography" (Startseite) oder „Rakku Photography | Unterseite" (überall sonst) — nicht mehr über ein freies, uneinheitlich genutztes Einstellungsfeld gesteuert (aus `/admin/seo` entfernt). Dabei zwei echte Bugs gefunden: Portfolio-Seite nutzte versehentlich die UI-Überschrift „Meine Arbeiten" statt eines Seitentitels als SEO-Titel; Conventions-Seite hatte einen ganzen Satz statt eines kurzen Namens im Titel. Live über alle öffentlichen Seiten verifiziert. Details in Masterplan.md.

## Neu: Leere Seite bei Arbeitsverträgen behoben, Fußzeile mit Seitenzahl überall (12.08.2026) ✅
Ursache gefunden: die Fußzeile im Dokumenten-Creator lief im normalen react-pdf-Textfluss statt als `fixed`-Element — bei längeren Dokumenten blieb dadurch kein Platz mehr auf der letzten Seite und eine fast leere Zusatzseite entstand. Behoben, und gleich in **allen vier PDF-Erzeugern der Anwendung** (Dokumenten-Creator, Auftragsverträge, Rechnungen, Mahnungen — vollständige Codesuche ergab genau diese vier) eine feste, nicht abschaltbare Fußzeile mit Dokumentname + „Seite X von Y" ergänzt. Live verifiziert: Test-Arbeitsvertrag zeigt jetzt 3 vollständig gefüllte Seiten statt 3 mit einer leeren am Ende, Test-Rechnung zeigt korrekt „Seite 1 von 1". Details in Masterplan.md.

## Neu: Echte Arbeitsvertrags-/Arbeitszeugnis-Texte (11.08.2026) ✅
Die 10 bereits bestehenden Dokumentvorlagen unter `/admin/documents` überarbeitet. Debug-Reste aus der Abmahnung-Vorlage entfernt, Arbeitsvertrag/Minijob-Vertrag/Ausbildungsvertrag um gesetzlich vorgeschriebene, bisher fehlende Klauseln ergänzt (Arbeitsort, Tarifvertrags-Hinweis, Datenschutz, Berichtsheft-Pflicht, Klagefrist). Wichtigste Ergänzung: die Platzhalter-Erklärung im Dokumenten-Creator zeigt jetzt bei den Zeugnis-Vorlagen die vollständige deutsche „Zeugnissprache"-Notenskala (sehr gut bis mangelhaft) für Leistungs- und Verhaltensbeurteilung — bewusst kein fester Text vorgegeben, da die Note zwangsläufig pro Person variiert. Kompetent nach BGB/NachwG/BBiG/KSchG/GewO formuliert, aber keine anwaltliche Beratung — einmalige Prüfung vor dem ersten echten Vertrag empfohlen. Details in Masterplan.md.

## Neu: Intranet ausgebaut — Abteilungs-Sichtbarkeit, Farben, Suche, Navigation (11.08.2026) ✅
Auf „mach das Intranet mal etwas besser... tob dich aus" hin deutlich erweitert. Navigation umgebaut: „Intranet" ist jetzt eine echte Dropdown-Kategorie (vor „Verwaltung") mit „Abläufe" und „ToDos" als Unterpunkten. Beiträge lassen sich jetzt auf bestimmte Abteilungen beschränken (`IntranetArticle.visibleToDepartmentIds`, leer = für alle) — serverseitig gefiltert, mit echtem Test verifiziert (Marketing-Betrachter sieht einen Fotografie-only-Beitrag nachweislich nicht). Jede Abteilung hat jetzt eine eigene Farbe (`Department.color`, Farbwähler unter „Team & Rechte"), Intranet-Beiträge zeigen farbige Abteilungs-Badges. Dazu: Anpinnen wichtiger Beiträge (eigener Bereich oben), „Zuletzt bearbeitet von" automatisch getrackt, ein Suchfeld direkt auf der Intranet-Seite sowie Anbindung an die globale Admin-Suche (Treffer klappt den Beitrag automatisch auf). Details in Masterplan.md.

## Neu: Intranet-Bereich + erste Team-Struktur (11.08.2026) ✅
`/admin/intranet` — interne Abläufe/SOPs fürs Team, getrennt von der öffentlichen FAQ, strukturell an FAQ angelehnt (Kategorien, Sortierung, Sichtbar-Schalter). Bewusste Design-Entscheidung: **Ansehen ist für jede:n eingeloggte:n Mitarbeiter:in immer möglich**, unabhängig von Rechten — nur Anlegen/Bearbeiten/Löschen ist eingeschränkt. Dafür eine neue Berechtigungs-Kategorie ergänzt (`CREATE_EDIT_DELETE_AREAS`, Bereiche ganz ohne eigenes „Ansehen"-Recht), analog zum bestehenden Muster bei ToDos. Eigene Dashboard-Kachel und Nav-Link ergänzt.

Dabei auch eine erste Team-Struktur unter „Team & Rechte" angelegt: fünf neue Abteilungen (Fotografie, Bildbearbeitung, Kundenbetreuung, Buchhaltung, Marketing) mit passend zugeschnittenen Rechten nach dem Least-Privilege-Prinzip — sensible Bereiche bleiben bei „Inhaber/in". Reiner Vorschlag, unter `/admin/team` jederzeit anpassbar. Details siehe Masterplan.md.

## 🚀 Go-Live: rakku.de zeigt jetzt V2 (11.08.2026) ✅
Auf „du kannst alles live schalten auf rakku.de und das alte rakku.de runter nehmen" hin die Umschaltung durchgeführt:
- `SITE_URL` in `.env.local` von `v2.rakku.de` auf `https://rakku.de` umgestellt (steuert alle E-Mail-Links, Sitemap-URLs, Google-OAuth-Redirect)
- Wartungsmodus deaktiviert — die Seite ist jetzt für alle Besucher:innen normal erreichbar, nicht mehr nur fürs Team
- nginx-Vhost `rakku.de` von V1 (Port 3008) auf V2 (Port 3009) umgeschaltet, bestehendes Let's-Encrypt-Zertifikat für `rakku.de` passt unverändert weiter (kein neues Zertifikat nötig)
- `v2.rakku.de` leitet jetzt per 301 auf `rakku.de` weiter statt denselben Inhalt doppelt auszuliefern (Duplicate-Content-Vermeidung für Google)
- **Dabei einen echten Bug gefunden und behoben, bevor er unbemerkt geblieben wäre:** alle 9 Cronjobs (Backup, Mahnwesen-Eskalation, Wiedervorlage, Gutschein-Ablauf, Probezeit-Erinnerung, Erinnerungs-Mail vor Termin, Feedback-Anfrage, Google-Sync, Wetter-Check) riefen noch `v2.rakku.de` auf — durch die neue Weiterleitung wären sie ab sofort stillschweigend ins Leere gelaufen (`curl` ohne `-L` folgt keiner Weiterleitung). Crontab auf `rakku.de` umgestellt, einen Cronjob (Wetter-Check) direkt gegen die echte Domain erfolgreich nachgetestet.
- **„Das alte rakku.de runter nehmen" bewusst so umgesetzt:** `rakku.de` zeigt jetzt ausschließlich V2 — der V1-Prozess selbst (systemd-Dienst `rakku-website`, läuft seit Juli direkt als root, nicht über pm2 verwaltet) läuft aber bewusst unangetastet im Hintergrund weiter, nur noch über `old.rakku.de` erreichbar. Ihn komplett zu stoppen wäre eine schwerer rückgängig zu machende, eigene Entscheidung und war in der Formulierung nicht eindeutig „ja, wirklich stoppen" gemeint — daher separat nachgefragt statt einfach mit angenommen (siehe „Als Nächstes von dir gebraucht" im Dashboard).
- Live extern verifiziert: `https://rakku.de` liefert den echten Seiteninhalt (Titel, Hero, hreflang-Links) mit gültigem Zertifikat und den heute ergänzten Sicherheits-Headern.

**Neu offen dadurch:** Google-OAuth-Redirect-URI `https://rakku.de/api/auth/google/callback` muss in der Google-Cloud-Console beim OAuth-Client ergänzt werden, bevor ein *neues* „Mit Google verbinden" funktioniert (bestehende Verbindungen laufen unverändert weiter, da Refresh-Tokens nicht an die Redirect-URI gebunden sind).

## Neu: Go-Live-Vorbereitung — kompletter Audit + Härtung (11.08.2026) ✅
Auf „mach jetzt alles ready für den Wechsel auf rakku.de, checke alles auf Bugs und Logikfehler" hin: zuerst die Netto-/USt.-Aufschlüsselung auf Ausgaben- und Steuer-Seite fertiggestellt (zeigt zusätzlich zum Bruttobetrag, wie viel davon Umsatzsteuer ist — rein intern, Kund:innen sehen weiterhin nur den einen vereinbarten Preis), danach fünf parallele Audit-Durchgänge (Sicherheit/Berechtigungen, Geld-Logik, Korrektheit der neuen Systeme aus der fünften Runde, Daten-Integrität/Konto-Löschung, SEO/i18n) über den gesamten aktuellen Stand. **13 echte, bestätigte Bugs gefunden und behoben:**

- **Pfad-Traversal in den Foto-Auslieferungsrouten** (`api/photos/[folderId]/[filename]`, `api/photos/shared/[token]/[filename]`) — der Dateiname aus der URL wurde ungeprüft an Nextcloud/WebDAV weitergereicht, ein eingeschleustes „../" hätte (mit gültigem Teilen-Token, seit der neuen Foto-Ordner-Freigabe sogar **ohne Login**) Dateien außerhalb des eigenen Ordners erreichen können. Höchste Priorität, sofort behoben.
- **Rabattcode wird durch Kontolöschung öffentlich einlösbar**: eine harte Kontolöschung setzte bei persönlichen Prämien-Codes (Empfehlung/Bewertung/Stammkunde) nur die Besitzer-Verknüpfung zurück statt sie zu entfernen — ein noch nicht eingelöster Code wurde dadurch für **jeden** einlösbar. Jetzt: unbenutzte Prämien-Codes werden beim Löschen entfernt statt „freigegeben".
- **Doppelte Bewertungs-Prämie möglich** bei zwei fast gleichzeitigen Feedback-Einsendungen (Wettlaufbedingung) — jetzt atomar abgesichert wie an anderen Stellen im System.
- **Wartelisten-Beitritt konnte abstürzen** bei zwei gleichzeitigen Beitritts-Versuchen (z. B. zwei offene Tabs) — zeigt jetzt die freundliche „schon eingetragen"-Meldung statt eines rohen Fehlers.
- **Stammkunden-Prämie und Steuer-Seite zählten stornierte/abgelehnte, aber schon bezahlte Aufträge mit** — beide jetzt korrekt gefiltert.
- **Wetter-Check brach beim ersten Fehler den ganzen Cron-Durchlauf ab** statt nur den einen betroffenen Auftrag zu überspringen — behoben.
- **Wettervorhersage konnte am falschen Tag abgefragt werden** (UTC- statt deutsches Kalenderdatum, betraf Shootings kurz nach Mitternacht) — behoben.
- **Aktionspreise auf `/angebote` liefen ca. einen Tag zu kurz** (Enddatum wurde als UTC-Mitternacht statt Tagesende interpretiert) — behoben.
- **Teilen-Link-Umschalter zeigte einen Fehlschlag wie ein erfolgreiches Deaktivieren an** (z. B. bei abgelaufener Sitzung) — jetzt mit echter Fehlermeldung statt stiller Fehlinterpretation.
- **Instagram-Feed-Verwaltung**: Sortieren/Sichtbarkeit waren fälschlich an keine Berechtigung geknüpft (jeder mit Portfolio-Ansicht konnte sie auslösen, auch ohne Bearbeiten-Recht), und der „Löschen"-Knopf hing versehentlich an der falschen Berechtigung (`portfolio_delete` statt korrekt weiterhin `portfolio_delete` fürs Löschen, aber `portfolio_edit` fürs Sortieren/Sichtbarkeit) — beides korrekt getrennt.
- **robots.txt** sperrte die unlisted Teilen-/Feedback-Links (`/galerie/…`, `/feedback/…`) nicht explizit für Crawler (waren zwar schon `noindex`, aber crawlbar) — ergänzt.
- **Nicht-übersetzte Fehlermeldungen** bei der Bewertungs-Abgabe und beim Wartelisten-Beitritt (Englisch sah deutschen Text) — jetzt zweisprachig wie der Rest der Seite.
- Ein größerer, aber sehr seltener Randfall (Rabatt-Änderung an einem Auftrag zeitgleich mit Rechnungserstellung durch zwei verschiedene Team-Mitglieder) wurde bewusst **nicht** behoben — würde echte Datenbank-Transaktionen erfordern, die im restlichen System nirgends genutzt werden. Dokumentiert, nicht dringend bei aktuell einem/wenigen Team-Mitgliedern.

**Zusätzlich gebaut (Härtung fürs Go-Live, keine expliziten Bugs, aber sinnvolle Ergänzungen):**
- Sicherheits-Header site-weit ergänzt (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS) — vorher gar keine gesetzt, anders als die anderen Dienste auf demselben Server
- Echte, zweisprachige 404-Seite statt der nackten Next.js-Standardseite (dabei ein bekanntes, seit Jahren offenes Next.js-Verhalten entdeckt: eine `not-found.tsx` innerhalb von `[locale]/` wird nie verwendet, es zählt immer nur die auf App-Wurzel-Ebene — daher jetzt dort, Sprache kommt aus dem von next-intl gesetzten Cookie)
- `/api/health`-Endpunkt für künftiges externes Uptime-Monitoring
- `old.rakku.de`-Nginx-Vhost für V1 als Fallback nach dem Umzug bereits angelegt (DNS zeigt schon hierher) — das eigentliche SSL-Zertifikat dafür (certbot) wartet noch auf deine Freigabe, siehe unten
- Beiläufig entdeckt: ein Stripe-**Live**-Schlüssel ist inzwischen in der `.env.local` hinterlegt (vorher stand hier noch „wartet auf Rakkus Stripe-Konto") — die Direktzahlung selbst bleibt aber wie gehabt über den Schalter in den Einstellungen deaktiviert, bis du sie bewusst aktivierst

Build/TypeScript nach jedem Fix sauber, alle Änderungen live auf v2.rakku.de deployt und per Smoke-Test über alle betroffenen Seiten verifiziert (24 Seiten, alle 200, keine neuen Fehler im Server-Log).

## Neu: Fünfte Runde Erweiterungsideen — 9 neue Systeme (11.08.2026) ✅
Auf Wunsch aus einer eigenen 10er-Ideenliste alles bis auf den Print-Shop umgesetzt (Print-Shop bewusst ausgelassen), plus die Conventions-Warteliste komplett überarbeitet.

**Conventions-Warteliste, jetzt „richtig":** Braucht jetzt ein Kundenkonto (vorher freie Name-/E-Mail-Eingabe ohne Login) — der Kunde sieht seinen Wartelisten-Eintrag jetzt in einem neuen Profilbereich (`/account/conventions`) inkl. Status (wartend/Slot frei) und kann dort **direkt mit uns chatten**, genau wie beim bestehenden Auftrags-Chat (gleiche Komponente wiederverwendet). Im Admin-Bereich zeigt jede Convention jetzt die einzelnen Wartelisten-Einträge mit Name, Nachricht und eigenem Chat-Verlauf statt nur einer Zahl. Öffnet ein Slot wieder, werden alle Wartenden weiterhin automatisch per Mail benachrichtigt — jetzt zusätzlich mit Glocken-Benachrichtigung.

**Sofort-Preisrechner** (`/angebote`): Angebot + Preisstufe wählen, Zusatzoptionen dazuklicken, der Preis rechnet live mit — Klick auf „Jetzt anfragen" geht direkt mit vorausgefüllter Anfrage weiter.

**Teilbare Foto-Galerie-Links**: Kund:innen können in ihrer Fotogalerie einen Lese-Link erzeugen (an-/abschaltbar, jederzeit widerrufbar) — Familie/Freunde ohne eigenes Konto können die Fotos ansehen (inkl. Wasserzeichen-Schutz bei unbezahlten Aufträgen), aber nicht bewerten oder herunterladen. Live mit echten Fotos aus dem produktiven Nextcloud getestet.

**Wetter-Hinweis bei Outdoor-Shootings**: Automatischer Check der Wettervorhersage (über Open-Meteo, kostenlos, kein eigener API-Key nötig) für anstehende Outdoor-Termine — sieht es nach Regen/Gewitter aus, bekommt der Kunde eine formlose Mail mit Umplanungs-Angebot. Läuft alle 2 Stunden per neuem Cronjob, mit echtem Regenwetter (Reykjavik) end-to-end getestet.

**Shooting-Vorbereitungsguide**: Die bestehende Erinnerungs-Mail vor dem Termin enthält jetzt zusätzlich ein paar allgemeine Vorbereitungs-Tipps passend zur Shooting-Art (Convention, Outdoor, Portrait, …) — kein separates System, direkt in die vorhandene Mail integriert, damit niemand zwei getrennte „vor deinem Shooting"-Mails bekommt.

**Instagram-Feed auf der Startseite**: Instagrams eigene automatische Feed-API verlangt inzwischen einen App-Zugang, den nur du selbst über dein Instagram/Facebook-Business-Konto einrichten kannst — als funktionierende Alternative ohne zusätzliches Konto gibt's jetzt eine kuratierte Liste (Link einfügen im Portfolio-Bereich), die echte Instagram-Embeds auf der Startseite zeigt.

**Bewertungs-Anreiz**: Hinterlässt jemand beim Feedback-Formular einen geschriebenen Kommentar (nicht schon bei einer reinen Sternebewertung), gibt's automatisch einen einmaligen Rabattcode als Dankeschön — direkt auf der Bestätigungsseite, per Mail und als Glocken-Benachrichtigung.

**Charakter-/Konzept-Wunschliste**: Neuer Profilbereich (`/account/wishlist`), in dem Kund:innen eintragen können, welchen Charakter sie sich mal shooten lassen würden. Admin-Ansicht (`/admin/wishlist`) gruppiert nach Charakter, damit Nachfrage-Trends fürs eigene Planen sichtbar werden.

**Stammkunden-Prämie**: Alle X bezahlten Aufträge (einstellbar, Standard 3) gibt's automatisch einen neuen Rabattcode als Dankeschön — nicht nur einmalig, gilt für jede weitere Schwelle. Dabei `markOrderPaidAction` gleich mit demselben atomaren Absicherungsmuster gehärtet, das beim letzten Audit schon beim Stripe-Webhook zum Einsatz kam (verhindert doppelte Prämienvergabe bei einem Doppelklick).

Alle 9 Systeme einzeln live end-to-end getestet (u. a. ein kompletter 3-Bestellungen-Testlauf für die Stammkunden-Prämie, echtes Regenwetter für den Wetter-Check, echte Nextcloud-Fotos für die Galerie-Links), `tsc --noEmit` und Build durchgehend fehlerfrei, alle Testdaten restlos entfernt.

## Neu: Vierter kompletter Fehler-Audit — 4 parallele Prüf-Durchgänge, 13 echte Bugs behoben (11.08.2026) ✅
Auf „prüfe alles auf Fehler, Logikfehler und Bugs, erweitere zu kleine Systeme, verbessere Optik wo sinnvoll" hin vier spezialisierte Audits parallel laufen lassen: Zahlungen/Preise/Rechnungen, Berechtigungen/Sicherheit, Datenintegrität/Löschkaskaden, und die zuletzt selbst geschriebenen Änderungen (Breiten-System, Navigation, Rechtstexte-Seiten). Alle Funde einzeln geprüft, echte Bugs behoben, unbegründete Funde (2 bei den Berechtigungen) als bewusste, bereits im Code dokumentierte Design-Entscheidungen bestätigt und **nicht** verändert.

**Geld-Logik (5 behoben):**
- Zwei nahezu gleichzeitige Mahnungs-Auslösungen (Doppelklick oder überlappender Cron-Lauf) konnten dieselbe Mahnstufe doppelt anlegen und den Kunden zweimal per Mahnmail behelligen — jetzt durch einen eindeutigen Datenbank-Index (Rechnung + Stufe) verhindert, der zweite Versuch bricht sauber ab statt eine doppelte Mahnung zu verschicken.
- **Rabattcode/Gutschein/manueller Rabatt ließen sich noch einlösen bzw. entfernen, nachdem für den Auftrag bereits eine (unveränderliche) Rechnung erstellt wurde** — der tatsächlich fällige Betrag (z. B. bei der Stripe-Direktzahlung) hätte dann von der schon ausgestellten Rechnung abweichen können. Jetzt an allen drei Stellen blockiert, sobald eine Rechnung existiert, mit klarer Fehlermeldung.
- Umsatzsteuersatz in den Einstellungen ließ sich auf jeden beliebigen Wert setzen (auch negativ) — bei ausgeschalteter Kleinunternehmerregelung hätte das die Rechnungsbeträge kaputt gerechnet (Division durch nahe Null). Jetzt auf 0–100 % begrenzt.
- Ein manuell zurückgesetzter „nächste Rechnungsnummer"-Zähler konnte unbemerkt mit einer bereits vergebenen Nummer kollidieren, wodurch eine Rechnungserstellung im Hintergrund fehlschlug, ohne dass das im Admin-Bereich auffiel. Jetzt wird vor dem Speichern geprüft, ob die Nummer schon vergeben ist.
- Der Nutzungszähler eines Rabattcodes hatte keine Untergrenze — konnte durch einen doppelten „Entfernen"-Klick unter 0 rutschen und dadurch unbeabsichtigt zusätzliche gültige Einlösungen freigeben.

**Datenintegrität bei Kontolöschung (4 behoben):** Beim harten Löschen eines Kundenkontos blieben bisher zurück: ein bei diesem Kunden eingelöster Geschenkgutschein blieb für immer als „eingelöst" gegen einen nicht mehr existierenden Auftrag gesperrt (Guthaben unwiderruflich verloren) — jetzt komplett zurückgesetzt, genau wie beim manuellen Entfernen eines Rabatts. Der Nutzungszähler eines vom gelöschten Kunden verwendeten Rabattcodes wurde nie zurückgesetzt (Code erschien fälschlich „aufgebraucht"). Benachrichtigungen des gelöschten Kontos blieben für immer als Datenleiche bestehen. ToDos mit Bezug zum gelöschten Auftrag zeigten auf einen toten Auftrag (Verknüpfung jetzt entfernt, das ToDo selbst bleibt für das Team erhalten).

**Eigene Änderungen von heute nachgeprüft (3 behoben):** Die AGB-/Datenschutz-Textverarbeitung war zerbrechlich — vergisst der Admin beim Bearbeiten im neuen Rechtstexte-Panel eine Leerzeile nach einer Überschrift, wäre die betroffene Überschrift unerkannt als Fließtext im vorherigen Abschnitt gelandet (genau der Fehler, der beim ersten Einspielen der Texte selbst passiert war) — Parser jetzt zeilenbasiert und robust dagegen, unabhängig von Leerzeilen. Die Admin-Navigation klappte beim Klicken einer bereits per Hover geöffneten Kategorie wieder zu, statt offen zu bleiben — behoben. Vier öffentliche Seiten (Conventions, Blog-Artikel, Gutscheine, Feedback-Formular) hatten das neue, breitenflüssige Randkonzept vom letzten Durchgang noch nicht bekommen — jetzt nachgezogen für einheitliches Verhalten auf großen Monitoren.

**Geprüft und bewusst nicht verändert:** Neu zu Mitarbeiter:innen beförderte Konten ohne zugewiesene Position gelten automatisch als voll berechtigt — im Code ausdrücklich als Sicherheitsnetz gegen versehentliche Selbstaussperrung dokumentiert, kein Versehen. Ebenso die Möglichkeit, mit der Rechte-Verwaltungsberechtigung sich selbst Vollzugriff zu geben — bei einem Ein-Personen-Betrieb mit sehr kleinem Team ein akzeptables, bewusstes Risiko.

Alle Fixes live gegen die echte Datenbank verifiziert: ein echter Testauftrag mit Rechnung angelegt und bestätigt, dass Rabatt-Einlösung danach sauber blockiert wird (Rabattcode blieb unangetastet, keine Seiteneffekte); eine komplette Kontolöschung mit Gutschein/Rabattcode/Benachrichtigung/ToDo durchgespielt und jede einzelne Aufräum-Regel direkt in der Datenbank bestätigt; ungültiger Umsatzsteuersatz (−150 %) im Formular abgelehnt, gespeicherter Wert blieb unverändert. `tsc --noEmit` und Build fehlerfrei, alle Testdaten restlos entfernt.

## Neu: Admin-Navigation vereinheitlicht + fluides Breiten-Konzept für die ganze öffentliche Seite (11.08.2026) ✅
Zwei Themen: erst drei konkrete Beschwerden zur Admin-Navigation behoben, danach ein grundsätzliches Problem gelöst — auf großen Monitoren wirkte die ganze öffentliche Seite zu klein/schmal, weil feste `max-w-*`-Grenzen (z. B. 1024px) auf sehr breiten Bildschirmen riesige, feste Ränder erzeugen, während der eigentliche Inhalt in einem schmalen Streifen verschwindet.

**Admin-Navigation:**
1. `/admin/steuer` und `/admin/fahrtkosten` nutzten schmalere Container (`max-w-3xl`/`max-w-2xl`) als alle anderen Admin-Seiten (`max-w-5xl`) — da `AdminNavLinks` innerhalb dieses Containers liegt, wurde die Navigation dort schmaler gequetscht und ist anders umgebrochen als überall sonst. Jetzt einheitlich `max-w-5xl` für den äußeren Rahmen, der schmalere Lesebereich sitzt als innerer `<div>` darin.
2. Die Kategorie-Dropdowns ließen sich nach dem früheren Mobile-Fix nur noch per Klick öffnen, nicht mehr per Hover wie am Desktop gewohnt — jetzt beides gleichzeitig: Hover öffnet zusätzlich, aber nur auf echten Zeigegeräten (`matchMedia("(hover: hover) and (pointer: fine)")`), damit der ursprüngliche Mobile-Bug (hängendes Dropdown nach Antippen) nicht wiederkehrt.
3. Navigationslinks innerhalb jeder Kategorie neu sortiert — von der am breitesten vergebenen Berechtigung zur sensibelsten (z. B. unter „Verwaltung" jetzt: Dokumente → QM & Personal → SEO → Team & Rechte → Einstellungen).

**Fluides Breiten-Konzept (öffentliche Seite):** Neue CSS-Variable `--rk-gutter` (`max(1.5rem, calc(25vw - 12rem))`) plus Utility-Klasse `.rk-shell` in `globals.css` — bis knapp unter Laptop-Breite identisch mit dem bisherigen festen 1,5rem-Rand, wächst darüber aber als echter Prozentsatz der Fensterbreite mit, statt bei einer festen Pixelzahl stehen zu bleiben. Ergebnis: Ränder bleiben auf jedem großen Monitor (1920px bis 4K) bei 15–25 % je Seite, der Inhalt füllt die restlichen 50–70 % — anstatt bei fester max-width auf riesigen Screens in einem schmalen Streifen unterzugehen. Angewendet auf Startseite (außer dem Hero — der bleibt bewusst randlos/vollflächig, wie er es schon war), Portfolio-Galerie, Blog, Angebote, Verfügbarkeitskalender und Urlaubshinweis. Reine Lesetexte (FAQ, Blog-Artikel, Datenschutz, Conventions-Liste) bleiben bewusst schmal für gute Lesbarkeit — das Konzept sagt ausdrücklich, dass nicht jeder Bereich die volle Breite ausfüllen muss.

Dabei zwei echte Bugs unterwegs gefunden und behoben: (1) eine im selben Zug hinzugefügte 4-Spalten-Regel für die Portfolio-Galerie ab 1600px griff wegen Tailwinds CSS-Reihenfolge nicht (ein "arbiträrer" Breakpoint landete vor dem eingebauten `lg:` im Stylesheet und wurde von diesem überstimmt) — behoben, indem der Breakpoint als echter Tailwind-Breakpoint registriert wurde (`--breakpoint-3xl` in `@theme`). (2) an vier Stellen wurde `max-w-*` und die neue `.rk-shell`-Innenabstands-Klasse versehentlich auf demselben Element kombiniert — durch `box-sizing: border-box` frisst der Innenabstand dann direkt die max-width auf (bei `max-w-7xl` + großem `.rk-shell`-Innenabstand blieb auf großen Monitoren nur noch ein ca. 384px schmaler Kern übrig statt echter 1280px). Behoben, indem Randabstand und max-width konsequent auf zwei verschachtelte Elemente aufgeteilt wurden statt auf eines.

Live auf allen Zielgrößen mit Playwright geprüft (Mobile 390px, Laptop 1440px, 1920px, 2560px, 4K 3840px) — inkl. eines zunächst falschen Screenshot-Befunds („Abschnitte komplett leer"), der sich als Test-Artefakt herausstellte (Scroll-Reveal-Elemente feuern nur beim tatsächlichen Scrollen, nicht bei einem reinen `goto()`+Wartezeit). `tsc --noEmit` und Build fehlerfrei, Testdateien/Symlinks entfernt.

## Neu: Automatisches Mahnwesen mit Gebühren, Azubi-Probezeit, echter Bug in Foto-Vorschau gefunden (10.08.2026) ✅
Drei Anliegen auf einmal: Probezeit für Azubis in der Personalakte nachgerüstet, ein komplett neues automatisches Mahnwesen mit gestaffelten Zusatzgebühren gebaut, und beim Nachprüfen des Instagram-Story-Recap-Generators einen echten, bisher unbemerkten Bug in der Foto-Vorschau gefunden und behoben (betraf potenziell auch die normale Kunden-Galerie).

**Azubi-Probezeit:** Das Probezeit-Feld in der Personalakte war fest auf „Mitarbeiter (fest angestellt)" beschränkt — Azubis haben laut § 20 BBiG aber ebenfalls eine Probezeit (1–4 Monate). Jetzt bei beiden Arten verfügbar, inkl. der Probezeit-Übersicht oben in der Kundenakte und der automatischen Erinnerungs-Mail 14 Tage vor Ablauf.

**Automatisches Mahnwesen (komplett neu):**
- Mahnstufen (Zahlungserinnerung → 1. Mahnung → 2. Mahnung, …) lassen sich wie bisher manuell auslösen — laufen jetzt aber auch **automatisch** im Hintergrund weiter, wenn unter „Einstellungen" aktiviert (bewusst standardmäßig AUS, damit nicht ungefragt echte Kund:innen gemahnt werden).
- Jede Stufe kann eine **gestaffelte Zusatzgebühr** haben (Standard: 0 € / 5 € / 15 €, frei einstellbar), die sich zum offenen Rechnungsbetrag addiert.
- Dieser erhöhte Betrag steht jetzt überall dort, wo es zählt: in der Auftragsansicht, in der **Direktzahlung per Stripe** (Kunde zahlt tatsächlich den erhöhten Betrag) und auf der Mahnung selbst (mit Aufschlüsselung Rechnungsbetrag + Gebühr).
- Der Kunde bekommt bei jeder Stufe automatisch eine **E-Mail** mit dem neuen Betrag und einer Glocken-Benachrichtigung — bisher passierte bei einer Mahnung serverseitig gar nichts Sichtbares für den Kunden, nur ein interner Datenbank-Eintrag.
- Fristen (Tage bis zur 1. Erinnerung, Tage zwischen den Stufen) und die Gebühren sind alle unter „Einstellungen" konfigurierbar.

**Bug gefunden (beim Nachprüfen des Instagram-Story-Recap-Generators aus der letzten Runde):** Nextclouds eigener Vorschaubild-Dienst liefert für Dateipfade mit eckigen Klammern (`[...]`) einen 404 — genau das Namensschema, das in eurem echten Nextcloud für alle Kundenordner verwendet wird (`[PHOTOGRAPHY]`, `[FOTOS]` usw.). Das hatte bisher niemand bemerkt, weil noch kein einziger echter Foto-Ordner im System zugewiesen war. Betraf nicht nur den Story-Recap, sondern potenziell auch die ganz normale Kunden-Fotogalerie (Vorschaubilder). Behoben mit einem automatischen Rückfall auf das Originalfoto + lokale Verkleinerung, falls Nextclouds schneller Vorschau-Dienst fehlschlägt — betrifft beide Stellen. Zusätzlich beim Story-Recap eine zweite, kleinere Unsauberkeit behoben: Foto-Ordner enthalten oft dieselbe Aufnahme zweimal (mit und ohne Wasserzeichen für die Kunden-Vorschau) — vorher landete dieselbe Aufnahme dadurch teils doppelt im Rückblick, jetzt wird pro Motiv nur eine Version verwendet.

**Vollständigkeits-Check der letzten Runde:** Auf Wunsch alle 11 Punkte der vorherigen Erweiterungsrunde (Erinnerungs-Mail, Checkliste, Feedback-Anfrage, Nutzungsrechte, Conventions-Zuteilung, anonyme QM-Einreichung, Steuerrücklage, mehrsprachige Rechnungen, Datenbank-Backup, Story-Recap, Preisvorlagen) plus die Fahrtkosten-/Steuer-Seiten noch einmal live durchgeklickt — alle vorhanden und funktionsfähig (der Story-Recap-Bug oben war der einzige echte Fund).

Live getestet: Mahnstufe mit Gebühr über einen echten Testauftrag ausgelöst, Betrag korrekt in DB/Stripe-Berechnung/PDF nachvollzogen; Azubi-Probezeit-Feld erscheint jetzt korrekt; Story-Recap mit echten Fotos aus dem produktiven Nextcloud generiert (vorher 404-Absturz, jetzt sauberes Ergebnis mit vier tatsächlich unterschiedlichen Motiven statt Duplikaten). Testdaten restlos entfernt, `tsc --noEmit` und Build fehlerfrei, PM2-Logs ohne neue Fehler, neuer Cronjob für die automatische Eskalation eingetragen (inaktiv, bis du sie einschaltest).

## Neu: Kompletter Fehler-Audit + Fahrtkosten-Rechner + Steuer-Seite (10.08.2026) ✅
Auf „prüfe alles auf Error, Fehler und Logikfehler" hin drei spezialisierte Audit-Durchgänge parallel laufen lassen (Zahlungen/Preise/Rechnungen, Berechtigungsprüfungen, Daten-Integrität/Löschungen) — dabei 9 echte Bugs gefunden und alle behoben, plus zwei neue Seiten gebaut, die als „zu klein"/fehlend auffielen.

**Neue Seiten:**
- **Fahrtkosten-Rechner** (`/admin/mileage`, neuer Menüpunkt „Fahrtkosten" unter Finanzen): Kilometer × Kilometerpauschale eintippen, Ergebnis live, direkt als Ausgabe (Kategorie „Reisekosten") erfassbar — Formular vorausgefüllt, aber vor dem Speichern noch änderbar. Standard-Satz (0,30 €/km) unter „Einstellungen" anpassbar, pro Fahrt überschreibbar.
- **Steuer-Seite** (`/admin/tax`, neuer Menüpunkt „Steuer" unter Finanzen): der bisher am unteren Ende der langen Statistik-Seite versteckte EÜR-CSV-Export (für Steuerberater/ELSTER) ist jetzt an einem eigenen, leicht auffindbaren Ort — plus laufende Einnahmen/Ausgaben/Gewinn/Rücklage-Übersicht für das aktuelle Jahr und Link zum Fahrtkosten-Rechner. Wichtig: es gibt weiterhin keine automatisch erstellte „Steuererklärung" selbst (das ist gesetzlich ELSTER/Steuerberater vorbehalten) — die Seite bereitet die Zahlen dafür nur sauber auf.

**Gefundene und behobene Bugs (aus dem Audit):**
1. Statistik-Umsatz/Gewinn zählte akzeptierte Preisvorschläge auch dann mit, wenn der Auftrag danach storniert/abgelehnt wurde — dauerhaft zu hoher ausgewiesener Umsatz.
2. „Rabatt entfernen" bei einem mit einem Geschenkgutschein bezahlten Auftrag setzte den Gutschein nicht zurück — das Guthaben wäre unwiderruflich verloren gewesen.
3. Rabattcode-/Gutschein-Einlösung hatte eine Rennbedingung: zwei nahezu gleichzeitige Einlösungen für denselben Auftrag konnten beide einen Code verbrauchen, aber nur einer wurde am Auftrag gespeichert — jetzt atomar abgesichert (mit automatischem Zurückrollen des verbrauchten Codes, falls die Wettlauf-Situation eintritt).
4. Ein Doppelklick auf „Rechnung erstellen" konnte zwei Rechnungen für denselben Auftrag anlegen — jetzt durch einen eindeutigen Datenbank-Index verhindert.
5. Eine harte Kundenkonto-Löschung entfernte Preisvorschläge und den Auftrags-Verlauf nicht mit — verwaiste Preisvorschläge hätten die Umsatzstatistik dauerhaft und unbemerkt verfälscht.
6. Beim Ersetzen eines Profilbilds, eines Kundenstimme-Bilds, eines Blog-Titelbilds oder des Social-Preview-Bilds (SEO) blieb jeweils die alte Bilddatei für immer verwaist auf der Festplatte liegen — jetzt wird sie beim Ersetzen bzw. Löschen mit entfernt.
7. Der Stripe-Zahlungs-Webhook nutzte kein rennsicheres Muster — eine doppelt zugestellte Stripe-Benachrichtigung (laut Stripe selbst ein bekanntes, gelegentliches Verhalten) hätte theoretisch zwei gültige Gutscheincodes für eine einzige Zahlung erzeugen können. Jetzt wie die übrige Rabatt-/Gutschein-Logik atomar abgesichert.

Die Berechtigungsprüfung (zweiter Audit-Durchgang) fand keine echten Sicherheitslücken — alle serverseitigen Aktionen prüfen konsistent die richtige Berechtigung.

Alle 9 Fixes einzeln live gegen die echte Datenbank verifiziert (u. a. ein direkter Nachweis, dass der Datenbank-Unique-Index eine doppelte Rechnung tatsächlich ablehnt; ein Testkonto komplett angelegt und hart gelöscht, um die Kaskade zu bestätigen; ein Testbild ersetzt und geprüft, dass die alte Datei tatsächlich von der Platte verschwindet). Fahrtkosten-Rechner und Steuer-Seite ebenfalls live durchgeklickt. Testdaten restlos entfernt, `tsc --noEmit` und Build fehlerfrei, PM2-Logs ohne neue Fehler.

## Neu: Datenbank von MongoDB Atlas auf lokal selbst gehostetes MongoDB umgestellt (10.08.2026) ✅
Auslöser: Sorge um ein „MB-Limit". Vor der Umsetzung erst geprüft, statt direkt loszulegen — die Datenbank war zu dem Zeitpunkt nur 1,2 MB groß (reine Test-/Entwicklungsdaten, noch kein Go-Live), und die eigentlichen Fotos liegen ohnehin in Nextcloud, nicht in MongoDB. Das vermutete Limit ist ein Atlas-Tarif-Limit (512 MB beim kostenlosen M0-Tarif), kein MongoDB-Limit an sich. Auf dieser Grundlage drei Optionen vorgelegt: MongoDB lokal selbst hosten, bei Atlas bleiben und nur den Tarif upgraden, oder komplett auf MariaDB umsteigen (hätte den Umbau von ~30 Datenmodellen und praktisch jeder Datenbankabfrage im gesamten Code bedeutet — mehrtägiges Projekt mit entsprechendem Risiko, für ein Problem, das so nicht bestand). Entscheidung: MongoDB lokal selbst hosten — löst das eigentliche Anliegen vollständig, ganz ohne Code-Änderungen in der App selbst.

**Umsetzung:**
- MongoDB Community Server 7.0 direkt auf dem Server installiert (offizielles MongoDB-Repository für Debian 12), inkl. `mongodb-database-tools` für `mongodump`/`mongorestore`.
- Standardmäßig nur auf `127.0.0.1` gebunden (kein externer Zugriff möglich, keine Firewall-Regel nötig — geprüft mit `ss -tlnp`), Zugriffsschutz zusätzlich per Passwort-Authentifizierung aktiviert (vorher unauthentifiziert). Eigener App-Nutzer mit auf die `rakkuV2`-Datenbank beschränkten Rechten (kein Root-/Admin-Zugriff aus der App heraus), Zugangsdaten mit `openssl rand` erzeugt und nur root-lesbar unter `/root/.mongo_local_creds` hinterlegt.
- Alle Daten per `mongodump`/`mongorestore` vom Atlas-Cluster übertragen (inkl. aller Indizes), Dokumentenzahl je Collection vorher/nachher exakt verglichen — beides identisch, keine Daten verloren.
- `.env.local` auf die neue lokale Verbindung umgestellt, alte Atlas-Verbindung als Backup unter `/root/.env.local.pre-mongo-migration.bak` gesichert, PM2 neugestartet.
- **Datenbank-Backup-Skript entsprechend angepasst** (worum ausdrücklich gebeten wurde): lief bisher mit einer selbstgebauten Lösung, weil auf dem Server bis dahin keine MongoDB-Tools installiert waren — jetzt auf echtes `mongodump` umgestellt (robuster, Industriestandard, direkt mit `mongorestore` wiederherstellbar, erhält auch alle Indizes). Läuft weiterhin täglich per Cron, letzte 14 Sicherungen werden aufgehoben, Status + manueller „Jetzt sichern"-Knopf unter „Einstellungen" unverändert nutzbar.

Live komplett end-to-end getestet: alle öffentlichen Seiten weiterhin erreichbar, Admin-Dashboard/Auftragsliste/Auftragsdetail/Statistiken/Team/Einstellungen laden korrekt mit den echten (übertragenen) Daten, ein echter Schreibtest (neue Preisvorlage anlegen) erfolgreich gegen die neue Datenbank verifiziert. Neues Backup-Skript einmal live ausgelöst und die erzeugte Sicherung probeweise in eine Test-Datenbank zurückgespielt (alle 158 Dokumente korrekt wiederhergestellt, danach wieder gelöscht) — die Sicherung ist also nachweislich echt wiederherstellbar, nicht nur „läuft durch". `tsc --noEmit` und Build fehlerfrei, PM2-Logs ohne neue Fehler.

**Für dich:** der alte Atlas-Cluster wird nicht mehr gebraucht — du kannst ihn in Ruhe im Atlas-Dashboard löschen, sobald du selbst einmal geprüft hast, dass alles läuft (er kostet auf dem kostenlosen Tarif nichts, es eilt also nicht). Die alte Verbindung liegt sicherheitshalber noch unter `/root/.env.local.pre-mongo-migration.bak`, falls doch mal ein Blick zurück nötig sein sollte.

## Neu: Vierte Runde Erweiterungsideen — 11 neue Systeme (10.08.2026) ✅
Auf „darfst du gerne alles umsetzen" hin komplett durchgearbeitet, alle 11 Systeme ausführlich und vollständig gebaut (keine Minimal-Versionen), mit granularen Berechtigungen wo sinnvoll:

- **Erinnerungs-Mail vor dem Termin:** automatisch ca. 48h vor dem vereinbarten Shooting-Termin an den Kunden, mit Treffpunkt und der internen Vorbereitungs-Checkliste des Auftrags. Läuft alle 2 Stunden per Cron, Ein/Aus + Vorlauf-Stunden + eigener Text in den Einstellungen, pro Termin nur einmal (setzt sich zurück, wenn der Termin geändert wird).
- **Interne Vorbereitungs-Checkliste pro Auftrag:** direkt in der Auftragsansicht, startet mit vier Standardpunkten (Vertrag unterschrieben/Anzahlung erhalten/Ausrüstung gepackt/Treffpunkt bestätigt), beliebig um eigene Punkte erweiterbar, mit Fortschrittsbalken. Läuft über die bestehende „Aufträge bearbeiten"-Berechtigung.
- **Automatische Feedback-Anfrage:** Sicherheitsnetz zum bereits bestehenden Weg (Zufriedenheits-Mail beim Archivieren eines Auftrags) — falls ein Auftrag nie archiviert wird, obwohl die Foto-Galerie längst freigegeben ist, fragt ein täglicher Cron trotzdem automatisch nach (Verzögerung einstellbar, Standard 5 Tage). Verschickt pro Auftrag garantiert nur einmal.
- **Modellfreigabe/Nutzungsrechte pro Auftrag:** bei bezahlten Standard-Aufträgen (TFP deckt das im ausführlichen TFP-Vertrag längst ab) jetzt ein Feld, ob Fotos fürs Portfolio/Social Media verwendet werden dürfen (ja/nein/nur anonymisiert) — fließt als Schnappschuss in den Standard-Vertrag ein und ändert dort direkt die Nutzungsrechte-Klausel (vorher war der Text immer gleich).
- **Team-Zuteilung für Conventions:** in der Conventions-Übersicht sieht das ganze Team jetzt, wer vor Ort ist, und kann sich per „Ich bin dabei" selbst eintragen/wieder abmelden. Mit der bestehenden „Conventions bearbeiten"-Berechtigung lässt sich auch für andere eintragen/entfernen.
- **Anonyme Einreichung bei der Personalabteilung:** neue Checkbox im QM-Einreichungsformular, nur wenn „Personalabteilung" als Ziel gewählt ist (bei QM-Vorgängen bewusst nicht anonym möglich, da dort Nachvollziehbarkeit wichtiger ist). Die Admin-Ansicht zeigt dann „Anonym" statt des Namens.
- **Steuerrücklage-Hinweis:** auf der Statistik-Seite ein grober Richtwert (Gewinn × einstellbarer Prozentsatz, Standard 28%) als Orientierung — ausdrücklich kein Ersatz für eine steuerliche Beratung, im Text auch so vermerkt.
- **Mehrsprachige Rechnungen:** die Rechnungs-PDF hat jetzt ein komplettes deutsches und englisches Label-Set (alle Feldbezeichnungen, Beträge-Tabelle, Zahlungshinweis, Kleinunternehmer-Hinweis) und wählt automatisch nach der hinterlegten Sprache der/des Rechnungsempfänger:in — unabhängig davon, wer die Rechnung gerade abruft.
- **Eigenes Datenbank-Backup mit Warnhinweis:** da auf dem Server kein `mongodump` installiert ist, eine eigene, abhängigkeitsfreie Node-Lösung gebaut, die täglich per Cron alle Collections als komprimierte Dateien nach `/var/backups/rakku-v2-db` sichert (letzte 14 Sicherungen aufgehoben). Unter „Einstellungen" ein Status-Panel mit Zeitpunkt der letzten Sicherung (rote Warnung, falls überfällig) und einem „Jetzt sichern"-Knopf für den sofortigen manuellen Anstoß.
- **Instagram-Story-Recap-Generator:** neuer Download-Link im Auftrag setzt automatisch aus den höchstbewerteten Fotos (Kunden-Sterne) ein 1080×1920-Story-Format-Bild mit Rakku-Branding-Overlay zusammen — spart das manuelle Zusammenstellen für Instagram Stories.
- **Preisvorschlags-Vorlagen:** neuer Verwaltungsbereich „Preisvorlagen" (eigene granulare Berechtigung `price_templates`) für wiederkehrende Preisvorschlags-Kombinationen (Name, Standardbetrag, Notiztext) — im Preisvorschlags-Formular eines Auftrags als Dropdown wählbar, füllt Betrag und Notiz automatisch vor, bleibt danach weiter editierbar.
- Zusätzlich beim Nachsehen festgestellt: die schon länger im Backlog stehende „Admin-Navigation nach Berechtigung filtern" war tatsächlich längst umgesetzt — nur eine veraltete Notiz im Plan, jetzt bereinigt.

**Dabei gefundener und behobener Bug:** beim Live-Testen der neuen Checkliste fiel auf, dass ein Haken nach dem Anklicken kurz wieder leer erschien, obwohl die Datenbank korrekt gespeichert hatte — die kontrollierte Checkbox sprang für einen Moment auf den alten Anzeigestand zurück, bevor die Seite die aktualisierten Serverdaten geladen hatte. Mit einem optimistischen lokalen Zustand behoben, der den Haken sofort und stabil anzeigt.

Live komplett end-to-end gegen die echte Datenbank getestet: Preisvorlage angelegt und im Preisvorschlags-Formular korrekt angewendet, Checkliste (Standardpunkt abhaken, eigenen Punkt hinzufügen — inkl. Bugfix oben), Nutzungsrechte gesetzt und in der Datenbank verifiziert, anonyme QM-Einreichung korrekt ohne Namen in der Admin-Ansicht, Steuerrücklage-Hinweis sichtbar, Datenbank-Sicherung manuell ausgelöst und Datei-Ausgabe verifiziert, alle drei neuen Einstellungs-Panels sichtbar, Convention angelegt und „Ich bin dabei" korrekt in der Datenbank, echte Rechnung mehrsprachig abgerufen (200 OK), Story-Recap-Route sauber erreichbar (404 bei fehlenden Fotos statt Absturz), Dashboard weiterhin fehlerfrei nach allen neuen Datenmodellen. Alle Testdaten restlos entfernt. `tsc --noEmit` und Build nach jedem einzelnen System sowie am Ende fehlerfrei, PM2-Logs ohne neue Fehler. Cronjobs für alle vier neuen automatischen Abläufe (Erinnerungs-Mail, Feedback-Anfrage, Backup) in der System-Crontab eingetragen.

**Als Nächstes (nicht mehr Teil dieser Runde):** gemeinsame Sitzung zur Umstellung der Datenbank auf die Root-Domain.

## Neu: Admin-Dashboard als Startseite + Benachrichtigungssystem (10.08.2026) ✅
Beim Öffnen des Admin-Bereichs landet man jetzt zuerst auf einer echten Startseite (`/admin`) statt direkt in den Aufträgen. Login-Weiterleitung entsprechend angepasst.

**Startseite:**
- Oben eine große, live tickende Uhr mit Datum und Begrüßung, die sich nach Tageszeit richtet (Guten Morgen/Tag/Abend, spätnachts „Noch spät unterwegs"), personalisiert mit Vornamen.
- Darunter Kacheln mit den wichtigsten Infos auf einen Blick — **welche Kacheln erscheinen, richtet sich nach den eigenen Berechtigungen** (Position): „Aufträge" (offene Anzahl, offene Zahlungen, letzte 4 Aufträge) und „Nächste Termine" nur mit Auftrags-Berechtigung, „QM & Personal" nur mit QM- oder Personal-Berechtigung, „Umsatz diesen Monat" nur mit Statistik-Berechtigung. „ToDos" (eigene offene) und „Benachrichtigungen" sieht dagegen jede:r, unabhängig von Berechtigungen — das sind rein persönliche Bereiche.

**Benachrichtigungssystem (neu, für Team und Kunden):**
- Glocken-Icon in der Kopfzeile, für Mitarbeiter:innen UND Kund:innen gleichermaßen (Kunden bekommen z. B. eine Benachrichtigung, wenn das Team ihnen im Auftrags-Chat antwortet). Zeigt die Anzahl ungelesener Benachrichtigungen als Badge, aktualisiert sich automatisch (Abfrage alle 45 Sekunden). Klick auf eine Benachrichtigung markiert sie als gelesen und führt direkt zur passenden Seite (z. B. dem betroffenen Auftrag).
- „Alle anzeigen" öffnet eine vollständige Liste (eigene Seite für Team unter `/admin/notifications`, für Kunden unter `/account/notifications`, dort auch übersetzt für EN), mit „Alle als gelesen markieren"-Knopf.
- **Ausgelöst wird eine Benachrichtigung bei:** neuer Auftragsanfrage (an alle Mitarbeiter:innen mit Auftrags-Berechtigung), neuer Kundennachricht im Auftrags-Chat (an die/den zuständige:n Mitarbeiter:in, falls der Auftrag übernommen wurde, sonst an alle mit Auftrags-Berechtigung), neuer Team-Antwort im Chat (an die/den Kund:in, in deren/dessen eigener Sprache verlinkt), sowie neuer QM-/Personal-Einreichung (an alle mit der jeweiligen Berechtigung).

Live end-to-end gegen die echte Datenbank getestet (nicht nur die Oberfläche): Dashboard-Darstellung inkl. Uhr/Begrüßung/Kacheln per Screenshot geprüft; ein eigens angelegtes, stark eingeschränktes Test-Konto ganz ohne Berechtigungen bekommt korrekt nur die beiden immer-sichtbaren Kacheln (ToDos, Benachrichtigungen) zu sehen. Echte Kundennachricht im Chat erzeugt korrekt eine Benachrichtigung fürs Team (samt Glocken-Badge und Eintrag im Dropdown), echte Team-Antwort erzeugt korrekt eine Benachrichtigung für den Kunden. Klick auf eine einzelne Benachrichtigung markiert genau diese als gelesen (Nachbarn bleiben ungelesen) — direkt in der Datenbank verifiziert. „Alle als gelesen markieren" auf der Vollansicht setzt korrekt alle auf gelesen. Echte QM-Einreichung erzeugt korrekt eine Benachrichtigung. Alle Testdaten (Testkonto, Test-Position, Test-Nachrichten, Test-Benachrichtigungen, Test-QM-Einreichung) danach restlos entfernt. `tsc --noEmit` und Build fehlerfrei, PM2-Logs ohne neue Fehler.

## Neu: ToDo-System für das ganze Team (10.08.2026) ✅
Komplett neuer Bereich unter „ToDos" (eigener Punkt in der Admin-Navigation, immer sichtbar für jede:n Mitarbeiter:in — nicht an eine bestimmte Berechtigung gebunden, damit ihn wirklich jede:r für sich nutzen kann).

**Grundprinzip — eigene ToDos immer, fremde nur mit Berechtigung:**
- Jede:r sieht und verwaltet die eigenen ToDos (zugewiesen ODER selbst angelegt) immer, ganz ohne besondere Rechte.
- Mit der neuen Berechtigung „ToDos" (Ansehen/Erstellen/Bearbeiten/Löschen, wie bei den anderen Bereichen granular einzeln vergebbar) sieht man zusätzlich die ToDos **aller** Mitarbeiter:innen, kann ToDos für andere anlegen/zuweisen und fremde bearbeiten/löschen. Ohne diese Rechte bleibt der Bereich rein persönlich — genau wie gewünscht.

**Funktionsumfang:**
- Titel, Beschreibung, Priorität (Niedrig/Normal/Hoch, farbcodiert), Fälligkeitsdatum, Status (Offen/In Bearbeitung/Erledigt).
- Optionaler Auftragsbezug — ein ToDo lässt sich mit einem konkreten Auftrag verknüpfen, mit direktem Link dorthin.
- **Notizen:** zu jedem ToDo lassen sich fortlaufend Notizen hinzufügen (mit Name + Zeitpunkt) — z. B. Status-Updates oder Rückfragen, ohne die eigentliche Aufgabe zu verändern. Wer ein ToDo sehen darf, darf auch Notizen dazu schreiben.
- Statistik-Kacheln (Offen/In Bearbeitung/Überfällig/Erledigt), Filter nach Status und (bei Team-weiter Sicht) nach Mitarbeiter:in, Ansicht „Meine ToDos" vs. „Alle ToDos".
- Überfällige ToDos werden farblich hervorgehoben.

Live komplett end-to-end getestet, inklusive eines eigens angelegten, stark eingeschränkten Testkontos ganz ohne ToDo-Rechte: sieht nur die eigenen ToDos, das Zuweisen-Feld beim Anlegen ist gesperrt (nur sich selbst zuweisbar), kann aber trotzdem eigene ToDos anlegen, bearbeiten, den Status ändern und Notizen hinzufügen. Mit deinem echten Konto zusätzlich Status ändern, Notiz hinzufügen, bearbeiten und löschen einzeln direkt gegen die Datenbank verifiziert (nicht nur die Oberfläche) — alles korrekt gespeichert. Testdaten restlos entfernt. `tsc --noEmit` und Build fehlerfrei.

## Adresse bei Terminvergabe: strukturierte Felder statt Freitext (10.08.2026) ✅
Weitere Nachbesserung zur Adresse bei der Terminvergabe: auf Rückfrage entschieden — strukturierte Felder statt einer Google-Maps-Adresssuche (letztere hätte einen neuen, kostenpflichtigen Google-API-Schlüssel gebraucht). Jetzt genau wie bei der Rechnungsadresse im eigenen Profil: „Straße & Hausnummer" (eigene Zeile), „PLZ"/„Ort" nebeneinander, „Land" darunter — statt einem einzelnen Freitextfeld.

**Technisch:** der Auftrag hat jetzt eigene Adressfelder (Straße/PLZ/Ort/Land) getrennt vom ursprünglichen, vom Kunden bei der Anfrage frei eingegebenen „Ort/Event"-Feld — beide bleiben erhalten, aber der Kalender-Export, die Kundenanzeige und die Google-Kalender-Übertragung nutzen jetzt automatisch die genaue Adresse, sobald das Team sie einträgt, und fallen andernfalls auf die ursprüngliche freie Angabe zurück.

Live end-to-end erneut getestet: Adresse strukturiert eingegeben und gespeichert, korrekt auf der Kundenseite, im Google-Kalender-Link und in der heruntergeladenen .ics-Datei angekommen. Testdaten danach entfernt. `tsc --noEmit` und Build fehlerfrei.

## Bugfix: Adressfeld bei Terminvergabe zu klein (10.08.2026) ✅
Feedback zum letzten Punkt (Adresse bei Terminvergabe): das einzeilige Textfeld war unpraktisch zum Eintragen einer vollständigen Adresse. Jetzt wie beim eigenen Profil ein mehrzeiliges, in der Höhe ziehbares Textfeld (Straße/PLZ/Ort auf eigenen Zeilen). Die Zeilenumbrüche werden jetzt auch überall korrekt mit angezeigt (Kundenseite, Admin-Auftragsansicht), statt zu einer Zeile zusammengefasst zu werden. Live geprüft: Adresse mit mehreren Zeilen eingegeben und gespeichert, erscheint auf der Kundenseite korrekt mit Zeilenumbrüchen. Testdaten danach entfernt. `tsc --noEmit` und Build fehlerfrei.

## Neu: Aufträge übernehmen (10.08.2026) ✅
Vorgriff auf ein Team mit mehreren Fotograf:innen: Aufträge lassen sich jetzt „übernehmen" — sichtbar fürs ganze Team (wer macht was) und für den Kunden (weiß, wer sich kümmert).

- **In der Auftragsübersicht** zeigt jeder Auftrag jetzt ein Abzeichen: entweder „👤 Name" oder „Nicht übernommen" (auffällig hervorgehoben).
- **In der Auftragsansicht** gibt es oben einen „Übernehmen"-Knopf (bzw. „Abgeben", wenn man selbst übernommen hat). Bewusst als Selbst-Übernahme statt einer Zuweisung an andere gebaut — wer übernimmt, meldet sich aktiv. Übernimmt jemand einen bereits von einer Kollegin/einem Kollegen übernommenen Auftrag, kommt vorher eine Rückfrage. Landet auch im Auftrags-Verlauf.
- **Berechtigung:** bewusst keine neue eigene Berechtigung — läuft über die bereits bestehende „Aufträge bearbeiten"-Berechtigung, wie gewünscht. Mit reiner Ansicht-Berechtigung sieht man weiterhin, wer übernommen hat, aber keine Knöpfe zum Ändern.
- **Kundenseite:** zeigt „<Name> kümmert sich um deinen Auftrag" mit Profilbild, sobald jemand übernommen hat.

Live mit zwei echten Testkonten durchgetestet (End-to-End über die echte Oberfläche, nicht nur Datenbank): Übernehmen, Ansicht in der Übersicht, Ansicht beim Kunden, Übernahme von einer Kollegin mit Rückfrage-Dialog, Abgeben — alles korrekt. Testkonten und -daten danach vollständig entfernt. `tsc --noEmit` und Build fehlerfrei.

## Adresse bei Terminvergabe + korrekte Kalender-Übertragung (10.08.2026) ✅
Ergänzung zum letzten Punkt (Kalender-Export): bei der Terminvergabe im Auftrag (`/admin/orders/…`) gibt es jetzt ein eigenes Adressfeld direkt neben Von/Bis — der Treffpunkt lässt sich also genau dann festlegen, wenn der Termin steht, nicht mehr nur vage vorab.

**Wo die Adresse jetzt überall ankommt:**
- **Kundenseite:** erscheint direkt im „Termin"-Kasten (📍 …), zusammen mit dem „Zum Kalender hinzufügen"-Menü vom letzten Mal — Google/Outlook/​.ics enthalten die Adresse jetzt automatisch.
- **Admin-Kalender:** erscheint in der Tooltip-Vorschau beim Überfahren eines Termins.
- **Google-Kalender-Sync:** bisher landete die Adresse (falls überhaupt gesetzt) nur in der Beschreibung des übertragenen Google-Termins — dabei einen echten Bug gefunden und behoben: Google erkennt eine Beschreibung nicht als navigierbaren Ort (keine Kartenvorschau, keine Wegbeschreibung, keine Suche). Jetzt wird das eigene `location`-Feld der Google-Kalender-API befüllt, wie es auch für die eigentliche Zielsetzung „damit sie wissen, wo sie hinmüssen" nötig ist. Betraf beide Übertragungswege (laufende Terminvergabe und den nachträglichen Massen-Abgleich).

Live komplett end-to-end getestet (echter Testauftrag über die echte Oberfläche): Termin + Adresse im Admin gesetzt → Adresse erscheint korrekt im Admin-Kalender-Tooltip, auf der Kundenseite und in allen drei Kalender-Export-Optionen (Google-Link, Outlook-Link, .ics-Datei mit korrekt maskierter `LOCATION`-Zeile). Datenbank direkt geprüft, keine Fehler im Server-Log. Testdaten danach zurückgesetzt. `tsc --noEmit` und Build fehlerfrei.

## Neu: Auftragstermine zum eigenen Kalender hinzufügen (10.08.2026) ✅
Kunden konnten ihren bestätigten Shooting-Termin bisher nirgends in ihrem eigenen Kalender (Google, Outlook, Apple) speichern — der genaue Termin (von dir im Admin-Bereich gesetzt) wurde auf der Auftragsseite des Kunden bisher noch nicht einmal angezeigt, nur der grobe „Wunschzeitraum" aus der ursprünglichen Anfrage.

**Neu:** sobald du einem Auftrag einen Termin gibst, erscheint auf der Auftragsseite des Kunden ein „Termin"-Kasten mit Datum/Uhrzeit (inkl. Enddatum, falls gesetzt) und einem Klapp-Menü „Zum Kalender hinzufügen" mit drei Optionen:
- **Google Kalender** — öffnet Google Kalender mit bereits ausgefülltem Termin.
- **Outlook** — dasselbe für Outlook.com.
- **Apple Kalender / .ics-Datei** — lädt eine Standard-Kalenderdatei herunter, die sich in praktisch jeder Kalender-App (Apple Kalender, Outlook Desktop, Thunderbird u. a.) öffnen lässt.

Titel, Ort (aus dem Auftragsfeld) und Zeit sind bei allen drei Varianten schon korrekt vorausgefüllt. Ohne gesetztes Enddatum wird wie beim internen Kalender automatisch eine Stunde als Standarddauer angenommen.

Live end-to-end getestet: Termin auf einem echten Testauftrag gesetzt, Anzeige auf der Kundenseite korrekt (inkl. korrekter Zeitzonen-Umrechnung UTC → Ortszeit), alle drei Links mit korrektem Titel/Zeit/Ort geprüft, .ics-Datei heruntergeladen und den Inhalt Zeile für Zeile verifiziert (gültiges Format, Sonderzeichen im Ort korrekt maskiert). Zugriffsschutz geprüft: nicht eingeloggt oder mit fremder Sitzung → 401, kein Zugriff auf fremde Termine. Testdaten danach entfernt. `tsc --noEmit` und Build fehlerfrei.

## Bugfix: Live-Chat war im Wartungsmodus nicht erreichbar (10.08.2026) ✅
Gemeldet: der Tawk-Live-Chat sollte auch bei aktiviertem Wartungsmodus sichtbar bleiben, war es aber nicht — er stand im Code an einer Stelle, die nur beim normalen Seitenaufbau lief, nicht auf der Wartungsseite. Jetzt außerhalb dieser Weiche platziert, läuft also unabhängig vom Wartungsmodus (weiterhin nur, wenn der Chat in den Einstellungen aktiviert ist). Mit aktivem Wartungsmodus live geprüft: das Tawk-Skript lädt jetzt auch auf der Wartungsseite. Wartungsmodus danach wieder deaktiviert. `tsc --noEmit` und Build fehlerfrei.

## Bugfix: Ecken-Rahmen auf der Wartungsseite falsch positioniert (09.08.2026) ✅
Direkt nach dem letzten Test gemeldet (Screenshot): die vier Ecken-Klammern um den Wartungshinweis saßen fast am Rand des gesamten Bildschirms statt eng um den Textkasten, und oben mit deutlich mehr Abstand als unten.

**Ursache 1:** dem Textkasten fehlte `position: relative` — die Ecken positionierten sich dadurch nicht relativ zu ihm, sondern relativ zum nächsten Elternelement mit Positionierung (die volle Bildschirmhöhe), daher der große, willkürliche Versatz.

**Ursache 2:** die Ecken-Klammern sind ein wiederverwendetes Motiv, das auf der Startseite absichtlich oben tiefer sitzt, damit es unter dem schwebenden Header bleibt — dieser Versatz war aber fest in die gemeinsame Regel eingebacken statt nur für die Startseite zu gelten, wurde also unpassend auch auf der Wartungsseite (ganz ohne schwebenden Header) übernommen.

**Behoben:** `position: relative` beim Textkasten ergänzt, der Header-Versatz in einen eigenen, nur auf der Startseite genutzten Zusatz verschoben — auf der Wartungsseite sitzen alle vier Ecken jetzt gleich weit innen. Mit Screenshots von Wartungsseite (jetzt korrekt eng um den Text) und Startseite (unverändert, Ecken weiterhin unter dem Header) verifiziert. `tsc --noEmit` und Build fehlerfrei.

## Wartungsmodus: An/Aus-Schalter + Passwort-Vorschau (09.08.2026) ✅
Der Wartungsmodus (Kunden sehen statt der Seite einen „Wir sind kurz im Fotolabor"-Hinweis) existierte technisch schon länger im Datenmodell und in der Anzeigelogik, hatte aber **nirgends eine Bedienoberfläche** — er ließ sich also bisher gar nicht ein-/ausschalten. Jetzt nachgeholt, unter „Einstellungen" (gleiche Berechtigung wie alle anderen Systemeinstellungen, wie gewünscht):

- **Ein-/Ausschalten** + optionale eigene Nachricht statt des Standardtexts.
- **Vorschau-Passwort (optional):** wer es auf der Wartungsseite eingibt, sieht die Seite trotzdem normal — praktisch, um selbst z. B. in einem privaten Browserfenster zu prüfen, wie die Seite gerade für echte Besucher:innen aussieht, ohne extra einen Mitarbeiter-Login zu brauchen. Mitarbeiter-Logins kommen ohnehin immer automatisch durch (das gab es schon).
- Bewusst als Cookie umgesetzt, das aus dem Passwort abgeleitet wird (nicht das Passwort selbst gespeichert) — ändert sich das Passwort später, werden alte Freigaben automatisch ungültig.

Dabei einen echten, bisher unbemerkten Bug fürs nächste Mal vermieden: die Wartungsseite nutzte einen Übersetzungs-Hook, der eigentlich nur in interaktiven Bereichen funktioniert, war aber nicht als solcher markiert — da der Wartungsmodus bisher nie eingeschaltet werden konnte, ist das nie aufgefallen. Beim Umbau zur interaktiven Passwort-Vorschau gleich mit behoben.

Live komplett durchgetestet (mit automatischer Rückstellung direkt danach): anonymer Besuch sieht die Wartungsseite ✅, Mitarbeiter-Login kommt weiterhin ungehindert durch ✅, falsches Passwort zeigt eine Fehlermeldung und lässt niemanden durch ✅, richtiges Passwort schaltet frei und bleibt auch nach einer neuen Seitennavigation gespeichert ✅. Wartungsmodus danach wieder korrekt deaktiviert. `tsc --noEmit` und Build fehlerfrei.

## Verfügbarkeitskalender: beliebig weit vorblätterbar (09.08.2026) ✅
Der öffentliche Frei/Belegt-Kalender auf `/angebote` zeigte bisher fest nur den aktuellen und den folgenden Monat, ohne Möglichkeit, weiter vorauszuschauen. Auf Wunsch („mindestens 1 Jahr im Voraus schauen können") umgebaut: weiterhin zwei Monate gleichzeitig sichtbar, jetzt aber mit „‹"/„›"-Pfeilen daneben, die jeweils um zwei Monate weiterblättern — nach vorne unbegrenzt (kein künstliches Ende nach einem Jahr), nach hinten nur bis zum aktuellen Monat (Pfeil dort automatisch deaktiviert, in die Vergangenheit lässt sich nicht blättern).

**Technisch:** aus der bisherigen Server-Komponente (die Daten fest beim Seitenaufbau berechnete) wurde eine Client-Komponente, die die Belegt-Tage für das jeweils angezeigte Zwei-Monats-Fenster über eine neue, öffentliche Route (`/api/availability`) nachlädt — dadurch lässt sich beliebig weit blättern, ohne dass die ganze Seite neu geladen werden muss. Während des Nachladens werden die Tage neutral (nicht fälschlich als „frei") angezeigt, bis die echten Daten da sind.

Live getestet: 6× „weiter" geklickt → springt korrekt von August/September 2026 auf August/September 2027 (genau ein Jahr voraus), „zurück"-Pfeil war zu Beginn korrekt deaktiviert und wurde nach dem Vorblättern korrekt wieder aktiv, keine Konsolenfehler, Englische Version ebenfalls geprüft (eigene Übersetzungen für die Pfeile ergänzt).

## Fehler-Durchgang: drei echte Bugs gefunden und behoben (09.08.2026) ✅
Auf Wunsch einen kompletten Fehler-Check über die letzten Änderungen laufen lassen (`tsc --noEmit`, vollständiger Build, ESLint, pm2-Logs, Code-Durchsicht der neueren Funktionen). Build/TypeScript waren durchgehend sauber, aber bei der Code-Durchsicht drei echte, noch nicht bemerkte Logikfehler gefunden:

1. **Telefonnummer konnte beim Speichern eines Mitarbeiter-Profils stillschweigend gelöscht werden.** Das neue Telefonnummer-Feld war wie das Geburtsdatum nur im Kunden-Teil des Profilformulars sichtbar — anders als beim Geburtsdatum fehlte aber die Absicherung dagegen, dass ein leeres Feld eine vorhandene Nummer überschreibt. Betraf z. B. einen zum Mitarbeiter beförderten Kunden mit bereits hinterlegter Nummer. Behoben mit derselben Absicherung, die beim Geburtsdatum schon bestand.
2. **Anonymisieren eines Kundenkontos (bei vorhandenen Rechnungen) löschte die Telefonnummer nicht mit.** Die Anonymisierungs-Funktion wurde geschrieben, bevor es das Telefonnummer-Feld gab, und wusste entsprechend noch nichts davon — Name, Adresse, Geburtsdatum etc. wurden korrekt überschrieben, die Telefonnummer blieb aber unbemerkt stehen. Ergänzt.
3. **Referenzbilder blieben nach einer echten Kontolöschung als verwaiste Dateien auf der Festplatte liegen.** Beim harten Löschen eines Kundenkontos wurden die zugehörigen Auftrags-Datenbankeinträge korrekt entfernt, die hochgeladenen Referenzbild-Dateien selbst (lokal gespeichert, nicht in Nextcloud) aber nie — ohne den Datenbankeintrag ließ sich ihr Pfad danach auch nicht mehr nachvollziehen, sie wären für immer liegen geblieben. Jetzt werden die zugehörigen Ordner beim harten Löschen mit entfernt.

Zusätzlich ein kleinerer Schönheitsfehler behoben: beim automatischen Wasserzeichen auf Original-Downloads (siehe letzter Eintrag) wurde bei einer ursprünglichen PNG/WebP-Datei der Dateiname beim Download trotzdem mit alter Endung ausgeliefert, obwohl das Wasserzeichen die Datei technisch immer als JPEG ausliefert — Dateiname wird jetzt passend angepasst.

Alle vier Korrekturen mit einem eigenen Testskript direkt gegen die echte Datenbank verifiziert (Anonymisieren löscht jetzt die Nummer, hartes Löschen entfernt jetzt den Datei-Ordner), zusätzlich ein kompletter Seiten-Smoketest (13 zentrale Admin- und Kundenseiten, alle 200 OK) und ein Blick in die pm2-Fehlerprotokolle (keine neuen Fehler, nur eine bekannte, harmlose Mongoose-Warnung). `tsc --noEmit` und Build liefen am Ende erneut fehlerfrei durch.

**Nicht angerührt:** ~70 vorbestehende ESLint-Stilhinweise quer durchs Projekt (z. B. `<a>` statt `<Link>`, ungeschützte Anführungszeichen in JSX) — rein kosmetisch, blockieren den Build nicht und stammen nicht aus den heutigen Änderungen. Auf Wunsch kann ich die in einem eigenen Durchgang aufräumen.

## Telefonnummer jetzt Pflichtfeld für Kundenkonten (09.08.2026) ✅
Auf Wunsch genau wie beim Geburtsdatum: Telefonnummer ist jetzt ein Pflichtfeld für Kundenkonten — an allen drei Stellen, an denen auch Geburtsdatum/Anschrift schon Pflicht waren.

- **Registrierung** (`/register`): neues Pflichtfeld, gleiche leichte Validierung wie bei den anderen Feldern (mind. 5 Ziffern).
- **Profil bearbeiten**: greift auch für Bestandskonten von vor der Umstellung — sobald so ein Konto das nächste Mal das Profil speichert, muss die Telefonnummer nachgetragen werden (exakt dasselbe Muster wie bei Geburtsdatum/Anschrift).
- **„+ Neuer Kunde"** (Team legt Konto vor Ort an): ebenfalls Pflichtfeld, damit auch auf diesem Weg angelegte Konten vollständig sind.

Telefonnummer erscheint jetzt auch in der Kundenakte (unter der E-Mail) und im CSV-Export der Kundenliste. Mitarbeiter-Profile sind davon unberührt (nur für Kundenkonten Pflicht, wie beim Geburtsdatum).

Live getestet: Registrierung ohne Telefonnummer wird blockiert (mit Fehlermeldung), mit Telefonnummer erfolgreich, Feld korrekt in der Datenbank gespeichert; „+ Neuer Kunde"-Formular zeigt das Feld ebenfalls. Testkonto danach entfernt. `tsc --noEmit` und Build fehlerfrei.

## Fünf neue Erweiterungsideen umgesetzt (09.08.2026) ✅
Auf „alles bis auf Punkt 6" hin fünf der sechs zuletzt vorgeschlagenen Ideen umgesetzt (die WhatsApp/SMS-Erinnerung bewusst ausgelassen, da sie einen bezahlten Drittanbieter braucht).

**1. Wasserzeichen auf Vorschaufotos bis zur Bezahlung.** Foto-Ordner, die direkt aus einem Auftrag heraus zugewiesen werden, sind jetzt optional mit diesem Auftrag verknüpft (`ClientPhotoFolder.orderId`, komplett optional — Foto-Ordner ohne Auftragsbezug bleiben wie bisher unangetastet). Solange der verknüpfte Auftrag nicht als bezahlt markiert ist, bekommen sowohl die Vorschaubilder als auch die Originaldateien beim Ausliefern automatisch ein Wasserzeichen (per `sharp` live eingerechnet, nicht gespeichert — verschwindet also automatisch und sofort, sobald der Auftrag als bezahlt markiert wird, kein Zutun nötig). Betrifft sowohl Kunden- als auch Admin-Ansicht der Galerie, damit sofort auffällt, wenn eine Zahlung noch aussteht. Kunde sieht dazu einen erklärenden Hinweistext auf der Fotoseite. Mit einem echten Testauftrag + echtem Nextcloud-Testordner (das schon bekannte „Marcel"-Verzeichnis) verifiziert: Vorschau- UND Volldatei zeigten das Wasserzeichen deutlich lesbar, nach Markieren als „bezahlt" verschwand es sofort bei beiden Varianten, danach wieder auf „nicht bezahlt" zum Gegentest — Wasserzeichen erschien wieder.

**2. Fandom-/Charakter-Auswertung.** Neuer Abschnitt „Beliebteste Charaktere & Fandoms" in den Statistiken, ausgewertet aus dem Freitextfeld „Charakter/Konzept" bei der Auftragsanfrage (Top 15, grob normalisiert). Zeigt, welche Fandoms/Conventions sich für dich lohnen. Live gegen die echten Auftragsdaten geprüft, Abschnitt erscheint korrekt.

**3. Kunden-CSV-Export.** Neuer „CSV exportieren"-Knopf auf `/admin/clients` (Name, Spitzname, E-Mail, Unternehmen, Anschrift, Tags, Verifiziert, Registriert am, Auftragsanzahl) — z. B. für Weihnachtskarten oder einen Newsletter-Import außerhalb der Seite. Mit echtem Download getestet, BOM korrekt für Umlaute in Excel.

**4. Referenzbild-Upload nachträglich möglich.** Die Referenzbild-Funktion bei der Auftragsanfrage gab es schon (bis zu 8 Dateien) — neu ist, dass Kunden jetzt auch **nachträglich** weitere Referenzbilder zu einem bestehenden, noch offenen Auftrag hochladen können (z. B. wenn die Pose-Idee erst später konkret wird), nicht mehr nur einmalig bei der Anfrage. Bei abgelehnten/archivierten Aufträgen erscheint die Upload-Möglichkeit bewusst nicht mehr. Landet auch im Auftrags-Verlauf sichtbar für dich. Live getestet: Upload bei offenem Auftrag funktionierte und wurde korrekt geloggt, bei einem abgelehnten+archivierten Testauftrag war die Upload-Möglichkeit korrekt nicht vorhanden.

**5. Zufriedenheits-Trend in den Statistiken.** Neuer Abschnitt „Zufriedenheit über Zeit" mit Balkendiagramm der durchschnittlichen Sternebewertung pro Monat (aus dem bestehenden Feedback-Formular), damit Qualitätsschwankungen früh auffallen. Mit zwei Test-Bewertungen verifiziert (Diagramm + Gesamtdurchschnitt korrekt berechnet), danach entfernt.

Alle fünf Punkte einzeln gegen die Live-Seite getestet, Testdaten/-konten restlos entfernt. `tsc --noEmit` und vollständiger Build liefen beide fehlerfrei durch.

## Personalakte: zwei neue Eintragsarten — Kündigung und Notiz (09.08.2026) ✅
Feedback zur „Art"-Auswahl in der Personalakte: es fehlten Eintragsarten für Kündigungen und allgemeine Bemerkungen, die nicht direkt zu einem Beschäftigungsverhältnis gehören.

- **Kündigung:** eigene Eintragsart mit Datum und Pflichtfeld „Kündigungsart" (Ordentliche Kündigung durch Arbeitgeber, Fristlose Kündigung durch Arbeitgeber, Eigenkündigung durch Arbeitnehmer, Aufhebungsvertrag) — rot markiert, klar erkennbar als bedeutsames Ereignis.
- **Notiz:** freie Bemerkung mit Datum, unabhängig von einem laufenden Beschäftigungsverhältnis (z. B. eine Verwarnung oder Absprache festhalten) — grau markiert, neutral.
- Beide sind Einzel-Ereignisse mit nur einem Datum (kein „Von/Bis", keine Probezeit) — das Formular blendet die dafür nicht passenden Felder automatisch aus, sobald eine der beiden Arten gewählt ist.
- Die Probezeit-Anzeige oben in der Übersicht wurde dabei robuster gemacht: sie bezieht sich jetzt gezielt auf den letzten „Mitarbeiter"-Eintrag, nicht mehr auf den zeitlich letzten Eintrag überhaupt — eine später hinzugefügte Notiz oder Kündigung verdeckt die Probezeit-Anzeige dadurch nicht mehr versehentlich.

Mit allen fünf Eintragsarten (inkl. der beiden neuen) gegen die Live-Seite getestet, inklusive eines gezielten Fehlversuchs (Kündigung ohne ausgewählte Kündigungsart abschicken) — wird vom Browser blockiert, bevor überhaupt eine Anfrage rausgeht. Testdaten danach entfernt. `tsc --noEmit` und Build fehlerfrei.

## Personalakte überarbeitet: Farben, Reihenfolge, Probezeit-Countdown (09.08.2026) ✅
Direktes Feedback zur neuen Personalakte: das Formular zum Hinzufügen eines Eintrags stand unter der Liste (wirkt verkehrt herum), die Einträge waren optisch nicht unterscheidbar, das Bemerkungsfeld war nur durch einen Platzhaltertext erkennbar (kein richtiges Label wie die anderen Felder) und eine laufende Probezeit war nirgends auf einen Blick sichtbar.

- **Formular jetzt oben**, die bestehenden Einträge direkt darunter — man legt erst an, sieht dann die Historie.
- **Bemerkung** hat jetzt ein richtiges Feld-Label wie „Von"/„Bis"/„Probezeit", statt nur als Platzhaltertext zu verschwinden, sobald man tippt.
- **Farbcodierung je Art:** Mitarbeiter (fest angestellt) grün, Teamler blau, Praktikant:in amber, Azubi violett — als farbiger Rand + dezente Hintergrundfarbe je Eintrag, sofort erkennbar auch bei vielen Einträgen.
- **Laufende Probezeit jetzt oben in der Übersicht** (bei Abteilung/Position/Teams), nicht mehr nur versteckt in der Personalakte weiter unten — mit genauem Enddatum und Countdown in Tagen (z. B. „Bis 1.10.2026 — noch 53 Tage"), automatisch nur sichtbar, solange die Probezeit noch läuft.

Mit drei echten Testeinträgen (unterschiedliche Arten, eine mit laufender Probezeit) gegen die Live-Seite verifiziert — Formularreihenfolge, Farben, Label und Countdown korrekt, danach wieder entfernt. `tsc --noEmit` und Build fehlerfrei.

## Neu: Kunden können ihr Konto löschen, Team kann es auch (09.08.2026) ✅
Unter „Profil bearbeiten" gibt es jetzt einen roten „Konto löschen"-Bereich (nur für Kundenkonten, nicht für Mitarbeiter). Nach Passwort-Bestätigung passiert eines von zwei Dingen:

- **Keine Rechnung vorhanden** (z. B. Konto ohne Auftrag, oder Auftrag ohne Rechnungsstellung): Konto wird sofort und vollständig gelöscht — inklusive aller Aufträge, Verträge, Foto-Ordner-Zuordnungen, Erinnerungen, Bewertungen und dem Profilbild. Rabattcode-/Gutschein-Verknüpfungen werden nur entkoppelt, die Codes/Gutscheine selbst bleiben bestehen.
- **Rechnung(en) vorhanden:** eine echte Löschung ist rechtlich nicht möglich — Rechnungen müssen in Deutschland 10 Jahre aufbewahrt werden (§ 147 AO). Stattdessen wird die Löschung **beantragt**: das Team bekommt eine E-Mail (an die in den Einstellungen hinterlegte Geschäfts-Mail-Adresse) und sieht in der Kundenakte einen Hinweis „Löschung beantragt". Der Kunde kann die Beantragung jederzeit selbst wieder zurückziehen.

**Team-Seite:** In jeder Kundenakte (bei „Kundenkonten"-Berechtigung) gibt es jetzt ebenfalls einen „Kunde löschen"-Bereich, der automatisch erkennt, ob Rechnungen vorhanden sind: ohne Rechnung ein echtes Löschen (inkl. Kaskade wie oben), mit Rechnung eine **Anonymisierung** — Name, Adresse, Geburtsdatum, E-Mail und Passwort werden überschrieben (Konto danach nicht mehr nutzbar), die Rechnungen selbst und die Auftragshistorie bleiben für die Buchhaltung unangetastet bestehen. Beide Wege landen im Aktivitätsprotokoll. In der Kundenliste zeigt ein roter Hinweis „Löschung beantragt" sofort, wo das Team noch tätig werden muss.

Mit vier echten End-to-End-Tests gegen die Live-Seite verifiziert: (1) Kunde ohne Rechnung löscht sich selbst → Konto sofort weg, (2) Kunde mit Rechnung beantragt Löschung → Hinweistext erscheint, Datenbank-Feld gesetzt, Team-Mail-Adresse ermittelt; (3) Team löscht einen rechnungslosen Kunden → Konto und zugehöriger Auftrag komplett entfernt; (4) Team anonymisiert einen Kunden mit Rechnung → alle Personendaten überschrieben, Rechnung und Auftrag blieben unverändert erhalten, Beantragungs-Hinweis verschwunden. Alle Testkonten/-daten danach entfernt. `tsc --noEmit` und vollständiger Build fehlerfrei.

## Neu: Team kann Kundenkonten vor Ort anlegen (09.08.2026) ✅
Angefragt für den zukünftigen Fall, dass ein Kunde direkt im (geplanten) Fotostudio vor Ort ist und selbst kein Konto anlegen kann/will — bisher ging das nur per Selbstregistrierung mit E-Mail-Bestätigung.

Unter „Kunden" gibt es jetzt einen „+ Neuer Kunde"-Knopf, der ein Formular mit denselben Pflichtfeldern wie die normale Registrierung aufklappt (Vor-/Nachname, Geburtsdatum, Anschrift, E-Mail, Passwort mind. 8 Zeichen). Bewusst zwei Unterschiede zur Selbstregistrierung: das Konto gilt sofort als verifiziert (kein E-Mail-Bestätigungslink nötig, da das Team die Person persönlich vor sich hat) und das Passwort wird direkt vom Team vergeben statt vom Kunden selbst gewählt.

**Berechtigung:** bewusst über die bereits bestehende „Kundenkonten"-Berechtigung (`client_accounts`) abgesichert statt einer neuen — die galt bisher schon für das Setzen von Kundenpasswörtern, ein neues Konto anzulegen ist derselbe sensible Eingriffstyp, kein reines Ansehen der Kundenakte. Die Beschreibung dieser Berechtigung in der Rechte-Tabelle wurde entsprechend ergänzt. Landet wie andere sensible Kontoänderungen im Aktivitätsprotokoll.

End-to-End mit echtem Mitarbeiter-Login gegen die Live-Seite getestet: Knopf erscheint, Formular legt ein neues, sofort verifiziertes Kundenkonto an, springt automatisch zur neuen Kundenakte, Aktivitätsprotokoll-Eintrag korrekt vorhanden. Testkonto danach wieder gelöscht. `tsc --noEmit` und vollständiger Build fehlerfrei.

## Bugfix: Rechte-Tabelle konnte beim Abhaken von „Vollzugriff" versehentlich alle Rechte löschen (09.08.2026) ✅
Direkt nach der letzten Runde gemeldet: in der neuen Rechte-Tabelle unter „Team & Rechte" speicherte jede Häkchen-Änderung sofort automatisch — auch das Abhaken von „Vollzugriff" selbst. Dabei wurde aber im selben Moment gespeichert, BEVOR die Detail-Tabelle mit den einzelnen Häkchen überhaupt sichtbar war, wodurch die Position mit „keinerlei Rechten" gespeichert worden wäre (leeres Array statt einer bewussten Auswahl).

**Geprüft:** deine eigene Position „Inhaber/in" war zum Glück nicht betroffen — das Feld war zu diesem Zeitpunkt noch nie geschrieben worden, zählt automatisch als Vollzugriff (Sicherheitsnetz gegen genau solche Fälle).

**Behoben:** die Tabelle speichert jetzt nicht mehr automatisch bei jeder Änderung, sondern erst nach einem bewussten Klick auf „Speichern" — vorher werden Änderungen nur lokal angezeigt („Ungespeicherte Änderungen"). Wird „Vollzugriff" abgehakt, sind alle einzelnen Häkchen zunächst wie bisher voll angehakt (entspricht dem vorherigen Zustand) — man schränkt dann gezielt ein, statt bei null anzufangen. Mit einem echten Testlauf bestätigt: Abhaken zeigt sofort alle 58 Häkchen aktiviert, nichts wird gespeichert, bis „Speichern" geklickt wird; danach wieder auf Vollzugriff zurückgestellt und gespeichert, Datenbank korrekt geprüft.

## Echte Personalakten + granulare Rechte (Ansehen/Erstellen/Bearbeiten/Löschen einzeln) (09.08.2026) ✅

**Personalakte mit echten Einträgen.** Bisher gab es nur ein einzelnes Feld „Beschäftigt seit" + eine Probezeit-Angabe. Jetzt eine echte Personalakte pro Mitarbeiter: beliebig viele Einträge über die Zeit, jeder mit Art (Mitarbeiter fest angestellt / Teamler frei / Praktikant:in / Azubi), Von-Datum, Bis-Datum (bei Praktikum/Ausbildung/Teamler sinnvoll, bei fester Anstellung leer = unbefristet), bei „Mitarbeiter" zusätzlich optional die Probezeit in Monaten, und einer freien Notiz. Ältere Einträge bleiben als Historie erhalten (z. B. erst Praktikum, später fest angestellt — beides sichtbar). Die bestehende Probezeit-Erinnerungsmail (täglicher Cron) läuft jetzt über diese Einträge statt über die alten Einzelfelder.

**Berechtigungen jetzt granular statt pauschal.** Bisher gab eine einzelne Berechtigung pro Bereich (z. B. „Aufträge") automatisch Zugriff auf ALLES in diesem Bereich — Ansehen, Anlegen, Bearbeiten UND Löschen zusammen, keine Trennung. Jetzt lässt sich für die meisten Bereiche (Portfolio, Blog, Kundenstimmen, FAQ, Conventions, Rabattcodes, Kalender, Dokumente, Team & Rechte, Ablehnungsgründe, Angebote, Pakete) einzeln festlegen, wer nur ansehen, wer zusätzlich erstellen, wer bearbeiten und wer löschen darf — vier separate Häkchen statt einem. Bei Bereichen ohne echte Lösch-/Erstell-Funktion in der Oberfläche (z. B. Aufträge selbst, Kundenakten) blieb es bei „Ansehen/Bearbeiten", bei reinen Einzel-Fähigkeiten (Statistiken, SEO, Einstellungen, Rechnungen, QM, Personalabteilung) blieb es bei einer einzelnen Berechtigung wie bisher — eine weitere Aufteilung hätte dort keinen echten Unterschied gemacht.

Die Rechte-Verwaltung unter „Team & Rechte" zeigt das jetzt als Tabelle (Bereich × Ansehen/Erstellen/Bearbeiten/Löschen) statt einer langen Liste einzelner Häkchen. „Vollzugriff" (alle Bereiche, alle Aktionen) bleibt als ein Klick weiterhin möglich — betrifft aktuell dich selbst, da du der einzige echte Mitarbeiter-Account bist, lohnt sich aber sobald du weitere Leute einstellst und ihnen z. B. nur Ansehen, aber kein Löschen geben willst.

**Technischer Umfang, zur Einordnung:** das betraf über 170 einzelne Berechtigungsprüfungen quer durch praktisch den gesamten Admin-Bereich — jede einzelne wurde auf die passende neue, genauere Berechtigung umgestellt. Mit einem eigens angelegten Testkonto mit stark eingeschränkten Rechten (nur „Portfolio ansehen", nichts weiter) real durchgetestet: Portfolio-Seite war zugänglich, Blog-Seite war korrekt gesperrt, ein Versuch, trotzdem einen neuen Portfolio-Eintrag anzulegen, wurde auf Server-Seite abgeblockt (kein Eintrag in der Datenbank gelandet, obwohl das Formular abgeschickt wurde). Danach die neue Rechte-Tabelle selbst getestet: ein Häkchen gesetzt, in der Datenbank korrekt als neue Berechtigung angekommen. Alle Testkonten/-daten danach entfernt. TypeScript-Vollprüfung (`tsc --noEmit`) und Build liefen beide fehlerfrei durch — starkes Signal, dass wirklich jede Stelle im Code mit umgestellt wurde, nicht nur die offensichtlichen.

## Erweiterungsideen: 20 von 21 umgesetzt (09.08.2026) ✅
Auf „darfst du alles umsetzen" hin die komplette Erweiterungsideen-Liste durchgearbeitet (16 aus der ursprünglichen Sammlung + 5 neu vorgeschlagene). Details je Bereich:

**Statistiken:** Ablehnungsgründe-Auswertung war schon vorhanden (Fund beim Durchgehen). Neu: Umsatz-Trend-Chart (Balken je Monat, Vormonatsvergleich in %) und eine Auslastungs-Heatmap nach Monat über alle Jahre (nach tatsächlichem Shooting-Termin, nicht Anfragedatum — zeigt Saisonspitzen).

**Aufträge:** Interne Team-Notiz pro Auftrag (nur intern, nicht im Kunden-Chat sichtbar). Mehrfachauswahl + Bulk-Archivieren in der Auftragsliste.

**Kalender:** Terminkonflikt-Warnung beim Setzen eines Auftragstermins — prüft Überschneidung mit anderen Aufträgen UND mit Kalender-Einträgen wie Urlaub, blockiert nicht, warnt nur. Öffentliche Frei/Belegt-Übersicht (2 Monate, ohne Kundennamen) auf `/angebote`. Bidirektionaler Google-Sync: Zeit-Änderungen, die direkt in Google an bereits verknüpften Terminen gemacht werden, fließen jetzt alle 2 Stunden zurück in die Seite (nur Zeiten, keine Titel, keine Löschungen — bewusst konservativ). Aktuell nicht mit einem echten Google-Kalender testbar, da die Verbindung gerade getrennt ist (siehe „Was noch offen ist").

**Kunden:** Tags/Segmente (frei vergebbar, in Kundenliste + -akte sichtbar, auch durchsuchbar). Gesamtumsatz in der Kundenakte war schon vorhanden.

**Pakete/Angebote:** Zusatzoptionen mit Preisaufschlag pro Preisstufe (z. B. „Express-Bearbeitung +50 €"). Zeitlich begrenzte Aktionspreise mit eigenem Label und Zeitraum, automatisch aktiv/inaktiv je nach Datum.

**Blog:** Tags/Kategorien mit Filterleiste, „ähnliche Beiträge" am Artikelende (gemeinsames Tag). RSS-Feed unter `/blog/rss.xml`.

**Gutscheine:** Bulk-Erstellung mehrerer Codes auf einmal (z. B. Convention-Verlosung), Codes zum Kopieren. Automatische Ablauf-Erinnerung an dich (nicht den Kunden), 30 Tage vor Verjährung, täglicher Cron-Lauf.

**Team:** Aktivitätsprotokoll für sensible Änderungen (Rechte, Mitarbeiterstatus, Kundenkonten — nicht jede einzelne Auftragsaktion, die läuft schon über die bestehende Auftrags-Historie). Digitale Unterschrift direkt am erstellten Dokument (Name + Zeitstempel + IP, wie beim bestehenden TFP-Vertrag — kein gezeichnetes Bild), erscheint auch im PDF.

**Neu (aus der zweiten Ideenrunde):** Probezeit-Erinnerung in der Personalakte (Monate eintragen, automatische Mail an dich 14 Tage vor Ende). Globale Admin-Suche (Aufträge, Kunden, Dokumente auf einmal, oben in jeder Admin-Seite). E-Mail-Textvorlage bewusst nur für die Wiedervorlage-Mail umgesetzt (eigenes Feld in den Einstellungen) statt für alle System-Mails — ein kompletter Umbau aller ~10 Mail-Versandfunktionen auf ein generisches Vorlagen-System wäre ein großer Umbau mit echtem Risiko für die Zustellung gewesen, das wollte ich nicht ungefragt durchziehen.

**Nicht umgesetzt:** Automatische Datenbank-Sicherung — deine Datenbank läuft auf MongoDB Atlas (Cloud), nicht lokal auf dem Server, und `mongodump` ist dort nicht installiert. Je nach Atlas-Tarif gibt es dort eventuell schon automatische Backups eingebaut (nur ab M10 aufwärts). Auf Rückfrage hin sollte ich erst prüfen statt blind etwas Neues zu bauen — ich habe aber keine Atlas-API-Zugangsdaten hier hinterlegt, kann dein Dashboard also nicht selbst einsehen. **Bitte einmal selbst in Atlas unter „Backup" beim Cluster nachschauen** — dann sage ich dir, ob/was noch zu bauen ist.

Alle 20 umgesetzten Punkte einzeln gegen die Live-Seite getestet (Playwright + direkte API-Aufrufe), Testdaten entfernt, Build+Deploy sauber, pm2-Fehlerlog leer. Zwei neue tägliche Cron-Jobs ergänzt (Gutschein-Ablauf, Probezeit-Erinnerung), der bidirektionale Google-Sync läuft im bereits bestehenden 2-Stunden-Cron mit.

## Urlaub: eigener Menüpunkt + Bug beim „Mit Google verbinden"-Knopf behoben (09.08.2026) ✅
Auf Wunsch einen direkten „Urlaub"-Punkt im Admin-Menü ergänzt (unter „Aufträge" → „Urlaub"), statt erst über den allgemeinen „Kalender"-Punkt suchen zu müssen — springt direkt zur Kalenderansicht, gefiltert auf nur den Urlaubskalender.

Dabei einen echten Bug gefunden: der „Urlaub"-Kalender, den ich beim letzten Mal per Skript angelegt hatte, war versehentlich auf dein Kunden-Testkonto statt dein echtes Mitarbeiterkonto (`contact@rakku.de`) als „Besitzer" eingetragen — dadurch fehlten für dich als Admin alle Verwalten-Optionen, inklusive dem „Mit Google verbinden"-Knopf. Der Knopf selbst war im Code nie das Problem (er erscheint automatisch bei jedem Kalender, auch neu angelegten, sobald man ihn wirklich verwalten darf) — behoben, indem der Besitzer korrigiert wurde. Mit deinem echten Mitarbeiter-Login nachgetestet: „Mit Google verbinden" erscheint jetzt korrekt beim Urlaubskalender.

**Wichtiger Nebenfund:** dabei aufgefallen, dass der „Aufträge"-Systemkalender aktuell NICHT mehr mit Google verbunden ist (kein Google-Token mehr hinterlegt) — war es nachweislich noch vor ein paar Tagen. Neue Auftragstermine werden also gerade nicht automatisch in dein echtes Google Kalender übertragen. Kann nur über den „Mit Google verbinden"-Knopf bei „Aufträge" in `/admin/calendar` von dir selbst wiederhergestellt werden (erfordert deinen Google-Login) — das kann ich nicht für dich erledigen.

## Nachbesserung: Admin-Menü ganz raus, Smooth-Scroll komplett entfernt, Urlaub jetzt öffentlich sichtbar (09.08.2026) ✅
Rückmeldung nach dem letzten Durchgang: das Admin-Menü sah auf der Auftragsseite immer noch komisch aus, der Scroll-Bug trat weiterhin auf mehreren Seiten auf, und der Urlaub sollte nicht nur intern, sondern auch für Kund:innen sichtbar sein.

- **Admin-Menü auf der Auftragsseite komplett entfernt**, wie gewünscht — dort reicht der „← Alle Aufträge"-Link zur Navigation, das lange Menü hat auf dieser fokussierten Einzelansicht nur unnötig Platz gekostet.
- **Smooth-Scrolling (Lenis) komplett entfernt**, wie beim ersten Mal als Rückfalloption angeboten. Grund: im Code fand sich ein alter, expliziter Kommentar, der genau das gemeldete Symptom vorab beschrieb — natives und Lenis-Scrollen konkurrierten um die Scroll-Position, was sich als kurzes „Hängenbleiben"/Nicht-mehr-Scrollen-Können äußert, vor allem an den Seitenrändern. Mein erster Fix (Scrollleiste reagiert auf Höhenänderungen) hat nur EIN Symptom behoben, nicht die eigentliche Ursache. Jetzt läuft die ganze Seite auf normalem, nativem Browser-Scrollen — kein Lenis, keine eigene Scrollleiste mehr, „Nach oben"-Button und Sanft-Scroll-Pfeile nutzen jetzt die native `scroll-behavior: smooth`-Animation.
- **Urlaub jetzt auch für Kund:innen sichtbar:** unter „Kalender verwalten" gibt es pro Kalender jetzt ein Häkchen „Auf der Angebote-Seite öffentlich sichtbar" — für den „Urlaub"-Kalender bereits aktiviert. Anstehende/laufende Termine erscheinen dann automatisch als Hinweis oben auf `/angebote` (z. B. „Vom 1.10. bis 5.10. sind wir im Urlaub"), DE + EN, ohne dass das manuell im Hinweis-Banner gepflegt werden müsste.

Alle drei Punkte mit echten Tests gegen die Live-Seite bestätigt (Auftragsseite ohne Admin-Menü, kein Lenis/Scrollleiste mehr im DOM, öffentlicher Urlaubshinweis mit Test-Termin auf Deutsch und Englisch sichtbar), Testdaten danach entfernt.

## Fünf Punkte auf einmal: Admin-Menü-Bug, Conventions-Link, Portfolio-Footer/Scroll, Urlaubsplanung, Pflichtfelder bei Registrierung (09.08.2026) ✅

**Admin-Menü sah „doppelt"/kaputt aus, sobald ein Auftrag offen war.** Ursache gefunden: das breite Admin-Menü (Aufträge, Kalender, Kunden, …) wurde auf jeder Admin-Seite direkt an die Bildschirmoberkante gerendert — dort liegt aber exakt der schwebende, halbtransparente Haupt-Header (Logo + Portfolio/Angebote/Konto/Admin), der auf JEDER Seite immer oben schwebt. Beide lagen genau übereinander, das Admin-Menü schimmerte durch den halbtransparenten Header durch. Betraf tatsächlich JEDE Admin-Seite, nicht nur die Auftragsseite — dort ist es nur aufgefallen, weil das Menü dort auf zwei Zeilen umbricht und dadurch mehr sichtbar wurde. An einer zentralen Stelle behoben (das Admin-Menü bekommt jetzt selbst genug Abstand von oben), wirkt sich automatisch auf alle ~20 Admin-Seiten aus. Mit einem echten Login am Auftrags-Screenshot bestätigt: beide Leisten jetzt sauber getrennt.

**Conventions- und Blog-Link fehlten in der öffentlichen Navigation.** Beide Seiten (`/conventions`, `/blog`) existierten und funktionierten schon länger — es gab nur keinen Menüpunkt dorthin, weshalb sie praktisch nicht auffindbar waren (auch die Übersetzungstexte dafür lagen schon bereit, wurden nur nie eingebaut). Jetzt in der Hauptnavigation ergänzt, DE + EN.

**Portfolio: Footer „fehlte" und Scrollen ruckelte gelegentlich.** Ursache: die eigene, individuell gestaltete Scrollleiste (ersetzt die Standard-Browser-Scrollleiste) hat ihre Höhen-Berechnung nur bei einem echten Scroll- oder Fenster-Größe-Ereignis aktualisiert. Wächst die Seite aber durch „Mehr laden" oder einen Kategorie-Filterwechsel im Portfolio, ohne dass eines dieser beiden Ereignisse auftritt, blieb die Scrollleiste auf dem alten (zu kurzen) Stand hängen — man konnte dann nicht mehr bis zum Footer runterziehen, obwohl er da war. Behoben: die Scrollleiste beobachtet jetzt direkt Größenänderungen des Seiteninhalts selbst, unabhängig von Scroll-/Resize-Ereignissen. Mit einem gezielten Test bestätigt (Seite künstlich verlängert, ohne zu scrollen — Leiste hat sich sofort korrekt angepasst).

**Neu: Urlaubsplanung fürs Team.** Das bestehende Zusatz-Kalender-System (schon länger für genau diesen Zweck gedacht, siehe Berechtigungstext „z. B. Urlaub, Team-Termine") konnte technisch schon Start-/Enddatum, aber im Monatsraster wurde ein mehrtägiger Termin bisher nur an seinem Starttag angezeigt — für eine Woche Urlaub praktisch unbrauchbar, sah aus wie ein Termin an einem einzigen Tag. Jetzt zieht sich ein solcher Termin korrekt über alle betroffenen Tage im Kalenderraster. Dafür extra einen Kalender „Urlaub" angelegt (Team-sichtbar, unter `/admin/calendar` in der Seitenleiste) — einfach „+ Neuer Termin" mit Von-/Bis-Datum und „Ganztägig" nutzen, plannbar beliebig weit im Voraus (getestet mit einem 5-tägigen Testeintrag über den September hinweg, danach wieder entfernt). Kein separates neues System nötig, das vorhandene Kalender-System deckt es jetzt vollständig ab.

**Registrierung: Geburtsdatum und Anschrift jetzt Pflicht.** Bisher optional (nur nachträglich im Kontobereich editierbar) — jetzt bei der Registrierung selbst erforderlich, das Konto lässt sich ohne diese Angaben gar nicht erst anlegen. Gilt nur für Kundenkonten, nicht für Mitarbeiterkonten (die legt ohnehin der Admin selbst an, nie über die Registrierung). Geburtsdatum ist im Kontobereich (`/account/profile`) jetzt auch nachträglich einsehbar/korrigierbar, analog zur Anschrift. Mit einer echten Testregistrierung bestätigt: Absenden ohne diese Felder wird vom Formular blockiert, mit Werten läuft die Registrierung durch und beide Felder landen korrekt in der Datenbank.

Alle fünf Punkte einzeln mit echten Tests gegen die Live-Seite verifiziert (Playwright, Wegwerf-Konten), alle Testdaten danach entfernt — bis auf den neu angelegten „Urlaub"-Kalender selbst, der bleibt als echte Funktion bestehen.

## Termine: Enddatum/-zeit für Shootings (09.08.2026) ✅
Auf Wunsch: bei der Terminvergabe für ein Shooting lässt sich jetzt zusätzlich zum Start auch ein Ende festlegen, da ein Shooting mal 2 und mal 4 Stunden dauern kann. Bisher gab es nur einen Zeitpunkt, keine Dauer.

- Neues Feld „Bis" (optional) direkt neben „Von" im Termin-Bereich einer Anfrage/eines Auftrags. Bleibt es leer, wird wie bisher automatisch 1 Stunde als Standarddauer angenommen (unverändert für alle bestehenden Termine).
- „Bis" muss zeitlich nach „Von" liegen — bei einem ungültigen Wert erscheint ein Warnhinweis und Speichern ist blockiert.
- Wirkt sich überall aus, wo Termine angezeigt oder synchronisiert werden: interner Kalender (`/admin/calendar`, zeigt jetzt „14:00–18:00" statt nur „14:00"), Google-Kalender-Sync (Push + 2-stündlicher Hintergrundabgleich), und der abonnierbare ICS-Kalenderfeed (Apple/Google/Outlook-Kalenderabo).
- **Mit einem echten End-to-End-Test über die echte Weboberfläche verifiziert** (Wegwerf-Mitarbeiterkonto, Wegwerf-Auftrag, Termin 15.09.2026 14:00–18:00 gesetzt): Datenbank korrekt (`shootDate`/`shootEndDate`), ICS-Feed zeigt korrekt `DTEND` 4 Stunden nach `DTSTART`, Kalenderansicht zeigt korrekt „14:00–18:00". Da dein Auftrags-Kalender mit deinem echten, primären Google-Kalender verbunden ist, wurde dabei kurzzeitig ein echter Testtermin dort angelegt — direkt danach über den bestehenden „Termin entfernen"-Button in der echten Oberfläche wieder entfernt (nicht nur in der Datenbank gelöscht, sondern über den regulären Lösch-Weg der App, der den Google-Termin sauber mit entfernt). Anschließend verifiziert, dass Datenbank-Felder wirklich leer sind. Alle Testkonten/-daten danach vollständig entfernt.

## Tiefergehender Bug-Sweep: Kalender, Rechnungen, Rabattcodes, Stripe, EÜR (09.08.2026) ✅
Zweiter, tieferer Durchgang über Bereiche, die beim ersten Rechtschreib-/Logik-Check noch nicht dran waren: Kalender-System, QM-Ablauf, Statistiken/EÜR, Rechnungen/Mahnungen, Rabattcode-Einlösung, Stripe-Integration. QM-Ablauf und Statistiken waren sauber, keine Änderung nötig. Gefunden und behoben:

- **Doppelte Rechnungsnummern möglich (latent, nicht akut):** Der Zähler für die nächste Rechnungsnummer nutzte `findOneAndUpdate` mit `upsert: true` und `returnDocument: "before"` — bei einer Neuanlage des Einstellungs-Dokuments (z. B. ganz frisches System, bevor je jemand `/admin/settings` geöffnet hat) liefert „before" bei einem Insert `null`, wodurch zwei nacheinander erstellte Rechnungen dieselbe Nummer „RG-0001" bekommen und die zweite mit einem Datenbankfehler abbricht (eindeutiger Index). In der echten Datenbank bereits unkritisch, weil das Einstellungs-Dokument längst existiert — trotzdem sauber behoben (Dokument wird jetzt zuerst sichergestellt, bevor der Zähler erhöht wird), damit das bei einer Neuinstallation nie auftreten kann. Mit einer echten Rechnungserstellung nachgetestet — korrekt „RG-0006" nach dem bestehenden „RG-0005", Zähler korrekt auf 7 weitergesprungen.
- **Rabattcode-/Gutschein-Einlösung nicht rennsicher:** bei einem auf 1 Nutzung begrenzten Rabattcode oder einem Geschenkgutschein konnten zwei gleichzeitige Einlöse-Versuche (z. B. zwei Browser-Tabs, oder Zufall bei zwei Kund:innen mit demselben Code) beide durchkommen, weil Prüfung und Erhöhen zwei getrennte Schritte waren. Auf atomare Datenbank-Operationen umgestellt. **Mit echten gleichzeitigen Anfragen nachgestellt** (zwei Kundenkonten, ein Code mit `maxUses: 1`, beide klicken im selben Moment „Einlösen") — korrekt: genau eine Einlösung ging durch, die andere bekam die reguläre Fehlermeldung, `usedCount` blieb korrekt bei 1 statt 2. Dasselbe für Gutscheine wiederholt, ebenfalls korrekt.
- **Stripe-Zahlung leitete IMMER auf die deutsche Seite zurück**, unabhängig von der Sprache — betraf `createCheckoutSessionAction` (fehlender `locale`-Parameter), analog zum bereits behobenen Gutschein-Problem. Ein englischsprachiger Kunde, der online bezahlt, landete nach Stripe auf der deutschen statt der englischen Auftragsseite. Behoben nach demselben, bereits bewährten Muster.
- **Google-Kalender: ganztägige Termine ohne Ende bekamen ein Enddatum gleich dem Startdatum** — bei Google ist das Ende eines ganztägigen Termins exklusiv (muss der Tag danach sein), ein gleiches Start-/Enddatum ergibt einen Termin mit Länge null, den Google ablehnt oder falsch anzeigt. Jetzt korrekt Start + 1 Tag als Standard, wenn kein Ende angegeben ist. (Nicht gegen einen echten Google-Kalender getestet — das wäre dein echter, verbundener Kalender gewesen, bewusst nicht angefasst; die Korrektur folgt exakt der Google-API-Spezifikation für ganztägige Termine.)
- **EÜR-Export konnte Einnahmen unbemerkt zu niedrig ausweisen:** ein als „bezahlt" markierter Auftrag OHNE zugehörige Rechnung (z. B. bar bezahlt, Rechnung vergessen) wurde bisher stillschweigend mit 0 € aus der Einnahmen-Liste rausgefiltert — bei einer Steuer-relevanten Datei ein ernstes Problem, weil es niemand bemerkt hätte. Zeigt jetzt stattdessen eine auffällige Warnzeile mit „⚠ KEINE RECHNUNG, BETRAG PRÜFEN" plus eine Zusammenfassung ganz oben im Export, welche Aufträge betroffen sind.
- Kleinigkeit: zwei Dateien (Rechnungs-/Mahn-PDF) hatten die Euro-Formatierung lokal dupliziert statt die gemeinsame Funktion zu nutzen — zusammengeführt, keine Verhaltensänderung.

**Versehentlicher Fehler bei mir während des Testens, zur Transparenz:** beim Aufsetzen eines Testfalls für die Rechnungsnummer habe ich kurzzeitig `PriceProposal.deleteMany({})` ohne Einschränkung ausgeführt (sollte nur meinen eigenen Testeintrag löschen). Betroffen waren dadurch die Preisverhandlungs-Verläufe deiner eigenen drei „Test"/„TEst"-Aufträge (deine eigenen Klick-Tests von früher, kein echter Kunde, keine echte Rechnung betroffen — die echte Rechnung „RG-0005" blieb unangetastet). Für die Zukunft handhabe ich Lösch-Operationen in Testskripten strenger (immer mit engem Filter). Nur der guten Ordnung halber erwähnt — kein Kunden- oder Geschäftsschaden entstanden.

Alle Fixes mit echten End-to-End-Tests verifiziert (Rechnungserstellung, zwei parallele Rabattcode-Einlösungen, zwei parallele Gutschein-Einlösungen), Testdaten danach vollständig entfernt (Konten, Aufträge, Preisverhandlungen, Rabattcode, Gutschein, Testrechnung). `nextInvoiceNumber` bewusst nicht zurückgesetzt (bleibt bei 7 statt wieder auf 6, damit die Testrechnungsnummer „RG-0006" nicht doppelt vergeben werden kann — Rechnungsnummern werden grundsätzlich nie wiederverwendet, das ist so beabsichtigt).

## Gestrige Fixes dreifach gegengetestet (09.08.2026) ✅
Auf Wunsch die wichtigsten Fixes vom Vortag nochmal unabhängig nachgetestet, nicht nur einmal: Mitarbeiter-Login-Weiterleitung (diesmal zusätzlich mit einem englischsprachigen Konto über `/en/login` — landet korrekt bei `/en/admin/orders`), Sitemap-Einträge, FAQ-Kategorien EN, Gutschein-Formular-Sprache — jeweils mehrfach bestätigt. Dabei außerdem den Preisstufen-Checkbox-Fix (`priceFrom`/„ab"-Häkchen) draufgetestet, der beim ersten Durchgang nur im Code korrigiert, aber nie tatsächlich durchgeklickt wurde — jetzt mit drei echten Durchläufen (Anlegen mit deaktiviertem Häkchen, Bearbeiten mit Aktivieren, Bearbeiten mit Deaktivieren) am echten Formular bestätigt, alle drei korrekt in der Datenbank gelandet. Alle Testdaten entfernt.

## Dokumenten-Creator: Platzhalter-System erklärt + erweitert (09.08.2026) ✅
Nutzer-Feedback: „ich habe keine Ahnung, wo ich die Platzhalter finde … da sind Platzhalter dabei, die nicht gehen." Der Kern des „Problems": es gab nirgends eine sichtbare Erklärung, welche `{{platzhalter}}` automatisch befüllt werden und welche bewusst zum manuellen Ausfüllen stehen bleiben (so funktioniert das System seit Einführung — unbekannte Platzhalter bleiben absichtlich im Text stehen, damit sie auffallen, statt lautlos leer zu werden). Ohne Erklärung sieht das aber wie ein Fehler aus, vor allem wenn ein generiertes PDF dann `{{gehalt}}` wörtlich enthält, weil der Platzhalter vor dem Erstellen nicht ersetzt wurde.

**Neu:**
- Feste Info-Box oben auf `/admin/documents`, die das ganze System einmal erklärt.
- Sobald eine Vorlage ausgewählt ist, zeigt ein Kasten direkt über dem Dokumenttext **live**, welche Platzhalter in genau dieser Vorlage automatisch ausgefüllt wurden (grün) und welche noch von Hand ergänzt werden müssen (gelb) — mit Klartext-Erklärung zu jedem (z. B. „`{{verguetungJ1}}` — Ausbildungsvergütung im 1. Jahr"), nicht nur dem rohen Variablennamen.
- Zwei neue automatische Platzhalter: `{{ausstellungsdatum}}` (immer das heutige Datum) und `{{geburtsdatum}}` (aus der Personalakte) — dafür ein neues Geburtsdatum-Feld in der Personalakte (`/admin/team/[Mitarbeiter]`, gleiches Muster wie „Beschäftigt seit") ergänzt. Betrifft 4 der 10 Vorlagen (Arbeitszeugnisse, Ausbildungszeugnis), die den Platzhalter schon länger nutzen.

**Wichtig:** die übrigen Platzhalter (Gehalt, Kündigungsfrist, Urlaubstage, Tätigkeitsbeschreibung usw.) bleiben bewusst manuell — die brauchen ohnehin für jedes Dokument einen individuellen Wert, den nur du kennst, ein „automatisch" wäre hier falsch.

Mit drei unabhängigen Testläufen über verschiedene Vorlagen und Datenlagen verifiziert (u. a. Mitarbeiter mit vollständigem vs. unvollständigem Profil, um zu bestätigen, dass wirklich nur vorhandene Daten automatisch einspringen und alles andere korrekt manuell bleibt). Testdaten danach entfernt.

## Vollständiger Bug-/Rechtschreib-Check der ganzen Seite (08.08.2026) ✅
Auf Nutzeranfrage: einmal die komplette Seite durchgecheckt (öffentlich + Logik in den Server-Actions), alle gefundenen Fehler behoben. Vorgehen: eigener Review (Build-Check, Screenshots aller öffentlichen Seiten DE+EN, gezielte Nachprüfung bekannter Fehlerklassen) plus ein spezialisierter Hintergrund-Check über den gesamten Code (Rechtschreibung, i18n-Lücken, Logikfehler, tote Links, Formatierung).

**Gefunden und behoben:**
- **Kaputte Weiterleitung nach Mitarbeiter-Login:** Nach dem Einloggen (ganz normal über `/login`, ohne vorherigen „Weiter zu"-Link) landete Personal bislang auf `/admin/orders` — ohne Sprachpräfix `/de/` gibt es diese Route nicht, es gab keine passende Seite dafür. Betraf sowohl den normalen Login als auch den Login mit Zwei-Faktor-Code. Am echten Login-Formular nachgestellt und den Fix bestätigt: führt jetzt korrekt zu `/de/admin/orders`. **Das war vermutlich schon länger so** — meine bisherigen Tests haben immer direkt eine Sitzung eingeschleust statt über das echte Formular zu gehen, deshalb ist mir das nie aufgefallen.
- **„Sonstig" statt „Sonstiges"** bei den Zahlungsarten (Auftragsseite, Statistiken, Aktivitätsprotokoll) — korrigiert.
- **Sitemap unvollständig:** Gutscheine, Conventions und Blog fehlten in der sitemap.xml (waren beim jeweiligen Bauen schlicht vergessen worden) — Suchmaschinen hätten diese drei Bereiche nie regulär gefunden. Ergänzt.
- **Vertrags-PDF mit falscher Zahlenformatierung:** „150.00 €" (Punkt) statt „150,00 €" (Komma) beim vereinbarten Preis im generierten TFP-Vertrag — jetzt über dieselbe Formatierungsfunktion wie Rechnungen/Mahnungen.
- **Empfehlungsprämien-Anzeige** im Kundenkonto hatte dieselbe Punkt-statt-Komma-Formatierung bei festen Beträgen — behoben.
- **Blog-Startseite:** Rubrik-Text war auf Deutsch UND Englisch identisch „Behind the Scenes" (keine echte Übersetzung, nur zufällig gleicher Text) — Deutsch jetzt „Hinter den Kulissen".
- **FAQ-Kategorien komplett unübersetzt:** die Fragen selbst waren auf der englischen Seite korrekt übersetzt, aber die Kategorie-Überschriften darüber („Preise & Bezahlung" usw.) blieben immer Deutsch. Neues Feld `categoryEn` ergänzt (gleiches Muster wie bei Frage/Antwort), Admin-Formular um ein optionales Englisch-Feld erweitert, bestehende 5 Kategorien direkt übersetzt.
- **Gutschein-Anfrageformular komplett unlokalisiert:** auf der englischen Gutscheine-Seite war das eigentliche Formular (Betrag, Name, E-Mail usw.) trotzdem komplett auf Deutsch — das Formular bekam nie übersetzte Labels übergeben. Läuft jetzt über dasselbe Label-Props-Muster wie das Warteliste-/Feedback-Formular.
- **Gutschein-Bestätigungsmails immer auf Deutsch**, unabhängig von der Sprache der Anfrageseite — betraf sowohl die sofortige Anfrage-Bestätigung als auch die Code-Mail, die oft erst Tage später (nach manueller oder Stripe-Zahlungsbestätigung) rausgeht. Neues `locale`-Feld direkt am Gutschein-Datensatz (da der spätere Versand außerhalb des ursprünglichen Anfrage-Kontexts passiert), berücksichtigt jetzt auch die Stripe-Erfolgs-/Abbruch-URLs.
- **Angebote-Seite:** derselbe „Checkbox wird beim Erstellen/Bearbeiten einer Preisstufe immer als angehakt gespeichert"-Fehler wie bei den bereits in der letzten Runde behobenen Stellen (FAQ, Testimonials, Portfolio) — betraf hier das „Ab"-Häkchen bei Preisstufen. Behoben.
- **Toter Button im Feedback-Bereich:** „Als Kundenstimme übernehmen" erschien auch für Mitarbeiter:innen ohne die `testimonials`-Berechtigung, tat dann aber nichts. Button erscheint jetzt nur noch, wenn die Berechtigung wirklich da ist.
- Kleine Schreibweisen-Inkonsistenz bei „Nennen/Verlinken" im TFP-Vertragsformular vs. generiertem PDF vereinheitlicht.

**Bewusst nicht verändert** (geprüft, aber kein echter Fehler): „Nickname" im Registrierungsformular ist zwar ein englisches Wort, aber im Cosplay-/Gaming-Umfeld ganz normales Alltagsdeutsch — belassen. Preisangaben auf der englischen Seite verwenden weiterhin deutsches Zahlenformat (Komma statt Punkt) — bewusste/vertretbare Entscheidung für ein deutsches Unternehmen, keine Änderung vorgenommen ohne Rückfrage.

**Aufgefallen, aber keine Code-Sache:** die Test-Convention „Test" (Ort „TEszt") ist ein eigener Test-Eintrag von dir und aktuell noch live auf `/conventions` sichtbar — kann in `/admin/conventions` bei Bedarf gelöscht oder überschrieben werden.

Alle Fixes einzeln verifiziert (u. a. echter Login-Formular-Test mit Wegwerf-Mitarbeiterkonto, Gutschein-Anfrage auf der EN-Seite mit DB-Kontrolle des gespeicherten `locale`-Felds), danach alle Testdaten entfernt. Build+Deploy sauber, pm2-Fehlerlog leer.

## Angebote + FAQ: fotografischer Seiten-Header (07.08.2026) ✅
Feedback: beide Seiten wirkten „leblos, blank schwarz" — zu Recht, es waren die einzigen zwei öffentlichen Seiten komplett ohne Foto, obwohl der Rest der Website stark auf Fotografie setzt (Hero, Portfolio, Testimonials-Avatare).

Neue, wiederverwendbare Komponente `PageHeaderBanner` — nutzt dieselbe Korn-/Verdunkelungs-Bildsprache wie der große Hero auf der Startseite (`rk-grain`, Gradient-Vignette), nur kompakter (46vh statt Vollbild) und ohne die Ecken-Rahmen (die sind ans Vollbild gebunden, wirken in dieser Höhe abgeschnitten). Zeigt ein zufälliges, sichtbares Portfolio-Bild (`$sample`-Aggregation) — bei jedem Seitenaufruf potenziell ein anderes, damit sich die Seite auch bei wiederholten Besuchen frisch anfühlt. Fällt auf `/img/hero.jpg` zurück, falls noch kein sichtbares Portfolio-Bild mit Bildern existiert.

Für den Vollbild-Effekt musste `px-6` von den beiden `<main>`-Elementen auf die jeweiligen Inhalts-Wrapper verschoben werden (vorher saß die Seiten-Polsterung direkt am `<main>`, wodurch das Banner links/rechts einen Rand gehabt hätte statt randlos zu sein — gleiches Strukturmuster wie beim Hero auf der Startseite, wo jede Sektion ihr eigenes `px-6` trägt statt eines gemeinsamen auf `<main>`).

FAQ zusätzlich mit einem kleinen farbigen Punkt vor jeder Kategorie-Überschrift (`lib/colorHash.ts` wiederverwendet, bislang nur im Admin-Bereich genutzt) — etwas mehr Leben auch im restlichen Seiteninhalt, nicht nur im Header.

**Beim Testen einen Nicht-Bug aufgeklärt:** ein automatisierter Screenshot ohne echtes Scrollen zeigte auf der Angebote-Seite scheinbar eine riesige leere Lücke vor dem Footer. Ursache war nicht die neue Änderung, sondern das bestehende Scroll-Reveal-Verhalten (Einträge starten unsichtbar, bis sie per IntersectionObserver beim Scrollen ins Bild kommen) — die Seite hat inzwischen ganz reell 4 Angebote (Portrait/Cosplay/Convention/Event-Fotografie) statt der ursprünglich angenommenen 2, die unteren zwei waren im Screenshot nur noch nicht „enthüllt". Mit echtem Scrollen sauber bestätigt: alle 4 Angebote korrekt und einmalig sichtbar, keine Lücke, keine Dopplung.

## Startseiten-Reihenfolge korrigiert + Fließband-Duplikat-Problem behoben (07.08.2026) ✅
Zwei Nachbesserungen auf direktes Feedback:

**Reihenfolge:** „Was Kunden sagen" (großes Zitat-Fließband) läuft jetzt unter „Ein Blick in mein Portfolio" statt davor. Reihenfolge jetzt: Hero → Kurzbewertungen-Band → Portfolio-Ausschnitt → Zitat-Testimonials → Rest.

**„Alles wird doppelt angezeigt":** war kein Bug im eigentlichen Sinne, sondern eine Nebenwirkung des Fließband-Tricks — für den nahtlosen Endlos-Loop wird die Eintragsliste im Markup verdoppelt (`[...items, ...items]`), was bei genug Einträgen wie ein durchlaufender Strom wirkt, bei nur 1–2 Einträgen aber wie derselbe Eintrag zweimal hintereinander aussieht. Fix: neue Schwelle (`MIN_ITEMS_FOR_MARQUEE = 5`) für beide Fließbänder (Kurzbewertungen + Zitat-Testimonials) — darunter erscheinen die Einträge einmalig als statische, zentrierte Reihe ohne Animation, erst ab 5 Einträgen lohnt sich der Lauf-Effekt.

Verifiziert mit 1 Testeintrag (erscheint korrekt nur einmal im echten HTML — der zweite Treffer beim ersten Check war nur der unsichtbare React-Hydration-Payload, kein echtes Duplikat, per gezieltem `class=` vs. `\"className\":` Vergleich sauber unterschieden) und mit 5 Testeinträgen (Fließband-Modus greift korrekt, Liste wird wie vorgesehen verdoppelt). Testdaten danach entfernt.

## Kurzbewertungen-Fließband + Startseiten-Umbau, Empfehlungscode-Missbrauchsschutz (07.08.2026) ✅
Nutzer-Feedback nach dem Testen der Dritten Runde: Conventions/Pakete/Bewertungen erschienen ihm als „fehlend" — waren tatsächlich nur leer (0 Einträge, Funktion selbst lief korrekt, per Live-Check bestätigt). Das eigentliche Bewertungs-Layout entsprach aber ohnehin nicht dem Wunsch (Vorbild: ein Avada-Theme-Testimonial-Slider-Screenshot).

**Neues, drittes Content-Typ „Kurzbewertungen"** (`QuickReview`-Model, `testimonials`-Berechtigung wiederverwendet, dritter Reiter in `/admin/testimonials` neben Kundenstimmen/Google-Bewertungen — bewusst eigene Liste, nicht dieselben Einträge wie die großen Zitat-Testimonials, laut Nutzerentscheidung). Nur Name + 1–5-Sterne, kein Zitat. Admin trägt den vollen Namen ein (z. B. „Till Kuhlmann"), die Seite kürzt automatisch zu „Till K." (`lib/formatName.ts`) — Formularvorschau zeigt die Kürzung schon beim Tippen.

**Startseite umgebaut:** beide Fließbänder liegen jetzt direkt unter dem Hero-Banner (vorher: großes Testimonial-Band ganz unten vor dem Footer). Reihenfolge: kompaktes Kurzbewertungen-Band (Name + Sterne, schnellerer Lauf) direkt über dem großen Zitat-Testimonial-Band (unverändertes Layout, jetzt nur anders platziert).

**Wichtiger Datenschutz-Fund während der Tests:** der volle Name landete zunächst trotzdem im HTML — nicht sichtbar im Text, aber im React-Hydration-Payload (React-„key"-Prop nutzte den vollen Namen). Behoben, indem die Kürzung zu „Till K." schon serverseitig beim Datenabruf passiert, bevor der Name überhaupt an die Komponente/den Key übergeben wird — der volle Name verlässt den Server jetzt gar nicht erst.

**Empfehlungscode-Missbrauchsschutz:** auf Hinweis des Nutzers — bislang bekam jedes Konto sofort beim ersten Aufruf von `/account` einen Empfehlungscode, unabhängig davon, ob je ein echter Auftrag angelegt wurde. Das hätte Missbrauch ermöglicht (Konto anlegen, sofort Code ziehen, über ein zweites Konto gegen sich selbst einlösen). `getOrCreateReferralCode()` prüft jetzt zuerst `Order.countDocuments({clientId}) > 0` — ohne mindestens einen eigenen Auftrag bleibt der Code-Bereich im Konto schlicht unsichtbar (kein Fehler, keine leere Box). Die Rabatthöhen für Empfehlungscode-Einlösung (Neukunden-Rabatt und Werber-Prämie) sind bereits seit der zweiten Runde unter „Empfehlungsprogramm" in `/admin/settings` einstellbar — Nutzer darauf hingewiesen, da ihm das offenbar nicht bekannt war.

End-to-End mit Wegwerf-Konten verifiziert: Kurzbewertung „Till Kuhlmann" mit 5 Sternen angelegt → erscheint korrekt als „Till K." ganz oben auf der Startseite, vor dem großen Testimonial-Band → Text bearbeitet, Sichtbarkeit blieb erhalten → nach dem Fix kein voller Name mehr irgendwo im HTML nachweisbar (vorher/nachher per `curl` verglichen). Empfehlungscode-Test: frisches Konto ohne Auftrag zeigt korrekt keinen Code → nach Anlage eines Auftrags erscheint der Code korrekt. Alle Testdaten danach entfernt.

## Dritte Runde geplant (07.08.2026) — Umsetzung läuft
Nutzer bat um weitere Ideen, hat aus 8 vorgeschlagenen Punkten „setze gerne alles um" gesagt — dazu ausdrücklich: richtige Einbindung ins Admin-Dashboard, gutes Aussehen (nicht basic), Berechtigungen mitdenken, und dieselbe optische Politur auch auf die bestehende Admin-FAQ- und Admin-Aufträge-Seite anwenden.

### Festgelegte Reihenfolge
0. **Design-Politur Admin-FAQ + Admin-Aufträge** — zuerst, damit der optische Maßstab für alle neuen Panels feststeht, bevor sie gebaut werden
1. **Gutscheine** — Kunden kaufen einen Geschenkgutschein (fester Betrag), einlösbar wie ein Rabattcode. Neue eigene Berechtigung `vouchers` (nicht `discounts` wiederverwendet — hier fließt echtes Geld vorab, das ist ein eigener Geschäftsvorgang, kein reiner Marketing-Rabatt, verdient dieselbe bewusste Trennung wie seinerzeit `client_accounts` von `clients`)
2. **Paket-/Kombi-Angebote** — admin-definierte Kombination bestehender Angebote/Preisstufen zu einem Gesamtpreis, auf der Angebote-Seite. Erweiterung des bestehenden Service/PriceTier-Systems, reine `orders`-Berechtigung (dieselbe wie für Angebote-Verwaltung), keine neue nötig
3. **Convention-Übersicht** (öffentlich) — Seite „Wo trefft ihr Rakku?" mit anstehenden Conventions + Slot-Anfrage direkt dafür. Neue eigene Berechtigung `conventions` (eigener Content-Typ, admin-kuratiert, analog zu `testimonials`)
4. **Warteliste** — für ausgebuchte Zeiträume/Conventions, Kunde trägt sich ein, wird bei Freiwerden benachrichtigt. Baut auf Punkt 3 auf, nutzt `orders`-Berechtigung
5. **Zufriedenheits-Nachfrage** — eine kurze Frage nach Auftragsabschluss, liefert nebenbei Rohmaterial für neue Testimonials. `orders`-Berechtigung zum Einsehen der Antworten
6. **Wiedervorlage-Mail** — automatische „ein Jahr seit deinem letzten Shooting"-Mail, per Cron (analog zum bestehenden Google-Calendar-Sync-Cronjob). Konfiguration über bestehende `settings`-Berechtigung
7. **Externe Google-Bewertungen** — als zweiter Reiter im bestehenden Testimonials-Bereich (`testimonials`-Berechtigung wiederverwendet, kein neuer Content-Typ). **Wichtiger Vorbehalt:** eine echte Live-Anbindung an die Google-Places-API bräuchte einen Google-Cloud-API-Key von Rakku (wie seinerzeit bei Stripe/Google Calendar) — falls der nicht vorliegt, wird das zunächst als admin-eingepflegte externe Zitate umgesetzt (Rakku kopiert die Bewertung von Google rüber), mit der Möglichkeit, später auf eine echte Live-Anbindung umzusteigen
8. **Blog/Behind-the-Scenes** — kurze Beiträge pro Shooting fürs SEO. Neue eigene Berechtigung `blog` (wieder eigener Content-Typ, analog zu `testimonials`/`faq`)

Details, Umsetzung und Tests zu jedem Punkt folgen unten, jeweils mit eigenem Eintrag sobald fertig.

## Blog/Behind-the-Scenes (07.08.2026) ✅ — Punkt 8, Dritte Runde komplett abgeschlossen
Neues, eigenständiges `BlogPost`-Model mit eigener Berechtigung `blog` (analog zu FAQ/Testimonials/Conventions). Kurze Beiträge — Titel, Kurzbeschreibung, Fließtext mit Absätzen (Leerzeile trennt), optionales Titelbild, Veröffentlichungsdatum, DE/EN — bewusst kein Rich-Text-Editor, da laut Planung nur „kurze Beiträge" fürs SEO gefragt waren, keine vollwertige CMS-Funktionalität.

**Slugs:** automatisch aus dem Titel erzeugt (Umlaute transliteriert: ä→ae, ö→oe, ü→ue, ß→ss, statt sie einfach zu entfernen — bessere Lesbarkeit in der URL), bei Namensgleichheit automatisch mit `-2`, `-3`, … eindeutig gemacht. Einmal vergeben, bleibt der Slug bei späteren Textänderungen stabil (nur bei Neuanlage neu berechnet), damit einmal geteilte/verlinkte Blog-URLs nicht durch spätere Titelkorrekturen brechen.

**Öffentlich:** `/blog` (Karten-Übersicht, neuste zuerst, verlinkt im Footer neben Conventions) und `/blog/[slug]` (Detailseite mit Titelbild, Absätzen, eigenen SEO-Metadaten pro Beitrag — Kurzbeschreibung wird automatisch zur Meta-Description, das war der eigentliche SEO-Zweck dieses Punkts).

**Admin** (`/admin/blog`, gleicher Design-Standard wie Conventions/FAQ aus dieser Runde): Statistik-Kacheln (Beiträge gesamt/Veröffentlicht/Entwürfe), Sortierung nach Veröffentlichungsdatum (kein manuelles Sortieren nötig, anders als bei FAQ/Testimonials/Pakete), Sichtbarkeits-Schalter in der Zeilen-Übersicht wie bei den anderen Content-Typen dieser Runde (nicht Teil des Bearbeiten-Formulars, gleicher Bugfix-Mechanismus).

End-to-End mit Wegwerf-Mitarbeiterkonto verifiziert: Beitrag mit Umlauten im Titel angelegt → Slug korrekt zu „convention-uemlaut-test-ae" transliteriert → zweiter Beitrag mit demselben Titel angelegt → Slug korrekt zu „…-2" disambiguiert → beide erscheinen auf der öffentlichen Übersichtsseite, Detailseiten laden korrekt mit Absätzen → Text bearbeitet, Sichtbarkeit blieb korrekt erhalten. Alle Testdaten (Konto, Beiträge, hochgeladenes Titelbild) danach entfernt.

**Damit ist die komplette „Dritte Runde" (Punkte 0–8, festgelegt am 07.08.2026) abgeschlossen.**

## Externe Google-Bewertungen im Testimonials-Bereich (07.08.2026) ✅ — Punkt 7
Kein Google-Cloud-API-Key vorhanden (wie beim Setup schon vermerkt) — daher wie geplant als manuell gepflegte zweite Quelle im bestehenden Testimonials-Bereich umgesetzt, keine Live-Anbindung. Bestehendes `Testimonial`-Model um `source` ("manual" | "google"), `rating` (1–5, nur bei Google-Quelle gesetzt) und `reviewUrl` (Link zur echten Bewertung, für Nachprüfbarkeit) erweitert — kein neues Model, keine neue Berechtigung, wie geplant weiterhin `testimonials`.

**Admin-Bereich** (`/admin/testimonials`): jetzt mit echtem Reiter-Umschalter „Kundenstimmen" / „Google-Bewertungen" (per Query-Parameter, serverseitig gefiltert), eigene Statistik-Kachel für Google-Bewertungen (Anzahl + Ø Sterne). Das Anlage-Formular hat oben einen Quelle-Umschalter — je nach Auswahl wechseln die Felder darunter: bei „Google-Bewertung" ein Sterne-Klick-Widget statt Profilbild-Upload, plus optionalem Link zur echten Bewertung. Die Quelle ist nach Anlage nicht mehr änderbar (ein Eintrag ist entweder das eine oder das andere), im Bearbeiten-Formular erscheinen Sterne/Link-Feld entsprechend nur bei bestehenden Google-Einträgen.

**Startseite:** Google-Bewertungen erscheinen im selben Fließband wie die eigenen Kundenstimmen, aber mit sichtbarer Sterne-Reihe über dem Zitat und einem kleinen „· Google"-Hinweis beim Namen — bewusst kein nachgebautes Google-Logo (Marken-/Lizenzfragen), nur ehrliche Text-Kennzeichnung, damit klar bleibt, woher die Bewertung stammt.

End-to-End mit Wegwerf-Mitarbeiterkonto verifiziert: Reiter „Google-Bewertungen" aufgerufen → neue Bewertung mit 4 Sternen + Link angelegt → erscheint korrekt mit Google-Badge und „4★"-Kennzeichnung in der Liste → im Reiter „Kundenstimmen" korrekt NICHT sichtbar → Text bearbeitet und gespeichert → Sichtbarkeit blieb korrekt erhalten (gleicher Bugfix-Mechanismus wie bei den anderen Content-Typen in dieser Runde) → Sterne-Auswahlfeld im Bearbeiten-Formular vorhanden und funktionsfähig → Startseite zeigt den bearbeiteten Text korrekt mit Sterne-Reihe und „Google"-Hinweis. Alle Testdaten danach entfernt.

## Wiedervorlage-Mail nach einem Jahr (07.08.2026) ✅ — Punkt 6
Automatische „ist schon eine Weile her"-Erinnerungsmail an Kunden, deren letztes vereinbartes Shooting (`Order.shootDate`, der vom Admin fest vereinbarte Termin, nicht der freitextliche Wunschzeitraum) eine einstellbare Anzahl Tage zurückliegt (Standard 365, analog zu einem Cronjob-Sicherheitsnetz wie beim bestehenden Google-Calendar-Sync). Läuft über einen neuen täglichen System-Crontab-Eintrag (`0 6 * * *`) gegen `/api/cron/reengagement`, secret-abgesichert wie der bestehende Google-Sync-Cronjob — kein Nutzer-Login hier nötig oder möglich.

**Kein Doppelversand:** neues Feld `Client.lastReengagementShootDate` merkt sich, für welches shootDate zuletzt schon eine Mail rausging. Der tägliche Lauf ermittelt per Aggregation pro Kunde das jeweils späteste vergangene shootDate und verschickt nur, wenn dieses Datum entweder noch nie gemeldet wurde oder neuer ist als beim letzten Versand — bucht ein Kunde nach der Erinnerung ein neues Shooting, das später ebenfalls die Frist überschreitet, bekommt er dafür ganz regulär eine frische Erinnerung.

**Admin-Einstellung** (`/admin/settings`, `settings`-Berechtigung wie geplant, gleiches Baukasten-Muster wie das bestehende Empfehlungsprogramm-Panel direkt daneben): an/aus-Schalter, Tage-Frist frei einstellbar (Minimum 30, gegen Verklicker).

End-to-End über den echten Cron-Endpunkt verifiziert (nicht nur die Kernlogik isoliert, sondern exakt der Weg, den der spätere Systemcron auch nimmt): zwei Wegwerf-Testkunden angelegt, einer mit einem Shooting vor 400 Tagen, einer mit einem Shooting vor 10 Tagen → Aufruf mit korrektem Secret findet genau 1 Kandidaten (den mit dem alten Termin), verschickt genau 1 Mail (E-Mail-Zustellung schlug erwartungsgemäß fehl, erfundene `.test@rakku.de`-Adresse, sauber von `safeSend()` abgefangen — bekanntes Muster) → `lastReengagementShootDate` korrekt gesetzt, der Kunde mit dem aktuellen Shooting blieb unangetastet → erneuter Aufruf verschickt korrekt **keine** zweite Mail (0 statt 1) → nach Anlage eines weiteren, noch älteren aber innerhalb der Frist liegenden Shootings für denselben Kunden verschickt der nächste Lauf korrekt wieder eine (neue) Mail → falsches Secret korrekt mit 401 abgelehnt. Admin-Einstellungspanel im Browser getestet (Frist geändert, gespeichert, nach Neuladen persistiert bestätigt). Alle Testdaten (Kunden, Aufträge, Testkonto) danach entfernt, Standardfrist von 365 Tagen wiederhergestellt.

## Zufriedenheits-Nachfrage nach Auftragsabschluss (07.08.2026) ✅ — Punkt 5
Neues `OrderFeedback`-Model, ein Eintrag pro Auftrag (eindeutiger Index auf `orderId`). Auslöser ist bewusst der bestehende „Archivieren"-Button auf der Admin-Auftragsseite — der bislang schon informell „Auftrag ist fertig" bedeutet hat, kein neues Statusfeld nötig. Beim ersten Archivieren eines Auftrags wird automatisch ein zufälliger Token erzeugt und eine kurze Mail mit Link an den Kunden verschickt; wird ein Auftrag wieder aus dem Archiv geholt und erneut archiviert, verhindert der eindeutige Index einen zweiten Versand.

**Öffentliche Seite** (`/feedback/[token]`, kein Login nötig — gleiches Token-Link-Muster wie `/verify/[token]` und `/reset-password/[token]`): 1–5-Sterne-Bewertung (Pflicht) + freier Kommentar (optional) + eine Checkbox „Mein Feedback darf (ggf. gekürzt) als Kundenstimme verwendet werden". Ein bereits benutzter oder ungültiger Token zeigt jeweils eine passende Meldung statt eines Formulars.

**Admin-Übersicht** (`/admin/feedback`, `orders`-Berechtigung wie geplant): Statistik-Kacheln (Antworten/Gesamt, Ø Bewertung, Anzahl für Kundenstimme freigegeben), Liste mit Sternen, Kommentar und Link zum Auftrag. Bei freigegebenem Feedback erscheint ein Button „Als Kundenstimme übernehmen" — erzeugt direkt einen neuen Testimonial-Eintrag (Name, Zitat, Kontext = Art des Shootings), bewusst mit `visible: false`, damit Rakku Wortlaut/Kürzung erst in der Bewertungen-Verwaltung gegenlesen kann, bevor er live auf der Startseite erscheint. Genau das war die Idee aus der ursprünglichen Vorschlagsliste: „liefert nebenbei Rohmaterial für neue Testimonials".

End-to-End mit Wegwerf-Konten (Mitarbeiter + Kunde + Testauftrag) verifiziert: Auftrag archiviert → `OrderFeedback` mit Token korrekt angelegt (E-Mail-Versand schlug erwartungsgemäß fehl, da eine erfundene `.test@rakku.de`-Adresse verwendet wurde — bekanntes, harmloses Muster von den Convention-Tests, sauber von `safeSend()` abgefangen) → öffentliche Feedback-Seite mit Token aufgerufen, 4 Sterne + Kommentar + Freigabe-Haken abgeschickt → Seite erneut mit demselben Token aufgerufen zeigt korrekt „bereits gespeichert" → ungültiger Token zeigt korrekt eine Fehlermeldung → Admin-Feedback-Seite zeigt Kommentar und Freigabe-Button → Umwandlung in Kundenstimme erfolgreich, neuer (unsichtbarer) Testimonial-Eintrag mit korrektem Zitat/Kontext in der DB bestätigt → erneutes Aus-Archiv-Holen und Wieder-Archivieren erzeugt korrekt **keinen** zweiten Feedback-Eintrag. Alle Testdaten (Konten, Auftrag, Feedback, Test-Testimonial) danach entfernt.

## Convention-Übersicht + Warteliste (07.08.2026) ✅ — Punkte 3+4
Zusammen umgesetzt, da eng verzahnt (Warteliste ergibt ohne die öffentliche Convention-Seite keinen Sinn). Neue eigene Berechtigung `conventions`.

**Öffentliche Seite** (`/conventions`, „Wo trefft ihr Rakku?", verlinkt im Footer statt im Hauptmenü — das wäre mit Gutscheinen als sechstem Punkt zu voll geworden): zeigt anstehende, sichtbare Conventions chronologisch sortiert. Hat die Convention noch freie Slots, ein Button „Slot anfragen" (führt direkt in den bestehenden Auftragsanfrage-Flow, mit vorbefülltem Konzept „Slot-Anfrage: {Name}"). Sind keine Slots mehr frei, erscheint stattdessen ein Warteliste-Formular (Name/E-Mail/optionale Nachricht).

**Warteliste:** `WaitlistEntry`-Model, ein Eintrag pro Convention+E-Mail (Doppel-Eintrag wird abgelehnt). Admin sieht die Anzahl Wartender direkt in der Zeilen-Übersicht jeder Convention und bekommt, sobald Slots wieder frei werden, einen kombinierten Button „Warteliste benachrichtigen" — öffnet die Slots UND verschickt in einem Rutsch an alle noch nicht benachrichtigten Wartenden eine Mail, jede einzeln in einem try/catch (schlägt eine Zustellung fehl, blockiert das nicht die anderen; nur erfolgreich benachrichtigte Einträge werden als `notified` markiert, damit ein späterer erneuter Versuch fehlgeschlagene Zustellungen automatisch nachholt statt sie zu verlieren).

**Wichtiger Bugfix im Rahmen dieser Runde (betrifft auch bereits früher ausgelieferte Bereiche):** Beim Bauen des Convention-Formulars fiel eine ganze Fehlerklasse auf, die schon länger im Code steckte. Unbestätigte HTML-Checkboxen senden beim Abschicken eines Formulars gar keinen Wert (nicht etwa `"off"`) — der bisher an mehreren Stellen verwendete Vergleich `formData.get("feld") !== "off"` wertet das fälschlich als „angehakt". Betraf die Neuanlage-Formulare für FAQ, Testimonials, Portfolio, und jetzt neu auch Pakete und Conventions — überall auf `=== "on"` umgestellt. Beim Bearbeiten-Formular ist es komplizierter: FAQ und Testimonials haben in ihrem Bearbeiten-Formular gar kein „Sichtbar"-Feld (Sichtbarkeit läuft dort über den separaten Schalter in der Zeilen-Übersicht) — ein blindes `=== "on"` hätte dort beim nächsten Text-Speichern die Sichtbarkeit jedes Mal auf „aus" zurückgesetzt. Für `updateConventionAction` galt dieselbe Falle und wurde vor dem Live-Test noch behoben (das „Sichtbar"-Feld komplett aus dem Update-Payload entfernt, „Slots frei" bleibt als echtes Formularfeld unangetastet). Portfolio ist unbetroffen, da dessen Bearbeiten-Formular ein verstecktes Fallback-Feld mitschickt.

End-to-End mit Wegwerf-Mitarbeiterkonto verifiziert: zwei Test-Conventions angelegt → Textänderung an einer bestehenden Convention gespeichert, Sichtbarkeit blieb korrekt erhalten (Regressionstest für den Bugfix oben) → „Slots frei" abgeschaltet → öffentliche Seite zeigt korrekt das Warteliste-Formular statt des Anfrage-Buttons → Eintragung erfolgreich, doppelte Eintragung mit derselben E-Mail korrekt abgelehnt → „Warteliste benachrichtigen" geklickt → Slots wieder offen, Wartende als benachrichtigt markiert. Ein erster Testlauf mit einer erfundenen `.test@rakku.de`-Adresse zeigte `notified: false` trotz erfolgreichem Klick — kein Bug, sondern der SMTP-Server von rakku.de weist unbekannte Empfänger auf der eigenen Domain zurück (`550 User unknown`); mit einer echten zustellbaren Adresse lief die Zustellung anschließend sauber durch. Alle Testdaten (Conventions, Warteliste-Einträge, Testkonto) danach entfernt.

## Paket-/Kombi-Angebote (07.08.2026) ✅ — Punkt 2
Neues, eigenständiges `Bundle`-Model — bewusst NICHT als Verknüpfung bestehender Service/PriceTier-Datensätze gebaut (das würde bei künftigen Preisänderungen an den Einzelangeboten den Paketpreis unerwartet verschieben), sondern als admin-gepflegter Gesamtpreis mit freier Beschreibung, was enthalten ist. Optionales `compareAtPrice` für eine „Du sparst X"-Anzeige. Reguläre `orders`-Berechtigung (wie geplant, keine neue nötig), da Pakete inhaltlich zu den Angeboten gehören.

Admin-Bereich unter `/admin/bundles` (neuer Nav-Punkt „Pakete"), gleiches Baukasten-Muster wie FAQ/Testimonials (Pfeiltasten-Sortierung, Sichtbar-Schalter, deaktivieren statt löschen). Auf der öffentlichen Angebote-Seite erscheint unterhalb der regulären Preisstufen ein neuer Abschnitt „Kombi-Angebote" (nur sichtbare Pakete), jede Karte mit durchgestrichenem Vergleichspreis + Ersparnis-Hinweis, falls hinterlegt.

Buchung läuft über denselben, bereits bestehenden Preisstufen-Mechanismus: `Order` bekam ein paralleles Schnappschuss-Feldpaar (`bookedBundleTitle`/`bookedBundlePrice`, analog zu `bookedServiceTitle`/`bookedTierName`/`bookedTierPrice`), `createOrderAction` prüft den `bundleId`-Parameter serverseitig (nie dem Client vertraut) genau wie bei Service/Tier. Admin-Auftragsseite zeigt bei einer Paket-Buchung denselben „Nur zur Orientierung"-Hinweisblock wie bei Angebots-Buchungen.

End-to-End mit Wegwerf-Konten verifiziert: Paket im Admin angelegt (400 € statt 450 €) → erscheint korrekt auf der öffentlichen Angebote-Seite mit „Du sparst 50 €" → Buchungslink enthält die richtige `bundleId` → Kunde schließt die Anfrage ab → Schnappschuss-Felder im Auftrag korrekt gesetzt (400 €, „Test-Jahrespaket"). Testdaten danach entfernt.

## Gutscheine (07.08.2026) ✅ — Punkt 1
Neues `GiftVoucher`-Model, bewusst getrennt von `DiscountCode` (echtes, vorab bezahltes Geld statt Marketing-Rabatt — dieselbe Trennungs-Logik wie seinerzeit bei `client_accounts` vs. `clients`). Neue eigene Berechtigung `vouchers`.

**Öffentliche Anfrage** (`/gutscheine`, neuer Nav-Punkt): Betrag wählen (Preset-Buttons 50/100/150/250 € oder frei 20–1000 €), Name/E-Mail/optionaler Empfänger/Nachricht. Läuft Stripe UND ist Online-Zahlung in den Einstellungen aktiv → sofort Stripe-Checkout (dieselbe Infrastruktur wie beim Auftrags-Bezahlen, über Metadata `voucherId` von Auftragszahlungen unterschieden). Ist beides nicht der Fall (aktuell der Fall — `paymentSettings.enabled` steht noch auf aus) → Anfrage landet als „offen" im Admin-Bereich, Rakku und Käufer:in bekommen je eine Bestätigungsmail, Rakku bestätigt den Zahlungseingang später manuell.

**Code-Vergabe:** Erst bei tatsächlicher Bezahlung (Stripe-Webhook oder manuelle Bestätigung im Admin) wird ein Code erzeugt und per Mail verschickt — nie vorher, damit kein unbezahlter Gutschein im Umlauf sein kann. Admin kann zusätzlich direkt einen bereits bezahlten Gutschein manuell ausstellen (Bar-/Vor-Ort-Zahlung, Werbegeschenk).

**Einlösung:** `redeemDiscountCodeAction` in `discounts.ts` um eine dritte Fallback-Stufe erweitert (nach Rabattcode, nach Empfehlungscode): Gutschein-Code wird wie ein fester Rabattbetrag behandelt (`computeDiscountAmount("fixed", ...)`, an der Auftragssumme gedeckelt — Restguthaben über dem Auftragswert verfällt bewusst, Standard bei Gutscheinen). Einmalig einlösbar, geprüft auf bezahlt/nicht storniert/nicht abgelaufen (3 Jahre Standardgültigkeit, § 195 BGB).

**Admin-Übersicht** (`/admin/vouchers`): Statistik-Kacheln (offene Anfragen, Wert im Umlauf, eingelöst), Liste mit Status-Badges, „Als bezahlt bestätigen" (erzeugt Code + verschickt Mail) und „Stornieren" (soft, kein Hard-Delete — Geld-Historie bleibt nachvollziehbar).

End-to-End mit Wegwerf-Konten vollständig verifiziert, bewusst **ohne** eine echte Stripe-Zahlung anzustoßen (Umgebung hat einen `STRIPE_SECRET_KEY` hinterlegt, dessen Live/Test-Status ungeklärt ist — daher nur der manuelle Zweig getestet, der aktuell ohnehin der einzig aktive ist): öffentliche Anfrage → Admin bestätigt Zahlung → Code erzeugt → Einlösung auf einem echten Test-Auftrag (150 € Vorschlag, 100 € Gutschein → korrekt 100 € Rabatt) → Wiederverwendung auf einem zweiten Auftrag korrekt abgelehnt → manuelles Direkt-Ausstellen verifiziert → Stornieren verifiziert. Alle Testdaten danach entfernt.

## Design-Politur Admin-FAQ + Admin-Aufträge (07.08.2026) ✅ — Punkt 0
Admin-FAQ-Seite: Fragen jetzt nach Kategorie gruppiert (dieselbe Gruppierungslogik wie auf der öffentlichen FAQ-Seite, Admin sieht so direkt die spätere Struktur), farbige Kategorie-Badges (deterministisch aus dem Kategorienamen abgeleitet, neue Hilfsfunktion `lib/colorHash.ts` — dieselbe Kategorie hat immer dieselbe Farbe, ohne dass Rakku sie manuell zuweisen muss), Statistik-Kacheln oben (Fragen gesamt/Kategorien/Ausgeblendet), ausgeblendete Fragen jetzt sichtbar abgedimmt statt optisch identisch zu sichtbaren.

Admin-Auftrags-Detailseite: war bisher ein einziger langer, undifferenzierter Stapel aus ca. 10 Panels ohne visuelle Gliederung — jetzt in drei beschriftete Abschnitte gruppiert („Preis & Zahlung", „Vertrag & Fotos", „Verwaltung", dieselbe Trenner-Optik wie bei den FAQ-Kategorien). Neue Statistik-Kachelreihe direkt unter dem Kopfbereich (aktueller Preis/Zahlungsstatus/Tage offen) für einen schnellen Überblick, ohne erst scrollen zu müssen.

Mit Screenshot-Vergleich (Playwright, echtes Wegwerf-Mitarbeiterkonto) verifiziert — beide Seiten optisch strukturiert statt „basic", keine neuen Fehler im pm2-Log. Für den Aufträge-Screenshot einen bereits vorhandenen echten Testdatensatz („Test"-Auftrag) nur gelesen, nicht verändert.

## Umsetzungsreihenfolge Punkt 7: Zwei-Faktor-Login fürs Team (07.08.2026) ✅ — Roadmap komplett
Letzter und bewusst zuletzt umgesetzter Punkt der Reihenfolge (sicherheitskritisch, verdient ungeteilte Aufmerksamkeit). TOTP-basiert (Authenticator-App wie Google Authenticator/Authy/1Password), ausschließlich für Mitarbeiter-Accounts (`role: "staff"`) nutzbar, in den eigenen Profileinstellungen (`/account/profile`) einricht- und wieder deaktivierbar.

**Technisch:**
- Eigene RFC-6238-Implementierung in `lib/totp.ts` auf Basis von Node's `crypto` (HMAC-SHA1, Base32) — bewusst keine zusätzliche Bibliothek wie otplib/speakeasy, analog zum bereits bestehenden Muster „eigener REST-Client statt SDK" bei der Google-Calendar-Anbindung. Einzige neue Abhängigkeit: `qrcode` (nur für die QR-Code-Grafik selbst, das nachzubauen wäre unverhältnismäßig).
- Neue Felder am `Client`-Model: `totpEnabled`, `totpSecret`, `totpRecoveryCodeHashes` (wie Passwörter gehasht, nie im Klartext gespeichert). Secret wird schon beim Start der Einrichtung gespeichert, `totpEnabled` springt aber erst nach einem erfolgreich verifizierten Code aus der Authenticator-App auf `true` — ein abgebrochener Einrichtungsversuch kann so niemanden aussperren.
- Login-Flow zweigeteilt: Passwort-Prüfung wie bisher, aber bei aktivem 2FA entsteht noch keine echte Session — stattdessen ein kurzlebiges (5 Min.) signiertes Zwischen-Cookie (`rakku_2fa_pending`, neue Helper in `lib/auth.ts`), das erst nach gültigem TOTP- oder Wiederherstellungscode gegen die echte Session (`rakku_session`) eingetauscht wird.
- 8 Wiederherstellungscodes werden bei erfolgreicher Einrichtung einmalig angezeigt (danach nicht mehr abrufbar) — je einmal verwendbar, für den Fall eines verlorenen Geräts. Neu erzeugbar oder die ganze 2FA-Einrichtung aufhebbar, beides nur nach erneuter Passwort-Bestätigung.

End-to-End mit einem Wegwerf-Mitarbeiterkonto vollständig durchgespielt (TOTP-Codes dafür mit einer eigenständigen Kopie desselben Algorithmus lokal nachgerechnet, kein echtes Handy nötig): QR-Code+Secret angezeigt → Einrichtung mit echtem Code bestätigt → 8 Wiederherstellungscodes ausgegeben → Login mit Passwort zeigt korrekt die 2FA-Abfrage statt direkt einzuloggen → falscher Code abgelehnt → korrekter Code loggt ein → Login mit Wiederherstellungscode funktioniert → derselbe Code beim zweiten Versuch korrekt abgelehnt (Einmal-Verwendung) → Deaktivieren mit falschem Passwort abgelehnt, mit korrektem Passwort erfolgreich. Keine neuen Fehler im pm2-Log während der gesamten Testreihe. Alle Testdaten danach aus der DB entfernt.

Damit ist die komplette, am 07.08.2026 festgelegte 7-Punkte-Umsetzungsreihenfolge abgeschlossen.

## Umsetzungsreihenfolge Punkt 6: Bewertungen/Testimonials (07.08.2026) ✅
Kurze Kundenstimmen auf der Startseite, admin-kuratiert — kein offenes öffentliches Bewertungsformular, Rakku trägt Zitate selbst ein. Technisch bewusst 1:1 nach dem FAQ-Muster gebaut (eigenes `Testimonial`-Model mit `visible`/`sortOrder`, Pfeil-Buttons zum Sortieren, gleiche Admin-UI-Struktur mit `<details>`-Zeilen). Felder: Name, Zitat (DE/EN), optionaler Kontext (z. B. „Cosplay Shooting"). Neue Sektion ganz unten auf der Startseite, direkt vor dem Footer — rendert nichts, solange keine sichtbaren Einträge existieren, damit die Seite nie mit einer leeren Sektion dasteht.

Neue, eigene Berechtigung `testimonials` angelegt (nicht die bestehende `faq`- oder `portfolio`-Berechtigung wiederverwendet) — passend zum bisherigen Muster im Rechtesystem, das für unterschiedliche Content-Typen jeweils eigene, präzise Berechtigungen vergibt statt sie zu bündeln.

End-to-End mit Wegwerf-Mitarbeiterkonto verifiziert: Kundenstimme angelegt → erscheint korrekt auf der Live-Startseite (per curl direkt gegen die ausgelieferte Seite geprüft) → Sichtbarkeit ausgeschaltet → verschwindet von der Startseite, bleibt aber in der Admin-Liste. Eine erste Testrunde mit `.uncheck()`/verzögerter Prüfung lieferte einen falschen Fehlalarm (bekanntes Playwright-Timing-Muster aus dieser Session, kein echter Bug) — mit `.click()` und erneuter DB-Prüfung sauber verifiziert. Testdaten danach entfernt.

## Umsetzungsreihenfolge Punkt 5: Empfehlungsprogramm (07.08.2026) ✅
Jeder Kunde hat jetzt einen eigenen, automatisch vergebenen Empfehlungscode (`Client.referralCode`, lazy vergeben beim ersten Aufruf des Kontos — kein Backfill-Skript nötig, Bestandskonten bekommen ihn einfach beim nächsten Dashboard-Besuch). Sichtbar im eigenen Konto samt Kopieren-Button, unter „Freunde werben".

Löst ein Kunde den Empfehlungscode eines anderen ein, greift dieselbe Einlöse-Funktion wie bei normalen Rabattcodes (`redeemDiscountCodeAction`) — findet sie keinen passenden `DiscountCode`, wird zusätzlich gegen `Client.referralCode` geprüft. Baut bewusst auf Punkt 3 und 4 auf: die Neukunden-Prüfung ist dieselbe Logik wie bei „nur Neukunden"-Rabattcodes (dieser Auftrag muss der einzige des Kunden sein), und die Prämie für den Werber ist technisch ein ganz normaler, additiver Rabattcode. Selbstwerbung ist ausgeschlossen. Bei Erfolg: der Neukunde bekommt sofort einen Rabatt auf seinen Auftrag (Standard 10 %, admin-einstellbar), der Werber automatisch einen neuen, nur für ihn gültigen Rabattcode (`DiscountCode.ownerId`, neues Feld — beschränkt die Einlösung auf genau dieses eine Konto) gutgeschrieben, sichtbar in seinem Konto unter „Deine verdienten Rabattcodes". In der Admin-Rabattcode-Liste sind automatisch erzeugte Prämien-Codes mit einem eigenen Badge („Empfehlungsprämie · Kundenname") gekennzeichnet, damit sie nicht mit manuell angelegten Marketing-Codes verwechselt werden.

Neuer Einstellungsbereich unter „Empfehlungsprogramm" in den Admin-Einstellungen: Programm an/aus, Rabatthöhe für den Neukunden und Prämienhöhe für den Werber jeweils separat einstellbar (Prozent oder fester Betrag) — reine Business-Entscheidung, sollte nicht im Code festgehämmert sein.

End-to-End mit drei Wegwerf-Konten verifiziert (Werber, geworbener Neukunde, Mitarbeiter mit Vollzugriff): Empfehlungscode im Konto sichtbar → von Neukunde eingelöst → korrekter Rabatt (20 € auf 200 €) → Werber bekommt automatisch einen neuen, korrekten Prämien-Code gutgeschrieben und sieht ihn im eigenen Konto → Selbstwerbung korrekt abgelehnt → Admin-Liste zeigt den Prämien-Code mit Empfehlungs-Badge und Namen des Werbers. Alle Testdaten danach aus der DB entfernt.

## Umsetzungsreihenfolge Punkt 4: Manueller Rabatt direkt am Auftrag (07.08.2026) ✅
Team kann jetzt direkt an einem Auftrag einen Kulanz-Rabatt gewähren (Betrag + optionaler Grund), unabhängig von einem eingelösten Rabattcode. Bewusst als eigenes Feldpaar `manualDiscountAmount`/`manualDiscountNote` am Order-Model (nicht `discountCode`/`discountAmount` wiederverwendet), damit Code- und manuell gewährte Rabatte für Statistik/Nachvollziehbarkeit unterscheidbar bleiben — additiv zueinander. Neue Berechtigungsgrundlage: `pricing`-Berechtigung (nicht `discounts`), wie in der Planung festgehalten, da es eine Preisentscheidung am einzelnen Auftrag ist. Jede Vergabe/Entfernung wird zusätzlich im Auftrags-Aktivitätsverlauf protokolliert.

Kombinierter Rabatt (`getTotalOrderDiscount()`, neuer Helper in `lib/discounts.ts`) wird jetzt überall dort verwendet, wo vorher nur der Code-Rabatt einfloss: Stripe-Checkout-Betrag, Rechnungserstellung (inkl. neuem Schnappschussfeld auf der Rechnung + PDF-Zeile), Umsatzstatistik, Kunden-Umsatzsumme in der Kundendetailseite, sowie die Kunden- und Admin-Ansicht des Auftrags. Ein während der Prüfung entdeckter echter UI-Lücke behoben: die Admin-Seite zeigte den „zu zahlen"-Endbetrag vorher nur an, wenn zusätzlich ein Rabattcode vorlag — bei einem rein manuellen Rabatt fehlte die Anzeige komplett; jetzt zeigt das Manueller-Rabatt-Panel den kombinierten Endbetrag eigenständig.

End-to-End mit Wegwerf-Konten (Kunde + Mitarbeiter mit Vollzugriff) verifiziert: Rabatt gewährt (30 € auf 300 €) → korrekt in Admin- und Kundenansicht sichtbar (270 € zu zahlen) → Rechnung erstellt mit korrektem Gesamtbetrag (270 €) und beiden Rabattfeldern im Schnappschuss. Testrechnung danach wieder gelöscht — dabei auch die verbrauchte Rechnungsnummer im Zähler zurückgesetzt, damit keine Lücke in der Rechnungsnummern-Sequenz entsteht (steuerlich relevant). Alle übrigen Testdaten aus der DB entfernt.

## Umsetzungsreihenfolge Punkt 3: Rabattcodes nur für Neukunden (07.08.2026) ✅
Gegenstück zum bereits bestehenden „nur Stammkunden"-Schalter. Neues Feld `neukundenOnly` am `DiscountCode`-Model, in beiden Admin-Formularen (Neu anlegen + Bearbeiten) als eigene Checkbox, eigenes Badge in der Codeliste. Einlösen läuft weiterhin über die bestehende `discounts`-Berechtigung, keine neue Berechtigung nötig (wie schon in der Planung festgehalten). Durchsetzung in `redeemDiscountCodeAction`: zählt die Gesamtaufträge des Kunden — bei `neukundenOnly` wird abgelehnt, sobald mehr als der aktuelle Auftrag existiert (spiegelbildlich zur bestehenden `stammkundenOnly`-Logik, die bei ≤1 Auftrag ablehnt). Mit Wegwerf-Testkonto verifiziert: Einlösung gelang beim ersten Auftrag (echter Neukunde, −15,00 € auf 150,00 €), wurde beim zweiten Auftrag desselben Kunden korrekt mit „Dieser Rabattcode ist nur für Neukunden gültig." abgelehnt. Alle Testdaten (Client, 2 Aufträge, 2 Preisvorschläge, 2 Codes) danach aus der DB entfernt.

## Umsetzungsreihenfolge Punkt 2: „Nochmal buchen"-Button (07.08.2026) ✅
Auf der Auftrags-Detailseite im Kundenkonto gibt es jetzt einen „Nochmal buchen"-Link. Führt zu `/account/new-order?fromOrderId=<id>` — die Seite lädt server-seitig den alten Auftrag (mit Eigentümer-Check: nur der eigene Auftrag wird akzeptiert, sonst wird der Parameter stillschweigend ignoriert statt einen Fehler zu zeigen, da reines Komfort-Feature ohne sicherheitsrelevante Konsequenz) und befüllt das neue Auftragsformular mit allen Freitextfeldern (Art des Shootings, Charakter/Konzept, Ort, Zeitraum, Instagram/Discord-Handle, Idee/Stimmung, Zusatznotizen) vor. Bewusst keine automatische Wiederverknüpfung zu einer Angebots-Preisstufe — Preise/Verfügbarkeit können sich seit dem alten Auftrag geändert haben, dafür bleibt die Angebote-Seite der richtige Weg. Mit Wegwerf-Testkonto+-Auftrag end-to-end verifiziert (Vorbefüllung korrekt, Formular tatsächlich absendbar, neuer Auftrag landet im Konto), Testdaten danach vollständig aus der DB entfernt.

## Umsetzungsreihenfolge Punkt 1: Positionen/Abteilungen/Teams umbenennen (07.08.2026) ✅
Erster Punkt der am 07.08.2026 festgelegten Umsetzungsreihenfolge. Drei neue Server-Actions in `lib/actions/team.ts` (`updateDepartmentNameAction`, `updatePositionNameAction`, `updateTeamNameAction`), alle hinter der bestehenden `team_management`-Berechtigung, keine neue Berechtigung nötig. Neue wiederverwendbare Komponente `RenameField.tsx` (Klick-zum-Bearbeiten-Inline-Umbenennung) in Team-Übersicht (Abteilungen, Teams) und `PositionRow.tsx` (Positionen) eingebaut. Team-Umbenennen von mir mit ergänzt, obwohl nicht explizit verlangt — gleiche Lücke wie bei Positionen/Abteilungen, gleicher Aufwand. Mit einem Wegwerf-Testkonto (`.test@rakku.de`-Muster, danach aus der DB entfernt) end-to-end per Playwright verifiziert: alle drei Entitätstypen lassen sich umbenennen, keine neuen Fehler im pm2-Log.

## Schwerpunkt-Sektion von V1 übernommen + Gesamt-Review (07.08.2026) ✅
Nutzer bat darum, den "Schwerpunkt"-Bereich der echten Live-Seite rakku.de (V1) zu übernehmen. Per WebFetch die V1-Seite angesehen: dreispaltiger Block unter der bereits vorhandenen "Cosplay Photography"-Überschrift mit Convention/Outdoor/Portraits samt Kurzbeschreibung — V2 hatte die Überschrift/den Einleitungstext schon (Philosophy-Sektion), aber nicht die dreispaltige Aufschlüsselung darunter. Wortgleich (DE) übernommen + ins Englische übersetzt, im bestehenden Karten-Look ergänzt (kein neues Icon-System eingeführt, passt so zur bestehenden Formsprache der Seite).

Danach vollständiger Regressions-Durchlauf über alle heute gebauten Features (Kalender+Google-Sync, Rabattcodes, Angebote/Preisstufen, Ablehnungsgründe, SEO, Social-Links, E-Mail-Benachrichtigungen) — 26/26 Seiten laden fehlerfrei, keine neuen Server-Fehler.

## Angebote mit Preisstufen (07.08.2026) ✅
Nutzer-Wunsch: fertige Angebote mit mehreren Preisstufen anlegen, damit Kunden direkt die passende Preisklasse wählen und buchen können — jede Preisklasse mit eigenen Leistungen. Zusätzlich: Angebote/Preisstufen im Admin hinzufügbar und **deaktivierbar statt nur löschbar**.

- **Datenmodell umgebaut:** `Service` (= Angebots-Kategorie, z. B. "Portrait Shooting") hat jetzt ein eingebettetes `tiers`-Array — jede Preisstufe mit eigenem Namen (DE/EN), Preis, „ab“-Kennzeichnung, Leistungsbeschreibung (DE/EN) und eigenem `active`-Schalter. Die alte freitextliche `priceText` (nur ein einzelner Preis-String pro Angebot) komplett ersetzt
- **Deaktivieren statt löschen — auf beiden Ebenen:** Angebote hatten das schon (Status verfügbar/auf Anfrage/nicht verfügbar, unverändert), **neu für Preisstufen**: eigener Aktiv-Schalter pro Preisstufe, unabhängig vom Löschen. Admin kann z. B. die teuerste Preisstufe vorübergehend ausblenden, ohne sie und ihre ganze Konfiguration zu verlieren
- **`/admin/services/[id]/edit` erweitert:** unter den Angebots-Basisdaten jetzt volle Preisstufen-Verwaltung (hinzufügen, bearbeiten, aktiv/inaktiv, löschen) — Preisstufen leben eingebettet im Service-Dokument, kein eigener Datenbank-Layer nötig
- **`/angebote` komplett neu gestaltet:** jedes Angebot als Karte mit allen aktiven Preisstufen nebeneinander (klassische Pricing-Table-Optik), Preis + Leistungen + eigener Buchen-Button pro Preisstufe. Inaktive Preisstufen werden automatisch ausgeblendet, ohne dass der Admin an mehreren Stellen etwas anpassen muss
- **Bestellung ↔ Angebot jetzt nachvollziehbar:** vorher ging beim Buchen nur der Angebotstitel als Freitext in die Anfrage ein, keine strukturierte Verbindung. Jetzt speichert die Anfrage einen Schnappschuss (Angebotstitel, Preisstufen-Name, Preis) — server-seitig aus der echten Datenbank nachgeschlagen, nicht aus der URL vertraut (Manipulationsschutz). Admin sieht das direkt auf der Auftragsseite ("Über Angebot gebucht: Portrait Shooting — Standard (150,00 €)") als Ausgangspunkt für die eigentliche Preisverhandlung, die weiterhin ganz normal über das bestehende Preisvorschlag-System läuft
- **Inhalt:** 4 Angebots-Kategorien mit je 3 Preisstufen angelegt (Portrait Shooting, Cosplay Shooting, Convention-Fotografie, Event-Fotografie — 80–550 € je nach Umfang), realistisch nach Aufwand/Umfang gestaffelt
- Getestet: kompletter Kundenweg (Angebot browsen → Preisstufe wählen → Login-Umweg falls nötig → Formular korrekt vorbefüllt → Bestellung mit korrektem Schnappschuss in der DB), Admin sieht die Buchung, Preisstufe deaktivieren wirkt sofort auf die öffentliche Seite ohne sie zu löschen, reaktivieren stellt sie wieder her

## E-Mail-Benachrichtigungen für Auftrags-Ereignisse (07.08.2026) ✅
Nutzer-Wunsch: Kunden per E-Mail informieren, wenn das Team einen Preisvorschlag sendet, einen Gegenvorschlag annimmt/ablehnt, den Auftragsstatus ändert oder den Auftrag ablehnt.
- Neues Modul `lib/orderEmails.ts` mit vier Benachrichtigungen, gleicher Marken-Look wie die bestehenden Verifizierungs-/Reset-Mails (`emailShell`). DE/EN je nach Kundenspracheinstellung, Status-Bezeichnung wird direkt aus den bestehenden Übersetzungen gezogen (keine doppelte Pflege). Enthält immer einen Link zurück zum Auftrag
- Eingehängt in `adminProposePriceAction`, `adminRespondPriceAction`, `updateOrderTagAction`, `rejectOrderAction` — jeweils nach dem eigentlichen Speichern
- **Bewusst nach demselben Prinzip wie die Google-Calendar-Anbindung:** ein Mailversand-Fehler darf die eigentliche Aktion nie blockieren, wird nur geloggt (`safeSend`-Wrapper)
- Getestet: echten Preisvorschlag + echte Statusänderung über die Oberfläche ausgelöst, im Server-Log bestätigt, dass der Mailserver beide Male korrekt kontaktiert wurde (Ablehnung nur wegen absichtlich ungültiger Test-Adresse, SMTP-Verbindung/Auth selbst einwandfrei) — Auftrags-Speicherung lief in beiden Fällen unabhängig vom Mailausgang sauber durch
- **Kleine Selbstkorrektur:** beim Aufräumen der Testdaten ein zu breit gefasstes `deleteMany({})` auf der PriceProposal-Sammlung verwendet statt auf die Test-Bestellung einzugrenzen. Geprüft: betraf nur längst vorhandene Alt-Testaufträge ("Test"/"TEst" vom 03.–04.08.), keine echten Kundendaten (V2 ist noch nicht live) — trotzdem als Erinnerung für mich selbst notiert, Lösch-Skripte konsequent einzugrenzen

## Ablehnungsgründe befüllt + Kunden sehen den Grund jetzt auch (07.08.2026) ✅
`/admin/rejection-reasons` stand seit Einführung leer — Funktion existierte, aber ohne Inhalt nutzlos. 14 praxisnahe Gründe angelegt (kein freier Termin, zu kurzfristig, außerhalb Einzugsgebiet, Stil passt nicht, Preis passt nicht, unvollständige Angaben, Kunde antwortet nicht, keine TFP-Kapazität, Minderjährig ohne vollständige Zustimmung, Grundsätze, technische Anforderungen, Doppelanfrage, persönliche Gründe, Sonstiges).

**Auf Nutzer-Nachfrage nachgezogen:** erst als kurze Stichworte angelegt, dann auf Wunsch zu vollständigen, freundlichen Sätzen umgeschrieben ("Leider habe ich im gewünschten Zeitraum keinen freien Termin mehr." statt "Kein freier Termin"). Dabei einen echten, bis dahin unbemerkten Lücke gefunden: der Ablehnungsgrund wurde **nie an den Kunden angezeigt** — `ClientCancelOrderPanel` zeigte bei Admin-Ablehnung nur eine generische Meldung ohne Grund. Jetzt wird der Grund (plus admin-Notiz, falls vorhanden) auch auf der Kunden-Auftragsseite angezeigt. Getestet: Auftrag über die Oberfläche echt abgelehnt, per Kunden-Login bestätigt, dass der volle Satz plus Notiz ankommt.

## Social-Media-Links im Admin editierbar (07.08.2026) ✅
Bisher konnten `socialLinks` (Instagram, X/Twitter, TikTok, Twitch, YouTube, Discord, E-Mail) nur per DB-Skript gesetzt werden, keine Admin-Oberfläche dafür. Neues Formular auf `/admin/settings` unter der bestehenden `settings`-Berechtigung — wirkt sich sofort auf Footer und die strukturierten Daten (LocalBusiness `sameAs`) aus. Getestet: Formular zeigt echte aktuelle Werte korrekt vorbefüllt, Speichern-Rundlauf verändert die echten Daten nicht versehentlich.

## Lokales SEO für Bielefeld (07.08.2026) ✅
Nutzer-Wunsch: bei Google für Bielefeld möglichst weit oben erscheinen, "Bielefeld" auch auf der Seite selbst weit oben sichtbar machen.
- **Startseite:** Hero-Unterzeile (direkt unter der H1, ganz oben im ersten Sichtbereich) nennt jetzt "…in Bielefeld", ebenso Hero-Fließtext und "Über mich"-Text. Seiten-Tagline auf "Portrait · Cosplay · Events · Bielefeld" erweitert
- **Footer:** neue 📍-Zeile "Fotograf in Bielefeld" auf jeder Seite (NAP-Konsistenz — Name/Adresse/Ort passend zum späteren Google-Unternehmensprofil)
- **Title-Tag & Meta-Beschreibung der Startseite** enthalten jetzt Bielefeld (vorher nur "Rakku Photography" ohne Ortsbezug — das ist einer der stärksten On-Page-Rankingfaktoren für lokale Suche)
- **`LocalBusiness`-Schema (JSON-LD) erweitert:** echte Geokoordinaten (per Nominatim/OpenStreetMap aus der echten Geschäftsadresse ermittelt, nicht geraten) plus `areaServed` (Bielefeld + Ostwestfalen-Lippe) ergänzt — vorher fehlten beide komplett
- **`seoSettings.keywords` war bisher ein totes Feld** (im Admin editierbar, aber nie tatsächlich ins Meta-Tag geschrieben) — jetzt tatsächlich verdrahtet, plus mit Bielefeld-Suchbegriffen befüllt
- **Echter, während der Umsetzung gefundener Bug:** `defaultMetaDescription`/`keywords` waren im Schema nur einsprachig — nachdem ich sie befüllt hatte, zeigte die englische Startseite plötzlich die deutsche Beschreibung. Nach dem etablierten DE/EN-Muster (wie bei FAQ-Einträgen) behoben: neue Felder `defaultMetaDescriptionEn`/`keywordsEn` im Schema + Admin-Formular + `buildPageMetadata`, beide Sprachen jetzt korrekt getrennt (verifiziert per curl gegen beide Live-Seiten)
- **Neuer FAQ-Eintrag** "Bietest du Shootings auch außerhalb von Bielefeld an?" — nützlicher Content, der zusätzlich auf lokale Umgebungssuchen einzahlt
- **Wichtige Einschränkung, dem Nutzer klar kommuniziert:** das hier ist alles On-Page-SEO — der mit Abstand größte Hebel für "oben bei Google" bei lokalen Suchen wie "Fotograf Bielefeld" ist ein **Google-Unternehmensprofil** (zeigt in der Maps-/Local-Pack-Box, die meist über den normalen Suchergebnissen steht) — das kann ich nicht selbst anlegen/verifizieren, das muss der Nutzer selbst einrichten. Genauso Google Search Console/Bing Webmaster Tools zur Verifizierung (Felder existieren schon im Admin-SEO-Formular, aber die Codes muss der Nutzer selbst von dort holen). Auch: `SITE_URL` zeigt aktuell noch auf die Vorschau-Domain `v2.rakku.de`, nicht `rakku.de` — muss beim eigentlichen Go-Live berücksichtigt werden, da Google erst ab dann auf der richtigen Domain zu ranken beginnt

## Google Calendar — Bugfix (fehlendes Zielfeld) + 2-Stunden-Cronjob als Sicherheitsnetz (07.08.2026) ✅
Nutzer meldete: Termin nach Verbinden trotzdem nicht in Google. Root Cause gefunden: der lazy angelegte "Aufträge"-Kalender entstand vor Einführung der Google-Felder, ihm fehlte dadurch `googleCalendarId` (kein Schema-Default bei `$setOnInsert` ohne `setDefaultsOnInsert`) — Google antwortete mit 404, aber der Push-Code loggt Fehler nur, statt sie zu werfen (bewusst, damit Google-Probleme nie das normale Speichern blockieren), daher blieb es unbemerkt bis zum Live-Test. Sowohl Code (`getOrCreateOrdersCalendar`) als auch das bestehende Datenbank-Dokument direkt repariert, betroffenen Termin sofort nachträglich übertragen (verifiziert: Push liefert jetzt `200` von der echten Google-API).
- **Neuer Cronjob** (System-Crontab, alle 2 Stunden: `0 */2 * * *`) ruft `/api/cron/google-sync?secret=…` auf (Secret in `.env.local` als `CRON_SECRET`, kein Nutzer-Login nötig) — holt bei allen verbundenen Kalendern liegengebliebene Termine automatisch nach. Sync-Kernlogik dafür aus `lib/actions/calendars.ts` in ein neues, nicht "use server"-markiertes Modul `lib/googleSync.ts` ausgelagert (keine eigene Berechtigungsprüfung mehr nötig, da Aufrufer — Admin-UI-Action mit Session-Prüfung, oder Cron-Route mit Secret-Prüfung — das jeweils selbst absichert).
- Zusätzlich manueller **"Jetzt synchronisieren"**-Link direkt in der Kalender-Seitenleiste neben "Trennen", falls man nicht bis zum nächsten Cron-Lauf warten will.

## Google Calendar — echte OAuth-Push-Sync (07.08.2026) ✅
Nutzer hat sich für die volle Variante entschieden und selbst ein Google-Cloud-Projekt ("Rakku Photography", ID `rakku-photography`) mit OAuth-Client angelegt, Zugangsdaten per Chat übermittelt. In `.env.local` als `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` hinterlegt.
- **Einseitige Push-Sync pro Kalender** (unsere Termine → Google, wie besprochen — keine Rückrichtung). Eigener schlanker REST-Client gegen `oauth2.googleapis.com` + `www.googleapis.com/calendar/v3` per `fetch`, bewusst ohne die `googleapis`-Bibliothek als zusätzliche Abhängigkeit (`lib/googleCalendar.ts`).
- **OAuth-Flow mit CSRF-Schutz:** `/api/auth/google/connect?calendarId=…` prüft Berechtigung/Besitz, setzt einen kurzlebigen signierten State-Wert als HttpOnly-Cookie, leitet zu Google weiter (`access_type=offline`, `prompt=consent` erzwingt jedes Mal einen Refresh-Token). `/api/auth/google/callback` gleicht State gegen das Cookie ab, tauscht den Code gegen Access-/Refresh-Token, speichert sie am jeweiligen `Calendar`-Dokument.
- **Automatische Token-Erneuerung:** `ensureValidAccessToken()` prüft Ablauf vor jedem Push und erneuert bei Bedarf selbstständig über den Refresh-Token — kein manuelles Neuverbinden nötig, solange die Verbindung nicht per "Trennen" aufgehoben wird.
- Bei jedem Anlegen/Löschen eines Termins (eigene Kalender) bzw. Setzen/Entfernen eines Auftrags-Termins ("Aufträge"-Systemkalender) wird — sofern der jeweilige Kalender verbunden ist — automatisch bei Google mitgeschrieben; Google-Event-ID wird am jeweiligen Datensatz gespeichert für spätere Updates. **Fehler bei Google blockieren nie das eigentliche Speichern bei uns** (nur geloggt) — bewusste Entscheidung, damit eine kaputte Google-Verbindung nie den normalen Betrieb stört.
- "Mit Google verbinden"/"Trennen" direkt in der Kalender-Seitenleiste, nur für Besitzer bzw. bei "Aufträge" für alle mit `orders`-Recht sichtbar.
- Automatisiert getestet: Connect-Route leitet mit korrekten Parametern zu echtem `accounts.google.com` weiter (Client-ID, Redirect-URI, Scope, `access_type=offline`), nicht eingeloggt → Login statt Google, State-Cookie wird gesetzt, Callback mit falschem/fehlendem State scheitert sauber ohne Absturz, bestehende Kalenderfunktionen laufen unverändert weiter (Push no-opt sauber, wenn (noch) nicht verbunden). **Der eigentliche Consent-Klick + Login bei Google selbst kann nicht automatisiert getestet werden** (bräuchte Tills echten Google-Login) — das muss er einmal live ausprobieren: auf "Mit Google verbinden" klicken, mit seinem Google-Konto einloggen, danach einen Test-Termin anlegen und prüfen, ob er in Google Calendar ankommt.

## Mehrere Kalender mit Berechtigungen + Google/Apple/Outlook-Abo (06.08.2026) ✅
Nachfrage zum eben gebauten Kalender: „mehrere Kalender... wie bei Google... auch mit Berechtigungen... bei Google reintun... automatisch synchronisiert". Echte beidseitige Google-OAuth-Sync würde ein Google-Cloud-Projekt mit Zugangsdaten vom Nutzer voraussetzen (nicht selbst anlegbar) — stattdessen entschieden: **abonnierbare ICS-Kalenderlinks**, die Google/Apple/Outlook selbstständig regelmäßig neu abrufen. Erfüllt „reintun" + „automatisch synchronisiert" ohne jede Google-Zugangsdaten; Nutzer wurde die Alternative (volle OAuth) transparent genannt, falls später gewünscht.
- **Neue Modelle `Calendar` + `CalendarEvent`.** Der bisherige, aus Aufträgen abgeleitete Kalender wurde zum automatisch angelegten Systemkalender „Aufträge" (kind: `orders`, nicht löschbar, Termine weiterhin nur über die Auftragsseite setzbar). Zusätzlich können Mitarbeiter mit der neuen `calendars`-Berechtigung eigene Kalender anlegen (z. B. „Urlaub", „Team-Termine") — mit eigenem Namen, Farbe und Sichtbarkeit **privat** (nur Ersteller + gezielt geteilte Kollegen) oder **Team** (alle mit `calendars`-Recht sehen mit, bearbeiten dürfen weiterhin nur Ersteller + geteilte Personen — bewusst zwei getrennte Stufen, sehen vs. bearbeiten).
- **`/admin/calendar` komplett überarbeitet:** linke Seitenleiste listet alle sichtbaren Kalender mit Ein/Aus-Kästchen (Zustand in der URL, `?cals=…`, kein Server-Rendering-Bruch), Monatsraster rechts zeigt alle eingeschalteten Kalender überlagert farbcodiert, „Nächste Termine"-Liste ebenfalls kalenderübergreifend.
- **ICS-Feed pro Kalender:** neue Route `/api/calendar/[calendarId]/feed.ics?token=…`, öffentlich per Geheim-Token abrufbar (kein Login — genau das Modell, das Kalender-Abos brauchen), eigener kleiner iCalendar-Generator (`lib/ics.ts`, RFC 5545, keine externe Bibliothek). Link direkt in der Seitenleiste mit Kopieren-Button, Token bei Bedarf neu erzeugbar (z. B. falls der Link versehentlich weitergegeben wurde).
- Getestet mit vier Testkonten unterschiedlicher Rechte (voller Zugriff, „nur calendars"-Kollege, „nur calendars"-Außenstehender, gar keine Rechte): privater Kalender ist für den Außenstehenden unsichtbar, wird nach gezieltem Teilen für den Kollegen sichtbar (inkl. der Termine darin), ICS-Feed liefert korrekten Inhalt mit richtigem Token und 404 mit falschem, Kalender-Ein/Ausblenden wirkt sofort auf Raster + Liste, Termin- und Kalender-Löschen funktionieren. Mitarbeiter ganz ohne `orders`/`calendars` wird von der Seite komplett weggeleitet.

## Kalenderansicht für Shootings (06.08.2026) ✅
Aus der Ideen-Liste ausgewählt: „Kalender wäre doch ganz gut". Neues Feld `Order.shootDate` (echter, strukturierter Zeitpunkt) ergänzt — bewusst getrennt vom bereits bestehenden `datePeriod` (Freitext-Wunschzeitraum des Kunden bei der Anfrage). Admin setzt den Termin auf der Auftragsseite über ein neues „Termin"-Feld (`ShootDatePanel`, datetime-local-Eingabe). Neue Seite `/admin/calendar` (unter der bestehenden `orders`-Berechtigung, keine neue Berechtigung nötig): waagerechte Monatsübersicht (Mo–So, angrenzende Monate abgeblendet, heutiger Tag hervorgehoben) mit farbcodierten Auftrags-Kürzeln pro Tag (Zeit + Kundenname, verlinkt zur Auftragsseite), Monats-Navigation über einfache Links (kein Client-JS nötig, wie schon bei der bestehenden Pagination), darunter zusätzlich eine chronologische „Nächste Termine"-Liste unabhängig vom gerade angezeigten Monat. Getestet: Termin setzen erscheint korrekt im Raster und in der Liste, Entfernen lässt ihn wieder verschwinden, Monatsnavigation crasht nicht, Mitarbeiter ohne `orders`-Berechtigung wird sauber weitergeleitet.

## Rabattcodes, Hinweis-Banner, Kundenkonten-Verwaltung (06.08.2026) ✅
Nutzer-Wunschliste in einer Nachricht: Rabattaktionen mit eigener Verwaltungsseite (Benutzungslimit + Gültigkeitsdauer, optional nur für Stammkunden), ein aktivierbarer Hinweis-Banner ganz oben auf jeder Seite, und die Möglichkeit, Kundenkonten ein Passwort zu setzen sowie ein Unternehmensfeld einzutragen — **explizit mit der Vorgabe, dass nicht jeder Mitarbeiter Passwörter setzen können darf**.

- **Rabattcodes:** neues Model `DiscountCode` (Prozent oder fester Betrag, `maxUses`, `validUntil`, `stammkundenOnly`, `active`), neue Seite `/admin/discounts` (eigene Berechtigung `discounts`). Kunden lösen den Code selbst auf ihrer Auftragsseite ein, sobald mindestens ein Preisvorschlag existiert — der Rabatt wird als Schnappschuss (`Order.discountAmount`) gespeichert, nicht bei jeder Anzeige neu berechnet. Bewusst **nicht** in die Preisverhandlung (`PriceProposal`) selbst eingegriffen, sondern als Abzug obendrauf behandelt — Verhandlungsverlauf bleibt so unverfälscht nachvollziehbar
- **Rabatt korrekt bis in alle Geldflüsse durchgezogen** (das war der heikelste Teil, da bereits echtes Stripe/Rechnungs-/EÜR-System dranhängt): Stripe-Checkout-Betrag, Rechnungserstellung (`netAmount`/`totalAmount` inkl. sichtbarer Hinweiszeile auf der PDF-Rechnung), Umsatz-Statistik und die „Umsatz"-Kachel auf der Kundenakte — alle nutzen jetzt denselben Helfer `getPayableAmount()` statt des rohen Verhandlungsbetrags. EÜR-Export brauchte keine Änderung, da der schon auf `Invoice.totalAmount` basiert (automatisch korrekt)
- **Hinweis-Banner:** neues `Settings.bannerSettings` (an/aus, Text DE/EN), schmaler fixierter Balken über der Navigation auf jeder Seite, Kopfzeile weicht bei aktivem Banner automatisch nach unten aus (Höhe fest in `BANNER_HEIGHT` definiert, an drei Stellen konsistent verwendet: Banner selbst, Header-Offset, Content-Padding). Bewusst **nicht** nutzerseitig wegklickbar gemacht — der Admin-Schalter selbst ist der „Aus"-Knopf, das hält die Umsetzung einfach
- **Kundenkonten-Verwaltung:** neue, bewusst **von der normalen `clients`-Berechtigung getrennte** Berechtigung `client_accounts` — auf der Kundenakte ein neues Formular „Kundenkonto verwalten" (Unternehmen eintragen, neues Passwort setzen), nur sichtbar/wirksam mit dieser engeren Berechtigung. Direkte Nutzer-Vorgabe, siehe [[feedback_client_password_permission_gate]]
- Getestet per Headless-Browser mit drei Testkonten (Vollzugriff, ein Mitarbeiter ganz ohne Berechtigungen, ein Kunde): Rabattcode anlegen/einlösen/entfernen, abgelaufener Code wird abgelehnt, Umsatz vor/nach Einlösung und nach Entfernen korrekt (200,00 € → 190,00 € → 200,00 €), eingeschränkter Mitarbeiter sieht weder Rabatt- noch Kundenkonto-Verwaltung und wird sauber weitergeleitet, Banner erscheint/verschwindet korrekt inkl. Kopfzeilen-Verschiebung, per Admin gesetztes neues Passwort funktioniert beim Login. Ein eigener Fehler in den Testdaten (ungültiger `shootingType`-Enum-Wert direkt in die DB geschrieben) hätte fast als Produktbug durchgehen können — beim Debuggen richtiggestellt, kein echter Bug im Code

## Berechtigungen auf alle Admin-Bereiche ausgeweitet (05.08.2026) ✅
Nutzerin hat beim Audit gezielt nachgefragt: „Berechtigungen greifen erst bei 3 von 10 Admin-Bereichen — kannst du das richtig machen?" Komplett nachgezogen, nicht nur oberflächlich.
- **Alle 26 Admin-Server-Actions** in `admin.ts` sowie die Actions in `expenses.ts` und `payments.ts` von der pauschalen „ist überhaupt Mitarbeiter"-Prüfung (`isAdminSession()`) auf die passende feine Berechtigung umgestellt: `orders` (Aufträge, Verträge, Fotofreigabe, Angebote, Ablehnungsgründe), `finance` (Zahlungsstatus, Rechnungen, Mahnungen, Ausgaben), `settings` (Rechnungs-/Geschäftseinstellungen)
- **Alle 12 Admin-Seiten** entsprechend gated, inkl. der bereits vorher gebauten (Team, Dokumente, QM)
- **8 authentifizierte Datei-Download-Routen** (Auftrags-Anhänge, Kundenfotos, Verträge, Rechnungen, Mahnungen, Beleg-Uploads, Nextcloud-Ordner-Browser) ebenfalls von `isAdminSession()` auf die passende Berechtigung umgestellt
- **Echter, vorher unentdeckter Bug gefunden und behoben:** `updateOrderTagAction` (Kanban-Status eines Auftrags ändern) hatte überhaupt **keine** Berechtigungsprüfung — jeder eingeloggte Kunde hätte theoretisch den Status fremder Aufträge ändern können, wenn er die Server-Action direkt angesprochen hätte. Jetzt mit `orders`-Berechtigung abgesichert
- **Zweiter, selbst verursachter Bug beim Umbau gefunden und sofort behoben:** die ersten Versionen der Berechtigungs-Weiterleitungen (Team/Dokumente/QM, ursprünglich am 04./05.08. gebaut) leiteten bei fehlender Berechtigung auf `/de/admin/orders` weiter — das führt jetzt aber selbst eine Berechtigungsprüfung durch. Ein Mitarbeiter ganz ohne „Aufträge"-Recht wäre in eine **Endlosschleife** gelaufen. Alle Weiterleitungen zeigen jetzt auf `/de/account` (immer erreichbar, keine Berechtigung nötig)
- **Auftragsdetailseite differenziert jetzt zusätzlich innerhalb der Seite:** wer `orders`, aber nicht `finance` hat, sieht den Auftrag ganz normal, aber die Zahlungsstatus-, Rechnungs- und Mahnungs-Kästen bleiben unsichtbar (keine Buttons, die bei Klick wirkungslos wären)
- **`/account`-Schnellzugriffsleiste zeigt jetzt nur noch Links, die tatsächlich funktionieren** — vorher wurden alle zehn Admin-Links immer angezeigt, unabhängig von der eigenen Berechtigung
- **Sicherheitsnetz bleibt bestehen:** ein Mitarbeiter ganz ohne zugewiesenen Rang gilt weiterhin als voll berechtigt (verhindert versehentliches Aussperren), erst ein zugewiesener Rang grenzt tatsächlich ein
- Getestet mit einem eigens angelegten Testkonto mit einem Rang, der **nur** die `orders`-Berechtigung hat: konnte Aufträge/Angebote sehen und bearbeiten, wurde aber von allen anderen 7 Admin-Bereichen sauber auf `/de/account` umgeleitet (keine Schleife), die Zahlungs-/Rechnungs-Kästen auf der Auftragsseite blieben unsichtbar (per echtem sichtbaren-Text-Check verifiziert, nicht nur HTML-Substring-Suche), die Schnellzugriffsleiste zeigte nur die erlaubten Links. Parallel verifiziert, dass Rakkus eigener Account (alle 7 Berechtigungen) weiterhin uneingeschränkten Zugriff auf alles hat — keine Regression
- **Nachgezogen auf Nutzer-Nachfrage (05.08.2026):** „Wenn jemand kein Zugriff hat, sollte die Person es doch auch nicht sehen" — zurecht. Die Pill-Navigation (`AdminNavLinks`) filtert jetzt ebenfalls nach Berechtigung: neuer Sammel-Helfer `getAllPermissions()` (alle 7 Berechtigungen in einer Abfrage statt sieben Einzelabfragen), an allen 16 Stellen ergänzt, wo die Navigation eingebunden wird. Ein Mitarbeiter mit nur „Aufträge"-Recht sieht dort jetzt wirklich nur noch Aufträge/Angebote/Ablehnungsgründe, nichts anderes — getestet mit demselben eingeschränkten Testkonto wie oben (3 sichtbare Links statt 10, Rakkus voller Account weiterhin alle 10)

## Personalmanagement (Personalakte) + Qualitätsmanagement (05.08.2026) ✅
Nutzerin hat mir die weitere Priorisierung überlassen ("du entscheidest, womit es logisch am besten passt") — beide restlichen Punkte aus ihrer ursprünglichen Liste jetzt gebaut, beide bauen auf dem frisch fertigen Team-System + Dokumenten-Creator auf.

**Personalmanagement:**
- Neue **Personalakte pro Mitarbeiter** (`/admin/team/[clientId]`, verlinkt vom Mitarbeiternamen auf `/admin/team`): zeigt Abteilung/Position/Rang/Teams sowie **alle für diese Person erstellten Dokumente** (aus dem Dokumenten-Creator) an einem Ort
- Neues Feld `employmentStartDate` (Beschäftigt seit) am Client-Model, direkt auf der Personalakte editierbar
- Der Dokumenten-Creator kann jetzt zusätzlich `{{startDate}}` automatisch aus diesem Feld befüllen (neben den bereits bestehenden `name`/`email`/`department`/`position`/`rank`/`address`)

**Qualitätsmanagement:**
- Neue Seite **`/account/qm`**: jeder Mitarbeiter (nicht nur wer eine bestimmte Berechtigung hat) kann dort einen QM-Vorgang einreichen (Titel, Beschreibung, optionaler Anhang als PDF/Bild), sieht den Status der eigenen Einreichungen (In Prüfung/Freigegeben/Abgelehnt) inkl. Rückmeldung
- Neue Seite **`/admin/qm`**: Übersicht aller Einreichungen (offen zuerst), pro Vorgang freigeben/ablehnen mit optionaler Rückmeldung, gated über die neue `qm`-Berechtigung im Team-/Rechtesystem — das Einreichen selbst ist bewusst nicht rechte-eingeschränkt (jeder Mitarbeiter darf einreichen, nur das Prüfen ist eingeschränkt)
- Referenz-Vorlagen aus dem Dokumenten-Creator mit Kategorie „Qualitätsmanagement" werden zur Übersicht mit angezeigt (Checklisten-Vorlagen lassen sich also weiterhin zentral über `/admin/documents` pflegen, QM-Seite verlinkt nur dorthin)
- Neue Berechtigung `qm` im Team-/Rechtesystem ergänzt; Rakkus „Management"-Rang nachträglich um `documents` und `qm` erweitert (fehlten beim ursprünglichen Team-System-Setup, da diese Berechtigungen erst mit den jeweiligen Folge-Features entstanden sind)
- Getestet: kompletter Ablauf per Headless-Browser (Personalakte lädt mit Dokumenten-Liste, Beschäftigt-seit-Datum speichern, `{{startDate}}`-Token wird korrekt automatisch befüllt, QM-Einreichung als Mitarbeiter, QM-Freigabe als Admin inkl. Rückmeldung, Mitarbeiter sieht die Rückmeldung danach)

**Damit sind alle fünf ursprünglich vom Nutzer genannten fehlenden Bausteine (Team-/Rechtesystem, EÜR-Export, Qualitätsmanagement, Dokumenten-Creator, Personalmanagement) mindestens in einer ersten, funktionsfähigen Version umgesetzt, inklusive vollständiger Berechtigungsdurchsetzung über alle Admin-Bereiche (05.08.2026, siehe Eintrag oben).** Echte Rechtstexte (Arbeitsvertrag/-zeugnis) bleiben bewusst offen (siehe jeweilige Einträge) — das ist eine inhaltliche, keine technische Aufgabe.

## Dokumenten-Creator (04.08.2026) ✅
Eigenständig entschieden weiterzumachen (Nutzerin: „du entscheidest, womit es logisch für das System am besten passt") — Dokumenten-Creator zuerst gebaut, weil sowohl Personalmanagement als auch Qualitätsmanagement laut Plan darauf aufbauen sollen; erst die gemeinsame Engine, dann die Einzelbereiche, statt zweimal dieselbe Logik zu bauen.
- **Neue Seite `/admin/documents`:** eigene Vorlagen mit Platzhaltern im Format `{{token}}` anlegen/bearbeiten/löschen, Kategorien (Arbeitsvertrag, Arbeitszeugnis, **Ausbildungsvertrag, Ausbildungszeugnis** — wie vom Nutzer als Pflicht vorgegeben, Qualitätsmanagement, Sonstiges), daraus für einen bestimmten Mitarbeiter oder allgemein ein fertiges PDF-Dokument erzeugen
- **Bewusste Design-Entscheidung — kein selbst erfundener Rechtstext:** ich habe keine eigenen Arbeitsvertrags-/Arbeitszeugnis-Klauseln vorformuliert (anders als z. B. beim TFP-Vertrag, wo ein Ausgangstext ohnehin vom Anwalt gegengecheckt werden sollte). Arbeitszeugnisse insbesondere haben in Deutschland eine eigene, rechtlich bedeutsame „Zeugnissprache" (wohlwollende Formulierungspflicht) — das sollte von einem echten Vorlagentext (z. B. vom Steuerberater/Anwalt/einer Fachvorlage) kommen, den Rakku selbst einträgt. Die Engine liefert nur das Werkzeug (Platzhalter, PDF-Erzeugung, Kategorisierung), keinen Inhalt
- **Automatische Platzhalter-Befüllung:** wählt man beim Dokument-Erstellen einen Mitarbeiter aus, werden bekannte Platzhalter (`name`, `email`, `department`, `position`, `rank`, `address`) automatisch aus dessen Profil vorbefüllt (nutzt das neue Team-/Rechtesystem) — alle anderen selbst erfundenen Platzhalter (z. B. `{{startDate}}`, `{{note}}`) werden als leeres Textfeld zum manuellen Ausfüllen angezeigt
- **Jedes erstellte Dokument wird als Schnappschuss gespeichert** (ausgefüllter Text zum Erstellungszeitpunkt, nicht nur ein Verweis auf die Vorlage) — ändert sich die Vorlage später, bleiben bereits ausgestellte Dokumente unverändert, genau wie bei Rechnungen/Verträgen. Verlauf der letzten 50 erstellten Dokumente mit Download-Link direkt auf der Seite
- Über die neue `documents`-Berechtigung im Team-/Rechtesystem geschützt (nicht nur `isAdminSession()`) — Rakkus „Management"-Rang wurde entsprechend nachträglich ergänzt (die Berechtigung existierte beim ursprünglichen Team-System-Setup noch nicht)
- Getestet: Vorlage mit mehreren Platzhaltern angelegt, Dokument für einen Mitarbeiter erstellt (automatische + manuelle Platzhalter-Befüllung geprüft), PDF-Inhalt per `pdftotext` verifiziert (korrekt ausgefüllter Text), Verlaufsliste zeigt das Dokument

## EÜR-Export (04.08.2026) ✅
Zweiter der beiden vom Nutzer priorisierten Punkte, Scope bewusst eng gehalten (nur EÜR-Export, keine echte Steuererklärung im System, da rechtliche Tragweite).
- Neuer Button „Als CSV herunterladen" mit Jahresauswahl in der Statistik-Seite (Abschnitt „Einnahmen & Ausgaben"), neue Route `/api/admin/euer-export?year=…`
- **Nach dem Zufluss-/Abfluss-Prinzip (Ist-Besteuerung)** aufgebaut, wie bei einer echten EÜR üblich: Einnahmen zählen zum Zeitpunkt des tatsächlichen Zahlungseingangs (`Order.paidAt`), nicht wenn der Preis vereinbart oder die Rechnung gestellt wurde — bewusst anders als die bestehende Umsatz-Statistik (die zählt nach Vereinbarungsdatum). Ausgaben zählen an ihrem erfassten Datum
- CSV mit Jahres-Zusammenfassung oben (Einnahmen/Ausgaben/Gewinn) plus zwei Detail-Tabellen (alle Einnahmen mit Rechnungsnummer, alle Ausgaben mit Kategorie + enthaltener USt.) — UTF-8 mit BOM, damit Umlaute in Excel korrekt ankommen
- Gedacht als saubere Zahlengrundlage zum Weitergeben an Steuerberater/für ELSTER — keine offiziellen Anlage-EÜR-Kennziffern nachgebaut, um dort nichts Rechtlich-Falsches vorzugeben
- Gated über die neue `finance`-Berechtigung aus dem Team-/Rechtesystem (siehe unten) statt nur `isAdminSession()` — da neu gebaute Funktionalität, kein Risiko für bestehenden Zugriff
- Getestet: echter Ablauf (Auftrag als bezahlt markiert, Ausgabe erfasst, CSV heruntergeladen und Inhalt geprüft — Summen, Zeilen, BOM alle korrekt)

## Team- & Rechtesystem — Phase 6 gestartet (04.08.2026) ✅
Nutzer hat bemerkt, dass mehrere aus Phase 6 angekündigte Bausteine noch komplett fehlten (Abteilungen/Positionen/Ränge/Teams als echte Entitäten, Steuererklärung, Qualitätsmanagement, Dokumenten-Creator, Personalmanagement) und wollte priorisieren. Entscheidung: **Team-/Rechtesystem zuerst** (Fundament für Personalmanagement + QM), Steuererklärung als **nur EÜR-Export** (kein echtes Steuererklärungs-Tool — bewusst so eng gehalten, hat rechtliche Tragweite).
- **Vier neue, vom Nutzer selbst verwaltbare Entitäten** (`/admin/team`): **Abteilungen**, **Positionen** (je einer Abteilung zugeordnet), **Ränge** (mit Stufe/Level, „im Live-Chat anzeigen"-Schalter, und einer Liste an Berechtigungen), **Teams** (mit Farbe) — alles frei anlegbar/löschbar, kein Freitext mehr
- **Client-Model umgebaut:** `department`/`position` (Freitext) ersetzt durch `departmentId`/`positionId`/`rankId`/`teamIds` (echte Referenzen). Da noch keine echten Daten drinstanden, ohne Datenverlust möglich
- **Mitarbeiter-Verwaltung neu:** bisher gab es dafür überhaupt keine Oberfläche (nur einmalige DB-Skripte) — jetzt auf der Kundenakte ein „Zum Mitarbeiter machen"-Button, und auf `/admin/team` pro Mitarbeiter Abteilung/Position/Rang/Teams zuweisen
- **Erster echter Berechtigungs-Baustein:** fünf grobe Berechtigungen (Aufträge, Kunden, Finanzen, Einstellungen, Team-Verwaltung), pro Rang einstellbar. `hasPermission()`-Helfer prüft das serverseitig — aktuell nur für `/admin/team` selbst scharf geschaltet (Berechtigung „Team-Verwaltung"), der Rest der Admin-Seiten bleibt vorerst beim bisherigen groben Staff-Ja/Nein (`isAdminSession()`), um nicht in einem Rutsch alles umzubauen und dabei versehentlich jemanden auszusperren. **Wichtig zur Einordnung: das ist der Anfang eines größeren, laufenden Ausbaus, nicht die fertige Lösung** — genau wie vom Nutzer selbst beschrieben ("muss noch viel weiter ausgebaut werden")
- **Sicherheitsnetz eingebaut:** Mitarbeiter ohne zugewiesenen Rang gelten als voll berechtigt (nicht eingeschränkt) — verhindert, dass sich jemand versehentlich selbst aussperrt, bevor Ränge sauber vergeben sind
- **Live-Chat zeigt jetzt** (bei Mitarbeiter-Nachrichten): Rang-Name (nur wenn der jeweilige Rang auf „sichtbar" steht) + Position + Abteilung — genau wie vom Nutzer gewünscht ("z. B. Management angezeigt wird")
- **Migration:** Rakkus eigener Account wurde direkt mit echten Startdaten befüllt (Abteilung „Geschäftsführung", Position „Inhaberin", Rang „Management" mit allen fünf Berechtigungen + Live-Chat-Sichtbarkeit, Team „Kernteam") — sie steht damit nicht vor einem leeren System
- Getestet: kompletter Ablauf per Headless-Browser (Seite lädt mit den Migrationsdaten, neue Abteilung anlegbar, Profilseite zeigt aufgelöste Namen statt IDs, Chat-Nachricht zeigt „Management" als Titel)
- **Noch offen für später (bewusst nicht in diesem Schritt):** Berechtigungen auf weitere Admin-Bereiche ausweiten (aktuell nur Team-Verwaltung selbst gated), individuelle Rechte-Overrides pro Person (aktuell nur über den Rang), Dokumenten-Creator/Personalmanagement/QM bauen auf diesem Fundament auf

## Direktes Bezahlen über die Seite — Stripe (04.08.2026) ✅ Code fertig, Konto-Setup steht noch aus
Letzter offener Punkt aus Phase 4. Nutzer wollte PayPal + Sofortüberweisung + Kreditkarte (Visa etc.) — technisch läuft das am saubersten über einen Anbieter, der alle drei bündelt, statt drei Einzel-Integrationen. Empfehlung war Mollie oder Stripe, Nutzer hat sich für **Stripe** entschieden.
- **Wichtige Info dazu:** Stripe hat „Sofortüberweisung" als eigenständige Zahlungsart zum 31.03.2025 eingestellt und in **Klarna** überführt — Klarna übernimmt exakt diese Funktion (direkte Banküberweisung) im Stripe-Checkout. PayPal und Karten (Visa/Mastercard) sind ebenfalls über Stripe Checkout verfügbar
- **Ganz bewusst wie vom Nutzer gefordert (04.08.2026 bestätigt): jederzeit über einen Schalter im Admin-Dashboard deaktivierbar** (`/admin/settings`, „Direktes Bezahlen aktivieren") — der Schalter existiert unabhängig davon, ob Stripe überhaupt eingerichtet ist, und lässt sich nur aktivieren, wenn ein echter Stripe-Schlüssel hinterlegt ist (siehe unten)
- **Welche Zahlungsmethoden genau angezeigt werden (Karte/PayPal/Klarna), stellt Rakku direkt im eigenen Stripe-Dashboard ein** — im Code wird bewusst keine feste Methodenliste vorgegeben, damit sie das selbst steuern kann, ohne dass dafür Code geändert werden muss
- Technischer Ablauf: Kunde klickt „Jetzt online bezahlen" auf der eigenen Auftragsseite (nur sichtbar, wenn Preis final vereinbart, Auftrag noch nicht bezahlt, und die Funktion aktiv ist) → Stripe Checkout-Session wird serverseitig erzeugt → Kunde bezahlt bei Stripe → Stripe bestätigt per Webhook (`/api/stripe/webhook`) → Auftrag wird automatisch als bezahlt markiert (Zahlungsart „Online-Zahlung"), inkl. Aktivitäts-Log-Eintrag — **das ist die einzige Stelle im ganzen Zahlungssystem, an der ein Auftrag automatisch als bezahlt markiert wird**, weil hier (anders als bei Überweisung) eine echte, geprüfte Zahlungsbestätigung von Stripe selbst vorliegt
- **Noch offen — braucht Rakku selbst, kann ich nicht für sie erledigen:**
  1. Ein Stripe-Konto anlegen (falls noch nicht vorhanden) unter stripe.com, Geschäftsdaten/Bankverbindung hinterlegen
  2. Im Stripe-Dashboard unter „Zahlungsmethoden" Karte, PayPal und Klarna aktivieren
  3. Mir den **Secret Key** (`sk_live_...` bzw. erst `sk_test_...` zum Testen) geben, den ich als `STRIPE_SECRET_KEY` in `.env.local` hinterlege
  4. Im Stripe-Dashboard unter „Webhooks" einen Endpunkt auf `https://v2.rakku.de/api/stripe/webhook` für das Event `checkout.session.completed` anlegen, den daraus generierten **Signing Secret** (`whsec_...`) ebenfalls an mich geben (als `STRIPE_WEBHOOK_SECRET`)
  5. Danach kann ich einmal einen echten Test-Zahlungsdurchlauf machen (mit Stripes Testkarten, kein echtes Geld) und den Schalter für sie freigeben
- Bis diese Schlüssel hinterlegt sind, bleibt die Funktion **komplett unsichtbar** für Kunden (fail-safe getestet: selbst wenn der Schalter in der Datenbank versehentlich auf „an" stehen würde, erscheint der Button nicht ohne echten Stripe-Schlüssel)
- Getestet (das, was ohne echtes Stripe-Konto testbar ist): Einstellungsseite zeigt den Schalter + korrekten „noch nicht eingerichtet"-Hinweis, Schalter lässt sich ohne Schlüssel nicht aktivieren, Bezahlen-Button bleibt in jedem Fall unsichtbar ohne Schlüssel (auch bei erzwungenem Datenbank-Wert, per echtem Playwright-DOM-Check verifiziert, nicht nur Text-Suche)

## Phase 5 — Interne Buchhaltung, erste Version (04.08.2026) ✅
Nutzer wollte den Zahlungsanbieter fürs Online-Bezahlen noch nicht festlegen (siehe „Direktes Bezahlen" im Backlog unten) — stattdessen mit Phase 5 weitergemacht, die dafür ohnehin als Nächstes anstand:
- **Neue Admin-Seite `/admin/expenses`:** Ausgaben erfassen (Datum, Kategorie, Beschreibung, Betrag brutto, optional enthaltene USt., optionaler Beleg-Upload als PDF/Bild), Liste aller Ausgaben mit Lösch-Funktion, drei Stat-Cards (Gesamt/Diesen Monat/Darin USt.)
- **Feste Kategorien** (Ausrüstung, Software & Abos, Marketing, Reisekosten, Miete/Studio, Versicherung, Fortbildung, Sonstiges) statt frei anlegbarer Kategorien — bewusst einfach gehalten, lässt sich bei Bedarf später zu einem admin-verwaltbaren System ausbauen (analog zu den Ablehnungsgründen)
- **Belege** landen wie Auftrags-Anhänge/Avatare in `private-uploads/receipts/<expenseId>/`, Auslieferung nur für Mitarbeiter über eine authentifizierte Route (`/api/receipts/[expenseId]/[filename]`) — Ausgaben sind Finanzdaten, nie öffentlich abrufbar
- **Neue Statistik-Section „Einnahmen & Ausgaben"**: Gesamt-Einnahmen (aus bereits bestehender Umsatz-Berechnung) vs. Gesamt-Ausgaben vs. Gewinn, dazu eine Monatstabelle mit Einnahmen/Ausgaben/Gewinn nebeneinander (rot markiert bei Verlust) — nutzt dieselbe Monats-Aggregation, die es für den Umsatz schon gab
- USt-Vorbereitung bewusst minimal gehalten (nur das optionale USt.-Feld pro Ausgabe, aufsummiert) — passend dazu, dass Rakku aktuell Kleinunternehmerin ist (kein Vorsteuerabzug möglich); reicht als Grundlage, falls sich der Status später ändert
- Getestet: kompletter Ablauf per Headless-Browser (Ausgabe mit Beleg anlegen, Beleg-Download funktioniert, Beträge/Kategorie korrekt angezeigt, Statistik-Section zeigt korrekte Summen, Löschen funktioniert inkl. Datei-Aufräumen — per DB-Check verifiziert, nicht nur UI-Text)

## ZUGFeRD/E-Rechnung (04.08.2026) ✅
Letzter offener Punkt aus Phase 4. Rechtliche Prüfung vorab: die aktuelle E-Rechnungspflicht betrifft primär B2B — für Privatkunden (Rakkus Hauptzielgruppe) wäre eine normale PDF-Rechnung weiterhin ausreichend. ZUGFeRD wurde trotzdem umgesetzt, da es keinen Nachteil hat (jede Rechnung bleibt eine ganz normale, für Menschen lesbare PDF-Datei — die E-Rechnungs-Daten stecken unsichtbar als Anhang mit drin) und Kunden, die selbst Unternehmen sind (z. B. Kooperationen/Firmenaufträge), sie direkt in ihre eigene Buchhaltungssoftware einlesen können.
- **Jede erstellte Rechnung ist jetzt automatisch eine "hybride" PDF/A-3-Datei** — die normale, gestaltete Rechnung von vorhin, plus eine unsichtbar eingebettete `factur-x.xml` mit denselben Daten in maschinenlesbarer Form (Profil **BASIC**, das laut GoBD-Vorgabe minimal nötige Profil, das in Deutschland überhaupt als elektronische Rechnung zählt — die schwächeren Profile MINIMUM/BASIC-WL reichen rechtlich nicht)
- Umgesetzt über die Bibliothek `node-zugferd` (npm), XML-Erzeugung + Einbettung passiert erst beim PDF-Download, nicht bei der Rechnungserstellung — dadurch kein zusätzlicher Speicherbedarf
- **Datenmodell musste dafür strukturiert werden:** die Adressfelder (Geschäftsadresse in den Einstellungen, Rechnungsadresse im Kundenprofil) waren bisher freier Fließtext — ZUGFeRD verlangt zwingend einzelne Felder (Straße, PLZ, Ort, Ländercode). Beide Formulare (`/admin/settings`, `/account/profile`) jetzt entsprechend umgebaut. Da noch niemand echte Daten dort eingetragen hatte, war das ohne Datenverlust möglich
- Enthält: Verkäufer/Käufer mit vollständiger Adresse, Steuernummer, USt.-Kategorie korrekt nach Kleinunternehmerregelung (Code „E" = steuerbefreit, mit Freitext-Begründung) oder Normalsatz, SEPA-Überweisungsdaten (IBAN/BIC), Zahlungsreferenz, Fälligkeitsdatum, alle Summen
- **Reale Stolperfalle beim Bauen gefunden und behoben:** die Bibliothek validiert die erzeugte XML automatisch gegen das offizielle XSD-Schema (per Java, ist auf dem Server vorhanden) — das crashte zunächst nur im laufenden Next.js-Server (nicht in einem isolierten Test-Skript), weil Turbopack beim Bündeln die internen Dateipfade der Bibliothek zerschossen hat. Behoben durch `serverExternalPackages` in `next.config.ts` (node-zugferd/xsd-schema-validator/pdf-lib von der Bündelung ausgenommen, laufen unverändert als echte Node-Module)
- Falls die eingebetteten Daten aus irgendeinem Grund doch mal fehlschlagen sollten, bricht der Download nie ab — es gibt dann automatisch die normale (nicht-hybride) PDF-Rechnung als Fallback, geloggt für spätere Prüfung
- Getestet: XML-Erzeugung besteht die offizielle XSD-Validierung (`✔`), Anhang wirklich in der PDF eingebettet und mit einem unabhängigen PDF-Tool (`pikepdf`) ausgelesen und inhaltlich geprüft (alle Felder korrekt: Adressen, Steuernummer, IBAN, Kleinunternehmer-Begründung, Summen), menschenlesbare Ansicht weiterhin unverändert korrekt (`pdftotext`), kompletter Ablauf inkl. neuer strukturierter Adressformulare per Headless-Browser gegen den Live-Server getestet

## Mahnungen (04.08.2026) ✅
Letzter offener Punkt aus Phase 4, den der Nutzer priorisiert hat (Auswahl zwischen Mahnungen/Online-Bezahlen-Vorbereitung/ZUGFeRD/Phase 5 — Mahnungen gewählt, da sofort ohne externe Entscheidungen umsetzbar):
- **Neues `Reminder`-Model**, an eine bestehende Rechnung gekoppelt (Mahnung ohne Rechnung nicht möglich) — Mahnstufen zählen pro Rechnung automatisch hoch: 1 = „Zahlungserinnerung", 2 = „1. Mahnung", 3 = „2. Mahnung", usw.
- **Admin-Panel „Mahnungen"** auf der Auftragsseite (nur sichtbar, wenn eine Rechnung existiert): Verlauf aller bisherigen Mahnstufen mit Datum + PDF-Link, Button für die jeweils nächste Stufe (mit optionalem Freitext-Hinweis). Sobald der Auftrag als bezahlt markiert wird, verschwindet der Button automatisch („Auftrag ist bezahlt, keine Mahnung nötig")
- **Mahnungs-PDF** (`@react-pdf/renderer`, gleicher Stil wie Rechnung/Vertrag): verweist auf die Original-Rechnung, nennt den offenen Betrag, setzt eine neue Zahlungsfrist (+7 Tage ab Mahnungsdatum) und wiederholt die Bankdaten/den Verwendungszweck
- **Für den Kunden sichtbar:** alle erhaltenen Mahnungen erscheinen im Zahlungsblock der eigenen Auftragsseite, mit Download-Link — genau wie schon die Rechnung
- Bewusst **kein automatischer E-Mail-Versand** — passend zum Rest des Projekts (auch neue Chat-Nachrichten/Rechnungen lösen keine automatische Mail aus), Admin erzeugt die Mahnung und der Kunde sieht sie beim nächsten Login; ein automatischer Versand könnte bei Bedarf später ergänzt werden
- Getestet: kompletter Ablauf per Headless-Browser (Zahlungserinnerung erzeugen → 1. Mahnung erzeugen → beide als echtes PDF abrufbar und inhaltlich korrekt, auf Kundenseite sichtbar, nach „Als bezahlt markieren" verschwindet der Mahnen-Button automatisch)

## Zahlungsart pro Auftrag (04.08.2026) ✅
Nächster Backlog-Punkt aus Phase 4 nach der Rechnungsstellung:
- Neues `paymentMethod`-Feld am Order-Model (Überweisung/PayPal/Bar/Sonstig) — wird beim „Als bezahlt markieren" per Dropdown mitgesetzt, nicht mehr nur binärer Status
- Sichtbar auf der Admin-Auftragsseite (neben dem Bezahlt-Badge), auf der Kunden-Auftragsseite, und als neue Aufschlüsselung „Eingegangene Zahlungen nach Zahlungsart" in der Statistik-Seite (Summe pro Methode)
- Beim Zurücksetzen auf „Offen" wird die Zahlungsart mit zurückgesetzt (leer), damit sie beim nächsten „Bezahlt"-Markieren neu gewählt werden muss
- Bewusst noch **kein** Auswahlfeld beim Kunden selbst (nur informativ nach Zahlungseingang) — Zahlungsart wird weiterhin ausschließlich vom Admin beim Verbuchen gesetzt, passend zum bestehenden Prinzip "Zahlungsstatus wird nie automatisch gesetzt"
- Getestet: echter Ablauf per Headless-Browser (Methode auswählen → als bezahlt markieren → Anzeige auf Admin-, Kunden- und Statistikseite korrekt, Zurücksetzen auf offen funktioniert)

## Phase 4 — Rechnungen (04.08.2026) ✅
Erste Hälfte von Phase 4 aus dem Masterplan umgesetzt (Rechnungsstellung selbst; E-Rechnung/ZUGFeRD-Export bewusst noch nicht angefasst):
- **Neue Admin-Seite `/admin/settings`** — erste echte Einstellungs-Oberfläche der Seite überhaupt (vorher wurden alle Settings nur per Direkt-DB-Skript gesetzt). Enthält alle für Rechnungen nötigen Pflichtangaben: Geschäftsname, Anschrift, E-Mail, Steuernummer, Kleinunternehmer-Schalter (§ 19 UStG), USt.-Satz, IBAN/BIC/Bank, Rechnungsnummern-Präfix + nächste laufende Nummer
  - **Wichtig — noch mit Platzhaltern befüllt:** um die Funktion zu testen, wurden dort testweise Platzhalterwerte eingetragen (`PLATZHALTER-STNR`, `DE00 0000...` etc.). Bitte einmal unter `/de/admin/settings` die echten Geschäftsdaten (Anschrift, Steuernummer, IBAN/BIC, Bank) eintragen, bevor die erste echte Rechnung an einen Kunden geht — sonst stehen Platzhalter drauf
- **Neues `Invoice`-Model** — Rechnungen sind unveränderliche Schnappschüsse (Geschäfts- und Kundendaten werden zum Ausstellungszeitpunkt eingefroren, ändern sich also nicht rückwirkend, wenn sich später die Einstellungen oder Kundendaten ändern) — gleiches Prinzip wie bei den bestehenden Vertrags-PDFs
- **„Rechnung erstellen"-Button** auf der Admin-Auftragsseite, nur sichtbar/aktiv bei kostenpflichtigen Aufträgen mit final vereinbartem Preis, nur einmal pro Auftrag möglich. Erzeugt eine echte PDF-Rechnung (`@react-pdf/renderer`, gleicher Stil wie die Vertrags-PDFs) mit fortlaufender Rechnungsnummer (`RG-0001`, atomar hochgezählt, keine doppelte Vergabe bei gleichzeitigem Klicken)
- **Kleinunternehmerregelung berücksichtigt:** ist der Schalter aktiv, wird keine Umsatzsteuer ausgewiesen (nur Hinweistext „Gemäß § 19 UStG…"); ist er aus, wird die Rechnung mit Netto/USt./Brutto getrennt ausgewiesen
- **Eigene Annahme, bitte prüfen:** der vereinbarte Preis aus dem Preisvorschlag-Workflow wird als **Bruttopreis** behandelt (bei USt.-Pflicht wird der Nettobetrag daraus zurückgerechnet). Falls die Preise eigentlich als Nettopreise gemeint sind, bitte kurz Bescheid geben, dann wird das umgestellt
- Rechnung nutzt dieselbe Zahlungsreferenz (`RK-XXXXXXXX`) wie schon der bestehende Zahlungsstatus-Block, als Verwendungszweck für die Überweisung
- **Sichtbar für den Kunden:** eigener Rechnungs-Download-Link (PDF) direkt im Zahlungsblock der Auftragsseite, sobald eine Rechnung existiert
- **Quick Win nebenbei erledigt:** „Shooting anfragen"-Button jetzt echt verlinkt — neuer zweiter Button im Hero direkt neben „Portfolio ansehen“, sowie ein CTA-Block am Ende der Portfolio-Seite, beide zu `/account/new-order` (ohne Login automatischer Umweg über `/login?next=...`, danach direkt zurück zum Formular)
- **Bug beim Umsetzen gefunden und behoben:** das bestehende Settings-Dokument in der Datenbank existierte schon vor der Schema-Erweiterung, daher fehlte `invoiceSettings` dort komplett und die neue Einstellungsseite crashte mit 500 — behoben durch defensives Nachrüsten von Standardwerten beim Lesen (`getSettings()`), nicht nur für den einen Fall, sondern grundsätzlich robust für künftige Schema-Erweiterungen an bestehenden Dokumenten
- Getestet: kompletter Ablauf per echtem Headless-Browser gegen den Live-Server (Einstellungen speichern + persistieren, Rechnung aus einem echten Auftrag mit vereinbartem Preis erzeugen, erzeugtes PDF wirklich als PDF abrufbar und inhaltlich korrekt per `pdftotext` geprüft, Kunden-Downloadlink sichtbar, neue CTA-Buttons auf Startseite und Portfolio klickbar)

## Echter Zahlungsstatus + Überweisungs-Referenz (Ad-hoc, 03.08.2026) ✅
Klarstellung zum Nutzer-Feedback: „Vergütung: Bezahlt" war nie ein echter Zahlungsstatus, sondern nur die Auftragsart (bezahlter Auftrag vs. TFP) — es gab nie eine automatische „als bezahlt markiert"-Logik, aber das Label war irreführend genug, um genau danach auszusehen. Jetzt sauber getrennt:
- **Auftragsart-Label umbenannt** von „Bezahlt" zu „Kostenpflichtig" (DE) / „Chargeable" (EN), um die Verwechslung mit einem Zahlungsstatus zu vermeiden
- **Neues, komplett unabhängiges `paid`-Feld** am Order-Model (Standard: `false`) — wird **nie automatisch** gesetzt, nur explizit über einen Button im Admin (`markOrderPaidAction`/`markOrderUnpaidAction`), inkl. Zeitstempel und Aktivitäts-Log-Eintrag
- **Zahlungsreferenz für den Verwendungszweck:** `formatPaymentReference()` erzeugt deterministisch aus der Auftrags-ID einen kurzen Code (`RK-XXXXXXXX`) — kein extra DB-Feld nötig, keine Kollisionsprüfung, einfach aus der ohnehin eindeutigen Auftrags-ID abgeleitet. Wird dem Kunden auf der Auftragsseite angezeigt, sobald ein Preis vereinbart ist, und dem Admin überall dort, wo der Zahlungsstatus auftaucht
- **Sichtbar an drei Stellen:** Admin-Auftragsseite (Status-Pill + Umschalt-Button + Referenz), Admin-Auftragsliste (kompaktes Offen/Bezahlt-Label pro Zeile, nur bei kostenpflichtigen Aufträgen), Statistik-Seite (eigene Kennzahl „Offene Zahlungen" + Liste aller offenen Zahlungen mit Betrag und Referenz, verlinkt zum jeweiligen Auftrag)
- Kunden sehen ihren Zahlungsstatus nur lesend, kein Selbst-„als bezahlt markieren"
- Getestet: echte HTTP-Requests gegen den Live-Server über den kompletten Zyklus (neuer Auftrag startet offen trotz vereinbartem Preis, Referenz korrekt angezeigt auf beiden Seiten, nach Markieren als bezahlt verschwindet der Auftrag aus der offene-Zahlungen-Liste und beide Seiten zeigen „Bezahlt")

## Admin-Aufträge/Kunden für Skalierung umgebaut (Ad-hoc, 03.08.2026) ✅
Nutzer-Feedback: die einklappbaren Auftragskarten wirkten wie ein Dropdown (nicht gewollt), und das ganze System muss für **300 Aufträge/Monat mit mehreren Mitarbeitern** funktionieren — die alte „alles auf einer Seite laden"-Architektur hätte das nicht mitgemacht. Kompletter Umbau:
- **Aufträge sind jetzt eine echte, schlanke Liste** (`/admin/orders`) — kompakte Zeilen (Avatar, Titel, Kunde, Status, Datum), Klick navigiert auf eine **eigene Seite** (`/admin/orders/[orderId]`), kein `<details>`/Akkordeon mehr
- **Serverseitige Suche** (Kundenname/-E-Mail/Nickname oder Auftragstitel) über eine MongoDB-Aggregation mit `$lookup` auf die Kunden — skaliert, weil nicht mehr alles ins JS geladen und dort gefiltert wird
- **Echte Paginierung** (20 pro Seite) statt alles auf einmal zu laden — das war vorher der größte Skalierungs-Blocker
- **Archiv:** neues `archived`-Feld am Order-Model, Archivieren/aus-dem-Archiv-holen-Button auf der Auftragsseite, Aktiv/Archiv-Umschalter in der Liste — hält die Hauptliste übersichtlich, ohne je etwas zu löschen
- **Kundenakten** (`/admin/clients`, `/admin/clients/[clientId]`) — wie gewünscht „wie bei Ärzten mit Patientenakten": durchsuchbare Kundenliste (mit Auftragsanzahl pro Kunde), Akte pro Kunde mit Profil (Avatar, Kontakt, Anschrift/Rechnungsadresse falls hinterlegt), Umsatz-Summe (aus angenommenen Preisvorschlägen) und der vollständigen Auftragsliste dieses Kunden (inkl. archivierter, klar markiert)
- Neue Komponenten: `AdminOrderSearchBox`, `AdminArchiveToggle`, `AdminPagination`, `ArchiveOrderButton` — alle klein und wiederverwendbar
- Mehrere zuvor unbemerkt falsche Admin-Links ohne `/de`-Sprachpräfix beim Umbau mitgefixt (TFP-Vertrag-Formular-Redirect, Foto-Ordner-Link) — funktionierten vorher nur über einen zusätzlichen Redirect-Hop
- Getestet: echte HTTP-Requests gegen den Live-Server (Suche über Kundenname, Archiv-Filter, Auftragsseite mit allen Panels inkl. Chat, Kundenakte mit korrektem Umsatz/Adresse/Auftragsliste, Kunden-Suche mit Auftragsanzahl, Navigation)

## Bugfix + Profil/Chat/Vertrag-Runde (Ad-hoc, 03.08.2026) ✅
- **Kritischer Bug behoben:** Profilbild-Upload warf 500 (Next.js Server Actions haben standardmäßig ein 1-MB-Body-Limit — betraf potenziell auch Auftrags-Anhänge bei größeren Dateien). `next.config.ts` jetzt mit `experimental.serverActions.bodySizeLimit: "50mb"`, passend zum nginx-`client_max_body_size` von 50M
  - **Wichtig zur Kenntnisnahme:** beim Debuggen wurde versehentlich eine bereits erfolgreich hochgeladene Profilbild-Datei des Nutzers gelöscht (zu pauschaler `rm`-Befehl vor der eigentlichen Ursachenprüfung) — Nutzer wurde direkt informiert, Bild muss einmal neu hochgeladen werden
- **Profilbild löschen** jetzt möglich (`deleteAvatarAction`), vorher gab es nur „Ändern"
- **Client-Model erweitert:** `address` (Anschrift), `billingAddress` (Rechnungsadresse, für spätere Rechnungen), `title` (Titel/Abteilung, nur für Mitarbeiter sichtbar/editierbar) — alle im Profilformular editierbar
- **Vertrags-Unterschrift vorbefüllt:** Das Namensfeld beim Signieren übernimmt jetzt automatisch den Namen aus dem Kundenprofil (weiterhin änderbar). Bei minderjährigen Models bleiben die Erziehungsberechtigten-Felder bewusst manuell, da diese Personen kein eigenes Konto/Profil haben
- **Auftrags-Chat komplett neu gebaut** (Nutzer-Feedback: bisheriger Chat wirkte "binär"/zu simpel):
  - Echtes Chat-Bubble-Design (eigene Nachrichten rechts/weiß, fremde links/grau), mit Absender-Avatar, Namen und bei Mitarbeitern zusätzlich Titel/Abteilung (z. B. "Rakku · Inhaberin")
  - `OrderMessage`-Model bekam ein `senderId`-Feld (statt nur „admin"/„client" pauschal) — dadurch lässt sich der tatsächliche Absender (Avatar, Name, Titel) auflösen; ein Fallback verhindert Abstürze bei älteren Nachrichten ohne dieses Feld
  - **Admin-Auftragsliste umgebaut:** Aufträge sind jetzt eingeklappt (kompakte Zeile mit Avatar, Titel, Kunde, Status-Pill) und öffnen sich erst per Klick — vorher stand alles für jeden Auftrag permanent ausgeklappt da. Beim Öffnen erscheint eine Zwei-Spalten-Ansicht: Auftragsdetails links, Chat als eigenes Panel rechts daneben
  - **Kunden-Auftragsseite** ebenfalls zweispaltig auf großen Bildschirmen: Details links, Chat rechts als eigenes, beim Scrollen mitlaufendes Panel
- Getestet: echte HTTP-Requests gegen den Live-Server (Zwei-Spalten-Layout, Chat aus beiden Perspektiven mit Namen/Titel, Profil-Felder inkl. Vorbefüllung, Vertrags-Namensvorbefüllung), zusätzlich Screenshot-Vergleich der neuen Chat-Optik im Kunden- und Admin-Bereich

## Vertrags-PDFs angereichert (Ad-hoc, 03.08.2026) ✅
Nutzer bat darum, das TFP-Vertrags-Word-Dokument aus `/home/CLAUDE/UPLOAD/` noch einmal als Quelle heranzuziehen — Abgleich ergab, dass mehrere bereits erfasste Daten im PDF schlicht nie gerendert wurden (Formular sammelte sie, die PDF-Vorlage zeigte sie aber nicht an):
- **Neuer „Vertragsparteien"-Block oben in beiden Vertrags-PDFs** (Standard + TFP), analog zur Tabelle 1 im Original-Dokument: Fotograf/in-Seite (Name, E-Mail, Instagram, Webseite — aus dem bestehenden `Settings.socialLinks`, mit `contact@rakku.de`-Fallback) und Kunden-/Model-Seite (Name, E-Mail aus dem Client-Datensatz — funktioniert jetzt zuverlässig, weil die Registrierung seit dem letzten Umbau echten Vor-/Nachnamen erzwingt)
- **Beim TFP-Vertrag fehlten Geburtsdatum, Anschrift und Instagram des Models im PDF** — waren im Formular und im Datenmodell längst vorhanden, wurden aber nie ausgegeben. Jetzt Teil des Vertragsparteien-Blocks
- **Stylist/in-Anschrift und -Instagram fehlten ebenfalls im PDF** (nur Name/Nutzung/Credit wurden gezeigt) — ergänzt
- **Hinweis zur kommerziellen Nutzung** aus dem Original-Dokument ergänzt (nur nach vorheriger schriftlicher Absprache)
- `renderContractPdf()` bekommt jetzt zusätzlich Kunden- und Fotograf-Kontaktdaten übergeben (`ContractPartyInfo`), die PDF-Route lädt dafür den zugehörigen `Client`-Datensatz und die `Settings` mit
- Getestet: beide Vertragstypen (Standard + TFP mit Stylist) als echtes PDF erzeugt und der Text per `pdftotext` extrahiert, um zu verifizieren, dass wirklich alle Felder angezeigt werden — nicht nur Build-Erfolg angenommen

## Großes Feedback-Paket umgesetzt (Ad-hoc, 03.08.2026) ✅
Alle sieben Punkte aus der Nutzer-Feedback-Warteschlange abgearbeitet:
- **Tags farbig:** neue `ORDER_TAG_COLORS`/`SHOOTING_TYPE_COLORS`-Konstanten (nach Kanban-Phase gruppiert: Shoot=blau, Edit=amber, Done=grün, Abgebrochen=rot). Kunden-Dashboard zeigt den Status als farbige Pille, Admin-Auftragskarten haben einen farbigen linken Rahmen passend zum Tag
- **Sortierung im Admin:** neues Dropdown (`AdminOrderSortSelect`) auf `/admin/orders` — Neueste/Älteste/Nach Status/Nach Kunde, steuert über `?sort=`-Query-Parameter
- **Statistiken visueller:** neue `DonutChart`-Komponente (reines SVG, keine externe Chart-Bibliothek) ersetzt die Balken beim Auftrags-Funnel und bei der nachgefragten Art des Shootings — farbige Ringsegmente mit Legende und Gesamtzahl in der Mitte
- **Registrierung fordert jetzt Vor- und Nachname** (statt nur „Name"), plus optionales Nickname-Feld. Client-Model um `firstName`/`lastName`/`nickname`/`avatarPath` erweitert, `name` bleibt als berechnetes Feld (`Vorname Nachname`) bestehen — dadurch mussten bestehende Stellen, die `client.name` lesen (Admin-Liste, Verträge, Begrüßung), nicht angefasst werden
- **Profil-Bearbeitung für alle** (Kunden und Mitarbeiter gleichermaßen, da Mitarbeiter = Client mit Rolle): neue Seite `/account/profile` — Vorname/Nachname/Nickname ändern, Profilbild hochladen (lokal in `private-uploads/avatars/`, ausgeliefert über eine authentifizierte Route `/api/avatars/[clientId]/[filename]`, keine Nextcloud). Avatar erscheint auf `/account` (Begrüßung nutzt jetzt Nickname/Vorname) und in der Admin-Auftragsliste neben dem Kundennamen
- **Kunden können ihr Shooting selbst stornieren:** neuer Button auf der Auftragsdetailseite, verlangt eine eigene Begründung (Freitext, bewusst getrennt von der admin-verwalteten Ablehnungsgründe-Liste, da andere Zielgruppe/andere Gründe). Neues `rejectedBy`-Feld am Order-Model unterscheidet Admin- von Kunden-Stornierung, wirkt sich auch auf die Anzeige aus („Du hast storniert" vs. „Wurde abgelehnt")
- **Auftragsdetailseite deutlich angereichert:** Vergütungsart, Ort/Zeitraum, Instagram/Discord, Eingangsdatum als übersichtlicher Detail-Block; dazu ein Kunden-seitiges Aktivitäts-Log (aufklappbar) — bisher gab es dort nur Titel, Preis und Notizen
- **Auftrags-Chat:** neues `OrderMessage`-Model + wiederverwendbare `OrderChat`-Komponente — pro Auftrag können Kunde und Rakku Nachrichten austauschen, sichtbar sowohl auf der Kunden-Auftragsseite als auch direkt in der Admin-Auftragskarte. Bewusst als einfacher, serverseitig aktualisierter Nachrichtenthread umgesetzt (kein Echtzeit-Websocket), passend zum Rest des Projekts (auch die Preisverhandlung ist nicht live) — ergänzt den allgemeinen Tawk-Chat, ersetzt ihn nicht
- Getestet: DB-Logik + echte HTTP-Requests gegen den Live-Server (Registrierungs-Validierung, Profil-Vorbefüllung, Farbdarstellung, Stornierung inkl. Botschaft „Du hast storniert", Chat aus beiden Perspektiven, Admin-Avatar-Anzeige, Sortierung) sowie Screenshot-Prüfung der wichtigsten neuen Seiten (Auftragsdetail, stornierter Auftrag, Profil, Statistik-Kreisdiagramme)

## Gebrandete Transaktions-Mails (Ad-hoc, 03.08.2026) ✅
- Verifizierungs- und Passwort-Reset-Mail von reinem Textlink auf ein richtiges HTML-Layout umgestellt: dunkler Header mit Wordmark (passend zum Marken-Look), heller Content-Bereich (bessere E-Mail-Client-Kompatibilität als komplett dunkel), dunkler Button statt nacktem Link, Fallback-Linktext bleibt erhalten
- Neuer gemeinsamer Baustein `lib/emailTemplate.ts` (`emailShell()`), von beiden Mail-Typen genutzt — neue Transaktions-Mails künftig darauf aufbauen statt eigene Inline-Strings zu bauen
- Vorschau ohne echten Mail-Versand erzeugt (Playwright-Screenshot der gerenderten HTML-Datei), damit der Nutzer das Ergebnis sehen konnte, ohne sich neu registrieren zu müssen

## Admin-Bereich in die normale Seite integriert (Ad-hoc, 03.08.2026) ✅
- **Kein separater "Admin-App"-Bereich mehr.** Auf Nutzer-Feedback hin ("kein extra Admin-Dashboard, sondern das normale User-Dashboard plus die Admin-Sachen wenn jemand Staff ist") liegen alle Admin-Seiten jetzt unter `/[locale]/admin/...` (technisch: `src/app/[locale]/admin/*`) statt in einem eigenen, nicht-lokalisierten `src/app/admin`-Baum mit eigenem `<html>`/`<body>`
- Admin-Seiten laufen jetzt durch dieselbe `[locale]/layout.tsx` wie der Rest der Seite — **normaler `SiteHeader` + `SiteFooter`**, keine eigene `AdminNav`/`AdminFooter`-Hülle mehr. Innerhalb der Seiten gibt es noch eine kleine Pill-Unternavigation (`AdminNavLinks`) zum Wechseln zwischen Aufträge/Angebote/Ablehnungsgründe/Statistiken
- `/account` zeigt Mitarbeitern jetzt direkt einen „Admin-Bereich"-Kasten mit Schnellzugriff-Buttons — die Admin-Funktionen sind sichtbarer Teil des normalen Kunden-Dashboards, nicht mehr komplett getrennt
- `proxy.ts`-Middleware schließt `/admin` nicht mehr aus — alte Lesezeichen auf `/admin/orders` (ohne Sprachpräfix) werden von der bestehenden next-intl-Middleware automatisch zu `/de/admin/orders` weitergeleitet, ganz ohne Extra-Code
- **Wichtige Nebenwirkung erkannt und behoben:** Da Admin-Seiten jetzt im `[locale]`-Baum liegen, hätten sie sonst auch vom Wartungsmodus und Kopierschutz betroffen sein können — das hätte im schlimmsten Fall dazu geführt, dass sich niemand mehr ins Admin-Panel einloggen kann, um den Wartungsmodus wieder abzuschalten (Deadlock). Behoben: `[locale]/layout.tsx` prüft jetzt einmal pro Request die Mitarbeiter-Rolle und überspringt für Mitarbeiter sowohl den Wartungsmodus als auch den Kopierschutz — Mitarbeiter können die Seite nie versehentlich für sich selbst sperren
- Alle internen Admin-Links/Redirects/`revalidatePath`-Aufrufe bewusst fest auf `/de/admin/...` verdrahtet (nicht dynamisch je Sprache) — Admin bleibt wie schon in Phase 2 entschieden ein internes, deutschsprachiges Werkzeug, jetzt aber technisch Teil derselben Routing-Struktur
- Getestet: echte HTTP-Requests gegen den Live-Server (alte URL-Weiterleitung, Zugriffsschutz für Nicht-Mitarbeiter, Mitarbeiter-Zugriff auf Admin- und Kundenbereich mit derselben Session, Wartungsmodus-Bypass für Mitarbeiter mit anschließendem Wiederherstellen des Normalzustands) + visuelle Screenshot-Prüfung gegen die Live-Seite

## Einheitlicher Login (Mitarbeiter = Client mit Rolle) + Admin-Redesign (Ad-hoc, 03.08.2026) ✅
- **Der separate env-basierte Admin-Login ist komplett weg.** Mitarbeiter sind jetzt normale Client-Accounts mit `role: "staff"` (neues Feld am Client-Model, Standard `"client"`) — ein einziger Login unter `/login` für Kunden und Mitarbeiter, keine zwei Systeme mehr
- `isAdminSession()` (in `adminAuth.ts`) prüft jetzt live die Rolle des eingeloggten Client-Accounts statt eines separaten Cookies — alle bestehenden Admin-Routen/Server-Actions mussten dadurch nicht angefasst werden (gleicher Funktionsname, andere Implementierung)
- Login-Redirect ist jetzt rollenbasiert: Mitarbeiter landen nach dem Login automatisch in `/admin/orders`, normale Kunden in `/account` — ein vorhandener `next`-Parameter (z. B. vom Angebote-Deep-Link) hat weiterhin Vorrang
- `/admin/login` existiert als reiner Redirect zu `/login` weiter (alte Bookmarks funktionieren), die alte `AdminLoginForm`/`adminLoginAction`/`adminLogoutAction` wurden entfernt
- **Mitarbeiter können jetzt mit demselben Account ganz normal eigene Aufträge über `/account/new-order` anlegen** — genau der Nutzer-Wunsch, kam quasi kostenlos aus der Account-Vereinheitlichung mit
- Kopfzeile der öffentlichen Seite zeigt eingeloggten Mitarbeitern zusätzlich einen „Admin"-Link
- Rakkus eigener Zugang wurde per einmaligem Migrations-Skript auf `contact@rakku.de` (role „staff") umgestellt, `ADMIN_USERNAME`/`ADMIN_PASSWORD` aus `.env.local` entfernt (waren danach tote Konfiguration)
- **Admin-Bereich komplett visuell überarbeitet** (Nutzer-Feedback: sah „billig"/„wie eine Gratis-Seite" aus): neue Kopfzeile mit Quantify-Wordmark + Pill-Navigation mit aktivem Zustand, wiederverwendbare Bausteine (`AdminPageHeader`, `StatCard`, `AdminEmptyState`) auf allen Admin-Seiten, Karten jetzt mit `--rk-surface`-Hintergrund statt transparent, Stat-Cards (Insgesamt/Aktiv/Abgelehnt etc.) oben auf Aufträge- und Statistik-Seite, sauberere Leerzustände mit Icon statt nacktem Text, Emoji-Anhang-Icon durch SVG ersetzt
- Getestet: komplette Redirect-Kette (Rolle → Ziel-Seite), Zugriffsschutz (normaler Kunde kommt weiterhin nicht ins Admin-Panel), Mitarbeiter-Session funktioniert gleichzeitig für `/admin/*` und `/account`, alle Admin-Seiten per echtem Headless-Browser-Screenshot gegen den Live-Server visuell verifiziert (mit und ohne Daten)

## Statistiken, Ablehnungsgründe & Aktivitäts-Log (Ad-hoc-Feature, 03.08.2026) ✅
- **Neue Statistik-Seite** (`/admin/statistics`): Auftrags-Funnel (Verteilung nach Kanban-Tag), Preise & Umsatz (Gesamt, Durchschnitt, Umsatz pro Monat), Ablehnungsgründe (Häufigkeit je Grund), Kunden & Angebote (Neuregistrierungen pro Monat, nachgefragte Art des Shootings) — als einfache Balken/Tabellen, keine externe Chart-Bibliothek nötig
- **Auftrag ablehnen:** neue generelle Ablehnungsfunktion pro Auftrag (unabhängig vom bestehenden Preisverhandlungs-Workflow) — setzt den Kanban-Tag automatisch auf „Abgebrochen" und verlangt einen Grund aus einer admin-konfigurierbaren Liste + optionale Notiz
- **Vorgefertigte Ablehnungsgründe** admin-verwaltbar unter `/admin/rejection-reasons` (anlegen, aktiv/inaktiv schalten, löschen) — der gewählte Grund wird beim Ablehnen als Text-Snapshot am Auftrag gespeichert, bleibt also auch erhalten, wenn der Grund später gelöscht wird
- **Aktivitäts-Log pro Auftrag:** neue Timeline (aufklappbar direkt in der Auftragskarte im Admin) protokolliert automatisch alle wichtigen Ereignisse — Auftrag erstellt, Status-Tag geändert, Preisvorschlag/-annahme/-ablehnung/-gegenvorschlag, Vertrag erstellt/gesendet/unterschrieben, Vergütungsart geändert, Auftrag abgelehnt/Ablehnung aufgehoben — jeweils mit Zeitstempel und Akteur (Du/Kunde)
- Admin-Navigation bekam eine Kopfzeile mit Links zu Aufträge/Angebote/Ablehnungsgründe/Statistiken (vorher nur Logout, keine Querverlinkung)
- Getestet: DB-Logik + echte HTTP-Requests gegen den Live-Server (Ablehnungsgrund anlegen → erscheint in Auswahl, Auftrag ablehnen → Badge+Notiz+Verlauf erscheinen im Admin, Statistik-Seite zeigt Umsatz und Ablehnungsgrund korrekt), zusätzlich verifiziert dass die Server Actions ohne gültige Admin-Session fail-closed abbrechen

## Angebote-Seite (Ad-hoc-Feature, 03.08.2026) ✅
- Neue öffentliche Unterseite `/angebote`: Liste aller Leistungen mit ungefährer Preisspanne, zweisprachig
- **Admin** (`/admin/services`): Angebote anlegen/bearbeiten/löschen, Status per Dropdown umschalten (verfügbar / auf Anfrage / nicht verfügbar) — eigenes `Service`-Model, nicht Teil des ursprünglichen Masterplans, auf Nutzerwunsch ergänzt
- **Wichtig:** Angebote bleiben auf der öffentlichen Seite immer sichtbar, auch wenn „nicht verfügbar" — nur das Status-Badge ändert sich, nichts wird ausgeblendet (explizite Anforderung)
- Jedes Angebot (außer „nicht verfügbar") hat einen Buchen/Anfragen-Button, der `/account/new-order` mit vorbelegter Auftragsart + Konzept öffnet; ohne Login führt der Weg erst über `/login?next=...` und landet danach automatisch vorbefüllt beim Auftragsformular
- Der freie/individuelle Auftrag (`/account/new-order` ohne Parameter) bleibt unverändert nutzbar, unten auf der Angebote-Seite verlinkt
- „Angebote"-Link in der Hauptnavigation ergänzt
- Kompletter Ablauf über echte HTTP-Requests gegen den Live-Server + direkte DB-Skripte getestet: alle drei Status-Darstellungen, Sichtbarkeit bei „nicht verfügbar", Buchen-Link-Erzeugung, Login-Redirect-Kette mit erhaltenen Query-Parametern

## Kleinkorrektur (03.08.2026)
- Abstand zwischen „Ablauf"-Sektion und Footer auf der Startseite vergrößert (`pb-36` → `pb-44`), damit der letzte Absatz nicht direkt am Footer klebt

## Gerade fertig
- Hero überarbeitet: echtes Foto als Hintergrund, Sucher-Rahmen ums ganze Bild (nicht mehr um den Text), weicher Übergang zur nächsten Sektion, „Ausgewählte Arbeiten"-Teaser mit drei echten Bildern
- Scroll-Animationen (Reveal, Scroll-Down-Cue, Nach-oben-Button) als wiederverwendbare Komponenten
- Live-Chat (tawk.to) + Wartungsmodus, beide über ein zentrales `Settings`-Dokument in MongoDB steuerbar, live getestet

## Gerade fertig (2)
- About-Sektion bekam ein echtes Portrait von Rakku (kameraführend) statt reinem Text — Nutzer hat explizit bestätigt: Hero zeigt Arbeit, About zeigt die Person. Nicht mehr ändern, ohne das erneut zu hinterfragen.

## Phase 1 abgeschlossen ✅
- Portfolio-Seite live: 14 echte Einträge + 47 Bilder aus V1 migriert (DB + Dateien), Galerie mit Lightbox (Pfeiltasten/Escape), zweisprachig
- Seitenweiter Kopierschutz 1:1 aus V1 übernommen (kein Rechtsklick/Textauswahl/Kopieren/Speichern-Shortcut etc.), über Settings steuerbar
- Minimale Site-Navigation (Wordmark → Home, Portfolio-Link, DE/EN-Umschalter), Hero hat wieder einen „Portfolio ansehen"-Button
- Scroll-Cue-Zentrierungsbug behoben (CSS-Animation überschrieb transform), About-Bild-Format nach Nutzer-Feedback zurück auf 1:1 (kein Beschnitt)
- Datenschutzerklärung (DE/EN, echter Text aus V1 migriert) + Footer mit Social-Links + externem Impressum-Link (impressum4u.de)

## Nachbesserungen (Nutzer-Feedback nach Phase-1-Review)
- About-Foto: zurück auf quadratisch, Blur-Rand-Trick wieder entfernt (Nutzer wollte es „wie das angehängte Bild" — einfachste, sicherste Lösung ohne jeden Beschnitt gewählt)
- Portfolio: Kategorie-Filter (Alle/Cosplay/Portrait), Meta-Chips (Kategorie/Event/Tags), anklickbare Instagram-Credits (Fotograf + Model), Bilder-Filmstreifen in der Lightbox — alles aus V1 nachgebaut, vorher gefehlt

## Phase 2 — Auth fertig ✅
- Registrierung (Name/E-Mail/Passwort) + E-Mail-Verifizierung (Link, 24h gültig) + Login (nur nach Verifizierung) + Logout
- Passwort-Reset-Flow (Link 1h gültig, verrät nicht ob E-Mail existiert)
- Sessions über httpOnly-Cookie (JWT via `jose`, 30 Tage), Passwörter mit bcrypt (12 Runden) gehasht
- Header zeigt Login/Konto-Link je nach Session-Status, `/account` als geschützte Platzhalter-Seite (echtes Dashboard folgt mit Aufträgen)
- SMTP-Versand verifiziert (Verbindungstest erfolgreich), Kernlogik (Passwort-Hash, Duplikat-Schutz, Verifizierung) per Skript getestet

## Scroll-Feinschliff (Nutzer-Feedback)
- Hero-Text-Lesbarkeit: Vignette hinter dem Textblock ergänzt (vorher war die Bildmitte der am wenigsten verdunkelte Bereich — genau verkehrt), Schatten auf allen Hero-Textzeilen
- Header ohne backdrop-blur (war vermutlich Hauptursache für Scroll-Ruckeln bei fixierten Elementen)
- Butterweiches Scrollen über **Lenis** ersetzt jetzt die native "stückhafte" Mausrad-Physik, site-weit
- Eigene schmale Scrollbar rechts (Custom, native Browser-Scrollbar ausgeblendet), draggable, synchron mit Lenis
- `prefers-reduced-motion` respektiert (deaktiviert Lenis-Glättung für Nutzer, die das eingestellt haben)

## Auftragserstellung fertig ✅
- Formularfelder 1:1 aus dem alten V1-Anfrageformular übernommen (`shootingFields` in server.js): Art des Shootings, Charakter/Idee, Ort, Wunschzeitraum, Instagram, Discord, Idee & Stimmung, Anmerkungen — Name/E-Mail/Sprache entfallen, kommen jetzt aus dem Kundenkonto
- Datei-Upload (Referenzbilder/PDF, max. 8 Dateien, je 15 MB) — landet **nicht** in der Nextcloud, sondern in `private-uploads/orders/<orderId>/` (bewusste Architekturentscheidung), Auslieferung nur für den Auftrags-Besitzer über `/api/files/[orderId]/[filename]`
- Aufträge bekommen automatisch den Kanban-Tag „Neue Anfrage", `/account` zeigt jetzt die eigene Auftragsliste mit Status-Pill
- Getestet: Anlegen, Datei speichern/lesen, Zuordnung über clientId, Zugriffsschutz — alles per Skript verifiziert

## Admin-Grundgerüst fertig ✅
- `/admin/login` — einfacher Einzel-Admin-Login (env-basiert wie in V1: `ADMIN_USERNAME`/`ADMIN_PASSWORD` in `.env.local`), eigene Session getrennt vom Kunden-Login. **Wird in Phase 6 durch das echte Mitarbeiter/Rollen-System abgelöst**, bewusst als Zwischenlösung, damit Phase 2 jetzt schon nutzbar ist.
- `/admin/orders` — alle Aufträge aller Kunden, mit Kunde-Zuordnung (Name/E-Mail), allen Formularfeldern, Anhängen zum Download, und einem Dropdown, um den Kanban-Tag direkt zu ändern
- `/admin` liegt bewusst außerhalb der Sprach-Struktur (kein Deutsch/Englisch nötig für ein internes Tool), Middleware entsprechend angepasst

## Preisvorschlag-Workflow fertig ✅
- Nur Admin darf den ersten Vorschlag machen (serverseitig erzwungen, nicht nur UI-Beschränkung)
- Kunde kann auf jeden offenen Vorschlag mit Annehmen/Ablehnen/Gegenvorschlag reagieren, unbegrenzt viele Runden
- Preis gilt erst als fest, wenn eine Seite den Vorschlag der anderen annimmt — dann „agreed", keine weiteren Aktionen mehr möglich
- Nach Ablehnung kann Admin jederzeit neu vorschlagen
- Neue Seite `/account/orders/[orderId]` beim Kunden (Auftragsdetails + Preisverlauf), Admin sieht denselben Verlauf direkt in der Auftragsliste
- Kompletter Zustandsautomat (Vorschlag → Gegenvorschlag → Annahme, sowie Ablehnung → neuer Vorschlag) per Skript durchgetestet

## Vertrag + TFP + digitale Signatur fertig ✅ — Phase 2 komplett
- **Standard-Vertrag:** Admin erstellt (nur wenn Preis „agreed"), Klauseln neu formuliert (keine bestehende Vorlage — analog zum TFP-Text vor Produktivnutzung von einem Anwalt gegenchecken lassen), PDF via `@react-pdf/renderer`
- **TFP-Vertrag:** volles Intake-Formular unter `/admin/orders/[id]/tfp-contract` (Model, Stylist optional, Shooting-Details, Nutzungsrechte, Veröffentlichungskanäle, Credit, Sondervereinbarungen) — 1:1 aus dem hochgeladenen Word-Dokument übernommen
- **Minderjährigen-Erkennung automatisch** aus Geburtsdatum, blendet Erziehungsberechtigte-Felder ein und verlangt deren Unterschrift beim Signieren (nur so viele wie tatsächlich hinterlegt, nicht immer zwei)
- **Signatur-Flow:** Admin „sendet" → Fotograf-Signatur wird dabei automatisch gesetzt; Kunde unterschreibt mit Name + Zustimmungs-Checkbox (einfache elektronische Signatur, IP+Zeitstempel protokolliert) — kein Drittanbieter, keine Kosten, wie geplant
- PDF abrufbar für Kunde (eigener Vertrag) und Admin (alle), über eine gemeinsame authentifizierte Route
- Kompletter Ablauf inkl. echter PDF-Erzeugung für beide Vertragstypen per Skript getestet

## Phase 3 — Kunden-Dashboard & Nextcloud fertig ✅
- **Admin:** Ordner-Browser direkt gegen die echte Nextcloud (WebDAV `PROPFIND`), Ordner einem Kunden zuweisen/entfernen — pro Auftrag im Admin-Panel sichtbar
- **Admin-Freigabeseite** (`/admin/folders/[id]`): alle Bilder eines zugewiesenen Ordners, Bewertungen des Kunden sichtbar, einzelne Fotos für den Download freigeben
- **Kunde** (`/account/photos`): eigene zugewiesene Ordner, Galerie mit Sternebewertung, Lightbox, Download-Button nur bei freigegebenen Fotos aktiv
- **Performance:** Galerie nutzt Nextclouds eigenen Vorschaubild-Dienst (kleine JPEGs, nicht die Originale — die Ordner enthalten auch RAW/ARW-Dateien, die werden rausgefiltert), Originale nur beim tatsächlichen Download geladen
- Kompletter Ablauf **über echte HTTP-Requests gegen den Live-Server getestet** (nicht nur DB-Logik): Ordner-Browser, Foto-Vorschau, Zugriffsschutz (401 ohne Login) — alles gegen die echte Nextcloud-Instanz verifiziert

## Gerade dran / als Nächstes
- **Phase 4 ist jetzt vollständig fertig** — auch das direkte Bezahlen ist code-seitig fertig (Details siehe Eintrag oben), es fehlt nur noch, dass Rakku ein Stripe-Konto anlegt und mir die Zugangsdaten gibt (Schritte 1-4 im Eintrag oben) — bis dahin bleibt die Funktion unsichtbar
- **Phase 5 (Ausgaben-Erfassung, Einnahmen/Ausgaben-Übersicht, EÜR-Export) ist fertig.**
- **Phase 6 (Team-/Rechtesystem, Dokumenten-Creator, Personalmanagement, Qualitätsmanagement) hat jetzt überall eine erste funktionsfähige Version, inklusive vollständiger Berechtigungsdurchsetzung über alle Admin-Bereiche.** Was bewusst noch fehlt: echte Rechtstexte für Arbeitsvertrag/-zeugnis (muss von Rakku selbst/ihrem Anwalt kommen), Intranet-Bereich (interne Abläufe, separat von der öffentlichen FAQ), kosmetische Filterung der Admin-Pill-Navigation nach Berechtigung (Zugriffskontrolle selbst ist überall korrekt, das ist nur die Anzeige der Nav-Links)
- Bitte einmal echte Geschäftsdaten unter `/de/admin/settings` eintragen (aktuell noch Platzhalter, jetzt inkl. strukturierter Anschrift Straße/PLZ/Ort/Land)

## Nutzer-Feedback-Warteschlange (03.08.2026)
Alle sieben Punkte aus dieser Runde sind umgesetzt — siehe „Großes Feedback-Paket umgesetzt" oben. Ein Punkt aus einer früheren Nachricht wurde nicht erneut erwähnt und ist daher noch offen:
- **Nach Verifizierung + erstem Login:** Geburtsdatum (und ggf. weitere Vertragsdaten) abfragen, damit es automatisch in Verträge übernommen werden kann — noch nicht umgesetzt, war in einer späteren Nachricht nicht mehr enthalten, könnte aber weiterhin gewünscht sein

## Backlog (nicht jetzt, aber vorgemerkt)
- **FAQ-Seite** (öffentlich) — ✅ **umgesetzt** (05.08.2026, siehe eigener Eintrag), aus dem Backlog erledigt
- **Instagram-Story-Recap-Generator** (03.08.2026 ergänzt) — wenn ein Auftrag abgeschlossen/veröffentlicht ist, automatisch passende Instagram-Story-Grafiken (9:16) als Rückblick generieren, die der Nutzer auf seinem eigenen Insta-Account posten kann. Vermutlich am Auftrags-Status „done_posted"/„done_phase1" aufgehängt. Noch kein Konzept für Bildaufbau/Vorlage festgelegt. **Weiterhin offen.**
- **Team-/Rollen-/Berechtigungssystem** — ✅ **komplett umgesetzt** (04.–05.08.2026: Abteilungen/Positionen/Ränge/Teams als echte Entitäten, Live-Chat-Sichtbarkeit pro Rang, Berechtigungsprüfung auf allen Admin-Bereichen). Nur echte Arbeitsvertrags-/Zeugnistexte (Inhalt, kein Code) und Intranet-Bereich bleiben offen, siehe Phase 6 im Masterplan.
### Umsetzungsreihenfolge (festgelegt 07.08.2026, Nutzer hat mir die Reihenfolge überlassen)
1. Positionen/Abteilungen umbenennen — kleinster, isolierter Punkt, guter Start — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
2. „Nochmal buchen"-Button für Stammkunden — ebenfalls klein und isoliert, schneller sichtbarer Nutzen für Kunden — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
3. Rabattcodes nur für Neukunden — spiegelt das bestehende „nur Stammkunden"-Muster, geringes Risiko — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
4. Manueller Rabatt direkt am Auftrag — größer, geht ans Geld (Stripe/Rechnung/Statistik), bewusst erst nachdem die kleineren Punkte erledigt sind — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
5. Empfehlungsprogramm — baut inhaltlich auf Punkt 3 (Neukunden-Erkennung) und Punkt 4 (additiver Rabatt-Mechanismus) auf, daher erst danach sinnvoll — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
6. Bewertungen/Testimonials — isoliert, keine Abhängigkeiten, eher Marketing-Politur als dringendes Feature — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben)
7. Zwei-Faktor-Login fürs Team — bewusst zuletzt, sicherheitskritisch und verdient ungeteilte Aufmerksamkeit ohne parallel wartende Punkte — ✅ **umgesetzt** (07.08.2026, siehe eigener Eintrag oben) — **Roadmap komplett**

### Vorgeschlagene Ideen, alle mittlerweile umgesetzt (siehe Umsetzungsreihenfolge oben)
- Bewertungen/Testimonials, Empfehlungsprogramm, „Nochmal buchen"-Button und Zwei-Faktor-Login — alle vier ursprünglich hier vorgeschlagenen Ideen sind jetzt Teil der am 07.08.2026 abgeschlossenen 7-Punkte-Umsetzungsreihenfolge (Details in den jeweiligen Einträgen oben)

## Portfolio-Performance behoben (07.08.2026) ✅
Erster Punkt der zweiten Runde (siehe Analyse weiter unten). Alle vier gefundenen Ursachen behoben:
1. **Bildverkleinerung beim Upload** — `savePortfolioImage()` in `lib/uploads.ts` verkleinert jetzt serverseitig auf max. 2400px Kantenlänge und re-encodiert als WebP (Qualität 82), statt das oft mehrere MB große Kamera-Original 1:1 zu speichern. Neue direkte Abhängigkeit: `sharp` (war vorher nur transitiv über Next.js selbst vorhanden, jetzt explizit in `package.json`)
2. **Admin-Vorschaubilder** (`PortfolioItemRow.tsx`) laufen jetzt über `next/image` statt über normale `<img>`-Tags
3. **Bilder im Bearbeiten-Formular werden jetzt erst gerendert, wenn die Zeile tatsächlich aufgeklappt wird** (React-State am `<details onToggle>`, vorher lagen sie unsichtbar aber ladend im DOM) — mit Playwright verifiziert: vor dem Aufklappen 0 Bild-Elemente im DOM, danach korrekt vorhanden
4. **Pagination**: Admin-Portfolio-Seite jetzt seitenweise (20 pro Seite, mit Zurück/Weiter), öffentliche Portfolio-Seite rendert anfangs nur 24 Einträge mit „Mehr laden"-Button (Kategorie-Filter bleibt dabei clientseitig voll funktionsfähig, da weiterhin alle Metadaten geladen werden — nur die tatsächlich ins DOM gerenderten Karten sind begrenzt)

Mit einem synthetischen 4000×3000-Testbild verifiziert: Ergebnis nach Upload war 2400×1800 WebP mit 7,8 KB (vorher wäre das Original 1:1 gespeichert worden). Testdaten (Portfolio-Eintrag, Bilddatei, Testkonto) danach entfernt.

## Testimonials: Profilbild + Fließband-Ansicht (07.08.2026) ✅ — zweite Runde komplett
Letzter Punkt der zweiten Runde. `Testimonial`-Model bekam ein neues `imagePath`-Feld. Admin-Formulare (Neu anlegen + Bearbeiten) haben jetzt ein Datei-Feld für ein rundes Profilbild, mit Hinweistext direkt im Formular: „Am besten ein Bild der Kundin/des Kunden selbst oder eines der beim Shooting entstandenen Fotos." Bild ist optional — ohne Bild zeigt die Karte einen schlichten Kreis mit dem ersten Buchstaben des Namens statt eines Layout-Sprungs.

Neue Upload-Funktion `saveTestimonialImage()` in `lib/uploads.ts`, nach demselben öffentlichen Muster wie Portfolio-/SEO-Bilder (kein Login nötig, läuft im öffentlichen Marquee auf der Startseite) — quadratisch auf 200×200 zugeschnitten und als WebP gespeichert.

Startseiten-Anzeige von einem statischen 2-spaltigen Grid auf ein durchlaufendes Fließband (rechts nach links, endlos) umgebaut: reines CSS (`@keyframes rk-marquee` in `globals.css`), keine neue Bibliothek. Für einen nahtlosen Loop wird die Kundenstimmen-Liste im Markup einmal dupliziert und die Animation läuft exakt über die erste Hälfte. Pausiert bei Hover/Fokus, damit sich ein Zitat in Ruhe lesen lässt; `prefers-reduced-motion` war schon vorher global abgedeckt. Auswahl, welche Kundenstimmen überhaupt im Banner laufen, bleibt wie geplant über den bestehenden „Sichtbar"-Schalter geregelt — keine zusätzliche Auswahl-Logik nötig.

Mit Wegwerf-Mitarbeiterkonto verifiziert: Kundenstimme mit hochgeladenem Testbild angelegt → Admin-Liste zeigt den runden Avatar korrekt → Startseite zeigt die Kundenstimme im Fließband, Avatar erscheint zweifach (einmal pro Duplikat für den nahtlosen Loop) → per curl direkt gegen die ausgelieferte Seite bestätigt, dass Name und Zitat im echten HTML stehen (eine erste Playwright-Prüfung hatte fälschlich „nicht gefunden" gemeldet, reines Timing-Artefakt, kein echter Bug). Testbild und Testdaten danach vollständig entfernt.

Damit ist die komplette zweite Runde (alle vier vom Nutzer nachträglich gewünschten Punkte: Portfolio-Performance, Foto-Ordner ohne Auftrag, „Meine Fotos", Testimonials-Fließband) abgeschlossen.

## Foto-Ordner ohne Auftrag + „Meine Fotos" aufgewertet (07.08.2026) ✅
Zweiter und dritter Punkt der zweiten Runde, zusammen umgesetzt (hängen an denselben Datenfeldern).

**Foto-Ordner ohne Auftrag:** `ClientPhotoFolder` bekam ein neues `sortOrder`-Feld (Titel-Feld `label` gab es schon, dient jetzt offiziell als „Titel"). Das `AdminPhotoPanel` (bisher nur auf der Auftragsseite eingebunden) ist jetzt zusätzlich direkt auf der Kundendetailseite (`/admin/clients/[clientId]`) eingebunden — Rakku kann Bestandskunden, mit denen schon vor einem Auftrag im System fotografiert wurde, direkt einen Ordner zuweisen. Beim Zuweisen sind jetzt Titel (vorausgefüllt mit dem Nextcloud-Ordnernamen, aber änderbar) und Datum der Fotos abfragbar; beides nachträglich über eine „Bearbeiten"-Zeile änderbar. Neue Pfeiltasten zum Sortieren — bewusst pro Kunde skopiert (anders als bei FAQ/Testimonials, wo global sortiert wird), da Ordner verschiedener Kunden sich nicht gegenseitig einordnen lassen.

**Berechtigung:** da derselbe Mechanismus jetzt von zwei verschiedenen Admin-Bereichen aus erreichbar ist (Auftragsseite = `orders`, Kundendetailseite = `clients`), zählt für alle Foto-Ordner-Aktionen jetzt entweder die eine oder die andere Berechtigung — keine neue eigene Berechtigung eingeführt. Dabei einen echten, durch die Erweiterung selbst verursachten Bug gefunden und sofort behoben: die Nextcloud-Ordner-Browse-API-Route prüfte weiterhin nur `orders`, wäre für einen Mitarbeiter mit ausschließlich `clients`-Berechtigung also mit 401 fehlgeschlagen.

**„Meine Fotos" aufgewertet:** Übersichtsseite zeigt jetzt pro Ordner ein Vorschaubild (erstes Foto im Ordner, über Nextclouds eigenen schnellen Vorschaubild-Dienst), Titel und Datum statt nur eines nackten Ordnernamens als Text-Link. Detailseite bekam einen „Zurück"-Link sowie Titel/Datum/Fotoanzahl als Unterzeile. Bewusst normale `<img>`-Tags statt `next/image` für die Vorschaubilder — next/image würde serverseitig proxyen und dabei das Session-Cookie verlieren, das dieselbe authentifizierte Route wie die Foto-Galerie selbst benötigt (bestehendes, bewusstes Muster aus `PhotoGalleryClient` übernommen).

End-to-End mit Wegwerf-Kunde (bewusst **ohne** Auftrag angelegt, um genau den Anwendungsfall zu testen) und Wegwerf-Mitarbeiterkonto gegen einen echten, bereits vorhandenen Nextcloud-Ordner (`Marcel`) verifiziert: Ordner ohne Auftrag zugewiesen, Titel/Datum gesetzt und nachträglich bearbeitet, zweiter Ordner zum Sortieren hinzugefügt und Sortierung verifiziert, beide entfernt — dabei den oben genannten Berechtigungs-Bug entdeckt und noch vor dem finalen Test behoben. Kundenseite zeigt Titel, Datum, Fotoanzahl und Vorschaubild korrekt auf der Übersicht sowie auf der Detailseite. Alle Testdaten (Kunde, Mitarbeiter, Position, Foto-Ordner-Verweise — keine echten Nextcloud-Dateien angefasst, nur gelesen) danach entfernt.

## Neu angefragt, zweite Runde (07.08.2026) — Analyse abgeschlossen, Umsetzung läuft
Nutzer hat direkt „mach weiter" gesagt, bevor er zum Testen der ersten Runde kam — Umsetzung läuft daher parallel zum Testen.

### Neu angefragt (zweite Runde, 07.08.2026) — alle vier Punkte jetzt umgesetzt, siehe Einträge oben

**1. Testimonials erweitern — Bild + Fließband-Ansicht + Bild-Empfehlung — ✅ umgesetzt (07.08.2026)**

**2. Foto-Ordner für Kunden ohne Auftrag — ✅ umgesetzt (07.08.2026), siehe eigener Eintrag oben**

**3. „Meine Fotos" (Kundenkonto) optisch aufwerten — ✅ umgesetzt (07.08.2026), siehe eigener Eintrag oben**

**4. Portfolio-Performance-Problem — ✅ umgesetzt (07.08.2026), siehe eigener Eintrag oben**

## Startseite ergänzt (V1-Content-Nachtrag)
- "Ablauf"-Sektion (4 Schritte: Anfrage/Planung/Shooting/Auswahl) + "Cosplay Photography"-Philosophie-Sektion, beide 1:1 aus V1 migriert
- **Bewusst nicht übernommen:** VTuber/Creator-Bereich aus V1 — Nutzer hat sich dagegen entschieden, V2 bleibt reine Fotografie-Seite. Nicht erneut vorschlagen, außer der Nutzer bringt es selbst auf.

## Bekannte offene Punkte (siehe auch Artifact-Plan „Offene Punkte")
- Quantify-Font: geklärt, Datei liegt vor, eingebunden (nur Headlines)
- V1 hat eine unwatermarkte Originaldatei in voller Auflösung öffentlich unter `/uploads/` liegen (`lordfischy-official_14.jpg`) — dem Nutzer gemeldet, noch keine Entscheidung/Aktion
- E-Signatur-Umsetzung: entschieden (eigene, kostenlose Lösung)
- ELSTER-Anbindung: bewusst nicht im Scope, außer der Nutzer sagt explizit, dass er sie will

## Server-Fakten (für spätere Sessions, nicht aus dem Code ersichtlich)
- **Live-Preview: https://v2.rakku.de** — läuft jetzt dauerhaft über pm2 (Prozessname „rakku-v2"), nginx-Vhost + Let's-Encrypt-Zertifikat eingerichtet. Nach Code-Änderungen: `npx next build` dann `pm2 restart rakku-v2`
- V1 (Port 3008) bleibt unverändert die Live-Seite bis Phase 7 — v2.rakku.de ist ein komplett separater, zusätzlicher Vhost, rakku.de selbst wurde nicht angefasst
- Nextcloud-Zugang: App-Passwort auf dem bestehenden `Rakku`-Account (Name „rakku-v2-app"), kein eigener technischer Nextcloud-User nötig
- Reale Kundenordner liegen unter `[PHOTOGRAPHY]/<Kundenname>` in der Rakku-Nextcloud, inkl. `[VERTRÄGE]/[VORLAGEN]` mit DE+EN TFP-Vertrag
