Berechnungsfelder – Twig-Referenz

Berechnungsfelder sind spezielle Formularfelder, die ihren Wert automatisch berechnen – auf Basis anderer Felder, Schichten, Verträge, Tickets und vieler weiterer Daten. Du hinterlegst einmalig eine Formel, und festiware berechnet den Wert bei jeder Speicherung neu.

Das klingt technisch – ist es aber nur auf den ersten Blick. Für viele Anwendungsfälle reicht es, ein fertiges Beispiel aus diesem Artikel zu kopieren und nur den Feldnamen anzupassen.

Wozu sind Berechnungsfelder nützlich?

Berechnungsfelder lösen ein häufiges Problem: Informationen, die du brauchst, stecken an verschiedenen Stellen – und du willst sie nicht jedes Mal manuell zusammensuchen.

Einige typische Einsatzfälle:

  • Übersicht auf einen Blick: Ein Feld „Status" zeigt direkt in der Listenansicht VIP · Ticket #A2F1 · 3 Schichten , ohne dass du jedes Profil einzeln aufklappen musst.
  • Automatisch aktuelle Kennzahlen: Sobald eine neue Schicht eingetragen wird, zeigt das Feld „Schichtstunden gesamt" den aktuellen Stand.
  • Berechnungen: Die Dauer zwischen zwei Datumsfeldern, die Summe aller Zahlungseingänge, die Anzahl der Verträge.
  • Bedingte Texte: „VIP" oder „Standard" – je nachdem, welcher Ticket-Typ hinterlegt ist.
  • Kombinierte Informationen: Vor- und Nachname in einem Feld, kommagetrennte Liste aller Teammitglieder.

Berechnungsfelder basieren auf Twig, einer einfachen Formelsprache. Du musst Twig nicht von Grund auf lernen – für die meisten Fälle reicht es, ein Beispiel aus diesem Artikel anzupassen.

Ein Berechnungsfeld anlegen

Berechnung ist ein Feldtyp im Formular-Editor – du findest ihn in der Auswahlliste, wenn du ein neues Feld hinzufügst. Wähle dort einfach Berechnung aus.

Sobald du Berechnung gewählt hast, erscheint darunter die Vorlage-Auswahl. Dort kannst du aus fertigen Vorlagen wählen – festiware befüllt den Twig-Code dann automatisch. Du musst also für viele Fälle gar nichts selbst schreiben:

Vorlage-Auswahl im Formular-Editor: Feldtyp Berechnung mit allen verfügbaren Vorlagen

Willst du eine Formel selbst schreiben oder eine Vorlage anpassen, wähle ohne Vorlage und trage den Twig-Code manuell ein.

Schnellstart

Einfache Ausdrücke benötigen keine Klammern:

self.firstname ~ " " ~ self.name

Komplexere Logik kann vollständige Twig-Templates enthalten:

{% if self.vip == "yes" %}
  VIP · {{ tickets_owned|first.code }}
{% else %}
  Standard
{% endif %}

Feldnamen werden direkt über ihren Kurznamen angesprochen (z.B. self.firma , self.ankunftsdatum ). Den Kurznamen siehst du im Formular-Editor beim jeweiligen Feld – er ist üblicherweise kleingeschrieben.

Wie du ein Berechnungsfeld im Formular anlegst, beschreibt der Artikel Fenster „Feld hinzufügen / Feld bearbeiten".

Wenn du Twig-Formeln in Verträgen, PDFs oder E-Mails einsetzen möchtest, beachte den Artikel Widgets: Twig-Formeln in Vorlagen und PDFs richtig einsetzen – der Umgang mit dem WYSIWYG-Editor unterscheidet sich.

Funktionsweise

  • Automatische Neuberechnung beim Speichern von Personen/Gruppen sowie bei jeder Änderung verknüpfter Daten
  • Werte bleiben gespeichert bis zur nächsten Berechnung
  • Gegenseitige Referenzierung möglich zwischen Berechnungsfeldern
  • Fehlerbehandlung: Fehlerhafte Felder zeigen error: <Meldung>
  • Kaskadische Updates bei Änderungen abhängiger Datensätze (z.B. neue Schicht, neuer Vertrag)
  • Massenberechnung bei Formularänderungen – den Status siehst du in der jeweiligen Formulardefinition (Detail- oder Bearbeitungsansicht)

