app2dat Automatisierungshandbuch

Automatisierungshandbuch

Automatisierung mit app2dat

Vom ersten Script bis zur kompletten Vereins-Automatisierung: Dieses Handbuch führt dich Schritt für Schritt in das Automatisierungssystem ein, dient als vollständiges Nachschlagewerk der a2d-Script-API und zeigt an großen Praxisbeispielen, wie echte Vereinsaufgaben automatisiert werden.

Stand: Juli 2026 Für Admins & Automatisierer WebApp · Android · iOS · Windows

Einführung

Wiederkehrende Verwaltungsarbeit — Listen exportieren, Urkunden erzeugen, Daten zwischen Gruppen übertragen, Benachrichtigungen verschicken — muss in app2dat niemand von Hand erledigen. Das Automatisierungssystem führt dafür kleine JavaScript-Programme (Scripts) aus, die über die dokumentierte a2d-Schnittstelle auf deine Vereinsdaten zugreifen: Mitglieder, Datenfelder, Termine, Anwesenheiten, Chats, Dokumente und mehr.

Dieses Handbuch ist als Lehrbuch aufgebaut: Die Kapitel 24 führen mit einfachen Beispielen in die Script-Welt ein. Die Kapitel 59 bilden die vollständige Referenz der a2d-API. Danach folgen ein Kapitel über Ereignis-Trigger, drei ausführliche Praxisbeispiele aus echten Vereinen — und zum Abschluss die Arbeit mit dem KI-Automatisierungs-Assistenten, der dir die Scripts auf Zuruf schreibt.

Kein Programmierer? Kein Problem. Du kannst jedes Script vom eingebauten Automatisierungs-Assistenten schreiben lassen — beschreib ihm einfach in normalem Deutsch, was passieren soll. Dieses Handbuch hilft dir trotzdem: Du verstehst, was die Scripts tun, kannst Ergebnisse prüfen und kleine Anpassungen selbst vornehmen.

Wo Automatisierungen leben: die vier Ebenen

Automatisierungen gibt es auf vier Ebenen (Scopes) — jede mit eigenem Automatisierungsmanager und eigenem Zauberstab-Menü :

EbeneTypische ScriptsKontext
BenutzerPersönliche Helfer, profilübergreifende Werkzeuge.Nur userId
ProfilOrganisationsweite Aufgaben: Strukturen anlegen, mehrere Gruppen bedienen.profileId
GruppeDer Normalfall: Mitgliederlisten, Datenfelder, Exporte, Urkunden dieser Gruppe. Auch Eventgruppen (Typ E) sind Gruppen — ihre Termin- und Anwesenheits-Scripts gehören hierher.profileId + groupId
Event (Ordner)Scripts eines Eventordners — z. B. die Eventgruppen des Ordners automatisiert anlegen oder ordnerweit auswerten.profileId + eventId

Der Scope bestimmt, welchen Kontext ein Script automatisch kennt (Kapitel 3) und wie weit gespeicherte Variablen reichen (Kapitel 9). Die meisten Beispiele dieses Handbuchs sind Gruppen-Scripts — sie werden in der Gruppe ausgeführt, deren Daten sie bearbeiten.

Sicherheit: Sandbox und Berechtigungen

  • Serverseitige Sandbox. Scripts laufen abgeschottet auf dem Server (Jint-Engine) — getrennt von der Basissoftware, mit harten Grenzen: 50 MB Speicher, 100.000 Anweisungen, begrenzte Rekursionstiefe. Ein fehlerhaftes Script kann die App nicht beschädigen.
  • Deine Rechte, nie mehr. Jede a2d-Methode prüft die Berechtigung des ausführenden Benutzers: Lesen ab Mitglied, Schreiben ab Verwalter (Manager/Admin). Ein Script sieht und ändert nur, was du auch in der App sehen und ändern dürftest.
  • Alles protokolliert. Jeder Lauf erzeugt einen Ausführungsbericht mit Protokoll, Ergebnis und Fehlermeldungen.

Dein erstes Script

Der beste Einstieg ist ein Script, das nichts verändert — es begrüßt dich nur. Dabei lernst du den kompletten Arbeitszyklus kennen: anlegen, schreiben, ausführen, Bericht lesen.

Den Automatisierungsmanager öffnen

  1. Öffne eine Gruppe, in der du Admin bist, und wechsle in die Verwaltung.
  2. Öffne den Automatisierungsmanager über den Menüpunkt Automatisierung. Links siehst du die Baumansicht (Scripts und Ordner), rechts den Code-Editor.
  3. Lege mit Neues Script ein Script an und gib ihm einen sprechenden Namen, z. B. Begrüßung.

Zauberstab vs. Manager: Das Zauberstab-Menü führt vorhandene Scripts nur aus — je nach Freigabe auch für Nicht-Verwalter (Kapitel 14). Der Automatisierungsmanager zum Anlegen und Bearbeiten ist der Verwaltung vorbehalten: Admin und Manager, auf allen vier Ebenen.

Bei Gruppen führen zwei gleichwertige Wege hinein: das Manager-Symbol in der Kopfleiste der Gruppen-Verwaltung oder das ⋮-Menü der Gruppe in der Gruppenliste („Automatisierungsmanager“).

Hallo app2dat

Tippe (oder kopiere) diesen Code in den Editor und speichere:

// Mein erstes Script: begrüßt den Ausführenden mit seinem Profilnamen.
log("Script gestartet");

const profil = await a2d.profile.get(a2d.context.profileId);
await a2d.ui.showMessage("Hallo", "Schön, dass du da bist, " + profil.firstName + "!");

result.set("message", "Begrüßung angezeigt für " + profil.firstName);

Führe das Script mit der Ausführen-Schaltfläche im Editor aus. Es passieren drei Dinge, die du in jedem Script wiederfindest:

  • log(…) schreibt eine Zeile ins Ausführungsprotokoll — dein wichtigstes Werkzeug beim Fehlersuchen.
  • await a2d.… ruft die Script-API auf. Fast alle a2d-Methoden sind asynchron — das await davor ist Pflicht (Kapitel 3).
  • result.set("message", …) übergibt das Ergebnis des Laufs — es erscheint im Ausführungsbericht.

Der Ausführungsbericht

Nach jedem Lauf zeigt dir der Editor den Ausführungsbericht mit drei Bereichen: Protokoll (alle log-Zeilen), Ergebnis (was result übergeben hat) und Fehler (JavaScript-Fehlermeldung samt Zeilennummer, falls etwas schiefging). Der Server bewahrt je Ebene die letzten fünf Läufe auf — über das Verlauf-Symbol holst du sie jederzeit zurück, auch für Scripts, die per Ereignis-Trigger im Hintergrund liefen.

Trau dich: Solange ein Script nur liest (list, get, Exporte), kann nichts kaputtgehen. Schreibende Methoden (set, create, delete) erkennst du in der Referenz auf einen Blick — und auch sie können nie mehr als deine eigene Rolle erlaubt.

Script-Grundlagen

Scripts sind normales, modernes JavaScript. Wenn du schon einmal programmiert hast, fühlst du dich sofort zu Hause — es gibt aber ein paar Eigenheiten der Script-Umgebung, die du kennen solltest. Dieses Kapitel ist die Grundlage für alles Weitere.

Der Ausführungskontext: a2d.context

Jedes Script weiß, wo es läuft. Das (nur lesbare) Objekt a2d.context liefert:

EigenschaftTypBedeutung
userIdnumberID des angemeldeten Benutzers.
profileIdnumberAktives Profil.
groupIdnumberAktuelle Gruppe (0 = kein Gruppenkontext).
eventIdnumberAktueller Eventordner (0 = kein Eventkontext).
scopestring"User", "Profile", "Group" oder "Event".
culturestringSprache/Region des Frontends (nutzt a2d.date.format).

Ein Gruppen-Script beginnt fast immer mit dieser Absicherung:

const groupId = a2d.context.groupId;
if (!groupId) { result.set("message", "Bitte in einer Gruppe ausführen."); return; }

Kein Funktions-Wrapper nötig: Das Script ist bereits die „Funktion“ — ein return; auf oberster Ebene beendet es sauber. Verpacke den Code nicht selbst in eine IIFE ((async () => { … })()) — das ist unnötig und kann je nach Schreibweise das Ergebnis verschlucken.

await — immer, aber nacheinander

Fast alle a2d-Methoden sind asynchron und brauchen ein await. Synchron (ohne await) sind nur a2d.vars.set/get/getAll, a2d.member.getSelected() und a2d.date.*.

// FALSCH — parallele a2d-Aufrufe sind in der Sandbox nicht erlaubt:
// const [a, b] = await Promise.all([a2d.group.get(1), a2d.group.get(2)]);

// RICHTIG — Aufrufe nacheinander ausführen:
const a = await a2d.group.get(1);
const b = await a2d.group.get(2);

Die Sandbox arbeitet mit einer einzelnen Engine-Instanz: Promise.all/race über a2d-Aufrufe führen zu Laufzeitfehlern — der statische Prüfschritt des KI-Assistenten meldet dieses Muster als Fehler.

