Skip to content

Sektions-Architektur

Die Editor-Oberfläche ist sektionsgetrieben. Verfügbarkeit, Reihenfolge, Labels, Dirty-Status und Komponentenwahl werden aus Setup-Daten, einem zentralen Descriptor-Register und app-nahen Section-Metadaten zusammengesetzt.

Wenn du eine neue Sektion konkret anlegen willst, starte am besten mit /add-new-section. Diese Seite hier erklärt vor allem das zugrunde liegende Architektur- und Wrapper-Modell.

Sektions-Registry

Die Section-Metadaten sind bewusst auf kleine Dateien verteilt, damit eine neue Standardsektion nicht mehr in mehreren Consumer-Dateien manuell nachgetragen werden muss.

app/utils/sectionDescriptors.ts definiert pro logischer Sektion:

  • kanonischen Runtime-Key
  • Alias-Keys fuer Setup, Backend oder Legacy-Verbraucher
  • Standard-Übersetzungsschlüssel
  • optionale gemeinsame Flags wie globale Templates

app/utils/sectionRuntime.ts ergänzt pro Descriptor:

  • Icon
  • Store-ID
  • Server-Endpunkt
  • Lade-Text

app/utils/sectionComponents.ts hält das Vue-Komponenten-Mapping. app/utils/pdf/section-generators.ts hält das PDF-Generator-Mapping.

app/composables/protocol/useMainAccordionSections.ts und app/utils/pdf/section-registry.ts konsumieren diese Metadaten. Neue Standardsektionen sollen dort nicht mehr als eigene lokale Mapping-Einträge ergänzt werden.

Die aktuelle Descriptor-Registry umfasst IDs wie:

  • meter
  • key
  • room
  • additional_partners
  • bank_details
  • other_remarks
  • termination_reason
  • object_equipment
  • bauliche_veraenderungen
  • tenant_confirmation

Zusätzlich bleiben Runtime-Keys wie bankverbindung, sonstige_bemerkungen, kuendigungsgrund, object_ausstattung und Alias-Keys wie obuilding_changes über dasselbe Descriptor-Register verfügbar.

Setup-Merge

Die im Editor sichtbare Sektionsliste ist nicht nur aus der Registry hart kodiert.

useMainAccordionSections() führt zusammen:

  • das Descriptor-Register für gemeinsame Schlüssel- und Label-Metadaten
  • die Runtime- und Komponenten-Metadaten für Icons, Store-Zuordnung und Komponenten
  • setupStore.sections für serverdefinierte Sektionsdatensätze
  • setupStore.getMergedSection(sectionKey, protocolTypeId) für Überschreibungen je Protokolltyp

Sektionen mit isActive === false werden ausgeschlossen.

Reihenfolge

Die endgültige Reihenfolge kombiniert:

  • serverseitige Sortierung aus setupStore.sections
  • lokale Benutzerreihenfolge aus settingsStore.sectionOrder

Das Composable führt beide Quellen zusammen und behält nur aktuell aktive Sektionen.

Persistenz der offenen Sektion

Die aktuell offene Akkordeon-Sektion wird pro Acceptance-ID über VueUse-Storage gespeichert.

  • Storage-Key: accordion-open-sections
  • Wert: Zuordnung von Acceptance-ID zu Section-Key

Wrapper-Komponenten

Für einfache Collection-Sektionen ist CollectionSection.vue die bevorzugte Hülle. Sie kombiniert useSectionWrapperProps(...), SectionWrapper.vue, SectionTable.vue, den Empty-State und optional den Standard-Add-Footer in einem Baustein.

Direktes Wiring über SectionWrapper.vue bleibt sinnvoll, wenn eine Sektion kein normales Einzeltabellen-Layout hat, zum Beispiel bei Singleton-Formularen oder stärker gruppierten Spezialfällen.

CollectionSection.vue

Gemeinsame Hülle für standardisierte Collection-Sektionen.

Aufgaben:

  • bindet den Sektionsstore über useSectionWrapperProps(...) an
  • rendert standardmäßig SectionTable.vue mit Empty-State
  • kann über Props einen SectionAddFooter erzeugen
  • erlaubt über Slots weiterhin angepasste Zellen, Footer oder komplexeren Content
  • rendert Modals und andere aus dem Tabellenlayout herausgelöste UI über den expliziten #overlay Slot; der Default-Slot bleibt für Tabellen-/Content-Slots reserviert

