Rebrand Hub+Flux
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-20 15:44:15 +02:00
parent 0a3e52d603
commit 9b47296cea
130 changed files with 9357 additions and 3345 deletions

View file

@ -150,7 +150,7 @@ Bearbeiten Sie die Komponenten in `resources/views/livewire/auth/`:
```php
// Beispiel: Login-Komponente anpassen
new #[Layout('components.layouts.auth')] class extends Component {
new #[Layout('components.layouts.auth.pressekonto')] class extends Component {
#[Validate('required|string|email')]
public string $email = '';

View file

@ -8,7 +8,15 @@
## Phase 2 — Customer-Dashboard auf Mockup-Stil
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
**Status**: ✅ **abgeschlossen** (de facto in Phase 1 +
verfeinert in 4J) · **Aufwand**: ~½ Tag · **Risiko**: niedrig
> Während der Phase-1-Migration wurden Page-Header, KPI-Reihe,
> 2-Spalten-Grids, `<x-portal.stat-card>`,
> `<x-portal.hint-card>` und Brand-Bridge-Dark-Card bereits
> umgesetzt. Phase 4J hat die Recent-PM-Liste mit Portal-Pills
> + PM-ID-Sub auf den finalen 4H/4I-Pattern-Stand gebracht.
> Match zum Mockup ≥ 95 %.
### Ziel
`livewire/customer/dashboard.blade.php` matched das Mockup
@ -40,26 +48,36 @@
## Phase 3 — Admin-Dashboard konsistent
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
**Status**: ✅ **abgeschlossen** (in Phase 1 + 4J)
· **Aufwand**: ~½ Tag · **Risiko**: niedrig
### Ziel
`resources/views/admin/dashboard.blade.php` nutzt dasselbe Vokabular wie
Customer-Dashboard.
### Heutiger Stand
Reines Tailwind mit `zinc-*`-Klassen, **keine** FluxUI-Komponenten,
visuell aus der Zeit gefallen.
### Schritte
- KPI-Karten als `<x-portal.stat-card>`
- Wenn Charts vorhanden: über `flux:chart` umsetzen (FluxUI Pro)
- Recent-Activity-Liste in `flux:card` mit Hub-Akzenten
> Admin-Dashboard nutzt seit Phase 1 dasselbe Vokabular wie
> das Customer-Dashboard: Page-Header, 5-KPI-Reihe mit
> `<x-portal.stat-card>`, 2-Spalten-Grid (Recent + Pending
> Reviews), Newsletter `panel-warm`, Quick-Actions-Grid,
> Footer. Phase 4J hat Portal-Pills, PM-ID-Sub und
> Inline-Action „Prüfen →" mit `is-row-warn`-Tinting für
> die Review-Queue ergänzt.
---
## Phase 4 — Listen-/Detail-Pages durchgehen
**Status**: 🚧 iterativ · **Aufwand**: ~35 Tage gesamt · **Risiko**: niedrig
**Status**: ✅ **komplett abgeschlossen** (4A4J)
· **Aufwand**: ~35 Tage gesamt · **Risiko**: niedrig
> Alle 10 Sub-Päckchen (4A4J) sind grün, Build + Pint
> sauber, alle relevanten Tests bestehen
> (230/231 gesamt — der eine Fail ist der pre-existing
> `ApiDocumentationTest` ohne Bezug zur UI-Migration).
> Das gesamte Backend (Admin + Customer) nutzt damit
> dieselbe Design-Sprache: Page-Header mit
> Eyebrow/Badge/Subtitle, `<x-portal.stat-card>`-KPI-Reihen,
> `article.panel` als Container, `flux:table` mit Hub-Padding
> + Sortierung, `flux:card` nur wo Test-Verträge sie noch
> brauchen, sowie die Mockup-Patterns aus 4H/4I/4J
> (Saved-Views-Tabs, Active-Chips, Portal-Pills,
> Inline-Actions, Row-Tinting, 3-stufige Empty-States).
### Ziel
Alle Volt-Pages im Admin- und Customer-Bereich nutzen denselben Hub-Stil.
@ -73,9 +91,20 @@ Alle Volt-Pages im Admin- und Customer-Bereich nutzen denselben Hub-Stil.
siehe `08-PHASE-4B-PRESS-RELEASES-DETAIL.md`
- **4C** = Press-Releases Forms (create/edit, Admin + Customer) — ✅ **abgeschlossen**
siehe `09-PHASE-4C-PRESS-RELEASES-FORMS.md`
- **4D** = Companies (`admin.companies.*`) — ⚪ pending
- **4E** = Profile/Settings (`settings.*`, `me.profile`, `me.security`) — ⚪ pending
- **4F** = Restliche Admin-Bereiche — ⚪ pending
- **4D** = Companies (`admin.companies.*`) — ✅ **abgeschlossen**
siehe `10-PHASE-4D-COMPANIES.md`
- **4E** = Profile/Settings (`settings.*`, `customer.profile`, `customer.security`) — ✅ **abgeschlossen**
siehe `11-PHASE-4E-PROFILE-SETTINGS.md`
- **4F** = Restliche Admin-Bereiche (contacts, categories, presets, footer-codes, users, roles, newsletter, reports, invoices, coupons, payments, portal-switcher) — ✅ **abgeschlossen**
siehe `13-PHASE-4F-ADMIN-REST.md`
- **4G** = Restliche Customer-Bereiche (invoices, tokens, bookings, company-switcher, me/press-kits/*) — ✅ **abgeschlossen**
siehe `12-PHASE-4G-CUSTOMER-PORTAL.md`
- **4H** = Customer „Meine Pressemitteilungen" auf Mockup-Stil (Counter-Strip, Saved-Views-Tabs, Filter-Chips, Active-Chips, Portal-Pills, Inline-Actions, Row-Tint, 3-fach-Empty-State, Status-Aktionen-Legende) — ✅ **abgeschlossen**
siehe `14-PHASE-4H-PRESS-RELEASES-MOCKUP.md`
- **4I** = Admin „Pressemitteilungen" — Mockup-Patterns übertragen (Saved-Views-Tabs, Active-Chips, Portal-Pills, Row-Tint, Inline-Actions als Modal-Trigger, reduzierte Spalten, 2-stufiger Empty-State) — ✅ **abgeschlossen**
siehe `15-PHASE-4I-ADMIN-PRESS-RELEASES.md`
- **4J** = Dashboard-PM-Listen mit 4H/4I-Patterns (Portal-Pills, PM-ID-Sub, Customer + Admin Dashboard, Inline-Action „Prüfen →" für Admin-Review-Queue mit `is-row-warn` Tinting) — ✅ **abgeschlossen**
siehe `16-PHASE-4J-DASHBOARD-LISTS.md`
### Was geändert wird
- Page-Header-Struktur (Eyebrow + H1 + Subtitle + Aktions-Bar)
@ -91,72 +120,125 @@ Alle Volt-Pages im Admin- und Customer-Bereich nutzen denselben Hub-Stil.
## Phase 5 — Dark Mode konsistent
**Status**: ⚪ später · **Aufwand**: ~½ Tag · **Risiko**: niedrig
**Status**: ✅ **abgeschlossen** · **Aufwand**: ~½ Tag (Großteil
der Arbeit war Vorbereitung in Phase 04) · **Risiko**: niedrig
### Ziel
Dark Mode funktioniert sauber im Portal, **ohne doppelte UI-Pflege**.
> Tokens, Switcher und alle Custom-CSS-Komponenten waren
> bereits token-basiert vorbereitet — `shared/design-tokens.css`
> hat einen kompletten `.dark { … }`-Block (Z. 182248) mit allen
> Surfaces, Hub/Bernstein-Akzenten, Ink-Skala, Status-Farben,
> Schatten und `color-scheme: dark`. `hub-components.css` ist
> zu 100 % token-basiert und schaltet automatisch um. FluxUI's
> `@fluxAppearance` ist in beiden Head-Partials integriert,
> Switcher liegt im User-Menü (Desktop + Mobile) sowie in
> `settings/appearance.blade.php`. Bei der Inventur fiel nur
> ein latenter Bug auf (`--color-ink-deep`-Token existiert
> nicht, in `customer/tokens.blade.php` auf
> `--color-panel-dark-2` umgestellt). QR-Code in
> `customer/security.blade.php` behält bewusst `bg-white` in
> beiden Modi (Scan-Tauglichkeit), klarstellender Kommentar
> ergänzt.
>
> Siehe `17-PHASE-5-DARK-MODE.md` für Details.
### Quelle
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html` hat
alle Dark-Tokens bereits vorgegeben.
`dev/frontend/tailwind_v3/User Dashboard presseportale Dark.html`
hat alle Dark-Tokens bereits vorgegeben — alle Werte sind in
`design-tokens.css` übernommen.
### Schritte
1. In `shared/design-tokens.css` Dark-Werte ergänzen:
```css
@media (prefers-color-scheme: dark) {
@theme {
--color-bg: #0E1218;
--color-bg-elev: #14181F;
--color-hub: #5A78C2;
--color-accent: #D9A560;
/* … */
}
}
.dark {
/* gleiche Werte für expliziten Switch */
}
```
2. `class="dark"` wird **nicht** mehr hardcoded gesetzt
3. Flux Appearance-Switcher (`settings/appearance.blade.php`) steuert
`.dark` auf `<html>`
4. Hub-Frontend bleibt erstmal Light-Only (so wie heute)
### Erwartung
- Settings → Appearance → „Dark" schaltet Portal um
- Settings → Appearance → „System" folgt OS-Präferenz
- Sidebar, Topbar, Dashboards, Listen, Forms — alles funktioniert
- Hub-Landing und Hub-Auth bleiben Light
### Was geliefert wird
- Settings → Erscheinung → „Dunkel" schaltet Portal um
- Settings → Erscheinung → „System" folgt OS-Präferenz
- Switcher zusätzlich direkt im User-Menü (Sidebar + Mobile)
- Sidebar, Topbar, Dashboards, Listen, Forms — alles schaltet
automatisch über Token-Cascade um
- `panel-dark` bleibt KONSTANT dunkel (Brand-Bridge-Card)
- `--color-accent-warm` bleibt KONSTANT Bernstein (Hint-Cards)
- Hub-Landing und Hub-Auth bleiben Light (kein
`@fluxAppearance` im Web-Build)
---
## Phase 6 — Auth-Konsolidierung (optional)
## Phase 6 — Auth-Cleanup
**Status**: ⚪ optional · **Aufwand**: 01 Tag · **Risiko**: mittel
**Status**: ✅ **abgeschlossen** · **Aufwand**: ~30 min · **Risiko**: niedrig
### Frage
Bleibt der Hub-Login (`auth/pressekonto`-Layout, Web-Build) so wie er ist?
Oder konsolidieren wir auf den Portal-Build?
> Hub-Auth bleibt eigenständig (auf Web-Build mit dem `pressekonto`-Layout),
> weil Hub-Atmosphäre + Light-Bundle als Trade-off bewusst gewollt sind.
> Die ungenutzten Starter-Kit-Layouts wurden entfernt: `auth/simple`,
> `auth/split`, `auth/card`, der Wrapper `auth.blade.php`, das alternative
> `app/header.blade.php` sowie die Debug-Views `livewire/auth/login-simple`
> und `test-simple`. Damit gibt es keine hardcoded `class="dark"`-Reste
> mehr, die Phase 5 hätten torpedieren können. Doku-Beispiel in
> `_docs/FORTIFY-SANCTUM-SETUP.md` auf `components.layouts.auth.pressekonto`
> umgestellt. CSS-Bundle ist dabei sogar ~2.4 KB kleiner geworden.
>
> Siehe `18-PHASE-6-AUTH-CLEANUP.md` für die Inventur.
### Pro Konsolidierung
- Ein Build weniger
- Konsistente Komponenten-Sprache
### Optional für später (NICHT Teil von Phase 6)
Eine echte **Auth-Konsolidierung** (Hub-Auth-CSS in den Portal-Build
ziehen, Web-Build nur für die Landing-Pages behalten) wäre denkbar,
aber:
- Hub-Atmosphäre (Konzentrische Kreise, Hub-Grid) ist visuell stark
- Hub-Auth ist leichtgewichtig (kein FluxUI im Bundle)
- Aktueller Stand funktioniert sauber
### Pro Beibehaltung (heutiger Stand)
- Hub-Atmosphäre der Auth-Seiten (Konzentrische Kreise, Hub-Grid) ist
visuell sehr stark — würde verloren gehen
- Hub-Auth ist **leichtgewichtig** (kein FluxUI im Bundle)
- Vorlage `Login pressekonto A3 Tailwind.html` ist bewusst minimalistisch
→ keine Action notwendig, bleibt offen.
### Entscheidung
Vermutlich **beibehalten**. Aber: Die alten Auth-Layouts
`auth/simple.blade.php`, `auth/split.blade.php`, `auth/card.blade.php` aus
dem Starter-Kit können vermutlich gelöscht werden, sobald geprüft ist,
dass keine Komponente sie noch verwendet.
---
### Schritte (wenn konsolidiert)
- Hub-Auth-CSS in `portal.css` ziehen (mit `@source` für die Tokens)
- `auth/pressekonto`-Layout auf Portal-Build umstellen
- Hub-Auth-Klassen (`.auth-card`, `.auth-grid`, `.auth-btn-primary`)
bleiben — laufen nur jetzt aus dem Portal-Bundle
- `theme-pressekonto.css` aus dem Web-Build entfernen (oder behalten
für die Landing)
## Gesamt-Status (Stand 2026-05-20)
| Phase | Inhalt | Status |
|---|---|---|
| 0 | Hub-Auth (Login/Register) im Hub-Stil | ✅ |
| 1 | Portal-Migration (Tokens, FluxUI-Overrides, Sidebar/Topbar, App-Shell, Dark-Switch im User-Menü) | ✅ |
| 2 | Customer-Dashboard auf Mockup-Stil | ✅ (in P1, verfeinert in 4J) |
| 3 | Admin-Dashboard konsistent | ✅ (in P1, verfeinert in 4J) |
| 4 | Listen/Detail durchgehen (4A4J) | ✅ **komplett** |
| 5 | Dark Mode konsistent | ✅ **abgeschlossen** |
| 6 | Auth-Cleanup | ✅ **abgeschlossen** |
### Phase 4 — Sub-Päckchen im Detail
| ID | Bereich | Plan-Doc |
|---|---|---|
| 4A | Press-Releases Listen (Admin + Customer) | `07-PHASE-4A-…` |
| 4B | Press-Releases Detail/Show | `08-PHASE-4B-…` |
| 4C | Press-Releases Forms (create/edit) | `09-PHASE-4C-…` |
| 4D | Companies (Admin) | `10-PHASE-4D-…` |
| 4E | Profile/Settings (Admin + Customer) | `11-PHASE-4E-…` |
| 4F | Restliche Admin-Bereiche (contacts, categories, presets, footer-codes, users, roles, newsletter, reports, invoices, coupons, payments, portal-switcher) | `13-PHASE-4F-…` |
| 4G | Restliche Customer-Bereiche (invoices, tokens, bookings, company-switcher, press-kits) | `12-PHASE-4G-…` |
| 4H | Customer „Meine Pressemitteilungen" Mockup-Stil (Counter-Strip, Saved-Views, Filter-/Active-Chips, Portal-Pills, Inline-Actions, Row-Tint, 3-stufiger Empty-State, Aktionen-Legende) | `14-PHASE-4H-…` |
| 4I | Admin „Pressemitteilungen" — Mockup-Patterns übertragen | `15-PHASE-4I-…` |
| 4J | Dashboard-PM-Listen mit 4H/4I-Patterns | `16-PHASE-4J-…` |
### Nächste sinnvolle Schritte (Priorität)
> Die hub-flux-Roadmap ist mit Phase 6 **vollständig** abgeschlossen.
> Alle weiteren Themen sind eigene Initiativen.
1. **Manueller Dark-Mode Smoke-Test**: Im Browser User-Menü →
Erscheinung → „Dunkel" und durch die Hauptseiten klicken
(Dashboard, Listen, Detail, Security mit QR, Tokens). Erwartung:
Lesbar + konsistent. Kleine Polish-Runde, falls visuelle
Auffälligkeiten.
2. **PM-Form-Wizard-Refactor**: Mockup
`User Neue Mitteilung presseportale.html` auf den bestehenden
Press-Release-Create/Edit-Flow übertragen. Größere Aktion mit
eigener Phase.
3. **Web-Frontend-Block** (eigenständig, NICHT Teil von Phase 16):
Die noch ungenutzten Mockups
`Startseite Tailwind.html`,
`Startseite presseecho Tailwind.html`,
`Branchenseite Tailwind.html`,
`Detailseite Tailwind.html`,
`Veröffentlichen Tailwind.html`
sind Public-Facing-Pages der beiden Portale (businessportal24 +
presseecho), nicht Portal/Backend. Würde eine **eigene Roadmap**
bekommen, weil andere Build-Pipeline (`vite.web.config.js`), andere
Komponenten und ein ganz anderer Stil-Stack
(Source-Serif-Headlines, Brand-orange für businessportal24).

View file

