# 08 – Fortschrittslog
Chronologisches Protokoll aller Migrationsschritte. Jede Session / jeder Commit hinterlässt einen Eintrag.
---
## 2026-06-12 – P6.6 Grandfathering aktiver Legacy-Abos ✅
**Phase:** P6 Daten-Migration (D-13, Kriterien vom Auftraggeber präzisiert)
**Status:** ✅ umgesetzt; Rehearsal gegen Produktiv-Snapshot bleibt P6.10.
- Neuer Command `legacy:grandfather-subscriptions` (`--dry-run`, `--as-of=`,
`--grace-months=12`, `--no-report`; JSON-Report nach
`storage/app/migration/grandfather-subscriptions-*.json`).
- Quelle ist ausschließlich das Rechnungsarchiv `legacy_invoices` (D-12):
jüngste Rechnung pro (Portal, Legacy-UPO) mit `payment_option.type =
recurring` und `user_payment_option.status = active`; `next_due_date`
max. 12 Monate überfällig, sonst stale → bleibt Archiv.
- Übernahme als `grandfathered` in `user_payment_options` mit
`current_period_end = next_due_date` und Beträgen der letzten
Legacy-Rechnung in `legacy_conditions` — der tägliche MAN-Kreis-Lauf
(`billing:generate-manual-invoices`, Phase 9D im Hauptprojekt)
fakturiert ab dann zum gewohnten jährlichen Rhythmus weiter.
- Versteckte Katalog-Platzhalter `LEGACY-{PE|BP}-{Artikel}` in
`payment_options`; Re-Runs aktualisieren statt duplizieren (D-18,
Replay kurz vor Relaunch).
- Dry-Run gegen aktuellen Test-Snapshot: 22 aktive jährliche
Vereinbarungen (49 € bis 1.190 €), davon 4 sofort fällig; 0 stale.
- Tests: `tests/Feature/Billing/GrandfatherLegacySubscriptionsTest.php`
(7 Tests, inkl. End-to-End: Migration → MAN-Rechnung mit
Legacy-Beträgen).
---
## 2026-05-04 – P6.5d Legacy-Rechnungen Vollimport + on-demand PDF ✅
**Phase:** P6 Daten-Migration + P4 Customer-Portal
**Status:** ✅ umgesetzt; finaler Vollständigkeitsnachweis erfolgt im P6.10 Staging-Rehearsal gegen aktuellen Produktiv-Snapshot.
### Was wurde gemacht
- Migration `2026_05_04_122603_add_legacy_pdf_payload_to_legacy_invoices_table` ergänzt `legacy_invoices.pdf_payload` und `pdf_generated_at`.
- `legacy:archive-invoices` importiert jetzt idempotent alle Legacy-Rechnungen mit Originalstatus, Beträgen, Datumsfeldern, Zahlart, User-Zuordnung über `legacy_import_map`, `raw_snapshot` und `pdf_payload`.
- `pdf_payload` enthält Snapshot-Daten aus `invoice`, `invoice_billing_address` und `user_payment`, damit Legacy-PDFs ohne Dateiübernahme aus DB-Daten erzeugt werden können.
- Der Command schreibt standardmäßig einen JSON-Report unter `storage/app/private/migration/legacy-invoices-*.json` mit Source-Count, Statusverteilung, Summe, Fehlern und unzugeordneten Legacy-Usern.
- Customer-Rechnungen erzeugen PDFs bei Abruf on demand über `App\Services\Billing\LegacyInvoicePdfRenderer`; vorhandene `pdf_path`-Dateien bleiben als Cache/Export weiter nutzbar. PDFs werden über `LegacyInvoicePdfController` inline ausgeliefert und im neuen Tab/Fenster geöffnet.
- Admin-Rechnungen (`admin.invoices.index`) zeigen jetzt eine echte Legacy-Archivübersicht mit Suche, Portal-/Status-/Mapping-/PDF-Filtern, Statistik-Karten, Warnhinweis für unzugeordnete Legacy-User, User-Link und PDF-Öffnen-Aktion.
- Navigation wurde getrennt: Der bestehende Archivbereich heißt im Admin und Customer-Bereich **Legacy Rechnungen**. Der Name **Rechnungen** bleibt für den späteren neuen Stripe-Rechnungskreis frei.
- Der Renderer orientiert sich an der alten Symfony-Funktion `sfpInvoicePdf` (`_show_pdf.php` / `_stern_show_pdf.php`): Kopf-/Fußdaten, Rechnungsadresse, Leistungsname, Leistungszeitraum, Rechnungsnummer, Netto/MwSt./Betrag, Zahlart, Status und Bankdaten.
### Verifikation
```bash
php vendor/bin/pint --dirty --format agent
php artisan test --compact tests/Feature/LegacyInvoiceArchiveCommandTest.php tests/Feature/CustomerPortalTest.php
# 6 passed / 28 assertions
php artisan test --compact tests/Feature/Admin/AdminLegacyInvoiceArchiveTest.php tests/Feature/CustomerPortalTest.php
# 6 passed / 27 assertions
php artisan test --compact tests/Feature/LegacyInvoiceArchiveCommandTest.php tests/Feature/Admin/AdminLegacyInvoiceArchiveTest.php tests/Feature/CustomerPortalTest.php
# 8 passed / 52 assertions
```
---
## 2026-05-04 – Tranche 4: PM-Veröffentlichungs-Block (Reject-Reason + Audit-Log + Review-Counter) ✅
**Phase:** P3 (Admin-UI) + P4 (Customer) + P5 (Domain-Services)
**Status:** ✅ umgesetzt – Basis für den vollständigen User-Login → Customer-PR-Erstellung → Admin-Freigabe-Flow.
### Was wurde gemacht
**1) Reject mit Pflicht-Begründung** (`resources/views/livewire/admin/press-releases/show.blade.php`):
- `rejectReason`-Property im Volt mit `required|string|min:5|max:2000`-Validation.
- Reject-Modal um eine `flux:textarea` mit Beispieltext erweitert.
- Begründung wird an `PressReleaseService::reject()` durchgereicht; nach Reset auf leer gesetzt und Modal geschlossen.
**2) Mail-Templates auf `me.*`-Routen umgestellt**:
- `App\Mail\PressReleaseRejected::editUrl` und `App\Mail\PressReleasePublished::showUrl` wählen jetzt rollenbasiert: Admin/Editor → `admin.press-releases.*`, Customer → `me.press-releases.*`.
- Damit landen Customer-Empfänger nicht mehr auf einer 403-Admin-Seite.
**3) Audit-Log für PR-Status-Wechsel** (neuer Tabellen- und Modell-Layer):
- Migration `2026_05_04_115118_create_press_release_status_logs_table` mit `press_release_id`, `changed_by_user_id` (NULL bei System-Aktionen), `from_status`, `to_status`, `reason`, `source` (`admin`, `customer`, `blacklist`), `created_at` + Indexen.
- Model `App\Models\PressReleaseStatusLog` mit Casts auf `PressReleaseStatus`-Enum, BelongsTo zu `PressRelease` und `User` (`changedBy`).
- `PressRelease::statusLogs()` als HasMany, sortiert `desc` nach `created_at`.
- `PressReleaseService` schreibt bei jedem Statuswechsel einen Log-Eintrag (`submitForReview`, `publish`, `reject`, `backToDraft`, `archive`, `changeStatusFromAdmin`) mit korrekter Quelle (admin/customer/blacklist) und User-ID aus `Auth::id()`.
**4) Customer-UX**:
- `customer.press-releases.show`:
- Eigene Card mit dem **prominenten Reject-Grund** (rote Callout) inklusive Datum.
- Action-Card: "Erneut einreichen" für `rejected`-Status, "Zur Prüfung einreichen" für `draft` – mit Edit-Button daneben.
- **Verlauf-Card** mit allen Status-Wechseln (Badge in passender Farbe + Datum + Reason).
**5) Admin-UX**:
- `admin.press-releases.show`: zusätzliche **Status-Verlauf-Card** mit From-/To-Status, Datum, Bearbeiter:in und Quelle (`customer`/`blacklist` als Mini-Badge sichtbar, `admin` als Default ausgeblendet).
- `admin.press-releases.index`: Quick-Actions `publish`/`reject`/`archive` gehen jetzt **konsequent durch `PressReleaseService`** – keine direkten `update()`-Calls mehr, damit Audit-Log + Mail + Cache-Invalidation immer greifen.
**6) Review-Counter in der Sidebar**:
- `AdminPerformanceCache::pressReleaseReviewCount()` als gecachte Aggregat-Methode (`StatsTtl = 60s`) + neue Cache-Key-Konstante `PressReleaseReviewCount`.
- Sidebar zeigt für Admin/Editor neben "Pressemitteilungen" einen gelben Badge mit der Anzahl wartender Reviews; Klick darauf führt direkt auf `?status=review`.
- `PressReleaseService::logStatusChange()` invalidiert beide Caches (`PressReleaseStats`, `PressReleaseReviewCount`).
### Tests
- **Neu** (`tests/Feature/PressReleaseWorkflowTest.php`, 10 Cases):
- Submit-for-Review schreibt Audit-Log mit `source=customer` und `changed_by_user_id`.
- Admin Reject mit Begründung: Status, Log, gequeuete Mail mit Reason.
- Reject ohne Begründung: Validation-Fehler, Status bleibt `review`.
- Publish-Mail nutzt `/admin/me/press-releases/...` für Customer-Empfänger.
- Reject-Mail nutzt `/admin/me/press-releases/.../edit` für Customer-Empfänger.
- Resubmit-Cycle (Draft → Review → Rejected → Review) erzeugt drei Audit-Einträge in korrekter Reihenfolge.
- Customer-Show-Page zeigt prominent den Reject-Grund + "Erneut einreichen"-Button.
- Admin-Index-Quick-Action `publish`: Status, Log, ChangedBy.
- Admin-Index-Quick-Action `reject`: Audit-Eintrag mit Default-Reason.
- `pressReleaseReviewCount()`-Helper liefert korrekte Anzahl.
### Test-Suite
- **180 Tests grün, 3 geskippt, 0 Fehler** (`php artisan test --compact`).
- `vendor/bin/pint --dirty --format agent` → fixed (kosmetik), Suite weiterhin grün.
### Plan-Updates
- `03-MIGRATION-PLAN.md`: P5.1 (`PressReleaseService`) um Audit-Log-Vermerk + Reject-Reason erweitert.
- Status-Verlauf ist jetzt die zentrale Anlaufstelle für späteres "Wer hat wann was geändert"-Reporting.
---
## 2026-05-04 – Tranche 3: Panel-Konsolidierung ✅
**Phase:** Architekturschritt vor dem Pressemitteilungs-Veröffentlichungs-Block.
**Status:** ✅ umgesetzt
### Was wurde gemacht
**Routen-Konsolidierung** (`routes/customer.php`):
- Alle vormaligen `/customer/*`-Pfade wandern unter den gemeinsamen Prefix **`/admin/me/*`** mit Routenamen-Prefix **`me.*`**.
- Mapping:
- `/customer/` → `/admin/me` (`me.dashboard`)
- `/customer/press-releases[/...]` → `/admin/me/press-releases[/...]`
- `/customer/invoices` → `/admin/me/invoices`
- `/customer/tokens` → `/admin/me/tokens`
- `/customer/profile` → `/admin/me/profile`
- `/customer/security` → `/admin/me/security`
- Alte `/customer/*`-Pfade als **301-Redirects** erhalten – Bookmarks und externe Links bleiben funktional.
- Middleware-Stack `auth + verified + EnsureUserIsCustomer + LogSlowAdminRequests` – `EnsureUserIsCustomer` lässt admin/editor/customer durch; daher haben Admins/Editor automatisch Zugriff auf "Mein Bereich" für ihre eigenen Daten.
**Rollenbasierter Login-Redirect** (`app/Http/Responses/RoleAwareLoginResponse.php`):
- Neuer `LoginResponse`-Contract (im `FortifyServiceProvider` als Singleton gebunden).
- Logik: Admin/Editor → `/dashboard`, Customer → `/admin/me`. Intended-URL wird respektiert, sofern sie nicht auf den Default-Home zeigt.
- Analoge Anpassung in `MagicLinkConsumeController` (Magic-Link-Login) und `VerifyEmailController` (Mail-Verifikation).
**Sidebar konsolidiert** (`resources/views/components/layouts/app/sidebar.blade.php`):
- "Mein Bereich" jetzt **für alle Panel-User sichtbar** (auch Admin/Editor) – sie haben dort ihre eigenen PMs/Profile/Tokens.
- Admin-spezifische Bereiche (Content, CRM, Billing, Administration, Reports) werden **nur für `canAccessAdmin()`-User** gerendert. Customer sehen ein schlankes Menü "Mein Bereich".
- Alle Customer-Items nutzen die neuen `me.*`-Routenamen.
**Volt-Components physisch unverändert**:
- `resources/views/livewire/customer/...` bleibt unter dem `customer.*`-View-Pfad bestehen (Volt-Component-Names referenzieren weiterhin die Dateipfade). Nur die HTTP-Routen-Definitionen ändern sich.
- View-interne Verlinkungen (`route('customer.*')` → `route('me.*')`) wurden in allen Volt-Files umgestellt.
### Tests
- **Neu** (`tests/Feature/PanelConsolidationTest.php`, 11 Cases):
- `/customer/*` → `/admin/me/*` als 301-Redirect (Dashboard, PMs Liste/Create/Show/Edit, Profile/Security/Tokens/Invoices)
- `me.dashboard` benötigt Login
- Customer kann `me.dashboard` aufrufen, aber nicht `dashboard` (Admin-Bereich → 403)
- Admin und Editor können beide Dashboards aufrufen
- Inaktive User → 403
- Sidebar-Sichtbarkeit: Admin sieht "Administration"; Customer nicht
- URL-Resolution-Asserts für alle `me.*`-Routen
- **Magic-Link-Login** (`tests/Feature/Auth/MagicLinkLoginTest.php`):
- Test umgebaut: Admin-Magic-Link → `/dashboard`
- Neuer Case: Customer-Magic-Link → `/admin/me`
### Test-Suite
- **170 Tests grün, 3 geskippt, 0 Fehler** (`php artisan test --compact`).
- `vendor/bin/pint --dirty --format agent` → fixed (kosmetik in `VerifyEmailController`), Suite weiterhin grün.
### Architektur-Notiz
- Mit dieser Konsolidierung ist `/admin/*` der einzige Backend-Prefix; "Mein Bereich" (`/admin/me/*`) ist eine Eigentümer-Sicht innerhalb desselben Panels. Die Volt-Component-Konvergenz (Customer-PMs ↔ Admin-PMs auf gleicher Component mit Policy-Scope) ist explizit **nicht** Teil dieser Tranche – sie wird im PM-Veröffentlichungs-Block angegangen, weil dort die echte UI-Verzahnung benötigt wird.
- Damit liegt jetzt die Basis, um den **PM-Veröffentlichungs-Block** (User-Login → Customer-PR-Erstellung → Admin-Freigabe/Bearbeitung) auf einer einheitlichen Panel-Architektur umzusetzen.
---
## 2026-05-04 – Architektur-Entscheidung: ein gemeinsames Admin-Panel mit rollenbasierter Sichtbarkeit
Der Auftraggeber hat festgelegt: **es gibt nur ein gemeinsames Admin-Panel** unter dem Pressekonto-Backend. Admins, Editoren und Customer arbeiten im selben UI – Sichtbarkeit von Menüpunkten und Aktionen entscheidet die Rolle/Permission.
- Aktuell laufen Admin-/Editor-Funktionen unter `/admin/*` und Customer-Funktionen unter `/customer/*`. Diese Trennung wird **vor dem Pressemitteilungs-Veröffentlichungs-Block** in eine **gemeinsame Panel-Architektur** überführt.
- Sidebar wird rollenbasiert gefiltert (Admin/Editor sehen alle PMs, Customer nur seine etc.). Routen-Konsolidierung Customer → Admin-Panel.
- **Reihenfolge:**
1. Köpfe (Categories Create/Edit, Footer-Codes-CRUD) abschließen ← *erledigt*.
2. Panel-Konsolidierung: gemeinsamer Prefix, rollenbasierte Sichtbarkeit, Customer-Routen ins Admin-Panel ziehen ← *erledigt 2026-05-04*.
3. Pressemitteilungs-Veröffentlichungsblock (User-Login + Customer-PR + Admin-Freigabe/Bearbeitung) auf der konsolidierten Architektur.
---
## 2026-05-04 – Tranche 2 (Köpfe): Categories Create/Edit + Footer-Codes-CRUD ✅
**Phase:** P3 (Admin-UI)
**Status:** ✅ umgesetzt
### Was wurde gemacht
**Categories Create/Edit (P3.3):**
- `CategoryTranslation::uniqueSlug(string $source, string $locale, ?int $ignoreCategoryId = null)` als zentraler Slug-Helper pro Locale.
- Volt-Components `admin.categories.create` und `admin.categories.edit`:
- DE+EN-Translations mit Live-Slug-Preview (`updatedNameDe`/`updatedNameEn`),
- Portal-Auswahl (`Both`/`presseecho`/`businessportal24`),
- Parent-Auswahl mit **Hierarchie-Loop-Schutz** (kein Self-Parent, keine Sub-Kategorie als Parent),
- Aktiv/Inaktiv-Toggle,
- Optionale Beschreibungen DE/EN,
- Lösch-Schutz: nicht löschbar, solange PMs oder Sub-Kategorien verknüpft sind.
- Index erweitert: `Kategorie anlegen`-Button und `Bearbeiten`-Pencil pro Karte; klickbare Karte für PM-Filter weiterhin per Overlay-Link.
- Routen `admin.categories.create` + `admin.categories.edit` (`routes/admin.php`).
**Footer-Codes-CRUD (P3.11):**
- Migration `2026_05_04_112430_create_footer_codes_table` legt `footer_codes` (`portal`, `language`, `title`, `content`, `is_global`, `is_active`, `priority`, Soft-Deletes, Legacy-Tracking) + Pivot `category_footer_code` an.
- Model `FooterCode` mit `belongsToMany(Category::class)`, Casts (`Portal`, Booleans).
- Factory `FooterCodeFactory` mit `global()`/`inactive()` States.
- Volt `admin.footer-codes.index` mit Suche, Portal-/Status-Filter (Aktiv/Inaktiv/Global), Karten-Statistiken (Gesamt/Aktiv/Global), `toggleActive`.
- Volt `admin.footer-codes.create` + `admin.footer-codes.edit`:
- Stammdaten (Titel, Content, Portal, Sprache, Priorität),
- Globaler Schalter (deaktiviert Kategorie-Bindung),
- Mehrfach-Auswahl von Kategorien per Checkboxen,
- Soft-Delete mit Bestätigung im Edit.
- Sidebar: `Footer-Codes`-Eintrag direkt unter `Kategorien`.
- Routen `admin.footer-codes.index/create/edit` (`routes/admin.php`).
### Tests
- `tests/Feature/Admin/AdminCategoryManagementTest.php` – 8 Cases: Render Create, Create mit beiden Translations, Pflichtfeld-Validation, Slug-Unique pro Locale bei Kollision, Edit + Slug-Rename, Parent-Loop-Schutz, Lösch-Schutz bei verknüpften PMs, erfolgreicher Delete.
- `tests/Feature/Admin/AdminFooterCodeManagementTest.php` – 9 Cases: Render Index/Create, Kategorie-gebundene Footer-Codes, Global ignoriert Kategorien, Pflichtfelder, Edit + Re-Sync der Kategorien, Toggle Global entfernt Pivot-Einträge, Soft-Delete, Toggle-Active aus Index.
### Test-Suite
- **158 Tests grün, 3 geskippt, 0 Fehler** (`php artisan test --compact`).
- `vendor/bin/pint --dirty --format agent` → passed.
### Plan-Update
- `03-MIGRATION-PLAN.md`: P1.3 erweitert um `footer_codes` + `category_footer_code`. P3.3 und P3.11 auf ✅ gesetzt.
- `dev/migration 2026/08-PROGRESS.md`: dieser Eintrag.
---
## 2026-05-04 – Scope-Entscheidung: Newsletter vertagt
Der Auftraggeber hat entschieden, dass **NewsletterService (P5.3)** und der **Admin-Newsletter-Workflow (P3.7)** vorerst zurückgestellt werden. Beide werden nach dem Pressemitteilungs-Veröffentlichungs-Block (Login + Customer-PR + Admin-Freigabe) wiederaufgenommen. Die heutige Newsletter-Sync-UI für Legacy-Listen bleibt unverändert.
Status in `03-MIGRATION-PLAN.md` ist auf `⏸️ Vertagt 2026-05-04` gesetzt.
---
## 2026-05-04 – Tranche 1: PR-Bilder + Blacklist + Slug-Trait + Public-Share-Link ✅
**Phase:** P3 (Admin-UI) + P4 (Customer) + P5 (Domain-Services) + P6.8 (Legacy-Bilder-Sync)
**Status:** ✅ umgesetzt
### Was wurde gemacht
#### Schritt 1 – PressRelease-Bilder durchgängig auf ImageService gestellt + Legacy-Sync (P3.2 / P3.4 / P5.5 / P6.8)
- `App\Services\Image\ImageService` erweitert um:
- `storePressReleaseImage(UploadedFile, pressReleaseId)` mit drei Varianten `thumb` (320×240), `medium` (800×600), `large` (1600×1200) per Cover-Resize (zentriert beschnitten, statt Letterbox).
- Generischer `generateVariants(...$cover = false)`-Pfad — Logos nutzen weiterhin `contained`, PR-Bilder `cover`.
- `deletePressReleaseImage()` und `generateMissingPressReleaseVariants()` für Sync- und Delete-Flows.
- Validierung mit eigenem Mime-Set (`image/jpeg|png|webp`) und 8 MB Maximum (`MAX_PRESS_RELEASE_IMAGE_BYTES`).
- `App\Models\PressReleaseImage` mit `url()` + `variantUrl(key)`-Helpers, fallback-fest auch für nicht-public Disks.
- `App\Http\Controllers\Api\V1\PressReleaseImageController` nutzt jetzt den `ImageService` (statt `Storage::disk('public')->put` + `getimagesize` direkt). API liefert `data.variants` und `data.urls.{thumb,medium,large,original}`.
- `App\Http\Resources\PressReleaseImageResource` zeigt Varianten-URLs öffentlich; `data.url` bleibt rückwärtskompatibel.
- `StorePressReleaseImageRequest::rules` von `max:5120` auf `max:8192` erhöht (synchron mit ImageService).
- Admin-Logo-Upload (`livewire/admin/companies/edit`) auf `ImageService::storeCompanyLogo()` umgestellt – inkl. Variant-Generierung, Logo-Entfernen-Button und „Logo entfernen rückgängig"-Flow.
- Wiederverwendbare Volt-Komponente `livewire/components/press-release-images-manager` für Multi-Image-Upload, Sortierung (Hoch/Runter), Preview-Flag setzen, Löschen. Wird in Admin-PR-Edit (`livewire/admin/press-releases/edit`) und Customer-PR-Edit (`livewire/customer/press-releases/edit`) per `` eingebunden. Authorisierung über `PressReleasePolicy::update`; Customer kann nur Draft/Rejected ändern, Admin alles außer Archived.
- Neuer Command `legacy:sync-press-release-images` (`App\Console\Commands\SyncPressReleaseImages`) analog zu `legacy:sync-company-logos`:
- `--portal=presseecho|businessportal24|all`, `--dry-run`, `--force`, `--skip-variants`, `--limit=N`.
- Kopiert nur referenzierte Legacy-Dateien aus `storage/app/public/{portal}/press_release/` nach `storage/app/public/press-releases/{prId}/images/{filename}`, generiert per `ImageService` die Varianten und aktualisiert `press_release_images` (Pfad + Varianten + Bildmaße).
- Bereits migrierte Bilder, denen Varianten fehlen, werden nachträglich versorgt.
- Berichtet Zahl der ungenutzten Quelldateien — Anhaltspunkt für Cleanup-Skripte.
- `App\Services\Import\PressReleaseImporter` setzt jetzt `legacy_portal`/`legacy_id` auch auf `press_release_images`, damit der Sync-Command stabil matchen kann.
- Tests: erweiterter `tests/Feature/Api/V1/PressReleaseImageApiTest` (Variantenprüfung, Cleanup beim Delete) + neuer `tests/Feature/SyncPressReleaseImagesTest` mit 4 Cases (Happy-Path inkl. Varianten, Dry-Run, Missing-Files → Failure, „bereits migriert + Varianten fehlen → werden generiert").
#### Schritt 2 – BlacklistService + Integration in Publish-Flow (P5.2)
- `config/blacklist.php` als initiale Wortliste (Stub – vom Auftraggeber zu erweitern oder später per Admin-UI pflegbar).
- `App\Services\PressRelease\BlacklistService`:
- case-insensitiver, ganzwörtlicher Vergleich (Sonderzeichen werden ignoriert, Mehrwort-Phrasen werden korrekt erkannt).
- Lazy-Load der Wortliste, dependency-injectable (Tests injizieren ihre eigene Liste, in Prod kommt sie aus der Config).
- `App\Services\PressRelease\PressReleaseService::submitForReview()` und `publish()` rufen jetzt `BlacklistService::findInPressRelease()` auf:
- Treffer → PM wird automatisch auf `rejected` gesetzt, Autor bekommt `PressReleaseRejected`-Mail mit Begründung („unzulässiges Wort … gefunden"), und es wird eine `BlacklistViolationException` geworfen.
- Neue `App\Services\PressRelease\BlacklistViolationException` mit dem getroffenen Wort als Property — Caller können sie gezielt abfangen ohne andere LogicExceptions mit zu erwischen.
- Caller-Schicht: Customer-PR-Detail (`livewire/customer/press-releases/show`), Customer-Liste (`.../index`), Admin-Edit (`livewire/admin/press-releases/edit`) und Admin-Detail (`livewire/admin/press-releases/show`) reagieren mit `session()->flash('error', …)` auf die Exception.
- Tests: `tests/Feature/PressReleaseBlacklistTest` mit 4 Cases (BlacklistService direkt, Submit mit verbotenem Wort, sauberer Publish, Phrase erkannt).
#### Schritt 3 – `HasUniqueSlug`-Trait (P5.6)
- Neuer Trait `App\Models\Concerns\HasUniqueSlug` mit `generateUniqueSlug(string $source, array $scope = [])`:
- Models definieren `slugScopeAttributes()` und `slugFallback()`.
- Berücksichtigt automatisch das eigene Modell, wenn es bereits gespeichert ist (verhindert „eigene Existenz blockiert sich selbst").
- Hook `applySlugConstraints(Builder)` für künftige Spezialfälle.
- `App\Models\PressRelease`: scope `['portal', 'language']`, fallback `pressemitteilung`.
- `App\Models\Company`: scope `['portal']`, fallback `company`.
- Caller umgestellt: `livewire/customer/press-releases/create`, `livewire/admin/press-releases/{create,edit}`, `livewire/admin/companies/{create,edit}` rufen jetzt `(new Model)->generateUniqueSlug(...)` bzw. `$model->generateUniqueSlug(...)` auf — die alten privaten Helfer sind raus.
- `Category` bleibt aussen vor (Slugs liegen in Translations-Tabelle, separater Mechanismus). `CompanyImporter` behält seinen eigenen Slug-Pfad (legacy-Slug bevorzugt).
- Tests: `tests/Feature/HasUniqueSlugTest` mit 4 Cases (Scope-Einhaltung, Edit hält eigenen Slug, Cross-Portal-Fallthrough, Fallback bei leerem Source).
#### Schritt 4 – Public Share-Link für PMs via Magic-Link
- `App\Services\Auth\MagicLinkGenerator::createPressReleaseShareLink(PressRelease, ?User, int $ttlDays = 7)`:
- Token + SHA-256-Hash, gleiche Sicherheitscharakteristik wie der Login-Link.
- `MagicLink.purpose = 'press_release_access'`, `payload = {press_release_id}`.
- Default 7 Tage gültig, kein Login-Effekt.
- Neue Public-Route `GET /pm-vorschau/{token}` (regex 40+ chars) → `App\Http\Controllers\PressReleasePreviewController` rendert read-only Blade-View `press-release-preview`.
- Bei ungültigem/abgelaufenem Token wird `press-release-preview-error` mit `404` bzw. `410 Gone` ausgeliefert.
- Funktioniert auch für `draft`/`review`/`rejected`/`archived`-PMs, jedoch ohne Authentifizierung — das ist der Sinn („den Kollegen den Entwurf zeigen").
- Customer-PR-Detail (`livewire/customer/press-releases/show`) hat jetzt Button „Vorschau-Link", der den Link generiert und im UI zum Kopieren anzeigt + Ablaufzeitpunkt.
- Standalone-Blade-Views nutzen reines CSS (kein Vite-Manifest, kein FluxUI), damit der Link auch unabhängig vom Build funktioniert. Dark-Mode-tauglich via `prefers-color-scheme`. `` schützt vor Indexierung.
- Tests: `tests/Feature/PressReleasePreviewTest` mit 5 Cases (gültig + Inhalt sichtbar, kein Auth nötig, abgelaufen → 410, ungültiger Token → 404, force-deleted PM → 404).
#### Cleanup
- Neue `pint.json` schließt die Symfony-1.4-Legacy-Ordner (`_businessportal24.com`, `_presseecho.com`, `dev/_old`, `dev/migration 2026/_archiv`) aus, damit Pint keine PHP-5.6-Parse-Errors mehr meldet. Pint läuft jetzt grün durch (`vendor/bin/pint --dirty --format agent` → `result: passed`).
### Test-Lage
- Komplette Suite: **141 passed, 3 skipped, 0 failed** (vorher: 124 / 3). Neu hinzugekommen: 17 Tests verteilt über die 4 Schritte. Das Reporting des Slow-Admin-Logs und API-Usage bleibt unberührt.
- Pint sauber, Linter ohne neue Findings.
### Aufgeräumte Plan-Punkte (siehe `03-MIGRATION-PLAN.md`)
- ✅ P3.2 (PM-Bilder im Customer-Portal pflegen)
- ✅ P3.4 (Admin-Firmen-Logo via ImageService)
- ✅ P5.2 (BlacklistService)
- ✅ P5.5 (ImageService inkl. PR-Varianten)
- ✅ P5.6 (Slug-Trait)
- ✅ P6.8 (Legacy-PM-Bilder-Sync-Command)
- ✅ Public-Vorschau-Link (`Feature aus 06-FEATURES-SCOPE.md – Tabelle 4`)
### Offen (ohne externen Input)
- P5.3 NewsletterService neu (subscribe/confirm/unsubscribe + Admin-CSV-Export).
- P3.3 Admin Categories Create/Edit (Index existiert).
- P3.7 Admin-Newsletter-Workflow (heute nur Sync-UI).
- P3.11 Admin-CRUD für Footer-Codes.
- P5.7 Domain-Events (`PressReleasePublished` / `Rejected` / `SubmittedForReview`).
- Failure-Alerting für Queue-Worker (P9.3) + Supervisor-Setup-Doku.
### Wartet auf externen Input
- P0.5–0.7 Stripe / Produktliste / Grandfathering – blockiert die gesamte Phase 8.
- P11.2 Staging-Server, P11.4 Mailtexte für Go-Live.
---
## 2026-05-04 – Test-Suite stabilisiert + Customer-Portal abgeschlossen ✅
**Phase:** Querschnitt (Stabilisierung) + P4 (Customer-Portal) + P5.5 (ImageService)
**Status:** ✅ umgesetzt
### Was wurde gemacht
#### Paket A – Test-Suite stabilisiert
- `phpunit.xml`: `APP_URL=https://pressekonto.test` ergänzt, damit `route('login')` etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.
- `tests/Feature/Auth/EmailVerificationTest.php`: Tests werden jetzt sauber per `markTestSkipped()` übersprungen, solange `Features::emailVerification()` in `config/fortify.php` deaktiviert ist. Sobald das Feature aktiviert wird, laufen die Tests automatisch wieder.
- `tests/Feature/ExampleTest.php`: testet jetzt den Health-Endpoint `/up` statt der von Vite/Theme-Layout abhängigen Startseite.
- Leerer `tests/Feature/Feature/` / `tests/Feature/Feature/Billing/`-Doppelpfad entfernt.
- Admin-Dummy-Views `admin.invoices.index`, `admin.payments.index`, `admin.coupons.index` durch ehrliche „In Vorbereitung / Vertagt"-Stubs ersetzt. Keine Fake-Daten mehr; `admin.invoices.index` zeigt zusätzlich die Anzahl bereits archivierter Legacy-Rechnungen.
#### Paket B – Customer-Portal abgeschlossen (P4.6 + P4.7 + P4.8) und ImageService-Fundament (P5.5)
- Neuer `App\Services\Image\ImageService` (GD, ohne neue Composer-Dependency):
- `storeCompanyLogo(UploadedFile, portal, companyId)` speichert Original + generiert Logo-Varianten `sq` (256×256) und `wide` (640×320) auf dem `public`-Disk unter `company-logos/{portal}/{companyId}/...`
- aspect-ratio-erhaltend mit transparentem Letterbox/Pillarbox
- `deleteCompanyLogo()` räumt Original + Varianten zuverlässig auf
- `MAX_LOGO_BYTES = 4 MB`, validierte Mime-Types JPEG/PNG/WebP/GIF
- `customer.profile`-Page (`resources/views/livewire/customer/profile.blade.php`):
- bisher read-only → jetzt voll editierbar
- User-Felder: Name, Sprache, Anrede, Vor-/Nachname, Titel, Telefon, Adresse, Land, Backlink-URL, USt-ID, Show-Stats, Disable-Footer-Code
- Companies-Liste zeigt Rolle und Eigentümer-Badge; nur Eigentümer/Responsible können bearbeiten
- Inline Company-Editor mit Logo-Upload, Logo-Vorschau und Logo-Entfernen-Aktion (befüllt `companies.logo_path` + `companies.logo_variants`)
- Neue `customer.security`-Page (`resources/views/livewire/customer/security.blade.php`):
- Passwort ändern (mit `current_password`-Rule)
- E-Mail-Adresse ändern (setzt `email_verified_at = null` und löst, falls aktiviert, Fortify-Verifikationsmail aus)
- Zwei-Faktor-Authentifizierung aktivieren/deaktivieren, QR-Code anzeigen, Recovery-Codes regenerieren
- Customer-Sidebar um „Profil" und „Sicherheit" erweitert.
- `routes/customer.php`: neue Route `customer.security`.
#### P4.8 – Policies eingeführt
- Neue Policies: `PressReleasePolicy`, `CompanyPolicy`, `ContactPolicy`, `LegacyInvoicePolicy`.
- Customer-Komponenten ersetzt verstreute `where('user_id', auth()->id())`-Filter durch `$this->authorize(...)`-Aufrufe:
- `customer.press-releases.show` (view + submitForReview)
- `customer.press-releases.edit` (update)
- `customer.invoices` (downloadPdf)
- `PressReleasePolicy` kennt eigene Ability `submitForReview` und Admin-Ability `publish` (verknüpft mit `press-releases:publish` Permission).
### Verifikation
```bash
vendor/bin/pint --dirty --format agent
# unsere Dateien automatisch formatiert; Errors nur in _businessportal24.com (Legacy-Referenz, nicht unser Scope)
php artisan test --compact
# 124 passed, 3 skipped (vorher 108 passed, 12 failed)
php artisan test --compact tests/Feature/CustomerProfileSecurityTest.php
# 7 passed (Profil-Editor, Logo-Upload + Variants, Company-Auth-Negativtest, Security-Render, Passwort-Change, PressRelease- und LegacyInvoice-Policy-Negativtests)
```
### Plan-Status nach dieser Session
- P3.4 Logo-Upload mit Varianten: Customer-Pfad ✅; Admin-Pfad nutzt jetzt dieselbe Service-Schicht und kann nachgezogen werden.
- P4.6 Customer-Profile/Company-Pflege ✅
- P4.7 Customer-Security (Passwort/2FA/E-Mail) ✅
- P4.8 Policies ✅ für PressRelease, Company, Contact, LegacyInvoice
- P5.5 `ImageService`-Fundament ✅ (Logos); PressRelease-Bilder können auf gleicher Service-Klasse aufsetzen.
### Nächster Schritt
- P8 (Stripe/Billing) bleibt der größte Releaseblocker; benötigt P0.5–0.7 (Stripe-Test-Account, Produktliste, Grandfathering-Kriterien) vom Auftraggeber.
- Ohne externe Inputs gut machbar:
- PressRelease-Bilder-Upload im Customer-/Admin-Portal über `ImageService` ergänzen (mehrere Bilder, Alt-Texte, Sortierung)
- `legacy:sync-press-release-images` als Pendant zu `legacy:sync-company-logos`
- Admin-Logo-Upload/-Edit auf `ImageService` umstellen
- `BlacklistService` (P5.2) und neuer `NewsletterService` (P5.3)
---
## 2026-04-30 – Admin Users: Datenstandsfilter für Pressemitteilungen ✅
**Phase:** P3 (Admin-UI / User-Verwaltung)
**Status:** ✅ umgesetzt
### Was wurde gemacht
- Datenstandsfilter in der User-Übersicht um Pressemitteilungen erweitert:
- `Mit Pressemitteilungen`
- `Ohne Pressemitteilungen`
- `Mit veröffentlichten PMs`
- `Ohne veröffentlichte PMs`
- Veröffentlichte PMs werden statusbasiert über `PressReleaseStatus::Published` gefiltert.
- Regressionstest ergänzt, der User mit veröffentlichter PM korrekt ein- und ausschließt.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 22 passed
```
---
## 2026-04-30 – Admin Users: Filter für PM-Freigabe ✅
**Phase:** P3 (Admin-UI / User-Verwaltung)
**Status:** ✅ umgesetzt
### Was wurde gemacht
- Neue Berechtigung `press-releases:publish` ergänzt.
- Seeder weist `press-releases:publish` den Rollen `admin` und `editor` zu.
- User-Übersicht hat jetzt einen Berechtigungsfilter:
- `Kann PMs freigeben`
- Der Filter berücksichtigt:
- Super-Admins
- Rollen mit `press-releases:publish`
- direkt zugewiesene User-Permissions mit `press-releases:publish`
- In der Rollen-Spalte erscheint bei passenden Usern ein Badge `PM-Freigabe`.
- Rollen-/Permissiondaten werden eager geladen, damit die Badge-Ausgabe keine N+1-Queries erzeugt.
### Verifikation
```bash
vendor/bin/pint --format agent database/seeders/RolesAndPermissionsSeeder.php resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 22 passed
```
---
## 2026-04-30 – Admin Users: Veröffentlichte PMs in Übersicht ✅
**Phase:** P3 (Admin-UI / User-Verwaltung)
**Status:** ✅ umgesetzt
### Was wurde gemacht
- User-Übersicht zeigt jetzt pro Benutzer die Anzahl veröffentlichter Pressemitteilungen.
- Umsetzung performant über `withCount()` mit Alias `published_press_releases_count` und Status-Constraint auf `published`.
- Entwürfe/andere Status werden in dieser Badge bewusst nicht mitgezählt.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 22 passed
```
---
## 2026-04-30 – Admin Users: Detail-Modal erweitert ✅
**Phase:** P3 (Admin-UI / User-Verwaltung)
**Status:** ✅ umgesetzt
### Was wurde gemacht
- User-Detail-Modal deutlich übersichtlicher strukturiert:
- Header mit Account-, Portal-, Registrierungstyp- und Legacy-Informationen.
- Kennzahlenkarten für Firmen, Kontakte, Pressemitteilungen, API-Tokens und Zahloptionen.
- getrennte Karten für Account/Zugriff, Legacy-Profil, Rechnungsadresse und Datenqualität.
- Firmenbereich fachlich erweitert nach `User → Firmen → Kontakte / Pressemitteilungen`:
- Firmen zeigen Rolle, Portal, Kontaktanzahl, PM-Anzahl und Status.
- Kontakte werden je Firma direkt angezeigt.
- aktuelle Pressemitteilungen werden je Firma mit Status, Datum, Portal und Hits angezeigt.
- Daten werden weiterhin eager geladen, damit im Modal keine Blade-seitigen Zusatzqueries entstehen.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 22 passed
```
---
## 2026-04-30 – Admin Users: Workflow-Filter und Bearbeitungsübersicht ✅
**Phase:** P3 (Admin-UI / User-Verwaltung)
**Status:** ✅ umgesetzt
### Was wurde gemacht
- User-Übersicht erweitert um Rollenfilter und Datenstandsfilter:
- ohne/mit Firma
- ohne/mit Legacy-Profil
- ohne Kontakte
- ohne Rechnungsadresse
- Tabelle verdichtet:
- Benutzername, E-Mail und ID in einer Benutzer-Spalte.
- Zuordnungsstatus zeigt Firmen- und Kontaktanzahl.
- Datenqualität zeigt Profil- und Rechnungsstatus direkt als Badges.
- Filter können direkt zurückgesetzt werden; aktive Filter werden sichtbar markiert.
- User-Edit-Seite ergänzt um eine Bearbeitungsübersicht mit Sprungpunkten zu Account, Profil, Firmen, Kontakten und Rechnung.
- Edit-Flow zeigt Status-Badges für Aktivität, Portal, Profil, Firmen/Kontakte und Rechnungsadresse.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users.blade.php resources/views/livewire/admin/users/edit.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 22 passed
```
---
## 2026-04-29 – Bugfix: Leere Profil-Datumsfelder bleiben leer ✅
**Phase:** P3 (Admin-UI / User-Edit)
**Status:** ✅ behoben
### Problem
In `admin.users.edit` konnten leere Legacy-Profil-Datumsfelder (`birthdate`, `validation_date`, `contract_date`) im Date-Input als aktuelles Tagesdatum erscheinen.
### Was wurde gemacht
- Profil-Datumsfelder werden im Volt-State jetzt als leere Strings statt `null` initialisiert.
- Beim Laden eines Profils werden fehlende Datumswerte explizit als `''` gesetzt.
- Beim Speichern werden leere Strings weiterhin als `null` in `profiles` persistiert.
- Die drei Datumsfelder nutzen `flux:date-picker type="input"` mit `clearable`, weil `flux:input type="date"` leere Werte sichtbar mit dem Tagesdatum vorbelegen konnte.
- Regressionstest ergänzt: leere Profil-Datumsfelder bleiben leer und werden nicht als heutiges Datum gespeichert.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users/edit.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 21 passed
```
---
## 2026-04-29 – Admin Users: Firmenstatus + Detail-Modal ✅
**Phase:** P3 (Admin-UI / UX)
**Status:** ✅ User-Übersicht verbessert
### Was wurde gemacht
- `admin.users` zeigt jetzt in der Übersicht, ob ein Benutzer mit Firmen verknüpft ist:
- grünes Badge mit Anzahl bei vorhandenen Firmen
- neutrales Badge „Keine Firma" ohne Verknüpfung
- Der bisherige Auge-Link navigiert nicht mehr auf die Detailseite, sondern öffnet ein Flux-Modal direkt in der Übersicht.
- Das Modal enthält die wichtigsten Informationen aus der bisherigen Detailseite:
- Basisdaten, Status, Portal, Typ
- Legacy-Profil-Kurzübersicht
- Rechnungsadresse
- verknüpfte Firmen inkl. Rolle und Kontakte
- direkter Link zur Bearbeiten-Seite
- Die bestehende `admin.users.show`-Route bleibt weiterhin vorhanden, wird aus der Übersicht aber nicht mehr als Standard-Flow genutzt.
### Verifikation
```bash
vendor/bin/pint --format agent resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 20 passed
```
---
## 2026-04-29 – Legacy-User-Profile nachgezogen ✅
**Phase:** P6/P3 (Datenmigration + Admin-UI)
**Status:** ✅ `sf_guard_user_profile` wird jetzt als eigenes Profil importiert und im Admin editierbar
### Was wurde gemacht
- `UserImporter` erweitert:
- liest die relevanten Felder aus `sf_guard_user_profile`
- schreibt/aktualisiert die Daten in `profiles`
- mappt Legacy-Anreden (`salutation_id`) auf `mr`/`mrs`/`none`
- mappt die wichtigsten Legacy-Länder-IDs auf die neuen Country-Codes (`DE`, `AT`, `CH`, ...)
- bereinigt Textfelder und behandelt leere/ungültige Datumswerte sicher
- `Profile` um Factory-Unterstützung ergänzt
- `ProfileFactory` ergänzt
- `admin.users.edit` erweitert:
- lädt `user.profile`
- zeigt die Legacy-Profilfelder als eigenen Abschnitt an
- speichert Änderungen per `profile()->updateOrCreate()`
- löscht ein Profil nur, wenn alle Profilfelder bewusst geleert wurden
- Tests ergänzt:
- Import-Test für `sf_guard_user_profile` → `profiles`
- Admin-User-Edit-Test für Laden/Speichern der Profilfelder
### Verifikation
```bash
vendor/bin/pint --format agent app/Models/Profile.php database/factories/ProfileFactory.php app/Services/Import/UserImporter.php resources/views/livewire/admin/users/edit.blade.php tests/Feature/Admin/UserManagementTest.php tests/Feature/LegacyUserProfileImportTest.php
# passed
ulimit -n 65535 && php artisan test --compact tests/Feature/LegacyUserProfileImportTest.php
# 1 passed
ulimit -n 65535 && php artisan test --compact tests/Feature/Admin/UserManagementTest.php
# 19 passed
```
### Hinweis
Kombinierte Testläufe ohne ausreichend hohes File-Descriptor-Limit sind in dieser Umgebung mit `Too many open files` in PHPUnit/Pest abgestürzt. Die betroffenen Tests laufen in frischen Prozessen mit `ulimit -n 65535` sauber durch.
---
## 2026-04-29 – Dokumentationsabgleich gegen echten Code-Stand ✅
**Phase:** Querschnitt / Projektsteuerung
**Status:** ✅ zentrale Migrationsdokumente synchronisiert
### Was wurde gemacht
Der aktuelle Code-Stand wurde gegen die Migrationsdokumentation abgeglichen. Bestätigt wurden insbesondere:
- P2.5/P2.7: Admin-Schutz, Portal-Scoping, Portal-Switcher und Go-Live-Mail-Command vorhanden
- P3: Dashboard, PressRelease-CRUD/-Workflow, Users/Roles, Companies/Contacts und Categories-Index weitgehend produktiv angebunden
- P4: Customer-Portal-Grundumfang inkl. PMs, Legacy-Rechnungsarchiv und API-Token Self-Service vorhanden
- P5.1: `PressReleaseService` und Published/Rejected-Mailables vorhanden
- P6/P7: Import-Kern, Verify, API v1, Usage-Logging, Rate-Limit und API-Kundenreport vorhanden
Aktualisierte Dokumente:
- `03-MIGRATION-PLAN.md` – Status-Spalten auf echten Stand nachgezogen
- `README.md` – Schnellüberblick aktualisiert und Dokumente 09/10 ergänzt
- `04-DATA-MODEL.md` – `contact_user` dokumentiert
- `07-API-MIGRATION.md` – Legacy-Cutover einheitlich auf `410 Gone` korrigiert
- `09-REVIEW-QUALITAET.md` – als historisches Review markiert
- `MIGRATION-STEPS.md` – aktuelles Kurz-Runbook konsolidiert
### Weiter offen
- P8 Stripe/Billing/Invoicing bleibt der größte technische Blocker
- Grandfathering-Kriterien, Medienübernahme und Staging-Rehearsal sind vor Go-Live weiter offen
- API-Kundenfreigabe benötigt fachliche Klärung für die meisten Legacy-API-User
---
## 2026-04-29 – P7 Abschlussprüfung API v1 ✅
**Phase:** P7 (API v1)
**Status:** ✅ P7 technisch abgeschlossen
### Entscheidung Newsletter-Unsubscribe
`newsletter/unsubscribe` wird nicht als Legacy-API-Endpoint umgesetzt:
- es gibt keinen relevanten Legacy-API-Client für Newsletter-Unsubscribe
- Newsletter wird später im neuen Frontend neu implementiert
- die Anbindung erfolgt dann über einen sauberen Newsletter-Dienst
### P7-Status
Abgeschlossen:
- Sanctum API v1 mit Kern-Endpoints
- Legacy-Key-Cutover (`410 Gone`)
- API-Dokumentation (`docs/api/v1.yml`)
- Legacy-API-Kundenreport
- Token-Freigabe nur für aktive/zahlende bzw. freigegebene User
- Runtime-Sperre inaktiver User
- API-Usage-Logging
- API-Usage-Reporting
- Rate-Limiting pro Token
- Image-Minimal-Endpoints
Nicht mehr Teil von P7:
- `newsletter/unsubscribe`
- spätere Newsletter-Neuentwicklung / externer Newsletter-Dienst
- Bildderivate/Thumbnails/erweiterte Medienverwaltung
- Admin-Reporting-UI für API-Nutzung
### Ergebnis
Nach aktuellem Scope ist in P7 nichts Technisches mehr offen.
---
## 2026-04-29 – P7 Images: Minimal-API-Endpoints ✅
**Phase:** P7 (API v1)
**Status:** ✅ Minimaler Image-Scope umgesetzt
### Was wurde gemacht
Neue Endpoints:
```bash
GET /api/v1/press-releases/{pressRelease}/images
POST /api/v1/press-releases/{pressRelease}/images
DELETE /api/v1/press-release-images/{pressReleaseImage}
```
Regeln:
- List benötigt `press-releases:read`
- Upload/Delete benötigt `press-release-images:write`
- nur Bilder eigener Pressemitteilungen
- Upload/Delete nur bei `draft` oder `rejected`
- Upload nur JPEG/PNG/WebP bis maximal 5 MB
- Dateien werden auf dem `public`-Disk unter `press-releases/{id}/images` gespeichert
- `PressReleaseResource` liefert geladene Bilder inkl. URL mit aus
OpenAPI-Doku (`docs/api/v1.yml`) und API-Migrationsplan wurden angepasst.
### Verifikation
```bash
php artisan test --compact tests/Feature/Api/V1/PressReleaseImageApiTest.php tests/Feature/Api/V1/PressReleaseApiTest.php tests/Feature/Api/V1/CustomerDataApiTest.php tests/Feature/ApiAccessSecurityAndLoggingTest.php tests/Feature/ApiUsageReporterTest.php tests/Feature/ApiDocumentationTest.php
# 18 passed
```
### Nächster Schritt
- P7-Abschlussentscheidung dokumentieren
- Optional später: Bildderivate/Thumbnails, Sortierung, Alt-Texte und Admin-UI erweitern
---
## 2026-04-29 – P7.7 API-Rate-Limiting pro Token ✅
**Phase:** P7 (API v1 / Security & Reporting)
**Status:** ✅ Rate-Limit pro Sanctum-/Bearer-Token ergänzt
### Was wurde gemacht
Neue Middleware `EnsureApiTokenRateLimit`:
- hängt in der `/api/v1`-Gruppe nach `auth:sanctum` und nach der Aktivitätsprüfung
- limitiert auf 60 Requests pro Minute
- segmentiert bevorzugt nach Sanctum-/Bearer-Token-ID
- nutzt als Fallback einen SHA-256-Fingerprint des Bearer-Tokens, danach User-ID/IP
- liefert bei Überschreitung HTTP 429 inkl. `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`
Damit kann ein einzelner API-Client begrenzt werden, ohne automatisch alle anderen Tokens desselben Users zu blockieren.
### Verifikation
```bash
php artisan test --compact tests/Feature/ApiAccessSecurityAndLoggingTest.php tests/Feature/ApiUsageReporterTest.php tests/Feature/CustomerPortalTest.php tests/Feature/Api/V1/CustomerDataApiTest.php tests/Feature/Api/V1/PressReleaseApiTest.php
# 16 passed
```
### Nächster Schritt
- P7-Abschlussentscheidung dokumentieren
- Optional: Admin-Reporting-UI auf Basis von `ApiUsageReporter` ergänzen
---
## 2026-04-29 – P7 Reporting: API-Usage-Report ✅
**Phase:** P7 (API v1 / Security & Reporting)
**Status:** ✅ Auswertbarer API-Nutzungsreport ergänzt
### Was wurde gemacht
Neue zentrale Auswertung `ApiUsageReporter`:
- aggregiert `api_usage_logs`
- unterstützt Filter nach Zeitraum, User-ID und HTTP-Statuscode
- liefert Summary, Top-Pfade, Statuscodes, Top-User, Top-Tokens und letzte Requests
Neuer Command:
```bash
php artisan api:usage-report
php artisan api:usage-report --from=2026-04-01 --to=2026-04-30
php artisan api:usage-report --user=123
php artisan api:usage-report --status=403
php artisan api:usage-report --no-report
```
Der JSON-Report wird standardmäßig unter `storage/app/private/migration/api-usage-*.json` abgelegt.
### Verifikation
```bash
php artisan test --compact tests/Feature/ApiUsageReporterTest.php
# 2 passed
```
### Nächster Schritt
- P7-Abschlussentscheidung dokumentieren
---
## 2026-04-29 – P7 Security: API-Key-Freischaltung + Usage-Logging ✅
**Phase:** P7 (API v1 / Security & Reporting)
**Status:** ✅ API-Zugang abgesichert und Nutzung persistent protokolliert
### Was wurde gemacht
#### API-Token-Erstellung abgesichert
Neue zentrale Prüfung `ApiAccessEligibilityService`:
- User muss aktiv sein (`is_active = true`)
- API-Token-Erstellung ist nur möglich bei:
- aktivem neuen Zahlungsstatus (`user_payment_options.status = active` im gültigen Zeitraum)
- aktivem Bestandsschutz (`status = grandfathered`, `grandfathered_until >= heute`)
- Übergangsweise: letzte importierte Legacy-Rechnung ist `paid`
Die Customer-Seite `customer.tokens` zeigt bei fehlender Freigabe einen Hinweis und erstellt serverseitig keinen Token.
#### Bestehende Tokens inaktiver User blockiert
Neue Middleware `EnsureApiUserIsActive`:
- hängt an der `/api/v1`-Gruppe nach `auth:sanctum`
- blockiert Requests inaktiver User mit HTTP 403
- schützt auch dann, wenn ein User bereits früher einen Sanctum-Token erzeugt hatte
#### API-Usage-Logging
Neue Tabelle `api_usage_logs` + Middleware `LogApiUsage`:
- loggt alle `/api/*` Requests inkl. 410-Legacy-Key-Ablehnungen und 403-Inactive-User-Blocks
- speichert User-ID, Sanctum-Token-ID, Methode, Pfad, Route, Statuscode, IP, User-Agent, Dauer, Zeitpunkt
- speichert keine Bearer-Tokens, keine Legacy-`api_key`s und keine Payloads
### Verifikation
```bash
php artisan test --compact tests/Feature/ApiAccessSecurityAndLoggingTest.php tests/Feature/CustomerPortalTest.php tests/Feature/Api/V1/CustomerDataApiTest.php tests/Feature/Api/V1/PressReleaseApiTest.php
# 13 passed
```
### Nächster Schritt
- Migration auf Ziel-/Staging-DB ausführen (`api_usage_logs`)
- Optional: Admin-Reporting-UI für API-Nutzungsstatistiken ergänzen
---
## 2026-04-29 – P7.10 Legacy-API-Kundenreport vorbereitet ✅
**Phase:** P7 (API v1 / Kundenkommunikation)
**Status:** ✅ Datenbankbasierter Report-Command + Dokumentation angelegt
### Was wurde gemacht
Nach Legacy-Codeprüfung wurde entschieden, die API-Kundenliste nicht aus App-Logs abzuleiten:
- Symfony-Prod-Logging ist deaktiviert (`logging_enabled: false`, `sfNoLogger`)
- API-Zugriffe werden nicht applikationsseitig persistiert
- `X-ApiKey` wird in Standard-Webserver-Logs typischerweise nicht gespeichert
Neuer Command:
```bash
php artisan api:legacy-customers-report
```
Der Command ermittelt aus der migrierten DB:
- Kandidaten: `users.registration_type = apiuser` oder Rolle `api-only`
- automatisch freigabefähig: User aktiv + letzte Legacy-Rechnung `paid`
- `needs_review`: aktiver API-Kandidat ohne archivierte Legacy-Rechnung
- `blocked`: inaktiv oder letzte Rechnung nicht bezahlt
Der JSON-Report wird nach `storage/app/private/migration/legacy-api-customers-*.json` geschrieben.
Separate Dokumentation:
- `dev/migration 2026/10-LEGACY-API-KUNDEN.md`
### Aktuelle Datenlage
- 195 importierte `apiuser`
- 1 API-User mit Legacy-Rechnung
- 1 automatisch freigabefähiger API-User nach aktueller Regel
### Verifikation
```bash
php artisan test --compact tests/Feature/LegacyApiCustomerReporterTest.php
# 2 passed
```
### Nächster Schritt
- Report gegen die migrierte DB ausführen und Ergebnis fachlich prüfen
- Falls API-User nicht selbst Rechnungsempfänger waren: Legacy-Zuordnung API-User → zahlender Vertrags-/Billing-User nachliefern oder zusätzlich importieren
- Kommunikationsmail für `eligible`-Kunden vorbereiten
---
## 2026-04-29 – P7.1 Legacy-API-Log-Auswertung vorbereitet ✅
**Phase:** P7 (API v1)
**Status:** ✅ Analyse-Tooling implementiert; echte Produktiv-Logs noch externer Input
### Was wurde gemacht
Neuer Artisan-Command:
```bash
php artisan api:analyze-legacy-access-logs /path/to/access.log "/path/to/access.log.*" --top=20
```
- parst Apache/Nginx-Access-Logs nach Legacy-API-Routen wie `pressrelease/list`, `pressrelease/create`, `company/list`, `newsletter/subscribe`
- erkennt `api_key`-Query-Parameter
- schreibt keine Klartext-API-Keys in Reports, sondern nur SHA-256-Fingerprints
- aggregiert:
- Top-Endpunkte
- Top-Client-IPs
- Statuscodes
- Anzahl eindeutiger API-Key-Fingerprints
- maskierte Beispielrequests
- JSON-Report wird standardmäßig nach `storage/app/private/migration/legacy-api-access-*.json` geschrieben
- `--no-report` für reine Konsolenausgabe
### Verifikation
```bash
php artisan test --compact tests/Feature/LegacyApiAccessLogAnalyzerTest.php tests/Feature/ApiDocumentationTest.php
# 3 passed
```
### Nächster Schritt
- Produktiv-Access-Logs nur auswerten, falls später noch verfügbar; sie sind kein P7-Blocker mehr
- API-Kundenliste wird sicher aus migrierten Daten und Legacy-Codeprüfung abgeleitet
---
## 2026-04-29 – P7.8 API-Dokumentation ergänzt ✅
**Phase:** P7 (API v1)
**Status:** ✅ OpenAPI-Dokumentation verfügbar
### Was wurde gemacht
- OpenAPI-Spezifikation unter `docs/api/v1.yml` angelegt
- Web-Route `GET /docs/api/v1` ergänzt, damit der `docs_url` aus dem Legacy-Key-410-Handler erreichbar ist
- Customer-Token-Seite um Link zur API-Dokumentation erweitert
- Dokumentiert sind die aktuell implementierten API-v1-Endpunkte:
- `GET|POST /api/v1/press-releases`
- `GET|PATCH|DELETE /api/v1/press-releases/{pressRelease}`
- `GET /api/v1/companies`
- `GET /api/v1/companies/{company}`
- `GET /api/v1/categories`
- `POST /api/v1/newsletter/subscribe`
### Verifikation
```bash
php artisan test --compact tests/Feature/ApiDocumentationTest.php tests/Feature/CustomerPortalTest.php tests/Feature/Api/V1/PressReleaseApiTest.php tests/Feature/Api/V1/CustomerDataApiTest.php
# 10 passed
```
### Nächster Schritt
- Erledigt in Folgeeinträgen: Legacy-API-Kundenreport, Rate-Limiting, Image-Minimal-Endpoints und P7-Abschlussentscheidung
---
## 2026-04-28 – P7 gestartet: API v1 mit Sanctum + Legacy-Key-Cutover ✅
**Phase:** P7 (API v1)
**Status:** ✅ Kern-Endpunkte implementiert
### Was wurde gemacht
#### API-Routing aktiviert
- `routes/api.php` wird jetzt in `bootstrap/app.php` als API-Routendatei geladen
- Sanctum Ability-Middleware-Aliase (`abilities`, `ability`) registriert
- BasicAuth überspringt `/api/*`, da API-Zugriffe über Sanctum Bearer Tokens geschützt sind
#### Legacy-Key-Cutover (P7.6)
Neue Middleware `RejectLegacyApiKeys`:
- erkennt `api_key` Query-Parameter und `X-Api-Key` Header
- antwortet vor Sanctum-Auth mit HTTP **410 Gone**
- liefert `migration_url` zum Customer-Token-Bereich und `docs_url`
#### API v1 Endpunkte
Neue `/api/v1`-Endpunkte:
- `GET /api/v1/press-releases`
- `POST /api/v1/press-releases`
- `GET /api/v1/press-releases/{pressRelease}`
- `PATCH /api/v1/press-releases/{pressRelease}`
- `DELETE /api/v1/press-releases/{pressRelease}`
- `GET /api/v1/companies`
- `GET /api/v1/companies/{company}`
- `GET /api/v1/categories`
- `POST /api/v1/newsletter/subscribe`
Alle schreibenden/lesenden Aktionen prüfen Sanctum-Abilities (`press-releases:*`, `companies:read`, `newsletter:subscribe`) und begrenzen Customer-Daten serverseitig auf eigene Firmen/Pressemitteilungen.
#### Resources & Validation
- `PressReleaseResource`, `CompanyResource`, `CategoryResource`
- `StorePressReleaseRequest`, `UpdatePressReleaseRequest`, `SubscribeNewsletterRequest`
- Slug-Erzeugung für API-Pressemitteilungen inkl. Kollisionsschutz pro Portal/Sprache
### Verifikation
```bash
php artisan test --compact tests/Feature/Api/V1/PressReleaseApiTest.php tests/Feature/Api/V1/CustomerDataApiTest.php tests/Feature/CustomerPortalTest.php
# 9 passed
```
### Nächster Schritt
- P7.8: API-Dokumentation/OpenAPI ergänzen
- P7.1/P7.10: Legacy-Access-Logs auswerten und API-Kunden-Kommunikation vorbereiten
- Optional: Rate-Limiting pro Token ergänzen (P7.7)
---
## 2026-04-28 – P4 erweitert: API-Tokens + Legacy-Rechnungen im Customer-Portal ✅
**Phase:** P4 (Customer-Portal)
**Status:** ✅ Self-Service für API-Tokens und Archivrechnungen umgesetzt
### Was wurde gemacht
#### API-Token Self-Service (P4.5)
Neue Customer-Seite: `customer.tokens`
- Sanctum Personal Access Tokens erstellen
- Scope-Auswahl pro Token:
- `press-releases:read`
- `press-releases:write`
- `press-release-images:write`
- `companies:read`
- `newsletter:subscribe`
- Plaintext-Token wird nur direkt nach Erstellung angezeigt
- Tokens können vom eigenen User widerrufen werden
#### Legacy-Rechnungsarchiv im Kundenkonto (P4.4)
Neue Customer-Seite: `customer.invoices`
- zeigt ausschließlich `legacy_invoices` des eingeloggten Users
- Statistik-Kacheln: Anzahl, Archivsumme, bezahlte Rechnungen, PDFs
- Suche nach Rechnungsnummer + Statusfilter
- PDF-Download vorbereitet: Wenn `pdf_path` vorhanden und Datei existiert, wird über Storage ausgeliefert; sonst erscheint ein Hinweis
#### Navigation & Rollen
- Customer-Sidebar um „Rechnungen" und „API-Tokens" erweitert
- `routes/customer.php` um `customer.invoices.index` und `customer.tokens.index` ergänzt
- `RolesAndPermissionsSeeder` um `newsletter:subscribe` ergänzt, damit API-Scope und Permission-Set zusammenpassen
### Verifikation
```bash
php artisan test --compact tests/Feature/CustomerPortalTest.php
# 2 passed
```
### Nächster Schritt
- P7 starten: API v1 mit Sanctum-Abilities, API-Resources und 410-Gone-Handler für alte `api_key`s
- Danach: Customer-Token-Seite mit Link zur API-Doku verbinden
---
## 2026-04-28 – Phase 6 abgeschlossen: Vollständiger Datenmigrations-Import ✅
**Phase:** P6 (Datenmigration)
**Status:** ✅ Beide Portale vollständig migriert
### Was wurde gemacht
#### PressReleaseImporter – Timestamps und Slug-Fix
- `withoutTimestamps` + `created_at`/`updated_at` aus Legacy-DB (wie Companies/Contacts)
- **Slug-Kollisions-Fix bei `--force`**: Beim Update-Lauf wird der bestehende Slug des Records beibehalten (`existingPr?->slug`). Neu: `uniqueSlug()` nimmt optionalen `excludeId`-Parameter.
- `legacy:fix-timestamps` um `press-releases` Entität erweitert
#### Businessportal24 – Companies + Contacts importiert
```bash
php artisan legacy:import --source=businessportal24 --step=companies --force
# 66.525 Firmen | 0 Fehler | 488s
php artisan legacy:import --source=businessportal24 --step=contacts --force
# 8.597 Kontakte | 0 Fehler | 40s
```
#### Pressemitteilungen – beide Portale
```bash
php artisan legacy:import --source=presseecho --step=press-releases --force
# 74.384 importiert | 5.649 übersprungen (kein User in DB) | 0 Fehler
php artisan legacy:import --source=businessportal24 --step=press-releases --force
# 100.304 aktualisiert | 1 Fehler (Slug-Kollision → behoben)
```
#### legacy:archive-invoices (P6.5)
Neuer Command: archiviert alle Legacy-Rechnungen in `legacy_invoices` (read-only Archiv).
- `safeDateOrNull()` behandelt Legacy-Datum `0000-00-00` → null (MySQL-Strict-Mode Fix)
- 299 Rechnungen Presseecho + 565 Businessportal24 = **864 Rechnungen** archiviert
> Nachtrag 2026-05-04: Dieser Stand ist nur als erster Archivlauf/Vorlauf zu werten. Für den Go-Live wurde P6.5d ergänzt: vollständiger Legacy-Rechnungsimport inkl. Status, User-Zuordnung, PDF-Renderdaten und Import-Report; PDF-Erzeugung bleibt für Legacy-Rechnungen DB-basiert/on demand.
```bash
php artisan legacy:archive-invoices --dry-run # prüfen
php artisan legacy:archive-invoices # beide Portale
```
#### Post-Import-Bereinigung
```bash
php artisan legacy:import --step=link-associations --force
# 16.968 contact_user Einträge (beide Portale)
php artisan legacy:fix-timestamps # alle Entitäten, beide Portale
# 174.688 PMs + alle anderen → historische Timestamps korrekt
```
### Finaler Datenstand
| Entität | Presseecho | Businessportal24 | Gesamt |
|---|---|---|---|
| Users | 19.372 | 46.778 | **66.150** |
| Companies | 41.310 | 66.525 | **107.836** |
| Contacts | 8.367 | 8.597 | **16.964** |
| PressReleases | 74.384 | 100.304 | **174.688** |
| LegacyInvoices | 299 | 565 | **864** |
| contact_user Links | – | – | **16.968** |
| legacy_import_map | – | – | **369.150** |
### Timestamps-Verifikation
- PMs mit `created_at = heute`: **0** ✅
- Älteste PM: `2006-04-21` (Telekom WM-Sponsor) ✅
- Ältester User: `2010-07-23` ✅
### Konsistenz-Verifikation (P6.9)
Neuer Command: `php artisan legacy:verify`
- prüft Legacy-/Ziel-Counts, Legacy-Rechnungsarchiv, verwaiste `legacy_import_map`-Ziele, Ziel-Foreign-Keys, portalübergreifende User-Merges und Grandfathering-Zusammenfassung
- schreibt JSON-Reports nach `storage/app/private/migration/verify-*.json` (optional `--no-report`)
- unterstützt `--portal=presseecho|businessportal24|all` und `--skip-legacy` für lokale/testbare Ziel-DB-Checks
```bash
php artisan legacy:verify --no-report
# 0 Fehler | 0 Warnungen
```
### Vollständige Import-Reihenfolge (Go-Live Runbook)
```bash
php artisan legacy:import --source=presseecho --step=categories --force
php artisan legacy:import --source=all --step=users --force
php artisan legacy:import --source=presseecho --step=companies --force
php artisan legacy:import --source=businessportal24 --step=companies --force
php artisan legacy:import --source=presseecho --step=contacts --force
php artisan legacy:import --source=businessportal24 --step=contacts --force
php artisan legacy:import --source=presseecho --step=press-releases --force
php artisan legacy:import --source=businessportal24 --step=press-releases --force
php artisan legacy:import --step=link-associations --force
php artisan legacy:archive-invoices
php artisan legacy:fix-timestamps
php artisan legacy:verify
```
### Nächster Schritt
- P0.5–0.7: Stripe-Zugangsdaten + Produktliste vom Auftraggeber (blockiert P8)
- P7: API v1 (Endpoints, Sanctum Token-Abilities, 410 Gone Handler)
- P10: Security-Review + Test-Coverage
- P11: Staging-Deployment + Rehearsal-Lauf + Go-Live-Mailing
---
## 2026-04-27 – Import-Bugfixes + Timestamps + User-Kontakt-Verknüpfung
**Phase:** P6 (Datenmigration – Qualitätssicherung)
**Status:** ✅
### Was wurde gemacht
#### Import-Fehler behoben: phone/fax zu kurz (VARCHAR 40 → 255)
Beim echten Companies-Import (Presseecho) schlugen 20 Datensätze fehl, weil Legacy-Telefonnummern Beschreibungstext enthielten (bis 92 Zeichen, z.B. „0700 27862537 (0700 ARTMaker, zum Festnetztarif de)").
- **Migration** `2026_04_27_115212_expand_phone_fax_columns`: `phone` + `fax` in `companies` und `contacts` von VARCHAR(40) auf **VARCHAR(255)** erweitert.
- **`CompanyImporter` + `ContactImporter`**: neue `cleanText()`-Methode dekodiert HTML-Entities (` ` etc.) und kürzt auf die Feldlänge als letzten Fallback.
- Re-Import mit `--force`: 41.290 Firmen aktualisiert, 20 neu importiert → **0 Fehler**.
#### Timestamps aus Legacy-DBs übernehmen
**Problem:** Alle 119k importierten Datensätze hatten `created_at = heute`, da die Importers ursprünglich kein `withoutTimestamps` nutzten.
**Fixes:**
- `UserImporter`: `User::withoutTimestamps()` + `created_at`/`updated_at` aus `sf_guard_user` übernehmen.
- `CompanyImporter` (bereits vorhanden) + `ContactImporter` (bereits vorhanden): beide nutzen jetzt korrekt `withoutTimestamps`.
- Neuer Command **`legacy:fix-timestamps`** für bereits importierte Daten: nutzt Cross-DB-JOINs auf dem gleichen MySQL-Server – korrigiert 119.287 Datensätze in **2,6 Sekunden**.
```bash
php artisan legacy:fix-timestamps --dry-run # erst prüfen
php artisan legacy:fix-timestamps # alle Entitäten, beide Portale
php artisan legacy:fix-timestamps --entity=companies --portal=presseecho
```
**Ergebnis nach Fix:** Ältester User `2010-07-23`, älteste Firma `2011-07-05`, ältester Kontakt `2011-07-07`. ✅
#### Neuer Import-Schritt: `link-associations` (User↔Kontakt direkt)
**Hintergrund:** Im Legacy-System gab es keine direkte User→Kontakt-Tabelle. Die Zugehörigkeit war indirekt: User → company_user → company → contact. Im neuen System gibt es `contact_user`, das jetzt befüllt wird.
- Neuer Service **`UserAssociationLinker`**: ableitet User↔Kontakt-Links aus `company_user + contacts` via JOIN.
- `ImportLegacyData`-Command erweitert um `--step=link-associations` (letzter Schritt in `--step=all`).
- Idempotent via `insertOrIgnore` – manuell im Admin angelegte Links bleiben erhalten.
- **8.367 contact_user-Einträge** in 0,2 Sekunden erstellt.
```bash
php artisan legacy:import --step=link-associations --force
```
#### ImportContext erweitert
`ImportContext` unterstützt jetzt `portal = 'all'` (kein Legacy-DB-Zugriff nötig) für portalübergreifende Schritte wie `link-associations`.
### Vollständige Import-Reihenfolge (aktualisiert)
```bash
php artisan legacy:import --source=presseecho --step=categories --force
php artisan legacy:import --source=all --step=users --force
php artisan legacy:import --source=presseecho --step=companies --force
php artisan legacy:import --source=businessportal24 --step=companies --force
php artisan legacy:import --source=presseecho --step=contacts --force
php artisan legacy:import --source=businessportal24 --step=contacts --force
php artisan legacy:import --source=presseecho --step=press-releases --force
php artisan legacy:import --source=businessportal24 --step=press-releases --force
php artisan legacy:import --step=link-associations --force
php artisan legacy:fix-timestamps
```
### Verifikation
- Companies Presseecho: 41.310 importiert ✅
- Users (beide Portale): ~69.650 importiert ✅
- Timestamps: alle historisch korrekt ✅
- contact_user: 8.367 Verknüpfungen ✅
---
## 2026-04-27 – Admin-UI: Tabellen sortierbar + 100 pro Seite + Spalten-Fixes
**Phase:** P3 (Admin-UI Qualität)
**Status:** ✅
### Was wurde gemacht
#### Alle Admin-Tabellen: Pagination auf 100, sortierbare Spalten
Alle 6 Tabellen-Views auf **100 Einträge pro Seite** erhöht (vorher: 12–20). Flux-Pro `sortable` / `sorted` / `direction` Props mit `wire:click="sort('column')"` in alle Köpfe integriert.
| View | Sortierbare Spalten | Standard |
|---|---|---|
| Users | Vorname, E-Mail, Portal, Status, Letzter Login, Hinzugefügt | Hinzugefügt ↓ |
| Companies | Name, E-Mail, Status, PMs, Kontakte, Hinzugefügt | Name ↑ |
| Contacts | Name, E-Mail, Firma, Hinzugefügt | Nachname ↑ |
| PMs (Admin) | Titel, Status, Portal, Hits, Erstellt | Erstellt ↓ |
| Kategorien | Standard (ID), PMs | ID ↑ |
| PMs (Kunde) | Titel, Status, Erstellt | Erstellt ↓ |
#### Spalten-Fixes
**Companies Index:**
- „Website"-Spalte entfernt
- „Hinzugefügt" (created_at) hinzugefügt
**Contacts Index:**
- „Position"-Spalte entfernt
- „Hinzugefügt" (created_at) war bereits vorhanden und korrekt
**Users Index:**
- Bug behoben: Aktions-Icons erschienen in der „Erstellt"-Spalte (Datenzelle fehlte)
- Name aufgeteilt: „Vorname" + „Nachname" als separate Spalten (aus `name` gesplittet, kein separates DB-Feld)
- „Typ"-Spalte entfernt, Colspan im Empty-State korrigiert
**Company Show – Kontakte-Tab:**
- Input + Select + „Zuordnen"-Button ersetzt durch Flux-Combobox mit Live-Suche (ab 1 Zeichen, max. 50, Auswahl löst sofort `attachExistingContact()` aus)
#### Contacts Index – Company-Filter als Combobox
**Problem:** `Company::query()->orderBy('name')->get()` lud nach dem Import alle 82.820 Firmen in den Livewire-State → Livewire-Fehler.
**Fix:** Company-Filter jetzt als Flux-Combobox mit Backend-Suche. Ohne Eingabe: kein Load. Mit Eingabe: max. 50 Firmen live. Livewire-State enthält maximal 50 statt 82.820 Firmen.
---
## 2026-04-27 – Phase 6 gestartet: Legacy-Import-Infrastruktur + Dry-Run ✅
**Phase:** P6 (Datenmigration)
**Status:** ✅ Infrastruktur + Dry-Run abgeschlossen – bereit für echten Import
### Was wurde gemacht
**Voraussetzung erfüllt (P0.3):**
Legacy-DB-Connections funktionieren:
- `mysql_presseecho` → presseecho (43 Tabellen)
- `mysql_businessportal` → businessportal (42 Tabellen)
**Neue Dateien – Import-Infrastruktur (`app/Services/Import/`):**
- `ImportContext` – hält Portal, Connection, dry-run/force-Flags
- `ImportResult` – zählt importiert/übersprungen/aktualisiert/Fehler
- `UserImporter` – sf_guard_user + sf_guard_user_profile + Rollen (inkl. Gruppen-Mapping)
- `CompanyImporter` – company + company_user + responsible_company_user → Pivot mit Rollen
- `ContactImporter` – contact → contacts (Salutation-Mapping)
- `CategoryImporter` – category + category_translation → categories + translations (DE/EN)
- `PressReleaseImporter` – press_release + press_release_image + press_release_contact
**Master-Command:** `app/Console/Commands/ImportLegacyData.php`
- Befehl: `php artisan legacy:import`
- Optionen: `--source=presseecho|businessportal24|all`, `--step=categories|users|companies|contacts|press-releases|all`, `--dry-run`, `--force`
- Idempotent via `legacy_import_map` (skip bereits importierter Datensätze)
- Chunk-basiert (500 Records pro DB-Query) – speichereffizient
**Status-Mapping Legacy → Neu:**
| Legacy | Neu |
|---|---|
| `new` / `edited` | `draft` |
| `prepublished` | `review` |
| `published` | `published` |
| `rejected` | `rejected` |
**Gruppen-Mapping Legacy → Rollen:**
| Gruppe | Rolle |
|---|---|
| admin (1) | admin |
| editor (2) | editor |
| dataFetcher (3) | api-only |
| unlimitedPressReleasesPayment (4) | customer |
| keine Gruppe | customer |
### Dry-Run-Ergebnisse (0 Fehler)
| Schritt | presseecho | businessportal24 | Zeit |
|---|---|---|---|
| categories | 14 | – (identisch) | <1s |
| users | 19.372 (292 skip) | ~47k | 2s |
| companies | 41.310 | 66.525 | 4–7s |
| contacts | 8.366 | 8.597 | <1s |
| press-releases | 80.033 | 100.305 | 31–45s |
| **Gesamt** | **~149k** | **~223k** | **37 / 58s** |
### Nächster Schritt
- **Echten Import starten:** `php artisan legacy:import --source=presseecho --step=categories --force` (Kategorien zuerst)
- Danach: `--step=users` → `--step=companies` → `--step=contacts` → `--step=press-releases`
- Ggf. Legacy-Rechnungen archivieren (`legacy:archive-invoices` – noch nicht implementiert, P6.5)
---
## 2026-04-27 – P2.7 + P4 + P5.1 + P9.2: GoLive-Mailing, Customer-Portal, Services, Scheduler
**Phase:** P2.7 · P4 · P5.1 · P9.2
**Status:** ✅
### Was wurde gemacht
**P2.7 – GoLive-Mailing**
- `App\Mail\GoLivePasswordReset` + Template `emails/auth/go-live-password-reset.blade.php`
- `App\Console\Commands\SendGoLiveMails` (`auth:send-go-live-mails`):
- `--dry-run` → zählt nur, schickt nichts
- `--portal=presseecho` → filtert nach Portal
- `--limit=10` → für Tests
- `--force` → überspringt Bestätigungsabfrage
- Erzeugt Fortify-Passwort-Reset-Token pro User, sendet personalisierten Link
**P5.1 – PressReleaseService + Benachrichtigungen**
- `App\Services\PressRelease\PressReleaseService` mit Methoden:
- `submitForReview()`, `publish()`, `reject($reason)`, `backToDraft()`, `archive()`
- `assertStatus()` wirft `LogicException` bei ungültigem Übergang
- `notifyAuthor()` sendet automatisch Mail nach `publish`/`reject` via Queue
- `App\Mail\PressReleasePublished` + Template
- `App\Mail\PressReleaseRejected` + Template (mit optionalem Ablehnungsgrund)
- Admin-Views `press-releases/edit.blade.php` + `show.blade.php` nutzen jetzt den Service
- Service als Singleton in `AppServiceProvider` gebunden
**P9.2 – Scheduler-Commands**
- `App\Console\Commands\PurgeMagicLinks` (`magic-links:purge --days=30`): löscht verbrauchte/abgelaufene Links
- `App\Console\Commands\PurgeExpiredPressReleaseDrafts` (`press-releases:purge-drafts --days=180 --dry-run`): archiviert Zombie-Entwürfe
- `routes/console.php` mit Laravel-Scheduler-Einträgen:
- täglich 03:00: Magic-Links bereinigen
- wöchentlich sonntags 04:00: PM-Entwürfe archivieren
**P4 – Customer-Portal Grundgerüst**
- `App\Http\Middleware\EnsureUserIsCustomer` → prüft `canAccessCustomer()`
- `routes/customer.php`: Route-Gruppe `customer.*` mit Middleware, in `domains.php` eingebunden
- Neue Volt-Komponenten unter `resources/views/livewire/customer/`:
- `dashboard.blade.php`: Statistik-Widgets, letzte eigene PMs, Firmenübersicht
- `press-releases/index.blade.php`: gefilterte Liste eigener PMs, inline „Zur Prüfung einreichen"
- `press-releases/create.blade.php`: Firma aus eigenem Bestand (kein Combobox-Vollzugriff), Sicherheitscheck
- `press-releases/edit.blade.php`: nur für `draft`/`rejected` PMs erlaubt (403 sonst)
- `press-releases/show.blade.php`: mit `authorizeAccess()` (nur eigene PMs), Prüfungs-Workflow
- `profile.blade.php`: Konto-Info + zugeordnete Firmen
- Sidebar: Customer-Only-Navigation für User ohne Admin-Rechte
### Verifikation
- `php artisan auth:send-go-live-mails --dry-run` → ✅ zählt korrekt
- `php artisan magic-links:purge` + `press-releases:purge-drafts --dry-run` → ✅
- `php artisan test --compact` → 33 relevante Tests grün ✅
### Nächster Schritt
- P0.3: `.env` mit Legacy-DB-Connections (blockiert P6)
- P6: Datenmigration sobald Legacy-Credentials vorliegen
- P4 erweitern: API-Token Self-Service, Rechnungen-Archiv-Tab
---
## 2026-04-27 – P3 abgeschlossen: PressRelease-CRUD + Categories + Dashboard
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Restliche Dummy-Data-Views auf echte DB-Queries umgestellt + UX-Verbesserungen.
**Status:** ✅
### Was wurde gemacht
**Kategorien-Index** (`admin.categories.index`)
- Echte Eloquent-Queries mit `withCount('pressReleases')` und DE/EN-Translations
- Suchfilter via `whereHas('translations', ...)`
- Statistik-Widgets (Gesamt, Mit PMs, PMs gesamt) mit echten Counts
**Pressemitteilungen – komplett neu (alle 4 Views)**
- **Index** (`admin.press-releases.index`): Tabelle mit echten Daten, Filter nach Status/Portal/Sprache, Live-Schnellaktionen (Veröffentlichen, Ablehnen, Archivieren) direkt aus der Liste
- **Create** (`admin.press-releases.create`): 2-Spalten-Layout (Content | Sidebar), Firma per Flux-Combobox (Live-Suche ab 1 Zeichen), Kategorie-Select, Slug-Generierung, „Als Entwurf" / „Zur Prüfung einreichen"
- **Edit** (`admin.press-releases.edit`): lädt echte DB-Daten, vollständiger Status-Workflow (Entwurf → Prüfung → Veröffentlicht → Archiviert), Slug-Update nur bei Titel-/Portal-/Sprachänderung
- **Show** (`admin.press-releases.show`): Read-only Detail mit Status-Badge, Prüfungs-Aktionsleiste (Veröffentlichen / Ablehnen direkt auf der Seite), Bilder-Liste
**Status-Workflow:**
- Draft → Review (`submitForReview`) → Published (`publish`) / Rejected (`reject`)
- Published → Archived (`archive`)
- Review/Rejected → Draft (`backToDraft`)
**Dashboard** (`resources/views/admin/dashboard.blade.php`)
- 5 Statistik-Kacheln (PMs, Firmen, Kontakte, Benutzer, Newsletter) – alle klickbar, echte DB-Counts
- Letzte 8 PMs mit Status-Badge
- Prüfwarteschlange (PMs im Status `review`) mit Direktlinks
**UX-Verbesserungen (Contacts + Users)**
- `contacts/create.blade.php` + `contacts/edit.blade.php`: Firma-Auswahl auf Flux-Combobox mit Live-Backend-Suche umgestellt (kein All-Load mehr)
- `users/edit.blade.php`: Firmen- und Kontakt-Lookup auf Flux-Combobox umgestellt, Auswahl triggert sofortige Zuordnung via `updatedSelectedLookup*`-Hooks (kein separater „Zuordnen"-Button)
- Suche startet ab 1 Zeichen, max. 50 Treffer, `withoutGlobalScopes()` für portalübergreifenden Admin-Zugriff
### Verifikation
- `php artisan test --compact` → 33 relevante Tests grün ✅
- Alle Views auf echte Eloquent-Models umgestellt
### Nächster Schritt
- P2.7: `GoLivePasswordReset`-Mailable + Command `auth:send-go-live-mails`
- P0.3: `.env` mit Legacy-DB-Connections konfigurieren (Voraussetzung für P6)
- Phase 6 (Datenmigration) – kritischer Pfad zum Go-Live
---
## 2026-04-27 – Qualitätsreview + kritische Fixes (Sicherheit, Architektur, Testqualität)
**Phase:** Querschnitt P2/P3 (Qualitätssicherung + Sicherheit)
**Aufgaben:** Umsetzung aller kritischen und wichtigen Punkte aus `09-REVIEW-QUALITAET.md`.
**Status:** ✅
### Was wurde gemacht
#### 🔴 Kritische Sicherheitslücken behoben
**1. Admin-Middleware `EnsureUserIsAdmin`**
- Neu: `app/Http/Middleware/EnsureUserIsAdmin.php`
- Prüft `$request->user()?->canAccessAdmin()`, sonst HTTP 403
- Angewendet auf alle Admin-Routen in `routes/admin.php`
- Settings-Routen (`settings/profile`, `settings/password`, `settings/appearance`) aus der Admin-Gruppe herausgelöst – diese brauchen nur `auth`, kein Admin-Recht
**2. `MagicLinkConsumeController` – Eager-Load + Null-Check**
- `->with('user')` ergänzt: User wird in einer statt drei DB-Abfragen geladen
- Null-Check für `$magicLink->user` hinzugefügt: verhindert `TypeError` bei soft-deleted Usern
- Datei: `app/Http/Controllers/Auth/MagicLinkConsumeController.php`
**3. `UserRolePermissionSyncService` – Anti-Pattern entfernt**
- Direkte Permission-Zuweisung (`syncDirectPermissionsFromRoles`) komplett entfernt
- Rollen-Checks laufen ausschließlich über Spatie-Rollen (kein Doppel-Speichern)
- Methode jetzt: `assignRoleAndSyncPermissions()` = `$user->syncRoles([$role])`
- `DatabaseSeeder` unverändert funktionsfähig (Service-Interface bleibt gleich)
#### 🏗️ P2.5 – Portal-Scoping (Mandantentrennung)
**Neue Dateien:**
- `app/Services/CurrentPortalContext.php` – statischer Singleton für aktives Portal (get/set/clear)
- `app/Scopes/PortalScope.php` – Global Scope: filtert nach `portal = context OR portal = 'both'`; kein Filter wenn context = null (CLI, Tests, Import-Commands)
- `app/Http/Middleware/SetCurrentPortal.php` – setzt Portal-Kontext aus Session-Override (Admin-Wahl) oder Domain-Theme; registered in `bootstrap/app.php`
- `resources/views/livewire/admin/portal-switcher.blade.php` – Volt-Komponente für Sidebar-Portal-Filter (setzt `admin_portal_filter` Session-Key)
**PortalScope angewendet auf:**
- `App\Models\Company`
- `App\Models\Contact`
- `App\Models\PressRelease`
- `App\Models\NewsletterSubscription`
**Portal-Switcher** in `resources/views/components/layouts/app/sidebar.blade.php` eingebaut (nur für Admin-User sichtbar). Ermöglicht Filterung nach Presseecho / Businessportal24 / Alle.
#### 🐛 Bugfix: Billing-Address-Felder leerbar machen
Datei: `resources/views/livewire/admin/users/edit.blade.php` (Methode `save()`)
**Vorher:** `collect($billingPayload)->filter(fn ($v) => filled($v))` – leere Werte wurden silently ignoriert; Felder konnten einmal gesetzt nicht mehr geleert werden.
**Nachher:** Payload direkt ohne Merge übergeben. Pflichtfelder (name, address1, postal_code, city, country_code) werden auf filled() geprüft, optional Felder können geleert werden.
#### 🗄️ Migrations-Konsolidierung: `user_filter_presets`
Drei separate Migrations vom 2026-04-24 (120000 / 121500 / 123000) zu einer konsolidierten Migration zusammengeführt:
- `2026_04_24_120000_create_user_filter_presets_table.php` (ersetzt alle drei)
- Enthält: `id`, `user_id FK`, `page`, `name`, `is_default`, `last_used_at`, `filters JSON`, `timestamps`
- Alle vier Indizes in einer einzigen `Schema::create()`-Migration
#### ✉️ Session-Flash in Livewire-Inline-Actions ersetzt
`session()->flash()` in Inline-Actions (ohne Redirect) durch `$this->notification` Property ersetzt:
- `resources/views/livewire/admin/users/edit.blade.php` – 4 Inline-Actions (Firmen/Kontakt zuordnen/entfernen)
- `resources/views/livewire/admin/contacts/index.blade.php` – 5 Inline-Actions (Preset CRUD, Contact-Delete)
Template zeigt jetzt eine Alpine.js-gesteuerte Auto-Dismiss-Meldung (3 Sekunden) für Erfolg und Fehler.
#### 🏭 Fehlende Factories ergänzt
Neue Factories:
- `database/factories/CompanyFactory.php` – mit States `presseecho()`, `businessportal24()`, `inactive()`
- `database/factories/ContactFactory.php`
- `database/factories/CategoryFactory.php` – mit `withTranslations()` State
- `database/factories/PressReleaseFactory.php` – mit States `published()`, `inReview()`, `forPortal()`
`HasFactory`-Trait + typisierte `@use`-PHPDoc ergänzt auf:
- `App\Models\Company`, `App\Models\Contact`, `App\Models\Category`, `App\Models\PressRelease`
**UserFactory** um die neuen Pflichtfelder erweitert (`portal`, `registration_type`, `language`, `is_active`, `is_super_admin`) – behebt Bug, bei dem `is_active` nach `User::factory()->create()` als `null` im Model-Attribut stand (DB-Defaults werden nicht automatisch in Attribute übernommen).
#### 🧪 Tests angepasst
- `tests/Pest.php`: `beforeEach()` räumt `CurrentPortalContext::clear()` auf (verhindert Static-State-Leakage zwischen Tests)
- `tests/Feature/Admin/UserManagementTest.php`: HTTP-GET-Test für User-Detailseite auf `LivewireVolt::test()` umgestellt (korrekte Art für Volt-Komponenten; verhindert Vite-Manifest-Fehler in Testumgebung)
### Verifikation
- `php artisan migrate:fresh --seed` ✅ (alle Migrations grün, Seeder grün)
- `php artisan test --compact tests/Feature/Admin/` ✅ (19 passed)
- `php artisan test --compact tests/Feature/Billing/ tests/Feature/Categories/ tests/Feature/Auth/UserAccessTest.php tests/Feature/Auth/MagicLinkLoginTest.php` ✅
- Gesamttestlauf: 47 passed, 13 failed (alle 13 Fehlschläge sind pre-existing Domain-Routing-Probleme in der Test-Umgebung, unverändert seit P1)
### Nächster Schritt
- P3 fortsetzen: PressRelease-CRUD auf echte Daten umstellen (derzeit Dummy-Daten)
- Categories-CRUD auf echte Daten umstellen
- Dashboard-Widgets mit echten Queries
- Phase 0.3: `.env` mit Legacy-DB-Connections konfigurieren (Voraussetzung für P6)
---
## 2026-04-24 – P3 Bugfix: Persistente Kontakt-Verknuepfung im User-Edit
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Sofort-Aktionen im User-Edit (`Zuordnen`/`Entfernen`) auch fuer Kontakte reload-stabil machen.
**Status:** ✅
### Was wurde gemacht
- Neue Pivot-Tabelle `contact_user` eingefuehrt:
- Migration `2026_04_24_183000_create_contact_user_table.php`
- Many-to-many zwischen `users` und `contacts`
- Modelle erweitert:
- `App\Models\User::contacts()`
- `App\Models\Contact::users()`
- `resources/views/livewire/admin/users/edit.blade.php` angepasst:
- direkte Persistenz der Links ueber `persistUserLinks()` fuer Firmen **und** Kontakte
- `addLinkedContact()` / `removeLinkedContact()` schreiben jetzt sofort in `contact_user`
- beim Entfernen einer Firma werden zugehoerige Kontakt-Links ebenfalls bereinigt
- Initialbefuellung bleibt kompatibel (Fallback auf max. 40 Firmenkontakte, falls noch keine expliziten Kontaktlinks existieren)
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert/angepasst:
- Kontakt-Lookup testet jetzt auch Persistenz in `user->contacts()`
- neues Szenario `removing linked contact in user edit is persisted immediately`
- Testlauf:
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` ✅ (19 passed)
### Nächster Schritt
- Optional: In der UI die Anzahl explizit verknuepfter Kontakte anzeigen (Badge), getrennt von allen Firmenkontakten.
---
## 2026-04-24 – P3 Bugfix: User-Edit Verknuepfungen direkt persistieren
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Firmen-/Kontakt-Zuordnung im User-Edit sofort in der DB speichern (ohne extra Save).
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/users/edit.blade.php` angepasst:
- neue interne Persistenz `persistCompanyLinks()` synchronisiert `user_companies` sofort
- `addLinkedCompany()` speichert Firmenverknuepfung direkt
- `removeLinkedCompany()` entfernt Firmenverknuepfung direkt
- `addLinkedContact()` speichert bei Bedarf die zugehoerige Firmenverknuepfung direkt
- direkte UI-Feedbacks per Flash (`direkt verknuepft` / `direkt entfernt`)
- damit ist kein zusaetzlicher Klick auf `Speichern` mehr noetig, um Zuordnen/Entfernen zu persistieren.
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert/angepasst:
- `removing linked company in user edit is persisted immediately`
- `contact lookup in user edit finds results from unlinked companies and auto-links company` (ohne Save)
- `adding linked company in user edit is persisted immediately`
- Testlauf:
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` ✅ (18 passed)
### Nächster Schritt
- Optional: Rollenwechsel pro Firmenkarte ebenfalls sofort persistieren (on-change), damit auch diese Aenderung ohne finalen Save direkt wirksam ist.
---
## 2026-04-24 – P3 Bugfix: User-Edit Verknuepfung Entfernen + Suche
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Fehlerbehebung fuer User-Edit bei Entfernen von Verknuepfungen und Live-Suche.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/users/edit.blade.php` korrigiert:
- Kontakt-Lookup ist nicht mehr auf bereits verknuepfte Firmen eingeschraenkt
- beim Hinzufuegen eines Kontakts wird dessen Firma bei Bedarf automatisch als Firmenverknuepfung aufgenommen
- Action-Buttons (`Zuordnen`/`Entfernen`) explizit als `type=\"button\"`, damit kein unbeabsichtigter Form-Submit ausgeloest wird
- Suche bleibt performant: serverseitig, ab 2 Zeichen, max. 40 Treffer
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- `removing linked company in user edit is persisted after save`
- `contact lookup in user edit finds results from unlinked companies and auto-links company`
- Testlauf:
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` ✅ (17 passed)
### Nächster Schritt
- Optional: fuer bessere UX eine kleine Inline-Info anzeigen, wenn eine Kontakt-Zuordnung automatisch auch die Firma verknuepft hat.
---
## 2026-04-24 – P3: User-Edit mit Live-Zuordnung fuer Firmen und Kontakte
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Firmen- und Kontaktverknuepfung im User-Edit auf performante Live-Suche/Select-Logik umstellen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/users/edit.blade.php` erweitert:
- neue Lookup-States fuer Firmen und Kontakte:
- `companyLookup`, `selectedLookupCompanyId`
- `contactLookup`, `selectedLookupContactId`
- neue Actions:
- `addLinkedCompany()`, `removeLinkedCompany()`
- `addLinkedContact()`, `removeLinkedContact()`
- `with()` liefert jetzt:
- `linkedCompanies` (nur aktuell verknuepfte Firmen)
- `companyLookupResults` (serverseitig gefiltert, max. 40 Treffer, ab 2 Zeichen)
- `contactLookupResults` (serverseitig gefiltert, max. 40 Treffer, ab 2 Zeichen)
- UI im User-Edit angepasst:
- Firmenverknuepfung ueber Live-Suche + Select + `Zuordnen`
- verknuepfte Firmen als Liste mit Rollenwahl + `Entfernen`
- Kontaktverknuepfung ueber Live-Suche + Select + `Zuordnen`
- verknuepfte Kontakte als editierbare Liste mit `Entfernen`
- `syncContactFormsToSelectedCompanies()` angepasst:
- laedt initial nicht mehr unlimitiert, sondern max. 40 Kontakte (Performance)
- haelt bestehend ausgewaehlte Kontakte konsistent, wenn Firmenzuordnung geaendert wird.
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- neues Szenario `admin can link companies and contacts in user edit via live lookup flow`
- prueft Live-Zuordnung von Firmen/Kontakt und korrektes Speichern inkl. Pivot-Rolle.
- Testlauf:
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` ✅ (15 passed)
### Nächster Schritt
- Optional: Asynchrone Suchresultate visuell nach Relevanz sortieren (Name-StartsWith vor Contains) fuer noch schnellere Zuordnung.
---
## 2026-04-24 – P3: Company Contacts Tab mit Live-Select fuer bestehende Kontakte
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** In der Firmen-Detailansicht unter `Kontakte` bestehende Kontakte per Live-Suche finden und direkt der Firma zuordnen (performant, limitiert).
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/companies/show.blade.php` erweitert:
- neue State-Properties:
- `contactLookup` (Suchbegriff)
- `selectedExistingContactId` (ausgewaehlter Kontakt)
- neue Action `attachExistingContact()`:
- validiert Auswahl
- verschiebt Kontakt per `company_id` auf die aktuelle Firma
- behandelt Fehlerfaelle robust (Firma/Kontakt fehlt, bereits zugeordnet)
- Suchlogik in `with()`:
- serverseitiger Filter auf `first_name`, `last_name`, `email`
- Suche erst ab 2 Zeichen
- Ausschluss bereits zugeordneter Kontakte (`company_id != aktuelle Firma`)
- Ergebnislimit `40` zur Performance-Sicherung
- UI im Kontakte-Tab:
- Suchfeld mit Live-Debounce
- Select mit dynamischen Treffern
- Button `Zuordnen`
- Hinweistext zu Suchschwelle und Ergebnislimit
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um neues Szenario erweitert:
- `admin can live-search and assign existing contacts in company detail with result limit`
- prueft:
- Trefferliste wird bei vielen Treffern auf `40` limitiert
- ausgewaehlter Kontakt wird nach `attachExistingContact()` der Ziel-Firma zugeordnet.
### Nächster Schritt
- Optional: Async-Select UX weiter verfeinern (z. B. Treffergruppierung nach Firma oder Tastatur-Navigation).
---
## 2026-04-24 – P3: Quick-Delete fuer Kontakte im Index
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Kontakte direkt in der Kontakte-Liste loeschbar machen, inkl. Warn-Modal und Soft-Delete.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/contacts/index.blade.php` erweitert:
- neue Action `deleteContactFromIndex(int $contactId)`
- Soft Delete via `Contact::delete()` und Flash-Feedback
- Fehlerfall fuer nicht existente Kontakt-ID
- Pro Tabellenzeile neue Quick-Delete-UI:
- neues Trash-Icon in den Aktionen
- zeilenbezogenes Flux-Modal mit Warntext und expliziter Bestaetigung
- Bestaetigungsbutton ruft `deleteContactFromIndex(...)` auf
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- neues Szenario `admin can soft delete contact directly from contacts index flow`
- prueft, dass der Kontakt in der Standard-Query verschwindet und in `withTrashed()` als geloescht markiert bleibt.
### Nächster Schritt
- Optional: Modal um Kontextdetails erweitern (Kontaktname/Firma), damit die Bestaetigung noch eindeutiger wird.
---
## 2026-04-24 – P3: Contact Delete-Flow mit Warn-Modal
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Kontakte in der Admin-UI loeschbar machen und vor Loeschung per Modal explizit bestaetigen lassen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/contacts/edit.blade.php` erweitert:
- neue Action `deleteContact()` im Volt-Component
- Soft Delete via `Contact::delete()` inkl. Redirect + Flash-Meldung
- robuste Behandlung fuer nicht existente Kontakt-ID
- UI im Bereich "Aktionen" angepasst:
- Delete-Button oeffnet nun ein Flux-Modal statt direkter Loeschung
- Modal mit klarer Warnung und expliziter Bestaetigung (`Loeschung bestaetigen`)
- `Abbrechen` schliesst das Modal ohne Aktion
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- bestehendes Kontakt-CRUD-Szenario prueft nun zusaetzlich den Delete-Flow
- Assertions: Redirect auf `admin.contacts.index`, kein Treffer in Standard-Query, Datensatz in `withTrashed()` als geloescht vorhanden.
### Nächster Schritt
- Optional: Delete-Aktion auch als Quick-Action im Contacts-Index mit row-spezifischem Modal ergaenzen.
---
## 2026-04-24 – P3: Company Delete-Flow mit Warn-Modal
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Loeschprozess fuer Firmen aktivieren und per bestaetigungspflichtigem Modal absichern.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/companies/edit.blade.php` erweitert:
- neue Action `deleteCompany()` im Volt-Component
- Soft Delete via `Company::delete()` inkl. Redirect + Flash-Meldung
- robuste Behandlung fuer nicht existente Firmen-ID
- UI im Bereich "Aktionen" angepasst:
- Delete-Button oeffnet jetzt ein Flux-Modal statt direkt zu loeschen
- Modal zeigt klare Warnung und verlangt explizite Bestaetigung
- Buttons: `Abbrechen` und `Loeschung bestaetigen`
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- neues Szenario "admin can soft delete company from edit flow"
- prueft Redirect und dass Datensatz in Standard-Query verschwindet, aber in `withTrashed()` als geloescht vorhanden ist.
### Nächster Schritt
- Optional: Beziehungen beim Loeschen sichtbar auflisten (z. B. Anzahl Kontakte/Benutzer im Modal), damit Admins den Impact vor Bestaetigung sehen.
---
## 2026-04-24 – P3: Companies-CRUD von Dummy auf DB umgestellt
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Firmenliste, Firmen-Erstellung und Firmen-Bearbeitung mit echten Eloquent-Daten anbinden.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/companies/index.blade.php` umgestellt:
- entfernt Dummy-Collection, ersetzt durch `Company`-Query mit `withCount(['pressReleases', 'contacts'])`
- Suche + Aktiv/Inaktiv-Filter auf DB-Ebene
- Pagination (`paginate(15)`) und Live-Statistiken (`total`, `active`, `inactive`)
- Tabellenzugriff auf echte Model-Attribute statt Array-Daten
- `resources/views/livewire/admin/companies/create.blade.php` umgestellt:
- `save()` erzeugt jetzt echte `Company`-Datensaetze
- Mapping auf vorhandene Spalten (`portal`, `type`, `name`, `slug`, `address`, `country_code`, `phone`, `email`, `website`, `logo_path`, `is_active`)
- eindeutige Slug-Generierung pro Portal
- Flux-Form um `Portal`- und `Typ`-Auswahl ergaenzt
- `resources/views/livewire/admin/companies/edit.blade.php` umgestellt:
- `mount()` laedt reale Firmendaten aus der DB und behandelt fehlende IDs robust per Redirect
- `update()` persistiert Aenderungen in der DB inkl. neuer Slug-Logik
- bestehendes Logo wird geladen/angezeigt, neues Logo wird bei Upload gespeichert
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- neues Szenario fuer Company Create + Edit (Persistenz inkl. `portal`, `type`, `slug`, `is_active`)
- Lokaler Testlauf:
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` ✅ (11 passed)
### Nächster Schritt
- Optional: Delete-Flow fuer Companies (Soft-Delete + UI-Aktion + Tests) aktivieren.
---
## Template
```markdown
## YYYY-MM-DD –
**Phase:**
**Aufgaben:**
**Status:** ⬜ 🔄 ✅ ⏸️ ❌
### Was wurde gemacht
- …
### Probleme
- …
### Entscheidungen
- …
### Nächster Schritt
- …
```
---
## 2026-04-23 – Migrationsplan aufgesetzt
**Phase:** 0
**Aufgaben:** Initiale Analyse + Dokumentationsordner `dev/migration 2026/` angelegt
**Status:** ✅
### Was wurde gemacht
- Legacy-Projekt `/_businessportal24.com/` systematisch untersucht (Struktur, Module, Datenmodell, Routen, Plugins)
- Aktuellen Laravel-Stack gesichtet (PHP 8.4, Laravel 12, Livewire 4, Volt, Flux 2, Fortify, Sanctum, Spatie Permission)
- Vorarbeiten gefunden & berücksichtigt:
- Admin-UI-Gerüst in `resources/views/admin/*` + `resources/views/livewire/admin/*`
- 40 deklarierte Admin-Routes in `routes/admin.php`
- Domain-basiertes Theme-System in `config/domains.php`
- Früheres Core-Package-Experiment unter `_businessportal24.com/dev/`
- Altbestand Migrations + Models in `dev/_old/`
- Acht Migrationsdokumente angelegt (README + 00–08)
### Entscheidungen (siehe README.md §D-01 … D-08)
- D-01 Neues DB-Schema (nicht 1:1 aus Doctrine)
- D-02 `portal`-Spalte für Mandanten-Disambiguierung
- D-03 Payment ausschließlich Stripe
- D-04 Country/Salutation als Config-Dateien
- D-05 API-Keys per Sanctum + Kompatibilitäts-Middleware
- D-06 Admin = Livewire Volt + Flux UI
- D-07 DE als primäre UI-Sprache
- D-08 Altes Symfony-Frontend wird nicht übernommen
### Offene Fragen (siehe 00-OVERVIEW.md §5)
- Q-01 DB-Dumps beider Portale vorhanden?
- Q-02 Legacy-Passwörter übernehmen?
- Q-03 API-Keys behalten?
- Q-04 Rechnungs-PDFs archivieren?
- Q-05 Media-Storage (S3 / lokal)?
- Q-06 Welche neuen Pflichtfelder?
- Q-07 Mehrsprachigkeit-Umfang?
- Q-08 PromotionLinks / FooterCode im Scope?
- Q-09 Coupons via Stripe oder intern?
- Q-10 Company/Agency trennen oder fusionieren?
### Nächster Schritt
- Auftraggeber-Review der Doku + Klärung der offenen Fragen
- Scope freigeben (siehe [`06-FEATURES-SCOPE.md`](./06-FEATURES-SCOPE.md) §13)
- Dann: Phase 1 starten (Migrations, Models, Factories, Seeders)
---
## 2026-04-23 – Scope-Entscheidungen eingearbeitet (Q-01 … Q-10)
**Phase:** 0 → Übergang zu P1
**Aufgaben:** Antworten des Auftraggebers zu allen offenen Fragen als verbindliche Entscheidungen in die Dokumentation integriert.
**Status:** ✅
### Was wurde gemacht
Alle 10 offenen Fragen sind mit den Antworten aus dem Kickoff beantwortet und als Entscheidungen **D-09 bis D-18** in `README.md` aufgenommen. Folgende Dokumente wurden überarbeitet:
- `00-OVERVIEW.md` – Fragen-Tabelle zu Entscheidungs-Tabelle, Nicht-Ziele + Phasen + Risiken + Erfolgskriterien aktualisiert
- `README.md` – D-09 … D-18, Dump-Infos (578 / 369 MB)
- `02-TARGET-ARCHITECTURE.md` – Magic-Link Auth, Customer-Portal, Rollen, Grandfathering, i18n DE+EN, Legacy-Invoice-Archiv
- `04-DATA-MODEL.md` – Legacy-Password/API-Key raus, `magic_links`-Tabelle, `legacy_invoices`-Archiv, `user_payment_options.grandfathered_until`, Company-Logo-Varianten, Promotion/Coupons entfernt
- `05-DATABASE-MERGE.md` – Dumps-Ist-Stand, Quellmodi (dump/direct/fixture), wiederholbares Skript, Go-Live-Runbook, Rehearsal-Pflicht
- `06-FEATURES-SCOPE.md` – neue IN/OUT/NEU-Matrix mit Customer-Portal §12, Rechnung-out, Promotion-out, Coupons-vertagt
- `07-API-MIGRATION.md` – Cut-over statt Keep-Alive, 410-Handler, Token-Abilities, Kommunikationsplan T-30/T-7/T-0
- `03-MIGRATION-PLAN.md` – Phasen auf 11 erweitert (P4 Customer-Portal neu, P8 Billing neu gebaut, P6 Daten-Migration als kritischer Pfad). Aufwand 16 → ~20 MT.
### Entscheidungen (neu)
- **D-09** Passwörter werden NICHT übernommen (Go-Live-Mailing)
- **D-10** Magic-Link-Login (Einmal-Passwort per Mail) ⭐
- **D-11** Backend = Admin + Customer-Portal ⭐
- **D-12** Legacy-Rechnungen → Archiv (`legacy_invoices`)
- **D-13** Neue Stripe-Produkte + Grandfathering für Alt-Kunden
- **D-14** Promotion Links entfallen
- **D-15** Newsletter wird neu aufgebaut
- **D-16** Coupons vertagt
- **D-17** `portal`-Spalte + `deleted_at` überall verpflichtend
- **D-18** Import als wiederholbares Skript (für Go-Live-Replay)
### Offene Inputs vom Auftraggeber
- Stripe-Test-Credentials + Live-Account
- Preisliste / Definition der neuen Stripe-Produkte
- Liste / Kriterien der "aktiven Alt-Abos" für Grandfathering
- Freigabe Rollen-Mapping `sfGuardGroup` → `admin`/`editor`/`customer`/`api-only`
- Liste der API-Key-Kunden für T-30-Mailing
### Nächster Schritt
- **Phase 0 abschließen:** lokale Schemas aus SQL-Dumps importieren, `.env` mit 3 DB-Connections konfigurieren
- **Phase 1 starten:** Migrations + Models + Factories + Seeders (ohne Coupons/PromotionLinks, mit `magic_links` + `legacy_invoices`)
---
## 2026-04-23 – Dokumentationskonsolidierung (Unstimmigkeiten bereinigt)
**Phase:** 0 (Qualitätssicherung Doku)
**Aufgaben:** Konsistenzabgleich zwischen Migrationsdokumenten und aktuellem Projektstand.
**Status:** ✅
### Was wurde gemacht
- `00-OVERVIEW.md`: API-Zieltext auf harten Cut-over vereinheitlicht (Sanctum-only, Legacy-Keys `410 Gone`).
- `README.md`: deklarierte Admin-Route-Anzahl von 40 auf 39 korrigiert.
- `resources/views/admin/BACKEND_STATUS.md`: Status von "komplett" auf "Gerüst weitgehend vorhanden" korrigiert; Route-Anzahl auf 39 angepasst; Liste fehlender Volt-Zielkomponenten ergänzt.
- `routes/ADMIN_ROUTES.md`: Kennzahl "Views erstellt" auf Basis 39 Routen korrigiert.
- `06-FEATURES-SCOPE.md`: Coupon-Zeile um Klarstellung ergänzt, dass bestehende Coupon-Routen/Views nur UI-Skeleton sind (nicht priorisiert in P1).
### Probleme
- Historische Statusdokumente enthalten teils veraltete Aussagen zum Umsetzungsgrad.
### Entscheidungen
- Für die API gilt verbindlich der dokumentierte Cut-over-Ansatz ohne Legacy-Key-Kompatibilität.
- Coupon-Funktion bleibt in Phase 1 vertagt; vorhandene Routen/Views haben keinen fachlichen Lieferanspruch.
### Nächster Schritt
- Optional: `routes/admin.php` und Volt-Dateistruktur in einem separaten Cleanup synchronisieren (fehlende Zielkomponenten ergänzen oder Routen temporär deaktivieren).
---
## 2026-04-23 – Admin-Routing mit Volt-Struktur synchronisiert
**Phase:** 0 (Qualitätssicherung Doku/Struktur)
**Aufgaben:** `routes/admin.php` auf existierende Volt-Komponenten ausgerichtet; Route-Dokumentation aktualisiert.
**Status:** ✅
### Was wurde gemacht
- In `routes/admin.php` alle Volt-Routen entfernt, deren Zielkomponenten aktuell nicht existieren.
- Benutzer-Index-Route auf vorhandene Volt-Komponente gemappt (`admin.users` statt nicht vorhandenes `admin.users.index`).
- Aktive Admin-Routeanzahl auf **24** konsolidiert (nur valide Mappings).
- `routes/ADMIN_ROUTES.md` vollständig auf den aktuellen Stand neu geschrieben (aktive Routen + Backlog).
- `README.md` und `resources/views/admin/BACKEND_STATUS.md` auf den neuen, konsistenten Routenstand angepasst.
### Probleme
- Historischer Dokumentationsstand ging von vollständigem Routingset aus, obwohl mehrere Zielkomponenten nicht vorhanden waren.
### Entscheidungen
- Kurzfristig wurden keine Dummy-Volt-Komponenten erzeugt; stattdessen wurde das Routing auf tatsächlich vorhandene Komponenten reduziert.
### Nächster Schritt
- Bei Umsetzung neuer Admin-Bereiche (System, Create/Edit-Views, Detailseiten) die jeweiligen Routen gezielt wieder aktivieren.
---
## 2026-04-23 – Sidebar auf aktive Admin-Routen bereinigt
**Phase:** 0 (Qualitätssicherung UI-Navigation)
**Aufgaben:** Sidebar-Navigation mit aktuellem Routenstand synchronisiert.
**Status:** ✅
### Was wurde gemacht
- In `resources/views/components/layouts/app/sidebar.blade.php` die komplette System-Navigationsgruppe entfernt.
- Entfernte Links: `admin.scheduler.index`, `admin.newsletter.index`, `admin.settings` (waren nicht mehr im Routing aktiv).
- Verbleibende Sidebar-Links zeigen auf aktive Admin-Routen.
### Probleme
- Keine technischen Probleme; es handelte sich um UI-Navigation auf nicht mehr deklarierte Routen.
### Entscheidungen
- Nicht aktive Bereiche bleiben vorerst aus der Sidebar ausgeblendet, bis die zugehörigen Volt-Komponenten und Routen wieder eingeführt werden.
### Nächster Schritt
- Bei Reaktivierung von System-Bereichen die Sidebar-Gruppe gezielt wieder hinzufügen.
---
## 2026-04-23 – Legacy-Admin-Layouts auf gültige Route-Namen umgestellt
**Phase:** 0 (Qualitätssicherung UI-Navigation)
**Aufgaben:** Verbleibende Legacy-Admin-Layouts auf die aktuellen Route-Namen (`dashboard`, `settings.*`) synchronisiert.
**Status:** ✅
### Was wurde gemacht
- `resources/views/layouts/admin-master.blade.php` bereinigt:
- `admin.dashboard` → `dashboard`
- `admin.settings.profile` → `settings.profile`
- `admin.settings.appearance` → `settings.appearance`
- `resources/views/web/layouts/admin-master.blade.php` identisch bereinigt.
### Probleme
- Keine. Es handelte sich um konsistente Umbenennungen auf bestehende Route-Namen.
### Entscheidungen
- Auch wenig genutzte/legacy-nahe Layout-Dateien bleiben auf gültigem Routing-Stand, um spätere Regressionen zu vermeiden.
### Nächster Schritt
- Optional: prüfen, ob eines der beiden Legacy-Layouts noch aktiv verwendet wird; falls nicht, auf ein einziges Layout konsolidieren.
---
## 2026-04-23 – Admin-Layout konsolidiert (Single Source)
**Phase:** 0 (Qualitätssicherung UI-Struktur)
**Aufgaben:** Doppelte Admin-Layout-Dateien auf eine zentrale Quelle reduziert.
**Status:** ✅
### Was wurde gemacht
- Nutzungssuche durchgeführt: keine aktiven Referenzen auf `web.layouts.admin-master` oder `layouts.admin-master` außerhalb der Doku gefunden.
- `resources/views/web/layouts/admin-master.blade.php` auf einen Alias reduziert:
- enthält jetzt nur noch `@extends('layouts.admin-master')`
- Damit ist `resources/views/layouts/admin-master.blade.php` die einzige echte Layout-Implementierung.
### Probleme
- Keine technischen Probleme; Konsolidierung wurde abwärtskompatibel umgesetzt.
### Entscheidungen
- Kompatibilitätsalias bleibt bestehen, um mögliche versteckte Verwendungen nicht zu brechen.
### Nächster Schritt
- Bei Gelegenheit prüfen, ob der Alias langfristig entfernt werden kann, sobald sicher ist, dass keine Legacy-Referenzen mehr existieren.
---
## 2026-04-23 – Phase 1 gestartet (Enums, Config, erste Domain-Tabellen)
**Phase:** P1 (Fundament)
**Aufgaben:** Enums, Stammdaten-Config, Users-Extension, erste Domain-Migrationen und Models.
**Status:** 🔄
### Was wurde gemacht
- Enums unter `app/Enums` angelegt:
- `Portal`, `PressReleaseStatus`, `InvoiceStatus`, `PaymentOptionType`,
`UserPaymentOptionStatus`, `CompanyType`, `RegistrationType`, `PaymentStatus`
- Stammdaten als Config eingeführt:
- `config/countries.php`
- `config/salutations.php`
- Users-Fundament erweitert:
- neue Migration `expand_users_table_for_migration_2026`
- Felder wie `portal`, `registration_type`, `language`, `is_active`, `legacy_*`, `deleted_at`
- `password` auf nullable gesetzt
- entsprechendes Casting/Fillable in `User` ergänzt, inklusive Relations zu `Profile` und `MagicLink`
- Erste Domain-Tabellen erstellt:
- `profiles` (inkl. User-1:1 und Kernfeldern)
- `magic_links` (inkl. Purpose, Token-Hash, TTL, Single-Use-Felder)
- Modelle ergänzt:
- `app/Models/Profile.php`
- `app/Models/MagicLink.php`
- Spatie-Permission-Migration publiziert (`create_permission_tables`)
- Seeder auf P1-Rollenmodell umgestellt:
- Rollen: `admin`, `editor`, `customer`, `api-only`
- Basale API-/Admin-Permissions + Zuordnung
- `DatabaseSeeder` erstellt Admin-User und weist Rolle `admin` zu
### Probleme
- Globaler Testlauf zeigt weiterhin bestehende Routing-/Domain-Differenzen in Auth/Settings-Feature-Tests (404/Redirect), unabhängig von den neuen Phase-1-Artefakten.
- `vendor/bin/pint --dirty` war wegen vieler historischer Änderungen im Working Tree nicht sinnvoll; deshalb nur geänderte Dateien gezielt formatiert.
### Entscheidungen
- Soft-Deletes auf `users` bleiben gemäß Migrationsentscheidung aktiv; Account-Löschung wurde auf `forceDelete()` angepasst, damit der bestehende Löschtest weiterhin korrekt bleibt.
### Verifikation
- ✅ `php artisan test --compact --filter="user can delete their account"` (grün)
- ⚠️ `php artisan test --compact` aktuell nicht vollständig grün wegen bereits bestehender Routing-/Domain-Themen
### Nächster Schritt
- P1 fortsetzen mit weiteren Kernmigrationen (`companies`, `contacts`, `categories`, `press_releases`) und zugehörigen Basismodellen.
---
## 2026-04-23 – Phase 1 erweitert: Companies & Contacts Fundament
**Phase:** P1 (Fundament)
**Aufgaben:** Companies/Contacts inkl. Pivot modellieren und migrierbar machen.
**Status:** 🔄
### Was wurde gemacht
- Neue Migrationen erstellt und umgesetzt:
- `create_companies_table`
- `create_company_user_table`
- `create_contacts_table`
- `companies` enthält u. a.:
- `portal`, `owner_user_id`, `type`, `name`, `slug`, Kontaktfelder, Logo-Felder, `legacy_*`, SoftDeletes
- Unique-Indizes auf `(portal, slug)` und `(legacy_portal, legacy_id)`
- `company_user` als Pivot umgesetzt:
- `company_id`, `user_id`, `role (member|responsible|owner)`, PK `(company_id, user_id)`
- `contacts` enthält u. a.:
- `company_id`, `portal`, Stammdatenfelder, `legacy_*`, SoftDeletes
- Unique auf `(legacy_portal, legacy_id)`
- Basismodelle ergänzt:
- `App\Models\Company` (Casts, Relations: owner/users/contacts)
- `App\Models\Contact` (Casts, Relation: company)
- `App\Models\User` erweitert um `ownedCompanies()` und `companies()` Relations
### Probleme
- Keine migrationsbezogenen Fehler im neuen Block.
### Verifikation
- ✅ `php artisan migrate:fresh --seed` läuft vollständig grün.
### Nächster Schritt
- P1 fortsetzen mit `categories` + `category_translations`, danach `press_releases` + `press_release_images` + Pivot-Tabellen.
---
## 2026-04-23 – Phase 1 fokussiert: Newsletter-Subscriptions only
**Phase:** P1 (Fundament)
**Aufgaben:** Nur `newsletter_subscriptions` umsetzen und übrige Newsletter-nahe Tabellen vorerst aussetzen.
**Status:** ✅
### Was wurde gemacht
- Neue Migration `create_newsletter_subscriptions_table` umgesetzt:
- Felder gemäß Datenmodell (`portal`, `user_id`, Name/Kontakt, `is_confirmed`, `confirmation_token`, `subscribed_at`, `unsubscribed_at`, `legacy_*`)
- Constraints/Indizes ergänzt:
- Unique `(portal, email)`
- Unique `confirmation_token`
- Unique `(legacy_portal, legacy_id)`
- Index `(portal, is_confirmed)`
- Neues Basismodell `App\Models\NewsletterSubscription` angelegt:
- Fillable + Casts (`portal`, `is_confirmed`, Zeitfelder)
- Relation `user()`
- `App\Models\User` um Relation `newsletterSubscriptions()` erweitert.
- Dokumentation aktualisiert:
- `03-MIGRATION-PLAN.md` um Scope-Hinweis ergänzt (Newsletter-Block nur `newsletter_subscriptions`).
### Entscheidungen
- Auf Wunsch des Auftraggebers wird der Newsletter-Bereich in P1 bewusst reduziert.
- `blacklists`, `footer_codes` und `category_footer_code` werden aktuell **nicht** umgesetzt.
- Geplante externe Newsletter-Synchronisierung erfolgt später über separate API-Integration.
### Verifikation
- ✅ Migration läuft in bestehender Datenbank durch.
- ✅ Code-Formatierung und Lint ohne neue Fehler.
### Nächster Schritt
- Nächsten priorisierten P1-Baustein nur nach expliziter Freigabe umsetzen (kein automatisches Nachziehen der ausgesetzten Tabellen).
---
## 2026-04-23 – Newsletter-Sync Skeleton vorbereitet
**Phase:** P1 (Vorbereitung externe Integration)
**Aufgaben:** Technisches Grundgeruest fuer spaetere API-Synchronisierung anlegen, ohne aktive Provider-Implementierung.
**Status:** ✅
### Was wurde gemacht
- Contract fuer externe Newsletter-Synchronisierung eingefuehrt:
- `App\Contracts\NewsletterSyncClient` (`subscribe()` / `unsubscribe()`)
- Platzhalter-Implementierung erstellt:
- `App\Services\Newsletter\NullNewsletterSyncClient`
- Orchestrierungsservice erstellt:
- `App\Services\Newsletter\NewsletterSyncService`
- entscheidet anhand von `is_confirmed` und `unsubscribed_at`, ob subscribe oder unsubscribe ausgeloest wird
- Container-Binding in `AppServiceProvider` ergaenzt:
- `NewsletterSyncClient::class` -> `NullNewsletterSyncClient::class`
- Konfigurationsdatei fuer spaetere externe Anbindung hinzugefuegt:
- `config/newsletter.php` (`enabled`, `provider`, `endpoint`, `api_key`, `timeout`)
### Entscheidungen
- Es wurde bewusst kein externer API-Client angebunden.
- Der aktuelle Stand ist ein sicherer No-Op, bis Provider und Credentials freigegeben sind.
### Nächster Schritt
- Bei Freigabe des externen Dienstes einen echten Adapter (z. B. `...Client`) implementieren und nur das Binding austauschen.
---
## 2026-04-23 – Newsletter-Sync Admin-View + Navigation ergänzt
**Phase:** P1 (Admin-Oberflaeche)
**Aufgaben:** Bedienoberflaeche fuer Newsletter-Synchronisierung sichtbar machen und in Navigation integrieren.
**Status:** ✅
### Was wurde gemacht
- Neue Volt-Route hinzugefuegt:
- `GET /admin/newsletter-sync` -> `admin.newsletter.sync`
- Neue Livewire-Volt-Seite erstellt:
- `resources/views/livewire/admin/newsletter/sync.blade.php`
- Enthält Uebersicht zu Newsletter-Status (gesamt, bestaetigt, unbestaetigt, abgemeldet)
- Zeigt aktuelle Sync-Konfiguration aus `config/newsletter.php`
- Sidebar erweitert:
- Unterpunkt `Newsletter Sync` im Bereich **Billing** hinzugefuegt.
- Routen-Dokumentation aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 25 aktive Routen angepasst.
### Entscheidungen
- Die Seite dient aktuell als Monitoring-/Konfigurationsansicht fuer das vorbereitete Sync-Skeleton.
- Keine aktive externe API-Kommunikation auf dieser Seite; Provider bleibt weiterhin als No-Op gebunden.
### Nächster Schritt
- Optional: manuellen Test-Button fuer "Einzelnen Datensatz synchronisieren" erst nach Freigabe des externen API-Vertrags einfuehren.
---
## 2026-04-23 – Newsletter-Sync UI abgeschlossen (Test + Dry-Run)
**Phase:** P1 (Admin-Oberflaeche)
**Aufgaben:** Newsletter-Sync-Seite funktional abschliessen, ohne externe API-Kommunikation.
**Status:** ✅
### Was wurde gemacht
- In `resources/views/livewire/admin/newsletter/sync.blade.php` zwei UI-Aktionen finalisiert:
- `Test-Sync ausfuehren` (No-Op-Serviceaufruf ueber `NewsletterSyncService`)
- `Dry Run` (Vorschau ohne Serviceaufruf)
- Rueckmeldungen auf der Seite ergaenzt (`syncMessage`, `dryRunMessage`), inkl. Zielaktion (`subscribe` / `unsubscribe`) und Datensatzbezug.
- Damit ist der angeforderte Newsletter-Sync-Bedienpunkt inkl. Navigation und Test-Interaktion abgeschlossen.
### Entscheidungen
- Funktionalitaet bleibt absichtlich provider-neutral (kein echter API-Call), bis externer Dienst final freigegeben ist.
### Nächster Schritt
- Phase 1 mit Billing-/Payment-Fundament fortsetzen.
---
## 2026-04-23 – Phase 1 erweitert: Billing-Adressfundament
**Phase:** P1 (Fundament)
**Aufgaben:** Rechnungsadressen fuer Benutzer und Rechnungssnapshots modellieren.
**Status:** 🔄
### Was wurde gemacht
- Neue Migrationen umgesetzt:
- `create_billing_addresses_table`
- `create_invoice_billing_addresses_table`
- `billing_addresses` enthaelt:
- `user_id` (FK, unique), Anrede-/Adressfelder, `country_code`
- `invoice_billing_addresses` enthaelt:
- Snapshot-Adressfelder ohne `user_id`-FK (fuer historische Rechnungsdaten)
- Basismodelle angelegt:
- `App\Models\BillingAddress` inkl. `user()`-Relation
- `App\Models\InvoiceBillingAddress`
- `App\Models\User` um `billingAddress()` erweitert (1:1).
### Entscheidungen
- `billingAddress` ist als 1:1-Relation ausgelegt (technisch via Unique-FK abgesichert).
### Nächster Schritt
- P1 fortsetzen mit `payment_options` + `payment_option_translations` und anschliessend `user_payment_options`.
---
## 2026-04-23 – Phase 1 erweitert: Payment-Options Fundament
**Phase:** P1 (Fundament)
**Aufgaben:** Produktkatalog fuer Zahlungen inkl. Uebersetzungen modellieren.
**Status:** 🔄
### Was wurde gemacht
- Neue Migrationen umgesetzt:
- `create_payment_options_table`
- `create_payment_option_translations_table`
- `payment_options` enthaelt:
- `article_number` (unique), `type` (Enum recurring/onetime), `price_cents`, `currency`, `interval`, `is_hidden`, Stripe-IDs
- SoftDeletes und relevante Indizes
- `payment_option_translations` enthaelt:
- FK auf `payment_options`, `locale`, `name`, `description`
- Unique auf `(payment_option_id, locale)`
- Basismodelle angelegt:
- `App\Models\PaymentOption` (Casts inkl. `PaymentOptionType`, Relation `translations()`)
- `App\Models\PaymentOptionTranslation` (Relation `paymentOption()`)
### Entscheidungen
- `interval` bleibt als fachliches Enum-Feld (`monthly`, `yearly`, `once`) in der DB, bis ein separates PHP-Enum benoetigt wird.
### Nächster Schritt
- P1 fortsetzen mit `user_payment_options` + `user_payment_option_company`, danach `user_payments`.
---
## 2026-04-23 – Phase 1 erweitert: User-Payment Fundament
**Phase:** P1 (Fundament)
**Aufgaben:** Aktive Abos, Firmenzuordnung und Zahlungen modellieren.
**Status:** 🔄
### Was wurde gemacht
- Neue Migrationen umgesetzt:
- `create_user_payment_options_table`
- `create_user_payment_option_company_table`
- `create_user_payments_table`
- `user_payment_options` enthaelt:
- FK `user_id`, FK `payment_option_id`
- `status` (Enum `active|past_due|cancelled|grandfathered`)
- `grandfathered_until`, `legacy_conditions` (JSON)
- `current_period_start`, `current_period_end`, `stripe_subscription_id`, `cancelled_at`
- `user_payment_option_company` als Pivot umgesetzt:
- `user_payment_option_id`, `company_id`, `is_active`, `timestamps`
- zusammengesetzter Primary Key auf beiden IDs
- `user_payments` enthaelt:
- optionale FK `user_payment_option_id`
- `amount_cents`, `currency`, `status` (Enum `pending|succeeded|failed|refunded`)
- `stripe_charge_id`, `stripe_invoice_id`
- Basismodelle angelegt/erweitert:
- `App\Models\UserPaymentOption` (Casts, Relations: user, paymentOption, companies, payments)
- `App\Models\UserPayment` (Casts, Relation: userPaymentOption)
- `App\Models\PaymentOption` erweitert um `userPaymentOptions()`
- `App\Models\Company` erweitert um `userPaymentOptions()`
- `App\Models\User` erweitert um `userPaymentOptions()`
### Entscheidungen
- Pivot `user_payment_option_company` bleibt bewusst als Composite-Key-Tabelle ohne eigene `id`.
- Stripe-IDs werden als nullable Felder gefuehrt und initial nur indexiert.
### Nächster Schritt
- P1 fortsetzen mit `invoices` + `legacy_invoices` + `legacy_import_map`.
---
## 2026-04-23 – Phase 1 erweitert: Invoice- und Importfundament
**Phase:** P1 (Fundament)
**Aufgaben:** Neuer Rechnungskreis, Legacy-Rechnungsarchiv und Import-Tracking modellieren.
**Status:** 🔄
### Was wurde gemacht
- Neue Migrationen umgesetzt:
- `create_invoices_table`
- `create_legacy_invoices_table`
- `create_legacy_import_map_table`
- `invoices` enthaelt:
- FK `user_id`, optionale FK `user_payment_id`, FK `invoice_billing_address_id`
- `number`, `status`, Betragsfelder (`amount_cents`, `tax_cents`, `total_cents`)
- `currency`, `is_netto`, `invoice_date`, `due_date`, `paid_at`
- `stripe_invoice_id`, `pdf_path`, SoftDeletes + Indizes
- `legacy_invoices` als read-only Archiv enthaelt:
- `legacy_portal`, `legacy_id` (unique kombiniert), optionale FK `user_id`, `legacy_user_id`
- Nummer, Betrags- und Datumsfelder, `payment_method`, `pdf_path`, `raw_snapshot`, `imported_at`
- `legacy_import_map` enthaelt:
- `legacy_portal`, `legacy_table`, `legacy_id`, `target_table`, `target_id`, `imported_at`
- Unique auf `(legacy_portal, legacy_table, legacy_id)`
- Basismodelle angelegt/erweitert:
- `App\Models\Invoice` (Casts inkl. `InvoiceStatus`, Relations: user, userPayment, invoiceBillingAddress)
- `App\Models\LegacyInvoice` (read-only via `timestamps=false`, Casts inkl. `raw_snapshot`)
- `App\Models\LegacyImportMap` (Table-Mapping auf `legacy_import_map`, `timestamps=false`)
- `App\Models\UserPayment` erweitert um `invoices()`
- `App\Models\InvoiceBillingAddress` erweitert um `invoices()`
- `App\Models\User` erweitert um `invoices()` und `legacyInvoices()`
### Entscheidungen
- `legacy_invoices` und `legacy_import_map` werden ohne `created_at/updated_at` gefuehrt; Zeitbezug erfolgt ueber `imported_at`.
- `legacy_portal` bleibt als Enum-Feld im Datenmodell, um die Herkunft eindeutig und validierbar zu halten.
### Nächster Schritt
- P1 Restblock vorbereiten: Factories/Seeder-Ergaenzungen und anschliessend `migrate:fresh --seed` Volltest.
---
## 2026-04-23 – Phase 1 validiert: Factory-/Seeder-Erweiterung + Full Reset
**Phase:** P1 (Qualitaetssicherung)
**Aufgaben:** Factories und Seeder fuer den neuen Billing-/Invoice-Block ergaenzen und End-to-End validieren.
**Status:** ✅
### Was wurde gemacht
- Neue Factories angelegt und befuellt:
- `PaymentOptionFactory`
- `PaymentOptionTranslationFactory`
- `UserPaymentOptionFactory`
- `UserPaymentFactory`
- `BillingAddressFactory`
- `InvoiceBillingAddressFactory`
- `InvoiceFactory`
- Neue Seeder-Klasse umgesetzt:
- `PaymentOptionSeeder` mit idempotentem `updateOrCreate` (inkl. DE/EN-Translations)
- `DatabaseSeeder` erweitert:
- ruft jetzt `PaymentOptionSeeder` zusaetzlich auf.
- `HasFactory` auf relevante Modelle ergaenzt, damit die neuen Factories konsistent nutzbar sind.
### Verifikation
- ✅ `php artisan migrate:fresh --seed` laeuft vollstaendig grün.
- ✅ Alle aktuellen P1-Migrationen werden beim Full-Reset erfolgreich aufgebaut.
- ✅ Seeder-Lauf inkl. Rollen/Berechtigungen und Payment-Optionen erfolgreich.
### Nächster Schritt
- Optionaler Abschlusslauf mit zielgerichteten Feature-Tests fuer Billing/Invoice-Flows.
---
## 2026-04-23 – Fehlende Admin-View ergänzt (Roles Edit)
**Phase:** P1 (Admin-Oberflaeche / Konsistenz)
**Aufgaben:** Fehlende Volt-Zielkomponente fuer aktive Route nachziehen.
**Status:** ✅
### Was wurde gemacht
- Fehlende View `resources/views/livewire/admin/roles/edit.blade.php` erstellt.
- Die aktive Route `admin.roles.edit` zeigt jetzt auf eine vorhandene Volt-Komponente.
- Komponente enthaelt:
- Laden der echten Spatie-Rolle inkl. bestehender Permissions
- Validierung mit Unique-Check (guard-spezifisch) und Permission-Existenzpruefung
- Speichern mit `syncPermissions()` auf der Rolle
- Warnhinweis fuer Systemrollen (`admin`, `editor`, `customer`, `api-only`)
### Entscheidungen
- UI ist konsistent mit dem bestehenden Rollen-Create-Pattern.
- Persistenz fuer Rollenname und Permissions ist produktiv auf Spatie-Basis angebunden.
### Nächster Schritt
- Optional: `roles/create` ebenfalls auf echten Spatie-Flow umstellen (analog zu `roles/edit`).
---
## 2026-04-23 – Rollenverwaltung konsolidiert (Create auf Spatie umgestellt)
**Phase:** P1 (Admin-Oberflaeche / Konsistenz)
**Aufgaben:** `roles/create` auf dieselbe produktive Spatie-Logik wie `roles/edit` umstellen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/roles/create.blade.php` komplett auf echte Spatie-Daten umgestellt:
- Role-Erstellung via `Role::create(...)`
- Permission-Validierung via `Rule::exists(...)` guard-spezifisch
- Permission-Sync via `syncPermissions(...)`
- Live-Gruppierung der Permissions aus DB statt statischer Demo-Listen
- Nicht persistente Demo-Felder (`display_name`, `description`, `color`) entfernt, da sie nicht zum Spatie-Rollenmodell gehoeren.
### Entscheidungen
- Rollenverwaltung (Create/Edit) folgt jetzt einheitlich dem minimalen Spatie-Datenmodell (`name`, `guard_name`, Permissions).
- Zusätzliche Metadaten fuer Rollen bleiben optionales Folgefeature und sind aktuell nicht Teil des Schemas.
### Nächster Schritt
- Optional: `roles/index` von statischen Demo-Daten auf echte Spatie-Queries umstellen.
---
## 2026-04-23 – Rollenübersicht auf echte Spatie-Daten umgestellt
**Phase:** P1 (Admin-Oberflaeche / Konsistenz)
**Aufgaben:** `roles/index` von statischer Demo-Liste auf produktive Rollen-/Permission-Daten umstellen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/roles/index.blade.php` umgestellt auf echte Queries:
- Rollen aus `Spatie\Permission\Models\Role`
- `withCount(['users', 'permissions'])` fuer Live-Kennzahlen
- Tabellen-Rendering auf echte Role-Objekte angepasst (`$role->...` statt Array-Mockdaten).
- System-/Custom-Tagging anhand der Basisrollen (`admin`, `editor`, `customer`, `api-only`) beibehalten.
- Empty-State fuer den Fall ohne Rollen ergaenzt.
### Entscheidungen
- Anzeige bleibt bewusst nah am bisherigen UI, aber Datenquelle ist jetzt voll produktiv.
### Nächster Schritt
- Optional: Rollenname-Labeling (z. B. lokalisierte Anzeigenamen) ueber separate Mapping-Konfiguration verfeinern.
---
## 2026-04-23 – Billing/Invoice Feature-Tests ergänzt
**Phase:** P1 (Qualitaetssicherung)
**Aufgaben:** Kernbeziehungen und Seeder-Integritaet im neuen Billing-/Invoice-Block testseitig absichern.
**Status:** ✅
### Was wurde gemacht
- Neuer Pest-Test angelegt:
- `tests/Feature/Billing/BillingModelsTest.php`
- Abgedeckte Szenarien:
- `PaymentOptionSeeder` erzeugt die erwarteten multilingualen Payment-Optionen
- `UserPaymentOption` Pivot- und Payment-Relationen funktionieren inkl. Persistenz
- `Invoice`-Factory erzeugt gueltigen Relation-Graph (`user`, `invoiceBillingAddress`, `userPayment`)
### Entscheidungen
- Fokus auf schnelle Integritaets-Checks entlang der wichtigsten neuen P1-Modelle.
- Tests bleiben datenbanknah (Feature-Ebene mit `RefreshDatabase`) fuer hohe Aussagekraft.
### Nächster Schritt
- Optional: weitere Feature-Tests fuer Rollenverwaltung (Create/Edit/Sync) und Newsletter-Sync-UI-Flow.
---
## 2026-04-23 – Permissions zusaetzlich Usern zugeordnet
**Phase:** P1 (Auth/Rollen-Konsistenz)
**Aufgaben:** Sicherstellen, dass Basis-User neben Rollen auch direkte Permission-Zuordnung erhalten.
**Status:** ✅
### Was wurde gemacht
- `database/seeders/DatabaseSeeder.php` angepasst:
- Admin-User erhaelt Rolle `admin` **und** direkte Permissions aus `getPermissionsViaRoles()`
- Test-User erhaelt Rolle `customer` **und** direkte Permissions aus `getPermissionsViaRoles()`
### Entscheidungen
- Permissions werden bewusst doppelt abgesichert (Rollenvererbung + direkte Zuordnung), um Zugriffsverhalten fuer Basis-Accounts eindeutig zu halten.
### Nächster Schritt
- Optional: gleiche Logik fuer weitere Seed-/Import-User anwenden, wenn diese initial mit Rollen angelegt werden.
---
## 2026-04-23 – Rollen/Permissions-Sync fuer Importflow vorbereitet
**Phase:** P1 (Auth/Rollen-Konsistenz)
**Aufgaben:** Rollen- und Permission-Synchronisierung in einen wiederverwendbaren Service auslagern.
**Status:** ✅
### Was wurde gemacht
- Neuer Service angelegt:
- `App\Services\Auth\UserRolePermissionSyncService`
- Methoden:
- `assignRoleAndSyncPermissions(User $user, string $role)`
- `syncDirectPermissionsFromRoles(User $user)`
- `DatabaseSeeder` auf den Service umgestellt:
- Admin-/Test-User erhalten Rollen und direkte Permissions jetzt ueber die zentrale Service-Logik.
### Entscheidungen
- Service dient als zentrale Stelle fuer denselben Mechanismus im kuenftigen Legacy-Import.
- Damit wird verhindert, dass Seeder und spaetere Import-Commands divergierendes Rollen-/Permission-Verhalten haben.
### Nächster Schritt
- Bei Implementierung der Legacy-Import-Commands denselben Service direkt nach dem Rollen-Mapping aufrufen.
---
## 2026-04-24 – Phase 2 gestartet: User-Access-Gates im Model
**Phase:** P2 (Auth & Tenancy)
**Aufgaben:** `canAccessAdmin()` und `canAccessCustomer()` im `User`-Model einfuehren und testseitig absichern.
**Status:** ✅
### Was wurde gemacht
- `App\Models\User` erweitert:
- `canAccessAdmin()` implementiert (aktiv + Rolle `admin|editor` oder `is_super_admin`)
- `canAccessCustomer()` implementiert (aktiv + Rolle `admin|editor|customer`)
- Neuer Pest-Feature-Test `tests/Feature/Auth/UserAccessTest.php` erstellt:
- positive und negative Faelle fuer Admin-/Customer-Zugriff
- Inaktiv- und `api-only`-Faelle explizit abgedeckt
- `is_super_admin`-Bypass fuer Admin-Zugang verifiziert
### Entscheidungen
- Zugriffsmethoden liegen zentral im `User`-Model, damit UI-/Policy-Code dieselbe Logik nutzt.
- `api-only` bleibt explizit ohne Customer-Portal-Zugriff.
### Nächster Schritt
- P2.3 umsetzen: Magic-Link-Flow (`Generator`, `Consume`, `Mailable`) auf Basis der bereits vorhandenen `magic_links`-Tabelle starten.
---
## 2026-04-24 – P1 nachgezogen: CategorySeeder + Tests
**Phase:** P1 (Fundament / Seeder-Block)
**Aufgaben:** Fehlenden Kategorie-Seed (`DE/EN`) aus dem Plan umsetzen und absichern.
**Status:** ✅
### Was wurde gemacht
- Neuer Seeder `database/seeders/CategorySeeder.php` angelegt:
- initialisiert drei Basiskategorien
- legt pro Kategorie DE/EN-Translation an
- arbeitet idempotent ueber vorhandene DE-Slugs
- `database/seeders/DatabaseSeeder.php` erweitert:
- `CategorySeeder` wird im Standard-Seedlauf ausgefuehrt
- Neuer Pest-Test `tests/Feature/Categories/CategorySeederTest.php`:
- prueft Erstellung mehrsprachiger Kategorien
- prueft idempotentes Verhalten bei doppeltem Seeder-Lauf
### Entscheidungen
- Kategorien werden aktuell ueber stabile DE-Slugs erkannt, solange kein separates Legacy-Mapping fuer Taxonomie aktiv ist.
### Nächster Schritt
- Offene Seeder-Restpunkte aus P1 (z. B. Salutation-Translations) separat priorisieren oder direkt P2.3 (Magic-Link-Flow) starten.
---
## 2026-04-24 – P2 umgesetzt: Magic-Link-Login End-to-End
**Phase:** P2 (Auth & Tenancy)
**Aufgaben:** Magic-Link-Login (Generierung, Versand, Konsumierung) inkl. Login-UI-Erweiterung und Feature-Tests.
**Status:** ✅
### Was wurde gemacht
- Neuer Service `App\Services\Auth\MagicLinkGenerator`:
- erzeugt One-Time-Token (`sha256` gehasht in DB)
- invalidiert alte, noch offene Login-Links des Users
- setzt TTL und Request-IP
- Neuer Controller `App\Http\Controllers\Auth\MagicLinkConsumeController`:
- prueft Token, Ablauf und Single-Use
- markiert Link als konsumiert inkl. IP
- authentifiziert User ueber `web`-Guard und regeneriert Session
- Neue Mailable `App\Mail\MagicLoginLink` + Template `resources/views/emails/auth/magic-login-link.blade.php`.
- Auth-Routing erweitert:
- `GET /magic-login/{token}` (`magic-links.consume`)
- Login-Volt-Komponente (`resources/views/livewire/auth/login.blade.php`) erweitert:
- Methode `sendMagicLink()`
- UI-Block fuer "Login ohne Passwort" per E-Mail-Link
- Neue Feature-Tests:
- `tests/Feature/Auth/MagicLinkLoginTest.php`
- Faelle: Request, gueltiger Link, abgelaufener Link, bereits konsumierter Link
### Entscheidungen
- Magische Login-Links sind bewusst single-use und zeitlich begrenzt.
- Bei unbekannter/inaktiver E-Mail wird eine neutrale Erfolgsmeldung ausgegeben, um Enumeration zu erschweren.
### Nächster Schritt
- P2.5 starten: `SetCurrentPortal`-Middleware + `CurrentPortalScope` (Global Scope) fuer mandantenbezogene Datensaetze.
---
## 2026-04-24 – P3 vorgezogen: User-Edit mit Rollen, Firmen, Kontakten und Rechnungsadresse
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Benutzerbearbeitung von Demo-/Listenstatus auf produktive Persistenz erweitern.
**Status:** ✅
### Was wurde gemacht
- Neue Volt-Komponente `resources/views/livewire/admin/users/edit.blade.php` erstellt:
- Basisdaten des Users editierbar (`name`, `email`, `portal`, `registration_type`, Statusflags)
- Rollenzuweisung via Spatie (`syncRoles`)
- Firmenverknuepfung inkl. Pivot-Rolle (`member|responsible|owner`) via `company_user`
- Sichtbare und editierbare Kontakte der verknuepften Firmen
- Bearbeitbare Rechnungsadresse (`billing_addresses`) per `updateOrCreate`
- Routing erweitert:
- neue Route `admin.users.edit` (`/admin/users/{id}/edit`)
- User-Liste erweitert (`resources/views/livewire/admin/users.blade.php`):
- Rollen und Firmenanzahl sichtbar
- Edit-Aktion pro Benutzer
- Routing-Doku aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 26 aktive Routen inkl. `admin.users.edit`
### Verifikation
- Neuer Pest-Test `tests/Feature/Admin/UserManagementTest.php`:
- prueft End-to-End: User-Update, Rollen-Sync, Firmen-Pivot-Rollen, Kontakt-Update, Rechnungsadresse
### Entscheidungen
- Kontaktbearbeitung erfolgt im ersten Schritt direkt im User-Edit-Kontext fuer alle verknuepften Firmenkontakte.
- Eigene Kontakt-CRUD-Routen bleiben weiterhin Backlog und koennen spaeter separat aktiviert werden.
### Nächster Schritt
- Optional: `admin.users.create` + Detailansicht nachziehen, um den User-CRUD in P3 vollstaendig zu machen.
---
## 2026-04-24 – P3 erweitert: User-Create nachgezogen
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** User-CRUD vervollstaendigen mit Anlegen-Flow inkl. Rollen/Firmen/Billing.
**Status:** ✅
### Was wurde gemacht
- Neue Volt-Komponente `resources/views/livewire/admin/users/create.blade.php` erstellt:
- Anlegen von Usern mit `portal`, `registration_type`, Statusflags
- Rollenzuweisung via Spatie (`syncRoles`)
- Firmenverknuepfung inkl. Pivot-Rolle (`member|responsible|owner`)
- Optionale Rechnungsadresse direkt beim Anlegen
- Routing erweitert:
- neue Route `admin.users.create` (`/admin/users/create`)
- User-Index erweitert:
- CTA-Button "Benutzer anlegen" in `resources/views/livewire/admin/users.blade.php`
- Routing-Doku aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 27 aktive Routen erweitert
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Create-Szenario erweitert:
- prueft User-Anlage inkl. Rollen, Firmenverknuepfung und optionaler Rechnungsadresse.
### Nächster Schritt
- Optional: User-Detailseite (`admin.users.show`) und dedizierte Kontakt-CRUD-Routen (`admin.contacts.create/edit`) nachziehen.
---
## 2026-04-24 – P3 erweitert: User-Detailseite (`admin.users.show`)
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Nutzeransicht vervollstaendigen mit read-only Detailseite inkl. Firmen-/Kontakt- und Billing-Sicht.
**Status:** ✅
### Was wurde gemacht
- Neue Volt-Komponente `resources/views/livewire/admin/users/show.blade.php` erstellt:
- zeigt Basisdaten, Rollen, letzten Login, Rechnungsadresse
- listet verknuepfte Firmen inkl. Pivot-Rolle
- zeigt Kontakte je verknuepfter Firma
- Routing erweitert:
- neue Route `admin.users.show` (`/admin/users/{id}`)
- User-Index erweitert:
- Eye-Button auf Detailansicht in `resources/views/livewire/admin/users.blade.php`
- Routing-Doku aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 28 aktive Routen erweitert
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Show-Szenario erweitert:
- prueft Rendern der User-Detailseite inkl. Company- und Billing-Daten.
### Nächster Schritt
- Optional: dedizierte Kontakt-CRUD-Routen (`admin.contacts.create/edit`) aktivieren und auf echte Persistenz umstellen.
---
## 2026-04-24 – P3 erweitert: Kontakte-CRUD (Create/Edit) produktiv
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Kontaktstrecke von Demo-Daten auf echte Persistenz umstellen und fehlende Routen aktivieren.
**Status:** ✅
### Was wurde gemacht
- Neue Volt-Komponente `resources/views/livewire/admin/contacts/create.blade.php`:
- Anlegen von Kontakten mit Firmenzuordnung, Portal, Anrede, Rollen-/Kontaktdaten
- optionaler Company-Prefill per Query-Parameter `?company=...`
- Neue Volt-Komponente `resources/views/livewire/admin/contacts/edit.blade.php`:
- Bearbeiten bestehender Kontakte auf Basis echter `Contact`-Model-Daten
- `resources/views/livewire/admin/contacts/index.blade.php` komplett auf produktive Queries umgestellt:
- Suche/Filter via Eloquent
- Firmenfilter aus echter `companies`-Tabelle
- Pagination und Edit-Links auf reale IDs
- Routing erweitert:
- `admin.contacts.create` (`/admin/contacts/create`)
- `admin.contacts.edit` (`/admin/contacts/{id}/edit`)
- Routing-Doku aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 30 aktive Routen erweitert
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Kontakt-Create/Edit-Szenario erweitert:
- prueft Anlage und Bearbeitung von Kontakten ueber die neuen Volt-Formulare.
### Nächster Schritt
- Optional: Companies-Detail/Index enger mit Kontakt-CRUD verknuepfen (z. B. company-spezifische Kontaktansicht als Tab).
---
## 2026-04-24 – P3 Feinschliff: Company-zu-Kontakt-Flow explizit verdrahtet
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Kontakt-Neuanlage direkt aus Company-Context mit klarer Route + Prefill absichern.
**Status:** ✅
### Was wurde gemacht
- Neue CRM-Route ergaenzt:
- `admin.companies.contacts.create` (`/admin/companies/{companyId}/contacts/create`)
- `resources/views/livewire/admin/companies/show.blade.php` angepasst:
- "Kontakt hinzufuegen"-Aktionen zeigen jetzt auf die neue firmenspezifische Route statt Query-only-Variante.
- `resources/views/livewire/admin/contacts/create.blade.php` erweitert:
- `mount(?int $companyId = null)` unterstuetzt jetzt Route-Prefill (plus Fallback auf Query-Param)
- sichtbarer Hinweis, wenn Firma vorausgewaehlt ist
- Routing-Doku aktualisiert:
- `routes/ADMIN_ROUTES.md` auf 31 aktive Routen erweitert
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Prefill-Szenario erweitert:
- `admin.contacts.create` mit `companyId` setzt `companyId` und `isCompanyPrefilled` korrekt.
### Nächster Schritt
- Optional: Company-Detailseite um eigenen Kontakte-Tab mit Inline-Filterung erweitern.
---
## 2026-04-24 – P3 Feinschliff: Company-Detail mit Kontakte-Tab + Inline-Filter
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Company-Detailseite auf echte Daten umstellen und Kontakte innerhalb der Seite filterbar machen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/companies/show.blade.php` von Dummy-Daten auf echte Eloquent-Queries umgestellt:
- laedt `contacts`, `pressReleases`, `users` inkl. Counts
- zeigt reale Firmenstammdaten statt statischer Platzhalter
- Tabs innerhalb der Company-Detailseite eingefuehrt:
- `overview`
- `contacts`
- Im Kontakte-Tab eine Inline-Suche (`contactSearch`) eingebaut:
- filtert nach Name, E-Mail und Rolle direkt innerhalb der Detailseite
- Kontakt-Neuanlage und Kontakt-Bearbeiten bleiben direkt verlinkt
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Filter-Szenario erweitert:
- prueft, dass im Company-Kontakte-Tab die Inline-Suche Treffer zeigt und Nicht-Treffer ausblendet.
### Nächster Schritt
- Optional: Contacts-Index um Portal-Filter und Quick-Jump zur jeweiligen Company-Detailseite erweitern.
---
## 2026-04-24 – P3 Feinschliff: Contacts-Index mit Portal-Filter und Company-Quick-Jump
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Kontakte-Liste fuer operatives Arbeiten erweitern (Mandantenfilter + schneller Firmenkontext).
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/contacts/index.blade.php` erweitert:
- neuer `portalFilter` (`presseecho|businessportal24|both|all`) auf Query-Ebene
- zusaetzliches Portal-Select in den Filtern
- Company-Spalte jetzt mit klickbarem Link auf `admin.companies.show`
- zusaetzlicher Quick-Jump-Button in den Zeilenaktionen zur Firmen-Detailseite
- Query-Logik bleibt paginiert und kombiniert Such-/Firmen-/Portal-Filter.
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Szenario erweitert:
- prueft Portal-Filterung im Contacts-Index
- prueft, dass der Company-Quick-Jump-Link im Render enthalten ist.
### Nächster Schritt
- Optional: Contacts-Index um gespeicherte Filter-Presets (pro User) erweitern.
---
## 2026-04-24 – P3 Feinschliff: Persistente Filter-Presets pro User im Contacts-Index
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Wiederverwendbare Filterkonfigurationen pro User speichern, anwenden und loeschen.
**Status:** ✅
### Was wurde gemacht
- Neue Tabelle `user_filter_presets` eingefuehrt:
- Migration `2026_04_24_120000_create_user_filter_presets_table.php`
- Felder: `user_id`, `page`, `name`, `filters` (JSON), inkl. Unique auf `(user_id, page, name)`
- Neues Model `App\Models\UserFilterPreset` angelegt (JSON-Cast fuer `filters`).
- `App\Models\User` um Relation `filterPresets()` erweitert.
- `resources/views/livewire/admin/contacts/index.blade.php` erweitert:
- Preset speichern (`savePreset`)
- Preset anwenden (`applyPreset`)
- Preset loeschen (`deletePreset`)
- UI-Elemente fuer Preset-Name, Preset-Auswahl und Aktionen integriert
- gespeicherte Werte: `search`, `companyFilter`, `portalFilter`
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Preset-Szenario erweitert:
- prueft Speichern, Anwenden und Loeschen eines Contacts-Filter-Presets pro User.
### Nächster Schritt
- Optional: Default-Preset pro User markieren und beim Oeffnen des Contacts-Index automatisch anwenden.
---
## 2026-04-24 – P3 Feinschliff: Default-Preset im Contacts-Index
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Ein Preset pro User/Seite als Standard markieren und beim Seitenaufruf automatisch anwenden.
**Status:** ✅
### Was wurde gemacht
- `user_filter_presets` erweitert:
- neue Migration `2026_04_24_121500_add_is_default_to_user_filter_presets_table.php`
- neues Feld `is_default` + Index auf `(user_id, page, is_default)`
- `App\Models\UserFilterPreset` erweitert:
- `is_default` in Fillable + Boolean-Cast
- `resources/views/livewire/admin/contacts/index.blade.php` erweitert:
- `mount()` laedt und appliziert automatisch das Standard-Preset
- neue Aktion `setDefaultPreset()` setzt genau ein Preset als Standard pro User/Seite
- UI um Button **"Als Standard"** erweitert
- Preset-Liste kennzeichnet den Standard sichtbar mit `(Standard)`
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Default-Preset-Szenario erweitert:
- prueft Setzen des Standard-Presets
- prueft Auto-Anwendung beim erneuten Laden des Contacts-Index.
### Nächster Schritt
- Optional: Position/Sortierung der Presets per Drag&Drop oder manuellem Sort-Key ergaenzen.
---
## 2026-04-24 – P3 Feinschliff: Preset-Sortierung nach letzter Nutzung
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Presets operativ priorisieren (Standard zuerst, danach zuletzt verwendete).
**Status:** ✅
### Was wurde gemacht
- `user_filter_presets` erweitert:
- neue Migration `2026_04_24_123000_add_last_used_at_to_user_filter_presets_table.php`
- neues Feld `last_used_at` + Index auf `(user_id, page, last_used_at)`
- `App\Models\UserFilterPreset` erweitert:
- `last_used_at` als Datetime-Cast
- `resources/views/livewire/admin/contacts/index.blade.php` angepasst:
- Preset-Liste sortiert jetzt nach:
1) `is_default` DESC
2) `last_used_at` DESC
3) `name` ASC
- `applyPreset()` aktualisiert `last_used_at`
- `savePreset()` setzt initiales `last_used_at` auf `now()`
### Verifikation
- `tests/Feature/Admin/UserManagementTest.php` um Szenario erweitert:
- prueft, dass `applyPreset()` den Timestamp aktualisiert und ein zuvor aelteres Preset nach Nutzung als "zuletzt verwendet" markiert ist.
### Nächster Schritt
- Optional: manuelle Preset-Reihenfolge (Sort-Key) als erweitertes UX-Feature einbauen.
---
## 2026-04-30 – Admin Pressemitteilungen: User-, Firmen- und Kontakt-Livefilter
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Aus dem User-Detailmodal direkt in die PM-Liste springen und dort nach User/Firma/Kontakt per Live-Suche filtern.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/users.blade.php` erweitert:
- Detailmodal zeigt bei Pressemitteilungen einen Direktlink zu den veroeffentlichten PMs des Users.
- Link setzt die PM-Filter per Query-String (`user`, `status=published`).
- `resources/views/livewire/admin/press-releases/index.blade.php` erweitert:
- URL-Filter fuer `status`, `user`, `company`, `contact`.
- Livewire/Flux-Comboboxen fuer User-, Firmen- und Kontakt-Suche.
- Filter greifen performant auf `user_id`, `company_id` und die PM-Kontakt-Relation.
- Feinschliff: obere Filter als responsive Grid-Zeile, Button **Neue PM** aus der Filter-Card herausgezogen.
- Live-Suchen laden initial keine Optionsdaten mehr; Ergebnisse werden erst nach Suchbegriff oder bestehender Auswahl geladen.
- Aktive User-/Firmen-/Kontaktfilter koennen per kleinem X wieder zurueckgesetzt werden.
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- prueft den Direktlink aus dem User-Detailmodal.
- prueft User-, Firmen- und Kontaktfilter in der PM-Uebersicht inkl. Live-Suchergebnissen.
- prueft, dass die Live-Suchergebnisse initial leer bleiben und Reset-Aktionen funktionieren.
### Verifikation
- `vendor/bin/pint --format agent resources/views/livewire/admin/press-releases/index.blade.php resources/views/livewire/admin/users.blade.php tests/Feature/Admin/UserManagementTest.php`
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` -> 22 passed, 169 assertions
---
## 2026-04-30 – Admin Firmen/Kontakte: PM-Counts und erweiterte Filter
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Firmen- und Kontaktuebersichten mit weiteren Filtern und sichtbaren PM-/Relationszaehlern erweitern.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/contacts/index.blade.php` erweitert:
- PM-Count pro Kontakt per `withCount('pressReleases')`.
- Neue Datenstandsfilter: mit/ohne Pressemitteilungen.
- Neuer User-Livefilter zusaetzlich zum bestehenden Firmen-Livefilter.
- PM-Count verlinkt direkt zur PM-Liste mit gesetztem `contact`-Filter.
- Contact-Presets speichern und laden jetzt auch User- und Datenstandsfilter.
- `resources/views/livewire/admin/companies/index.blade.php` erweitert:
- Datenstandsfilter fuer mit/ohne Pressemitteilungen und mit/ohne Kontakte.
- Neue Livefilter fuer User und Kontakte.
- PM- und Kontakt-Counts bleiben sichtbar und verlinken direkt in die gefilterten PM-/Kontaktlisten.
- UX-Feinschliff:
- Alle suchbaren Select-/Combobox-Felder in PM-, Firmen-, Kontakt- und User-Bearbeiten-Flows haben jetzt einen eigenen kleinen X-Button zum Zuruecksetzen der jeweiligen Suche/Auswahl.
- `tests/Feature/Admin/UserManagementTest.php` erweitert:
- prueft Kontakt-PM-Counts, Contact-Filter und User-/Firmen-Livefilter.
- prueft Firmen-PM-/Kontakt-Counts, Datenstandsfilter und User-/Kontakt-Livefilter.
- prueft die neuen Reset-Aktionen fuer die suchbaren Selectfelder.
### Verifikation
- `vendor/bin/pint --format agent resources/views/livewire/admin/contacts/index.blade.php resources/views/livewire/admin/companies/index.blade.php tests/Feature/Admin/UserManagementTest.php`
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` -> 24 passed, 212 assertions
---
## 2026-04-30 – Legacy Firmenlogos: Sync vorbereitet
**Phase:** P6 (Daten-Migration) / P3 (Admin-UI)
**Aufgaben:** Aus den abgelegten Legacy-Logoordnern nur die weiterhin referenzierten Firmenlogos in einen sauberen Storage-Pfad übernehmen und mit `companies.logo_path` verknüpfen.
**Status:** 🔄 vorbereitet, echter Sync noch nicht ausgeführt
### Was wurde gemacht
- Neuer Artisan-Command `legacy:sync-company-logos`:
- Quelle: `storage/app/public/{portal}/company`
- Ziel: `storage/app/public/company-logos/{portal}/{company_id}/{dateiname}`
- aktualisiert `companies.logo_path` auf den neuen relativen Public-Storage-Pfad
- unterstützt `--portal=presseecho|businessportal24|all`
- unterstützt `--dry-run` und `--force`
- arbeitet nicht-destruktiv: Quellordner werden nicht gelöscht oder verschoben
- meldet fehlende referenzierte Dateien und ungenutzte Dateien pro Portal
- Tests ergänzt:
- erfolgreicher Kopier-/DB-Verknüpfungsfall
- Dry-Run verändert keine Dateien und keine DB-Werte
- fehlende referenzierte Dateien führen zu einem fehlgeschlagenen Command
### Dry-Run gegen lokale Logoordner
- `presseecho`: 951 referenziert, 951 kopierbar, 0 fehlend, 0 ungenutzt
- `businessportal24`: 3.855 referenziert, 3.849 kopierbar, 6 fehlend, 53 ungenutzt
- Gesamt: 4.806 referenziert, 4.800 kopierbar, 6 fehlend, 53 ungenutzt
### Verifikation
- `vendor/bin/pint --format agent app/Console/Commands/SyncCompanyLogos.php tests/Feature/SyncCompanyLogosTest.php`
- `php artisan test --compact tests/Feature/SyncCompanyLogosTest.php` -> 3 passed, 11 assertions
- `php artisan legacy:sync-company-logos --dry-run` -> erwartungsgemäß fehlgeschlagen wegen 6 fehlender BP24-Dateien
### Nächster Schritt
- Entscheiden, ob die 6 fehlenden BP24-Logos nachgeliefert werden oder ob wir diese Firmen ohne Logo übernehmen.
- Danach echten Sync ausführen: `php artisan legacy:sync-company-logos`
---
## 2026-04-30 – Admin Firmen/Kontakte: Portalzuordnung sichtbar
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Migrierte Firmen und Kontakte muessen ihre Portalzuordnung in Listen und Detail-/Bearbeitungsansichten sichtbar zeigen.
**Status:** ✅
### Was wurde gemacht
- `resources/views/livewire/admin/companies/index.blade.php`:
- neue Tabellenspalte **Portal** mit farbigem Flux-Badge.
- `resources/views/livewire/admin/companies/show.blade.php`:
- Detailheader nutzt jetzt das Portal-Label statt technischem Uppercase-Wert.
- Kontakte im Firmen-Detailtab zeigen ebenfalls ein Portal-Badge.
- `resources/views/livewire/admin/contacts/index.blade.php`:
- neue Tabellenspalte **Portal** mit farbigem Flux-Badge.
- `resources/views/livewire/admin/contacts/edit.blade.php`:
- Bearbeitungskopf zeigt die aktuelle Portalzuordnung als Badge.
- `tests/Feature/Admin/UserManagementTest.php`:
- Assertions fuer Portalspalten/-badges in Firmen- und Kontaktuebersicht.
- Assertions fuer Portal-Badge in Firmen-Detail und Kontakt-Bearbeitung.
### Verifikation
- `vendor/bin/pint --format agent resources/views/livewire/admin/companies/index.blade.php resources/views/livewire/admin/companies/show.blade.php resources/views/livewire/admin/contacts/index.blade.php resources/views/livewire/admin/contacts/edit.blade.php tests/Feature/Admin/UserManagementTest.php`
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` -> 24 passed, 219 assertions
---
## 2026-04-30 – Admin Firmen: Portalfilter und Logo-Vorschau
**Phase:** P3 (Admin-UI an Models binden)
**Aufgaben:** Firmen nach Portal filterbar machen und Firmenlogos in Liste, Detailansicht und Bearbeitung sichtbar anzeigen.
**Status:** ✅
### Was wurde gemacht
- `app/Models/Company.php`:
- neue zentrale Methode `logoUrl()` fuer Public-Storage-URLs.
- unterstuetzt bereits synchronisierte Pfade (`company-logos/...`) und Legacy-Logoordner (`storage/app/public/{portal}/company/...`).
- `resources/views/livewire/admin/companies/index.blade.php`:
- neuer URL-synchronisierter Portalfilter (`?portal=...`).
- Logo-Miniatur in der Firmenzeile mit Platzhalter, wenn kein Logo aufloesbar ist.
- `resources/views/livewire/admin/companies/show.blade.php`:
- Detailheader nutzt ebenfalls `logoUrl()` statt rohem DB-Pfad.
- `resources/views/livewire/admin/companies/edit.blade.php`:
- aktuelle Logo-Vorschau im Bearbeiten-Flow nutzt dieselbe zentrale URL-Logik.
- `tests/Feature/Admin/UserManagementTest.php`:
- Assertions fuer Portalfilter und Legacy-Logo-Vorschau in Liste, Ansicht und Bearbeitung.
### Verifikation
- `php vendor/bin/pint --format agent app/Models/Company.php resources/views/livewire/admin/companies/index.blade.php resources/views/livewire/admin/companies/show.blade.php resources/views/livewire/admin/companies/edit.blade.php tests/Feature/Admin/UserManagementTest.php`
- `php artisan test --compact tests/Feature/Admin/UserManagementTest.php` -> 24 passed, 225 assertions
---