useSingletonSection(...)

Bevorzugtes Composable für einfache Singleton-Sektionen mit genau einem aktiven Eintrag.

Aufgaben:

  • bindet den Sektionsstore über useSectionWrapperProps(...) an
  • liefert entry als ersten aktiven Eintrag
  • erzeugt beim Mount automatisch einen leeren Eintrag, falls die Sektion noch leer ist
  • reduziert Boilerplate für activeEntries[0] und onMounted(addEmpty)

SectionWrapper.vue

Gemeinsame Hülle für Sektionsinhalt.

Aufgaben:

  • Header mit Text zur letzten Änderung
  • Speichern-Button mit Dirty-Indikator
  • Fehleranzeige über SectionSyncError
  • optionale Anzeige von pdfTextTop und pdfTextBottom als Formularhinweise, wenn showSectionTextsInForm aktiv ist und ein Section-Key über SectionFormTextProvider.vue bereitsteht
  • optionaler Footer-Slot für Add-Row-Aktionen

SectionTable.vue

Schmale gemeinsame Hülle um UTable.

Aufgaben:

  • konsistente Tabellenhülle und Border-Styling
  • Weitergabe des Expanded-Row-Zustands
  • Weitergabe des Column-Pinnings
  • Weitergabe der Slots an die zugrunde liegende Tabelle

Descriptor-backed Table Cells

Für häufige storegebundene Tabellenzellen gibt es Column-Descriptor-Helper in app/utils/sectionTableColumns.ts:

  • textColumn(...) für Texteingaben
  • numberColumn(...) für Zahleneingaben
  • dateColumn(...) für Datumsfelder
  • selectColumn(...) für Auswahlfelder
  • checkboxColumn(...) für boolesche Felder

Diese Helper schreiben über updateFieldByKey(...) in den Store und vermeiden wiederholte Template-Blöcke aus #<field>-cell, StoreCell-Wrappern und @update:model-value-Handlern.

Die Wrapper-Komponenten StoreCellInput.vue, StoreCellDate.vue, StoreSelectCell.vue und StoreCheckboxCell.vue bleiben interne Bausteine der Descriptor-Helper. StoreCell.vue bleibt die niedrigere Primitive für Spezialfälle.

Aufgaben:

  • computed Zwei-Wege-Bindung auf ein Entry-Feld
  • optionale debouncte Updates
  • optionale String-Konvertierung für numerische oder optionale Eingaben
  • sofortiges UI-Feedback, solange der debouncte Write noch aussteht

Für Tabellenspalten liegt das gemeinsame Basismuster in app/utils/sectionTableColumns.ts:

  • sectionColumn(...) erzeugt einfache accessorKey-Spalten mit Header und Layoutoptionen
  • textColumn(...), numberColumn(...), dateColumn(...), selectColumn(...) und checkboxColumn(...) erzeugen Standardzellen mit Store-Binding
  • entityActionsColumn(...) erzeugt die Standard-Action-Spalte für EntityActions.vue

Row-Action-Atoms

Für Tabellenaktionen liefert EntityActions.vue den gemeinsamen Pfad für Delete plus die häufigen Standard-Aktionen Note, Image, Edit und Clone.

Die eigentlichen wiederverwendbaren Action-Atome bleiben kleine Bausteine, zum Beispiel:

  • app/components/Shared/EntityNoteAction.vue
  • app/components/Shared/EntityImageToggleAction.vue

Einfache Tabellen hängen diese Aktionen über entityActionsColumn(...) an EntityActions.vue und reichen nur noch die benötigten Konfigurationen und Handler durch.

Bevorzugtes Muster:

ts
entityActionsColumn<Thing>((entry) => ({
    deleteTitle: t('thing.deleteConfirm'),
    note: {
      modelValue: entry.note,
      placeholder: t('thing.notePlaceholder'),
      title: t('thing.note'),
    },
    imageToggle: {
      acceptanceId: setupStore.acceptanceId,
      parentType: 'thing',
      parentId: entry.id,
      isExpanded: expandedId.value === entry.id,
    },
    showEdit: isEditMode.value,
    onDelete: () => thingStore.remove(entry.id),
    onNoteSave: (value) => thingStore.updateField(entry.id, 'note', value),
    onImageToggle: () => toggleExpand(entry.id),
    onEdit: () => edit(entry),
  }))