Protokoll, Ergebnis, Parameter

ObjektZweck
log(text)Zeile ins Ausführungsprotokoll schreiben (console.log/warn/error sind Aliasse).
result.set(key, wert)Ergebniswert übergeben. Ein einzelnes result.set("message", …) kommt bei einem aufrufenden Script als einfacher String an.
result.setJson(jsonString)Strukturiertes Ergebnis als lesbares JSON übergeben — der Aufrufer kann direkt auf Eigenschaften zugreifen.
params.get(key)Eingabeparameter lesen, wenn das Script von einem anderen Script (Kapitel 9) oder einem Ereignis-Trigger (Kapitel 10) gestartet wurde. Alle Werte sind Strings.
// Strukturierte Ergebnisse IMMER über setJson ausgeben:
result.setJson(JSON.stringify({ anzahl: 3, namen: ["Anna", "Ben", "Cem"] }, null, 2));

// NICHT: result.set("x", objekt)            → erscheint als "[object Object]"
// NICHT: result.set("x", JSON.stringify(…)) → doppelt maskiert und schwer lesbar

Typen & Formate — die drei goldenen Regeln

  1. Datenfeld-IDs sind Strings. Auch wenn sie numerisch aussehen ("1", "17"): Felder werden immer über ihre String-ID angesprochen. Mit a2d.memberData.getFieldId(groupId, "Feldname") ermittelst du die ID aus dem Namen.
  2. Datumswerte sind ISO. Kanonisches Format ist yyyy-MM-dd. Eingaben aus Dialogen können lokal formatiert sein — vor dem Speichern oder Übergeben mit a2d.date.toIso(wert) normalisieren; für die Anzeige a2d.date.format(wert).
  3. Dialog- und Parameterwerte sind Strings. showInput liefert auch für number- und bool-Felder Strings — selbst umwandeln: parseInt(res.x, 10), res.flag === "1". Dasselbe gilt für alles aus params.get(…).

Fehler behandeln

Wirft eine a2d-Methode einen Fehler (z. B. fehlende Berechtigung), bricht das Script ab und der Ausführungsbericht zeigt die Meldung samt Zeilennummer. Wo ein Einzelfehler den Lauf nicht stoppen soll — etwa in einer Schleife über viele Mitglieder — hilft try/catch:

for (const pid of mitglieder) {
    try {
        await a2d.memberData.set(groupId, pid, feldId, "OK");
    } catch (e) {
        log("Übersprungen (Profil " + pid + "): " + e.message);
    }
}

Vorsicht bei Ergebnis-Objekten aus dem Backend (z. B. von attendance.profilesAttendances): Sie verhalten sich wie .NET-Dictionaries — der Zugriff auf einen nicht vorhandenen Schlüssel kann einen Fehler werfen. Greife defensiv zu (try/catch oder Existenzprüfung).

Ausführen im Editor

Die Ausführen-Schaltfläche im Editor startet das Script echt — mit echten Daten und echten Schreibzugriffen; einen separaten Probelauf („Dry-Run“) gibt es nicht. Einzige Besonderheit: Nutzt das Script a2d.member.getSelected(), fragt der Editor die Mitgliederauswahl vorab in einem Dialog ab — beim Start aus der Mitgliederliste gilt dagegen deren aktuelle Selektion. Taste dich deshalb schrittweise heran: Baue neue Scripts zuerst nur lesend (list/get + result), prüfe den Ausführungsbericht — und ergänze Schreibzugriffe erst danach, für den ersten echten Lauf mit einer kleinen Mitgliederauswahl.

Sandbox-Grenzen: 50 MB Speicher, 100.000 Anweisungen, Rekursionstiefe 100. Auf einen offenen Dialog wartet ein Script 5 Minuten (showInput, confirm, showSelect) bzw. 10 Minuten bei pickFile — danach bricht der Aufruf ab. Für Vereinsdaten ist das mehr als genug — Endlosschleifen stoppt das Anweisungslimit zuverlässig.

Mitglieder, Daten & Dialoge

Jetzt wird es praktisch: Die drei häufigsten Zutaten fast aller Vereins-Scripts sind die Mitgliederliste, die Datenfelder der Gruppe und Dialoge für Ein- und Ausgabe. Wir bauen sie Schritt für Schritt zusammen.

Mitglieder lesen und filtern

a2d.member.list(groupId) liefert alle Mitglieder der Gruppe — jedes mit profileId, status, dem eingebetteten profile-Objekt (Vorname, Nachname, …) und den Datenfeldern in data. Das folgende Script listet alle inaktiven Mitglieder (Status ungleich "ACT") lesbar auf:

// Listet Mitglieder der aktuellen Gruppe nach Status gefiltert auf.
const groupId = a2d.context.groupId;
if (!groupId) { result.set("message", "Bitte in einer Gruppe ausführen."); return; }

const mitglieder = await a2d.member.list(groupId);

const inaktive = (mitglieder || [])
    .filter(m => (m.status || "") !== "ACT")
    .map(m => {
        const vn = m.profile ? (m.profile.firstName || "") : "";
        const nn = m.profile ? (m.profile.secondName || "") : "";
        return {
            profileId: m.profileId,
            name: (vn + " " + nn).trim() || ("Profil " + m.profileId),
            status: m.status
        };
    });

log("Inaktive Mitglieder: " + inaktive.length);
result.setJson(JSON.stringify({ anzahl: inaktive.length, mitglieder: inaktive }, null, 2));

Es gibt kein m.name: Der Anzeigename wird immer aus profile.firstName und profile.secondName zusammengesetzt. Statuscodes: ACT aktiv, INV eingeladen, REQ angefragt, DEL entfernt — die vollständige Liste steht in Kapitel 5.

Die Auswahl der Mitgliederliste nutzen

Viele Scripts sollen nicht auf alle Mitglieder wirken, sondern auf die, die der Benutzer gerade in der Mitgliederliste ausgewählt hat. Genau das liefert a2d.member.getSelected() — synchron, als Array von Profil-IDs:

const auswahl = a2d.member.getSelected() || [];
if (auswahl.length === 0) {
    await a2d.ui.showMessage("Hinweis", "Bitte zuerst Mitglieder in der Liste auswählen.");
    return;
}
for (const pid of auswahl) {
    // pid ist eine Zahl (Profil-ID), kein Objekt
}

Datenfelder lesen und schreiben

Die selbst definierten Spalten einer Gruppe (siehe Administrationshandbuch, Kapitel 8) erreichst du über a2d.memberData. Felder werden über ihre String-ID angesprochen; die ID ermittelst du einmalig aus dem Feldnamen:

const notizId = await a2d.memberData.getFieldId(groupId, "Notiz");
if (!notizId) { result.set("message", "Datenfeld 'Notiz' nicht gefunden."); return; }

// Lesen: alle Feldwerte eines Mitglieds als { feldId: wert }
const daten = (await a2d.memberData.get(groupId, pid)) || {};
log("Bisherige Notiz: " + (daten[notizId] || "—"));

// Schreiben: einzelnes Feld setzen (ab Verwalter-Rolle)
await a2d.memberData.set(groupId, pid, notizId, "Beitrag 2026 bezahlt");

Nach schreibenden Änderungen lohnt ein await a2d.ui.refresh(); am Script-Ende — die sichtbare Mitgliederliste aktualisiert sich sofort, ohne dass der Benutzer die Ansicht neu laden muss.

Dialoge: fragen statt hartcodieren

Mit a2d.ui spricht dein Script mit dem Benutzer. Das folgende Beispiel kombiniert einen Eingabedialog mit der Mitgliedersuche — beachte das Muster: ein Dialog, dessen Ergebnis über res[id] gelesen wird, und null-Prüfung für „Abbrechen“:

// Sucht Mitglieder per Eingabedialog (Vor- oder Nachname).
const res = await a2d.ui.showInput("Mitglieder suchen", [
    { id: "q", name: "Vor- oder Nachname", type: "string", required: false }
]);
if (!res) return; // Benutzer hat abgebrochen

const begriff = ((res && res.q) || "").toString().trim().toLowerCase();

const mitglieder = await a2d.member.list(a2d.context.groupId);
const treffer = (mitglieder || []).filter(m => {
    const vn = (m.profile && m.profile.firstName ? m.profile.firstName : "").toLowerCase();
    const nn = (m.profile && m.profile.secondName ? m.profile.secondName : "").toLowerCase();
    return begriff === "" || vn.includes(begriff) || nn.includes(begriff);
});

log("Treffer: " + treffer.length);
result.setJson(JSON.stringify({ begriff: begriff, anzahl: treffer.length }, null, 2));

Neben showInput gibt es showMessage (OK-Meldung), confirm (Ja/Nein), showSelect (Auswahlliste) und pickFile (Datei wählen — liefert den Inhalt der Datei). Alle Details und Feldtypen findest du in Kapitel 9.

