feat: mabea-doku Zwei-Apps-Support + Ressourcenbuchung Opt-in
Backend/Frontend-Seite: /app-download/info und /app-download/qr.png bekommen app-Query-Parameter (scan/doku), Download-Seite /app?app=... mit Umschalter für die beiden eigenständigen Flutter-Apps. ASSET-012 Nutzer-Feedback: Ressourcenbuchung war in jeder Akte sichtbar, unabhängig vom Objekttyp - jetzt Opt-in je Objekttyp (Objekttyp.ressourcenbuchung_aktiv, Migration 0054, Default false), Admin-Checkbox in ObjekttypSection.tsx, AktePage.tsx zeigt das Panel nur noch wenn aktiviert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
1 parent
3d560738de
commit
ce99aa6fbb
15 files changed
+231
-29
No files matched your search
@@ -478,6 +478,13 @@ Details zu ASSET-001 … ASSET-010. ASSET-010 ergänzt am 2026-09-09 aus Open-So
|
||||
(kein Kalender-Widget, wie in der Kachel als MVP-Ausreichend markiert) in
|
||||
der Akte JEDES Objekts (nicht nur Fahrzeuge — Material-Sets/Geräte können
|
||||
genauso gebucht werden). `backend/tests/test_ressourcenbuchung.py`.
|
||||
**Bugfix 2026-09-10 (Nutzer-Feedback):** "in jeder Akte" wurde als
|
||||
aufdringlich empfunden — meiste Objekttypen (Verbrauchsmaterial-Rucksäcke
|
||||
etc.) brauchen keinen Buchungskalender. Auf Opt-in je Objekttyp
|
||||
umgestellt: neues Feld `Objekttyp.ressourcenbuchung_aktiv` (Migration
|
||||
0054, Default `false`), Admin-Checkbox "Ressourcenbuchung in der Akte
|
||||
anzeigen" in `ObjekttypSection.tsx`, `AkteRead.ressourcenbuchung_aktiv`
|
||||
steuert die bedingte Anzeige des Panels in `AktePage.tsx`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ Build-Host, kein Dauerbetrieb der App dort (App läuft auf Endgeräten).
|
||||
Store-Veröffentlichung (Sideload/interner Test-Build reicht für die
|
||||
Sondierung).
|
||||
- **DoD:** ✅ **Prototyp läuft, iterativ über den ursprünglichen Scope hinaus
|
||||
gewachsen (2026-09-10, v0.1.0 → v0.7.0)** — Ergebnis noch nicht formal
|
||||
gewachsen (2026-09-10, mabea-scan v0.1.0 → v0.8.0, mabea-doku v0.1.0 →
|
||||
v0.2.0 als eigenständige zweite App)** — Ergebnis noch nicht formal
|
||||
bewertet (keine Grundsatzentscheidung Ergänzung-vs-Ablösung-PWA getroffen),
|
||||
aber der Prototyp selbst ist weit über "ein Screen QR-Scan" hinausgewachsen:
|
||||
- **v0.1.0** — Basis-Scope: Login-Screen (JWT), QR-Scan-Screen gegen
|
||||
@@ -90,13 +91,45 @@ Build-Host, kein Dauerbetrieb der App dort (App läuft auf Endgeräten).
|
||||
— ein normaler Frontend-Deploy mit `rsync --delete` hätte den Ordner sonst
|
||||
gelöscht, real passiert und behoben). Download-Link im Portal nur für
|
||||
Administrator sichtbar (`AdminPage.tsx`).
|
||||
- **v0.7.0 (2026-09-10):** mehr Scan-Funktionen — Akte-Kurzinfo nach
|
||||
- **Zwei-Apps-Entscheidung (2026-09-10):** Nutzer-Vorgabe — statt einer
|
||||
gemeinsamen App ZWEI eigenständige: **mabea-scan** (dieser Prototyp,
|
||||
FLUT-001, Inventur/Kontroll-fokussiert) und **mabea-doku** (neu,
|
||||
FLUT-003, Fristen-Übersicht + perspektivisch Erfassen). Getrennte
|
||||
Android-`applicationId`s (`de.mabea.mabea_scan_prototyp` /
|
||||
`de.perlbach.mabea.mabea_doku`), beide parallel installierbar, getrennte
|
||||
`version*.json`/APK-Dateien unter `/downloads/`. Backend-Endpunkte
|
||||
`/app-download/info` und `/app-download/qr.png` bekamen einen `app`-
|
||||
Query-Parameter (`scan` Default / `doku`), Portal-Seite `/app?app=scan`
|
||||
bzw. `/app?app=doku` mit Umschalter.
|
||||
- **mabea-doku v0.1.0/v0.2.0 (2026-09-10, FLUT-003, neues Projekt unter
|
||||
`/mnt/projekte/flutter/mabea_doku`):** Login (gleiches Muster wie
|
||||
mabea-scan: `flutter_secure_storage`, optionale Zugangsdaten-Speicherung
|
||||
per Checkbox, Auto-Relogin bei 401) + Fristen-Übersicht-Screen, der
|
||||
`/dashboard/prueftermine`, `/dashboard/ablaufdaten` und
|
||||
`/dashboard/faellige-wartungen` zusammenführt und nach Dringlichkeit
|
||||
sortiert (fehlgeschlagen/überfällig zuerst). In-App-Update-Banner (v0.2.0,
|
||||
identisches Muster wie mabea-scan) gegen `/downloads/version-doku.json`.
|
||||
**Phase 1 = reine Lesesicht.** Erfassen (Kontrolle/Mangel/Wartung direkt
|
||||
aus mabea-doku eintragen, vom Nutzer für eine Ausbaustufe gewünscht) ist
|
||||
bewusst NICHT Teil dieses ersten Wurfs — eigener, größerer Schritt
|
||||
(Formulare, Validierung, ggf. Offline-Queue analog PWA), noch nicht
|
||||
begonnen.
|
||||
- **v0.7.0/v0.8.0 (2026-09-10, mabea-scan):** mehr Scan-Funktionen — Akte-Kurzinfo nach
|
||||
Objekt-Scan (Einsatzbereitschafts-Ampel + Gründe, letzte Kontrolle,
|
||||
Anzahl offener Mängel, via `GET /akte/objekt/{id}`, Ladefehler nicht
|
||||
fatal — Basis-Objekt bleibt sichtbar); Taschenlampe-Toggle
|
||||
(`MobileScannerController.toggleTorch()`); Geräte-Exemplar-Code als
|
||||
Fallback, wenn kein Objekt-Code passt (`GET /objektpositionen/code/
|
||||
{code}` — Geräte-Etiketten waren bis dahin gar nicht scanbar).
|
||||
**v0.8.0:** "Kontrolle / Inventur"-Button im Objekt-Ergebnis — startet
|
||||
eine Kontrolle (`POST /objekte/{id}/kontrollen`, mit
|
||||
Übernahme-Dialog bei 409/bereits aktiver Kontrolle) und öffnet einen
|
||||
Erfassungs-Screen mit Ist-Mengen-Eingabe je Beladungsposition (einzeln
|
||||
speicherbar über `PUT /kontrollen/{id}/positionen/{material_id}`,
|
||||
Abschließen über `POST /kontrollen/{id}/abschliessen` mit Warnung bei
|
||||
unvollständiger Erfassung) — behebt die Lücke, dass nach dem Scan bis
|
||||
dahin nur Lesezugriff möglich war, obwohl mabea-scan explizit für die
|
||||
Inventur gedacht ist.
|
||||
- **Feldtest-Vorbereitung 2026-09-10:** öffentliche Download-Seite
|
||||
`/app` (KEIN Login nötig — Henne-Ei, die App IST der Login-Client)
|
||||
zeigt QR-Code + Download-Button + Installationsanleitung für Helfer.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""ASSET-012: Ressourcenbuchung Opt-in je Objekttyp statt global erzwungen.
|
||||
|
||||
Revision ID: 0054_ressourcenbuchung_optin
|
||||
Revises: 0053_systemeinstellung
|
||||
Create Date: 2026-09-10
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0054_ressourcenbuchung_optin"
|
||||
down_revision: Union[str, None] = "0053_systemeinstellung"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE objekttyp ADD COLUMN ressourcenbuchung_aktiv BOOLEAN NOT NULL DEFAULT false"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE objekttyp DROP COLUMN ressourcenbuchung_aktiv")
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response, status
|
||||
|
||||
@@ -8,32 +9,42 @@ from app.services.qr_png import generiere_qr_png
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
AppName = Literal["scan", "doku"]
|
||||
|
||||
def _lade_version_info() -> dict:
|
||||
pfad = Path(settings.downloads_dir) / "version.json"
|
||||
# FLUT-003 (2026-09-10): Nutzer-Entscheidung - ZWEI eigenständige Apps statt
|
||||
# einer gemeinsamen. mabea-scan (FLUT-001, "version.json") bleibt
|
||||
# schlank/Inventur-fokussiert, mabea-doku (FLUT-003, "version-doku.json")
|
||||
# deckt die Fristen-Übersicht ab (perspektivisch Erfassen, Phase 2).
|
||||
_VERSION_DATEI = {"scan": "version.json", "doku": "version-doku.json"}
|
||||
|
||||
|
||||
def _lade_version_info(app: AppName) -> dict:
|
||||
pfad = Path(settings.downloads_dir) / _VERSION_DATEI[app]
|
||||
if not pfad.exists():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Keine App-Version veröffentlicht")
|
||||
return json.loads(pfad.read_text())
|
||||
|
||||
|
||||
@router.get("/app-download/info")
|
||||
async def app_download_info() -> dict:
|
||||
"""FLUT-001: bewusst OHNE Auth - Helfer im Feld sollen die Scan-App auch
|
||||
ohne MABEA-Login installieren können (Henne-Ei: die App IST der
|
||||
Login-Client). Liefert dieselbe version.json wie der In-App-Update-Check,
|
||||
plus die volle URL fürs Frontend/den QR-Code."""
|
||||
daten = _lade_version_info()
|
||||
async def app_download_info(app: AppName = "scan") -> dict:
|
||||
"""Bewusst OHNE Auth - Helfer im Feld sollen die Apps auch ohne
|
||||
MABEA-Login installieren können (Henne-Ei: die App IST der
|
||||
Login-Client). Liefert dieselbe version*.json wie der In-App-Update-
|
||||
Check (nur mabea-scan hat aktuell einen In-App-Update-Check), plus die
|
||||
volle URL fürs Frontend/den QR-Code."""
|
||||
daten = _lade_version_info(app)
|
||||
return {
|
||||
"app": app,
|
||||
"version": daten["version"],
|
||||
"apk_url": f"{settings.portal_url}{daten['apk_url']}",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/app-download/qr.png")
|
||||
async def app_download_qr() -> Response:
|
||||
async def app_download_qr(app: AppName = "scan") -> Response:
|
||||
"""QR-Code direkt auf die aktuelle APK - zum Ausdrucken (Poster/Aushang)
|
||||
oder Scannen von einem zweiten Gerät aus. Bewusst OHNE Auth, siehe
|
||||
app_download_info()."""
|
||||
daten = _lade_version_info()
|
||||
daten = _lade_version_info(app)
|
||||
apk_url = f"{settings.portal_url}{daten['apk_url']}"
|
||||
return Response(content=generiere_qr_png(apk_url), media_type="image/png")
|
||||
@@ -52,6 +52,11 @@ class Objekttyp(Base):
|
||||
# fahrzeug_id) zugeordnet werden - grenzt die Fahrzeug-Dropdowns serverseitig
|
||||
# ein, ohne einen eigenen Fahrzeug-Objekttyp fachlich zu erzwingen.
|
||||
ist_zugfahrzeug: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
# ASSET-012 Nutzer-Feedback 2026-09-10: Ressourcenbuchung war in JEDER
|
||||
# Akte sichtbar, unabhängig vom Objekttyp - für die meisten Objekttypen
|
||||
# (Verbrauchsmaterial-Rucksäcke etc.) ergibt eine Buchungskalender-Ansicht
|
||||
# keinen Sinn. Jetzt Opt-in je Objekttyp statt global erzwungen.
|
||||
ressourcenbuchung_aktiv: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class Nummernschema(Base):
|
||||
|
||||
@@ -40,6 +40,7 @@ class AkteRead(BaseModel):
|
||||
einsatzbereitschaft_status: str
|
||||
einsatzbereitschaft_gruende: list[str]
|
||||
objekttyp_name: str
|
||||
ressourcenbuchung_aktiv: bool
|
||||
standort_name: str
|
||||
fahrzeugdetails: FahrzeugdetailsRead | None
|
||||
zustaendige_benutzer: list[ZustaendigerBenutzer]
|
||||
|
||||
@@ -90,6 +90,7 @@ class ObjekttypCreate(BaseModel):
|
||||
kategorie_id: int | None = None
|
||||
name: str
|
||||
ist_zugfahrzeug: bool = False
|
||||
ressourcenbuchung_aktiv: bool = False
|
||||
|
||||
|
||||
class ObjekttypRead(ObjekttypCreate):
|
||||
@@ -102,6 +103,7 @@ class ObjekttypUpdate(BaseModel):
|
||||
kategorie_id: int | None = None
|
||||
name: str | None = None
|
||||
ist_zugfahrzeug: bool | None = None
|
||||
ressourcenbuchung_aktiv: bool | None = None
|
||||
|
||||
|
||||
class FachCreate(BaseModel):
|
||||
|
||||
@@ -139,6 +139,7 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
|
||||
einsatzbereitschaft_status=readiness["status"],
|
||||
einsatzbereitschaft_gruende=readiness["gruende"],
|
||||
objekttyp_name=objekttyp.name if objekttyp else "",
|
||||
ressourcenbuchung_aktiv=objekttyp.ressourcenbuchung_aktiv if objekttyp else False,
|
||||
standort_name=standort.name if standort else "",
|
||||
fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None,
|
||||
zustaendige_benutzer=zustaendige,
|
||||
|
||||
@@ -48,6 +48,29 @@ async def test_akte_buendelt_teilbereiche(client, db_session, objekt_mit_positio
|
||||
assert len(body["maengel"]) == 1
|
||||
assert body["maengel"][0]["beschreibung"] == "Riss im Gurt"
|
||||
assert len(body["historie"]) == 1
|
||||
assert body["ressourcenbuchung_aktiv"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_akte_ressourcenbuchung_aktiv_folgt_objekttyp_flag(client, objekt_mit_position, admin_user):
|
||||
"""ASSET-012 Nutzer-Feedback 2026-09-10: Ressourcenbuchung ist Opt-in je
|
||||
Objekttyp, nicht mehr in jeder Akte automatisch sichtbar."""
|
||||
from app.models.stammdaten import Objekttyp
|
||||
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "admin1")
|
||||
|
||||
objekttyp_id = objekt.objekttyp_id
|
||||
aendern = await client.patch(
|
||||
f"/api/v1/objekttypen/{objekttyp_id}",
|
||||
json={"ressourcenbuchung_aktiv": True},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert aendern.status_code == 200
|
||||
|
||||
antwort = await client.get(f"/api/v1/akte/objekt/{objekt.id}", headers=auth_header(token))
|
||||
assert antwort.status_code == 200
|
||||
assert antwort.json()["ressourcenbuchung_aktiv"] is True
|
||||
assert body["historie"][0]["entitaet_typ"] == "objekt"
|
||||
assert body["einsatzbereitschaft_status"] == "not-ready"
|
||||
assert "mangel_kritisch_offen" in body["einsatzbereitschaft_gruende"]
|
||||
|
||||
@@ -51,3 +51,25 @@ async def test_app_download_ohne_auth_erreichbar(client, tmp_path, monkeypatch):
|
||||
response = await client.get("/api/v1/app-download/info")
|
||||
assert response.status_code != 401
|
||||
assert response.status_code != 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_download_info_zwei_apps_getrennt(client, tmp_path, monkeypatch):
|
||||
"""FLUT-003: mabea-scan und mabea-doku sind eigenständige Apps mit
|
||||
getrennten version*.json-Dateien, kein gemeinsamer Zustand."""
|
||||
monkeypatch.setattr(settings, "downloads_dir", str(tmp_path))
|
||||
monkeypatch.setattr(settings, "portal_url", "https://example.test")
|
||||
(tmp_path / "version.json").write_text(
|
||||
json.dumps({"version": "0.7.0", "apk_url": "/downloads/mabea-scan-prototyp-v0.7.apk"})
|
||||
)
|
||||
(tmp_path / "version-doku.json").write_text(
|
||||
json.dumps({"version": "0.1.0", "apk_url": "/downloads/mabea-doku-v0.1.apk"})
|
||||
)
|
||||
|
||||
scan = await client.get("/api/v1/app-download/info?app=scan")
|
||||
assert scan.status_code == 200
|
||||
assert scan.json()["version"] == "0.7.0"
|
||||
|
||||
doku = await client.get("/api/v1/app-download/info?app=doku")
|
||||
assert doku.status_code == 200
|
||||
assert doku.json()["version"] == "0.1.0"
|
||||
@@ -419,6 +419,7 @@ export interface Objekttyp {
|
||||
kategorie_id: number | null;
|
||||
name: string;
|
||||
ist_zugfahrzeug: boolean;
|
||||
ressourcenbuchung_aktiv: boolean;
|
||||
}
|
||||
|
||||
export interface Nummernschema {
|
||||
@@ -476,6 +477,7 @@ export interface Akte {
|
||||
einsatzbereitschaft_status: "ready" | "limited" | "not-ready" | "unknown";
|
||||
einsatzbereitschaft_gruende: string[];
|
||||
objekttyp_name: string;
|
||||
ressourcenbuchung_aktiv: boolean;
|
||||
standort_name: string;
|
||||
fahrzeugdetails: Fahrzeugdetails | null;
|
||||
zustaendige_benutzer: ZustaendigerBenutzer[];
|
||||
|
||||
@@ -371,10 +371,14 @@ export function AdminPage() {
|
||||
function ScanAppDownloadHinweis() {
|
||||
return (
|
||||
<p className="text-muted" style={{ marginTop: "2rem", fontSize: "0.85rem" }}>
|
||||
<a href="/app" target="_blank" rel="noreferrer">
|
||||
MABEA Scan-App (Android, Prototyp) — Download-Seite mit QR-Code
|
||||
<a href="/app?app=scan" target="_blank" rel="noreferrer">
|
||||
MABEA Scan-App
|
||||
</a>{" "}
|
||||
— FLUT-001-Sondierung, kein Produktivstand.
|
||||
/{" "}
|
||||
<a href="/app?app=doku" target="_blank" rel="noreferrer">
|
||||
MABEA Doku-App
|
||||
</a>{" "}
|
||||
(Android, Prototypen) — Download-Seite mit QR-Code — kein Produktivstand.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -314,10 +314,12 @@ export function AktePage() {
|
||||
<ReadinessVerlaufPanel objektId={objekt.id} />
|
||||
</div>
|
||||
|
||||
<div className="akte-ressourcenbuchung card">
|
||||
<h3 style={{ marginTop: 0 }}>Buchungen</h3>
|
||||
<RessourcenbuchungPanel objektId={objekt.id} onFehler={setFehler} />
|
||||
</div>
|
||||
{akte.ressourcenbuchung_aktiv && (
|
||||
<div className="akte-ressourcenbuchung card">
|
||||
<h3 style={{ marginTop: 0 }}>Buchungen</h3>
|
||||
<RessourcenbuchungPanel objektId={objekt.id} onFehler={setFehler} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="akte-historie card">
|
||||
<div className="row-between" style={{ alignItems: "baseline" }}>
|
||||
|
||||
@@ -1,37 +1,70 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api/v1";
|
||||
|
||||
type AppName = "scan" | "doku";
|
||||
|
||||
interface AppInfo {
|
||||
app: AppName;
|
||||
version: string;
|
||||
apk_url: string;
|
||||
}
|
||||
|
||||
// FLUT-001: öffentliche Seite (kein Login nötig - Henne-Ei, die App IST der
|
||||
// Login-Client), damit Helfer im Feld die Scan-App per Link oder QR-Code
|
||||
// installieren können, ohne vorher im Portal angemeldet zu sein.
|
||||
const APP_META: Record<AppName, { titel: string; beschreibung: string }> = {
|
||||
scan: {
|
||||
titel: "MABEA Scan",
|
||||
beschreibung: "Android-Prototyp (FLUT-001) zum QR-Scan von Objekten inkl. Kontroll-/Inventur-Erfassung.",
|
||||
},
|
||||
doku: {
|
||||
titel: "MABEA Doku",
|
||||
beschreibung: "Android-App (FLUT-003) — Fristen-Übersicht (Prüfungen/Ablaufdaten/Wartung) auf einen Blick.",
|
||||
},
|
||||
};
|
||||
|
||||
// FLUT-001/FLUT-003: öffentliche Seite (kein Login nötig - Henne-Ei, die
|
||||
// App IST der Login-Client), damit Helfer im Feld eine der beiden MABEA-
|
||||
// Apps per Link oder QR-Code installieren können. ?app=scan (Default) oder
|
||||
// ?app=doku wählt die Ziel-App - zwei eigenständige APKs (Nutzer-
|
||||
// Entscheidung 2026-09-10), kein gemeinsames Bundle.
|
||||
export function AppDownloadPage() {
|
||||
const [suchparameter] = useSearchParams();
|
||||
const app: AppName = suchparameter.get("app") === "doku" ? "doku" : "scan";
|
||||
|
||||
const [info, setInfo] = useState<AppInfo | null>(null);
|
||||
const [fehler, setFehler] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/app-download/info`)
|
||||
setInfo(null);
|
||||
setFehler(false);
|
||||
fetch(`${API_BASE}/app-download/info?app=${app}`)
|
||||
.then((r) => (r.ok ? r.json() : Promise.reject()))
|
||||
.then(setInfo)
|
||||
.catch(() => setFehler(true));
|
||||
}, []);
|
||||
}, [app]);
|
||||
|
||||
const meta = APP_META[app];
|
||||
|
||||
return (
|
||||
<main style={{ maxWidth: "28rem", margin: "3rem auto", padding: "0 1rem", textAlign: "center" }}>
|
||||
<h1>MABEA Scan-App</h1>
|
||||
<p className="text-muted">Android-Prototyp (FLUT-001) zum QR-Scan von Objekten.</p>
|
||||
<div className="row" style={{ justifyContent: "center", gap: "0.5rem", marginBottom: "1rem" }}>
|
||||
<a href="/app?app=scan" className={`btn ${app === "scan" ? "btn-primary" : "btn-secondary"}`}>
|
||||
MABEA Scan
|
||||
</a>
|
||||
<a href="/app?app=doku" className={`btn ${app === "doku" ? "btn-primary" : "btn-secondary"}`}>
|
||||
MABEA Doku
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h1>{meta.titel}</h1>
|
||||
<p className="text-muted">{meta.beschreibung}</p>
|
||||
|
||||
{fehler && <p className="alert alert-danger">Aktuell keine App-Version verfügbar.</p>}
|
||||
|
||||
{info && (
|
||||
<>
|
||||
<img
|
||||
src={`${API_BASE}/app-download/qr.png`}
|
||||
src={`${API_BASE}/app-download/qr.png?app=${app}`}
|
||||
alt="QR-Code zum App-Download"
|
||||
style={{ width: "220px", height: "220px", margin: "1.5rem auto", display: "block" }}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,8 @@ export function ObjekttypSection({
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
const [istZugfahrzeug, setIstZugfahrzeug] = useState(false);
|
||||
const [bearbeitungsZugfahrzeug, setBearbeitungsZugfahrzeug] = useState(false);
|
||||
const [ressourcenbuchungAktiv, setRessourcenbuchungAktiv] = useState(false);
|
||||
const [bearbeitungsRessourcenbuchung, setBearbeitungsRessourcenbuchung] = useState(false);
|
||||
const [schemaBearbeitenId, setSchemaBearbeitenId] = useState<number | null>(null);
|
||||
const [schemaPraefix, setSchemaPraefix] = useState("");
|
||||
const [schemaStellenzahl, setSchemaStellenzahl] = useState(5);
|
||||
@@ -42,13 +44,18 @@ export function ObjekttypSection({
|
||||
setBearbeiteteId(o.id);
|
||||
setBearbeitungsName(o.name);
|
||||
setBearbeitungsZugfahrzeug(o.ist_zugfahrzeug);
|
||||
setBearbeitungsRessourcenbuchung(o.ressourcenbuchung_aktiv);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/objekttypen/${id}`, {
|
||||
method: "PATCH",
|
||||
body: { name: bearbeitungsName, ist_zugfahrzeug: bearbeitungsZugfahrzeug },
|
||||
body: {
|
||||
name: bearbeitungsName,
|
||||
ist_zugfahrzeug: bearbeitungsZugfahrzeug,
|
||||
ressourcenbuchung_aktiv: bearbeitungsRessourcenbuchung,
|
||||
},
|
||||
});
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
@@ -67,10 +74,17 @@ export function ObjekttypSection({
|
||||
try {
|
||||
await apiRequest("/objekttypen", {
|
||||
method: "POST",
|
||||
body: { name, bereich_id: bereichId, kategorie_id: kategorieId || null, ist_zugfahrzeug: istZugfahrzeug },
|
||||
body: {
|
||||
name,
|
||||
bereich_id: bereichId,
|
||||
kategorie_id: kategorieId || null,
|
||||
ist_zugfahrzeug: istZugfahrzeug,
|
||||
ressourcenbuchung_aktiv: ressourcenbuchungAktiv,
|
||||
},
|
||||
});
|
||||
setName("");
|
||||
setIstZugfahrzeug(false);
|
||||
setRessourcenbuchungAktiv(false);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Objekttyp konnte nicht angelegt werden.");
|
||||
@@ -167,6 +181,14 @@ export function ObjekttypSection({
|
||||
/>
|
||||
Zugfahrzeug (koppelbar)
|
||||
</label>
|
||||
<label className="row" style={{ alignItems: "center", gap: "0.4rem", marginTop: "1.5rem" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ressourcenbuchungAktiv}
|
||||
onChange={(e) => setRessourcenbuchungAktiv(e.target.checked)}
|
||||
/>
|
||||
Ressourcenbuchung in der Akte anzeigen
|
||||
</label>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ marginTop: "1.5rem" }}
|
||||
@@ -200,6 +222,14 @@ export function ObjekttypSection({
|
||||
/>
|
||||
Zugfahrzeug
|
||||
</label>
|
||||
<label className="row" style={{ alignItems: "center", gap: "0.4rem" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={bearbeitungsRessourcenbuchung}
|
||||
onChange={(e) => setBearbeitungsRessourcenbuchung(e.target.checked)}
|
||||
/>
|
||||
Ressourcenbuchung
|
||||
</label>
|
||||
<button className="btn btn-primary" onClick={() => speichern(o.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
@@ -212,6 +242,7 @@ export function ObjekttypSection({
|
||||
<span>
|
||||
{o.name}
|
||||
{o.ist_zugfahrzeug && <span className="text-muted"> (Zugfahrzeug)</span>}
|
||||
{o.ressourcenbuchung_aktiv && <span className="text-muted"> (Buchungen)</span>}
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(o)}>
|
||||
Bearbeiten
|
||||
|
||||
Reference in new issue
Block a user