add features

This commit is contained in:
Matthias Hochmeister
2026-03-03 17:01:53 +01:00
parent 92b05726d4
commit 5a6fc85a75
30 changed files with 1104 additions and 198 deletions

View File

@@ -563,7 +563,7 @@ const WartungTab: React.FC<WartungTabProps> = ({ equipmentId, wartungslog, onAdd
function AusruestungDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { isAdmin, canChangeStatus } = usePermissions();
const { isAdmin, canManageCategory } = usePermissions();
const notification = useNotification();
const [equipment, setEquipment] = useState<AusruestungDetail | null>(null);
@@ -630,6 +630,16 @@ function AusruestungDetailPage() {
equipment.pruefung_tage_bis_faelligkeit !== null &&
equipment.pruefung_tage_bis_faelligkeit < 0;
// Derive an inline category object so canManageCategory can do the motorisiert check
const equipmentKategorie = {
id: equipment.kategorie_id,
name: equipment.kategorie_name,
kurzname: equipment.kategorie_kurzname,
sortierung: 0,
motorisiert: equipment.kategorie_motorisiert,
};
const canWrite = canManageCategory(equipmentKategorie);
const subtitle = [
equipment.kategorie_name,
equipment.seriennummer ? `SN: ${equipment.seriennummer}` : null,
@@ -665,7 +675,7 @@ function AusruestungDetailPage() {
label={AusruestungStatusLabel[equipment.status]}
color={STATUS_CHIP_COLOR[equipment.status]}
/>
{canChangeStatus && (
{canWrite && (
<Tooltip title="Gerät bearbeiten">
<IconButton
size="small"
@@ -714,7 +724,7 @@ function AusruestungDetailPage() {
<UebersichtTab
equipment={equipment}
onStatusUpdated={fetchEquipment}
canChangeStatus={canChangeStatus}
canChangeStatus={canWrite}
/>
</TabPanel>
@@ -723,7 +733,7 @@ function AusruestungDetailPage() {
equipmentId={equipment.id}
wartungslog={equipment.wartungslog ?? []}
onAdded={fetchEquipment}
canWrite={canChangeStatus}
canWrite={canWrite}
/>
</TabPanel>

View File

@@ -82,11 +82,11 @@ function toDateInput(iso: string | null | undefined): string {
function AusruestungForm() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { canChangeStatus } = usePermissions();
const { canManageEquipment } = usePermissions();
const isEditMode = Boolean(id);
// -- Permission guard: only authorized users may create or edit equipment ----
if (!canChangeStatus) {
if (!canManageEquipment) {
return (
<DashboardLayout>
<Container maxWidth="lg">

View File

@@ -13,8 +13,6 @@ import UpcomingEventsWidget from '../components/dashboard/UpcomingEventsWidget';
import AtemschutzDashboardCard from '../components/atemschutz/AtemschutzDashboardCard';
import EquipmentDashboardCard from '../components/equipment/EquipmentDashboardCard';
import VehicleDashboardCard from '../components/vehicles/VehicleDashboardCard';
import PersonalWarningsBanner from '../components/dashboard/PersonalWarningsBanner';
function Dashboard() {
const { user } = useAuth();
const canViewAtemschutz = user?.groups?.some(g =>
@@ -56,17 +54,6 @@ function Dashboard() {
</Box>
)}
{/* Personal Warnings Banner — full width, conditionally rendered */}
{user && (
<Box sx={{ gridColumn: '1 / -1' }}>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '150ms' }}>
<Box>
<PersonalWarningsBanner user={user} />
</Box>
</Fade>
</Box>
)}
{/* Vehicle Status Card */}
<Box>
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>

View File

@@ -837,6 +837,19 @@ function VeranstaltungFormDialog({
}, [open, editingEvent]);
const handleChange = (field: keyof CreateVeranstaltungInput, value: unknown) => {
if (field === 'kategorie_id' && !editingEvent) {
// Auto-fill zielgruppen / alle_gruppen from the selected category (only for new events)
const kat = kategorien.find((k) => k.id === value);
if (kat) {
setForm((prev) => ({
...prev,
kategorie_id: value as string | null,
alle_gruppen: kat.alle_gruppen,
zielgruppen: kat.alle_gruppen ? [] : kat.zielgruppen,
}));
return;
}
}
setForm((prev) => ({ ...prev, [field]: value }));
};

View File

@@ -48,6 +48,7 @@ interface KategorieFormData {
beschreibung: string;
farbe: string;
icon: string;
alle_gruppen: boolean;
zielgruppen: string[];
}
@@ -56,6 +57,7 @@ const EMPTY_FORM: KategorieFormData = {
beschreibung: '',
farbe: '#1976d2',
icon: '',
alle_gruppen: false,
zielgruppen: [],
};
@@ -80,7 +82,8 @@ function KategorieDialog({ open, onClose, onSaved, editing, groups }: KategorieD
beschreibung: editing.beschreibung ?? '',
farbe: editing.farbe,
icon: editing.icon ?? '',
zielgruppen: editing.zielgruppen ?? [],
alle_gruppen: editing.alle_gruppen ?? false,
zielgruppen: editing.alle_gruppen ? [] : (editing.zielgruppen ?? []),
});
} else {
setForm({ ...EMPTY_FORM });
@@ -112,7 +115,8 @@ function KategorieDialog({ open, onClose, onSaved, editing, groups }: KategorieD
beschreibung: form.beschreibung.trim() || undefined,
farbe: form.farbe,
icon: form.icon.trim() || undefined,
zielgruppen: form.zielgruppen,
alle_gruppen: form.alle_gruppen,
zielgruppen: form.alle_gruppen ? [] : form.zielgruppen,
};
if (editing) {
await eventsApi.updateKategorie(editing.id, payload);
@@ -188,7 +192,22 @@ function KategorieDialog({ open, onClose, onSaved, editing, groups }: KategorieD
placeholder="z.B. EmojiEvents"
helperText="Name eines MUI Material Icons"
/>
{/* Group checkboxes */}
{/* alle_gruppen toggle */}
<FormControlLabel
control={
<Checkbox
checked={form.alle_gruppen}
onChange={(e) => setForm((prev) => ({
...prev,
alle_gruppen: e.target.checked,
zielgruppen: e.target.checked ? [] : prev.zielgruppen,
}))}
size="small"
/>
}
label="Alle Mitglieder"
/>
{/* Group checkboxes — disabled when alle_gruppen is set */}
{groups.length > 0 && (
<Box>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}>
@@ -203,6 +222,7 @@ function KategorieDialog({ open, onClose, onSaved, editing, groups }: KategorieD
checked={form.zielgruppen.includes(group.id)}
onChange={() => handleGroupToggle(group.id)}
size="small"
disabled={form.alle_gruppen}
/>
}
label={group.label}
@@ -435,7 +455,15 @@ export default function VeranstaltungKategorien() {
{/* Gruppen */}
<TableCell>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(kat.zielgruppen ?? []).length === 0
{kat.alle_gruppen ? (
<Chip
label="Alle Mitglieder"
size="small"
color="primary"
variant="outlined"
sx={{ fontSize: '0.75rem' }}
/>
) : (kat.zielgruppen ?? []).length === 0
? <Typography variant="body2" color="text.secondary"></Typography>
: (kat.zielgruppen ?? []).map((gId) => {
const group = groups.find((g) => g.id === gId);

View File

@@ -610,6 +610,19 @@ function EventFormDialog({
}, [open, editingEvent]);
const handleChange = (field: keyof CreateVeranstaltungInput, value: unknown) => {
if (field === 'kategorie_id' && !editingEvent) {
// Auto-fill zielgruppen / alle_gruppen from the selected category (only for new events)
const kat = kategorien.find((k) => k.id === value);
if (kat) {
setForm((prev) => ({
...prev,
kategorie_id: value as string | null,
alle_gruppen: kat.alle_gruppen,
zielgruppen: kat.alle_gruppen ? [] : kat.zielgruppen,
}));
return;
}
}
setForm((prev) => ({ ...prev, [field]: value }));
};