Damit kannst du schon viel: Mitglieder filtern, Werte schreiben, nachfragen, Ergebnis ausgeben — das ist das Grundgerüst der meisten Automatisierungen. Die folgenden fünf Kapitel sind dein Nachschlagewerk für alles Weitere.

Referenz: Profile, Gruppen & Mitglieder

Ab hier wird das Handbuch zum Nachschlagewerk. Konventionen der Referenz: Alle Methoden sind asynchron (await!), sofern nicht ausdrücklich „synchron“ vermerkt. Lesen verlangt mindestens die Rolle Mitglied, Schreiben mindestens Verwalter (Manager/Admin) — Methoden, die darüber hinausgehen, sind gekennzeichnet. Optionale Parameter tragen ein ?.

a2d.profile — Profile

MethodeBeschreibung
get(profileId)Profil laden: id, type ("PRS"/"ORG"), firstName, secondName, title, email, phoneNumber, dateOfBirth, gender, idNumber, imageUrl.
getInfo(profileId)Kurzinfo — leichter als get, wenn nur Name/Bild gebraucht werden.
getField(profileId, feld)Einzelnes Profilfeld als String (oder null).
list()Alle Profile des angemeldeten Benutzers.
create(daten)Neues verwaltetes Profil. firstName ist Pflicht (bei type:"ORG" = Organisationsname); weitere Felder: type (Default "PRS"), secondName, title, email, phoneNumber, dateOfBirth (ISO), gender, idNumber, imageUrl, templateGroupId?/pin? (nur ORG: Mitgliedergruppe aus Vorlage befüllen).
edit(profileId, daten)Profilfelder ändern — jeder Schlüssel in daten ist ein Feldname.

ORG-Profile bringen ihre Mitgliedergruppe mit: Nach profile.create({ type: "ORG", … }) existiert automatisch die Mitglieder-Systemgruppe — hole sie mit a2d.group.getMemberGroup(profileId). Das privacy-aufgelöste Geburtsdatum eines Mitglieds liefert übrigens nur a2d.member.get(groupId, pid), nicht profile.get.

a2d.group — Gruppen

MethodeBeschreibung
get(groupId)Gruppe laden: id, profileId, name, description, type ("P" Standard / "E" Eventgruppe / "M" Mitglieder-Systemgruppe), access ("PRV"/"MMB"/"PUB"), childrenType ("PRS"/"ORG"), membVisible.
findByName(name)Gruppe über den Namen suchen — zuerst im aktuellen Profil, dann in den übrigen; erster Treffer gewinnt. Wichtig, weil Systemgruppen wie „Mitglieder“ in jedem ORG-Profil existieren.
list(profileId)Alle Gruppen eines Profils.
create(daten)Neue Gruppe. name Pflicht; optional description, type (Default "P"; mit eventId automatisch "E"), access ("PUB" nur für verifizierte Organisationen), eventId (ordnet die Gruppe einem Eventordner zu), childrenType, profileId (Default: Kontext).
createFromTemplate(daten)Neue Gruppe aus einer Vorlagen-Gruppe: übernimmt Datenfeld-Definitionen samt Sichtbarkeiten, Vorlagen-Dateien, die Automatisierungs-Ordnerstruktur (Scripts als Live-Referenzen) und Themen-Definitionen (ohne Nachrichten). Zusätzlich zu den create-Feldern: templateGroupId (Pflicht), pin? (bei fremder, PIN-geschützter Vorlage).
getMemberGroup(profileId)Mitglieder-Systemgruppe (Typ "M") eines ORG-Profils.
edit(groupId, daten)Gruppe ändern (Verwalter): name, description, access — nur übergebene Felder.
getMembers(groupId)Alle Mitglieder inklusive Profildaten (wie member.list).
getMemberIds(groupId)Nur die Profil-IDs.
delete(groupId)Gruppe löschen inkl. Daten, Dateien und Terminen — verlangt als einzige Methode die Admin-Rolle (Manager genügt nicht); nur Typ "P"/"E".

a2d.event — Eventordner

Eventordner gruppieren Eventgruppen auf Profil-Ebene (siehe Administrationshandbuch, Kapitel 6). Die Gruppen selbst legst du mit group.create({ …, eventId }) an.

MethodeBeschreibung
list(profileId)Alle Eventordner eines Profils.
get(eventId)Einzelnen Eventordner laden.
create(daten)Neuer Eventordner: name Pflicht; description, profileId (Default: Kontext), imageUrl?.
edit(eventId, daten)Ändern: name, description.

a2d.member — Mitglieder

MethodeBeschreibung
list(groupId)Alle Mitglieder mit profileId, status, statusDate, eingebettetem profile-Objekt und den Datenfeldern in data.
listActive(groupId)Wie list, aber nur aktive Mitglieder (Status "ACT" oder "RNAC") — Eingeladene, Angefragte und Entfernte sind ausgeschlossen.
get(groupId, profileId)Einzelnes Mitglied mit allen Daten — inklusive privacy-aufgelöstem profile.dateOfBirth.
getSelected()Synchron. Die in der App ausgewählten Mitglieder als Array von Profil-ID-Zahlen (null, wenn nichts ausgewählt).
inviteMember(groupId, profileId)Einladung senden — das Profil muss den Beitritt annehmen.
addMember(groupId, profileId)Direkt hinzufügen ohne Einladung (Verwalter).
setStatus(groupId, profileId, status)Status setzen (Verwalter), z. B. "ACT", "NAC", "DEL" — nur real existierende Codes verwenden (einen Status "INA" gibt es nicht).
getStatus(groupId, profileId)Aktuellen Status abfragen (oder null).
remove(groupId, profileId)Mitglied entfernen (Status wird "DEL").
count(groupId, status)Anzahl der Mitglieder mit einem bestimmten Status.

Wichtige Statuscodes: ACT aktiv · RNAC registriertes Mitglied (zählt als aktiv) · INV eingeladen · REQ Beitritt angefragt · RACT Reaktivierung angefragt · NAC abgemeldet/inaktiv · DEL entfernt.

a2d.memberData — Datenfelder

MethodeBeschreibung
get(groupId, profileId)Alle Feldwerte eines Mitglieds als { feldId: wert }.
set(groupId, profileId, feldId, wert)Einzelnes Feld setzen.
setMultiple(groupId, profileId, daten)Mehrere Felder auf einmal: daten = { feldId: wert }. Datumswerte ISO; bei Listenfeldern den Schlüssel (key) speichern, nicht den Anzeigetext.
getDataFields(groupId)Feld-Definitionen: { id, name, type, list? }. Listenfelder haben list = Array aus { key, text }.
getFieldId(groupId, name)Feld-ID aus dem Feldnamen ermitteln (gecacht; null wenn nicht vorhanden).
createField(groupId, optionen)Neues Datenfeld anlegen (Verwalter); gibt das Feld inkl. id zurück. optionen: name (Pflicht), type (Pflicht), access (Default "-"), visible (Default true = Spalte in der Mitgliederliste), description, section.
copy(quellGroupId, quellProfileId, zielGroupId, feldZuordnung)Feldwerte zwischen Gruppen kopieren; feldZuordnung = { Quellfeld: Zielfeld }.
delete(groupId, profileId, feldIds)Bestimmte Felder eines Mitglieds leeren (feldIds = Array).

Feldtypen für createField: string, number (Ganzzahl), decimal, date, time, list, bool, pay (Zahlung), doc (Dokument), grmmb (Mitglieder-Verknüpfung). Gebräuchliche Aliasse wie text, int, select, checkbox, payment, file, members werden automatisch übersetzt.

Sichtbarkeits-Codes (access) — was Mitglieder vom Feld sehen: "-" unsichtbar (nur Verwalter) · "MR" sichtbar, aber nicht änderbar · "MW" vom Mitglied selbst bearbeitbar.

Referenz: Termine & Anwesenheit

a2d.session — Termine

MethodeBeschreibung
list(groupId)Alle Termine der Gruppe.
get(groupId, sessionId)Einzelnen Termin laden.
create(groupId, daten)Termin anlegen (Verwalter). Pflicht: type ("S" Einzeltermin / "W" wöchentlich wiederkehrend), start ("yyyy-MM-dd HH:mm"; Alias date für ganztägig), end, locationId (Ort des Profils). Optional: name.
edit(groupId, sessionId, daten)Termin ändern: type, start, end, name. Der Ort (locationId) und der date-Alias werden hier nicht unterstützt — ein Ortswechsel ist per Script nicht möglich.
delete(groupId, sessionId)Termin löschen.

Asymmetrie beim Ort: list/get liefern den Ort als verschachteltes Objekt — die ID liest du mit s.location?.id (s.locationId ist undefined!). Beim Anlegen übergibst du dagegen das flache Feld locationId.

a2d.attendance — Anwesenheit

