# Ansichten und das View-SDK

> Wie ein NXM-Modul seine Datensätze zeigt. Deklarative Ansichten, die nexxus für Sie zeichnet, eigene HTML-Ansichten in einer Sandbox und das View-SDK, das beide mit der Plattform verbindet.

Ein NXM-Modul hat zwei Wege, etwas auf den Bildschirm zu bringen. Die meisten Seiten brauchen gar keinen Code: Sie beschreiben eine Liste oder eine Datensatzseite in der `module.json`, und nexxus zeichnet sie mit denselben Bausteinen wie seine eingebauten Module. Braucht eine Seite ein eigenes Layout, etwa einen Kalender, ein Cockpit aus mehreren Blöcken oder ein PDF mit Notizen daneben, schreiben Sie eine HTML-Datei. nexxus führt diese Datei in einem abgeschotteten Rahmen aus und gibt ihr eine kleine JavaScript-Schnittstelle mit, das **View-SDK** (`window.nx`).

Diese Seite behandelt beides. Fangen Sie mit den deklarativen Ansichten an und greifen Sie erst dort zu einer eigenen Ansicht, wo diese nicht mehr reichen.

## Zwei Arten von Ansichten

| | Deklarative Ansichten | Eigene Ansichten |
|---|---|---|
| Geschrieben als | JSON in der `module.json` (`type`: `list`, `record`, `related`) | Eine HTML-Datei je Ansicht (`type: "custom"`, `html: "ui/<name>.html"`) |
| Gezeichnet von | nexxus selbst | Ihrem HTML, in einem abgeschotteten Rahmen |
| Sieht aus wie | der Rest von nexxus, ganz von selbst | nexxus, wenn Sie das UI-Kit und die Farb-Tokens verwenden |
| Datenzugriff | eingebaut: Listen, Filter, Formulare, Bezugslisten | über `window.nx`, als die Person, die die Ansicht betrachtet |
| Gut für | Listen, Datensatzformulare, Listen verknüpfter Datensätze | Seiten, Cockpits, Kalender, Dialoge, PDF-Prüfung |

Beide Arten handeln mit den Rechten der Person auf der Seite. Eine eigene Ansicht bekommt nie mehr Zugriff als die Person, die sie geöffnet hat, und der Server prüft jeden ihrer Aufrufe noch einmal.

<Flow
  steps={[
    { icon: 'file-code', title: 'Ihre Ansicht', text: 'Eine HTML-Datei mit eingebettetem Skript und Stil, in einem abgeschotteten Rahmen.' },
    { icon: 'arrow-left-right', title: 'Das View-SDK', text: 'window.nx schickt jede Anfrage über einen eigenen Nachrichtenkanal.', tone: 'lavender' },
    { icon: 'app-window', title: 'Der Host in nexxus', text: 'Ergänzt Modul und Sitzung, zeigt Meldungen, Dialoge und den PDF-Viewer.', tone: 'cyan' },
    { icon: 'shield-check', title: 'Die Plattform-API', text: 'Prüft die Rechte der betrachtenden Person und antwortet.' },
  ]}
  caption="Eine eigene Ansicht sieht nie eine Sitzung, eine Mandanten-ID oder eine Benutzer-ID. Der Host ergänzt sie, und der Server prüft jede Anfrage."
/>

## Deklarative Ansichten

Diese vier Typen brauchen kein HTML. nexxus baut daraus Listen, Formulare, Reiter und Karten.

- **`list`**: die Listenseite eines Fachobjekts unter `/x/<modul>/<fachobjekt>`. Sie wählen die `columns` (einen Feldschlüssel oder `{ "field": "status", "display": "badge" }`), die `filters` und eine Standardsortierung `sort`; `"create": true` zeigt die Schaltfläche für einen neuen Datensatz. Filtern und sortieren lässt sich nur nach indizierten Feldern. Fehlt eine Listenansicht, baut nexxus sie aus dem `titleField` und den ersten vier indizierten Feldern.
  Die Liste zeigt jeden Datensatz als Zeile im Listenstil von nexxus, und jede Spalte bleibt sichtbar: Das `titleField` wird die Überschrift, Verweise und Textfelder stehen darunter, die erste Person (`core:user`) mit ihrer Bezeichnung daneben. Auswahlfelder, Daten und Zahlen stehen rechts, der erste Betrag hervorgehoben, und die Badge-Spalte `status` (sonst die erste Badge-Spalte) als Chip. Unter <Ui>Ansichtsoptionen</Ui> wählt jede Person die Dichte <Ui>Normal</Ui> oder <Ui>Kompakt</Ui> und speichert eigene Ansichten.
- **`record`**: der Reiter <Ui>Daten</Ui> eines Datensatzes. `sections` gliedern die Felder, `related` fügt Listen der Datensätze hinzu, die auf diesen verweisen, und `actions` legt fest, welche Aktionen die Aktionsleiste anbietet.
- **`related`**: ein Reiter auf der Seite eines anderen Fachobjekts, der die Datensätze auflistet, die darauf verweisen. Bei Kerndaten ist das die Seite eines Partners, Projekts, einer Rechnung oder einer Opportunity (`"target": "core:partner"`, `"core:project"`, `"core:invoice"` oder `"core:opportunity"`, Slot `record.tab`).
- **`map`**: die Datensätze eines Fachobjekts auf einer Karte, zusammen mit ihrer Liste, als Seite des Moduls unter `/x/<modul>/p/<ansicht>`. `geoField` nennt das `geo`-Feld, das einen Datensatz auf die Karte setzt.

Die vollständige Feldreferenz dieser Ansichten steht unter [Paketaufbau](/de/modules/structure/).

