Hauptmenue: Gutschriften/Lieferscheine-Gesamtuebersicht (portal-scoped)

Neuer Menuepunkt 'Gutschriften' -> Seite /credit-notes mit Tabelle
aller Belege (Beleg-Nr, Art, Kunde, Vertrag, Betrag, Datum, PDF),
Suche + Pagination.

Neuer Endpoint GET /credit-notes (NICHT staff-only wie die uebrigen
Credit-Note-Endpoints): Staff sieht alle Belege aller Kunden, Portal-
Kunden nur eigene + vertretene (Vollmacht via hasAuthorization).
customerIds kommt aus dem JWT, nicht aus Query/Body -> nicht
manipulierbar. Fuer Portal wird receiptPath aus der Response entfernt
(Belege bleiben staff-only). Route requirePermission contracts:read.

Verifiziert: Staff -> alle Belege; Portal-scoped -> nur eigene, korrekt
zugeordnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 10:23:53 +02:00
co-authored by Claude Opus 4.8
parent bbe1aed230
commit 48e65be91c
9 changed files with 281 additions and 2 deletions
+54 -1
View File
@@ -7,7 +7,7 @@ import { ApiError } from '../utils/apiError.js';
import { assignNextNumber } from './creditNoteNumberRange.service.js';
import { assignNextNumber as assignNextDeliveryNoteNumber } from './deliveryNoteNumberRange.service.js';
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
import { CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';
import { Prisma, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
@@ -180,6 +180,59 @@ export async function getCreditNotesByContract(contractId: number) {
});
}
// Gesamtübersicht aller Belege (Gutschriften + Lieferscheine). `customerIds`
// scoped die Liste (Portal: eigene + vertretene Kunden); ohne = alle (Staff).
export async function getAllCreditNotes(opts: {
customerIds?: number[];
page?: number;
limit?: number;
search?: string;
}) {
const { customerIds, page = 1, limit = 50, search } = opts;
const skip = (Math.max(page, 1) - 1) * limit;
const where: Prisma.CreditNoteWhereInput = {};
if (customerIds) {
where.contract = { customerId: { in: customerIds } };
}
if (search && search.trim()) {
const s = search.trim();
where.OR = [
{ number: { contains: s } },
{ deliveryNoteNumber: { contains: s } },
{ sachwertDescription: { contains: s } },
{ contract: { contractNumber: { contains: s } } },
{ contract: { customer: { customerNumber: { contains: s } } } },
{ contract: { customer: { lastName: { contains: s } } } },
{ contract: { customer: { companyName: { contains: s } } } },
];
}
const [items, total] = await Promise.all([
prisma.creditNote.findMany({
where,
include: {
contract: {
select: {
id: true,
contractNumber: true,
type: true,
customer: {
select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true },
},
},
},
},
orderBy: { createdAt: 'desc' },
skip,
take: limit,
}),
prisma.creditNote.count({ where }),
]);
return { items, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };
}
export async function getCreditNoteById(id: number) {
return prisma.creditNote.findUnique({ where: { id } });
}