feat(flutter): Mangelmeldung-Offline-Queue + Threat-Model-Fund als Kachel
Android-Client zieht die Draft-first-Offline-Queue für Mangelmeldung nach (bisher nur Kontrolle abgedeckt), inkl. Foto-Upload per Multipart und eigenem Melde-Screen. Zusätzlich Backend-Threat-Model erstellt und daraus DOC-006 (IDOR bei Dokument-Download) als Kachel im Backlog angelegt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
1 parent
d6786396d1
commit
230237ac94
14 files changed
+3388
-5
No files matched your search
@@ -120,6 +120,39 @@ Details zu DOC-001 … DOC-005 (vollständig).
|
||||
|
||||
---
|
||||
|
||||
## DOC-006 — Standort-/Entitäts-Zugriffsprüfung beim Download (Sicherheitsfund)
|
||||
|
||||
- **Ziel:** Verhindern, dass ein authentifizierter Nutzer Dokumente fremder Standorte/
|
||||
Objekte abrufen kann, nur weil er die `dokument_id` (UUID) kennt.
|
||||
- **Beschreibung:** `GET /dokumente/{dokument_id}/download` und `/versionen` prüfen aktuell
|
||||
nur `_pruefe_eingeschraenkten_zugriff` (Dokumenttyp-Rolle), aber nicht, ob der Nutzer
|
||||
überhaupt Zugriffsrecht auf die referenzierte Entität (`entitaet_typ`/`entitaet_id`, z.B.
|
||||
Standort über Objekt) hat. Threat-Model-Fund (2026-09-12, IDOR, Schweregrad Hoch), siehe
|
||||
`backend/THREAT_MODEL_BACKEND.md` Finding #4.
|
||||
- **Benutzerwert:** Vertraulichkeit über Standortgrenzen hinweg, verhindert Datenabfluss
|
||||
bei UUID-Erraten/-Leak (Logs, geteilte Links, Referer).
|
||||
- **Abhängigkeiten:** DOC-001, ZUST-003 (Standort-Berechtigungsprüfung existiert bereits als
|
||||
Baustein, `hat_berechtigung_am_standort`/`pruefe_berechtigung_am_standort`)
|
||||
- **Datenmodell:** keine Änderung nötig — `entitaet_typ`/`entitaet_id` bereits auf `Dokument`
|
||||
- **Backend:** Download/Versionen-Endpunkt (`app/api/v1/endpoints/dokument.py`) muss
|
||||
entitätsabhängig auflösen, welcher Standort betroffen ist, und
|
||||
`pruefe_berechtigung_am_standort` (oder äquivalent) zusätzlich zur bestehenden
|
||||
Dokumenttyp-Prüfung aufrufen.
|
||||
- **Frontend:** keine Änderung erwartet (403 wird bereits generisch behandelt)
|
||||
- **Mobile:** keine Änderung erwartet
|
||||
- **QR-Code/Seriennummer/Inventarnummer:** nein
|
||||
- **Akte:** keine Änderung
|
||||
- **Rechte:** wie ZUST-003 (Standort-Scoping)
|
||||
- **Audit:** abgelehnter Zugriffsversuch könnte geloggt werden (P3-Detail, siehe DOC-005)
|
||||
- **Akzeptanzkriterien:** Nutzer ohne Standort-Berechtigung für die zugehörige Entität
|
||||
bekommt 403 bei Download/Versionen, auch mit korrekter/gültiger `dokument_id`.
|
||||
- **Tests:** IDOR-Regressionstest — Nutzer A lädt Dokument an Standort X hoch, Nutzer B
|
||||
(nur Standort Y berechtigt) versucht Download/Versionen, erwartet 403.
|
||||
- **DoD:** Sicherheitslücke behoben, Regressionstest grün, kein Verhalten für berechtigte
|
||||
Nutzer verändert.
|
||||
|
||||
---
|
||||
|
||||
**MABEA-Ist-Stand-Abgleich:** DOC-001 ist vollständig und solide umgesetzt (inkl.
|
||||
Sicherheits-Whitelist gegen Path-Traversal, generisch wiederverwendbares Panel). DOC-002…005
|
||||
fehlen alle komplett, sind aber bewusst als P2 eingestuft — keine dieser vier Lücken
|
||||
|
||||
@@ -136,6 +136,19 @@ Details zu MOBILE-001 … MOBILE-005 (vollständig).
|
||||
Kamera-Sensor statt nur Datei-Auswahl) statt erst nachträglich über
|
||||
`DokumentePanel` — Foto wird zusammen mit der Meldung in die Offline-Queue
|
||||
gelegt und nach erfolgreicher Mangel-Erstellung automatisch hochgeladen.
|
||||
**Update 2026-09-12 (Flutter-Parität nachgezogen):** Android-Client hatte
|
||||
bislang nur Offline-Queue für Kontrolle, nicht für Mangelmeldung — Lücke
|
||||
geschlossen. Neu: `flutter_app/lib/offline/mangel_queue.dart` (sqflite,
|
||||
gleiches Muster wie `offline/queue.ts`/`mangelQueue.ts` der PWA), Multipart-
|
||||
Upload in `api/api_client.dart` (`ladeDokumentHoch`), Formular-Screen
|
||||
`screens/mangel_melden_screen.dart` (Beschreibung/Priorität/Kamera-Foto via
|
||||
`image_picker`), erreichbar über Icon-Button in `objekt_list_screen.dart`
|
||||
und AppBar-Action in `kontroll_screen.dart`. Foto wird vor dem Enqueue in
|
||||
ein App-eigenes Verzeichnis kopiert (`path_provider`), da der `image_picker`-
|
||||
Cache-Pfad vom System vor dem Sync geräumt werden könnte. **Ungetestet**
|
||||
(kein Flutter-SDK lokal verfügbar) — vor Produktivnutzung `flutter analyze`
|
||||
+ `flutter build apk` und Kamera-Berechtigung in `android/app/src/main/
|
||||
AndroidManifest.xml` (siehe `flutter_app/README.md`) prüfen.
|
||||
|
||||
## MOBILE-005 — Mobile Beladungskontrolle
|
||||
|
||||
|
||||
@@ -724,3 +724,154 @@ Keine Commits in dieser Session.
|
||||
- frontend/package.json | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-10 01:02 – 01:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** backend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/alembic/versions/0035_akte_notiz.py | 34 ++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/notiz.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/notiz.py | 22 +++++++++++++++++++++
|
||||
- backend/app/schemas/notiz.py | 22 +++++++++++++++++++++
|
||||
- backend/app/services/notiz.py | 38 ++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/test_notiz.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 9 +++++++++
|
||||
- frontend/src/components/NotizenPanel.tsx | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/AktePage.tsx | 6 ++++++
|
||||
- frontend/src/styles/global/akte-grid.css | 2 ++
|
||||
- frontend/src/styles/global/responsive.css | 1 +
|
||||
|
||||
---
|
||||
## 2026-09-10 08:40 – 08:43 (3m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-10 10:05 – 10:13 (8m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 76a984a fix(tests): login als Helper-Funktion statt Fixture importieren (CI-Fix FOUND-007)
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/tests/test_import_export.py | 10 +++++-----
|
||||
|
||||
---
|
||||
## 2026-09-10 10:38 – 10:40 (2m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/10_maintenance.md | 9 ++++++++-
|
||||
- backend/alembic/versions/0036_tankbuch.py | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/tankbuch.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/tankbuch.py | 25 +++++++++++++++++++++++++
|
||||
- backend/app/schemas/tankbuch.py | 25 +++++++++++++++++++++++++
|
||||
- backend/app/services/tankbuch.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/test_tankbuch.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 11 +++++++++++
|
||||
- frontend/src/components/TankbuchPanel.tsx | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/AktePage.tsx | 8 ++++++++
|
||||
- frontend/src/styles/global/akte-grid.css | 1 +
|
||||
- frontend/src/styles/global/responsive.css | 1 +
|
||||
|
||||
---
|
||||
## 2026-09-10 11:17 – 11:20 (3m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** backend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/tests/test_akte.py | 1 +
|
||||
- backend/tests/test_historie.py | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-10 19:59 – 20:01 (2m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 8 ++++----
|
||||
- arbeitskacheln/09_inspections.md | 47 +++++++++++++++++++++++++++++++++++++++++++++--
|
||||
- backend/app/api/v1/endpoints/pruefung.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
- backend/app/schemas/pruefung.py | 12 ++++++++++++
|
||||
- backend/app/services/dashboard.py | 16 +++++++++++++++-
|
||||
- backend/app/services/pruefung.py | 35 ++++++++++++++++++++++++++++++++++-
|
||||
- backend/tests/test_insp_phase2.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 31 +++++++++++++++++++++++++++++++
|
||||
- frontend/src/components/status/FristBadge.tsx | 17 ++++++++++++-----
|
||||
- frontend/src/pages/AdminPage.tsx | 10 +++++++++-
|
||||
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
|
||||
- frontend/src/pages/admin/PruefartenSection.tsx | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 20:51 – 20:57 (5m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
- 777bf5e feat(flutter): öffentliche Download-Seite für Scan-App-Feldtest
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 12 ++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/app_download.py | 39 +++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/core/app_settings.py | 9 +++++++++
|
||||
- backend/tests/test_app_download.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/App.tsx | 2 ++
|
||||
- frontend/src/pages/AdminPage.tsx | 26 +++++++-------------------
|
||||
- frontend/src/pages/AppDownloadPage.tsx | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 20:57 – 20:57 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** backend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 12 ++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/app_download.py | 39 +++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/core/app_settings.py | 9 +++++++++
|
||||
- backend/tests/test_app_download.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/App.tsx | 2 ++
|
||||
- frontend/src/pages/AdminPage.tsx | 26 +++++++-------------------
|
||||
- frontend/src/pages/AppDownloadPage.tsx | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-12 21:31 – 21:33 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/components/ui/sidebar.tsx | 8 ++++----
|
||||
- frontend/src/pages/AdminPage.tsx | 2 +-
|
||||
|
||||
---
|
||||
@@ -0,0 +1,69 @@
|
||||
# Threat Model: MABEA-Backend
|
||||
|
||||
Stand: 2026-09-12
|
||||
Scope: gesamtes FastAPI-Backend (`backend/app`) — Auth/RBAC, Dokument-Upload/-Download, Systemeinstellungen, DB-Zugriff. Ausgeklammert: Frontend (React/PWA), Server-Härtung (nginx/systemd/Firewall — eigener Skill), Flutter-App.
|
||||
|
||||
## Systemüberblick
|
||||
|
||||
```
|
||||
Internet/Tablet (Client)
|
||||
│ HTTPS (nginx terminiert TLS)
|
||||
▼
|
||||
nginx (Rate-Limit /auth/login 5r/m, Reverse Proxy)
|
||||
│
|
||||
▼
|
||||
FastAPI-Backend (uvicorn, systemd)
|
||||
│ JWT Bearer (sub=login, exp, kein Rollen-Claim)
|
||||
▼
|
||||
Postgres (SQLAlchemy async)
|
||||
│
|
||||
▼
|
||||
Upload-Verzeichnis (lokales Filesystem, UUID-Dateinamen)
|
||||
│
|
||||
▼
|
||||
Sentry (extern, optional, DSN aus DB/ENV) — send_default_pii=False
|
||||
```
|
||||
|
||||
Trust-Boundaries:
|
||||
1. Client ↔ nginx/Backend (nicht vertrauenswürdig — jeder Request potenziell manipuliert)
|
||||
2. Backend ↔ Postgres (vertrauenswürdig, aber SQL-Injection-relevant bei Rohkonstrukten)
|
||||
3. Backend ↔ lokales Filesystem (Uploads)
|
||||
4. Backend ↔ externes Sentry (Datenabfluss-Grenze)
|
||||
|
||||
## Findings
|
||||
|
||||
| # | STRIDE | Komponente | Bedrohung | Schweregrad | Status | Gegenmaßnahme |
|
||||
|---|--------|------------|-----------|--------------|--------|----------------|
|
||||
| 1 | Spoofing | `auth.py` Login | Login-Enumeration per Antwortzeit (Timing-Angriff) | Mittel | **abgedeckt** | `verify_password_konstante_zeit` + Dummy-Hash (`security.py:11-27`) |
|
||||
| 2 | Spoofing | JWT | Schwacher/Default-Secret in Produktion | Kritisch | **abgedeckt** | Startup-Guard in `main.py:20-24` verweigert Boot bei Platzhalter-Secret |
|
||||
| 3 | Elevation of Privilege | `deps.py` Rollen | Rollenänderung wirkt erst nach Token-Ablauf, falls Rollen im JWT gecacht wären | Niedrig | **abgedeckt** | Bewusst keine Rollen im Token, `get_current_user` liest live aus DB (`deps.py:30-38`, `security.py:31-32`) |
|
||||
| 4 | Elevation of Privilege | `dokument.py` Download/Versionen | IDOR: `dokument_id` per UUID direkt aus Pfad, jeder eingeloggte Nutzer könnte fremde Dokumente abrufen | **Hoch** | **teilweise offen** | Nur Rollen-Check für eingeschränkte Dokumenttypen (`_pruefe_eingeschraenkten_zugriff`), aber **kein Check, ob der Nutzer überhaupt Zugriff auf die zugehörige Entität (`entitaet_typ`/`entitaet_id`, z.B. Standort/Objekt) hat** — jeder authentifizierte Nutzer mit `dokument_id` (UUID, aber z.B. aus geteiltem Link/Referer/Logs erratbar) kann `GET /dokumente/{id}/download` und `/versionen` aufrufen |
|
||||
| 5 | Denial of Service | Uploads (`dokument.py`) | Kein Rate-Limit auf `/dokumente` POST — viele große Uploads (bis `max_upload_size_mb`) könnten Storage/CPU (SHA-256-Hashing) erschöpfen | Mittel | fehlt | Rate-Limit analog `auth/login` (`slowapi`) auf Upload-Endpunkt erwägen |
|
||||
| 6 | Information Disclosure | `dokument.py` Upload-Antwort/Fehler | Bei `DateityperlaubtError`/`DateizugrossError` — prüfen, ob Exception-Handler generische Meldung zurückgibt (nicht Stacktrace) | Niedrig | nicht geprüft (Exception-Handler außerhalb Scope dieser Analyse) | Global Exception-Handler auf generische 4xx-Meldung ohne Interna prüfen |
|
||||
| 7 | Tampering | Uploads | Whitelist MIME-Type serverseitig, aber `mime_type` kommt aus `UploadFile.content_type` (Client-gesteuert, fälschbar) — ein `.html`/`.svg` mit falschem `Content-Type: application/pdf` könnte durchrutschen | Mittel | fehlt | Magic-Bytes-Prüfung (z.B. `python-magic`) zusätzlich zum Client-Content-Type, bevor Datei akzeptiert wird |
|
||||
| 8 | Spoofing/CSRF | Bearer-Token allgemein | Kein CORS-Middleware im Code gefunden (`grep CORSMiddleware` leer) | Mittel | zu klären | Falls Frontend von anderer Origin lädt (z.B. während Dev), fehlt CORS-Konfiguration → Requests würden von Browsern blockiert (eher Verfügbarkeits- als Sicherheitsproblem) oder, falls doch `allow_origins=["*"]` irgendwo gesetzt ist (nicht gefunden), wäre das ein Credential-Leak-Risiko. Mit reinem Bearer-Token (kein Cookie) ist CSRF strukturell kein Thema — sauber |
|
||||
| 9 | Repudiation | Kritische Aktionen (Dokument löschen, Rollen ändern, Materialbewegungen) | Kein zentrales Audit-Log erkennbar auf Modell-Ebene — nur `historie.py` als eigenes Modell, dessen Abdeckung nicht verifiziert wurde | Mittel | teilweise (historie.py existiert) | Prüfen, ob `historie`-Modell alle sicherheitsrelevanten Aktionen (Löschung, Rollenvergabe, Sentry-DSN-Änderung) tatsächlich mitschreibt |
|
||||
| 10 | Denial of Service | `auth/login` | App-seitiges Rate-Limit (`5/minute`) nur per `get_remote_address` — hinter NAT/Firewall teilen sich viele Nutzer eine IP, ein böswilliger Nutzer kann andere aussperren (Fail2ban-ähnlicher Nebeneffekt) | Niedrig | bekannter Trade-off | Dokumentiert als Verteidigung-in-der-Tiefe, primärer Schutz ist nginx — akzeptabel für Zielgruppe (Rettungsdienst-Standorte, kein Massenpublikum) |
|
||||
| 11 | Information Disclosure | `/einstellungen/sentry` GET | Bewusst ohne Auth erreichbar | Niedrig | **abgedeckt/bewusst** | Begründet: DSN kein Geheimnis, muss vor Login ladbar sein (Kommentar in Code) — korrekt eingeschätzt |
|
||||
| 12 | Elevation of Privilege | `permission.py` Custom-Rollen | `BenutzerRolleZuordnung` PK ist `(benutzer_id, rolle_id)` — keine Mehrfachzuweisung derselben Rolle an verschiedenen Standorten möglich | Niedrig | bekannte Einschränkung (dokumentiert im Code) | Kein Sicherheitsrisiko, nur funktionale Grenze — kein Handlungsbedarf aus Security-Sicht |
|
||||
|
||||
## Bereits abgedeckt
|
||||
|
||||
- Timing-sicherer Login (`security.py`)
|
||||
- Startup-Guard gegen Default-JWT-Secret (`main.py`)
|
||||
- Whitelist-Ansatz bei Datei-Uploads (MIME-Type, Größe, serverseitig generierter Dateiname gegen Path-Traversal) (`services/dokument.py`)
|
||||
- Rollen/Berechtigungen frisch aus DB pro Request, kein Stale-Token-Problem
|
||||
- Granulares Berechtigungssystem zusätzlich zum festen 4-Rollen-System, inkl. Standort-Scoping (`ZUST-003`)
|
||||
- Rate-Limiting auf Login (defense in depth: nginx + slowapi)
|
||||
- Sentry ohne PII (`send_default_pii=False`), DSGVO-bewusst kommentiert
|
||||
- SHA-256-Duplikaterkennung bei Uploads
|
||||
|
||||
## Offene Empfehlungen (priorisiert)
|
||||
|
||||
1. **Hoch** — IDOR bei Dokument-Download/Versionen (`dokument.py:113-126,175-186`): Zugriff zusätzlich gegen Berechtigung auf die referenzierte Entität (`entitaet_typ`/`entitaet_id`, z.B. Standort des Objekts) prüfen, nicht nur gegen den Dokumenttyp.
|
||||
2. **Mittel** — Magic-Bytes-Validierung bei Upload ergänzen, nicht nur auf Client-`Content-Type` vertrauen.
|
||||
3. **Mittel** — Rate-Limit auf `/dokumente` POST (Upload) ergänzen.
|
||||
4. **Mittel** — Verifizieren, dass `historie`-Modell sicherheitsrelevante Aktionen (Löschen, Rollenvergabe, Sentry-DSN-Änderung) tatsächlich protokolliert.
|
||||
5. **Niedrig** — Prüfen, ob CORS explizit konfiguriert ist (aktuell keine `CORSMiddleware` gefunden) — falls Frontend je von separater Origin ausgeliefert wird, Konfiguration nachtragen statt improvisiert `allow_origins=["*"]` zu setzen.
|
||||
|
||||
Kein Ticket automatisch angelegt — bei Bedarf sag Bescheid, dann lege ich Kacheln zu Punkt 1–3 im Backlog an.
|
||||
@@ -47,3 +47,47 @@ Keine Commits in dieser Session.
|
||||
- flutter_app/pubspec.yaml | 3 +
|
||||
|
||||
---
|
||||
## 2026-09-10 20:17 – 20:23 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 2 +-
|
||||
- arbeitskacheln/15_mobile.md | 36 +++++++++++++++++++++++++++++++++---
|
||||
- frontend/src/components/status/SyncStatus.tsx | 9 ++++++---
|
||||
- frontend/src/offline/mangelQueue.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/useOnlineStatus.ts | 13 +++++++++++--
|
||||
- frontend/src/pages/MangelListePage.tsx | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
|
||||
|
||||
---
|
||||
## 2026-09-10 20:23 – 20:23 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** flutter_app
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 2 +-
|
||||
- arbeitskacheln/15_mobile.md | 36 +++++++++++++++++++++++++++++++++---
|
||||
- frontend/src/components/status/SyncStatus.tsx | 9 ++++++---
|
||||
- frontend/src/offline/mangelQueue.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/useOnlineStatus.ts | 13 +++++++++++--
|
||||
- frontend/src/pages/MangelListePage.tsx | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
|
||||
|
||||
---
|
||||
## 2026-09-12 21:35 – 21:36 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/components/ui/sidebar.tsx | 8 ++++----
|
||||
- frontend/src/pages/AdminPage.tsx | 2 +-
|
||||
|
||||
---
|
||||
@@ -28,11 +28,12 @@ flutter build apk
|
||||
(`192.168.1.238`) gesetzt. Für Produktivbetrieb auf `--dart-define` umstellen
|
||||
oder zumindest vor jedem Build prüfen/anpassen.
|
||||
|
||||
## Kamera-Berechtigung (Kamera-Scan, Karte 10)
|
||||
## Kamera-Berechtigung (Kamera-Scan, Karte 10; Mangel-Foto, MOBILE-004)
|
||||
|
||||
`mobile_scanner` braucht die Kamera-Berechtigung im generierten `android/`-Ordner
|
||||
(existiert erst nach `flutter create .`, siehe oben) - nach dem Erstlauf einmalig
|
||||
in `android/app/src/main/AndroidManifest.xml` ergänzen, oberhalb von `<application>`:
|
||||
`mobile_scanner` (QR-Scan) UND `image_picker` (Mangel-Foto) brauchen die Kamera-
|
||||
Berechtigung im generierten `android/`-Ordner (existiert erst nach `flutter
|
||||
create .`, siehe oben) - nach dem Erstlauf einmalig in
|
||||
`android/app/src/main/AndroidManifest.xml` ergänzen, oberhalb von `<application>`:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -169,6 +170,35 @@ class ApiClient {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
/// Multipart-Upload wie POST /dokumente (Backend: app/api/v1/endpoints/
|
||||
/// dokument.py) - eigener Pfad statt request(), da JSON-Body dort nicht
|
||||
/// passt (multipart/form-data mit Datei-Anhang).
|
||||
Future<Map<String, dynamic>> ladeDokumentHoch({
|
||||
required String entitaetTyp,
|
||||
required String entitaetId,
|
||||
required String dokumenttyp,
|
||||
required bool istOriginal,
|
||||
required File datei,
|
||||
String? beschreibung,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl/dokumente');
|
||||
final anfrage = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll({if (_token != null) 'Authorization': 'Bearer $_token'})
|
||||
..fields['entitaet_typ'] = entitaetTyp
|
||||
..fields['entitaet_id'] = entitaetId
|
||||
..fields['dokumenttyp'] = dokumenttyp
|
||||
..fields['ist_original'] = istOriginal.toString()
|
||||
..files.add(await http.MultipartFile.fromPath('datei', datei.path));
|
||||
if (beschreibung != null) anfrage.fields['beschreibung'] = beschreibung;
|
||||
|
||||
final gestreamt = await _http.send(anfrage);
|
||||
final antwort = await http.Response.fromStream(gestreamt);
|
||||
if (antwort.statusCode >= 400) {
|
||||
throw ApiException(antwort.statusCode, _tryDecode(antwort.body));
|
||||
}
|
||||
return jsonDecode(antwort.body) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
dynamic _tryDecode(String body) {
|
||||
try {
|
||||
return jsonDecode(body);
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
|
||||
/// Android-Pendant zu frontend/src/offline/mangelQueue.ts (MOBILE-003-Ausweitung):
|
||||
/// Mangelmeldung ist neben der Kontroll-Erfassung die häufigste Feld-Aktion,
|
||||
/// soll also genau wie diese nie an einer kurzen Funklücke scheitern. Das
|
||||
/// Foto wird - anders als in der PWA (File/Blob per IndexedDB) - als Kopie in
|
||||
/// ein App-eigenes Verzeichnis abgelegt (sqflite kennt keine Blobs im selben
|
||||
/// strukturierten Sinn, ein Dateipfad ist hier robuster) und erst NACH
|
||||
/// erfolgreicher Mangel-Erstellung separat hochgeladen.
|
||||
class MangelQueueItem {
|
||||
MangelQueueItem({
|
||||
required this.key,
|
||||
required this.objektId,
|
||||
this.entitaetTyp,
|
||||
this.entitaetId,
|
||||
required this.beschreibung,
|
||||
required this.prioritaet,
|
||||
this.fotoPfad,
|
||||
this.status = 'wartet',
|
||||
this.fehlerText,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final int objektId;
|
||||
final String? entitaetTyp;
|
||||
final String? entitaetId;
|
||||
final String beschreibung;
|
||||
final String prioritaet;
|
||||
final String? fotoPfad;
|
||||
String status; // wartet | wird_uebertragen | fehler
|
||||
String? fehlerText;
|
||||
|
||||
Map<String, dynamic> toRow() => {
|
||||
'key': key,
|
||||
'objekt_id': objektId,
|
||||
'entitaet_typ': entitaetTyp,
|
||||
'entitaet_id': entitaetId,
|
||||
'beschreibung': beschreibung,
|
||||
'prioritaet': prioritaet,
|
||||
'foto_pfad': fotoPfad,
|
||||
'status': status,
|
||||
'fehler_text': fehlerText,
|
||||
};
|
||||
|
||||
factory MangelQueueItem.fromRow(Map<String, dynamic> row) => MangelQueueItem(
|
||||
key: row['key'] as String,
|
||||
objektId: row['objekt_id'] as int,
|
||||
entitaetTyp: row['entitaet_typ'] as String?,
|
||||
entitaetId: row['entitaet_id'] as String?,
|
||||
beschreibung: row['beschreibung'] as String,
|
||||
prioritaet: row['prioritaet'] as String,
|
||||
fotoPfad: row['foto_pfad'] as String?,
|
||||
status: row['status'] as String,
|
||||
fehlerText: row['fehler_text'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class MangelQueue {
|
||||
MangelQueue._();
|
||||
static final MangelQueue instance = MangelQueue._();
|
||||
|
||||
Database? _db;
|
||||
bool _synchronisierungLaeuft = false;
|
||||
|
||||
Future<Database> _database() async {
|
||||
final bestehend = _db;
|
||||
if (bestehend != null) return bestehend;
|
||||
final pfad = p.join(await getDatabasesPath(), 'mabea_mangel_queue.db');
|
||||
final db = await openDatabase(
|
||||
pfad,
|
||||
version: 1,
|
||||
onCreate: (db, version) => db.execute('''
|
||||
CREATE TABLE mangel_queue (
|
||||
key TEXT PRIMARY KEY,
|
||||
objekt_id INTEGER NOT NULL,
|
||||
entitaet_typ TEXT,
|
||||
entitaet_id TEXT,
|
||||
beschreibung TEXT NOT NULL,
|
||||
prioritaet TEXT NOT NULL,
|
||||
foto_pfad TEXT,
|
||||
status TEXT NOT NULL,
|
||||
fehler_text TEXT
|
||||
)
|
||||
'''),
|
||||
);
|
||||
_db = db;
|
||||
return db;
|
||||
}
|
||||
|
||||
Future<void> enqueue({
|
||||
required int objektId,
|
||||
String? entitaetTyp,
|
||||
String? entitaetId,
|
||||
required String beschreibung,
|
||||
required String prioritaet,
|
||||
String? fotoPfad,
|
||||
}) async {
|
||||
final db = await _database();
|
||||
final key = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
await db.insert(
|
||||
'mangel_queue',
|
||||
MangelQueueItem(
|
||||
key: key,
|
||||
objektId: objektId,
|
||||
entitaetTyp: entitaetTyp,
|
||||
entitaetId: entitaetId,
|
||||
beschreibung: beschreibung,
|
||||
prioritaet: prioritaet,
|
||||
fotoPfad: fotoPfad,
|
||||
).toRow(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<MangelQueueItem>> alleEintraege() async {
|
||||
final db = await _database();
|
||||
final rows = await db.query('mangel_queue');
|
||||
return rows.map(MangelQueueItem.fromRow).toList();
|
||||
}
|
||||
|
||||
Future<void> entferneFehlerEintrag(String key) async {
|
||||
MangelQueueItem? item;
|
||||
for (final e in await alleEintraege()) {
|
||||
if (e.key == key) item = e;
|
||||
}
|
||||
final db = await _database();
|
||||
await db.delete('mangel_queue', where: 'key = ?', whereArgs: [key]);
|
||||
final fotoPfad = item?.fotoPfad;
|
||||
if (fotoPfad != null) {
|
||||
final datei = File(fotoPfad);
|
||||
if (await datei.exists()) await datei.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setzeStatus(String key, String status, {String? fehlerText}) async {
|
||||
final db = await _database();
|
||||
await db.update(
|
||||
'mangel_queue',
|
||||
{'status': status, 'fehler_text': fehlerText},
|
||||
where: 'key = ?',
|
||||
whereArgs: [key],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendeEintrag(ApiClient apiClient, MangelQueueItem item) async {
|
||||
await _setzeStatus(item.key, 'wird_uebertragen');
|
||||
try {
|
||||
final mangel = await apiClient.request(
|
||||
'/maengel',
|
||||
method: 'POST',
|
||||
body: {
|
||||
'objekt_id': item.objektId,
|
||||
'entitaet_typ': item.entitaetTyp,
|
||||
'entitaet_id': item.entitaetId,
|
||||
'beschreibung': item.beschreibung,
|
||||
'prioritaet': item.prioritaet,
|
||||
},
|
||||
) as Map<String, dynamic>;
|
||||
|
||||
final fotoPfad = item.fotoPfad;
|
||||
if (fotoPfad != null && await File(fotoPfad).exists()) {
|
||||
try {
|
||||
await apiClient.ladeDokumentHoch(
|
||||
entitaetTyp: 'mangel',
|
||||
entitaetId: mangel['id'] as String,
|
||||
dokumenttyp: 'sonstiges',
|
||||
istOriginal: true,
|
||||
datei: File(fotoPfad),
|
||||
);
|
||||
} catch (_) {
|
||||
// Mangel selbst ist bereits erfolgreich angelegt (Kernnutzen erfüllt) -
|
||||
// ein fehlgeschlagener Foto-Upload darf den Eintrag nicht in der Queue
|
||||
// hängen lassen, sonst wird derselbe Mangel bei jedem Sync-Versuch neu
|
||||
// angelegt. Foto-Verlust ist bewusst der kleinere Schaden hier
|
||||
// (gleiche Abwägung wie in der PWA, mangelQueue.ts).
|
||||
}
|
||||
await File(fotoPfad).delete().catchError((_) => File(fotoPfad));
|
||||
}
|
||||
|
||||
final db = await _database();
|
||||
await db.delete('mangel_queue', where: 'key = ?', whereArgs: [item.key]);
|
||||
} on ApiException catch (error) {
|
||||
if (error.status >= 400 && error.status < 500) {
|
||||
await _setzeStatus(
|
||||
item.key,
|
||||
'fehler',
|
||||
fehlerText: 'Server hat abgelehnt (${error.status}) - bitte erneut versuchen.',
|
||||
);
|
||||
} else {
|
||||
await _setzeStatus(item.key, 'wartet');
|
||||
}
|
||||
rethrow;
|
||||
} catch (error) {
|
||||
await _setzeStatus(item.key, 'wartet');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> synchronisiere(ApiClient apiClient) async {
|
||||
if (_synchronisierungLaeuft) return;
|
||||
_synchronisierungLaeuft = true;
|
||||
try {
|
||||
final eintraege = await alleEintraege();
|
||||
for (final item in eintraege) {
|
||||
if (item.status == 'fehler') continue; // braucht bewusste Nutzer-Aktion
|
||||
try {
|
||||
await _sendeEintrag(apiClient, item);
|
||||
} on ApiException {
|
||||
// schon in _sendeEintrag behandelt, weiter mit dem nächsten Eintrag.
|
||||
} catch (_) {
|
||||
return; // vermutlich offline - restliche Queue in Ruhe lassen
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_synchronisierungLaeuft = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../offline/queue.dart';
|
||||
import 'mangel_melden_screen.dart';
|
||||
|
||||
/// Prompt 11 Screens 2-6 + Prompt 17 Offline-Härtung: Eingaben laufen über
|
||||
/// OfflineQueue (sqflite), nie direkt blockierend - gleiches Muster wie die
|
||||
@@ -244,7 +245,20 @@ class _KontrollScreenState extends State<KontrollScreen> {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_objekt?['name'] as String? ?? 'Kontrolle')),
|
||||
appBar: AppBar(
|
||||
title: Text(_objekt?['name'] as String? ?? 'Kontrolle'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.report_problem_outlined),
|
||||
tooltip: 'Mangel melden',
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MangelMeldenScreen(apiClient: widget.apiClient, objektId: widget.objektId),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (!_online)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../offline/mangel_queue.dart';
|
||||
|
||||
/// MOBILE-004-Nachzug: Android-Pendant zu frontend/src/pages/MangelListePage.tsx
|
||||
/// (Melde-Teil) - läuft wie die Kontroll-Erfassung immer über die lokale
|
||||
/// Queue (MangelQueue), nie direkt blockierend gegen die API.
|
||||
class MangelMeldenScreen extends StatefulWidget {
|
||||
const MangelMeldenScreen({super.key, required this.apiClient, required this.objektId});
|
||||
final ApiClient apiClient;
|
||||
final int objektId;
|
||||
|
||||
@override
|
||||
State<MangelMeldenScreen> createState() => _MangelMeldenScreenState();
|
||||
}
|
||||
|
||||
class _MangelMeldenScreenState extends State<MangelMeldenScreen> {
|
||||
final _beschreibungController = TextEditingController();
|
||||
String _prioritaet = 'normal';
|
||||
File? _foto;
|
||||
bool _wirdGespeichert = false;
|
||||
String? _fehler;
|
||||
bool _online = true;
|
||||
|
||||
final _connectivity = Connectivity();
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySub;
|
||||
Timer? _periodischerSyncTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectivity.checkConnectivity().then((ergebnis) {
|
||||
if (mounted) setState(() => _online = !ergebnis.contains(ConnectivityResult.none));
|
||||
});
|
||||
_connectivitySub = _connectivity.onConnectivityChanged.listen((ergebnis) {
|
||||
final online = !ergebnis.contains(ConnectivityResult.none);
|
||||
if (online && !_online) {
|
||||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||||
}
|
||||
if (mounted) setState(() => _online = online);
|
||||
});
|
||||
_periodischerSyncTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connectivitySub?.cancel();
|
||||
_periodischerSyncTimer?.cancel();
|
||||
_beschreibungController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fotoAufnehmen() async {
|
||||
final bild = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70);
|
||||
if (bild == null || !mounted) return;
|
||||
setState(() => _foto = File(bild.path));
|
||||
}
|
||||
|
||||
Future<void> _melden() async {
|
||||
final beschreibung = _beschreibungController.text.trim();
|
||||
if (beschreibung.isEmpty) {
|
||||
setState(() => _fehler = 'Beschreibung darf nicht leer sein.');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_wirdGespeichert = true;
|
||||
_fehler = null;
|
||||
});
|
||||
|
||||
// Foto in App-eigenes Verzeichnis kopieren - die von image_picker gelieferte
|
||||
// Datei liegt in einem temporären Cache, der vom System jederzeit geräumt
|
||||
// werden kann, während der Mangel noch in der Queue auf Sync wartet.
|
||||
String? fotoPfad;
|
||||
final foto = _foto;
|
||||
if (foto != null) {
|
||||
final verzeichnis = await getApplicationDocumentsDirectory();
|
||||
final ziel = p.join(
|
||||
verzeichnis.path,
|
||||
'mangel_foto_${DateTime.now().microsecondsSinceEpoch}${p.extension(foto.path)}',
|
||||
);
|
||||
await foto.copy(ziel);
|
||||
fotoPfad = ziel;
|
||||
}
|
||||
|
||||
await MangelQueue.instance.enqueue(
|
||||
objektId: widget.objektId,
|
||||
beschreibung: beschreibung,
|
||||
prioritaet: _prioritaet,
|
||||
fotoPfad: fotoPfad,
|
||||
);
|
||||
unawaited(MangelQueue.instance.synchronisiere(widget.apiClient));
|
||||
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Mangel melden')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!_online)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Colors.amber.shade100,
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: const Text(
|
||||
'Kein Netz – Meldung wird lokal gespeichert und automatisch übertragen, '
|
||||
'sobald wieder Verbindung besteht.',
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _beschreibungController,
|
||||
decoration: const InputDecoration(labelText: 'Beschreibung'),
|
||||
maxLines: 4,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _prioritaet,
|
||||
decoration: const InputDecoration(labelText: 'Priorität'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'niedrig', child: Text('Niedrig')),
|
||||
DropdownMenuItem(value: 'normal', child: Text('Normal')),
|
||||
DropdownMenuItem(value: 'hoch', child: Text('Hoch')),
|
||||
],
|
||||
onChanged: (wert) => setState(() => _prioritaet = wert ?? 'normal'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _fotoAufnehmen,
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: Text(_foto == null ? 'Foto aufnehmen' : 'Foto ersetzen'),
|
||||
),
|
||||
if (_foto != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Image.file(_foto!, height: 120),
|
||||
),
|
||||
if (_fehler != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(_fehler!, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: _wirdGespeichert ? null : _melden,
|
||||
child: const Text('Melden'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import 'kontroll_screen.dart';
|
||||
import 'mangel_melden_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
|
||||
/// Prompt 11 Screen 1: Standort/Objekt wählen. Objekt-Code (Karte 10) →
|
||||
@@ -118,6 +119,18 @@ class _ObjektListScreenState extends State<ObjektListScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.report_problem_outlined),
|
||||
tooltip: 'Mangel melden',
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MangelMeldenScreen(
|
||||
apiClient: widget.apiClient,
|
||||
objektId: objekt['id'] as int,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -15,6 +15,8 @@ dependencies:
|
||||
sqflite: ^2.3.3
|
||||
path: ^1.9.0
|
||||
connectivity_plus: ^6.0.5
|
||||
image_picker: ^1.1.2
|
||||
path_provider: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -905,3 +905,390 @@ Keine Commits in dieser Session.
|
||||
- frontend/package.json | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-10 08:38 – 08:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 6 ++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 10:25 – 10:29 (3m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/api/v1/api.py | 8 +++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 11:21 – 11:50 (29m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** backend
|
||||
|
||||
### Commits
|
||||
- 1147ec0 feat(ui): UI2-001/006/007/009/010 Design-Tokens, Toast, AlertDialog, Formular-a11y
|
||||
- eeb87d8 feat(permission): ZUST-003 Standort-Scoping für granulare Rollen-Berechtigungen
|
||||
|
||||
### Geänderte Dateien
|
||||
- .claude/agents/flutter-expert.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- .claude/agents/ios-developer.md | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- .claude/agents/swift-expert.md | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- arbeitskacheln/22_ui_redesign_v2.md | 53 +++++++++++++++++++++++++++++++++++++----
|
||||
- frontend/src/App.tsx | 3 +++
|
||||
- frontend/src/components/TankbuchPanel.tsx | 20 +++++++++++++++-
|
||||
- frontend/src/components/ui/alert-dialog.tsx | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/components/ui/form-field.tsx | 38 ++++++++++++++++++++++++++++++
|
||||
- frontend/src/components/ui/toast.tsx | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/styles/global/cards.css | 3 ++-
|
||||
- frontend/src/styles/global/tokens.css | 11 +++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 12:11 – 12:13 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** timemaster
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 2 +-
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
|
||||
|
||||
---
|
||||
## 2026-09-10 12:28 – 12:36 (7m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/18_notifications.md | 20 +++++++++++++++++++-
|
||||
- backend/alembic/versions/0041_wartung_benachrichtigung.py | 33 +++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/endpoints/dashboard.py | 12 ++++++++++++
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/wartung_benachrichtigung.py | 30 ++++++++++++++++++++++++++++++
|
||||
- backend/app/schemas/dashboard.py | 16 ++++++++++++++++
|
||||
- backend/app/services/benachrichtigung.py | 18 ++++++++++++++++++
|
||||
- backend/app/services/wartung.py | 29 +++++++++++++++++++++++++++++
|
||||
- backend/app/services/wartung_benachrichtigung.py | 43 +++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/scripts/wartung_benachrichtigung_pruefen.py | 29 +++++++++++++++++++++++++++++
|
||||
- backend/tests/test_wartung_benachrichtigung.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/dashboard/DashboardPage.tsx | 4 ++++
|
||||
- frontend/src/pages/dashboard/layout.ts | 5 ++++-
|
||||
- frontend/src/pages/dashboard/tiles/FaelligeWartungenTile.tsx | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 12:42 – 12:49 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/22_ui_redesign_v2.md | 37 +++++++++++++++++++++++++++++++++----
|
||||
- frontend/src/components/TankbuchPanel.tsx | 5 +++--
|
||||
- frontend/src/components/ui/sheet.tsx | 43 +++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/components/ui/skeleton.tsx | 8 ++++++++
|
||||
- frontend/src/components/ui/table-wrapper.tsx | 8 ++++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 48 ++++++++++++++++++++++++++++++++++++++----------
|
||||
- frontend/src/pages/AktePage.tsx | 5 +++--
|
||||
- frontend/src/pages/admin/AusgabeSection.tsx | 5 +++--
|
||||
- frontend/src/pages/admin/HistorieSection.tsx | 5 +++--
|
||||
- frontend/src/pages/admin/LagerbewegungSection.tsx | 5 +++--
|
||||
- frontend/src/pages/admin/ObjektAnlegenFormular.tsx | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------
|
||||
- frontend/src/pages/admin/ObjekttypSection.tsx | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
|
||||
- frontend/src/pages/dashboard/tiles/FaelligeWartungenTile.tsx | 10 +++++++++-
|
||||
- frontend/src/pages/dashboard/tiles/ReadinessVerlaufTile.tsx | 10 +++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 12:55 – 13:01 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/13_readiness.md | 25 +++++++++++++++++++------
|
||||
- backend/alembic/versions/0042_readiness_regel.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/readiness_regel.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/readiness_regel.py | 24 ++++++++++++++++++++++++
|
||||
- backend/app/schemas/readiness_regel.py | 12 ++++++++++++
|
||||
- backend/app/services/dashboard.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++--------
|
||||
- backend/tests/test_readiness_regel.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 6 ++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 6 +++++-
|
||||
- frontend/src/pages/admin/ReadinessRegelSection.tsx | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 17:31 – 17:39 (8m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/04_assets.md | 15 +++++++++------
|
||||
- backend/alembic/versions/0049_ressourcenbuchung.py | 37 +++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/ressourcenbuchung.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/ressourcenbuchung.py | 24 ++++++++++++++++++++++++
|
||||
- backend/app/schemas/ressourcenbuchung.py | 22 ++++++++++++++++++++++
|
||||
- backend/app/services/ressourcenbuchung.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/test_ressourcenbuchung.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 10 ++++++++++
|
||||
- frontend/src/components/RessourcenbuchungPanel.tsx | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/AktePage.tsx | 6 ++++++
|
||||
- frontend/src/styles/global/akte-grid.css | 2 ++
|
||||
- frontend/src/styles/global/responsive.css | 1 +
|
||||
|
||||
---
|
||||
## 2026-09-10 19:25 – 19:33 (7m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 18 +++++++--------
|
||||
- arbeitskacheln/07_warehouse.md | 17 +++++++++++---
|
||||
- backend/alembic/versions/0052_inventur.py | 57 +++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/inventur.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/models/__init__.py | 4 ++++
|
||||
- backend/app/models/inventur.py | 57 +++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/schemas/inventur.py | 45 ++++++++++++++++++++++++++++++++++++
|
||||
- backend/app/services/inventur.py | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- backend/tests/test_wh006_inventur.py | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/api/types.ts | 28 +++++++++++++++++++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 6 ++++-
|
||||
- frontend/src/pages/admin/InventurSection.tsx | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 19:43 – 19:52 (9m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/00_index.md | 2 +-
|
||||
- arbeitskacheln/01_foundation.md | 20 +++++++++++++++++---
|
||||
- backend/alembic/versions/0053_systemeinstellung.py | 31 +++++++++++++++++++++++++++++++
|
||||
- backend/app/api/v1/api.py | 2 ++
|
||||
- backend/app/api/v1/endpoints/systemeinstellung.py | 30 ++++++++++++++++++++++++++++++
|
||||
- backend/app/core/app_settings.py | 5 +++++
|
||||
- backend/app/main.py | 17 ++++++++++++++++-
|
||||
- backend/app/models/__init__.py | 2 ++
|
||||
- backend/app/models/systemeinstellung.py | 16 ++++++++++++++++
|
||||
- backend/app/schemas/systemeinstellung.py | 10 ++++++++++
|
||||
- backend/app/services/systemeinstellung.py | 27 +++++++++++++++++++++++++++
|
||||
- backend/pyproject.toml | 1 +
|
||||
- backend/tests/test_found013_systemeinstellung.py | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
- deploy/README.md | 28 ++++++++++++++++++++++++++++
|
||||
- deploy/glitchtip-install.sh | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/package-lock.json | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/package.json | 1 +
|
||||
- frontend/src/main.tsx | 3 +++
|
||||
- frontend/src/monitoring/sentry.ts | 20 ++++++++++++++++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 6 +++++-
|
||||
- frontend/src/pages/admin/FehlerTrackingSection.tsx | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-10 20:07 – 20:14 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/02_identity.md | 7 +++++++
|
||||
- backend/app/services/label.py | 21 +++++++++++++--------
|
||||
|
||||
---
|
||||
## 2026-09-10 20:14 – 20:14 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/02_identity.md | 7 +++++++
|
||||
- backend/app/services/label.py | 21 +++++++++++++--------
|
||||
|
||||
---
|
||||
## 2026-09-10 20:37 – 20:39 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 66 ++++++++++++++++++++++++++++++++++++++++++++----------------------
|
||||
|
||||
---
|
||||
## 2026-09-10 20:47 – 20:51 (3m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 7 +++++++
|
||||
- frontend/src/pages/AdminPage.tsx | 33 +++++++++++++++++++++++++++------
|
||||
|
||||
---
|
||||
## 2026-09-10 21:12 – 21:36 (24m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 3d56073 docs(flutter): FLUT-001 v0.7.0 - Akte-Details, Taschenlampe, Geräte-Code
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 9 ++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 21:36 – 21:36 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 9 ++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 21:38 – 21:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 9 ++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 21:39 – 21:44 (4m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 9 ++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 21:44 – 21:44 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 9 ++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-10 22:51 – 22:52 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 11 +++++++++--
|
||||
|
||||
---
|
||||
## 2026-09-10 22:53 – 23:00 (6m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- arbeitskacheln/23_flutter_begleitapp.md | 11 +++++++++--
|
||||
|
||||
---
|
||||
## 2026-09-10 23:11 – 23:11 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
|
||||
|
||||
---
|
||||
## 2026-09-10 23:12 – 23:12 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
|
||||
|
||||
---
|
||||
## 2026-09-11 14:45 – 14:46 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** backend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-11 15:28 – 15:28 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** frontend
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 2 +-
|
||||
|
||||
---
|
||||
## 2026-09-11 15:43 – 15:44 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/admin/ObjektAktionenFormulare.tsx | 77 +++++++++++++++++++++
|
||||
- frontend/src/pages/admin/ObjektKarte.tsx | 55 +++++++++++++++
|
||||
- frontend/src/pages/admin/ObjektSection.tsx | 279 ++++++++++++++++++++++++++++++++-------------------------------------------
|
||||
- frontend/src/pages/admin/ObjektZeile.tsx | 69 +++++++++++++++++++
|
||||
- frontend/src/pages/admin/objektZeileTypen.ts | 43 ++++++++++++
|
||||
|
||||
---
|
||||
Reference in new issue
Block a user