### So ist eine Datensatzseite aufgebaut

Die Datensatzseite eines Paket-Fachobjekts zeigt ihre Reiter in dieser Reihenfolge:

1. alle Ansichten mit Slot `record.detail` (eigene Ansichten zu diesem Datensatz),
2. den Reiter <Ui>Daten</Ui> (die `record`-Ansicht oder, falls es keine gibt, alle Felder),
3. alle Ansichten des Moduls mit Slot `record.tab`,
4. die Bezugslisten.

Dort erscheinen nur Ansichten des eigenen Pakets: Pakete bauen nicht aufeinander auf, also bringt kein anderes Paket Reiter oder Dialoge auf die Seite. Aktionen stehen in der Aktionsleiste unten im Reiter.

<Screenshot src={shot_nxm_14} url="/x/curiaos.kanzlei/akte/1/ansicht/akte.cockpit" alt={"Datensatzseite eines Pakets mit einer eigenen Ansicht als erstem Reiter"} caption={"Zuerst die eigenen Ansichten, dann „Daten“, dann die Reiter des Moduls, dann die Bezugslisten."} />

## Slots: wo eine eigene Ansicht erscheint

Eine eigene Ansicht nennt einen `slot`. Er bestimmt, wo nexxus sie zeigt und welchen Kontext sie bekommt.

