feat(admin): centralize tool & module settings in Werkzeuge tab with per-tool permissions, DB-backed config, connection tests, and cog-button navigation
This commit is contained in:
@@ -20,15 +20,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Tab,
|
||||
Tabs,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Checkbox,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
@@ -38,12 +29,10 @@ import {
|
||||
ContentCopy,
|
||||
Cancel,
|
||||
Edit,
|
||||
Delete,
|
||||
Save,
|
||||
Close,
|
||||
EventBusy,
|
||||
Settings,
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
@@ -103,8 +92,6 @@ function FahrzeugBuchungen() {
|
||||
const canCreate = hasPermission('fahrzeugbuchungen:create');
|
||||
const canManage = hasPermission('fahrzeugbuchungen:manage');
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
// ── Filters ────────────────────────────────────────────────────────────────
|
||||
const today = new Date();
|
||||
const defaultFrom = format(today, 'yyyy-MM-dd');
|
||||
@@ -120,11 +107,6 @@ function FahrzeugBuchungen() {
|
||||
queryFn: fetchVehicles,
|
||||
});
|
||||
|
||||
const { data: kategorien = [] } = useQuery({
|
||||
queryKey: ['buchungskategorien-all'],
|
||||
queryFn: kategorieApi.getAll,
|
||||
});
|
||||
|
||||
const { data: activeKategorien = [] } = useQuery({
|
||||
queryKey: ['buchungskategorien'],
|
||||
queryFn: kategorieApi.getActive,
|
||||
@@ -191,46 +173,6 @@ function FahrzeugBuchungen() {
|
||||
}
|
||||
};
|
||||
|
||||
// ── Einstellungen: Categories management ───────────────────────────────────
|
||||
const [editRowId, setEditRowId] = useState<number | null>(null);
|
||||
const [editRowData, setEditRowData] = useState<Partial<BuchungsKategorie>>({});
|
||||
const [newKatDialog, setNewKatDialog] = useState(false);
|
||||
const [newKatForm, setNewKatForm] = useState({ bezeichnung: '', farbe: '#1976d2' });
|
||||
|
||||
const createKatMutation = useMutation({
|
||||
mutationFn: (data: Omit<BuchungsKategorie, 'id'>) => kategorieApi.create(data),
|
||||
onSuccess: () => {
|
||||
notification.showSuccess('Kategorie erstellt');
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] });
|
||||
setNewKatDialog(false);
|
||||
setNewKatForm({ bezeichnung: '', farbe: '#1976d2' });
|
||||
},
|
||||
onError: () => notification.showError('Fehler beim Erstellen'),
|
||||
});
|
||||
|
||||
const updateKatMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<BuchungsKategorie> }) =>
|
||||
kategorieApi.update(id, data),
|
||||
onSuccess: () => {
|
||||
notification.showSuccess('Kategorie aktualisiert');
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] });
|
||||
setEditRowId(null);
|
||||
},
|
||||
onError: () => notification.showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
const deleteKatMutation = useMutation({
|
||||
mutationFn: (id: number) => kategorieApi.delete(id),
|
||||
onSuccess: () => {
|
||||
notification.showSuccess('Kategorie deaktiviert');
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien-all'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['buchungskategorien'] });
|
||||
},
|
||||
onError: () => notification.showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
if (!isFeatureEnabled('fahrzeugbuchungen')) {
|
||||
return <ServiceModePage message="Fahrzeugbuchungen befinden sich aktuell im Wartungsmodus." />;
|
||||
@@ -244,23 +186,23 @@ function FahrzeugBuchungen() {
|
||||
<Typography variant="h4" fontWeight={700}>
|
||||
Fahrzeugbuchungen
|
||||
</Typography>
|
||||
<Button startIcon={<IosShare />} onClick={handleIcalOpen} variant="outlined" size="small">
|
||||
iCal abonnieren
|
||||
</Button>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{canManage && (
|
||||
<Tooltip title="Einstellungen">
|
||||
<IconButton onClick={() => navigate('/admin/settings?tab=1&subtab=fahrzeugbuchungen')}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button startIcon={<IosShare />} onClick={handleIcalOpen} variant="outlined" size="small">
|
||||
iCal abonnieren
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Tabs */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={tabIndex} onChange={(_, v) => setTabIndex(v)}>
|
||||
<Tab label="Buchungen" />
|
||||
{canManage && <Tab label="Einstellungen" />}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* ── Tab 0: Buchungen ─────────────────────────────────────────────── */}
|
||||
{tabIndex === 0 && (
|
||||
<>
|
||||
{/* Filters */}
|
||||
{/* ── Buchungen ─────────────────────────────────────────────── */}
|
||||
<>
|
||||
{/* Filters */}
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="center">
|
||||
<GermanDateField
|
||||
@@ -410,173 +352,9 @@ function FahrzeugBuchungen() {
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Tab 1: Einstellungen ─────────────────────────────────────────── */}
|
||||
{tabIndex === 1 && canManage && (
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Buchungskategorien</Typography>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setNewKatDialog(true)}
|
||||
>
|
||||
Neue Kategorie
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Farbe</TableCell>
|
||||
<TableCell>Sortierung</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell align="right">Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{kategorien.map((kat) => {
|
||||
const isEditing = editRowId === kat.id;
|
||||
return (
|
||||
<TableRow key={kat.id}>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editRowData.bezeichnung ?? kat.bezeichnung}
|
||||
onChange={(e) =>
|
||||
setEditRowData((d) => ({ ...d, bezeichnung: e.target.value }))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
kat.bezeichnung
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="color"
|
||||
value={editRowData.farbe ?? kat.farbe}
|
||||
onChange={(e) =>
|
||||
setEditRowData((d) => ({ ...d, farbe: e.target.value }))
|
||||
}
|
||||
style={{ width: 40, height: 28, border: 'none', cursor: 'pointer' }}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 1,
|
||||
bgcolor: kat.farbe,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={editRowData.sort_order ?? kat.sort_order}
|
||||
onChange={(e) =>
|
||||
setEditRowData((d) => ({
|
||||
...d,
|
||||
sort_order: parseInt(e.target.value) || 0,
|
||||
}))
|
||||
}
|
||||
sx={{ width: 80 }}
|
||||
/>
|
||||
) : (
|
||||
kat.sort_order
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<Checkbox
|
||||
checked={editRowData.aktiv ?? kat.aktiv}
|
||||
onChange={(e) =>
|
||||
setEditRowData((d) => ({ ...d, aktiv: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Checkbox checked={kat.aktiv} disabled />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{isEditing ? (
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
updateKatMutation.mutate({ id: kat.id, data: editRowData })
|
||||
}
|
||||
>
|
||||
<Save fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditRowId(null);
|
||||
setEditRowData({});
|
||||
}}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditRowId(kat.id);
|
||||
setEditRowData({
|
||||
bezeichnung: kat.bezeichnung,
|
||||
farbe: kat.farbe,
|
||||
sort_order: kat.sort_order,
|
||||
aktiv: kat.aktiv,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => deleteKatMutation.mutate(kat.id)}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{kategorien.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} align="center">
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
|
||||
Keine Kategorien vorhanden
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* ── FAB ── */}
|
||||
{canCreate && tabIndex === 0 && (
|
||||
{canCreate && (
|
||||
<ChatAwareFab
|
||||
color="primary"
|
||||
aria-label="Buchung erstellen"
|
||||
@@ -655,57 +433,6 @@ function FahrzeugBuchungen() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* ── New category dialog ── */}
|
||||
<Dialog
|
||||
open={newKatDialog}
|
||||
onClose={() => setNewKatDialog(false)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Neue Kategorie</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Bezeichnung"
|
||||
required
|
||||
value={newKatForm.bezeichnung}
|
||||
onChange={(e) =>
|
||||
setNewKatForm((f) => ({ ...f, bezeichnung: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||
Farbe
|
||||
</Typography>
|
||||
<input
|
||||
type="color"
|
||||
value={newKatForm.farbe}
|
||||
onChange={(e) => setNewKatForm((f) => ({ ...f, farbe: e.target.value }))}
|
||||
style={{ width: 60, height: 36, border: 'none', cursor: 'pointer' }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setNewKatDialog(false)}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
createKatMutation.mutate({
|
||||
bezeichnung: newKatForm.bezeichnung,
|
||||
farbe: newKatForm.farbe,
|
||||
aktiv: true,
|
||||
sort_order: kategorien.length,
|
||||
})
|
||||
}
|
||||
disabled={!newKatForm.bezeichnung || createKatMutation.isPending}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user