Admin-Portal: Bearbeiten für Bereich/Kategorie/Objekttyp/Standort/Benutzer,
Vorlagen-Versionierung und Objekt-Duplizieren im Frontend nachgerüstet Backend: PATCH-Endpoints für Bereich/Kategorie/Standort/Objekttyp ergänzt (analog zu Material), die bislang fehlten und Bearbeiten grundsätzlich unmöglich machten. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -2051,3 +2051,215 @@ Keine Commits in dieser Session.
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:01 – 09:04 (3m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- d70558f Inline-Nachfüll-Dialog nach Abweichung in der Kontrolle (Karte 07)
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/queue.ts | 18 +++++++++++++++++-
|
||||
- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++-------
|
||||
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++--
|
||||
- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:05 – 09:05 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/queue.ts | 18 +++++++++++++++++-
|
||||
- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++-------
|
||||
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++--
|
||||
- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:11 – 09:11 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/queue.ts | 18 +++++++++++++++++-
|
||||
- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++-------
|
||||
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++--
|
||||
- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:11 – 09:11 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/queue.ts | 18 +++++++++++++++++-
|
||||
- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++-------
|
||||
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++--
|
||||
- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:11 – 09:12 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/offline/queue.ts | 18 +++++++++++++++++-
|
||||
- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++-------
|
||||
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++
|
||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++--
|
||||
- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
## 2026-09-04 09:12 – 09:14 (2m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 032126c fix(kontrolle): Abbruch löscht bereits nachgefüllten Fehlbestand nicht mehr
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/services/kontrolle/lifecycle.py | 8 +++++++-
|
||||
- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-04 09:21 – 09:21 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/services/kontrolle/lifecycle.py | 8 +++++++-
|
||||
- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-04 09:21 – 09:22 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/services/kontrolle/lifecycle.py | 8 +++++++-
|
||||
- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-04 09:23 – 09:23 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/services/kontrolle/lifecycle.py | 8 +++++++-
|
||||
- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-04 09:26 – 09:26 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- backend/app/services/kontrolle/lifecycle.py | 8 +++++++-
|
||||
- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-09-04 09:29 – 09:30 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 2c9ca3e Admin-Portal: Tab-Menü statt einer langen Scroll-Seite
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
|
||||
|
||||
---
|
||||
## 2026-09-04 09:38 – 09:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
|
||||
|
||||
---
|
||||
## 2026-09-04 09:43 – 09:43 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
|
||||
|
||||
---
|
||||
## 2026-09-04 09:43 – 09:44 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
- 1987818 Admin-Portal: Benutzerverwaltung (anlegen, Rollen, aktivieren/deaktivieren)
|
||||
|
||||
### 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 09:44 – 09:45 (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 09:47 – 09:47 (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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
|
||||
@@ -9,15 +9,19 @@ from app.models.stammdaten import Bereich, Kategorie, Material, Objekttyp, Stand
|
||||
from app.schemas.stammdaten import (
|
||||
BereichCreate,
|
||||
BereichRead,
|
||||
BereichUpdate,
|
||||
KategorieCreate,
|
||||
KategorieRead,
|
||||
KategorieUpdate,
|
||||
MaterialCreate,
|
||||
MaterialRead,
|
||||
MaterialUpdate,
|
||||
ObjekttypCreate,
|
||||
ObjekttypRead,
|
||||
ObjekttypUpdate,
|
||||
StandortCreate,
|
||||
StandortRead,
|
||||
StandortUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
@@ -45,6 +49,22 @@ async def erstelle_bereich(
|
||||
return bereich
|
||||
|
||||
|
||||
@router.patch("/bereiche/{bereich_id}", response_model=BereichRead)
|
||||
async def aendere_bereich(
|
||||
bereich_id: int,
|
||||
payload: BereichUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Bereich:
|
||||
bereich = await db.get(Bereich, bereich_id)
|
||||
if bereich is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bereich nicht gefunden")
|
||||
for feld, wert in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(bereich, feld, wert)
|
||||
await db.flush()
|
||||
return bereich
|
||||
|
||||
|
||||
# -- Kategorie ------------------------------------------------------------------
|
||||
|
||||
@router.get("/kategorien", response_model=list[KategorieRead])
|
||||
@@ -67,6 +87,22 @@ async def erstelle_kategorie(
|
||||
return kategorie
|
||||
|
||||
|
||||
@router.patch("/kategorien/{kategorie_id}", response_model=KategorieRead)
|
||||
async def aendere_kategorie(
|
||||
kategorie_id: int,
|
||||
payload: KategorieUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Kategorie:
|
||||
kategorie = await db.get(Kategorie, kategorie_id)
|
||||
if kategorie is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kategorie nicht gefunden")
|
||||
for feld, wert in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(kategorie, feld, wert)
|
||||
await db.flush()
|
||||
return kategorie
|
||||
|
||||
|
||||
# -- Standort ------------------------------------------------------------------
|
||||
|
||||
@router.get("/standorte", response_model=list[StandortRead])
|
||||
@@ -89,6 +125,22 @@ async def erstelle_standort(
|
||||
return standort
|
||||
|
||||
|
||||
@router.patch("/standorte/{standort_id}", response_model=StandortRead)
|
||||
async def aendere_standort(
|
||||
standort_id: int,
|
||||
payload: StandortUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Standort:
|
||||
standort = await db.get(Standort, standort_id)
|
||||
if standort is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Standort nicht gefunden")
|
||||
for feld, wert in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(standort, feld, wert)
|
||||
await db.flush()
|
||||
return standort
|
||||
|
||||
|
||||
# -- Objekttyp ------------------------------------------------------------------
|
||||
|
||||
@router.get("/objekttypen", response_model=list[ObjekttypRead])
|
||||
@@ -111,6 +163,22 @@ async def erstelle_objekttyp(
|
||||
return objekttyp
|
||||
|
||||
|
||||
@router.patch("/objekttypen/{objekttyp_id}", response_model=ObjekttypRead)
|
||||
async def aendere_objekttyp(
|
||||
objekttyp_id: int,
|
||||
payload: ObjekttypUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Objekttyp:
|
||||
objekttyp = await db.get(Objekttyp, objekttyp_id)
|
||||
if objekttyp is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekttyp nicht gefunden")
|
||||
for feld, wert in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(objekttyp, feld, wert)
|
||||
await db.flush()
|
||||
return objekttyp
|
||||
|
||||
|
||||
# -- Material (Prompt 07) --------------------------------------------------
|
||||
|
||||
@router.get("/materialien", response_model=list[MaterialRead])
|
||||
|
||||
@@ -13,6 +13,11 @@ class BereichRead(BereichCreate):
|
||||
id: int
|
||||
|
||||
|
||||
class BereichUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
beschreibung: str | None = None
|
||||
|
||||
|
||||
class KategorieCreate(BaseModel):
|
||||
bereich_id: int
|
||||
name: str
|
||||
@@ -24,6 +29,12 @@ class KategorieRead(KategorieCreate):
|
||||
id: int
|
||||
|
||||
|
||||
class KategorieUpdate(BaseModel):
|
||||
bereich_id: int | None = None
|
||||
name: str | None = None
|
||||
ueberkategorie_id: int | None = None
|
||||
|
||||
|
||||
class StandortCreate(BaseModel):
|
||||
name: str
|
||||
adresse: str | None = None
|
||||
@@ -34,6 +45,11 @@ class StandortRead(StandortCreate):
|
||||
id: int
|
||||
|
||||
|
||||
class StandortUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
adresse: str | None = None
|
||||
|
||||
|
||||
class ObjekttypCreate(BaseModel):
|
||||
bereich_id: int
|
||||
kategorie_id: int | None = None
|
||||
@@ -45,6 +61,12 @@ class ObjekttypRead(ObjekttypCreate):
|
||||
id: int
|
||||
|
||||
|
||||
class ObjekttypUpdate(BaseModel):
|
||||
bereich_id: int | None = None
|
||||
kategorie_id: int | None = None
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class MaterialCreate(BaseModel):
|
||||
name: str
|
||||
artikelnummer: str | None = None
|
||||
|
||||
@@ -60,3 +60,65 @@ async def test_material_not_found(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
response = await client.get("/api/v1/materialien/999999", headers=auth_header(token))
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_update_bereich(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
erstellt = await client.post("/api/v1/bereiche", json={"name": "Alt"}, headers=auth_header(token))
|
||||
bereich_id = erstellt.json()["id"]
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/bereiche/{bereich_id}", json={"name": "Neu"}, headers=auth_header(token)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Neu"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_update_kategorie(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
bereich = await client.post("/api/v1/bereiche", json={"name": "B"}, headers=auth_header(token))
|
||||
erstellt = await client.post(
|
||||
"/api/v1/kategorien",
|
||||
json={"bereich_id": bereich.json()["id"], "name": "Alt"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
kategorie_id = erstellt.json()["id"]
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/kategorien/{kategorie_id}", json={"name": "Neu"}, headers=auth_header(token)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Neu"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_update_standort(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
erstellt = await client.post("/api/v1/standorte", json={"name": "Alt"}, headers=auth_header(token))
|
||||
standort_id = erstellt.json()["id"]
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/standorte/{standort_id}", json={"name": "Neu"}, headers=auth_header(token)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Neu"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_update_objekttyp(client, admin_user):
|
||||
token = await login(client, "admin1")
|
||||
bereich = await client.post("/api/v1/bereiche", json={"name": "B"}, headers=auth_header(token))
|
||||
erstellt = await client.post(
|
||||
"/api/v1/objekttypen",
|
||||
json={"bereich_id": bereich.json()["id"], "name": "Alt"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
objekttyp_id = erstellt.json()["id"]
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/objekttypen/{objekttyp_id}", json={"name": "Neu"}, headers=auth_header(token)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Neu"
|
||||
|
||||
@@ -61,12 +61,21 @@ export interface Standort {
|
||||
adresse: string | null;
|
||||
}
|
||||
|
||||
export interface Vorlagenposition {
|
||||
id: string;
|
||||
vorlage_id: number;
|
||||
material_id: number;
|
||||
fach: string | null;
|
||||
sollmenge: string;
|
||||
}
|
||||
|
||||
export interface Beladungsvorlage {
|
||||
id: number;
|
||||
objekttyp_id: number;
|
||||
name: string;
|
||||
version: number;
|
||||
status: string;
|
||||
positionen: Vorlagenposition[];
|
||||
}
|
||||
|
||||
export interface Bereich {
|
||||
|
||||
@@ -23,6 +23,10 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [rollen, setRollen] = useState<Rolle[]>(["mitarbeiter"]);
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
const [bearbeitungsEmail, setBearbeitungsEmail] = useState("");
|
||||
const [bearbeitungsRollen, setBearbeitungsRollen] = useState<Rolle[]>([]);
|
||||
|
||||
function rolleUmschalten(rolle: Rolle) {
|
||||
setRollen((bisherige) =>
|
||||
@@ -59,6 +63,32 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(b: Benutzer) {
|
||||
setBearbeiteteId(b.id);
|
||||
setBearbeitungsName(b.name);
|
||||
setBearbeitungsEmail(b.email ?? "");
|
||||
setBearbeitungsRollen(b.rollen as Rolle[]);
|
||||
}
|
||||
|
||||
function bearbeitungsRolleUmschalten(rolle: Rolle) {
|
||||
setBearbeitungsRollen((bisherige) =>
|
||||
bisherige.includes(rolle) ? bisherige.filter((r) => r !== rolle) : [...bisherige, rolle]
|
||||
);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/benutzer/${id}`, {
|
||||
method: "PATCH",
|
||||
body: { name: bearbeitungsName, email: bearbeitungsEmail || null, rollen: bearbeitungsRollen },
|
||||
});
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Benutzer konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Benutzer anlegen</h2>
|
||||
@@ -99,14 +129,60 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
||||
{benutzer.length > 0 && (
|
||||
<ul className="card-list" style={{ marginTop: "1rem" }}>
|
||||
{benutzer.map((b) => (
|
||||
<li key={b.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
<li key={b.id} style={{ padding: "0.4rem 0" }}>
|
||||
{bearbeiteteId === b.id ? (
|
||||
<div className="stack">
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsEmail}
|
||||
onChange={(e) => setBearbeitungsEmail(e.target.value)}
|
||||
placeholder="E-Mail"
|
||||
/>
|
||||
</div>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
|
||||
{ALLE_ROLLEN.map((r) => (
|
||||
<label key={r} className="row" style={{ gap: "0.3rem" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={bearbeitungsRollen.includes(r)}
|
||||
onChange={() => bearbeitungsRolleUmschalten(r)}
|
||||
/>
|
||||
{r}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="row">
|
||||
<button className="btn btn-primary" onClick={() => speichern(b.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row-between">
|
||||
<span>
|
||||
<strong>{b.name}</strong> ({b.login}) – {b.rollen.join(", ")}
|
||||
{!b.aktiv && " – deaktiviert"}
|
||||
</span>
|
||||
<div className="row">
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(b)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => aktivSchalten(b)}>
|
||||
{b.aktiv ? "Deaktivieren" : "Aktivieren"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Props {
|
||||
export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
|
||||
async function anlegen() {
|
||||
setWirdAngelegt(true);
|
||||
@@ -26,6 +28,21 @@ export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(b: Bereich) {
|
||||
setBearbeiteteId(b.id);
|
||||
setBearbeitungsName(b.name);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/bereiche/${id}`, { method: "PATCH", body: { name: bearbeitungsName } });
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Bereich konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Bereich anlegen</h2>
|
||||
@@ -39,8 +56,29 @@ export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
||||
{bereiche.length > 0 && (
|
||||
<ul className="card-list">
|
||||
{bereiche.map((b) => (
|
||||
<li key={b.id} style={{ padding: "0.4rem 0" }}>
|
||||
{b.name}
|
||||
<li key={b.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
{bearbeiteteId === b.id ? (
|
||||
<div className="row" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => speichern(b.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>{b.name}</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(b)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -14,6 +14,8 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
||||
const [name, setName] = useState("");
|
||||
const [bereichId, setBereichId] = useState<number | "">("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
|
||||
async function anlegen() {
|
||||
if (bereichId === "") {
|
||||
@@ -36,6 +38,21 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
||||
return bereiche.find((b) => b.id === id)?.name ?? `#${id}`;
|
||||
}
|
||||
|
||||
function bearbeiten(k: Kategorie) {
|
||||
setBearbeiteteId(k.id);
|
||||
setBearbeitungsName(k.name);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/kategorien/${id}`, { method: "PATCH", body: { name: bearbeitungsName } });
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Kategorie konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Kategorie anlegen</h2>
|
||||
@@ -64,8 +81,30 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
||||
<ul className="card-list">
|
||||
{kategorien.map((k) => (
|
||||
<li key={k.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
<span>{k.name}</span>
|
||||
<span className="text-muted">{bereichName(k.bereich_id)}</span>
|
||||
{bearbeiteteId === k.id ? (
|
||||
<div className="row" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => speichern(k.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>
|
||||
{k.name} <span className="text-muted">({bereichName(k.bereich_id)})</span>
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(k)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -20,6 +20,10 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
||||
const [standortId, setStandortId] = useState<number | "">("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
||||
const [duplizierId, setDuplizierId] = useState<number | null>(null);
|
||||
const [neuerCode, setNeuerCode] = useState("");
|
||||
const [neuerName, setNeuerName] = useState("");
|
||||
const [neuerStandortId, setNeuerStandortId] = useState<number | "">("");
|
||||
|
||||
async function anlegen() {
|
||||
if (vorlageId === "" || standortId === "") {
|
||||
@@ -49,6 +53,30 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
||||
}
|
||||
}
|
||||
|
||||
function duplizierenStarten(o: Objekt) {
|
||||
setDuplizierId(duplizierId === o.id ? null : o.id);
|
||||
setNeuerCode("");
|
||||
setNeuerName(`${o.name} (Kopie)`);
|
||||
setNeuerStandortId(o.standort_id);
|
||||
}
|
||||
|
||||
async function duplizieren(quelleId: number) {
|
||||
if (neuerStandortId === "") {
|
||||
onFehler("Standort auswählen.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiRequest(`/objekte/${quelleId}/duplizieren`, {
|
||||
method: "POST",
|
||||
body: { neuer_code: neuerCode, neuer_name: neuerName, neuer_standort_id: neuerStandortId },
|
||||
});
|
||||
setDuplizierId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Objekt konnte nicht dupliziert werden (Code evtl. schon vergeben).");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Objekt aus Vorlage anlegen</h2>
|
||||
@@ -107,18 +135,57 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
||||
<span>
|
||||
{o.name} <span className="text-muted">({o.code})</span>
|
||||
</span>
|
||||
<div className="row">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
||||
>
|
||||
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => duplizierenStarten(o)}>
|
||||
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{ausgeklapptId === o.id && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<ObjektPositionenPanel objektId={o.id} materialien={materialien} onFehler={onFehler} />
|
||||
</div>
|
||||
)}
|
||||
{duplizierId === o.id && (
|
||||
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||
<input
|
||||
className="input"
|
||||
value={neuerCode}
|
||||
onChange={(e) => setNeuerCode(e.target.value)}
|
||||
placeholder="Neuer Code (eindeutig)"
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={neuerName}
|
||||
onChange={(e) => setNeuerName(e.target.value)}
|
||||
placeholder="Neuer Name"
|
||||
/>
|
||||
<select
|
||||
className="input"
|
||||
value={neuerStandortId}
|
||||
onChange={(e) => setNeuerStandortId(Number(e.target.value) || "")}
|
||||
>
|
||||
{standorte.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => duplizieren(o.id)}
|
||||
disabled={!neuerCode || !neuerName}
|
||||
>
|
||||
Duplizieren
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -16,6 +16,23 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
|
||||
const [bereichId, setBereichId] = useState<number | "">("");
|
||||
const [kategorieId, setKategorieId] = useState<number | "">("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
|
||||
function bearbeiten(o: Objekttyp) {
|
||||
setBearbeiteteId(o.id);
|
||||
setBearbeitungsName(o.name);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/objekttypen/${id}`, { method: "PATCH", body: { name: bearbeitungsName } });
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Objekttyp konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function anlegen() {
|
||||
if (bereichId === "") {
|
||||
@@ -74,8 +91,29 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
|
||||
{objekttypen.length > 0 && (
|
||||
<ul className="card-list">
|
||||
{objekttypen.map((o) => (
|
||||
<li key={o.id} style={{ padding: "0.4rem 0" }}>
|
||||
{o.name}
|
||||
<li key={o.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
{bearbeiteteId === o.id ? (
|
||||
<div className="row" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => speichern(o.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>{o.name}</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(o)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Props {
|
||||
export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
|
||||
async function anlegen() {
|
||||
setWirdAngelegt(true);
|
||||
@@ -26,6 +28,21 @@ export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(s: Standort) {
|
||||
setBearbeiteteId(s.id);
|
||||
setBearbeitungsName(s.name);
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/standorte/${id}`, { method: "PATCH", body: { name: bearbeitungsName } });
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Standort konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Standort anlegen</h2>
|
||||
@@ -45,7 +62,28 @@ export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
||||
<ul className="card-list">
|
||||
{standorte.map((s) => (
|
||||
<li key={s.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
{bearbeiteteId === s.id ? (
|
||||
<div className="row" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => speichern(s.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>{s.name}</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(s)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -24,6 +24,40 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert
|
||||
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
||||
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
||||
const [neueVersionZeilen, setNeueVersionZeilen] = useState<PositionZeile[]>([]);
|
||||
const [wirdVersioniert, setWirdVersioniert] = useState(false);
|
||||
|
||||
function versionBearbeiten(v: Beladungsvorlage) {
|
||||
setAusgeklapptId(ausgeklapptId === v.id ? null : v.id);
|
||||
setNeueVersionZeilen(
|
||||
v.positionen.map((p) => ({
|
||||
materialId: p.material_id,
|
||||
fach: p.fach ?? "",
|
||||
sollmenge: p.sollmenge,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function neueVersionZeileAendern(index: number, zeile: Partial<PositionZeile>) {
|
||||
setNeueVersionZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
|
||||
}
|
||||
|
||||
async function neueVersionSpeichern(vorlageId: number) {
|
||||
const positionen = neueVersionZeilen
|
||||
.filter((z) => z.materialId !== "")
|
||||
.map((z) => ({ material_id: z.materialId, fach: z.fach || null, sollmenge: z.sollmenge }));
|
||||
setWirdVersioniert(true);
|
||||
try {
|
||||
await apiRequest(`/vorlagen/${vorlageId}/neue-version`, { method: "POST", body: { positionen } });
|
||||
setAusgeklapptId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Neue Version konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setWirdVersioniert(false);
|
||||
}
|
||||
}
|
||||
|
||||
function zeileAendern(index: number, zeile: Partial<PositionZeile>) {
|
||||
setZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
|
||||
@@ -124,9 +158,68 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert
|
||||
{vorlagen.length > 0 && (
|
||||
<ul className="card-list" style={{ marginTop: "1.25rem" }}>
|
||||
{vorlagen.map((v) => (
|
||||
<li key={v.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
<span>{v.name}</span>
|
||||
<span className="badge badge-neutral">v{v.version}</span>
|
||||
<li key={v.id} style={{ padding: "0.4rem 0" }}>
|
||||
<div className="row-between">
|
||||
<span>
|
||||
{v.name} <span className="badge badge-neutral">v{v.version}</span>
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => versionBearbeiten(v)}>
|
||||
{ausgeklapptId === v.id ? "Schließen" : "Positionen / neue Version"}
|
||||
</button>
|
||||
</div>
|
||||
{ausgeklapptId === v.id && (
|
||||
<div className="stack" style={{ marginTop: "0.6rem" }}>
|
||||
<p className="text-muted" style={{ marginBottom: 0 }}>
|
||||
Änderungen erzeugen eine neue Version – die aktuelle v{v.version} bleibt unverändert
|
||||
erhalten.
|
||||
</p>
|
||||
{neueVersionZeilen.map((zeile, i) => (
|
||||
<div className="row" key={i}>
|
||||
<select
|
||||
className="input"
|
||||
value={zeile.materialId}
|
||||
onChange={(e) =>
|
||||
neueVersionZeileAendern(i, { materialId: Number(e.target.value) || "" })
|
||||
}
|
||||
>
|
||||
<option value="">Material…</option>
|
||||
{materialien.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: "8rem" }}
|
||||
value={zeile.fach}
|
||||
onChange={(e) => neueVersionZeileAendern(i, { fach: e.target.value })}
|
||||
placeholder="Fach"
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: "5rem" }}
|
||||
type="number"
|
||||
value={zeile.sollmenge}
|
||||
onChange={(e) => neueVersionZeileAendern(i, { sollmenge: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setNeueVersionZeilen((z) => [...z, LEERE_ZEILE])}
|
||||
>
|
||||
+ Position
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => neueVersionSpeichern(v.id)}
|
||||
disabled={wirdVersioniert}
|
||||
>
|
||||
Neue Version speichern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user