feat: widget icons, dark theme tables, breadcrumb removal, bookkeeping rework, personal equipment pages, PDF/order improvements
This commit is contained in:
@@ -5,55 +5,42 @@ import {
|
||||
Chip,
|
||||
Container,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon } from '@mui/icons-material';
|
||||
import CheckroomIcon from '@mui/icons-material/Checkroom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { personalEquipmentApi } from '../services/personalEquipment';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { membersService } from '../services/members';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||
import { FormDialog, PageHeader } from '../components/templates';
|
||||
import { PageHeader } from '../components/templates';
|
||||
import { KatalogTab } from '../components/shared/KatalogTab';
|
||||
import {
|
||||
ZUSTAND_LABELS,
|
||||
ZUSTAND_COLORS,
|
||||
} from '../types/personalEquipment.types';
|
||||
import type {
|
||||
PersoenlicheAusruestungZustand,
|
||||
CreatePersoenlicheAusruestungPayload,
|
||||
} from '../types/personalEquipment.types';
|
||||
import type { AusruestungArtikel } from '../types/ausruestungsanfrage.types';
|
||||
import type { PersoenlicheAusruestungZustand } from '../types/personalEquipment.types';
|
||||
|
||||
const ZUSTAND_OPTIONS = Object.entries(ZUSTAND_LABELS) as [PersoenlicheAusruestungZustand, string][];
|
||||
|
||||
function PersoenlicheAusruestungPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
|
||||
const canCreate = hasPermission('persoenliche_ausruestung:create');
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [filterZustand, setFilterZustand] = useState<string>('');
|
||||
const [filterUser, setFilterUser] = useState<string>('');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
// Form state
|
||||
const [formBezeichnung, setFormBezeichnung] = useState<string | AusruestungArtikel | null>(null);
|
||||
const [formKategorie, setFormKategorie] = useState('');
|
||||
const [formUserId, setFormUserId] = useState<{ id: string; name: string } | null>(null);
|
||||
const [formBenutzerName, setFormBenutzerName] = useState('');
|
||||
const [formGroesse, setFormGroesse] = useState('');
|
||||
const [formZustand, setFormZustand] = useState<PersoenlicheAusruestungZustand>('gut');
|
||||
const [formNotizen, setFormNotizen] = useState('');
|
||||
|
||||
// Data queries
|
||||
const { data: items, isLoading } = useQuery({
|
||||
queryKey: ['persoenliche-ausruestung', 'all'],
|
||||
@@ -61,12 +48,6 @@ function PersoenlicheAusruestungPage() {
|
||||
staleTime: 2 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: catalogItems } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage-items-catalog'],
|
||||
queryFn: () => ausruestungsanfrageApi.getItems(),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: membersList } = useQuery({
|
||||
queryKey: ['members-list-compact'],
|
||||
queryFn: () => membersService.getMembers({ pageSize: 500 }),
|
||||
@@ -81,48 +62,6 @@ function PersoenlicheAusruestungPage() {
|
||||
}));
|
||||
}, [membersList]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: CreatePersoenlicheAusruestungPayload) => personalEquipmentApi.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['persoenliche-ausruestung'] });
|
||||
showSuccess('Persönliche Ausrüstung erstellt');
|
||||
setDialogOpen(false);
|
||||
resetForm();
|
||||
},
|
||||
onError: () => {
|
||||
showError('Fehler beim Erstellen');
|
||||
},
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setFormBezeichnung(null);
|
||||
setFormKategorie('');
|
||||
setFormUserId(null);
|
||||
setFormBenutzerName('');
|
||||
setFormGroesse('');
|
||||
setFormZustand('gut');
|
||||
setFormNotizen('');
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
const bezeichnung = typeof formBezeichnung === 'string'
|
||||
? formBezeichnung
|
||||
: formBezeichnung?.bezeichnung ?? '';
|
||||
if (!bezeichnung.trim()) return;
|
||||
|
||||
const payload: CreatePersoenlicheAusruestungPayload = {
|
||||
bezeichnung: bezeichnung.trim(),
|
||||
kategorie: formKategorie || undefined,
|
||||
artikel_id: typeof formBezeichnung === 'object' && formBezeichnung ? formBezeichnung.id : undefined,
|
||||
user_id: formUserId?.id || undefined,
|
||||
benutzer_name: formBenutzerName || undefined,
|
||||
groesse: formGroesse || undefined,
|
||||
zustand: formZustand,
|
||||
notizen: formNotizen || undefined,
|
||||
};
|
||||
createMutation.mutate(payload);
|
||||
};
|
||||
|
||||
// Filter logic
|
||||
const filtered = useMemo(() => {
|
||||
let result = items ?? [];
|
||||
@@ -148,9 +87,15 @@ function PersoenlicheAusruestungPage() {
|
||||
<Container maxWidth="lg">
|
||||
<PageHeader
|
||||
title="Persönliche Ausrüstung"
|
||||
breadcrumbs={[{ label: 'Persönliche Ausrüstung' }]}
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} sx={{ mb: 3 }}>
|
||||
<Tab label="Zuweisungen" />
|
||||
<Tab label="Katalog" />
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
@@ -293,108 +238,21 @@ function PersoenlicheAusruestungPage() {
|
||||
</tbody>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && <KatalogTab />}
|
||||
</Container>
|
||||
|
||||
{/* FAB */}
|
||||
{canCreate && (
|
||||
{canCreate && activeTab === 0 && (
|
||||
<ChatAwareFab
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => navigate('/persoenliche-ausruestung/neu')}
|
||||
aria-label="Persönliche Ausrüstung hinzufügen"
|
||||
>
|
||||
<AddIcon />
|
||||
</ChatAwareFab>
|
||||
)}
|
||||
|
||||
{/* Create Dialog */}
|
||||
<FormDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => { setDialogOpen(false); resetForm(); }}
|
||||
title="Persönliche Ausrüstung hinzufügen"
|
||||
onSubmit={handleCreate}
|
||||
submitLabel="Erstellen"
|
||||
isSubmitting={createMutation.isPending}
|
||||
>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={catalogItems ?? []}
|
||||
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
|
||||
value={formBezeichnung}
|
||||
onChange={(_e, v) => {
|
||||
setFormBezeichnung(v);
|
||||
if (v && typeof v !== 'string' && v.kategorie) {
|
||||
setFormKategorie(v.kategorie);
|
||||
}
|
||||
}}
|
||||
onInputChange={(_e, v) => {
|
||||
if (typeof formBezeichnung === 'string' || formBezeichnung === null) {
|
||||
setFormBezeichnung(v);
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Bezeichnung" required size="small" />
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{canViewAll && (
|
||||
<Autocomplete
|
||||
options={memberOptions}
|
||||
getOptionLabel={(o) => o.name}
|
||||
value={formUserId}
|
||||
onChange={(_e, v) => setFormUserId(v)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Benutzer" size="small" />
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!canViewAll && (
|
||||
<TextField
|
||||
label="Benutzer (Name)"
|
||||
size="small"
|
||||
value={formBenutzerName}
|
||||
onChange={(e) => setFormBenutzerName(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Kategorie"
|
||||
size="small"
|
||||
value={formKategorie}
|
||||
onChange={(e) => setFormKategorie(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Größe"
|
||||
size="small"
|
||||
value={formGroesse}
|
||||
onChange={(e) => setFormGroesse(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Zustand"
|
||||
select
|
||||
size="small"
|
||||
value={formZustand}
|
||||
onChange={(e) => setFormZustand(e.target.value as PersoenlicheAusruestungZustand)}
|
||||
>
|
||||
{ZUSTAND_OPTIONS.map(([key, label]) => (
|
||||
<MenuItem key={key} value={key}>{label}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="Notizen"
|
||||
size="small"
|
||||
multiline
|
||||
rows={2}
|
||||
value={formNotizen}
|
||||
onChange={(e) => setFormNotizen(e.target.value)}
|
||||
/>
|
||||
</Stack>
|
||||
</FormDialog>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user