presseportale/dev/migration 2026/08-PROGRESS.md
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

143 KiB
Raw Blame History

08 Fortschrittslog

Chronologisches Protokoll aller Migrationsschritte. Jede Session / jeder Commit hinterlässt einen Eintrag.


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

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 Presseportale-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.

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.


Phase: P3 (Admin-UI) + P4 (Customer) + P5 (Domain-Services) + P6.8 (Legacy-Bilder-Sync) Status: umgesetzt

Was wurde gemacht

Schritt 1 PressRelease-Bilder durchgängig auf ImageService gestellt + Legacy-Sync (P3.2 / P3.4 / P5.5 / P6.8)

  • App\Services\Image\ImageService erweitert um:
    • storePressReleaseImage(UploadedFile, pressReleaseId) mit drei Varianten thumb (320×240), medium (800×600), large (1600×1200) per Cover-Resize (zentriert beschnitten, statt Letterbox).
    • Generischer generateVariants(...$cover = false)-Pfad — Logos nutzen weiterhin contained, PR-Bilder cover.
    • deletePressReleaseImage() und generateMissingPressReleaseVariants() für Sync- und Delete-Flows.
    • Validierung mit eigenem Mime-Set (image/jpeg|png|webp) und 8 MB Maximum (MAX_PRESS_RELEASE_IMAGE_BYTES).
  • App\Models\PressReleaseImage mit url() + variantUrl(key)-Helpers, fallback-fest auch für nicht-public Disks.
  • App\Http\Controllers\Api\V1\PressReleaseImageController nutzt jetzt den ImageService (statt Storage::disk('public')->put + getimagesize direkt). API liefert data.variants und data.urls.{thumb,medium,large,original}.
  • App\Http\Resources\PressReleaseImageResource zeigt Varianten-URLs öffentlich; data.url bleibt rückwärtskompatibel.
  • StorePressReleaseImageRequest::rules von max:5120 auf max:8192 erhöht (synchron mit ImageService).
  • Admin-Logo-Upload (livewire/admin/companies/edit) auf ImageService::storeCompanyLogo() umgestellt inkl. Variant-Generierung, Logo-Entfernen-Button und „Logo entfernen rückgängig"-Flow.
  • Wiederverwendbare Volt-Komponente livewire/components/press-release-images-manager für Multi-Image-Upload, Sortierung (Hoch/Runter), Preview-Flag setzen, Löschen. Wird in Admin-PR-Edit (livewire/admin/press-releases/edit) und Customer-PR-Edit (livewire/customer/press-releases/edit) per <livewire:components.press-release-images-manager :press-release-id="$id" /> eingebunden. Authorisierung über PressReleasePolicy::update; Customer kann nur Draft/Rejected ändern, Admin alles außer Archived.
  • Neuer Command legacy:sync-press-release-images (App\Console\Commands\SyncPressReleaseImages) analog zu legacy:sync-company-logos:
    • --portal=presseecho|businessportal24|all, --dry-run, --force, --skip-variants, --limit=N.
    • Kopiert nur referenzierte Legacy-Dateien aus storage/app/public/{portal}/press_release/ nach storage/app/public/press-releases/{prId}/images/{filename}, generiert per ImageService die Varianten und aktualisiert press_release_images (Pfad + Varianten + Bildmaße).
    • Bereits migrierte Bilder, denen Varianten fehlen, werden nachträglich versorgt.
    • Berichtet Zahl der ungenutzten Quelldateien — Anhaltspunkt für Cleanup-Skripte.
  • App\Services\Import\PressReleaseImporter setzt jetzt legacy_portal/legacy_id auch auf press_release_images, damit der Sync-Command stabil matchen kann.
  • Tests: erweiterter tests/Feature/Api/V1/PressReleaseImageApiTest (Variantenprüfung, Cleanup beim Delete) + neuer tests/Feature/SyncPressReleaseImagesTest mit 4 Cases (Happy-Path inkl. Varianten, Dry-Run, Missing-Files → Failure, „bereits migriert + Varianten fehlen → werden generiert").

Schritt 2 BlacklistService + Integration in Publish-Flow (P5.2)

  • config/blacklist.php als initiale Wortliste (Stub vom Auftraggeber zu erweitern oder später per Admin-UI pflegbar).
  • App\Services\PressRelease\BlacklistService:
    • case-insensitiver, ganzwörtlicher Vergleich (Sonderzeichen werden ignoriert, Mehrwort-Phrasen werden korrekt erkannt).
    • Lazy-Load der Wortliste, dependency-injectable (Tests injizieren ihre eigene Liste, in Prod kommt sie aus der Config).
  • App\Services\PressRelease\PressReleaseService::submitForReview() und publish() rufen jetzt BlacklistService::findInPressRelease() auf:
    • Treffer → PM wird automatisch auf rejected gesetzt, Autor bekommt PressReleaseRejected-Mail mit Begründung („unzulässiges Wort … gefunden"), und es wird eine BlacklistViolationException geworfen.
  • Neue App\Services\PressRelease\BlacklistViolationException mit dem getroffenen Wort als Property — Caller können sie gezielt abfangen ohne andere LogicExceptions mit zu erwischen.
  • Caller-Schicht: Customer-PR-Detail (livewire/customer/press-releases/show), Customer-Liste (.../index), Admin-Edit (livewire/admin/press-releases/edit) und Admin-Detail (livewire/admin/press-releases/show) reagieren mit session()->flash('error', …) auf die Exception.
  • Tests: tests/Feature/PressReleaseBlacklistTest mit 4 Cases (BlacklistService direkt, Submit mit verbotenem Wort, sauberer Publish, Phrase erkannt).

Schritt 3 HasUniqueSlug-Trait (P5.6)

  • Neuer Trait App\Models\Concerns\HasUniqueSlug mit generateUniqueSlug(string $source, array $scope = []):
    • Models definieren slugScopeAttributes() und slugFallback().
    • Berücksichtigt automatisch das eigene Modell, wenn es bereits gespeichert ist (verhindert „eigene Existenz blockiert sich selbst").
    • Hook applySlugConstraints(Builder) für künftige Spezialfälle.
  • App\Models\PressRelease: scope ['portal', 'language'], fallback pressemitteilung.
  • App\Models\Company: scope ['portal'], fallback company.
  • Caller umgestellt: livewire/customer/press-releases/create, livewire/admin/press-releases/{create,edit}, livewire/admin/companies/{create,edit} rufen jetzt (new Model)->generateUniqueSlug(...) bzw. $model->generateUniqueSlug(...) auf — die alten privaten Helfer sind raus.
  • Category bleibt aussen vor (Slugs liegen in Translations-Tabelle, separater Mechanismus). CompanyImporter behält seinen eigenen Slug-Pfad (legacy-Slug bevorzugt).
  • Tests: tests/Feature/HasUniqueSlugTest mit 4 Cases (Scope-Einhaltung, Edit hält eigenen Slug, Cross-Portal-Fallthrough, Fallback bei leerem Source).
  • App\Services\Auth\MagicLinkGenerator::createPressReleaseShareLink(PressRelease, ?User, int $ttlDays = 7):
    • Token + SHA-256-Hash, gleiche Sicherheitscharakteristik wie der Login-Link.
    • MagicLink.purpose = 'press_release_access', payload = {press_release_id}.
    • Default 7 Tage gültig, kein Login-Effekt.
  • Neue Public-Route GET /pm-vorschau/{token} (regex 40+ chars) → App\Http\Controllers\PressReleasePreviewController rendert read-only Blade-View press-release-preview.
    • Bei ungültigem/abgelaufenem Token wird press-release-preview-error mit 404 bzw. 410 Gone ausgeliefert.
    • Funktioniert auch für draft/review/rejected/archived-PMs, jedoch ohne Authentifizierung — das ist der Sinn („den Kollegen den Entwurf zeigen").
  • Customer-PR-Detail (livewire/customer/press-releases/show) hat jetzt Button „Vorschau-Link", der den Link generiert und im UI zum Kopieren anzeigt + Ablaufzeitpunkt.
  • Standalone-Blade-Views nutzen reines CSS (kein Vite-Manifest, kein FluxUI), damit der Link auch unabhängig vom Build funktioniert. Dark-Mode-tauglich via prefers-color-scheme. <meta name="robots" content="noindex,nofollow"> schützt vor Indexierung.
  • Tests: tests/Feature/PressReleasePreviewTest mit 5 Cases (gültig + Inhalt sichtbar, kein Auth nötig, abgelaufen → 410, ungültiger Token → 404, force-deleted PM → 404).

Cleanup

  • Neue pint.json schließt die Symfony-1.4-Legacy-Ordner (_businessportal24.com, _presseecho.com, dev/_old, dev/migration 2026/_archiv) aus, damit Pint keine PHP-5.6-Parse-Errors mehr meldet. Pint läuft jetzt grün durch (vendor/bin/pint --dirty --format agentresult: passed).

Test-Lage

  • Komplette Suite: 141 passed, 3 skipped, 0 failed (vorher: 124 / 3). Neu hinzugekommen: 17 Tests verteilt über die 4 Schritte. Das Reporting des Slow-Admin-Logs und API-Usage bleibt unberührt.
  • Pint sauber, Linter ohne neue Findings.

Aufgeräumte Plan-Punkte (siehe 03-MIGRATION-PLAN.md)

  • P3.2 (PM-Bilder im Customer-Portal pflegen)
  • P3.4 (Admin-Firmen-Logo via ImageService)
  • P5.2 (BlacklistService)
  • P5.5 (ImageService inkl. PR-Varianten)
  • P5.6 (Slug-Trait)
  • P6.8 (Legacy-PM-Bilder-Sync-Command)
  • Public-Vorschau-Link (Feature aus 06-FEATURES-SCOPE.md Tabelle 4)

Offen (ohne externen Input)

  • P5.3 NewsletterService neu (subscribe/confirm/unsubscribe + Admin-CSV-Export).
  • P3.3 Admin Categories Create/Edit (Index existiert).
  • P3.7 Admin-Newsletter-Workflow (heute nur Sync-UI).
  • P3.11 Admin-CRUD für Footer-Codes.
  • P5.7 Domain-Events (PressReleasePublished / Rejected / SubmittedForReview).
  • Failure-Alerting für Queue-Worker (P9.3) + Supervisor-Setup-Doku.

Wartet auf externen Input

  • P0.50.7 Stripe / Produktliste / Grandfathering blockiert die gesamte Phase 8.
  • P11.2 Staging-Server, P11.4 Mailtexte für Go-Live.

2026-05-04 Test-Suite stabilisiert + Customer-Portal abgeschlossen

Phase: Querschnitt (Stabilisierung) + P4 (Customer-Portal) + P5.5 (ImageService) Status: umgesetzt

Was wurde gemacht

Paket A Test-Suite stabilisiert

  • phpunit.xml: APP_URL=https://presseportale.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

vendor/bin/pint --dirty --format agent
# unsere Dateien automatisch formatiert; Errors nur in _businessportal24.com (Legacy-Referenz, nicht unser Scope)

php artisan test --compact
# 124 passed, 3 skipped (vorher 108 passed, 12 failed)

php artisan test --compact tests/Feature/CustomerProfileSecurityTest.php
# 7 passed (Profil-Editor, Logo-Upload + Variants, Company-Auth-Negativtest, Security-Render, Passwort-Change, PressRelease- und LegacyInvoice-Policy-Negativtests)

Plan-Status nach dieser Session

  • P3.4 Logo-Upload mit Varianten: Customer-Pfad ; Admin-Pfad nutzt jetzt dieselbe Service-Schicht und kann nachgezogen werden.
  • P4.6 Customer-Profile/Company-Pflege
  • P4.7 Customer-Security (Passwort/2FA/E-Mail)
  • P4.8 Policies für PressRelease, Company, Contact, LegacyInvoice
  • P5.5 ImageService-Fundament (Logos); PressRelease-Bilder können auf gleicher Service-Klasse aufsetzen.

Nächster Schritt

  • P8 (Stripe/Billing) bleibt der größte Releaseblocker; benötigt P0.50.7 (Stripe-Test-Account, Produktliste, Grandfathering-Kriterien) vom Auftraggeber.
  • Ohne externe Inputs gut machbar:
    • PressRelease-Bilder-Upload im Customer-/Admin-Portal über ImageService ergänzen (mehrere Bilder, Alt-Texte, Sortierung)
    • legacy:sync-press-release-images als Pendant zu legacy:sync-company-logos
    • Admin-Logo-Upload/-Edit auf ImageService umstellen
    • BlacklistService (P5.2) und neuer NewsletterService (P5.3)

2026-04-30 Admin Users: Datenstandsfilter für Pressemitteilungen

Phase: P3 (Admin-UI / User-Verwaltung) Status: umgesetzt

Was wurde gemacht

  • Datenstandsfilter in der User-Übersicht um Pressemitteilungen erweitert:
    • Mit Pressemitteilungen
    • Ohne Pressemitteilungen
    • Mit veröffentlichten PMs
    • Ohne veröffentlichte PMs
  • Veröffentlichte PMs werden statusbasiert über PressReleaseStatus::Published gefiltert.
  • Regressionstest ergänzt, der User mit veröffentlichter PM korrekt ein- und ausschließt.

Verifikation

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

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

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

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

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

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

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_profileprofiles
    • Admin-User-Edit-Test für Laden/Speichern der Profilfelder

Verifikation

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:

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

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

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:

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

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_keys und keine Payloads

Verifikation

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:

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

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:

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

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

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

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

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_keys
  • 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

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

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.

php artisan legacy:archive-invoices --dry-run  # prüfen
php artisan legacy:archive-invoices            # beide Portale

Post-Import-Bereinigung

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
php artisan legacy:verify --no-report
# 0 Fehler | 0 Warnungen

Vollständige Import-Reihenfolge (Go-Live Runbook)

php artisan legacy:import --source=presseecho        --step=categories      --force
php artisan legacy:import --source=all               --step=users           --force
php artisan legacy:import --source=presseecho        --step=companies       --force
php artisan legacy:import --source=businessportal24  --step=companies       --force
php artisan legacy:import --source=presseecho        --step=contacts        --force
php artisan legacy:import --source=businessportal24  --step=contacts        --force
php artisan legacy:import --source=presseecho        --step=press-releases  --force
php artisan legacy:import --source=businessportal24  --step=press-releases  --force
php artisan legacy:import --step=link-associations   --force
php artisan legacy:archive-invoices
php artisan legacy:fix-timestamps
php artisan legacy:verify

Nächster Schritt

  • P0.50.7: Stripe-Zugangsdaten + Produktliste vom Auftraggeber (blockiert P8)
  • P7: API v1 (Endpoints, Sanctum Token-Abilities, 410 Gone Handler)
  • P10: Security-Review + Test-Coverage
  • P11: Staging-Deployment + Rehearsal-Lauf + Go-Live-Mailing

2026-04-27 Import-Bugfixes + Timestamps + User-Kontakt-Verknüpfung

Phase: P6 (Datenmigration Qualitätssicherung) Status:

Was wurde gemacht

Import-Fehler behoben: phone/fax zu kurz (VARCHAR 40 → 255)

Beim echten Companies-Import (Presseecho) schlugen 20 Datensätze fehl, weil Legacy-Telefonnummern Beschreibungstext enthielten (bis 92 Zeichen, z.B. „0700 27862537 (0700 ARTMaker, zum Festnetztarif de)").

  • Migration 2026_04_27_115212_expand_phone_fax_columns: phone + fax in companies und contacts von VARCHAR(40) auf VARCHAR(255) erweitert.
  • CompanyImporter + ContactImporter: neue cleanText()-Methode dekodiert HTML-Entities (&#8201; etc.) und kürzt auf die Feldlänge als letzten Fallback.
  • Re-Import mit --force: 41.290 Firmen aktualisiert, 20 neu importiert → 0 Fehler.

Timestamps aus Legacy-DBs übernehmen

Problem: Alle 119k importierten Datensätze hatten created_at = heute, da die Importers ursprünglich kein withoutTimestamps nutzten.

Fixes:

  • UserImporter: User::withoutTimestamps() + created_at/updated_at aus sf_guard_user übernehmen.
  • CompanyImporter (bereits vorhanden) + ContactImporter (bereits vorhanden): beide nutzen jetzt korrekt withoutTimestamps.
  • Neuer Command legacy:fix-timestamps für bereits importierte Daten: nutzt Cross-DB-JOINs auf dem gleichen MySQL-Server korrigiert 119.287 Datensätze in 2,6 Sekunden.
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.

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.
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)

php artisan legacy:import --source=presseecho        --step=categories      --force
php artisan legacy:import --source=all               --step=users           --force
php artisan legacy:import --source=presseecho        --step=companies       --force
php artisan legacy:import --source=businessportal24  --step=companies       --force
php artisan legacy:import --source=presseecho        --step=contacts        --force
php artisan legacy:import --source=businessportal24  --step=contacts        --force
php artisan legacy:import --source=presseecho        --step=press-releases  --force
php artisan legacy:import --source=businessportal24  --step=press-releases  --force
php artisan legacy:import --step=link-associations   --force
php artisan legacy:fix-timestamps

Verifikation

  • Companies Presseecho: 41.310 importiert
  • Users (beide Portale): ~69.650 importiert
  • Timestamps: alle historisch korrekt
  • contact_user: 8.367 Verknüpfungen

2026-04-27 Admin-UI: Tabellen sortierbar + 100 pro Seite + Spalten-Fixes

Phase: P3 (Admin-UI Qualität) Status:

Was wurde gemacht

Alle Admin-Tabellen: Pagination auf 100, sortierbare Spalten

Alle 6 Tabellen-Views auf 100 Einträge pro Seite erhöht (vorher: 1220). Flux-Pro sortable / sorted / direction Props mit wire:click="sort('column')" in alle Köpfe integriert.

View Sortierbare Spalten Standard
Users Vorname, E-Mail, Portal, Status, Letzter Login, Hinzugefügt Hinzugefügt ↓
Companies Name, E-Mail, Status, PMs, Kontakte, Hinzugefügt Name ↑
Contacts Name, E-Mail, Firma, Hinzugefügt Nachname ↑
PMs (Admin) Titel, Status, Portal, Hits, Erstellt Erstellt ↓
Kategorien Standard (ID), PMs ID ↑
PMs (Kunde) Titel, Status, Erstellt Erstellt ↓

Spalten-Fixes

Companies Index:

  • „Website"-Spalte entfernt
  • „Hinzugefügt" (created_at) hinzugefügt

Contacts Index:

  • „Position"-Spalte entfernt
  • „Hinzugefügt" (created_at) war bereits vorhanden und korrekt

Users Index:

  • Bug behoben: Aktions-Icons erschienen in der „Erstellt"-Spalte (Datenzelle fehlte)
  • Name aufgeteilt: „Vorname" + „Nachname" als separate Spalten (aus name gesplittet, kein separates DB-Feld)
  • „Typ"-Spalte entfernt, Colspan im Empty-State korrigiert

Company Show Kontakte-Tab:

  • Input + Select + „Zuordnen"-Button ersetzt durch Flux-Combobox mit Live-Suche (ab 1 Zeichen, max. 50, Auswahl löst sofort attachExistingContact() aus)

Contacts Index Company-Filter als Combobox

Problem: Company::query()->orderBy('name')->get() lud nach dem Import alle 82.820 Firmen in den Livewire-State → Livewire-Fehler.

Fix: Company-Filter jetzt als Flux-Combobox mit Backend-Suche. Ohne Eingabe: kein Load. Mit Eingabe: max. 50 Firmen live. Livewire-State enthält maximal 50 statt 82.820 Firmen.


2026-04-27 Phase 6 gestartet: Legacy-Import-Infrastruktur + Dry-Run

Phase: P6 (Datenmigration) Status: Infrastruktur + Dry-Run abgeschlossen bereit für echten Import

Was wurde gemacht

Voraussetzung erfüllt (P0.3): Legacy-DB-Connections funktionieren:

  • mysql_presseecho → presseecho (43 Tabellen)
  • mysql_businessportal → businessportal (42 Tabellen)

Neue Dateien Import-Infrastruktur (app/Services/Import/):

  • ImportContext hält Portal, Connection, dry-run/force-Flags
  • ImportResult zählt importiert/übersprungen/aktualisiert/Fehler
  • UserImporter sf_guard_user + sf_guard_user_profile + Rollen (inkl. Gruppen-Mapping)
  • CompanyImporter company + company_user + responsible_company_user → Pivot mit Rollen
  • ContactImporter contact → contacts (Salutation-Mapping)
  • CategoryImporter category + category_translation → categories + translations (DE/EN)
  • PressReleaseImporter press_release + press_release_image + press_release_contact

Master-Command: app/Console/Commands/ImportLegacyData.php

  • Befehl: php artisan legacy:import
  • Optionen: --source=presseecho|businessportal24|all, --step=categories|users|companies|contacts|press-releases|all, --dry-run, --force
  • Idempotent via legacy_import_map (skip bereits importierter Datensätze)
  • Chunk-basiert (500 Records pro DB-Query) speichereffizient

Status-Mapping Legacy → Neu:

Legacy Neu
new / edited draft
prepublished review
published published
rejected rejected

Gruppen-Mapping Legacy → Rollen:

Gruppe Rolle
admin (1) admin
editor (2) editor
dataFetcher (3) api-only
unlimitedPressReleasesPayment (4) customer
keine Gruppe customer

Dry-Run-Ergebnisse (0 Fehler)

Schritt presseecho businessportal24 Zeit
categories 14 (identisch) <1s
users 19.372 (292 skip) ~47k 2s
companies 41.310 66.525 47s
contacts 8.366 8.597 <1s
press-releases 80.033 100.305 3145s
Gesamt ~149k ~223k 37 / 58s

Nächster Schritt

  • Echten Import starten: php artisan legacy:import --source=presseecho --step=categories --force (Kategorien zuerst)
  • Danach: --step=users--step=companies--step=contacts--step=press-releases
  • Ggf. Legacy-Rechnungen archivieren (legacy:archive-invoices noch nicht implementiert, P6.5)

2026-04-27 P2.7 + P4 + P5.1 + P9.2: GoLive-Mailing, Customer-Portal, Services, Scheduler

Phase: P2.7 · P4 · P5.1 · P9.2 Status:

Was wurde gemacht

P2.7 GoLive-Mailing

  • App\Mail\GoLivePasswordReset + Template emails/auth/go-live-password-reset.blade.php
  • App\Console\Commands\SendGoLiveMails (auth:send-go-live-mails):
    • --dry-run → zählt nur, schickt nichts
    • --portal=presseecho → filtert nach Portal
    • --limit=10 → für Tests
    • --force → überspringt Bestätigungsabfrage
    • Erzeugt Fortify-Passwort-Reset-Token pro User, sendet personalisierten Link

P5.1 PressReleaseService + Benachrichtigungen

  • App\Services\PressRelease\PressReleaseService mit Methoden:
    • submitForReview(), publish(), reject($reason), backToDraft(), archive()
    • assertStatus() wirft LogicException bei ungültigem Übergang
    • notifyAuthor() sendet automatisch Mail nach publish/reject via Queue
  • App\Mail\PressReleasePublished + Template
  • App\Mail\PressReleaseRejected + Template (mit optionalem Ablehnungsgrund)
  • Admin-Views press-releases/edit.blade.php + show.blade.php nutzen jetzt den Service
  • Service als Singleton in AppServiceProvider gebunden

P9.2 Scheduler-Commands

  • App\Console\Commands\PurgeMagicLinks (magic-links:purge --days=30): löscht verbrauchte/abgelaufene Links
  • App\Console\Commands\PurgeExpiredPressReleaseDrafts (press-releases:purge-drafts --days=180 --dry-run): archiviert Zombie-Entwürfe
  • routes/console.php mit Laravel-Scheduler-Einträgen:
    • täglich 03:00: Magic-Links bereinigen
    • wöchentlich sonntags 04:00: PM-Entwürfe archivieren

P4 Customer-Portal Grundgerüst

  • App\Http\Middleware\EnsureUserIsCustomer → prüft canAccessCustomer()
  • routes/customer.php: Route-Gruppe customer.* mit Middleware, in domains.php eingebunden
  • Neue Volt-Komponenten unter resources/views/livewire/customer/:
    • dashboard.blade.php: Statistik-Widgets, letzte eigene PMs, Firmenübersicht
    • press-releases/index.blade.php: gefilterte Liste eigener PMs, inline „Zur Prüfung einreichen"
    • press-releases/create.blade.php: Firma aus eigenem Bestand (kein Combobox-Vollzugriff), Sicherheitscheck
    • press-releases/edit.blade.php: nur für draft/rejected PMs erlaubt (403 sonst)
    • press-releases/show.blade.php: mit authorizeAccess() (nur eigene PMs), Prüfungs-Workflow
    • profile.blade.php: Konto-Info + zugeordnete Firmen
  • Sidebar: Customer-Only-Navigation für User ohne Admin-Rechte

Verifikation

  • php artisan auth:send-go-live-mails --dry-run zählt korrekt
  • php artisan magic-links:purge + press-releases:purge-drafts --dry-run
  • php artisan test --compact → 33 relevante Tests grün

Nächster Schritt

  • P0.3: .env mit Legacy-DB-Connections (blockiert P6)
  • P6: Datenmigration sobald Legacy-Credentials vorliegen
  • P4 erweitern: API-Token Self-Service, Rechnungen-Archiv-Tab

2026-04-27 P3 abgeschlossen: PressRelease-CRUD + Categories + Dashboard

Phase: P3 (Admin-UI an Models binden) Aufgaben: Restliche Dummy-Data-Views auf echte DB-Queries umgestellt + UX-Verbesserungen. Status:

Was wurde gemacht

Kategorien-Index (admin.categories.index)

  • Echte Eloquent-Queries mit withCount('pressReleases') und DE/EN-Translations
  • Suchfilter via whereHas('translations', ...)
  • Statistik-Widgets (Gesamt, Mit PMs, PMs gesamt) mit echten Counts

Pressemitteilungen komplett neu (alle 4 Views)

  • Index (admin.press-releases.index): Tabelle mit echten Daten, Filter nach Status/Portal/Sprache, Live-Schnellaktionen (Veröffentlichen, Ablehnen, Archivieren) direkt aus der Liste
  • Create (admin.press-releases.create): 2-Spalten-Layout (Content | Sidebar), Firma per Flux-Combobox (Live-Suche ab 1 Zeichen), Kategorie-Select, Slug-Generierung, „Als Entwurf" / „Zur Prüfung einreichen"
  • Edit (admin.press-releases.edit): lädt echte DB-Daten, vollständiger Status-Workflow (Entwurf → Prüfung → Veröffentlicht → Archiviert), Slug-Update nur bei Titel-/Portal-/Sprachänderung
  • Show (admin.press-releases.show): Read-only Detail mit Status-Badge, Prüfungs-Aktionsleiste (Veröffentlichen / Ablehnen direkt auf der Seite), Bilder-Liste

Status-Workflow:

  • Draft → Review (submitForReview) → Published (publish) / Rejected (reject)
  • Published → Archived (archive)
  • Review/Rejected → Draft (backToDraft)

Dashboard (resources/views/admin/dashboard.blade.php)

  • 5 Statistik-Kacheln (PMs, Firmen, Kontakte, Benutzer, Newsletter) alle klickbar, echte DB-Counts
  • Letzte 8 PMs mit Status-Badge
  • Prüfwarteschlange (PMs im Status review) mit Direktlinks

UX-Verbesserungen (Contacts + Users)

  • contacts/create.blade.php + contacts/edit.blade.php: Firma-Auswahl auf Flux-Combobox mit Live-Backend-Suche umgestellt (kein All-Load mehr)
  • users/edit.blade.php: Firmen- und Kontakt-Lookup auf Flux-Combobox umgestellt, Auswahl triggert sofortige Zuordnung via updatedSelectedLookup*-Hooks (kein separater „Zuordnen"-Button)
  • Suche startet ab 1 Zeichen, max. 50 Treffer, withoutGlobalScopes() für portalübergreifenden Admin-Zugriff

Verifikation

  • php artisan test --compact → 33 relevante Tests grün
  • Alle Views auf echte Eloquent-Models umgestellt

Nächster Schritt

  • P2.7: GoLivePasswordReset-Mailable + Command auth:send-go-live-mails
  • P0.3: .env mit Legacy-DB-Connections konfigurieren (Voraussetzung für P6)
  • Phase 6 (Datenmigration) kritischer Pfad zum Go-Live

2026-04-27 Qualitätsreview + kritische Fixes (Sicherheit, Architektur, Testqualität)

Phase: Querschnitt P2/P3 (Qualitätssicherung + Sicherheit) Aufgaben: Umsetzung aller kritischen und wichtigen Punkte aus 09-REVIEW-QUALITAET.md. Status:

Was wurde gemacht

🔴 Kritische Sicherheitslücken behoben

1. Admin-Middleware EnsureUserIsAdmin

  • Neu: app/Http/Middleware/EnsureUserIsAdmin.php
  • Prüft $request->user()?->canAccessAdmin(), sonst HTTP 403
  • Angewendet auf alle Admin-Routen in routes/admin.php
  • Settings-Routen (settings/profile, settings/password, settings/appearance) aus der Admin-Gruppe herausgelöst diese brauchen nur auth, kein Admin-Recht

2. MagicLinkConsumeController Eager-Load + Null-Check

  • ->with('user') ergänzt: User wird in einer statt drei DB-Abfragen geladen
  • Null-Check für $magicLink->user hinzugefügt: verhindert TypeError bei soft-deleted Usern
  • Datei: app/Http/Controllers/Auth/MagicLinkConsumeController.php

3. UserRolePermissionSyncService Anti-Pattern entfernt

  • Direkte Permission-Zuweisung (syncDirectPermissionsFromRoles) komplett entfernt
  • Rollen-Checks laufen ausschließlich über Spatie-Rollen (kein Doppel-Speichern)
  • Methode jetzt: assignRoleAndSyncPermissions() = $user->syncRoles([$role])
  • DatabaseSeeder unverändert funktionsfähig (Service-Interface bleibt gleich)

🏗️ P2.5 Portal-Scoping (Mandantentrennung)

Neue Dateien:

  • app/Services/CurrentPortalContext.php statischer Singleton für aktives Portal (get/set/clear)
  • app/Scopes/PortalScope.php Global Scope: filtert nach portal = context OR portal = 'both'; kein Filter wenn context = null (CLI, Tests, Import-Commands)
  • app/Http/Middleware/SetCurrentPortal.php setzt Portal-Kontext aus Session-Override (Admin-Wahl) oder Domain-Theme; registered in bootstrap/app.php
  • resources/views/livewire/admin/portal-switcher.blade.php Volt-Komponente für Sidebar-Portal-Filter (setzt admin_portal_filter Session-Key)

PortalScope angewendet auf:

  • App\Models\Company
  • App\Models\Contact
  • App\Models\PressRelease
  • App\Models\NewsletterSubscription

Portal-Switcher in resources/views/components/layouts/app/sidebar.blade.php eingebaut (nur für Admin-User sichtbar). Ermöglicht Filterung nach Presseecho / Businessportal24 / Alle.

🐛 Bugfix: Billing-Address-Felder leerbar machen

Datei: resources/views/livewire/admin/users/edit.blade.php (Methode save())

Vorher: collect($billingPayload)->filter(fn ($v) => filled($v)) leere Werte wurden silently ignoriert; Felder konnten einmal gesetzt nicht mehr geleert werden.

Nachher: Payload direkt ohne Merge übergeben. Pflichtfelder (name, address1, postal_code, city, country_code) werden auf filled() geprüft, optional Felder können geleert werden.

🗄️ Migrations-Konsolidierung: user_filter_presets

Drei separate Migrations vom 2026-04-24 (120000 / 121500 / 123000) zu einer konsolidierten Migration zusammengeführt:

  • 2026_04_24_120000_create_user_filter_presets_table.php (ersetzt alle drei)
  • Enthält: id, user_id FK, page, name, is_default, last_used_at, filters JSON, timestamps
  • Alle vier Indizes in einer einzigen Schema::create()-Migration

✉️ Session-Flash in Livewire-Inline-Actions ersetzt

session()->flash() in Inline-Actions (ohne Redirect) durch $this->notification Property ersetzt:

  • resources/views/livewire/admin/users/edit.blade.php 4 Inline-Actions (Firmen/Kontakt zuordnen/entfernen)
  • resources/views/livewire/admin/contacts/index.blade.php 5 Inline-Actions (Preset CRUD, Contact-Delete)

Template zeigt jetzt eine Alpine.js-gesteuerte Auto-Dismiss-Meldung (3 Sekunden) für Erfolg und Fehler.

🏭 Fehlende Factories ergänzt

Neue Factories:

  • database/factories/CompanyFactory.php mit States presseecho(), businessportal24(), inactive()
  • database/factories/ContactFactory.php
  • database/factories/CategoryFactory.php mit withTranslations() State
  • database/factories/PressReleaseFactory.php mit States published(), inReview(), forPortal()

HasFactory-Trait + typisierte @use-PHPDoc ergänzt auf:

  • App\Models\Company, App\Models\Contact, App\Models\Category, App\Models\PressRelease

UserFactory um die neuen Pflichtfelder erweitert (portal, registration_type, language, is_active, is_super_admin) behebt Bug, bei dem is_active nach User::factory()->create() als null im Model-Attribut stand (DB-Defaults werden nicht automatisch in Attribute übernommen).

🧪 Tests angepasst

  • tests/Pest.php: beforeEach() räumt CurrentPortalContext::clear() auf (verhindert Static-State-Leakage zwischen Tests)
  • tests/Feature/Admin/UserManagementTest.php: HTTP-GET-Test für User-Detailseite auf LivewireVolt::test() umgestellt (korrekte Art für Volt-Komponenten; verhindert Vite-Manifest-Fehler in Testumgebung)

Verifikation

  • php artisan migrate:fresh --seed (alle Migrations grün, Seeder grün)
  • php artisan test --compact tests/Feature/Admin/ (19 passed)
  • php artisan test --compact tests/Feature/Billing/ tests/Feature/Categories/ tests/Feature/Auth/UserAccessTest.php tests/Feature/Auth/MagicLinkLoginTest.php
  • Gesamttestlauf: 47 passed, 13 failed (alle 13 Fehlschläge sind pre-existing Domain-Routing-Probleme in der Test-Umgebung, unverändert seit P1)

Nächster Schritt

  • P3 fortsetzen: PressRelease-CRUD auf echte Daten umstellen (derzeit Dummy-Daten)
  • Categories-CRUD auf echte Daten umstellen
  • Dashboard-Widgets mit echten Queries
  • Phase 0.3: .env mit Legacy-DB-Connections konfigurieren (Voraussetzung für P6)

2026-04-24 P3 Bugfix: Persistente Kontakt-Verknuepfung im User-Edit

Phase: P3 (Admin-UI an Models binden) Aufgaben: Sofort-Aktionen im User-Edit (Zuordnen/Entfernen) auch fuer Kontakte reload-stabil machen. Status:

Was wurde gemacht

  • Neue Pivot-Tabelle contact_user eingefuehrt:
    • Migration 2026_04_24_183000_create_contact_user_table.php
    • Many-to-many zwischen users und contacts
  • Modelle erweitert:
    • App\Models\User::contacts()
    • App\Models\Contact::users()
  • resources/views/livewire/admin/users/edit.blade.php angepasst:
    • direkte Persistenz der Links ueber persistUserLinks() fuer Firmen und Kontakte
    • addLinkedContact() / removeLinkedContact() schreiben jetzt sofort in contact_user
    • beim Entfernen einer Firma werden zugehoerige Kontakt-Links ebenfalls bereinigt
    • Initialbefuellung bleibt kompatibel (Fallback auf max. 40 Firmenkontakte, falls noch keine expliziten Kontaktlinks existieren)

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert/angepasst:
    • Kontakt-Lookup testet jetzt auch Persistenz in user->contacts()
    • neues Szenario removing linked contact in user edit is persisted immediately
  • Testlauf:
    • php artisan test --compact tests/Feature/Admin/UserManagementTest.php (19 passed)

Nächster Schritt

  • Optional: In der UI die Anzahl explizit verknuepfter Kontakte anzeigen (Badge), getrennt von allen Firmenkontakten.

2026-04-24 P3 Bugfix: User-Edit Verknuepfungen direkt persistieren

Phase: P3 (Admin-UI an Models binden) Aufgaben: Firmen-/Kontakt-Zuordnung im User-Edit sofort in der DB speichern (ohne extra Save). Status:

Was wurde gemacht

  • resources/views/livewire/admin/users/edit.blade.php angepasst:
    • neue interne Persistenz persistCompanyLinks() synchronisiert user_companies sofort
    • addLinkedCompany() speichert Firmenverknuepfung direkt
    • removeLinkedCompany() entfernt Firmenverknuepfung direkt
    • addLinkedContact() speichert bei Bedarf die zugehoerige Firmenverknuepfung direkt
    • direkte UI-Feedbacks per Flash (direkt verknuepft / direkt entfernt)
  • damit ist kein zusaetzlicher Klick auf Speichern mehr noetig, um Zuordnen/Entfernen zu persistieren.

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert/angepasst:
    • removing linked company in user edit is persisted immediately
    • contact lookup in user edit finds results from unlinked companies and auto-links company (ohne Save)
    • adding linked company in user edit is persisted immediately
  • Testlauf:
    • php artisan test --compact tests/Feature/Admin/UserManagementTest.php (18 passed)

Nächster Schritt

  • Optional: Rollenwechsel pro Firmenkarte ebenfalls sofort persistieren (on-change), damit auch diese Aenderung ohne finalen Save direkt wirksam ist.

2026-04-24 P3 Bugfix: User-Edit Verknuepfung Entfernen + Suche

Phase: P3 (Admin-UI an Models binden) Aufgaben: Fehlerbehebung fuer User-Edit bei Entfernen von Verknuepfungen und Live-Suche. Status:

Was wurde gemacht

  • resources/views/livewire/admin/users/edit.blade.php korrigiert:
    • Kontakt-Lookup ist nicht mehr auf bereits verknuepfte Firmen eingeschraenkt
    • beim Hinzufuegen eines Kontakts wird dessen Firma bei Bedarf automatisch als Firmenverknuepfung aufgenommen
    • Action-Buttons (Zuordnen/Entfernen) explizit als type=\"button\", damit kein unbeabsichtigter Form-Submit ausgeloest wird
    • Suche bleibt performant: serverseitig, ab 2 Zeichen, max. 40 Treffer

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • removing linked company in user edit is persisted after save
    • contact lookup in user edit finds results from unlinked companies and auto-links company
  • Testlauf:
    • php artisan test --compact tests/Feature/Admin/UserManagementTest.php (17 passed)

Nächster Schritt

  • Optional: fuer bessere UX eine kleine Inline-Info anzeigen, wenn eine Kontakt-Zuordnung automatisch auch die Firma verknuepft hat.

2026-04-24 P3: User-Edit mit Live-Zuordnung fuer Firmen und Kontakte

Phase: P3 (Admin-UI an Models binden) Aufgaben: Firmen- und Kontaktverknuepfung im User-Edit auf performante Live-Suche/Select-Logik umstellen. Status:

Was wurde gemacht

  • resources/views/livewire/admin/users/edit.blade.php erweitert:
    • neue Lookup-States fuer Firmen und Kontakte:
      • companyLookup, selectedLookupCompanyId
      • contactLookup, selectedLookupContactId
    • neue Actions:
      • addLinkedCompany(), removeLinkedCompany()
      • addLinkedContact(), removeLinkedContact()
    • with() liefert jetzt:
      • linkedCompanies (nur aktuell verknuepfte Firmen)
      • companyLookupResults (serverseitig gefiltert, max. 40 Treffer, ab 2 Zeichen)
      • contactLookupResults (serverseitig gefiltert, max. 40 Treffer, ab 2 Zeichen)
    • UI im User-Edit angepasst:
      • Firmenverknuepfung ueber Live-Suche + Select + Zuordnen
      • verknuepfte Firmen als Liste mit Rollenwahl + Entfernen
      • Kontaktverknuepfung ueber Live-Suche + Select + Zuordnen
      • verknuepfte Kontakte als editierbare Liste mit Entfernen
  • syncContactFormsToSelectedCompanies() angepasst:
    • laedt initial nicht mehr unlimitiert, sondern max. 40 Kontakte (Performance)
    • haelt bestehend ausgewaehlte Kontakte konsistent, wenn Firmenzuordnung geaendert wird.

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • neues Szenario admin can link companies and contacts in user edit via live lookup flow
    • prueft Live-Zuordnung von Firmen/Kontakt und korrektes Speichern inkl. Pivot-Rolle.
  • Testlauf:
    • php artisan test --compact tests/Feature/Admin/UserManagementTest.php (15 passed)

Nächster Schritt

  • Optional: Asynchrone Suchresultate visuell nach Relevanz sortieren (Name-StartsWith vor Contains) fuer noch schnellere Zuordnung.

2026-04-24 P3: Company Contacts Tab mit Live-Select fuer bestehende Kontakte

Phase: P3 (Admin-UI an Models binden) Aufgaben: In der Firmen-Detailansicht unter Kontakte bestehende Kontakte per Live-Suche finden und direkt der Firma zuordnen (performant, limitiert). Status:

Was wurde gemacht

  • resources/views/livewire/admin/companies/show.blade.php erweitert:
    • neue State-Properties:
      • contactLookup (Suchbegriff)
      • selectedExistingContactId (ausgewaehlter Kontakt)
    • neue Action attachExistingContact():
      • validiert Auswahl
      • verschiebt Kontakt per company_id auf die aktuelle Firma
      • behandelt Fehlerfaelle robust (Firma/Kontakt fehlt, bereits zugeordnet)
    • Suchlogik in with():
      • serverseitiger Filter auf first_name, last_name, email
      • Suche erst ab 2 Zeichen
      • Ausschluss bereits zugeordneter Kontakte (company_id != aktuelle Firma)
      • Ergebnislimit 40 zur Performance-Sicherung
    • UI im Kontakte-Tab:
      • Suchfeld mit Live-Debounce
      • Select mit dynamischen Treffern
      • Button Zuordnen
      • Hinweistext zu Suchschwelle und Ergebnislimit

Verifikation

  • tests/Feature/Admin/UserManagementTest.php um neues Szenario erweitert:
    • admin can live-search and assign existing contacts in company detail with result limit
    • prueft:
      • Trefferliste wird bei vielen Treffern auf 40 limitiert
      • ausgewaehlter Kontakt wird nach attachExistingContact() der Ziel-Firma zugeordnet.

Nächster Schritt

  • Optional: Async-Select UX weiter verfeinern (z. B. Treffergruppierung nach Firma oder Tastatur-Navigation).

2026-04-24 P3: Quick-Delete fuer Kontakte im Index

Phase: P3 (Admin-UI an Models binden) Aufgaben: Kontakte direkt in der Kontakte-Liste loeschbar machen, inkl. Warn-Modal und Soft-Delete. Status:

Was wurde gemacht

  • resources/views/livewire/admin/contacts/index.blade.php erweitert:
    • neue Action deleteContactFromIndex(int $contactId)
    • Soft Delete via Contact::delete() und Flash-Feedback
    • Fehlerfall fuer nicht existente Kontakt-ID
  • Pro Tabellenzeile neue Quick-Delete-UI:
    • neues Trash-Icon in den Aktionen
    • zeilenbezogenes Flux-Modal mit Warntext und expliziter Bestaetigung
    • Bestaetigungsbutton ruft deleteContactFromIndex(...) auf

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • neues Szenario admin can soft delete contact directly from contacts index flow
    • prueft, dass der Kontakt in der Standard-Query verschwindet und in withTrashed() als geloescht markiert bleibt.

Nächster Schritt

  • Optional: Modal um Kontextdetails erweitern (Kontaktname/Firma), damit die Bestaetigung noch eindeutiger wird.

2026-04-24 P3: Contact Delete-Flow mit Warn-Modal

Phase: P3 (Admin-UI an Models binden) Aufgaben: Kontakte in der Admin-UI loeschbar machen und vor Loeschung per Modal explizit bestaetigen lassen. Status:

Was wurde gemacht

  • resources/views/livewire/admin/contacts/edit.blade.php erweitert:
    • neue Action deleteContact() im Volt-Component
    • Soft Delete via Contact::delete() inkl. Redirect + Flash-Meldung
    • robuste Behandlung fuer nicht existente Kontakt-ID
  • UI im Bereich "Aktionen" angepasst:
    • Delete-Button oeffnet nun ein Flux-Modal statt direkter Loeschung
    • Modal mit klarer Warnung und expliziter Bestaetigung (Loeschung bestaetigen)
    • Abbrechen schliesst das Modal ohne Aktion

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • bestehendes Kontakt-CRUD-Szenario prueft nun zusaetzlich den Delete-Flow
    • Assertions: Redirect auf admin.contacts.index, kein Treffer in Standard-Query, Datensatz in withTrashed() als geloescht vorhanden.

Nächster Schritt

  • Optional: Delete-Aktion auch als Quick-Action im Contacts-Index mit row-spezifischem Modal ergaenzen.

2026-04-24 P3: Company Delete-Flow mit Warn-Modal

Phase: P3 (Admin-UI an Models binden) Aufgaben: Loeschprozess fuer Firmen aktivieren und per bestaetigungspflichtigem Modal absichern. Status:

Was wurde gemacht

  • resources/views/livewire/admin/companies/edit.blade.php erweitert:
    • neue Action deleteCompany() im Volt-Component
    • Soft Delete via Company::delete() inkl. Redirect + Flash-Meldung
    • robuste Behandlung fuer nicht existente Firmen-ID
  • UI im Bereich "Aktionen" angepasst:
    • Delete-Button oeffnet jetzt ein Flux-Modal statt direkt zu loeschen
    • Modal zeigt klare Warnung und verlangt explizite Bestaetigung
    • Buttons: Abbrechen und Loeschung bestaetigen

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • neues Szenario "admin can soft delete company from edit flow"
    • prueft Redirect und dass Datensatz in Standard-Query verschwindet, aber in withTrashed() als geloescht vorhanden ist.

Nächster Schritt

  • Optional: Beziehungen beim Loeschen sichtbar auflisten (z. B. Anzahl Kontakte/Benutzer im Modal), damit Admins den Impact vor Bestaetigung sehen.

2026-04-24 P3: Companies-CRUD von Dummy auf DB umgestellt

Phase: P3 (Admin-UI an Models binden) Aufgaben: Firmenliste, Firmen-Erstellung und Firmen-Bearbeitung mit echten Eloquent-Daten anbinden. Status:

Was wurde gemacht

  • resources/views/livewire/admin/companies/index.blade.php umgestellt:
    • entfernt Dummy-Collection, ersetzt durch Company-Query mit withCount(['pressReleases', 'contacts'])
    • Suche + Aktiv/Inaktiv-Filter auf DB-Ebene
    • Pagination (paginate(15)) und Live-Statistiken (total, active, inactive)
    • Tabellenzugriff auf echte Model-Attribute statt Array-Daten
  • resources/views/livewire/admin/companies/create.blade.php umgestellt:
    • save() erzeugt jetzt echte Company-Datensaetze
    • Mapping auf vorhandene Spalten (portal, type, name, slug, address, country_code, phone, email, website, logo_path, is_active)
    • eindeutige Slug-Generierung pro Portal
    • Flux-Form um Portal- und Typ-Auswahl ergaenzt
  • resources/views/livewire/admin/companies/edit.blade.php umgestellt:
    • mount() laedt reale Firmendaten aus der DB und behandelt fehlende IDs robust per Redirect
    • update() persistiert Aenderungen in der DB inkl. neuer Slug-Logik
    • bestehendes Logo wird geladen/angezeigt, neues Logo wird bei Upload gespeichert

Verifikation

  • tests/Feature/Admin/UserManagementTest.php erweitert:
    • neues Szenario fuer Company Create + Edit (Persistenz inkl. portal, type, slug, is_active)
  • Lokaler Testlauf:
    • php artisan test --compact tests/Feature/Admin/UserManagementTest.php (11 passed)

Nächster Schritt

  • Optional: Delete-Flow fuer Companies (Soft-Delete + UI-Aktion + Tests) aktivieren.

Template

## YYYY-MM-DD  <Titel>

**Phase:** <P1/…>
**Aufgaben:** <Kurzer Stichpunkt>
**Status:** ⬜ 🔄 ✅ ⏸️ ❌

### Was wurde gemacht
-### Probleme
-### Entscheidungen
-### Nächster Schritt
-

2026-04-23 Migrationsplan aufgesetzt

Phase: 0 Aufgaben: Initiale Analyse + Dokumentationsordner dev/migration 2026/ angelegt Status:

Was wurde gemacht

  • Legacy-Projekt /_businessportal24.com/ systematisch untersucht (Struktur, Module, Datenmodell, Routen, Plugins)
  • Aktuellen Laravel-Stack gesichtet (PHP 8.4, Laravel 12, Livewire 4, Volt, Flux 2, Fortify, Sanctum, Spatie Permission)
  • Vorarbeiten gefunden & berücksichtigt:
    • Admin-UI-Gerüst in resources/views/admin/* + resources/views/livewire/admin/*
    • 40 deklarierte Admin-Routes in routes/admin.php
    • Domain-basiertes Theme-System in config/domains.php
    • Früheres Core-Package-Experiment unter _businessportal24.com/dev/
    • Altbestand Migrations + Models in dev/_old/
  • Acht Migrationsdokumente angelegt (README + 0008)

Entscheidungen (siehe README.md §D-01 … D-08)

  • D-01 Neues DB-Schema (nicht 1:1 aus Doctrine)
  • D-02 portal-Spalte für Mandanten-Disambiguierung
  • D-03 Payment ausschließlich Stripe
  • D-04 Country/Salutation als Config-Dateien
  • D-05 API-Keys per Sanctum + Kompatibilitäts-Middleware
  • D-06 Admin = Livewire Volt + Flux UI
  • D-07 DE als primäre UI-Sprache
  • D-08 Altes Symfony-Frontend wird nicht übernommen

Offene Fragen (siehe 00-OVERVIEW.md §5)

  • Q-01 DB-Dumps beider Portale vorhanden?
  • Q-02 Legacy-Passwörter übernehmen?
  • Q-03 API-Keys behalten?
  • Q-04 Rechnungs-PDFs archivieren?
  • Q-05 Media-Storage (S3 / lokal)?
  • Q-06 Welche neuen Pflichtfelder?
  • Q-07 Mehrsprachigkeit-Umfang?
  • Q-08 PromotionLinks / FooterCode im Scope?
  • Q-09 Coupons via Stripe oder intern?
  • Q-10 Company/Agency trennen oder fusionieren?

Nächster Schritt

  • Auftraggeber-Review der Doku + Klärung der offenen Fragen
  • Scope freigeben (siehe 06-FEATURES-SCOPE.md §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 sfGuardGroupadmin/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.dashboarddashboard
    • admin.settings.profilesettings.profile
    • admin.settings.appearancesettings.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.

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