diff --git a/DEVLOG.md b/DEVLOG.md index 1095d7d..2cf9279 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2395,3 +2395,150 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/VorlageSection.tsx | 99 ++++++++++++++++++++++++++++++++-- --- +## 2026-09-04 10:21 – 10:24 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- a2877f5 Feste Fächer-Liste je Objekttyp (Vorlagen-Positionen: Dropdown + Sonstiges) + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++++++++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 ++++++++++++++++++++++++++++++- +- backend/app/models/stammdaten.py | 13 ++++++++ +- backend/app/schemas/stammdaten.py | 16 +++++++++ +- backend/tests/test_stammdaten.py | 56 +++++++++++++++++++++++++++++++ +- frontend/src/api/types.ts | 7 ++++ +- frontend/src/pages/AdminPage.tsx | 15 +++++++-- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++++++-------- + +--- +## 2026-09-04 10:24 – 10:25 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++++++++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 ++++++++++++++++++++++++++++++- +- backend/app/models/stammdaten.py | 13 ++++++++ +- backend/app/schemas/stammdaten.py | 16 +++++++++ +- backend/tests/test_stammdaten.py | 56 +++++++++++++++++++++++++++++++ +- frontend/src/api/types.ts | 7 ++++ +- frontend/src/pages/AdminPage.tsx | 15 +++++++-- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++++++-------- + +--- +## 2026-09-04 10:31 – 10:32 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++++++++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 ++++++++++++++++++++++++++++++- +- backend/app/models/stammdaten.py | 13 ++++++++ +- backend/app/schemas/stammdaten.py | 16 +++++++++ +- backend/tests/test_stammdaten.py | 56 +++++++++++++++++++++++++++++++ +- frontend/src/api/types.ts | 7 ++++ +- frontend/src/pages/AdminPage.tsx | 15 +++++++-- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++++++-------- + +--- +## 2026-09-04 16:59 – 16:59 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- +## 2026-09-04 17:01 – 17:01 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** backend + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- +## 2026-09-04 17:03 – 17:29 (25m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- +## 2026-09-04 17:33 – 17:33 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** backend + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- diff --git a/backend/DEVLOG.md b/backend/DEVLOG.md new file mode 100644 index 0000000..bb10f6c --- /dev/null +++ b/backend/DEVLOG.md @@ -0,0 +1,78 @@ +# backend – Dev Log + +## 2026-09-04 09:49 – 09:50 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- frontend/src/api/types.ts | 11 +++++++ +- frontend/src/pages/AdminPage.tsx | 14 +++++++-- +- frontend/src/pages/admin/BenutzerSection.tsx | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 16:59 – 17:00 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- +## 2026-09-04 17:29 – 17:31 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- +## 2026-09-04 17:32 – 17:32 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** backend + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 132 ++++++++++++++++++++++++++++ +- backend/alembic/versions/0004_add_fach.py | 36 ++++++++ +- backend/app/api/v1/endpoints/stammdaten.py | 56 +++++++++++- +- backend/app/models/stammdaten.py | 13 +++ +- backend/app/schemas/stammdaten.py | 16 ++++ +- backend/tests/test_stammdaten.py | 56 ++++++++++++ +- frontend/src/api/types.ts | 7 ++ +- frontend/src/pages/AdminPage.tsx | 15 +++- +- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++++++ +- frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++++++++ +- frontend/src/pages/admin/VorlageSection.tsx | 26 +++--- + +--- diff --git a/backend/alembic/versions/0005_add_objektposition_code.py b/backend/alembic/versions/0005_add_objektposition_code.py new file mode 100644 index 0000000..75922c3 --- /dev/null +++ b/backend/alembic/versions/0005_add_objektposition_code.py @@ -0,0 +1,32 @@ +"""objektposition.code: Code128-Etikett für Geräte-Exemplare + +Revision ID: 0005_add_objektposition_code +Revises: 0004_add_fach +Create Date: 2026-09-04 + +Konzept-Ergänzung zu Karte 10 / Prompt 09: objekt.code und material.code +identifizieren Objekt bzw. Materialart, aber bei materialtyp='geraet_sn' +identifiziert der Code das konkrete Exemplar an der Objektposition, nicht +den Materialstamm. Eigenes nullable/unique Feld, kein Umbau bestehender +Spalten (seriennummer bleibt separates Attribut). +""" +from typing import Sequence, Union + +from alembic import op + +revision: str = "0005_add_objektposition_code" +down_revision: Union[str, None] = "0004_add_fach" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + ALTER TABLE objektposition ADD COLUMN code TEXT UNIQUE; + """ + ) + + +def downgrade() -> None: + op.execute("ALTER TABLE objektposition DROP COLUMN code;") diff --git a/backend/app/api/v1/endpoints/objekte.py b/backend/app/api/v1/endpoints/objekte.py index 95dd8c2..a6a231e 100644 --- a/backend/app/api/v1/endpoints/objekte.py +++ b/backend/app/api/v1/endpoints/objekte.py @@ -19,7 +19,13 @@ from app.schemas.objekt import ( ObjektpositionUpdate, ObjektRead, ) -from app.services.objekte import dupliziere_objekt, objekt_aus_vorlage, sollmenge_effektiv, sollmenge_map +from app.services.objekte import ( + dupliziere_objekt, + naechster_freier_code, + objekt_aus_vorlage, + sollmenge_effektiv, + sollmenge_map, +) router = APIRouter() @@ -53,6 +59,53 @@ async def hole_objekt_per_code( return objekt +@router.get("/objekte/naechster-code") +async def naechster_objekt_code( + praefix: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) +) -> dict[str, str]: + """Code-Vorschlag beim Anlegen/Duplizieren (Konzept-Ergänzung Karte 10/Prompt 09). + Vorschlag ist überschreibbar, Eindeutigkeit erzwingt weiterhin objekt.code UNIQUE.""" + return {"code": await naechster_freier_code(db, Objekt.code, praefix)} + + +@router.get("/objektpositionen/naechster-code") +async def naechster_objektposition_code( + praefix: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) +) -> dict[str, str]: + """Code-Vorschlag für Geräte-Exemplare (objektposition.code, eigener + Nummernkreis je Präfix, unabhängig von objekt.code).""" + return {"code": await naechster_freier_code(db, Objektposition.code, praefix)} + + +@router.get("/objektpositionen/code/{code}", response_model=ObjektpositionRead) +async def hole_objektposition_per_code( + code: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) +) -> ObjektpositionRead: + """Geräte-Scan (Konzept-Ergänzung Karte 10/Prompt 09): Code128-Etikett am + Exemplar identifiziert die Objektposition direkt, unabhängig vom Objekt-Code - + löst SN-vorbelegte Kontroll-Erfassung ohne Umweg über Objekt-Scan aus.""" + result = await db.execute(select(Objektposition).where(Objektposition.code == code)) + position = result.scalar_one_or_none() + if position is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objektposition nicht gefunden") + + objekt = await db.get(Objekt, position.objekt_id) + vorlagen_sollmengen = await sollmenge_map(db, objekt.vorlage_id if objekt else None) + return ObjektpositionRead( + id=position.id, + objekt_id=position.objekt_id, + material_id=position.material_id, + sollmenge_override=position.sollmenge_override, + sollmenge_effektiv=sollmenge_effektiv(position, vorlagen_sollmengen), + ist_status=position.ist_status, + istmenge=position.istmenge, + seriennummer=position.seriennummer, + ablaufdatum=position.ablaufdatum, + chargennummer=position.chargennummer, + code=position.code, + ) + + @router.get("/objekte/{objekt_id}", response_model=ObjektRead) async def hole_objekt( objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user) @@ -86,6 +139,7 @@ async def liste_objektpositionen( seriennummer=p.seriennummer, ablaufdatum=p.ablaufdatum, chargennummer=p.chargennummer, + code=p.code, ) for p in positionen ] @@ -176,6 +230,7 @@ async def aendere_objektposition( seriennummer=position.seriennummer, ablaufdatum=position.ablaufdatum, chargennummer=position.chargennummer, + code=position.code, ) @@ -219,4 +274,5 @@ async def fuege_zusatzposition_hinzu( seriennummer=position.seriennummer, ablaufdatum=position.ablaufdatum, chargennummer=position.chargennummer, + code=position.code, ) diff --git a/backend/app/models/objektposition.py b/backend/app/models/objektposition.py index b5e9515..7658ebb 100644 --- a/backend/app/models/objektposition.py +++ b/backend/app/models/objektposition.py @@ -39,3 +39,7 @@ class Objektposition(Base): seriennummer: Mapped[str | None] = mapped_column(String) ablaufdatum: Mapped[date | None] = mapped_column(Date) chargennummer: Mapped[str | None] = mapped_column(String) + # Code128-Etikett für Geräte-Exemplare (Konzept-Ergänzung Karte 10/Prompt 09), + # separat von seriennummer: Code identifiziert das gedruckte Label, SN das + # Herstellerattribut - beide können divergieren (Etikett verloren/neu vergeben). + code: Mapped[str | None] = mapped_column(String, unique=True) diff --git a/backend/app/schemas/objekt.py b/backend/app/schemas/objekt.py index d52babd..ee07976 100644 --- a/backend/app/schemas/objekt.py +++ b/backend/app/schemas/objekt.py @@ -45,6 +45,7 @@ class ObjektpositionRead(BaseModel): seriennummer: str | None ablaufdatum: date | None chargennummer: str | None + code: str | None class ObjektpositionUpdate(BaseModel): @@ -54,6 +55,7 @@ class ObjektpositionUpdate(BaseModel): seriennummer: str | None = None ablaufdatum: date | None = None chargennummer: str | None = None + code: str | None = None class ObjektpositionCreate(BaseModel): diff --git a/backend/app/services/objekte.py b/backend/app/services/objekte.py index acbfe40..ae3eb2f 100644 --- a/backend/app/services/objekte.py +++ b/backend/app/services/objekte.py @@ -1,3 +1,4 @@ +import re from decimal import Decimal from sqlalchemy import select @@ -7,6 +8,26 @@ from app.models.objekt import Objekt from app.models.objektposition import Objektposition, ObjektpositionStatus from app.models.vorlage import Beladungsvorlage, Vorlagenposition +_CODE_MUSTER = re.compile(r"^-(\d+)$") + + +async def naechster_freier_code(db: AsyncSession, spalte, praefix: str) -> str: + """Konzept-Ergänzung Karte 10/Prompt 09: Code-Vorschlag PRAEFIX-NNN mit + fortlaufender Nummer je Präfix. Zahlenbreite folgt der bisher größten + vergebenen Nummer (mind. 5 Stellen bei Erstvergabe), damit die Sortierung im + Regal stabil bleibt, solange nicht mehr als 10^Breite Objekte existieren.""" + result = await db.execute(select(spalte).where(spalte.like(f"{praefix}-%"))) + hoechste = 0 + breite = 5 + for (code,) in result.all(): + treffer = _CODE_MUSTER.match(code[len(praefix) :]) + if treffer is None: + continue + zahl = treffer.group(1) + breite = max(breite, len(zahl)) + hoechste = max(hoechste, int(zahl)) + return f"{praefix}-{hoechste + 1:0{breite}d}" + async def sollmenge_map(db: AsyncSession, vorlage_id: int | None) -> dict[int, Decimal]: """material_id -> Sollmenge laut Vorlage (Prompt 10: Basis für die dynamische diff --git a/backend/tests/test_objekte.py b/backend/tests/test_objekte.py index 70136c4..f9ab1da 100644 --- a/backend/tests/test_objekte.py +++ b/backend/tests/test_objekte.py @@ -166,3 +166,86 @@ async def test_mitarbeiter_kann_objekte_nicht_anlegen( headers=auth_header(token), ) assert response.status_code == 403 + + +@pytest.mark.asyncio +async def test_objektposition_code_lookup( + client, db_session, objekt_mit_position, materialverantwortlicher_user, mitarbeiter_user +): + objekt, material = objekt_mit_position + mv_token = await login(client, "materialverantwortlicher1") + + positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(mv_token)) + position_id = positionen.json()[0]["id"] + + patch_response = await client.patch( + f"/api/v1/objekte/{objekt.id}/positionen/{position_id}", + json={"code": "GR-000123"}, + headers=auth_header(mv_token), + ) + assert patch_response.status_code == 200 + assert patch_response.json()["code"] == "GR-000123" + + token = await login(client, "mitarbeiter1") + lookup_response = await client.get("/api/v1/objektpositionen/code/GR-000123", headers=auth_header(token)) + assert lookup_response.status_code == 200 + body = lookup_response.json() + assert body["id"] == position_id + assert body["material_id"] == material.id + + missing_response = await client.get("/api/v1/objektpositionen/code/UNBEKANNT", headers=auth_header(token)) + assert missing_response.status_code == 404 + + +@pytest.mark.asyncio +async def test_naechster_objekt_code_ohne_bestand(client, admin_user): + token = await login(client, "admin1") + response = await client.get("/api/v1/objekte/naechster-code?praefix=RK", headers=auth_header(token)) + assert response.status_code == 200 + assert response.json()["code"] == "RK-00001" + + +@pytest.mark.asyncio +async def test_naechster_objekt_code_zaehlt_hoch( + client, db_session, objekttyp_factory, standort_factory, admin_user +): + objekttyp = await objekttyp_factory("Rucksack Vorschlag") + standort = await standort_factory("Wache-Vorschlag") + vorlage = await erstelle_vorlage( + db_session, objekttyp_id=objekttyp.id, name="Vorschlag Standard", positionen=[] + ) + token = await login(client, "admin1") + await client.post( + "/api/v1/objekte", + json={ + "code": "RK-00017", + "name": "Rucksack 17", + "objekttyp_id": objekttyp.id, + "vorlage_id": vorlage.id, + "standort_id": standort.id, + }, + headers=auth_header(token), + ) + response = await client.get("/api/v1/objekte/naechster-code?praefix=RK", headers=auth_header(token)) + assert response.json()["code"] == "RK-00018" + + +@pytest.mark.asyncio +async def test_naechster_objektposition_code( + client, db_session, objekt_mit_position, materialverantwortlicher_user +): + objekt, material = objekt_mit_position + token = await login(client, "materialverantwortlicher1") + + positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token)) + position_id = positionen.json()[0]["id"] + await client.patch( + f"/api/v1/objekte/{objekt.id}/positionen/{position_id}", + json={"code": "GR-001042"}, + headers=auth_header(token), + ) + + response = await client.get( + "/api/v1/objektpositionen/naechster-code?praefix=GR", headers=auth_header(token) + ) + assert response.json()["code"] == "GR-001043"