M2: Status & Wochentage – Schritt 2 (UI): Tagesansicht, htmx-CheckOff/Uncheck-Partials, Archiv-Tab + Feature-Tests

This commit is contained in:
2026-09-19 11:46:15 +02:00
parent 3519ff08f2
commit ce77785003
8 changed files with 340 additions and 46 deletions
+82 -6
View File
@@ -6,25 +6,30 @@ namespace App\UI\Http\Essen;
use App\Logic\Essen\Dto\MahlzeitCreateRequest;
use App\Logic\Essen\Dto\MahlzeitListRequest;
use App\Logic\Essen\Dto\MahlzeitResponse;
use App\Logic\Essen\Dto\MahlzeitUpdateRequest;
use App\Logic\Essen\Manager\MahlzeitManagerInterface;
use App\Logic\Essen\Model\Mahlzeit;
use App\Logic\Essen\Model\MahlzeitStatus;
use App\Logic\Essen\Model\Wochentag;
use App\Logic\Essen\Query\MahlzeitListQuery;
use App\Logic\Essen\UseCase\MahlzeitCheckOffUseCase;
use App\Logic\Essen\UseCase\MahlzeitCreateUseCase;
use App\Logic\Essen\UseCase\MahlzeitDeleteUseCase;
use App\Logic\Essen\UseCase\MahlzeitUncheckUseCase;
use App\Logic\Essen\UseCase\MahlzeitUpdateUseCase;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* CRUD-Slice "Mahlzeit" (M1) – Web-UI.
*
* Stil: klassisches POST/Redirect/Flash (htmx-Toggles kommen in M2).
* CRUD-Slice "Mahlzeit" (M1) + Status & Tagesansicht (M2) – Web-UI.
*
* - Reads über `MahlzeitListQuery` / `MahlzeitManagerInterface::find`.
* - Writes über die UseCases (Create/Update/Delete).
* - Writes über die UseCases (Create/Update/Delete/CheckOff/Uncheck).
* - M1: klassisches POST/Redirect/Flash (Anlegen/Bearbeiten/Löschen).
* - M2: Check-Off als htmx-Toggle – die Endpunkte liefern nur das geänderte
* Mahlzeiten-Partial zurück (Fragment, kein Voll-HTML).
* - Syntaktische Pflichtfeld-Prüfung (Name) passiert hier im UI-Layer
* (rein PHP, s. architektur.md §1.13); fachliche Invarianten bleiben
* im UseCase/Model und laufen zentral über den ExceptionSubscriber.
@@ -35,15 +40,30 @@ final class MahlzeitController extends AbstractController
private readonly MahlzeitCreateUseCase $create,
private readonly MahlzeitUpdateUseCase $update,
private readonly MahlzeitDeleteUseCase $delete,
private readonly MahlzeitCheckOffUseCase $checkOff,
private readonly MahlzeitUncheckUseCase $uncheck,
private readonly MahlzeitListQuery $list,
private readonly MahlzeitManagerInterface $mahlzeiten,
) {}
/** GET /mahlzeiten — Planungsliste. */
/** GET /mahlzeiten — Tagesansicht: alle Mahlzeiten nach Wochentag gruppiert. */
public function index(): Response
{
$mahlzeiten = $this->list->execute(new MahlzeitListRequest())->items;
return $this->render('mahlzeiten/index.html.twig', [
'mahlzeiten' => $this->list->execute(new MahlzeitListRequest())->items,
'tage' => $this->gruppierenNachTag($mahlzeiten),
'total' => \count($mahlzeiten),
]);
}
/** GET /mahlzeiten/archiv — Archiv: gegessene Mahlzeiten. */
public function archiv(): Response
{
$mahlzeiten = $this->list->execute(new MahlzeitListRequest(status: MahlzeitStatus::Gegessen))->items;
return $this->render('mahlzeiten/archiv.html.twig', [
'mahlzeiten' => $mahlzeiten,
]);
}
@@ -118,6 +138,31 @@ final class MahlzeitController extends AbstractController
return $this->redirectToRoute('mahlzeiten_index');
}
/**
* POST /mahlzeiten/{id}/checkoff — htmx-Partial: abhaken (geplant → gegessen).
*
* Liefert nur das aktualisierte Mahlzeiten-Partial zurück (Fragment), das
* htmx an der Stelle des Klicks austauscht. Wirft 404 bei unbekannter ID.
*/
public function checkoff(Request $request, int $id): Response
{
$mahlzeit = $this->checkOff->execute($id);
return $this->render('mahlzeiten/_mahlzeit.html.twig', ['m' => $mahlzeit]);
}
/**
* POST /mahlzeiten/{id}/uncheck — htmx-Partial: aufheben (gegessen → geplant).
*
* Liefert nur das aktualisierte Mahlzeiten-Partial zurück (Fragment).
*/
public function uncheck(Request $request, int $id): Response
{
$mahlzeit = $this->uncheck->execute($id);
return $this->render('mahlzeiten/_mahlzeit.html.twig', ['m' => $mahlzeit]);
}
/**
* Bindet die Formularfelder (Name/Notiz/Wochentage) aus dem Request.
*
@@ -145,4 +190,35 @@ final class MahlzeitController extends AbstractController
'wochentage_choices' => Wochentag::alle(),
];
}
/**
* Gruppiert Mahlzeiten nach ihren Wochentagen (Mo…So).
*
* Eine Mahlzeit mit mehreren Wochentagen erscheint unter jedem dieser Tage.
* Mahlzeiten ohne Wochentag landen in der Gruppe "Ohne Tag". Leere Gruppen
* werden herausgefiltert; die Reihenfolge folgt Mo…So, danach "Ohne Tag".
*
* @param list<MahlzeitResponse> $mahlzeiten
* @return array<string, list<MahlzeitResponse>>
*/
private function gruppierenNachTag(array $mahlzeiten): array
{
$tage = [];
foreach (Wochentag::alle() as $tag) {
$tage[$tag->name] = [];
}
$tage['Ohne Tag'] = [];
foreach ($mahlzeiten as $m) {
if ($m->wochentage === []) {
$tage['Ohne Tag'][] = $m;
continue;
}
foreach ($m->wochentage as $tag) {
$tage[$tag->name][] = $m;
}
}
return \array_filter($tage, static fn (array $liste) => $liste !== []);
}
}