resolve issues with new features

This commit is contained in:
Matthias Hochmeister
2026-03-12 17:51:57 +01:00
parent 34ca007f9b
commit 67b7d5ccd2
10 changed files with 138 additions and 68 deletions

View File

@@ -37,29 +37,28 @@ const ChatPanelInner: React.FC = () => {
}); });
const nextcloudUrl = externalLinks?.nextcloud; const nextcloudUrl = externalLinks?.nextcloud;
// Keep a ref to rooms so the effect can access the latest list without // Dismiss internal notifications when panel opens
// re-running every time room data refreshes.
const roomsRef = React.useRef(rooms);
roomsRef.current = rooms;
React.useEffect(() => { React.useEffect(() => {
if (chatPanelOpen) { if (!chatPanelOpen) return;
// Dismiss our internal notification-centre entries
notificationsApi.dismissByType('nextcloud_talk').then(() => { notificationsApi.dismissByType('nextcloud_talk').then(() => {
queryClient.invalidateQueries({ queryKey: ['notifications'] }); queryClient.invalidateQueries({ queryKey: ['notifications'] });
queryClient.invalidateQueries({ queryKey: ['unreadNotificationCount'] }); queryClient.invalidateQueries({ queryKey: ['unreadNotificationCount'] });
}).catch(() => {}); }).catch(() => {});
}, [chatPanelOpen, queryClient]);
// Also mark all unread rooms as read directly in Nextcloud so that // Mark unread rooms as read in Nextcloud whenever panel is open and rooms update
// Nextcloud's own notification badges clear as well. React.useEffect(() => {
roomsRef.current if (!chatPanelOpen) return;
.filter((r) => r.unreadMessages > 0) rooms.filter((r) => r.unreadMessages > 0).forEach((r) => {
.forEach((r) => {
nextcloudApi.markAsRead(r.token).catch(() => {}); nextcloudApi.markAsRead(r.token).catch(() => {});
}); });
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] }); }, [chatPanelOpen, rooms]);
}
}, [chatPanelOpen, queryClient]); // Mark the selected room as read when a conversation is opened
React.useEffect(() => {
if (!selectedRoomToken) return;
nextcloudApi.markAsRead(selectedRoomToken).catch(() => {});
}, [selectedRoomToken]);
if (!chatPanelOpen) { if (!chatPanelOpen) {
return ( return (

View File

@@ -21,9 +21,10 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { notificationsApi } from '../../services/notifications'; import { notificationsApi } from '../../services/notifications';
import { useNotification } from '../../contexts/NotificationContext';
import type { Notification, NotificationSchwere } from '../../types/notification.types'; import type { Notification, NotificationSchwere } from '../../types/notification.types';
const POLL_INTERVAL_MS = 60_000; // 60 seconds const POLL_INTERVAL_MS = 15_000; // 15 seconds
/** /**
* Only allow window.open for URLs whose origin matches the current app origin. * Only allow window.open for URLs whose origin matches the current app origin.
@@ -58,29 +59,50 @@ function formatRelative(iso: string): string {
const NotificationBell: React.FC = () => { const NotificationBell: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { showNotificationToast } = useNotification();
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [loadingList, setLoadingList] = useState(false); const [loadingList, setLoadingList] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Track known notification IDs to detect new ones; null = not yet initialized
const knownIdsRef = useRef<Set<string> | null>(null);
const fetchUnreadCount = useCallback(async () => { const fetchAndToastNew = useCallback(async () => {
try { try {
const count = await notificationsApi.getUnreadCount(); const data = await notificationsApi.getNotifications();
const unread = data.filter((n) => !n.gelesen);
const count = unread.length;
setUnreadCount(count); setUnreadCount(count);
if (knownIdsRef.current === null) {
// First load — initialize without toasting
knownIdsRef.current = new Set(data.map((n) => n.id));
return;
}
// Find notifications we haven't seen before
const newOnes = unread.filter((n) => !knownIdsRef.current!.has(n.id));
newOnes.forEach((n) => {
knownIdsRef.current!.add(n.id);
const severity = n.schwere === 'fehler' ? 'error' : n.schwere === 'warnung' ? 'warning' : 'info';
showNotificationToast(n.titel, severity);
});
// Also add all known IDs to avoid re-toasting on re-fetch
data.forEach((n) => knownIdsRef.current!.add(n.id));
} catch { } catch {
// non-critical // non-critical
} }
}, []); }, [showNotificationToast]);
// Poll unread count every 60 seconds // Poll for new notifications every 15 seconds
useEffect(() => { useEffect(() => {
fetchUnreadCount(); fetchAndToastNew();
pollTimerRef.current = setInterval(fetchUnreadCount, POLL_INTERVAL_MS); pollTimerRef.current = setInterval(fetchAndToastNew, POLL_INTERVAL_MS);
return () => { return () => {
if (pollTimerRef.current) clearInterval(pollTimerRef.current); if (pollTimerRef.current) clearInterval(pollTimerRef.current);
}; };
}, [fetchUnreadCount]); }, [fetchAndToastNew]);
const handleOpen = async (event: React.MouseEvent<HTMLElement>) => { const handleOpen = async (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -88,6 +110,12 @@ const NotificationBell: React.FC = () => {
try { try {
const data = await notificationsApi.getNotifications(); const data = await notificationsApi.getNotifications();
setNotifications(data); setNotifications(data);
// Mark all as known so we don't toast them again
if (knownIdsRef.current === null) {
knownIdsRef.current = new Set(data.map((n) => n.id));
} else {
data.forEach((n) => knownIdsRef.current!.add(n.id));
}
// Refresh count after loading full list // Refresh count after loading full list
const count = await notificationsApi.getUnreadCount(); const count = await notificationsApi.getUnreadCount();
setUnreadCount(count); setUnreadCount(count);

View File

@@ -135,7 +135,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
const vehicleSubItems: SubItem[] = useMemo( const vehicleSubItems: SubItem[] = useMemo(
() => () =>
(vehicleList ?? []).map((v) => ({ (vehicleList ?? []).map((v) => ({
text: v.kurzname ?? v.bezeichnung, text: v.bezeichnung ?? v.kurzname,
path: `/fahrzeuge/${v.id}`, path: `/fahrzeuge/${v.id}`,
})), })),
[vehicleList], [vehicleList],
@@ -196,7 +196,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
> >
<ListItemButton <ListItemButton
selected={isActive} selected={isActive}
onClick={() => handleNavigation(hasSubItems ? item.subItems![0].path : item.path)} onClick={() => handleNavigation(item.path)}
aria-label={`Zu ${item.text} navigieren`} aria-label={`Zu ${item.text} navigieren`}
sx={{ sx={{
justifyContent: sidebarCollapsed ? 'center' : 'initial', justifyContent: sidebarCollapsed ? 'center' : 'initial',

View File

@@ -12,6 +12,7 @@ interface NotificationContextType {
showError: (message: string) => void; showError: (message: string) => void;
showWarning: (message: string) => void; showWarning: (message: string) => void;
showInfo: (message: string) => void; showInfo: (message: string) => void;
showNotificationToast: (message: string, severity?: AlertColor) => void;
} }
const NotificationContext = createContext<NotificationContextType | undefined>(undefined); const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
@@ -24,6 +25,9 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
const [_notifications, setNotifications] = useState<Notification[]>([]); const [_notifications, setNotifications] = useState<Notification[]>([]);
const [currentNotification, setCurrentNotification] = useState<Notification | null>(null); const [currentNotification, setCurrentNotification] = useState<Notification | null>(null);
// Left-side toast queue for new backend notifications
const [toastQueue, setToastQueue] = useState<Notification[]>([]);
const addNotification = useCallback((message: string, severity: AlertColor) => { const addNotification = useCallback((message: string, severity: AlertColor) => {
const id = Date.now(); const id = Date.now();
const notification: Notification = { id, message, severity }; const notification: Notification = { id, message, severity };
@@ -52,6 +56,11 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
addNotification(message, 'info'); addNotification(message, 'info');
}, [addNotification]); }, [addNotification]);
const showNotificationToast = useCallback((message: string, severity: AlertColor = 'info') => {
const id = Date.now() + Math.random();
setToastQueue((prev) => [...prev, { id, message, severity }]);
}, []);
const handleClose = (_event?: React.SyntheticEvent | Event, reason?: string) => { const handleClose = (_event?: React.SyntheticEvent | Event, reason?: string) => {
if (reason === 'clickaway') { if (reason === 'clickaway') {
return; return;
@@ -71,16 +80,23 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
}, 200); }, 200);
}; };
const handleToastClose = (_event?: React.SyntheticEvent | Event, reason?: string) => {
if (reason === 'clickaway') return;
setToastQueue((prev) => prev.slice(1));
};
const value: NotificationContextType = { const value: NotificationContextType = {
showSuccess, showSuccess,
showError, showError,
showWarning, showWarning,
showInfo, showInfo,
showNotificationToast,
}; };
return ( return (
<NotificationContext.Provider value={value}> <NotificationContext.Provider value={value}>
{children} {children}
{/* Right-side: action feedback */}
<Snackbar <Snackbar
open={currentNotification !== null} open={currentNotification !== null}
autoHideDuration={6000} autoHideDuration={6000}
@@ -96,6 +112,22 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
{currentNotification?.message} {currentNotification?.message}
</Alert> </Alert>
</Snackbar> </Snackbar>
{/* Left-side: new backend notification toasts */}
<Snackbar
open={toastQueue.length > 0}
autoHideDuration={5000}
onClose={handleToastClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
>
<Alert
onClose={handleToastClose}
severity={toastQueue[0]?.severity || 'info'}
variant="filled"
sx={{ width: '100%' }}
>
{toastQueue[0]?.message}
</Alert>
</Snackbar>
</NotificationContext.Provider> </NotificationContext.Provider>
); );
}; };

