Berichtsfilter (im Detail)

Zuletzt aktualisiert 25 May 2026

Ein Berichtsfilter-Editor links, die Steuerelemente, die ein Betrachter in der Mitte sieht, und die resultierende Abfrage mit angewendeten Filterwerten rechts

Die Abfrage eines Berichts ist die Form einer Antwort. Filter sind die Art und Weise, wie die Person, die den Bericht öffnet, diese Form beim Betrachten verfeinert, ohne die Abfrage zu bearbeiten.

Ein Bericht mit dem Titel „Gewonnene Deals nach Vertriebsmitarbeiter" mit einem Date Added-Filter ist derselbe Bericht, egal ob jemand diese Woche, dieses Quartal oder die letzten zwölf Monate sehen möchte. Sie wählen das Zeitfenster über dem Raster; der Bericht läuft gegen dieses Fenster; der CSV-Export verwendet dasselbe Fenster.

Ein Bericht ohne Filter ist in Ordnung. Ein Bericht mit den richtigen Filtern ist ein Bericht statt zwölf.

Die Filter eines Berichts werden aus dem Bericht selbst heraus bearbeitet; der Filter-Editor liegt unter der direkten URL /reports/filters/{id}.

Wie Filter durch das System wandern

1 · ADMIN SCHREIBT JSON [ { "alias": "is_won", "type": "select", "properties": { "options": { "1": "Won" }}} ] 2 · BETRACHTER WÄHLT Outcome ▾ Won From 2026-01-01 Apply 3 · ABFRAGE LÄUFT SELECT id, name FROM deals WHERE is_published=1 AND is_won = '1' AND date_added >= '2026-01-01' ORDER BY amount DESC

Sie schreiben das JSON einmal; jeder, der den Bericht öffnet, sieht die von Ihnen definierten Steuerelemente; ihre Eingaben fließen in Ihre Abfrage.

Das Filter-JSON

Die Filters-Einstellung eines Berichts ist ein einzelnes JSON-Array. Jeder Eintrag ist entweder:

  • ein Objekt, das einen benutzerseitigen Filter definiert, oder
  • das String-Literal "manual", ein Flag, das den gesamten Bericht in den manuellen Modus umschaltet.

Der Editor ist ein JSON-Code-Editor mit Syntaxhervorhebung und Autovervollständigung für die bekannten Schlüssel (alias, label, type, properties, options und die Type-Werte).

Aufbau eines Filter-Objekts

{
  "alias": "date_added",
  "label": "Date Added",
  "type": "datetime",
  "properties": {}
}
Feld Erforderlich Was es bedeutet
alias ja Der Spaltenname (oder tabellenqualifiziert t.col), den der Filter in Ihrer Abfrage anspricht. Dies wird mit dem Wert des Benutzers verglichen.
label ja Die sichtbare Beschriftung, die über dem Raster angezeigt wird. Reiner Text, beliebig wählbar.
type ja Das zu rendernde Steuerelement. Einer der sechs Filtertypen.
properties bedingt Typspezifische Konfiguration. Erforderlich für select (dessen options-Map). Wird für andere Typen ignoriert.

Ein minimales Beispiel

[
  {
    "alias": "date_added",
    "label": "Date Added",
    "type": "datetime"
  },
  {
    "alias": "close_date",
    "label": "Close Date",
    "type": "datetime"
  },
  {
    "alias": "valid_email",
    "label": "Valid Email",
    "type": "select",
    "properties": {
      "options": {
        "1": "Yes",
        "0": "No"
      }
    }
  }
]

Das sind drei Filter: zwei Datumsbereiche und ein Ja/Nein-Dropdown.

Validierung beim Speichern

Wenn Sie im Filter-Editor auf Save klicken, geht Flexie so vor:

  1. Es rendert das JSON zunächst durch die Flexie Scripting-Engine (damit alle {{ ... }}-Ausdrücke, die Sie geschrieben haben, ihre endgültigen Werte erzeugen).
  2. Es parst das Ergebnis als JSON.
  3. Es durchläuft das Array und weist alles Fehlerhafte zurück.

