> [← 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.