---
title: "Arc & Ninox Development Rules"
slug: "development-rules"
category: "Knowledge Base"
hosts: "ninox"
locale: "de"
---

# Arc & Ninox Development Rules

Diese Datei dient als Wissensbasis für die Entwicklung von Arc Widgets in Ninox. Sie wird kontinuierlich erweitert.

## 1. Ninox Script Syntax (Strict)

Ninox verwendet eine eigene Skriptsprache, die JavaScript ähnelt, aber wichtige Unterschiede aufweist.

### Kommentare & Block-Strings
*   ❌ **VERBOTEN**: `//` oder `/* ... */` (Führt zu Syntaxfehlern in bestimmten Kontexten).
*   ✅ **ERLAUBT**: `--- Mein Kommentar ---;` (WICHTIG: Mit Semikolon `;` abschließen, wenn es als Statement steht).
*   ⚠️ **ACHTUNG**: Innerhalb von `--- ---` dürfen keine geschweiften Klammern `{ }` verwendet werden, außer zur Variablen-Interpolation.
    *   Falsch: `--- User: { name: string } ---` (Ninox versucht `name: string` als Code auszuführen).
    *   Richtig: `--- User: ( name: string ) ---` oder `--- User: [Name: String] ---`.

#### ⚠️ **KRITISCH: Keine Kommentare innerhalb von JSON-Objekten**

Kommentare sind NUR als eigenständige Statements erlaubt, NIEMALS innerhalb von JSON-Objekt-Definitionen.

```javascript
// ❌ FALSCH: Kommentar innerhalb von JSON-Objekt
blocks: [{
    --- Das ist mein Block ---;  // FEHLER!
    width: "100%",
    value: "..."
}]

// ❌ FALSCH: comment Property (keine gültige Ninox-Property)
blocks: [{
    comment: "Das ist mein Block",  // FEHLER!
    width: "100%",
    value: "..."
}]

// ✅ RICHTIG: Kommentar als eigenständiges Statement vor dem Objekt
--- Das ist mein Block ---;
let myBlock := {
    width: "100%",
    value: "..."
};
```

**Faustregel:** Kommentare mit `--- ... ---;` sind Statements und können nur dort erscheinen, wo Statements erlaubt sind - NICHT innerhalb von `{ }` Objekt-Literalen.

### Variablen & Deklaration
*   ❌ **VERBOTEN**: `const`, `let`, `var` innerhalb von Widget-Konfigurationsobjekten.
*   ✅ **ERLAUBT**: `let myVar := ...` (nur im äußeren Ninox-Skript-Scope, nicht im JSON-Objekt).
*   **Zuweisung**: Ninox nutzt `:=` für Zuweisungen im Skript-Teil.

#### Variablen-Zuweisung für Widget-Snippets

Bei der Zuweisung komplexer Widget-Konfigurationen zu Variablen, KEINE `do...end` Blöcke verwenden. Stattdessen interne `let` Statements vor die Variablen-Zuweisung setzen.

```javascript
// ❌ FALSCH: Variable mit do...end Block
let my_card := do
    let list := (select Items);
    arcCustomCard({
        uniqueId: "card-1",
        value: list.[{ ... }]
    })
end;

// ✅ RICHTIG: let Statements davor, Variable ohne do...end
let list := (select Items);
let my_card := arcCustomCard({
    uniqueId: "card-1",
    value: list.[{ ... }]
});
```

**Für wiederverwendbare Funktionen in Snippet-Dateien:**
```javascript
// Funktionen global definieren (auf Top-Level)
function myHelper(data : any) do
    arcCustomLayout({ ... })
end;

// Dann in Variablen-Definitionen verwenden
let list := (select Items);
let my_card := arcCustomCard({
    uniqueId: "card-1",
    value: myHelper({ items: list })
});
```

**Vorteile:**
- Saubere Code-Struktur
- Funktionen sind global verfügbar
- Variablen sind direkt nutzbar ohne extra Scoping

### Strings & Interpolation
*   **HTML**: HTML-Strings müssen in Triple-Dashes `---` eingeschlossen werden.
*   **Interpolation**: Variablen in HTML-Strings werden mit `{ variableName }` eingefügt.
    *   Beispiel: `value: --- <div style="color: {data.color}">Text</div> ---`

### Funktions-Scope
*   ✅ **GLOBAL**: Funktionen sollten immer auf Top-Level definiert werden.
*   ❌ **LOKAL**: Verschachtelte Funktionen (Funktion innerhalb einer Funktion) vermeiden, da Ninox diese nicht im globalen Scope findet.

#### Funktions-Reihenfolge (Top-to-Bottom)
*   **Funktionen müssen vor ihrer Verwendung definiert sein.** Ninox wertet das Script von oben nach unten aus. Ruft `navBar` `pillButton` auf, muss `pillButton` vor `navBar` stehen, sonst: "Die Funktion ist nicht definiert: pillButton(any)".
*   Neue Helper-Funktionen, die von bestehendem Code genutzt werden, immer über dem ersten Aufruf platzieren.

## 2. Widget Konfiguration

### Grundstruktur
Die Konfiguration erfolgt immer als Funktionsaufruf, der ein JSON-Objekt zurückgibt.

```javascript
arcCustomWidgetName({
    uniqueId: "unique-id-" + Nr,
    property: "value",
    blocks: [ ... ]
})
```

### Best Practices
1.  **uniqueId**: MUSS vorhanden und einzigartig sein. Oft kombiniert mit der Record-ID (`Nr`).
    *   ⚠️ **WICHTIG**: Niemals Sonderzeichen, Kommas, Punkte oder Leerzeichen in IDs verwenden
    *   Diese IDs landen in HTML-IDs (`id="..."`) und können das Layout zerstören
    *   ❌ Falsch: `uniqueId: data.id + "-time-" + timeItem.label` (wenn label "0,5 Std." enthält)
    *   ✅ Richtig: `uniqueId: data.id + "-time-" + index` oder sanitized key
    ```javascript
    // ❌ Falsch: Label mit Sonderzeichen
    for timeItem in timeDetails do
        {
            value: arcCustomLayout({
                uniqueId: "detail-" + timeItem.label  // "0,5 Std." → HTML-Fehler!
            })
        }
    end
    
    // ✅ Richtig: Index oder sanitized key verwenden
    for timeItem in timeDetails do
        let idx := index(timeDetails, timeItem);
        {
            value: arcCustomLayout({
                uniqueId: "detail-" + idx  // Saubere ID
            })
        }
    end
    ```
2.  **embedded**: Meistens `true`, wenn es Teil eines Layouts ist.
3.  **Kein JSDoc**: Ninox-Editor unterstützt kein JSDoc. Dokumentation erfolgt separat oder via `--- Text ---`.
4.  **Widget Values**: Werte immer direkt übergeben. KEIN `if/else` für leere Werte – Widgets halten intern immer empty/null. Besser: `value: selectedTab.content` statt `value: if x then x else "" end`.

#### ⚠️ **HOHE PRIORITÄT: Null- und Leer-Fallbacks – KRITISCH FÜR AI**

**Arc Widgets fangen `null` automatisch ab.** Wenn eine Bedingung nichts zurückgibt (kein `else`-Zweig), erhält das Widget `null` und rendert korrekt. Daher:

- **NIEMALS** `else "" end` für Widget-Werte – Leerstring bricht das Rendering.
- **NIEMALS** `else null end` – redundant; den `else`-Zweig komplett weglassen.
- **IMMER** `if Bedingung then widget end` – kein `else` nötig. Widgets fangen null ab.

```javascript
// ❌ FALSCH: else "" end
leftSideContent: if hasData then arcCustomButton({...}) else "" end

// ❌ FALSCH: else null end (redundant)
leftSideContent: if hasData then arcCustomButton({...}) else null end

// ✅ RICHTIG: else weglassen – Widget erhält null automatisch
leftSideContent: if hasData then arcCustomButton({...}) end
```

**In den meisten Fällen ist `else "" end` NICHT nötig** – unsere Widgets fangen `null` automatisch ab, wenn es als Wert übergeben wird. Diese Regel ist wichtig, weil sie:
- Den Code verschlankt
- Typfehler mit `any` und gemischten Typen vermeidet (z.B. „dann/sonst liefern unterschiedliche Typen“)
- Redundanz reduziert

**`else [] end` ist NIEMALS nötig** – `null` übergeben oder den `else`-Zweig weglassen; Widgets handhaben leere Arrays.

```javascript
// ❌ VERBOTEN: else "" end
leftSideContent: if hasData then arcCustomButton({...}) else "" end

// ❌ REDUNDANT: else null end (niemals nötig)
leftSideContent: if hasData then arcCustomButton({...}) else null end

// ✅ RICHTIG: else weglassen – Widgets fangen null automatisch ab
leftSideContent: if hasData then arcCustomButton({...}) end

// ❌ Unnötig: else "" end für einfache Werte
value: if data.name then data.name else "" end

// ✅ Richtig: else weglassen – Widget erhält null, wenn Bedingung falsch
value: if data.name then data.name end

// ❌ Unnötig: else [] end
participants: if mitarbeiterData then [{ value: mitarbeiterData.name }] else [] end

// ✅ Richtig: else weglassen – Widget erhält null
participants: if mitarbeiterData then [{ value: mitarbeiterData.name }] end
```

**Ausnahme**: `else` nur verwenden, wenn der Fallback semantische Bedeutung hat (z.B. ein Standard-Label, das von „leer“ abweicht).

## 3. Datenzugriff & Actions

### Queries
Ninox-Abfragen werden direkt in die Widget-Struktur integriert, oft um Arrays für `blocks` zu generieren.
*   **Select**: `(select TableName)`
*   **Relation**: `record.RelationName`
*   **Mapping**: `(select Table).[{ value: ... }]`

#### ⚠️ **WICHTIG: Performance bei Filterung**

**Grundregel**: Bei `select`-Queries immer `where` verwenden, nicht die Bracket-Syntax `[...]`.

```javascript
// ❌ Langsamer: Bracket-Syntax bei select
let list := (select Tickets)[number(Status) = 1];

// ✅ Schneller: where-Syntax bei select
let list := (select Tickets where number(Status) = 1);

// ✅ Mit konditionaler Filterung
let filterPrio := number(filter_prio);
let list := (select Tickets where if filterPrio then number(Prio) = filterPrio else true end);
```

