Planning Board
The planning board puts rows and time side by side: employees, rooms, machines or projects on the left, a time axis at the top, and entries as bars, points or cells in between. It shows who is present when, what is booked when, and where minimum staffing is not met. All data comes from SQL queries; every reaction to a click is a configured action.
Typical Use Cases
| Use case | Rows | Entries |
|---|---|---|
| Absence planner | Employees by team | Vacation, sick leave, training, home office |
| Part-time and shift plan | Employees | Shifts, days off |
| Room, vehicle, machine booking | Rooms, vehicles, machines | Bookings, maintenance windows |
| Project and phase plan | Projects | Phases, milestones |
| Inspection deadlines | Equipment, contracts | Due dates as points |
Common Stumbling Blocks
These five points cause the most confusion when setting up a board.
1. Entering means selecting days
A click on an empty day selects that day; dragging with the mouse button pressed selects several days of a row. Then a popover opens with the intents offered "on empty days (after selecting)". The popover shows the selected period like the card of an entry, with the last day as the end. While the popover is open, the next click on an empty day only closes it.
- Selecting is only possible if at least one layer has such an intent.
- On the week and month scale, whole weeks or months are selected.
- Without selecting, entries are made with a button on the row or in the toolbar, see Entering an absence.
- With the row source "Derive from the entries", a person without an entry in the selected period has no row – nothing can be selected for them. A rows query (for example from the sample schema "Employees") shows everybody.
2. "End" does not mean the same everywhere
| Place | "End" is |
|---|---|
| Layer with End column names the last day (inclusive) on | the last day |
| Layer with this switch off | the day after |
| Query for blocked and special periods, Markers from SQL | always the last day |
| Static markers on the "Markers" tab (End (exclusive)) | the day after |
$(entry.end) and $(range.end) in questions, messages and URLs |
the day after |
| Prefill End (last day) or End (day after) | as named |
For absences with "from" and "to" date fields, this almost always fits: layer with "inclusive" on, prefill "End (last day)".
3. One faulty value hides the whole board
Instead of the board, a list of error messages appears. Typical causes:
- The same record appears twice – for example because a JOIN duplicates rows or two layers read the same records.
- A quantity is negative.
- With timestamps, the end lies before or on the start.
- Two layers or two markers have the same id.
- A static period on the "Markers" tab does not end after its start. Add period first creates it exactly like that (start and end today) – so adjust the end right away.
- "Shades of the base color" or "fixed layer color" is selected, but no base color is set.
4. "Hide" does not remove an entry
A hidden status (such as "Abgelehnt", rejected) or a hidden category is only not drawn. The entry still counts as absent and still triggers hints on minimum presence, overlaps and substitutes. In addition, every entry of a layer with a capacity effect above 0 counts as an absence regardless of its status – including "Beantragt" (requested).
Whatever should not count is therefore filtered out in the layer query, for example with AND COALESCE(status, '') NOT IN ('Abgelehnt', 'Storniert'), or goes into a layer of its own with capacity effect 0. Only a click in the legend also takes entries out of the count – and only for the person who clicked.
5. Entering needs a prepared process
The board itself never creates a record. New entries are only created through the step Start process: it needs a deployed process whose start form contains the prefilled fields under exactly these technical names and which saves the record the layer query reads. Only then does the entry appear on the board. The procedure step by step is described under Entering an absence.
Also important: the severity Blocking currently prevents nothing. A move is saved anyway, even if it violates a blocking rule and the hint in the dialog says otherwise.
More stumbling blocks
- Row keys must match exactly, including upper and lower case. Entries whose key matches no row are not drawn. The safest way is to write the key with
lower(...)in both the rows query and the layer query. - "Only in the user's own row" compares the row id exactly with the e-mail address the person signs in with.
- Moving with the mouse needs Field for start (and for spans Field for end) in the layer's "Behavior" sub-tab, plus Record id and Data schema id in the column mapping. A separate "Move" action is not needed, even though the hint in the dialog mentions one. Entries are moved in time only, never to another row.
- Saved view: The period and scale a person has chosen stay stored in their browser and take precedence over a default changed later.
- Date comparisons in the queries: Never compare date columns with plain text; use
CAST(@periodStart AS date)as in the examples. More under SQL in Widgets.
First Steps
The board needs at least one layer: an SQL query that returns one row per bar, with columns for the row (who or what), the start and the end. The rows of the board come either from a query of their own or are derived from the entries. After that, the dialog only needs to know which column is which.
Quickest way with the sample schemas:
- Add the "Planning board" widget to the dashboard and choose Configure planning board.
- Rows tab: Create sample schema “Employees”. In the dialog, check Schema name and Technical name (suggestion
mitarbeiter; this is also the SQL table name) and choose Create. The rows query and column mapping are filled in. - Layers tab: Create sample schema “Absences”, select the schema just created as Employees schema and choose Create. A layer with query and column mapping is created, together with the look of the categories (Krank red, Homeoffice outline only) and statuses (Beantragt hatched, Genehmigt solid, Abgelehnt hidden), the intent "Stornieren" (cancel) and moving with the mouse into the fields
erster_tagandletzter_tag. - Test run, Apply and publish the dashboard.
- Enter employees and absences in the database. The fields of the sample schemas are listed under Sample schemas.
Rejected absences are only hidden in this layer and still count as absent (see stumbling blocks). If they should not count, add AND COALESCE(a.status, '') <> 'Abgelehnt' to the layer query.
Existing data schemas for people or absences are connected with their own query and column mapping, see Rows and Layers.
Every tab of the dialog has an info button that opens the matching section of this page.
Use Case: Entering an Absence
Goal: in a person's row, the mouse is dragged over the desired days, a form opens with the person, first and last day already filled in, and after submitting, the absence appears as a bar.
What must exist first
- A data schema for the absences, in the example
urlaubsantraegewith the fieldsantragsteller(e-mail address of the person),vonandbis(date, from and to),art(type, e.g. Urlaub, Krank),statusandbemerkung(note). - A deployed process, for example "Enter absence", with a start event that has a form. The start form contains fields with exactly the technical names
antragsteller,von,bisandart, and the process saves a record intourlaubsantraegeon submit. The board only offers start events of deployed processes. - A layer that reads these records: kind "Span", temporal mode "Date", End column names the last day (inclusive) on, with this query:
SELECT id,
data_schema_id,
updated_at,
lower(antragsteller) AS row_key,
von AS start_day,
COALESCE(bis, von) AS end_day,
art AS category,
COALESCE(status, 'Beantragt') AS status,
bemerkung AS label
FROM urlaubsantraege
WHERE antragsteller IS NOT NULL AND antragsteller <> ''
AND COALESCE(status, '') NOT IN ('Abgelehnt', 'Storniert')
AND von < CAST(@periodEnd AS date)
AND COALESCE(bis, von) >= CAST(@periodStart AS date)
Column mapping: Row (link) → row_key, Start → start_day, End → end_day, Category → category, Status → status, Label → label, Record id → id, Data schema id → data_schema_id, Updated at → updated_at.
With the row source "Derive from the entries", only people who already have an entry in the period appear. So that everybody has a row, the rows come from a rows query whose row id is also the lower-case e-mail address, for example lower(COALESCE(email, id)) AS id from the sample schema "Employees".
Setting up the intent
- Configure planning board → Layers tab → the layer → Actions sub-tab.
- Add intent.
- Label: "Enter absence"; Button: highlighted; Icon:
event_available. - Offered: "on empty days (after selecting)".
- Optionally, under Only when, the Condition (expert)
range.workdays > 0 ? true : 'No working days selected'– if only weekends or holidays are selected, the button is then disabled with this reason. - Under Then → Add step: Step "Start process", Process the start event of the absence process, Open as "Dialog". Prefilling only works in the dialog, not in a new tab.
- Under Prefill, choose Add prefill four times and fill it in as shown in the table below.
- Apply and publish the dashboard.
| Target field | Value from | Fixed value |
|---|---|---|
antragsteller |
Row (id, e.g. e-mail) | |
von |
Start | |
bis |
End (last day) | |
art |
Fixed value (with $(…) placeholders) | Urlaub |
The Target field is always the technical name of the field in the start form – not the name in the query (start_day) and not the field name in the data schema, if it differs.
Entering on the dashboard
- In the person's row, click an empty day or drag the mouse over the desired days.
- A popover shows the period (first to last selected day), the number of days and working days and the Enter absence button. The form receives the last day because the prefill uses "End (last day)".
- The button opens the start form with person, first and last day and type.
- Check and submit the form. As soon as the process has saved the record, the bar appears the next time the board loads. If the process saves the record only after an approval, the entry appears only then. How to make the entry visible at once is described under Visible right after sending.
Visible right after sending
Without further settings, the board shows a new entry only the next time it loads. Two settings on the "Start process" step (only with Open as "Dialog") make it visible at once:
- Signal for reloading: name of a "throw signal" intermediate event in the process, right after the step that saves the record, e.g.
gespeichert. When the signal arrives, the board reloads at once, and so do the other widgets of the dashboard. Without a signal it reloads when the run ends, after 30 seconds at the latest. Only with signal value: only a signal with this value counts; empty = any value. So that the entry is already there when the board reloads, the database step should save synchronously. -
Show provisional bar: right after sending, a grey striped bar with the text "Saving …" appears in the row over the days from the form. It counts nowhere (staffing, notes, totals) and cannot be clicked or moved. As soon as the real entry is loaded, it takes the bar's place; at the latest with the reload after the signal or the end of the run, the grey bar is gone. While it is shown, the legend also lists "Saving …".
- What the bar shows (only for buttons on an entry): "Show the new entry" as above, or "Hide the clicked entry" for "Cancel" or "Delete" through a process. Then the clicked entry turns into a grey striped bar with the text "Being removed …" right after sending. It counts nowhere any more and offers no actions. After the reload (signal or end of the run, after 30 seconds at the latest) it is gone, or back as before if the process kept the entry, for example because the cancellation was declined. Layer, start, end and row are not needed for this.
- Text on the bar: your own text instead of "Saving …" or "Being removed …", e.g. "Requesting leave …" or "Cancelling …".
- Layer of the entry (only for buttons on the row and in the toolbar): the layer on which the real entry appears; "First bar layer" takes the first layer with bars. For intents on a layer it is always that layer.
- Field for start, Field for end, Field for the row: technical names in the start form. Empty = the fields from the prefill ("Start", "End (last day)", "Row (id, e.g. e-mail)"); they are shown in grey in the empty field. Without a field for the row, the bar appears in the clicked row, otherwise in the row of the signed-in user.
The bar shows the days sent in the form, even if the user changed them there. Without a start in the form and without selected days, no bar appears.
Entering single days
A click on an empty day selects exactly that day; the popover offers the same buttons as after dragging. Without selecting, two more intents with the same "Start process" step do it:
- Button on the row: Rows tab → What can be done with a row → Add intent, label "Enter absence for this person", prefill
antragsteller← "Row (id, e.g. e-mail)". A click on the person's name opens the row card with this button; first and last day are entered in the form. - Button in the toolbar: Display tab → Buttons in the toolbar → Add intent, label "Request absence", prefill
antragsteller← "Signed-in user (e-mail)". This also works for people who do not have a row yet.
Optional: cancelling and moving
- Cancelling: intent "Cancel", Offered "on an entry (card and right-click)", Statuses (comma list)
Beantragt, Genehmigt, Only in the user's own row on, question "Really cancel the absence starting $(entry.start)?", step Set field with Field namestatusand ValueStorniert. Because the query filters out cancelled entries, the bar disappears the next time the board loads. - Cancelling through a process: if the cancellation can be checked or declined, use the step Start process with the cancellation process instead of Set field. With Show provisional bar, What the bar shows "Hide the clicked entry" and the text "Cancelling …", the bar turns grey at once and no longer counts. If the process declines, it is back as before after the reload.
- Moving: layer's Behavior sub-tab, Move and Resize on, Field for start
von, Field for endbis, Locked for status (comma list)Genehmigt. Moving writesvonandbisdirectly into the record, without the process.
Building Blocks
- Rows: an SQL query with row id, display name, optionally subtitle, group, subgroup, sort order, capacity, workdays and state. Rows appear even without entries.
- Layers: one query of entries per layer. Each layer defines whether it contains spans or points, dates or timestamps, whether the end column means the last day, and what an empty end means. If a setting is missing, the board shows the error instead of a guessed picture.
- Availability: working days per row (a column such as
1,2,3,4,5), public holidays from the account's work calendar or per state, optionally a query for blocked and special periods. - Style rules: field plus value gives a color, a pattern (solid, hatched, dashed, dotted, striped, outline), a code for the cells view, or hiding. Example: status "Beantragt" hatched, "Abgelehnt" hidden.
- Groups: frames around rows with "present/total" per day or as a heatmap.
- Result columns: values from the rows query to the right of the board, for example vacation taken, remaining vacation as a progress bar, status as a badge. The database computes the sums; the board displays them.
- Rules: minimum presence per group (people, percent or full-time equivalents), overlaps, absent substitutes, plus own hints from SQL. Every hint has a severity and a sentence.
- Markers: dates and periods without a row, such as a trade fair, a release or company holidays.
- Intents: what the user can do, in their words ("Request vacation", "Cancel"), configured on the layer, the rows or the board; behind them, steps such as starting a process with a prefilled form, setting a field, deleting a record, publishing on a channel or opening a URL. A click on an entry opens a card with facts and the matching intents.
Configuration
The dialog has seven tabs: Rows, Layers, Display, Groups & rules, Result columns, Style rules and Markers. Inside, each layer has the sub-tabs Data, Look, Behavior and Actions. For all queries: Test run runs the query with five rows and shows the columns found; the mapping fields then offer these columns. The placeholders work as in all SQL widgets.
Rows
Source of the rows: SQL query (default; one row per employee, room, machine) or Derive from the entries (every row key of the layers becomes a row, without groups and capacity).
Column mapping of the rows query (in brackets the technical key as it appears in the saved configuration):
| Mapping | Required | Meaning |
|---|---|---|
Row id (id) |
yes | Row key. The layers refer to exactly this value with their "Row (link)" column, for example the user's e-mail. |
Display name (label) |
yes | Name in the name column |
Subtitle (sublabel) |
such as role or e-mail | |
Group (id), Group (name) (groupId, groupLabel) |
Group (team, department, building) | |
Subgroup (id), Subgroup (name) (subgroupId, subgroupLabel) |
Subgroup | |
Sort order (sortOrder) |
Sort order within the group | |
Capacity (FTE) (capacity) |
Job share 0 to 1 for presence in full-time equivalents | |
Workdays (1,2,4 or Mo,Tu; 0 or 7 = Sunday; empty = workdays of the board) (workdays) |
Workdays as numbers 1,2,4 (1 = Monday … 6 = Saturday, 0 or 7 = Sunday) or day names monday,tuesday, Mo,Di, as a comma list or array; other days turn grey. Empty = the board's workdays from the "Groups & rules" tab |
|
State (holidays per row, e.g. BY) (region) |
German state code (BW, BY, …) for the public holidays of this row if it differs from the account. Empty = the board's "Holidays" setting |
The switch Show initials as avatar (default on) puts a circle with the row's initials in front of the name.
Further columns of the query are available as result columns.
Insert example offers the query for the employees from the sample schema (table mitarbeiter):
SELECT COALESCE(email, id) AS id,
name AS label,
email AS sublabel,
team AS group_id,
team AS group_label,
array_join(arbeitstage, ',') AS workdays,
bundesland AS region,
fte AS capacity
FROM mitarbeiter
WHERE COALESCE(aktiv, true) = true
ORDER BY team, name
Mapping: Row id → id, Display name → label, Subtitle → sublabel, Group (id) → group_id, Group (name) → group_label, Workdays → workdays, State → region, Capacity (FTE) → capacity.
Availability on the same tab: the Holidays setting defines where days off come from – Work calendar of the account (public and regional holidays plus company days off from Administration → System → Work calendar; half days as half availability; default for new boards), State of the board (built-in rules) (only the public holidays of the state selected in the widget; boards created before the work calendar keep this setting) or No holidays. Rows with their own value in State get the public holidays of their state if it differs. Holidays are computed by rules (fixed days plus the Easter formula, rules per state with validity years), without maintaining any data. In addition, an optional availability query for blocked and special periods with the mapping Row (empty = all rows), Start, End (last day), State (unavailable, partial, available), Capacity (0 to 1 for partial) and Label. The button Example: company holidays inserts this query for all rows:
SELECT CAST(NULL AS varchar) AS row_key,
CAST(concat(CAST(year(current_date) AS varchar), '-12-24') AS date) AS start_day,
CAST(concat(CAST(year(current_date) AS varchar), '-12-31') AS date) AS end_day,
'unavailable' AS state,
'Betriebsferien' AS label
Layers
Each layer has four required settings that leave nothing to guesswork:
| Setting | Values | Meaning |
|---|---|---|
| Kind | Span, Point | Bar with start and end, or a point |
| Temporal mode | Date, Timestamp | Date as YYYY-MM-DD or a timestamp with time |
| End column names the last day (inclusive) | on, off | Only for span + date: the end column names the last day (on) or the day after (off) |
| Empty end means | Error (not allowed), runs until now, runs to the edge | Only for spans: what applies when the end column is empty |
In addition Color from: "Shades of the base color" (default: every category from the data automatically gets a stable shade), "Category (palette)" (every category its own strong color), "fixed layer color" or "style rules only". The Base color is chosen from named colors: None (default), Blue, Teal, Green, Orange, Red, Violet, Pink, Slate or "Custom color…". Below that two tables: Categories sets individual values (Krank red, Homeoffice outline only), Statuses sets the pattern per status value (Beantragt dotted, Genehmigt solid, Abgelehnt hidden). A layer created with Add layer starts with Beantragt dotted, the example "Sample schema Absences" sets Beantragt hatched. Everything not listed there stays automatic; with "Distinguish unknown statuses automatically", unlisted statuses also get a fixed pattern based on their name. Order: base color → category → status → style rules; each step changes only what it sets. Also here: the Capacity effect (0–1) (1 = the entry makes the row absent, 0 = no effect, for example working time) and the Unit of the amount.
Column mapping of the layer query (in brackets the technical key):
| Mapping | Required | Meaning |
|---|---|---|
Row (link) (row) |
yes | Row key, must match the row id of the rows |
Start (start) |
yes | Date or timestamp |
End (end) |
for spans | Date or timestamp; with "End column names the last day (inclusive)" the last day |
Day part (am/pm/full) (dayPart) |
am, pm or full for half days (span + date only) |
|
Amount (quantity) |
Amount in the layer's unit, for example days or hours counted | |
Category (category) |
Category for color and legend | |
Status (status) |
Status for style rules, locks and conditions | |
Label (label) |
Text on the bar | |
Linked row (substitute) (linkRow) |
Row key of the substitute; the rule "Check substitutes" warns if they are absent themselves. If the substitute is not on the board (for example from another team), the card still shows them, but they are not checked | |
Record id, Data schema id (id, dataSchemaId) |
for actions | so that "Open record", "Set field" and moving work |
Updated at (updatedAt) |
Time of change, used to match the data after writes |
Below the mapping is the field Additional columns on the card: a comma list of technical column names of the query whose values appear in the tooltip and on the card of the entry.
Timestamps arrive as ISO text with a time zone (2026-10-09T08:00:00+02:00) or as an Athena timestamp, which is read as UTC.
Insert example offers two queries. Absences from the sample schema (span, date, "End column names the last day (inclusive)" on; tables abwesenheiten and mitarbeiter; the reference fields contain the id of the employee record):
SELECT a.id,
a.data_schema_id,
a.updated_at,
COALESCE(m.email, m.id) AS row_key,
a.erster_tag AS start_day,
a.letzter_tag AS end_day,
CASE WHEN a.erster_tag = a.letzter_tag AND a.erster_tag_vollhalb = 'Halb' THEN 'am' ELSE 'full' END AS day_part,
a.kategorie AS category,
a.status AS status,
a.tage AS quantity,
a.bemerkung AS label,
COALESCE(v.email, v.id) AS link_row
FROM abwesenheiten a
LEFT JOIN mitarbeiter m ON m.id = a.mitarbeiter
LEFT JOIN mitarbeiter v ON v.id = a.vertretung
WHERE a.erster_tag < CAST(@periodEnd AS date)
AND a.letzter_tag >= CAST(@periodStart AS date)
AND COALESCE(a.status, '') <> 'Storniert'
Mapping: Row (link) → row_key, Start → start_day, End → end_day, Day part → day_part, Category → category, Status → status, Amount → quantity, Label → label, Linked row (substitute) → link_row, Record id → id, Data schema id → data_schema_id, Updated at → updated_at. The example also fills the layer's Categories table (Krank red, Homeoffice outline only) and Statuses table (Beantragt hatched, Genehmigt solid, Abgelehnt hidden), adds the intent "Stornieren" (cancel) and, on the "Behavior" sub-tab, moving and resizing into the fields erster_tag and letzter_tag, locked for "Genehmigt" and "Storniert". The values of the sample schema are German: Beantragt (requested), Genehmigt (approved), Abgelehnt (rejected), Storniert (cancelled), Krank (sick).
Time entries from the "Time tracking" app (span, timestamp, capacity effect 0, unit hours). Date and time are combined into a timestamp in Europe/Berlin:
SELECT id,
data_schema_id,
updated_at,
app_mitarbeiter AS row_key,
to_iso8601(with_timezone(date_parse(concat(CAST(app_datum AS varchar), ' ', app_arbeitsbeginn), '%Y-%m-%d %H:%i'), 'Europe/Berlin')) AS start_at,
to_iso8601(with_timezone(date_parse(concat(CAST(app_datum AS varchar), ' ', app_arbeitsende), '%Y-%m-%d %H:%i'), 'Europe/Berlin')) AS end_at,
app_kategorie AS category,
app_arbeitsstunden AS quantity,
app_bemerkung AS label
FROM app_time_entries
WHERE app_datum < CAST(@periodEnd AS date)
AND app_datum >= CAST(@periodStart AS date)
Here date_parse is correct: working start and end are stored as text (08:00) and are combined with the date into a text first. date_parse never belongs on a date field itself, see SQL in Widgets.
The board's period filter loads only the visible range, so the condition with @periodStart and @periodEnd belongs in every layer query.
Display
The dialog shows theme, scales and periods with their technical values, not translated: theme light, soft, midnight, contrast, pantarey or auto (following the system setting); scales hour, day, week, month; periods today … all.
| Setting | Values (default) |
|---|---|
| Display | Bars | Cells (day scale); the cells view shows a code per day on the day scale |
| Density | compact | comfortable (default) | spacious |
| Theme | light, soft, midnight, contrast, pantarey, auto |
| Legend | bottom (default) | top | none |
| Hints below the board | none | compact (default) | all; a click on a hint jumps to the day and the row |
| Default scale | one of the scales |
| Scales (chips) | default day, week, month; hour = hour scale, only available via chip |
| Sub-rows per row | 1–6 (default 3) |
| Width of the name column (px) | 120–480 (empty = 240) |
| Show period filter | switch |
| Default period | default quarter |
| Offered periods (chips) | default month, quarter, year |
| Calendar selection, Month selection, Year selection | switches |
| Week numbers, Now line, Minimap, Totals row, Headcount present/total | switches, all on |
| Full-time equivalents, Zebra rows | switches, off |
| Search field, Context menu (right-click) | switches, on |
| Snap for timestamps (minutes) | 1–240 (default 15) |
| Buttons in the toolbar | intents for the whole board, see Actions |
Moving and resizing with the mouse are not set here but per layer on the Behavior sub-tab under "Change period with the mouse": Move, Resize, Field for start, Field for end and Locked for status (comma list).
Groups and Rules
Groups come from the rows query. Frame rows by group (on) draws the frame around the rows of a group; Group row per day shows nothing, present/total (default) or a heatmap; Order of the groups is as in the data or alphabetical. Workdays of the board: tick Mo–Su, default Mo–Fr.
Rules: Check minimum presence (off) with Value (default 2), Unit (People | Percent | FTE), Severity (Info | Warning | Blocking) and Scope (per group | whole board); Check overlaps (on, Info) within a row, with the chips Only layers optionally only for certain layers; Check substitutes (on, Warning) reports a substitute who is absent themselves. "Blocking" currently only highlights and does not prevent a move.
Own hints from SQL
Besides the built-in rules, the board can show hints from a query of your own, for example days with more than 10 hours or missing breaks. Every row of the result becomes a hint: in the counter at the top and in the list under the board, with Jump to the date.
This is set in the Groups & rules tab under Own hints from SQL: enter the query, Test run, then map the columns.
| Column | Required | Meaning |
|---|---|---|
| Message | yes | text of the hint |
| Severity | info, warning or blocking (also Hinweis, Warnung, Blockierend); otherwise warning |
|
| Row | row key as in the rows query; hints for rows that are not on the board are left out. With row and start, the day is also colored in the group row | |
| Start | first day (date or timestamp) | |
| End | last day; empty = only the start day | |
| Entry | id of an entry (as in the layer's Id column): its bar gets the hint's color, the text is in the tooltip | |
| Id | optional, a fixed key of the hint |
Example with the time entries of the "Time tracking" app: one hint per person and day with more than 10 hours. Mapping: Message message, Severity severity, Row row_key, Start day.
SELECT app_mitarbeiter AS row_key,
app_datum AS day,
'More than 10 hours worked: ' || CAST(ROUND(SUM(app_arbeitsstunden), 1) AS varchar) || ' h' AS message,
'warning' AS severity
FROM app_time_entries
WHERE app_datum < CAST(@periodEnd AS date)
AND app_datum >= CAST(@periodStart AS date)
GROUP BY app_mitarbeiter, app_datum
HAVING SUM(app_arbeitsstunden) > 10
The message appears in the list exactly as the query delivers it. To include the person's name, fetch it in the query, for example with a JOIN on the table of the rows. Like all queries of the board, this one runs with the rights of the person viewing the dashboard.
Result Columns
Per column: Id, Header, Column of the rows query (required), Maximum (for progress) as a further column, Format (Number | Progress | Badge | Text), Unit, Decimals (0–3, default 1), Totals (none | sum rows), Width (px) (60–300, default 110). Values such as "vacation taken this year" are computed by the rows query, for example with SUM(...) FILTER (WHERE ...) and a LEFT JOIN on the absences.
Style Rules
An expert tool for everything that cannot be set on the layer, for example "all entries with note X hatched". Rules apply after the layer's base color, category and status, in order; later ones override earlier ones. The field is category, status, layerId, label or a column of the query; the value a comma list or *. Effect: color, pattern, code for the cells view, opacity, hide. Optionally only for certain layers.
Markers
Points in time and periods with label and color, drawn across the whole board without a row. Static entries are created in the dialog with Add point in time (field Date) or Add period (fields Start and End (exclusive)), each with a Color; in addition, an optional query Markers from SQL with the columns start (date, required), end (last day; filled gives a period, empty a point), label, color and id, for example from a data schema "Dates". Both sources are drawn together.
Actions
What a user can do is called an intent on the board and is configured where it belongs:
- On the layer (the layer's "Actions" sub-tab): intents on an entry ("Cancel", "Approve") and on empty days ("Request vacation" after selecting empty cells).
- With the rows: intents when clicking a name ("Show remaining vacation").
- In the display: buttons for the whole board in the toolbar.
Every intent has a label in the user's words, an icon, a Button (neutral, highlighted, green (confirming), red (attention)), conditions (only for certain statuses, only in the user's own row, only for user groups, optionally a JSONata condition), optionally a question with its own title, text (default "Really execute?") and buttons (each button proceeds or cancels; its value is available to the steps as $(choice)), a success message (default "Done.") with Display as a short message at the bottom or a dialog with OK, and a list of steps that run one after another:
| Step | Effect |
|---|---|
| Start process | opens the start form of a start event of a deployed process in a dialog, with Prefill: target field ← Row (id, e.g. e-mail), Row (display name), Start, End (last day), End (day after), Record id, Category, Status, Signed-in user (e-mail) or fixed value; optionally Signal for reloading and Show provisional bar (Visible right after sending) |
| Set field | writes a fixed value into a field of the record, such as status "Storniert" |
| Open record | Card with facts and intents or Record in the database (only for the latter: In new tab); users who do not have the permission to open the database get the card instead, and the card leaves out an intent that only opens the database |
| Delete record | after a confirmation, cannot be undone |
| Publish on channel | sends the entry or the selection to other widgets of the dashboard |
| Open URL | opens a URL with placeholders such as $(entry.id), $(row.id), $(range.start) |
| Refresh | reloads all widgets of the dashboard, not only the board |
If a step aborts (cancel in the form, error), the following steps do not run. Moving and resizing with the mouse are not intents; they are set on the layer's "Behavior" sub-tab with the fields written to and the locked statuses.
The card. A click on an entry always opens the same card: the facts in plain words at the top (row, layer, period, category, status, amount, substitute, additional columns), below them the intents that currently apply to this entry as buttons, and the link "Open in the database". Only users with the permission to open the database see the link; whether they may read the record there is decided by the permissions. A right-click shows the same intents as a menu, plus the entry Details, which opens the card. A click on a name opens the row card with group and result columns. If no intent currently applies to an entry, the card shows the facts and below them "No action is configured for this entry."
A new board has no intents. The card still always opens on a click on an entry; a separate "Open" action is not needed for that.
Placeholders
In all queries of the board:
| Placeholder | Becomes |
|---|---|
@periodStart, @periodEnd |
Limits of the selected period as a timestamp expression; compare with date columns using CAST(@periodEnd AS date). @periodEnd is the first day after the period. |
@currentUserEmail, @currentUserSub |
the signed-in user, for example for "only my row" |
@currentDate, @currentYear, @currentMonth, @currentTimeStamp |
today's date, year, month, point in time |
$(parameter) |
value from the surrounding filter container |
Quotes, date comparisons and more examples for all placeholders are described under SQL in Widgets.
Sample Schemas
The two "Create sample schema" dialogs create normal data schemas that can be extended freely in the Data Designer. The technical name is also the table name in SQL; the suggested names and field names are German.
Employees (suggested name mitarbeiter):
| Field | Type | Use on the board |
|---|---|---|
email |
Pantarey user | Row key; the "Absences" layer points to this row through the reference mitarbeiter |
name |
Text, required | Display name |
team |
Text | Group |
arbeitstage |
Multiple selection monday … sunday |
working days; tick only for fixed part-time days; empty = the board's working days. In SQL, array_join(arbeitstage, ',') turns it into a list |
fte |
Number, default 1 | Job share for full-time equivalents |
bundesland |
Selection | state: public holidays of this row if they differ from the account; empty = the board's "Holidays" setting |
aktiv |
Yes/No, default yes | active: hide employees who have left |
Absences (suggested name abwesenheiten):
| Field | Type | Use on the board |
|---|---|---|
mitarbeiter |
Reference to Employees, required | Row |
erster_tag, letzter_tag |
Date, required | first and last day: start and end (inclusive) |
erster_tag_vollhalb, letzter_tag_vollhalb |
Voll/Halb (full/half) | Half days |
kategorie |
Jahresurlaub, Krank, Fortbildung, Homeoffice | category: color and legend |
status |
Beantragt, Genehmigt, Abgelehnt, Storniert | Styles, locks, conditions |
tage |
Number | days: quantity |
vertretung |
Reference to Employees | substitute: substitute check |
bemerkung |
Text | note: text on the bar |
Reference fields store the id of the linked record. In SQL, the join is therefore made with m.id = a.mitarbeiter.
Usage
- Switch the scale at the top right; Today jumps to the now line; the minimap below the toolbar shows the loaded period and jumps on click. The period picker is missing from the toolbar when a filter container sets the period.
- States: as long as nothing is configured, the widget says "Planning board not configured"; while loading "Board is loading …"; if a query fails, "Error loading" with the error message.
- Tooltip with period, status, amount and hints when hovering over a bar.
- Legend at the bottom, grouped per layer: the layer, its categories from the data, its status patterns. A click hides the layer, a category or a status. The browser remembers the choice. While something is hidden, the toolbar shows for example "2 hidden · Show all"; hovering over it names what is hidden, a click shows everything again. With Legend: none, nothing is hidden, not even a hiding saved earlier in the browser.
- Hints: the counter in the toolbar and the button "Show all … hints" under the board open a list of all hints, blocking first, then warning, then info, each by date. Chips filter by severity, the search field by name, text or date. A click on a hint closes the list and jumps to the day and the row of the person.
- Expand and collapse groups; the group row counts per day, and days below minimum presence are colored.
- Dragging: move bars, extend them at the edges. While dragging, a hint shows how the team's presence changes on the most critical day. Locked statuses refuse with a reason.
- Selection: click an empty cell or drag over empty cells, then the configured intents appear with the number of days and working days; half days off count as 0.5. Another click closes the popover.
- Search (field "Search…" in the toolbar) filters rows and entries; Escape clears the selection; Tab moves through the bars.
Notes
- The board never changes data by itself. When moving or with "Set field", the widget writes the configured fields and shows the entry striped until the database returns the new state.
- The selected period is loaded. Result columns such as "vacation taken this year" therefore belong in the rows query, not in the layer.
- The query decides who sees which rows:
WHERE m.email = '@currentUserEmail'shows only the user's own row, a condition on the team only the colleagues. - Entries whose row key matches no row are not drawn. In the designer, the preview shows how many.