Gutschriften Phase 2b: Vertrags-UI + Beleg-Upload + Nummernkreis-Seite
Frontend: - CreditNotesSection im Vertragsdetail (eigene Section wie Rechnungen): Liste + Anlegen/Bearbeiten (Modal). Geld/Sachwert mit passenden Feldern, USt-Block mit Live-Vorschau Netto/USt/Brutto, Datum, Ort, Unterschrift, 'Ware erhalten'. Loeschen mit Bestaetigung. - Ueberweisungsbeleg-Upload/-Anzeige/-Entfernen (nur Geld-Gutschriften). - Nummernkreis-Verwaltung als Settings-Seite mit Live-Vorschau der naechsten Nummer. - Frontend-Typen (CreditNote etc.) + creditNoteApi. Backend: - Ueberweisungsbeleg-Upload-Route /upload/credit-notes/:id/receipt (+ DELETE), Portal geblockt, Ownership ueber Vertrag. - fileDownload: Ownership-Aufloesung fuer credit-note-receipts + credit-notes (Vertrag) ergaenzt. Als Section statt Tab-Balken, weil die Vertragsansicht kartenbasiert ist (kein bestehender Tab-Balken); spaeter umbaubar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -946,4 +946,87 @@ router.delete(
|
||||
}
|
||||
);
|
||||
|
||||
// Upload für Gutschrift-Überweisungsbeleg. Interner Bereich → Portal blocken.
|
||||
router.post(
|
||||
'/credit-notes/:id/receipt',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('credit-note-receipts'),
|
||||
upload.single('document'),
|
||||
validateUploadedFile,
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user?.isCustomerPortal) {
|
||||
cleanupFile(req.file?.path);
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
const id = parseInt(req.params.id);
|
||||
const cn = await prisma.creditNote.findUnique({
|
||||
where: { id },
|
||||
select: { contractId: true, receiptPath: true },
|
||||
});
|
||||
if (!cn) {
|
||||
cleanupFile(req.file.path);
|
||||
res.status(404).json({ success: false, error: 'Gutschrift nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
if (!(await canAccessContract(req, res, cn.contractId))) {
|
||||
cleanupFile(req.file.path);
|
||||
return;
|
||||
}
|
||||
const relativePath = `/uploads/credit-note-receipts/${req.file.filename}`;
|
||||
if (cn.receiptPath) cleanupFile(path.join(process.cwd(), cn.receiptPath));
|
||||
await prisma.creditNote.update({ where: { id }, data: { receiptPath: relativePath } });
|
||||
await logChange({
|
||||
req, action: 'UPDATE', resourceType: 'CreditNote', resourceId: id.toString(),
|
||||
label: 'Überweisungsbeleg zur Gutschrift hochgeladen',
|
||||
});
|
||||
res.json({ success: true, data: { receiptPath: relativePath } });
|
||||
} catch (error) {
|
||||
console.error('Credit-note receipt upload error:', error);
|
||||
cleanupFile(req.file?.path);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/credit-notes/:id/receipt',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (req.user?.isCustomerPortal) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff' });
|
||||
return;
|
||||
}
|
||||
const id = parseInt(req.params.id);
|
||||
const cn = await prisma.creditNote.findUnique({
|
||||
where: { id },
|
||||
select: { contractId: true, receiptPath: true },
|
||||
});
|
||||
if (!cn) {
|
||||
res.status(404).json({ success: false, error: 'Gutschrift nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
if (!(await canAccessContract(req, res, cn.contractId))) return;
|
||||
if (cn.receiptPath) cleanupFile(path.join(process.cwd(), cn.receiptPath));
|
||||
await prisma.creditNote.update({ where: { id }, data: { receiptPath: null } });
|
||||
await logChange({
|
||||
req, action: 'UPDATE', resourceType: 'CreditNote', resourceId: id.toString(),
|
||||
label: 'Überweisungsbeleg zur Gutschrift entfernt',
|
||||
});
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Credit-note receipt delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -93,6 +93,19 @@ export async function findUploadOwner(uploadPath: string): Promise<FileOwner | n
|
||||
return r?.contractId ? { kind: 'contract', contractId: r.contractId } : null;
|
||||
}
|
||||
|
||||
case 'credit-note-receipts':
|
||||
case 'credit-notes': {
|
||||
// Überweisungsbeleg bzw. generiertes Gutschrift-PDF – Owner ist der
|
||||
// Vertrag der Gutschrift.
|
||||
const r = await prisma.creditNote.findFirst({
|
||||
where: subDir === 'credit-note-receipts'
|
||||
? { receiptPath: uploadPath }
|
||||
: { pdfPath: uploadPath },
|
||||
select: { contractId: true },
|
||||
});
|
||||
return r ? { kind: 'contract', contractId: r.contractId } : null;
|
||||
}
|
||||
|
||||
case 'cancellation-letters':
|
||||
case 'cancellation-confirmations':
|
||||
case 'cancellation-letters-options':
|
||||
|
||||
Reference in New Issue
Block a user