new features
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user