| Slot | Im Katalog angezeigt als | Wo sie erscheint | `target` |
|---|---|---|---|
| `page` | <Ui>Seite</Ui> | Eine eigene Seite unter `/x/<modul>/p/<ansicht>`. Sie füllt den Bereich und kann ein Menüeintrag sein. | keins |
| `record.detail` | <Ui>Ansicht am Datensatz</Ui> | Die ersten Reiter einer Datensatzseite | ein Fachobjekt des Pakets |
| `record.tab` | <Ui>Reiter am Datensatz</Ui> | Ein Reiter nach <Ui>Daten</Ui>; mit einem Kernziel ein Reiter auf der Partner-, Projekt-, Rechnungs- oder Opportunity-Seite | ein Fachobjekt oder `core:partner`, `core:project`, `core:invoice`, `core:opportunity` |
| `record.action` | <Ui>Dialog am Datensatz</Ui> | Ein Dialog, den die Aktionsleiste des Datensatzes öffnet | ein Fachobjekt |
| `dashboard.widget` | <Ui>Kachel auf dem Dashboard</Ui> | Eine Kachel mit `size` `s`, `m` oder `l` | keins |
| `member` | <Ui>Seite im Kundenbereich</Ui> | Eine Seite des [Kundenbereichs](/de/modules/portal/#kundenbereich) auf der Portal-Domain, unter `/konto/<ansicht>`, fensterfüllend | keins |

Über einer Seite und einer Ansicht am Datensatz zeigt nexxus den Titel: auf einer Seite die Beschriftung der Ansicht, auf einem Datensatz dessen Titel. Die Ansicht wiederholt ihn nicht und beginnt mit dem, was sie ergänzt, etwa einem Zeitraum, einer Anzahl oder Filtern. Auch Kachel und Dialog bekommen ihre Titelleiste von nexxus.

Auf die Startseite kommt eine Kachel auf zwei Wegen: Eine Person legt sie mit <Ui>Anpassen</Ui> und <Ui>Widget hinzufügen</Ui> an, wo Beschriftung, Symbol und Modulname der Ansicht stehen, oder sie wendet eine Vorlage des Pakets an (siehe [Startseiten-Vorlagen](/de/modules/structure/#startseiten-vorlagen-dashboards)). Von selbst erscheint keine. Ist das Modul deaktiviert oder entfernt, zeigt eine platzierte Kachel <Ui>Modul nicht verfügbar</Ui> und kommt mit dem Modul zurück.

```json
{ "key": "faellig", "type": "custom", "slot": "page", "html": "ui/faellig.html",
  "label": { "de": "Fällige Rückgaben", "en": "Due returns" } }
```

### Eine Ansicht nur zeigen, wenn sie passt

Ansichten und Aktionen nehmen eine optionale Bedingung `when`. nexxus blendet die Ansicht aus, wenn die Bedingung für den aktuellen Datensatz nicht gilt, und der Server prüft sie vor jeder Aktion erneut.

```json
"when": { "field": "status", "in": ["verfuegbar"] }
"when": { "field": "dokument", "set": true }
```

`in` gilt, wenn der Wert des Felds einer der genannten ist (bei einem Listenfeld, wenn es irgendein Element ist). `set: true` gilt, wenn das Feld einen Wert hat, `set: false`, wenn es leer ist.

## Die Sandbox

Jede eigene Ansicht läuft in einem `<iframe>` mit `sandbox="allow-scripts allow-forms"` ohne eigenen Ursprung und unter derselben Inhaltsrichtlinie wie Apps:

```
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';
img-src data: blob:; font-src data:; media-src data: blob:; form-action 'none'; base-uri 'none'
```

Die Sandbox gibt es, weil der Code eines Pakets nicht von nexxus stammt. Was die Ansicht auch tut, sie erreicht kein Netzwerk, liest keine Sitzung und berührt nicht die Seite um sie herum. Im Alltag heißt das:

- **Alles eingebettet.** Skripte, Stile, Schriften und Bilder stehen in der Datei oder als `data:`- bzw. `blob:`-URL. `fetch`, XHR, WebSockets und `<script src>` aus dem Netz sind gesperrt.
- **Keine Cookies, kein `localStorage`.** Der Rahmen hat keinen Ursprung. Was eine Person wiederfinden soll, legen Sie mit `nx.storage` ab.
- **Formulare ja, Absenden nein.** Ein `<form>` löst sein `submit`-Ereignis aus, aber `form-action 'none'` sperrt alles, was die Seite verlassen würde. Rufen Sie `preventDefault()` auf und verarbeiten Sie die Daten selbst.
- **Keine Pop-ups, keine Downloads.**
- **Keine Symbolschrift.** Die Schrift Material Symbols von nexxus steht im Rahmen nicht zur Verfügung. Verwenden Sie eingebettetes SVG.
- **Text als Text.** Datensätze enthalten, was Menschen eingetippt haben. Setzen Sie solche Werte mit `textContent` ein, nie mit `innerHTML`.

SDK und UI-Kit werden vor Ihrem eigenen Code eingefügt, `window.nx` steht also bereit, wenn Ihr erstes Skript läuft. Eine HTML-Datei darf bis zu 2 MB groß sein.

## Eine minimale Ansicht

Diese Seite listet die verliehenen Geräte aus dem [Tutorial zum ersten Modul](/de/modules/first-module/):

```html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>body { padding: 16px; }</style>
</head>
<body>
  <div class="nx-list" id="list"></div>
  <script>
    (async () => {
      const ctx = await nx.ready();
      const {items} = await nx.records.search('geraet', {
        filter: {status: ['verliehen']}, sort: 'rueckgabe_am', size: 50,
      });
      for (const geraet of items) {
        const item = document.createElement('button');
        item.className = 'nx-list__item';
        item.textContent = geraet.title;
        item.onclick = () => nx.nav.open({entity: 'geraet', id: geraet.id});
        document.getElementById('list').append(item);
      }
    })();
  </script>
</body>
</html>
```

## Kontext und Ereignisse

`await nx.ready()` kehrt zurück, sobald der Host verbunden ist, und liefert den Kontext der Ansicht:

| Feld | Bedeutung |
|---|---|
| `sdk` | Protokollversion, derzeit `1` |
| `module`, `version`, `view`, `slot` | Wo die Ansicht läuft |
| `locale` | `de`, `en` oder `pl`, die Sprache der Person |
| `theme` | `light` oder `dark` |
| `user` | `{id, name}` der Person, die die Ansicht betrachtet |
| `record` | `{entity, id}` auf der Datensatzseite eines Fachobjekts aus dem Paket der Ansicht, z. B. `{entity: 'akte', id: 12}` |
| `target` | `{type, id}` auf einem Kerndatensatz, z. B. `{type: 'core:project', id: 7}` |
| `params` | Was der Aufrufer mitgegeben hat |

`nx.on(name, fn)` meldet einen Empfänger für ein Ereignis an und gibt eine Funktion zurück, die ihn wieder abmeldet.

- **`context`**: Der Kontext hat sich geändert (ein anderer Datensatz, eine andere Sprache). Das Argument ist der neue Kontext.
- **`theme`**: Hell oder dunkel hat gewechselt, als `{theme, tokens}`. Das SDK hat den Wechsel schon übernommen. Hören Sie nur zu, wenn Sie selbst zeichnen, etwa auf einem Canvas oder in einem Diagramm.
- **`record`**: nexxus hat den Datensatz, um den es in Ihrer Ansicht geht, außerhalb der Ansicht geändert, als `{entity, id, version}`. Das passiert, nachdem eine Aktion darauf lief, ein Dialog ihn geschrieben hat, der Reiter <Ui>Daten</Ui> gespeichert wurde oder eine andere Ansicht ihn geändert hat. Bei Kerndatensätzen ist `version` `null`. Ihre eigenen Schreibvorgänge kommen nicht als Ereignis zurück.

Das Ereignis sagt, dass sich etwas geändert hat, nicht was. Lesen Sie den Datensatz neu:

```js
const ctx = await nx.ready();
let shown = await nx.records.get(ctx.record.entity, ctx.record.id);
render(shown);

nx.on('record', async ({version}) => {
  if (version !== null && version === shown.version) return;
  shown = await nx.records.get(ctx.record.entity, ctx.record.id);
  render(shown);
});
```

Änderungen, die nexxus nicht in diesem Browser-Tab vorgenommen hat, etwa von einer anderen Person oder von einem [Moduldienst](/de/modules/services/) im Hintergrund, werden nicht gemeldet.

## SDK-Referenz

Jeder Aufruf gibt ein Promise zurück. Ein fehlgeschlagener Aufruf wird mit einem `nx.NxError` abgelehnt, der `code`, `message` und bei Prüffehlern `fieldErrors` trägt. Lehnt ein [Moduldienst](/de/modules/services/) eine Aktion ab, ist `code` `SERVICE_REJECTED`, `serviceCode` der eigene Code des Dienstes (etwa `COMPOSE_STALE`) und `serviceStatus` sein HTTP-Status; prüfen Sie `serviceCode`, nicht den Text, der in der Sprache der Person steht.

| Aufruf | Ergebnis |
|---|---|
| `nx.ready()` | Der Kontext (siehe oben) |
| `nx.on(name, fn)` | Meldet einen Empfänger für `context`, `theme` oder `record` an; gibt eine Abmeldefunktion zurück |
| `nx.records.search(entity, {q?, filter?, sort?, from?, size?})` | `{items, total}`. `filter` je Feld: ein Wert, eine Liste von Werten oder `{from, to}`. `sort`: `'feld'`, `'feld,asc'` oder `'feld,desc'` (siehe unten). `size` höchstens 200 |
| `nx.records.get(entity, id)` | Der Datensatz |
| `nx.records.create(entity, data)` | Der neue Datensatz |
| `nx.records.update(entity, id, patch, {version?})` | Der Datensatz. Mit `version` schlägt der Aufruf mit `CONFLICT` fehl, wenn auf dem Server ein neuerer Stand liegt |
| `nx.records.remove(entity, id)` | `true` |
| `nx.lookup(target, {q?, ids?})` | `[{id, label, sublabel}]`. Ziele: ein Fachobjekt des Moduls, `'core:partner'`, `'core:partnerContact'`, `'core:project'`, `'core:invoice'`, `'core:opportunity'`, `'core:user'`, `'core:dmsDocument'`, `'core:ismsMeasure'`. Ohne `q` die ersten 20 |
| `nx.invoke(action, {recordId?, payload?})` | `{result, record?}`. Führt eine Aktion des Moduls aus; der Server prüft ihr Recht und ihr `when`. Eine Aktion, die eine E-Mail versendet, fragt die Person vorher, siehe unten |
| `nx.actions.preview(action, recordId)` | `{recipient: {name, address}, subject, text, fileName}`: was eine Aktion, die eine E-Mail versendet, für den Datensatz versenden würde. Versendet nichts |
| `nx.nav.open(target)` | Öffnet `{entity, id}`, `{list: entity}`, `{view}` (eine Seite des Moduls) oder `{core, id}` mit `core` gleich `'partner'`, `'project'`, `'invoice'` oder `'opportunity'`. `{view, id}` mit einer `record.action`-Ansicht öffnet deren Dialog für diesen Datensatz und liefert, was der Dialog an `nx.dialog.close()` übergeben hat, oder `null`, wenn er abgebrochen wurde. `{view, id}` mit einer `map`-Ansicht öffnet die Karte auf diesem Datensatz (`?focus=<id>`) |
| `nx.ui.toast(message, {kind?})` | Zeigt eine Meldung; `kind` ist `success`, `error` oder `info` |
| `nx.ui.confirm({title, message, confirmLabel?, danger?})` | `true`, wenn bestätigt. nexxus zeigt dafür seinen eigenen Dialog |
| `nx.dialog.close(result?)` | Nur in `record.action`-Ansichten: schließt den Dialog und gibt `result` an den Aufrufer |
| `nx.storage.get(key)` | Der gespeicherte Wert, je Person und Ansicht |
| `nx.storage.set(key, value)` | Speichert einen Wert im Profil der Person, er folgt ihr also auf andere Geräte. `set(key, null)` entfernt ihn |
| `nx.assistant.setContext({summary, records?})` | Sagt dem KI-Assistenten, was die Ansicht zeigt. `records` ist `[{entity, id}]`; ein leeres Objekt zieht den Kontext zurück |
| `nx.assistant.ask(prompt)` | Öffnet die Assistentenspalte und stellt `prompt` als neues Gespräch |
| `nx.t({de, en, pl})` | Der Text in der Sprache der Person, sonst der englische, sonst der deutsche. `pl` ist optional |
| `nx.pdf.mount(element, {documentId, page?, highlights?})` | Ein Handle für einen PDF-Viewer über `element` (siehe unten) |
| `nx.images.url(documentId, {width?})` | Eine `blob:`-Adresse für das Bild des Dokuments, für `<img src>`. nexxus lädt die Datei mit den Leserechten der Person, die die Ansicht ansieht, und nimmt nur Bilder bis 15 MB an. Mit `width`, der Breite auf dem Bildschirm in CSS-Pixeln, kommt eine passend verkleinerte Kopie statt des Originals, und der Browser behält sie zehn Minuten. Dieselbe ID und Breite gibt dieselbe Adresse und wird einmal geladen; höchstens vier laden gleichzeitig |
| `nx.files.upload(file, {entity, field, name?})` | `{id, documentId, name, type, size, url}`. Legt eine Datei aus dem eigenen Eingabefeld der Ansicht, aus einem Ablegen oder von der Kamera als Dokument der Person ab, für ein Feld von `entity`, das auf Dokumente verweist. `documentId` schreibt die Ansicht selbst in dieses Feld; `url` zeigt die Datei sofort. In Ansichten der Slots `record.detail`, `record.tab` und `page`, mit dem Scope `core:dmsDocument:create`, siehe [Fotos und Dateien aus der Ansicht](#fotos-und-dateien-aus-der-ansicht) |

**Fachobjekte** sind die des eigenen Pakets, genannt mit ihrem Schlüssel (`'geraet'`). Eine Ansicht erreicht kein anderes Paket: Ein Name mit einem Paketschlüssel davor, etwa `'curiaos.kanzlei:akte'`, scheitert mit `INVALID_PARAMS`. Suchen filtern nur nach indizierten Feldern.

`sort` nimmt ein indiziertes Feld oder eines von `id`, `createdAt` (angelegt) und `updatedAt` (zuletzt geändert). Ohne Richtung sortiert es aufsteigend. Ohne `sort` kommt der zuletzt angelegte Datensatz zuerst. Datensätze ohne Wert im Sortierfeld stehen am Ende, bei gleichem Wert steht der zuletzt angelegte vorn. `nx.records.search('geraet', {sort: 'updatedAt,desc', size: 10})` liefert die zehn zuletzt geänderten Datensätze.

### Aktionen, die eine E-Mail versenden

Eine Aktion vom Typ `mail` lässt nexxus im Namen Ihrer Organisation eine E-Mail mit einem PDF an die Adresse eines Kontakts versenden. Die Adresse nimmt nexxus aus dem Kontakt selbst, nie aus Ihrer Ansicht, und fragt die Person, bevor etwas hinausgeht.

Ruft Ihre Ansicht `nx.invoke` für eine solche Aktion auf, öffnet nexxus außerhalb Ihres Rahmens seinen eigenen Dialog <Ui>E-Mail versenden</Ui>. Er zeigt den `confirm`-Text der Aktion, dann <Ui>An</Ui> mit Name und Adresse des Empfängers, <Ui>Betreff</Ui>, den Text der E-Mail und <Ui>Anhang</Ui> mit dem Dateinamen des PDFs, alles so, wie der Server es ermittelt hat. Nur <Ui>Versenden</Ui> versendet, und zwar genau das, was der Dialog gezeigt hat. <Ui>Abbrechen</Ui>, das Schließen-Symbol oder Escape lehnen Ihren Aufruf mit `CANCELLED` ab. Derselbe Dialog öffnet sich, wenn jemand die Aktion in der Aktionsleiste des Datensatzes anklickt.

- Fragen Sie nicht vorher mit `nx.ui.confirm`. Die Person müsste sonst zweimal bestätigen.
- Hat der Kontakt keine E-Mail-Adresse, zeigt der Dialog die Meldung des Servers und bietet nur <Ui>Schließen</Ui> an. Ihr Aufruf scheitert dann mit `CANCELLED`, `nx.actions.preview` mit `NO_ADDRESS`.
- Haben sich der Datensatz oder der Empfänger zwischen Vorschau und Versand geändert, scheitert der Aufruf mit `CONFIRMATION_STALE`. Rufen Sie `nx.invoke` erneut auf, dann kommt eine frische Vorschau.
- `nx.actions.preview(action, recordId)` liefert dieselbe Vorschau, damit Ihre Ansicht sie neben ihrem Knopf zeigen kann. Sie enthält nie das Token, das einen Versand an die Vorschau bindet. Eine Ansicht kann den Dialog also nicht umgehen.

```js
const mail = await nx.actions.preview('entwurf-versenden', ctx.record.id);
showPreview(mail.recipient.name, mail.recipient.address, mail.subject, mail.text, mail.fileName);

try {
  await nx.invoke('entwurf-versenden', {recordId: ctx.record.id});
} catch (e) {
  if (e.code !== 'CANCELLED') nx.ui.toast(e.message, {kind: 'error'});
}
```

### Fotos und Dateien aus der Ansicht

Eine Ansicht auf einer Datensatzseite oder auf einer eigenen Seite kann Fotos und andere Dateien aufnehmen, etwa auf einem Tablet vor Ort. Das `<input type="file">` bleibt das der Ansicht. nexxus legt die Datei unter <Ui>Dateien</Ui> als Dokument der Person ab und gibt ihre ID zurück, die die Ansicht in ein Feld schreibt, das auf Dokumente verweist.

1. Tragen Sie den Scope `core:dmsDocument:create` unter `scopes` ein. Die Administration stimmt ihm beim Installieren des Pakets zu (<Ui>Dokumente anlegen</Ui>). Ohne ihn scheitert der Aufruf mit `CONSENT_REQUIRED`.
2. Geben Sie der Entität ein `ref`-Feld mit `"target": "core:dmsDocument"`, mit `"multiple": true` für mehrere Dateien und `"accept": ["image/*"]`, wenn dort nur Bilder hingehören.
3. Rufen Sie `nx.files.upload(file, {entity, field})` auf und speichern Sie die zurückgegebene `documentId` im Feld.

```js
// <input type="file" id="photo" accept="image/*" capture="environment">
const input = document.querySelector('#photo');
input.addEventListener('change', async () => {
  const file = input.files[0];
  if (!file) return;
  try {
    const photo = await nx.files.upload(file, {entity: 'component', field: 'photos'});
    preview.src = photo.url;
    const record = await nx.records.get('component', ctx.record.id);
    const photos = [...(record.data.photos || []), photo.documentId];
    await nx.records.update('component', record.id, {photos}, {version: record.version});
  } catch (e) {
    nx.ui.toast(e.message, {kind: 'error'});
  }
});
```

- **Was nexxus prüft:** Die Person darf Datensätze der Entität ändern, das Feld verweist auf Dokumente und sein `accept` nimmt die Datei, die Datei ist ein Dokument, Bild, Office-Datei oder eine E-Mail, und sie ist höchstens 25 MB groß. Eine größere Datei weist nexxus ab, bevor sie gesendet wird (`TOO_LARGE`).
- **Wo die Datei landet:** in einem Ordner mit dem Namen des Pakets in den eigenen Dateien der Person, angelegt beim ersten Hochladen. Einzurichten ist nichts, denn wer einen Datensatz lesen darf, der auf die Datei verweist, darf auch die Datei lesen.
- **Wahlweise ein Ordner fürs Team:** Die Administration legt unter <Ui>Dateien</Ui> einen obersten Ordner mit dem Namen des Pakets in der Sprache der Organisation an und gibt ihn mit dem Recht zum Bearbeiten frei. Die Dateien aller, die ihn sehen und keinen eigenen Ordner dieses Namens haben, landen dann dort. Ist er nur zum Lesen freigegeben, scheitert ihr Hochladen.
- **Wer sie sieht:** die Person, die sie hochgeladen hat, alle, für die der Ordner freigegeben ist, und alle, die einen Datensatz des Pakets lesen dürfen, der auf die Datei verweist.
- **Das Hochladen allein ändert keinen Datensatz.** Eine Datei, die nie in ein Feld kommt, bleibt in den Dateien der Person.
- **HEIC:** iPads und iPhones geben einem Dateifeld meist ein JPEG. Eine HEIC-Datei wird so abgelegt, wie sie ist, und außer Safari zeigt sie kein Browser an.
- **Nicht verfügbar** in Kacheln der Startseite, in Aktionsdialogen (`record.action`) und in öffentlichen Formularen: Dort scheitert der Aufruf mit `NOT_AVAILABLE`.

Später zeigt die Ansicht ein abgelegtes Foto mit `nx.images.url(documentId)`.

### Fehlercodes

| Code | Stammt von | Bedeutung |
|---|---|---|
| `VALIDATION` | Server | Die Daten wurden abgelehnt; `fieldErrors` nennt die Felder |
| `FORBIDDEN` | Server | Der Person fehlt das Recht |
| `NOT_FOUND` | Server | Datensatz, Aktion oder Ansicht gibt es nicht |
| `MODULE_INACTIVE` | Server | Das Modul ist deaktiviert |
| `CONFLICT` | Server | Jemand hat den Datensatz geändert, seit Sie ihn gelesen haben |
| `UNIQUE` | Server | Ein eindeutiger Wert ist schon vergeben |
| `NO_ADDRESS` | Server | Der Kontakt einer Aktion, die eine E-Mail versendet, hat keine E-Mail-Adresse |
| `CONFIRMATION_STALE` | Server | Datensatz oder Empfänger haben sich geändert, seit die Vorschau der E-Mail gezeigt wurde, oder die Vorschau ist abgelaufen |
| `CONSENT_REQUIRED` | Server | Dem Paket fehlt der Scope, den der Aufruf braucht, etwa `core:dmsDocument:create` für `nx.files.upload` |
| `INVALID_PARAMS` | Host | Ein Parameter hatte die falsche Form; die Meldung nennt ihn |
| `UNKNOWN_METHOD` | Host | Diese SDK-Methode gibt es nicht |
| `BUSY` | Host | Zu viele Aufrufe gleichzeitig, oder eine Bestätigung ist schon offen |
| `CANCELLED` | Host | Die Person hat den Dialog einer Aktion, die eine E-Mail versendet, geschlossen, ohne zu versenden |
| `STORAGE_FULL` | Host | Das Speicherlimit dieser Ansicht ist erreicht |
| `NOT_IN_DIALOG` | Host | `nx.dialog.close()` außerhalb einer `record.action`-Ansicht |
| `NO_USER_ACTION` | Host | Der Aufruf braucht zuerst einen Klick oder Tastendruck in der Ansicht |
| `NOT_AVAILABLE` | Host | Den Aufruf gibt es in dieser Art von Ansicht nicht, etwa `nx.files.upload` in einer Kachel |
| `NOT_AN_IMAGE` | Host | `nx.images.url`: Das Dokument ist kein Bild |
| `TOO_LARGE` | Host | `nx.images.url`: Das Bild ist größer als 15 MB. `nx.files.upload`: Die Datei ist größer als 25 MB |
| `LIMIT` | Host | Ein Limit wurde erreicht, etwa zu viele PDF-Viewer |
| `NETWORK` | Host | Die Anfrage hat den Server nicht erreicht |
| `UNAUTHENTICATED` | Host | Die Sitzung ist abgelaufen |

Zeigen Sie der Person `error.message`. Bei Prüffehlern ist die Meldung allgemein, und `fieldErrors` enthält die Einzelheiten: Eine Suche, die nach einem nicht indizierten Feld sortiert, scheitert mit „Die Suche ist ungültig.“ und `fieldErrors.sort` „Nach 'notiz' lässt sich nicht sortieren: das Feld ist nicht indexed.“

## Farben und UI-Kit

Der Rahmen ist durchsichtig und folgt dem Farbschema von nexxus: `<html data-theme="light">` oder `"dark"`. Auf einer Datensatzseite liegt keine Karte dahinter: Ihre Ansicht steht direkt auf der Seite, wie die Abschnitte im Reiter <Ui>Daten</Ui>, also gehören ihre Inhalte in Karten (`nx-card`). Das UI-Kit ist eine CSS-Datei, die nexxus in jede Ansicht einfügt. Sie dürfen es ignorieren, mit ihm sieht Ihre Ansicht aber in beiden Farbschemata und in der Markenfarbe des Mandanten wie nexxus aus.

### Farb- und Form-Tokens

| Variable | Verwendung |
|---|---|
| `--nx-text`, `--nx-text-muted`, `--nx-text-faint` | Text, Nebentext, Hinweise |
| `--nx-surface`, `--nx-surface-raised`, `--nx-surface-sunken`, `--nx-page` | Karten, Menüs, vertiefte Flächen, der Seitengrund |
| `--nx-line`, `--nx-line-soft` | Rahmen und Trennlinien |
| `--nx-brand`, `--nx-brand-contrast`, `--nx-brand-ink` | Markenfarbe als Fläche, Text darauf, Markenfarbe als Text |
| `--nx-accent`, `--nx-success`, `--nx-warning`, `--nx-danger`, `--nx-info` | Signalfarben |
| `--nx-radius`, `--nx-radius-sm`, `--nx-shadow`, `--nx-font`, `--nx-font-mono` | Form und Schrift |

### Klassen

| Klasse | Für |
|---|---|
| `nx-btn` mit `nx-btn--primary`, `--danger`, `--quiet`, `--sm`, `--icon` | Schaltflächen |
| `nx-badge` mit `nx-badge--success`, `--warning`, `--danger`, `--info`, `--brand` | Statusetiketten |
| `nx-card`, `nx-card__title` | Eine Karte |
| `nx-field`, `nx-field__label`, `nx-field__hint`, `nx-field__error`, `nx-input`, `nx-select`, `nx-textarea` | Formulare; `aria-invalid="true"` markiert einen Fehler |
| `nx-list`, `nx-list__item` (`is-active`) | Listen; ein `<button class="nx-list__item">` ist anklickbar |
| `nx-rows`, `nx-entity-row` (`is-active`), `nx-meta` | Datensätze so, wie nexxus seine Listen zeichnet; `nx-list-compact` an der Liste macht daraus Zeilen in einem Rahmen |
| `nx-table` | Tabellen |
| `nx-empty`, `nx-empty__icon`, `nx-empty__title` | Leere Zustände |
| `nx-title`, `nx-subtitle`, `nx-label`, `nx-muted`, `nx-faint`, `nx-mono`, `nx-num` | Schrift |
| `nx-stack`, `nx-row`, `nx-spacer`, `nx-divider` | Layout-Helfer |
| `nx-kbd`, `nx-progress` (`--nx-value`), `nx-spinner` | Kleinigkeiten |
| `nx-pdf-slot` | Die Fläche, die für ein PDF reserviert ist |

## Ein PDF in der Ansicht

Eine Ansicht kann ein PDF aus den Dateien zeigen, ohne je seine Bytes in der Hand zu haben. Sie reservieren ein Element, nexxus legt seinen eigenen PDF-Viewer darüber und lädt das Dokument mit den Rechten der Person, die es ansieht.

```js
const handle = nx.pdf.mount(document.getElementById('pdf'), {
  documentId: 88, highlights: [{text: 'binnen zwei Wochen', page: 2}],
});
handle.on('select', ({page, text, rects}) => showNoteButton(page, text));
handle.on('page', page => updatePageLabel(page));
await handle.goTo(3);
const {total, index} = await handle.find('Zustellung');
await handle.setHighlights([{text: quote, page}]);
await handle.destroy();
```

- Geben Sie dem Element eine Größe, vor allem eine Höhe: `<div id="pdf" class="nx-pdf-slot"></div>`. Der Viewer folgt dem Element, wenn es sich bewegt, scrollt oder seine Größe ändert, und wird dort abgeschnitten, wo Ihre scrollenden Container das Element abschneiden.
- Ereignisse: `select` (`{page, text, rects}`, `rects` als Anteile 0 bis 1 der Seite, Ursprung oben links; eine aufgehobene Auswahl kommt mit leerem `text`), `page` (die sichtbare Seite), `load` (`{pages}`), `error` (`{code}`). `handle.ready` erfüllt sich, sobald der Viewer steht.
- Markierungen treffen unabhängig von Groß- und Kleinschreibung und Leerraum, über Zeilenumbrüche hinweg und auch dann, wenn typografische Anführungszeichen und Striche einfach getippt sind. Ohne `page` markiert der Viewer jedes Vorkommen.

<Screenshot src={shot_nxm_17} url="/x/curiaos.kanzlei/aktendokument/26/ansicht/aktendokument.ansicht" alt={"Eine eigene Ansicht mit dem PDF-Viewer von nexxus und einer Notizspalte"} caption={"Ein Klick auf eine Notiz markiert ihr Zitat; nexxus lädt das PDF mit den Rechten der lesenden Person."} />

## Größe, Tasten und Dialoge

- **Die Höhe** folgt Ihrem Inhalt, bis zu 20 000 px. Eine `page`-Ansicht füllt stattdessen den Bereich unter ihrer Oberkante (mindestens 320 px). Lassen Sie `<html>` bei seiner natürlichen Höhe, solange Sie keine Seite sind.
- **Tasten** mit Strg, Cmd oder Alt sowie Escape gehen weiter an nexxus, es sei denn, Ihr Handler ruft `preventDefault()` auf. Escape schließt deshalb den Dialog einer `record.action`-Ansicht.
- **Dialoge** (`record.action`): nexxus zeichnet den Kopf des Dialogs mit Beschriftung, Symbol und Schließen-Schaltfläche der Ansicht. Ihre Ansicht zeichnet den Inhalt und ihre eigenen Schaltflächen und endet mit `nx.dialog.close(result)`. Wird der Dialog auf anderem Weg geschlossen, gilt die Aktion als nicht ausgeführt. Eine andere Ansicht des Moduls öffnet den Dialog mit `nx.nav.open({view, id})`.
- **Kacheln** (`dashboard.widget`): nexxus zeichnet die Karte mit Symbol, Beschriftung und Modulname, die Ansicht nur den Inhalt. Größen auf einem breiten Bildschirm: `s` ein Viertel der Zeile, `m` die Hälfte, `l` die ganze Zeile.

<Screenshot src={shot_nxm_16} url="/x/curiaos.kanzlei/akte/5/daten" alt={"Eine Ansicht im Slot record.action als Dialog"} caption={"nexxus zeichnet den Kopf, die Ansicht Inhalt und Schaltflächen."} />

## Limits

| Was | Limit |
|---|---|
| Gleichzeitige Aufrufe je Ansicht | 32 |
| Meldungen (Toasts) | 20 pro Minute, mindestens 400 ms Abstand |
| `nx.storage` je Person und Ansicht | 15 000 Zeichen als JSON; Schlüssel `[A-Za-z0-9._-]`, höchstens 64 Zeichen |
| Datensatz-Nutzlast je Aufruf | 256 KB als JSON |
| Höhe der Ansicht | 20 000 px |
| PDF-Viewer je Ansicht | 4, mit bis zu 50 Markierungen von je 2 000 Zeichen |
| HTML-Datei | 2 MB |

## Ansichten und der KI-Assistent

Paket-Module bringen keinen eigenen Chat mit. Der KI-Assistent von nexxus arbeitet auf jeder Paketseite, und eine Ansicht kann ihm sagen, was sie zeigt.

- Auf Listen-, Datensatz- und Seitenansichten teilt nexxus dem KI-Assistenten bereits Modul, Fachobjekt, Datensatz und Ansicht mit.
- `nx.assistant.setContext({summary, records})` ergänzt, was nur Ihre Ansicht weiß, zum Beispiel „3 Geräte überfällig“ samt der Datensätze dahinter. Das erscheint als Chip in der Assistentenspalte und geht mit jeder Nachricht mit, solange die Ansicht zu sehen ist.
- `nx.assistant.ask(prompt)` öffnet die Assistentenspalte mit einem neuen Gespräch. Das klappt nur direkt nach einem Klick oder Tastendruck in Ihrer Ansicht, eine Ansicht kann also kein Gespräch von sich aus beginnen.
- Die Beispiel-Anweisungen aus dem Manifest (`assistant.prompts` mit einem Schlüssel `view`) erscheinen als Chips in der Assistentenspalte, solange diese Ansicht zu sehen ist.

<Screenshot src={shot_nxm_20} url="/x/curiaos.kanzlei/akte/1/ansicht/akte.cockpit" alt={"Die Assistentenspalte mit Beispiel-Anweisungen und Kontext aus einer eigenen Ansicht"} caption={"Die Chips stammen aus den Beispiel-Anweisungen des Manifests; der Kontext-Chip nennt die Ansicht."} />

Wie der KI-Assistent die Werkzeuge eines Moduls nutzt, beschreiben [Mit dem KI-Assistenten arbeiten](/de/use/assistant/) und [Paketaufbau](/de/modules/structure/).

## Das Protokoll, zur Fehlersuche

Sie brauchen es selten, aber es hilft beim Lesen der Nachrichten im Browser.

1. Das SDK schickt `{type: 'nxm:ready', sdk}` an das übergeordnete Fenster.
2. nexxus prüft, dass die Nachricht aus dem eigenen Rahmen stammt (`event.source`), und antwortet mit `{type: 'nxm:connect'}` und einem `MessagePort`.
3. Alles Weitere läuft über diesen Port: Anfragen `{t: 'req', id, method, params}`, Antworten `{t: 'res', id, ok, result}` oder `{t: 'res', id, ok: false, error}` und Ereignisse `{t: 'evt', name, data}` in beide Richtungen.

Von nexxus an die Ansicht gehen die Ereignisse `init`, `context`, `theme`, `record` und `pdf.*`, von der Ansicht an nexxus `height`, `key` und `pdf.rect`. Die Methoden heißen `records.search`, `records.get`, `records.create`, `records.update`, `records.remove`, `lookup`, `invoke`, `actions.preview`, `nav.open`, `ui.toast`, `ui.confirm`, `dialog.close`, `storage.get`, `storage.set`, `assistant.setContext`, `assistant.ask`, `images.get`, `pdf.mount`, `pdf.update`, `pdf.goTo`, `pdf.find` und `pdf.destroy`.

## Bekannte Lücken

- **Keine Modulbeschreibung in der Ansicht.** Eine Ansicht kann die Feld- und Werte-Beschriftungen des Moduls nicht lesen. Wiederholen Sie die Beschriftungen, die Sie zeigen, mit `nx.t({de, en, pl})` und halten Sie sie mit dem Manifest gleich.
- **Keine Rechte in der Ansicht.** Eine Ansicht kann nicht fragen, ob die Person eine Aktion ausführen darf. Bieten Sie die Schaltfläche an und zeigen Sie die Meldung des Servers, wenn `FORBIDDEN` zurückkommt, oder legen Sie die Aktion in die Aktionsleiste des Datensatzes, die nexxus nur Personen zeigt, die sie ausführen dürfen.
- **Keine Symbolschrift.** Verwenden Sie eingebettetes SVG.

## Wenn etwas schiefgeht

Kann eine Ansicht nicht laufen, zeigt nexxus an ihrer Stelle eine eigene Meldung:

| Meldung | Ursache und Abhilfe |
|---|---|
| „Diese Ansicht konnte nicht geladen werden.“ | Die Ansicht ließ sich nicht laden, etwa nach einem Netzwerkfehler. <Ui>Erneut versuchen</Ui> lädt sie noch einmal. |
| „Für diese Ansicht fehlt die Berechtigung.“ | Der Person fehlt ein Recht des Moduls. Geben Sie ihr eine Rolle des Pakets, siehe [Rollen](/de/setup/roles/). |
| „Diese Ansicht gibt es nicht (mehr).“ | Den Ansichtsschlüssel gibt es in der installierten Version nicht, zum Beispiel weil ein Update ihn entfernt hat. |
| „Das Modul dieser Ansicht ist nicht aktiv.“ | Das Paket ist deaktiviert. Aktivieren Sie es auf seiner Paketseite. |

In Ihrer eigenen Ansicht sind die häufigsten Überraschungen ein `fetch`, das die Inhaltsrichtlinie sperrt (nehmen Sie `nx.records`), eine Sortierung oder ein Filter auf einem Feld ohne `indexed` (`VALIDATION`) und `nx.assistant.ask` ohne vorherigen Klick (`NO_USER_ACTION`).

## Wie es weitergeht

<CardGrid>
  <LinkCard title="Ihr erstes Modul" description="Eine Liste, ein Dialog und eine Seite, vom Manifest bis zum installierten Paket." href="/de/modules/first-module/" />
  <LinkCard title="Moduldienste" description="Wenn eine Ansicht nicht reicht: Code, der außerhalb von nexxus läuft und auf Änderungen reagiert." href="/de/modules/services/" />
  <LinkCard title="Paketaufbau" description="Jedes Feld der module.json, einschließlich Ansichten, Aktionen und Assistent." href="/de/modules/structure/" />
</CardGrid>