presseportale/dev/migration 2026/08-PROGRESS.md
Kevin Adametz 1cd4d8e33a P6.6: legacy:grandfather-subscriptions — aktive Legacy-Abos aus dem Rechnungsarchiv migrieren
Kriterien vom Auftraggeber (12.06.2026): Quelle der Aktiv-Erkennung ist
ausschliesslich das read-only Rechnungsarchiv legacy_invoices (D-12).
Legacy-Rechnungen bleiben Archiv; neue manuelle Rechnungen entstehen im
MAN-Rechnungskreis.

- Aktiv-Regel: juengste Rechnung pro (Portal, Legacy-Vereinbarung) mit
  payment_option.type=recurring und user_payment_option.status=active;
  next_due_date max. --grace-months (Default 12) ueberfaellig, sonst
  stale -> bleibt reines Archiv. Einmal-Kaeufe werden nie uebernommen.
- Uebernahme als grandfathered in user_payment_options:
  current_period_end = next_due_date, Betraege/Intervall der letzten
  Legacy-Rechnung in legacy_conditions -> der taegliche MAN-Lauf
  (billing:generate-manual-invoices) fakturiert zum gewohnten
  jaehrlichen Rhythmus weiter. Versteckte Katalog-Platzhalter
  LEGACY-{PE|BP}-{Artikel} in payment_options.
- Replay-faehig (D-18): Re-Runs aktualisieren anhand der Legacy-IDs in
  legacy_conditions statt zu duplizieren — die Kern-Migration laeuft
  kurz vor dem Relaunch erneut.
- Optionen: --dry-run, --as-of, --grace-months, --no-report; JSON-Report
  nach storage/app/migration/. Dry-Run gegen Test-Snapshot: 22 aktive
  jaehrliche Vereinbarungen, davon 4 sofort faellig, 0 stale.
- Doku: MIGRATION-STEPS.md (Runbook-Reihenfolge nach archive-invoices),
  05-DATABASE-MERGE §5.6, 12-NAECHSTE-SCHRITTE 6.6, 08-PROGRESS,
  PHASE-9-Plan + Checkliste.

Tests: GrandfatherLegacySubscriptionsTest (7, inkl. End-to-End
Migration -> MAN-Rechnung mit Legacy-Betraegen). Suite: 475 passed,
4 skipped. Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:35:48 +00:00

3096 lines
145 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `<livewire:components.press-release-images-manager :press-release-id="$id" />` 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`. `<meta name="robots" content="noindex,nofollow">` 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.50.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.50.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.50.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 (`&#8201;` 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: 1220). 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 | 47s |
| contacts | 8.366 | 8.597 | <1s |
| press-releases | 80.033 | 100.305 | 3145s |
| **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 <Titel>
**Phase:** <P1/…>
**Aufgaben:** <Kurzer Stichpunkt>
**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 + 0008)
### 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
---