MethodeBeschreibung
get(groupId, sessionId, datum)Abmeldungen eines Datums: { profileId: "Bemerkung" }.
signOff(groupId, profileId, sessionId, datum, bemerkung)Mitglied für einen Termin abmelden.
signOn(groupId, profileId, sessionId, datum)Abmeldung zurücknehmen.
getStats(groupId, sessionId, vonDatum, bisDatum)Statistik: totalDates (Terminanzahl) und absences = { profileId: anzahl }.
profilesAttendances(profileIds, groupId, vonDatum, bisDatum, option)Anwesenheits-Matrix mehrerer Profile (Verwalter). Rückgabe: { profileId: { "yyyy-MM-dd HH:mm": "1" | "0" } } — pro Profil ein Termin→Status-Verzeichnis ("1" anwesend, "0" abgemeldet, fehlender Schlüssel = nicht erfasst; Schlüssel ist der Terminbeginn mit Uhrzeit). option: null = normale Anwesenheit; "_R" = nur wo das Mitglied zuständig und anwesend war. Der Sondereintrag [-1] enthält als Schlüssel alle Termine des Zeitraums (gleiches Format) — ideal als Spaltenliste für Matrizen.

Datums-Strenge: Bei get/signOff/signOn wird das Datum Teil des Speichernamens — nur echte Datumswerte (yyyy-MM-dd empfohlen) werden akzeptiert, alles andere bricht mit „Ungültiges Datum“ ab. Selbst-Abmeldung setzt eine aktive Mitgliedschaft voraus; für fremde Mitglieder braucht es Verwalter-Rechte. Ergebnisse defensiv lesen: att[pid] || {}, Zugriff im try/catch (Kapitel 3).

Referenz: Kommunikation & Aufgaben

a2d.topic — Themen/Chats

MethodeBeschreibung
list(groupId)Alle Themen der Gruppe.
get(groupId, topicId)Einzelnes Thema laden.
create(groupId, daten)Thema anlegen (Verwalter). title Pflicht (Alias description); comments (Default false: true = Antworten erlaubt, false = reiner Aushang), emojis (Default ["👍","👎"]; Array oder ;-String; leer = keine Reaktionen), members (Default alle; "all"/"none" oder Profil-ID-Array).
edit(groupId, topicId, daten)Ändern: title, comments.
delete(groupId, topicId)Thema löschen.
getMembers(groupId, topicId)Teilnehmer als Profil-IDs ([-1] = alle Gruppenmitglieder).

a2d.workTask — Aufgaben

MethodeBeschreibung
list(groupId)Alle Aufgaben der Gruppe.
get(groupId, taskId)Einzelne Aufgabe laden.
create(groupId, daten)Aufgabe anlegen (Verwalter). title (Alias description), priority ("0" keine / "1" niedrig / "2" mittel / "3" hoch / "4" höchste), byDate (ISO), byTime (HH:mm), status, comments, spentTime.
edit(groupId, taskId, daten)Ändern: title/description, priority, byDate, byTime, status, spentTimecomments ist nur beim Anlegen setzbar. Ein Statuswechsel setzt automatisch das Änderungsdatum.
delete(groupId, taskId)Aufgabe löschen.
getMembers(groupId, taskId)Zugewiesene Profile.

a2d.chat — Nachrichten

MethodeBeschreibung
sendMessage(groupId, topicId, topicType, text)Nachricht in ein Thema (topicType "T") oder eine Aufgabe ("W") senden.
getMessages(groupId, topicId, topicType, limit)Nachrichten lesen (Default-Limit 50).
sendPrivateMessage(zielProfileId, text, vonProfileId?)Private 1:1-Nachricht. vonProfileId = Absenderprofil (muss ein eigenes Profil des Ausführenden sein); in Ereignis-Scripts event.actorProfileId übergeben. Erlaubt an Profile, mit denen der Absender eine Gruppe teilt, oder — bei Gruppen-Verwaltung — an deren aktive Mitglieder.

a2d.notification — Push-Benachrichtigungen

MethodeBeschreibung
sendPush(zielProfileId, titel, text)Push an ein einzelnes Profil.
sendPushToGroup(groupId, titel, text)Push an alle aktiven Gruppenmitglieder (Status ACT/RNAC); gibt die Anzahl gesendeter Nachrichten zurück.

Ratenbegrenzung: Höchstens 100 Push-Nachrichten und 50 E-Mails pro Stunde — Massen-Aktionen entsprechend planen.

Referenz: PDF, Export & Import

a2d.document — Dokumente & Ordner

MethodeBeschreibung
getFolders(groupId)Dokumentordner der Gruppe.
createFolder(groupId, titel)Neuen Ordner anlegen.
getDocuments(groupId, folderId)Dokumente eines Ordners.
deleteFolder(groupId, folderId)Ordner löschen.

a2d.pdf — PDF-Erzeugung

PDFs entstehen immer in derselben Reihenfolge: prepare()addPage()addText()/addImage()close…(). Alle Maße in Millimetern; ein A4-Blatt ist 210 × 297.

MethodeBeschreibung
prepare(profileId, groupId, titel, betreff, breite, hoehe, vorlagenLink, mitgliedProfileId, vorlagenPin?)PDF beginnen (Verwalter). mitgliedProfileId > 0 = das fertige PDF wird mit diesem Mitglied verknüpft; 0 oder -1 = reines Download-PDF. vorlagenLink = Hintergrund-PDF (leerer String = keine Vorlage).
addPage()Neue Seite — vor dem ersten Text/Bild aufrufen.
addText(textId, text, schriftart, groesse_mm, fett, kursiv, farbe, x_mm, y_mm, zentriert)Text platzieren. Positionale Parameter, kein Options-Objekt. Grundlinie bei y_mm; zentriert = horizontal an x_mm zentriert; farbe = [r,g,b] (leeres Array = Standard).
addImage(x_mm, y_mm, skalierung, pfad, pin?)PNG/JPG platzieren (positional). skalierung 1.0 = Pixelgröße.
close()Schließen ohne Speichern.
closeAndDownload(dateiname)Speichern und zum Download geben (nur wenn mitgliedProfileId 0/-1 war).
closeAndLinkToMember(feldId)Speichern und unter dem Datenfeld feldId mit dem Mitglied verknüpfen (nur wenn mitgliedProfileId > 0); gibt den Link-Pfad zurück.

Dateizugriff ist geschützt: vorlagenLink und der addImage-Pfad erwarten einen vollständigen app2dat-Datei-Link (wie ihn ein Dokument-Datenfeld enthält) — freie Pfade oder abgetippte Texte funktionieren nicht. Dateien fremder Organisationen sind nur mit Vorlagen-Freigabe und PIN erreichbar; verwende dabei den Platzhalter %GPIN% statt eines hartcodierten PINs — er bleibt in referenzierten Scripts dynamisch (Kapitel 14).

a2d.export — Exporte & Downloads

MethodeBeschreibung
membersCSV(groupId, feldIds, optionen)Mitglieder als CSV-String (Verwalter). Listen-/Mitglieder-Felder werden in Anzeigetexte aufgelöst, Datumswerte lokalisiert. feldIds: Array von Spalten-IDs oder null = Standardspalten; zusätzlich erlaubt: "Name", "Status", "StatusDate", "Respons", "ParentId", "Profile.<Feld>" (z. B. "Profile.Email"), "Profile.Location.<Feld>".
profileImages(groupId, profileIds, dateiname, layout)Profilbilder als Etikettenbogen-PDF erzeugen und zum Download geben (Verwalter). profileIds = null = alle aktiven. Alle vier Argumente sind Pflicht — nicht benötigte als null übergeben.
qrCodes(groupId, profileIds, dateiname, layout)QR-Codes (kodieren P<profileId>, darunter Vor-/Nachname) als Etikettenbogen-PDF — Parameter identisch zu profileImages.
download(inhalt, dateiname)Textdatei (z. B. CSV) zum Download geben.
downloadBytes(daten, dateiname)Binärdaten zum Download geben.

optionen für membersCSV: separator (Default ";"), includeHeader (Default true), onlyActive, profileIds (nur diese, in Ausgabereihenfolge), headers ({ feldId: "Überschrift" }) sowie attendance für Eventgruppen: from/to (Pflicht, ISO), count (Spalte „anwesend/gesamt“), sessions (eine Spalte je Termin), responsibility (transponierte Zuständigkeits-Matrix), allDates.

layout für Etikettenbögen (mm; Defaults = Avery-Zweckform 3652, 3 × 7 Etiketten je 70 × 42,3 auf A4): pageWidth, pageHeight, width, height, cols, rows, marginLeft, marginTop, gapHorizontal, gapVertical, skip (bereits verbrauchte Etiketten überspringen).

Excel-Tipp: Stelle eigenen CSV-Texten ein UTF-8-BOM voran ("\uFEFF" + csv), bevor du sie mit download ausgibst — sonst zeigt Excel Umlaute falsch an.

a2d.import — CSV-Import

MethodeBeschreibung
membersFromCSV(groupId, csvText, zuordnung, optionen)Schreibt CSV-Werte in Datenfelder bestehender Mitglieder (Verwalter). zuordnung = { "CSV-Spalte": "zielFeldId" }; optionen: separator (";"), skipHeader (true), matchBy ("ProfilId", "Name" oder "Email"). Rückgabe { imported, errors }.
membersFromCSVMap(groupId, csvText, mapText, optionen)Legt neue verwaltete Mitglieder aus CSV + Map-Datei an — wie der GUI-Import: Profil inkl. Adresse und Kategorien, Gruppenbeitritt, Datenfelder. Bestehende Mitglieder (gleicher Vor- + Nachname) werden übersprungen. Rückgabe { imported, skipped, errors }.