Feldtypen

Der Hauptfeldtyp ist immer Berechnung. Daneben gibt es einen Sub-Feldtyp, der festlegt, wie der berechnete Wert gespeichert und in Filtern oder Übersichten verwendet wird. Du wählst ihn im Formular-Editor direkt neben der Vorlage-Auswahl.

Sub-Feldtyp (UI) Format Verwendung
Texteingabe Zeichenkette HTML-sicher gerendert
Zahl Numerische Zeichenkette Summen, Zählwerte, Dauern
Datum JJJJ-MM-TT Datumswerte
Datum und Uhrzeit ISO-Format Zeitstempel
Auswahl-Feld Optionsschlüssel Aus definierten Optionen
Mehrfach-Auswahl-Feld JSON-Array Mehrfachauswahl

Die Variable self

self steht für die aktuelle Person bzw. Gruppe, in deren Profil das Berechnungsfeld ausgewertet wird. Über self.{feldname} greifst du auf jedes Formularfeld dieses Profils zu – z.B. self.firma oder self.ankunftsdatum. Den passenden Kurznamen findest du im Formular-Editor beim jeweiligen Feld.

Darüber hinaus stellt self die folgenden System-Eigenschaften und Relationen bereit:

System-Eigenschaften

  • self.id  — vollständige UUID
  • self.id_partial  — Kurz-ID (nur Person)
  • self.created_at  — Erstellungszeit
  • self.updated_at  — Änderungszeit

Relationen (allgemein für Person und Gruppe)

contracts — Verträge

contracts|first.contractType.id  // Vertragstyp-ID
contracts|length  // Anzahl

events — Schichtplanungs-Events

events|first.start_at  // Startzeit
events|filter(e => e.schedulerEventType.name == "Aufbau")|first.start_at  // Nach Typ filtern

actions — Aktionsprotokoll

last_action.name  // Name der letzten Aktion
last_action.created_at  // Zeitstempel

teams — Teams (hauptsächlich für Orga-Personentypen; für Volunteer-Typen bitte teams_suggested  / teams_assigned  / teams_confirmed  verwenden)

teams|map(t => t.name)|join(", ")  // Liste der Teamnamen

Nur für Personen

tickets_owned — Tickets

tickets_owned|first.code  // Ticket-Code
tickets_owned|length  // Anzahl
tickets_owned|filter(e => e.ticket_type_id == 3)|first.code  // Nach Typ filtern

orders — Shop-Bestellungen

orders|first.current_order_status.order_status_type.name
orders|length

jobs — Schichten (zeitabhängig)

jobs|first.name  // Schichtname
jobs|map(j => j.durationInMinutes("Event"))|sum|as_hours  // Geplante Stunden
jobs|map(j => j.durationInMinutes("Job"))|sum|as_hours  // Tatsächliche Stunden
jobs|first.data.details  // Details-Freitext der Schicht
jobs|first.frozenHourlyWage()  // Stundenlohn: eingefroren, sonst aktuell gültig
jobs|first.data.hourly_wage  // Stundenlohn: nur der eingefrorene Rohwert

Hinweis: details und der Stundenlohn gehören zum Zeiterfassungs-Modul (nur im Professional-Tarif). data.details und data.hourly_wage lesen den rohen gespeicherten Wert; frozenHourlyWage() liefert den eingefrorenen Lohn und fällt sonst auf den aktuell gültigen zurück. Leere Werte solltest du mit ?? '' bzw. ?? 0 abfangen, z. B. jobs|first.frozenHourlyWage() ?? 0.

guests — Gastpersonen

guests|map(p => "#{p.firstname} #{p.name}")|join(", ")

meal_orders — Essensbestellungen  |  vouchers_received — Essens-/Getränke-Gutscheine  |  coupons — Shop-Coupons

meal_orders|length  // Anzahl

transactions — Geldeingänge

transactions|map(t => t.balance)|sum  // Gesamtsumme

focus_people — Betreute Personen

focus_people|map(p => "#{p.firstname} #{p.name}")|join(", ")

applicationsAsMember / applicationsAsOwner — Gruppenmitgliedschaften

applicationsAsMember|map(a => a.name)|join(", ")
applicationsAsMember|filter(t => t.applicationType.id == 7)|first.name