View File

@@ -44,6 +44,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom'; import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { equipmentApi } from '../services/equipment'; import { equipmentApi } from '../services/equipment';
import { fromGermanDate } from '../utils/dateInput'; import { fromGermanDate } from '../utils/dateInput';
import { import {
@@ -351,6 +352,7 @@ interface WartungTabProps {
} }
const WartungTab: React.FC<WartungTabProps> = ({ equipmentId, wartungslog, onAdded, canWrite }) => { const WartungTab: React.FC<WartungTabProps> = ({ equipmentId, wartungslog, onAdded, canWrite }) => {
const { chatPanelOpen } = useLayout();
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null); const [saveError, setSaveError] = useState<string | null>(null);
@@ -446,7 +448,7 @@ const WartungTab: React.FC<WartungTabProps> = ({ equipmentId, wartungslog, onAdd
color="primary" color="primary"
size="small" size="small"
aria-label="Wartung eintragen" aria-label="Wartung eintragen"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={() => { setForm(emptyForm); setSaveError(null); setDialogOpen(true); }} onClick={() => { setForm(emptyForm); setSaveError(null); setDialogOpen(true); }}
> >
<Add /> <Add />

View File

@@ -55,6 +55,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { vehiclesApi } from '../services/vehicles'; import { vehiclesApi } from '../services/vehicles';
import { fromGermanDate } from '../utils/dateInput'; import { fromGermanDate } from '../utils/dateInput';
import { equipmentApi } from '../services/equipment'; import { equipmentApi } from '../services/equipment';
@@ -322,6 +323,7 @@ const WARTUNG_ART_ICONS: Record<string, React.ReactElement> = {
}; };
const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdded, canWrite }) => { const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdded, canWrite }) => {
const { chatPanelOpen } = useLayout();
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null); const [saveError, setSaveError] = useState<string | null>(null);
@@ -398,7 +400,7 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
color="primary" color="primary"
size="small" size="small"
aria-label="Wartung eintragen" aria-label="Wartung eintragen"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={() => { setForm(emptyForm); setDialogOpen(true); }} onClick={() => { setForm(emptyForm); setDialogOpen(true); }}
> >
<Add /> <Add />