Vorsicht bei Listenfeldern: membersFromCSV schreibt Werte roh — CSV-Anzeigetexte vorher über memberData.getDataFields in die gespeicherten Listen-keys auflösen, sonst landen unbekannte Werte im Feld. Das Map-Format für membersFromCSVMap ist in Kapitel 13 ausführlich beschrieben.

Referenz: Dialoge, Variablen & Werkzeuge

a2d.ui — Dialoge & Oberfläche

MethodeBeschreibung
showMessage(titel, text)OK-Dialog anzeigen; wartet auf Bestätigung.
confirm(titel, text)Ja/Nein-Dialog; gibt true/false zurück.
showInput(titel, felder)Dynamischer Eingabedialog. Rückgabe: Objekt mit den Werten je Feld-id — oder null bei Abbruch. Instanz-Variablen aus a2d.vars.set() dienen als Vorbelegung, wenn die Feld-id dem Variablennamen entspricht.
showSelect(titel, optionen)Auswahlliste (Array von Strings); gibt die gewählte Option als String zurück (nicht den Index), null bei Abbruch/Timeout.
pickFile(titel, endungen?)Nativer Datei-Dialog. Gibt den Textinhalt der gewählten Datei zurück (nicht Pfad oder Name), null bei Abbruch. endungen z. B. ".csv" oder ".csv,.map".
refresh()Aktualisiert die sichtbare Liste der App (Mitglieder/Gruppen/Profile — je nach Kontext). Als letzter Schritt jedes schreibenden Scripts empfohlen.

Feld-Objekte für showInput: id (Pflicht, außer bei type:"label"), name (Beschriftung), type, required (Default false), description. Typen (immer kleingeschrieben): string · number · decimal · date · time · bool · label (reiner Hinweistext); unbekannte Typen fallen auf string zurück. Alle Rückgabewerte sind Strings (Kapitel 3).

a2d.vars — Variablen

MethodeBeschreibung
set(name, wert) / get(name) / getAll()Synchron. Instanz-Variablen — leben nur während der aktuellen Ausführung; dienen u. a. als Vorbelegung für showInput.
save(name, wert, scope?)Wert dauerhaft speichern — über Ausführungen hinweg.
load(name, scope?)Dauerhaft gespeicherten Wert laden.

Reichweite (scope): ohne Angabe = aktuelle Ebene · "group" diese Gruppe · "event" dieser Eventordner · "profile" alle Gruppen desselben Profils · "user" alle Profile des Benutzers. Speichern und Laden müssen denselben Scope verwenden; für die Übergabe zwischen Gruppen verschiedener Profile eignet sich "user".

a2d.date — Datumswerte (synchron)

MethodeBeschreibung
format(wert, kultur?)Kurzdatum im Format der Kultur (ohne Angabe: a2d.context.culture) — für die Anzeige.
toIso(wert)Beliebige Datums-Schreibweise ins kanonische yyyy-MM-dd normalisieren — vor dem Speichern/Übergeben.

a2d.automation — Scripts aus Scripts aufrufen

MethodeBeschreibung
executeScript(scriptName, parameter)Führt ein anderes Script aus und gibt dessen Ergebnis zurück. scriptName wird im eigenen Scope aufgelöst; für Scripts anderer Gruppen legst du eine lokale Referenz an und rufst deren Namen auf (der Code wird live gelesen, läuft aber im Kontext des Aufrufers). Parameterwerte kommen im Sub-Script als Strings über params.get(…) an; das Ergebnis stammt aus dessen result. Wirft bei Fehlern — bei Bedarf try/catch.
// Aufrufer: Unter-Script starten, Parameter übergeben, Ergebnis verwenden
const r = await a2d.automation.executeScript("Beitrag berechnen", {
    basis: "42.50",
    faktor: "1.2"
});
log("Betrag: " + r.betrag + " " + r.waehrung);

// Unter-Script "Beitrag berechnen":
// const basis  = parseFloat(params.get("basis"));
// const faktor = parseFloat(params.get("faktor"));
// if (isNaN(basis) || isNaN(faktor)) { result.set("message", "Ungültige Parameter"); return; }
// result.setJson(JSON.stringify({ betrag: (basis * faktor).toFixed(2), waehrung: "EUR" }));

So zerlegst du größere Automatisierungen in wiederverwendbare Bausteine — etwa eine zentrale Beitragsberechnung, die mehrere Scripts nutzen.

Ereignis-Trigger

Bisher hast du Scripts von Hand gestartet. Ereignis-Trigger drehen den Spieß um: Das Script läuft automatisch, sobald sich in der Gruppe etwas ändert — ganz ohne Zutun. Konfiguriert werden Ereignis-Trigger an Gruppen-Scripts — im Editor über den Auslöser-Dialog des Scripts (Kapitel 14). Zusätzlich wertet der Server profilweite Ereignis-Scripts (Profil-Scope) aus, die für alle Gruppen des Profils feuern.

Die drei Ereignis-Typen

Ereignisevent.typeLöst aus bei …
StatusänderungMemberStatusChangedBeitritt, Abmeldung, Statuswechsel eines oder mehrerer Mitglieder.
DatenänderungMemberDataChangedÄnderung von Datenfeldern eines Mitglieds.
AnwesenheitsänderungMemberAttendanceChangedAn-/Abmeldung zu einem Termin oder Terminabsage.

Ein Script kann mehrere Ereignis-Typen gleichzeitig abonnieren und über event.type verzweigen. Wichtig: Ereignis-Scripts laufen im Namen des Benutzers, der die Änderung ausgelöst hat — nicht als „System“. Sie können also genau das, was der Auslöser selbst dürfte.

Die event.*-Parameter

Das Script erhält die Ereignisdaten über params.get(…) — alle Werte als Strings, Listen kommagetrennt:

ParameterInhalt
event.typeEreignis-Typ (siehe oben) — immer vorhanden.
event.groupId / event.groupProfileIdGruppe des Ereignisses und deren Organisationsprofil.
event.actorUserId / event.actorProfileIdAuslösender Benutzer und dessen handelndes Profil (Selbständerung: das geänderte Profil; Admin-Aktion: das Profil des Admins). Idealer Absender für chat.sendPrivateMessage.
Bei Statusänderung: event.profileIds (Liste), event.memberCount, event.status.<profileId> (neuer Status je Mitglied); bei genau einem Betroffenen zusätzlich event.profileId und event.status.
Bei Datenänderung: event.profileId, event.changedFields (Liste der Feld-Schlüssel), event.field.<schluessel> (neuer Wert je Feld).
Bei Anwesenheitsänderung: event.sessionId, event.date (yyyy-MM-dd), event.memberIds (Liste; der Wert -1 bedeutet: der ganze Termin wurde abgesagt), event.memberCount.

Beispiel: ein Wächter-Script für alle drei Ereignisse

Dieses Script (aus der eingebauten Beispiel-Bibliothek) abonniert alle drei Ereignis-Typen und schickt einer hinterlegten Person eine private Nachricht mit den Details:

// Reagiert auf alle drei Ereignis-Typen und verschickt eine private Nachricht.
// Als Ereignis-Trigger einrichten und alle drei Typen anhaken (Gruppen-Scope).

const EMPFAENGER_PROFIL_ID = 1;   // Empfänger der Benachrichtigung (anpassen)

const typ = params.get("event.type");
if (!typ) { result.set("message", "Dieses Script ist für einen Ereignis-Trigger gedacht."); return; }

const groupId  = params.get("event.groupId");
const absender = parseInt(params.get("event.actorProfileId") || "0", 10); // 0 = automatisch

log("Ereignis empfangen: " + typ + " (Gruppe " + groupId + ")");

// Kommagetrennte Liste → Array (leere Einträge entfernt)
function alsListe(csv) {
    return (csv || "").split(",").map(s => s.trim()).filter(s => s.length > 0);
}

let text;

if (typ === "MemberStatusChanged") {
    const ids = alsListe(params.get("event.profileIds"));
    const teile = ids.map(id => "Profil " + id + " → " + (params.get("event.status." + id) || "?"));
    text = "Statusänderung in Gruppe " + groupId + ": " + teile.join(", ");

} else if (typ === "MemberDataChanged") {
    const profilId = params.get("event.profileId");
    const felder = alsListe(params.get("event.changedFields"));
    const teile = felder.map(f => f + " = " + (params.get("event.field." + f) || ""));
    text = "Datenänderung bei Profil " + profilId + " in Gruppe " + groupId
         + (teile.length ? ": " + teile.join(", ") : "");

} else if (typ === "MemberAttendanceChanged") {
    const datum     = params.get("event.date");
    const terminId  = params.get("event.sessionId");
    const ids       = alsListe(params.get("event.memberIds"));
    if (ids.length === 1 && ids[0] === "-1") {
        text = "Termin " + terminId + " am " + datum + " (Gruppe " + groupId + ") wurde abgesagt.";
    } else {
        text = "Anwesenheitsänderung in Gruppe " + groupId + ", Termin " + terminId
             + " am " + datum + ": Profil(e) " + ids.join(", ");
    }

} else {
    text = "Unbekanntes Ereignis: " + typ;
}