teams_suggested , teams_assigned , teams_confirmed — Volunteer-Teamzuweisungen (vorgeschlagen / zugeteilt / bestätigt)

teams_confirmed|map(t => t.name)|join(", ")

Nur für Gruppen

owner — Ansprechperson

owner.firstname ~ " " ~ owner.name

members — Mitglieder

members|map(p => "#{p.firstname} #{p.name}")|join(", ")
members|length
members|filter(e => "Catering" in e.pivot.roleArray)

opuses — Werke/Darbietungen

opuses|map(o => o.pivot.duration_minutes|default(o.duration_minutes))|sum

subordinatedApplications — Untergeordnete Gruppen

subordinatedApplications|map(a => a.name)|join(", ")

festiware-spezifische Funktionen & Filter

Funktionen

diff_minutes(t1, t2) — Minuten zwischen zwei Zeitpunkten. Reihenfolge beachten: früherer Zeitpunkt zuerst, späterer als zweites (diff_minutes(start, ende)). Vertauscht man die beiden, ist das Ergebnis negativ.

diff_minutes(self.aufbau_start, self.aufbau_ende)  // in Minuten
diff_minutes(self.aufbau_start, self.aufbau_ende)|as_hours  // als HH:MM

getFormattedEventDate(event, mitBereich = false) — Kompaktes Datumsformat

getFormattedEventDate(events|first)  // "10.8. 14:00 - 16:30"
getFormattedEventDate(jobs|first, true)  // mit Bereichsangabe

projectsetting('Klassenname') — Projekteinstellungen abrufen

projectsetting('ContractIntroText')

collect_push(name, wert) / collect_get(name) — Hilfsfunktion für PDF-Vorlagen: Beim Durchlauf über mehrere Ressourcen können mit collect_push  Werte gesammelt und anschließend mit collect_get  im PDF-Layout ausgegeben werden. Nicht für Berechnungsfelder im Formular geeignet.

collect_push('gesamtsumme', self.teilsumme_a|force_to_int)
collect_get('gesamtsumme')|sum

Filter

Filter Funktion
\|as_hours Minuten zu „HH:MM" — immer als positiver Betrag (-9001:30)
\|sum Summiert numerische Werte
\|to_int_list String-Array zu Ganzzahlen
\|force_to_int / \|force_to_float Typumwandlung
\|get('schlüssel') Dotted-Notation mit Null-Sicherheit

Test

{% if self.budget is numeric %}
  {{ self.budget|force_to_float * 1.19 }}
{% endif %}

Standard-Twig-Referenz

Häufige Filter

Filter Funktion
\|date('d.m.Y') Datumsformatierung
\|length Elemente zählen
\|join(', ') Array verbinden
\|map(x => ...) Transformieren
\|filter(x => ...) Filtern
\|first / \|last Erstes/letztes Element
\|default('–') Fallback-Wert
\|upper / \|lower Groß-/Kleinschreibung
\|trim Leerzeichen entfernen
\|json_encode Als JSON kodieren
\|round(2) Runden
\|abs Absolutwert
\|replace({'alt': 'neu'}) Ersetzen

Häufige Funktionen

max(a, b) / min(a, b)
range(1, 5)  // [1, 2, 3, 4, 5]
date('Y')  // Aktuelles Jahr

Kontrollstrukturen

