feat: user data purge, breadcrumbs, first-login dialog, widget consolidation, bookkeeping cascade

- Admin can purge all personal data for a user (POST /api/admin/users/:userId/purge-data)
  while keeping the account; clears profile, notifications, bookings, ical tokens, preferences
- Add isNewUser flag to auth callback response; first-login dialog prompts for Standesbuchnummer
- Add PageBreadcrumbs component and apply to 18 sub-pages across the app
- Cascade budget_typ changes from parent pot to all children recursively, converting amounts
  (detailliert→einfach: sum into budget_gesamt; einfach→detailliert: zero all for redistribution)
- Migrate NextcloudTalkWidget to use shared WidgetCard template for consistent header styling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-04-13 16:15:28 +02:00
parent a0b3c0ec5c
commit b477e5dbe0
32 changed files with 485 additions and 49 deletions

View File

@@ -32,6 +32,7 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Navigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { settingsApi } from '../services/settings';
@@ -293,6 +294,10 @@ function AdminSettings() {
return (
<DashboardLayout>
<Container maxWidth="lg">
<PageBreadcrumbs items={[
{ label: 'Admin', href: '/admin' },
{ label: 'Einstellungen' },
]} />
<Typography variant="h4" gutterBottom sx={{ mb: 4 }}>
Admin-Einstellungen
</Typography>

View File

@@ -779,6 +779,10 @@ function AusruestungDetailPage() {
<DetailLayout
title={equipment.bezeichnung}
backTo="/ausruestung"
breadcrumbs={[
{ label: 'Ausrüstung', href: '/ausruestung' },
{ label: equipment.bezeichnung },
]}
tabs={tabs}
actions={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>

View File

@@ -11,6 +11,7 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
@@ -285,6 +286,10 @@ export default function AusruestungsanfrageArtikelDetail() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Ausrüstungsanfragen', href: '/ausruestungsanfrage' },
{ label: isCreate ? 'Neuer Katalogartikel' : (artikel?.bezeichnung ?? 'Artikel') },
]} />
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<IconButton onClick={() => navigate('/ausruestungsanfrage?tab=2')}>

View File

@@ -13,6 +13,7 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useAuth } from '../contexts/AuthContext';
@@ -193,6 +194,10 @@ export default function AusruestungsanfrageDetail() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Ausrüstungsanfragen', href: '/ausruestungsanfrage' },
{ label: anfrage ? `Anfrage ${formatOrderId(anfrage)}` : 'Anfrage' },
]} />
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<IconButton onClick={() => navigate('/ausruestungsanfrage')}>

View File

@@ -7,6 +7,7 @@ import { ArrowBack, Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-mate
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
@@ -168,6 +169,10 @@ export default function AusruestungsanfrageNeu() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Ausrüstungsanfragen', href: '/ausruestungsanfrage' },
{ label: 'Neue Bestellung' },
]} />
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<IconButton onClick={() => navigate('/ausruestungsanfrage')}>

View File