// Senden: Absender = handelndes Profil des Auslösers
try {
    await a2d.chat.sendPrivateMessage(EMPFAENGER_PROFIL_ID, text, absender);
    result.set("message", "Gesendet an Profil " + EMPFAENGER_PROFIL_ID + ": " + text);
} catch (e) {
    result.set("message", "Senden fehlgeschlagen: " + e.message + " | " + text);
}

Wo sehe ich, was passiert ist? Ereignis-Trigger laufen im Hintergrund — ihre Berichte findest du im Editor über den Ausführungsverlauf (die letzten fünf Läufe je Ebene werden aufbewahrt).

Praxis: Anwesenheiten über mehrere Gruppen auswerten

Das erste große Praxisbeispiel stammt — wie die beiden folgenden — aus der kuratierten Beispiel-Bibliothek, aus der sich auch der Automatisierungs-Assistent bedient. Die Aufgabe: Ein Verein trainiert in mehreren Gruppen. Für die ausgewählten Mitglieder soll gezählt werden, wie oft jedes im abgefragten Zeitraum irgendwo anwesend war — und Anzahl sowie Prozentsatz sollen als Datenfelder in die aktuelle Gruppe zurückgeschrieben werden.

Voraussetzungen

  • Die aktuelle Gruppe hat die Datenfelder „Anzahl“ und „Prozent %“.
  • Der Ausführende ist Verwalter der beteiligten Trainingsgruppen (Anwesenheits-Matrix ist Admin-Funktion).
  • Vor dem Start werden Mitglieder in der Liste ausgewählt.

Das Script

// Trainings-Anwesenheiten berechnen
// Zählt je ausgewähltem Mitglied die Anwesenheiten über mehrere Trainingsgruppen
// im abgefragten Zeitraum und schreibt Anzahl ("Anzahl") und Prozent ("Prozent %")
// in die aktuelle Gruppe. Die Prozent-Basis (max. Trainings) wird im Dialog abgefragt.

const groupId = a2d.context.groupId;
if (!groupId) { await a2d.ui.showMessage("Fehler", "Bitte in einer Gruppe ausführen."); return; }

const auswahl = a2d.member.getSelected() || [];
if (auswahl.length === 0) {
    await a2d.ui.showMessage("Fehler", "Keine Mitglieder ausgewählt");
    return;
}

const anwesendId = await a2d.memberData.getFieldId(groupId, "Anzahl");
const prozentId  = await a2d.memberData.getFieldId(groupId, "Prozent %");
if (!anwesendId || !prozentId) {
    await a2d.ui.showMessage("Fehler", "Datenfelder 'Anzahl' und/oder 'Prozent %' nicht gefunden.");
    return;
}

// Vorbelegung aus dauerhaften Variablen
a2d.vars.set("datVon", await a2d.vars.load("datVon") || new Date().toLocaleDateString("de-DE"));
a2d.vars.set("datBis", await a2d.vars.load("datBis") || new Date().toLocaleDateString("de-DE"));
a2d.vars.set("maxTrainings", await a2d.vars.load("maxTrainings") || "");

const felder = [
    { type: "label", name: "Alle Felder müssen ausgefüllt sein" },
    { type: "date", id: "datVon", name: "Datum von*", description: "" },
    { type: "date", id: "datBis", name: "Datum bis*", description: "" },
    { type: "number", id: "maxTrainings", name: "Max. Anzahl Trainings*",
      description: "Maximal mögliche Trainings für die %-Berechnung." }
];
const eingabe = await a2d.ui.showInput("Anwesenheiten auswerten", felder);
if (!eingabe) return; // Vom Benutzer abgebrochen

const mt = parseInt(eingabe["maxTrainings"], 10);
if (isNaN(mt) || mt <= 0) {
    await a2d.ui.showMessage("Fehler", "Bitte eine gültige max. Trainingsanzahl (größer 0) angeben.");
    return;
}

await a2d.vars.save("datVon", eingabe["datVon"]);
await a2d.vars.save("datBis", eingabe["datBis"]);
await a2d.vars.save("maxTrainings", eingabe["maxTrainings"]);

// Trainingsgruppen, über die summiert wird (IDs der eigenen Gruppen eintragen)
const gruppenIds = [111, 222, 333];
const zaehler = {};

for (const gid of gruppenIds) {
    const pa = await a2d.attendance.profilesAttendances(auswahl, gid, eingabe["datVon"], eingabe["datBis"], null);
    if (!pa) continue;

    for (const pid of auswahl) {
        // pa ist ein .NET-Dictionary; Zugriff auf einen fehlenden Schlüssel
        // kann einen Fehler werfen → defensiv absichern.
        let termine;
        try { termine = pa[pid]; } catch (e) { termine = null; }
        if (!termine) continue;

        if (!zaehler[pid]) zaehler[pid] = 0;
        for (const dat of Object.keys(termine)) {
            if (termine[dat] === "1") zaehler[pid]++;
        }
    }
}

let geschrieben = 0;
for (const pid of Object.keys(zaehler)) {
    const anzahl = zaehler[pid];
    await a2d.memberData.set(groupId, pid, anwesendId, anzahl.toString());
    await a2d.memberData.set(groupId, pid, prozentId, (anzahl / mt * 100).toFixed(1));
    geschrieben++;
}

// Sichtbare Mitgliederliste aktualisieren, damit die neuen Werte sofort erscheinen
await a2d.ui.refresh();

await a2d.ui.showMessage("app2dat", geschrieben + " Mitglieder aktualisiert.");

Was du hier lernst

  1. Dialog mit Gedächtnis. Das Muster a2d.vars.set(id, await a2d.vars.load(id) || Standard) vor showInput belegt die Felder mit den Werten des letzten Laufs vor — beim Speichern nach dem Dialog werden sie mit vars.save wieder gemerkt. Der Benutzer tippt Zeitraum und Maximalzahl nur beim ersten Mal.
  2. Eingaben validieren. showInput liefert Strings — parseInt + isNaN-Prüfung verhindern eine Division durch Null oder Unsinn in den Prozentwerten.
  3. Die Anwesenheits-Matrix. profilesAttendances liefert je Profil ein Termin→Status-Verzeichnis (Schlüssel "yyyy-MM-dd HH:mm"); gezählt wird jeder Eintrag mit Wert "1". Die Schleife über mehrere gruppenIds summiert gruppenübergreifend — und der try/catch schützt vor dem .NET-Dictionary-Verhalten bei fehlenden Schlüsseln.
  4. Zurückschreiben + auffrischen. Zwei memberData.set-Aufrufe je Mitglied, am Ende ui.refresh() — die neuen Spaltenwerte erscheinen sofort in der Liste.

Variante: Sollen die Gruppen nicht hartcodiert sein, frage sie per showInput ab oder lege sie mit vars.save im Profil-Scope ab — oder lass den Assistenten das Script an deine Gruppennamen anpassen.

Praxis: Urkunden als PDF je Mitglied

Nach der Gürtelprüfung sollen alle erfolgreichen Mitglieder ihre Urkunde bekommen — als PDF, automatisch beim jeweiligen Mitglied hinterlegt. Dieses Beispiel erzeugt ein eigenes PDF pro ausgewähltem Mitglied und verknüpft es mit dessen Dokument-Datenfeld „Urkunde“. Es zeigt das komplette a2d.pdf-Handwerk: vorbereiten, Seite anlegen, Texte millimetergenau platzieren, Stempelbild einfügen, verknüpfen.

Voraussetzungen

  • Datenfelder „Urkunde“ (Typ Dokument) und „Kyu“ (Graduierung) in der aktuellen Gruppe.
  • Ein Stempel-/Unterschriftsbild als Datei der Gruppe — dessen vollständigen Datei-Link trägst du im Script ein (Platzhalter im Beispiel).
  • Mitglieder in der Liste auswählen, dann ausführen.

Das Script

// Urkunden erstellen (für ausgewählte Mitglieder)
// Erzeugt EIN PDF pro Mitglied und verknüpft es mit dem Datenfeld "Urkunde".

const groupId   = a2d.context.groupId;
const profileId = a2d.context.profileId;
if (!groupId) { await a2d.ui.showMessage("Fehler", "Bitte in einer Gruppe ausführen."); return; }

const auswahl = a2d.member.getSelected() || [];
if (auswahl.length === 0) {
    await a2d.ui.showMessage("Fehler", "Keine Mitglieder ausgewählt");
    return;
}