{# Bedingung #}
{% if self.bestaetigt == "yes" %}
  bestätigt
{% elseif self.bestaetigt == "maybe" %}
  ausstehend
{% else %}
  nein
{% endif %}

{# Ternär #}
{{ self.vip == "yes" ? "VIP" : "Standard" }}

{# Schleife #}
{% for event in events %}
  {{ event.start_at|date('j.n.') }} – {{ event.area.name }}
{% endfor %}

{# Variable #}
{% set gesamt = jobs|map(j => j.durationInMinutes("Event"))|sum %}

Twig-Vorlagen

Unter Projekt → Einstellungen → Berechnungsfeld-Vorlagen kannst du wiederverwendbare Ausdrücke einmalig speichern und in beliebigen Formularen einsetzen. Vorlagen werden beim Berechnen aufgelöst – eine Anpassung wirkt sich also beim nächsten Speichern auf alle Felder aus, die sie verwenden.

Praktische Beispiele

Die folgenden Beispiele kannst du direkt übernehmen – passe nur die Feldnamen (z.B. self.firma ) an die Kurznamen deiner eigenen Felder an.

1. Vollständiger Name

Was du siehst: Den kombinierten Vor- und Nachnamen, z.B. Max Mustermann . Nützlich als Suchfeld in der Übersicht, wenn du nach dem vollen Namen filtern möchtest.

self.firstname ~ " " ~ self.name

2. Events nach Typ zählen

Was du siehst: Wie oft ein bestimmter Event-Typ (hier: „Aufbau") vorkommt, z.B. 3 .

events|filter(e => e.schedulerEventType.name == "Aufbau")|length

3. Geplante Schichtstunden

Was du siehst: Die Gesamtdauer aller geplanten Schichten im Format HH:MM , z.B. 14:30 . So siehst du auf einen Blick, wie viele Stunden jemand eingeteilt ist – ohne alle Schichten manuell zusammenzuzählen.

jobs|map(j => j.durationInMinutes("Event"))|sum|as_hours

4. Tatsächliche Schichtstunden (nur abgeschlossen)

Was du siehst: Wie viele Stunden tatsächlich geleistet wurden – Schichten ohne „abgeschlossen"-Zeitstempel werden nicht gezählt. Nützlich für die Auswertung nach dem Festival.

jobs|filter(e => e["accomplished_at"] is not null)
  |map(j => j.durationInMinutes("Job"))|sum|as_hours

5. Vertragsstatus mit Fallback

Was du siehst: Den aktuellen Status des Vertrags, z.B. Unterschrieben . Wenn kein Vertrag vorhanden ist, erscheint . So erkennt das Vertragsteam in der Listenansicht sofort, wer noch keinen Vertrag hat.

{% set c = contracts|filter(e => e.contract_type_id == 3)|first %}
{{ c ? c.current_state : "–" }}

6. Dauer zwischen zwei Feldern

Was du siehst: Die Differenz zwischen zwei Zeitfeldern als Minuten oder als HH:MM . Nützlich z.B. um die geplante Aufbaudauer automatisch auszurechnen.

{{ diff_minutes(self.aufbau_start, self.aufbau_ende) }} min
{{ diff_minutes(self.aufbau_start, self.aufbau_ende)|as_hours }}

7. Bedingter Ticket-Code

Was du siehst: Je nach Feldwert entweder VIP · A2F1-XK7  oder Standard . Nützlich für eine kompakte Statusspalte in der Übersicht.

{% if self.vip == "yes" %}
  VIP · {{ tickets_owned|filter(e => e.ticket_type_id == 1)|first.code|default("kein Ticket") }}
{% else %}
  Standard
{% endif %}

8. Mitglieder nach Rolle

Was du siehst: Eine kommagetrennte Liste der Gruppenmitglieder mit einer bestimmten Rolle, z.B. Max Mustermann, Anna Beispiel .

members|filter(e => "Catering" in e.pivot.roleArray)
       |map(p => "#{p.firstname} #{p.name}")|join(", ")

9. Events als formatierte Liste

Was du siehst: Eine mehrzeilige Auflistung aller Events – nützlich als Zusammenfassung in der Übersicht oder im PDF-Export.

events|map(e => "#{e.start_at|date('j.n.Y H:i')} – #{e.end_at|date('H:i')} #{e.name} (#{e.area.name})")
      |join("<br>")

10. Summe der Geldeingänge

Was du siehst: Den Gesamtbetrag aller Transaktionen für diese Person, z.B. 250 . Nützlich um zu prüfen, ob eine Kaution vollständig eingegangen ist.

transactions|map(t => t.balance)|sum

Fehlersuche

Problem Lösung
error: ... angezeigt Code prüfen, Datensatz erneut speichern
Feld ist leer Feldkurznamen prüfen (Groß-/Kleinschreibung beachten), Relation auf Einträge prüfen
\|first liefert nichts Mit \|default('–') absichern
Falscher Wert nach Vorlagenänderung Datensatz erneut speichern
=== funktioniert nicht == verwenden

Leere Felder entstehen am häufigsten durch Tippfehler im Kurznamen.

Hat das Deine Frage beantwortet? Danke für Dein Feedback Es gab ein Problem beim Absenden Deines Feedbacks

Brauchst Du weitere Hilfe? Schreib uns! Schreib uns!