resolve issues with new features

This commit is contained in:
Matthias Hochmeister
2026-03-12 11:37:25 +01:00
parent d5be68ca63
commit 71a04aee89
38 changed files with 699 additions and 108 deletions

View File

@@ -26,6 +26,7 @@ import Veranstaltungen from './pages/Veranstaltungen';
import VeranstaltungKategorien from './pages/VeranstaltungKategorien';
import Wissen from './pages/Wissen';
import AdminDashboard from './pages/AdminDashboard';
import AdminSettings from './pages/AdminSettings';
import NotFound from './pages/NotFound';
function App() {
@@ -221,6 +222,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/admin/settings"
element={
<ProtectedRoute>
<AdminSettings />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</AuthProvider>

View File

@@ -6,6 +6,7 @@ import ChatIcon from '@mui/icons-material/Chat';
import Typography from '@mui/material/Typography';
import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
@@ -39,6 +40,7 @@ const ChatPanelInner: React.FC = () => {
overflowY: 'auto',
}}
>
<Toolbar />
<IconButton onClick={() => setChatPanelOpen(true)} aria-label="Chat öffnen">
<ChatIcon />
</IconButton>
@@ -96,6 +98,7 @@ const ChatPanelInner: React.FC = () => {
overflow: 'hidden',
}}
>
<Toolbar />
<Box
sx={{
px: 1.5,

View File

@@ -77,7 +77,7 @@ const BookStackRecentWidget: React.FC = () => {
});
const configured = data?.configured ?? true;
const pages = data?.data ?? [];
const pages = (data?.data ?? []).slice(0, 5);
if (!configured) {
return (

View File

@@ -19,6 +19,7 @@ import { de } from 'date-fns/locale';
import { nextcloudApi } from '../../services/nextcloud';
import type { NextcloudConversation } from '../../types/nextcloud.types';
import { safeOpenUrl } from '../../utils/safeOpenUrl';
import { useCountUp } from '../../hooks/useCountUp';
const POLL_INTERVAL = 2000;
const POLL_TIMEOUT = 5 * 60 * 1000;
@@ -112,6 +113,7 @@ const NextcloudTalkWidget: React.FC = () => {
const connected = data?.connected ?? false;
const conversations = data?.conversations?.slice(0, 5) ?? [];
const totalUnread = data?.totalUnread ?? 0;
const animatedUnread = useCountUp(totalUnread);
const stopPolling = useCallback(() => {
if (pollIntervalRef.current) {
@@ -199,7 +201,7 @@ const NextcloudTalkWidget: React.FC = () => {
</Typography>
{connected && totalUnread > 0 && (
<Chip
label={`${totalUnread} ungelesen`}
label={`${animatedUnread} ungelesen`}
size="small"
color="primary"
variant="outlined"

View File

@@ -15,6 +15,7 @@ import { de } from 'date-fns/locale';
import { vikunjaApi } from '../../services/vikunja';
import type { VikunjaTask } from '../../types/vikunja.types';
import { safeOpenUrl } from '../../utils/safeOpenUrl';
import { useCountUp } from '../../hooks/useCountUp';
const PRIORITY_LABELS: Record<number, { label: string; color: 'default' | 'warning' | 'error' }> = {
0: { label: 'Keine', color: 'default' },
@@ -95,6 +96,7 @@ const VikunjaMyTasksWidget: React.FC = () => {
const configured = data?.configured ?? true;
const tasks = data?.data ?? [];
const animatedTaskCount = useCountUp(tasks.length);
if (!configured) {
return (
@@ -129,7 +131,7 @@ const VikunjaMyTasksWidget: React.FC = () => {
Meine Aufgaben
</Typography>
{!isLoading && !isError && tasks.length > 0 && (
<Chip label={tasks.length} size="small" color="primary" />
<Chip label={animatedTaskCount} size="small" color="primary" />
)}
</Box>

View File

@@ -215,7 +215,7 @@ const IncidentStatsChart: React.FC<IncidentStatsChartProps> = ({ stats, loading
outerRadius={100}
paddingAngle={2}
dataKey="value"
label={({ name, percent }) =>
label={({ percent }: { percent: number }) =>
percent > 0.05 ? `${(percent * 100).toFixed(0)}%` : ''
}
labelLine={false}

View File

@@ -1,4 +1,4 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Component, ErrorInfo, ReactNode } from 'react';
import { Box, Card, CardContent, Typography, Button } from '@mui/material';
import { ErrorOutline, Refresh } from '@mui/icons-material';

View File

@@ -91,9 +91,12 @@ function Header({ onMenuClick }: HeaderProps) {
};
const linkEntries = externalLinks
? Object.entries(externalLinks).filter(([, url]) => !!url)
? Object.entries(externalLinks).filter(([key, url]) => key !== 'customLinks' && !!url)
: [];
const customLinks: Array<{ name: string; url: string }> =
externalLinks?.customLinks ?? [];
const linkLabels: Record<string, string> = {
nextcloud: 'Nextcloud Dateien',
bookstack: 'Wissensdatenbank',
@@ -125,7 +128,7 @@ function Header({ onMenuClick }: HeaderProps) {
{user && (
<>
{linkEntries.length > 0 && (
{(linkEntries.length > 0 || customLinks.length > 0) && (
<>
<Button
variant="text"
@@ -133,11 +136,11 @@ function Header({ onMenuClick }: HeaderProps) {
onClick={handleToolsOpen}
size="small"
startIcon={<Launch />}
aria-label="Externe Links"
aria-label="FF Rems Tools"
aria-controls="tools-menu"
aria-haspopup="true"
>
Externe Links
FF Rems Tools
</Button>
<Menu
@@ -166,6 +169,15 @@ function Header({ onMenuClick }: HeaderProps) {
{linkLabels[key] || key}
</MenuItem>
))}
{customLinks.length > 0 && linkEntries.length > 0 && <Divider />}
{customLinks.map((link, index) => (
<MenuItem key={`custom-${index}`} onClick={() => handleOpenExternal(link.url)}>
<ListItemIcon>
<Launch fontSize="small" />
</ListItemIcon>
{link.name}
</MenuItem>
))}
</Menu>
</>
)}

View File

@@ -20,6 +20,7 @@ import {
CalendarMonth,
MenuBook,
AdminPanelSettings,
Settings,
Menu as MenuIcon,
} from '@mui/icons-material';
import { useNavigate, useLocation } from 'react-router-dom';
@@ -78,6 +79,12 @@ const adminItem: NavigationItem = {
path: '/admin',
};
const adminSettingsItem: NavigationItem = {
text: 'Einstellungen',
icon: <Settings />,
path: '/admin/settings',
};
interface SidebarProps {
mobileOpen: boolean;
onMobileClose: () => void;
@@ -92,7 +99,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
const navigationItems = useMemo(() => {
return isAdmin ? [...baseNavigationItems, adminItem] : baseNavigationItems;
return isAdmin ? [...baseNavigationItems, adminItem, adminSettingsItem] : baseNavigationItems;
}, [isAdmin]);
const handleNavigation = (path: string) => {

View File

@@ -25,12 +25,6 @@ function alertTypeLabel(type: InspectionAlertType): string {
type Urgency = 'overdue' | 'urgent' | 'warning';
function getUrgency(tage: number): Urgency {
if (tage < 0) return 'overdue';
if (tage <= 14) return 'urgent';
return 'warning';
}
const URGENCY_CONFIG: Record<Urgency, { severity: 'error' | 'warning'; label: string }> = {
overdue: { severity: 'error', label: 'Überfällig' },
urgent: { severity: 'error', label: 'Dringend (≤ 14 Tage)' },
@@ -96,11 +90,11 @@ const InspectionAlerts: React.FC<InspectionAlertsProps> = ({
const urgent = alerts.filter((a) => a.tage >= 0 && a.tage <= 14);
const warning = alerts.filter((a) => a.tage > 14);
const groups: Array<{ urgency: Urgency; items: InspectionAlert[] }> = [
{ urgency: 'overdue', items: overdue },
{ urgency: 'urgent', items: urgent },
{ urgency: 'warning', items: warning },
].filter((g) => g.items.length > 0);
const groups: Array<{ urgency: Urgency; items: InspectionAlert[] }> = ([
{ urgency: 'overdue' as Urgency, items: overdue },
{ urgency: 'urgent' as Urgency, items: urgent },
{ urgency: 'warning' as Urgency, items: warning },
] as Array<{ urgency: Urgency; items: InspectionAlert[] }>).filter((g) => g.items.length > 0);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>

View File

@@ -1,5 +1,5 @@
import React, { createContext, useCallback, useContext, useState, useEffect, ReactNode } from 'react';
import { AuthContextType, AuthState, User } from '../types/auth.types';
import { AuthContextType, AuthState } from '../types/auth.types';
import { authService } from '../services/auth';
import { getToken, setToken, removeToken, getUser, setUser, removeUser, setRefreshToken, removeRefreshToken } from '../utils/storage';
import { useNotification } from './NotificationContext';

View File

@@ -22,11 +22,20 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
const [selectedRoomToken, setSelectedRoomToken] = useState<string | null>(null);
const { chatPanelOpen } = useLayout();
const { data: connData } = useQuery({
queryKey: ['nextcloud', 'connection'],
queryFn: () => nextcloudApi.getConversations(),
refetchInterval: chatPanelOpen ? 30000 : 120000,
retry: false,
});
const isConnected = connData?.connected ?? false;
const { data } = useQuery({
queryKey: ['nextcloud', 'rooms'],
queryFn: () => nextcloudApi.getRooms(),
refetchInterval: chatPanelOpen ? 30000 : 120000,
enabled: true,
enabled: isConnected,
});
const rooms = data?.rooms ?? [];

View File

@@ -21,7 +21,7 @@ interface NotificationProviderProps {
}
export const NotificationProvider: React.FC<NotificationProviderProps> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [_notifications, setNotifications] = useState<Notification[]>([]);
const [currentNotification, setCurrentNotification] = useState<Notification | null>(null);
const addNotification = useCallback((message: string, severity: AlertColor) => {

View File

@@ -0,0 +1,318 @@
import { useState, useEffect } from 'react';
import {
Container,
Typography,
Card,
CardContent,
Box,
Divider,
TextField,
Button,
IconButton,
MenuItem,
Select,
FormControl,
InputLabel,
Stack,
CircularProgress,
} from '@mui/material';
import {
Delete,
Add,
Link as LinkIcon,
Timer,
Info,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Navigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useAuth } from '../contexts/AuthContext';
import { useNotification } from '../contexts/NotificationContext';
import { settingsApi } from '../services/settings';
interface ExternalLink {
name: string;
url: string;
}
interface RefreshIntervals {
dashboardWidgets: number;
adminServices: number;
}
const DASHBOARD_INTERVAL_OPTIONS = [
{ value: 30, label: '30 Sekunden' },
{ value: 60, label: '1 Minute' },
{ value: 300, label: '5 Minuten' },
{ value: 600, label: '10 Minuten' },
];
const ADMIN_INTERVAL_OPTIONS = [
{ value: 5, label: '5 Sekunden' },
{ value: 15, label: '15 Sekunden' },
{ value: 30, label: '30 Sekunden' },
{ value: 60, label: '60 Sekunden' },
];
function AdminSettings() {
const { user } = useAuth();
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
// State for external links
const [externalLinks, setExternalLinks] = useState<ExternalLink[]>([]);
// State for refresh intervals
const [refreshIntervals, setRefreshIntervals] = useState<RefreshIntervals>({
dashboardWidgets: 60,
adminServices: 15,
});
// Fetch all settings
const { data: settings, isLoading } = useQuery({
queryKey: ['admin-settings'],
queryFn: () => settingsApi.getAll(),
enabled: isAdmin,
});
// Initialize state from fetched settings
useEffect(() => {
if (settings) {
const linksSetting = settings.find((s) => s.key === 'external_links');
if (linksSetting?.value) {
setExternalLinks(linksSetting.value);
}
const intervalsSetting = settings.find((s) => s.key === 'refresh_intervals');
if (intervalsSetting?.value) {
setRefreshIntervals({
dashboardWidgets: intervalsSetting.value.dashboardWidgets ?? 60,
adminServices: intervalsSetting.value.adminServices ?? 15,
});
}
}
}, [settings]);
// Mutation for saving external links
const linksMutation = useMutation({
mutationFn: (links: ExternalLink[]) => settingsApi.update('external_links', links),
onSuccess: () => {
showSuccess('Externe Links gespeichert');
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
queryClient.invalidateQueries({ queryKey: ['external-links'] });
},
onError: () => {
showError('Fehler beim Speichern der externen Links');
},
});
// Mutation for saving refresh intervals
const intervalsMutation = useMutation({
mutationFn: (intervals: RefreshIntervals) => settingsApi.update('refresh_intervals', intervals),
onSuccess: () => {
showSuccess('Aktualisierungsintervalle gespeichert');
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
},
onError: () => {
showError('Fehler beim Speichern der Aktualisierungsintervalle');
},
});
if (!isAdmin) {
return <Navigate to="/dashboard" replace />;
}
const handleAddLink = () => {
setExternalLinks([...externalLinks, { name: '', url: '' }]);
};
const handleRemoveLink = (index: number) => {
setExternalLinks(externalLinks.filter((_, i) => i !== index));
};
const handleLinkChange = (index: number, field: keyof ExternalLink, value: string) => {
const updated = [...externalLinks];
updated[index] = { ...updated[index], [field]: value };
setExternalLinks(updated);
};
const handleSaveLinks = () => {
linksMutation.mutate(externalLinks);
};
const handleSaveIntervals = () => {
intervalsMutation.mutate(refreshIntervals);
};
// Find the most recent updated_at
const lastUpdated = settings?.reduce((latest, s) => {
if (!latest) return s.updated_at;
return new Date(s.updated_at) > new Date(latest) ? s.updated_at : latest;
}, '' as string);
if (isLoading) {
return (
<DashboardLayout>
<Container maxWidth="lg">
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
</Container>
</DashboardLayout>
);
}
return (
<DashboardLayout>
<Container maxWidth="lg">
<Typography variant="h4" gutterBottom sx={{ mb: 4 }}>
Admin-Einstellungen
</Typography>
<Stack spacing={3}>
{/* Section 1: External Links */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<LinkIcon color="primary" sx={{ mr: 2 }} />
<Typography variant="h6">FF Rems Tools Externe Links</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
<Stack spacing={2}>
{externalLinks.map((link, index) => (
<Box key={index} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<TextField
label="Name"
value={link.name}
onChange={(e) => handleLinkChange(index, 'name', e.target.value)}
size="small"
sx={{ flex: 1 }}
/>
<TextField
label="URL"
value={link.url}
onChange={(e) => handleLinkChange(index, 'url', e.target.value)}
size="small"
sx={{ flex: 2 }}
/>
<IconButton
color="error"
onClick={() => handleRemoveLink(index)}
aria-label="Link entfernen"
>
<Delete />
</IconButton>
</Box>
))}
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
startIcon={<Add />}
onClick={handleAddLink}
variant="outlined"
size="small"
>
Link hinzufügen
</Button>
<Button
onClick={handleSaveLinks}
variant="contained"
size="small"
disabled={linksMutation.isPending}
>
Speichern
</Button>
</Box>
</Stack>
</CardContent>
</Card>
{/* Section 2: Refresh Intervals */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Timer color="primary" sx={{ mr: 2 }} />
<Typography variant="h6">Aktualisierungsintervalle</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
<Stack spacing={3}>
<FormControl size="small" sx={{ maxWidth: 300 }}>
<InputLabel>Dashboard Widgets</InputLabel>
<Select
value={refreshIntervals.dashboardWidgets}
label="Dashboard Widgets"
onChange={(e) =>
setRefreshIntervals((prev) => ({
...prev,
dashboardWidgets: Number(e.target.value),
}))
}
>
{DASHBOARD_INTERVAL_OPTIONS.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ maxWidth: 300 }}>
<InputLabel>Admin Services</InputLabel>
<Select
value={refreshIntervals.adminServices}
label="Admin Services"
onChange={(e) =>
setRefreshIntervals((prev) => ({
...prev,
adminServices: Number(e.target.value),
}))
}
>
{ADMIN_INTERVAL_OPTIONS.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
</FormControl>
<Box>
<Button
onClick={handleSaveIntervals}
variant="contained"
size="small"
disabled={intervalsMutation.isPending}
>
Speichern
</Button>
</Box>
</Stack>
</CardContent>
</Card>
{/* Section 3: Info */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Info color="primary" sx={{ mr: 2 }} />
<Typography variant="h6">Info</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
<Typography variant="body2" color="text.secondary">
{lastUpdated
? `Letzte Aktualisierung: ${new Date(lastUpdated).toLocaleString('de-DE')}`
: 'Noch keine Einstellungen gespeichert.'}
</Typography>
</CardContent>
</Card>
</Stack>
</Container>
</DashboardLayout>
);
}
export default AdminSettings;

View File

@@ -17,7 +17,6 @@ import {
Tooltip,
Paper,
TextField,
IconButton,
} from '@mui/material';
import {
ArrowBack,
@@ -37,7 +36,7 @@ import { de } from 'date-fns/locale';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import {
incidentsApi,
EinsatzDetail,
EinsatzDetail as EinsatzDetailType,
EinsatzStatus,
EINSATZ_ART_LABELS,
EINSATZ_STATUS_LABELS,
@@ -99,7 +98,7 @@ function initials(givenName: string | null, familyName: string | null, name: str
return '??';
}
function displayName(p: EinsatzDetail['personal'][0]): string {
function displayName(p: EinsatzDetailType['personal'][0]): string {
if (p.given_name && p.family_name) return `${p.given_name} ${p.family_name}`;
if (p.name) return p.name;
return p.email;
@@ -166,7 +165,7 @@ function EinsatzDetail() {
const navigate = useNavigate();
const notification = useNotification();
const [einsatz, setEinsatz] = useState<EinsatzDetail | null>(null);
const [einsatz, setEinsatz] = useState<EinsatzDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -398,7 +398,7 @@ function FahrzeugBuchungen() {
<TableContainer component={Paper} elevation={1}>
<Table size="small" sx={{ tableLayout: 'fixed' }}>
<TableHead>
<TableRow sx={{ bgcolor: 'grey.100' }}>
<TableRow sx={{ bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100' }}>
<TableCell sx={{ width: 160, fontWeight: 700 }}>
Fahrzeug
</TableCell>
@@ -409,7 +409,7 @@ function FahrzeugBuchungen() {
sx={{
fontWeight: isToday(day) ? 700 : 400,
color: isToday(day) ? 'primary.main' : 'text.primary',
bgcolor: isToday(day) ? 'primary.50' : undefined,
bgcolor: isToday(day) ? (theme) => theme.palette.mode === 'dark' ? 'primary.900' : 'primary.50' : undefined,
}}
>
<Typography variant="caption" display="block">
@@ -445,10 +445,10 @@ function FahrzeugBuchungen() {
isFree ? handleCellClick(vehicle.id, day) : undefined
}
sx={{
bgcolor: isFree ? 'success.50' : undefined,
bgcolor: isFree ? (theme) => theme.palette.mode === 'dark' ? 'success.900' : 'success.50' : undefined,
cursor: isFree && canCreate ? 'pointer' : 'default',
'&:hover': isFree && canCreate
? { bgcolor: 'success.100' }
? { bgcolor: (theme) => theme.palette.mode === 'dark' ? 'success.800' : 'success.100' }
: {},
p: 0.5,
verticalAlign: 'top',
@@ -511,9 +511,9 @@ function FahrzeugBuchungen() {
sx={{
width: 16,
height: 16,
bgcolor: 'success.50',
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'success.900' : 'success.50',
border: '1px solid',
borderColor: 'success.300',
borderColor: (theme) => theme.palette.mode === 'dark' ? 'success.700' : 'success.300',
borderRadius: 0.5,
}}
/>

View File

@@ -59,7 +59,7 @@ import { vehiclesApi } from '../services/vehicles';
import { fromGermanDate } from '../utils/dateInput';
import { equipmentApi } from '../services/equipment';
import {
FahrzeugDetail,
FahrzeugDetail as FahrzeugDetailType,
FahrzeugWartungslog,
FahrzeugStatus,
FahrzeugStatusLabel,
@@ -121,7 +121,7 @@ function inspectionBadgeColor(tage: number | null): 'success' | 'warning' | 'err
// ── Übersicht Tab ─────────────────────────────────────────────────────────────
interface UebersichtTabProps {
vehicle: FahrzeugDetail;
vehicle: FahrzeugDetailType;
onStatusUpdated: () => void;
canChangeStatus: boolean;
}
@@ -523,7 +523,7 @@ interface AusruestungTabProps {
vehicleId: string;
}
const AusruestungTab: React.FC<AusruestungTabProps> = ({ equipment, vehicleId }) => {
const AusruestungTab: React.FC<AusruestungTabProps> = ({ equipment, vehicleId: _vehicleId }) => {
const navigate = useNavigate();
const hasProblems = equipment.some(
@@ -642,7 +642,7 @@ function FahrzeugDetail() {
const { isAdmin, canChangeStatus } = usePermissions();
const notification = useNotification();
const [vehicle, setVehicle] = useState<FahrzeugDetail | null>(null);
const [vehicle, setVehicle] = useState<FahrzeugDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(0);

View File

@@ -11,7 +11,6 @@ import {
Container,
Fab,
Grid,
IconButton,
InputAdornment,
TextField,
Tooltip,

View File

@@ -13,9 +13,6 @@ import {
Tab,
Grid,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
CircularProgress,
Alert,
@@ -44,11 +41,9 @@ import {
MemberWithProfile,
StatusEnum,
DienstgradEnum,
FunktionEnum,
TshirtGroesseEnum,
DIENSTGRAD_VALUES,
STATUS_VALUES,
FUNKTION_VALUES,
TSHIRT_GROESSE_VALUES,
STATUS_LABELS,
STATUS_COLORS,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import {
Container,
Typography,

View File

@@ -1,6 +1,5 @@
import {
Container,
Paper,
Box,
Typography,
Avatar,

View File

@@ -5,7 +5,6 @@ import {
Typography,
Chip,
Button,
Divider,
Accordion,
AccordionSummary,
AccordionDetails,
@@ -301,7 +300,7 @@ export default function UebungDetail() {
const rsvpMutation = useMutation({
mutationFn: (status: 'zugesagt' | 'abgesagt') =>
trainingApi.updateRsvp(id!, status),
onSuccess: (_data, status) => {
onSuccess: (_data, _status) => {
queryClient.invalidateQueries({ queryKey: ['training', 'event', id] });
queryClient.invalidateQueries({ queryKey: ['training', 'upcoming'] });
setRsvpLoading(null);
@@ -342,7 +341,7 @@ export default function UebungDetail() {
}
const isPast = new Date(event.datum_von) < new Date();
const isAlreadyRsvp = event.eigener_status === 'zugesagt' || event.eigener_status === 'abgesagt';
// const isAlreadyRsvp = event.eigener_status === 'zugesagt' || event.eigener_status === 'abgesagt';
return (
<DashboardLayout>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback } from 'react';
import {
Box,
Container,

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
Box,
Container,
Typography,
Button,
IconButton,

View File

@@ -504,13 +504,13 @@ const AuditLog: React.FC = () => {
const handleApplyFilters = () => {
setApplied(filters);
setPaginationModel((prev) => ({ ...prev, page: 0 }));
setPaginationModel((prev: any) => ({ ...prev, page: 0 }));
};
const handleResetFilters = () => {
setFilters(DEFAULT_FILTERS);
setApplied(DEFAULT_FILTERS);
setPaginationModel((prev) => ({ ...prev, page: 0 }));
setPaginationModel((prev: any) => ({ ...prev, page: 0 }));
};
// -------------------------------------------------------------------------
@@ -723,7 +723,7 @@ const AuditLog: React.FC = () => {
noRowsLabel: 'Keine Eintraege gefunden',
MuiTablePagination: {
labelRowsPerPage: 'Eintraege pro Seite:',
labelDisplayedRows: ({ from, to, count }) =>
labelDisplayedRows: ({ from, to, count }: { from: any; to: any; count: any }) =>
`${from}${to} von ${count !== -1 ? count : `mehr als ${to}`}`,
},
}}

View File

@@ -0,0 +1,18 @@
import { api } from './api';
interface AppSetting {
key: string;
value: any;
updated_at: string;
}
interface ApiResponse<T> {
success: boolean;
data: T;
}
export const settingsApi = {
getAll: () => api.get<ApiResponse<AppSetting[]>>('/api/admin/settings').then(r => r.data.data),
get: (key: string) => api.get<ApiResponse<AppSetting>>(`/api/admin/settings/${key}`).then(r => r.data.data),
update: (key: string, value: any) => api.put<ApiResponse<AppSetting>>(`/api/admin/settings/${key}`, { value }).then(r => r.data.data),
};

View File

@@ -2,4 +2,5 @@ export interface ExternalLinks {
nextcloud?: string;
bookstack?: string;
vikunja?: string;
customLinks?: Array<{ name: string; url: string }>;
}