**Ausnahme**: Bei Untertabellen/Relations funktioniert nur die Bracket-Syntax:
```javascript
// ✅ Richtig: Bracket-Syntax bei Relations (where nicht möglich)
let items := record.Positionen[Aktiv = true];

// ❌ Falsch: where bei Relations (funktioniert nicht)
let items := record.Positionen where Aktiv = true;
```

**Zusammenfassung**:
- `(select Table where ...)` → performant bei Haupttabellen
- `relation[...]` → nur bei Untertabellen/Relations (dort geht kein `where`)

#### ⚠️ Filterwerte MÜSSEN außerhalb der Query definiert werden

Filterwerte für `where`-Klauseln oder `[...]`-Filter MÜSSEN als Variable VOR der Query definiert werden. Ninox kann Feldverweise aus anderen Kontexten (Dashboard, Parent-Record etc.) innerhalb von Query-Ausdrücken nicht zuverlässig auflösen.

```javascript
--- ❌ FALSCH: Inline-Feldzugriff in der Query ---;
let list := (select Termine where Status = dashboard.filter_status);
let items := record.Positionen[Typ = parent.SelectedType];

--- ✅ RICHTIG: Filterwert vorher extrahieren ---;
let filterStatus := dashboard.filter_status;
let list := (select Termine where Status = filterStatus);

let selectedType := parent.SelectedType;
let items := record.Positionen[Typ = selectedType];
```

Dies gilt für ALLE Queries – `select ... where`, Untertabellen `[...]` und `any`/`all`-Ausdrücke.

#### Mehrfachauswahl-Felder (Dynamic Choice Fields)

Ninox Mehrfachauswahl-Felder speichern intern eine kommaseparierte Liste von IDs. Um diese dynamisch zu verarbeiten:

**1. IDs extrahieren mit `numbers()`:**
```javascript
// numbers() gibt ein Array der ausgewählten IDs zurück
let selectedIds := numbers(Niederlassung.Typ);  // z.B. [1, 3, 5]
```

**2. Record anhand ID holen mit `record()`:**
```javascript
// record(Tabelle, ID) holt den vollständigen Datensatz
let typRecord := record(Typ_Firma, number(typId));
```

**3. Auf Felder des Records zugreifen:**
```javascript
typRecord.Titel              // Text-Feld
typRecord.'Farbe Hintergrund' // Feld mit Leerzeichen → in Hochkommas
```

**Vollständiges Pattern für dynamische Badges:**
```javascript
badges: for typId in numbers(Niederlassung.Typ) do
    let typRecord := record(Typ_Firma, number(typId));
    {
        width: "auto",
        height: "auto",
        value: arcCustomBadge({
            uniqueId: Nr + "-typ-badge-" + typId,
            value: text(typRecord.Titel),
            backgroundColor: if typRecord.'Farbe Hintergrund' then 
                text(typRecord.'Farbe Hintergrund') 
            else 
                "#888888" 
            end
        })
    }
end
```

**Wichtig:**
- `numbers()` funktioniert nur bei Mehrfachauswahl-Feldern
- Bei Einfachauswahl: direkt `number(Feld)` verwenden
- `record()` benötigt den Tabellennamen als erstes Argument (nicht die Tabellen-ID)

#### JSON-Lookup-Tabellen (for...do item end Pattern)

Um eine JSON-Liste filterbar zu machen, muss sie mit `for item in [...] do item end` gewrapped werden. Nur so kann man später mit `first(list[condition])` auf die Elemente zugreifen.

```javascript
// ❌ Falsch: Direktes Array ist nicht filterbar in Ninox
let labels := [{ diff: 0, label: "Heute" }, { diff: 1, label: "Morgen" }];
first(labels[diff = 0]);  // Funktioniert nicht!

// ✅ Richtig: Mit for...do item end aktivieren
let labels := for item in [
    { diff: 0, label: "Heute" },
    { diff: 1, label: "Morgen" },
    { diff: 2, label: "Übermorgen" },
    { diff: -1, label: "Gestern" },
    { diff: -2, label: "Vorgestern" }
] do item end;

// Jetzt filterbar!
let matchedLabel := first(labels[number(myDiff) = number(item.diff)]);
if matchedLabel then text(matchedLabel.label) else "Fallback" end
```

**Pattern für relative Datums-Texte:**
```javascript
// Oben definieren
let relativeDateLabels := for item in [
    { diff: 0, label: "Heute" },
    { diff: 1, label: "Morgen" },
    { diff: 2, label: "Übermorgen" },
    { diff: -1, label: "Gestern" },
    { diff: -2, label: "Vorgestern" }
] do item end;

// Unten verwenden
let actDate := date(myRecord.Datum);
let diff := round((actDate - today()) / 86400000);  // Millisekunden → Tage
let matchedLabel := first(relativeDateLabels[number(diff) = number(item.diff)]);
title: if matchedLabel then
    text(matchedLabel.label)
else
    if diff > 1 then
        "In " + diff + " Tagen"
    else
        if diff = 1 then
            "In 1 Tag"  // Singular!
        else
            if diff = -1 then
                "Vor 1 Tag"  // Singular!
            else
                "Vor " + abs(diff) + " Tagen"
            end
        end
    end
end
```

**Wichtig - Datums-Differenz in Ninox:**
- ❌ `days()` funktioniert nur mit der appointment-Funktion
- ✅ `round((datum1 - datum2) / 86400000)` - rechnet Millisekunden in Tage um
- 86400000 = 24h × 60min × 60s × 1000ms
- `round()` rundet auf ganze Tage
- ⚠️ **Grammatik beachten**: Singular bei 1 Tag ("In 1 Tag", nicht "In 1 Tagen")

**Vorteile:**
- Zentrale Definition, leicht erweiterbar
- Sauberer Code in der Anwendung
- Beliebig viele Spezialfälle (Heute, Morgen, Übermorgen...) ohne verschachtelte if/else
- Korrekte Singular/Plural-Grammatik

#### IMMER Anzeigenamen, NIEMALS interne IDs in NinoxScript

In NinoxScript-Code (Formeln, .arc-Dateien) **IMMER** den **Anzeigenamen** (Caption) der Felder verwenden, **NIEMALS** die interne `_id`.

Das YAML-Schema zeigt Felder so:
```
Aufgabe:           ← ANZEIGENAME → im Code verwenden
  _id: A           ← INTERNE ID → NIEMALS im NinoxScript
```

- `Aufgabe: { _id: A }` → im Code `Aufgabe` verwenden, NICHT `A`
- `Fällig: { _id: B }` → im Code `'Fällig'` verwenden, NICHT `B`
- Feldnamen mit **Umlauten** (ä, ö, ü, ß) oder **Sonderzeichen** MÜSSEN in **einfache Anführungszeichen** gesetzt werden: `'Fällig'`, `'Größe'`, `'Straße'`, `'E-Mail'`
- Feldnamen ohne Sonderzeichen brauchen keine Anführungszeichen: `Aufgabe`, `Status`, `Datum`

```javascript
--- ❌ FALSCH: interne _id verwenden ---;
if A = "offen" then 10 else 0 end
let x := B + 5
(select Termine where C = "aktiv")

--- ✅ RICHTIG: Anzeigenamen verwenden ---;
if Aufgabe = "offen" then 10 else 0 end
let x := 'Fällig' + 5
(select Termine where Status = "aktiv")
```

**Ausnahme:** Seed-Daten JSON (`ninox:seed`) verwenden `_id` als Feld-Keys, da die Ninox REST API diese erwartet.

#### fieldId immer verwenden

Bei allen Widget-Actions und Select-Fields muss `fieldId()` verwendet werden, nicht nur der Feldname als String. Die `Nr` ist entscheidend für die korrekte Feld-ID.

```javascript
// ❌ Falsch: Nur Feldname als String
field: "Bearbeiter"

// ✅ Richtig: fieldId mit korrekter Record-Nr
field: fieldId(Nr, "Bearbeiter")

// ✅ Bei JSON-Konfigurationen: fieldId oben definieren wo Nr verfügbar ist
selectField: fieldId(Nr, "Bearbeiter")  // Im JSON wo Ninox-Kontext verfügbar ist
```

**Wichtig:** In verschachtelten Strukturen oder `any`-Parametern hat Ninox keinen Zugriff mehr auf `Nr`. Daher `fieldId` immer dort aufrufen, wo der Record-Kontext noch vorhanden ist.

#### arcCustomSelect / Verknüpfungsfelder – value muss number(Nr) sein

Bei dynamischen Selects und Verknüpfungsfeldern muss der `value` in items `number(Nr)` sein, nicht rohes `Nr`. Ninox erwartet numerische Werte für Record-Referenzen.

```javascript
// ❌ FALSCH: rohes Nr
items: (select Artikel).[{ title: text('Beschreibung Excel'), value: Nr }]

// ✅ RICHTIG: number(Nr)
items: (select Artikel).[{ title: text('Beschreibung Excel'), value: number(Nr) }]
```

#### fieldId – Tabellenname erforderlich

`fieldId()` für tabellenübergreifende Referenzen (z.B. in changeFieldValues) muss den **Anzeigenamen der Tabelle** verwenden, nicht die interne Tabellen-ID. `fieldId("XE", "FieldName")` funktioniert nicht.

```javascript
// ✅ RICHTIG: Tabellen-Anzeigename
fieldId: fieldId("Auftraege", "Bearbeiter")

// ❌ FALSCH: Tabellen-ID funktioniert nicht
fieldId: fieldId("A", "Bearbeiter")
```

#### if/else Typ-Konsistenz – KRITISCH

**Ninox verlangt, dass beide Zweige `then` und `else` denselben Datentyp zurückgeben.** Sonst: „Die Ausdrücke für 'dann' und 'sonst' liefern unterschiedliche Datentypen zurück“.

```javascript
// ❌ FALSCH: string vs nid – unterschiedliche Typen
title: text(if 'Beschreibung Excel' then 'Beschreibung Excel' else Nr end)

// ✅ RICHTIG: text() in beiden Zweigen – gleicher Typ
title: if 'Beschreibung Excel' then text('Beschreibung Excel') else text(Nr) end
```

**Bei Mischung von Text und Widget** (z.B. Tabellenspalten-Wert: Text ODER arcCustomSelect) müssen beide Zweige denselben Typ liefern. Für den Textfall `arcCustomText` verwenden, damit beide Widgets zurückgeben:

```javascript
// ❌ FALSCH: html(text(...)) vs widget – weiterhin string vs nid
value: if Artikel then html(text(Artikel.'Beschreibung Excel')) else arcCustomSelect({...}) end

// ✅ RICHTIG: arcCustomText vs arcCustomSelect – beide Widgets
value: if Artikel then arcCustomText({ value: text(...), fontSize: "13px" }) else arcCustomSelect({...}) end
```

#### Dashboard-Filterfelder – `current.` Prefix verwenden

Wenn Filterfelder (z.B. `filter_status_quotes`, `filter_category`) auf dem **Dashboard-Record** (dem Formular/Record, auf dem das Widget läuft) gespeichert sind, immer über `current.` in Queries und Datenlogik zugreifen. Der Dashboard-Record ist `this` / `current`; ohne Prefix kann Ninox das Feld im falschen Kontext auflösen (z.B. in einer Schleife über Tickets).

```javascript
// ❌ Falsch: filter_status_quotes ohne current – falscher Kontext in Queries
let list := list_tickets[number(Status) = number(filter_status_quotes)];

// ✅ Richtig: current.filter_status_quotes – expliziter Dashboard-Record
let list := list_tickets[number(Status) = 4 and if current.filter_status_quotes then
    number(Status) = number(current.filter_status_quotes)
else
    true
end];
```

**Faustregel:** Filterfelder auf dem Dashboard → in Queries und Filterlogik `current.filter_feldname` verwenden.

#### Dynamische IDs in Schleifen (Counter-Pattern)

Wenn du dynamische uniqueIds in Schleifen brauchst, muss die Counter-Variable einmal mit `let` definiert und dann ohne `let` überschrieben werden.

```javascript
// ❌ Falsch: let in jeder Iteration → IDs bleiben gleich
for item in items do
    let idx := idx + 1;  // Falsch! Neue Variable in jeder Iteration
    { uniqueId: "item-" + idx }
end

// ✅ Richtig: let einmal vor der Schleife, dann ohne let überschreiben
let idx := 0;
for item in items do
    idx := idx + 1;  // Richtig! Überschreibt die äußere Variable
    { uniqueId: "item-" + idx }
end
```

**Problem bei falschem Pattern:** Alle Widgets bekommen dieselbe ID → Widgets werden nicht angezeigt (besonders bei Select/Input).

#### setItem – Mutiert Objekt In-Place

**`setItem` mutiert das übergebene Objekt in-place.** Es gibt kein neues Objekt mit der Änderung zurück. Alles, was nach dem `setItem`-Aufruf kommt, sieht das mutierte Objekt. Wenn der modifizierte State an eine Action übergeben wird (z.B. `type: "update"` mit `value`), die **gleiche Variable** verwenden, die an `setItem` übergeben wurde – sie enthält nun die Änderung.

```javascript
// ❌ Falsch: rowActionDataTeil enthält ggf. nicht die Änderung (Rückgabewert von setItem)
let rowActionDataTeil := setItem(parseJSON(...), "teiluntersuchung_recordId", text(Nr));
value: formatJSON(rowActionDataTeil)  // Hat ggf. nicht die Änderung

// ✅ Bevorzugt: dataBag patch
dataBag: { base: helper_state, patch: { teiluntersuchung_recordId: text(Nr) } }

// ✅ Manuell: Geparstes Objekt in Variable, setItem mutiert es, diese Variable verwenden
let parsedStateTeil := parseJSON(text(current.helper_state));
setItem(parsedStateTeil, "teiluntersuchung_recordId", text(Nr));
value: formatJSON(parsedStateTeil)  // parsedStateTeil hat die Änderung
```

**Faustregel:** In Variable parsen → `setItem` darauf aufrufen → diese Variable für `formatJSON` / `value` nutzen.

#### Flexible Block-Breiten statt fixer Werte

Statt feste Pixel-Breiten für Widgets zu verwenden, setze den Block auf `width: "fraction"` und das Widget auf `width: "100%"`. So passt sich die Breite dynamisch an.

```javascript
// ❌ Unnötig komplex: Feste Breiten und extra Konfiguration
{
    width: "auto",
    value: arcCustomSelect({
        width: "140px"  // Fix
    })
}

// ✅ Besser: Block fraction, Widget 100%
{
    width: "fraction",
    value: arcCustomSelect({
        width: "100%"  // Füllt Block aus
    })
}
```

### Actions

#### ⚠️ **WICHTIG: Action-Typ Hierarchie**

**Grundregel**: Bei Feld-Updates IMMER widget-vorgefertigte Actions verwenden, NIEMALS customJS für Standard-Operationen.

1. **ERSTE WAHL: `update` Action** 
   - Für alle Feld-Updates verwenden
   - Ninox Trigger werden automatisch ausgelöst
   - Sauber, wartbar, performant
   
2. **ZWEITE WAHL: Ninox Trigger + Helper-Feld**
   - Für komplexe Operationen, die Ninox-Funktionen benötigen
   - Z.B. `openURL()` für Mobile-Kompatibilität
   
3. **LETZTE WAHL: `customJS`**
   - NUR für spezielle UI-Anforderungen
   - Niemals für Feld-Updates
   - Niemals für Standard-Operationen

```javascript
// ❌ FALSCH: customJS für Feld-Updates
actions: [{
    type: "customJS",
    value: "ninoxUpdate(" + Nr + ", '" + fieldId(Nr, "Status") + "', 'Aktiv'); " +
           "ninoxUpdate(" + Nr + ", '" + fieldId(Nr, "Tab") + "', 'day');"
}]

// ✅ RICHTIG: Separate update Actions
actions: [{
    type: "update",
    recordId: Nr,
    field: fieldId(Nr, "Status"),
    value: "Aktiv"
}, {
    type: "update",
    recordId: Nr,
    field: fieldId(Nr, "Tab"),
    value: "day"
}]
```

**Vorteile von `update` Actions**:
- ✅ Ninox Trigger werden automatisch ausgeführt
- ✅ Validierungen greifen
- ✅ Änderungen werden korrekt protokolliert
- ✅ Bessere Performance
- ✅ Wartbarer Code

#### Update Action Details
*   ✅ **UPDATE**: Für Datenänderungen `type: "update"` verwenden (sauberer als customJS).
    *   **recordId**: Innerhalb von Mappings/Schleifen (`select Table`) meist `Nr` (Referenz auf den aktuellen Datensatz).
    *   **field/fieldId**: Parameter für das Feld. Achtung: Manche Widgets verlangen `field`, andere `fieldId`.
        *   Wert ist oft die interne ID (z.B. `"A1"`) oder dynamisch ermittelt via `fieldId("TableName", "FieldName")` (besser, da robust gegen Umbenennung).
    ```javascript
    actions: [{
        type: "update",
        recordId: Nr, // Aktueller Datensatz in der Schleife
        field: fieldId("MyTable", "Status"), // oder "A1"
        value: "newValue"
    }]
    ```

#### ⚠️ **KRITISCH: recordId bei Actions - NIEMALS number() verwenden**

Bei `type: "popup"` oder `type: "update"` muss `recordId` immer die **rohe `Nr`** sein, NICHT `number(Nr)`.

**Grund:** Ninox benötigt die versteckte `nxid` (Tabelle + Record-Nummer), die in `Nr` enthalten ist. `number()` entfernt diese Information!

```javascript
// ❌ FALSCH: number() entfernt nxid → Record kann nicht geöffnet werden
actions: [{
    type: "popup",
    recordId: number(helper_nextOpenActivity.Nr)  // Fehler!
}]

// ✅ RICHTIG: Nr direkt verwenden
actions: [{
    type: "popup",
    recordId: helper_nextOpenActivity.Nr  // Behält nxid
}]

// ✅ RICHTIG: Bei update auch Nr direkt
actions: [{
    type: "update",
    recordId: Nr,  // Nicht number(Nr)
    field: fieldId(Nr, "Status"),
    value: "active"
}]
```

**Faustregel:** `recordId` in Actions = immer die rohe `Nr` ohne Konvertierung!

#### Multiple Actions Pattern
Widgets unterstützen Arrays von Actions, die sequenziell ausgeführt werden:

```javascript
// Navigation + Datum setzen
actions: [{
    type: "update",
    recordId: Nr,
    field: fieldId(Nr, "Datum von"),
    value: today()
}, {
    type: "update",
    recordId: Nr,
    field: fieldId(Nr, "helper_mobileNav"),
    value: "day"
}]
```

#### Bestätigungsdialog (confirm)

Jede Action kann vor der Ausführung einen Bestätigungsdialog anzeigen, indem eine `confirm`-Property hinzugefügt wird. Der Dialog wird außerhalb des Ninox-Widget-DOM gerendert und überlebt daher Hintergrund-Neuladungen des Funktionsfelds (z.B. Datenaktualisierungen durch andere User).

```javascript
// ✅ Destruktive Aktion mit Bestätigung
actions: [{
    type: "delete",
    recordId: Nr,
    confirm: {
        title: "Datensatz löschen",
        message: "Möchten Sie diesen Datensatz wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
        confirmLabel: "Löschen",
        cancelLabel: "Abbrechen",
        destructive: true
    }
}]

// ✅ Nicht-destruktive Aktion mit Bestätigung
actions: [{
    type: "update",
    recordId: Nr,
    field: fieldId(Nr, "Status"),
    value: "Archiviert",
    confirm: {
        title: "Datensatz archivieren",
        message: "Dieser Datensatz wird ins Archiv verschoben.",
        confirmLabel: "Archivieren",
        destructive: false
    }
}]
```

**Properties:**

| Property | Type | Default | Beschreibung |
|----------|------|---------|--------------|
| `title` | string | — | Dialog-Titel |
| `message` | string | — | Beschreibungstext |
| `confirmLabel` | string | `"Bestätigen"` | Label des Bestätigen-Buttons (frei wählbar) |
| `cancelLabel` | string | `"Abbrechen"` | Label des Abbrechen-Buttons (frei wählbar) |
| `destructive` | boolean | `false` | Roter Confirm-Button + Warn-Icon; `false` = blaues Info-Icon |

**Verhalten:**
- Bei Bestätigung: Die Aktion wird normal ausgeführt
- Bei Abbruch: Die Aktion wird übersprungen, keine Seiteneffekte
- Funktioniert mit ALLEN Action-Typen (`update`, `delete`, `popup`, `create`, etc.)

