PM-Detailseite: Inhaltspruefung (Gruen/Gelb/Rot) + Qualitaets-Score sichtbar

- Neue Kachel "Inhaltspruefung" in Status & Verlauf (Badge je Ergebnis,
  "Ausstehend" ohne Pruefung); bei Gelb neutraler Hinweis, dass die
  Boost-Platzierung gruen gepruefte Meldungen voraussetzt
- Content-Score-Panel des Editors in gemeinsame Komponente
  x-content-score-panel extrahiert und auf der Detailseite wiederverwendet
  (Score/100, Stufe, Abstand zur naechsten Schwelle)
- PressReleaseClassification::badgeClass() (ok/warn/err)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-13 14:59:33 +00:00
parent 4f2be1e0a8
commit 6f96e4a4cc
5 changed files with 126 additions and 32 deletions

View file

@ -24,4 +24,13 @@ enum PressReleaseClassification: string
self::Red => 'Rot',
};
}
public function badgeClass(): string
{
return match ($this) {
self::Green => 'ok',
self::Yellow => 'warn',
self::Red => 'err',
};
}
}

View file

@ -0,0 +1,42 @@
{{-- Content-Score-Panel (Qualitäts-Feedback): Score 0–100 + Stufe samt
Abstand zur nächsten Schwelle. Gemeinsam genutzt von PM-Editor und
PM-Detailseite (13.07.); `note` trägt den kontextspezifischen Fußtext. --}}
@props(['score', 'tier' => null, 'note' => null])
@php
$tierEnum = $tier instanceof \App\Enums\PressReleaseContentTier
? $tier
: ($tier ? \App\Enums\PressReleaseContentTier::from($tier) : null);
$tiers = config('scoring.content_score.tiers');
$nextThreshold = $score < (int) $tiers['gepruft']
? (int) $tiers['gepruft']
: ($score < (int) $tiers['hochwertig'] ? (int) $tiers['hochwertig'] : null);
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Qualität') }}</span>
@if ($tierEnum)
<span class="badge {{ $tierEnum === \App\Enums\PressReleaseContentTier::Hochwertig ? 'ok' : ($tierEnum === \App\Enums\PressReleaseContentTier::Geprueft ? 'hub' : 'muted') }}">{{ $tierEnum->label() }}</span>
@endif
</div>
<div class="p-4">
<div class="text-[26px] font-bold leading-none text-[color:var(--color-ink)]">
{{ $score }}<span class="text-[14px] font-medium text-[color:var(--color-ink-3)]">/100</span>
</div>
@if ($nextThreshold)
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Noch :points Punkte bis zur nächsten Stufe.', ['points' => $nextThreshold - $score]) }}
</p>
@else
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Höchste Stufe erreicht.') }}
</p>
@endif
@if ($note)
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ $note }}
</p>
@endif
</div>
</article>

View file

@ -809,38 +809,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{-- Content-Score (Qualitäts-Feedback während des Schreibens) --}}
@if (! is_null($contentScore))
@php
$tierEnum = $contentTier ? \App\Enums\PressReleaseContentTier::from($contentTier) : null;
$tiers = config('scoring.content_score.tiers');
$nextThreshold = $contentScore < (int) $tiers['gepruft']
? (int) $tiers['gepruft']
: ($contentScore < (int) $tiers['hochwertig'] ? (int) $tiers['hochwertig'] : null);
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Qualität') }}</span>
@if ($tierEnum)
<span class="badge {{ $tierEnum === \App\Enums\PressReleaseContentTier::Hochwertig ? 'ok' : ($tierEnum === \App\Enums\PressReleaseContentTier::Geprueft ? 'hub' : 'muted') }}">{{ $tierEnum->label() }}</span>
@endif
</div>
<div class="p-4">
<div class="text-[26px] font-bold leading-none text-[color:var(--color-ink)]">
{{ $contentScore }}<span class="text-[14px] font-medium text-[color:var(--color-ink-3)]">/100</span>
</div>
@if ($nextThreshold)
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Noch :points Punkte bis zur nächsten Stufe.', ['points' => $nextThreshold - $contentScore]) }}
</p>
@else
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Höchste Stufe erreicht.') }}
</p>
@endif
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ __('Der Score wird nach dem Speichern automatisch neu berechnet.') }}
</p>
</div>
</article>
<x-content-score-panel :score="$contentScore" :tier="$contentTier"
:note="__('Der Score wird nach dem Speichern automatisch neu berechnet.')" />
@endif
{{-- Portal (Read-only, Badge in Portal-Farbe) --}}

View file

@ -928,6 +928,23 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
</div>
</div>
{{-- Ergebnis der KI-Inhaltsprüfung (13.07.): erklärt sichtbar,
warum die Boost-Platzierung (nur Grün) verfügbar ist oder nicht. --}}
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Inhaltsprüfung') }}</div>
<div class="mt-1.5">
@if ($pr->classification)
<span class="badge {{ $pr->classification->badgeClass() }} dot">{{ $pr->classification->label() }}</span>
@else
<span class="badge muted">{{ __('Ausstehend') }}</span>
@endif
</div>
@if ($pr->classification === \App\Enums\PressReleaseClassification::Yellow)
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-1.5 leading-[1.45]">
{{ __('Die Boost-Platzierung ist grün geprüften Meldungen vorbehalten.') }}
</div>
@endif
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Erstellt') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
@ -1047,6 +1064,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@endif
</div>
</article>
{{-- Content-Score wie im Editor (gemeinsame Komponente, 13.07.). --}}
@if (! is_null($pr->content_score))
<x-content-score-panel :score="$pr->content_score" :tier="$pr->content_tier" />
@endif
</div>
{{-- ============== TITELBILD (Hero) ============== --}}

View file

@ -1,6 +1,8 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseClassification;
use App\Enums\PressReleaseContentTier;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
@ -211,3 +213,52 @@ test('admin show zeigt Boilerplate-Override nicht wenn leer', function () {
LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id])
->assertDontSee('Eigener Abbinder (Boilerplate)');
});
test('customer show zeigt das Pruefergebnis und den Qualitaets-Score', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a([
'status' => PressReleaseStatus::Published->value,
'published_at' => now(),
'classification' => PressReleaseClassification::Green->value,
'content_score' => 87,
'content_tier' => PressReleaseContentTier::Hochwertig->value,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Inhaltsprüfung')
->assertSee('Grün')
->assertSee('Qualität')
->assertSee('87')
->assertSee('Hochwertig')
->assertSee('Höchste Stufe erreicht.');
});
test('customer show erklaert bei Gelb den fehlenden Boost neutral', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a([
'status' => PressReleaseStatus::Published->value,
'published_at' => now(),
'classification' => PressReleaseClassification::Yellow->value,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Inhaltsprüfung')
->assertSee('Gelb')
->assertSee('Die Boost-Platzierung ist grün geprüften Meldungen vorbehalten.');
});
test('customer show zeigt ohne Pruefung Ausstehend und kein Qualitaets-Panel', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a([
'classification' => null,
'content_score' => null,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Inhaltsprüfung')
->assertSee('Ausstehend')
->assertDontSee('Höchste Stufe erreicht.');
});