API: Werkstatttermine
Endpunkte zum Anlegen von Werkstattterminen in Werkstattkalender (calendar_timeline in Doku genannt)
sowie zur Ermittlung freier Termin-Slots. Folgt dem Auth-Muster von /api/invoices/create.
Alle Endpunkte erfordern den Parameter
token mit dem gültigencatama_public_api_token(Systemeinstellungen > API). Bei ungueltigem Token:
HTTP 401mit
{ "status": 0, "message": "Invalid Token" }.Übersicht der Endpunkte
| Methode | Pfad | Zweck |
|---|---|---|
| POST | /api/workshop-appointments/free-slots | Freie Slots im Zeitraum berechnen, mit optionalen Filtern (Section, Mitarbeiter, Resource, Werkstattwagen). |
| POST | /api/workshop-appointments/create | Termin in calendar_timeline anlegen, optional inkl. Find-or-Create für Kunde & Fahrzeug. |
Web-DEMOs
Werkstatttermin per API anlegen – Demo
Freie Werkstatt-Slots ermitteln
Slot suchen und Termin anlegen
POST /api/workshop-appointments/free-slots
Berechnet freie Termin-Slots im angegebenen Zeitraum über alle Tage hinweg.
Berücksichtigt belegte Einträge in calendar_timeline
mit Branch-Scope und optionalen Filtern.
Request-Felder
| Name | Typ | Beschreibung |
|---|---|---|
| token | string | API-Token Pflicht |
| date_from | string | Startdatum Y-m-d Pflicht |
| date_to | string | Enddatum, max. 92 Tage Spanne Pflicht |
| slot_minutes | integer | Slot-Raster in Minuten (5–720, Default 30). |
| workshop_start | H:i | Überschreibt Systemeinstellungen -> Kalender Start (Default 07:00). |
| workshop_end | H:i | Überschreibt (Default 17:00). |
| section_id | integer | Direkter Filter auf Team-Name aus Kalender z.B. Werkstatt (999X für Team / users.id). Ueberschreibt department_id/assigned_user_id/section_name. |
| department_id | integer | Komfort-Filter: Team-Name aus Kalender z.B. Werkstatt wird intern zu section_id=999X gemappt. |
| assigned_user_id | integer | Komfort-Filter: users.id wird direkt als section_id verwendet. |
| section_name | string | Alternative zu Section-IDs: zuerst (status=1), Fallback Klarname des Benutzers z.B. Max Mustermann (active=1). Mehrdeutigkeit → HTTP 404. |
| employee_id | integer | Filter auf Mitarbeiter. Matcht mechanic_id ODER section_id (Legacy-Termine speichern den User direkt in section_id). |
| employee_name | string | Alternative zu employee_id: Klarname des Benutzers z.B. Max Mustermann. |
| resource_id | integer | Filter auf Werkstattbahn / Hebebühne (ID der Resource, status=1). |
| resource_name | string | Alternative zu resource_id: resources.name. |
| workshop_vehicle_id | integer | Filter auf Werkstattwagen (mobile_ressource_id, gemappt auf resources.id type=2). |
| include_weekends | integer | 1 = Sa/So einbeziehen, 0 = ausschließen. Default aus Kalender-Range aus Systemeinstellungen. |
| exclude_status_ids | string | Komma-getrennte Status-IDs, deren Termine nicht blockieren. Default "-99" (Id des Status, -99 = storniert) (storniert). |
| branch_id | integer | Filial-ID, überschreibt Session-Default. |
| max_slots | integer | Maximalanzahl freier Slots (Default 200, Hardlimit 2000). |
workshop_startbis
workshop_end) in Slots der Längeslot_minutes zerlegt.Ein Slot gilt als frei, wenn kein Termin mit den aktiven Filtern den Slot überlappt
(
busy.start < slot.end UNDbusy.end > slot.start).Beispiel-Aufruf
POST /api/workshop-appointments/free-slots
Content-Type: application/json
{
"token": "xxxxxxxx",
"date_from": "2026-05-22",
"date_to": "2026-05-26",
"slot_minutes": 30,
"section_name": "Werkstatt",
"employee_name": "Max Mustermann",
"resource_name": "Hebebühne 1",
"workshop_start": "08:00",
"workshop_end": "17:00",
"include_weekends": 0,
"max_slots": 50
}
Erfolgsantwort (HTTP 200)
{
"data": {
"range": { "date_from": "2026-05-22", "date_to": "2026-05-26" },
"slot_minutes": 30,
"workshop_start": "08:00",
"workshop_end": "17:00",
"include_weekends": 0,
"filters": {
"section_id": 9991,
"section_name": "Werkstatt",
"employee_id": 42,
"employee_name": "Max Mustermann",
"resource_id": 3,
"resource_name": "Hebebühne 1",
"branch_id": 1
},
"count": 3,
"slots": [
{
"date": "2026-05-22",
"start_date": "2026-05-22 08:00:00",
"end_date": "2026-05-22 08:30:00",
"section_id": 9991,
"section_name": "Werkstatt",
"employee_id": 42,
"employee_name": "Max Mustermann",
"resource_id": 3,
"resource_name": "Hebebühne 1"
}
]
},
"status": 1,
"message": "Success"
}
Fehler-Antworten
HTTP 401 { "status": 0, "message": "Invalid Token" }
HTTP 422 { "status": 0, "message": "date_from and date_to are required (Y-m-d)" }
HTTP 422 { "status": 0, "message": "date_from must be <= date_to" }
HTTP 422 { "status": 0, "message": "date range must not exceed 92 days" }
HTTP 422 { "status": 0, "message": "slot_minutes must be between 5 and 720" }
HTTP 404 { "status": 0, "message": "section_name not found: ..." }
HTTP 404 { "status": 0, "message": "employee_name not found: ..." }
HTTP 500 { "status": 0, "message": "DB connect failed" }
POST /api/workshop-appointments/create
Legt einen neuen Werkstatttermin in calendar_timeline an.
Optional können Kunde, Fahrzeug, Auftrag, Abteilung, Mitarbeiter, Werkstattbahn (Resource) und Werkstattwagen verknüpft werden.
Wenn kein customer_id bekannt ist, kann mit dem Objekt
customer ein Find‑or‑Create ausgelöst werden.
Analog für Fahrzeuge mit vehicle.
Request-Felder
| Name | Typ | Beschreibung |
|---|---|---|
| token | string | API-Token Pflicht |
| title | string | Terminbezeichnung (event_name) Pflicht |
| start_date | string | Y-m-d H:i:s, Y-m-d H:i, d.m.Y H:i usw. Pflicht |
| end_date | string | Muss nach start_date liegen. Pflicht |
| customer_id | integer | Bestehender Kunde aus Kundendatenbank ID des Kunden. |
| customer | object | Find-or-Create, nur wenn customer_id fehlt. |
| vehicle_id | integer | Kundenfahrzeug aus Kundenfahrzeuge (Mapping mobile_id). |
| vehicle | object | Find-or-Create, nur wenn vehicle_id fehlt. |
| invoice_id | integer | Verknüpfter Auftrag/Beleg aus invoices. |
| section_id | integer | Direkter Wert (999X Team / ID des Mitarbeiters). Höchste Priorität. |
| department_id | integer | Abteilung als ID des Teams; intern als 999X gespeichert. |
| assigned_user_id | integer | Statt einer Abteilung kann ein einzelner Benutzer als section_id gespeichert werden. |
| section_name | string | Alternative zu Section-IDs (Team oder User). |
| employee_id | integer | Mitarbeiter / Mechaniker (users.id) → mechanic_id. |
| employee_name | string | Alternative zu . |
| resource_id | integer | Werkstattbahn / Hebebühne aus resources (status=1). |
| resource_name | string | Alternative zu resource_id. |
| workshop_vehicle_id | integer | Werkstattwagen (resources.id, validiert). |
| work_type | integer | Arbeitsart aus planning_light_work_types. |
| resource_status_id | integer | Terminstatus (Default 0). -99 = storniert. |
| customer_waiting | integer | 1 = Kunde wartet, 0 = nein. |
| notes | string | Interne Notizen (extra_notes). |
| description | string | Beschreibung / Termininhalt. |
| branch_id | integer | Filial-ID, überschreibt Session-Default. |
| added_by | integer | Anlegender Benutzer. |
| check_conflicts | boolean | Default false. Wenn true: vor Insert prüfen, ob Slot bereits belegt ist (Section/Mitarbeiter/Resource/Werkstattwagen). Bei Konflikt → HTTP 409. |
Validierung der ID-Felder:
section_idals Rohwert wird geprüft — akzeptiert wird entweder999XmitX = userteams.utid(status=1) oderBenutzer ID(active=1). Ungültige Werte → HTTP 404.employee_idmuss aktiverBenutzer IDsein.resource_id/workshop_vehicle_idmüssen inresources(status=1) existieren.vehicle_idausclientmobiles; wenncustomer_idübergeben, muss das Fahrzeug zu diesem Kunden gehören.invoice_idwird gegeninvoiceinventory::getInvoiceDataById()validiert.
Customer Find-or-Create
Wenn customer_id nicht gesetzt ist und stattdessen
customer übergeben wird, sucht das System einen passenden Kunden in
customers und legt nur dann einen neuen Datensatz an, wenn kein eindeutiger Treffer vorliegt.
Match-Reihenfolge (deterministisch, jeweils nur wenn vorherige Stufe leer):
- Exakter Treffer auf
email,email_2oderemail_3. - Telefon (normalisiert, nur Ziffern, ≥ 5 Stellen) gegen
phone_mobile,phone_business,phone_private. - Kombination
zip+ (surnameodercompany_name).
| Feld | Beschreibung |
|---|---|
| company_name / forename / surname | Kunden-/Firmenname. |
| email / email_2 / email_3 | E-Mail-Adressen. |
| phone_mobile / phone_business / phone_private | Telefonnummern. |
| street / zip / city | Anschrift. |
| country / country_id | Land als Klartext (Lookup) oder direkt als ID. |
| customer_kind_id | Kundenart (Default 1). |
| customer_match_mode | strict (Default, 409 bei Mehrdeutigkeit) oder newest. |
data.customer_resolution:provided | matched | created | none.Vehicle Find-or-Create
Wenn vehicle_id nicht gesetzt ist und stattdessen
vehicle übergeben wird, sucht das System ein passendes Fahrzeug in
clientmobiles und legt es nur dann neu an, wenn kein eindeutiger Treffer vorliegt.
- Exakter Treffer auf
chassis_number(VIN), optional eingeschränkt aufcustomer_idbzw.current_owner_id. - Exakter Treffer auf
mobile_sign(Kennzeichen), analog kunden-eingeschränkt.
Mindestens eines von chassis_number oder
mobile_sign ist erforderlich.
| Feld | Beschreibung |
|---|---|
| chassis_number | Fahrgestellnummer / VIN. |
| mobile_sign | Amtliches Kennzeichen. |
| fabricator | Hersteller als Klartext (z. B. "Volkswagen") → auf Hersteller.ID aufgelöst. |
| fabricator_id | Direkte ID, übersteuert fabricator. |
| modell / name / color / mileage | Modellname, Farbe, Kilometerstand. |
| ez | Erstzulassung (Y-m-d, d.m.Y oder Unix-TS). |
| vehicle_key_code / vehicle_registration_code | HSN / TSN (KBA-Schlüsselnummern). |
| mobile_kind | Fahrzeugart (Default 1 = PKW). |
| vehicle_match_mode | strict (Default) oder newest. |
| create_if_not_found | Default true. Auf false: bei Miss HTTP 404 statt Anlage. |
data.vehicle_resolution:provided | matched | created | none.Zusätzlich gibt es
vehicle_label, vehicle_sign, vehicle_vin als lesbaren Kontext.Beispiel-Aufruf (JSON)
POST /api/workshop-appointments/create
Content-Type: application/json
{
"token": "xxxxxxxx",
"title": "Inspektion 30tkm",
"start_date": "2026-05-22 09:00:00",
"end_date": "2026-05-22 11:00:00",
"invoice_id": 14074,
"department_id": 1,
"employee_name": "Max Mustermann",
"resource_name": "Hebebühne 1",
"work_type": 2,
"notes": "Kunde bringt Schlüssel morgens",
"check_conflicts": true,
"customer": {
"email": "max.mustermann@example.com",
"forename": "Max",
"surname": "Mustermann",
"zip": "80331",
"city": "München",
"street": "Musterstr. 1",
"phone_mobile": "+49 170 1234567",
"customer_match_mode": "strict"
},
"vehicle": {
"chassis_number": "WVWZZZ3CZWE123456",
"mobile_sign": "B-AB 1234",
"fabricator": "Volkswagen",
"modell": "Golf VII",
"ez": "2018-03-15",
"mileage": 85000,
"vehicle_match_mode": "strict"
}
}
Erfolgsantwort (HTTP 200)
{
"data": {
"event_id": 5821,
"title": "Inspektion 30tkm",
"start_date": "2026-05-22 09:00:00",
"end_date": "2026-05-22 11:00:00",
"customer_id": 12034,
"customer_resolution": "matched",
"vehicle_id": 1085,
"vehicle_resolution": "matched",
"vehicle_label": "Volkswagen Golf VII (B-AB 1234)",
"vehicle_sign": "B-AB 1234",
"vehicle_vin": "WVWZZZ3CZWE123456",
"invoice_id": 14074,
"section_id": 9991,
"section_name": "Werkstatt",
"department_id": 1,
"employee_id": 42,
"employee_name": "Max Mustermann",
"resource_id": 3,
"resource_name": "Hebebühne 1",
"work_type": 2,
"branch_id": 1,
"check_conflicts": 1
},
"status": 1,
"message": "Success"
}
Fehler-Antworten (Auswahl)
HTTP 401 { "status": 0, "message": "Invalid Token" }
HTTP 422 { "status": 0, "message": "title is required" }
HTTP 422 { "status": 0, "message": "start_date must be before end_date" }
HTTP 404 { "status": 0, "message": "Customer not found" }
HTTP 404 { "status": 0, "message": "Vehicle not found" }
HTTP 404 { "status": 0, "message": "Invalid section_id (no matching active team or user)" }
HTTP 404 { "status": 0, "message": "employee_name not found: ..." }
HTTP 404 { "status": 0, "message": "Workshop vehicle (resources.id) not found or inactive" }
HTTP 422 { "status": 0, "message": "Vehicle does not belong to provided customer" }
HTTP 409 { "status": 0, "message": "Ambiguous customer match: 3 candidates",
"data": { "candidates": [12034, 12089, 12190] } }
HTTP 409 { "status": 0, "message": "Ambiguous vehicle match: 2 candidates",
"data": { "vehicle_candidates": [1085, 1240], "vehicle_match_strategy": "chassis_number" } }
HTTP 409 { "status": 0, "message": "Slot conflict detected (check_conflicts=true)",
"data": { "conflicts": [ {"event_id": 5810, "start_date": "...", "end_date": "..."} ] } }
HTTP 500 { "status": 0, "message": "Failed to create customer (check PHP error log for SQL details)" }
HTTP 500 { "status": 0, "message": "Failed to insert appointment: <sql error>" }
Field Mapping — calendar_timeline
| API-Feld | DB-Spalte | Bemerkung |
|---|---|---|
| title | event_name | String |
| start_date / end_date | start_date / end_date | DATETIME |
| customer_id | customer_id | customers.id |
| vehicle_id / vehicle | mobile_id | clientmobiles.id (Find-or-Create über VIN/Kennzeichen) |
| invoice_id | invoice_id | invoices.id |
| section_id / department_id / assigned_user_id / section_name | section_id | 999X = Team (utid), sonst direkter users.id |
| employee_id / employee_name | mechanic_id | users.id (Mechaniker) |
| resource_id / resource_name | resource_id | Werkstattbahn / Hebebühne |
| workshop_vehicle_id | mobile_ressource_id | Werkstattwagen (resources.id, type=2) |
| work_type | work_type | planning_light_work_types.id |
| resource_status_id | resource_status_id | Terminstatus (-99 = storniert) |
| customer_waiting | customer_waiting | 0/1 |
| notes | extra_notes | Interne Notizen |
| description | description | Termininhalt |
| branch_id | branch_id | Filiale |
| added_by | added_by / updated_by | users.id |
Race Conditions & Hinweise
Bekannte Race-Windows
- Free-Slots → Create: Zwischen dem Ergebnis von
/free-slotsund dem Aufruf von/createkann ein anderer Aufrufer denselben Slot reservieren. Mitcheck_conflicts=truewird das Risiko stark reduziert (best-effort). - Customer Find-or-Create: Ohne UNIQUE-Constraint auf
customers.emailkönnen zwei parallele Calls beide den Match verfehlen und je einen Datensatz anlegen. Empfehlung: Calls je Kunde sequenziell auslösen oder UNIQUE-Index pflegen. - Vehicle Find-or-Create: Analog — ohne UNIQUE auf
chassis_number/mobile_signsind in seltenen Fällen Doppel-Inserts möglich. - Soft-deleted Fahrzeuge: Match filtert auf
mobile_status=1; archivierte Fahrzeuge gelten als „nicht vorhanden“ und werden ggf. neu angelegt.
start_date/end_date werden in der lokalen Server-Zeitzone (PHP-Default) verarbeitet und so in calendar_timeline gespeichert. Externe Aufrufer sollten dieselbe Zeitzone verwenden wie der Server.Empfohlener Workflow
- Mit
POST /api/workshop-appointments/free-slotsfreie Slots für den gewünschten Mitarbeiter / die Werkstattbahn ermitteln. - Vom Anwender den Slot auswählen lassen.
- Mit
POST /api/workshop-appointments/createden Termin anlegen — idealerweise mitcheck_conflicts=true, damit konkurrierende Buchungen einHTTP 409auslösen. - Bei
409 Slot conflict detected: Slots erneut abfragen und neu anbieten.