**Limitation:** Bei `actions`-Arrays mit mehreren Einträgen `confirm` auf eine einzelne Action setzen (typischerweise die erste oder einzige destruktive Action). Jede Action im Array wird unabhängig bestätigt.

#### Verkettete Actions (`then`)

Actions können mit der `then`-Property Folge-Aktionen nach Abschluss ausführen. Nützlich, wenn z.B. nach einem Löschen der UI-State zurückgesetzt werden soll (z.B. Detail-Ansicht schließen).

```javascript
// Datensatz löschen, dann Auswahl zurücksetzen
actions: [{
    type: "delete",
    recordId: selectedItem.Nr,
    confirm: {
        title: "Datensatz löschen",
        message: "Wirklich löschen?",
        confirmLabel: "Löschen",
        cancelLabel: "Abbrechen",
        destructive: true
    },
    then: [{
        type: "update",
        recordId: Nr,
        field: mainContentStateField,
        value: formatJSON({ selectedId: "" })
    }]
}]
```

- `then` muss ein Array von Action-Objekten sein
- Wird sequenziell nach Abschluss der Haupt-Aktion ausgeführt
- Funktioniert mit `confirm` – `then` läuft nur nach Bestätigung und erfolgreicher Haupt-Aktion

#### URLs und Telefonnummern öffnen (Mobile App Kompatibilität)

⚠️ **Problem**: `window.open()` via `customJS` funktioniert im Web, aber **nicht in Ninox Mobile Apps**

✅ **Lösung**: Ninox Helper-Feld mit `openUrl()` Trigger verwenden

**Pattern**:
1. Erstelle ein Text-Feld (z.B. `trigger_openUrl`) mit folgendem Trigger:
   ```ninox
   openURL(trigger_openUrl);
   
   trigger_openUrl := null
   ```

2. Nutze `update` Action statt `customJS`:
   ```javascript
   // ❌ Falsch: Funktioniert nur im Web
   actions: [{
       type: "customJS",
       value: "window.open('tel:+49123456789')"
   }]
   
   // ✅ Richtig: Funktioniert Web + Mobile App
   actions: [{
       type: "update",
       recordId: current.Nr,
       field: fieldId(current.Nr, "trigger_openUrl"),  // Aktuell: field, später: fieldId
       value: "tel:+49123456789"  // oder "https://..." für Links
   }]
   ```

**Beispiele**:
```javascript
// Telefonnummer
actions: [{
    type: "update",
    recordId: current.Nr,
    field: fieldId(current.Nr, "trigger_openUrl"),
    value: "tel:" + phoneNumber
}]

// Google Maps
actions: [{
    type: "update",
    recordId: current.Nr,
    field: fieldId(current.Nr, "trigger_openUrl"),
    value: "https://www.google.com/maps/search/?api=1&query=" + urlEncode(address)
}]

// Externe Website
actions: [{
    type: "update",
    recordId: current.Nr,
    field: fieldId(current.Nr, "trigger_openUrl"),
    value: "https://example.com"
}]
```

> ⚠️ **Hinweis**: Aktuell muss `field` statt `fieldId` verwendet werden. Dies wird in zukünftigen Widget-Versionen vereinheitlicht.

**Vorteile**:
- ✅ Funktioniert in Web **und** Mobile App
- ✅ Zentralisierte URL-Logik (ein Trigger für alle URL-Typen)
- ✅ Keine `customJS` Security-Einschränkungen

*   **Generic Slots**: Für flexible Komponenten `actionsLeft` / `actionsRight` statt nur `actions` verwenden, wenn das Layout es erfordert.
*   *Feature Request*: Vereinheitlichung von `field` und `fieldId` Parameter über alle Widgets hinweg.

### Formatierung (format-Funktion)

Ninox bietet die `format()`-Funktion zum Formatieren von Zahlen, Datum und Zeit. Die Funktion verwendet spezielle Format-Token (ähnlich Moment.js).

