11 KiB
Phase 7 — Press-Release-Form-Refactor
Großes Modul-Refactor: das zentrale „Neue Pressemitteilung"-Form wird auf das Mockup
User Neue Mitteilung presseportale.htmlgehoben. Bekommt deshalb eine eigene Phase außerhalb der bisherigenhub-flux-Roadmap (Phase 0–6 sind dort abgeschlossen).
Status: ✅ abgeschlossen · Aufwand: 2–3 Tage · Risiko: mittel (Datenmodell-Erweiterung, Editor-Format-Migration, Composer-Dependency)
Ausgangslage
| Datei | Status |
|---|---|
resources/views/livewire/customer/press-releases/create.blade.php |
nur 1-Spalter, <flux:textarea>, fehlende Felder |
resources/views/livewire/customer/press-releases/edit.blade.php |
gleicher Stand, plus Image-Manager |
resources/views/livewire/admin/press-releases/create.blade.php |
Admin-Variante, dünner |
resources/views/livewire/admin/press-releases/edit.blade.php |
Admin-Variante |
Das Mockup verlangt einen 2-Spalter mit eigener Settings-Sidebar (Status & Submit, Portal, Pressekontakt, Tags, Veröffentlichung, SEO). Linke Spalte: Firma-Selector, Titel, Untertitel, Editor, Medien, Anhänge, Boilerplate.
Entscheidungen (vom User abgesegnet)
| Frage | Entscheidung |
|---|---|
| Scope | full — Anhänge-Tabelle + Schema-Vorbereitung für Scheduling/Embargo |
| Portal-Auswahl | read-only — Portal kommt immer aus der Firma; UI zeigt nur Badge |
| HTML-Sanitizer | mews/purifier — explizit approved, wird in 7B installiert |
| Pressekontakt | genau 1 pro PM, Single-Select aus Firmen-Kontakten; Datenmodell bleibt n:m-Pivot, Validation erzwingt count == 1 |
| Admin-Forms | mitziehen in 7C+7D, gleiches Layout + Admin-only Felder |
| Default-Kontakt | erster Firmen-Kontakt alphabetisch (keine Schema-Änderung) |
Päckchen-Aufteilung
7A — Migrations + Models
Scope:
press_releases.subtitle(string 255, nullable)press_releases.boilerplate_override(text, nullable) — pro PM überschreibbare Firmen-Boilerplatepress_releases.scheduled_at(timestamp, nullable) — Schema da, UI „bald"press_releases.embargo_at(timestamp, nullable) — Schema da, UI „bald"companies.boilerplate(text, nullable) — Firmenprofil-Boilerplate- Neue Tabelle
press_release_attachmentsanalogpress_release_images(disk,path,original_name,mime,size,sort_order,title,description,legacy_portal,legacy_id, soft-deletes, timestamps). - Models:
PressRelease,Company, neuesPressReleaseAttachment- Factory + Relationen + Casts.
- Bestehende Tests müssen grün bleiben (alle Felder nullable, keine Verhaltensänderung).
Akzeptanz
- Migration up/down sauber
php artisan test --compactgrün (Baseline)php artisan db:show press_releases / companies / press_release_attachmentszeigt neue Spalten
7B — Editor-Integration
Scope:
composer require mews/purifier(explizite Approval einholen)- Service
App\Services\PressRelease\PressReleaseHtmlSanitizer(Allowlist:p,br,h2,h3,strong,em,u,ul,ol,li,blockquote,a[href|rel|target]) - Create/Edit-Form:
<flux:textarea>→<flux:editor>mit reduzierter Toolbar:toolbar="heading | bold italic | bullet ordered blockquote | link | undo redo" - Save-Pfad:
$this->text = $sanitizer->clean($this->text) - Display-Pfad (
show.blade.php, Portal-Detail-Seiten,PressReleaseResource):- Wenn
textHTML-Tags enthält →{!! $clean !!}(sanitized) - Wenn nicht (legacy) →
{!! nl2br(e($text)) !!} - Helper
PressRelease::renderedText(): HtmlString
- Wenn
- API:
PressReleaseResourceliefert weiterhin String (HTML), Doku-Update_docs/api/v1.ymlunddev/migration 2026/07-API-MIGRATION.md.
Akzeptanz
mews/purifierincomposer.json- Alle bestehenden Plain-Text-PMs werden korrekt angezeigt
- Neue HTML-PMs werden korrekt sanitized gespeichert
- Pest-Test:
<script>-Tag wird bei Save gestrippt - Pest-Test: legacy plain text →
<p>/<br>Rendering
7C — Customer-Create-Form (UI)
Scope:
- Page-Header wie Mockup (Crumb-Trail + Topbar mit Autosave-Status, „Speichern", „Vorschau-bald")
- 2-Spalter
grid-cols-[1fr,360px] - Linke Spalte (Schreibfläche):
- Firma-Selector als kompakte Inline-Pille (
flux:dropdown) mit „Neue Firma anlegen"-Link - Titel (
<flux:input>mit Title-Font, Counter-Pillemeter good/warn40–90 Zeichen empfohlen) - Untertitel (optional, Counter-Pille 0/200)
- Fließtext (
<flux:editor>mit reduzierter Toolbar, Counter-Pille 600–3500 Z., KI-Lektorat-bald-Hint) - Medien (bestehende
press-release-images-manager-Komponente im neuen Tile-Style anpassen — Titelbild-Flag, Caption/Alt-Text Pflicht-Hint) - Anhänge (neue Livewire-Komponente in 7E)
- Boilerplate (Read-only-Box mit Toggle „Für diese PM
überschreiben" → öffnet
<flux:textarea>)
- Firma-Selector als kompakte Inline-Pille (
- Rechte Spalte (Settings-Sidebar, sticky):
- Status & Absenden (Pre-Submit-Checkliste aus Pflichtfeldern, primärer Submit „Zur Prüfung senden")
- Portal (read-only-Badge aus Firma)
- Pressekontakt (Single-Select aus Firmen-Kontakten, Pflichtfeld, Warn-Hint falls Telefon fehlt)
- Themen-Tags (Chip-Eingabe, parst
keywordsals Komma-getrennt, max 5 Tags, Vorschläge aus Firma) - Veröffentlichung (RadioGroup: „Sofort nach Freigabe"
aktiv, „Geplanter Termin" als
bald-Badge) - SEO (Collapsed, „automatisch aus Titel" als
bald-Hint) - Phase-2-Footer-Card wie im Mockup
- Validation:
title: required, 5–255subtitle: nullable, max 255text: required, min 50 (HTML-stripped)company_id: required, muss zur User-Firma gehörencategory_id: requiredcontact_id: required, muss zur Firma gehörenkeywords: nullable, max 5 Tagsboilerplate_override: nullable
- Pre-Submit-Check (Read-only-Anzeige, blockiert nicht):
- Titel vorhanden + Länge ok → ok
- Fließtext min 600 Z. → ok / sonst warn
- Firma gewählt → ok
- Mind. 1 Bild + Titelbild gesetzt → ok / sonst warn
- Mind. 1 Tag → ok / sonst warn
- Pressekontakt mit Telefon → ok / sonst warn
Was NICHT ins 7C kommt:
- Anhänge-Manager UI (kommt in 7E)
- Scheduling/Embargo-Logik (kommt in 7F)
- Autosave („alle paar Sek.") — erstmal nur Status-Anzeige
(„Manuell gespeichert vor X") via Livewire-Event nach
save(); echtes Autosave optional in 7F
Akzeptanz
- Bestehender Test
CustomerCompanyContextTest > customer press releases create …grün - Neuer Test: Create mit allen Pflichtfeldern → PR persistiert
- Neuer Test: Create ohne Pressekontakt → Validation-Fehler
- Pint + Build grün
7D — Customer-Edit-Form
Scope:
- Gleiches Layout wie 7C
- Submit-Sektion zeigt aktuellen Status (Entwurf/Abgelehnt) + ggf. letzten Reject-Grund
- Image-Manager und Attachments-Manager weiterhin verfügbar
- Form ist nur editierbar, wenn
status in [draft, rejected](bestehende Policy bleibt)
Akzeptanz
- Edit-Test (Update aller neuen Felder) grün
- Reject-Reason-Anzeige bleibt sichtbar
- Bestehende Tests bleiben grün
7E — Anhänge-Manager
Scope:
- Neue Volt-Komponente
resources/views/livewire/components/press-release-attachments-manager.blade.phpanalogpress-release-images-manager.blade.php - Methoden:
upload,remove,moveUp,moveDown - Storage via
Storage::disk('public')unterpress-release-attachments/{press_release_id}/ - Validation: PDF/DOCX/XLSX/PPTX, max. 25 MB pro Datei
- Tile-Layout wie im Mockup (PDF-Badge + Filename + Größe + Aktionen)
- Berechtigung wie beim Image-Manager (Policy
updateauf PR- Status
draft|rejectedfür Customer)
- Status
Akzeptanz
- Upload-Test
- Remove-Test
- Reorder-Test
- Datei-Type-Validation grün
7F — Scheduling + Embargo ✅
Was umgesetzt wurde:
- UI: Customer Create + Edit haben eine zweite Radio-Option
„Geplanter Termin" mit
<flux:input type="datetime-local">und einen separaten Embargo-Switch + Date-Picker.bald-Badge im Veröffentlichungs-Block entfernt. - Validation:
scheduled_atmuss min. 5 Min in der Zukunft liegen (passend zum Scheduler-Intervall),embargo_atmuss in der Zukunft liegen. Beide Felder werden nur validiert, wenn der jeweilige Toggle aktiv ist. - Save:
scheduled_at+embargo_atwerden in den Forms bei Create + Update persistiert (Customer Variante; Admin bleibt vorerst ohne UI). - Service:
PressReleaseService::publish()nutzt jetztresolvePublishedAt():- bereits gesetztes
published_atbleibt unangetastet - sonst
scheduled_at(falls vorhanden) embargo_atverschiebt zusätzlich nach hinten- Fallback
now()Damit greift der vorhandene Sichtbarkeits-Filterwhere('published_at', '<=', now())inroutes/web.phpautomatisch — keine zusätzlichen Embargo-Filter nötig.
- bereits gesetztes
- Background-Command
press-releases:publish-scheduled(App\Console\Commands\PublishScheduledPressReleases) findet alle Review-PRs mitscheduled_at <= now, publisht sie via Service (source: 'scheduler'im Status-Log) und respektiert weiterhin Blacklist + Mail-Benachrichtigung. Optionen:--dry-run,--limit=N. - Scheduler-Eintrag in
routes/console.php: alle 5 Min,withoutOverlapping(),runInBackground().
Tests:
tests/Feature/PressReleaseSchedulingTest.php— 11 Tests für Service + Command (resolvePublishedAt-Matrix, Source-Log, Command-Dry-Run, Command-Limit).tests/Feature/CustomerPressReleaseSchedulingFormTest.php— 5 Tests für die Form (Persistierung, Past-Date-Validation für beide Felder, Now-Mode setzt scheduled_at zurück, Edit-Hydration).
Risiken & Mitigation
| Risiko | Mitigation |
|---|---|
| Editor-HTML bricht alte Anzeigen | Helper renderedText() mit Fallback auf nl2br(e(…)) |
Composer-Dependency mews/purifier ungeprüft |
Vor Install explizite User-Approval; alternative decide_later möglich |
| Pressekontakt-Pivot mit > 1 Eintrag in DB | Migrations-Skript prüft & loggt; Validation in Save erzwingt 1 |
| Test-Suite-Regression | Jedes Päckchen einzeln + php artisan test --compact zwischen den Päckchen |
Out of Scope (bewusst nicht in Phase 7)
- KI-Titel-Optimierung (Phase 8)
- KI-Lektorat (Phase 8)
- KI-Bildgenerierung (Phase 8)
- Mehrfach-Portal-Publishing (Phase 2 laut Mockup-Disclaimer)
- Versionshistorie (Phase 2 laut Mockup-Disclaimer)
- Portal-Vorschau (Phase 2)
- Echtes Autosave alle paar Sekunden (Polish, in 7F optional)
Reihenfolge
- 7A Migrations + Models
- 7B Editor + Sanitizer
- 7C Customer-Create-Form
- 7D Customer-Edit-Form
- 7E Anhänge-Manager
- 7F Scheduling/Embargo (optional, eigene Sub-Phase)
Nach jedem Päckchen: User-Approval + Test-Run + Build + Pint + PROGRESS-Update.