diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index eb14a22..4f2ce5f 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -19,8 +19,16 @@ function extractNames(userInfo: { name?: string; given_name?: string; family_nam const familyName = userInfo.family_name?.trim(); // If Authentik provides both and they differ, use them directly + // BUT: guard against the case where given_name is actually the full name + // (e.g. Authentik sends given_name="Matthias Hochmeister", family_name="Hochmeister") if (givenName && familyName && givenName !== familyName) { - return { given_name: givenName, family_name: familyName }; + const looksLikeFullName = + givenName.includes(' ') && + (givenName.endsWith(' ' + familyName) || givenName === familyName); + if (!looksLikeFullName) { + return { given_name: givenName, family_name: familyName }; + } + // Fall through to split the name field } // Fall back to splitting the name field diff --git a/backend/src/database/migrations/018_fix_aktualisiert_am_trigger.sql b/backend/src/database/migrations/018_fix_aktualisiert_am_trigger.sql new file mode 100644 index 0000000..48bc988 --- /dev/null +++ b/backend/src/database/migrations/018_fix_aktualisiert_am_trigger.sql @@ -0,0 +1,26 @@ +-- Migration 018: Fix BEFORE UPDATE triggers on event tables +-- Problem: update_updated_at_column() sets NEW.updated_at but both event tables +-- use aktualisiert_am instead. This causes every UPDATE to fail inside the trigger. + +-- Create a new trigger function that references the correct column name +CREATE OR REPLACE FUNCTION update_aktualisiert_am_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.aktualisiert_am = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Fix veranstaltungen table trigger +DROP TRIGGER IF EXISTS update_veranstaltungen_aktualisiert_am ON veranstaltungen; +CREATE TRIGGER update_veranstaltungen_aktualisiert_am + BEFORE UPDATE ON veranstaltungen + FOR EACH ROW + EXECUTE FUNCTION update_aktualisiert_am_column(); + +-- Fix veranstaltung_kategorien table trigger (if it was added) +DROP TRIGGER IF EXISTS update_veranstaltung_kategorien_aktualisiert_am ON veranstaltung_kategorien; +CREATE TRIGGER update_veranstaltung_kategorien_aktualisiert_am + BEFORE UPDATE ON veranstaltung_kategorien + FOR EACH ROW + EXECUTE FUNCTION update_aktualisiert_am_column(); diff --git a/backend/src/routes/booking.routes.ts b/backend/src/routes/booking.routes.ts index 7dbb409..0ac4fdc 100644 --- a/backend/src/routes/booking.routes.ts +++ b/backend/src/routes/booking.routes.ts @@ -21,7 +21,7 @@ router.get('/calendar-token', authenticate, bookingController.getCalendarToken.b // ── Write operations ────────────────────────────────────────────────────────── -router.post('/', authenticate, requireGroups(WRITE_GROUPS), bookingController.create.bind(bookingController)); +router.post('/', authenticate, bookingController.create.bind(bookingController)); router.patch('/:id', authenticate, requireGroups(WRITE_GROUPS), bookingController.update.bind(bookingController)); // Soft-cancel (sets abgesagt=TRUE) diff --git a/frontend/src/components/vehicles/VehicleDashboardCard.tsx b/frontend/src/components/vehicles/VehicleDashboardCard.tsx index dd14164..4b2a78e 100644 --- a/frontend/src/components/vehicles/VehicleDashboardCard.tsx +++ b/frontend/src/components/vehicles/VehicleDashboardCard.tsx @@ -11,7 +11,9 @@ import { } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import { vehiclesApi } from '../../services/vehicles'; +import { equipmentApi } from '../../services/equipment'; import type { VehicleStats, InspectionAlert } from '../../types/vehicle.types'; +import type { VehicleEquipmentWarning } from '../../types/equipment.types'; interface VehicleDashboardCardProps { hideWhenEmpty?: boolean; @@ -22,6 +24,7 @@ const VehicleDashboardCard: React.FC = ({ }) => { const [stats, setStats] = useState(null); const [alerts, setAlerts] = useState([]); + const [equipmentWarnings, setEquipmentWarnings] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -31,13 +34,15 @@ const VehicleDashboardCard: React.FC = ({ try { setLoading(true); setError(null); - const [statsData, alertsData] = await Promise.all([ + const [statsData, alertsData, warningsData] = await Promise.all([ vehiclesApi.getStats(), vehiclesApi.getAlerts(30), + equipmentApi.getVehicleWarnings(), ]); if (mounted) { setStats(statsData); setAlerts(alertsData); + setEquipmentWarnings(warningsData); } } catch { if (mounted) setError('Fahrzeugstatus konnte nicht geladen werden.'); @@ -84,7 +89,8 @@ const VehicleDashboardCard: React.FC = ({ const hasConcerns = overdueAlerts.length > 0 || upcomingAlerts.length > 0 || - stats.ausserDienst > 0; + stats.ausserDienst > 0 || + equipmentWarnings.length > 0; const allGood = stats.einsatzbereit === stats.total && !hasConcerns; @@ -133,6 +139,14 @@ const VehicleDashboardCard: React.FC = ({ )} + {equipmentWarnings.length > 0 && ( + + Ausrüstung nicht verfügbar + + {equipmentWarnings.length} Ausrüstungsgegenstand{equipmentWarnings.length !== 1 ? 'stände' : 'stand'} nicht einsatzbereit + + + )} )} diff --git a/frontend/src/pages/FahrzeugBuchungen.tsx b/frontend/src/pages/FahrzeugBuchungen.tsx index 8a23b82..84e109d 100644 --- a/frontend/src/pages/FahrzeugBuchungen.tsx +++ b/frontend/src/pages/FahrzeugBuchungen.tsx @@ -81,6 +81,7 @@ const EMPTY_FORM: CreateBuchungInput = { }; const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_moderator']; +const MANAGE_ART_GROUPS = ['dashboard_admin', 'dashboard_moderator']; // --------------------------------------------------------------------------- // Main Page @@ -89,8 +90,11 @@ const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_mod function FahrzeugBuchungen() { const { user } = useAuth(); const notification = useNotification(); + const canCreate = !!user; // All authenticated users can create bookings const canWrite = - user?.groups?.some((g) => WRITE_GROUPS.includes(g)) ?? false; + user?.groups?.some((g) => WRITE_GROUPS.includes(g)) ?? false; // Can edit/cancel + const canChangeBuchungsArt = + user?.groups?.some((g) => MANAGE_ART_GROUPS.includes(g)) ?? false; // Can change booking type // ── Week navigation ──────────────────────────────────────────────────────── const [currentWeekStart, setCurrentWeekStart] = useState(() => @@ -194,7 +198,7 @@ function FahrzeugBuchungen() { }; const handleCellClick = (vehicleId: string, day: Date) => { - if (!canWrite) return; + if (!canCreate) return; const dateStr = format(day, "yyyy-MM-dd'T'08:00"); const dateEndStr = format(day, "yyyy-MM-dd'T'17:00"); setEditingBooking(null); @@ -339,7 +343,7 @@ function FahrzeugBuchungen() { > Kalender - {canWrite && ( + {canCreate && (