View File

@@ -29,6 +29,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { vehiclesApi } from '../services/vehicles'; import { vehiclesApi } from '../services/vehicles';
import { equipmentApi } from '../services/equipment'; import { equipmentApi } from '../services/equipment';
import type { VehicleEquipmentWarning } from '../types/equipment.types'; import type { VehicleEquipmentWarning } from '../types/equipment.types';
@@ -271,6 +272,7 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick, warnings =
function Fahrzeuge() { function Fahrzeuge() {
const navigate = useNavigate(); const navigate = useNavigate();
const { isAdmin } = usePermissions(); const { isAdmin } = usePermissions();
const { chatPanelOpen } = useLayout();
const [vehicles, setVehicles] = useState<FahrzeugListItem[]>([]); const [vehicles, setVehicles] = useState<FahrzeugListItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -415,7 +417,7 @@ function Fahrzeuge() {
<Fab <Fab
color="primary" color="primary"
aria-label="Fahrzeug hinzufügen" aria-label="Fahrzeug hinzufügen"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={() => navigate('/fahrzeuge/neu')} onClick={() => navigate('/fahrzeuge/neu')}
> >
<Add /> <Add />

View File

@@ -70,6 +70,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDate, isValidGermanDateTime } from '../utils/dateInput'; import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDate, isValidGermanDateTime } from '../utils/dateInput';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
@@ -1681,6 +1682,7 @@ export default function Kalender() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { user } = useAuth(); const { user } = useAuth();
const notification = useNotification(); const notification = useNotification();
const { chatPanelOpen } = useLayout();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@@ -2218,34 +2220,6 @@ export default function Kalender() {
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
{/* Category filter */}
{kategorien.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
<Chip
label="Alle"
onClick={() => setSelectedKategorie('all')}
color={selectedKategorie === 'all' ? 'primary' : 'default'}
variant={selectedKategorie === 'all' ? 'filled' : 'outlined'}
size="small"
/>
{kategorien.map((k) => (
<Chip
key={k.id}
label={k.name}
onClick={() =>
setSelectedKategorie(selectedKategorie === k.id ? 'all' : k.id)
}
size="small"
sx={{
bgcolor: selectedKategorie === k.id ? k.farbe : undefined,
color: selectedKategorie === k.id ? 'white' : undefined,
}}
variant={selectedKategorie === k.id ? 'filled' : 'outlined'}
/>
))}
</Box>
)}
{/* Kategorien verwalten */} {/* Kategorien verwalten */}
{canWriteEvents && ( {canWriteEvents && (
<Tooltip title="Kategorien verwalten"> <Tooltip title="Kategorien verwalten">
@@ -2296,6 +2270,34 @@ export default function Kalender() {
</Button> </Button>
</Box> </Box>
{/* Category filter — between controls and navigation */}
{kategorien.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
<Chip
label="Alle"
onClick={() => setSelectedKategorie('all')}
color={selectedKategorie === 'all' ? 'primary' : 'default'}
variant={selectedKategorie === 'all' ? 'filled' : 'outlined'}
size="small"
/>
{kategorien.map((k) => (
<Chip
key={k.id}
label={k.name}
onClick={() =>
setSelectedKategorie(selectedKategorie === k.id ? 'all' : k.id)
}
size="small"
sx={{
bgcolor: selectedKategorie === k.id ? k.farbe : undefined,
color: selectedKategorie === k.id ? 'white' : undefined,
}}
variant={selectedKategorie === k.id ? 'filled' : 'outlined'}
/>
))}
</Box>
)}
{/* Navigation */} {/* Navigation */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2, gap: 1 }}>
<IconButton onClick={handlePrev} size="small"> <IconButton onClick={handlePrev} size="small">
@@ -2554,7 +2556,7 @@ export default function Kalender() {
{canWriteEvents && ( {canWriteEvents && (
<Fab <Fab
color="primary" color="primary"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={() => { onClick={() => {
setVeranstEditing(null); setVeranstEditing(null);
setVeranstFormOpen(true); setVeranstFormOpen(true);
@@ -2562,7 +2564,6 @@ export default function Kalender() {
> >
<Add /> <Add />
</Fab> </Fab>
)}
{/* Day Popover */} {/* Day Popover */}
<DayPopover <DayPopover
@@ -2927,7 +2928,7 @@ export default function Kalender() {
{canCreateBookings && ( {canCreateBookings && (
<Fab <Fab
color="primary" color="primary"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={openBookingCreate} onClick={openBookingCreate}
> >
<Add /> <Add />

View File

@@ -33,6 +33,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { membersService } from '../services/members'; import { membersService } from '../services/members';
import { import {
@@ -74,7 +75,7 @@ function useDebounce<T>(value: T, delay: number): T {
function Mitglieder() { function Mitglieder() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const canWrite = useCanWrite(); const { chatPanelOpen } = useLayout(); const canWrite = useCanWrite();
// --- redirect non-admin/non-kommando users to their own profile --- // --- redirect non-admin/non-kommando users to their own profile ---
useEffect(() => { useEffect(() => {
@@ -434,7 +435,8 @@ function Mitglieder() {
sx={{ sx={{
position: 'fixed', position: 'fixed',
bottom: 32, bottom: 32,
right: 32, right: chatPanelOpen ? 376 : 80,
transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)',
zIndex: (theme) => theme.zIndex.speedDial, zIndex: (theme) => theme.zIndex.speedDial,
}} }}
> >

View File

@@ -51,6 +51,7 @@ import {
Delete as DeleteIcon, Delete as DeleteIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useLayout } from '../contexts/LayoutContext';
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput'; import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
@@ -1004,6 +1005,7 @@ function EventListView({ events, canWrite, onEdit, onCancel, onDelete }: ListVie
export default function Veranstaltungen() { export default function Veranstaltungen() {
const { user } = useAuth(); const { user } = useAuth();
const { chatPanelOpen } = useLayout();
const notification = useNotification(); const notification = useNotification();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@@ -1317,7 +1319,7 @@ export default function Veranstaltungen() {
<Fab <Fab
color="primary" color="primary"
aria-label="Veranstaltung erstellen" aria-label="Veranstaltung erstellen"
sx={{ position: 'fixed', bottom: 32, right: 32 }} sx={{ position: 'fixed', bottom: 32, right: chatPanelOpen ? 376 : 80, transition: 'right 225ms cubic-bezier(0.4, 0, 0.6, 1)' }}
onClick={() => { setEditingEvent(null); setFormOpen(true); }} onClick={() => { setEditingEvent(null); setFormOpen(true); }}
> >
<Add /> <Add />