@@ -14,6 +14,7 @@ import {
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
@@ -236,6 +237,11 @@ export default function AusruestungsanfrageZuBestellung() {
return (
<DashboardLayout>
<Box sx={{ maxWidth: 960, mx: 'auto', px: 2, py: 3 }}>
<PageBreadcrumbs items={[
{ label: 'Ausrüstungsanfragen', href: '/ausruestungsanfrage' },
{ label: anfrage.bezeichnung || `Anfrage ${formatOrderId(anfrage)}`, href: `/ausruestungsanfrage/${requestId}` },
{ label: 'Bestellen' },
]} />
{/* ── Header ── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 3 }}>

View File

@@ -673,6 +673,10 @@ export default function BestellungDetail() {
<PageHeader
title={bestellung.bezeichnung}
backTo="/bestellungen"
breadcrumbs={[
{ label: 'Bestellungen', href: '/bestellungen' },
{ label: bestellung.bezeichnung },
]}
actions={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{canExport && !editMode && (

View File

@@ -23,6 +23,7 @@ import { ArrowBack } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { buchhaltungApi } from '../services/buchhaltung';
import { TRANSAKTION_TYP_LABELS } from '../types/buchhaltung.types';
import type { BankkontoStatementRow } from '../types/buchhaltung.types';
@@ -61,6 +62,10 @@ export default function BuchhaltungBankkontoDetail() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Buchhaltung', href: '/buchhaltung' },
{ label: data?.bankkonto?.bezeichnung || 'Bankkonto' },
]} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Tooltip title="Zurueck">
<IconButton onClick={() => navigate('/buchhaltung?tab=2')}>

View File

@@ -9,6 +9,7 @@ import {
} from '@mui/material';
import { ArrowBack, KeyboardArrowDown, KeyboardArrowUp } from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { buchhaltungApi } from '../services/buchhaltung';
import { AUSGABEN_TYP_LABELS } from '../types/buchhaltung.types';
import type { AusgabenTyp, BuchhaltungAudit } from '../types/buchhaltung.types';
@@ -132,6 +133,10 @@ export default function BuchhaltungKontoDetail() {
return (
<DashboardLayout>
<Box sx={{ p: 3 }}>
<PageBreadcrumbs items={[
{ label: 'Buchhaltung', href: '/buchhaltung' },
{ label: `${konto.kontonummer}${konto.bezeichnung}` },
]} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/buchhaltung')}>
Zurück

View File

@@ -8,6 +8,7 @@ import {
} from '@mui/material';
import { ArrowBack, Delete, Edit, Save, Cancel } from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { buchhaltungApi } from '../services/buchhaltung';
import { useNotification } from '../contexts/NotificationContext';
import type { KontoFormData, BudgetTyp } from '../types/buchhaltung.types';
@@ -172,6 +173,11 @@ export default function BuchhaltungKontoManage() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Buchhaltung', href: '/buchhaltung' },
{ label: `${konto.kontonummer}${konto.bezeichnung}`, href: `/buchhaltung/konto/${id}` },
{ label: 'Verwalten' },
]} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/buchhaltung?tab=2')}>
Zurück

View File

@@ -19,6 +19,7 @@ import { ArrowBack, CheckCircle, Cancel, RemoveCircle } from '@mui/icons-materia
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { checklistenApi } from '../services/checklisten';
@@ -243,6 +244,10 @@ export default function ChecklistAusfuehrung() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Checklisten', href: '/checklisten' },
{ label: execution.vorlage_name ?? 'Checkliste' },
]} />
<Button startIcon={<ArrowBack />} onClick={() => navigate('/checklisten')} sx={{ mb: 2 }} size="small">
Checklisten
</Button>

View File

@@ -284,6 +284,10 @@ function EinsatzDetail() {
title={`Einsatz ${einsatz.einsatz_nr}`}
subtitle={address || undefined}
backTo="/einsaetze"
breadcrumbs={[
{ label: 'Einsätze', href: '/einsaetze' },
{ label: `Einsatz ${einsatz.einsatz_nr}` },
]}
actions={
<Stack direction="row" spacing={1} alignItems="center">
<Chip

View File

@@ -1006,6 +1006,10 @@ function FahrzeugDetail() {
<DetailLayout
title={titleText}
backTo="/fahrzeuge"
breadcrumbs={[
{ label: 'Fahrzeuge', href: '/fahrzeuge' },
{ label: titleText },
]}
tabs={tabs}
actions={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import {
Alert,
Box,
@@ -143,6 +144,10 @@ export default function HaushaltsplanDetail() {
return (
<DashboardLayout>
<PageBreadcrumbs items={[
{ label: 'Haushaltsplan', href: '/haushaltsplan' },
{ label: planung.bezeichnung },
]} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<IconButton onClick={() => navigate('/haushaltsplan')}><ArrowBack /></IconButton>
<Typography variant="h4" sx={{ flexGrow: 1 }}>{planung.bezeichnung}</Typography>

View File

@@ -264,6 +264,10 @@ export default function IssueDetail() {
<PageHeader
title={`${formatIssueId(issue)}${issue.titel}`}
backTo="/issues"
breadcrumbs={[
{ label: 'Issues', href: '/issues' },
{ label: `${formatIssueId(issue)}${issue.titel}` },
]}
actions={
<Chip
label={getStatusLabel(statuses, issue.status)}

View File

@@ -419,6 +419,10 @@ function MitgliedDetail() {
<PageHeader
title={displayName}
backTo="/mitglieder"
breadcrumbs={[
{ label: 'Mitglieder', href: '/mitglieder' },
{ label: displayName },
]}
/>
{/* Header card */}

View File

@@ -42,6 +42,7 @@ import {
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { usePermissionContext } from '../contexts/PermissionContext';
import { trainingApi } from '../services/training';
import type { TeilnahmeStatus, UebungTyp, Teilnahme } from '../types/training.types';
@@ -332,6 +333,10 @@ export default function UebungDetail() {
return (
<DashboardLayout>
<Box sx={{ maxWidth: 720, mx: 'auto' }}>
<PageBreadcrumbs items={[
{ label: 'Kalender', href: '/kalender' },
{ label: event.titel || 'Übung' },
]} />
{/* Back button */}
<Button
startIcon={<BackIcon />}

View File

@@ -34,6 +34,7 @@ import {
Category as CategoryIcon,
} from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageBreadcrumbs } from '../components/common';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { eventsApi } from '../services/events';
@@ -339,6 +340,10 @@ export default function VeranstaltungKategorien() {
return (
<DashboardLayout>
<Container maxWidth="lg" sx={{ py: 3 }}>
<PageBreadcrumbs items={[
{ label: 'Veranstaltungen', href: '/veranstaltungen' },
{ label: 'Kategorien' },
]} />
{/* Header */}
<Box
sx={{