bug fixes
This commit is contained in:
@@ -84,7 +84,7 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Atemschutz
|
||||
|
||||
@@ -82,7 +82,7 @@ const EquipmentDashboardCard: React.FC<EquipmentDashboardCardProps> = ({
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Ausrüstung
|
||||
|
||||
@@ -98,7 +98,7 @@ const VehicleDashboardCard: React.FC<VehicleDashboardCardProps> = ({
|
||||
if (hideWhenEmpty && allGood) return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Fahrzeuge
|
||||
|
||||
@@ -37,17 +37,19 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
// Optionally verify token is still valid
|
||||
try {
|
||||
await authService.getCurrentUser();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Token validation failed:', error);
|
||||
// Token is invalid, clear it
|
||||
removeToken();
|
||||
removeUser();
|
||||
setState({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
// Only clear auth for explicit 401 Unauthorized
|
||||
// Network errors or server errors (5xx) should not log out the user
|
||||
if (error?.status === 401) {
|
||||
removeToken();
|
||||
removeUser();
|
||||
setState({ user: null, token: null, isAuthenticated: false, isLoading: false });
|
||||
} else {
|
||||
// Keep existing auth state on non-auth errors (network issues, server down)
|
||||
// The user may still be authenticated, just the server is temporarily unavailable
|
||||
setState({ user, token, isAuthenticated: true, isLoading: false });
|
||||
}
|
||||
} finally {
|
||||
setAuthInitialized(true);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ function Dashboard() {
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
<Container maxWidth="xl">
|
||||
<Grid container spacing={3}>
|
||||
{/* Welcome Message */}
|
||||
<Grid item xs={12}>
|
||||
@@ -76,48 +76,48 @@ function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* Vehicle Status Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'flex' }}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '380ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<VehicleDashboardCard />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
{/* Equipment Status Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'flex' }}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '450ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<EquipmentDashboardCard />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
{/* Atemschutz Status Card */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'flex' }}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '420ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<AtemschutzDashboardCard />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
{/* Upcoming Events Widget */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={12} md={6} sx={{ display: 'flex' }}>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<UpcomingEventsWidget />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Grid>
|
||||
|
||||
{/* Nextcloud Talk Widget */}
|
||||
<Grid item xs={12} md={6} lg={4}>
|
||||
<Grid item xs={12} md={6} lg={5} sx={{ display: 'flex' }}>
|
||||
{dataLoading ? (
|
||||
<SkeletonCard variant="basic" />
|
||||
) : (
|
||||
<Fade in={true} timeout={600} style={{ transitionDelay: '520ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<NextcloudTalkWidget />
|
||||
</Box>
|
||||
</Fade>
|
||||
@@ -125,12 +125,12 @@ function Dashboard() {
|
||||
</Grid>
|
||||
|
||||
{/* Activity Feed */}
|
||||
<Grid item xs={12}>
|
||||
<Grid item xs={12} md={6} lg={7} sx={{ display: 'flex' }}>
|
||||
{dataLoading ? (
|
||||
<SkeletonCard variant="detailed" />
|
||||
) : (
|
||||
<Fade in={true} timeout={600} style={{ transitionDelay: '550ms' }}>
|
||||
<Box>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<ActivityFeed />
|
||||
</Box>
|
||||
</Fade>
|
||||
|
||||
@@ -79,6 +79,7 @@ import type {
|
||||
VeranstaltungKategorie,
|
||||
GroupInfo,
|
||||
CreateVeranstaltungInput,
|
||||
WiederholungConfig,
|
||||
} from '../types/events.types';
|
||||
import type {
|
||||
FahrzeugBuchungListItem,
|
||||
@@ -768,6 +769,11 @@ function VeranstaltungFormDialog({
|
||||
const notification = useNotification();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form, setForm] = useState<CreateVeranstaltungInput>({ ...EMPTY_VERANSTALTUNG_FORM });
|
||||
const [wiederholungAktiv, setWiederholungAktiv] = useState(false);
|
||||
const [wiederholungTyp, setWiederholungTyp] = useState<WiederholungConfig['typ']>('wöchentlich');
|
||||
const [wiederholungIntervall, setWiederholungIntervall] = useState(1);
|
||||
const [wiederholungBis, setWiederholungBis] = useState('');
|
||||
const [wiederholungWochentag, setWiederholungWochentag] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -787,12 +793,22 @@ function VeranstaltungFormDialog({
|
||||
anmeldung_erforderlich: editingEvent.anmeldung_erforderlich,
|
||||
anmeldung_bis: null,
|
||||
});
|
||||
setWiederholungAktiv(false);
|
||||
setWiederholungTyp('wöchentlich');
|
||||
setWiederholungIntervall(1);
|
||||
setWiederholungBis('');
|
||||
setWiederholungWochentag(0);
|
||||
} else {
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
const later = new Date(now);
|
||||
later.setHours(later.getHours() + 2);
|
||||
setForm({ ...EMPTY_VERANSTALTUNG_FORM, datum_von: now.toISOString(), datum_bis: later.toISOString() });
|
||||
setWiederholungAktiv(false);
|
||||
setWiederholungTyp('wöchentlich');
|
||||
setWiederholungIntervall(1);
|
||||
setWiederholungBis('');
|
||||
setWiederholungWochentag(0);
|
||||
}
|
||||
}, [open, editingEvent]);
|
||||
|
||||
@@ -816,12 +832,29 @@ function VeranstaltungFormDialog({
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const createPayload: CreateVeranstaltungInput = {
|
||||
...form,
|
||||
wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis)
|
||||
? {
|
||||
typ: wiederholungTyp,
|
||||
bis: wiederholungBis,
|
||||
intervall: wiederholungTyp === 'wöchentlich' ? wiederholungIntervall : undefined,
|
||||
wochentag: (wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag')
|
||||
? wiederholungWochentag
|
||||
: undefined,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
if (editingEvent) {
|
||||
await eventsApi.updateEvent(editingEvent.id, form);
|
||||
notification.showSuccess('Veranstaltung aktualisiert');
|
||||
} else {
|
||||
await eventsApi.createEvent(form);
|
||||
notification.showSuccess('Veranstaltung erstellt');
|
||||
await eventsApi.createEvent(createPayload);
|
||||
notification.showSuccess(
|
||||
wiederholungAktiv && wiederholungBis
|
||||
? 'Veranstaltung und Wiederholungen erstellt'
|
||||
: 'Veranstaltung erstellt'
|
||||
);
|
||||
}
|
||||
onSaved();
|
||||
onClose();
|
||||
@@ -855,11 +888,14 @@ function VeranstaltungFormDialog({
|
||||
fullWidth
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Kategorie</InputLabel>
|
||||
<InputLabel id="kategorie-select-label" shrink>Kategorie</InputLabel>
|
||||
<Select
|
||||
labelId="kategorie-select-label"
|
||||
label="Kategorie"
|
||||
value={form.kategorie_id ?? ''}
|
||||
onChange={(e) => handleChange('kategorie_id', e.target.value || null)}
|
||||
displayEmpty
|
||||
notched
|
||||
>
|
||||
<MenuItem value=""><em>Keine Kategorie</em></MenuItem>
|
||||
{kategorien.map((k) => (
|
||||
@@ -975,6 +1011,79 @@ function VeranstaltungFormDialog({
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
{/* Wiederholung (only for new events) */}
|
||||
{!editingEvent && (
|
||||
<>
|
||||
<Divider />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={wiederholungAktiv}
|
||||
onChange={(e) => setWiederholungAktiv(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Wiederkehrende Veranstaltung"
|
||||
/>
|
||||
{wiederholungAktiv && (
|
||||
<Stack spacing={2}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="wiederholung-typ-label">Wiederholung</InputLabel>
|
||||
<Select
|
||||
labelId="wiederholung-typ-label"
|
||||
label="Wiederholung"
|
||||
value={wiederholungTyp}
|
||||
onChange={(e) => setWiederholungTyp(e.target.value as WiederholungConfig['typ'])}
|
||||
>
|
||||
<MenuItem value="wöchentlich">Wöchentlich</MenuItem>
|
||||
<MenuItem value="zweiwöchentlich">Vierzehntägig (alle 2 Wochen)</MenuItem>
|
||||
<MenuItem value="monatlich_datum">Monatlich (gleicher Tag)</MenuItem>
|
||||
<MenuItem value="monatlich_erster_wochentag">Monatlich (erster Wochentag)</MenuItem>
|
||||
<MenuItem value="monatlich_letzter_wochentag">Monatlich (letzter Wochentag)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{wiederholungTyp === 'wöchentlich' && (
|
||||
<TextField
|
||||
label="Intervall (Wochen)"
|
||||
type="number"
|
||||
size="small"
|
||||
value={wiederholungIntervall}
|
||||
onChange={(e) => setWiederholungIntervall(Math.max(1, Math.min(52, parseInt(e.target.value) || 1)))}
|
||||
inputProps={{ min: 1, max: 52 }}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
|
||||
{(wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag') && (
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="wiederholung-wochentag-label">Wochentag</InputLabel>
|
||||
<Select
|
||||
labelId="wiederholung-wochentag-label"
|
||||
label="Wochentag"
|
||||
value={wiederholungWochentag}
|
||||
onChange={(e) => setWiederholungWochentag(Number(e.target.value))}
|
||||
>
|
||||
{['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'].map((d, i) => (
|
||||
<MenuItem key={i} value={i}>{d}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Wiederholungen bis"
|
||||
type="date"
|
||||
size="small"
|
||||
value={wiederholungBis}
|
||||
onChange={(e) => setWiederholungBis(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
helperText="Letztes Datum für Wiederholungen"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -1211,7 +1320,7 @@ export default function Kalender() {
|
||||
setCancelEventGrund('');
|
||||
loadCalendarData();
|
||||
} catch (e: unknown) {
|
||||
notification.showError(e instanceof Error ? e.message : 'Fehler beim Absagen');
|
||||
notification.showError((e as any)?.message || 'Fehler beim Absagen');
|
||||
} finally {
|
||||
setCancelEventLoading(false);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ function KategorieDialog({ open, onClose, onSaved, editing, groups }: KategorieD
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
const msg = (e as any)?.message || 'Fehler beim Speichern';
|
||||
notification.showError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -247,7 +247,7 @@ function DeleteDialog({ open, kategorie, onClose, onDeleted }: DeleteDialogProps
|
||||
onDeleted();
|
||||
onClose();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Löschen';
|
||||
const msg = (e as any)?.message || 'Fehler beim Löschen';
|
||||
notification.showError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -306,7 +306,7 @@ export default function VeranstaltungKategorien() {
|
||||
setKategorien(data);
|
||||
setGroups(groupData);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Laden der Kategorien';
|
||||
const msg = (e as any)?.message || 'Fehler beim Laden der Kategorien';
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -3,9 +3,17 @@ import { API_URL } from '../utils/config';
|
||||
import { getToken, removeToken, removeUser } from '../utils/storage';
|
||||
|
||||
let authInitialized = false;
|
||||
let isRedirectingToLogin = false;
|
||||
|
||||
export function setAuthInitialized(value: boolean): void {
|
||||
authInitialized = value;
|
||||
if (value === true) {
|
||||
isRedirectingToLogin = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetRedirectFlag(): void {
|
||||
isRedirectingToLogin = false;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
@@ -46,7 +54,8 @@ class ApiService {
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
if (authInitialized) {
|
||||
if (authInitialized && !isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true;
|
||||
// Clear tokens and redirect to login
|
||||
console.warn('Unauthorized request, redirecting to login');
|
||||
removeToken();
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
// Frontend events types — mirrors backend events model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface WiederholungConfig {
|
||||
typ: 'wöchentlich' | 'zweiwöchentlich' | 'monatlich_datum' | 'monatlich_erster_wochentag' | 'monatlich_letzter_wochentag';
|
||||
intervall?: number;
|
||||
bis: string; // YYYY-MM-DD
|
||||
wochentag?: number; // 0=Mon...6=Sun
|
||||
}
|
||||
|
||||
export interface VeranstaltungKategorie {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -29,6 +36,8 @@ export interface VeranstaltungListItem {
|
||||
alle_gruppen: boolean;
|
||||
abgesagt: boolean;
|
||||
anmeldung_erforderlich: boolean;
|
||||
wiederholung?: WiederholungConfig | null;
|
||||
wiederholung_parent_id?: string | null;
|
||||
}
|
||||
|
||||
export interface Veranstaltung extends VeranstaltungListItem {
|
||||
@@ -41,6 +50,8 @@ export interface Veranstaltung extends VeranstaltungListItem {
|
||||
abgesagt_am?: string | null;
|
||||
erstellt_am: string;
|
||||
aktualisiert_am: string;
|
||||
wiederholung?: WiederholungConfig | null;
|
||||
wiederholung_parent_id?: string | null;
|
||||
}
|
||||
|
||||
export interface GroupInfo {
|
||||
@@ -62,4 +73,5 @@ export interface CreateVeranstaltungInput {
|
||||
max_teilnehmer?: number | null;
|
||||
anmeldung_erforderlich: boolean;
|
||||
anmeldung_bis?: string | null;
|
||||
wiederholung?: WiederholungConfig | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user