resolve issues with new features
This commit is contained in:
@@ -37,30 +37,29 @@ 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(() => {});
|
|
||||||
|
|
||||||
// Also mark all unread rooms as read directly in Nextcloud so that
|
|
||||||
// Nextcloud's own notification badges clear as well.
|
|
||||||
roomsRef.current
|
|
||||||
.filter((r) => r.unreadMessages > 0)
|
|
||||||
.forEach((r) => {
|
|
||||||
nextcloudApi.markAsRead(r.token).catch(() => {});
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
|
||||||
}
|
|
||||||
}, [chatPanelOpen, queryClient]);
|
}, [chatPanelOpen, queryClient]);
|
||||||
|
|
||||||
|
// Mark unread rooms as read in Nextcloud whenever panel is open and rooms update
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!chatPanelOpen) return;
|
||||||
|
rooms.filter((r) => r.unreadMessages > 0).forEach((r) => {
|
||||||
|
nextcloudApi.markAsRead(r.token).catch(() => {});
|
||||||
|
});
|
||||||
|
}, [chatPanelOpen, rooms]);
|
||||||
|
|
||||||
|
// 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 (
|
||||||
<Paper
|
<Paper
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user