This commit is contained in:
Matthias Hochmeister
2026-03-13 14:01:06 +01:00
parent 3361f1e28d
commit 7215e7f472
10 changed files with 119 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
import { useRef, useEffect, useCallback } from 'react';
import { useState, useRef, useEffect, useCallback } from 'react';
import {
Box, Button, Card, CardContent, Chip, CircularProgress, IconButton, Tooltip, Typography,
Box, Button, Card, CardContent, Checkbox, Chip, CircularProgress, FormControlLabel, IconButton, Tooltip, Typography,
} from '@mui/material';
import SyncIcon from '@mui/icons-material/Sync';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
@@ -12,6 +12,7 @@ function FdiskSyncTab() {
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const logBoxRef = useRef<HTMLDivElement>(null);
const [force, setForce] = useState(false);
const { data, isLoading, isError } = useQuery({
queryKey: ['admin', 'fdisk-sync', 'logs'],
@@ -27,7 +28,7 @@ function FdiskSyncTab() {
}, [data?.logs.length]);
const triggerMutation = useMutation({
mutationFn: adminApi.fdiskSyncTrigger,
mutationFn: (forceSync: boolean) => adminApi.fdiskSyncTrigger(forceSync),
onSuccess: () => {
showSuccess('Sync gestartet');
queryClient.invalidateQueries({ queryKey: ['admin', 'fdisk-sync', 'logs'] });
@@ -71,12 +72,16 @@ function FdiskSyncTab() {
<Button
variant="contained"
startIcon={<SyncIcon />}
onClick={() => triggerMutation.mutate()}
onClick={() => triggerMutation.mutate(force)}
disabled={running || triggerMutation.isPending}
>
Jetzt synchronisieren
</Button>
</Box>
<FormControlLabel
control={<Checkbox checked={force} onChange={(e) => setForce(e.target.checked)} />}
label="Alle Mitglieder erzwungen synchronisieren"
/>
</CardContent>
</Card>

View File

@@ -7,6 +7,7 @@ import {
CardContent,
CardHeader,
Avatar,
Autocomplete,
Button,
Chip,
Tabs,
@@ -41,9 +42,11 @@ import {
MemberWithProfile,
StatusEnum,
DienstgradEnum,
FunktionEnum,
TshirtGroesseEnum,
DIENSTGRAD_VALUES,
STATUS_VALUES,
FUNKTION_VALUES,
TSHIRT_GROESSE_VALUES,
STATUS_LABELS,
STATUS_COLORS,
@@ -239,6 +242,7 @@ function MitgliedDetail() {
funktion: member.profile.funktion,
status: member.profile.status,
eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined,
austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined,
geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined,
telefon_mobil: member.profile.telefon_mobil ?? undefined,
telefon_privat: member.profile.telefon_privat ?? undefined,
@@ -248,6 +252,7 @@ function MitgliedDetail() {
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
schuhgroesse: member.profile.schuhgroesse ?? undefined,
bemerkungen: member.profile.bemerkungen ?? undefined,
fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined,
});
}
}, [member]);
@@ -263,6 +268,7 @@ function MitgliedDetail() {
const payload: UpdateMemberProfileData = {
...formData,
eintrittsdatum: formData.eintrittsdatum ? fromGermanDate(formData.eintrittsdatum) || undefined : undefined,
austrittsdatum: formData.austrittsdatum ? fromGermanDate(formData.austrittsdatum) || undefined : undefined,
geburtsdatum: formData.geburtsdatum ? fromGermanDate(formData.geburtsdatum) || undefined : undefined,
dienstgrad_seit: formData.dienstgrad_seit ? fromGermanDate(formData.dienstgrad_seit) || undefined : undefined,
};
@@ -282,12 +288,23 @@ function MitgliedDetail() {
// Reset form to current profile values
if (member?.profile) {
setFormData({
mitglieds_nr: member.profile.mitglieds_nr ?? undefined,
dienstgrad: member.profile.dienstgrad ?? undefined,
dienstgrad_seit: toGermanDate(member.profile.dienstgrad_seit) || undefined,
funktion: member.profile.funktion,
status: member.profile.status,
eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined,
austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined,
geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined,
telefon_mobil: member.profile.telefon_mobil ?? undefined,
telefon_privat: member.profile.telefon_privat ?? undefined,
notfallkontakt_name: member.profile.notfallkontakt_name ?? undefined,
notfallkontakt_telefon: member.profile.notfallkontakt_telefon ?? undefined,
fuehrerscheinklassen: member.profile.fuehrerscheinklassen,
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
schuhgroesse: member.profile.schuhgroesse ?? undefined,
bemerkungen: member.profile.bemerkungen ?? undefined,
fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined,
});
}
};
@@ -542,6 +559,35 @@ function MitgliedDetail() {
onChange={(e) => handleFieldChange('geburtsdatum', e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
/>
<TextField
label="Austrittsdatum"
fullWidth
size="small"
placeholder="TT.MM.JJJJ"
value={formData.austrittsdatum ?? ''}
onChange={(e) => handleFieldChange('austrittsdatum', e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
/>
<TextField
label="Standesbuchnummer"
fullWidth
size="small"
value={formData.fdisk_standesbuch_nr ?? ''}
onChange={(e) => handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)}
/>
<Autocomplete
multiple
options={[...FUNKTION_VALUES]}
value={formData.funktion ?? []}
onChange={(_e, newValue) => handleFieldChange('funktion', newValue as FunktionEnum[])}
renderInput={(params) => (
<TextField {...params} label="Funktion" size="small" />
)}
size="small"
/>
</Stack>
) : (
<>
@@ -574,6 +620,13 @@ function MitgliedDetail() {
: null
}
/>
<FieldRow
label="Austrittsdatum"
value={profile?.austrittsdatum
? new Date(profile.austrittsdatum).toLocaleDateString('de-AT')
: null}
/>
<FieldRow label="Standesbuchnummer" value={profile?.fdisk_standesbuch_nr ?? null} />
</>
)}
</CardContent>
@@ -700,7 +753,19 @@ function MitgliedDetail() {
title="Führerscheinklassen"
/>
<CardContent>
{profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? (
{editMode && canWrite ? (
<Autocomplete
multiple
freeSolo
options={['AM', 'A', 'B', 'BE', 'C', 'CE', 'C1', 'C1E', 'D', 'DE', 'D1', 'D1E', 'F']}
value={formData.fuehrerscheinklassen ?? []}
onChange={(_e, newValue) => handleFieldChange('fuehrerscheinklassen', newValue)}
renderInput={(params) => (
<TextField {...params} label="Führerscheinklassen" size="small" placeholder="Klasse hinzufügen…" />
)}
size="small"
/>
) : profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{profile.fuehrerscheinklassen.map((k) => (
<Chip key={k} label={k} size="small" variant="outlined" />

View File

@@ -28,5 +28,5 @@ export const adminApi = {
broadcast: (data: BroadcastPayload) => api.post<ApiResponse<{ sent: number }>>('/api/admin/notifications/broadcast', data).then(r => r.data.data),
getPingHistory: (serviceId: string) => api.get<ApiResponse<PingHistoryEntry[]>>(`/api/admin/services/${serviceId}/ping-history`).then(r => r.data.data),
fdiskSyncLogs: () => api.get<ApiResponse<FdiskSyncLogsResponse>>('/api/admin/fdisk-sync/logs').then(r => r.data.data),
fdiskSyncTrigger: () => api.post<ApiResponse<{ started: boolean }>>('/api/admin/fdisk-sync/trigger').then(r => r.data.data),
fdiskSyncTrigger: (force = false) => api.post<ApiResponse<{ started: boolean }>>('/api/admin/fdisk-sync/trigger', { force }).then(r => r.data.data),
};

View File

@@ -73,6 +73,7 @@ export interface MitgliederProfile {
tshirt_groesse: TshirtGroesseEnum | null;
schuhgroesse: string | null;
bemerkungen: string | null;
fdisk_standesbuch_nr: string | null;
bild_url: string | null;
created_at: string;
updated_at: string;