261 lines
11 KiB
Markdown
261 lines
11 KiB
Markdown
# Phase 7 — Press-Release-Form-Refactor
|
||
|
||
> Großes Modul-Refactor: das zentrale „Neue Pressemitteilung"-Form
|
||
> wird auf das Mockup `User Neue Mitteilung presseportale.html` gehoben.
|
||
> Bekommt deshalb eine eigene Phase außerhalb der bisherigen
|
||
> `hub-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-Boilerplate
|
||
- `press_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_attachments` analog `press_release_images`
|
||
(`disk`, `path`, `original_name`, `mime`, `size`, `sort_order`,
|
||
`title`, `description`, `legacy_portal`, `legacy_id`, soft-deletes,
|
||
timestamps).
|
||
- Models: `PressRelease`, `Company`, neues `PressReleaseAttachment`
|
||
+ Factory + Relationen + Casts.
|
||
- Bestehende Tests müssen grün bleiben (alle Felder nullable,
|
||
keine Verhaltensänderung).
|
||
|
||
**Akzeptanz**
|
||
- [ ] Migration up/down sauber
|
||
- [ ] `php artisan test --compact` grün (Baseline)
|
||
- [ ] `php artisan db:show press_releases / companies / press_release_attachments` zeigt 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 `text` HTML-Tags enthält → `{!! $clean !!}` (sanitized)
|
||
- Wenn nicht (legacy) → `{!! nl2br(e($text)) !!}`
|
||
- Helper `PressRelease::renderedText(): HtmlString`
|
||
- API: `PressReleaseResource` liefert weiterhin String (HTML),
|
||
Doku-Update `_docs/api/v1.yml` und `dev/migration 2026/07-API-MIGRATION.md`.
|
||
|
||
**Akzeptanz**
|
||
- [ ] `mews/purifier` in `composer.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):
|
||
1. **Firma-Selector** als kompakte Inline-Pille (`flux:dropdown`)
|
||
mit „Neue Firma anlegen"-Link
|
||
2. **Titel** (`<flux:input>` mit Title-Font, Counter-Pille
|
||
`meter good/warn` 40–90 Zeichen empfohlen)
|
||
3. **Untertitel** (optional, Counter-Pille 0/200)
|
||
4. **Fließtext** (`<flux:editor>` mit reduzierter Toolbar,
|
||
Counter-Pille 600–3500 Z., KI-Lektorat-bald-Hint)
|
||
5. **Medien** (bestehende `press-release-images-manager`-Komponente
|
||
im neuen Tile-Style anpassen — Titelbild-Flag,
|
||
Caption/Alt-Text Pflicht-Hint)
|
||
6. **Anhänge** (neue Livewire-Komponente in 7E)
|
||
7. **Boilerplate** (Read-only-Box mit Toggle „Für diese PM
|
||
überschreiben" → öffnet `<flux:textarea>`)
|
||
- Rechte Spalte (Settings-Sidebar, sticky):
|
||
1. **Status & Absenden** (Pre-Submit-Checkliste aus
|
||
Pflichtfeldern, primärer Submit „Zur Prüfung senden")
|
||
2. **Portal** (read-only-Badge aus Firma)
|
||
3. **Pressekontakt** (Single-Select aus Firmen-Kontakten,
|
||
Pflichtfeld, Warn-Hint falls Telefon fehlt)
|
||
4. **Themen-Tags** (Chip-Eingabe, parst `keywords` als
|
||
Komma-getrennt, max 5 Tags, Vorschläge aus Firma)
|
||
5. **Veröffentlichung** (RadioGroup: „Sofort nach Freigabe"
|
||
aktiv, „Geplanter Termin" als `bald`-Badge)
|
||
6. **SEO** (Collapsed, „automatisch aus Titel" als `bald`-Hint)
|
||
7. **Phase-2-Footer-Card** wie im Mockup
|
||
- Validation:
|
||
- `title`: required, 5–255
|
||
- `subtitle`: nullable, max 255
|
||
- `text`: required, min 50 (HTML-stripped)
|
||
- `company_id`: required, muss zur User-Firma gehören
|
||
- `category_id`: required
|
||
- `contact_id`: required, muss zur Firma gehören
|
||
- `keywords`: nullable, max 5 Tags
|
||
- `boilerplate_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.php`
|
||
analog `press-release-images-manager.blade.php`
|
||
- Methoden: `upload`, `remove`, `moveUp`, `moveDown`
|
||
- Storage via `Storage::disk('public')` unter
|
||
`press-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 `update` auf PR
|
||
+ Status `draft|rejected` für Customer)
|
||
|
||
**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_at` muss min. 5 Min in der Zukunft
|
||
liegen (passend zum Scheduler-Intervall), `embargo_at` muss in
|
||
der Zukunft liegen. Beide Felder werden nur validiert, wenn der
|
||
jeweilige Toggle aktiv ist.
|
||
- **Save**: `scheduled_at` + `embargo_at` werden in den Forms bei
|
||
Create + Update persistiert (Customer Variante; Admin bleibt
|
||
vorerst ohne UI).
|
||
- **Service**: `PressReleaseService::publish()` nutzt jetzt
|
||
`resolvePublishedAt()`:
|
||
1. bereits gesetztes `published_at` bleibt unangetastet
|
||
2. sonst `scheduled_at` (falls vorhanden)
|
||
3. `embargo_at` verschiebt zusätzlich nach hinten
|
||
4. Fallback `now()`
|
||
Damit greift der vorhandene Sichtbarkeits-Filter
|
||
`where('published_at', '<=', now())` in `routes/web.php`
|
||
automatisch — keine zusätzlichen Embargo-Filter nötig.
|
||
- **Background-Command** `press-releases:publish-scheduled`
|
||
(`App\Console\Commands\PublishScheduledPressReleases`) findet
|
||
alle Review-PRs mit `scheduled_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
|
||
|
||
1. **7A** Migrations + Models
|
||
2. **7B** Editor + Sanitizer
|
||
3. **7C** Customer-Create-Form
|
||
4. **7D** Customer-Edit-Form
|
||
5. **7E** Anhänge-Manager
|
||
6. **7F** Scheduling/Embargo (optional, eigene Sub-Phase)
|
||
|
||
Nach jedem Päckchen: User-Approval + Test-Run + Build + Pint + PROGRESS-Update.
|