From 43b709399678742f6b0edd85acbf7b6c073c5b46 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Wed, 25 Mar 2026 08:22:32 +0100 Subject: [PATCH] fix permissions --- frontend/src/pages/Issues.tsx | 482 ++++++++++++---------------------- 1 file changed, 171 insertions(+), 311 deletions(-) diff --git a/frontend/src/pages/Issues.tsx b/frontend/src/pages/Issues.tsx index 364c12b..85dbf67 100644 --- a/frontend/src/pages/Issues.tsx +++ b/frontend/src/pages/Issues.tsx @@ -530,226 +530,48 @@ function FilterBar({ ); } -// ── Issue Type Admin ── +// ── Shared color picker helpers ── -function IssueTypeAdmin() { - const queryClient = useQueryClient(); - const { showSuccess, showError } = useNotification(); - const [editId, setEditId] = useState(null); - const [editData, setEditData] = useState>({}); - const [createOpen, setCreateOpen] = useState(false); - const [createData, setCreateData] = useState>({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true }); - - const { data: types = [], isLoading } = useQuery({ - queryKey: ['issue-types'], - queryFn: issuesApi.getTypes, - }); - - const createMut = useMutation({ - mutationFn: (data: Partial) => issuesApi.createType(data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['issue-types'] }); - showSuccess('Kategorie erstellt'); - setCreateOpen(false); - setCreateData({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true }); - }, - onError: () => showError('Fehler beim Erstellen'), - }); - - const updateMut = useMutation({ - mutationFn: ({ id, data }: { id: number; data: Partial }) => issuesApi.updateType(id, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['issue-types'] }); - showSuccess('Kategorie aktualisiert'); - setEditId(null); - }, - onError: () => showError('Fehler beim Aktualisieren'), - }); - - const deleteMut = useMutation({ - mutationFn: (id: number) => issuesApi.deleteType(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['issue-types'] }); - showSuccess('Kategorie gelöscht'); - }, - onError: () => showError('Fehler beim Löschen'), - }); - - const startEdit = (t: IssueTyp) => { - setEditId(t.id); - setEditData({ name: t.name, icon: t.icon, farbe: t.farbe, erlaubt_abgelehnt: t.erlaubt_abgelehnt, sort_order: t.sort_order, aktiv: t.aktiv, parent_id: t.parent_id }); - }; - - // Flatten types with children indented - const flatTypes = useMemo(() => { - const roots = types.filter(t => !t.parent_id); - const result: { type: IssueTyp; indent: boolean }[] = []; - for (const root of roots) { - result.push({ type: root, indent: false }); - const children = types.filter(t => t.parent_id === root.id); - for (const child of children) { - result.push({ type: child, indent: true }); - } - } - // Add orphans (parent_id set but parent missing) - const listed = new Set(result.map(r => r.type.id)); - for (const t of types) { - if (!listed.has(t.id)) result.push({ type: t, indent: false }); - } - return result; - }, [types]); - - if (isLoading) return ; +const MUI_CHIP_COLORS = ['default', 'primary', 'secondary', 'error', 'info', 'success', 'warning'] as const; +const MUI_THEME_COLORS: Record = { default: '#9e9e9e', primary: '#1976d2', secondary: '#9c27b0', error: '#d32f2f', info: '#0288d1', success: '#2e7d32', warning: '#ed6c02', action: '#757575' }; +const ICON_COLORS = ['action', 'error', 'info', 'success', 'warning', 'primary', 'secondary'] as const; +function ColorSwatch({ colors, value, onChange }: { colors: readonly string[]; value: string; onChange: (v: string) => void }) { return ( - - - Issue-Kategorien - - - - - - - - Name - Icon - Farbe - Abgelehnt erlaubt - Sort - Aktiv - Aktionen - - - - {flatTypes.map(({ type: t, indent }) => ( - - - - {indent && } - {editId === t.id ? ( - setEditData({ ...editData, name: e.target.value })} /> - ) : ( - {t.name} - )} - - - - {editId === t.id ? ( - - ) : ( - {getTypIcon(t.icon, t.farbe)} - )} - - - {editId === t.id ? ( - - ) : ( - - )} - - - {editId === t.id ? ( - setEditData({ ...editData, erlaubt_abgelehnt: e.target.checked })} size="small" /> - ) : ( - {t.erlaubt_abgelehnt ? 'Ja' : 'Nein'} - )} - - - {editId === t.id ? ( - setEditData({ ...editData, sort_order: parseInt(e.target.value) || 0 })} /> - ) : ( - t.sort_order - )} - - - {editId === t.id ? ( - setEditData({ ...editData, aktiv: e.target.checked })} size="small" /> - ) : ( - - )} - - - {editId === t.id ? ( - - - - - ) : ( - - startEdit(t)}> - deleteMut.mutate(t.id)}> - - )} - - - ))} - -
-
- - {/* Create Type Dialog */} - setCreateOpen(false)} maxWidth="sm" fullWidth> - Neue Kategorie erstellen - - setCreateData({ ...createData, name: e.target.value })} autoFocus /> - - Übergeordnete Kategorie - - - - Icon - - - - Farbe - - - setCreateData({ ...createData, erlaubt_abgelehnt: e.target.checked })} />} - label="Abgelehnt erlaubt" - /> - setCreateData({ ...createData, sort_order: parseInt(e.target.value) || 0 })} /> - - - - - - + + {colors.map((c) => ( + onChange(c)} + sx={{ + width: 22, height: 22, borderRadius: '50%', cursor: 'pointer', + bgcolor: MUI_THEME_COLORS[c] ?? c, + border: value === c ? '2.5px solid' : '2px solid transparent', + borderColor: value === c ? 'text.primary' : 'transparent', + '&:hover': { opacity: 0.8 }, + }} + /> + ))} ); } -// ── Issue Settings (Status + Prioritäten + Statusmeldungen + Kategorien) ── +function HexColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { + return ( + + ) => onChange(e.target.value)} + sx={{ width: 28, height: 28, border: 'none', borderRadius: 1, cursor: 'pointer', p: 0, bgcolor: 'transparent' }} + /> + {value} + + ); +} -const MUI_CHIP_COLORS = ['default', 'primary', 'secondary', 'error', 'info', 'success', 'warning']; +// ── Issue Settings (consolidated: Status + Prioritäten + Kategorien) ── function IssueSettings() { const queryClient = useQueryClient(); @@ -767,17 +589,18 @@ function IssueSettings() { const [prioEditId, setPrioEditId] = useState(null); const [prioEditData, setPrioEditData] = useState>({}); - const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({ - queryKey: ['issue-statuses'], - queryFn: issuesApi.getStatuses, - }); + // ── Kategorien state ── + const [typeCreateOpen, setTypeCreateOpen] = useState(false); + const [typeCreateData, setTypeCreateData] = useState>({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true }); + const [typeEditId, setTypeEditId] = useState(null); + const [typeEditData, setTypeEditData] = useState>({}); - const { data: issuePriorities = [], isLoading: prioLoading } = useQuery({ - queryKey: ['issue-priorities'], - queryFn: issuesApi.getPriorities, - }); + // ── Queries ── + const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({ queryKey: ['issue-statuses'], queryFn: issuesApi.getStatuses }); + const { data: issuePriorities = [], isLoading: prioLoading } = useQuery({ queryKey: ['issue-priorities'], queryFn: issuesApi.getPriorities }); + const { data: types = [], isLoading: typesLoading } = useQuery({ queryKey: ['issue-types'], queryFn: issuesApi.getTypes }); - // Status mutations + // ── Status mutations ── const createStatusMut = useMutation({ mutationFn: (data: Partial) => issuesApi.createStatus(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-statuses'] }); showSuccess('Status erstellt'); setStatusCreateOpen(false); setStatusCreateData({ schluessel: '', bezeichnung: '', farbe: 'default', ist_abschluss: false, ist_initial: false, benoetigt_typ_freigabe: false, sort_order: 0 }); }, @@ -794,7 +617,7 @@ function IssueSettings() { onError: () => showError('Fehler beim Deaktivieren'), }); - // Priority mutations + // ── Priority mutations ── const createPrioMut = useMutation({ mutationFn: (data: Partial) => issuesApi.createPriority(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-priorities'] }); showSuccess('Priorität erstellt'); setPrioCreateOpen(false); setPrioCreateData({ schluessel: '', bezeichnung: '', farbe: '#9e9e9e', sort_order: 0 }); }, @@ -811,10 +634,39 @@ function IssueSettings() { onError: () => showError('Fehler beim Deaktivieren'), }); + // ── Type mutations ── + const createTypeMut = useMutation({ + mutationFn: (data: Partial) => issuesApi.createType(data), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie erstellt'); setTypeCreateOpen(false); setTypeCreateData({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true }); }, + onError: () => showError('Fehler beim Erstellen'), + }); + const updateTypeMut = useMutation({ + mutationFn: ({ id, data }: { id: number; data: Partial }) => issuesApi.updateType(id, data), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie aktualisiert'); setTypeEditId(null); }, + onError: () => showError('Fehler beim Aktualisieren'), + }); + const deleteTypeMut = useMutation({ + mutationFn: (id: number) => issuesApi.deleteType(id), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie deaktiviert'); }, + onError: () => showError('Fehler beim Deaktivieren'), + }); + + const flatTypes = useMemo(() => { + const roots = types.filter(t => !t.parent_id); + const result: { type: IssueTyp; indent: boolean }[] = []; + for (const root of roots) { + result.push({ type: root, indent: false }); + for (const child of types.filter(t => t.parent_id === root.id)) result.push({ type: child, indent: true }); + } + const listed = new Set(result.map(r => r.type.id)); + for (const t of types) { if (!listed.has(t.id)) result.push({ type: t, indent: false }); } + return result; + }, [types]); + return ( - {/* Section 1: Status */} + {/* ──── Section 1: Status ──── */} Status @@ -832,47 +684,33 @@ function IssueSettings() { Initial Sort Aktiv - Aktionen + Aktionen {issueStatuses.length === 0 ? ( - Keine Status + Keine Status vorhanden ) : issueStatuses.map((s) => ( - {statusEditId === s.id ? ( - <> - setStatusEditData({ ...statusEditData, bezeichnung: e.target.value })} /> - {s.schluessel} - - - - setStatusEditData({ ...statusEditData, ist_abschluss: e.target.checked })} size="small" /> - setStatusEditData({ ...statusEditData, ist_initial: e.target.checked })} size="small" /> - setStatusEditData({ ...statusEditData, sort_order: parseInt(e.target.value) || 0 })} /> - setStatusEditData({ ...statusEditData, aktiv: e.target.checked })} size="small" /> - - updateStatusMut.mutate({ id: s.id, data: statusEditData })}> - setStatusEditId(null)}> - - - ) : ( - <> - - {s.schluessel} - {s.farbe} - {s.ist_abschluss ? '✓' : '-'} - {s.ist_initial ? '✓' : '-'} - {s.sort_order} - updateStatusMut.mutate({ id: s.id, data: { aktiv: e.target.checked } })} size="small" /> - - { setStatusEditId(s.id); setStatusEditData({ bezeichnung: s.bezeichnung, farbe: s.farbe, ist_abschluss: s.ist_abschluss, ist_initial: s.ist_initial, benoetigt_typ_freigabe: s.benoetigt_typ_freigabe, sort_order: s.sort_order, aktiv: s.aktiv }); }}> - deleteStatusMut.mutate(s.id)}> - - - )} + {statusEditId === s.id ? (<> + setStatusEditData({ ...statusEditData, bezeichnung: e.target.value })} /> + {s.schluessel} + setStatusEditData({ ...statusEditData, farbe: v })} /> + setStatusEditData({ ...statusEditData, ist_abschluss: e.target.checked })} size="small" /> + setStatusEditData({ ...statusEditData, ist_initial: e.target.checked })} size="small" /> + setStatusEditData({ ...statusEditData, sort_order: parseInt(e.target.value) || 0 })} /> + setStatusEditData({ ...statusEditData, aktiv: e.target.checked })} size="small" /> + updateStatusMut.mutate({ id: s.id, data: statusEditData })}> setStatusEditId(null)}> + ) : (<> + + {s.schluessel} + + {s.ist_abschluss ? '✓' : '-'} + {s.ist_initial ? '✓' : '-'} + {s.sort_order} + updateStatusMut.mutate({ id: s.id, data: { aktiv: e.target.checked } })} size="small" /> + { setStatusEditId(s.id); setStatusEditData({ bezeichnung: s.bezeichnung, farbe: s.farbe, ist_abschluss: s.ist_abschluss, ist_initial: s.ist_initial, benoetigt_typ_freigabe: s.benoetigt_typ_freigabe, sort_order: s.sort_order, aktiv: s.aktiv }); }}> deleteStatusMut.mutate(s.id)}> + )} ))} @@ -881,7 +719,7 @@ function IssueSettings() { )} - {/* Section 2: Prioritäten */} + {/* ──── Section 2: Prioritäten ──── */} Prioritäten @@ -897,44 +735,29 @@ function IssueSettings() { Farbe Sort Aktiv - Aktionen + Aktionen {issuePriorities.length === 0 ? ( - Keine Prioritäten + Keine Prioritäten vorhanden ) : issuePriorities.map((p) => ( - {prioEditId === p.id ? ( - <> - setPrioEditData({ ...prioEditData, bezeichnung: e.target.value })} /> - {p.schluessel} - setPrioEditData({ ...prioEditData, farbe: e.target.value })} placeholder="#hex" sx={{ width: 90 }} /> - setPrioEditData({ ...prioEditData, sort_order: parseInt(e.target.value) || 0 })} /> - setPrioEditData({ ...prioEditData, aktiv: e.target.checked })} size="small" /> - - updatePrioMut.mutate({ id: p.id, data: prioEditData })}> - setPrioEditId(null)}> - - - ) : ( - <> - - - - {p.bezeichnung} - - - {p.schluessel} - {p.farbe} - {p.sort_order} - updatePrioMut.mutate({ id: p.id, data: { aktiv: e.target.checked } })} size="small" /> - - { setPrioEditId(p.id); setPrioEditData({ bezeichnung: p.bezeichnung, farbe: p.farbe, sort_order: p.sort_order, aktiv: p.aktiv }); }}> - deletePrioMut.mutate(p.id)}> - - - )} + {prioEditId === p.id ? (<> + setPrioEditData({ ...prioEditData, bezeichnung: e.target.value })} /> + {p.schluessel} + setPrioEditData({ ...prioEditData, farbe: v })} /> + setPrioEditData({ ...prioEditData, sort_order: parseInt(e.target.value) || 0 })} /> + setPrioEditData({ ...prioEditData, aktiv: e.target.checked })} size="small" /> + updatePrioMut.mutate({ id: p.id, data: prioEditData })}> setPrioEditId(null)}> + ) : (<> + {p.bezeichnung} + {p.schluessel} + + {p.sort_order} + updatePrioMut.mutate({ id: p.id, data: { aktiv: e.target.checked } })} size="small" /> + { setPrioEditId(p.id); setPrioEditData({ bezeichnung: p.bezeichnung, farbe: p.farbe, sort_order: p.sort_order, aktiv: p.aktiv }); }}> deletePrioMut.mutate(p.id)}> + )} ))} @@ -943,53 +766,90 @@ function IssueSettings() { )} - {/* Section 3: Kategorien */} + {/* ──── Section 3: Kategorien ──── */} - + + Kategorien + + + {typesLoading ? : ( + + + + + Name + Icon + Farbe + Abgelehnt + Sort + Aktiv + Aktionen + + + + {flatTypes.length === 0 ? ( + Keine Kategorien vorhanden + ) : flatTypes.map(({ type: t, indent }) => ( + + {indent && }{typeEditId === t.id ? setTypeEditData({ ...typeEditData, name: e.target.value })} /> : {t.name}} + {typeEditId === t.id ? () : {getTypIcon(t.icon, t.farbe)}} + {typeEditId === t.id ? setTypeEditData({ ...typeEditData, farbe: v })} /> : } + {typeEditId === t.id ? setTypeEditData({ ...typeEditData, erlaubt_abgelehnt: e.target.checked })} size="small" /> : (t.erlaubt_abgelehnt ? '✓' : '-')} + {typeEditId === t.id ? setTypeEditData({ ...typeEditData, sort_order: parseInt(e.target.value) || 0 })} /> : t.sort_order} + { if (typeEditId === t.id) setTypeEditData({ ...typeEditData, aktiv: e.target.checked }); else updateTypeMut.mutate({ id: t.id, data: { aktiv: e.target.checked } }); }} size="small" /> + {typeEditId === t.id ? ( updateTypeMut.mutate({ id: t.id, data: typeEditData })}> setTypeEditId(null)}>) : ( { setTypeEditId(t.id); setTypeEditData({ name: t.name, icon: t.icon, farbe: t.farbe, erlaubt_abgelehnt: t.erlaubt_abgelehnt, sort_order: t.sort_order, aktiv: t.aktiv, parent_id: t.parent_id }); }}> deleteTypeMut.mutate(t.id)}>)} + + ))} + +
+
+ )}
- {/* Create Status Dialog */} + {/* ──── Create Status Dialog ──── */} setStatusCreateOpen(false)} maxWidth="sm" fullWidth> Neuer Status setStatusCreateData({ ...statusCreateData, schluessel: e.target.value })} helperText="Maschinenwert, z.B. 'in_pruefung' (nicht änderbar)" autoFocus /> setStatusCreateData({ ...statusCreateData, bezeichnung: e.target.value })} /> - - Farbe - - + Farbe setStatusCreateData({ ...statusCreateData, farbe: v })} /> setStatusCreateData({ ...statusCreateData, sort_order: parseInt(e.target.value) || 0 })} /> setStatusCreateData({ ...statusCreateData, ist_abschluss: e.target.checked })} />} label="Abschluss-Status (erledigte Issues)" /> setStatusCreateData({ ...statusCreateData, ist_initial: e.target.checked })} />} label="Initial-Status (Ziel beim Wiedereröffnen)" /> setStatusCreateData({ ...statusCreateData, benoetigt_typ_freigabe: e.target.checked })} />} label="Nur bei Typ-Freigabe erlaubt" /> - - - - + - {/* Create Priority Dialog */} + {/* ──── Create Priority Dialog ──── */} setPrioCreateOpen(false)} maxWidth="sm" fullWidth> Neue Priorität setPrioCreateData({ ...prioCreateData, schluessel: e.target.value })} helperText="Maschinenwert, z.B. 'kritisch'" autoFocus /> setPrioCreateData({ ...prioCreateData, bezeichnung: e.target.value })} /> - setPrioCreateData({ ...prioCreateData, farbe: e.target.value })} placeholder="#d32f2f" /> + Farbe setPrioCreateData({ ...prioCreateData, farbe: v })} /> setPrioCreateData({ ...prioCreateData, sort_order: parseInt(e.target.value) || 0 })} /> - - - - + + + + {/* ──── Create Kategorie Dialog ──── */} + setTypeCreateOpen(false)} maxWidth="sm" fullWidth> + Neue Kategorie + + setTypeCreateData({ ...typeCreateData, name: e.target.value })} autoFocus /> + Übergeordnete Kategorie + Icon + Farbe setTypeCreateData({ ...typeCreateData, farbe: v })} /> + setTypeCreateData({ ...typeCreateData, erlaubt_abgelehnt: e.target.checked })} />} label="Abgelehnt erlaubt" /> + setTypeCreateData({ ...typeCreateData, sort_order: parseInt(e.target.value) || 0 })} /> + +
); } - // ── Main Page ── export default function Issues() {