// === Prüfungsdaten (vorbelegt aus dauerhaften Variablen) ===
a2d.vars.set("pruefer", await a2d.vars.load("pruefer") || "Max Mustermann");
a2d.vars.set("ort", await a2d.vars.load("ort") || "Musterstadt");
a2d.vars.set("dat", await a2d.vars.load("dat") || new Date().toLocaleDateString("de-DE"));

const felder = [
    { type: "label", name: "Mit * markierte Felder sind Pflicht" },
    { type: "label", name: "Wichtig! Beim Drucken die Skalierung auf 100% stellen!" },
    { type: "string", id: "pruefer", name: "Prüfer*", description: "Nur den Vorsitzenden der Prüfungskommission eintragen" },
    { type: "string", id: "ort", name: "Ort*", description: "" },
    { type: "date", id: "dat", name: "Datum*", description: "" }
];
const eingabe = await a2d.ui.showInput("Prüfungsdaten erfassen", felder);
if (!eingabe) return; // Vom Benutzer abgebrochen

await a2d.vars.save("pruefer", eingabe["pruefer"]);
await a2d.vars.save("ort", eingabe["ort"]);
await a2d.vars.save("dat", eingabe["dat"]);

const datumFormatiert = new Date(eingabe["dat"])
    .toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" })
    .replace(/(\d+)\s/, "$1. ")
    .replace(",", "");
// "6 März, 2026" → "6. März 2026"

const urkId = await a2d.memberData.getFieldId(groupId, "Urkunde");
const kyuId = await a2d.memberData.getFieldId(groupId, "Kyu");
if (!urkId || !kyuId) { await a2d.ui.showMessage("Fehler", "Datenfeld 'Urkunde' und/oder 'Kyu' nicht gefunden."); return; }

for (const pid of auswahl) {
    const mitgliedProfil = await a2d.profile.get(pid);
    if (!mitgliedProfil) continue;
    const daten = (await a2d.memberData.get(groupId, pid)) || {};

    await a2d.pdf.prepare(profileId, groupId, "Kyu-Urkunde", "Kyu-Prüfung", 210, 297, "", pid);
    await a2d.pdf.addPage();

    const name = (mitgliedProfil.firstName || "") + " " + (mitgliedProfil.secondName || "").toUpperCase();
    await a2d.pdf.addText("gross7", name, "Arial", 7, true, false, [], 105, 135, true);

    const kyu = daten[kyuId] || "?";
    await a2d.pdf.addText("gross9", kyu + ". Kyu", "Arial", 9, true, false, [], 105, 172, true);

    await a2d.pdf.addText("kleinKursiv", eingabe["pruefer"], "Arial", 2.7, true, false, [], 40, 277, true);
    await a2d.pdf.addText("kleinFett", eingabe["ort"], "Arial", 2.7, true, false, [], 105, 277, true);
    await a2d.pdf.addText("kleinFett", datumFormatiert, "Arial", 2.7, true, false, [], 105, 281, true);

    // Vollständigen app2dat-Datei-Link des Stempelbilds einsetzen (z. B. aus einem Dokument-Datenfeld kopieren)
    await a2d.pdf.addImage(60, 247, 0.2, "https://api.app2dat.com|0/0/0/2/groups/GRUPPEN_ID/stempel.png¦Stempel_Unterschrift.png");
    await a2d.pdf.CloseAndLinkToMember(urkId);
}

// Mitgliederliste aktualisieren (verknüpfte Urkunden erscheinen sofort)
await a2d.ui.refresh();

await a2d.ui.showMessage("app2dat", auswahl.length.toString() + " Urkunden erstellt");

Was du hier lernst

  1. Ein PDF pro Mitglied. Der entscheidende Kniff steckt in pdf.prepare(…, pid): Der letzte Parameter verknüpft das PDF mit genau diesem Mitglied — darum steht der komplette prepare-→-close-Zyklus innerhalb der Schleife. CloseAndLinkToMember(urkId) legt die fertige Urkunde direkt im Dokument-Feld des Mitglieds ab.
  2. Millimeter statt Pixel. Alle Positionen sind mm auf dem A4-Blatt (210 × 297): addText(…, 105, 135, true) zentriert den Namen horizontal bei x = 105 mm — der Blattmitte. So passt das Layout auf vorgedruckte Urkundenbögen.
  3. Feldwerte einbetten. Der Kyu-Grad kommt per daten[kyuId] aus dem Datenfeld des Mitglieds; Profildaten (Name) aus profile.get. Prüfer, Ort und Datum fragt der Dialog ab — wieder mit vars-Gedächtnis wie in Kapitel 11.
  4. Bilder über Datei-Links. addImage verlangt den vollständigen app2dat-Datei-Link inklusive Anzeigename (nach dem ¦-Zeichen). Den Link bekommst du z. B. aus einem Dokument-Datenfeld — freie Pfade akzeptiert die Sandbox nicht (Kapitel 8).

Alles in einem PDF? Sollen alle Urkunden gesammelt als eine Datei zum Download entstehen (eine Seite pro Mitglied, optional mit Hintergrund-Vorlage), rufst du prepare nur einmal mit mitgliedProfileId = -1 auf und schließt mit closeAndDownload — genau so macht es das Schwester-Beispiel Urkunden-Export aus der Beispiel-Bibliothek.

Praxis: Neue Mitglieder aus CSV importieren

Beim Umstieg auf app2dat existiert die Mitgliederliste meist schon — als Excel- oder CSV-Datei. Dieses Beispiel legt daraus neue verwaltete Mitglieder an: Es öffnet den nativen Datei-Dialog für die CSV- und eine Zuordnungs-Datei (.map), erstellt Profile samt Adresse und Kategorien, tritt der Gruppe bei und befüllt die Datenfelder — Duplikate werden übersprungen.

Die Map-Datei: Spalten zuordnen

Die .map-Datei ist eine einfache Textdatei nach dem Muster Zielfeld=CSV-Spalte, eine Zeile pro Zuordnung:

Profile.FirstName=Vorname
Profile.SecondName=Nachname
Profile.Gender[list:M|männlich:W|weiblich:D|divers]=Geschlecht
Profile.DateOfBirth[date]=Geburtsdatum
Profile.Location.City=Ort
Profile.Categories=martialarts|karate
Data.Graduierung=Grad
ZielfeldBedeutung
Profile.<Feld>Profilfeld des neuen Mitglieds (FirstName, SecondName, DateOfBirth, Gender, …).
Profile.Location.<Feld>Adressfeld (Street, City, ZipCode, …).
Data.<Feldname>Gruppendatenfeld — angesprochen über den Feldnamen.
[date]Modifikator: CSV-Wert als Datum interpretieren.
[list:key|text:…]Modifikator: CSV-Anzeigetexte in gespeicherte Listen-Schlüssel übersetzen.
Feld=konstante ohne CSV-Spalte / [value]Konstanter Wert für alle Zeilen (z. B. Profile.Categories).

Das Script

// Mitglieder aus CSV importieren (neue Profile anlegen)
// Wählt CSV- und MAP-Datei über den nativen Datei-Dialog und legt daraus neue
// Mitglieder an — wie der GUI-Import "CSV importieren". In der Mitgliedergruppe ausführen.

const groupId = a2d.context.groupId;
if (!groupId) { await a2d.ui.showMessage("Import", "Bitte in der Mitgliedergruppe ausführen."); return; }

// CSV-Datei wählen (zurück kommt der INHALT der Datei als Text)
const csv = await a2d.ui.pickFile("CSV-Datei wählen", ".csv");
if (!csv) return; // Abgebrochen

// MAP-Datei wählen
const map = await a2d.ui.pickFile("MAP-Datei wählen", ".map");
if (!map) return; // Abgebrochen

// Import: legt neue Mitglieder an; bestehende (Vor- + Nachname) werden übersprungen.
const res = await a2d.import.membersFromCSVMap(groupId, csv, map, { separator: ";" });

let meldung = res.imported + " neue Mitglieder angelegt";
if (res.skipped > 0) meldung += ", " + res.skipped + " übersprungen (bereits vorhanden)";
if (res.errors && res.errors.length > 0) meldung += ", " + res.errors.length + " Fehler";

// Sichtbare Mitgliederliste aktualisieren
await a2d.ui.refresh();

await a2d.ui.showMessage("CSV-Import", meldung);
result.set("message", meldung);

Was du hier lernst

  1. pickFile liefert Inhalt, nicht Pfad. Die Rückgabe ist der Text der gewählten Datei — sie wandert direkt in membersFromCSVMap. Zweimal null prüfen: Der Benutzer kann jeden Dialog abbrechen.
  2. Eine Methode, der ganze Import. membersFromCSVMap übernimmt Profil-Anlage, Adresse, Kategorien, Gruppenbeitritt und Datenfelder in einem Zug — und dedupliziert über Vor- + Nachname. Das Ergebnis-Objekt (imported/skipped/errors) wird zur Erfolgsmeldung.
  3. Ergebnis doppelt melden. showMessage informiert den Benutzer sofort; result.set("message", …) hält dieselbe Zusammenfassung im Ausführungsbericht fest.

