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:
@@ -6,6 +6,7 @@ import * as creditNoteService from '../services/creditNote.service.js';
|
||||
import { effectiveNumber } from '../services/creditNote.service.js';
|
||||
import * as numberRangeService from '../services/creditNoteNumberRange.service.js';
|
||||
import * as deliveryRangeService from '../services/deliveryNoteNumberRange.service.js';
|
||||
import * as authorizationService from '../services/authorization.service.js';
|
||||
import { generateCreditNotePdf } from '../services/creditNotePdf.service.js';
|
||||
|
||||
// Gutschriften sind ein reiner Mitarbeiter-/Admin-Bereich (interne
|
||||
@@ -42,6 +43,43 @@ function handleError(res: Response, error: unknown, fallback: string) {
|
||||
} as ApiResponse);
|
||||
}
|
||||
|
||||
// ---- Gesamtübersicht (Hauptmenü) ----
|
||||
// Anders als die übrigen Credit-Note-Endpunkte NICHT staff-only: Portal-Kunden
|
||||
// dürfen ihre eigenen (+ vertretene) Belege sehen. Scoping über customerIds
|
||||
// aus dem JWT – eine im Body/Query mitgeschickte customerId hat keinen Effekt.
|
||||
export async function listAll(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
// Portal-User: nur eigene + vertretene Kunden MIT Vollmacht.
|
||||
let customerIds: number[] | undefined;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
customerIds = [req.user.customerId];
|
||||
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
||||
for (const repCustId of representedIds) {
|
||||
if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) {
|
||||
customerIds.push(repCustId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const page = parseInt((req.query.page as string) || '1') || 1;
|
||||
const limit = Math.min(parseInt((req.query.limit as string) || '50') || 50, 200);
|
||||
const search = typeof req.query.search === 'string' ? req.query.search : undefined;
|
||||
|
||||
const result = await creditNoteService.getAllCreditNotes({ customerIds, page, limit, search });
|
||||
|
||||
// Portal-Kunden dürfen keine Überweisungsbelege laden → receiptPath aus der
|
||||
// Response entfernen (Beleg-Download bleibt ohnehin staff-only).
|
||||
const isPortal = !!req.user?.isCustomerPortal;
|
||||
const items = isPortal
|
||||
? result.items.map((cn) => ({ ...cn, receiptPath: null }))
|
||||
: result.items;
|
||||
|
||||
res.json({ success: true, data: { items, pagination: result.pagination } } as ApiResponse);
|
||||
} catch (error) {
|
||||
handleError(res, error, 'Fehler beim Laden der Belegübersicht');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Gutschriften pro Vertrag ----
|
||||
|
||||
export async function listByContract(req: AuthRequest, res: Response): Promise<void> {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Gesamtübersicht aller Belege (Hauptmenü) – portal-scoped (nicht staff-only).
|
||||
// VOR /:id, damit die Wurzel nicht als ID interpretiert wird.
|
||||
router.get('/', authenticate, requirePermission('contracts:read'), creditNoteController.listAll);
|
||||
|
||||
// Nummernkreis-Verwaltung (Einstellungen). VOR /:id, damit "number-range"
|
||||
// nicht als ID interpretiert wird.
|
||||
router.get('/number-range', authenticate, requirePermission('settings:read'), creditNoteController.getNumberRange);
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user