Nur wenn eine Zeile echte Sonderlogik braucht, sollte dafür noch eine kleine sibling Action-Komponente entstehen. Der Default ist jetzt der Shared-Pfad über entityActionsColumn(...) und EntityActions.vue.

Neue Sektion hinzufügen

Beim Anlegen einer neuen Sektion sollte zuerst entschieden werden, welches UI-Muster gemeint ist.

Für die vollständige Schritt-für-Schritt-Liste der Implementierungs-Touchpoints siehe /add-new-section.

Wahl des Musters

  • table: mehrere Einträge, standardisierte Tabelle, Add-Footer und Empty-State
  • singleton: genau ein aktiver Eintrag mit direkter Feldbindung
  • setup-driven oder Spezialfall: stark eigenes Layout oder setup-/registry-getriebener Inhalt

Welche Hülle oder welches Composable wird verwendet?

  • Für table-Sektionen: CollectionSection.vue plus Descriptor-Helper für Standardzellen
  • Für singleton-Sektionen: useSingletonSection(...) zusammen mit SectionWrapper.vue
  • Für Spezialfälle: direkt useSectionWrapperProps(...) zusammen mit SectionWrapper.vue

Relevante Form-Composables

  • useSectionWrapperProps(...): gemeinsame Save-, Dirty- und Error-Anbindung für fast alle Sektionen
  • useSectionSync(...): eigentliche Save-Logik unterhalb von useSectionWrapperProps(...)
  • useSingletonSection(...): Wrapper-Binding plus auto-create und entry für Singleton-Formulare
  • useStoreField(...): optional für debouncte Zwei-Wege-Bindung einzelner Store-Felder
  • useExpandedRow(...): gemeinsame Quelle für expandierbare Tabellenzeilen; Tabellen geben expandedState und handleUpdate weiter, gruppierte Tabellen nutzen getExpandedStateForGroup(...) und handleUpdateScoped(...)
  • useConditionOptions(...): optional für Status-/Condition-Auswahl mit gemeinsamen Labels, Farben und Icons

Generator oder manuell?

Der Section-Generator in scripts/section-generator/templates.ts erzeugt das Grundgerüst bereits passend zum gewählten Typ:

  • table erzeugt eine CollectionSection-basierte Form mit Descriptor-Helpern wie textColumn(...)
  • singleton erzeugt eine Form mit useSingletonSection(...)
  • setup-driven erzeugt eine minimal verdrahtete SectionWrapper-Hülle

Manuelles Anlegen bleibt sinnvoll, wenn die Sektion stark vom Standardmuster abweicht.

Typische Dateien für eine neue Sektion

  • app/schemas/<entity>.ts
  • app/schemas/entities.ts
  • app/stores/entityStores.ts oder ein spezialisierter Store unter app/stores/<entity>.ts
  • optional app/stores/<entity>.ts als expliziter Exportpfad fuer Standard-Stores
  • app/stores/registry.ts, falls die Sektion nicht ueber die Standard-Store-Registry laeuft
  • app/components/<Section>/<Section>Form.vue
  • app/utils/sectionDescriptors.ts
  • app/utils/sectionRuntime.ts
  • app/utils/sectionComponents.ts
  • i18n/locales/de.json
  • app/db/index.ts nur dann, wenn eine neue persistierte Tabelle hinzukommt

Sichtbarkeit im Accordion

Damit die neue Sektion im Haupt-Accordion erscheint, braucht sie einen Descriptor, einen Runtime-Eintrag und ein Komponenten-Mapping. useMainAccordionSections.ts leitet daraus die sichtbare Accordion-Registry ab.

Dazu gehören mindestens:

  • der kanonische Section-Key und nötige Aliase
  • Icon, Store-ID, Server-Endpunkt und Lade-Text
  • Form-Komponente
  • optional spezielles Invalid-Wiring, wenn der Consumer es noch nicht generisch ableiten kann