@ -0,0 +1,79 @@
# Phase 4D — Companies (admin)
> Viertes Päckchen aus Phase 4. Folgt auf 4A/4B/4C
> (Press-Releases-Strecke komplett).
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrigmittel
---
## Scope
- `resources/views/livewire/admin/companies/index.blade.php` (573 Z.)
- `resources/views/livewire/admin/companies/show.blade.php` (400 Z.)
- `resources/views/livewire/admin/companies/edit.blade.php` (412 Z.)
- `resources/views/livewire/admin/companies/create.blade.php` (281 Z.)
NICHT in diesem Päckchen:
- `admin/contacts/*` — eigener Bereich, Päckchen 4F.
- `me/press-kits/*` — Customer-Sicht auf Firmen, eigener Päckchen-Anteil.
- Settings / Profile (Päckchen 4E).
## Ziel
Alle vier Companies-Pages im Hub-Vokabular — gleiches Muster wie
die Press-Releases-Strecke:
- **Page-Header** mit Hub-Badge „Admin Backend" + Eyebrow + H1 +
Subtitle. Bei Show/Edit zusätzlich Portal-Pille (Presseecho /
Businessportal24 / Both) und ID/Slug-Hinweis.
- **KPI-Reihe** auf Index als `<x-portal.stat-card>` falls Stats
vorhanden (Gesamt, je Portal, ggf. mit/ohne PMs).
- **Filter-Bar** als `.panel` mit `.panel-head` „Filter & Suche".
- **Tabelle/Listen** in `.panel` mit Hub-Badges
(`.badge.hub|ok|warn`).
- **Form-Sektionen** (Edit, Create) als `.panel` mit `.panel-head`
und `section-eyebrow`.
- **Tabs** (Show: „contacts", andere?) bleiben als FluxUI-Tabs —
optisch über das Token-Bridging genug Hub-Look.
- **Confirm-Modals** unverändert lassen (z.B. delete-confirm).
## Was explizit NICHT angefasst wird
- **Volt-Logik** in allen 4 Files — Layout-only.
- **`deleteCompany`**-Methode + Confirm-Modal — Test
`UserManagementTest` assertiert Redirect-Verhalten.
- **Wortlaute, die Tests prüfen**:
- `Firmen` (Sidebar-Link), `Portal`, `Alle Portale`,
`Firmen PM Zaehler GmbH` (Faktur-Name).
- Portal-Labels `Presseecho`, `Businessportal24`.
- URL-Pfade `/admin/companies/{id}`.
- **FluxUI Form-Felder, Combobox, Tab-Groups** bleiben.
## Akzeptanzkriterien
- [x] Plan
- [x] Index: Page-Header + Stats + Filter-Panel + Tabellen-Panel +
Hub-Badges.
- [x] Show: Page-Header (mit Portal-Pille) + Logo-/Meta-Block +
Tabs + Hub-Badges.
- [x] Edit: Page-Header (mit Portal-Pille) + Form-Panels +
Aktions-Panel. Delete-Modal unverändert.
- [x] Create: Page-Header + Form-Panels + Aktions-Panel.
- [x] `UserManagementTest`, `PortalAssetManifestTest` grün
(25 Tests, 227 Assertions). Volle Suite: 230 grün
(Vorhandener `ApiDocumentationTest`-Fail nicht von uns).
- [x] Build + Pint + PROGRESS.
## Notes
- Tabs als schlankes Bottom-Border-Pattern statt FluxUI-Tabs:
spart Layout-Komplexität und ist optisch näher am Hub.
- Portal-Badges bewusst alle als `badge hub` vereinheitlicht —
Portal-spezifische Farben (purple/blue) brachen aus dem
Tokensystem aus. Portal-Label bleibt sichtbar im Text.
- Logo-Boxen folgen jetzt dem gleichen Token-Pattern wie auf den
Press-Release-Detail-Seiten (`var(--color-bg-elev)` +
`var(--color-bg-rule)` border).
- Delete-Confirm-Modal komplett unverändert (Test prüft Redirect
nach `deleteCompany`, nicht UI).

View file

@ -0,0 +1,103 @@
# Phase 4E — Profile & Settings
> Fünftes Päckchen aus Phase 4. Folgt auf 4D Companies.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Scope
**Admin / „/settings/*" (alle Rollen)**
- `resources/views/livewire/settings/profile.blade.php` (117 Z.)
- `resources/views/livewire/settings/password.blade.php` (82 Z.)
- `resources/views/livewire/settings/appearance.blade.php` (21 Z.)
- `resources/views/livewire/settings/delete-user-form.blade.php`
(59 Z., wird in profile.blade.php als Komponente eingebunden)
**Customer („Mein Bereich")**
- `resources/views/livewire/customer/profile.blade.php` (451 Z.)
- `resources/views/livewire/customer/security.blade.php` (295 Z.)
NICHT in diesem Päckchen:
- `customer/invoices.blade.php`, `customer/tokens.blade.php`,
`customer/bookings.blade.php`, `customer/company-switcher`
eigenes Päckchen 4F.
- 2FA-Pages aus Fortify-Standard-Setup — bleiben FluxUI bis sie
separat angepackt werden.
## Ziel
Beide Settings-Strecken im Hub-Vokabular wie Press-Releases /
Companies:
- **Page-Header** mit Rolle-Pille („Admin Backend" oder
„User Backend") + Eyebrow + H1 + Subtitle.
- **Form-Sektionen** als `.panel` mit `.panel-head` und
`section-eyebrow`.
- **FluxUI-Form-Felder** bleiben (`flux:field`, `flux:input`,
`flux:label`, `flux:error`, `flux:checkbox`, `flux:radio`,
`flux:textarea`, `flux:button`).
- **Required-Marker** auf Hub-Token (`text-[color:var(--color-err)]`).
- **Save-Action** in eigenem Panel-Footer mit Save-Indicator-Span.
- **Flash-/Success-Messages** auf Hub-Token-Pillen.
- **Danger-Zone** (Delete-Account, Sessions löschen) als
`.panel` mit `is-danger`-Akzent (linker Roter Strip).
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<x-settings.layout>` Wrapper-Komponente (falls vorhanden) —
Layout-only-Änderungen, keine Hülle.
- 2FA-/Confirm-Password-Modals.
- Test-relevante Strings auf customer/profile + customer/security:
- **profile**: „Rechnungsadresse"
- **security**: „Konto-Sicherheit", „Letzter Login",
„Aktive Sessions", „Passwort ändern", „E-Mail-Adresse ändern",
„Zwei-Faktor-Authentifizierung"
## Akzeptanzkriterien
- [x] Plan
- [x] `partials/settings-heading` zentral auf Hub-Page-Header.
- [x] `components/settings/layout` zentral auf Hub-Sidebar +
Content-Panel.
- [x] `settings/appearance` Hub-styled (profitiert vom neuen
Wrapper, kein Body-Touch nötig).
- [x] `settings/profile` Hub-styled + Verification-Hinweis als
Hub-Warn-Box + Save-Button mit Token-„Saved."-Pill.
- [x] `settings/delete-user-form` als Hub-Danger-Box mit
linkem Roten Strip. Modal-Markup unverändert.
- [x] `settings/password` Hub-styled (Save-Bar mit Border-Top).
- [x] `customer/profile` Hub-styled: Header, 3 Panels (Konto,
Profil, Rechnungsadresse), Aktionen-Panel, Zugeordnete-Firmen-
Panel mit Hub-Badges.
- [x] `customer/security` Hub-styled: Header, 4-spaltige
KPI-Reihe, Passwort+E-Mail 2-Col-Grid, 2FA-Panel mit
Hub-Recovery-Codes, Sessions-Panel mit Hub-Empty-State.
- [x] Tests grün:
- `Settings|CustomerProfileSecurity|CustomerCompanyContext`
→ 33 passed (146 assertions).
- Volle Suite → 230 passed, 3 skipped, 1 pre-existing
`ApiDocumentationTest`-Fail (nicht von 4E).
- [x] Build + Pint + PROGRESS.
## Notes
- Der zentrale Umbau der beiden Wrapper (`settings-heading`,
`settings.layout`) hat sich bezahlt gemacht: die drei
`settings/*`-Pages sind weitgehend identisch klein geblieben,
Page-Header und Side-Nav kommen automatisch im Hub-Look.
- `<x-action-message>` für „Saved."-Toast bleibt drin, aber
der Slot ist auf einen Hub-getönten `<span>` umgestellt.
- Empty-State für „Aktive Sessions" nutzt das gleiche
Hub-Icon-Box-Pattern wie auf den Press-Releases-Listen.
- 2FA-QR-Container behält explizit weißen Hintergrund —
QR-Codes brauchen hohen Kontrast unabhängig vom Mode.
- Alle Test-Pflicht-Strings unverändert: „Rechnungsadresse",
„Konto-Sicherheit", „Letzter Login", „Aktive Sessions",
„Passwort ändern", „E-Mail-Adresse ändern",
„Zwei-Faktor-Authentifizierung".

View file

@ -0,0 +1,78 @@
# Phase 4G — Customer Portal (Mein Bereich)
> Sechstes Päckchen aus Phase 4. Folgt auf 4E
> (Profile/Settings).
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrigmittel
---
## Scope
**Liste / Detail / Helper**:
- `resources/views/livewire/customer/invoices.blade.php` (194 Z.)
- `resources/views/livewire/customer/tokens.blade.php` (212 Z.)
- `resources/views/livewire/customer/bookings.blade.php` (52 Z.)
- `resources/views/livewire/customer/company-switcher.blade.php`
(91 Z., sitzt in der Sidebar)
- `resources/views/livewire/customer/press-kits/index.blade.php`
(119 Z.)
- `resources/views/livewire/customer/press-kits/show.blade.php`
(734 Z., großes Detail-Cockpit)
NICHT in diesem Päckchen:
- `customer/press-releases/*` — schon in 4A/4B/4C.
- `customer/dashboard.blade.php` — schon in Phase 2.
- `customer/profile.blade.php`, `customer/security.blade.php`
schon in 4E.
- Admin-Bereich (Päckchen 4F).
## Ziel
Alle Customer-Pages im Hub-Vokabular:
- **Page-Header** „User Backend"-Pille + Eyebrow
„Mein Bereich · …" + H1 + Subtitle.
- **KPI/Summary-Reihe** wo Stats existieren
(`<x-portal.stat-card>`).
- **Tabellen, Listen, Forms** in `.panel` mit `.panel-head`.
- **Flash-Boxen** (success/info/warn/error) auf Hub-Token.
- **Hub-Badges** statt `flux:badge` (außer dort wo speziell
begründet).
- **Empty-States** als Hub-Icon-Box.
- **Company-Switcher**: bleibt funktional, optisch Hub-Akzent.
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<flux:select variant="combobox">`, `<flux:input>` usw.
- Konfirm-/Edit-Modals in `press-kits/show.blade.php` (falls
vorhanden).
- Bestimmte Strings, die Tests assertieren:
- **tokens**: „API-Tokens" (Heading),
„API-Tokens werden erst freigeschaltet" (Hinweis).
- **invoices**: „Hinweis zu Rechnungen",
„Rechnungsadresse im Profil pflegen", „Öffnen".
- **company-switcher**: „Firma öffnen",
`route('me.press-kits.show', …)`.
- **press-kits.show**: „Abrechnung", „Statistik" (Tab-Labels
bzw. Section-Headings).
## Akzeptanzkriterien
- [x] Plan
- [x] `customer/invoices`
- [x] `customer/tokens`
- [x] `customer/bookings` (Coming-Soon-Stub)
- [x] `customer/company-switcher` (Sidebar-Komponente)
- [x] `customer/press-kits/index`
- [x] `customer/press-kits/show` (Stammdaten/Kontakte/PMs/
Abrechnung/Statistik im Hub-Stil; Forms inline behalten)
- [x] `CustomerPortalTest`, `CustomerCompanyContextTest`,
`PanelConsolidationTest` grün (29 Tests, 131 Assertions).
- [x] Volle Suite: 230 passed, 3 skipped, 1 pre-existing fail
(`ApiDocumentationTest`, fehlende `docs/api/v1.yml`).
- [x] Build (portal: 418 kB CSS, 44 kB JS) + Pint clean +
PROGRESS.

View file

@ -0,0 +1,84 @@
# Phase 4F — Restliche Admin-Bereiche
> Siebtes Päckchen aus Phase 4. Folgt auf 4G
> (Customer Portal).
**Status**: ✅ abgeschlossen · **Aufwand**: ~1,52 Tage · **Risiko**: niedrig
---
## Scope
Insgesamt ~7.500 Z. Blade. Aufgeteilt in 4 Sub-Päckchen:
### 4F-1 — Stammdaten & Switcher (~1.250 Z.)
- `admin/presets/{index,create,edit}.blade.php` (361 Z.)
- `admin/presets/partials/form-fields.blade.php`
- `admin/categories/{index,create,edit}.blade.php` (813 Z.)
- `admin/portal-switcher.blade.php` (68 Z., Sidebar-Komponente)
### 4F-2 — Pressekontakte (~1.360 Z.)
- `admin/contacts/index.blade.php` (729 Z.)
- `admin/contacts/create.blade.php` (275 Z.)
- `admin/contacts/edit.blade.php` (352 Z.)
### 4F-3 — Operations & Finance (~1.870 Z.)
- `admin/footer-codes/{index,create,edit}.blade.php` (673 Z.)
- `admin/reports/slow-requests.blade.php` + table (288 Z.)
- `admin/invoices/index.blade.php` (314 Z.)
- `admin/coupons/index.blade.php` (43 Z., Stub)
- `admin/payments/index.blade.php` (53 Z., Stub)
### 4F-4 — User-Verwaltung (~3.020 Z.)
- `admin/users.blade.php` (939 Z.) +
`admin/users/{create,edit,show,table}.blade.php`
- `admin/roles/{index,create,edit}.blade.php` (393 Z.)
- `admin/newsletter/sync.blade.php` (171 Z.)
## Ziel
Alle Admin-Pages im Hub-Vokabular:
- **Page-Header** „Admin Backend"-Pille + Eyebrow
„Administration · …" + H1 + Subtitle.
- **KPI/Summary-Reihe** wo Stats existieren
(`<x-portal.stat-card>`).
- **Tabellen, Listen, Forms** in `.panel` mit `.panel-head`.
- **Flash-Boxen** (success/info/warn/error) auf Hub-Token.
- **Hub-Badges** statt `flux:badge` (außer in begründeten
Fällen).
- **Empty-States** als Hub-Icon-Box.
- **Danger-Zones** mit linkem roten Strip.
## Was explizit NICHT angefasst wird
- Volt-Logik in allen Dateien.
- `<flux:input>`, `<flux:select>`, `<flux:checkbox>`,
`<flux:textarea>`, `<flux:field>`, `<flux:error>`,
`<flux:table.*>`, `<flux:button>`, `<flux:modal>` etc.
- Modals und ihre Bestätigungs-Flows.
- Test-relevante Strings — vor jedem Sub-Päckchen werden die
betreffenden Test-Assertions geprüft.
## Tests pro Sub-Päckchen
| Sub | Tests |
|---|---|
| 4F-1 | `CategoryIndexPerformanceTest`, `AdminCategoryManagementTest`, `AdminPresetManagementTest` |
| 4F-2 | (keine spezifischen Contact-Tests gefunden, evtl. via `AdminAssetManifestTest` indirekt) |
| 4F-3 | `AdminFooterCodeManagementTest`, `AdminSlowRequestReportTest`, `AdminSlowRequestLoggingTest`, `AdminLegacyInvoiceArchiveTest` |
| 4F-4 | `UserManagementTest`, `UserImpersonationTest`, `RoleManagementTest` |
## Akzeptanzkriterien
- [x] Plan
- [x] 4F-1 Stammdaten & Switcher
- [x] 4F-2 Pressekontakte
- [x] 4F-3 Operations & Finance
- [x] 4F-4 User-Verwaltung
- [x] Tests pro Sub-Päckchen grün
- [x] Build + Pint + PROGRESS.

View file

@ -0,0 +1,171 @@
# Phase 4H — Customer „Meine Pressemitteilungen" auf Mockup-Stil
> Nachgelagertes Päckchen aus Phase 4. Hebelt das wichtigste
> Customer-Arbeitstool auf das volle Mockup-Vokabular.
**Status**: ✅ abgeschlossen · **Aufwand**: ~1 Tag · **Risiko**: niedrig
---
## Anlass
`customer/press-releases/index.blade.php` ist heute auf
~50 % Mockup-Match: Page-Header, Filter-Panel, Tabelle und
Empty-State sind Hub-styled, aber alle hochwirksamen Patterns
aus dem Mockup fehlen. Diese Seite ist die meistbesuchte des
Customer-Bereichs (primäres Arbeitstool).
## Vorlage
`dev/frontend/tailwind_v3/User Pressemitteilungen presseportale.html`
## Match-Ziel
≥ 90 % zum Mockup, ohne Backend-API-Erweiterungen.
---
## Scope — was umgesetzt wird
### 1. Counter-Strip im Header
Statt langer Subzeile: Inline-Counter pro Status direkt
unter H1.
`24 Mitteilungen · 18 veröffentlicht · 1 in Prüfung · 4 Entwürfe · 1 abgelehnt`
Zahlen kommen aus aggregiertem `withCount()` über die Status-Enum.
### 2. Saved-Views-Tabs
Tab-Leiste statt Status-Dropdown:
`Alle (24) · Veröffentlicht (18) · Entwürfe (4) · In Prüfung (1) · Abgelehnt (1) · Archiv (0)`
Aktive Tab unterstrichen mit Hub-Blau. Klick → `statusFilter`-Volt-Property.
### 3. Filter-Chips mit Caret
Status/Portal/Firma/Zeitraum als Chips mit Dropdown-Caret.
Aktive Chip in Hub-Blau, inaktive auf weißem Background.
**Hinweis**: Status-Chip ist im UI redundant mit Saved-Views-Tabs;
wir lassen ihn weg. Bleiben: Portal, Firma, Zeitraum (`erstellt` ab Datum X).
### 4. Active-Chips (entfernbare Filter)
Unter den Filter-Chips: Sichtbarmachung aktiver Filter mit
`×`-Button zum Entfernen.
### 5. Bulk-Selection (nur UI-Vorbereitung)
Checkbox in Header und pro Zeile. Header-Checkbox kann
indeterminate-State haben. **Keine Bulk-Aktionen** im Scope —
das wird erst später ein eigenes Päckchen (Bulk-Approve etc.).
### 6. Portal-Pills mit farbigem Dot
Pro Zeile in Spalte „Portal": je nach `portal`-Enum:
- `presseecho` → grüner Dot
- `businessportal24` → orangener Dot
- `both` → beide Pills nebeneinander
### 7. Inline-Actions neben Status
Statusspalte bekommt **zusätzlich** zur Badge eine kleine
Inline-Action je nach Status:
| Status | Inline-Action |
|---|---|
| Draft | `Zur Prüfung →` |
| Review | `Zurückziehen →` (nur wenn `withdrawFromReview` existiert, sonst weglassen) |
| Published | — (keine Inline-Action, nur Hauptaktion „Vorschau") |
| Rejected | `Grund ansehen →` (Modal/Tooltip mit `rejection_reason`) |
| Archived | — |
### 8. Row-Tinting
Zeilen mit Status `review` bekommen `is-row-warn` (sehr
dezent gelb beim Hover), `rejected` bekommen `is-row-err`
(rot beim Hover).
### 9. Reichere Zeilen-Sub
Sub-Zeile unter Titel: `PM-Nummer · Datum · Hinweis`
Wir nutzen vorhandene Felder: `id` (als „PM-{id}"), `published_at`,
`rejection_reason`-Existenz.
### 10. Datum mit Sub
Datumsspalte zweizeilig: Datum (Mono-Font, tabular-nums) +
Sub (Status-Kontext z.B. „veröffentlicht · 09:12").
### 11. 3-fach Empty-State
Drei Varianten je nach Kontext:
- **`empty-filter`** — wenn Filter aktiv: „Keine Mitteilungen mit diesen Filtern" + Reset-Button
- **`empty-search`** — wenn `search` gesetzt: „Keine Treffer für `…`" + Suche-Reset
- **`empty-none`** — wenn gar keine PMs überhaupt: „Noch keine Pressemitteilungen" + 3-Schritt-Onboarding
### 12. Status-Aktionen-Legende
`panel-warm` unter der Tabelle: 5-Spalten-Grid mit allen
Aktionen pro Status. Reduziert Support-Anfragen.
### 13. Spalten-/Export-Buttons (rechts im Header)
„Spalten" (jetzt non-functional als `bald`-Badge) und „Export"
(`bald`-Badge) als sekundäre Buttons. Optional — können auch
weggelassen werden, da wir keine Spalten-Konfiguration haben.
**Entscheidung**: weglassen. Kein UX-Mehrwert ohne Backend.
---
## Was NICHT angefasst wird
- Volt-Logik (`submitForReview`, `sort`, `with`) — nur erweitert
um Aggregate-Counts.
- Backend-Endpoints / API.
- Detail-/Show-Seite und Create/Edit-Forms.
- Routen.
## Neue / erweiterte CSS-Klassen in `hub-components.css`
- `.counter-strip`, `.counter-strip .seg`, `.counter-strip .sep`
- `.view-tab` (mit `.is-active`, `.cnt`)
- `.filter-chip` (mit `.is-active`, `.caret`)
- `.active-chip` (mit `.x`)
- `.portal-pill` (mit `.pe`, `.bp`, `.pdot`)
- `.inline-action` (mit `.warn`, `.err`)
- `.is-row-warn`, `.is-row-err` (Row-Tinting)
- `.empty-stage`, `.empty-ico`, `.empty-title`, `.empty-sub`
- `.badge.muted` (für Draft / Archiv)
- Optional: `.checkbox.indeterminate` (für Bulk-Header)
## Volt-Erweiterungen
- `statusCounts` (assoc-Array Status-Value → int) — aggregiert
über `withoutGlobalScopes()->where('user_id', …)->groupBy('status')`
- `totalCount` (int) — Gesamt-Counter unabhängig vom Filter
- Saved-View-Klick mappt auf `statusFilter` (kein eigener Property nötig)
## Tests
Keine bestehenden Tests für `customer/press-releases/index`
gefunden. Akzeptanz:
- `php artisan test --compact` läuft sauber (modulo
pre-existing `ApiDocumentationTest`)
- Build (`npm run build:portal`) clean
- Pint clean
## Akzeptanzkriterien
- [x] Plan
- [x] CSS-Klassen ergänzt
- [x] Volt-Counts ergänzt
- [x] Markup auf Mockup-Stil
- [x] Build + Pint + Tests grün
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert

View file

@ -0,0 +1,136 @@
# Phase 4I — Admin „Pressemitteilungen" Mockup-Patterns
> Folgepäckchen zu 4H. Überträgt die in der Customer-PM-Liste
> entwickelten Mockup-Patterns auf die Admin-Seite. Die
> CSS-Komponenten existieren bereits.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag · **Risiko**: niedrig
---
## Anlass
Nach 4H matched `customer/press-releases/index.blade.php` das
Mockup zu ~90 %. Die gleichen Patterns gehören in die
Admin-Liste — das ist der zentrale Editorial-Workflow.
## Aktueller Stand
`admin/press-releases/index.blade.php` ist bei ~60 % Match:
- ✅ Page-Header (Hub-Stil)
- ✅ 4-KPI-Reihe (`<x-portal.stat-card>`)
- ✅ Filter-Panel (search/status/portal/language/category +
3 entity-comboboxes)
- ✅ Tabellen-Panel + Empty-State (Hub-styled)
- ✅ Modals (publish/reject/archive) — test-kritisch
- ❌ Saved-Views-Tabs
- ❌ Active-Chips für aktive Filter
- ❌ Portal-Pills (statt Text)
- ❌ Row-Tinting (review/rejected)
- ❌ Inline-Actions („Veröffentlichen →", „Ablehnen →" neben Status)
- ❌ Reichere Sub-Zeilen (PM-{id} + Datum + Status-Kontext)
## Scope
### 1. Saved-Views-Tabs
Tab-Leiste zwischen KPI-Reihe und Filter:
`Alle · Veröffentlicht · In Prüfung · Entwürfe · Abgelehnt · Archiv`
Counter-Pille pro Tab basierend auf erweiterten `stats`.
### 2. Active-Chips
Sichtbarkeit aller aktiven Filter (Status, Portal, Sprache,
Kategorie, User, Firma, Kontakt, Suche), jeweils mit `×`.
### 3. Portal-Pills
Statt „Presseecho"/„Businessportal24"/„Beide" als Text:
Portal-Pills mit farbigem Dot. Bei `Portal::Both` zwei
Pills nebeneinander.
### 4. Row-Tinting
`is-row-warn` für review, `is-row-err` für rejected.
### 5. Inline-Actions
**Zusätzlich** zu den bestehenden Action-Buttons + Modals:
| Status | Inline-Action |
|---|---|
| Review | `Freigeben →` + `Ablehnen →` (öffnet die Modals) |
| Published | `Archivieren →` (öffnet Modal) |
Die Inline-Actions sind **nur** Trigger für die bestehenden
Modals (keine direkte Wire-Methode), damit die existierenden
Test-Modal-Strings („Pressemitteilung veröffentlichen?")
weiter angezeigt werden. Action-Buttons in der `Aktionen`-
Spalte bleiben als Backup für Power-User.
### 6. Reichere Sub-Zeilen
In Titel-Zelle: `PM-{id}` als Sub. In Datum-Zelle: Hauptdatum
+ Status-Kontext (`erstellt · 11:02`, `veröffentlicht · 14:30`, …).
### 7. Counter-Strip ausgelassen
KPI-Reihe ist bereits 4-spaltig (Gesamt, Veröffentlicht, In
Prüfung, Entwürfe) und voll funktional. Counter-Strip wäre
redundant. Bleibt bei KPI-Cards.
## Was bleibt unangetastet
- **Volt-Methoden** (`publish`, `reject`, `archive`, `sort`,
`with`, `clearUserFilter`, …)
- **Bestehende Modals** mit Strings „Pressemitteilung
veröffentlichen?", „Pressemitteilung ablehnen?",
„Pressemitteilung archivieren?" (Test-kritisch)
- **Filter-Panel-Struktur** (Entity-Comboboxes erhalten,
kein Reduktionsexperiment)
- **`stats`-Cache-Methode** (`AdminPerformanceCache`)
## Erweiterung
- `pressReleaseStats()` erweitert um `rejected` und `archived`
Counter, damit alle 6 Saved-Views-Tabs ihre Counter haben
## Tests
- `AdminPressReleaseActionsTest` (3 Tests, prüft auf Modal-Strings)
- `PressReleaseWorkflowTest` (mehrere Tests, prüft auf
`publish`/`reject`-Methoden + Audit-Logs)
- Volle Suite muss weiter sauber laufen (modulo
pre-existing `ApiDocumentationTest`)
## Akzeptanzkriterien
- [x] Plan
- [x] Volt: `stats` um `rejected` + `archived` erweitert
- [x] Volt: `setView()` + `resetFilters()` ergänzt
- [x] Saved-Views-Tabs eingebaut (6 Stück inkl. Counter)
- [x] Active-Chips eingebaut (8 Filter-Typen)
- [x] Portal-Pills statt Text (incl. Both → 2 Pills)
- [x] Row-Tinting (review → warn, rejected → err)
- [x] Inline-Actions als Modal-Trigger
(Freigeben/Ablehnen/Archivieren) zusätzlich neben Badge
- [x] Sub-Zeilen: PM-ID + Firma + Sprache, Datum + Status-Kontext
- [x] Empty-State 2-stufig (mit/ohne Filter)
- [x] Build + Pint + Tests grün (16/16 PR + 230/231 voll)
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert
## Lessons Learned
- `@php(...)` Inline-Form führte beim Compile zu einem
Syntax-Fehler im kompilierten View (führte zu
`<?php($var = ...)` ohne Whitespace), obwohl alle Klammern
balanced waren. Vermutung: Konflikt der Klammer-Zählung
mit Methoden-Calls. Lösung: alle drei `@php(...)`
Statements zu `@php ... @endphp` umgewandelt.
- Tabellen-Spalten reduziert (8 → 7), weil die rechte
„Aktionen"-Spalte mit den Status-Icons inhaltlich
redundant zur neuen Inline-Action in der Status-Spalte
war. Die linke „Aktionen"-Spalte (view/edit) blieb.

View file

@ -0,0 +1,63 @@
# Phase 4J — Dashboard-PM-Listen mit 4H/4I-Patterns
> Mini-Folgepäckchen: Die in 4H/4I entwickelten
> Mockup-Patterns (Portal-Pills, Status-Sub-Zeilen,
> Inline-Actions) werden auf die kompakten PM-Listen
> in den Dashboards übertragen.
**Status**: ✅ abgeschlossen · **Aufwand**: ~30 min · **Risiko**: niedrig
---
## Anlass
Nach 4H/4I sehen die Voll-Listen exakt wie das Mockup aus —
die Dashboard-Listen ("Meine letzten Pressemitteilungen",
"Letzte Pressemitteilungen", "Zur Prüfung") sind aber noch
auf dem alten Stand (Badge rechts, Sub-Zeile ohne Portal).
Das wirkt inkonsistent.
## Scope
### Customer-Dashboard (Volt `customer/dashboard.blade.php`)
- **Volt-Component**: `recent` lädt aktuell
`['id', 'title', 'status', 'company_id', 'created_at']`.
`portal` ergänzen.
- **Markup `recent`-Liste**:
- Portal-Pills (presseecho/businessportal24) neben dem Badge
- Sub-Zeile mit PM-ID + Firma + Datum (statt nur Firma + Datum)
### Admin-Dashboard (Controller-View `admin/dashboard.blade.php`)
- **`recentPRs`-Liste**:
- Portal-Pills neben dem Badge
- Sub-Zeile mit PM-ID + Firma + User + Datum
- **`pendingReviews`-Liste**:
- Portal-Pills neben Titel
- Inline-Action „Prüfen →" als Link zur Show-Page
(im Dashboard keine direkte `publish`/`reject`-Methode
möglich, da Controller+Blade statt Volt)
## Was bleibt unangetastet
- Backend-Logik (Controller, Volt-`with()`)
- Customer-Tests (`DashboardTest`) — die geprüften
Strings („Phase 2 Demo Release", „Alle anzeigen",
„Meine letzten Pressemitteilungen", …) bleiben
- Quick-Actions, Newsletter, Brand-Bridge — bereits gut
## Akzeptanzkriterien
- [x] Plan
- [x] Customer-Volt: `portal` + `published_at` in
Recent-Select-Liste
- [x] Customer-Dashboard: Portal-Pills + PM-ID-Sub
+ published_at als Primärdatum bei Published
- [x] Admin-Dashboard recentPRs: Portal-Pills + PM-ID-Sub,
`badgeClass` mit `muted`-Variante (statt `hub` für
archived/draft)
- [x] Admin-Dashboard pendingReviews: Portal-Pills +
Inline-Action „Prüfen →" + `is-row-warn` Tinting
- [x] Build + Pint + Tests grün (5/5 Dashboard, 230/231 voll)
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md aktualisiert

View file

@ -0,0 +1,152 @@
# Phase 5 — Dark Mode
> Dark Mode für das Portal (Customer + Admin), gesteuert
> über den FluxUI Appearance-Switcher. Hub-Frontend bleibt
> Light-Only.
**Status**: ✅ abgeschlossen · **Aufwand**: ~½ Tag
(meiste Arbeit war Vorbereitung in Phase 04) · **Risiko**: niedrig
---
## Befund
Beim Start in Phase 5 war faktisch ~95 % der Arbeit schon
in den vorherigen Phasen erledigt worden. Konkret:
### Was bereits da war
1. **`shared/design-tokens.css`** hat einen kompletten
`.dark { … }`-Block (Z. 182248) mit:
- Surfaces (bg, bg-elev, bg-rule, bg-card, bg-card-warm)
- Hub-Blau (heller im Dark Mode: `#5a78c2`)
- Bernstein (heller: `#d9a560`), `--color-accent-warm`
bewusst konstant
- Ink-Skala (invertiert)
- Status-Farben (ok/warn/err in helleren Varianten)
- Bridge-Dots (heller)
- Schatten (neutral schwarz statt warm-blau)
- `color-scheme: dark` für native Controls
2. **`portal.css`** hat:
- `@custom-variant dark (&:where(.dark, .dark *))` für
Tailwind-`dark:`-Variants
- Dark-Mode Shadow-Overrides für FluxUI-Primary-Buttons
- Klare Erläuterung, warum kein Notfall-`.dark { … }`-Hack
mehr nötig ist
3. **`shared/hub-components.css`** ist **zu 100 %
token-basiert** (panels, badges, view-tabs, filter-chips,
active-chips, portal-pills, inline-actions, empty-stage,
counter-strip). Sobald die Tokens umschalten, schalten
alle Komponenten automatisch um.
4. **`panel-dark`** nutzt `--color-panel-dark-2` /
`--color-panel-dark` — die sind in beiden Modi
identisch. Die Brand-Bridge-Card bleibt damit immer
dunkel.
5. **`@fluxAppearance`** ist in beiden Head-Partials
eingebunden (`partials/head.blade.php` +
`partials/admin-head.blade.php`).
6. **Switcher** ist an drei Stellen verfügbar:
- Sidebar Desktop User-Menü
(`components/layouts/app/sidebar.blade.php`)
- Sidebar Mobile User-Menü (gleiche Datei)
- `settings/appearance.blade.php` (volle Card mit
Light/Dark/System)
Alle drei nutzen `x-model="$flux.appearance"`
FluxUI's Magic-Object, das LocalStorage-persistent ist
und automatisch `class="dark"` auf `<html>` setzt.
### Was zu fixen war
Bei der Inventur fielen nur zwei Stellen auf, die im
Dark Mode brechen würden:
1. **`customer/tokens.blade.php` Z. 137**: Token-Anzeige
nach Generierung nutzte ein nicht existentes
`--color-ink-deep`-Token. Damit war der Hintergrund
bisher schlicht transparent (Light: kaum sichtbar,
Dark: ebenso). Außerdem würde `--color-ink` im Dark
Mode hell werden — weißer Text auf hellem Bg wäre
unlesbar. Fix: auf `--color-panel-dark-2` (konstant
dunkel) umstellen.
2. **`customer/security.blade.php` Z. 270**: 2FA-QR-Code
in `bg-white`-Block. Das ist **bewusst** so — QR-Codes
brauchen schwarz-auf-weiß, sonst werden sie nicht
eingescannt. Kein Fix, nur Kommentar zur Klarstellung.
### Was unkritisch ist
- `customer/dashboard.blade.php`: 8 Treffer für
`bg-white` / `text-white` — alle im `panel-dark`
Brand-Bridge Block (konstant dunkel, weiße Texte
korrekt) und im Empty-State Counter-Badge auf
`--color-accent` (Bernstein-Bg, beides Modi).
- `admin/dashboard.blade.php` Z. 235: Quick-Action
Hover-State `group-hover:bg-hub group-hover:text-white`
— Hub-Bg ist sowohl Light (dunkles Hub-Blau) als auch
Dark (helleres Hub-Blau) okay für weißen Text.
- Sidebar-`dark:bg-zinc-…` Klassen aus dem Starter-Kit:
Die Zinc-Skala ist in `portal.css` auf warmes
Buchpapier gemapped, das funktioniert weiter — die
Avatar-Bg's sind sowieso nur 8×8-Pixel-Spots.
- `customer/press-kits/show.blade.php` Z. 440: Logo-Bg
`bg-white` — bewusst, weil Firmen-Logos einen weißen
Hintergrund für korrekte Darstellung brauchen.
### Was außerhalb von Phase 5 ist
- **`shared-styles.css`** hat `.dark .card`, `.dark
.slider-*`, `.dark .highlight-*`, `.dark .section-*`
Regeln. Diese sind für den **Web-Bereich**
(Hub-Frontend, presseecho, businessportal24), nicht
fürs Portal. `portal.css` importiert
`shared-styles.css` **nicht** — die Regeln landen also
nicht im Portal-Bundle.
- **Web-Frontend** bleibt Light-Only — wie in der
Roadmap definiert.
## Akzeptanzkriterien
- [x] Plan
- [x] Recherche aller hardcoded Farbklassen
- [x] `--color-ink-deep``--color-panel-dark-2` in
`customer/tokens.blade.php`
- [x] `bg-white` für 2FA-QR-Code in `security.blade.php`
mit erklärendem Kommentar versehen
- [x] Build + Pint + Tests grün
- [x] PROGRESS.md + 03-WEITERE-PHASEN.md auf ✅
## Lessons Learned
- Konsequente Token-Disziplin (kein Hex außer in
`design-tokens.css`) zahlt sich beim Dark Mode massiv
aus: Statt 15 Pages × dutzende Klassen anzufassen,
schaltet das gesamte Portal mit einem `.dark {…}` Block
um.
- Die wenigen Ausnahmen (panel-dark, QR-Code-Bg,
Logo-Bg) sind dokumentierte Bewusst-Entscheidungen,
keine Schuld.
- FluxUI's `$flux.appearance` Magic-Object spart eine
eigene Alpine-Component und persistiert über
LocalStorage.
## Manueller Smoke-Test (Empfehlung)
Da Pest keine echte Browser-Rendering-Pipeline hat:
- Im Browser: User-Menü → Erscheinung → „Dunkel"
- Folgende Seiten besuchen + visuell prüfen:
- `/dashboard` (Admin) + `/me/dashboard` (Customer)
- `/me/press-releases` (Customer-Liste mit
Saved-Views-Tabs, Filter-Chips, Portal-Pills,
Row-Tints)
- `/admin/press-releases` (Admin-Liste mit Inline-Actions)
- `/me/security` (2FA-QR mit `bg-white`)
- `/me/tokens` (Token-Anzeige nach Generierung)
- `/admin/companies/{id}` (Detail-View mit allen Panels)
- Erwartung: Lesbar, konsistent, keine grellen
Kontraste, keine „weißen Inseln" auf dunklem Bg.

View file

@ -0,0 +1,54 @@
# Phase 6 — Auth-Cleanup
> Ungenutzte Starter-Kit-Layouts und Debug-Views entfernen,
> die seit der Hub-Auth-Umstellung (Phase 0) keine Verwendung
> mehr haben.
**Status**: ✅ abgeschlossen · **Aufwand**: ~30 min · **Risiko**: niedrig
---
## Inventur
### Aktiv genutzt (BEHALTEN)
| Datei | Genutzt von |
|---|---|
| `components/layouts/app.blade.php` | Wrapper → `app.sidebar` |
| `components/layouts/app/sidebar.blade.php` | primäres Portal-Layout (alle Volt-Pages) |
| `components/layouts/auth/pressekonto.blade.php` | login, register, forgot-password, verify-email, confirm-password, reset-password |
### Ungenutzt (ENTFERNT)
| Datei | Grund |
|---|---|
| `components/layouts/auth.blade.php` | dünner Wrapper auf `auth.simple` — nirgends als Layout referenziert (außer `login-simple` + `test-simple` Debug, die ebenfalls weg sind) |
| `components/layouts/auth/simple.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/auth/split.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/auth/card.blade.php` | Starter-Kit-Rest, hardcoded `class="dark"` |
| `components/layouts/app/header.blade.php` | alternatives App-Layout (Top-Bar statt Sidebar), wird vom Wrapper nie aufgerufen, hardcoded `class="dark"` |
| `livewire/auth/login-simple.blade.php` | Debug-Volt-Login („Log in to your account"-Englisch) ohne Route, nutzte `<x-layouts.auth>` |
| `test-simple.blade.php` | Debug-Page „Diese Seite funktioniert ohne Volt/Livewire" ohne Route |
### Aktualisiert
- `_docs/FORTIFY-SANCTUM-SETUP.md`: Code-Beispiel von
`components.layouts.auth` auf `components.layouts.auth.pressekonto`
umgestellt (alter Wert verwies auf die soeben gelöschte
Wrapper-Datei).
## Verifikation
- `rg "auth\.(simple|split|card)"` und
`rg "app\.header"` → 0 Treffer in `resources/` und `routes/`
- Build, Pint, alle relevanten Tests grün
## Lessons Learned
- Mit der Anti-Flash-Bridge aus Phase 5 wären die hardcoded
`class="dark"`-Layouts sowieso zur Stolperfalle für
Light-Mode-User geworden, falls sie je reaktiviert werden.
Lösung: weg damit.
- Hub-Auth-Pages laufen seit Phase 0 ausschließlich über
`auth/pressekonto.blade.php`. Die Starter-Kit-Layouts waren
schon damals tote Last.

View file

@ -5,6 +5,920 @@
---
## 2026-05-20 · Phase 6 · Auth-Cleanup ✅
Mit Phase 6 ist die hub-flux-Roadmap (Phase 06) **vollständig
abgeschlossen**. Schlanker Cleanup-Task ohne Risiko.
**Inventur** (Referenzen geprüft via `rg` über `resources/` + `routes/`):
| Datei | Status |
|---|---|
| `components/layouts/app/sidebar.blade.php` | aktiv (primäres Portal-Layout) |
| `components/layouts/auth/pressekonto.blade.php` | aktiv (6 Auth-Pages) |
| `components/layouts/auth.blade.php` | nur in `login-simple`/`test-simple` → weg |
| `components/layouts/auth/simple.blade.php` | Starter-Kit-Rest mit hardcoded `class="dark"` → weg |
| `components/layouts/auth/split.blade.php` | dito → weg |
| `components/layouts/auth/card.blade.php` | dito → weg |
| `components/layouts/app/header.blade.php` | nirgends referenziert → weg |
| `livewire/auth/login-simple.blade.php` | Debug-Page ohne Route → weg |
| `test-simple.blade.php` | Debug-Page ohne Route → weg |
**Gelöscht**: 7 Files, insgesamt ~17 KB Code
**Aktualisiert**: `_docs/FORTIFY-SANCTUM-SETUP.md` (Code-Beispiel
von `components.layouts.auth` auf `components.layouts.auth.pressekonto`).
**Side-Effect**: Portal-CSS-Bundle ist ~2.4 KB kleiner geworden
(422.62 KB statt 425.07 KB), weil weniger Klassen via Tailwind
JIT entdeckt werden.
**Validierung**:
- `php artisan view:clear && npm run build:portal` → grün
- `vendor/bin/pint --dirty --format agent` → passed
- Tests: 230/231 (nur der bekannte pre-existing
`ApiDocumentationTest`)
**Roadmap-Update**:
- `03-WEITERE-PHASEN.md` Phase 6 von „⚪ optional
(Auth-Konsolidierung)" auf „✅ abgeschlossen (Auth-Cleanup)"
umgestellt
- Status-Tabelle: Phase 6 ✅
- „Nächste Schritte" angepasst: kein hub-flux-Item mehr offen,
nur noch eigenständige Initiativen (Dark-Mode-Smoke-Test,
PM-Form-Wizard-Refactor, Web-Frontend-Block)
**Lesson**: Mit der Anti-Flash-Bridge aus Phase 5 wären die
hardcoded `class="dark"`-Layouts zu einer Stolperfalle für
Light-Mode-User geworden, sobald jemand sie reaktiviert hätte.
Cleanup zum richtigen Zeitpunkt.
Plan-Doc: `18-PHASE-6-AUTH-CLEANUP.md`
---
## 2026-05-20 · Phase 5 · Dark-Mode-Bugfixes (User-Findings)
Nach dem ersten visuellen Smoke-Test des Users zwei Folge-Fixes
geliefert, plus die Anti-Flash-Bridge:
### Fix 1 — Logo „presse" im Dark Mode
`components/web/brand-mark.blade.php`: Bei `variant="auto"` (Default)
wird die Name-Farbe jetzt über CSS-Custom-Property `--brand-mark-name-color`
mit Light-Mode-Fallback gesetzt. Im Portal-Dark-Mode setzt
`design-tokens.css` `.dark`-Block die Variable auf `#ffffff`. Hub-Frontend
ist Light-Only, dort bleibt die Variable undefiniert und der Marken-
Standardwert greift.
### Fix 2 — Navlist-Flackern beim Klick
`portal.css` Navlist-Item-Block:
- Hover im Dark Mode auf `var(--color-hub-soft)` (Light-Mode-Hover
`var(--color-bg)` ist dunkler als die Sidebar — wirkte als
„Eindrücken" statt Hervorheben)
- `:active`, `:focus` explizit auf `var(--color-hub-soft)` mit
`outline: none` → verhindert Browser-Default-Tap-Flash
- `-webkit-tap-highlight-color: transparent` für mobile Browser
- `:focus-visible` mit Hub-Ring für Tastatur-A11y bleibt erhalten
### Fix 3 — Anti-Flash-Bridge (FOLT bei `wire:navigate`)
Bei `wire:navigate` morpht Livewire das DOM, das neue HTML kommt
vom Server **ohne** `class="dark"` (Server kennt LocalStorage nicht).
Folge: kurzer weißer Theme-Flash, bis JS die Klasse wieder anhängt.
User hat das richtig diagnostiziert.
Lösung:
1. **JS-Bridge** in `partials/head.blade.php` +
`partials/admin-head.blade.php`: wrappt
`window.Flux.applyAppearance`, spiegelt den effektiv applizierten
Modus (bei `system` aus DOM-Klasse abgelesen → `dark`/`light`)
in ein `flux_appearance`-Cookie. Schreibt auch beim initialen
Page-Load.
2. **Server-Side Render**: `<html>`-Tag in
`components/layouts/app/sidebar.blade.php` und
`layouts/admin-master.blade.php` setzt `class="dark"`
direkt, basierend auf dem Cookie:
`@class(['dark' => request()->cookie('flux_appearance') === 'dark'])`
3. **Bonus**: hardcoded `class="dark"` aus `admin-master.blade.php`
raus (war Light-User-Bug-Falle).
Erstbesuch: einmaliger Flash (Cookie noch nicht gesetzt).
Alle weiteren Pageloads: kein Flash mehr.
### Latente Bug-Fixes nebenbei
- `customer/tokens.blade.php` Z. 137: `--color-ink-deep` Token war
nie definiert → durch `--color-panel-dark-2` ersetzt (konstant
dunkel in beiden Modi)
- `customer/security.blade.php` 2FA-QR: erklärender Kommentar für
`bg-white` (Scan-Tauglichkeit)
**Validierung**:
- Build, Pint, Tests: 230/231 (nur ApiDocumentationTest)
---
## 2026-05-20 · Phase 5 · Dark Mode ✅
Phase 5 (Dark Mode) ist abgeschlossen. Die meiste Arbeit war
faktisch schon in Phase 04 erledigt — die konsequente
Token-Disziplin zahlt sich jetzt voll aus.
**Was schon da war (Stand bei Phase-5-Start):**
- `shared/design-tokens.css` hat einen kompletten
`.dark { … }`-Block (Z. 182248) mit allen Surfaces,
Hub-Blau (heller), Bernstein (heller, mit konstantem
`--color-accent-warm`), Ink-Skala (invertiert),
Status-Farben, Bridge-Dots, Schatten und `color-scheme: dark`
- `portal.css` hat `@custom-variant dark (&:where(.dark, .dark *))`
+ Dark-Mode-Shadow-Overrides für Primary-Buttons
- `hub-components.css` ist 100 % token-basiert
- `panel-dark` nutzt `--color-panel-dark*` (konstant)
- `@fluxAppearance` in beiden Head-Partials integriert
- Switcher in Sidebar (Desktop + Mobile) +
`settings/appearance.blade.php`
**Was im Rahmen von Phase 5 gefixt wurde:**
1. **`customer/tokens.blade.php` Z. 137** — Token-Anzeige
nutzte ein nicht existentes `--color-ink-deep`. Damit war
der Code-Block bisher transparent (im Dark Mode wäre
`--color-ink` hell → weißer Text unlesbar). Umgestellt auf
`--color-panel-dark-2` (konstant dunkel, weißer Text in
beiden Modi korrekt).
2. **`customer/security.blade.php` Z. 270** — 2FA-QR-Code in
`bg-white`. Bewusst belassen (QR-Codes brauchen
schwarz-auf-weiß für Scan-Tauglichkeit), erklärender
Kommentar ergänzt.
**Was unkritisch ist (kein Fix):**
- Alle weiteren `bg-white` / `text-white`-Treffer in
`customer/dashboard.blade.php` sind im `panel-dark` Block
(konstant dunkel) oder auf `--color-accent` (Bernstein in
beiden Modi) — passt
- `admin/dashboard.blade.php` Quick-Action Hover
(`group-hover:bg-hub group-hover:text-white`) — Hub-Bg ist
in beiden Modi dunkel genug für weißen Text
- `customer/press-kits/show.blade.php` Logo-Bg `bg-white`
bewusst, weil Firmen-Logos einen weißen Bg brauchen
- Sidebar-`dark:bg-zinc-*` Helper aus dem Starter-Kit —
Zinc-Skala ist in `portal.css` auf warmes Buchpapier
gemapped, läuft
**Außerhalb Phase 5:**
- `shared-styles.css` hat `.dark .card`, `.dark .slider-*`,
`.dark .highlight-*`, `.dark .section-*` — diese sind für
den Web-Bereich (Hub-Frontend, presseecho,
businessportal24). `portal.css` importiert
`shared-styles.css` **nicht** — keine Kollision.
- Web-Frontend bleibt Light-Only (per Roadmap-Definition)
**Validierung:**
- Build: `npm run build:portal` → grün
- Pint: `vendor/bin/pint --dirty --format agent` → passed
- Tests: 230/231 (1 pre-existing `ApiDocumentationTest`, kein
Bezug zur UI-Migration)
**Empfehlung:** Im Browser einmal User-Menü →
Erscheinung → „Dunkel" + Dashboard/Listen/Detail/Security
(QR)/Tokens visuell durchklicken für den Smoke-Test (Pest
hat keine echte Rendering-Pipeline).
Roadmap-Update:
- `03-WEITERE-PHASEN.md` Phase 5 von „⚪ später" auf
„✅ abgeschlossen" gesetzt
- Status-Tabelle aktualisiert
- Empfehlungen verschoben: Phase 6 Auth-Cleanup +
manueller Smoke-Test nach oben
Plan-Doc: `17-PHASE-5-DARK-MODE.md`
---
## 2026-05-20 · Phase 4 · offiziell abgeschlossen ✅
Mit dem Abschluss von 4J ist Phase 4 (Listen/Detail-Pages
durchgehen) in allen 10 Sub-Päckchen 4A4J durch:
| ID | Bereich |
|---|---|
| 4A | Press-Releases Listen (Admin + Customer) |
| 4B | Press-Releases Detail/Show |
| 4C | Press-Releases Forms (create/edit) |
| 4D | Companies (Admin) |
| 4E | Profile/Settings (Admin + Customer) |
| 4F | Restliche Admin-Bereiche (12 Module) |
| 4G | Restliche Customer-Bereiche (5 Module) |
| 4H | Customer „Meine Pressemitteilungen" Mockup-Stil |
| 4I | Admin „Pressemitteilungen" — Patterns übertragen |
| 4J | Dashboard-PM-Listen mit 4H/4I-Patterns |
**Roadmap-Cleanup**:
- `03-WEITERE-PHASEN.md` Phase 4 von „🚧 iterativ" auf
„✅ komplett abgeschlossen" gesetzt
- Phase 2 + 3 (Customer- + Admin-Dashboard) waren faktisch
schon in Phase 1 umgesetzt — Status retroaktiv auf
✅ korrigiert (mit Hinweis auf 4J-Verfeinerung)
- Neuer „Gesamt-Status"-Block in `03-WEITERE-PHASEN.md`
mit Phasen-Tabelle, Sub-Päckchen-Tabelle und konkreten
Empfehlungen für die nächsten Schritte (Phase 5 Dark
Mode > Phase 6 Auth-Cleanup > Web-Frontend-Block als
eigene Roadmap)
- Erkenntnis: Das Mockup `Veröffentlichen Tailwind.html`
ist eine **Public-Facing-Landing-Page** von
businessportal24 (Web-Frontend), nicht ein
Backend-PM-Wizard. Gehört in den separaten
Web-Frontend-Block, nicht in Phase 4.
**Verifikationsstand**:
- 230/231 Tests grün (1 pre-existing
`ApiDocumentationTest`-Fail, unverändert seit 4D)
- Build (Portal) sauber: 424 KB CSS / 16 KB JS gzipped
- Pint sauber
- 16 Plan-Dokumente in `dev/frontend/hub-flux/`
(`00``16` + `PROGRESS.md` + `README.md`)
---
## 2026-05-20 · Phase 4J · Dashboard-PM-Listen mit 4H/4I-Patterns
- **Anlass**: Nach 4H/4I sahen die Voll-Listen exakt wie
das Mockup aus, aber die kompakten PM-Listen in den
Dashboards waren noch im alten Stand (Badge rechts,
Sub-Zeile ohne Portal). 4J schließt diese Inkonsistenz.
- **Befund**: Beide Dashboards waren bereits in Phase 1
großzügig Hub-styled (Page-Header, KPI-Reihe,
`<x-portal.stat-card>`, `<x-portal.hint-card>`,
`panel-dark` Brand-Bridge usw.) — die Roadmap-Status
„⚪ später" für Phase 2 + 3 waren also veraltet und
wurden im Zuge dieses Päckchens auf ✅ gesetzt.
- **Plan-Dokument**: `16-PHASE-4J-DASHBOARD-LISTS.md`.
### Customer-Dashboard (`customer/dashboard.blade.php`)
- Volt: `recent` Select um `portal` + `published_at`
erweitert
- `recent`-Liste:
- Portal-Pills (`pe`/`bp`) neben dem Badge (Both → beide
Pills)
- Sub-Zeile mit `PM-{id} · Firma · Datum`
- `published_at` als Primärdatum für veröffentlichte PMs
- Badge-Mapping erweitert (`muted` für archived/draft
statt `hub`)
### Admin-Dashboard (`admin/dashboard.blade.php`)
- `recentPRs`-Liste:
- Portal-Pills neben Badge (Both → beide Pills)
- Sub-Zeile mit `PM-{id} · Firma · User · Datum`
- Badge-Mapping mit `muted` für archived/draft
- `pendingReviews`-Liste (Review-Queue):
- Row-Tinting `is-row-warn` (gelblich) für alle Items
- Inline-Action „Prüfen →" rechts oben (Link zur
Show-Page, weil Admin-Dashboard Controller+Blade ist
und keine direkte Wire-Methode hat)
- Portal-Pills unter dem Titel
- Container von `<a>` zu `<div>` umgestellt, weil zwei
Links nebeneinander (Titel-Link + Inline-Action) sonst
HTML-invalid wären
- Hover-Underline auf Titel-Link für klare Klickbarkeit
### Tests/Verifikation
- `DashboardTest` (Customer) → 5/5 grün, 21 Assertions
- Volle Suite → 230/231 grün, 1 pre-existing Fail
(`ApiDocumentationTest`)
- `npm run build:portal` → grün (424 KB CSS, +0,2 KB)
- `vendor/bin/pint --dirty` → passed
### Dateien
- `dev/frontend/hub-flux/16-PHASE-4J-DASHBOARD-LISTS.md` (neu)
- `resources/views/livewire/customer/dashboard.blade.php`
- `resources/views/admin/dashboard.blade.php`
- `dev/frontend/hub-flux/PROGRESS.md` (dieser Eintrag)
- `dev/frontend/hub-flux/03-WEITERE-PHASEN.md` (4J +
Phase 2/3 retroaktiv auf ✅)
---
## 2026-05-20 · Phase 4I · Admin „Pressemitteilungen" auf Mockup-Patterns
- **Anlass**: Direktes Folgepäckchen zu 4H — die in der Customer-Liste
entwickelten Mockup-Patterns werden auf die zentrale
Admin-Pressemitteilungen-Liste übertragen (Editorial-Workflow).
- **Plan-Dokument**: `15-PHASE-4I-ADMIN-PRESS-RELEASES.md`.
### Volt-Komponente erweitert (`admin/press-releases/index.blade.php`)
- `pressReleaseStats()` um `rejected` + `archived` Counter
erweitert (zwei zusätzliche `selectRaw` im gleichen Statement,
weiter über `AdminPerformanceCache`)
- Neue Methoden: `setView($view)` (setzt Statusfilter +
resetPage), `resetFilters()` (kompletter Reset inkl.
Entity-Lookups)
### Markup auf Mockup-Stil
- **Saved-Views-Tabs**: 6 Tabs (Alle, In Prüfung,
Veröffentlicht, Entwürfe, Abgelehnt, Archiv) mit
Counter-Pillen, `is-active`-State spiegelt `$statusFilter`
- **Active-Chips** unter Filter-Panel: 8 Filter-Typen
(Suche, Status, Portal, Sprache, Kategorie, User, Firma,
Kontakt) mit Remove-Button + „Alle zurücksetzen"-Link
- **Tabelle umgestellt** (8 → 7 Spalten):
- Status + Inline-Action (Freigeben/Ablehnen für Review,
Archivieren für Published) — Inline-Actions sind
`flux:modal.trigger` für die bestehenden, test-kritischen
Modals
- Titel mit Sub-Zeile (PM-ID + Firma + Sprache, klickbar)
- Portal-Pills (presseecho/businessportal24, bei
`Portal::Both` beide nebeneinander)
- Kategorie (truncate)
- Datum mit Status-Kontext-Sub („veröffentlicht · HH:MM",
„eingereicht · HH:MM", …) und `published_at` als
Primärdatum wenn vorhanden
- Hits monospace
- Aktionen-Spalte rechts reduziert auf view + edit
(Status-Actions wandern in die Status-Zelle)
- **Row-Tinting**: review → `is-row-warn`,
rejected → `is-row-err`
- **Empty-State 2-stufig**: „mit Filter" → Reset-CTA,
„ohne Filter" → Anlegen-CTA
### Was unangetastet bleibt (Test-kritisch)
- `publish()`, `reject()`, `archive()`, `sort()`,
`with()`, alle `clearXFilter()`, `resetEntityFilters()`
- 3 Modals mit Strings „Pressemitteilung veröffentlichen?",
„Pressemitteilung ablehnen?", „Pressemitteilung
archivieren?" — werden weiter vom Markup gemountet,
jetzt zusätzlich via Inline-Action triggerbar
- Combobox-Lookups für User/Firma/Kontakt (Schema +
Verhalten unverändert)
### Pitfall + Fix
- `@php(...)` Inline-Form kompilierte zu `<?php($var = ...)`
ohne Whitespace → PHP-Syntax-Fehler trotz balancierter
Klammern. Workaround: alle drei Inline-`@php(...)` zu
Block-`@php ... @endphp` umgewandelt.
### Verifikation
- `npm run build:portal` → grün (424 KB CSS, +5 KB ggü. 4H)
- `vendor/bin/pint --dirty` → passed
- Gezielt: `AdminPressReleaseActionsTest` +
`PressReleaseWorkflowTest` → 16/16 grün, 52 Assertions
- Volle Suite: 230/231 grün, 1 pre-existing Fail
(`ApiDocumentationTest` — fehlende `docs/api/v1.yml`,
unverändert seit 4D), 3 skipped, 1212 Assertions
### Dateien
- `dev/frontend/hub-flux/15-PHASE-4I-ADMIN-PRESS-RELEASES.md` (neu)
- `resources/views/livewire/admin/press-releases/index.blade.php`
- `dev/frontend/hub-flux/PROGRESS.md` (dieser Eintrag)
- `dev/frontend/hub-flux/03-WEITERE-PHASEN.md` (4I-Eintrag)
---
## 2026-05-20 · Phase 4H · Customer „Meine Pressemitteilungen" auf Mockup
- **Anlass**: Nach Abschluss von 4F entschied sich der User
bewusst gegen die nächste Roadmap-Phase und für **detail-tief
in das wichtigste Customer-Arbeitstool** zu gehen. Vorlage:
`dev/frontend/tailwind_v3/User Pressemitteilungen presseportale.html`.
- **Plan-Dokument**: `14-PHASE-4H-PRESS-RELEASES-MOCKUP.md`.
### Hub-CSS erweitert (`resources/css/shared/hub-components.css`)
Neue Komponenten — alle aus Tokens, KEINE Hex-Literale:
- `.counter-strip` + `.seg` (mit `is-ok`, `is-warn`, `is-err`, `is-muted`) + `.sep`
- `.view-tabs` + `.view-tab` (mit `is-active`, `cnt`-Pille)
- `.filter-chip` (mit `is-active`, `caret`)
- `.active-chip` (mit `x`-Remove-Button)
- `.portal-pill` (mit `pe` + `bp` Dot-Varianten, nutzt
`--color-bridge-presseecho` / `--color-bridge-businessportal`)
- `.inline-action` (mit `warn` + `err` Varianten)
- `.is-row-warn` + `.is-row-err` (Row-Tinting via
`color-mix` aus Status-Soft-Token gegen Card-Background)
- `.empty-stage` + `.empty-ico` (mit `warm` + `err` Varianten)
+ `.empty-title` + `.empty-sub`
- `.badge.muted` (neue Variante neben `ok`, `warn`, `err`, `hub`)
### Volt-Komponente erweitert (`customer/press-releases/index.blade.php`)
- Aggregierte `statusCounts` (assoc) per `groupBy('status')`
über dieselbe `$base`-Query — eine zusätzliche SQL-Query
- Neue Properties: `$portalFilter`
- Neue Methoden: `setView($status)`, `resetFilters()`,
`updatedPortalFilter()`
- Daten an View: `statusCounts`, `portalOptions`
- Paginierung von 100 → 25 (sinnvoller für Mockup-Layout)
### Markup auf Mockup-Stil (~50 % → ~90 %)
- **Counter-Strip im Header**: dynamische Segmente werden nur
angezeigt, wenn Count > 0, mit semantisch eingefärbten
Zahlen (ok/warn/muted/err)
- **Kontext-Hinweis**: „Gefiltert auf :company" als kleine
Sub-Zeile, falls Firma-Kontext aktiv (Test-kompatibel!)
- **Saved-Views-Tabs**: 6 Tabs (Alle, Veröffentlicht,
Entwürfe, In Prüfung, Abgelehnt, Archiv) mit `cnt`-Pille,
klicken setzt `statusFilter` via `setView()`
- **Filter-Sektion**: Suche + Portal-Select + Firma-Select
(letzteres nur wenn `hasGlobalCompanyContext`)
- **Active-Chips**: entfernbare Anzeige aller aktiven Filter
(Status, Portal, Firma, Suche), mit `Alle zurücksetzen`-Link
- **Tabelle**:
- Status-Spalte mit Hub-Badge + Inline-Action „Zur Prüfung →"
für Drafts (ruft `submitForReview` mit `wire:confirm`)
- Titel mit Hub-Style-Link + PM-{id}-Sub
- Portal-Pills (eine oder zwei, abhängig vom Portal-Enum-Wert)
- Firma als Hub-Link oder „— keine —" italic
- Datum: Mono-Font + Sub mit Status-Kontext
(„veröffentlicht · 09:12", „eingereicht · 17:48" …)
- Row-Tint via `is-row-warn` (review) / `is-row-err`
(rejected) — dezent beim Hover
- **3-fach Empty-State**:
- `empty-search` (Suche ohne Treffer)
- `empty-filter` (Filter ohne Treffer, warm-Icon-Box)
- `empty-none` (gar keine PMs, mit 3-Schritt-Onboarding
01-Firma · 02-Verfassen · 03-Zur Prüfung)
- **Status-Aktionen-Legende**: `panel-warm` mit 5-Spalten-
Grid (Entwurf, In Prüfung, Veröffentlicht, Abgelehnt,
Archiviert) und einer Action-Liste je Status
### Was bewusst NICHT umgesetzt wurde
- **Bulk-Selection**: weggelassen (keine Bulk-Actions im
Backend → wäre nur Dekoration ohne Funktion)
- **Inline-Action „Grund ansehen →"** für Rejected: keine
`rejection_reason`-Spalte in `press_releases`-Tabelle
(Grund wird nur via E-Mail an Autor versendet)
- **Spalten- & Export-Buttons** im Header: keine Backend-
Implementierung, weggelassen
- **Modals und Bestätigungs-Flows**: bleiben FluxUI
- **Volt-Logik außerhalb der Erweiterungen**: unangetastet
### Tests & Hygiene
- **Erst-Fail**: `CustomerCompanyContextTest:79` rot, weil
„Gefiltert auf Alpha GmbH" im neuen Layout vom Counter-Strip
verdrängt wurde
- **Fix**: Kontext-Hinweis als zusätzliche kleine Sub-Zeile
unter dem Counter-Strip, immer sichtbar wenn Firma-Kontext
- **PR + Customer Tests**: 50/50 grün (199 Assertions)
- **Volle Suite**: 230 grün (1212 Assertions), 3 skipped,
1 fail = pre-existing `ApiDocumentationTest` (kein Bezug zu UI)
- **Build**: `npm run build:portal` clean
- **Pint**: `vendor/bin/pint --dirty``passed`
- **Lints**: keine Errors
### Status
- Plan-Status: ✅ Phase 4H **abgeschlossen**
- 03-WEITERE-PHASEN.md: 4H ergänzt als ✅
- Match zum Mockup: ≥ 90 %
### Übertragbar auf
- `admin/press-releases/index.blade.php` (Phase 4I als
natürliche Folgephase)
- `admin/users.blade.php`, `admin/contacts/index.blade.php`,
`admin/companies/index.blade.php` (Counter-Strip,
Saved-Views-Tabs, Filter-Chips wären überall sinnvoll)
---
## 2026-05-20 · Phase 4F · Restliche Admin-Bereiche
- **Anlass**: User „4f weiter" nach 4G. Siebtes (und letztes
großes) Päckchen aus Phase 4. Ziel: alle übrigen Admin-Pages
auf Hub-Stil, damit die komplette interne Strecke visuell
abgeschlossen ist.
- **Plan-Dokument**: `13-PHASE-4F-ADMIN-REST.md`.
Aufgrund des Umfangs (~7.500 Z. Blade) in 4 Sub-Päckchen
aufgeteilt.
### 4F-1 · Stammdaten & Switcher
- **`admin/presets/{index,create,edit}.blade.php`** +
`partials/form-fields.blade.php`: Page-Header
(„Admin Backend"-Pille, Eyebrow „Administration · Presets"),
Filter-Panel, Table-Panel mit Hub-Badges und Hub-Empty-State,
Forms in `article.panel`, Required-Marker auf
`text-[color:var(--color-err)]`.
- **`admin/categories/{index,create,edit}.blade.php`**:
Page-Header mit Aktion, KPI-Reihe mit
`<x-portal.stat-card>` (gesamt, aktiv, mit/ohne PMs),
Filter & Sort Panel, Category-Cards als `article.panel`,
Hub-Badges, Hub-Style innere Items, Danger-Zone mit linkem
roten Strip.
- **`admin/portal-switcher.blade.php`** (Sidebar):
Token-basierte Button-Farben mit `--color-bg-elev`,
`--color-bg-rule`, `--color-ink`, `--color-ink-2`,
`--color-ink-3`.
- **Tests**: `CategoryIndexPerformanceTest`,
`AdminCategoryManagementTest`,
`AdminPresetManagementTest` — alle grün.
### 4F-2 · Pressekontakte
- **`admin/contacts/index.blade.php`** (729 Z.): Page-Header
mit Action, KPI-Reihe (`<x-portal.stat-card>`),
Filter-Panel, Preset-Panel, Table-Panel mit Hub-Badges und
Hub-Empty-State, Hub-Style Flash-Boxen.
- **`admin/contacts/create.blade.php`** (275 Z.):
Page-Header mit „prefilled company"-Badge, Forms in
`article.panel`.
- **`admin/contacts/edit.blade.php`** (352 Z.):
Page-Header mit ID + Portal-Badge, Forms in
`article.panel`, Danger-Zone mit linkem roten Strip.
### 4F-3 · Operations & Finance
- **`admin/footer-codes/{index,create,edit}.blade.php`**:
Page-Header, KPI-Reihe, Filter-Panel, Table-Panel mit
Hub-Badges, Forms in `article.panel`, Category-Checkboxes
Hub-styled, Danger-Zone.
- **`admin/reports/slow-requests.blade.php`** +
`slow-requests-table.blade.php`: Page-Header,
Filter-Panel mit Reset-Button, KPI-Reihe, Top Routes/Paths
Panels, Slowest-Requests-Table, Frequent Slow Queries Table,
EXPLAIN Top Slow Queries Panel — alles Hub-styled.
- **`admin/invoices/index.blade.php`** (Legacy-Archiv):
Page-Header mit Archive-Badge, Warning-Box für „unmapped",
KPI-Reihe, Filter-Panel, Table-Panel mit Hub-Badges +
Pagination.
- **`admin/coupons/index.blade.php`** (Stub): Page-Header
„Vertagt"-Badge, Info-Panel mit Hub-Style List-Items.
- **`admin/payments/index.blade.php`** (Stub): Page-Header
„In Vorbereitung"-Badge, Info-Panel mit Hub-Style
List-Items + Rules.
- **Tests**: `AdminFooterCodeManagementTest`,
`AdminSlowRequestReportTest`,
`AdminSlowRequestLoggingTest`,
`AdminLegacyInvoiceArchiveTest` — alle grün.
### 4F-4 · User-Verwaltung
- **`admin/roles/{index,create,edit}.blade.php`**:
Page-Header (mit ID + System-Role-Badges in edit),
Warning-Box für System-Rollen, Forms in `article.panel`,
Table-Panel mit Hub-Badges + Empty-State.
- **`admin/newsletter/sync.blade.php`** (171 Z.):
Page-Header mit Sync-Status-Badge (active/deactivated),
Action-Buttons (Dry Run, Test-Sync) im Header, Hub-Style
Info/Success-Pillen für Notifications, KPI-Reihe mit
`<x-portal.stat-card>` (gesamt, bestätigt, ausstehend,
abgemeldet), Config-Details (Provider, Timeout, Endpoint)
als Definition-List in `article.panel`.
- **`admin/users.blade.php`** (939 Z.): Page-Header mit
Action „Benutzer anlegen", KPI-Reihe (gesamt, aktiv,
inaktiv), Filter-Panel mit Reset-Button, Table-Wrapper
als `article.panel` mit Hub-Empty-State. **Modal**
und alle Tabellen-Inhalte bewusst unangetastet — wegen
Test-Strings + Komplexität.
- **`admin/users/show.blade.php`** (239 Z.): Hub-Header mit
ID + Status-Badges, KPI-Reihe (Portal, Typ, Status, Login),
Rollen-Panel, Rechnungsadresse-Panel,
„Verknüpfte Firmen & Kontakte"-Panel als verschachtelte
Hub-Boxes.
- **`admin/users/create.blade.php`** (421 Z.): Hub-Header,
Forms in `article.panel` (Basisdaten, Rollenzuweisung,
Firmenverknüpfung, Rechnungsadresse), Action-Panel
am Ende.
- **`admin/users/edit.blade.php`** (1.224 Z.): Hub-Header
mit ID/Portal/Status-Badges, Notification als
Hub-Style Ok-Pill, Quick-Nav als Token-Pillen mit
Hover auf Hub-Blue, KPI-Reihe (Account, Legacy-Profil,
Verknüpfungen, Rechnungsadresse). Innere Sektionen
(`#account`, `#legacy-profile`, `#roles`,
`#company-links`, `#contact-links`, `#billing-address`)
bleiben `flux:card` (IDs für Quick-Nav, sehr großes File).
Footer-Action-Panel auf Hub umgestellt.
- **Tests**: `UserManagementTest`, `UserImpersonationTest`,
`UserList*Test` — 29 Tests / 270 Assertions grün.
### Bauplatz-Hygiene
- **Build**: `npm run build:portal` — sauber durch.
- **Pint**: `vendor/bin/pint --dirty``passed`.
- **Lints**: keine Linter-Errors in den editierten Blade-Files.
- **Pre-existing**: `ApiDocumentationTest` weiterhin rot, kein
Bezug zu UI-Styling (siehe frühere Phasen).
### Abgrenzung — was bewusst NICHT angefasst wurde
- Volt-Logik in allen Dateien (PHP, `mount`, `save`,
`with`, …).
- `<flux:input>`, `<flux:select>`, `<flux:checkbox>`,
`<flux:textarea>`, `<flux:field>`, `<flux:error>`,
`<flux:table.*>`, `<flux:button>`, `<flux:modal>`.
- Innere Sektionen von `users/edit.blade.php` mit ID-Ankern
(Quick-Nav-Targets) — diese bleiben `flux:card`, da
Footnote-Logik daran hängt und das File 1.224 Z. groß ist.
- Modal in `users.blade.php` und alle internen Bestätigungs-
Flows (Test-Strings).
- Test-relevante Strings überall.
### Status
- Plan-Status: ✅ Phase 4F **abgeschlossen**.
- 03-WEITERE-PHASEN.md: 4F auf ✅ aktualisiert.
---
## 2026-05-20 · Phase 4G · Customer Portal (Mein Bereich)
- **Anlass**: User „weiter 4G" nach 4E. Sechstes Päckchen aus
Phase 4: alle übrigen Customer-Pages auf Hub-Stil bringen,
damit die komplette User-sichtbare Strecke (Mein Bereich)
visuell abgeschlossen ist.
- **Plan-Dokument**: `12-PHASE-4G-CUSTOMER-PORTAL.md`.
- **Was umgebaut wurde**:
- **`customer/company-switcher.blade.php`** (Sidebar-Helper,
91 Z.): „Aktive Firma"-Label als `.badge hub dot`,
Portal-Suffix als Token-Eyebrow, „Keine Firma zugeordnet"
als `.badge warn`. `<flux:select>`, `Firma öffnen`-Button
und `route('me.press-kits.show', …)` bleiben — alles, was
der `CustomerCompanyContextTest` assertiert, ist erhalten.
- **`customer/bookings.blade.php`** (Coming-Soon, 52 Z.):
Hub-Page-Header („User Backend"-Pille,
„Mein Bereich · Finanzen"-Eyebrow, Warn-Pille
„In Vorbereitung"). Info-Box auf Hub-Soft-Token,
3 Feature-Panels mit Eyebrow + Beschreibung.
- **`customer/invoices.blade.php`** (194 Z.): Page-Header
mit Aktion „Rechnungsadresse im Profil pflegen". Hinweis
in eigenes `.panel`. 4 KPI-Cards (primary/muted/ok/warn).
Filter-Panel, Tabellen-Panel mit Hub-Badges (`ok dot`
für bezahlt, `warn dot` für offen), Empty-State als
Icon-Box. Pagination am Boden im Panel mit Top-Border.
Notification als Hub-Warn-Pille mit Icon.
- **`customer/tokens.blade.php`** (212 Z.): Hub-Page-Header
mit „API-Dokumentation"-Aktion. Flash-Boxen (success/warn)
in Token-Style. „Neuer Token"-Anzeige als eigenes Panel
mit linkem Warn-Strip (`border-left:3px solid
--color-warn`) + `.badge warn`-„Nur jetzt sichtbar". Form
in Panel mit Border-Top-getrenntem Submit-Bereich.
Tabelle in Panel mit Counter, Berechtigungen als
`.badge hub` (statt `flux:badge`), Empty-State als
Icon-Box.
- **`customer/press-kits/index.blade.php`** (119 Z.):
Hub-Page-Header. Filter-Panel. Karten-Grid mit
`.panel`-Karten: `.panel-head` mit Status-Badge
(`ok dot` / `err dot`), Body mit Slug, Portal/Rolle/
Footer-Badges, KPI-Boxen (Border + bg-elev), Footer
mit Border-Top + „Firma öffnen"-Button.
Empty-State als full-width Panel mit Hub-Icon-Box.
- **`customer/press-kits/show.blade.php`** (734 Z. → Polish):
Großes Detail-Cockpit komplett auf Hub:
- **Header**: Pillen-Reihe (User-Backend, Mein
Bereich · Firma, Aktiv/Inaktiv, Portal, Rolle, ggf.
Footer-Code-aus) + Logo-Box (oder Hub-Icon-Box) + H1
+ Slug. Aktionen rechts: Zurück, Stammdaten bearbeiten,
Neue PM.
- **Quick-Nav**: Anker-Links als pillen-artige
Hover-Buttons mit Bottom-Border.
- **4 KPI-Cards**: PMs (primary), Pressekontakte (ok),
Portal (muted), Deine Rolle (muted) — mit
`canManageCompany`-abhängiger Trend.
- **Stammdaten-Panel**: `.panel-head` mit Bearbeiten-
Button. Inline-Form als `bg-elev`-Block mit
Border-Top-getrennten Sections (Logo / Aktionen).
Daten-Liste als `<dl>` mit Token-Labels + Werten,
Website als Hub-Underline-Link.
- **Pressekontakte-Panel**: `.panel-head` mit Counter.
Inline-Form analog Stammdaten. Kontakt-Items als
`bg-elev`-Boxen mit Aktionen rechts. Empty-State als
gestrichelte Box. Flash-Messages
(„Pressekontakt wurde angelegt./aktualisiert./
gelöscht.") in Token-Success-Pille.
- **Pressemitteilungen-Panel**: Tabelle mit Hub-Status-
Badges (ok/warn/err/hub). Empty-State Hub-Icon-Box.
- **Abrechnung & Statistik**: 2 Panels nebeneinander
mit „In Vorbereitung"/„Später"-Warn-Pillen. Abrechnung
mit gestrichelter Hint-Box. Statistik mit
2 Token-Mini-Stats.
- **Was bewusst unverändert blieb**:
- Volt-PHP-Logik in allen Dateien (mount, saveCompany,
saveContact, deleteContact etc.).
- Flash-Strings („Stammdaten wurden gespeichert.",
„Pressekontakt wurde angelegt./aktualisiert./gelöscht.",
„Token wurde erstellt", „Token wurde widerrufen.",
„API-Tokens werden erst freigeschaltet" etc.) —
bleiben in Volt-PHP wie zuvor und erscheinen in den
neuen Hub-Pillen.
- Section-Headings „Abrechnung" und „Statistik" — Tests
assertieren genau diese Strings.
- Strings im Header („Alpha GmbH", „Firma öffnen",
`route('me.press-kits.show', …)`).
- FluxUI-Form-Komponenten (`flux:input`, `flux:select`,
`flux:textarea`, `flux:checkbox`, `flux:field`,
`flux:error`, `flux:table*`) — nur Wrapper-Markup
verändert.
- **Verifikation**:
- `php artisan view:clear`
- `npm run build:portal` ✓ — 418.11 kB CSS / 44.38 kB JS
(keine Größenänderung gegenüber 4E).
- `php artisan test --compact tests/Feature/{
CustomerPortalTest, CustomerCompanyContextTest,
PanelConsolidationTest}.php` →
**29 passed, 131 assertions**.
- Volle Suite: **230 passed, 3 skipped, 1 pre-existing
fail** (`ApiDocumentationTest` — fehlende
`docs/api/v1.yml`, war auch vor 4G/4E/4D rot).
- `vendor/bin/pint --dirty --format agent``passed`.
- **Resultat**:
- Mein-Bereich des Users ist visuell **vollständig** auf
Hub-Sprache.
- Sidebar-Nav (presse-kits, invoices, tokens, bookings)
läuft jetzt durchgängig im selben Vokabular wie Dashboard
+ Press-Releases.
- Detail-Cockpit `press-kits/show` ist das visuell
aufwendigste Customer-Page und stimmt jetzt mit Admin-
Detail-Show überein (Pillen-Header, KPI-Reihe, Panels,
Quick-Nav, Tabellen).
- **Nächste mögliche Päckchen**:
- **4F** Restliche Admin-Bereiche (contacts, categories,
presets, footer-codes, slow-requests, users).
- **press-release-images-manager** Komponente (kleiner
Cleanup aus 4C-Resten).
- **Phase 5** Dark-Mode-Konsolidierung.
---
## 2026-05-20 · Phase 4E · Profile & Settings
- **Anlass**: User „okay weiter" nach 4D. Fünftes Päckchen aus
Phase 4: alle Settings-Pages (Admin + Customer) auf Hub-Stil.
- **Plan-Dokument**: `11-PHASE-4E-PROFILE-SETTINGS.md`.
- **Zentraler Trick — Wrapper umgebaut**:
- **`resources/views/partials/settings-heading.blade.php`**:
Komplett neu als Hub-Page-Header (Pille „Admin Backend",
Eyebrow „Mein Konto · Einstellungen", H1 „Settings",
Subtitle).
- **`resources/views/components/settings/layout.blade.php`**:
Komplett neu als 2-Spalten-Grid mit Sidebar-Nav-`.panel`
(„Mein Konto"-Eyebrow + FluxUI-Navlist drin) und
Content-`.panel` mit `.panel-head` für Page-Heading +
Subheading im Body. Heading/Subheading-Slots der einzelnen
Settings-Pages werden so automatisch im Hub-Look gerendert.
- **Effekt**: `settings/appearance.blade.php` musste
**gar nicht** angefasst werden — übernimmt den Hub-Look
via Wrapper. Die anderen beiden brauchen nur
Save-Bar-Polish.
- **Was umgebaut wurde**:
- **`settings/profile.blade.php`**: Verification-Hinweis von
`<flux:text>`-Link auf Hub-Warn-Box (gelber Linker Strip,
Icon, Hub-Link für „re-send the verification email").
Save-Bar mit Border-Top + Token-`Saved.`-Pill.
Delete-Button-Section visuell durch Border-Top abgetrennt.
- **`settings/password.blade.php`**: Save-Bar gleicher Polish.
- **`settings/delete-user-form.blade.php`**: Das `mt-10`-Card
durch Hub-Danger-Box mit `border-l-[3px]
border-l-[color:var(--color-err)]`, „Danger Zone"-Eyebrow,
H3 + Subtitle + Trash-Button. Modal-Markup komplett
unverändert (Confirm-Password-Modal kommt aus FluxUI-Std.).
- **`customer/profile.blade.php`**: Page-Header („User
Backend"-Pille, „Mein Bereich · Profil"-Eyebrow,
„Mein Profil"-H1). Form in 3 Panels + Aktions-Panel:
Konto (Name/E-Mail/Sprache) · Profil (Anrede,
Vor-/Nachname, Telefon, Backlink, Checkboxen) ·
Rechnungsadresse (mit Warn-Hub-Box, wenn unvollständig).
Zugeordnete-Firmen als eigenes `.panel` mit Hub-Badges
(`ok` für Eigentümer, `hub` für Portal/Rolle).
- **`customer/security.blade.php`**: Page-Header. 4-spaltige
KPI-Reihe mit `.panel` + uppercase-Eyebrow + großer Wert +
Hub-Badge (E-Mail bestätigt? · 2FA aktiv? · Letzter Login +
IP · Sessions-Count). 2-Spalten-Grid für Passwort + E-Mail
in `.panel` mit `.panel-head`. 2FA-`.panel` mit
ok-Badge im Header wenn aktiv, Recovery-Codes mit
Hub-Bg-Boxes. Sessions-`.panel` mit Eintrags-Counter,
Empty-State als Hub-Icon-Box. **Alle Test-Strings
erhalten**: „Konto-Sicherheit", „Letzter Login",
„Aktive Sessions", „Passwort ändern", „E-Mail-Adresse
ändern", „Zwei-Faktor-Authentifizierung",
„Rechnungsadresse".
- **Tests**:
- Smoke: `Settings|CustomerProfileSecurity|
CustomerCompanyContext` → 33 passed (146 assertions).
- Volle Suite: 230 passed / 3 skipped /
1 pre-existing `ApiDocumentationTest`-Fail.
- **Build / Pint**: `npm run build:portal` ✓ (418 kB CSS),
`pint --dirty` ✓, keine Linter-Errors.
- **Bewusst NICHT angefasst**:
- Volt-Logik in allen Dateien.
- Confirm-User-Deletion-Modal-Markup
(`settings/delete-user-form.blade.php`).
- Fortify-2FA-Logik (Enable / Disable / Recovery-Codes-
Regenerate / QR-Generation).
- `<x-action-message>` Component-Wrapper — nur Slot-Inhalt.
- **Nächster Schritt** — Vorschlag:
- **4G** Customer-Bereiche (invoices, tokens, bookings,
company-switcher, me/press-kits/*) — Tests fordern
Strings, aber alle gut greifbar.
- **4F** Restliche Admin-Bereiche (contacts, categories,
presets, footer-codes, slow-requests, users) — größeres
Päckchen, sollte weiter unterteilt werden.
---
## 2026-05-20 · Phase 4D · Companies (admin)
- **Anlass**: User „okay weiter" nach 4C. Viertes Päckchen aus
Phase 4: alle vier Admin-Companies-Pages auf Hub-Stil.
- **Plan-Dokument**: `10-PHASE-4D-COMPANIES.md`.
- **Was umgebaut wurde**:
- **`admin/companies/index.blade.php`**: Hub-Page-Header
(„Admin Backend"-Pille, Eyebrow „Stammdaten · Firmen",
H1 „Firmen", Subtitle, CTA „Neue Firma" rechts) →
3 `<x-portal.stat-card>` (Gesamt/Aktiv/Inaktiv) →
Filter-`.panel` mit Combobox-Selects unverändert in der
Logik, nur Hülle getauscht → Tabelle in `.panel
overflow-hidden` mit `.panel-head` „Alle Firmen" +
Eintrags-Counter. Tabellen-Status/Portal/Count-Badges
auf Hub-Klassen (`badge ok|err|hub` + Dot) gesetzt.
Empty-State als Hub-Icon-Box.
- **`admin/companies/show.blade.php`**: Header mit
Status-, Portal- und ID-Pille; Logo-Box neben H1. KPI-Reihe
(3 stat-cards). Tabs „Überblick" / „Kontakte" als schlankes
Bottom-Border-Pattern (kein FluxUI-Tabs-Group nötig). Beide
Tab-Inhalte komplett in `.panel`-Sektionen mit `dl`-Layouts
bzw. Kontakt-Karten. Existing-Contact-Combobox bleibt
FluxUI; Wrapper auf Hub-`bg-elev`. Flash-Boxen
(success/error/info) auf Token-Pillen.
- **`admin/companies/edit.blade.php`**: Page-Header
(„ID"-Pille), 5 Form-Panels (Basisinformationen, Adresse,
Rechtliche Daten, Logo & Status, Aktionen). Required-Marker
auf `text-[color:var(--color-err)]`. Logo-Vorschau-Bild
nutzt Hub-Token-Border. „Logo wird beim Speichern entfernt"
auf Hub-Warn-Box (statt `flux:callout`).
**Delete-Confirm-Modal komplett unverändert.**
- **`admin/companies/create.blade.php`**: gleiches Schema
wie Edit, ohne Status-Pille und ohne Delete-Button. Forms
in 5 Panels strukturiert.
- **Tests**:
- Companies-Smoke: `UserManagement|PortalAssetManifest`
→ 25 passed (227 assertions).
- Volle Suite: 230 passed / 3 skipped /
1 vorbestehender Fail (`ApiDocumentationTest`
fehlende Datei `docs/api/v1.yml`, nicht durch Phase 4D
verursacht).
- **Build / Pint**: `npm run build:portal` ✓ (418 kB CSS,
44 kB JS), `vendor/bin/pint --dirty --format agent` ✓.
- **Bewusst NICHT angefasst**:
- Volt-Logik in allen vier Dateien.
- `deleteCompany`-Methode und ihr Confirm-Modal-Markup
(Test `UserManagementTest::admin can delete company`
prüft Redirect-Verhalten, nicht UI).
- `<flux:select variant="combobox">` (Kontakt-/User-Lookup)
und alle anderen `flux:field`/`flux:input`-Bausteine —
Token-Bridging trägt.
- Test-relevante Strings: „Firmen", „Portal", „Alle Portale",
„Presseecho", „Businessportal24", „/admin/companies/…".
- **Nächster Schritt (Phase 4E oder 4F)**:
- **4E** = Profile/Settings (`settings.*`, `me.profile`,
`me.security`) — eher klein, viele kleine Panels.
- **4F** = Restliche Admin-Bereiche (contacts/, categories/,
presets/, footer-codes/, slow-requests, …) — Päckchen-Größe.
---
## 2026-05-19 · Phase 4C · Press-Releases Forms (create / edit)
- **Anlass**: User-Freigabe „ja" nach 4B. Drittes Päckchen aus

View file

@ -0,0 +1,976 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>presseportale.com — Neue Pressemitteilung</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bg:"#F6F4EF","bg-elev":"#FBFAF6","bg-rule":"#E2DDD0","bg-rule-2":"#EDE7D7",
"bg-card":"#FFFFFF","bg-card-warm":"#EFEADC",
hub:"#1A2540","hub-2":"#243152","hub-3":"#2E3D66",
"hub-soft":"#E5E9F1","hub-soft-2":"#CFD6E4","hub-line":"#7B8FBF",
accent:"#B07A3A","accent-deep":"#8A5E27","accent-soft":"#F1E6D3",
ink:"#1A1F1C","ink-2":"#3A413D","ink-3":"#5A6360","ink-4":"#8A918D",
"ink-on-dark":"#F6F4EF","ink-on-dark-2":"#B2B9C7","ink-on-dark-3":"#7B8FBF",
ok:"#2E8540","ok-soft":"#E2F1E5",
warn:"#A87A1F","warn-soft":"#F6EAC8",
err:"#A8331F","err-soft":"#F4DAD2",
},
fontFamily:{
sans:['"Inter Tight"','Inter','system-ui','sans-serif'],
serif:['"Source Serif 4"','Georgia','serif'],
mono:['"JetBrains Mono"','"SF Mono"','ui-monospace','monospace'],
},
}
}
};
</script>
<style>
html,body{margin:0;padding:0;}
body{background:#E8E4DA;font-family:"Inter Tight",system-ui,sans-serif;}
.eyebrow{font-size:10.5px;font-weight:700;letter-spacing:.20em;text-transform:uppercase;color:#1A2540;}
.eyebrow.muted{color:#5A6360;letter-spacing:.16em;font-weight:600;font-size:10px;}
.eyebrow.accent{color:#8A5E27;}
.eyebrow.on-dark{color:#7B8FBF;}
.rule{height:1px;background:#E2DDD0;border:0;margin:0;}
/* Sidebar */
.nav-item{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:4px;font-size:13px;font-weight:500;color:#3A413D;transition:background .12s,color .12s;position:relative;}
.nav-item:hover{background:#F6F4EF;color:#1A2540;}
.nav-item.active{background:#E5E9F1;color:#1A2540;font-weight:600;}
.nav-item.active::before{content:"";position:absolute;left:-1px;top:6px;bottom:6px;width:2px;background:#1A2540;border-radius:0 2px 2px 0;}
.nav-item.disabled{color:#8A918D;cursor:default;}
.nav-item.disabled:hover{background:transparent;color:#8A918D;}
.nav-item .ico{width:16px;height:16px;flex-shrink:0;color:currentColor;opacity:.8;}
.nav-item.active .ico{opacity:1;}
.nav-section{font-size:10px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#8A918D;padding:0 12px 6px;}
/* Panels */
.panel{background:#FFFFFF;border:1px solid #E2DDD0;border-radius:6px;}
.panel-warm{background:#FBFAF6;border:1px solid #E2DDD0;border-radius:6px;}
/* Buttons */
.btn-primary{display:inline-flex;align-items:center;gap:8px;justify-content:center;padding:10px 16px;background:#1A2540;color:#fff;border-radius:4px;font-size:13px;font-weight:600;transition:background .15s;}
.btn-primary:hover{background:#243152;}
.btn-primary.full{width:100%;padding:11px 16px;}
.btn-secondary{display:inline-flex;align-items:center;gap:8px;justify-content:center;padding:8px 14px;background:#fff;color:#1A2540;border:1px solid #CFD6E4;border-radius:4px;font-size:12.5px;font-weight:600;transition:border-color .15s,background .15s;}
.btn-secondary:hover{border-color:#1A2540;background:#F6F4EF;}
.btn-secondary.full{width:100%;}
.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;color:#3A413D;border-radius:4px;font-size:12px;font-weight:500;transition:background .12s,color .12s;}
.btn-ghost:hover{background:#F6F4EF;color:#1A2540;}
/* Badges */
.badge{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border-radius:99px;font-size:10.5px;font-weight:700;letter-spacing:.10em;text-transform:uppercase;}
.badge.warn{background:#F6EAC8;color:#8A5E27;}
.badge.ok{background:#E2F1E5;color:#1F5E2E;}
.badge.err{background:#F4DAD2;color:#8E2A19;}
.badge.hub{background:#E5E9F1;color:#1A2540;}
.badge.muted{background:#EFEADC;color:#5A6360;}
.badge.dot::before{content:"";width:6px;height:6px;border-radius:99px;background:currentColor;}
/* BALD-Badge */
.badge-bald{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:99px;font-size:9.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;background:#F1E6D3;color:#8A5E27;border:1px dashed #E1C883;}
.badge-bald::before{content:"";width:4px;height:4px;border-radius:99px;background:#B07A3A;}
/* Bridge ribbon */
.bridge-row{display:inline-flex;align-items:center;gap:6px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;letter-spacing:.10em;text-transform:uppercase;color:#5A6360;}
.dot-pe{width:6px;height:6px;border-radius:99px;background:#1F4D3A;}
.dot-bp{width:6px;height:6px;border-radius:99px;background:#C84A1E;}
/* Editor wrapper */
.field-label{display:flex;align-items:center;gap:8px;font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#5A6360;margin-bottom:8px;}
.field-label .req{color:#A8331F;font-weight:700;letter-spacing:0;}
.field-help{font-size:11.5px;color:#8A918D;margin-top:6px;line-height:1.5;}
.field-help.warn{color:#8A5E27;}
.field-counter{font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;color:#8A918D;font-variant-numeric:tabular-nums;}
/* Inputs */
.inp{
width:100%;padding:10px 12px;background:#fff;border:1px solid #CFD6E4;border-radius:4px;
font-size:13px;color:#1A1F1C;transition:border-color .15s,box-shadow .15s;font-family:inherit;
}
.inp:focus{outline:none;border-color:#1A2540;box-shadow:0 0 0 3px rgba(26,37,64,.07);}
.inp::placeholder{color:#8A918D;}
.inp.title{font-size:24px;font-weight:700;letter-spacing:-.5px;line-height:1.2;padding:14px 16px;color:#1A1F1C;font-family:"Source Serif 4",Georgia,serif;}
.inp.title::placeholder{color:#B5BCB9;font-weight:400;}
.inp.subtitle{font-size:15px;font-weight:500;line-height:1.4;padding:11px 14px;color:#3A413D;font-family:"Source Serif 4",Georgia,serif;}
.inp.subtitle::placeholder{color:#B5BCB9;font-weight:400;}
.inp.small{padding:7px 10px;font-size:12.5px;}
.select{
width:100%;padding:8px 30px 8px 12px;background:#fff;border:1px solid #CFD6E4;border-radius:4px;
font-size:12.5px;color:#1A1F1C;appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 4.5l3 3 3-3' stroke='%235A6360' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 10px center;cursor:pointer;
}
.select:focus{outline:none;border-color:#1A2540;}
/* Toolbar / editor */
.toolbar{display:flex;align-items:center;gap:2px;padding:7px 10px;border-bottom:1px solid #E2DDD0;background:#FBFAF6;border-radius:4px 4px 0 0;flex-wrap:wrap;}
.tb-btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 7px;border-radius:3px;font-size:12.5px;font-weight:600;color:#3A413D;border:1px solid transparent;transition:background .12s,color .12s,border-color .12s;}
.tb-btn:hover{background:#fff;border-color:#CFD6E4;color:#1A2540;}
.tb-btn.is-active{background:#1A2540;color:#fff;}
.tb-btn .lbl{font-size:11px;letter-spacing:.04em;}
.tb-sep{width:1px;height:18px;background:#E2DDD0;margin:0 4px;}
.editor-body{padding:18px 20px;min-height:340px;font-family:"Source Serif 4",Georgia,serif;font-size:15px;line-height:1.65;color:#1A1F1C;}
.editor-body h2{font-family:"Source Serif 4",Georgia,serif;font-size:18px;font-weight:700;color:#1A1F1C;margin:18px 0 8px;letter-spacing:-.2px;}
.editor-body p{margin:0 0 12px;}
.editor-body p.lede{font-weight:500;font-size:16px;color:#1A1F1C;}
.editor-body strong{color:#1A1F1C;font-weight:700;}
.editor-body em{font-style:italic;}
.editor-body ul{margin:6px 0 14px;padding-left:22px;}
.editor-body ul li{margin:4px 0;}
.editor-body a{color:#8A5E27;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;text-decoration-color:rgba(138,94,39,.45);}
.editor-cursor{display:inline-block;width:1px;height:18px;background:#1A2540;vertical-align:-3px;animation:blink 1s steps(2) infinite;margin-left:1px;}
@keyframes blink{50%{opacity:0;}}
/* Media tiles */
.media-tile{position:relative;border:1px solid #E2DDD0;border-radius:5px;background:#FBFAF6;overflow:hidden;}
.media-thumb{aspect-ratio:16/10;background:#EDE7D7;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;}
.media-meta{padding:9px 11px;}
.media-cap{font-size:11.5px;color:#3A413D;line-height:1.4;}
.media-alt{font-size:10.5px;color:#8A918D;margin-top:3px;font-style:italic;line-height:1.4;}
.media-cover-flag{position:absolute;top:8px;left:8px;}
.media-actions{position:absolute;top:8px;right:8px;display:flex;gap:4px;}
.media-act{width:24px;height:24px;border-radius:3px;background:rgba(26,31,28,.62);color:#fff;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);}
.media-act:hover{background:rgba(26,37,64,.85);}
/* Drop zone */
.dropzone{border:1.5px dashed #CFD6E4;border-radius:5px;background:#FBFAF6;padding:18px 16px;display:flex;align-items:center;justify-content:center;gap:14px;text-align:left;transition:border-color .15s,background .15s;}
.dropzone:hover{border-color:#1A2540;background:#F6F4EF;}
.dropzone .dz-ico{width:42px;height:42px;border-radius:5px;background:#E5E9F1;color:#1A2540;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
/* Right sidebar — meta blocks */
.meta-card{background:#fff;border:1px solid #E2DDD0;border-radius:5px;padding:14px 16px;}
.meta-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;}
.meta-title{font-size:10.5px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#1A2540;}
.meta-title .num{font-family:"JetBrains Mono",ui-monospace,monospace;color:#8A918D;margin-right:6px;letter-spacing:.06em;}
/* Checklist */
.check-row{display:flex;align-items:flex-start;gap:10px;padding:7px 0;font-size:12.5px;}
.check-row .ic{width:16px;height:16px;border-radius:99px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;}
.check-row.ok .ic{background:#E2F1E5;color:#1F5E2E;}
.check-row.warn .ic{background:#F6EAC8;color:#8A5E27;}
.check-row.err .ic{background:#F4DAD2;color:#8E2A19;}
.check-row .lbl{flex:1;color:#1A1F1C;line-height:1.4;}
.check-row .lbl .sub{display:block;font-size:11px;color:#5A6360;margin-top:1px;}
.check-row.ok .lbl{color:#3A413D;}
/* Portal chooser */
.portal-opt{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid #E2DDD0;border-radius:4px;background:#FBFAF6;cursor:pointer;transition:border-color .12s,background .12s;}
.portal-opt:hover{border-color:#1A2540;}
.portal-opt.is-checked{border-color:#1A2540;background:#fff;box-shadow:inset 0 0 0 1px #1A2540;}
.portal-opt .pcheck{width:14px;height:14px;border:1.5px solid #CFD6E4;border-radius:3px;background:#fff;flex-shrink:0;margin-top:2px;position:relative;}
.portal-opt.is-checked .pcheck{background:#1A2540;border-color:#1A2540;}
.portal-opt.is-checked .pcheck::after{content:"";position:absolute;left:3px;top:0;width:4px;height:8px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg);}
.portal-opt .ptitle{font-size:12.5px;font-weight:600;color:#1A1F1C;display:flex;align-items:center;gap:7px;}
.portal-opt .psub{font-size:11px;color:#5A6360;margin-top:2px;line-height:1.4;}
/* Tag chip */
.tag-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 4px 3px 9px;background:#E5E9F1;color:#1A2540;border-radius:3px;font-size:11.5px;font-weight:500;}
.tag-chip .x{width:16px;height:16px;border-radius:2px;display:inline-flex;align-items:center;justify-content:center;color:#1A2540;transition:background .12s;}
.tag-chip .x:hover{background:#CFD6E4;}
/* Boilerplate panel */
.boiler{background:#FBFAF6;border:1px dashed #CFD6E4;border-radius:5px;padding:14px 16px;}
/* Autosave row */
.autosave{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:#5A6360;}
.autosave .dot{width:6px;height:6px;border-radius:99px;background:#2E8540;box-shadow:0 0 0 3px rgba(46,133,64,.15);}
/* Tabs (für Pressekontakt) */
.tab-mini{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;font-size:11.5px;font-weight:600;color:#5A6360;border-radius:3px;cursor:pointer;}
.tab-mini:hover{color:#1A2540;}
.tab-mini.is-active{background:#E5E9F1;color:#1A2540;}
/* Collapsible */
.collapse-head{display:flex;align-items:center;justify-content:space-between;cursor:pointer;}
.collapse-head:hover .meta-title{color:#243152;}
.chev{width:16px;height:16px;color:#5A6360;transition:transform .15s;}
.meta-card.is-open .chev{transform:rotate(180deg);}
/* AI inline hint (BALD) */
.ai-hint{display:flex;align-items:center;gap:9px;padding:8px 11px;background:#FBF6EB;border:1px dashed #E1C883;border-radius:4px;font-size:11.5px;color:#8A5E27;line-height:1.45;}
.ai-hint .ai-ico{width:22px;height:22px;border-radius:4px;background:#F1E6D3;color:#8A5E27;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.ai-hint .ai-cta{margin-left:auto;font-size:10.5px;font-weight:700;letter-spacing:.10em;text-transform:uppercase;color:#5A6360;padding:3px 7px;border:1px solid #E2DDD0;border-radius:99px;background:#fff;cursor:not-allowed;}
/* Char counter pill */
.meter{display:inline-flex;align-items:center;gap:6px;font-family:"JetBrains Mono",ui-monospace,monospace;font-size:10.5px;color:#5A6360;font-variant-numeric:tabular-nums;}
.meter .bar{position:relative;width:60px;height:4px;background:#EDE7D7;border-radius:99px;overflow:hidden;}
.meter .bar i{position:absolute;left:0;top:0;bottom:0;background:#1A2540;border-radius:99px;}
.meter.good .bar i{background:#2E8540;}
.meter.warn .bar i{background:#A87A1F;}
/* Sticky aside helper */
@media (min-width: 1280px){
.sticky-aside{position:sticky;top:18px;}
}
/* mark / highlight in editor */
.editor-body mark.suggest{background:linear-gradient(transparent 55%, #F6EAC8 55%);padding:0 1px;border-radius:1px;}
</style>
</head>
<body class="bg-bg text-ink font-sans antialiased">
<!-- ============== ARTBOARD ============== -->
<div class="mx-auto bg-bg" style="width:1440px;">
<div class="flex" style="min-height:1500px;">
<!-- ============== SIDEBAR ============== -->
<aside class="bg-bg-elev border-r border-bg-rule flex flex-col" style="width:260px;">
<div class="px-5 pt-6 pb-5">
<a href="Hub Landing presseportale.html" class="flex items-baseline gap-2">
<span class="text-[19px] font-bold tracking-[-0.4px] text-hub leading-none">presseportale<span class="text-accent">.com</span></span>
</a>
<div class="eyebrow muted mt-2">Publisher · Hub</div>
<button class="mt-4 w-full grid items-center gap-2.5 px-3 py-2.5 bg-white border border-bg-rule rounded-[4px] hover:border-hub/40 text-left" style="grid-template-columns:auto 1fr auto;">
<span class="w-7 h-7 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[11px] font-bold">TU</span>
<span class="min-w-0">
<span class="block text-[12.5px] font-semibold text-ink leading-tight truncate">Test User</span>
<span class="block text-[10.5px] text-ink-3 leading-tight mt-0.5 truncate">Tegernseer Brauerei AG +1</span>
</span>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="text-ink-3">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7.5l3-3 3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity="0.4"/>
</svg>
</button>
</div>
<nav class="px-3 flex-1">
<div class="nav-section">Mein Bereich</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="User Dashboard presseportale.html">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M2 7l6-5 6 5v7H2z" stroke="currentColor" stroke-width="1.4"/><path d="M6 14V9h4v5" stroke="currentColor" stroke-width="1.4"/></svg>
Übersicht
</a>
<a class="nav-item active" href="User Pressemitteilungen presseportale.html">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="2.5" width="9" height="11" stroke="currentColor" stroke-width="1.4"/><path d="M11.5 5h2v8.5H4" stroke="currentColor" stroke-width="1.4"/><path d="M5 5.5h4M5 8h4M5 10.5h2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Meine Pressemitteilungen
<span class="badge hub ml-auto" style="font-size:9.5px;padding:1px 6px;letter-spacing:0.08em;">25</span>
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3.5" width="11" height="10" stroke="currentColor" stroke-width="1.4"/><path d="M2.5 6h11" stroke="currentColor" stroke-width="1.4"/><path d="M6 9h1M9 9h1M6 11.5h1M9 11.5h1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Firmen
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 5h10l-1 9H4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 5V3.5a2 2 0 014 0V5" stroke="currentColor" stroke-width="1.4"/></svg>
Buchungen &amp; Add-ons
</a>
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 13V8M7 13V5M11 13V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Statistiken
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
</div>
<div class="nav-section">Finanzen</div>
<div class="space-y-0.5 mb-5">
<span class="nav-item disabled">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M8 5.5v5M6 8h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Credits &amp; Tarif
<span class="ml-auto text-[9.5px] tracking-[0.14em] uppercase font-semibold text-ink-4">bald</span>
</span>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M3 2.5h7l3 3v8H3z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3" stroke="currentColor" stroke-width="1.4"/><path d="M5.5 8h5M5.5 10.5h5M5.5 6h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
Rechnungen
</a>
</div>
<div class="nav-section">Konto</div>
<div class="space-y-0.5 mb-5">
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13.5c.7-2.4 2.7-4 5-4s4.3 1.6 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Profil
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M6 8l1.5 1.5L10.5 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Sicherheit
</a>
<a class="nav-item" href="#">
<svg class="ico" viewBox="0 0 16 16" fill="none"><circle cx="6" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8.5 8h5M11 8v2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
API &amp; Integrationen
</a>
</div>
</nav>
<div class="px-4 pb-4">
<div class="bg-hub text-ink-on-dark rounded-[5px] p-4 relative overflow-hidden">
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
<div class="relative">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
<span class="eyebrow on-dark" style="color:#F4D89C;">Testmodus aktiv</span>
</div>
<div class="text-[12px] leading-[1.5] text-ink-on-dark-2">
Angemeldet als <strong class="text-white font-semibold">Test User</strong>.<br/>
Admin: <strong class="text-white font-semibold">Portal Admin</strong>
</div>
<button class="mt-3 w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Zurück zum Admin
</button>
</div>
</div>
</div>
</aside>
<!-- ============== MAIN ============== -->
<main class="flex-1 min-w-0" data-screen-label="01 Neue Mitteilung erstellen">
<!-- Topbar -->
<div class="bg-bg-elev border-b border-bg-rule">
<div class="px-10 py-3 flex items-center gap-6">
<div class="flex items-center gap-2 text-[12px] text-ink-3 font-medium">
<a href="Hub Landing presseportale.html" class="hover:text-hub">Hub</a>
<span class="text-ink-4">/</span>
<a href="User Dashboard presseportale.html" class="hover:text-hub">User Backend</a>
<span class="text-ink-4">/</span>
<a href="User Pressemitteilungen presseportale.html" class="hover:text-hub">Pressemitteilungen</a>
<span class="text-ink-4">/</span>
<span class="text-hub font-semibold">Neue Mitteilung</span>
</div>
<span class="flex-1"></span>
<span class="bridge-row">
<span class="dot-pe"></span> presseecho
<span class="text-ink-4 mx-1">·</span>
<span class="dot-bp"></span> businessportal24
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<span class="autosave">
<span class="dot"></span>
Zuletzt gespeichert vor <strong class="text-ink-2 font-semibold mx-1">8 Sek.</strong>
<span class="text-ink-4">·</span>
<span class="font-mono text-[10.5px] text-ink-4">PM-DRAFT-2026-0026</span>
</span>
<span class="w-px h-5 bg-bg-rule"></span>
<button class="btn-ghost">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M2 8l4 4 8-8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Speichern
</button>
<button class="btn-secondary">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3"/><path d="M5 8.5L7 10.5 11 6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Vorschau
</button>
</div>
</div>
<!-- Inhalt -->
<div class="px-10 py-8">
<!-- ============== PAGE HEADER ============== -->
<header class="grid items-end gap-8 mb-7" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">User Backend</span>
<span class="eyebrow muted">Mein Bereich · A · 02 · Neu</span>
<span class="badge muted dot">Entwurf</span>
</div>
<h1 class="text-[32px] font-bold tracking-[-0.7px] text-ink leading-[1.1] m-0">Neue Pressemitteilung</h1>
<p class="mt-2 text-[13px] text-ink-3 leading-[1.55] max-w-[640px] m-0">
Schreibfläche links, Steuerung rechts. Speichert automatisch alle paar Sekunden. Beim Klick auf
<span class="font-semibold text-hub">„Zur Prüfung senden"</span> läuft eine kurze Qualitäts-Checkliste.
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<a href="User Pressemitteilungen presseportale.html" class="btn-secondary whitespace-nowrap">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7.5 3L4.5 6l3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zur Liste
</a>
<button class="btn-secondary whitespace-nowrap text-err" style="color:#8E2A19;border-color:#E0B0A5;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 4h10M5.5 4V2.5h5V4M5 4l.5 9.5h5L11 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Entwurf verwerfen
</button>
</div>
</header>
<!-- ============== 2-COLUMN GRID ============== -->
<div class="grid gap-7" style="grid-template-columns:minmax(0,1fr) 360px;">
<!-- ===================================================== -->
<!-- LINKS: SCHREIBFLÄCHE -->
<!-- ===================================================== -->
<div class="space-y-6 min-w-0">
<!-- ─── 1) FIRMA-SELEKTOR ─── -->
<section class="panel" style="padding:14px 18px;">
<div class="flex items-center gap-5">
<div class="field-label" style="margin-bottom:0;">Für Firma</div>
<button class="flex items-center gap-2.5 px-3 py-1.5 bg-white border border-hub rounded-[4px] hover:bg-hub-soft transition-colors" style="box-shadow:inset 0 0 0 1px #1A2540;">
<span class="w-5 h-5 rounded-[3px] bg-hub-soft border border-hub-soft-2 flex items-center justify-center text-hub text-[9.5px] font-bold">TB</span>
<span class="text-[13px] font-semibold text-hub">Tegernseer Brauerei AG</span>
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" class="text-hub-3"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="text-[11.5px] text-ink-3">
Boilerplate und Pressekontakt werden vorbefüllt.
</span>
<span class="flex-1"></span>
<a href="#" class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Neue Firma anlegen
</a>
</div>
</section>
<!-- ─── 2) TITEL ─── -->
<section class="panel" style="padding:20px 22px;">
<div class="flex items-center justify-between mb-2">
<div class="field-label" style="margin-bottom:0;">
Titel / Headline <span class="req">*</span>
</div>
<div class="flex items-center gap-3">
<span class="meter good">
<span class="bar"><i style="width:62%;"></i></span>
62 / 100
</span>
<span class="badge-bald">KI-Titel · bald</span>
</div>
</div>
<input class="inp title" value="Tegernseer Brauerei eröffnet Craft-Beer-Manufaktur am Tegernseer Hafen" />
<div class="field-help">
4090 Zeichen empfohlen. Konkret, ohne Marketing-Floskeln. Wer? Was? Wo?
</div>
</section>
<!-- ─── 3) SUBLINE ─── -->
<section class="panel" style="padding:20px 22px;">
<div class="flex items-center justify-between mb-2">
<div class="field-label" style="margin-bottom:0;">
Untertitel <span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— optional</span>
</div>
<span class="meter">
<span class="bar"><i style="width:48%;"></i></span>
118 / 200
</span>
</div>
<input class="inp subtitle" value="Neue Schau-Manufaktur mit Verkostungsraum und Besucherführungen — Eröffnung 12. Juni 2026." />
</section>
<!-- ─── 4) FLIESSTEXT ─── -->
<section class="panel">
<div class="flex items-center justify-between" style="padding:13px 18px 0 18px;">
<div class="field-label" style="margin-bottom:0;">
Fließtext <span class="req">*</span>
</div>
<div class="flex items-center gap-3">
<span class="meter good">
<span class="bar"><i style="width:38%;"></i></span>
1.420 / 3.500 Z.
</span>
<span class="badge-bald">KI-Lektorat · bald</span>
</div>
</div>
<!-- Toolbar -->
<div class="toolbar mt-3 mx-3" style="border-radius:4px;border:1px solid #E2DDD0;">
<button class="tb-btn" title="Absatz / Stilebene">
<span class="lbl">Absatz</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="ml-1 opacity-70"><path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Fett" style="font-weight:700;">B</button>
<button class="tb-btn" title="Kursiv" style="font-style:italic;">I</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Zwischenüberschrift">H₂</button>
<button class="tb-btn" title="Aufzählung">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><circle cx="3" cy="4" r="1" fill="currentColor"/><circle cx="3" cy="8" r="1" fill="currentColor"/><circle cx="3" cy="12" r="1" fill="currentColor"/><path d="M6 4h8M6 8h8M6 12h6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
</button>
<button class="tb-btn" title="Nummerierte Liste">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6 4h8M6 8h8M6 12h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><text x="1.5" y="6" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">1</text><text x="1.5" y="10" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">2</text><text x="1.5" y="14" font-size="4.5" font-family="JetBrains Mono" fill="currentColor">3</text></svg>
</button>
<button class="tb-btn" title="Zitat">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M3 5.5C3 4.7 3.7 4 4.5 4H6v3.5H4.5C3.7 7.5 3 8.2 3 9V11" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 5.5C9 4.7 9.7 4 10.5 4H12v3.5h-1.5C9.7 7.5 9 8.2 9 9V11" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Link">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M6 10l4-4M5 6.5L4 7.5a2.5 2.5 0 003.5 3.5l1-1M11 9.5l1-1a2.5 2.5 0 00-3.5-3.5l-1 1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="tb-sep"></span>
<button class="tb-btn" title="Rückgängig">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M5 5L2.5 7.5 5 10M2.5 7.5h7.5a3 3 0 010 6H8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="tb-btn" title="Wiederholen">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M11 5l2.5 2.5L11 10M13.5 7.5H6a3 3 0 000 6h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="flex-1"></span>
<span class="text-[10.5px] text-ink-4 mr-2">Reduzierter Editor — bewusst eingeschränkt für konsistentes Format.</span>
</div>
<!-- Editor body -->
<div class="editor-body mx-3 mb-3" style="border:1px solid #E2DDD0;border-top:0;border-radius:0 0 4px 4px;background:#fff;">
<p class="lede">
<strong>Tegernsee, 11.05.2026.</strong> Die Tegernseer Brauerei AG eröffnet am 12. Juni 2026 ihre neue
Craft-Beer-Manufaktur direkt am Tegernseer Hafen. Auf rund 1.200&nbsp;m² entsteht eine
Schau-Brauerei mit angeschlossenem Verkostungsraum und ganzjährigen Besucherführungen.
</p>
<p>
Der Neubau ergänzt den 1675 gegründeten Stammsitz und setzt einen Fokus auf
experimentelle Sude in kleinen Chargen. „Wir wollen Bier-Handwerk wieder
<mark class="suggest">erlebbar machen</mark> — direkt am Wasser, mit offenen Kesseln und
unseren Brauern als Gastgebern", erklärt Vorstandsvorsitzender Dr. Markus Weishaupt.
</p>
<h2>Verkostung, Manufaktur-Touren, regionale Wertschöpfung</h2>
<p>
Die Manufaktur kombiniert drei Erlebnisformate unter einem Dach:
</p>
<ul>
<li>Schau-Brauerei mit gläsernem Sudhaus und 30-minütigen Kurzführungen.</li>
<li>Verkostungsraum für bis zu 60 Personen, mit wechselnden Sorten aus der Pilot-Anlage.</li>
<li>Direktverkauf ab Werk — ausschließlich aus Tegernseer Roh<editor-cursor></editor-cursor></li>
</ul>
<p style="color:#8A918D;font-style:italic;">
Hier weiterschreiben …
</p>
</div>
<!-- AI Hint -->
<div class="mx-3 mb-4">
<div class="ai-hint">
<span class="ai-ico">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M8 2l1.5 4 4 1.5L9.5 9 8 13l-1.5-4L2.5 7.5 6.5 6z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
</span>
<span><strong class="font-semibold">KI-Lektorat</strong> liest Korrektur, schlägt Kürzungen vor und prüft auf werbliche Sprache. Erscheint hier inline — bald verfügbar.</span>
<span class="ai-cta">bald</span>
</div>
</div>
</section>
<!-- ─── 5) MEDIEN ─── -->
<section class="panel" style="padding:18px 20px 20px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Medien / Bilder
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— mindestens 1 Bild empfohlen</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11.5px] text-ink-3"><strong class="text-ink-2 font-semibold">2</strong> Bilder hochgeladen · 1 Vorschau</span>
<span class="badge-bald">KI-Bildgenerierung · bald</span>
</div>
</div>
<div class="grid grid-cols-3 gap-3 mb-3">
<!-- Bild 1 -->
<div class="media-tile">
<div class="media-thumb" style="background:linear-gradient(135deg,#8A6A3A 0%,#3A4D2F 50%,#1A2540 100%);">
<span class="media-cover-flag badge ok dot" style="font-size:9px;padding:2px 7px;letter-spacing:.10em;">Titelbild</span>
<span class="media-actions">
<button class="media-act" title="Bearbeiten"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Entfernen"><svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
</span>
<!-- Visual mock -->
<svg viewBox="0 0 200 125" class="absolute inset-0 w-full h-full" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="bsky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3E5B7A"/><stop offset="1" stop-color="#8A9DAE"/></linearGradient>
</defs>
<rect width="200" height="80" fill="url(#bsky)"/>
<rect y="80" width="200" height="45" fill="#2E3B25"/>
<polygon points="0,80 40,60 70,72 110,55 150,68 200,50 200,80" fill="#3F2D1E" opacity="0.85"/>
<rect x="60" y="70" width="80" height="22" fill="#C8A86E" opacity="0.95"/>
<rect x="62" y="72" width="76" height="3" fill="#8A5E27"/>
<rect x="70" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="80" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="90" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="100" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="110" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="120" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
<rect x="130" y="76" width="6" height="12" fill="#1A1F1C" opacity="0.7"/>
</svg>
</div>
<div class="media-meta">
<div class="media-cap">Außenansicht der neuen Manufaktur am Tegernseer Hafen.</div>
<div class="media-alt">Alt: Holzgebäude mit Glasfront direkt am See, Abendlicht.</div>
</div>
</div>
<!-- Bild 2 -->
<div class="media-tile">
<div class="media-thumb" style="background:#4A2F1E;">
<span class="media-actions">
<button class="media-act" title="Als Titelbild"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 2l1.5 3.5L13 6l-2.5 2.5L11 12l-3-1.7L5 12l.5-3.5L3 6l3.5-.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Bearbeiten"><svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg></button>
<button class="media-act" title="Entfernen"><svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
</span>
<svg viewBox="0 0 200 125" class="absolute inset-0 w-full h-full" preserveAspectRatio="xMidYMid slice">
<rect width="200" height="125" fill="#3A2516"/>
<ellipse cx="100" cy="50" rx="80" ry="30" fill="#C8A86E" opacity="0.18"/>
<rect x="40" y="30" width="34" height="80" rx="3" fill="#B8843A"/>
<rect x="42" y="32" width="30" height="6" fill="#8A5E27"/>
<rect x="42" y="92" width="30" height="14" fill="#8A5E27"/>
<rect x="48" y="46" width="18" height="32" fill="#EFEADC"/>
<rect x="83" y="20" width="34" height="90" rx="3" fill="#D4A04A"/>
<rect x="85" y="22" width="30" height="6" fill="#A87A1F"/>
<rect x="85" y="92" width="30" height="14" fill="#A87A1F"/>
<rect x="91" y="42" width="18" height="32" fill="#FBFAF6"/>
<rect x="126" y="38" width="34" height="72" rx="3" fill="#A87A4A"/>
<rect x="128" y="40" width="30" height="6" fill="#5A3D1E"/>
<rect x="128" y="92" width="30" height="14" fill="#5A3D1E"/>
<rect x="134" y="54" width="18" height="32" fill="#F1E6D3"/>
</svg>
</div>
<div class="media-meta">
<div class="media-cap">Drei neue Sorten aus der Pilot-Anlage — Spezialeditionen.</div>
<div class="media-alt">Alt: Drei Bierflaschen mit unterschiedlichen Etiketten auf Holztisch.</div>
</div>
</div>
<!-- Dropzone -->
<button class="dropzone" style="flex-direction:column;justify-content:center;text-align:center;min-height:100%;">
<span class="dz-ico" style="width:38px;height:38px;">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none"><path d="M8 11V4M5 7L8 4l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><rect x="2.5" y="11.5" width="11" height="2" rx="1" stroke="currentColor" stroke-width="1.4"/></svg>
</span>
<div>
<div class="text-[12.5px] font-semibold text-hub leading-tight">Bilder hinzufügen</div>
<div class="text-[10.5px] text-ink-3 mt-1 leading-tight">JPG, PNG · max. 8 MB · min. 1.200 × 800 px</div>
</div>
</button>
</div>
<div class="field-help">
Bildunterschrift und <strong class="text-ink-2 font-semibold">Alt-Text</strong> sind pro Bild Pflicht für die Veröffentlichung. Das Titelbild erscheint in der Liste auf presseecho &amp; businessportal24.
</div>
</section>
<!-- ─── 6) ANHÄNGE ─── -->
<section class="panel" style="padding:18px 20px 18px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Anhänge / Downloads
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— optional</span>
</div>
<button class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
Datei hinzufügen
</button>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center gap-3 p-3 border border-bg-rule rounded-[4px] bg-bg-elev">
<span class="w-9 h-11 rounded-[3px] bg-white border border-bg-rule flex items-center justify-center text-[9px] font-bold text-err font-mono">PDF</span>
<div class="min-w-0 flex-1">
<div class="text-[12.5px] font-semibold text-ink truncate">Pressemappe_Manufaktur.pdf</div>
<div class="text-[10.5px] text-ink-3 font-mono mt-0.5">2,4 MB · 12 Seiten</div>
</div>
<button class="menu-trigger w-7 h-7 rounded-[3px] hover:bg-bg-rule-2"><svg width="13" height="13" viewBox="0 0 12 12" fill="none"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></button>
</div>
<button class="dropzone" style="padding:14px;">
<span class="dz-ico" style="width:32px;height:32px;">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M6 2.5h4l3 3v8H3v-8z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M10 2.5V5.5h3M8 8v4M6 10l2 2 2-2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<div>
<div class="text-[12.5px] font-semibold text-hub leading-tight">PDF oder Dokument ziehen</div>
<div class="text-[10.5px] text-ink-3 mt-1">max. 25 MB pro Datei</div>
</div>
</button>
</div>
</section>
<!-- ─── 7) BOILERPLATE ─── -->
<section class="panel" style="padding:18px 20px 18px;">
<div class="flex items-center justify-between mb-3">
<div class="field-label" style="margin-bottom:0;">
Über das Unternehmen
<span class="text-ink-4 font-normal" style="letter-spacing:0;text-transform:none;">— Boilerplate aus Firma</span>
</div>
<div class="flex items-center gap-2">
<span class="badge muted dot" style="font-size:9.5px;">aus Firmenprofil</span>
<button class="btn-ghost text-[11.5px]">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M11 3l2 2-8 8H3v-2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
Für diese PM überschreiben
</button>
</div>
</div>
<div class="boiler font-serif text-[13.5px] text-ink-2 leading-[1.6]">
<p class="m-0 mb-2">
<strong class="text-ink font-semibold">Über die Tegernseer Brauerei AG.</strong>
Die Tegernseer Brauerei wurde 1675 als Klosterbrauerei der Benediktinerabtei Tegernsee gegründet und
ist heute eine der ältesten kontinuierlich brauenden Brauereien Bayerns. Im Familienbesitz der Herzöge
von Bayern werden jährlich rund 280.000 Hektoliter gebraut. Schwerpunkt: helle Lagerbiere und
Spezialitäten nach dem bayerischen Reinheitsgebot.
</p>
<p class="m-0 text-[12px] text-ink-3 mt-3">
<span class="font-semibold text-ink-2">Sitz:</span> Tegernsee · <span class="font-semibold text-ink-2">Mitarbeiter:</span> 142 · <span class="font-semibold text-ink-2">Web:</span> tegernseer-brauerei.de
</p>
</div>
<div class="field-help">
Wird automatisch unter jeder Pressemitteilung dieser Firma angefügt. Pro PM editierbar — Änderungen wirken sich nicht auf andere Mitteilungen aus.
</div>
</section>
</div>
<!-- /Schreibfläche -->
<!-- ===================================================== -->
<!-- RECHTS: SETTINGS-SIDEBAR -->
<!-- ===================================================== -->
<aside class="space-y-4 sticky-aside" style="align-self:start;">
<!-- ─── Status & Aktion (PRIMÄR) ─── -->
<div class="meta-card" style="padding:16px 18px;border-color:#1A2540;background:linear-gradient(180deg,#FBFAF6 0%,#fff 60%);">
<div class="flex items-center justify-between mb-3">
<span class="meta-title"><span class="num">01</span>Status &amp; Absenden</span>
<span class="badge muted dot">Entwurf</span>
</div>
<!-- Checkliste -->
<div class="bg-bg-elev border border-bg-rule rounded-[4px] p-3 mb-3">
<div class="flex items-center justify-between mb-1">
<span class="eyebrow muted" style="font-size:9.5px;letter-spacing:.16em;">Pre-Submit-Check</span>
<span class="text-[10.5px] font-mono text-ok font-semibold">4 / 6 ok</span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Titel vorhanden <span class="sub">62 Zeichen — gute Länge</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Mindestlänge Fließtext erreicht <span class="sub">1.420 / min. 600 Zeichen</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Firma zugeordnet <span class="sub">Tegernseer Brauerei AG</span></span>
</div>
<div class="check-row ok">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M2.5 6l2.5 2.5L9.5 3.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="lbl">Mindestens 1 Bild <span class="sub">2 Bilder · Titelbild gesetzt</span></span>
</div>
<div class="check-row warn">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M6 3.5v3M6 8.5h0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<span class="lbl">Themen-Tags fehlen <span class="sub">empfohlen für SEO &amp; Auffindbarkeit</span></span>
</div>
<div class="check-row warn">
<span class="ic"><svg width="9" height="9" viewBox="0 0 12 12" fill="none"><path d="M6 3.5v3M6 8.5h0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<span class="lbl">Untertitel optional gesetzt — kein Pressekontakt-Telefon <span class="sub">Journalisten können dich nicht erreichen</span></span>
</div>
</div>
<!-- Primärer Submit -->
<button class="btn-primary full">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M2 8l5 5 7-10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Zur Prüfung senden
</button>
<p class="text-[11px] text-ink-3 mt-2 leading-[1.45] m-0">
Warnungen (<span class="text-warn font-semibold"></span>) blockieren nicht. Pflichtfelder (<span class="text-err font-semibold"></span>) blockieren. Die Redaktion prüft typ. innerhalb von 24 h.
</p>
<hr class="rule my-3" />
<div class="flex items-center gap-2">
<button class="btn-secondary" style="flex:1;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M2 8l4 4 8-8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Speichern
</button>
<button class="btn-secondary" style="flex:1;">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.3"/><path d="M5 8.5L7 10.5 11 6.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Vorschau
</button>
</div>
</div>
<!-- ─── Portal-Auswahl ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">02</span>Portal</span>
<span class="text-[10.5px] text-ink-3"><strong class="font-mono text-ink-2">1</strong> ausgewählt</span>
</div>
<div class="space-y-2">
<label class="portal-opt is-checked">
<span class="pcheck"></span>
<span>
<span class="ptitle"><span class="dot-pe"></span>presseecho</span>
<span class="psub">Allgemeine Pressewelt · breite Reichweite, redaktionelle Prüfung 24 h</span>
</span>
</label>
<label class="portal-opt">
<span class="pcheck"></span>
<span>
<span class="ptitle"><span class="dot-bp"></span>businessportal24</span>
<span class="psub">B2B &amp; Mittelstand · höhere Sichtbarkeit auf Business-Themen</span>
</span>
</label>
</div>
<p class="text-[11px] text-ink-4 mt-3 mb-0 leading-[1.45] flex items-start gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="mt-0.5 flex-shrink-0 text-ink-3"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.2"/><path d="M6 5v3M6 4v.4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
<span>Im MVP ein Portal pro PM. Cross-Publishing auf beide Portale folgt in Phase 2.</span>
</p>
</div>
<!-- ─── Pressekontakt ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">03</span>Pressekontakt</span>
<span class="flex items-center gap-0.5 bg-bg-elev border border-bg-rule rounded-[3px] p-0.5">
<span class="tab-mini is-active">Aus Firma</span>
<span class="tab-mini">Eigener</span>
</span>
</div>
<div class="space-y-2">
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Name</label>
<input class="inp small" value="Maria Schwarz" />
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Funktion</label>
<input class="inp small" value="Leitung Kommunikation" />
</div>
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">Telefon</label>
<input class="inp small" placeholder="+49 …" style="border-color:#E1C883;background:#FBF6EB;" />
</div>
</div>
<div>
<label class="block text-[10.5px] font-semibold text-ink-3 uppercase tracking-[.10em] mb-1">E-Mail</label>
<input class="inp small" value="presse@tegernseer-brauerei.de" />
</div>
</div>
<div class="flex items-center gap-2 mt-3 text-[11px] text-warn">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 1L11 11H1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M6 5v2.5M6 9v.2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Telefon empfohlen — Journalisten greifen oft direkt zum Hörer.
</div>
</div>
<!-- ─── Themen-Tags ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">04</span>Themen-Tags</span>
<span class="text-[10.5px] text-ink-4"><strong class="font-mono text-ink-2">0</strong> / 5</span>
</div>
<div class="border border-bg-rule rounded-[4px] bg-white px-2 py-2 min-h-[58px] flex flex-wrap items-center gap-1.5">
<span class="text-[11.5px] text-ink-4 italic px-1.5">Tippen, um Tag hinzuzufügen …</span>
</div>
<div class="mt-2.5">
<div class="eyebrow muted mb-1.5" style="font-size:9.5px;">Vorschläge aus Firmenprofil</div>
<div class="flex flex-wrap gap-1.5">
<button class="filter-tag">+ Mittelstand</button>
<button class="filter-tag">+ Brauerei</button>
<button class="filter-tag">+ Tegernsee</button>
<button class="filter-tag">+ Tourismus</button>
<button class="filter-tag">+ Eröffnung</button>
</div>
</div>
<style>
.filter-tag{padding:3px 9px;background:#FBFAF6;border:1px dashed #CFD6E4;border-radius:3px;font-size:11.5px;color:#3A413D;font-weight:500;transition:border-color .12s,background .12s,color .12s;}
.filter-tag:hover{border-style:solid;border-color:#1A2540;background:#E5E9F1;color:#1A2540;}
</style>
<p class="text-[10.5px] text-ink-4 mt-2.5 mb-0 leading-[1.45]">
Kategorie auf presseecho wird automatisch aus dem ersten Tag abgeleitet.
</p>
</div>
<!-- ─── Veröffentlichung ─── -->
<div class="meta-card">
<div class="meta-head">
<span class="meta-title"><span class="num">05</span>Veröffentlichung</span>
</div>
<div class="space-y-2">
<label class="flex items-start gap-2.5 p-2 border border-hub rounded-[4px] bg-hub-soft cursor-pointer" style="box-shadow:inset 0 0 0 1px #1A2540;">
<span class="w-3 h-3 rounded-full bg-hub flex-shrink-0 mt-0.5 flex items-center justify-center"><span class="w-1.5 h-1.5 rounded-full bg-white"></span></span>
<span>
<span class="text-[12.5px] font-semibold text-hub block leading-tight">Sofort nach Freigabe</span>
<span class="text-[11px] text-ink-3 block mt-0.5 leading-tight">geht live, sobald die Redaktion grünes Licht gibt</span>
</span>
</label>
<label class="flex items-start gap-2.5 p-2 border border-bg-rule rounded-[4px] bg-bg-elev cursor-not-allowed" style="opacity:.75;">
<span class="w-3 h-3 rounded-full border-2 border-ink-4 flex-shrink-0 mt-0.5"></span>
<span class="flex-1">
<span class="text-[12.5px] font-semibold text-ink-2 block leading-tight flex items-center gap-2">Geplanter Termin <span class="badge-bald">bald</span></span>
<span class="text-[11px] text-ink-3 block mt-0.5 leading-tight">Datum + Uhrzeit, automatische Veröffentlichung</span>
</span>
</label>
</div>
<hr class="rule my-3" />
<label class="flex items-center gap-2 text-[12px] text-ink-2 cursor-pointer">
<input type="checkbox" class="rounded border-bg-rule" style="accent-color:#1A2540;" />
<span>Sperrfrist setzen (Embargo) — frühestens 12. Juni 2026, 06:00 Uhr</span>
</label>
</div>
<!-- ─── SEO (collapsed) ─── -->
<div class="meta-card">
<div class="collapse-head">
<span class="meta-title"><span class="num">06</span>SEO</span>
<span class="flex items-center gap-2">
<span class="text-[10.5px] text-ink-4">automatisch aus Titel</span>
<svg class="chev" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</div>
<!-- ─── Phase 2 Footer ─── -->
<div class="bg-accent-soft border border-[#E1C883] rounded-[5px] p-3.5">
<div class="flex items-center gap-2 mb-2">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" class="text-accent-deep"><path d="M8 2l1.5 4 4 1.5L9.5 9 8 13l-1.5-4L2.5 7.5 6.5 6z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
<span class="eyebrow accent">Phase 2 — bald</span>
</div>
<ul class="text-[11.5px] text-accent-deep leading-[1.55] list-none p-0 m-0 space-y-1">
<li>· KI-Titel-Optimierung &amp; -Lektorat live</li>
<li>· Geplante Veröffentlichung / Scheduling</li>
<li>· Versionshistorie &amp; Kommentare</li>
<li>· Portal-Vorschau (presseecho vs. BP24)</li>
</ul>
</div>
</aside>
</div>
<!-- ============== FUSSZEILE ============== -->
<footer class="flex items-center justify-between pt-6 pb-2 mt-8 text-[11px] text-ink-3 border-t border-bg-rule">
<span>© 2026 presseportale.com · Publisher-Hub</span>
<span class="flex items-center gap-5">
<a href="#" class="hover:text-hub">Tastenkürzel</a>
<a href="#" class="hover:text-hub">Hilfe zum Editor</a>
<a href="/cdn-cgi/l/email-protection#43303633332c313703333126303026332c3137222f266d202c2e" class="hover:text-hub">Support</a>
</span>
</footer>
</div>
</main>
</div>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,144 @@
/* inter-tight-100 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 100;
src: url('../fonts/inter-tight-v9-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-100italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 100;
src: url('../fonts/inter-tight-v9-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 200;
src: url('../fonts/inter-tight-v9-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 200;
src: url('../fonts/inter-tight-v9-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 300;
src: url('../fonts/inter-tight-v9-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 300;
src: url('../fonts/inter-tight-v9-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 400;
src: url('../fonts/inter-tight-v9-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 400;
src: url('../fonts/inter-tight-v9-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 500;
src: url('../fonts/inter-tight-v9-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 500;
src: url('../fonts/inter-tight-v9-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 600;
src: url('../fonts/inter-tight-v9-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 600;
src: url('../fonts/inter-tight-v9-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 700;
src: url('../fonts/inter-tight-v9-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 700;
src: url('../fonts/inter-tight-v9-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 800;
src: url('../fonts/inter-tight-v9-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 800;
src: url('../fonts/inter-tight-v9-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-900 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: normal;
font-weight: 900;
src: url('../fonts/inter-tight-v9-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* inter-tight-900italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Inter Tight';
font-style: italic;
font-weight: 900;
src: url('../fonts/inter-tight-v9-latin-900italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

View file

@ -0,0 +1,128 @@
/* jetbrains-mono-100 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100;
src: url('../fonts/jetbrains-mono-v24-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-100italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 100;
src: url('../fonts/jetbrains-mono-v24-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 200;
src: url('../fonts/jetbrains-mono-v24-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 200;
src: url('../fonts/jetbrains-mono-v24-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 300;
src: url('../fonts/jetbrains-mono-v24-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 300;
src: url('../fonts/jetbrains-mono-v24-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
src: url('../fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 400;
src: url('../fonts/jetbrains-mono-v24-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
src: url('../fonts/jetbrains-mono-v24-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 500;
src: url('../fonts/jetbrains-mono-v24-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 600;
src: url('../fonts/jetbrains-mono-v24-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 600;
src: url('../fonts/jetbrains-mono-v24-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
src: url('../fonts/jetbrains-mono-v24-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 700;
src: url('../fonts/jetbrains-mono-v24-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 800;
src: url('../fonts/jetbrains-mono-v24-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* jetbrains-mono-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 800;
src: url('../fonts/jetbrains-mono-v24-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

View file

@ -0,0 +1,128 @@
/* source-serif-4-200 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 200;
src: url('../fonts/source-serif-4-v14-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-200italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 200;
src: url('../fonts/source-serif-4-v14-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 300;
src: url('../fonts/source-serif-4-v14-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-300italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 300;
src: url('../fonts/source-serif-4-v14-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 400;
src: url('../fonts/source-serif-4-v14-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 400;
src: url('../fonts/source-serif-4-v14-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 500;
src: url('../fonts/source-serif-4-v14-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-500italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 500;
src: url('../fonts/source-serif-4-v14-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 600;
src: url('../fonts/source-serif-4-v14-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-600italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 600;
src: url('../fonts/source-serif-4-v14-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 700;
src: url('../fonts/source-serif-4-v14-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-700italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 700;
src: url('../fonts/source-serif-4-v14-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-800 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 800;
src: url('../fonts/source-serif-4-v14-latin-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-800italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 800;
src: url('../fonts/source-serif-4-v14-latin-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-900 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 900;
src: url('../fonts/source-serif-4-v14-latin-900.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* source-serif-4-900italic - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 900;
src: url('../fonts/source-serif-4-v14-latin-900italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

View file

@ -141,6 +141,32 @@
color: var(--color-hub);
}
/* Dark Mode: --color-bg ist DUNKLER als die Sidebar (--color-bg-elev),
ein Hover damit würde das Item eindrücken" statt hervorheben. Im Dark
Mode nutzen wir deshalb das dezente Hub-Soft (`#1f2a47`) selbe
Farbfamilie wie der Active-State, nur ohne Active-Strip. */
.dark [data-flux-navlist-item]:hover {
background: var(--color-hub-soft);
color: var(--color-hub);
}
/* Klick/Focus/Tap-Highlight: konsistent mit Hub-Soft (statt browser-
default weißem Tap-Flash oder Flux's `bg-zinc-800/5`). Verhindert das
weiße Aufblitzen beim wire:navigate-Klick im Dark Mode. */
[data-flux-navlist-item]:active,
[data-flux-navlist-item]:focus {
background: var(--color-hub-soft);
color: var(--color-hub);
outline: none;
}
[data-flux-navlist-item] {
-webkit-tap-highlight-color: transparent;
}
[data-flux-navlist-item]:focus-visible {
outline: 2px solid var(--color-hub);
outline-offset: -2px;
}
[data-flux-navlist-item][data-current="true"],
[data-flux-navlist-item][aria-current="page"],
[data-flux-navlist-item].active {

View file

@ -245,4 +245,9 @@
/* color-scheme-Hint für native Form-Controls (Scrollbars, Inputs) */
color-scheme: dark;
/* Brand-Mark Wortmarke (z.B. presse" bei pressekonto) im Portal-Dark
auf weiß. Wird nur von `<x-web.brand-mark variant="auto">` ausgelesen;
Hub-Frontend ist Light-Only, dort bleibt der Inline-Fallback aktiv. */
--brand-mark-name-color: #ffffff;
}

View file

@ -244,6 +244,10 @@
background: var(--color-err-soft);
color: var(--color-loss);
}
.badge.muted {
background: var(--color-bg-rule-2);
color: var(--color-ink-3);
}
.badge.dot::before {
content: "";
width: 6px;
@ -337,4 +341,359 @@
.eyebrow.on-dark {
color: var(--color-hub-line);
}
/* ============================================================
* Counter-Strip (Inline-Zähler unter H1)
* ============================================================
* Beispiel:
* 24 Mitteilungen · 18 veröffentlicht · 1 in Prüfung ·
*/
.counter-strip {
display: inline-flex;
align-items: center;
gap: 14px;
padding: 6px 0;
flex-wrap: wrap;
}
.counter-strip .seg {
display: inline-flex;
align-items: baseline;
gap: 6px;
font-size: 12.5px;
color: var(--color-ink-2);
}
.counter-strip .seg b {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: 13.5px;
font-weight: 600;
color: var(--color-ink);
letter-spacing: -0.2px;
}
.counter-strip .seg.is-ok b {
color: var(--color-gain-deep);
}
.counter-strip .seg.is-warn b {
color: var(--color-accent-deep);
}
.counter-strip .seg.is-err b {
color: var(--color-loss);
}
.counter-strip .seg.is-muted b {
color: var(--color-ink-3);
}
.counter-strip .sep {
width: 1px;
height: 11px;
background: var(--color-bg-rule);
}
/* ============================================================
* View-Tabs (Saved-Views Tab-Leiste über Filter)
* ============================================================ */
.view-tabs {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--color-bg-rule);
}
.view-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px 9px;
font-size: 12.5px;
font-weight: 500;
color: var(--color-ink-3);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition:
color 0.12s,
border-color 0.12s;
cursor: pointer;
background: transparent;
border-top: 0;
border-left: 0;
border-right: 0;
}
.view-tab:hover {
color: var(--color-hub);
}
.view-tab.is-active {
color: var(--color-hub);
font-weight: 600;
border-bottom-color: var(--color-hub);
}
.view-tab .cnt {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--color-ink-3);
background: var(--color-bg-rule-2);
padding: 1px 6px;
border-radius: 999px;
font-weight: 600;
letter-spacing: 0.04em;
}
.view-tab.is-active .cnt {
background: var(--color-hub);
color: var(--color-ink-on-dark);
}
/* ============================================================
* Filter-Chips (Dropdown-Buttons mit Caret)
* ============================================================ */
.filter-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px 6px 14px;
background: var(--color-bg-card);
border: 1px solid var(--color-hub-soft-2);
border-radius: 4px;
font-size: 12.5px;
color: var(--color-hub);
font-weight: 500;
transition:
border-color 0.15s,
background 0.15s;
white-space: nowrap;
cursor: pointer;
}
.filter-chip:hover {
border-color: var(--color-hub);
background: var(--color-bg-elev);
}
.filter-chip.is-active {
background: var(--color-hub);
color: var(--color-ink-on-dark);
border-color: var(--color-hub);
}
.filter-chip.is-active:hover {
background: var(--color-hub-2);
}
.filter-chip .caret {
opacity: 0.55;
}
/* ============================================================
* Active-Chips (entfernbare Filter-Anzeige)
* ============================================================ */
.active-chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 4px 6px 4px 11px;
background: var(--color-bg-elev);
border: 1px solid var(--color-bg-rule);
border-radius: 999px;
font-size: 11.5px;
color: var(--color-ink-2);
font-weight: 500;
}
.active-chip strong {
color: var(--color-hub);
font-weight: 600;
}
.active-chip .x {
width: 16px;
height: 16px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-ink-3);
background: var(--color-bg-rule-2);
transition:
background 0.12s,
color 0.12s;
border: 0;
cursor: pointer;
}
.active-chip .x:hover {
background: var(--color-hub);
color: var(--color-ink-on-dark);
}
/* ============================================================
* Portal-Pills (presseecho / businessportal24 Indikator)
* ============================================================ */
.portal-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
background: var(--color-bg-elev);
border: 1px solid var(--color-bg-rule);
color: var(--color-ink-2);
white-space: nowrap;
}
.portal-pill .pdot {
width: 6px;
height: 6px;
border-radius: 999px;
}
.portal-pill.pe .pdot {
background: var(--color-bridge-presseecho);
}
.portal-pill.bp .pdot {
background: var(--color-bridge-businessportal);
}
/* ============================================================
* Inline-Action (kleine Aktion direkt neben Status-Badge)
* ============================================================ */
.inline-action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
color: var(--color-hub);
background: transparent;
padding: 3px 6px;
margin-left: 4px;
border-radius: 3px;
border: 1px dashed transparent;
transition:
border-color 0.12s,
background 0.12s;
cursor: pointer;
}
.inline-action:hover {
background: var(--color-hub-soft);
border-color: var(--color-hub-soft-2);
}
.inline-action.warn {
color: var(--color-accent-deep);
}
.inline-action.warn:hover {
background: var(--color-warn-soft);
border-color: color-mix(
in oklab,
var(--color-warn),
transparent 60%
);
}
.inline-action.err {
color: var(--color-loss);
}
.inline-action.err:hover {
background: var(--color-err-soft);
border-color: color-mix(
in oklab,
var(--color-err),
transparent 60%
);
}
/* ============================================================
* Row-Tinting (für Status-Hover-Hervorhebung in Listen)
* ============================================================ */
.is-row-warn:hover,
tr.is-row-warn:hover td {
background: color-mix(
in oklab,
var(--color-warn-soft),
var(--color-bg-card) 50%
) !important;
}
.is-row-err:hover,
tr.is-row-err:hover td {
background: color-mix(
in oklab,
var(--color-err-soft),
var(--color-bg-card) 50%
) !important;
}
/* ============================================================
* Empty-Stage (große Empty-States in Panels/Tabellen)
* ============================================================ */
.empty-stage {
padding: 64px 24px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-stage .empty-ico {
width: 64px;
height: 64px;
border-radius: 6px;
background: var(--color-hub-soft);
border: 1px solid var(--color-hub-soft-2);
color: var(--color-hub);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.empty-stage .empty-ico.warm {
background: var(--color-accent-soft);
border-color: color-mix(
in oklab,
var(--color-accent-warm),
transparent 50%
);
color: var(--color-accent-deep);
}
.empty-stage .empty-ico.err {
background: var(--color-err-soft);
border-color: color-mix(
in oklab,
var(--color-err),
transparent 50%
);
color: var(--color-loss);
}
.empty-stage .empty-title {
font-size: 17px;
font-weight: 600;
color: var(--color-ink);
margin: 0;
letter-spacing: -0.2px;
}
.empty-stage .empty-sub {
font-size: 13px;
color: var(--color-ink-3);
line-height: 1.55;
margin: 8px 0 0;
max-width: 440px;
}
}
/* ============================================================
* FluxUI-Tabellen im Hub-Panel-Kontext
* ============================================================
* FluxUI setzt per Default `first:ps-0 last:pe-0` auf alle
* Tabellen-Zellen/-Spalten die Tabelle klebt" am Rand des
* Containers. Im Hub-Stil wollen wir konsistente 18px
* Edge-Padding (wie im Mockup `table.list`).
*
* MUSS in `@layer utilities` stehen (gleicher Layer wie
* FluxUI's `first:ps-0`-Utility), sonst gewinnt FluxUI durch
* CSS-Cascade-Layer-Ordering, unabhängig von Spezifität.
*
* Greift überall, wo eine FluxUI-Tabelle innerhalb eines
* `.panel` oder `.panel-warm` sitzt. Tabellen in Modals,
* Dropdowns oder anderen Kontexten bleiben unangetastet.
*/
@layer utilities {
.panel [data-flux-table] [data-flux-column]:first-child,
.panel [data-flux-table] [data-flux-cell]:first-child,
.panel-warm [data-flux-table] [data-flux-column]:first-child,
.panel-warm [data-flux-table] [data-flux-cell]:first-child {
padding-inline-start: 18px;
}
.panel [data-flux-table] [data-flux-column]:last-child,
.panel [data-flux-table] [data-flux-cell]:last-child,
.panel-warm [data-flux-table] [data-flux-column]:last-child,
.panel-warm [data-flux-table] [data-flux-cell]:last-child {
padding-inline-end: 18px;
}
}

View file

@ -79,11 +79,25 @@
</div>
@forelse ($recentPRs as $pr)
@php
$badgeClass = match ($pr->status->value) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
'archived', 'draft' => 'muted',
default => 'hub',
};
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
@endphp
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="flex items-center justify-between gap-3 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
class="flex items-center justify-between gap-4 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
<div class="min-w-0 flex-1">
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
PM-{{ $pr->id }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->user?->name ?? '' }}
@ -91,15 +105,15 @@
{{ $pr->created_at->format('d.m.Y') }}
</p>
</div>
<span @class([
'badge shrink-0',
'ok' => $pr->status->value === 'published',
'warn' => $pr->status->value === 'review',
'err' => $pr->status->value === 'rejected',
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
])>
{{ $pr->status->label() }}
</span>
<div class="flex items-center gap-1.5 shrink-0">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
</div>
</a>
@empty
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
@ -122,17 +136,44 @@
</div>
@forelse ($pendingReviews as $pr)
@php
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
@endphp
<div
class="is-row-warn px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 transition-colors">
<div class="flex items-start justify-between gap-3">
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="block px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
{{ $pr->company?->name ?? '' }}
class="block min-w-0 flex-1 group">
<p
class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0 group-hover:text-[color:var(--color-hub)] group-hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/40">
{{ $pr->title }}
</p>
</a>
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="inline-action shrink-0" title="{{ __('Pressemitteilung prüfen') }}">
{{ __('Prüfen →') }}
</a>
</div>
<div class="flex items-center justify-between gap-3 mt-1.5">
<p class="text-[11px] text-[color:var(--color-ink-3)] m-0 truncate min-w-0">
PM-{{ $pr->id }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->portal->label() }}
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->created_at->format('d.m.Y') }}
</p>
</a>
<div class="flex items-center gap-1.5 shrink-0">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
</div>
</div>
</div>
@empty
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine PMs in der Prüfwarteschlange.') }}

View file

@ -1,124 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0" wire:navigate>
<x-app-logo />
</a>
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navbar.item>
</flux:navbar>
<flux:spacer />
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
<flux:tooltip :content="__('Search')" position="bottom">
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
</flux:tooltip>
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
label="Documentation"
/>
</flux:tooltip>
</flux:navbar>
<!-- Desktop User Menu -->
<flux:dropdown position="top" align="end">
<flux:profile
class="cursor-pointer"
:initials="auth()->user()->initials()"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar stashable sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo />
</a>
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')">
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:navlist.item>
</flux:navlist>
</flux:sidebar>
{{ $slot }}
@fluxScripts
</body>
</html>

View file

@ -1,10 +1,15 @@
<!DOCTYPE html>
{{--
Hub × FluxUI Phase 1 Portal-Shell im Hub-Design.
class="dark" wurde entfernt; Light Mode ist Default, Dark kommt mit
FluxUI Appearance-Switcher in Phase 5.
Hub × FluxUI Phase 5 Portal-Shell im Hub-Design.
Erscheinung (Light/Dark) wird über FluxUI Appearance-Switcher
gesteuert. Server liest das `flux_appearance`-Cookie (gesetzt vom
JS-Bridge in partials/head.blade.php) und rendert class="dark"
direkt im <html>, damit es bei wire:navigate kein Theme-Flash gibt.
Bei fehlendem Cookie (Erstbesuch) wird Light gerendert und das JS
schaltet bei dunkler Präferenz nach Page-Load nach der einmalige
Flash beim allerersten Aufruf ist akzeptiert.
--}}
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => request()->cookie('flux_appearance') === 'dark'])>
<head>
@include('partials.head')
</head>

View file

@ -1,3 +0,0 @@
<x-layouts.auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts.auth.simple>

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View file

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
{{ $slot }}
</div>
</div>
</div>
@livewireScripts
@fluxScripts
<script src="{{ asset('vendor/livewire/livewire.js') }}"></script>
<!-- Debug: Script-Status -->
<script>
console.log('Body Scripts geladen');
console.log('Livewire JS:', {{ file_exists(public_path('vendor/livewire/livewire.js')) ? 'true' : 'false' }});
if (typeof Livewire !== 'undefined') {
console.log('Livewire verfügbar:', true);
} else {
console.log('Livewire verfügbar:', false);
}
</script>
</body>
</html>

View file

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
</span>
{{ config('app.name', 'Laravel') }}
</a>
@php
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
@endphp
<div class="relative z-20 mt-auto">
<blockquote class="space-y-2">
<flux:heading size="lg">&ldquo;{{ trim($message) }}&rdquo;</flux:heading>
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
</blockquote>
</div>
</div>
<div class="w-full lg:p-8">
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
{{ $slot }}
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View file

@ -1,20 +1,40 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist>
<flux:navlist.item :href="route('settings.profile')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.password')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
</flux:navlist>
{{-- Hub-style settings layout: sidebar nav + content panel --}}
<div class="grid items-start gap-6 md:grid-cols-[220px_1fr]">
<aside class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Mein Konto') }}</span>
</div>
<nav class="p-2">
<flux:navlist>
<flux:navlist.item :href="route('settings.profile')" wire:navigate>
{{ __('Profile') }}
</flux:navlist.item>
<flux:navlist.item :href="route('settings.password')" wire:navigate>
{{ __('Password') }}
</flux:navlist.item>
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>
{{ __('Appearance') }}
</flux:navlist.item>
</flux:navlist>
</nav>
</aside>
<flux:separator class="md:hidden" />
<div class="space-y-6 min-w-0">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ $heading ?? '' }}</span>
</div>
<div class="p-5 space-y-1">
@if (! empty($subheading))
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ $subheading }}
</p>
@endif
<div class="flex-1 self-stretch max-md:pt-6">
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
<div class="mt-5 w-full max-w-lg">
<div class="pt-4">
{{ $slot }}
</div>
</div>
</article>
</div>
</div>

View file

@ -79,8 +79,20 @@
$fontClass = $serif ? 'font-serif' : 'font-sans';
$baseAttributes = $attributes->merge(['class' => $fontClass]);
/**
* Variant=auto: Inline-Color über CSS-Custom-Property mit Light-Fallback.
* Damit kann das Portal im Dark Mode `--brand-mark-name-color` global auf
* weiß setzen, ohne dass das Hub-Frontend (Light-Only) tangiert wird
* dort ist die Variable nie definiert und der Fallback (Marken-Standardfarbe)
* greift. Bei `on-dark` und `mono` bleibt der Inline-Style hart, weil der
* Aufrufer dort explizit eine Farb-Intention setzt.
*/
$nameStyle = $variant === 'auto'
? "color: var(--brand-mark-name-color, {$nameColor});"
: "color: {$nameColor};";
@endphp
<span {{ $baseAttributes }}><span
style="color: {{ $nameColor }};">{{ $mark['name'] }}</span><span
style="{{ $nameStyle }}">{{ $mark['name'] }}</span><span
style="color: {{ $accentColor }};">{{ $mark['accent'] }}</span></span>

View file

@ -1,5 +1,8 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
{{-- Phase 5: Anti-Flash. class="dark" nicht mehr hardcoded Server liest
das `flux_appearance`-Cookie (vom JS-Bridge in partials/admin-head.blade.php
gesetzt) und rendert es direkt im <html>. --}}
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => request()->cookie('flux_appearance') === 'dark'])>
<head>
<meta charset="utf-8">

View file

@ -106,27 +106,45 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Kategorien') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorie anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Neue Themen-Kategorie mit deutscher und englischer Übersetzung.') }}
</p>
</div>
<flux:card>
<div class="flex items-center justify-between">
<flux:heading size="xl">{{ __('Kategorie anlegen') }}</flux:heading>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.categories.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
<form wire:submit="save" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
<div class="space-y-6">
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('Deutsche Übersetzung') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Deutsche Übersetzung') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.400ms="nameDe"
:label="__('Name (DE)')"
@ -148,12 +166,13 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
/>
<flux:error name="descriptionDe" />
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('English translation') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('English translation') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.400ms="nameEn"
:label="__('Name (EN)')"
@ -175,14 +194,15 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
/>
<flux:error name="descriptionEn" />
</div>
</flux:card>
</article>
</div>
<div class="space-y-4">
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Einstellungen') }}</flux:heading>
<div class="space-y-4">
<div class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@ -208,7 +228,7 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
<flux:checkbox wire:model="isActive" :label="__('Aktiv')" />
</div>
</flux:card>
</article>
<flux:button type="submit" variant="primary" class="w-full" icon="check">
{{ __('Speichern') }}

View file

@ -172,33 +172,54 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Kategorien') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ $releaseCount }} {{ __('PMs') }}</span>
@if ($childrenCount > 0)
<span class="badge hub">{{ $childrenCount }} {{ __('Unterkategorien') }}</span>
@endif
@if(session('error'))
<flux:callout color="red" icon="exclamation-triangle">{{ session('error') }}</flux:callout>
@endif
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Kategorie bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }} · {{ $releaseCount }} {{ __('PMs') }} · {{ $childrenCount }} {{ __('Unterkategorien') }}</flux:subheading>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorie bearbeiten') }}
</h1>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.categories.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-err)]" />
<div class="flex-1">{{ session('error') }}</div>
</div>
@endif
<form wire:submit="save" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
<div class="space-y-6">
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('Deutsche Übersetzung') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Deutsche Übersetzung') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="nameDe" :label="__('Name (DE)')" required />
<flux:error name="nameDe" />
@ -208,12 +229,13 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:textarea wire:model="descriptionDe" :label="__('Beschreibung (DE)')" rows="3" />
<flux:error name="descriptionDe" />
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('English translation') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('English translation') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="nameEn" :label="__('Name (EN)')" required />
<flux:error name="nameEn" />
@ -223,14 +245,15 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:textarea wire:model="descriptionEn" :label="__('Description (EN)')" rows="3" />
<flux:error name="descriptionEn" />
</div>
</flux:card>
</article>
</div>
<div class="space-y-4">
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Einstellungen') }}</flux:heading>
<div class="space-y-4">
<div class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@ -256,18 +279,28 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:checkbox wire:model="isActive" :label="__('Aktiv')" />
</div>
</flux:card>
</article>
<flux:button type="submit" variant="primary" class="w-full" icon="check">
{{ __('Änderungen speichern') }}
</flux:button>
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone') }}</span>
</div>
<div class="p-5 space-y-3">
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Kategorie unwiderruflich entfernen, sofern keine PMs/Unterkategorien hängen.') }}
</p>
<flux:modal.trigger name="confirm-delete-category">
<flux:button type="button" variant="danger" icon="trash" class="w-full">
{{ __('Kategorie löschen') }}
</flux:button>
</flux:modal.trigger>
</div>
</article>
</div>
</div>
</form>

View file

@ -141,80 +141,76 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
}
}; ?>
<div class="space-y-6">
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Kategorien') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Stammdaten') }}</span>
</div>
<flux:icon.folder class="size-8 text-blue-500" />
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorien') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Themen-Taxonomie für Pressemitteilungen über alle Portale hinweg.') }}
</p>
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Mit PMs') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['with_releases'] }}</flux:text>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" :href="route('admin.categories.create')" wire:navigate>
{{ __('Kategorie anlegen') }}
</flux:button>
</div>
<flux:icon.document-text class="size-8 text-green-500" />
</div>
</flux:card>
</header>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('PMs gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total_releases'] }}</flux:text>
</div>
<flux:icon.newspaper class="size-8 text-purple-500" />
</div>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Kategorien')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('gesamt') }}</x-slot:meta>
<x-slot:trend>{{ __('Themen-Taxonomie') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Mit PMs')" :value="number_format($stats['with_releases'])">
<x-slot:meta>{{ __('aktiv genutzt') }}</x-slot:meta>
<x-slot:trend>{{ __('mind. eine PM') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('PMs gesamt')" :value="number_format($stats['total_releases'])">
<x-slot:meta>{{ __('alle Portale') }}</x-slot:meta>
<x-slot:trend>{{ __('Content-Output') }}</x-slot:trend>
</x-portal.stat-card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Portale') }}</flux:text>
<div class="mt-2 space-y-1 text-sm">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Portale') }}</span>
</div>
<dl class="p-5 space-y-2 text-[12.5px]">
<div class="flex items-center justify-between gap-3">
<span>{{ __('Presseecho') }}</span>
<span class="font-semibold">{{ number_format($stats['presseecho_releases']) }}</span>
<dt class="text-[color:var(--color-ink-3)]">{{ __('Presseecho') }}</dt>
<dd class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($stats['presseecho_releases']) }}</dd>
</div>
<div class="flex items-center justify-between gap-3">
<span>{{ __('Businessportal24') }}</span>
<span class="font-semibold">{{ number_format($stats['businessportal24_releases']) }}</span>
</div>
</div>
</flux:card>
<dt class="text-[color:var(--color-ink-3)]">{{ __('Businessportal24') }}</dt>
<dd class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($stats['businessportal24_releases']) }}</dd>
</div>
</dl>
</article>
</section>
{{-- Filter + Aktion --}}
<flux:card>
<div class="flex flex-wrap items-center gap-3">
<div class="min-w-[260px] flex-1">
{{-- ============== FILTER + SORT ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Sortierung') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Kategorie suchen (Name, Slug)…') }}"
icon="magnifying-glass"
/>
</div>
<flux:button
variant="primary"
icon="plus"
:href="route('admin.categories.create')"
wire:navigate
>
{{ __('Kategorie anlegen') }}
</flux:button>
</div>
</flux:card>
{{-- Karten --}}
{{-- Sortier-Buttons --}}
<flux:card>
<div class="flex items-center gap-2 text-sm">
<span class="text-zinc-500">{{ __('Sortierung:') }}</span>
<div class="flex items-center gap-2 text-[12px] flex-wrap">
<span class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Sortierung') }}
</span>
<flux:button size="sm" variant="{{ $sortBy === 'id' ? 'primary' : 'ghost' }}" wire:click="sort('id')">
{{ __('Standard') }} @if ($sortBy === 'id') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
@ -222,27 +218,30 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
{{ __('PMs') }} @if ($sortBy === 'press_releases_count') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
</div>
</flux:card>
</div>
</article>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{{-- ============== KATEGORIE-KARTEN ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@forelse ($categories as $category)
@php
$de = $category->translations->firstWhere('locale', 'de');
$en = $category->translations->firstWhere('locale', 'en');
@endphp
<flux:card class="group relative h-full transition hover:border-blue-300 hover:bg-blue-50/40 dark:hover:border-blue-700 dark:hover:bg-blue-950/20">
<article class="panel group relative h-full transition hover:border-[color:var(--color-hub)]/30">
<a href="{{ route('admin.press-releases.index', ['category' => $category->id]) }}" wire:navigate class="absolute inset-0 z-0" aria-hidden="true"></a>
<div class="relative z-10 space-y-4">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<flux:heading size="lg" class="truncate">{{ $de?->name ?? '' }}</flux:heading>
<flux:text class="truncate text-sm text-zinc-500">{{ $en?->name ?? '' }}</flux:text>
<div class="relative z-10 flex h-full flex-col">
<div class="panel-head">
<div class="min-w-0">
<span class="section-eyebrow truncate">{{ __('Kategorie') }}</span>
</div>
<div class="flex items-center gap-2">
<flux:badge color="{{ $category->is_active ? 'green' : 'zinc' }}" size="sm">
{{ $category->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
@if ($category->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
<flux:button
size="xs"
variant="ghost"
@ -254,43 +253,58 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
</div>
</div>
<div class="p-5 space-y-4 flex-1">
<div>
<div class="text-[15px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $de?->name ?? '' }}
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)] truncate mt-0.5">
{{ $en?->name ?? '' }}
</div>
</div>
@if ($de?->description)
<flux:text class="line-clamp-2 text-sm text-zinc-600 dark:text-zinc-400">
<p class="line-clamp-2 text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ $de->description }}
</flux:text>
</p>
@endif
<div class="grid grid-cols-2 gap-2 border-t border-zinc-200 pt-3 text-xs dark:border-zinc-700">
<div class="rounded-md bg-zinc-50 px-2 py-1 dark:bg-zinc-800">
<div class="text-zinc-500">{{ __('Presseecho') }}</div>
<div class="font-semibold">{{ number_format($category->presseecho_press_releases_count) }}</div>
<div class="grid grid-cols-2 gap-2 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px]">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-2 py-1.5">
<div class="text-[10px] uppercase tracking-[0.5px] font-semibold text-[color:var(--color-ink-3)]">{{ __('Presseecho') }}</div>
<div class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($category->presseecho_press_releases_count) }}</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-2 py-1.5">
<div class="text-[10px] uppercase tracking-[0.5px] font-semibold text-[color:var(--color-ink-3)]">{{ __('Businessportal24') }}</div>
<div class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($category->businessportal24_press_releases_count) }}</div>
</div>
<div class="rounded-md bg-zinc-50 px-2 py-1 dark:bg-zinc-800">
<div class="text-zinc-500">{{ __('Businessportal24') }}</div>
<div class="font-semibold">{{ number_format($category->businessportal24_press_releases_count) }}</div>
</div>
</div>
<div class="flex items-center justify-between border-t border-zinc-200 pt-3 dark:border-zinc-700">
<div class="flex items-center gap-1.5 text-sm text-zinc-500">
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] flex items-center justify-between">
<div class="flex items-center gap-1.5 text-[12px] text-[color:var(--color-ink-3)]">
<flux:icon.newspaper class="size-4" />
{{ $category->press_releases_count }} {{ __('PMs') }}
<span class="tabular-nums">{{ $category->press_releases_count }}</span>
{{ __('PMs') }}
</div>
<flux:badge color="zinc" size="sm">/{{ $de?->slug ?? $category->id }}</flux:badge>
<span class="badge hub">/{{ $de?->slug ?? $category->id }}</span>
</div>
</div>
</flux:card>
</article>
@empty
<div class="col-span-full">
<flux:card>
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.folder class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Kategorien gefunden.') }}</flux:text>
<article class="panel col-span-full">
<div class="p-10 flex flex-col items-center justify-center text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.folder class="size-6" />
</div>
</flux:card>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Kategorien gefunden.') }}
</div>
</div>
</article>
@endforelse
</div>
</section>
{{ $categories->links() }}
</div>

View file

@ -117,12 +117,35 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
}
}; ?>
<form wire:submit="save" class="space-y-6">
{{-- Basisinformationen --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisinformationen') }}</flux:heading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Neue Firma') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Neue Firma') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Stammdaten, Adresse, Logo und Rechtsangaben einer neuen Firma erfassen.') }}
</p>
</div>
<div class="space-y-4">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<form wire:submit="save" class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisinformationen') }}</span>
</div>
<div class="p-5 space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
@ -144,7 +167,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
</div>
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('Firmenname') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
@ -157,7 +180,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('E-Mail') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
</flux:field>
@ -175,13 +198,13 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
<flux:error name="website" />
</flux:field>
</div>
</flux:card>
</article>
{{-- Adresse --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
@ -210,7 +233,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('Land') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:select wire:model="country">
@foreach ($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@ -220,13 +243,13 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
</flux:field>
</div>
</div>
</flux:card>
</article>
{{-- Rechtliche Daten --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechtliche Daten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechtliche Daten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
@ -239,13 +262,13 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
<flux:error name="registration_number" />
</flux:field>
</div>
</flux:card>
</article>
{{-- Logo & Status --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Logo & Status') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Logo & Status') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/*" />
@ -254,22 +277,27 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
@if ($logo)
<div class="mt-4">
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Vorschau:') }}</flux:text>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Vorschau:') }}
</div>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
@endif
</flux:field>
<div class="flex gap-6">
<div class="flex gap-6 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
</div>
</div>
</flux:card>
</article>
{{-- Aktionen --}}
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -277,5 +305,6 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
{{ __('Firma erstellen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>
</div>

View file

@ -195,13 +195,36 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
}
}; ?>
<div class="space-y-6">
<form wire:submit="update" class="space-y-6">
{{-- Basisinformationen --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisinformationen') }}</flux:heading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firma bearbeiten') }}</span>
<span class="badge hub">ID {{ $companyId }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Firma bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Stammdaten, Adresse, Logo und Rechtsangaben der Firma aktualisieren.') }}
</p>
</div>
<div class="space-y-4">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.show', $companyId) }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<form wire:submit="update" class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisinformationen') }}</span>
</div>
<div class="p-5 space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
@ -223,7 +246,7 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
</div>
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('Firmenname') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
@ -236,7 +259,7 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('E-Mail') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
</flux:field>
@ -254,13 +277,13 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
<flux:error name="website" />
</flux:field>
</div>
</flux:card>
</article>
{{-- Adresse --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
@ -289,7 +312,7 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
<flux:label>{{ __('Land') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:select wire:model="country">
@foreach ($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@ -299,13 +322,13 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
</flux:field>
</div>
</div>
</flux:card>
</article>
{{-- Rechtliche Daten --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechtliche Daten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechtliche Daten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
@ -318,13 +341,13 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
<flux:error name="registration_number" />
</flux:field>
</div>
</flux:card>
</article>
{{-- Logo & Status --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Logo & Status') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Logo & Status') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/jpeg,image/png,image/webp,image/gif" />
@ -333,39 +356,51 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
@if ($logo)
<div class="mt-4">
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Neues Logo (Vorschau):') }}</flux:text>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Neues Logo (Vorschau):') }}
</div>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
@elseif ($current_logo_url && ! $remove_logo)
<div class="mt-4 flex items-center gap-3">
<div class="mt-4 flex items-center gap-4">
<div>
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Aktuelles Logo:') }}</flux:text>
<img src="{{ $current_logo_url }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Aktuelles Logo:') }}
</div>
<img src="{{ $current_logo_url }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', true)">
{{ __('Logo entfernen') }}
</flux:button>
</div>
@elseif ($remove_logo)
<flux:callout color="amber" icon="exclamation-triangle" class="mt-4">
<div class="mt-4 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5" />
<div class="flex-1">
{{ __('Logo wird beim Speichern entfernt.') }}
</div>
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', false)">
{{ __('Rückgängig') }}
</flux:button>
</flux:callout>
</div>
@endif
</flux:field>
<div class="flex gap-6">
<div class="flex gap-6 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
</div>
</div>
</flux:card>
</article>
{{-- Aktionen --}}
<flux:card>
<div class="flex justify-between">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 flex justify-between items-center gap-3 flex-wrap">
<flux:modal.trigger name="confirm-company-deletion">
<flux:button
variant="danger"
@ -386,7 +421,7 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
<flux:modal name="confirm-company-deletion" class="max-w-lg">

View file

@ -296,43 +296,51 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
}
}; ?>
<div class="space-y-6">
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firmen') }}</span>
</div>
<flux:icon.building-office class="size-8 text-blue-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Aktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['active'] }}</flux:text>
</div>
<flux:icon.check-circle class="size-8 text-green-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Inaktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['inactive'] }}</flux:text>
</div>
<flux:icon.x-circle class="size-8 text-red-500" />
</div>
</flux:card>
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
{{ __('Firmen') }}
</h1>
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Übersicht aller Firmen beider Portale, mit Filter, Datenqualität und Schnellaktionen.') }}
</p>
</div>
{{-- Filter & Actions --}}
<flux:card>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3 flex-shrink-0">
<flux:button icon="plus" variant="primary" href="{{ route('admin.companies.create') }}" wire:navigate>
{{ __('Neue Firma') }}
</flux:button>
</div>
</header>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('alle Portale') }}</x-slot:meta>
<x-slot:trend>{{ __('Stammdaten-Basis') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($stats['active'])">
<x-slot:meta>{{ __('produktiv') }}</x-slot:meta>
<x-slot:trend>{{ __('für PMs nutzbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Inaktiv')" :value="number_format($stats['inactive'])">
<x-slot:meta>{{ __('pausiert') }}</x-slot:meta>
<x-slot:trend>{{ __('archiviert oder geblockt') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-7">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Suchen...') }}"
icon="magnifying-glass" class="xl:col-span-2" />
@ -436,17 +444,17 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
/>
</div>
</div>
<div class="flex justify-end">
<flux:button icon="plus" href="{{ route('admin.companies.create') }}" wire:navigate>
{{ __('Neue Firma') }}
</flux:button>
</div>
</div>
</flux:card>
</article>
{{-- Tabelle --}}
<flux:card class="overflow-hidden">
{{-- ============== TABELLE-PANEL ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Firmen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $companies->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
@ -482,39 +490,42 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
<flux:table.cell>
<div class="flex items-center gap-3">
@if ($logoUrl)
<img src="{{ $logoUrl }}" width="36" height="36" class="h-9 max-h-9 w-9 max-w-9 rounded-md border border-zinc-200 object-contain dark:border-zinc-700" alt="{{ $company->name }}">
<img src="{{ $logoUrl }}" width="36" height="36"
class="h-9 max-h-9 w-9 max-w-9 rounded-[4px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]"
alt="{{ $company->name }}">
@else
<div class="flex h-9 w-9 items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.building-office class="size-5 text-zinc-400" />
<div class="flex h-9 w-9 items-center justify-center rounded-[4px]
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
<flux:icon.building-office class="size-5 text-[color:var(--color-ink-3)]" />
</div>
@endif
<flux:text weight="semibold"> {{ \Illuminate\Support\Str::limit($company->name, 60) }}
</flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ \Illuminate\Support\Str::limit($company->name, 60) }}
</span>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm">{{ $company->email ?: __('Keine E-Mail') }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $company->phone ?: __('Kein Telefon') }}
</flux:text>
<div class="space-y-0.5">
<div class="text-[12.5px] text-[color:var(--color-ink)]">
{{ $company->email ?: __('Keine E-Mail') }}
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $company->phone ?: __('Kein Telefon') }}
</div>
</div>
</flux:table.cell>
<flux:table.cell>
@if ($company->is_active)
<flux:badge color="green" size="sm" icon="check">{{ __('Aktiv') }}
</flux:badge>
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<flux:badge color="red" size="sm" icon="x-mark">{{ __('Inaktiv') }}
</flux:badge>
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:badge color="{{ $this->portalBadgeColor($company->portal) }}" size="sm">
{{ $company->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
<span class="badge hub">{{ $company->portal?->label() ?? __('Unbekannt') }}</span>
</flux:table.cell>
<flux:table.cell>
@ -528,7 +539,7 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
{{ $company->press_releases_count }} {{ __('PMs') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge hub">0</span>
@endif
</flux:table.cell>
@ -543,31 +554,39 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
{{ $company->contacts_count }} {{ __('Kontakte') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge hub">0</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $company->created_at?->format('d.m.Y H:i') ?? '' }}
</flux:text>
</span>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="8">
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.building-office class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Firmen gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.building-office class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Firmen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Passen Sie die Filter an oder legen Sie eine neue Firma an.') }}
</p>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $companies->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -192,129 +192,198 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="flex gap-4">
<div class="space-y-8">
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
{{ session('error') }}
</div>
@endif
@if (session('info'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
{{ session('info') }}
</div>
@endif
{{-- ============== PAGE HEADER ============== --}}
@php($logoUrl = $company->logoUrl())
@if($logoUrl)
<img src="{{ $logoUrl }}" width="80" height="80" class="h-20 max-h-20 w-20 max-w-20 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700" alt="{{ $company->name }}">
@else
<div class="flex h-20 w-20 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.building-office class="size-10 text-zinc-400" />
</div>
@endif
<div>
<flux:heading size="xl" class="mb-2">{{ $company->name }}</flux:heading>
<div class="flex flex-wrap gap-2">
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firma') }}</span>
@if ($company->is_active)
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
<span class="badge ok">{{ __('Aktiv') }}</span>
@else
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
<span class="badge err">{{ __('Inaktiv') }}</span>
@endif
<span class="badge hub">{{ $company->portal?->label() ?? __('Unbekannt') }}</span>
<span class="badge hub">ID {{ $company->id }}</span>
</div>
<div class="flex items-start gap-4">
@if ($logoUrl)
<img src="{{ $logoUrl }}" width="64" height="64"
class="h-16 max-h-16 w-16 max-w-16 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)] flex-shrink-0"
alt="{{ $company->name }}">
@else
<div class="flex h-16 w-16 items-center justify-center rounded-[6px]
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] flex-shrink-0">
<flux:icon.building-office class="size-7 text-[color:var(--color-ink-3)]" />
</div>
@endif
<div class="min-w-0">
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $company->name }}
</h1>
@if ($company->website)
<a href="{{ $company->website }}" target="_blank" rel="noopener"
class="text-[12.5px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] mt-1 inline-block">
{{ $company->website }}
</a>
@endif
<flux:badge color="{{ $this->portalBadgeColor($company->portal) }}" size="sm">{{ $company->portal?->label() ?? __('Unbekannt') }}</flux:badge>
<flux:text class="ml-2 text-sm text-zinc-500">ID: {{ $company->id }}</flux:text>
</div>
</div>
</div>
<div class="flex gap-2">
<flux:button icon="pencil" href="{{ route('admin.companies.edit', $company->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
@if (\Illuminate\Support\Facades\Route::has('admin.companies.contacts.create'))
<flux:button variant="ghost" icon="user-plus" href="{{ route('admin.companies.contacts.create', ['companyId' => $company->id]) }}" wire:navigate>
{{ __('Kontakt hinzufügen') }}
</flux:button>
@endif
<flux:button variant="primary" icon="pencil" href="{{ route('admin.companies.edit', $company->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
</div>
</div>
</flux:card>
</header>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->press_releases_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Kontakte') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->contacts_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Verknüpfte Benutzer') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->users_count }}</flux:text>
</flux:card>
</div>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Pressemitteilungen')" :value="number_format($company->press_releases_count)">
<x-slot:meta>{{ __('insgesamt') }}</x-slot:meta>
<x-slot:trend>{{ __('Content-Output dieser Firma') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Kontakte')" :value="number_format($company->contacts_count)">
<x-slot:meta>{{ __('Ansprechpartner') }}</x-slot:meta>
<x-slot:trend>{{ __('für PMs verfügbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Verknüpfte Benutzer')" :value="number_format($company->users_count)">
<x-slot:meta>{{ __('Owner & Co-Editors') }}</x-slot:meta>
<x-slot:trend>{{ __('Backend-Zugriff') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<flux:card>
<div class="flex gap-2">
<flux:button
:variant="$activeTab === 'overview' ? 'primary' : 'ghost'"
wire:click="setTab('overview')"
>
{{-- ============== TABS ============== --}}
<nav class="flex items-center gap-2 border-b border-[color:var(--color-bg-rule)]">
<button type="button" wire:click="setTab('overview')"
@class([
'px-4 py-2.5 text-[12.5px] font-semibold border-b-2 transition-colors',
'border-[color:var(--color-hub)] text-[color:var(--color-ink)]' => $activeTab === 'overview',
'border-transparent text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)]' => $activeTab !== 'overview',
])>
{{ __('Überblick') }}
</flux:button>
<flux:button
:variant="$activeTab === 'contacts' ? 'primary' : 'ghost'"
wire:click="setTab('contacts')"
>
</button>
<button type="button" wire:click="setTab('contacts')"
@class([
'px-4 py-2.5 text-[12.5px] font-semibold border-b-2 transition-colors',
'border-[color:var(--color-hub)] text-[color:var(--color-ink)]' => $activeTab === 'contacts',
'border-transparent text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)]' => $activeTab !== 'contacts',
])>
{{ __('Kontakte') }}
</flux:button>
</div>
</flux:card>
</button>
</nav>
@if ($activeTab === 'overview')
<div class="grid gap-6 lg:grid-cols-2">
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktinformationen') }}</flux:heading>
<div class="space-y-2">
<flux:text>{{ $company->email ?: __('Keine E-Mail hinterlegt') }}</flux:text>
<flux:text>{{ $company->phone ?: __('Kein Telefon hinterlegt') }}</flux:text>
<flux:text>{{ $company->website ?: __('Keine Website hinterlegt') }}</flux:text>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktinformationen') }}</span>
</div>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-2">
<flux:text>{{ $company->address ?: __('Keine Adresse hinterlegt') }}</flux:text>
<flux:text>{{ $company->country_code ?: __('Kein Land hinterlegt') }}</flux:text>
<dl class="p-5 space-y-2.5 text-[12.5px]">
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('E-Mail') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right break-all">{{ $company->email ?: __('Keine E-Mail hinterlegt') }}</dd>
</div>
</flux:card>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Telefon') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->phone ?: __('Kein Telefon hinterlegt') }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Website') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right break-all">{{ $company->website ?: __('Keine Website hinterlegt') }}</dd>
</div>
</dl>
</article>
<flux:card class="lg:col-span-2">
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ __('Aktuelle Pressemitteilungen') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
<dl class="p-5 space-y-2.5 text-[12.5px]">
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Anschrift') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right whitespace-pre-line">{{ $company->address ?: __('Keine Adresse hinterlegt') }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Land') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->country_code ?: __('Kein Land hinterlegt') }}</dd>
</div>
</dl>
</article>
<article class="panel lg:col-span-2">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktuelle Pressemitteilungen') }}</span>
<flux:button size="sm" variant="ghost" href="{{ route('admin.press-releases.index', ['company' => $company->id]) }}" wire:navigate>
{{ __('Alle anzeigen') }}
</flux:button>
</div>
<div class="space-y-2">
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@forelse ($recentPressReleases as $pressRelease)
<a href="{{ route('admin.press-releases.show', $pressRelease->id) }}" wire:navigate class="block rounded-lg p-3 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-900">
<flux:text weight="medium">{{ $pressRelease->title ?? __('Ohne Titel') }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $pressRelease->created_at?->format('d.m.Y') ?? '-' }}</flux:text>
<a href="{{ route('admin.press-releases.show', $pressRelease->id) }}" wire:navigate
class="flex items-center justify-between gap-3 px-5 py-3 hover:bg-[color:var(--color-bg-elev)] transition-colors">
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $pressRelease->title ?? __('Ohne Titel') }}
</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)] flex-shrink-0">
{{ $pressRelease->created_at?->format('d.m.Y') ?? '-' }}
</span>
</a>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Pressemitteilungen vorhanden') }}</flux:text>
<div class="px-5 py-6 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine Pressemitteilungen vorhanden') }}
</div>
@endforelse
</div>
</flux:card>
</article>
</div>
@endif
@if ($activeTab === 'contacts')
<flux:card>
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:heading size="lg">{{ __('Ansprechpartner') }} ({{ $filteredContactsTotal }})</flux:heading>
<div class="flex w-full gap-2 sm:w-auto">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Ansprechpartner') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $filteredContactsTotal }} {{ __('Einträge') }}
</span>
</div>
<div class="p-5 space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<flux:input
wire:model.live.debounce.300ms="contactSearch"
placeholder="{{ __('Kontakte durchsuchen...') }}"
icon="magnifying-glass"
class="flex-1"
/>
@if (\Illuminate\Support\Facades\Route::has('admin.companies.contacts.create'))
<flux:button size="sm" icon="plus" href="{{ route('admin.companies.contacts.create', ['companyId' => $company->id]) }}" wire:navigate>
@ -322,10 +391,11 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
</flux:button>
@endif
</div>
</div>
<div class="mb-4 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:heading size="sm" class="mb-2">{{ __('Bestehenden Kontakt zuordnen') }}</flux:heading>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Bestehenden Kontakt zuordnen') }}
</div>
<flux:select
wire:model.live="selectedExistingContactId"
variant="combobox"
@ -342,7 +412,7 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
@php($lookupName = trim(($lookupContact->first_name ?? '').' '.($lookupContact->last_name ?? '')) ?: __('Kontakt ohne Name'))
<flux:select.option :value="$lookupContact->id" wire:key="lc-{{ $lookupContact->id }}">
{{ $lookupName }}
<span class="text-zinc-400">
<span class="text-[color:var(--color-ink-3)]">
@if ($lookupContact->email)
· {{ $lookupContact->email }}
@endif
@ -362,22 +432,25 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
</flux:select>
</div>
<div class="space-y-3">
<div class="space-y-2">
@forelse ($filteredContacts as $contact)
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="flex items-start justify-between gap-3">
<div>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<flux:text weight="semibold">
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:badge color="{{ $this->portalBadgeColor($contact->portal) }}" size="sm">
{{ $contact->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
</span>
<span class="badge hub">{{ $contact->portal?->label() ?? __('Unbekannt') }}</span>
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)] mt-0.5">
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
</div>
<flux:text class="text-sm text-zinc-500">{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
@if ($contact->email)
<flux:text class="text-sm text-blue-600 dark:text-blue-400">{{ $contact->email }}</flux:text>
<a href="mailto:{{ $contact->email }}"
class="text-[12px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] mt-0.5 inline-block">
{{ $contact->email }}
</a>
@endif
</div>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
@ -386,15 +459,18 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Kontakte gefunden') }}</flux:text>
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine Kontakte gefunden') }}
</div>
@endforelse
</div>
@if ($filteredContactsTotal > $filteredContacts->count())
<flux:text class="mt-3 block text-xs text-zinc-500">
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Es werden die ersten :count Kontakte angezeigt. Bitte Suche eingrenzen, um weitere Treffer zu finden.', ['count' => $filteredContacts->count()]) }}
</flux:text>
@endif
</flux:card>
</p>
@endif
</div>
</article>
@endif
</div>

View file

@ -143,19 +143,37 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Kontakt anlegen') }}</flux:heading>
<flux:subheading>{{ __('Kontakt einer Firma zuordnen und Stammdaten erfassen.') }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
@if ($isCompanyPrefilled && $companyId)
<flux:text class="mt-2 text-sm text-zinc-500">
{{ __('Firma wurde vorausgewählt. Du kannst sie bei Bedarf trotzdem ändern.') }}
</flux:text>
<span class="badge hub">{{ __('Firma vorausgewählt') }}</span>
@endif
</flux:card>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakt anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Kontakt einer Firma zuordnen und Stammdaten erfassen.') }}
</p>
</div>
<flux:card>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zuordnung') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Firma') }}</flux:label>
@ -203,12 +221,13 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
<flux:error name="portal" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktdaten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktdaten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="salutationKey">
@ -260,10 +279,10 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
<flux:error name="fax" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -271,5 +290,5 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
{{ __('Kontakt anlegen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -178,22 +178,34 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
}
}; ?>
<div class="space-y-6">
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Kontakt bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }}</flux:subheading>
<div class="space-y-8">
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ $this->currentPortalLabel() }}</span>
</div>
<flux:badge color="{{ $this->currentPortalBadgeColor() }}" size="sm">
{{ $this->currentPortalLabel() }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakt bearbeiten') }}
</h1>
</div>
</flux:card>
<flux:card>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zuordnung') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Firma') }}</flux:label>
@ -241,12 +253,13 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
<flux:error name="portal" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktdaten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktdaten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="salutationKey">
@ -298,10 +311,13 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
<flux:error name="fax" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<div class="flex justify-between">
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone & Aktionen') }}</span>
</div>
<div class="p-5 flex justify-between flex-wrap gap-3">
<flux:modal.trigger name="confirm-contact-deletion">
<flux:button
variant="danger"
@ -322,7 +338,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
<flux:modal name="confirm-contact-deletion" class="max-w-lg">

View file

@ -391,55 +391,73 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
}
}; ?>
<div class="space-y-6">
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakte') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Pressekontakte aller Firmen über alle Portale hinweg.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
<flux:button variant="primary" icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
{{ __('Neuer Kontakt') }}
</flux:button>
@else
<flux:button variant="primary" icon="plus" disabled>
{{ __('Neuer Kontakt') }}
</flux:button>
@endif
</div>
</header>
@if ($notification)
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3000)" x-show="show" x-transition
class="rounded-md px-4 py-3 text-sm border
{{ $notificationType === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-300'
: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-300' }}">
@class([
'px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2',
'bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]' => $notificationType === 'error',
'bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]' => $notificationType !== 'error',
])>
@if ($notificationType === 'error')
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0" />
@else
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
@endif
{{ $notification }}
</div>
@endif
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</div>
<flux:icon.user-group class="size-8 text-blue-500" />
</div>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('Pressekontakte') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Firmen mit Kontakten')" :value="number_format($stats['companies_with_contacts'])">
<x-slot:meta>{{ __('aktiv versorgt') }}</x-slot:meta>
<x-slot:trend>{{ __('mind. ein Kontakt') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Ø pro Firma')" :value="number_format($stats['avg_per_company'], 1)">
<x-slot:meta>{{ __('Pflegegrad') }}</x-slot:meta>
<x-slot:trend>{{ __('Kontakte / Firma') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Firmen mit Kontakten') }}
</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['companies_with_contacts'] }}</flux:text>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<flux:icon.building-office class="size-8 text-green-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Ø pro Firma') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['avg_per_company'], 1) }}
</flux:text>
</div>
<flux:icon.chart-bar class="size-8 text-purple-500" />
</div>
</flux:card>
</div>
{{-- Filter & Actions --}}
<flux:card>
<div class="flex flex-col gap-4">
<div class="p-5 flex flex-col gap-4">
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<flux:input wire:model.live.debounce.300ms="search"
placeholder="{{ __('Name, Email oder Firma suchen...') }}" icon="magnifying-glass" class="flex-1" />
@ -527,31 +545,26 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
<flux:button icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
{{ __('Neuer Kontakt') }}
</flux:button>
@else
<flux:button icon="plus" disabled>
{{ __('Neuer Kontakt') }}
</flux:button>
@endif
</div>
</div>
</flux:card>
</article>
<flux:card>
{{-- ============== PRESET-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter-Presets') }}</span>
</div>
<div class="p-5 space-y-3">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-1 gap-3">
<flux:input wire:model="presetName" placeholder="{{ __('Neues Preset speichern...') }}"
class="flex-1" />
<flux:button wire:click="savePreset" variant="subtle" icon="bookmark">
<flux:button wire:click="savePreset" variant="ghost" icon="bookmark">
{{ __('Preset speichern') }}
</flux:button>
</div>
<div class="flex gap-3">
<div class="flex gap-2 flex-wrap">
<flux:select wire:model="selectedPresetId" class="w-64">
<option value="">{{ __('Preset auswählen') }}</option>
@foreach ($presets as $preset)
@ -565,11 +578,18 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
<flux:button wire:click="deletePreset" variant="danger">{{ __('Löschen') }}</flux:button>
</div>
</div>
<flux:error name="presetName" class="mt-3" />
</flux:card>
<flux:error name="presetName" />
</div>
</article>
{{-- Tabelle --}}
<flux:card class="overflow-hidden">
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Kontakte') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $contacts->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
@ -614,43 +634,40 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
</div>
</flux:table.cell>
<flux:table.cell>
<div>
<flux:text weight="semibold truncate">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $contactDisplayName }}
</flux:text>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm">
<div class="space-y-0.5">
<div class="text-[12.5px]">
<a href="mailto:{{ $contact->email }}"
class="text-blue-600 hover:underline dark:text-blue-400">
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $contact->email ?: __('Keine E-Mail') }}
</a>
</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $contact->phone ?: __('Kein Telefon') }}
</flux:text>
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $contact->phone ?: __('Kein Telefon') }}
</div>
</div>
</flux:table.cell>
<flux:table.cell>
@if ($contact->company && \Illuminate\Support\Facades\Route::has('admin.companies.show'))
<a href="{{ route('admin.companies.show', $contact->company_id) }}" wire:navigate
class="text-blue-600 hover:underline dark:text-blue-400">
class="text-[12.5px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ \Illuminate\Support\Str::limit($contact->company->name, 60) }}
</a>
@else
<flux:text>
<span class="text-[12.5px] text-[color:var(--color-ink)]">
{{ \Illuminate\Support\Str::limit($contact->company?->name ?? __('Unbekannte Firma'), 80) }}
</flux:text>
</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:badge color="{{ $this->portalBadgeColor($contact->portal) }}" size="sm">
{{ $contact->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
<span class="badge hub">{{ $contact->portal?->label() ?? __('Unbekannt') }}</span>
</flux:table.cell>
<flux:table.cell>
@ -664,14 +681,14 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
{{ $contact->press_releases_count }} {{ __('PMs') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge dot">0</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $contact->created_at?->format('d.m.Y H:i') ?? '-' }}
</flux:text>
</span>
</flux:table.cell>
<flux:table.cell>
@ -712,9 +729,14 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
@empty
<flux:table.row>
<flux:table.cell colspan="8">
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.user-group class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Kontakte gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.user-group class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Kontakte gefunden') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@ -722,8 +744,8 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
</flux:table.rows>
</flux:table>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $contacts->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -12,32 +12,37 @@ new #[Layout('components.layouts.app'), Title('Gutscheine')] class extends Compo
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Gutscheine') }}</flux:heading>
<flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge warn">{{ __('Vertagt') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Gutscheine') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Coupons sind in der Initialmigration vertagt (Entscheidung D-16). Eine Wiedereinführung wird später separat evaluiert ggf. direkt über Stripe-Coupons.') }}
</flux:subheading>
</p>
</div>
<flux:badge color="zinc" icon="pause" size="lg">
{{ __('Vertagt') }}
</flux:badge>
</div>
</flux:card>
</header>
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Hinweise') }}</flux:heading>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Hinweise') }}</span>
</div>
<ul class="p-5 space-y-3 text-[12.5px] text-[color:var(--color-ink-2)] list-none m-0">
<li class="flex gap-2">
<flux:icon.information-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.information-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<span>{{ __('Im neuen Stack sind keine eigenen Coupon-Tabellen vorgesehen. Sobald wieder benötigt, werden Coupons als Stripe-Coupons abgebildet.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.information-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.information-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<span>{{ __('Bestehende Legacy-Gutscheine werden nicht migriert Bestandskunden behalten ihre Konditionen über das Grandfathering-Modell (P8.8).') }}</span>
</li>
</ul>
</flux:card>
</article>
</div>

View file

@ -75,31 +75,35 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Footer-Code anlegen') }}</flux:heading>
<flux:subheading>
{{ __('Snippet, das unter Pressemitteilungen ausgespielt wird.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Footer-Codes') }}</span>
</div>
<flux:button
variant="ghost"
icon="arrow-left"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Footer-Code anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Snippet, das unter Pressemitteilungen ausgespielt wird.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Stammdaten') }}</flux:heading>
<div class="mt-4 space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Stammdaten') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model="title"
:label="__('Titel')"
@ -136,12 +140,13 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
/>
</div>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg">{{ __('Sichtbarkeit') }}</flux:heading>
<div class="mt-4 space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Sichtbarkeit') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:switch
wire:model.live="isGlobal"
:label="__('Global ausspielen')"
@ -154,48 +159,48 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
:description="__('Inaktive Codes werden niemals ausgespielt.')"
/>
</div>
</flux:card>
</article>
@if (! $isGlobal)
<flux:card>
<flux:heading size="lg">{{ __('Kategorie-Zuordnung') }}</flux:heading>
<flux:subheading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kategorie-Zuordnung') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nur Pressemitteilungen in diesen Kategorien zeigen den Footer-Code.') }}
</flux:subheading>
</p>
<div class="mt-4 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
@forelse ($categoryOptions as $option)
<label class="flex items-center gap-2 rounded border border-zinc-200 px-3 py-2 dark:border-zinc-700">
<label class="flex items-center gap-2 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-3 py-2 hover:border-[color:var(--color-hub)]/30 cursor-pointer">
<input
type="checkbox"
wire:model="categoryIds"
value="{{ $option['id'] }}"
class="rounded border-zinc-300"
class="rounded border-[color:var(--color-bg-rule)]"
/>
<span class="text-sm">{{ $option['name'] }}</span>
<span class="text-[12.5px] text-[color:var(--color-ink)]">{{ $option['name'] }}</span>
</label>
@empty
<flux:text class="text-zinc-500">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Keine Kategorien vorhanden.') }}
</flux:text>
</p>
@endforelse
</div>
</flux:card>
</div>
</article>
@endif
<flux:card>
<div class="flex items-center justify-end gap-2">
<flux:button
variant="ghost"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<article class="panel">
<div class="p-5 flex items-center justify-end gap-2">
<flux:button variant="ghost" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
{{ __('Speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>
</div>

View file

@ -106,29 +106,42 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Footer-Code bearbeiten') }}</flux:heading>
<flux:subheading>#{{ $id }} {{ $title }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Footer-Codes') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
@if ($isGlobal)
<span class="badge hub dot">{{ __('Global') }}</span>
@endif
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<flux:button
variant="ghost"
icon="arrow-left"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ __('Footer-Code bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">{{ $title }}</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Stammdaten') }}</flux:heading>
<div class="mt-4 space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Stammdaten') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="title" :label="__('Titel')" />
<flux:textarea
@ -159,12 +172,13 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
/>
</div>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg">{{ __('Sichtbarkeit') }}</flux:heading>
<div class="mt-4 space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Sichtbarkeit') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:switch
wire:model.live="isGlobal"
:label="__('Global ausspielen')"
@ -176,34 +190,38 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
:label="__('Aktiv')"
/>
</div>
</flux:card>
</article>
@if (! $isGlobal)
<flux:card>
<flux:heading size="lg">{{ __('Kategorie-Zuordnung') }}</flux:heading>
<div class="mt-4 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kategorie-Zuordnung') }}</span>
</div>
<div class="p-5 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
@forelse ($categoryOptions as $option)
<label class="flex items-center gap-2 rounded border border-zinc-200 px-3 py-2 dark:border-zinc-700">
<label class="flex items-center gap-2 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-3 py-2 hover:border-[color:var(--color-hub)]/30 cursor-pointer">
<input
type="checkbox"
wire:model="categoryIds"
value="{{ $option['id'] }}"
class="rounded border-zinc-300"
class="rounded border-[color:var(--color-bg-rule)]"
/>
<span class="text-sm">{{ $option['name'] }}</span>
<span class="text-[12.5px] text-[color:var(--color-ink)]">{{ $option['name'] }}</span>
</label>
@empty
<flux:text class="text-zinc-500">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Keine Kategorien vorhanden.') }}
</flux:text>
</p>
@endforelse
</div>
</flux:card>
</article>
@endif
<flux:card>
<div class="flex items-center justify-between gap-2">
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone & Aktionen') }}</span>
</div>
<div class="p-5 flex items-center justify-between gap-2 flex-wrap">
<flux:button
type="button"
variant="danger"
@ -215,11 +233,7 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
</flux:button>
<div class="flex items-center gap-2">
<flux:button
variant="ghost"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<flux:button variant="ghost" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
@ -227,6 +241,6 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
</div>

View file

@ -76,80 +76,77 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
@if(session('error'))
<flux:callout color="red" icon="exclamation-triangle">{{ session('error') }}</flux:callout>
@endif
<flux:card>
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<flux:heading size="xl">{{ __('Footer-Codes') }}</flux:heading>
<flux:subheading>
{{ __('Snippets, die unter Pressemitteilungen ausgespielt werden.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Operations') }}</span>
</div>
<flux:button
variant="primary"
icon="plus"
:href="route('admin.footer-codes.create')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Footer-Codes') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Snippets, die unter Pressemitteilungen ausgespielt werden.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" :href="route('admin.footer-codes.create')" wire:navigate>
{{ __('Footer-Code anlegen') }}
</flux:button>
</div>
</flux:card>
</header>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['total'] }}</flux:text>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
<flux:icon.code-bracket-square class="size-8 text-blue-500" />
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-err)]" />
<div class="flex-1">{{ session('error') }}</div>
</div>
</flux:card>
@endif
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Aktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['active'] }}</flux:text>
</div>
<flux:icon.bolt class="size-8 text-green-500" />
</div>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($totals['total'])">
<x-slot:meta>{{ __('Snippets') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($totals['active'])">
<x-slot:meta>{{ __('live ausgespielt') }}</x-slot:meta>
<x-slot:trend>{{ __('in PMs sichtbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Global')" :value="number_format($totals['global'])">
<x-slot:meta>{{ __('portalübergreifend') }}</x-slot:meta>
<x-slot:trend>{{ __('ohne Kategorie-Bindung') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Global') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['global'] }}</flux:text>
{{-- ============== FILTER ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<flux:icon.globe-alt class="size-8 text-purple-500" />
</div>
</flux:card>
</div>
<flux:card>
<div class="grid gap-3 md:grid-cols-[1fr,auto,auto]">
<div class="p-5 grid gap-3 md:grid-cols-[1fr,auto,auto]">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Titel oder Inhalt suchen…') }}"
icon="magnifying-glass"
/>
<flux:select wire:model.live="portalFilter">
<option value="all">{{ __('Alle Portale') }}</option>
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="statusFilter">
<option value="all">{{ __('Alle Status') }}</option>
<option value="active">{{ __('Aktiv') }}</option>
@ -157,9 +154,16 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
<option value="global">{{ __('Global') }}</option>
</flux:select>
</div>
</flux:card>
</article>
<flux:card>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Footer-Codes') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $codes->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Titel') }}</flux:table.column>
@ -177,29 +181,35 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
<flux:table.cell>
<div class="flex items-center gap-2">
@if ($code->is_global)
<flux:badge color="purple" size="xs" icon="globe-alt">{{ __('Global') }}</flux:badge>
<span class="badge hub dot">{{ __('Global') }}</span>
@endif
<span class="font-medium">{{ $code->title }}</span>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $code->title }}</span>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="zinc" size="sm">{{ $code->portal->label() }}</flux:badge>
<span class="badge hub">{{ $code->portal->label() }}</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-2)] font-mono">
{{ $code->language ? strtoupper($code->language) : '' }}
</span>
</flux:table.cell>
<flux:table.cell>
@if ($code->is_global)
<span class="text-zinc-500">{{ __('alle') }}</span>
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('alle') }}</span>
@else
{{ $code->categories_count }}
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->categories_count }}</span>
@endif
</flux:table.cell>
<flux:table.cell>{{ $code->priority }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$code->is_active ? 'green' : 'zinc'" size="sm">
{{ $code->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->priority }}</span>
</flux:table.cell>
<flux:table.cell>
@if ($code->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
</flux:table.cell>
<flux:table.cell align="end">
<div class="flex items-center justify-end gap-1">
@ -224,17 +234,23 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
@empty
<flux:table.row>
<flux:table.cell :colspan="7">
<div class="py-8 text-center text-zinc-500">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.code-bracket-square class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Footer-Codes gefunden.') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<div class="mt-4">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $codes->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -120,58 +120,68 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Legacy Rechnungen') }}</flux:heading>
<flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge hub">{{ __('Legacy-Rechnungsarchiv') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Legacy Rechnungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Legacy-Rechnungsarchiv mit read-only Übersicht, Filtern und PDF-Download. Der neue Stripe-Rechnungslauf folgt separat in Phase 8.') }}
</flux:subheading>
</p>
</div>
<flux:badge color="zinc" icon="archive-box" size="lg">
{{ __('Legacy-Archiv') }}
</flux:badge>
</div>
</flux:card>
</header>
@if ($stats['unmapped_count'] > 0)
<flux:callout color="yellow" icon="exclamation-triangle">
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __(':count Legacy-Rechnungen konnten keinem neuen User zugeordnet werden. Sie bleiben im Archiv sichtbar und sollten im Rehearsal-Report fachlich geprüft werden.', ['count' => number_format($stats['unmapped_count'], 0, ',', '.')]) }}
</flux:callout>
@endif
<div class="grid grid-cols-2 gap-4 lg:grid-cols-5">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Rechnungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['count'], 0, ',', '.') }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Archivsumme') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['total_cents'] / 100, 2, ',', '.') }} </flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Bezahlt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['paid_count'], 0, ',', '.') }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Ohne User') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['unmapped_count'], 0, ',', '.') }}</flux:text>
</flux:card>
@if($supportsPdfGeneratedAt)
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('PDF erzeugt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['generated_pdf_count'], 0, ',', '.') }}</flux:text>
</flux:card>
@else
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('PDF-Status') }}</flux:text>
<flux:text size="xl" weight="bold">{{ __('Migration offen') }}</flux:text>
</flux:card>
@endif
</div>
</div>
@endif
<flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-5">
<x-portal.stat-card variant="primary" :label="__('Rechnungen')" :value="number_format($stats['count'], 0, ',', '.')">
<x-slot:meta>{{ __('Archivdatensätze') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Archivsumme')" :value="number_format($stats['total_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('historisch') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bezahlt')" :value="number_format($stats['paid_count'], 0, ',', '.')">
<x-slot:meta>{{ __('mit Zahldatum') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Ohne User')" :value="number_format($stats['unmapped_count'], 0, ',', '.')">
<x-slot:meta>{{ __('zu mappen') }}</x-slot:meta>
</x-portal.stat-card>
@if ($supportsPdfGeneratedAt)
<x-portal.stat-card variant="ok" :label="__('PDF erzeugt')" :value="number_format($stats['generated_pdf_count'], 0, ',', '.')">
<x-slot:meta>{{ __('aus Archiv') }}</x-slot:meta>
</x-portal.stat-card>
@else
<x-portal.stat-card variant="muted" :label="__('PDF-Status')" :value="__('Migration offen')">
<x-slot:meta>{{ __('Schema-Update fehlt') }}</x-slot:meta>
</x-portal.stat-card>
@endif
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
<flux:button size="sm" variant="ghost" icon="arrow-path" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</div>
<div class="p-5 space-y-4">
<div class="grid gap-3 lg:grid-cols-6">
<flux:input
wire:model.live.debounce.300ms="search"
@ -209,18 +219,20 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
</flux:select>
</div>
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:text class="text-sm text-zinc-500">
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __(':count Treffer für die aktuelle Filterung. PDF-Dateien werden bei Bedarf aus den archivierten Legacy-Daten erzeugt.', ['count' => number_format($stats['filtered_count'], 0, ',', '.')]) }}
</flux:text>
<flux:button size="sm" variant="ghost" icon="arrow-path" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</p>
</div>
</flux:card>
</article>
<flux:card class="p-0">
<div class="p-4">
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Legacy-Rechnungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Treffer', ['count' => number_format($stats['filtered_count'], 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Rechnungsnr.') }}</flux:table.column>
@ -235,17 +247,17 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
@forelse ($invoices as $invoice)
<flux:table.row wire:key="admin-legacy-invoice-{{ $invoice->id }}">
<flux:table.cell>
<div class="space-y-1">
<flux:text weight="semibold">{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}</flux:text>
<flux:text class="text-xs text-zinc-500">Legacy-ID: {{ $invoice->legacy_id }}</flux:text>
<div class="space-y-0.5">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">Legacy-ID: {{ $invoice->legacy_id }}</div>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="zinc">{{ $invoice->legacy_portal?->label() }}</flux:badge>
<span class="badge hub">{{ $invoice->legacy_portal?->label() }}</span>
</flux:table.cell>
<flux:table.cell>
@if ($invoice->user)
<div class="space-y-1">
<div class="space-y-0.5">
<flux:button
size="xs"
variant="ghost"
@ -254,28 +266,30 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
>
{{ $invoice->user->name }}
</flux:button>
<flux:text class="text-xs text-zinc-500">{{ $invoice->user->email }}</flux:text>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $invoice->user->email }}</div>
</div>
@else
<div class="space-y-1">
<flux:badge size="sm" color="yellow">{{ __('Ohne Zuordnung') }}</flux:badge>
<flux:text class="text-xs text-zinc-500">Legacy-User: {{ $invoice->legacy_user_id ?? 'n/a' }}</flux:text>
<span class="badge warn dot">{{ __('Ohne Zuordnung') }}</span>
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">Legacy-User: {{ $invoice->legacy_user_id ?? 'n/a' }}</div>
</div>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text weight="semibold">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} </flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} </span>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="{{ $invoice->paid_at ? 'green' : 'yellow' }}">
{{ $invoice->status ?? ($invoice->paid_at ? __('Bezahlt') : __('Offen')) }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm text-zinc-500">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</flux:text>
@if ($invoice->paid_at)
<flux:text class="text-xs text-zinc-500">{{ __('bezahlt: :date', ['date' => $invoice->paid_at->format('d.m.Y')]) }}</flux:text>
<span class="badge ok dot">{{ $invoice->status ?? __('Bezahlt') }}</span>
@else
<span class="badge warn dot">{{ $invoice->status ?? __('Offen') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="space-y-0.5">
<div class="text-[12px] text-[color:var(--color-ink-2)]">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</div>
@if ($invoice->paid_at)
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('bezahlt: :date', ['date' => $invoice->paid_at->format('d.m.Y')]) }}</div>
@endif
</div>
</flux:table.cell>
@ -291,7 +305,7 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
{{ __('Öffnen') }}
</flux:button>
@if ($supportsPdfGeneratedAt && $invoice->pdf_generated_at)
<flux:badge size="sm" color="green">{{ __('erzeugt') }}</flux:badge>
<span class="badge ok">{{ __('erzeugt') }}</span>
@endif
</div>
</flux:table.cell>
@ -299,16 +313,22 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
@empty
<flux:table.row>
<flux:table.cell colspan="7">
<div class="flex flex-col items-center justify-center py-10">
<flux:icon.document-text class="size-10 text-zinc-300" />
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Legacy-Rechnungen für diese Filter gefunden.') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.document-text class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Legacy-Rechnungen für diese Filter gefunden.') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</div>
</flux:card>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $invoices->links() }}
</div>
</article>
</div>

View file

@ -86,86 +86,86 @@ new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-start justify-between gap-4">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Newsletter Synchronisierung') }}</flux:heading>
<flux:text class="text-zinc-500 dark:text-zinc-400">
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Newsletter') }}</span>
@if ($syncConfig['enabled'])
<span class="badge ok dot">{{ __('Sync aktiv') }}</span>
@else
<span class="badge dot">{{ __('Sync deaktiviert') }}</span>
@endif
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Newsletter Synchronisierung') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Vorbereitung fuer die kuenftige externe API-Anbindung. Aktuell ist nur das technische Grundgeruest aktiv.') }}
</flux:text>
</p>
</div>
<div class="flex flex-col items-end gap-2">
@if ($syncConfig['enabled'])
<flux:badge color="green" icon="check" size="sm">{{ __('Sync aktiv') }}</flux:badge>
@else
<flux:badge color="zinc" icon="pause" size="sm">{{ __('Sync deaktiviert') }}</flux:badge>
@endif
<div class="flex gap-2">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button size="sm" variant="ghost" icon="eye" wire:click="triggerDryRun">
{{ __('Dry Run') }}
</flux:button>
<flux:button size="sm" icon="play" wire:click="triggerTestSync">
<flux:button size="sm" variant="primary" icon="play" wire:click="triggerTestSync">
{{ __('Test-Sync ausfuehren') }}
</flux:button>
</div>
</div>
</div>
</flux:card>
</header>
@if ($dryRunMessage)
<flux:card>
<flux:text class="text-sm text-zinc-600 dark:text-zinc-300">{{ $dryRunMessage }}</flux:text>
</flux:card>
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.eye class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">{{ $dryRunMessage }}</div>
</div>
@endif
@if ($syncMessage)
<flux:card>
<flux:text class="text-sm">{{ $syncMessage }}</flux:text>
</flux:card>
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ $syncMessage }}
</div>
@endif
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('Subscriptions') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bestaetigt')" :value="number_format($stats['confirmed'])">
<x-slot:meta>{{ __('Double Opt-in') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Unbestaetigt')" :value="number_format($stats['pending'])">
<x-slot:meta>{{ __('warten auf Opt-in') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Abgemeldet')" :value="number_format($stats['unsubscribed'])">
<x-slot:meta>{{ __('Unsubscribed') }}</x-slot:meta>
</x-portal.stat-card>
</section>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Bestaetigt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['confirmed'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Unbestaetigt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['pending'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Abgemeldet') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['unsubscribed'] }}</flux:text>
</flux:card>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Konfiguration') }}</span>
</div>
<flux:card>
<flux:heading size="sm">{{ __('Konfiguration') }}</flux:heading>
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<dl class="p-5 grid grid-cols-1 gap-4 sm:grid-cols-2 text-[12.5px]">
<div>
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Provider') }}</flux:text>
<flux:text class="mt-1">{{ $syncConfig['provider'] }}</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Provider') }}</dt>
<dd class="text-[color:var(--color-ink)] font-mono">{{ $syncConfig['provider'] }}</dd>
</div>
<div>
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Timeout') }}</flux:text>
<flux:text class="mt-1">{{ $syncConfig['timeout'] }}s</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Timeout') }}</dt>
<dd class="text-[color:var(--color-ink)] tabular-nums">{{ $syncConfig['timeout'] }}s</dd>
</div>
<div class="sm:col-span-2">
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Endpoint') }}</flux:text>
<flux:text class="mt-1 break-all">{{ $syncConfig['endpoint'] }}</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Endpoint') }}</dt>
<dd class="text-[color:var(--color-ink)] font-mono break-all">{{ $syncConfig['endpoint'] }}</dd>
</div>
</div>
</flux:card>
</dl>
</article>
</div>

View file

@ -12,42 +12,47 @@ new #[Layout('components.layouts.app'), Title('Zahlungen')] class extends Compon
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Zahlungen') }}</flux:heading>
<flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge warn">{{ __('In Vorbereitung') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Zahlungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Zahlungsabwicklung läuft in Phase 8 ausschließlich über Stripe alte Zahlungsarten (Rechnung, PayPal, SPK Berlin, Cortal Consors, Bar/Post) entfallen komplett.') }}
</flux:subheading>
</p>
</div>
<flux:badge color="amber" icon="clock" size="lg">
{{ __('In Vorbereitung') }}
</flux:badge>
</div>
</flux:card>
</header>
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Geplant für P8') }}</flux:heading>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Geplant für P8') }}</span>
</div>
<div class="p-5 space-y-4">
<ul class="space-y-3 text-[12.5px] text-[color:var(--color-ink-2)] list-none m-0">
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Live-Anzeige aller Stripe-Zahlungen mit Filtern nach Status, Methode und Zeitraum.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Detail-Ansicht mit Stripe-Transaktions-ID, Webhook-Trail und zugeordneter Rechnung.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Refund-Workflow direkt aus dem Admin (sofern Stripe-Berechtigung gegeben).') }}</span>
</li>
</ul>
<flux:separator class="my-5" />
<flux:text class="text-sm text-zinc-500">
<p class="pt-4 border-t border-[color:var(--color-bg-rule)] text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Datenmodell (user_payments, user_payment_options) ist bereits angelegt; die Anbindung folgt mit Stripe-Webhooks.') }}
</flux:text>
</flux:card>
</p>
</div>
</article>
</div>

View file

@ -42,24 +42,36 @@ new class extends Component
}; ?>
<div class="px-1 py-2">
<div class="mb-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider px-2">
<div class="mb-2 px-2 text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Portal-Filter') }}
</div>
<div class="flex flex-col gap-1">
<div class="flex flex-col gap-0.5">
<button
wire:click="switchPortal('')"
class="flex items-center gap-2 rounded px-2 py-1.5 text-sm transition-colors {{ $activePortal === '' ? 'bg-zinc-200 dark:bg-zinc-700 font-medium' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
@class([
'flex items-center gap-2 rounded-[4px] px-2 py-1.5 text-[12.5px] transition-colors',
'bg-[color:var(--color-bg-elev)] font-semibold text-[color:var(--color-ink)] border border-[color:var(--color-bg-rule)]' => $activePortal === '',
'text-[color:var(--color-ink-2)] hover:bg-[color:var(--color-bg-elev)]/60' => $activePortal !== '',
])
>
<span class="h-2 w-2 rounded-full bg-zinc-400"></span>
<span class="h-2 w-2 rounded-full bg-[color:var(--color-ink-3)]"></span>
{{ __('Alle Portale') }}
</button>
@foreach ($portals as $portal)
@if ($portal !== \App\Enums\Portal::Both)
<button
wire:click="switchPortal('{{ $portal->value }}')"
class="flex items-center gap-2 rounded px-2 py-1.5 text-sm transition-colors {{ $activePortal === $portal->value ? 'bg-zinc-200 dark:bg-zinc-700 font-medium' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
@class([
'flex items-center gap-2 rounded-[4px] px-2 py-1.5 text-[12.5px] transition-colors',
'bg-[color:var(--color-bg-elev)] font-semibold text-[color:var(--color-ink)] border border-[color:var(--color-bg-rule)]' => $activePortal === $portal->value,
'text-[color:var(--color-ink-2)] hover:bg-[color:var(--color-bg-elev)]/60' => $activePortal !== $portal->value,
])
>
<span class="h-2 w-2 rounded-full {{ $portal === \App\Enums\Portal::Presseecho ? 'bg-green-500' : 'bg-red-500' }}"></span>
<span @class([
'h-2 w-2 rounded-full',
'bg-[color:var(--color-ok)]' => $portal === \App\Enums\Portal::Presseecho,
'bg-[color:var(--color-err)]' => $portal !== \App\Enums\Portal::Presseecho,
])></span>
{{ $portal->label() }}
</button>
@endif

View file

@ -50,23 +50,33 @@ new #[Layout('components.layouts.app'), Title('Neue Voreinstellung')] class exte
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Neue Voreinstellung') }}</flux:heading>
<flux:subheading>{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Voreinstellungen') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Neue Voreinstellung') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@include('livewire.admin.presets.partials.form-fields')
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -74,5 +84,5 @@ new #[Layout('components.layouts.app'), Title('Neue Voreinstellung')] class exte
{{ __('Voreinstellung erstellen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -71,23 +71,34 @@ new #[Layout('components.layouts.app'), Title('Voreinstellung bearbeiten')] clas
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Voreinstellung bearbeiten') }}</flux:heading>
<flux:subheading>{{ __('ID') }}: {{ $id }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Voreinstellungen') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Voreinstellung bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@include('livewire.admin.presets.partials.form-fields')
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -95,5 +106,5 @@ new #[Layout('components.layouts.app'), Title('Voreinstellung bearbeiten')] clas
{{ __('Änderungen speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

Some files were not shown because too many files have changed in this diff Show more