diff --git a/backend/src/controllers/ausruestungsanfrage.controller.ts b/backend/src/controllers/ausruestungsanfrage.controller.ts index 907adb4..05510e5 100644 --- a/backend/src/controllers/ausruestungsanfrage.controller.ts +++ b/backend/src/controllers/ausruestungsanfrage.controller.ts @@ -572,6 +572,7 @@ class AusruestungsanfrageController { benutzerName?: string; groesse?: string; kategorie?: string; + eigenschaften?: Array<{ eigenschaft_id?: number; name: string; wert: string }>; }>; }; diff --git a/backend/src/controllers/personalEquipment.controller.ts b/backend/src/controllers/personalEquipment.controller.ts index ebac14d..341026a 100644 --- a/backend/src/controllers/personalEquipment.controller.ts +++ b/backend/src/controllers/personalEquipment.controller.ts @@ -15,6 +15,12 @@ const isoDate = z.string().regex( const ZustandEnum = z.enum(['gut', 'beschaedigt', 'abgaengig', 'verloren']); +const EigenschaftInput = z.object({ + eigenschaft_id: z.number().int().positive().nullable().optional(), + name: z.string().min(1).max(200), + wert: z.string().max(500), +}); + const CreateSchema = z.object({ bezeichnung: z.string().min(1).max(200), kategorie: z.string().max(100).optional(), @@ -27,6 +33,7 @@ const CreateSchema = z.object({ anschaffung_datum: isoDate.optional(), zustand: ZustandEnum.optional(), notizen: z.string().max(2000).optional(), + eigenschaften: z.array(EigenschaftInput).optional(), }); const UpdateSchema = z.object({ @@ -41,6 +48,7 @@ const UpdateSchema = z.object({ anschaffung_datum: isoDate.nullable().optional(), zustand: ZustandEnum.optional(), notizen: z.string().max(2000).nullable().optional(), + eigenschaften: z.array(EigenschaftInput).nullable().optional(), }); function isValidUUID(s: string): boolean { diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index 41feb71..c1543e3 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -959,6 +959,7 @@ interface AssignmentInput { benutzerName?: string; groesse?: string; kategorie?: string; + eigenschaften?: { eigenschaft_id?: number; name: string; wert: string }[]; } async function assignDeliveredItems( @@ -1057,20 +1058,31 @@ async function assignDeliveredItems( ); const newId = insertResult.rows[0].id; - // Copy position eigenschaften to persoenliche_ausruestung_eigenschaften - const posEigResult = await client.query( - `SELECT poe.eigenschaft_id, aae.name, poe.wert - FROM ausruestung_position_eigenschaften poe - JOIN ausruestung_artikel_eigenschaften aae ON aae.id = poe.eigenschaft_id - WHERE poe.position_id = $1`, - [a.positionId], - ); - for (const row of posEigResult.rows) { - await client.query( - `INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert) - VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`, - [newId, row.eigenschaft_id, row.name, row.wert], + // Copy eigenschaften: use frontend-provided values if any, otherwise copy from position + const providedEig = a.eigenschaften && a.eigenschaften.length > 0; + if (providedEig) { + for (const e of a.eigenschaften!) { + await client.query( + `INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert) + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`, + [newId, e.eigenschaft_id ?? null, e.name, e.wert], + ); + } + } else { + const posEigResult = await client.query( + `SELECT poe.eigenschaft_id, aae.name, poe.wert + FROM ausruestung_position_eigenschaften poe + JOIN ausruestung_artikel_eigenschaften aae ON aae.id = poe.eigenschaft_id + WHERE poe.position_id = $1`, + [a.positionId], ); + for (const row of posEigResult.rows) { + await client.query( + `INSERT INTO persoenliche_ausruestung_eigenschaften (persoenlich_id, eigenschaft_id, name, wert) + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`, + [newId, row.eigenschaft_id, row.name, row.wert], + ); + } } await client.query( diff --git a/backend/src/services/personalEquipment.service.ts b/backend/src/services/personalEquipment.service.ts index 68e6386..85cea4d 100644 --- a/backend/src/services/personalEquipment.service.ts +++ b/backend/src/services/personalEquipment.service.ts @@ -19,7 +19,7 @@ interface CreatePersonalEquipmentData { anschaffung_datum?: string; zustand?: string; notizen?: string; - eigenschaften?: { eigenschaft_id?: number; name: string; wert: string }[]; + eigenschaften?: { eigenschaft_id?: number | null; name: string; wert: string }[]; } interface UpdatePersonalEquipmentData { diff --git a/frontend/src/pages/AusruestungsanfrageZuweisung.tsx b/frontend/src/pages/AusruestungsanfrageZuweisung.tsx index e15ea7d..c11051a 100644 --- a/frontend/src/pages/AusruestungsanfrageZuweisung.tsx +++ b/frontend/src/pages/AusruestungsanfrageZuweisung.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from 'react'; import { Box, Typography, Container, Button, Chip, TextField, Autocomplete, ToggleButton, ToggleButtonGroup, - Stack, Divider, LinearProgress, + Stack, Divider, LinearProgress, MenuItem, } from '@mui/material'; import { Assignment as AssignmentIcon } from '@mui/icons-material'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -15,7 +15,7 @@ import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage'; import { personalEquipmentApi } from '../services/personalEquipment'; import { vehiclesApi } from '../services/vehicles'; import { membersService } from '../services/members'; -import type { AusruestungAnfragePosition } from '../types/ausruestungsanfrage.types'; +import type { AusruestungAnfragePosition, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types'; type AssignmentTyp = 'ausruestung' | 'persoenlich' | 'keine'; @@ -25,6 +25,7 @@ interface PositionAssignment { standort?: string; userId?: string; benutzerName?: string; + eigenschaften?: Record; // eigenschaft_id → wert } function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] { @@ -58,12 +59,38 @@ export default function AusruestungsanfrageZuweisung() { const [assignments, setAssignments] = useState>({}); + // Collect unique artikel_ids from unassigned positions to batch-load their eigenschaften + const uniqueArtikelIds = useMemo( + () => [...new Set(unassigned.filter(p => p.artikel_id).map(p => p.artikel_id!))], + [unassigned], + ); + + const { data: artikelEigenschaftenMap = {} } = useQuery>({ + queryKey: ['ausruestungsanfrage', 'eigenschaften-batch', uniqueArtikelIds], + queryFn: async () => { + const results: Record = {}; + await Promise.all( + uniqueArtikelIds.map(async (aid) => { + results[aid] = await ausruestungsanfrageApi.getArtikelEigenschaften(aid); + }), + ); + return results; + }, + enabled: uniqueArtikelIds.length > 0, + staleTime: 5 * 60 * 1000, + }); + // Initialize assignments when unassigned positions load useMemo(() => { if (unassigned.length > 0 && Object.keys(assignments).length === 0) { const init: Record = {}; for (const p of unassigned) { - init[p.id] = { typ: 'persoenlich' }; + // Pre-fill eigenschaften from position values + const prefilled: Record = {}; + for (const e of p.eigenschaften ?? []) { + prefilled[e.eigenschaft_id] = e.wert; + } + init[p.id] = { typ: 'persoenlich', eigenschaften: prefilled }; } setAssignments(init); } @@ -135,14 +162,30 @@ export default function AusruestungsanfrageZuweisung() { setSubmitting(true); try { const anfrage = detail.anfrage; - const payload = Object.entries(assignments).map(([posId, a]) => ({ - positionId: Number(posId), - typ: a.typ, - fahrzeugId: a.typ === 'ausruestung' ? a.fahrzeugId : undefined, - standort: a.typ === 'ausruestung' ? a.standort : undefined, - userId: a.typ === 'persoenlich' ? a.userId : undefined, - benutzerName: a.typ === 'persoenlich' ? (a.benutzerName || anfrage.fuer_benutzer_name || anfrage.anfrager_name) : undefined, - })); + const posMap = Object.fromEntries(unassigned.map(p => [p.id, p])); + const payload = Object.entries(assignments).map(([posId, a]) => { + const pos = posMap[Number(posId)]; + const artikelEigs: AusruestungEigenschaft[] = pos?.artikel_id + ? (artikelEigenschaftenMap[pos.artikel_id] ?? []) + : []; + return { + positionId: Number(posId), + typ: a.typ, + fahrzeugId: a.typ === 'ausruestung' ? a.fahrzeugId : undefined, + standort: a.typ === 'ausruestung' ? a.standort : undefined, + userId: a.typ === 'persoenlich' ? a.userId : undefined, + benutzerName: a.typ === 'persoenlich' ? (a.benutzerName || anfrage.fuer_benutzer_name || anfrage.anfrager_name) : undefined, + eigenschaften: a.typ === 'persoenlich' && a.eigenschaften + ? Object.entries(a.eigenschaften) + .filter(([, v]) => v.trim()) + .map(([eid, wert]) => ({ + eigenschaft_id: Number(eid), + name: artikelEigs.find(e => e.id === Number(eid))?.name ?? '', + wert, + })) + : undefined, + }; + }); await ausruestungsanfrageApi.assignItems(anfrageId, payload); showSuccess('Gegenstände zugewiesen'); navigate(`/ausruestungsanfrage/${id}`); @@ -266,34 +309,59 @@ export default function AusruestungsanfrageZuweisung() { )} {a.typ === 'persoenlich' && ( - - o.name} - value={memberOptions.find((m) => m.id === a.userId) ?? null} - onChange={(_e, v) => updateAssignment(pos.id, { userId: v?.id, benutzerName: v?.name })} - renderInput={(params) => ( + + + o.name} + value={memberOptions.find((m) => m.id === a.userId) ?? null} + onChange={(_e, v) => updateAssignment(pos.id, { userId: v?.id, benutzerName: v?.name })} + renderInput={(params) => ( + + )} + sx={{ minWidth: 200, flex: 1 }} + /> + {(() => { + if (!a.userId || !pos.artikel_id) return null; + const count = allPersonalItems.filter(i => i.user_id === a.userId && i.artikel_id === pos.artikel_id).length; + if (count === 0) return null; + return Hat bereits {count} Stk.; + })()} + + {/* Editable characteristic fields from article definitions */} + {pos.artikel_id && (artikelEigenschaftenMap[pos.artikel_id] ?? []).map(e => + e.typ === 'options' && e.optionen?.length ? ( updateAssignment(pos.id, { + eigenschaften: { ...a.eigenschaften, [e.id]: ev.target.value }, + })} + > + + {e.optionen.map(opt => {opt})} + + ) : ( + updateAssignment(pos.id, { + eigenschaften: { ...a.eigenschaften, [e.id]: ev.target.value }, + })} /> - )} - sx={{ minWidth: 200, flex: 1 }} - /> - {(() => { - if (!a.userId || !pos.artikel_id) return null; - const count = allPersonalItems.filter(i => i.user_id === a.userId && i.artikel_id === pos.artikel_id).length; - if (count === 0) return null; - return Hat bereits {count} Stk.; - })()} - {pos.eigenschaften && pos.eigenschaften.length > 0 && ( - - {pos.eigenschaften.map(e => ( - - ))} - + ) )} )} diff --git a/frontend/src/services/ausruestungsanfrage.ts b/frontend/src/services/ausruestungsanfrage.ts index 754245d..cb7d3a2 100644 --- a/frontend/src/services/ausruestungsanfrage.ts +++ b/frontend/src/services/ausruestungsanfrage.ts @@ -147,6 +147,7 @@ export const ausruestungsanfrageApi = { benutzerName?: string; groesse?: string; kategorie?: string; + eigenschaften?: Array<{ eigenschaft_id?: number; name: string; wert: string }>; }>, ): Promise => { await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/assign`, { assignments });