eat(ausruestung): allow create role to view full list, add Mitglieder pagination, add admin reset for persoenliche Ausruestung
This commit is contained in:
@@ -246,6 +246,7 @@ router.delete('/cleanup/reset-buchhaltung-transaktionen', authenticate, requireP
|
||||
router.delete('/cleanup/reset-buchhaltung-konten', authenticate, requirePermission('admin:write'), (req, res) => { req.params.resetTarget = 'reset-buchhaltung-konten'; return resetHandler(req, res); });
|
||||
router.delete('/cleanup/reset-buchhaltung-bankkonten', authenticate, requirePermission('admin:write'), (req, res) => { req.params.resetTarget = 'reset-buchhaltung-bankkonten'; return resetHandler(req, res); });
|
||||
router.delete('/cleanup/reset-checklist-history', authenticate, requirePermission('admin:write'), (req, res) => { req.params.resetTarget = 'reset-checklist-history'; return resetHandler(req, res); });
|
||||
router.delete('/cleanup/reset-persoenliche-ausruestung', authenticate, requirePermission('admin:write'), (req, res) => { req.params.resetTarget = 'reset-persoenliche-ausruestung'; return resetHandler(req, res); });
|
||||
router.delete('/cleanup/issues-all', authenticate, requirePermission('admin:write'), (req, res) => { req.params.resetTarget = 'issues-all'; return resetHandler(req, res); });
|
||||
|
||||
router.delete(
|
||||
@@ -289,6 +290,7 @@ const RESET_TARGETS: Record<string, (confirm: boolean) => Promise<{ count: numbe
|
||||
'reset-buchhaltung-transaktionen': (c) => cleanupService.resetBuchhaltungTransaktionen(c),
|
||||
'reset-buchhaltung-konten': (c) => cleanupService.resetBuchhaltungKonten(c),
|
||||
'reset-buchhaltung-bankkonten': (c) => cleanupService.resetBuchhaltungBankkonten(c),
|
||||
'reset-persoenliche-ausruestung': (c) => cleanupService.resetPersoenlicheAusruestung(c),
|
||||
};
|
||||
|
||||
const resetHandler = async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import personalEquipmentController from '../controllers/personalEquipment.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { requirePermission } from '../middleware/rbac.middleware';
|
||||
import { requirePermission, requireAnyPermission } from '../middleware/rbac.middleware';
|
||||
import { permissionService } from '../services/permission.service';
|
||||
|
||||
const router = Router();
|
||||
@@ -9,8 +9,8 @@ const router = Router();
|
||||
// Own items — any authenticated user with view permission
|
||||
router.get('/my', authenticate, personalEquipmentController.getMy.bind(personalEquipmentController));
|
||||
|
||||
// All items — requires view_all
|
||||
router.get('/', authenticate, requirePermission('persoenliche_ausruestung:view_all'), personalEquipmentController.list.bind(personalEquipmentController));
|
||||
// All items — requires view_all or create
|
||||
router.get('/', authenticate, requireAnyPermission('persoenliche_ausruestung:view_all', 'persoenliche_ausruestung:create'), personalEquipmentController.list.bind(personalEquipmentController));
|
||||
|
||||
// By user — own data or view_all
|
||||
router.get('/user/:userId', authenticate, async (req, res, next) => {
|
||||
|
||||
@@ -238,6 +238,21 @@ class CleanupService {
|
||||
logger.info(`Cleanup: truncated buchhaltung_bankkonten (${count} rows) and reset sequence`);
|
||||
return { count, deleted: true };
|
||||
}
|
||||
|
||||
async resetPersoenlicheAusruestung(confirm: boolean): Promise<CleanupResult> {
|
||||
if (!confirm) {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM persoenliche_ausruestung WHERE geloescht_am IS NULL');
|
||||
return { count: rows[0].count, deleted: false };
|
||||
}
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM persoenliche_ausruestung');
|
||||
const count = rows[0].count;
|
||||
// Clear FK references from request positions before truncating
|
||||
await pool.query(`UPDATE ausruestung_anfrage_positionen SET zuweisung_persoenlich_id = NULL, zuweisung_typ = 'keine' WHERE zuweisung_persoenlich_id IS NOT NULL`);
|
||||
// CASCADE removes persoenliche_ausruestung_eigenschaften rows automatically
|
||||
await pool.query('TRUNCATE persoenliche_ausruestung CASCADE');
|
||||
logger.info(`Cleanup: truncated persoenliche_ausruestung (${count} rows)`);
|
||||
return { count, deleted: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new CleanupService();
|
||||
|
||||
@@ -45,6 +45,7 @@ const RESET_SECTIONS: ResetSection[] = [
|
||||
{ key: 'reset-buchhaltung-transaktionen', label: 'Buchhaltung: Transaktionen loeschen', description: 'Alle Buchungen und Transaktionen loeschen und Nummerierung zuruecksetzen. Konten und Haushaltsjahre bleiben erhalten.' },
|
||||
{ key: 'reset-buchhaltung-konten', label: 'Buchhaltung: Konten loeschen', description: 'Alle Konten und alle zugehoerigen Transaktionen loeschen und Nummerierung zuruecksetzen. Haushaltsjahre bleiben erhalten.' },
|
||||
{ key: 'reset-buchhaltung-bankkonten', label: 'Buchhaltung: Bankkonten loeschen', description: 'Alle Bankkonten loeschen und Nummerierung zuruecksetzen.' },
|
||||
{ key: 'reset-persoenliche-ausruestung', label: 'Persoenliche Ausruestung zuruecksetzen', description: 'Alle persoenlichen Ausruestungszuweisungen loeschen. Zuordnungen in Anfragen werden zurueckgesetzt.' },
|
||||
];
|
||||
|
||||
interface SectionState {
|
||||
|
||||
@@ -344,6 +344,22 @@ function Mitglieder() {
|
||||
paginationEnabled={false}
|
||||
stickyHeader
|
||||
/>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={total}
|
||||
page={page}
|
||||
onPageChange={(_e, newPage) => setPage(newPage)}
|
||||
rowsPerPage={pageSize}
|
||||
rowsPerPageOptions={[25, 50, 100, { value: -1, label: 'Alle' }]}
|
||||
onRowsPerPageChange={(e) => {
|
||||
setPageSize(parseInt(e.target.value, 10));
|
||||
setPage(0);
|
||||
}}
|
||||
labelRowsPerPage="Einträge pro Seite:"
|
||||
labelDisplayedRows={({ from, to, count }) =>
|
||||
`${from}–${to} von ${count !== -1 ? count : `mehr als ${to}`}`
|
||||
}
|
||||
/>
|
||||
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
@@ -37,6 +37,7 @@ function PersoenlicheAusruestungPage() {
|
||||
|
||||
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
|
||||
const canCreate = hasPermission('persoenliche_ausruestung:create');
|
||||
const canSeeAll = canViewAll || canCreate;
|
||||
const canApprove = hasPermission('ausruestungsanfrage:approve');
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
@@ -47,7 +48,7 @@ function PersoenlicheAusruestungPage() {
|
||||
// Data queries
|
||||
const { data: items, isLoading } = useQuery({
|
||||
queryKey: ['persoenliche-ausruestung', 'all'],
|
||||
queryFn: () => canViewAll ? personalEquipmentApi.getAll() : personalEquipmentApi.getMy(),
|
||||
queryFn: () => canSeeAll ? personalEquipmentApi.getAll() : personalEquipmentApi.getMy(),
|
||||
staleTime: 2 * 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -55,7 +56,7 @@ function PersoenlicheAusruestungPage() {
|
||||
queryKey: ['members-list-compact'],
|
||||
queryFn: () => membersService.getMembers({ pageSize: 500 }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
enabled: canViewAll,
|
||||
enabled: canSeeAll,
|
||||
});
|
||||
|
||||
const { data: unassignedPositions, isLoading: unassignedLoading } = useQuery({
|
||||
@@ -129,7 +130,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<MenuItem key={key} value={key}>{label}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{canViewAll && (
|
||||
{canSeeAll && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={memberOptions}
|
||||
@@ -176,7 +177,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<tr>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Kategorie</th>
|
||||
{canViewAll && <th>Benutzer</th>}
|
||||
{canSeeAll && <th>Benutzer</th>}
|
||||
<th>Größe</th>
|
||||
<th>Zustand</th>
|
||||
<th>Anschaffung</th>
|
||||
@@ -185,7 +186,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={canViewAll ? 6 : 5}>
|
||||
<td colSpan={canSeeAll ? 6 : 5}>
|
||||
<Typography color="text.secondary" sx={{ py: 4, textAlign: 'center' }}>
|
||||
Lade Daten…
|
||||
</Typography>
|
||||
@@ -193,7 +194,7 @@ function PersoenlicheAusruestungPage() {
|
||||
</tr>
|
||||
) : filtered.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={canViewAll ? 6 : 5}>
|
||||
<td colSpan={canSeeAll ? 6 : 5}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6 }}>
|
||||
<CheckroomIcon sx={{ fontSize: 48, color: 'text.disabled', mb: 1 }} />
|
||||
<Typography color="text.secondary">
|
||||
@@ -231,7 +232,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<td>
|
||||
<Typography variant="body2">{item.kategorie ?? '—'}</Typography>
|
||||
</td>
|
||||
{canViewAll && (
|
||||
{canSeeAll && (
|
||||
<td>
|
||||
<Typography variant="body2">
|
||||
{item.user_display_name ?? item.benutzer_name ?? '—'}
|
||||
|
||||
@@ -40,7 +40,7 @@ function buildParams(filters?: MemberFilters): URLSearchParams {
|
||||
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
if (filters.pageSize) params.append('pageSize', String(filters.pageSize));
|
||||
if (filters.pageSize !== undefined) params.append('pageSize', String(filters.pageSize));
|
||||
|
||||
filters.status?.forEach((s) => params.append('status[]', s));
|
||||
filters.dienstgrad?.forEach((d) => params.append('dienstgrad[]', d));
|
||||
|
||||
Reference in New Issue
Block a user