import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Container, Typography, Box, TextField, InputAdornment, Chip, Avatar, Fab, Tooltip, Alert, CircularProgress, FormControl, InputLabel, Select, MenuItem, OutlinedInput, SelectChangeEvent, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Paper, } from '@mui/material'; import { Search as SearchIcon, Add as AddIcon, People as PeopleIcon, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { useAuth } from '../contexts/AuthContext'; import { membersService } from '../services/members'; import { MemberListItem, StatusEnum, DienstgradEnum, STATUS_VALUES, DIENSTGRAD_VALUES, STATUS_LABELS, STATUS_COLORS, getMemberDisplayName, formatPhone, } from '../types/member.types'; // ---------------------------------------------------------------- // Helper: determine whether the current user can write member data // ---------------------------------------------------------------- function useCanWrite(): boolean { const { user } = useAuth(); const groups: string[] = (user as any)?.groups ?? []; return groups.includes('feuerwehr-admin') || groups.includes('feuerwehr-kommandant'); } // ---------------------------------------------------------------- // Debounce hook // ---------------------------------------------------------------- function useDebounce(value: T, delay: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; } // ---------------------------------------------------------------- // Component // ---------------------------------------------------------------- function Mitglieder() { const navigate = useNavigate(); const canWrite = useCanWrite(); // --- data state --- const [members, setMembers] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // --- filter / pagination state --- const [searchInput, setSearchInput] = useState(''); const debouncedSearch = useDebounce(searchInput, 300); const [selectedStatus, setSelectedStatus] = useState([]); const [selectedDienstgrad, setSelectedDienstgrad] = useState([]); const [page, setPage] = useState(0); // MUI uses 0-based const pageSize = 25; // Track previous debounced search to reset page const prevSearch = useRef(debouncedSearch); // ---------------------------------------------------------------- // Data fetching // ---------------------------------------------------------------- const fetchMembers = useCallback(async () => { setLoading(true); setError(null); try { const { items, total: t } = await membersService.getMembers({ search: debouncedSearch || undefined, status: selectedStatus.length > 0 ? selectedStatus : undefined, dienstgrad: selectedDienstgrad.length > 0 ? selectedDienstgrad : undefined, page: page + 1, // convert to 1-based for API pageSize, }); setMembers(items); setTotal(t); } catch (err) { setError('Mitglieder konnten nicht geladen werden. Bitte versuchen Sie es erneut.'); } finally { setLoading(false); } }, [debouncedSearch, selectedStatus, selectedDienstgrad, page]); useEffect(() => { // Reset to page 0 when search changes if (debouncedSearch !== prevSearch.current) { prevSearch.current = debouncedSearch; setPage(0); return; } fetchMembers(); }, [fetchMembers, debouncedSearch]); // Also fetch when page/filters change useEffect(() => { fetchMembers(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, selectedStatus, selectedDienstgrad]); // ---------------------------------------------------------------- // Event handlers // ---------------------------------------------------------------- const handleStatusChange = (e: SelectChangeEvent) => { setSelectedStatus(e.target.value as StatusEnum[]); setPage(0); }; const handleDienstgradChange = (e: SelectChangeEvent) => { setSelectedDienstgrad(e.target.value as DienstgradEnum[]); setPage(0); }; const handleRowClick = (userId: string) => { navigate(`/mitglieder/${userId}`); }; const handleRemoveStatusChip = (status: StatusEnum) => { setSelectedStatus((prev) => prev.filter((s) => s !== status)); setPage(0); }; const handleRemoveDienstgradChip = (dg: DienstgradEnum) => { setSelectedDienstgrad((prev) => prev.filter((d) => d !== dg)); setPage(0); }; // ---------------------------------------------------------------- // Render // ---------------------------------------------------------------- const activeFilters = selectedStatus.length + selectedDienstgrad.length; return ( {/* Page heading */} Mitgliederverwaltung {loading ? '...' : `${total} Mitglieder`} {/* Toolbar: search + filters */} setSearchInput(e.target.value)} sx={{ flex: '1 1 280px', minWidth: 220 }} InputProps={{ startAdornment: ( ), }} /> {/* Status filter */} Status multiple value={selectedStatus} onChange={handleStatusChange} input={} renderValue={(selected) => selected.length === 0 ? 'Alle' : `${selected.length} gewählt` } > {STATUS_VALUES.map((s) => ( {STATUS_LABELS[s]} ))} {/* Dienstgrad filter */} Dienstgrad multiple value={selectedDienstgrad} onChange={handleDienstgradChange} input={} renderValue={(selected) => selected.length === 0 ? 'Alle' : `${selected.length} gewählt` } > {DIENSTGRAD_VALUES.map((dg) => ( {dg} ))} {/* Active filter chips */} {activeFilters > 0 && ( {selectedStatus.map((s) => ( handleRemoveStatusChip(s)} /> ))} {selectedDienstgrad.map((dg) => ( handleRemoveDienstgradChip(dg)} /> ))} )} {/* Error state */} {error && ( setError(null)}> {error} )} {/* Table */} Foto Name Mitgliedsnr. Dienstgrad Funktion Status Eintrittsdatum Telefon {loading ? ( ) : members.length === 0 ? ( Keine Mitglieder gefunden. ) : ( members.map((member) => { const displayName = getMemberDisplayName(member); const initials = [member.given_name?.[0], member.family_name?.[0]] .filter(Boolean) .join('') .toUpperCase() || member.email[0].toUpperCase(); return ( handleRowClick(member.id)} sx={{ cursor: 'pointer' }} aria-label={`Mitglied ${displayName} öffnen`} > {/* Avatar */} {initials} {/* Name + email */} {displayName} {member.email} {/* Mitgliedsnr */} {member.mitglieds_nr ?? '—'} {/* Dienstgrad */} {member.dienstgrad ? ( ) : ( )} {/* Funktion(en) */} {member.funktion.length > 0 ? member.funktion.map((f) => ( )) : } {/* Status */} {member.status ? ( ) : ( )} {/* Eintrittsdatum */} {member.eintrittsdatum ? new Date(member.eintrittsdatum).toLocaleDateString('de-AT') : '—'} {/* Telefon */} {formatPhone(member.telefon_mobil)} ); }) )}
setPage(newPage)} rowsPerPage={pageSize} rowsPerPageOptions={[pageSize]} labelDisplayedRows={({ from, to, count }) => `${from}–${to} von ${count !== -1 ? count : `mehr als ${to}`}` } />
{/* FAB — only visible to Kommandant/Admin */} {canWrite && ( navigate('/mitglieder/neu')} sx={{ position: 'fixed', bottom: 32, right: 32, zIndex: (theme) => theme.zIndex.speedDial, }} > )}
); } export default Mitglieder;