diff --git a/app/Livewire/Admin/OpenWork.php b/app/Livewire/Admin/OpenWork.php index 1812eef..db2602f 100644 --- a/app/Livewire/Admin/OpenWork.php +++ b/app/Livewire/Admin/OpenWork.php @@ -24,8 +24,10 @@ class OpenWork extends Component public function render() { return view('livewire.admin.open-work', [ - 'items' => Register::all(), + 'groups' => Register::grouped(), 'counts' => Register::counts(), + 'worst' => Register::worst(), + 'total' => count(Register::all()), ])->title(__('open_work.title')); } } diff --git a/app/Support/OpenWork.php b/app/Support/OpenWork.php index 510e3ab..f03b474 100644 --- a/app/Support/OpenWork.php +++ b/app/Support/OpenWork.php @@ -108,6 +108,55 @@ final class OpenWork ]; } + /** + * Die Zustände nach Dringlichkeit, und das ist keine Geschmacksfrage. + * + * Sortiert wird danach, was ein offener Punkt HEUTE anrichtet: verkauft und + * nicht geliefert kostet in diesem Moment Geld und Vertrauen. Halb gebaut + * kann jederzeit jemandem vor die Füße fallen. Ungeprüft ist ein Risiko, + * das noch niemand gesehen hat. Beschlossen tut gar nichts, bis jemand + * anfängt. + * + * Ohne diese Reihenfolge stand die Liste in der Reihenfolge, in der sie + * geschrieben wurde — also zufällig. + */ + public const ORDER = [self::SOLD, self::PARTIAL, self::UNPROVEN, self::PLANNED]; + + /** + * Nach Zustand gruppiert, dringendste Gruppe zuerst. Leere Gruppen fallen + * weg — eine Überschrift ohne Inhalt liest sich wie ein Fehler. + * + * @return array>> + */ + public static function grouped(): array + { + $groups = []; + + foreach (self::ORDER as $state) { + $items = array_values(array_filter(self::all(), fn (array $i) => $i['state'] === $state)); + + if ($items !== []) { + $groups[$state] = $items; + } + } + + return $groups; + } + + /** Der dringendste Zustand, der gerade vorkommt — oder null, wenn nichts offen ist. */ + public static function worst(): ?string + { + foreach (self::ORDER as $state) { + foreach (self::all() as $item) { + if ($item['state'] === $state) { + return $state; + } + } + } + + return null; + } + /** @return array Zustand => Anzahl */ public static function counts(): array { diff --git a/lang/de/open_work.php b/lang/de/open_work.php index c53022a..bc8bf7f 100644 --- a/lang/de/open_work.php +++ b/lang/de/open_work.php @@ -4,6 +4,8 @@ return [ 'title' => 'Offene Punkte', 'subtitle' => 'Was angefangen und nicht fertig ist — mit dem, was passiert, wenn es so bleibt.', + 'summary' => '{1} Ein offener Punkt.|[2,*] :n offene Punkte.', + 'empty' => 'Nichts offen. Das ist selten und meistens ein Zeichen, dass jemand vergessen hat einzutragen.', 'state' => [ diff --git a/lang/en/open_work.php b/lang/en/open_work.php index dd738db..31f65f3 100644 --- a/lang/en/open_work.php +++ b/lang/en/open_work.php @@ -4,6 +4,8 @@ return [ 'title' => 'Open work', 'subtitle' => 'What was started and is not finished — with what happens if it stays that way.', + 'summary' => '{1} One open item.|[2,*] :n open items.', + 'empty' => 'Nothing open. That is rare, and usually a sign somebody forgot to write it down.', 'state' => [ diff --git a/resources/views/livewire/admin/open-work.blade.php b/resources/views/livewire/admin/open-work.blade.php index 7ff0d4f..a81210e 100644 --- a/resources/views/livewire/admin/open-work.blade.php +++ b/resources/views/livewire/admin/open-work.blade.php @@ -1,61 +1,87 @@ -
- {{-- Kopf wie überall in der Konsole: Augenbraue, Titel, ein Satz darunter. --}} -
-

{{ __('admin.console') }}

-

- {{ __('open_work.title') }} -

-

{{ __('open_work.subtitle') }}

-
+{{-- Gebaut wie die Bereitschaftsseite, und das mit Absicht. - {{-- Die Zählung nach Zustand, ganz oben. Sie beantwortet die einzige Frage, - die man vor der Liste hat: ist etwas dabei, das gerade Geld kostet? --}} - @if ($counts) -
- @foreach (['sold', 'partial', 'unproven', 'planned'] as $state) - @if (($counts[$state] ?? 0) > 0) - - {{ $counts[$state] }} - {{ __('open_work.state.'.$state) }} - - @endif - @endforeach + Die erste Fassung nahm das Muster der Host-Anlegen-Seite: `lbl`-Etikett, + große Kopfzeile, pro Punkt eine fette Karte mit einer Beschreibungsliste + darin. Das ist die Sprache einer Formularseite, nicht die einer Liste mit + Zuständen — und für dieselbe Aufgabe gibt es in dieser Konsole längst eine: + Bereitschaft. Zwei Muster für ein Problem sind zwei Sprachen. + + Also: schlichte Kopfzeile, ein Sammelbanner mit dem dringendsten Zustand, + danach je Zustand eine Karte mit einer `divide-y`-Liste. Ein Punkt ist eine + Zeile mit Symbol, Titel und drei kleinen Angaben darunter — nicht + ein eigener Kasten. --}} + +@php + // Ton je Zustand, an einer Stelle. Nur „verkauft, nicht geliefert" ist rot: + // wenn alles auffällt, fällt nichts auf. + $tone = [ + App\Support\OpenWork::SOLD => ['alert-triangle', 'text-danger'], + App\Support\OpenWork::PARTIAL => ['alert-triangle', 'text-warning'], + App\Support\OpenWork::UNPROVEN => ['shield-check', 'text-muted'], + App\Support\OpenWork::PLANNED => ['bell', 'text-muted'], + ]; +@endphp + +
+
+

{{ __('open_work.title') }}

+

{{ __('open_work.subtitle') }}

+
+ + @if ($groups) + {{-- Ein Satz, der die Frage vor der Liste beantwortet: ist etwas dabei, + das gerade Geld kostet? Der Ton kommt vom dringendsten Zustand, der + wirklich vorkommt — nicht von einer festen Farbe. --}} + @php([$worstIcon, $worstText] = $tone[$worst]) +
+ +

+ {{ trans_choice('open_work.summary', $total, ['n' => $total]) }} +

+
+ + @foreach ($groups as $state => $items) + @php([$icon, $text] = $tone[$state]) +
+
+

{{ __('open_work.state.'.$state) }}

+ {{ count($items) }} +
+

{{ __('open_work.state_hint.'.$state) }}

+ +
    + @foreach ($items as $item) +
  • + + +
    + {{-- Keine Zustands-Plakette an der Zeile: die + Karte darüber sagt den Zustand schon, und + alle Zeilen darin teilen ihn. Sie stand + hier und wiederholte dasselbe Wort dreimal + pro Bildschirm, ohne eine Frage zu + beantworten. --}} +

    {{ $item['title'] }}

    + + {{-- Was fehlt, dann die Folge. Die Folge zuletzt, + weil sie den Satz ist, der über die + Reihenfolge entscheidet — und der letzte + bleibt hängen. --}} +

    {{ $item['missing'] }}

    +

    + {{ __('open_work.risk') }}: {{ $item['risk'] }} +

    +

    {{ $item['where'] }}

    +
    +
  • + @endforeach +
+
+ @endforeach + @else +
+

{{ __('open_work.empty') }}

@endif - - @forelse ($items as $item) -
-
-

{{ $item['title'] }}

- - {{-- „Verkauft, nicht geliefert" bekommt als einziger Zustand die - Warnfarbe. Wenn alles auffällt, fällt nichts auf. --}} - - {{ __('open_work.state.'.$item['state']) }} - -
- -
- @foreach (['where', 'missing', 'risk'] as $field) -
-
{{ __('open_work.'.$field) }}
-
- {{ $item[$field] }} -
-
- @endforeach -
-
- @empty -

- {{ __('open_work.empty') }} -

- @endforelse
diff --git a/tests/Feature/Admin/OpenWorkTest.php b/tests/Feature/Admin/OpenWorkTest.php index 6701719..29e9613 100644 --- a/tests/Feature/Admin/OpenWorkTest.php +++ b/tests/Feature/Admin/OpenWorkTest.php @@ -2,6 +2,7 @@ use App\Support\Navigation; use App\Support\OpenWork; +use Illuminate\Support\Facades\File; /** * Die Seite mit den offenen Punkten. @@ -56,3 +57,43 @@ it('steht jedem offen, der die Konsole öffnen darf', function () { expect($entry)->not->toBeNull() ->and($entry[3])->toBeNull(); }); + +it('stellt das nach vorne, was gerade Schaden anrichtet', function () { + // Nicht in der Reihenfolge, in der es geschrieben wurde — sondern danach, + // was ein Punkt HEUTE anrichtet. „Verkauft, nicht geliefert" kostet in + // diesem Moment Geld; „beschlossen" tut gar nichts, bis jemand anfängt. + expect(array_keys(OpenWork::grouped())) + ->toBe(array_values(array_filter( + OpenWork::ORDER, + fn (string $s) => (OpenWork::counts()[$s] ?? 0) > 0, + ))); + + // Und der Sammelsatz oben nennt den dringendsten Zustand, der wirklich + // vorkommt — nicht einen festen. + expect(OpenWork::worst())->toBe(array_key_first(OpenWork::grouped())); +}); + +it('lässt keine leere Gruppe stehen', function () { + // Eine Überschrift ohne Inhalt liest sich wie ein Ladefehler. Derselbe + // Fehler, der in der Seitenleiste schon einmal zuschlug. + foreach (OpenWork::grouped() as $state => $items) { + expect($items)->not->toBeEmpty("Gruppe {$state} ist leer."); + } +}); + +it('folgt der Formensprache der Bereitschaftsseite, nicht der eines Formulars', function () { + // Die erste Fassung nahm das Muster der Anlegen-Seite: `lbl`-Etikett, + // große Kopfzeile, pro Punkt eine eigene Karte. Für eine Liste mit + // Zuständen gibt es in dieser Konsole längst ein Muster, und zwei Muster + // für ein Problem sind zwei Sprachen. + $page = File::get(resource_path('views/livewire/admin/open-work.blade.php')); + + expect($page)->toContain('divide-y divide-line') + // Und keine Zustands-Plakette je Zeile: die Karte darüber sagt den + // Zustand, alle Zeilen darin teilen ihn. Sie wiederholte dasselbe Wort + // dreimal pro Bildschirm. + ->and($page)->not->toContain('and($page)->not->toContain('lbl">') + ->and($page)->not->toContain('min-[901px]:text-[30px]'); +});