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:
2026-08-06 13:15:02 +02:00
co-authored by Claude Opus 4.8
parent 3580c51cb1
commit 9e38cb32f0
10 changed files with 804 additions and 6 deletions
+49 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, CustomerReferrals, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, CustomerReferrals, CreditNote, CreditNoteDefaults, CreditNoteNumberRange, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
// ============================================================================
// In-Memory-Token-Store
@@ -263,6 +263,54 @@ export const referralApi = {
},
};
// Gutschriften (Subventionen am Vertrag)
export const creditNoteApi = {
listByContract: async (contractId: number) => {
const res = await api.get<ApiResponse<CreditNote[]>>(`/contracts/${contractId}/credit-notes`);
return res.data;
},
getDefaults: async (contractId: number) => {
const res = await api.get<ApiResponse<CreditNoteDefaults>>(`/contracts/${contractId}/credit-notes/defaults`);
return res.data;
},
create: async (contractId: number, payload: Record<string, unknown>) => {
const res = await api.post<ApiResponse<CreditNote>>(`/contracts/${contractId}/credit-notes`, payload);
return res.data;
},
get: async (id: number) => {
const res = await api.get<ApiResponse<CreditNote>>(`/credit-notes/${id}`);
return res.data;
},
update: async (id: number, payload: Record<string, unknown>) => {
const res = await api.put<ApiResponse<CreditNote>>(`/credit-notes/${id}`, payload);
return res.data;
},
remove: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/credit-notes/${id}`);
return res.data;
},
getNumberRange: async () => {
const res = await api.get<ApiResponse<CreditNoteNumberRange>>(`/credit-notes/number-range`);
return res.data;
},
updateNumberRange: async (payload: Partial<CreditNoteNumberRange>) => {
const res = await api.put<ApiResponse<CreditNoteNumberRange>>(`/credit-notes/number-range`, payload);
return res.data;
},
uploadReceipt: async (id: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ receiptPath: string }>>(`/upload/credit-notes/${id}/receipt`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteReceipt: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/credit-notes/${id}/receipt`);
return res.data;
},
};
// Addresses
export const addressApi = {
getByCustomer: async (customerId: number) => {