**Referenz**: [Ninox format() Dokumentation](https://forum.ninox.com/t/p8yzv31/format)

#### Datumsformatierung
```javascript
// ❌ Falsch: Ungültige Format-Tokens
format(myDate, "EEEE, dd.MM.yyyy")  // Produziert "2222, Di.12.2025"

// ✅ Richtig: Korrekte Ninox Format-Tokens
format(myDate, "dddd, DD.MM.YYYY")  // "Dienstag, 01.12.2025"

// ✅ Alternative: text() für Standard-Formatierung
text(myDate)  // Nutzt System-Locale

// ✅ Wochentag extrahieren
weekdayName(weekday(myDate))  // "Dienstag"
substr(weekdayName(weekday(myDate)), 0, 2)  // "Di"
```

**Format-Tokens für Datum:**
- `dddd` - Vollständiger Wochentag (Monday, Dienstag)
- `ddd` - Abgekürzter Wochentag (Mon, Di)
- `DD` - Tag mit führender Null (01-31)
- `MM` - Monat mit führender Null (01-12)
- `YYYY` - Jahr 4-stellig (2025)
- `Do` - Tag mit Ordnungszahl (1st, 2nd, 3rd)
- `MMMM` - Vollständiger Monatsname (January, Dezember)

**Format-Tokens für Zeit:**
- `HH` - Stunden 24h-Format mit führender Null (00-23)
- `mm` - Minuten mit führender Null (00-59)
- `ss` - Sekunden mit führender Null (00-59)

#### Zahlenformatierung
```javascript
// Format: format(number, "prefix#thousands.decimals suffix#thousands_sep#decimal_sep")
format(2385.97, "#,##0.00 €#,#0.0")  // "2.385,97 €" (DE)
format(2385.97, "$#,.#0,0")          // "$2,385.97" (US)
format(12345, "0000000000")          // "0000012345" (Leading zeros)
```

## 4. Design Patterns

### Nested Layouts
Um komplexe Designs zu erreichen, werden `arcCustomLayout` Widgets tief verschachtelt.
*   Level 1: Container (Vertical)
*   Level 2: Row (Horizontal)
*   Level 3: Item (Content)

### Generic Props
Wenn Funktionen erstellt werden, sollten die Parameter generisch benannt sein, um Wiederverwendbarkeit zu sichern.

*   **Inhalt**: Meistens `value` für den anzuzeigenden Text/Inhalt (z.B. bei `arcCustomText`, `arcCustomBadge`).
    *   *Ausnahme*: `arcCustomButton` nutzt `title`.
*   **Benennung**:
    *   `title` statt `sender` (für generische Karten-Header)
*   `subtitle` statt `company`
*   `meta` statt `timestamp`
*   `action` statt `clickFunction`

## 5. Layout & Dimensionen

### Fullscreen-Konfiguration
Für Dashboard-Layouts, die ohne Ninox-UI-Elemente angezeigt werden sollen:
*   ✅ `embedded: false` (kein eingebettetes Widget)
*   ✅ `fullscreen: true`
*   ✅ `fullscreenMode: if isAdminMode() then "" else "full" end` (im Nicht-Admin-Modus wird die Ninox-UI ausgeblendet)
*   ✅ `height: "100%"` für den äußersten Container (nicht `auto`)

### Width & Height nach Orientierung
Orientierung bestimmt, welche Dimension flexibel sein sollte:
*   **Vertical (`direction: "vertical"`)**: `width: "100%"` (Container nimmt volle Breite), `height: "auto"` (passt sich Inhalt an) oder `"100%"` für Fullscreen
*   **Horizontal (`direction: "horizontal"`)**: `height: "auto"` oder feste Höhe, `width` kann `"100%"`, `"auto"`, oder `"fraction"` sein

### Scrollbare Content-Bereiche
Um Navigation/Header oben sichtbar zu halten, während nur der Content scrollt:
*   Erstelle einen Wrapper mit `height: "fraction"` (nimmt verbleibenden Platz)
*   Setze `scrollSettings: { scrollY: true, scrollX: false }`
*   Setze `alignY: "top"` für die Ausrichtung
```javascript
// Navigation (height: auto) + Content Wrapper (height: fraction + scrollY: true)
blocks: [
    { /* Header/Nav */ height: "auto" },
    { /* Scrollable Content */ height: "fraction", value: arcCustomLayout({ scrollSettings: { scrollY: true } }) }
]
```

### CSS Positioning & Layering

#### Position Sticky
Für Sticky-Header, die beim Scrollen oben kleben bleiben:
*   ✅ **Styles im Block**: `styles: "position: sticky; top: 0px; z-index: 10; background-color: #F9FAFE;"`
*   ✅ **Overflow anpassen**: Parent-Container darf NICHT `overflow: hidden` haben
    *   Verwende `overflow-x: hidden` statt `overflow: hidden` wenn nötig
*   ✅ **Hintergrund setzen**: Ohne `background-color` scheint der Content durch
*   ✅ **Z-Index**: Für Layering über anderen Elementen (z.B. `z-index: 10`)

```javascript
// ❌ Blockiert Sticky:
arcCustomLayout({
    styles: "overflow: hidden;",
    blocks: [{ styles: "position: sticky; top: 0px;" }]
})

// ✅ Funktioniert:
arcCustomLayout({
    styles: "overflow-x: hidden;",  // Nur horizontal blockieren
    blocks: [{
        styles: "position: sticky; top: 0px; z-index: 10; background-color: #F9FAFE;"
    }]
})
```

**Wichtig:** `position: sticky` funktioniert nur, wenn zwischen dem sticky Element und dem scrollbaren Container KEIN `overflow: hidden` ist.

### Widget-spezifische Props

#### arcCustomSelect
*   **recordId** & **field**: Pflichtfelder für Daten-Binding
*   **autoFocus deaktivieren**: Verhindert automatisches Tastatur-Öffnen (wichtig auf Tablets)
    ```javascript
    focusAction: {
        autoFocus: false
    }
    ```

#### arcCustomBadge
*   **Text-Farbe**: `fontColor:` (nicht `color:` oder `textColor:`)
*   **Border**: Badge hat per default einen sichtbaren Border. Für randlose Badges `borderColor` auf `transparent` oder gleiche Farbe wie `backgroundColor` setzen.
    ```javascript
    arcCustomBadge({
        value: "Text",
        fontColor: "#FFFFFF",  // ✅ Korrekt
        backgroundColor: "#3388FF",
        borderColor: "#3388FF"  // ✅ Gleiche Farbe wie Background für unsichtbaren Border
    })
    ```

#### arcCustomButton
*   **Icon-Only Buttons**: Für Buttons ohne Text nur mit Icon
    *   Verwende `title: ""` (leerer String) für reine Icon-Buttons
    *   Setze `icon` Parameter mit `arcCustomIcon`
    *   Besser als verschachtelte Layouts, da hover-Actions und button states bereits integriert sind
    ```javascript
    // ✅ Sauber: Button mit Icon, ohne Text
    arcCustomButton({
        uniqueId: "close-btn",
        title: "",  // Leer für Icon-Only
        width: "30px",
        height: "30px",
        backgroundColor: "#EBEDF5",
        borderRadius: "50%",
        icon: arcCustomIcon({
            name: "x",
            color: "#555555",
            size: "16px"
        }),
        actions: [{ type: "update", recordId: Nr, field: "...", value: null }]
    })
    
    // ❌ Unnötig komplex: Layout-Wrapper mit Icon
    arcCustomLayout({
        width: "30px",
        height: "30px",
        clickAction: { ... },
        blocks: [{
            value: arcCustomIcon({ ... })
        }]
    })
    ```
*   **Actions im Widget**: Verwende `actions` Array im Button statt `clickAction` im Block-Wrapper
    *   Nutzt Button-Features (hover, active, disabled states)
    *   Ermöglicht zukünftige `hoverActions` Konfiguration

*   **Badge-System**: Buttons unterstützen Badges für Notifications/Counter
    *   ✅ **Separate Properties**: `showBadge`, `badgeTitle`, `badgeColor`, `badgeBackground`, `badgeBorderColor`, `badgePosition`, `badgeGap`
    *   ❌ **NICHT** ein verschachteltes `badge: { ... }` Objekt verwenden
    *   `showBadge` ist ein boolean und kann konditional gesetzt werden
    ```javascript
    // ✅ Richtig: Separate Badge-Properties
    arcCustomButton({
        uniqueId: "btn-kommentare",
        title: "Kommentare",
        showBadge: if hasNewComments then true else false end,
        badgeTitle: "3",
        badgeColor: "#FFFFFF",
        badgeBackground: "#FF3B30",
        badgeBorderColor: "#FFFFFF",
        badgePosition: "",  // Standard-Position
        badgeGap: "6px",   // Abstand zwischen Inhalt und Badge
        actions: [...]
    })
    
    // ❌ Falsch: Badge als Objekt (funktioniert nicht)
    arcCustomButton({
        uniqueId: "btn-kommentare",
        title: "Kommentare",
        badge: {
            value: "3",
            backgroundColor: "#FF3B30",
            fontColor: "#FFFFFF"
        }
    })
    ```
    
    **Conditional Badge Display Pattern**:
    ```javascript
    // Pattern: Badge nur bei bestimmter Tab-ID und wenn Content vorhanden
    showBadge: if tab.id = "kommentare" and tab.hasComment then true else false end,
    badgeTitle: text(commentCount),
    badgeColor: "#FFFFFF",
    badgeBackground: "#FF3B30"
    ```

#### arcCustomIcon
*   **Eingebautes Container-Handling**: Das Icon-Widget verwaltet seinen Container selbst
    *   Nutze `containerSize` statt externe Layout-Wrapper für Icon-Größe
    *   `size` Parameter steuert Icon-Größe innerhalb des Containers
    *   Kein zusätzliches `arcCustomLayout` um Icons nötig
*   **Container-Styling**: Alle Container-Styles können direkt am Icon gesetzt werden
    *   `backgroundColor`: Hintergrundfarbe des Icon-Containers
    *   `borderRadius`: Abrundung des Containers (z.B. `"50%"` für Kreis)
    *   `borderColor`: Rahmenfarbe des Containers
    *   `borderSize`: Rahmenstärke des Containers
    ```javascript
    // ✅ Icon mit vollständigem Container-Styling
    arcCustomIcon({
        name: "user",
        color: "#818599",              // Icon-Farbe
        size: "30px",                  // Icon-Größe
        containerSize: "52px",         // Container-Größe
        backgroundColor: "#EBEDF5",    // Container-Hintergrund
        borderRadius: "50%",           // Runder Container
        borderColor: "#DDD",           // Container-Rahmen
        borderSize: "1px"              // Rahmen-Stärke
    })
    
    // ❌ Unnötig: Zusätzliches Layout für Styling
    arcCustomLayout({
        width: "52px",
        height: "52px",
        backgroundColor: "#EBEDF5",
        styles: "border-radius: 50%; border: 1px solid #DDD;",
        blocks: [{
            value: arcCustomIcon({
                name: "user",
                color: "#818599",
                size: "30px"
            })
        }]
    })
    ```
*   **Vorteil**: Ein Widget statt verschachtelter Layouts → weniger Code, bessere Performance

#### arcCustomTable – Leerzustand
*   **Leerzustand via emptyTable**: Nutze `emptyTable` settings (title, value) statt die Tabelle mit `if hasItems then table else arcCustomLayout({ ... empty state ... }) end` zu umschließen.
*   Das Table-Widget zeigt leere Listen intern an; kein extra Layout für „Keine Daten“ nötig.

#### Ninox Zeitfelder – Anzeige
*   **`text()` statt `format()` für Zeit**: Ninox kann falsche Zeiten (z.B. 00:00) anzeigen bei `format(timeField, "HH:mm")` oder Zeit-Arithmetik. Nutze `text(timeField)` für zuverlässige Anzeige.
*   **Felder**: `'Uhrzeit von'` und `'Uhrzeit bis'` für Zeitbereiche.

## 6. Code-Qualität & Best Practices

### Leserliche Variablennamen
Verwende aussagekräftige Namen für Loop-Variablen, besonders bei verschachtelten Schleifen:
```javascript
// ❌ Schlecht:
for t in types do
    for i in items do
        // Welches "i"? Welches "t"?
    end
end

// ✅ Gut:
for typeItem in types do
    for iconItem in icons do
        // Klar und verständlich
    end
end
```

### Type Coercion bei `any` Parametern

⚠️ **Problem**: Wenn Werte als `any` in Funktionsparametern übergeben werden, verlieren sie ihre Typinformation in Ninox. Dies führt zu Fehlern bei Operationen oder Funktionsaufrufen, die spezifische Typen erwarten.

✅ **Lösung**: Explizite Typ-Konvertierung mit `number()`, `date()`, `text()` etc.

#### Zahlen-Operationen
```javascript
// ❌ Falsch: Type-Loss bei any Parameter
function calculateNextDay(data : any) do
    data.currentDate + 1  // Fehler: "Ungültiger Operator: any - number"
end

// ✅ Richtig: Explizite date() Konvertierung
function calculateNextDay(data : any) do
    date(data.currentDate) + 1  // Funktioniert
end
```

#### Datum-Formatierung
```javascript
// ❌ Falsch: format() erwartet Date-Typ
format(footerData.currentDate, "DD.MM.YYYY")  // Fehler bei any-Typ

// ✅ Richtig: date() Konvertierung vor format()
format(date(footerData.currentDate), "DD.MM.YYYY")  // Funktioniert

// ✅ Auch für month(), year(), day()
month(date(footerData.currentDate))
year(date(footerData.currentDate))
```

#### Nested Object Properties
```javascript
// ❌ Falsch: Verschachtelte any-Properties ohne Konvertierung
let weekEnd := date(data.dataDates.firstWeekDay) + 6  // firstWeekDay verliert Typ

// ✅ Richtig: Beide Werte konvertieren
let firstDay := date(data.dataDates.firstWeekDay);
let weekEnd := date(firstDay) + 6;  // Explizite Konvertierung
```

**Häufige Konvertierungen**:
- `number()` - Für Zahlen und Berechnungen
- `date()` - Für Datum-Operationen
- `text()` - Für String-Operationen
- `time()` - Für Zeit-Werte

### Text-Konvertierung in Schleifen
Werte aus Ninox-Objekten sollten mit `text()` konvertiert werden, besonders in verschachtelten Strukturen:
```javascript
// ✅ Konsistente Konvertierung:
for typeItem in cardData.types do
    {
        value: arcCustomBadge({
            value: text(typeItem.value),
            fontColor: if typeItem.color then text(typeItem.color) else "#A114A8" end,
            backgroundColor: if typeItem.backgroundColor then text(typeItem.backgroundColor) else "#FDDEFF" end
        })
    }
end
```

**Grund:** In tiefen Verschachtelungen können Ninox-Werte ihren Typ verlieren. `text()` stellt sicher, dass Strings korrekt übergeben werden.

#### ⚠️ **KRITISCH: Konsistente Datentypen in if/else Zweigen**

Beide Zweige eines if/else müssen denselben Datentyp zurückgeben. Bei Vergleichen oder Kombinationen von JSON-Werten mit String-Literalen, die JSON-Werte mit `text()` wrappen.

```javascript
// ❌ FALSCH: Vergleich von any-Typ mit String
backgroundColor: if emp.backgroundColor then emp.backgroundColor else "#888888" end
// Fehler: "any" Typ verglichen mit String-Literal

// ✅ RICHTIG: JSON-Wert mit text() wrappen
backgroundColor: if emp.backgroundColor then text(emp.backgroundColor) else "#888888" end

// ❌ FALSCH: Typ-Mischung bei Farb-Properties
fontColor: if record.Color then record.Color else "#000000" end

// ✅ RICHTIG: Konsistente text() Konvertierung
fontColor: if record.Color then text(record.Color) else "#000000" end
```

**Wann `text()` verwenden:**
- Wenn ein JSON-Objekt-Property in einem if/else mit einem String-Fallback verwendet wird
- Wenn JSON-Werte mit String-Literalen verkettet werden
- Wenn JSON-Properties an Widget-Properties übergeben werden, die Strings erwarten

**Faustregel:** Wenn der else-Zweig ein String-Literal wie `"#888888"` ist, muss der if-Zweig auch einen String via `text()` zurückgeben.

## 7. Array-Handling & Dynamic Content

### Array-Funktion vs. Layout-Blocks
Ninox bietet die `array()`-Funktion zum Zusammenführen von Arrays, aber in Widget-Kontexten ist die Verwendung von verschachtelten Layouts oft sauberer und flexibler.

#### ⚠️ **WICHTIG: array() ist meist unnötig**

Blöcke können direkt nebeneinander gesetzt werden - `array()` ist nur nötig wenn tatsächlich Arrays kombiniert werden müssen.

```javascript
// ❌ Unnötig: array() mit if-else
blocks: array(
    if condition then [{ value: widget1 }] else [] end,
    for item in items do { value: widget2 } end
)

// ✅ Besser: Direkt nebeneinander, kein array(), kein else
blocks: [
    if condition then { value: widget1 } end,
    for item in items do { value: widget2 } end
]
```

**Merke:** Ninox filtert `null`-Werte automatisch aus Arrays. Daher:
- Kein `else []` nötig bei bedingten Blöcken
- Kein `array()` nötig um Blöcke zu kombinieren
- Einfach Blöcke/Schleifen kommasepariert auflisten

#### ❌ Vermeiden: Verschachtelte array() Aufrufe
```javascript
// Problematisch: Schwer zu lesen, Verschachtelungstiefe begrenzt
blocks: array(typeBadges, array(teilnehmerBadges, durationBadges))
```

#### ✅ Bevorzugt: Wrapper-Layouts mit direkten Blocks
```javascript
// Sauber: Jede Badge-Gruppe hat eigenes Layout
blocks: [
    {
        value: arcCustomLayout({
            uniqueId: "types-wrapper",
            direction: "horizontal",
            gap: "6px",
            blocks: typeBadges  // Direkt einsetzen
        })
    },
    {
        value: arcCustomLayout({
            uniqueId: "teilnehmer-wrapper",
            direction: "horizontal",
            gap: "4px",
            blocks: teilnehmerBadges  // Direkt einsetzen
        })
    },
    if durationValue then
        {
            value: arcCustomBadge({ ... })
        }
    end
]
```

**Vorteile:**
- Bessere Lesbarkeit und Wartbarkeit
- Individuelle Styling-Kontrolle pro Gruppe (Gap, Ausrichtung)
- Keine Limit-Probleme bei `array()` (Ninox erlaubt nur 2-3 verschachtelte Arrays)
- Leichteres Debugging

**Wann array() nutzen:**
- Wenn nur 2 einfache Arrays kombiniert werden müssen
- Bei flachen Strukturen ohne weitere Styling-Anforderungen
- Beispiel: `actionsLeft: array(typeBadges, teilnehmerBadges)`

#### Array-Merge (Referenzen) – 3 oder mehr Arrays

Die Ninox-Funktion `array()` akzeptiert **nur 2 Argumente**. Um 3 oder mehr Arrays zusammenzuführen, muss die Funktion mehrfach sequentiell aufgerufen werden.

```javascript
// ✅ RICHTIG: Sequentielles array() für 3+ Arrays
let listCombined := array(list1, list2);
listCombined := array(listCombined, list3);
```

**Offizielle Ninox-Dokumentation:** „If you need to merge more than 2 arrays, just execute the function as often as needed.“ ([array() Function](https://docs.ninox.com/en/script/functions-overview/functions/array))

**concat() und join() sind KEINE Alternative** – sie liefern Strings, keine Arrays. Ein Workaround wie `split(concat(ar1) + ", " + concat(ar2), ",")` wandelt alle Werte in Text um und führt zu Typverlust (Zahlen werden zu Strings; bei Werten mit Komma im Text entstehen falsche Splits). Für echte Array-Merges immer `array()` verwenden.

**Referenzen:**
- [Ninox array()](https://docs.ninox.com/en/script/functions-overview/functions/array) – offizielle Dokumentation
- [Forum: How can I add two arrays?](https://forum.ninox.com/t/x2hr89p) – concat/join liefern Strings
- [Forum: Some User Defined Array Functions](https://forum.ninox.com/t/h7hr822) – aMerge-Workaround (textbasiert, nur für String-Arrays geeignet)

**Code-Referenz im Projekt:** `projects/desktop-calendar-planning/desktop-calendar-planning.arctemplate` Zeilen 2439–2440

#### ⚠️ **KRITISCH: Multiple for-Schleifen in blocks**

Ninox erlaubt KEINE zwei for-Schleifen direkt nebeneinander in einem `blocks`-Array. Jede for-Schleife braucht ein eigenes Wrapper-Layout.

```javascript
// ❌ FEHLER: Zwei for-Schleifen direkt nebeneinander
blocks: [
    for sel in data.selects do { value: arcCustomSelect({...}) } end,
    for btn in data.buttons do { value: arcCustomButton({...}) } end
]

// ✅ RICHTIG: Jede for-Schleife in eigenem Wrapper-Layout
blocks: [{
    width: "fraction",
    value: arcCustomLayout({
        uniqueId: "selects-wrapper",
        direction: "horizontal",
        blocks: for sel in data.selects do
            { value: arcCustomSelect({...}) }
        end
    })
}, {
    width: "auto",
    value: arcCustomLayout({
        uniqueId: "buttons-wrapper",
        direction: "horizontal",
        blocks: for btn in data.buttons do
            { value: arcCustomButton({...}) }
        end
    })
}]
```

**Grund:** Ninox interpretiert zwei for-Schleifen nacheinander als Syntaxfehler. Das Wrapper-Layout kapselt jede Schleife sauber ab.

#### ⚠️ **KRITISCH: Sections mit unterschiedlichen Daten – for-Schleife, kein statisches Array**

Bei mehreren Sections (z.B. "Heute", "Morgen", "Nächste 7 Tage"), wo jede Section eigene Listen mit **section-spezifischen Daten** hat (z.B. Terminkarten mit unterschiedlichen Teilnehmern pro Karte), **muss** eine for-Schleife über die Sections verwendet werden.

```javascript
// ❌ FALSCH: Statisches Array – Daten einer Section können in andere "leaken"
blocks: [
    dashboardSection({ sectionId: "heute", appointments: sectionHeuteList }),
    dashboardSection({ sectionId: "morgen", appointments: sectionMorgenList }),
    ...
]

// ✅ RICHTIG: for-Schleife stellt pro Section eigenen Auswertungskontext sicher
blocks: for section in sections[visible = true and cnt(list) > 0] do
    {
        width: "100%",
        value: dashboardSection({
            sectionId: section.sectionId,
            appointments: for item in section.list do
                { value: listCard({ participants: item.participants, ... }) }
            end
        })
    }
end
```

**Grund:** Ohne for-Schleife kann Ninox Referenzen falsch auflösen – z.B. Teilnehmer-Badges von "Heute"-Karten erscheinen in "Morgen"-Karten. Die for-Schleife erzeugt pro Section einen sauberen Auswertungskontext, sodass jede Karte die richtigen Daten aus ihrer eigenen Section/Liste erhält.

#### ⚠️ **KRITISCH: if/else mit einzelnem Block vs. for-Schleife**

Wenn ein if-Zweig einen einzelnen Block und der else-Zweig eine for-Schleife enthält, muss der einzelne Block in `[{...}]` (eckige Klammern) gesetzt werden.

```javascript
// ❌ FEHLER: Unterschiedliche Typen (Object vs. Array)
if isEmpty then
    { value: "Leer" }  // Object
else
    for item in items do { value: item } end  // Array
end

// ✅ RICHTIG: Beide als Array
if isEmpty then
    [{ value: "Leer" }]  // Array mit einem Element
else
    for item in items do { value: item } end  // Array
end
```

**Grund:** Ninox erwartet bei if/else gleiche Typen. Die for-Schleife gibt ein Array zurück, also muss der einzelne Block auch als Array `[{...}]` definiert werden.

#### ⚠️ **KRITISCH: if direkt in Arrays OHNE extra `{`**

Bei bedingten Blöcken innerhalb von `blocks: [...]` darf das `if` NICHT in ein extra `{ }` gewrappt werden.

```javascript
// ❌ FALSCH: Extra { vor if
blocks: [{
        width: "100%",
        value: "immer sichtbar"
    }, {  // ← Dieses extra { ist FALSCH!
        if condition then
            {
                width: "100%",
                value: "bedingt"
            }
        end
    }  // ← Und dieses schließende } ist auch FALSCH!
]

// ✅ RICHTIG: if direkt im Array, kein extra Wrapper
blocks: [{
        width: "100%",
        value: "immer sichtbar"
    },
    if condition then
        {
            width: "100%",
            value: "bedingt"
        }
    end
]
```

**Grund:** Das `if...then...end` selbst gibt das Block-Objekt zurück (oder null). Ein Wrapping in `{ }` erzeugt eine ungültige Objekt-Struktur, die Ninox nicht parsen kann.

### Bedingte Blöcke - else-Zweig oft unnötig
Widget-`blocks`-Arrays ignorieren automatisch `null`/leere Werte. Daher sind leere `else`-Blöcke meist unnötig.

#### ❌ Verbose: Leere else-Blöcke
```javascript
blocks: [
    if condition then
        { /* ... */ }
    else
        {
            width: "0px",
            height: "0px",
            value: ""
        }
    end
]
```

#### ✅ Clean: Ohne else
```javascript
blocks: [
    if condition then
        { /* ... */ }
    end
]
```

**Ninox filtert automatisch:**
- `null` Werte
- Leere Arrays `[]`
- Undefined/falsy Ergebnisse

### Dynamisch vs. Statisch
Vermeide hardcoded, starre Lösungen. Nutze immer Schleifen und Arrays, um flexibel auf Datenstrukturen zu reagieren.

#### ❌ Statisch: Nur erste 2 Elemente
```javascript
blocks: [
    if count(teilnehmer) > 0 then
        { value: arcCustomBadge({ value: item(teilnehmer, 0).value }) }
    end,
    if count(teilnehmer) > 1 then
        { value: arcCustomBadge({ value: item(teilnehmer, 1).value }) }
    end
]
```

#### ✅ Dynamisch: Alle Elemente
```javascript
// Vorbereitung (Top-Level oder in Funktion):
let teilnehmerBadges := if teilnehmer then
    for t in teilnehmer do
        {
            width: "auto",
            height: "auto",
            value: arcCustomBadge({
                uniqueId: "badge-" + t.value,
                value: t.value,
                fontColor: if t.color then text(t.color) else "#FFFFFF" end,
                backgroundColor: if t.backgroundColor then text(t.backgroundColor) else "#3388FF" end
            })
        }
    end
else
    []
end;

// Verwendung im Layout:
blocks: [
    {
        value: arcCustomLayout({
            uniqueId: "teilnehmer-wrapper",
            blocks: teilnehmerBadges  // Dynamisch, alle Elemente
        })
    }
]
```

**Vorteile der dynamischen Lösung:**
- Funktioniert mit beliebig vielen Elementen (1, 5, 100...)
- Keine Code-Duplizierung
- Änderungen am Badge-Styling nur an einer Stelle
- Wiederverwendbar (Variable kann mehrfach genutzt werden)

### Pattern: Wiederverwendbare Badge-Arrays
Definiere Badge-Arrays als Variablen, um sie in mehreren Layouts zu verwenden (z.B. Card-Footer UND Popover-Header):

```javascript
function myComponent(data : any) do
    // Einmal definieren
    let typeBadges := if data.types then
        for typeItem in data.types do
            {
                width: "auto",
                height: "auto",
                value: arcCustomBadge({
                    uniqueId: data.id + "-type-" + typeItem.value,
                    value: text(typeItem.value),
                    fontColor: if typeItem.color then text(typeItem.color) else "#A114A8" end,
                    backgroundColor: if typeItem.backgroundColor then text(typeItem.backgroundColor) else "#FDDEFF" end
                })
            }
        end
    else
        []
    end;
    
    // Mehrfach verwenden
    arcCustomLayout({
        blocks: [
            { value: arcCustomLayout({ blocks: typeBadges }) },  // In Card
            { value: arcCustomLayout({ blocks: typeBadges }) }   // In Popover
        ]
    })
end
```

## 8. UI Patterns & Component Architecture

### Tab-basierte Navigation

Für komplexe UI-Komponenten mit mehreren Content-Bereichen (z.B. Popovers, Modals):

#### Tab-Konfiguration
```javascript
tabs: [{
    id: "termin",
    title: "Termin",
    hasComment: false,  // Flag für Badge-Display
    contentBlock: arcCustomLayout({
        uniqueId: "tab-termin-content",
        height: "100%",
        scrollSettings: { scrollY: true },
        blocks: [/* Content */]
    })
}, {
    id: "kommentare",
    title: "Kommentare",
    hasComment: currentRecord.CommentField != null and text(currentRecord.CommentField) != "",
    contentBlock: arcCustomLayout({
        uniqueId: "tab-kommentare-content",
        height: "100%",
        scrollSettings: { scrollY: true },
        blocks: [/* Content */]
    })
}]
```

#### Tab-Navigation Rendering
```javascript
// Tab-Buttons mit Badge-Support
blocks: for tab in popoverData.tabs do
    {
        width: "auto",
        height: "auto",
        value: arcCustomButton({
            uniqueId: popoverData.id + "-tab-" + tab.id,
            title: tab.title,
            backgroundColor: if popoverData.activeTab = tab.id then "#333333" else "#EBEDF5" end,
            fontColor: if popoverData.activeTab = tab.id then "#FFFFFF" else "#818599" end,
            showBadge: if tab.id = "kommentare" and tab.hasComment then true else false end,
            badgeTitle: "1",
            badgeColor: "#FFFFFF",
            badgeBackground: "#FF3B30",
            actions: [{
                type: "update",
                recordId: Nr,
                field: popoverData.tabActionField,
                value: tab.id
            }]
        })
    }
end
```

#### Dynamic Tab Content Display
```javascript
// Content-Bereich: Dynamisches Rendering basierend auf activeTab
{
    width: "100%",
    height: "fraction",  // Nimmt verfügbaren Platz
    value: let activeTabData := first(for tab in popoverData.tabs do
        if tab.id = popoverData.activeTab then
            tab
        end
    end);
    if activeTabData and activeTabData.contentBlock then
        html(activeTabData.contentBlock)  // ⚠️ html() Wrapper erforderlich!
    else
        arcCustomLayout({
            uniqueId: popoverData.id + "-empty-content",
            blocks: [{
                value: arcCustomText({
                    value: "Kein Content verfügbar.",
                    fontColor: "#999999"
                })
            }]
        })
    end
}
```

**Wichtig**: `html()` Wrapper ist erforderlich, wenn `contentBlock` als `any` Type übergeben wird, um Type-Kompatibilität zwischen Widget-Return und erwartetem Layout zu gewährleisten.

### Footer-Struktur in Popovers/Modals

Footer sollten **NICHT** Teil des scrollbaren Tab-Contents sein, sondern als separater Block am Ende positioniert werden.

#### ❌ Problematisch: Footer im Content
```javascript
contentBlock: arcCustomLayout({
    blocks: [
        { /* Content */ },
        { /* Footer Buttons */ }  // Scrollt mit → schlecht
    ]
})
```

#### ✅ Richtig: Footer als separates Property
```javascript
// 1. Footer als separates Property im Data-Objekt
{
    tabs: [...],
    footerBlock: arcCustomLayout({
        uniqueId: "popover-footer",
        height: "auto",  // Footer passt sich Inhalt an
        gap: "10px",
        paddingY: "10px",
        blocks: [
            if phoneNumber then
                { value: arcCustomButton({ title: "Anrufen", ... }) }
            end,
            if mapsUrl then
                { value: arcCustomButton({ title: "In Google Maps öffnen", ... }) }
            end
        ]
    })
}
```

```javascript
// 2. Footer im Popover-Layout (nach Content)
blocks: [
    { /* Header */ },
    { /* Tabs */ },
    { 
        width: "100%",
        height: "fraction",  // Content scrollbar
        value: /* Tab Content */
    },
    if popoverData.footerBlock then
        {
            width: "100%",
            height: "auto",  // Footer bleibt unten
            value: html(popoverData.footerBlock)
        }
    end
]
```

#### Layout-Struktur mit Footer
```
┌─────────────────────────────┐
│ Popover Header (auto)       │
├─────────────────────────────┤
│ Tabs Navigation (auto)      │
├─────────────────────────────┤
│ Tab Content (fraction)      │ ← scrollY: true
│   - Scrollbarer Bereich     │
│   - height: "fraction"      │
├─────────────────────────────┤
│ Footer (auto)               │ ← Immer sichtbar
│  [Action Button 1]          │
│  [Action Button 2]          │
└─────────────────────────────┘
```

**Vorteile:**
- Footer bleibt immer sichtbar (kein Scrollen nötig)
- Klare Trennung zwischen Content und Actions
- Bessere UX für Action-Buttons (z.B. "Anrufen", "In Maps öffnen")
- Unabhängiges Styling für Footer-Bereich

**Pattern-Zusammenfassung:**
1. Content-Bereich: `height: "fraction"` + `scrollY: true`
2. Footer-Bereich: `height: "auto"` + außerhalb des scrollbaren Content
3. Footer als `footerBlock` Property im Data-Objekt definieren
4. Footer mit `html(popoverData.footerBlock)` rendern

### Pagination für Listen/Kanban

Bei großen Datenmengen (z.B. Inbox mit vielen Mails) kann das Widget crashen. Lösung: `arcCustomPagination` mit `slice()`.

#### Pagination-Variablen oben definieren
```javascript
let maxEntriesPerPage := 20;

// Pro Liste/Swimlane
let listAll := (select Items where ...);
let totalCards := cnt(listAll);
let currentPage := if page_field then number(page_field) else 1 end;
let totalPages := ceil(totalCards / maxEntriesPerPage);
let listPaginated := slice(listAll, maxEntriesPerPage * (currentPage - 1), maxEntriesPerPage * currentPage);
```

#### Pagination in Config-Objekt
```javascript
let swimlaneConfig := [{
    id: "inbox",
    title: "Inbox",
    showPagination: totalCards > maxEntriesPerPage,  // Nur wenn nötig
    pagination: {
        currentPage: currentPage,
        totalPages: totalPages,
        totalCards: totalCards,
        fieldId: fieldId(Nr, "page_inbox")  // Number-Feld für aktuelle Seite
    },
    sections: [{
        title: "Items",
        cards: listPaginated.[{...}]  // Paginierte Liste verwenden
    }]
}];
```

#### Pagination Widget am Ende der Swimlane
```javascript
if swimlane.showPagination then
    {
        width: "100%",
        height: "auto",
        alignX: "center",
        value: arcCustomPagination({
            uniqueId: "pagination-" + text(swimlane.id),
            editable: true,
            recordId: Nr,
            fieldId: text(swimlane.pagination.fieldId),
            direction: "horizontal",
            title: swimlane.pagination.currentPage,
            value: swimlane.pagination.currentPage,
            total: swimlane.pagination.totalPages,
            totalPrefix: " von "
        })
    }
end
```

**Wichtig:**
- `showPagination` als boolean für konditionale Anzeige
- `slice(list, start, end)` für die paginierte Teilmenge
- Separates Number-Feld pro Swimlane/Liste (z.B. `page_inbox`, `page_bearbeiten`)
- `editable: true` erlaubt direkte Seiteneingabe

#### Pagination bei Filterwechsel zurücksetzen

⚠️ **Problem:** Wenn ein Filter gewechselt wird (z.B. Postfach), kann die aktuelle Seite höher sein als die neue Gesamtseitenzahl → leere Anzeige.

**Lösung:** Im Ninox Trigger "Nach Änderung" des Filterfelds die Seite zurücksetzen:

```ninox
// Trigger: Nach Änderung von "selectedPostfach"
'Aktuelle Seite' := 1
```

**Gilt für alle Filter die die Listengröße beeinflussen:**
- Postfach/Mailbox Auswahl
- Status Filter
- Kategorie Filter
- Bearbeiter Filter

### Empty States für Listen/Swimlanes

Wenn eine Liste/Swimlane leer ist, sollte ein Fallback-Text angezeigt werden.

#### Pattern: Bedingte Section-Anzeige + Empty State

⚠️ **WICHTIG:** Bei if/else mit unterschiedlichen Block-Typen (einzelner Block vs. for-Schleife) muss der einzelne Block in `[{...}]` (eckige Klammern) gesetzt werden, damit Ninox beide als Arrays erkennt.

```javascript
// Config: emptyMessage pro Swimlane
emptyMessage: "Keine Einträge im Posteingang",

// Rendering: if/else Pattern (NICHT if...end, for...end nebeneinander!)
let totalSectionCards := sum(for sec in swimlane.sections do count(sec.cards) end);
if totalSectionCards = 0 then
    [{  // ← Eckige Klammern um einzelnen Block!
        width: "100%",
        height: "100%",
        value: arcCustomLayout({
            uniqueId: "empty-" + swimlaneId,
            alignX: "center",
            alignY: "center",  // ← Für vertikale Zentrierung braucht man Layout!
            width: "100%",
            height: "100%",
            blocks: [{
                value: arcCustomText({ value: text(swimlane.emptyMessage), fontColor: "#999999" })
            }]
        })
    }]
else
    for section in swimlane.sections do
        if count(section.cards) > 0 then
            { /* Section mit Title und Cards */ }
        end
    end
end  // ← Ein end für das gesamte if/else
```

**Warum `[{...}]`?** Ninox erwartet bei if/else gleiche Typen. Die for-Schleife gibt ein Array zurück, also muss der einzelne Block auch als Array `[{...}]` definiert werden.

**Warum Layout für vertikale Zentrierung?** Blocks haben nur `alignX` für horizontale Ausrichtung. Für `alignY` (vertikale Zentrierung) muss ein `arcCustomLayout` Wrapper verwendet werden.

### Reset-Button in Select ausblenden

Manche Selects haben "Alle" bereits als Record-Option und dürfen nicht auf null zurückgesetzt werden.

```javascript
// Config: hideReset pro Filter
filters: [{
    placeholder: "Posteingang",
    hideReset: true,  // Kein Reset-Button
    items: [...]
}, {
    placeholder: "Status",
    hideReset: false,  // Reset-Button "Alle" anzeigen
    items: [...]
}]

// Rendering: reset.hide aus Config lesen
reset: {
    title: "Alle",
    value: "",
    hide: if filter.hideReset then true else false end
}
```

## 9. Responsive Design: isMobile Pattern mit ninoxApp()

### Plattform-Erkennung

Verwende `ninoxApp()` um zwischen Mobile und Desktop/Tablet zu unterscheiden:

```javascript
let isMobile := ninoxApp() = "iphone" or ninoxApp() = "android";
```

Rückgabewerte von `ninoxApp()`: `"web"`, `"mac"`, `"iphone"`, `"ipad"`, `"android"`, `"tab"`, `"server"`.

### Desktop Layout Shell

Auf Desktop/Tablet: Zweispaltiges Layout:
- **Linke Sidebar** (fixe Breite, z.B. `370px`): Mobile-Ansichten als Sidebar adaptiert
- **Rechter Content-Bereich** (`width: "fraction"`): Desktop-spezifischer Content mit Header (fixe Höhe), Content (fraction), Footer (auto)

### isMobile Parameter in Funktionen

`isMobile` über das Data-Objekt an alle Funktionen weiterreichen, die plattformspezifische Anpassungen benötigen:

```javascript
--- In Funktion: Styles bedingt setzen ---
styles: if data.isMobile then
    "overflow-x: hidden; max-width: 400px; min-height: 700px;position:relative;"
else
    "overflow-x: hidden; min-height: 700px;position:relative;"
end,
```

### Views-Architektur (Einheitliche Navigation)

Zwei-Ebenen-Navigationssystem:
1. **`activeView`** (aus State abgeleitet): Bestimmt den Top-Level-Kontext (dashboard, ticket, customer)
2. **`activeTab`** (aus `helper_activeTab`): Wählt den Tab innerhalb der aktuellen View

Alle Navigationsaktionen schreiben in ein einziges Feld `helper_activeTab` über `tabActionField`.

```javascript
--- activeView wird aus globalem State abgeleitet, nicht in einem Feld gespeichert ---
let activeView := if helper_currentCustomer then
        "customer"
    else
        if currentTicket then "ticket" else "dashboard" end
    end;

--- Alle Tabs nutzen das gleiche Action-Feld ---
let tabActionField := fieldId(Nr, "helper_activeTab");
let activeTabValue := if helper_activeTab then helper_activeTab else "" end;
```

### View-Funktionen mit Lazy Content Loading

**Kritisch für Performance**: Schweren Content in View-Funktionen mit `if data.active then ... end` Guards wrappen. Das verhindert, dass Ninox teure Widget-Berechnungen für inaktive Views auswertet (da `let`-Variablen in Ninox eager evaluiert werden).

```javascript
function viewTicket(data : any) do
    if data.active then
        let activeTab := if data.activeTab then data.activeTab else "ticket" end;
        {
            desktop: {
                sidebar: { content: heavyWidget(), footer: null },
                main: { content: mainWidget(), header: null, footer: footerWidget(), contentSidebar: null }
            },
            mobile: {
                main: mobileWidget(),
                footer: null,
                header: null
            }
        }
    end
end;
```

**Warum Funktionen statt Inline-JSON?**: Wenn man `let myWidget := expensiveWidget();` auf Top-Level setzt und es im JSON mit `if activeView = "ticket" then myWidget end` referenziert, evaluiert Ninox `myWidget` trotzdem. Durch das Wrappen in eine Funktion mit `data.active`-Check wird die teure Berechnung nur bei aktiver View ausgelöst.

### Views-Konfiguration

Views als Array von Config-Objekten organisieren, jedes mit eigenen `navTabs` und lazy-geladenem `content`:

```javascript
let views := [{
        id: "dashboard",
        active: activeView = "dashboard",
        defaultTab: "cockpit",
        navTabs: [{ id: "cockpit", title: "Cockpit", visible: true, active: ..., actionField: tabActionField }, ...],
        content: viewDashboard({ active: activeView = "dashboard", activeTab: activeTabValue, ... })
    }, {
        id: "ticket",
        active: activeView = "ticket",
        navTabs: [{ id: "ticket", title: "Ticket", visible: true, active: ..., actionField: tabActionField }, ...],
        content: viewTicket({ active: activeView = "ticket", activeTab: activeTabValue, ... })
    }];
```

### Generisches Rendering aus Views-Config

Aktuelle View auflösen und plattformspezifischen Content rendern:

```javascript
let currentView := first(for v in views do if v.active then v end end);
let currentNavTabs := if currentView then currentView.navTabs else [] end;
let viewContent := if currentView then currentView.content else null end;

if isMobile then
    if viewContent and viewContent.mobile and viewContent.mobile.main then
        html(viewContent.mobile.main)
    end
else
    desktopLayout({
            sidebarContent: viewContent.desktop.sidebar.content,
            rightHeader: desktopTicketHeader({ tabs: currentNavTabs, tabRecordId: Nr, ... }),
            rightContent: viewContent.desktop.main.content,
            rightFooter: viewContent.desktop.main.footer
        })
end
```

Jede View-Funktion gibt sowohl `desktop`- als auch `mobile`-Content-Strukturen zurück. Der generische Renderer wählt die richtige basierend auf `isMobile`.

## 10. Helper-Feld Namenskonvention

Konsistentes Namensschema für Helper-Felder:

| Pattern | Beispiel | Verwendung |
|---------|----------|------------|
| `helper_current{Entity}` | `helper_currentCustomer` | Aktuell ausgewählter/aktiver Datensatz (ID) |
| `helper_active{Feature}` | `helper_activeView` | Aktiver Tab/View/Modus |
| `helper_selected{Entity}` | `helper_selectedTicket` | Ausgewähltes Element für Detail/Popover |
| `trigger_{action}` | `trigger_delete` | Boolean-Trigger-Felder für Ninox-Aktionen |

Plattform-spezifische Präfixe wie `desktop_` oder `mobile_` sowie kontext-spezifische wie `popover_` oder `filter_` in Feldnamen vermeiden. Generische Namen verwenden, damit dasselbe Feld plattformübergreifend funktioniert.

### Refactoring-Learnings (Detail-/List-Views)

**Skeleton-first-Architektur**: Layout-Shell (panelLayout, fullLayout) zuerst mit Platzhaltern bauen, dann reale Komponenten schrittweise integrieren. Vermeidet "alter Code läuft weiter" und ermöglicht sauberes Testen.

**Shared Data Objects**: Ein einziges `ticketData` (oder `currentTicket`) Objekt wenn ein Ticket ausgewählt ist. Alle Sub-Views (Details, Notizen, Kommentare, Historie) konsumieren daraus. Einmal berechnen, überall wiederverwenden.

**View-Level-Variablen-Scoping**: Variablen wie `ticketHeader`, `ticketSidebarContent`, `ticketFooter` innerhalb von `if activeView = "ticket" then ... end` werden nur bei aktiver View evaluiert. Verhindert eager evaluation schwerer Widgets für inaktive Views.

**Generische Komponenten-Namen**: `ticketSidebarHeader` statt `mobileTicketHeader` oder `popoverHeader`. Dieselbe Komponente dient Desktop-Sidebar und Mobile-Panel; später für andere Entitäten wiederverwendbar.

**Array-Projektion für Sections**: `list[visible = true and count(list) > 0].[{ block }]` direkt in blocks für effiziente Iteration. Query innerhalb `if hasSearchFilter then ... else [] end` für Performance, wenn eine "Später"-Section nur bei Suche relevant ist.

**Select-Items-Syntax**: Für arcCustomSelect `(select Table).[{ title: ..., active: ..., value: ... }]` verwenden, nicht `for m in select ... do { id, title } end`. Das `active`-Flag prüft `contains(numbers(current.Field), number(Nr))` bei Multi-Select.

**arcCustomSelect currentValue bei Verknüpfungsfeldern**: Wenn das Select-Feld eine Relation ist, muss `currentValue` das Anzeigefeld der verknüpften Tabelle nutzen, nicht `text(record.Relation)`. Beispiel: `currentValue: if record.Kategorie then text(record.Kategorie.Titel) else "" end`.

**Conditional Tab `main` für Detail-Views**: Wenn ein Tab sowohl Listen- als auch Detail-Ansicht hat, die `main`-Property conditional machen: `main: if detailActive then { showNav: false, headerLeft: backBtn, headerRight: actionBtn, content: detailContent, contentRight: detailSidebar, footer: detailFooter } else { showNav: true, content: listContent, ... } end`. Nutzt die `desktopMainLayout`-Slots (headerLeft, headerRight, contentRight, footer) statt alles in eine einzige Content-Funktion zu packen.

**Detail-View-Rendering auf Tab-Ebene, nicht in Content-Funktionen**: Nicht innerhalb von `untersuchungenContent()` zwischen Liste und Detail branchen. Content-Funktionen auf eine Aufgabe fokussieren (Liste ODER Detail) und die Tab-Definition entscheiden lassen, was gerendert wird. Ermöglicht korrekte Nutzung von Sidebar/Footer/Header-Slots.

## 11. Dokumentation für LLMs

Wenn Code generiert wird, muss er:
1.  Kompilierbar in Ninox sein (kein echtes JS).
2.  Die Widget-Referenz strikt befolgen.
