rework internal order system
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import ausruestungsanfrageService from '../services/ausruestungsanfrage.service';
|
||||
import notificationService from '../services/notification.service';
|
||||
import { permissionService } from '../services/permission.service';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
class AusruestungsanfrageController {
|
||||
@@ -129,9 +130,11 @@ class AusruestungsanfrageController {
|
||||
|
||||
async createRequest(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { items, notizen } = req.body as {
|
||||
const { items, notizen, bezeichnung, fuer_benutzer_id } = req.body as {
|
||||
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
|
||||
notizen?: string;
|
||||
bezeichnung?: string;
|
||||
fuer_benutzer_id?: string;
|
||||
};
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
@@ -150,7 +153,19 @@ class AusruestungsanfrageController {
|
||||
}
|
||||
}
|
||||
|
||||
const request = await ausruestungsanfrageService.createRequest(req.user!.id, items, notizen);
|
||||
// Determine anfrager: self or on behalf of another user
|
||||
let anfragerId = req.user!.id;
|
||||
if (fuer_benutzer_id && fuer_benutzer_id !== req.user!.id) {
|
||||
const groups = req.user?.groups ?? [];
|
||||
const canOrderForUser = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:order_for_user');
|
||||
if (!canOrderForUser) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung für Bestellung im Auftrag' });
|
||||
return;
|
||||
}
|
||||
anfragerId = fuer_benutzer_id;
|
||||
}
|
||||
|
||||
const request = await ausruestungsanfrageService.createRequest(anfragerId, items, notizen, bezeichnung);
|
||||
res.status(201).json({ success: true, data: request });
|
||||
} catch (error) {
|
||||
logger.error('AusruestungsanfrageController.createRequest error', { error });
|
||||
@@ -158,6 +173,56 @@ class AusruestungsanfrageController {
|
||||
}
|
||||
}
|
||||
|
||||
async updateRequest(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const { bezeichnung, notizen, items } = req.body as {
|
||||
bezeichnung?: string;
|
||||
notizen?: string;
|
||||
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
|
||||
};
|
||||
|
||||
// Validate items if provided
|
||||
if (items) {
|
||||
if (items.length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Mindestens eine Position ist erforderlich' });
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!item.bezeichnung || item.bezeichnung.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Bezeichnung ist für alle Positionen erforderlich' });
|
||||
return;
|
||||
}
|
||||
if (!item.menge || item.menge < 1) {
|
||||
res.status(400).json({ success: false, message: 'Menge muss mindestens 1 sein' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await ausruestungsanfrageService.getRequestById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permission: owner + status=offen, OR ausruestungsanfrage:edit
|
||||
const groups = req.user?.groups ?? [];
|
||||
const canEditAny = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:edit');
|
||||
const isOwner = existing.anfrager_id === req.user!.id;
|
||||
if (!canEditAny && !(isOwner && existing.status === 'offen')) {
|
||||
res.status(403).json({ success: false, message: 'Keine Berechtigung zum Bearbeiten dieser Anfrage' });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await ausruestungsanfrageService.updateRequest(id, { bezeichnung, notizen, items });
|
||||
res.status(200).json({ success: true, data: updated });
|
||||
} catch (error) {
|
||||
logger.error('AusruestungsanfrageController.updateRequest error', { error });
|
||||
res.status(500).json({ success: false, message: 'Anfrage konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateRequestStatus(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Migration 047: Update Ausrüstungsanfrage (Internal Orders) system
|
||||
-- - Add bezeichnung column to ausruestung_anfragen
|
||||
-- - Rename permissions: approve_requests → approve, view_overview → view_all
|
||||
-- - Add new permission: ausruestungsanfrage:edit
|
||||
|
||||
-- 1. Add bezeichnung column to anfragen table
|
||||
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bezeichnung TEXT;
|
||||
|
||||
-- 2. Rename permissions
|
||||
UPDATE permissions SET name = 'ausruestungsanfrage:approve'
|
||||
WHERE name = 'ausruestungsanfrage:approve_requests';
|
||||
|
||||
UPDATE permissions SET name = 'ausruestungsanfrage:view_all'
|
||||
WHERE name = 'ausruestungsanfrage:view_overview';
|
||||
|
||||
-- 3. Add new edit permission
|
||||
INSERT INTO permissions (name, beschreibung, feature_group)
|
||||
VALUES ('ausruestungsanfrage:edit', 'Alle Anfragen bearbeiten (unabhängig von Status/Besitzer)', 'ausruestungsanfrage')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- 4. Grant new edit permission to groups that had approve_requests (now approve)
|
||||
INSERT INTO group_permissions (group_name, permission_name)
|
||||
SELECT gp.group_name, 'ausruestungsanfrage:edit'
|
||||
FROM group_permissions gp
|
||||
WHERE gp.permission_name = 'ausruestungsanfrage:approve'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -21,18 +21,19 @@ router.get('/categories', authenticate, requirePermission('ausruestungsanfrage:v
|
||||
// Overview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:view_overview'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
|
||||
router.get('/overview', authenticate, requirePermission('ausruestungsanfrage:view_all'), ausruestungsanfrageController.getOverview.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
router.get('/requests', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.getRequests.bind(ausruestungsanfrageController));
|
||||
router.get('/requests', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.getRequests.bind(ausruestungsanfrageController));
|
||||
router.get('/requests/my', authenticate, ausruestungsanfrageController.getMyRequests.bind(ausruestungsanfrageController));
|
||||
router.get('/requests/:id', authenticate, ausruestungsanfrageController.getRequestById.bind(ausruestungsanfrageController));
|
||||
router.post('/requests', authenticate, requirePermission('ausruestungsanfrage:create_request'), ausruestungsanfrageController.createRequest.bind(ausruestungsanfrageController));
|
||||
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
|
||||
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve_requests'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
|
||||
router.patch('/requests/:id', authenticate, ausruestungsanfrageController.updateRequest.bind(ausruestungsanfrageController));
|
||||
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
|
||||
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linking requests to orders
|
||||
|
||||
@@ -189,7 +189,7 @@ async function getRequestById(id: number) {
|
||||
return {
|
||||
...reqResult.rows[0],
|
||||
positionen: positionen.rows,
|
||||
bestellungen: bestellungen.rows,
|
||||
linked_bestellungen: bestellungen.rows,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ async function createRequest(
|
||||
userId: string,
|
||||
items: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[],
|
||||
notizen?: string,
|
||||
bezeichnung?: string,
|
||||
) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -213,10 +214,10 @@ async function createRequest(
|
||||
const nextNr = maxResult.rows[0].next_nr;
|
||||
|
||||
const anfrageResult = await client.query(
|
||||
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bestell_nummer, bestell_jahr)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[userId, notizen || null, nextNr, currentYear],
|
||||
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
|
||||
);
|
||||
const anfrage = anfrageResult.rows[0];
|
||||
|
||||
@@ -252,6 +253,74 @@ async function createRequest(
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRequest(
|
||||
id: number,
|
||||
data: {
|
||||
bezeichnung?: string;
|
||||
notizen?: string;
|
||||
items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string }[];
|
||||
},
|
||||
) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Update anfrage fields
|
||||
const fields: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (data.bezeichnung !== undefined) {
|
||||
params.push(data.bezeichnung || null);
|
||||
fields.push(`bezeichnung = $${params.length}`);
|
||||
}
|
||||
if (data.notizen !== undefined) {
|
||||
params.push(data.notizen || null);
|
||||
fields.push(`notizen = $${params.length}`);
|
||||
}
|
||||
|
||||
if (fields.length > 0) {
|
||||
params.push(new Date());
|
||||
fields.push(`aktualisiert_am = $${params.length}`);
|
||||
params.push(id);
|
||||
await client.query(
|
||||
`UPDATE ausruestung_anfragen SET ${fields.join(', ')} WHERE id = $${params.length}`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
// Replace items if provided
|
||||
if (data.items) {
|
||||
await client.query('DELETE FROM ausruestung_anfrage_positionen WHERE anfrage_id = $1', [id]);
|
||||
for (const item of data.items) {
|
||||
let bezeichnung = item.bezeichnung;
|
||||
if (item.artikel_id) {
|
||||
const artikelResult = await client.query(
|
||||
'SELECT bezeichnung FROM ausruestung_artikel WHERE id = $1',
|
||||
[item.artikel_id],
|
||||
);
|
||||
if (artikelResult.rows.length > 0) {
|
||||
bezeichnung = artikelResult.rows[0].bezeichnung;
|
||||
}
|
||||
}
|
||||
await client.query(
|
||||
`INSERT INTO ausruestung_anfrage_positionen (anfrage_id, artikel_id, bezeichnung, menge, notizen)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[id, item.artikel_id || null, bezeichnung, item.menge, item.notizen || null],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return getRequestById(id);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('ausruestungsanfrageService.updateRequest failed', { error });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRequestStatus(
|
||||
id: number,
|
||||
status: string,
|
||||
@@ -353,6 +422,7 @@ export default {
|
||||
getMyRequests,
|
||||
getRequestById,
|
||||
createRequest,
|
||||
updateRequest,
|
||||
updateRequestStatus,
|
||||
deleteRequest,
|
||||
linkToOrder,
|
||||
|
||||
@@ -101,7 +101,7 @@ const PERMISSION_SUB_GROUPS: Record<string, Record<string, string[]>> = {
|
||||
},
|
||||
ausruestungsanfrage: {
|
||||
'Katalog': ['view', 'manage_catalog'],
|
||||
'Anfragen': ['create_request', 'approve_requests', 'link_orders', 'view_overview', 'order_for_user'],
|
||||
'Anfragen': ['create_request', 'approve', 'link_orders', 'view_all', 'order_for_user', 'edit'],
|
||||
'Widget': ['widget'],
|
||||
},
|
||||
admin: {
|
||||
|
||||
@@ -20,7 +20,7 @@ function AusruestungsanfrageWidget() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>Ausrüstungsanfragen</Typography>
|
||||
<Typography variant="h6" gutterBottom>Interne Bestellungen</Typography>
|
||||
<Skeleton variant="rectangular" height={60} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -31,7 +31,7 @@ function AusruestungsanfrageWidget() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>Ausrüstungsanfragen</Typography>
|
||||
<Typography variant="h6" gutterBottom>Interne Bestellungen</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Anfragen konnten nicht geladen werden.
|
||||
</Typography>
|
||||
@@ -46,7 +46,7 @@ function AusruestungsanfrageWidget() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>Ausrüstungsanfragen</Typography>
|
||||
<Typography variant="h6" gutterBottom>Interne Bestellungen</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'text.secondary' }}>
|
||||
<Build fontSize="small" />
|
||||
<Typography variant="body2">Keine offenen Anfragen</Typography>
|
||||
@@ -60,7 +60,7 @@ function AusruestungsanfrageWidget() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="h6">Ausrüstungsanfragen</Typography>
|
||||
<Typography variant="h6">Interne Bestellungen</Typography>
|
||||
<Chip label={`${pendingCount} offen`} size="small" color="warning" />
|
||||
</Box>
|
||||
<List dense disablePadding>
|
||||
|
||||
@@ -121,7 +121,7 @@ const baseNavigationItems: NavigationItem[] = [
|
||||
permission: 'bestellungen:view',
|
||||
},
|
||||
{
|
||||
text: 'Ausrüstungsanfragen',
|
||||
text: 'Interne Bestellungen',
|
||||
icon: <Build />,
|
||||
path: '/ausruestungsanfrage',
|
||||
// subItems computed dynamically in navigationItems useMemo
|
||||
@@ -189,8 +189,8 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
||||
const ausruestungSubItems: SubItem[] = [];
|
||||
let ausruestungTabIdx = 0;
|
||||
if (hasPermission('ausruestungsanfrage:create_request')) { ausruestungSubItems.push({ text: 'Meine Anfragen', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
||||
if (hasPermission('ausruestungsanfrage:approve_requests')) { ausruestungSubItems.push({ text: 'Alle Anfragen', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
||||
if (hasPermission('ausruestungsanfrage:view_overview')) { ausruestungSubItems.push({ text: 'Übersicht', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
||||
if (hasPermission('ausruestungsanfrage:approve')) { ausruestungSubItems.push({ text: 'Alle Anfragen', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
||||
if (hasPermission('ausruestungsanfrage:view_all')) { ausruestungSubItems.push({ text: 'Übersicht', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` }); ausruestungTabIdx++; }
|
||||
ausruestungSubItems.push({ text: 'Katalog', path: `/ausruestungsanfrage?tab=${ausruestungTabIdx}` });
|
||||
|
||||
// Build Issues sub-items dynamically (tab order must match Issues.tsx)
|
||||
|
||||
@@ -14,7 +14,7 @@ export const WIDGETS = [
|
||||
{ key: 'adminStatus', label: 'Admin Status', defaultVisible: true },
|
||||
{ key: 'links', label: 'Links', defaultVisible: true },
|
||||
{ key: 'bestellungen', label: 'Bestellungen', defaultVisible: true },
|
||||
{ key: 'ausruestungsanfragen', label: 'Ausrüstungsanfragen', defaultVisible: true },
|
||||
{ key: 'ausruestungsanfragen', label: 'Interne Bestellungen', defaultVisible: true },
|
||||
] as const;
|
||||
|
||||
export type WidgetKey = typeof WIDGETS[number]['key'];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,8 +52,20 @@ export const ausruestungsanfrageApi = {
|
||||
const r = await api.get(`/api/ausruestungsanfragen/requests/${id}`);
|
||||
return r.data.data;
|
||||
},
|
||||
createRequest: async (items: AusruestungAnfrageFormItem[], notizen?: string): Promise<AusruestungAnfrage> => {
|
||||
const r = await api.post('/api/ausruestungsanfragen/requests', { items, notizen });
|
||||
createRequest: async (
|
||||
items: AusruestungAnfrageFormItem[],
|
||||
notizen?: string,
|
||||
bezeichnung?: string,
|
||||
fuer_benutzer_id?: string,
|
||||
): Promise<AusruestungAnfrage> => {
|
||||
const r = await api.post('/api/ausruestungsanfragen/requests', { items, notizen, bezeichnung, fuer_benutzer_id });
|
||||
return r.data.data;
|
||||
},
|
||||
updateRequest: async (
|
||||
id: number,
|
||||
data: { bezeichnung?: string; notizen?: string; items?: AusruestungAnfrageFormItem[] },
|
||||
): Promise<AusruestungAnfrageDetailResponse> => {
|
||||
const r = await api.patch(`/api/ausruestungsanfragen/requests/${id}`, data);
|
||||
return r.data.data;
|
||||
},
|
||||
updateRequestStatus: async (id: number, status: string, admin_notizen?: string): Promise<AusruestungAnfrage> => {
|
||||
@@ -77,4 +89,10 @@ export const ausruestungsanfrageApi = {
|
||||
const r = await api.get('/api/ausruestungsanfragen/overview');
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
// ── Users ──
|
||||
getOrderUsers: async (): Promise<Array<{ id: string; name: string }>> => {
|
||||
const r = await api.get('/api/permissions/users-with', { params: { permission: 'ausruestungsanfrage:create_request' } });
|
||||
return r.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface AusruestungAnfrage {
|
||||
id: number;
|
||||
anfrager_id: string;
|
||||
anfrager_name?: string;
|
||||
bezeichnung?: string;
|
||||
status: AusruestungAnfrageStatus;
|
||||
notizen?: string;
|
||||
admin_notizen?: string;
|
||||
|
||||
Reference in New Issue
Block a user