Bereiche und Tische

Gastraum, Terrasse, Stüberl — mit Hierarchie und Saison. Und die Tische mit Kapazität und Tischplan-Geometrie, ohne einen einzigen personenbezogenen Wert.

Für Entwickler

Voraussetzungen

  • Recht `areas:read` für Bereiche, `tables:read` für Tische
  • Plan-Merkmal „Tischplan"

BEREICHE BILDEN EINE HIERARCHIE. `parentId` zeigt auf den übergeordneten Bereich; `null` heisst „oberste Ebene". Ein Filter nach `areaId` in der Reservierungsliste meint immer den Bereich SAMT seiner Unterbereiche.

DREI SCHALTER ENTSCHEIDEN ÜBER DIE BUCHUNG. `onlineBookable`: darf der Gast hier online reservieren? `allowSubAreaSelection`: darf er innerhalb dieses Bereichs einen Unterbereich wählen? `isDefaultForReservations`: welcher Bereich gilt, wenn der Gast keinen nennt? `guestInfo` ist der Freitext, den der Wirt dem Gast dazu zeigt („überdacht, Hunde willkommen").

`season` IST EIN FENSTER AUS MONAT UND TAG, ohne Jahr — eine Terrasse ist jedes Jahr von Mai bis September offen. Es gilt einschliesslich und kann über den Jahreswechsel laufen. Ein halb gesetztes Fenster wird als `null` ausgeliefert: „ab März, Ende unbekannt" wäre eine Aussage, die niemand getroffen hat.

DER BILDSCHIRM IM GASTRAUM IST HALB ÖFFENTLICH. Deshalb enthält die Tischantwort keinen einzigen personenbezogenen Wert — keine Reservierung, keinen Gastnamen, keine interne Notiz. Wer die Belegung dazu braucht, holt sie über `GET /api/v1/table-occupancies` (Recht `tables:read`) oder über die Reservierungsliste (Recht `reservations:read`).

DER TISCHPLAN STECKT IN `plan`. `positionX`, `positionY`, `width`, `height`, `rotation` und `shape` sind genau das, was ein Tablet am Empfang zeichnet. Die Werte sind `null`, solange der Betrieb den Tisch nicht auf den Plan gesetzt hat — zeichnen Sie ihn dann in eine Liste statt auf eine Fläche.

EINE FREMDE ODER UNBEKANNTE `areaId` IM TISCHFILTER ERGIBT 404 — dieselbe Antwort in beiden Fällen, damit sich aus dem Unterschied nicht ablesen lässt, welche Kennungen es anderswo gibt.

Die Routen

GET/api/v1/areas

Die Bereiche des Hauses samt Hierarchie, Saisonfenster und den Schaltern, die über die Online-Buchung entscheiden.

Rechte

areas:read

Plan-Merkmal reservations_tableplan — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Abfrageparameter

NameTypBedeutung
activetrue | falseNur aktive bzw. nur abgeschaltete Bereiche.
onlineBookabletrue | falseNur Bereiche, in denen der Gast online buchen darf — bzw. nur die übrigen.
updatedSinceISO-8601 mit ZoneNur seither geänderte Bereiche.
limitinteger 1–200Vorgabe: 50Zeilen je Seite.
cursorundurchsichtiger Zeiger`nextCursor` der vorigen Antwort.
includeTotaltrue | falseVorgabe: falseErgänzt `total`.

Mögliche Fehler

  • Sortiert nach `displayOrder`, dann `id` — die Reihenfolge, die der Wirt gesetzt hat.

GET/api/v1/tables

Die Tische mit Kapazität und Tischplan-Geometrie — die Zeichnung, die ein Tablet am Empfang darstellt.

Rechte

tables:read

Plan-Merkmal reservations_tableplan — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Abfrageparameter

NameTypBedeutung
areaIdUUIDNur Tische dieses Bereichs. Eine fremde oder unbekannte Kennung ergibt 404.
activetrue | falseNur Tische, die in Betrieb sind — bzw. nur die stillgelegten.
updatedSinceISO-8601 mit ZoneNur seither geänderte Tische.
limitinteger 1–200Vorgabe: 50Zeilen je Seite.
cursorundurchsichtiger Zeiger`nextCursor` der vorigen Antwort.
includeTotaltrue | falseVorgabe: falseErgänzt `total`.

Mögliche Fehler

  • not_foundDer genannte `areaId` gehört nicht zu diesem Betrieb oder existiert nicht.
  • validationUnbekannter oder doppelter Parameter, unlesbarer Cursor.
  • forbiddenDem Schlüssel fehlt `tables:read`.
  • Sortiert nach `name`, dann `id` — der Name ist das, was am Tisch klebt.
  • Enthält keinen personenbezogenen Wert. Ein Schlüssel für einen Gastraum-Bildschirm braucht nur `tables:read` und `areas:read`.

Codebeispiele

curl — Bereiche und die Tische eines Bereichs
curl
curl -sS "https://tactictable.com/api/v1/areas?active=true" \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" curl -sS "https://tactictable.com/api/v1/tables?areaId=ce2038c4-0bb6-51d7-b705-dc831177a2d8&active=true&limit=200" \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM"
Erst die Bereiche, dann je Bereich die Tische — oder die Tische ohne Filter und selbst nach `areaId` gruppieren. Bei einem Haus mit wenigen hundert Tischen ist der zweite Weg der schnellere.
Antwort 200 — GET /api/v1/areas
JSON
{  "data": [    {      "id": "ce2038c4-0bb6-51d7-b705-dc831177a2d8",      "name": "Gastraum",      "displayOrder": 0,      "isActive": true,      "parentId": null,      "onlineBookable": true,      "allowSubAreaSelection": false,      "isDefaultForReservations": true,      "guestInfo": null,      "season": null,      "timezone": "Europe/Vienna",      "createdAt": "2026-02-11T08:20:00.000Z",      "updatedAt": "2026-02-11T08:20:00.000Z"    },    {      "id": "100baca4-f01d-5c3c-8947-65e5a5061c6b",      "name": "Terrasse",      "displayOrder": 1,      "isActive": true,      "parentId": null,      "onlineBookable": true,      "allowSubAreaSelection": false,      "isDefaultForReservations": false,      "guestInfo": "Überdacht, Hunde willkommen. Bei Regen weichen wir in den Gastraum aus.",      "season": { "startMonth": 5, "startDay": 1, "endMonth": 9, "endDay": 30 },      "timezone": "Europe/Vienna",      "createdAt": "2026-02-11T08:20:00.000Z",      "updatedAt": "2026-05-02T09:41:12.000Z"    }  ],  "nextCursor": null,  "hasMore": false,  "limit": 50}
`season: null` heisst ganzjährig. Die Terrasse ist vom 1. Mai bis zum 30. September buchbar — ausserhalb dieses Fensters liefert die Verfügbarkeitsrechnung für sie keine Uhrzeiten.
Antwort 200 — GET /api/v1/tables
JSON
{  "data": [    {      "id": "653d49c8-cd70-53ea-839f-d3131a574417",      "name": "7",      "capacity": 4,      "areaId": "ce2038c4-0bb6-51d7-b705-dc831177a2d8",      "isActive": true,      "isCorner": true,      "isOutdoor": false,      "plan": {        "positionX": 320,        "positionY": 180,        "width": 90,        "height": 90,        "rotation": 0,        "shape": "square"      },      "timezone": "Europe/Vienna",      "createdAt": "2026-02-11T08:21:14.000Z",      "updatedAt": "2026-07-19T11:02:55.000Z"    },    {      "id": "bfdfcf1b-8d9b-5708-8a65-9f553849dfae",      "name": "12",      "capacity": 2,      "areaId": "ce2038c4-0bb6-51d7-b705-dc831177a2d8",      "isActive": true,      "isCorner": false,      "isOutdoor": false,      "plan": {        "positionX": null,        "positionY": null,        "width": null,        "height": null,        "rotation": null,        "shape": null      },      "timezone": "Europe/Vienna",      "createdAt": "2026-02-11T08:21:14.000Z",      "updatedAt": "2026-02-11T08:21:14.000Z"    }  ],  "nextCursor": null,  "hasMore": false,  "limit": 50}
Tisch 12 steht nicht auf dem Plan — alle `plan`-Werte sind `null`. Zeichnen Sie solche Tische in eine Liste neben der Fläche, statt sie auf Position 0/0 zu legen.
TypeScript — Tischplan zeichnen
TypeScript
interface Tisch {    id: string    name: string    capacity: number    areaId: string    isActive: boolean    plan: {        positionX: number | null        positionY: number | null        width: number | null        height: number | null        rotation: number | null        shape: string | null    }} export async function tische(token: string, areaId?: string): Promise<Tisch[]> {    const p = new URLSearchParams({ active: 'true', limit: '200' })    if (areaId) p.set('areaId', areaId)     const antwort = await fetch('https://tactictable.com/api/v1/tables?' + p, {        headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },    })    if (antwort.status === 404) throw new Error('Diesen Bereich gibt es hier nicht.')    if (!antwort.ok) throw new Error('HTTP ' + antwort.status)     return ((await antwort.json()) as { data: Tisch[] }).data} /** Nur Tische mit vollstaendiger Geometrie lassen sich zeichnen. */export function zeichenbar(t: Tisch): boolean {    const p = t.plan    return p.positionX !== null && p.positionY !== null && p.width !== null && p.height !== null}
Die 404 auf eine fremde `areaId` ist Absicht: eine leere Liste würde verraten, dass die Kennung anderswo existiert. Behandeln Sie sie als Eingabefehler, nicht als Ausfall.
Go — Bereiche und Tische in einem Zug
Go
package tactictable import (    "encoding/json"    "fmt"    "net/http") type Bereich struct {    ID                      string  `json:"id"`    Name                    string  `json:"name"`    ParentID                *string `json:"parentId"`    OnlineBookable          bool    `json:"onlineBookable"`    IsDefaultForReservations bool   `json:"isDefaultForReservations"`    DisplayOrder            int     `json:"displayOrder"`} type Tisch struct {    ID       string `json:"id"`    Name     string `json:"name"`    Capacity int    `json:"capacity"`    AreaID   string `json:"areaId"`    IsActive bool   `json:"isActive"`} func hole(klient *http.Client, token, adresse string, ziel any) error {    anfrage, err := http.NewRequest("GET", adresse, nil)    if err != nil {        return err    }    anfrage.Header.Set("Authorization", "Bearer "+token)     antwort, err := klient.Do(anfrage)    if err != nil {        return err    }    defer antwort.Body.Close()     if antwort.StatusCode != http.StatusOK {        return fmt.Errorf("HTTP %d fuer %s", antwort.StatusCode, adresse)    }     return json.NewDecoder(antwort.Body).Decode(ziel)} func Raumplan(klient *http.Client, token string) ([]Bereich, []Tisch, error) {    var bereiche struct {        Data []Bereich `json:"data"`    }    if err := hole(klient, token, "https://tactictable.com/api/v1/areas?active=true&limit=200", &bereiche); err != nil {        return nil, nil, err    }     var tische struct {        Data []Tisch `json:"data"`    }    if err := hole(klient, token, "https://tactictable.com/api/v1/tables?active=true&limit=200", &tische); err != nil {        return nil, nil, err    }     return bereiche.Data, tische.Data, nil}
`ParentID` ist ein Zeiger, weil das Feld `null` sein darf — ein `string` machte daraus einen leeren Text, und „oberste Ebene" wäre von „Kennung fehlt" nicht zu unterscheiden.