Material & Verleih
Vier Bereiche rund um Ausrüstung: Material (der Bestand), Fahrzeuge, Packlisten (was fährt wann mit) und Verleih (wer bekommt was wann).
Alle Pfade sind relativ zu https://<domain>/api/v1.
Material
Bereich material. Drei Begriffe, wie in der Anwendung:
| Begriff | Bedeutung |
|---|---|
| Exemplar | ein konkretes Stück mit eigener Material-Nummer |
| Artikel | mehrere gleichartige Exemplare, gruppiert |
| Set | eine benannte Zusammenstellung aus Exemplaren und Artikeln |
Kategorien und Lagerorte liegen im selben Bereich: Ein Werkzeug, das Material lesen darf, soll nicht zusätzlich um Kategorien bitten müssen – ohne sie ist die Liste kaum zu deuten.
| Methode | Pfad | Bereich | Beschreibung |
|---|---|---|---|
| GET | /material | material:read | Exemplare (Liste) |
| POST | /material | material:write | Exemplar anlegen |
| GET | /material/{id} | material:read | Exemplar lesen |
| PUT | /material/{id} | material:write | Exemplar ändern |
| DELETE | /material/{id} | material:write | Exemplar löschen |
| GET | /material/code/{code} | material:read | Exemplar über QR-/Barcode finden |
| GET | /material/artikel | material:read | Artikel (Liste) |
| POST | /material/artikel | material:write | Artikel anlegen |
| POST | /material/artikel/{id}/aufloesen | material:write | Artikel auflösen |
| GET | /material/sets | material:read | Sets (Liste) |
| POST | /material/sets | material:write | Set anlegen |
| GET | /material/sets/{id} | material:read | Set samt Positionen |
| PUT | /material/sets/{id} | material:write | Set ändern |
| DELETE | /material/sets/{id} | material:write | Set löschen |
| POST | /material/sets/{id}/positionen | material:write | Position anlegen oder Menge ändern |
| DELETE | /material/sets/{id}/positionen/{positionId} | material:write | Position entfernen |
| GET | /material/kategorien | material:read | Kategorien (Liste) |
| POST | /material/kategorien | material:write | Kategorie anlegen |
| PUT | /material/kategorien/{id} | material:write | Kategorie ändern |
| DELETE | /material/kategorien/{id} | material:write | Kategorie löschen |
| GET | /material/lagerorte | material:read | Lagerorte (Liste) |
| POST | /material/lagerorte | material:write | Lagerort anlegen |
| PUT | /material/lagerorte/{id} | material:write | Lagerort ändern |
| DELETE | /material/lagerorte/{id} | material:write | Lagerort löschen |
Liste filtern
| Parameter | Bedeutung |
|---|---|
q | Volltextsuche |
kategorie_id, lagerort_id | Kategorie bzw. Lagerort |
zustand | Zustand |
status | verfuegbar, reserviert, defekt, reparatur, ausgemustert |
fremd | true = nur fremdes Material |
nutzbar | true = nur einsetzbares |
archiviert | true = auch archivierte |
Sortierbar: title (Standard), material_id, weight_grams, condition,
created_at, updated_at, status. Auch diese Liste blättert in der Datenbank.
Felder eines Exemplars
| Feld | Typ | Anmerkung |
|---|---|---|
title | Text | Pflicht |
material_id | Text | die sichtbare Material-Nummer |
category_id, location_id | UUID | aus /material/kategorien bzw. /material/lagerorte |
content_description | Text | Inhalt eines Behälters |
weight_grams | Ganzzahl ≥ 0 | |
quantity | Ganzzahl ≥ 0 | Stückzahl bei Schüttgut |
color_code, dimensions, shelf | Text | |
manufacturer, serial_number | Text | |
purchase_date, repair_date | Datum | |
purchase_value, replacement_value | Zahl ≥ 0 | Anschaffung / Wiederbeschaffung |
condition | Text | Zustand |
status | verfuegbar | reserviert | defekt | reparatur | ausgemustert | |
defect_reason | Text | |
is_external, owner_name, owner_contact | fremdes Material | |
tags | Liste von Texten | |
notes | Text | |
is_archived | Wahrheitswert |
Was unter Einstellungen → Pflichtfelder konfiguriert ist, wird beim Anlegen und beim
Ändern geprüft (400 bad_request, die Meldung nennt die fehlenden Felder). Sonst könnte
ein Werkzeug Datensätze erzeugen, die die Oberfläche anschließend als unvollständig
anmeckert.
Artikel und Sets
POST /material/artikel erwartet nur {"name":"…"}. Auflösen entfernt den Artikel,
die Exemplare bleiben und stehen danach wieder einzeln – bewusst kein DELETE, weil
„Artikel löschen“ nahelegen würde, dass auch das Material weg ist.
Eine Set-Position benennt entweder ein Exemplar oder einen Artikel:
{ "material_id": "<uuid>", "quantity": 2 }
{ "group_id": "<uuid>", "quantity": 4 }
Beim Packen wird aus einem Artikel das konkrete Stück gewählt (Set-Substitution). Ein
erneutes POST mit derselben Kennung ändert die Menge.
Kategorien und Lagerorte haben je name (Pflicht), sort_order und is_active.
Fahrzeuge
Bereich fahrzeuge – eigener Bereich und nicht Teil von „Material“: Fahrzeuge haben in der
Anwendung eine eigene Rechtelage, und ein Werkzeug, das den Materialbestand abgleicht, hat
am Bulli nichts zu suchen.
| Methode | Pfad | Bereich | Beschreibung |
|---|---|---|---|
| GET | /fahrzeuge | fahrzeuge:read | Liste |
| POST | /fahrzeuge | fahrzeuge:write | Anlegen |
| GET | /fahrzeuge/{id} | fahrzeuge:read | Einzeln lesen |
| PUT | /fahrzeuge/{id} | fahrzeuge:write | Ändern |
| DELETE | /fahrzeuge/{id} | fahrzeuge:write | Löschen |
Filter: q (Name, Kennzeichen, Typ), archiviert (true = auch ausgemusterte).
Sortierbar: name (Standard), license_plate, seats, max_payload_kg,
created_at.
| Feld | Typ | Anmerkung |
|---|---|---|
name | Text | Pflicht |
license_plate | Text | Kennzeichen |
vehicle_type | Text | |
seats | Ganzzahl ≥ 0 | |
max_payload_kg | Zahl ≥ 0 | Zuladung |
cargo_dimensions | Text | Laderaum |
notes | Text | |
is_archived | Wahrheitswert | ausgemustert |
Packlisten
Bereich packlisten. Start- und Enddatum sind Pflicht – das ist keine Formalie: nur
mit Zeitraum lässt sich erkennen, ob Material auf zwei Listen gleichzeitig steht, also
genau die Prüfung, die eine Packliste nützlich macht.
| Methode | Pfad | Bereich | Beschreibung |
|---|---|---|---|
| GET | /packlisten | packlisten:read | Liste |
| POST | /packlisten | packlisten:write | Anlegen |
| GET | /packlisten/{id} | packlisten:read | Liste samt Positionen und Fahrzeugen |
| PUT | /packlisten/{id} | packlisten:write | Ändern |
| DELETE | /packlisten/{id} | packlisten:write | In den Papierkorb |
| GET | /packlisten/{id}/positionen | packlisten:read | Positionen (Liste) |
| POST | /packlisten/{id}/positionen | packlisten:write | Position hinzufügen |
| PUT | /packlisten/{id}/positionen/{positionId} | packlisten:write | Position ändern |
| DELETE | /packlisten/{id}/positionen/{positionId} | packlisten:write | Position entfernen |
| GET | /packlisten/{id}/fahrzeuge | packlisten:read | Fahrzeuge (Liste) |
| POST | /packlisten/{id}/fahrzeuge | packlisten:write | Fahrzeug zuordnen |
| DELETE | /packlisten/{id}/fahrzeuge/{fahrzeugId} | packlisten:write | Fahrzeug entfernen |
Filter: q, archiviert.
Sortierbar: start_date (Standard), name, end_date, status, created_at.
| Feld | Typ | Anmerkung |
|---|---|---|
name | Text | Pflicht |
description | Text | |
start_date, end_date | JJJJ-MM-TT | beide Pflicht; Ende nicht vor Beginn |
status | draft | planned | packed | done |
Position: material_id (Pflicht), quantity (Standard 1), comment. Beim Ändern
zusätzlich is_packed. Fahrzeug zuordnen: {"vehicle_id":"<uuid>"}.
In der Oberfläche ist das Einplanen ein interaktiver Vorgang mit Rückfrage. Über die Schnittstelle soll ein Abgleich eine Liste zusammenstellen können; was dabei doppelt verplant wird, zeigt die Anwendung anschließend an der Liste selbst.
DELETE legt die Liste in den Papierkorb – die Antwort nennt die Frist in
papierkorb_tage. Zurückholen geht nur in der Anwendung.
Verleih
Bereich verleih. Die Verfügbarkeitsprüfung kommt aus derselben Quelle wie Oberfläche und
öffentliches Formular und ist hier nicht optional: Eine über die Schnittstelle
angelegte Ausleihe, die den Bulli ein zweites Mal vergibt, fiele erst am Abfahrtstag auf.
| Methode | Pfad | Bereich | Beschreibung |
|---|---|---|---|
| GET | /verleih | verleih:read | Liste |
| POST | /verleih | verleih:write | Ausleihe anlegen |
| GET | /verleih/{id} | verleih:read | Einzeln lesen |
| PUT | /verleih/{id} | verleih:write | Ändern |
| DELETE | /verleih/{id} | verleih:write | Löschen |
| POST | /verleih/{id}/status | verleih:write | Statuswechsel |
| GET | /verleih/belegung | verleih:read | Belegte Zeiträume für eine Kalenderansicht |
| GET | /verleih/verleihbar | verleih:read | Was überhaupt verliehen werden darf |
| POST | /verleih/pruefen | verleih:read | Zeitraum prüfen, ohne etwas anzulegen |
Filter der Liste: q, art (material oder fahrzeug), status (kommagetrennt),
offen (true = nur laufende), ueberfaellig, mitglied_id.
Sortierbar: start_date (Standard), end_date, borrower_name, status,
created_at.
Felder
| Feld | Typ | Anmerkung |
|---|---|---|
kind | material | fahrzeug | Standard material |
borrower_name | Text | Pflicht |
contact_id | UUID | wenn die Person im Bestand steht |
borrower_email, borrower_phone | Text | |
start_date, end_date | JJJJ-MM-TT | Pflicht; Ende nicht vor Beginn |
purpose, notes | Text | |
items | Liste | mindestens eine Position |
Jede Position benennt genau eines von beiden – ein Material oder ein Fahrzeug –
plus quantity:
{ "items": [ { "vehicle_id": "<uuid>", "quantity": 1 } ] }
Anlegen und Konflikte
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"borrower_name":"Maja Beispiel","start_date":"2026-08-14","end_date":"2026-08-16",
"items":[{"vehicle_id":"<uuid>","quantity":1}]}' \
"$BASIS/verleih"
Ist im Zeitraum etwas vergeben, kommt 409 not_available mit den Gründen in
details.konflikte. Geprüft wird gegen andere Ausleihen und Packlisten.
Eine über die Schnittstelle angelegte Ausleihe startet direkt auf bestaetigt: Wer einen
Schlüssel mit Schreibrecht hat, hat sie damit entschieden. Wer einen Antrags-Workflow will,
nutzt das öffentliche Ausleihformular.
PUT nicht ändertStatus und Protokollfelder (status, source, decided_at, handed_out_at,
returned_at …) ändert ausschließlich der Statuswechsel. Mitgeschickte Werte werden
verworfen. Werden items mitgeschickt, ersetzen sie die Positionen vollständig – die
eigene Buchung bleibt bei der Konfliktprüfung ausgeklammert.
Statusfluss
angefragt ──bestaetigen──> bestaetigt ──ausgeben──> ausgegeben ──zurueck──> zurueck
│ │
└──ablehnen──> abgelehnt └──stornieren──> storniert
POST /verleih/{id}/status:
| Feld | Typ | Anmerkung |
|---|---|---|
aktion | bestaetigen | ablehnen | ausgeben | zurueck | stornieren | Pflicht |
note | Text | wird an die vorhandenen Notizen angehängt |
items | Liste aus item_id, returned_ok, damage_note | Zustandsprotokoll bei der Rücknahme |
Die erlaubten Übergänge sind fest verdrahtet; ein unpassender Wechsel antwortet mit
409 conflict. Beim Bestätigen wird noch einmal auf Konflikte geprüft – zwischen
Anfrage und Bestätigung kann jemand anderes gebucht haben.
Verfügbarkeit vorab prüfen
POST /verleih/pruefen legt nichts an und braucht deshalb nur verleih:read:
{
"start_date": "2026-08-14",
"end_date": "2026-08-16",
"items": [ { "vehicle_id": "<uuid>", "quantity": 1 } ],
"exclude_loan_id": null
}
Antwort:
{ "data": { "frei": false, "konflikte": ["Bulli ist vom 14.08. bis 16.08. bereits vergeben."] } }
Damit kann ein Anmeldetool schon im Formular sagen, dass der Zeitraum nicht frei ist.
exclude_loan_id klammert eine bestehende Buchung aus – nötig, wenn man einen vorhandenen
Zeitraum verschieben will.
GET /verleih/belegung liefert die belegten Zeiträume für eine Kalenderansicht
(art, ab), GET /verleih/verleihbar die Freigaben aus den Einstellungen, getrennt
nach material und fahrzeuge.
Hat dies deine Frage beantwortet?