143 KiB
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_tableergänztlegacy_invoices.pdf_payloadundpdf_generated_at. legacy:archive-invoicesimportiert jetzt idempotent alle Legacy-Rechnungen mit Originalstatus, Beträgen, Datumsfeldern, Zahlart, User-Zuordnung überlegacy_import_map,raw_snapshotundpdf_payload.pdf_payloadenthält Snapshot-Daten ausinvoice,invoice_billing_addressunduser_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-*.jsonmit Source-Count, Statusverteilung, Summe, Fehlern und unzugeordneten Legacy-Usern. - Customer-Rechnungen erzeugen PDFs bei Abruf on demand über
App\Services\Billing\LegacyInvoicePdfRenderer; vorhandenepdf_path-Dateien bleiben als Cache/Export weiter nutzbar. PDFs werden überLegacyInvoicePdfControllerinline 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 mitrequired|string|min:5|max:2000-Validation.- Reject-Modal um eine
flux:textareamit 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::editUrlundApp\Mail\PressReleasePublished::showUrlwä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_tablemitpress_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\PressReleaseStatusLogmit Casts aufPressReleaseStatus-Enum, BelongsTo zuPressReleaseundUser(changedBy). PressRelease::statusLogs()als HasMany, sortiertdescnachcreated_at.PressReleaseServiceschreibt bei jedem Statuswechsel einen Log-Eintrag (submitForReview,publish,reject,backToDraft,archive,changeStatusFromAdmin) mit korrekter Quelle (admin/customer/blacklist) und User-ID ausAuth::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ürdraft– 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/blacklistals Mini-Badge sichtbar,adminals Default ausgeblendet).admin.press-releases.index: Quick-Actionspublish/reject/archivegehen jetzt konsequent durchPressReleaseService– keine direktenupdate()-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-KonstantePressReleaseReviewCount.- 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=customerundchanged_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/.../editfü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.
- Submit-for-Review schreibt Audit-Log mit
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-Prefixme.*. - 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–EnsureUserIsCustomerlä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 (imFortifyServiceProviderals 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) undVerifyEmailController(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 demcustomer.*-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.dashboardbenötigt Login- Customer kann
me.dashboardaufrufen, aber nichtdashboard(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 umgebaut: Admin-Magic-Link →
Test-Suite
- 170 Tests grün, 3 geskippt, 0 Fehler (
php artisan test --compact). vendor/bin/pint --dirty --format agent→ fixed (kosmetik inVerifyEmailController), 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:
- Köpfe (Categories Create/Edit, Footer-Codes-CRUD) abschließen ← erledigt.
- Panel-Konsolidierung: gemeinsamer Prefix, rollenbasierte Sichtbarkeit, Customer-Routen ins Admin-Panel ziehen ← erledigt 2026-05-04.
- Pressemitteilungs-Veröffentlichungsblock (User-Login + Customer-PR + Admin-Freigabe/Bearbeitung) auf der konsolidierten Architektur.
2026-05-04 – Tranche 2 (Köpfe): Categories Create/Edit + Footer-Codes-CRUD ✅
Phase: P3 (Admin-UI) Status: ✅ umgesetzt
Was wurde gemacht
Categories Create/Edit (P3.3):
CategoryTranslation::uniqueSlug(string $source, string $locale, ?int $ignoreCategoryId = null)als zentraler Slug-Helper pro Locale.- Volt-Components
admin.categories.createundadmin.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.
- DE+EN-Translations mit Live-Slug-Preview (
- Index erweitert:
Kategorie anlegen-Button undBearbeiten-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_tablelegtfooter_codes(portal,language,title,content,is_global,is_active,priority, Soft-Deletes, Legacy-Tracking) + Pivotcategory_footer_codean. - Model
FooterCodemitbelongsToMany(Category::class), Casts (Portal, Booleans). - Factory
FooterCodeFactorymitglobal()/inactive()States. - Volt
admin.footer-codes.indexmit 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 unterKategorien. - 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 umfooter_codes+category_footer_code. P3.3 und P3.11 auf ✅ gesetzt.dev/migration 2026/08-PROGRESS.md: dieser Eintrag.
2026-05-04 – Scope-Entscheidung: Newsletter vertagt
Der Auftraggeber hat entschieden, dass NewsletterService (P5.3) und der Admin-Newsletter-Workflow (P3.7) vorerst zurückgestellt werden. Beide werden nach dem Pressemitteilungs-Veröffentlichungs-Block (Login + Customer-PR + Admin-Freigabe) wiederaufgenommen. Die heutige Newsletter-Sync-UI für Legacy-Listen bleibt unverändert.
Status in 03-MIGRATION-PLAN.md ist auf ⏸️ Vertagt 2026-05-04 gesetzt.
2026-05-04 – Tranche 1: PR-Bilder + Blacklist + Slug-Trait + Public-Share-Link ✅
Phase: P3 (Admin-UI) + P4 (Customer) + P5 (Domain-Services) + P6.8 (Legacy-Bilder-Sync) Status: ✅ umgesetzt
Was wurde gemacht
Schritt 1 – PressRelease-Bilder durchgängig auf ImageService gestellt + Legacy-Sync (P3.2 / P3.4 / P5.5 / P6.8)
App\Services\Image\ImageServiceerweitert um:storePressReleaseImage(UploadedFile, pressReleaseId)mit drei Variantenthumb(320×240),medium(800×600),large(1600×1200) per Cover-Resize (zentriert beschnitten, statt Letterbox).- Generischer
generateVariants(...$cover = false)-Pfad — Logos nutzen weiterhincontained, PR-Bildercover. deletePressReleaseImage()undgenerateMissingPressReleaseVariants()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\PressReleaseImagemiturl()+variantUrl(key)-Helpers, fallback-fest auch für nicht-public Disks.App\Http\Controllers\Api\V1\PressReleaseImageControllernutzt jetzt denImageService(stattStorage::disk('public')->put+getimagesizedirekt). API liefertdata.variantsunddata.urls.{thumb,medium,large,original}.App\Http\Resources\PressReleaseImageResourcezeigt Varianten-URLs öffentlich;data.urlbleibt rückwärtskompatibel.StorePressReleaseImageRequest::rulesvonmax:5120aufmax:8192erhöht (synchron mit ImageService).- Admin-Logo-Upload (
livewire/admin/companies/edit) aufImageService::storeCompanyLogo()umgestellt – inkl. Variant-Generierung, Logo-Entfernen-Button und „Logo entfernen rückgängig"-Flow. - Wiederverwendbare Volt-Komponente
livewire/components/press-release-images-managerfü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 überPressReleasePolicy::update; Customer kann nur Draft/Rejected ändern, Admin alles außer Archived. - Neuer Command
legacy:sync-press-release-images(App\Console\Commands\SyncPressReleaseImages) analog zulegacy: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/nachstorage/app/public/press-releases/{prId}/images/{filename}, generiert perImageServicedie Varianten und aktualisiertpress_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\PressReleaseImportersetzt jetztlegacy_portal/legacy_idauch aufpress_release_images, damit der Sync-Command stabil matchen kann.- Tests: erweiterter
tests/Feature/Api/V1/PressReleaseImageApiTest(Variantenprüfung, Cleanup beim Delete) + neuertests/Feature/SyncPressReleaseImagesTestmit 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.phpals 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()undpublish()rufen jetztBlacklistService::findInPressRelease()auf:- Treffer → PM wird automatisch auf
rejectedgesetzt, Autor bekommtPressReleaseRejected-Mail mit Begründung („unzulässiges Wort … gefunden"), und es wird eineBlacklistViolationExceptiongeworfen.
- Treffer → PM wird automatisch auf
- Neue
App\Services\PressRelease\BlacklistViolationExceptionmit 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 mitsession()->flash('error', …)auf die Exception. - Tests:
tests/Feature/PressReleaseBlacklistTestmit 4 Cases (BlacklistService direkt, Submit mit verbotenem Wort, sauberer Publish, Phrase erkannt).
Schritt 3 – HasUniqueSlug-Trait (P5.6)
- Neuer Trait
App\Models\Concerns\HasUniqueSlugmitgenerateUniqueSlug(string $source, array $scope = []):- Models definieren
slugScopeAttributes()undslugFallback(). - 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.
- Models definieren
App\Models\PressRelease: scope['portal', 'language'], fallbackpressemitteilung.App\Models\Company: scope['portal'], fallbackcompany.- 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. Categorybleibt aussen vor (Slugs liegen in Translations-Tabelle, separater Mechanismus).CompanyImporterbehält seinen eigenen Slug-Pfad (legacy-Slug bevorzugt).- Tests:
tests/Feature/HasUniqueSlugTestmit 4 Cases (Scope-Einhaltung, Edit hält eigenen Slug, Cross-Portal-Fallthrough, Fallback bei leerem Source).
Schritt 4 – Public Share-Link für PMs via Magic-Link
App\Services\Auth\MagicLinkGenerator::createPressReleaseShareLink(PressRelease, ?User, int $ttlDays = 7):- Token + SHA-256-Hash, gleiche Sicherheitscharakteristik wie der Login-Link.
MagicLink.purpose = 'press_release_access',payload = {press_release_id}.- Default 7 Tage gültig, kein Login-Effekt.
- Neue Public-Route
GET /pm-vorschau/{token}(regex 40+ chars) →App\Http\Controllers\PressReleasePreviewControllerrendert read-only Blade-Viewpress-release-preview.- Bei ungültigem/abgelaufenem Token wird
press-release-preview-errormit404bzw.410 Goneausgeliefert. - Funktioniert auch für
draft/review/rejected/archived-PMs, jedoch ohne Authentifizierung — das ist der Sinn („den Kollegen den Entwurf zeigen").
- Bei ungültigem/abgelaufenem Token wird
- 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/PressReleasePreviewTestmit 5 Cases (gültig + Inhalt sichtbar, kein Auth nötig, abgelaufen → 410, ungültiger Token → 404, force-deleted PM → 404).
Cleanup
- Neue
pint.jsonschließt die Symfony-1.4-Legacy-Ordner (_businessportal24.com,_presseecho.com,dev/_old,dev/migration 2026/_archiv) aus, damit Pint keine PHP-5.6-Parse-Errors mehr meldet. Pint läuft jetzt grün durch (vendor/bin/pint --dirty --format agent→result: passed).
Test-Lage
- Komplette Suite: 141 passed, 3 skipped, 0 failed (vorher: 124 / 3). Neu hinzugekommen: 17 Tests verteilt über die 4 Schritte. Das Reporting des Slow-Admin-Logs und API-Usage bleibt unberührt.
- Pint sauber, Linter ohne neue Findings.
Aufgeräumte Plan-Punkte (siehe 03-MIGRATION-PLAN.md)
- ✅ P3.2 (PM-Bilder im Customer-Portal pflegen)
- ✅ P3.4 (Admin-Firmen-Logo via ImageService)
- ✅ P5.2 (BlacklistService)
- ✅ P5.5 (ImageService inkl. PR-Varianten)
- ✅ P5.6 (Slug-Trait)
- ✅ P6.8 (Legacy-PM-Bilder-Sync-Command)
- ✅ Public-Vorschau-Link (
Feature aus 06-FEATURES-SCOPE.md – Tabelle 4)
Offen (ohne externen Input)
- P5.3 NewsletterService neu (subscribe/confirm/unsubscribe + Admin-CSV-Export).
- P3.3 Admin Categories Create/Edit (Index existiert).
- P3.7 Admin-Newsletter-Workflow (heute nur Sync-UI).
- P3.11 Admin-CRUD für Footer-Codes.
- P5.7 Domain-Events (
PressReleasePublished/Rejected/SubmittedForReview). - Failure-Alerting für Queue-Worker (P9.3) + Supervisor-Setup-Doku.
Wartet auf externen Input
- P0.5–0.7 Stripe / Produktliste / Grandfathering – blockiert die gesamte Phase 8.
- P11.2 Staging-Server, P11.4 Mailtexte für Go-Live.
2026-05-04 – Test-Suite stabilisiert + Customer-Portal abgeschlossen ✅
Phase: Querschnitt (Stabilisierung) + P4 (Customer-Portal) + P5.5 (ImageService) Status: ✅ umgesetzt
Was wurde gemacht
Paket A – Test-Suite stabilisiert
phpunit.xml:APP_URL=https://presseportale.testergänzt, damitroute('login')etc. nicht mehr auf eine fremde Domain ohne Auth-Routen zeigen.tests/Feature/Auth/EmailVerificationTest.php: Tests werden jetzt sauber permarkTestSkipped()übersprungen, solangeFeatures::emailVerification()inconfig/fortify.phpdeaktiviert ist. Sobald das Feature aktiviert wird, laufen die Tests automatisch wieder.tests/Feature/ExampleTest.php: testet jetzt den Health-Endpoint/upstatt 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.indexdurch ehrliche „In Vorbereitung / Vertagt"-Stubs ersetzt. Keine Fake-Daten mehr;admin.invoices.indexzeigt 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-Variantensq(256×256) undwide(640×320) auf dempublic-Disk untercompany-logos/{portal}/{companyId}/...- aspect-ratio-erhaltend mit transparentem Letterbox/Pillarbox
deleteCompanyLogo()räumt Original + Varianten zuverlässig aufMAX_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 = nullund löst, falls aktiviert, Fortify-Verifikationsmail aus) - Zwei-Faktor-Authentifizierung aktivieren/deaktivieren, QR-Code anzeigen, Recovery-Codes regenerieren
- Passwort ändern (mit
- Customer-Sidebar um „Profil" und „Sicherheit" erweitert.
routes/customer.php: neue Routecustomer.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)
PressReleasePolicykennt eigene AbilitysubmitForReviewund Admin-Abilitypublish(verknüpft mitpress-releases:publishPermission).
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.5–0.7 (Stripe-Test-Account, Produktliste, Grandfathering-Kriterien) vom Auftraggeber.
- Ohne externe Inputs gut machbar:
- PressRelease-Bilder-Upload im Customer-/Admin-Portal über
ImageServiceergänzen (mehrere Bilder, Alt-Texte, Sortierung) legacy:sync-press-release-imagesals Pendant zulegacy:sync-company-logos- Admin-Logo-Upload/-Edit auf
ImageServiceumstellen BlacklistService(P5.2) und neuerNewsletterService(P5.3)
- PressRelease-Bilder-Upload im Customer-/Admin-Portal über
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 PressemitteilungenOhne PressemitteilungenMit veröffentlichten PMsOhne veröffentlichte PMs
- Veröffentlichte PMs werden statusbasiert über
PressReleaseStatus::Publishedgefiltert. - 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:publishergänzt. - Seeder weist
press-releases:publishden Rollenadminundeditorzu. - 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 Aliaspublished_press_releases_countund Status-Constraint aufpublished. - 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
nullinitialisiert. - Beim Laden eines Profils werden fehlende Datumswerte explizit als
''gesetzt. - Beim Speichern werden leere Strings weiterhin als
nullinprofilespersistiert. - Die drei Datumsfelder nutzen
flux:date-picker type="input"mitclearable, weilflux: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.userszeigt 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
UserImportererweitert:- liest die relevanten Felder aus
sf_guard_user_profile - schreibt/aktualisiert die Daten in
profiles - mappt Legacy-Anreden (
salutation_id) aufmr/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
- liest die relevanten Felder aus
Profileum Factory-Unterstützung ergänztProfileFactoryergänztadmin.users.editerweitert:- 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
- lädt
- Tests ergänzt:
- Import-Test für
sf_guard_user_profile→profiles - Admin-User-Edit-Test für Laden/Speichern der Profilfelder
- Import-Test für
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:
PressReleaseServiceund 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 nachgezogenREADME.md– Schnellüberblick aktualisiert und Dokumente 09/10 ergänzt04-DATA-MODEL.md–contact_userdokumentiert07-API-MIGRATION.md– Legacy-Cutover einheitlich auf410 Gonekorrigiert09-REVIEW-QUALITAET.md– als historisches Review markiertMIGRATION-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
draftoderrejected - Upload nur JPEG/PNG/WebP bis maximal 5 MB
- Dateien werden auf dem
public-Disk unterpress-releases/{id}/imagesgespeichert PressReleaseResourceliefert 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 nachauth:sanctumund 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
ApiUsageReporterergä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 = activeim gültigen Zeitraum) - aktivem Bestandsschutz (
status = grandfathered,grandfathered_until >= heute) - Übergangsweise: letzte importierte Legacy-Rechnung ist
paid
- aktivem neuen Zahlungsstatus (
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 nachauth: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-ApiKeywird 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 = apiuseroder Rolleapi-only - automatisch freigabefähig: User aktiv + letzte Legacy-Rechnung
paid needs_review: aktiver API-Kandidat ohne archivierte Legacy-Rechnungblocked: 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-*.jsongeschrieben --no-reportfü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.ymlangelegt - Web-Route
GET /docs/api/v1ergänzt, damit derdocs_urlaus 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-releasesGET|PATCH|DELETE /api/v1/press-releases/{pressRelease}GET /api/v1/companiesGET /api/v1/companies/{company}GET /api/v1/categoriesPOST /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.phpwird jetzt inbootstrap/app.phpals 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_keyQuery-Parameter undX-Api-KeyHeader - antwortet vor Sanctum-Auth mit HTTP 410 Gone
- liefert
migration_urlzum Customer-Token-Bereich unddocs_url
API v1 Endpunkte
Neue /api/v1-Endpunkte:
GET /api/v1/press-releasesPOST /api/v1/press-releasesGET /api/v1/press-releases/{pressRelease}PATCH /api/v1/press-releases/{pressRelease}DELETE /api/v1/press-releases/{pressRelease}GET /api/v1/companiesGET /api/v1/companies/{company}GET /api/v1/categoriesPOST /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,CategoryResourceStorePressReleaseRequest,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:readpress-releases:writepress-release-images:writecompanies:readnewsletter: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_invoicesdes eingeloggten Users - Statistik-Kacheln: Anzahl, Archivsumme, bezahlte Rechnungen, PDFs
- Suche nach Rechnungsnummer + Statusfilter
- PDF-Download vorbereitet: Wenn
pdf_pathvorhanden und Datei existiert, wird über Storage ausgeliefert; sonst erscheint ein Hinweis
Navigation & Rollen
- Customer-Sidebar um „Rechnungen" und „API-Tokens" erweitert
routes/customer.phpumcustomer.invoices.indexundcustomer.tokens.indexergänztRolesAndPermissionsSeederumnewsletter:subscribeergä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_ataus 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 optionalenexcludeId-Parameter. legacy:fix-timestampsumpress-releasesEntitä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-Datum0000-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|allund--skip-legacyfü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.5–0.7: Stripe-Zugangsdaten + Produktliste vom Auftraggeber (blockiert P8)
- P7: API v1 (Endpoints, Sanctum Token-Abilities, 410 Gone Handler)
- P10: Security-Review + Test-Coverage
- P11: Staging-Deployment + Rehearsal-Lauf + Go-Live-Mailing
2026-04-27 – Import-Bugfixes + Timestamps + User-Kontakt-Verknüpfung
Phase: P6 (Datenmigration – Qualitätssicherung) Status: ✅
Was wurde gemacht
Import-Fehler behoben: phone/fax zu kurz (VARCHAR 40 → 255)
Beim echten Companies-Import (Presseecho) schlugen 20 Datensätze fehl, weil Legacy-Telefonnummern Beschreibungstext enthielten (bis 92 Zeichen, z.B. „0700 27862537 (0700 ARTMaker, zum Festnetztarif de)").
- Migration
2026_04_27_115212_expand_phone_fax_columns:phone+faxincompaniesundcontactsvon VARCHAR(40) auf VARCHAR(255) erweitert. CompanyImporter+ContactImporter: neuecleanText()-Methode dekodiert HTML-Entities ( etc.) und kürzt auf die Feldlänge als letzten Fallback.- Re-Import mit
--force: 41.290 Firmen aktualisiert, 20 neu importiert → 0 Fehler.
Timestamps aus Legacy-DBs übernehmen
Problem: Alle 119k importierten Datensätze hatten created_at = heute, da die Importers ursprünglich kein withoutTimestamps nutzten.
Fixes:
UserImporter:User::withoutTimestamps()+created_at/updated_ataussf_guard_userübernehmen.CompanyImporter(bereits vorhanden) +ContactImporter(bereits vorhanden): beide nutzen jetzt korrektwithoutTimestamps.- Neuer Command
legacy:fix-timestampsfü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. ✅
Neuer Import-Schritt: link-associations (User↔Kontakt direkt)
Hintergrund: Im Legacy-System gab es keine direkte User→Kontakt-Tabelle. Die Zugehörigkeit war indirekt: User → company_user → company → contact. Im neuen System gibt es contact_user, das jetzt befüllt wird.
- Neuer Service
UserAssociationLinker: ableitet User↔Kontakt-Links auscompany_user + contactsvia 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: 12–20). Flux-Pro sortable / sorted / direction Props mit wire:click="sort('column')" in alle Köpfe integriert.
| View | Sortierbare Spalten | Standard |
|---|---|---|
| Users | Vorname, E-Mail, Portal, Status, Letzter Login, Hinzugefügt | Hinzugefügt ↓ |
| Companies | Name, E-Mail, Status, PMs, Kontakte, Hinzugefügt | Name ↑ |
| Contacts | Name, E-Mail, Firma, Hinzugefügt | Nachname ↑ |
| PMs (Admin) | Titel, Status, Portal, Hits, Erstellt | Erstellt ↓ |
| Kategorien | Standard (ID), PMs | ID ↑ |
| PMs (Kunde) | Titel, Status, Erstellt | Erstellt ↓ |
Spalten-Fixes
Companies Index:
- „Website"-Spalte entfernt
- „Hinzugefügt" (created_at) hinzugefügt
Contacts Index:
- „Position"-Spalte entfernt
- „Hinzugefügt" (created_at) war bereits vorhanden und korrekt
Users Index:
- Bug behoben: Aktions-Icons erschienen in der „Erstellt"-Spalte (Datenzelle fehlte)
- Name aufgeteilt: „Vorname" + „Nachname" als separate Spalten (aus
namegesplittet, 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-FlagsImportResult– zählt importiert/übersprungen/aktualisiert/FehlerUserImporter– sf_guard_user + sf_guard_user_profile + Rollen (inkl. Gruppen-Mapping)CompanyImporter– company + company_user + responsible_company_user → Pivot mit RollenContactImporter– contact → contacts (Salutation-Mapping)CategoryImporter– category + category_translation → categories + translations (DE/EN)PressReleaseImporter– press_release + press_release_image + press_release_contact
Master-Command: app/Console/Commands/ImportLegacyData.php
- Befehl:
php artisan legacy:import - Optionen:
--source=presseecho|businessportal24|all,--step=categories|users|companies|contacts|press-releases|all,--dry-run,--force - Idempotent via
legacy_import_map(skip bereits importierter Datensätze) - Chunk-basiert (500 Records pro DB-Query) – speichereffizient
Status-Mapping Legacy → Neu:
| Legacy | Neu |
|---|---|
new / edited |
draft |
prepublished |
review |
published |
published |
rejected |
rejected |
Gruppen-Mapping Legacy → Rollen:
| Gruppe | Rolle |
|---|---|
| admin (1) | admin |
| editor (2) | editor |
| dataFetcher (3) | api-only |
| unlimitedPressReleasesPayment (4) | customer |
| keine Gruppe | customer |
Dry-Run-Ergebnisse (0 Fehler)
| Schritt | presseecho | businessportal24 | Zeit |
|---|---|---|---|
| categories | 14 | – (identisch) | <1s |
| users | 19.372 (292 skip) | ~47k | 2s |
| companies | 41.310 | 66.525 | 4–7s |
| contacts | 8.366 | 8.597 | <1s |
| press-releases | 80.033 | 100.305 | 31–45s |
| Gesamt | ~149k | ~223k | 37 / 58s |
Nächster Schritt
- Echten Import starten:
php artisan legacy:import --source=presseecho --step=categories --force(Kategorien zuerst) - Danach:
--step=users→--step=companies→--step=contacts→--step=press-releases - Ggf. Legacy-Rechnungen archivieren (
legacy:archive-invoices– noch nicht implementiert, P6.5)
2026-04-27 – P2.7 + P4 + P5.1 + P9.2: GoLive-Mailing, Customer-Portal, Services, Scheduler
Phase: P2.7 · P4 · P5.1 · P9.2 Status: ✅
Was wurde gemacht
P2.7 – GoLive-Mailing
App\Mail\GoLivePasswordReset+ Templateemails/auth/go-live-password-reset.blade.phpApp\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\PressReleaseServicemit Methoden:submitForReview(),publish(),reject($reason),backToDraft(),archive()assertStatus()wirftLogicExceptionbei ungültigem ÜbergangnotifyAuthor()sendet automatisch Mail nachpublish/rejectvia Queue
App\Mail\PressReleasePublished+ TemplateApp\Mail\PressReleaseRejected+ Template (mit optionalem Ablehnungsgrund)- Admin-Views
press-releases/edit.blade.php+show.blade.phpnutzen jetzt den Service - Service als Singleton in
AppServiceProvidergebunden
P9.2 – Scheduler-Commands
App\Console\Commands\PurgeMagicLinks(magic-links:purge --days=30): löscht verbrauchte/abgelaufene LinksApp\Console\Commands\PurgeExpiredPressReleaseDrafts(press-releases:purge-drafts --days=180 --dry-run): archiviert Zombie-Entwürferoutes/console.phpmit 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üftcanAccessCustomer()routes/customer.php: Route-Gruppecustomer.*mit Middleware, indomains.phpeingebunden- Neue Volt-Komponenten unter
resources/views/livewire/customer/:dashboard.blade.php: Statistik-Widgets, letzte eigene PMs, Firmenübersichtpress-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), Sicherheitscheckpress-releases/edit.blade.php: nur fürdraft/rejectedPMs erlaubt (403 sonst)press-releases/show.blade.php: mitauthorizeAccess()(nur eigene PMs), Prüfungs-Workflowprofile.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 korrektphp artisan magic-links:purge+press-releases:purge-drafts --dry-run→ ✅php artisan test --compact→ 33 relevante Tests grün ✅
Nächster Schritt
- P0.3:
.envmit 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 viaupdatedSelectedLookup*-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 + Commandauth:send-go-live-mails - P0.3:
.envmit 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 nurauth, 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->userhinzugefügt: verhindertTypeErrorbei 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]) DatabaseSeederunverä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 nachportal = 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 inbootstrap/app.phpresources/views/livewire/admin/portal-switcher.blade.php– Volt-Komponente für Sidebar-Portal-Filter (setztadmin_portal_filterSession-Key)
PortalScope angewendet auf:
App\Models\CompanyApp\Models\ContactApp\Models\PressReleaseApp\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 Statespresseecho(),businessportal24(),inactive()database/factories/ContactFactory.phpdatabase/factories/CategoryFactory.php– mitwithTranslations()Statedatabase/factories/PressReleaseFactory.php– mit Statespublished(),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äumtCurrentPortalContext::clear()auf (verhindert Static-State-Leakage zwischen Tests)tests/Feature/Admin/UserManagementTest.php: HTTP-GET-Test für User-Detailseite aufLivewireVolt::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:
.envmit 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_usereingefuehrt:- Migration
2026_04_24_183000_create_contact_user_table.php - Many-to-many zwischen
usersundcontacts
- Migration
- Modelle erweitert:
App\Models\User::contacts()App\Models\Contact::users()
resources/views/livewire/admin/users/edit.blade.phpangepasst:- direkte Persistenz der Links ueber
persistUserLinks()fuer Firmen und Kontakte addLinkedContact()/removeLinkedContact()schreiben jetzt sofort incontact_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)
- direkte Persistenz der Links ueber
Verifikation
tests/Feature/Admin/UserManagementTest.phperweitert/angepasst:- Kontakt-Lookup testet jetzt auch Persistenz in
user->contacts() - neues Szenario
removing linked contact in user edit is persisted immediately
- Kontakt-Lookup testet jetzt auch Persistenz in
- 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.phpangepasst:- neue interne Persistenz
persistCompanyLinks()synchronisiertuser_companiessofort addLinkedCompany()speichert Firmenverknuepfung direktremoveLinkedCompany()entfernt Firmenverknuepfung direktaddLinkedContact()speichert bei Bedarf die zugehoerige Firmenverknuepfung direkt- direkte UI-Feedbacks per Flash (
direkt verknuepft/direkt entfernt)
- neue interne Persistenz
- damit ist kein zusaetzlicher Klick auf
Speichernmehr noetig, um Zuordnen/Entfernen zu persistieren.
Verifikation
tests/Feature/Admin/UserManagementTest.phperweitert/angepasst:removing linked company in user edit is persisted immediatelycontact 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.phpkorrigiert:- 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 alstype=\"button\", damit kein unbeabsichtigter Form-Submit ausgeloest wird - Suche bleibt performant: serverseitig, ab 2 Zeichen, max. 40 Treffer
Verifikation
tests/Feature/Admin/UserManagementTest.phperweitert:removing linked company in user edit is persisted after savecontact 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.phperweitert:- neue Lookup-States fuer Firmen und Kontakte:
companyLookup,selectedLookupCompanyIdcontactLookup,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
- Firmenverknuepfung ueber Live-Suche + Select +
- neue Lookup-States fuer Firmen und Kontakte:
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.phperweitert:- 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.
- neues Szenario
- 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.phperweitert:- neue State-Properties:
contactLookup(Suchbegriff)selectedExistingContactId(ausgewaehlter Kontakt)
- neue Action
attachExistingContact():- validiert Auswahl
- verschiebt Kontakt per
company_idauf 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
40zur Performance-Sicherung
- serverseitiger Filter auf
- UI im Kontakte-Tab:
- Suchfeld mit Live-Debounce
- Select mit dynamischen Treffern
- Button
Zuordnen - Hinweistext zu Suchschwelle und Ergebnislimit
- neue State-Properties:
Verifikation
tests/Feature/Admin/UserManagementTest.phpum neues Szenario erweitert:admin can live-search and assign existing contacts in company detail with result limit- prueft:
- Trefferliste wird bei vielen Treffern auf
40limitiert - ausgewaehlter Kontakt wird nach
attachExistingContact()der Ziel-Firma zugeordnet.
- Trefferliste wird bei vielen Treffern auf
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.phperweitert:- neue Action
deleteContactFromIndex(int $contactId) - Soft Delete via
Contact::delete()und Flash-Feedback - Fehlerfall fuer nicht existente Kontakt-ID
- neue Action
- 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.phperweitert:- 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.
- neues Szenario
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.phperweitert:- neue Action
deleteContact()im Volt-Component - Soft Delete via
Contact::delete()inkl. Redirect + Flash-Meldung - robuste Behandlung fuer nicht existente Kontakt-ID
- neue Action
- UI im Bereich "Aktionen" angepasst:
- Delete-Button oeffnet nun ein Flux-Modal statt direkter Loeschung
- Modal mit klarer Warnung und expliziter Bestaetigung (
Loeschung bestaetigen) Abbrechenschliesst das Modal ohne Aktion
Verifikation
tests/Feature/Admin/UserManagementTest.phperweitert:- bestehendes Kontakt-CRUD-Szenario prueft nun zusaetzlich den Delete-Flow
- Assertions: Redirect auf
admin.contacts.index, kein Treffer in Standard-Query, Datensatz inwithTrashed()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.phperweitert:- neue Action
deleteCompany()im Volt-Component - Soft Delete via
Company::delete()inkl. Redirect + Flash-Meldung - robuste Behandlung fuer nicht existente Firmen-ID
- neue Action
- 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:
AbbrechenundLoeschung bestaetigen
Verifikation
tests/Feature/Admin/UserManagementTest.phperweitert:- 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.phpumgestellt:- entfernt Dummy-Collection, ersetzt durch
Company-Query mitwithCount(['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
- entfernt Dummy-Collection, ersetzt durch
resources/views/livewire/admin/companies/create.blade.phpumgestellt:save()erzeugt jetzt echteCompany-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- undTyp-Auswahl ergaenzt
resources/views/livewire/admin/companies/edit.blade.phpumgestellt:mount()laedt reale Firmendaten aus der DB und behandelt fehlende IDs robust per Redirectupdate()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.phperweitert:- neues Szenario fuer Company Create + Edit (Persistenz inkl.
portal,type,slug,is_active)
- neues Szenario fuer Company Create + Edit (Persistenz inkl.
- 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/
- Admin-UI-Gerüst in
- Acht Migrationsdokumente angelegt (README + 00–08)
Entscheidungen (siehe README.md §D-01 … D-08)
- D-01 Neues DB-Schema (nicht 1:1 aus Doctrine)
- D-02
portal-Spalte für Mandanten-Disambiguierung - D-03 Payment ausschließlich Stripe
- D-04 Country/Salutation als Config-Dateien
- D-05 API-Keys per Sanctum + Kompatibilitäts-Middleware
- D-06 Admin = Livewire Volt + Flux UI
- D-07 DE als primäre UI-Sprache
- D-08 Altes Symfony-Frontend wird nicht übernommen
Offene Fragen (siehe 00-OVERVIEW.md §5)
- Q-01 DB-Dumps beider Portale vorhanden?
- Q-02 Legacy-Passwörter übernehmen?
- Q-03 API-Keys behalten?
- Q-04 Rechnungs-PDFs archivieren?
- Q-05 Media-Storage (S3 / lokal)?
- Q-06 Welche neuen Pflichtfelder?
- Q-07 Mehrsprachigkeit-Umfang?
- Q-08 PromotionLinks / FooterCode im Scope?
- Q-09 Coupons via Stripe oder intern?
- Q-10 Company/Agency trennen oder fusionieren?
Nächster Schritt
- Auftraggeber-Review der Doku + Klärung der offenen Fragen
- Scope freigeben (siehe
06-FEATURES-SCOPE.md§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 aktualisiertREADME.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-Archiv04-DATA-MODEL.md– Legacy-Password/API-Key raus,magic_links-Tabelle,legacy_invoices-Archiv,user_payment_options.grandfathered_until, Company-Logo-Varianten, Promotion/Coupons entfernt05-DATABASE-MERGE.md– Dumps-Ist-Stand, Quellmodi (dump/direct/fixture), wiederholbares Skript, Go-Live-Runbook, Rehearsal-Pflicht06-FEATURES-SCOPE.md– neue IN/OUT/NEU-Matrix mit Customer-Portal §12, Rechnung-out, Promotion-out, Coupons-vertagt07-API-MIGRATION.md– Cut-over statt Keep-Alive, 410-Handler, Token-Abilities, Kommunikationsplan T-30/T-7/T-003-MIGRATION-PLAN.md– Phasen auf 11 erweitert (P4 Customer-Portal neu, P8 Billing neu gebaut, P6 Daten-Migration als kritischer Pfad). Aufwand 16 → ~20 MT.
Entscheidungen (neu)
- D-09 Passwörter werden NICHT übernommen (Go-Live-Mailing)
- D-10 Magic-Link-Login (Einmal-Passwort per Mail) ⭐
- D-11 Backend = Admin + Customer-Portal ⭐
- D-12 Legacy-Rechnungen → Archiv (
legacy_invoices) - D-13 Neue Stripe-Produkte + Grandfathering für Alt-Kunden
- D-14 Promotion Links entfallen
- D-15 Newsletter wird neu aufgebaut
- D-16 Coupons vertagt
- D-17
portal-Spalte +deleted_atüberall verpflichtend - D-18 Import als wiederholbares Skript (für Go-Live-Replay)
Offene Inputs vom Auftraggeber
- Stripe-Test-Credentials + Live-Account
- Preisliste / Definition der neuen Stripe-Produkte
- Liste / Kriterien der "aktiven Alt-Abos" für Grandfathering
- Freigabe Rollen-Mapping
sfGuardGroup→admin/editor/customer/api-only - Liste der API-Key-Kunden für T-30-Mailing
Nächster Schritt
- Phase 0 abschließen: lokale Schemas aus SQL-Dumps importieren,
.envmit 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-Keys410 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.phpund 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.phpalle Volt-Routen entfernt, deren Zielkomponenten aktuell nicht existieren. - Benutzer-Index-Route auf vorhandene Volt-Komponente gemappt (
admin.usersstatt nicht vorhandenesadmin.users.index). - Aktive Admin-Routeanzahl auf 24 konsolidiert (nur valide Mappings).
routes/ADMIN_ROUTES.mdvollständig auf den aktuellen Stand neu geschrieben (aktive Routen + Backlog).README.mdundresources/views/admin/BACKEND_STATUS.mdauf 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.phpdie 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.phpbereinigt:admin.dashboard→dashboardadmin.settings.profile→settings.profileadmin.settings.appearance→settings.appearance
resources/views/web/layouts/admin-master.blade.phpidentisch 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-masteroderlayouts.admin-masteraußerhalb der Doku gefunden. resources/views/web/layouts/admin-master.blade.phpauf einen Alias reduziert:- enthält jetzt nur noch
@extends('layouts.admin-master')
- enthält jetzt nur noch
- Damit ist
resources/views/layouts/admin-master.blade.phpdie 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/Enumsangelegt:Portal,PressReleaseStatus,InvoiceStatus,PaymentOptionType,UserPaymentOptionStatus,CompanyType,RegistrationType,PaymentStatus
- Stammdaten als Config eingeführt:
config/countries.phpconfig/salutations.php
- Users-Fundament erweitert:
- neue Migration
expand_users_table_for_migration_2026 - Felder wie
portal,registration_type,language,is_active,legacy_*,deleted_at passwordauf nullable gesetzt- entsprechendes Casting/Fillable in
Userergänzt, inklusive Relations zuProfileundMagicLink
- neue Migration
- 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.phpapp/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
DatabaseSeedererstellt Admin-User und weist Rolleadminzu
- Rollen:
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 --dirtywar wegen vieler historischer Änderungen im Working Tree nicht sinnvoll; deshalb nur geänderte Dateien gezielt formatiert.
Entscheidungen
- Soft-Deletes auf
usersbleiben gemäß Migrationsentscheidung aktiv; Account-Löschung wurde aufforceDelete()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 --compactaktuell 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_tablecreate_company_user_tablecreate_contacts_table
companiesenthä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_userals Pivot umgesetzt:company_id,user_id,role (member|responsible|owner), PK(company_id, user_id)
contactsenthä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\Usererweitert umownedCompanies()undcompanies()Relations
Probleme
- Keine migrationsbezogenen Fehler im neuen Block.
Verifikation
- ✅
php artisan migrate:fresh --seedläuft vollständig grün.
Nächster Schritt
- P1 fortsetzen mit
categories+category_translations, danachpress_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_tableumgesetzt:- 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)
- Unique
- Felder gemäß Datenmodell (
- Neues Basismodell
App\Models\NewsletterSubscriptionangelegt:- Fillable + Casts (
portal,is_confirmed, Zeitfelder) - Relation
user()
- Fillable + Casts (
App\Models\Userum RelationnewsletterSubscriptions()erweitert.- Dokumentation aktualisiert:
03-MIGRATION-PLAN.mdum Scope-Hinweis ergänzt (Newsletter-Block nurnewsletter_subscriptions).
Entscheidungen
- Auf Wunsch des Auftraggebers wird der Newsletter-Bereich in P1 bewusst reduziert.
blacklists,footer_codesundcategory_footer_codewerden 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_confirmedundunsubscribed_at, ob subscribe oder unsubscribe ausgeloest wird
- Container-Binding in
AppServiceProviderergaenzt: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 Syncim Bereich Billing hinzugefuegt.
- Unterpunkt
- Routen-Dokumentation aktualisiert:
routes/ADMIN_ROUTES.mdauf 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.phpzwei UI-Aktionen finalisiert:Test-Sync ausfuehren(No-Op-Serviceaufruf ueberNewsletterSyncService)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_tablecreate_invoice_billing_addresses_table
billing_addressesenthaelt:user_id(FK, unique), Anrede-/Adressfelder,country_code
invoice_billing_addressesenthaelt:- Snapshot-Adressfelder ohne
user_id-FK (fuer historische Rechnungsdaten)
- Snapshot-Adressfelder ohne
- Basismodelle angelegt:
App\Models\BillingAddressinkl.user()-RelationApp\Models\InvoiceBillingAddress
App\Models\UserumbillingAddress()erweitert (1:1).
Entscheidungen
billingAddressist als 1:1-Relation ausgelegt (technisch via Unique-FK abgesichert).
Nächster Schritt
- P1 fortsetzen mit
payment_options+payment_option_translationsund anschliessenduser_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_tablecreate_payment_option_translations_table
payment_optionsenthaelt:article_number(unique),type(Enum recurring/onetime),price_cents,currency,interval,is_hidden, Stripe-IDs- SoftDeletes und relevante Indizes
payment_option_translationsenthaelt:- FK auf
payment_options,locale,name,description - Unique auf
(payment_option_id, locale)
- FK auf
- Basismodelle angelegt:
App\Models\PaymentOption(Casts inkl.PaymentOptionType, Relationtranslations())App\Models\PaymentOptionTranslation(RelationpaymentOption())
Entscheidungen
intervalbleibt 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, danachuser_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_tablecreate_user_payment_option_company_tablecreate_user_payments_table
user_payment_optionsenthaelt:- FK
user_id, FKpayment_option_id status(Enumactive|past_due|cancelled|grandfathered)grandfathered_until,legacy_conditions(JSON)current_period_start,current_period_end,stripe_subscription_id,cancelled_at
- FK
user_payment_option_companyals Pivot umgesetzt:user_payment_option_id,company_id,is_active,timestamps- zusammengesetzter Primary Key auf beiden IDs
user_paymentsenthaelt:- optionale FK
user_payment_option_id amount_cents,currency,status(Enumpending|succeeded|failed|refunded)stripe_charge_id,stripe_invoice_id
- optionale FK
- Basismodelle angelegt/erweitert:
App\Models\UserPaymentOption(Casts, Relations: user, paymentOption, companies, payments)App\Models\UserPayment(Casts, Relation: userPaymentOption)App\Models\PaymentOptionerweitert umuserPaymentOptions()App\Models\Companyerweitert umuserPaymentOptions()App\Models\Usererweitert umuserPaymentOptions()
Entscheidungen
- Pivot
user_payment_option_companybleibt bewusst als Composite-Key-Tabelle ohne eigeneid. - 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_tablecreate_legacy_invoices_tablecreate_legacy_import_map_table
invoicesenthaelt:- FK
user_id, optionale FKuser_payment_id, FKinvoice_billing_address_id number,status, Betragsfelder (amount_cents,tax_cents,total_cents)currency,is_netto,invoice_date,due_date,paid_atstripe_invoice_id,pdf_path, SoftDeletes + Indizes
- FK
legacy_invoicesals read-only Archiv enthaelt:legacy_portal,legacy_id(unique kombiniert), optionale FKuser_id,legacy_user_id- Nummer, Betrags- und Datumsfelder,
payment_method,pdf_path,raw_snapshot,imported_at
legacy_import_mapenthaelt: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 viatimestamps=false, Casts inkl.raw_snapshot)App\Models\LegacyImportMap(Table-Mapping auflegacy_import_map,timestamps=false)App\Models\UserPaymenterweitert uminvoices()App\Models\InvoiceBillingAddresserweitert uminvoices()App\Models\Usererweitert uminvoices()undlegacyInvoices()
Entscheidungen
legacy_invoicesundlegacy_import_mapwerden ohnecreated_at/updated_atgefuehrt; Zeitbezug erfolgt ueberimported_at.legacy_portalbleibt 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 --seedVolltest.
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:
PaymentOptionFactoryPaymentOptionTranslationFactoryUserPaymentOptionFactoryUserPaymentFactoryBillingAddressFactoryInvoiceBillingAddressFactoryInvoiceFactory
- Neue Seeder-Klasse umgesetzt:
PaymentOptionSeedermit idempotentemupdateOrCreate(inkl. DE/EN-Translations)
DatabaseSeedererweitert:- ruft jetzt
PaymentOptionSeederzusaetzlich auf.
- ruft jetzt
HasFactoryauf relevante Modelle ergaenzt, damit die neuen Factories konsistent nutzbar sind.
Verifikation
- ✅
php artisan migrate:fresh --seedlaeuft 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.phperstellt. - Die aktive Route
admin.roles.editzeigt 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/createebenfalls auf echten Spatie-Flow umstellen (analog zuroles/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.phpkomplett 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
- Role-Erstellung via
- 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/indexvon 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.phpumgestellt auf echte Queries:- Rollen aus
Spatie\Permission\Models\Role withCount(['users', 'permissions'])fuer Live-Kennzahlen
- Rollen aus
- 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:
PaymentOptionSeedererzeugt die erwarteten multilingualen Payment-OptionenUserPaymentOptionPivot- und Payment-Relationen funktionieren inkl. PersistenzInvoice-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.phpangepasst:- Admin-User erhaelt Rolle
adminund direkte Permissions ausgetPermissionsViaRoles() - Test-User erhaelt Rolle
customerund direkte Permissions ausgetPermissionsViaRoles()
- Admin-User erhaelt Rolle
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)
DatabaseSeederauf 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\Usererweitert:canAccessAdmin()implementiert (aktiv + Rolleadmin|editoroderis_super_admin)canAccessCustomer()implementiert (aktiv + Rolleadmin|editor|customer)
- Neuer Pest-Feature-Test
tests/Feature/Auth/UserAccessTest.phperstellt:- 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-onlybleibt explizit ohne Customer-Portal-Zugriff.
Nächster Schritt
- P2.3 umsetzen: Magic-Link-Flow (
Generator,Consume,Mailable) auf Basis der bereits vorhandenenmagic_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.phpangelegt:- initialisiert drei Basiskategorien
- legt pro Kategorie DE/EN-Translation an
- arbeitet idempotent ueber vorhandene DE-Slugs
database/seeders/DatabaseSeeder.phperweitert:CategorySeederwird im Standard-Seedlauf ausgefuehrt
- Neuer Pest-Test
tests/Feature/Categories/CategorySeederTest.php:- prueft Erstellung mehrsprachiger Kategorien
- prueft idempotentes Verhalten bei doppeltem Seeder-Lauf
Entscheidungen
- Kategorien werden aktuell ueber stabile DE-Slugs erkannt, solange kein separates Legacy-Mapping fuer Taxonomie aktiv ist.
Nächster Schritt
- Offene Seeder-Restpunkte aus P1 (z. B. Salutation-Translations) separat priorisieren oder direkt P2.3 (Magic-Link-Flow) starten.
2026-04-24 – P2 umgesetzt: Magic-Link-Login End-to-End
Phase: P2 (Auth & Tenancy) Aufgaben: Magic-Link-Login (Generierung, Versand, Konsumierung) inkl. Login-UI-Erweiterung und Feature-Tests. Status: ✅
Was wurde gemacht
- Neuer Service
App\Services\Auth\MagicLinkGenerator:- erzeugt One-Time-Token (
sha256gehasht in DB) - invalidiert alte, noch offene Login-Links des Users
- setzt TTL und Request-IP
- erzeugt One-Time-Token (
- 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+ Templateresources/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
- Methode
- 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.phperstellt:- Basisdaten des Users editierbar (
name,email,portal,registration_type, Statusflags) - Rollenzuweisung via Spatie (
syncRoles) - Firmenverknuepfung inkl. Pivot-Rolle (
member|responsible|owner) viacompany_user - Sichtbare und editierbare Kontakte der verknuepften Firmen
- Bearbeitbare Rechnungsadresse (
billing_addresses) perupdateOrCreate
- Basisdaten des Users editierbar (
- Routing erweitert:
- neue Route
admin.users.edit(/admin/users/{id}/edit)
- neue Route
- User-Liste erweitert (
resources/views/livewire/admin/users.blade.php):- Rollen und Firmenanzahl sichtbar
- Edit-Aktion pro Benutzer
- Routing-Doku aktualisiert:
routes/ADMIN_ROUTES.mdauf 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.phperstellt:- Anlegen von Usern mit
portal,registration_type, Statusflags - Rollenzuweisung via Spatie (
syncRoles) - Firmenverknuepfung inkl. Pivot-Rolle (
member|responsible|owner) - Optionale Rechnungsadresse direkt beim Anlegen
- Anlegen von Usern mit
- Routing erweitert:
- neue Route
admin.users.create(/admin/users/create)
- neue Route
- User-Index erweitert:
- CTA-Button "Benutzer anlegen" in
resources/views/livewire/admin/users.blade.php
- CTA-Button "Benutzer anlegen" in
- Routing-Doku aktualisiert:
routes/ADMIN_ROUTES.mdauf 27 aktive Routen erweitert
Verifikation
tests/Feature/Admin/UserManagementTest.phpum 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.phperstellt:- 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})
- neue Route
- User-Index erweitert:
- Eye-Button auf Detailansicht in
resources/views/livewire/admin/users.blade.php
- Eye-Button auf Detailansicht in
- Routing-Doku aktualisiert:
routes/ADMIN_ROUTES.mdauf 28 aktive Routen erweitert
Verifikation
tests/Feature/Admin/UserManagementTest.phpum 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
- Bearbeiten bestehender Kontakte auf Basis echter
resources/views/livewire/admin/contacts/index.blade.phpkomplett 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.mdauf 30 aktive Routen erweitert
Verifikation
tests/Feature/Admin/UserManagementTest.phpum 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.phpangepasst:- "Kontakt hinzufuegen"-Aktionen zeigen jetzt auf die neue firmenspezifische Route statt Query-only-Variante.
resources/views/livewire/admin/contacts/create.blade.phperweitert: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.mdauf 31 aktive Routen erweitert
Verifikation
tests/Feature/Admin/UserManagementTest.phpum Prefill-Szenario erweitert:admin.contacts.createmitcompanyIdsetztcompanyIdundisCompanyPrefilledkorrekt.
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.phpvon Dummy-Daten auf echte Eloquent-Queries umgestellt:- laedt
contacts,pressReleases,usersinkl. Counts - zeigt reale Firmenstammdaten statt statischer Platzhalter
- laedt
- Tabs innerhalb der Company-Detailseite eingefuehrt:
overviewcontacts
- 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.phpum 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.phperweitert:- 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
- neuer
- Query-Logik bleibt paginiert und kombiniert Such-/Firmen-/Portal-Filter.
Verifikation
tests/Feature/Admin/UserManagementTest.phpum 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_presetseingefuehrt:- 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)
- Migration
- Neues Model
App\Models\UserFilterPresetangelegt (JSON-Cast fuerfilters). App\Models\Userum RelationfilterPresets()erweitert.resources/views/livewire/admin/contacts/index.blade.phperweitert:- Preset speichern (
savePreset) - Preset anwenden (
applyPreset) - Preset loeschen (
deletePreset) - UI-Elemente fuer Preset-Name, Preset-Auswahl und Aktionen integriert
- gespeicherte Werte:
search,companyFilter,portalFilter
- Preset speichern (
Verifikation
tests/Feature/Admin/UserManagementTest.phpum 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_presetserweitert:- 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)
- neue Migration
App\Models\UserFilterPreseterweitert:is_defaultin Fillable + Boolean-Cast
resources/views/livewire/admin/contacts/index.blade.phperweitert: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.phpum 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_presetserweitert:- 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)
- neue Migration
App\Models\UserFilterPreseterweitert:last_used_atals Datetime-Cast
resources/views/livewire/admin/contacts/index.blade.phpangepasst:- Preset-Liste sortiert jetzt nach:
is_defaultDESClast_used_atDESCnameASC
applyPreset()aktualisiertlast_used_atsavePreset()setzt initialeslast_used_ataufnow()
- Preset-Liste sortiert jetzt nach:
Verifikation
tests/Feature/Admin/UserManagementTest.phpum Szenario erweitert:- prueft, dass
applyPreset()den Timestamp aktualisiert und ein zuvor aelteres Preset nach Nutzung als "zuletzt verwendet" markiert ist.
- prueft, dass
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.phperweitert:- 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.phperweitert:- URL-Filter fuer
status,user,company,contact. - Livewire/Flux-Comboboxen fuer User-, Firmen- und Kontakt-Suche.
- Filter greifen performant auf
user_id,company_idund 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.
- URL-Filter fuer
tests/Feature/Admin/UserManagementTest.phperweitert:- 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.phpphp 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.phperweitert:- 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.
- PM-Count pro Kontakt per
resources/views/livewire/admin/companies/index.blade.phperweitert:- 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.phperweitert:- 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.phpphp 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_pathauf den neuen relativen Public-Storage-Pfad - unterstützt
--portal=presseecho|businessportal24|all - unterstützt
--dry-runund--force - arbeitet nicht-destruktiv: Quellordner werden nicht gelöscht oder verschoben
- meldet fehlende referenzierte Dateien und ungenutzte Dateien pro Portal
- Quelle:
- 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 ungenutztbusinessportal24: 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.phpphp artisan test --compact tests/Feature/SyncCompanyLogosTest.php-> 3 passed, 11 assertionsphp 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.phpphp 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/...).
- neue zentrale Methode
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.
- neuer URL-synchronisierter Portalfilter (
resources/views/livewire/admin/companies/show.blade.php:- Detailheader nutzt ebenfalls
logoUrl()statt rohem DB-Pfad.
- Detailheader nutzt ebenfalls
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.phpphp artisan test --compact tests/Feature/Admin/UserManagementTest.php-> 24 passed, 225 assertions