Das JSON wird zurückgewiesen, wenn:

  • Es nach dem Scripting-Lauf kein gültiges JSON ist.
  • Einem Filtereintrag alias, label oder type fehlt.
  • Der type keiner von text, select, datetime, date, time, number ist.
  • Ein select-Filter keine properties.options hat.

Den Fehler sehen Sie direkt über dem Editor. Die bestehende Filterdefinition des Berichts bleibt erhalten, bis Sie eine gültige speichern.

Die sechs Filtertypen

type Steuerelement, das der Betrachter sieht Wogegen es filtert
text Ein Texteingabefeld + Operator-Dropdown Eine String-Spalte
number Ein numerisches Eingabefeld + Operator-Dropdown Eine numerische Spalte
select Ein Dropdown, befüllt aus Ihrer properties.options-Map Eine Spalte, deren Werte in der Map vorkommen
date Ein Datumswähler (mit From / To für Bereiche) Eine DATE-Spalte
datetime Ein Datum-/Uhrzeit-Wähler (mit From / To für Bereiche) Eine DATETIME-Spalte
time Ein Uhrzeit-Wähler (mit From / To für Bereiche) Eine TIME-Spalte

Das Operator-Dropdown erscheint neben Text-, Zahlen- und Select-Eingaben. Date, datetime und time verwenden direkt From/To-Bereichsfelder, ohne expliziten Operator-Wähler.

Operatoren je Typ

Typ Operatoren
text equals, does not equal, contains, does not contain, starts with, ends with, empty, not empty
number equals, does not equal, greater than, less than, greater than or equal, less than or equal
select equals, does not equal, empty, not empty
date / datetime / time Nur Bereich (From / To), kein expliziter Operator-Wähler

Bei Datumsangaben bedeutet ein leeres From „keine Untergrenze"; ein leeres To bedeutet „keine Obergrenze"; nur eines von beiden auszufüllen ist in Ordnung und üblich.

Die options-Map von select

Ein select-Filter muss properties.options als Objekt angeben, dessen Schlüssel die in der Datenbank gespeicherten Werte sind und dessen Werte die im Dropdown angezeigten Beschriftungen sind:

{
  "alias": "is_won",
  "label": "Outcome",
  "type": "select",
  "properties": {
    "options": {
      "1": "Won",
      "0": "Lost or open"
    }
  }
}

Wenn der Benutzer „Won" wählt, ist es der Wert "1", der in die Abfrage fließt.

Die Beschriftungen werden von Flexie nicht lokalisiert. Was Sie eingeben, sieht der Benutzer. Verwenden Sie die Sprache, die Ihr Team verwendet.

Wie Benutzereingaben in Ihre Abfrage gelangen

Filterwerte werden in der Sitzung des Benutzers gespeichert, mit einem Schlüssel pro Bericht (flexie.report.{id}.filters). Sie überstehen Seitenneuladen und Paginierung für diesen Benutzer, und der CSV-Export verwendet dieselben Filter wie die aktuelle Ansicht.

Wo die Werte tatsächlich angewendet werden, hängt davon ab, wie Ihre Abfrage aufgebaut ist. Es gibt drei Modi, in der Reihenfolge, in der Flexie entscheidet.

Modus 1, {filters}-Platzhalter in der Abfrage

Das häufigste Muster. Sie setzen {filters} (oder {{filters}}, die beiden sind Synonyme) irgendwo in Ihre WHERE-Klausel, und Flexie ersetzt es durch ein fertig gebautes Fragment:

SELECT id, name, amount, stage_id, date_added
FROM   deals
WHERE  is_published = 1
  AND  {filters}
ORDER  BY date_added DESC

Mit der Eingabe des Benutzers, etwa Outcome = Won und Date Added From = 2026-01-01, expandiert das Token {filters} zu:

(`deals`.`is_won` = '1' AND `deals`.`date_added` >= '2026-01-01')

Wenn der Benutzer keine Filter gewählt hat, wird {filters} durch 1=1 ersetzt, eine Leeroperation, damit das umgebende AND nicht ins Leere läuft. Sie müssen kein bedingtes SQL schreiben.

