> [← open.SERVICE v2](<Überblick.md>)
# Architektur
## Die Plugin-Familie
Ladereihenfolge in der App: **core → ticket → schedule.** Core lädt zuerst, weil es
die Modelle registriert und die Navigationsgruppen anlegt. Zwischen den beiden
Domänen-Plugins ist die Reihenfolge beliebig — ihre Ports werden zur Laufzeit
aufgelöst.
| Plugin | Besitzt |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `@opendash/plugin-openservice-core` | **alle** Domain-Modelle (auch die der Wartung), Ports und Registry, Service-Admin-Navigation, Stammdaten-CRUD, den Berechtigungskatalog, i18n |
| `@opendash/plugin-openservice-ticket` | Tickets: UI, Formularkonfiguration, Regeln, Kommentare |
| `@opendash/plugin-openservice-schedule` | Wartung: UI, `OpenServiceScheduleService`, Monitoring-Widgets |
**Das Schedule-Plugin hat kein eigenes `src/models/`.** Ein Modell- oder
Feldwechsel für Wartungspläne ist eine Änderung in core und wird im
Schedule-Plugin nur konsumiert und re-exportiert.
Geteilter Code liegt in core oder hinter einem Port — nie quer zwischen den
Domänen-Plugins. Das gilt auch nach außen: die Wartung importiert nicht aus dem
BDE-Plugin, obwohl sie dessen Artikelstamm nutzt; der gemeinsame Baustein liegt in
core.
## Cross-Plugin-Verträge
| Richtung | Vertrag |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| schedule ↔ ticket | Runtime-Registry in core (`register*Service` / `get*Service` / `use*Service`). Ohne Ticket-Plugin blendet die Wartung die Ticket-Aktionen aus. |
| schedule → monitoring | Vier KPI-Widgets (`compliance`, `next-due`, `overdue`, `load`), alle maschinen-bezogen |
| schedule ↔ meta | Erfassungsfelder je Arbeitsschritt über das Meta-Plugin |
| schedule ← changelog | Ein Server-Hook protokolliert create/update/delete automatisch — nicht zusätzlich selbst loggen |
| schedule → Benachrichtigungen | Regeln am Plan; der Versand läuft serverseitig |
Der `ScheduleService` liefert Pläne je Maschine, kommende Termine mit fertig
gerechneten Zeitpunkten, Durchführungen und Deep-Links und kann eine Durchführung
starten. Das ist der Weg, auf dem ein eigenes Plugin mit der Wartung arbeitet.
Für die Ticket-Erstellung gibt es zwei Wege: aus der Oberfläche heraus
`openCreateTicket(prefill)` — der Benutzer sieht das Formular des Ticketsystems mit
allen seinen Regeln — und programmatisch `createTicket`.
Ein aus der Wartung erzeugtes Ticket trägt seine Herkunft in `Ticket.origin`:
| Form | Bedeutung |
| --------------------------------------- | --------------------------------------- |
| `schedule:<id>` | zum **Plan** |
| `schedule:<id>:<occurrenceISO>` | zu **einem Termin** der Serie |
| `schedule:<id>:execution:<executionId>` | aus **einer Durchführung** |
Gebaut und gelesen wird der Marker über `buildScheduleOrigin` und
`parseScheduleOrigin` in core, zurückgelesen über `getTicketsByOrigin`.
> [!info] Es gibt keine offene Schnittstelle für Fremdsysteme
> Pläne oder Erledigungen lassen sich derzeit nicht von außen über eine API
> anlegen. Die beiden vorhandenen Eingänge sind der Kalender-Abgleich und
> [[Developer/open.SERVICE/Alarme als Wartungsauslöser|Alarme]] — letztere erzeugen
> bewusst nur Einmaltermine.
## Datenzugriff
Aller Datenzugriff läuft über `@opendash/plugin-entity` — nicht über `Parse.Query`.
- `Entity`-Subklassen mit `static className` und `static fields`, Referenzen als
`EntityRef<T>`. Cross-Plugin-Referenzen brauchen einen **Wert**-Import der
Zielklasse, kein `import type`.
- Lesen und Schreiben über `useEntityList` / `Get` / `Create` / `Update` /
`Delete` und `useRelation`, imperativ über `useEntityService()`.
- `entity.acl` und das vorberechnete `entity.readonly` sagen, ob der aktuelle
Benutzer schreiben darf. Geschrieben wird die ACL ausschließlich serverseitig,
siehe [[Developer/open.SERVICE/Berechtigungen|Berechtigungen]].
- **Soft-Delete:** Wartungspläne und Vorlagen nutzen `deletedAt: Date`, und Listen
filtern **explizit** auf `deletedAt: null` — der Adapter filtert nicht von
selbst, und `delete()` ist ein Hard-Delete. Durchführungen haben kein
Soft-Delete: Protokolle sind unveränderlich.
> [!warning] Die Soft-Delete-Konvention ist nicht einheitlich
> Wartung nutzt `deletedAt`, die Ticket- und Stammdatenmodelle in core nutzen
> `deleted: boolean`, der Artikelstamm `isDeleted`. Wer eine fremde Klasse liest,
> muss auf das jeweilige Flag achten.
### Bearbeiten: Draft auf Abruf, Speichern als Patch
Zwei Regeln, die für jede Bearbeitungsoberfläche in diesem Modul gelten:
**Ein Draft wird beim Öffnen des Editors befüllt**, nicht in einem Effekt auf die
geladenen Daten. Sonst ersetzt jeder Refetch den Draft durch Serverdaten und wirft
ungespeicherte Eingaben weg.
**Gespeichert wird ein Diff, kein Draft.** Wer das ganze Draft schreibt, schreibt
einen Schnappschuss vom Öffnen des Editors zurück und überschreibt damit jedes
Feld, das ein Kollege zwischenzeitlich geändert hat. Ein geleertes Feld bleibt als
`undefined` im Patch — daraus macht der Parse-Adapter ein `unset`.
## Datenmodell
Die Modelle liegen in core.
| Klasse | Parse | Felder (Auszug) |
| ------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Schedule` | `OD3_Service_Schedule` | `title`, `active`, `color`, `cadence`, `cadenceExceptions[]`, `equipment`, `steps[]`, `coversTasks[]`, `serviceProviders[]`, `notifications[]`, `pausedUntil`, `leadDays`, `graceDays`, `escalationAfterDays`, `deletedAt` |
| `ScheduleExecution` | `OD3_Service_Schedule_Execution` | `schedule`, `stepResults[]`, `performedBy`, `signature`, `completedAt`, `coverageOrigin` |
| `ScheduleTemplate` | `OD3_Service_Schedule_Template` | `title`, `description`, `steps[]`, `cadence`, `notifications[]`, `serviceProviders[]`, `deletedAt` |
Geteilte Stammdaten — `Equipment`, `Contact`, `Company`, `Tag`, `Article` — gehören
core. Der **Artikelstamm** wird im BDE-Plugin geführt: die Wartung referenziert
Artikel und schreibt Snapshots, sie pflegt keine Artikeldaten. Bestands- und
Preisdaten liegen als Meta-Felder am Artikel, nicht als Spalten.
**Zuständigkeiten sind Parse-Relationen** — `assignedRoles` und `escalationRoles`
auf `_Role`, `assignedUsers` auf `_User`. Relationen verhalten sich anders als
Felder: beim Laden `undefined`, Bearbeitung über add/remove, sofort persistiert,
und beim Anlegen nicht setzbar. Und sie sind keine Etiketten, sondern eine
Rechtevergabe — der Server leitet die ACL des Plans aus ihnen ab.
### Eingebettete Typen
`Cadence` (siehe [[Developer/open.SERVICE/Wiederholungen|Wiederholungen]]) ·
`ScheduleStep` (`id`, `order`, `title`, `description`, `required`, `material[]`) ·
`StepResult` (Fortschritt plus verbrauchtes Material) · `CadenceException` ·
`CoveredTask` und `CoverageOrigin` · `ServiceProviderAssignment` ·
`ScheduleNotification`.
Material steht zweimal im Modell, und das ist beabsichtigt:
| Wo | Feld | Bedeutung |
| -------------- | ------------ | --------------------------------------- |
| `ScheduleStep` | `material[]` | **geplant** — wandert über Vorlagen mit |
| `StepResult` | `material[]` | **verbraucht** — der Nachweis, Snapshot |
`ScheduleNotification` ist **Opt-out** für Zugewiesene: fehlende `assignedRoles`
oder `serviceProviders` zählen als `true`. Wer eine Rolle oder einen Dienstleister
an einen Plan hängt, erwartet, dass sie informiert werden; die Schalter im Editor
machen die Ausnahme sichtbar, statt sie in einem Default zu verstecken.
## Schema
Ein Feld, das das Frontend schreibt, **muss im Parse-Schema deklariert sein** —
sowohl Name als auch Typ. Parse legt undeklarierte Felder beim ersten Schreiben
still an; sie überleben den nächsten Schema-Abgleich nicht. Weicht der Typ ab,
schlägt der Save erst zur Laufzeit fehl (`schema mismatch for <Klasse>.<Feld>`),
denn der TypeScript-Compiler sieht die Datenbank nicht.