#93 — Neuer Listentyp "Masterpackliste" mit Checkout zu Todo-Liste #93
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#93
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Story: Neuer Listentyp "Masterpackliste" mit Checkout zu Todo-Liste
As a Vielreisende(r),
I want to eine wiederverwendbare, getaggte Packlisten-Referenzdatenbank pflegen und daraus bei Bedarf gezielt
eine konkrete Packliste für eine bevorstehende Reise generieren,
so that ich nicht bei jeder Reise von Neuem überlegen muss, was ich mitnehmen will, sondern nur die
relevanten Eigenschaften der Reise auswähle.
Depends on:
#89(Listentyp-Vereinheitlichung — "Masterpackliste" ergänzt dessen Typenliste); erweitertaußerdem das bestehende Label-System (
#19) um ein neues "Label-Gruppen"-Konzept (siehe unten).Acceptance criteria:
Masterpackliste (Referenz-Datenmodell)
Climate (Mehrfachwert möglich, z. B. "Cold, Raining"), Prio (Essential/Not Essential/Info), Quantity
(optional, numerisch), Travel Type (Mehrfachwert möglich, z. B. "Camping, Schweigekloster").
#27) — wird für denImport-Sonderfall "Ohne Crew" unten benötigt, ist aber auch allgemein nützlich (z. B. "hier lieber die
Bio-Variante").
Erweiterung des bestehenden Label-Systems (
#19) um ein neues "Label-Gruppen"-Konzept: Labels können absofort optional einer benannten Gruppe zugeordnet werden (z. B. Gruppe "Climate", Gruppe "Travel Type"),
wiederverwendbar auch für künftige ähnliche Fälle statt einer Einmal-Lösung nur für diese Story.
ist reine Referenzdaten, kein Erledigungs-Workflow.
#89).globale Datenbank wie bei
#90's Produktkatalog — jede Masterpackliste ist in sich geschlossen).input/Packliste_template.csvimportiert (Spalten Eigenschaft/Category/Climate/Prio/Quantity/Travel Type,ca. 87 Items), damit der bestehende Bestand nicht manuell neu eingegeben werden muss. Wichtig: Dieser
Import ist ausschließlich für den Account des Menschen (lenny-lennox@hotmail.de), der diese CSV gepflegt
hat — kein generisches Seed-Verhalten für alle Accounts wie beim Demo-Seeding-Muster aus
#80. DieDaten sind persönlich, die App unterstützt beliebige registrierte Nutzer über Einladungslinks, ein
globaler Seed würde fremden Accounts ungefragt private Reisedaten unterschieben.
Wert "Ohne Crew" (z. B.
"All Climate, Ohne Crew"). Das ist inhaltlich keine Klimaangabe, sondern beziehtsich darauf, ob die Reise ohne Verpflegung/Küche durch eine Gruppe stattfindet. Beim Import wird "Ohne
Crew" nicht als Climate-Wert übernommen — stattdessen wird der Text "Ohne Crew" als Kommentar (siehe
oben) am jeweiligen Item gespeichert. Der Checkout-Dialog bietet "Ohne Crew" folglich nicht als
Climate-Filteroption an; betroffene Items behalten nur "All Climate" als Climate-Wert.
Checkout → Todo-Liste
erzeugt (normales Verhalten: Abhaken, Bearbeiten etc. — keine Sonderfunktionen).
Auswahl immer übernommen.
sind mehrfach auswählbar. "Immer" wird nicht zur Auswahl angeboten — Items mit diesem Tag werden
automatisch immer übernommen.
übernommen. "Not Essential" ist eine zusätzliche, separate Auswahl beim Checkout (z. B. Checkbox "auch
optionale Artikel mit aufnehmen").
schneiden") erscheinen nicht als abhakbarer Artikel, sondern als Notiz/Kommentar auf der generierten
Todo-Liste.
Mengenfeld auf
TodoEntity.#80) abgebildet, sodass die neueListe automatisch nach Clothing/Bathroom/etc. gruppiert ist.
eine der gewählten Kombinationen passen.
(einmalige Momentaufnahme beim Checkout).
Out of scope for this story:
Verified against code (2026-08-07):
input/Packliste_template.csventhält 87 Datenzeilen mit den SpaltenEigenschaft,Category,Climate,Prio,Quantity,Travel Type(Spaltenname "Eigenschaft" statt "Name" — inhaltlichidentisch mit der obigen Beschreibung). 8 dieser Zeilen enthalten "Ohne Crew" im Climate-Feld (siehe
Import-Sonderfall oben); die zwei reinen Info-Hinweis-Zeilen wurden korrekt über
Prio = "Not needed / Info"identifiziert (Zeilen "Nicht gebraucht: Buch, Spielsachen …" und "Hinweis: Fingernägel vorher kurz schneiden").
design (
93_masterpackliste_list_type_design.md)Design: Masterpackliste (
#93)Entity shape
Per
#89's explicit "no shared table, noListTypeenum" ruling, Masterpackliste is a full sibling entityset, not a bolt-on to
TodoListEntity:MasterPackingListEntity/MasterPackingListToUserEntity/MasterPackingListInvitationEntity— mirrorShoppingListEntity/ShoppingListToUserEntity/ShoppingListInvitationEntityexactly (full Owner/Membersharing + share-link invitations, since the AC explicitly requires "einheitliches Sharing-Modell aus
#89",unlike Pantry's derived-access model which doesn't apply here — there's no natural parent list to derive
from).
MasterPackItemEntity—Id, MasterPackingListId, Name, Category (CategoryName, plain field, no FK — see below), Priority (enum Essential/NotEssential/Info), Quantity (int?), SortOrder.MasterPackLabelEntity/MasterPackItemLabelEntity(join) — the "Label-Gruppen" extension applied to anew, list-scoped label table rather than generalizing the existing
LabelEntity(which is hard-wired toTodoListId). Turning that FK polymorphic (nullableTodoListId+ nullableMasterPackingListIdwith anexactly-one-set constraint) would touch every existing Label handler/test for a capability the existing Todo
UI doesn't ask for.
MasterPackLabelEntitycarries the same shape (Name,Color, plus a new nullableGroupfield) so a future story can lift the group concept into the generalLabelEntityunchanged — the"reusable for future similar cases" line in the AC is satisfied by an identical, liftable shape, not by a
shared table today.
LabelEntityitself is untouched by this story.MasterPackItemCommentEntity— same reasoning as labels:CommentEntityis keyed on(TodoListId, TodoNr),so a structural sibling scoped to
MasterPackItemIdis added rather than generalizing it.Category on
MasterPackItemEntityis a plainCategoryNamevalue, not aTodoCategoryEntity-shapedper-list managed entity: the AC gives a closed example set and items don't need per-list reordering/custom
category icons the way Todo/Shopping/Pantry categories do. At checkout, distinct
Categoryvalues become realTodoCategoryEntityrows on the generated list via the existingCreateCategoryCommand(same patternImportTodosFromCsvCommandHandleralready uses for its own category auto-creation).Scope cut: no WebSocket live-sync for v1
Every other shared list type got WS broadcasting (
ISubject<Change<...>>) as a follow-up story after itsinitial build (Shopping's own
CreateShoppingListCommandHandlercomment: "no dedicated WebSocket broadcastchannel... yet - deferred to a follow-up story", closed later by
#99). Masterpackliste follows the samestaging: v1 ships with plain request/response CRUD (frontend refetches after each mutation, same as
Shopping/Pantry did before their own WS stories landed). Given the roadmap's own framing of this feature
("Masterpackliste nur bei Reisen" — infrequent, low-concurrency use), this is a reasonable v1 cut, not a
correctness gap. A follow-up story can add live sync exactly the way
#99did for Shopping, if it turns out tomatter in practice.
Checkout logic (
CheckoutMasterPackingListCommand)Input:
MasterPackingListId, selectedClimatenames, selectedTravelTypenames,IncludeNotEssential: bool.Inclusion per item (independent gates):
has a label matching one of the selected climates.
has a label matching one of the selected travel types.
Essentialalways passes;NotEssentialpasses only ifIncludeNotEssential;Infonever becomes a todo (see below).
Included items are grouped by
Category, and the list is built by composing existing single-item commands(
CreateTodoListCommand,CreateCategoryCommandper distinct category,CreateTodoCommandper item) —mirrors
ImportTodosFromCsvCommandHandler's "seed through the normal pipeline" precedent, so Nr/SortOrder/etc.all come out correct for free. Title gets a
" (Nx)"suffix whenQuantity > 1.Info-priority items that pass the Climate/Travel-Type gates are never turned into todos; instead, if atleast one exists, a single non-actionable " Hinweise" todo is created on the new list and each Info item's
text is attached to it via the existing
CreateCommentCommand— reusing#27's comment feature is exactly whycomments are in scope for Masterpack items at all (also needed for the "Ohne Crew" import special-case below).
CSV import — one-time, single-account seed
Not the interactive client-side CSV import from
#83(that parses in the browser and only handles Todo/Shoppingrows) — this is a server-side, startup-time seed gated to one specific, already-registered account
(
lenny-lennox@hotmail.de), matching neither the genericDevDataSeeder(runs for every environment) nor amigration-embedded
INSERT(runs for every existing row/account) — both of those are explicitly the wrongshape per the AC ("kein generisches Seed-Verhalten... ein globaler Seed würde fremden Accounts ungefragt
private Reisedaten unterschieben").
input/Packliste_template.csvis not copied into the Docker build context (Dockerfileonly copiesCommon/,CqsTodo/,CqsTodo.WebApi/), so the file is copied intoCqsTodo/SeedData/masterpackliste.csvand embedded as a compiled resource — baked into
CqsTodo.dllat build time, readable at runtime regardless ofdeployment shape (works identically for
CqsTodo.WebApi's own migrate-and-seed path and the separateCqsTodo.MigrationService, since both reference theCqsTodoassembly).MasterPackingListDemoImporter.SeedAsync(dbContext), called from bothSetup.MigrateAndSeedAsyncandCqsTodo.MigrationService/Worker.cs(alongside, not inside,DevDataSeeder.SeedAsync— this seed isaccount-specific, the other is generic-environment): no-ops if the target user doesn't exist yet, or already
owns a Masterpackliste (idempotency guard, same shape as
DevDataSeeder's own existence check).Import special-cases (verified against the CSV, see story's "Verified against code" note):
"Ohne Crew"is stripped from the item's climate labels and instead becomes aMasterPackItemCommentEntitywith text "Ohne Crew" on that item.Prio = "Not needed / Info"map toMasterPackItemPriority.Info.Frontend
/masterpacking/:masterPackingListId(mirrors/pantry/:pantryId}), newMasterPackingListPage.tsx,MasterPackingListItem.tsx,CreateMasterPackingListDialog.tsx,MasterPackingListCheckoutDialog.tsx.TodoListMenu.tsx'sUnifiedListEntryunion gets a'masterPackingList'variant;AddListFlow.tsxgets a"Masterpackliste" button.
InvitePanel/MembersPanel(already generic two-branch components for'todo' | 'shopping') get a third'masterPacking'branch instead of new components, per their own stated reuse rationale.store.tsState, mirroring the existingpantries/shoppingListsblocks (no per-domain slices in this codebase).
LabelPicker.tsx's toggle-list pattern grouped byGroup; the checkout dialog's multi-select filters reuse the same toggle-button styling.Out of scope (explicit, from the story) / deferred
LabelEntity/TodoLabelPickerUI — the shape is keptidentical on purpose so this is a low-risk follow-up, not a redesign.