Das Fragment wird vom eigenen Query-Builder von Flexie erstellt. Werte sind parametersicher (er parametrisiert und fügt sie dann inline ein), und Spaltennamen werden in Backticks gesetzt. Die vom Benutzer gewählten Operatoren werden berücksichtigt (contains wird zu LIKE '%...%', greater than wird zu >).

Modus 2, kein Platzhalter, kein manual-Flag

Wenn Ihre Abfrage kein {filters} enthält und das Filter-Array kein "manual" enthält, werden die Filterwerte trotzdem angewendet. Flexie übergibt sie im Hintergrund an die Datenschicht des Berichts, die Ihre Abfrage umschließt und dieselbe WHERE-Klausel darum herum ergänzt.

Das ist praktisch: Sie können jedem beliebigen Bericht ein Filters-JSON hinzufügen, ohne sein SQL anzufassen, und die Filter funktionieren einfach.

Der Kompromiss: Sie können nicht wählen, wo die Filter landen. Wenn Ihre Abfrage eine HAVING-Klausel, eine Unterabfrage oder einen JOIN hat, bei dem der Filter gegen eine verknüpfte Tabelle gehört, bevorzugen Sie Modus 1, damit Sie die Platzierung steuern.

Modus 3, manual-Modus: Filterwerte als Scripting-Variablen

Wenn das Filter-Array den String "manual" enthält:

[
  { "alias": "year", "label": "Year", "type": "number" },
  { "alias": "owner_id", "label": "Owner", "type": "select",
    "properties": { "options": { "12": "John D.", "13": "Jon D." } } },
  "manual"
]

generiert Flexie kein WHERE-Fragment. Stattdessen werden die Eingaben des Benutzers als Flexie-Scripting-Variablen im SQL bereitgestellt, jeweils unter ihrem alias als Schlüssel, und das automatisch generierte SQL-Fragment wird ebenfalls als Variable __sql_filters bereitgestellt.

Sie verweben die Werte dann selbst in die Abfrage:

SELECT id, name, amount
FROM   deals
WHERE  YEAR(date_added) = {{ year.input | number_format(0, '', '') }}
  AND  owner_id         = {{ owner_id.input }}
{% if amount_min %}
  AND  amount >= {{ amount_min.input }}
{% endif %}
ORDER  BY amount DESC

Wie jede Variable aussieht:

{{ year       | json_encode | raw }}
{# →  {"operator":"eq","input":"2026"}                          #}

{{ owner_id   | json_encode | raw }}
{# →  {"operator":"eq","input":"12"}                            #}

{{ __sql_filters }}
{# →  (`year` = '2026' AND `owner_id` = '12')                   #}

Verwenden Sie .input, um den vom Benutzer getippten Wert zu erhalten. Verwenden Sie das ganze Objekt, wenn Ihnen auch der vom Benutzer gewählte Operator wichtig ist. Verwenden Sie __sql_filters, wenn Sie dieselbe WHERE-Klausel möchten, die Flexie gebaut hätte, praktisch für „nutze den automatisch gebauten Filter als Unterklausel und füge meine eigenen Bedingungen daneben hinzu".

Der manuelle Modus gibt Ihnen volle Kontrolle, macht den Filter aber zu Ihrer Sache. Sie entscheiden, wie jeder Wert verwendet wird, einschließlich der Behandlung leerer Eingaben. Umschließen Sie jede Filterverwendung mit {% if ... %}, damit leere Felder das SQL nicht kaputtmachen.

Die Filterdefinition selbst mit Flexie Scripting steuern

Das ist der mächtige, leicht zu übersehende Teil. Das Filter-JSON wird durch Flexie Scripting gerendert, bevor es geparst wird. Überall im JSON können Sie {{ ... }}-Ausdrücke schreiben, und sie werden in dem Moment, in dem der Bericht geöffnet wird, durch ihre Ergebnisse ersetzt. Die kombinierte Ausgabe muss dann gültiges JSON sein, damit der Parser sie akzeptiert.

Was das ermöglicht:

  • Ein Dropdown, dessen Optionen aus einer Live-Abfrage stammen. Wählen Sie aus Ihren tatsächlichen Nutzern, Phasen, Status, Produkten, Typen Eigener Datensätze.
  • Filter, die die Identität des aktuellen Nutzers in ihren Beschriftungen enthalten.
  • Bedingte Filter. Bestimmte Filter nur rendern, wenn der Betrachter ein Recht hat, oder nur, wenn bestimmte Daten vorhanden sind.
  • Datums-Standardwerte, die sich mit der Zeit aktualisieren. „Von: letzte 30 Tage" als Standard.

Muster 1, dynamische select-Optionen aus einer Abfrage

Bauen Sie die Optionen-Map, indem Sie über Zeilen iterieren. Die Ausgabe zwischen den Klammern muss nach dem Rendern ein JSON-Objekt sein, geben Sie also "key": "value"-Paare mit Kommas dazwischen aus:

[
  {
    "alias": "owner_id",
    "label": "Owner",
    "type": "select",
    "properties": {
      "options": {
        {% for u in query("SELECT id, first_name, last_name FROM users WHERE is_published = 1 ORDER BY first_name LIMIT 200") %}
          "{{ u.id }}": "{{ u.first_name }} {{ u.last_name }}"{% if not loop.last %},{% endif %}
        {% endfor %}
      }
    }
  }
]

Nachdem das Skript gelaufen ist, wird das gespeicherte JSON effektiv zu:

[
  {
    "alias": "owner_id",
    "label": "Owner",
    "type": "select",
    "properties": {
      "options": {
        "12": "John Doe",
        "13": "Jon Doe",
        "14": "Jane Doe"
      }
    }
  }
]

…was der JSON-Parser akzeptiert. Der Betrachter öffnet den Bericht und sieht ein Dropdown echter Eigentümer, das jedes Mal aktualisiert wird, wenn er den Bericht öffnet.

Halten Sie das LIMIT vernünftig. Das Dropdown wird bei jedem Öffnen gebaut; Tausende von Optionen sind unbrauchbar, sowohl für die Performance als auch für die Person, die die Liste durchsucht. Bauen Sie einen text-Filter gegen eine indizierte Spalte, wenn Ihr Wertebereich groß ist.

Muster 2, Status-Dropdown gesteuert durch eine kleine Entitäts-Suche

Für Status, Pipeline-Phasen, Typen Eigener Datensätze, alles, was Sie nicht von Hand pflegen:

[
  {
    "alias": "stage_id",
    "label": "Pipeline Stage",
    "type": "select",
    "properties": {
      "options": {
        {% for s in query("SELECT id, name FROM stages WHERE is_published = 1 ORDER BY ordering_no") %}
          "{{ s.id }}": "{{ s.name }}"{% if not loop.last %},{% endif %}
        {% endfor %}
      }
    }
  }
]

Muster 3, einen Datumsfilter standardmäßig auf „letzte 30 Tage" setzen

Das Filter-JSON definiert nur die Steuerelemente; Standardwerte sind nicht Teil des Schemas. Aber Sie können dynamische Beschriftungen einsetzen:

[
  {
    "alias": "date_added",
    "label": "Created (defaults to last 30 days, clear to widen)",
    "type": "datetime"
  }
]

Kombinieren Sie es mit dem manual-Modus, wenn der Standardwert tatsächlich greifen soll:

SELECT id, name, date_added
FROM   leads
WHERE  date_added >= '{% if date_added.from %}{{ date_added.from }}{% else %}{{ dateAdd(now(), -30, "days", "Y-m-d") }}{% endif %}'
  {% if date_added.to %}
    AND date_added <= '{{ date_added.to }}'
  {% endif %}
ORDER BY date_added DESC

Muster 4, einen Filter ausblenden, sofern der Betrachter die Daten nicht hat

[
  {% set has_quotes = query("SELECT COUNT(*) AS n FROM quotes LIMIT 1")[0].n %}
  {% if has_quotes > 0 %}
  {
    "alias": "quote_status",
    "label": "Quote Status",
    "type": "select",
    "properties": {
      "options": { "draft": "Draft", "sent": "Sent", "accepted": "Accepted" }
    }
  }
  {% endif %}
]

Achten Sie auf die JSON-Kommas, wenn Sie {% if %} zwischen Filterobjekten verwenden. Tipp zum Pre-Rendering: öffnen Sie die Bearbeitungsseite, beobachten Sie die Live-Ansicht und prüfen Sie im Browser die Seitenquelle des Filter-HTML. Was durchkommt, ist das JSON nachdem das Scripting gerendert hat.

Was Sie aus dem Filter-JSON heraus aufrufen können

Alles, was in Flexie Scripting verfügbar ist, derselbe sandboxed Funktionssatz, der überall sonst verwendet wird. Hier am nützlichsten:

  • query("SELECT ..."), für Optionslisten aus Ihren eigenen Daten.
  • findOne(...), findMany(...), findCount(...), kleine Suchen nach Feld.
  • now(), date(...), dateAdd(...), für datumsrelative Standardwerte und Beschriftungen.
  • getUserById(id), wenn Sie eine bestimmte Nutzer-id kennen und sie nachschlagen möchten.

Das Filter-JSON wird ohne Datensatzkontext gerendert. Es gibt kein „aktueller Betrachter"-Objekt, keine impliziten Globals. Wenn Sie eine Abfrage auf den aktuellen Betrachter eingrenzen müssen, dafür ist der {user_id}-Platzhalter da, verwendet in Ihrem SQL-Körper, nicht im Filter-JSON.

Dieselbe Zeilenobergrenze und dieselben Nur-Lese-Einschränkungen gelten wie überall sonst. Siehe Wo Scripting läuft und seine Grenzen.

Die Platzhalter-Familie in Abfragen

{filters} ist nicht das einzige Token, das Sie in eine Abfrage einsetzen können. Fünf Tokens werden ersetzt, bevor das SQL läuft (die Doppelklammer-Formen sind Synonyme):

Platzhalter Wird ersetzt durch
{user_id} / {{user_id}} Die id des angemeldeten Nutzers
{group_id} / {{group_id}} Dessen Gruppen-id (-1, falls keine)
{role_id} / {{role_id}} Dessen Rollen-id (-1, falls keine)
{timezone} / {{timezone}} Dessen Zeitzone (UTC, falls nicht gesetzt)
{filters} / {{filters}} Das generierte WHERE-Fragment, oder 1=1, falls keine Filter aktiv sind

Diese funktionieren in jedem Bericht, Datenraster oder HTML, und sie funktionieren in CTEs ebenso wie in der Hauptabfrage.

Wie Nutzer die Filtersteuerung verwenden

Wenn jemand den Bericht öffnet:

  • Über dem Raster erscheint eine Reihe von „+ Add filter"-Steuerelementen, eines pro definiertem Filter.
  • Das Auswählen eines Filters fügt ihn als Chip mit einem Operator-Dropdown (für text, number und select) und den Werteingaben hinzu.
  • Ein Klick auf Apply führt den Bericht mit den gewählten Werten aus.
  • Filter werden pro Nutzer, pro Bericht und über die Paginierung hinweg gemerkt, bis sie gelöscht werden.
  • Alle Filter zu löschen und anzuwenden, erzeugt das ungefilterte Ergebnis (wobei {filters} zu 1=1 aufgelöst wird).
  • Export to CSV verwendet dieselben aktiven Filter wie die aktuelle Ansicht.

Wenn der Nutzer den Bericht einen Tag später erneut öffnet, im selben Browser, mit demselben Login, ist sein letztes Filterset noch vorhanden.

Rezepte

Eigentümer-Dropdown, befüllt aus echten Nutzern

[
  {
    "alias": "owner_id",
    "label": "Owner",
    "type": "select",
    "properties": {
      "options": {
        {% for u in query("SELECT id, first_name, last_name FROM users WHERE is_published = 1 ORDER BY first_name LIMIT 200") %}
          "{{ u.id }}": "{{ u.first_name }} {{ u.last_name }}"{% if not loop.last %},{% endif %}
        {% endfor %}
      }
    }
  }
]

Muster „Nur ich". Ein Dropdown kann den aktuellen Betrachter nicht aus dem Filter-JSON heraus vorauswählen (hier ist kein Betrachter im Geltungsbereich). Was Sie tun können, ist einen separaten Bericht zu bauen, der über den {user_id}-SQL-Platzhalter fest auf den Betrachter eingrenzt, WHERE owner_id = {user_id}, und diesen als „Meine …"-Version des Berichts zu teilen.

Datumsbereich mit sinnvollem Standardwert im manual-Modus

[
  { "alias": "date_added", "label": "Date Added", "type": "datetime" },
  "manual"
]
SELECT id, name, amount, date_added
FROM   deals
WHERE  date_added >= '{{ date_added.from ?: dateAdd(now(), -30, "days", "Y-m-d") }}'
  {% if date_added.to %}
    AND date_added <= '{{ date_added.to }}'
  {% endif %}
ORDER BY date_added DESC

Vielseitiger Status-Filter, Optionen aus Daten

[
  {
    "alias": "stage_id",
    "label": "Stage",
    "type": "select",
    "properties": {
      "options": {
        {% for s in query("SELECT id, name FROM stages WHERE is_published = 1 ORDER BY ordering_no") %}
          "{{ s.id }}": "{{ s.name }}"{% if not loop.last %},{% endif %}
        {% endfor %}
      }
    }
  }
]

Text enthält, auf eine Spalte eingegrenzt

[
  { "alias": "email", "label": "Email contains", "type": "text" }
]
SELECT id, first_name, last_name, email
FROM   contacts
WHERE  is_published = 1
  AND  {filters}
ORDER  BY date_added DESC

Der Nutzer wählt contains (Standard für text), tippt acme, und das Token {filters} expandiert zu (`contacts`.`email` LIKE '%acme%').

Fallstricke

  • Ein select-Filter ohne properties.options lässt sich nicht speichern. Sie müssen die Map angeben.
  • Die Werte in der options-Map sind Strings, selbst für Integer-ids in der Datenbank. Flexie vergleicht sie als Strings gegen die Spalte; Vergleiche gegen Integer-Spalten funktionieren, weil die Datenbank castet.
  • select ist einwertig. Wenn Sie Mehrfachauswahl brauchen, bauen Sie einen text-Filter mit dem contains-Operator gegen eine CSV-Spalte, oder modellieren Sie die Werte als separate boolesche Flags.
  • {filters} löst sich zu 1=1 auf, wenn kein Filter aktiv ist. Gestalten Sie Ihre umgebende WHERE-Klausel so, dass sie damit weiterfunktioniert. AND {filters} ans Ende zu setzen, ist das sicherste Muster.
  • Im manual-Modus werden leere Eingaben nicht automatisch übersprungen. Ein Nutzer, der keinen Wert gewählt hat, bekommt trotzdem eine (leere) Variable injiziert. Sichern Sie jede Verwendung mit {% if ... %} ab, oder setzen Sie mit ?: einen Standardwert.
  • Die Filter-Aliasse müssen mit Ihren Spaltennamen übereinstimmen. Ein Tippfehler hier scheitert nicht an der Validierung, er erzeugt einfach einen SQL-Fehler beim Betrachten.
  • Vermeiden Sie LIMIT auf den datenliefernden query(...)-Aufrufen im Filter-JSON, die einige Hundert übersteigen. Dropdowns sind keine Listen, durch die man scrollt, und die Abfrage läuft bei jedem Öffnen des Berichts.

Wozu das gehört

  • Datenraster-Berichte: wo der {filters}-Platzhalter am häufigsten verwendet wird.
  • HTML-Berichte: Filter funktionieren auch für HTML-Berichte. Der Unterschied ist, dass Sie entscheiden, wo die Werte erscheinen (die Filterwerte stehen Ihrer Vorlage als Scripting-Variablen zur Verfügung, im manuellen Modus, oder sind über dasselbe {filters}-Token bereits ins SQL eingebacken).
  • Flexie Scripting: die Sprache, die sowohl im Filter-JSON selbst als auch in einem SQL-Körper im manuellen Modus verwendet wird.
  • Berichte in Workflows und Dashboards: wenn ein Bericht ein Dashboard-Widget speist, können die Filter des Berichts für den Widget-Betrachter voreingestellt werden; Filterbehandlung und Persistenz gelten weiterhin.

Nächste Schritte