new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 16:58:46 +01:00
parent 948b211f70
commit 55ded22a6f
8 changed files with 452 additions and 43 deletions

View File

@@ -9,14 +9,25 @@ import {
Chip,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
MenuItem,
Select,
Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
@@ -25,11 +36,16 @@ import {
Add,
Build,
CheckCircle,
Close,
Delete,
Edit,
Error as ErrorIcon,
LinkRounded,
PauseCircle,
RemoveCircle,
Save,
Search,
Settings,
Star,
Warning,
} from '@mui/icons-material';
@@ -44,6 +60,7 @@ import {
EquipmentStats,
} from '../types/equipment.types';
import { usePermissions } from '../hooks/usePermissions';
import { useNotification } from '../contexts/NotificationContext';
import ChatAwareFab from '../components/shared/ChatAwareFab';
// ── Status chip config ────────────────────────────────────────────────────────
@@ -219,11 +236,186 @@ const EquipmentCard: React.FC<EquipmentCardProps> = ({ item, onClick }) => {
);
};
// ── Category Management Dialog ───────────────────────────────────────────────
interface CategoryDialogProps {
open: boolean;
onClose: () => void;
categories: AusruestungKategorie[];
onRefresh: () => void;
}
const CategoryManagementDialog: React.FC<CategoryDialogProps> = ({ open, onClose, categories, onRefresh }) => {
const { showSuccess, showError } = useNotification();
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
const [editKurzname, setEditKurzname] = useState('');
const [editMotor, setEditMotor] = useState(false);
const [newName, setNewName] = useState('');
const [newKurzname, setNewKurzname] = useState('');
const [newMotor, setNewMotor] = useState(false);
const [saving, setSaving] = useState(false);
const startEdit = (cat: AusruestungKategorie) => {
setEditingId(cat.id);
setEditName(cat.name);
setEditKurzname(cat.kurzname);
setEditMotor(cat.motorisiert);
};
const cancelEdit = () => {
setEditingId(null);
setEditName('');
setEditKurzname('');
setEditMotor(false);
};
const saveEdit = async () => {
if (!editingId || !editName.trim() || !editKurzname.trim()) return;
setSaving(true);
try {
await equipmentApi.updateCategory(editingId, { name: editName.trim(), kurzname: editKurzname.trim(), motorisiert: editMotor });
showSuccess('Kategorie aktualisiert');
cancelEdit();
onRefresh();
} catch {
showError('Kategorie konnte nicht aktualisiert werden');
} finally {
setSaving(false);
}
};
const handleCreate = async () => {
if (!newName.trim() || !newKurzname.trim()) return;
setSaving(true);
try {
await equipmentApi.createCategory({ name: newName.trim(), kurzname: newKurzname.trim(), motorisiert: newMotor });
showSuccess('Kategorie erstellt');
setNewName('');
setNewKurzname('');
setNewMotor(false);
onRefresh();
} catch {
showError('Kategorie konnte nicht erstellt werden');
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
setSaving(true);
try {
await equipmentApi.deleteCategory(id);
showSuccess('Kategorie gelöscht');
onRefresh();
} catch (err: any) {
const msg = err?.response?.data?.message || 'Kategorie konnte nicht gelöscht werden';
showError(msg);
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
Kategorien verwalten
<IconButton onClick={onClose} size="small"><Close /></IconButton>
</DialogTitle>
<DialogContent dividers>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Kurzname</TableCell>
<TableCell>Motorisiert</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{categories.map((cat) => (
<TableRow key={cat.id}>
{editingId === cat.id ? (
<>
<TableCell>
<TextField size="small" value={editName} onChange={(e) => setEditName(e.target.value)} fullWidth />
</TableCell>
<TableCell>
<TextField size="small" value={editKurzname} onChange={(e) => setEditKurzname(e.target.value)} fullWidth />
</TableCell>
<TableCell>
<Switch checked={editMotor} onChange={(e) => setEditMotor(e.target.checked)} size="small" />
</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={saveEdit} disabled={saving || !editName.trim() || !editKurzname.trim()} color="primary">
<Save fontSize="small" />
</IconButton>
<IconButton size="small" onClick={cancelEdit} disabled={saving}>
<Close fontSize="small" />
</IconButton>
</TableCell>
</>
) : (
<>
<TableCell>{cat.name}</TableCell>
<TableCell>{cat.kurzname}</TableCell>
<TableCell>{cat.motorisiert ? 'Ja' : 'Nein'}</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => startEdit(cat)} disabled={saving}>
<Edit fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => handleDelete(cat.id)} disabled={saving} color="error">
<Delete fontSize="small" />
</IconButton>
</TableCell>
</>
)}
</TableRow>
))}
{/* New category row */}
<TableRow>
<TableCell>
<TextField size="small" placeholder="Name" value={newName} onChange={(e) => setNewName(e.target.value)} fullWidth />
</TableCell>
<TableCell>
<TextField size="small" placeholder="Kurzname" value={newKurzname} onChange={(e) => setNewKurzname(e.target.value)} fullWidth />
</TableCell>
<TableCell>
<Switch checked={newMotor} onChange={(e) => setNewMotor(e.target.checked)} size="small" />
</TableCell>
<TableCell align="right">
<Button
variant="contained"
size="small"
startIcon={<Add />}
onClick={handleCreate}
disabled={saving || !newName.trim() || !newKurzname.trim()}
>
Hinzufügen
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Schließen</Button>
</DialogActions>
</Dialog>
);
};
// ── Main Page ─────────────────────────────────────────────────────────────────
function Ausruestung() {
const navigate = useNavigate();
const { canManageEquipment } = usePermissions();
const { canManageEquipment, hasPermission } = usePermissions();
const canManageCategories = hasPermission('ausruestung:manage_categories');
// Category dialog state
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
// Data state
const [equipment, setEquipment] = useState<AusruestungListItem[]>([]);
@@ -310,9 +502,18 @@ function Ausruestung() {
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
Ausrüstungsverwaltung
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
Ausrüstungsverwaltung
</Typography>
{canManageCategories && (
<Tooltip title="Kategorien verwalten">
<IconButton onClick={() => setCategoryDialogOpen(true)} size="small">
<Settings />
</IconButton>
</Tooltip>
)}
</Box>
{!loading && stats && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
<Typography variant="body2" color="text.secondary">
@@ -472,6 +673,15 @@ function Ausruestung() {
<Add />
</ChatAwareFab>
)}
{/* Category management dialog */}
{canManageCategories && (
<CategoryManagementDialog
open={categoryDialogOpen}
onClose={() => setCategoryDialogOpen(false)}
categories={categories}
onRefresh={fetchData}
/>
)}
</Container>
</DashboardLayout>
);