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