Nur Daten aktualisieren? Sollen keine neuen Mitglieder entstehen, sondern Felder bestehender Mitglieder aus einer Tabelle befüllt werden, ist a2d.import.membersFromCSV mit matchBy "Name"/"Email" das richtige Werkzeug (Kapitel 8) — bei Listenfeldern zuerst Anzeigetexte in Listen-Schlüssel auflösen.

Der Automatisierungs-Assistent

Du kennst jetzt die ganze API — aber du musst sie nicht auswendig können. Der Automatisierungs-Assistent ist ein app2dat-Experte, der Scripts für dich schreibt, ändert und erklärt. Dieses Kapitel zeigt die Arbeitsweise mit ihm — und das Handwerkszeug drumherum: Scopes, Ordnerstruktur, Trigger und die Versionsverwaltung im Editor.

So arbeitet der Assistent

Den Assistenten öffnest du im Automatisierungsmanager über das KI-Symbol — er erscheint als Chat-Panel neben dem Editor. Beschreib in normalem Deutsch, was passieren soll:

  1. Er sieht sich um. Der Assistent kennt deinen Kontext (Ebene, Gruppe, ausgewählte Mitglieder) und schaut selbstständig nach: vorhandene Scripts und Ordner, deine Gruppen, deren Datenfelder und Mitglieder — genau so, wie du es dürftest, denn er arbeitet mit deinen Rechten.
  2. Er greift auf Bewährtes zurück. Für typische Vereinsaufgaben durchsucht er die kuratierte Beispiel-Bibliothek (aus der auch die Praxisbeispiele der Kapitel 11–13 stammen) und passt die Gold-Vorlage an deine Feldnamen und Gruppen an, statt bei null anzufangen.
  3. Er schreibt und prüft. Neue Scripts legt er an, bestehende ändert er gezielt. Vor der Übergabe prüft er den Code auf Syntaxfehler und korrekte API-Verwendung und erklärt dir den Ablauf. Bei offenen Entscheidungen fragt er nach, statt zu raten — seine Schritte siehst du live im Panel, und du kannst ihn jederzeit pausieren.
  4. Du behältst die Kontrolle. Der Assistent führt Scripts nie selbst aus und arbeitet nur mit Testdaten-freien Prüfungen. Sein Ergebnis erscheint im Editor als Vorschlags-Version (siehe unten) — ausgeführt wird erst, wenn du es willst.

Ein Gespräch pro Script: Jedes Script hat seinen eigenen Gesprächsverlauf — öffnest du es später wieder, kannst du nahtlos anknüpfen („ergänze noch eine Abfrage des Zeitraums“). Mit Neue Unterhaltung startest du einen freien Chat ohne Script-Bezug; erstellt der Assistent darin ein neues Script, wird das Gespräch automatisch daran gebunden. Auch Fragen zur API oder zu einem Bericht darfst du ihm stellen — er ist Lehrer und Programmierer zugleich.

Scopes: wo Scripts zu Hause sind

Jedes Script gehört zu genau einer der vier Ebenen aus Kapitel 1 — auch der Assistent arbeitet immer innerhalb der Ebene, in der du den Manager geöffnet hast:

EbeneManager öffnenVerwalten darf
BenutzerBenutzermenüAutomatisierungNur du selbst.
ProfilAm Organisationsprofil → AutomatisierungManager/Admin des Profils.
GruppeIn der Gruppe bzw. im Gruppenmenü → AutomatisierungGruppen-Manager/Admin; ausführen ab Mitglied (je nach Freigabe).
Event (Ordner)In der Event-Verwaltung des Ordners → AutomatisierungManager/Admin des veranstaltenden Profils.

Der Scope entscheidet, was a2d.context liefert, wo das Zauberstab-Menü das Script anbietet und wie weit vars.save reicht. Gruppen-Scripts sind der Normalfall; Profil-Scripts eignen sich für Aufgaben über mehrere Gruppen hinweg (z. B. das Anlegen ganzer Vereinsstrukturen), Event-Scripts arbeiten auf Ordner-Ebene — etwa um die Eventgruppen einer Veranstaltung automatisiert zu erzeugen.

Ordnerstruktur: Ordnung im Script-Baum

  • Ordner & Unterordner. Die Baumansicht links organisiert Scripts in Ordnern (eine Unterordner-Ebene). Neue Elemente legst du über die Kopfleisten-Schaltflächen Ordner und Script an — oder über das Kontextmenü eines Ordners.
  • Umbenennen, verschieben, sortieren. Das Kontextmenü jedes Eintrags bietet Umbenennen, Löschen, Verschieben in einen anderen Ordner sowie Hoch/Runter für die Reihenfolge — sie bestimmt auch die Reihenfolge im Zauberstab-Menü.
  • Referenzen: ein Script, viele Orte. Über Referenz kopieren und Referenz einfügen verweist ein Eintrag in einem anderen Scope live auf das Original — der Code wird bei jeder Ausführung frisch von der Quelle gelesen und ist im Editor schreibgeschützt (Kennzeichen „Referenz“). So pflegst du ein Script zentral und nutzt es in vielen Gruppen; auch Gruppen-Vorlagen rollen ihre Automatisierungen als Referenzen aus. Kopie erstellen macht aus der Referenz bei Bedarf ein eigenständiges Script (der Platzhalter %GPIN% wird dabei durch den aktuellen Vorlagen-PIN ersetzt).

Trigger: wie Scripts gestartet werden

Im Editor öffnet Auslöser die Trigger-Einstellungen des Scripts:

AuslöserWirkung
MenüDas Script erscheint im Zauberstab-Menü seiner Ebene und wird von dort manuell gestartet.
KI-AssistentDer Support-Assistent der App darf das Script auf Nachfrage anbieten und für den Benutzer ausführen — deine Automatisierung wird zur Ein-Klick-Antwort.
Event-TriggerAutomatischer Start bei Status-, Daten- oder Anwesenheitsänderung (einstellbar an Gruppen-Scripts) — Details und Parameter in Kapitel 10.

Freigaben erweitern beim Menü-Trigger den Kreis der Ausführenden über die Verwalter hinaus: Auch durch Zuständige ausführbar (Übungsleiter starten das Script aus der Anwesenheitsliste), Auch durch Organisations-Teilnehmer ausführbar (Verwalter angemeldeter Vereine in Verbands-Gruppen) und Auch in geteilten Gruppen ausführbar. Bei geteilten Gruppen gilt: Enthält die Freigabe kein Bearbeiten-Recht, läuft das Script dort nur lesend — schreibende a2d-Aufrufe werden abgelehnt.

Versionen: Änderungen sicher übernehmen

Der Editor führt für jedes geöffnete Script einen Versionsverlauf. Jeder Stand — geladen vom Server, von dir manuell bearbeitet oder vom Assistenten vorgeschlagen — wird eine eigene Version:

  1. Blättern und vergleichen. Gibt es mehr als eine Version, erscheinen in der Kopfleiste Pfeile („2/3“) zum Vor- und Zurückblättern. Die Diff-Ansicht hebt Zeilen hervor, die sich zur Vorversion geändert haben — so siehst du auf einen Blick, was der Assistent angefasst hat.
  2. Vorschlag prüfen. Nach einem Assistenten-Lauf erscheint sein Ergebnis als neue Version — noch nicht gespeichert. Lies die Erklärung im Chat, prüfe den Diff und führe es per Ausführen aus — das ist ein echter Lauf mit echten Daten (nutzt das Script die Mitglieder-Auswahl, fragt der Editor sie vorab in einem Dialog ab).
  3. Übernehmen oder verwerfen. Speichern (Strg+S) macht die angezeigte Version zum gültigen Stand — Alle speichern sichert mehrere geänderte Scripts auf einmal. Zum Verwerfen blätterst du einfach zur gewünschten Version zurück und speicherst diese; beim Schließen mit ungespeicherten Änderungen fragt der Editor nach.

Der Verlauf ist eine Arbeitshilfe, kein Archiv: Er lebt im Speicher, solange der Automatisierungsmanager geöffnet ist — dauerhaft gilt allein der gespeicherte Stand. Die Ausführungs-Historie (letzte fünf Läufe je Ebene) bleibt davon unabhängig über den Verlauf abrufbar.

Zusammenspiel: dein Werkzeugkasten komplett

Damit schließt sich der Kreis dieses Handbuchs: Du beschreibst dem Assistenten die Aufgabe, er baut auf den Gold-Beispielen auf und liefert eine Vorschlags-Version, die du im Diff prüfst, testest und speicherst. Über Trigger machst du das Script für die richtigen Personen verfügbar — vom Zauberstab-Menü über den Support-Assistenten bis zum vollautomatischen Ereignis-Trigger. Und mit Referenzen und Vorlagen rollst du die fertige Automatisierung zentral gepflegt in beliebig viele Gruppen aus.

Weiterlesen: Verwaltung, Gruppen, Datenfelder und Beiträge erklärt das Administrationshandbuch; den KI-Support-Assistenten für deine Mitglieder beschreibt das Benutzerhandbuch.