rework internal order system
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
-- Migration 050: Add missing columns to ausruestung_anfragen
|
||||||
|
-- These columns are referenced by the service code but were never created
|
||||||
|
|
||||||
|
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bearbeitet_am TIMESTAMPTZ;
|
||||||
|
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bestell_nummer INT;
|
||||||
|
ALTER TABLE ausruestung_anfragen ADD COLUMN IF NOT EXISTS bestell_jahr INT;
|
||||||
|
|
||||||
|
-- Backfill bestell_nummer for existing rows
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
yr INT;
|
||||||
|
rec RECORD;
|
||||||
|
nr INT;
|
||||||
|
BEGIN
|
||||||
|
FOR yr IN SELECT DISTINCT EXTRACT(YEAR FROM erstellt_am)::int FROM ausruestung_anfragen LOOP
|
||||||
|
nr := 0;
|
||||||
|
FOR rec IN SELECT id FROM ausruestung_anfragen WHERE EXTRACT(YEAR FROM erstellt_am)::int = yr AND bestell_nummer IS NULL ORDER BY erstellt_am LOOP
|
||||||
|
nr := nr + 1;
|
||||||
|
UPDATE ausruestung_anfragen SET bestell_nummer = nr, bestell_jahr = yr WHERE id = rec.id;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
@@ -393,21 +393,36 @@ async function createRequest(
|
|||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const maxResult = await client.query(
|
|
||||||
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
|
|
||||||
FROM ausruestung_anfragen
|
|
||||||
WHERE bestell_jahr = $1`,
|
|
||||||
[currentYear],
|
|
||||||
);
|
|
||||||
const nextNr = maxResult.rows[0].next_nr;
|
|
||||||
|
|
||||||
const anfrageResult = await client.query(
|
// Try with bestell_nummer/bestell_jahr (migration 050), fallback without
|
||||||
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
|
let anfrage: Record<string, unknown>;
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
try {
|
||||||
RETURNING *`,
|
await client.query('SAVEPOINT sp_bestell_nr');
|
||||||
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
|
const maxResult = await client.query(
|
||||||
);
|
`SELECT COALESCE(MAX(bestell_nummer), 0) + 1 AS next_nr
|
||||||
const anfrage = anfrageResult.rows[0];
|
FROM ausruestung_anfragen
|
||||||
|
WHERE bestell_jahr = $1`,
|
||||||
|
[currentYear],
|
||||||
|
);
|
||||||
|
const nextNr = maxResult.rows[0].next_nr;
|
||||||
|
const anfrageResult = await client.query(
|
||||||
|
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung, bestell_nummer, bestell_jahr)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING *`,
|
||||||
|
[userId, notizen || null, bezeichnung || null, nextNr, currentYear],
|
||||||
|
);
|
||||||
|
await client.query('RELEASE SAVEPOINT sp_bestell_nr');
|
||||||
|
anfrage = anfrageResult.rows[0];
|
||||||
|
} catch {
|
||||||
|
await client.query('ROLLBACK TO SAVEPOINT sp_bestell_nr');
|
||||||
|
const anfrageResult = await client.query(
|
||||||
|
`INSERT INTO ausruestung_anfragen (anfrager_id, notizen, bezeichnung)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *`,
|
||||||
|
[userId, notizen || null, bezeichnung || null],
|
||||||
|
);
|
||||||
|
anfrage = anfrageResult.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let itemBezeichnung = item.bezeichnung;
|
let itemBezeichnung = item.bezeichnung;
|
||||||
@@ -566,17 +581,34 @@ async function updateRequestStatus(
|
|||||||
adminNotizen?: string,
|
adminNotizen?: string,
|
||||||
bearbeitetVon?: string,
|
bearbeitetVon?: string,
|
||||||
) {
|
) {
|
||||||
const result = await pool.query(
|
// Use aktualisiert_am (always exists) + try bearbeitet_am (added in migration 050)
|
||||||
`UPDATE ausruestung_anfragen
|
try {
|
||||||
SET status = $1,
|
const result = await pool.query(
|
||||||
admin_notizen = COALESCE($2, admin_notizen),
|
`UPDATE ausruestung_anfragen
|
||||||
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
SET status = $1,
|
||||||
bearbeitet_am = NOW()
|
admin_notizen = COALESCE($2, admin_notizen),
|
||||||
WHERE id = $4
|
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
||||||
RETURNING *`,
|
bearbeitet_am = NOW(),
|
||||||
[status, adminNotizen || null, bearbeitetVon || null, id],
|
aktualisiert_am = NOW()
|
||||||
);
|
WHERE id = $4
|
||||||
return result.rows[0] || null;
|
RETURNING *`,
|
||||||
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
} catch {
|
||||||
|
// Fallback if bearbeitet_am column doesn't exist yet (migration 050 not run)
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE ausruestung_anfragen
|
||||||
|
SET status = $1,
|
||||||
|
admin_notizen = COALESCE($2, admin_notizen),
|
||||||
|
bearbeitet_von = COALESCE($3, bearbeitet_von),
|
||||||
|
aktualisiert_am = NOW()
|
||||||
|
WHERE id = $4
|
||||||
|
RETURNING *`,
|
||||||
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRequest(id: number) {
|
async function deleteRequest(id: number) {
|
||||||
|
|||||||
@@ -134,8 +134,10 @@ class CleanupService {
|
|||||||
}
|
}
|
||||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
|
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM bestellungen');
|
||||||
const count = rows[0].count;
|
const count = rows[0].count;
|
||||||
|
// Delete related linking tables first, then main table
|
||||||
|
await pool.query('DELETE FROM ausruestung_anfrage_bestellung WHERE bestellung_id IN (SELECT id FROM bestellungen)');
|
||||||
await pool.query('TRUNCATE bestellungen CASCADE');
|
await pool.query('TRUNCATE bestellungen CASCADE');
|
||||||
await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1');
|
try { await pool.query('ALTER SEQUENCE bestellungen_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ }
|
||||||
logger.info(`Cleanup: truncated bestellungen (${count} rows) and reset sequence`);
|
logger.info(`Cleanup: truncated bestellungen (${count} rows) and reset sequence`);
|
||||||
return { count, deleted: true };
|
return { count, deleted: true };
|
||||||
}
|
}
|
||||||
@@ -147,8 +149,10 @@ class CleanupService {
|
|||||||
}
|
}
|
||||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
|
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM ausruestung_anfragen');
|
||||||
const count = rows[0].count;
|
const count = rows[0].count;
|
||||||
|
// Delete linking table first
|
||||||
|
await pool.query('DELETE FROM ausruestung_anfrage_bestellung WHERE anfrage_id IN (SELECT id FROM ausruestung_anfragen)');
|
||||||
await pool.query('TRUNCATE ausruestung_anfragen CASCADE');
|
await pool.query('TRUNCATE ausruestung_anfragen CASCADE');
|
||||||
await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1');
|
try { await pool.query('ALTER SEQUENCE ausruestung_anfragen_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ }
|
||||||
logger.info(`Cleanup: truncated ausruestung_anfragen (${count} rows) and reset sequence`);
|
logger.info(`Cleanup: truncated ausruestung_anfragen (${count} rows) and reset sequence`);
|
||||||
return { count, deleted: true };
|
return { count, deleted: true };
|
||||||
}
|
}
|
||||||
@@ -161,7 +165,7 @@ class CleanupService {
|
|||||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
|
const { rows } = await pool.query('SELECT COUNT(*)::int AS count FROM issues');
|
||||||
const count = rows[0].count;
|
const count = rows[0].count;
|
||||||
await pool.query('TRUNCATE issues CASCADE');
|
await pool.query('TRUNCATE issues CASCADE');
|
||||||
await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1');
|
try { await pool.query('ALTER SEQUENCE issues_id_seq RESTART WITH 1'); } catch { /* sequence may not exist */ }
|
||||||
logger.info(`Cleanup: truncated issues (${count} rows) and reset sequence`);
|
logger.info(`Cleanup: truncated issues (${count} rows) and reset sequence`);
|
||||||
return { count, deleted: true };
|
return { count, deleted: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -518,39 +518,74 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
|||||||
) : (
|
) : (
|
||||||
/* ── View Mode ── */
|
/* ── View Mode ── */
|
||||||
<>
|
<>
|
||||||
{anfrage!.anfrager_name && (
|
{/* Meta info */}
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
|
||||||
Anfrager: {anfrage!.anfrager_name}
|
{anfrage!.anfrager_name && (
|
||||||
</Typography>
|
<Box>
|
||||||
)}
|
<Typography variant="caption" color="text.secondary">Anfrager</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={500}>{anfrage!.anfrager_name}</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">Erstellt am</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={500}>{new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')}</Typography>
|
||||||
|
</Box>
|
||||||
|
{anfrage!.bearbeitet_von_name && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary">Bearbeitet von</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={500}>{anfrage!.bearbeitet_von_name}</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{anfrage!.notizen && (
|
{anfrage!.notizen && (
|
||||||
<Typography variant="body2">Notizen: {anfrage!.notizen}</Typography>
|
<Paper variant="outlined" sx={{ p: 1.5, bgcolor: 'action.hover' }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 0.5 }}>Notizen</Typography>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{anfrage!.notizen}</Typography>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
{anfrage!.admin_notizen && (
|
{anfrage!.admin_notizen && (
|
||||||
<Typography variant="body2">Admin Notizen: {anfrage!.admin_notizen}</Typography>
|
<Paper variant="outlined" sx={{ p: 1.5, bgcolor: 'warning.main', color: 'warning.contrastText', borderColor: 'warning.dark' }}>
|
||||||
|
<Typography variant="caption" display="block" sx={{ mb: 0.5, opacity: 0.8 }}>Admin Notizen</Typography>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{anfrage!.admin_notizen}</Typography>
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Erstellt am: {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Typography variant="subtitle2">Positionen</Typography>
|
|
||||||
{detail.positionen.map(p => (
|
{/* Positionen */}
|
||||||
<Box key={p.id}>
|
<Typography variant="subtitle2">Positionen ({detail.positionen.length})</Typography>
|
||||||
<Typography variant="body2">
|
<Table size="small">
|
||||||
- {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''}
|
<TableHead>
|
||||||
</Typography>
|
<TableRow>
|
||||||
{p.eigenschaften && p.eigenschaften.length > 0 && (
|
<TableCell>Artikel</TableCell>
|
||||||
<Box sx={{ ml: 2, mt: 0.25 }}>
|
<TableCell align="right">Menge</TableCell>
|
||||||
{p.eigenschaften.map(e => (
|
<TableCell>Details</TableCell>
|
||||||
<Typography key={e.eigenschaft_id} variant="caption" color="text.secondary" display="block">
|
</TableRow>
|
||||||
{e.eigenschaft_name}: {e.wert}
|
</TableHead>
|
||||||
</Typography>
|
<TableBody>
|
||||||
))}
|
{detail.positionen.map(p => (
|
||||||
</Box>
|
<TableRow key={p.id}>
|
||||||
)}
|
<TableCell>
|
||||||
</Box>
|
<Typography variant="body2" fontWeight={500}>{p.bezeichnung}</Typography>
|
||||||
))}
|
{p.eigenschaften && p.eigenschaften.length > 0 && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||||
|
{p.eigenschaften.map(e => (
|
||||||
|
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Typography variant="body2" fontWeight={600}>{p.menge}x</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{p.notizen && <Typography variant="caption" color="text.secondary">{p.notizen}</Typography>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
{detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && (
|
{detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && (
|
||||||
<>
|
<>
|
||||||
@@ -1252,14 +1287,14 @@ function MeineAnfragenTab() {
|
|||||||
function AlleAnfragenTab() {
|
function AlleAnfragenTab() {
|
||||||
const { hasPermission } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
const [statusFilter, setStatusFilter] = useState<string>('alle');
|
||||||
const [detailId, setDetailId] = useState<number | null>(null);
|
const [detailId, setDetailId] = useState<number | null>(null);
|
||||||
|
|
||||||
const canEditAny = hasPermission('ausruestungsanfrage:edit');
|
const canEditAny = hasPermission('ausruestungsanfrage:edit');
|
||||||
|
|
||||||
const { data: requests = [], isLoading: requestsLoading, isError: requestsError } = useQuery({
|
const { data: requests = [], isLoading: requestsLoading, isError: requestsError } = useQuery({
|
||||||
queryKey: ['ausruestungsanfrage', 'allRequests', statusFilter],
|
queryKey: ['ausruestungsanfrage', 'allRequests', statusFilter],
|
||||||
queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter ? { status: statusFilter } : undefined),
|
queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter !== 'alle' ? { status: statusFilter } : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: overview } = useQuery<AusruestungOverview>({
|
const { data: overview } = useQuery<AusruestungOverview>({
|
||||||
@@ -1305,7 +1340,7 @@ function AlleAnfragenTab() {
|
|||||||
onChange={e => setStatusFilter(e.target.value)}
|
onChange={e => setStatusFilter(e.target.value)}
|
||||||
sx={{ minWidth: 200, mb: 2 }}
|
sx={{ minWidth: 200, mb: 2 }}
|
||||||
>
|
>
|
||||||
<MenuItem value="">Alle</MenuItem>
|
<MenuItem value="alle">Alle</MenuItem>
|
||||||
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
|
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
|
||||||
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
|
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user