Files
opencrm/frontend/src/services/api.ts
T
duffyduck 37df8c0c4a Backup-Operations-Log + EBUSY-Fix beim Restore
Backup-Seite zeigt zwei neue Log-Panels: links Backup-Erstellung,
rechts Backup-Wiederherstellung. Jeder Eintrag mit ✓/✗-Status,
Summary, Timestamp + User. Klick öffnet Modal mit vollständigem
Verlauf – alle console.log/error/warn/info-Zeilen werden während
der Operation in einen Puffer mitgefangen und im fullLog-Feld
persistiert. Auto-Refresh alle 5s.

Persistenz: neue Tabelle BackupLog mit Migration
20260519100000_backup_log (CREATE TABLE IF NOT EXISTS für Re-Deploys
auf DBs mit Vorab-db-push). fullLog auf 1 MB gecappt.

Endpoints (settings:update):
- GET /api/settings/backup-logs?operation=CREATE|RESTORE&limit=50
- GET /api/settings/backup-logs/:id

EBUSY-Fix: Der neue Log-Verlauf hat sofort einen alten Bug
sichtbar gemacht. backup.service.restoreBackup rief
deleteDirectory(UPLOADS_DIR) auf, dessen finales rmdirSync auf
/app/uploads ein EBUSY warf – das Verzeichnis ist im Container ein
Bind-Mount und lässt sich nicht aushängen. Fix: neuer Helper
emptyDirectory() löscht nur die Inhalte, das Verzeichnis bleibt
stehen.

Live-verifiziert: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt; Log-Modal zeigt den vollständigen Verlauf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:53:04 +02:00

1945 lines
74 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
// ============================================================================
// In-Memory-Token-Store
// ============================================================================
// Der Access-Token wird BEWUSST nicht in localStorage gespeichert (XSS-Schutz).
// Stattdessen lebt er im Modul-State + wird über den /api/auth/refresh-Endpoint
// nach Page-Reload neu geholt (Refresh-Token sitzt in einem httpOnly-Cookie,
// das JavaScript nie sieht).
let accessToken: string | null = null;
const tokenListeners = new Set<(t: string | null) => void>();
export function setAccessToken(t: string | null): void {
accessToken = t;
tokenListeners.forEach((l) => l(t));
}
export function getAccessToken(): string | null {
return accessToken;
}
export function subscribeToken(listener: (t: string | null) => void): () => void {
tokenListeners.add(listener);
return () => tokenListeners.delete(listener);
}
// ============================================================================
// Axios-Instance
// ============================================================================
const api = axios.create({
baseURL: '/api',
headers: { 'Content-Type': 'application/json' },
// withCredentials: Cookies werden bei same-origin-Requests mitgeschickt.
// Wichtig für den /auth/refresh-Endpoint (liest den refresh_token-Cookie).
withCredentials: true,
});
// Request: Bearer-Header aus dem in-memory-Store
api.interceptors.request.use((config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// Refresh-Retry-Mechanismus für 401-Antworten.
//
// Wenn der Access-Token abgelaufen ist (15-min-Lifetime), antwortet jeder
// API-Aufruf mit 401. Der Interceptor probiert dann einmal /auth/refresh →
// holt neuen Access-Token (Refresh-Token kommt automatisch via httpOnly-Cookie)
// → wiederholt den ursprünglichen Request transparent. Wenn der Refresh selbst
// scheitert (echt abgemeldet / Cookie weg): wir leiten zur Login-Seite um.
//
// Concurrent-Request-Protection: wenn 401 mehrfach parallel kommt, gibt's
// nur einen aktiven refresh-Aufruf; alle wartenden Requests teilen sich das
// Ergebnis.
let refreshInflight: Promise<string | null> | null = null;
async function doRefresh(): Promise<string | null> {
if (refreshInflight) return refreshInflight;
refreshInflight = (async () => {
try {
const res = await axios.post<ApiResponse<{ token: string }>>(
'/api/auth/refresh',
{},
{ withCredentials: true },
);
const newToken = res.data?.data?.token || null;
setAccessToken(newToken);
return newToken;
} catch {
setAccessToken(null);
return null;
} finally {
refreshInflight = null;
}
})();
return refreshInflight;
}
api.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config;
const status = error.response?.status;
const url: string = original?.url || '';
// Auth-Endpoints selbst nicht refreshen sonst Endlos-Schleife
const isAuthEndpoint =
url.includes('/auth/login') ||
url.includes('/auth/customer-login') ||
url.includes('/auth/refresh') ||
url.includes('/auth/logout');
if (status === 401 && !isAuthEndpoint && !original?._retried) {
original._retried = true;
const newToken = await doRefresh();
if (newToken) {
original.headers = original.headers || {};
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
}
// Refresh fehlgeschlagen → echt abmelden + zur Login-Seite
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
window.location.href = '/login';
}
}
const message = error.response?.data?.error || error.message || 'Ein Fehler ist aufgetreten';
return Promise.reject(new Error(message));
},
);
// Auth
export const authApi = {
login: async (email: string, password: string) => {
const res = await api.post<ApiResponse<{ token: string; user: User }>>('/auth/login', { email, password });
return res.data;
},
customerLogin: async (email: string, password: string) => {
const res = await api.post<ApiResponse<{ token: string; user: User }>>('/auth/customer-login', { email, password });
return res.data;
},
me: async () => {
const res = await api.get<ApiResponse<User>>('/auth/me');
return res.data;
},
logout: async () => {
const res = await api.post<ApiResponse<void>>('/auth/logout');
return res.data;
},
changeInitialPortalPassword: async (newPassword: string) => {
const res = await api.post<ApiResponse<void>>('/auth/change-initial-portal-password', { newPassword });
return res.data;
},
// Kurzlebiger Download-Token (60s) für URL-basierte Aufrufe wie
// iframe-PDF-Preview oder window.open mit ?token=. Selbst wenn dieser
// Token in einem Access-Log oder der Browser-History landet, ist er nach
// einer Minute wertlos.
getDownloadToken: async (): Promise<string | null> => {
try {
const res = await api.post<ApiResponse<{ token: string }>>('/auth/download-token');
return res.data?.data?.token || null;
} catch {
return null;
}
},
};
// Customers
export const customerApi = {
getAll: async (params?: { search?: string; type?: string; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<Customer[]>>('/customers', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Customer>>(`/customers/${id}`);
return res.data;
},
create: async (data: Partial<Customer>) => {
const res = await api.post<ApiResponse<Customer>>('/customers', data);
return res.data;
},
update: async (id: number, data: Partial<Customer>) => {
const res = await api.put<ApiResponse<Customer>>(`/customers/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/customers/${id}`);
return res.data;
},
// Portal-Einstellungen
getPortalSettings: async (customerId: number) => {
const res = await api.get<ApiResponse<PortalSettings>>(`/customers/${customerId}/portal`);
return res.data;
},
updatePortalSettings: async (customerId: number, data: { portalEnabled?: boolean; portalEmail?: string | null }) => {
const res = await api.put<ApiResponse<PortalSettings>>(`/customers/${customerId}/portal`, data);
return res.data;
},
setPortalPassword: async (customerId: number, password: string) => {
const res = await api.post<ApiResponse<void>>(`/customers/${customerId}/portal/password`, { password });
return res.data;
},
getPortalPassword: async (customerId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/customers/${customerId}/portal/password`);
return res.data;
},
generatePortalPassword: async (customerId: number) => {
const res = await api.post<ApiResponse<{ password: string }>>(`/customers/${customerId}/portal/password/generate`);
return res.data;
},
sendPortalCredentials: async (customerId: number) => {
const res = await api.post<ApiResponse<void>>(`/customers/${customerId}/portal/send-credentials`);
return res.data;
},
// Vertreter-Verwaltung
getRepresentatives: async (customerId: number) => {
const res = await api.get<ApiResponse<CustomerRepresentative[]>>(`/customers/${customerId}/representatives`);
return res.data;
},
addRepresentative: async (customerId: number, representativeId: number, notes?: string) => {
const res = await api.post<ApiResponse<CustomerRepresentative>>(`/customers/${customerId}/representatives`, { representativeId, notes });
return res.data;
},
removeRepresentative: async (customerId: number, representativeId: number) => {
const res = await api.delete<ApiResponse<void>>(`/customers/${customerId}/representatives/${representativeId}`);
return res.data;
},
searchForRepresentative: async (customerId: number, search: string) => {
const res = await api.get<ApiResponse<CustomerSummary[]>>(`/customers/${customerId}/representatives/search`, { params: { search } });
return res.data;
},
};
// Addresses
export const addressApi = {
getByCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<Address[]>>(`/customers/${customerId}/addresses`);
return res.data;
},
create: async (customerId: number, data: Partial<Address>) => {
const res = await api.post<ApiResponse<Address>>(`/customers/${customerId}/addresses`, data);
return res.data;
},
update: async (id: number, data: Partial<Address>) => {
const res = await api.put<ApiResponse<Address>>(`/addresses/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/addresses/${id}`);
return res.data;
},
};
// Bank Cards
export const bankCardApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<BankCard[]>>(`/customers/${customerId}/bank-cards`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<BankCard>) => {
const res = await api.post<ApiResponse<BankCard>>(`/customers/${customerId}/bank-cards`, data);
return res.data;
},
update: async (id: number, data: Partial<BankCard>) => {
const res = await api.put<ApiResponse<BankCard>>(`/bank-cards/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/bank-cards/${id}`);
return res.data;
},
};
// Identity Documents
export const documentApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<IdentityDocument[]>>(`/customers/${customerId}/documents`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<IdentityDocument>) => {
const res = await api.post<ApiResponse<IdentityDocument>>(`/customers/${customerId}/documents`, data);
return res.data;
},
update: async (id: number, data: Partial<IdentityDocument>) => {
const res = await api.put<ApiResponse<IdentityDocument>>(`/documents/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/documents/${id}`);
return res.data;
},
};
// Meters
export const meterApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<Meter[]>>(`/customers/${customerId}/meters`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<Meter>) => {
const res = await api.post<ApiResponse<Meter>>(`/customers/${customerId}/meters`, data);
return res.data;
},
update: async (id: number, data: Partial<Meter>) => {
const res = await api.put<ApiResponse<Meter>>(`/meters/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/meters/${id}`);
return res.data;
},
getReadings: async (meterId: number) => {
const res = await api.get<ApiResponse<MeterReading[]>>(`/meters/${meterId}/readings`);
return res.data;
},
addReading: async (meterId: number, data: Partial<MeterReading>) => {
const res = await api.post<ApiResponse<MeterReading>>(`/meters/${meterId}/readings`, data);
return res.data;
},
updateReading: async (meterId: number, readingId: number, data: Partial<MeterReading>) => {
const res = await api.put<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/${readingId}`, data);
return res.data;
},
deleteReading: async (meterId: number, readingId: number) => {
const res = await api.delete<ApiResponse<void>>(`/meters/${meterId}/readings/${readingId}`);
return res.data;
},
// Portal: Zählerstand melden
reportReading: async (meterId: number, data: { value: number; readingDate?: string; notes?: string }) => {
const res = await api.post<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/report`, data);
return res.data;
},
getMyMeters: async () => {
const res = await api.get<ApiResponse<Meter[]>>('/meters/my-meters');
return res.data;
},
// Status-Update
markTransferred: async (meterId: number, readingId: number) => {
const res = await api.patch<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/${readingId}/transfer`);
return res.data;
},
};
// Invoice API
export const invoiceApi = {
getInvoices: async (ecdId: number) => {
const res = await api.get<ApiResponse<Invoice[]>>(`/energy-details/${ecdId}/invoices`);
return res.data;
},
addInvoice: async (ecdId: number, data: Partial<Invoice>) => {
const res = await api.post<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices`, data);
return res.data;
},
addInvoiceByContract: async (contractId: number, data: Partial<Invoice>) => {
const res = await api.post<ApiResponse<Invoice>>(`/contracts/${contractId}/invoices`, data);
return res.data;
},
updateInvoice: async (ecdId: number, invoiceId: number, data: Partial<Invoice>) => {
const res = await api.put<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data);
return res.data;
},
deleteInvoice: async (ecdId: number, invoiceId: number) => {
const res = await api.delete<ApiResponse<void>>(`/energy-details/${ecdId}/invoices/${invoiceId}`);
return res.data;
},
uploadDocument: async (invoiceId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(
`/upload/invoices/${invoiceId}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return res.data;
},
deleteDocument: async (invoiceId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/invoices/${invoiceId}`);
return res.data;
},
};
// Stressfrei-Wechseln E-Mail-Adressen
export interface StressfreiEmail {
id: number;
customerId: number;
email: string;
platform?: string;
notes?: string;
isActive: boolean;
isProvisioned?: boolean;
hasMailbox: boolean;
createdAt: string;
updatedAt: string;
}
// Mailbox-Konto (für Dropdown-Auswahl)
export interface MailboxAccount {
id: number;
email: string;
notes?: string;
hasMailbox: boolean;
_count: {
cachedEmails: number;
};
}
// Gecachte E-Mail
export interface CachedEmail {
id: number;
stressfreiEmailId: number;
folder: 'INBOX' | 'SENT';
messageId: string;
uid: number;
subject?: string;
fromAddress: string;
fromName?: string;
toAddresses: string; // JSON Array
ccAddresses?: string; // JSON Array
receivedAt: string;
textBody?: string;
htmlBody?: string;
hasAttachments: boolean;
attachmentNames?: string; // JSON Array
contractId?: number;
assignedAt?: string;
assignedBy?: number;
isAutoAssigned?: boolean; // true = automatisch beim Senden aus Vertrag zugeordnet
isRead: boolean;
isStarred: boolean;
isDeleted: boolean;
deletedAt?: string;
createdAt: string;
updatedAt: string;
stressfreiEmail?: {
id: number;
email: string;
customerId: number;
};
contract?: {
id: number;
contractNumber: string;
} | null;
}
export interface SyncResult {
newEmails: number;
totalEmails: number;
}
export interface EmailAttachment {
filename: string;
content: string; // Base64-kodierter Inhalt
contentType?: string; // MIME-Type (z.B. 'application/pdf')
}
export interface SendEmailParams {
to: string | string[];
cc?: string | string[];
subject: string;
text?: string;
html?: string;
inReplyTo?: string;
references?: string[];
attachments?: EmailAttachment[];
contractId?: number; // Vertrag dem die gesendete E-Mail zugeordnet wird
}
// Anhang-Speicher-Ziele
export interface AttachmentTargetSlot {
key: string;
label: string;
field: string;
hasDocument: boolean;
currentPath?: string;
}
export interface AttachmentEntityWithSlots {
id: number;
label: string;
slots: AttachmentTargetSlot[];
}
export interface AttachmentTargetsResponse {
customer: {
id: number;
name: string;
type: 'PRIVATE' | 'BUSINESS';
slots: AttachmentTargetSlot[];
};
identityDocuments: AttachmentEntityWithSlots[];
bankCards: AttachmentEntityWithSlots[];
contract?: {
id: number;
contractNumber: string;
type: string;
energyDetailsId?: number;
slots: AttachmentTargetSlot[];
};
}
export const stressfreiEmailApi = {
getByCustomer: async (customerId: number, includeInactive = false) => {
const res = await api.get<ApiResponse<StressfreiEmail[]>>(`/customers/${customerId}/stressfrei-emails`, { params: { includeInactive } });
return res.data;
},
create: async (customerId: number, data: {
email: string;
platform?: string;
notes?: string;
provisionAtProvider?: boolean;
createMailbox?: boolean;
}) => {
const res = await api.post<ApiResponse<StressfreiEmail>>(`/customers/${customerId}/stressfrei-emails`, data);
return res.data;
},
update: async (id: number, data: Partial<StressfreiEmail>) => {
const res = await api.put<ApiResponse<StressfreiEmail>>(`/stressfrei-emails/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/stressfrei-emails/${id}`);
return res.data;
},
// Mailbox nachträglich aktivieren
enableMailbox: async (id: number) => {
const res = await api.post<ApiResponse<null>>(`/stressfrei-emails/${id}/enable-mailbox`);
return res.data;
},
// Mailbox-Status mit Provider synchronisieren
syncMailboxStatus: async (id: number) => {
const res = await api.post<ApiResponse<{ hasMailbox: boolean; wasUpdated: boolean }>>(`/stressfrei-emails/${id}/sync-mailbox-status`);
return res.data;
},
// Mailbox-Zugangsdaten abrufen (IMAP/SMTP)
getMailboxCredentials: async (id: number) => {
const res = await api.get<ApiResponse<{
email: string;
password: string;
imap: { server: string; port: number; encryption: string } | null;
smtp: { server: string; port: number; encryption: string } | null;
}>>(`/stressfrei-emails/${id}/credentials`);
return res.data;
},
// Passwort zurücksetzen (generiert neues Passwort beim Provider)
resetPassword: async (id: number) => {
const res = await api.post<ApiResponse<{ password: string }>>(`/stressfrei-emails/${id}/reset-password`);
return res.data;
},
// Weiterleitungen neu setzen (z.B. nach Änderung der Kunden-Stamm-E-Mail).
// Wenn die Adresse hasMailbox=true ist, wird zusätzlich das im CRM
// hinterlegte Passwort am Provider neu gesetzt (Self-Healing).
syncForwarding: async (id: number) => {
const res = await api.post<ApiResponse<{
forwardTargets: string[];
customerEmail: string;
passwordReset?: boolean;
}>>(`/stressfrei-emails/${id}/sync-forwarding`);
return res.data;
},
// E-Mails synchronisieren
syncEmails: async (id: number, fullSync = false) => {
const res = await api.post<ApiResponse<SyncResult>>(`/stressfrei-emails/${id}/sync`, {}, { params: { full: fullSync } });
return res.data;
},
// E-Mail senden
sendEmail: async (id: number, params: SendEmailParams) => {
const res = await api.post<ApiResponse<{ messageId: string }>>(`/stressfrei-emails/${id}/send`, params);
return res.data;
},
// Ordner-Anzahlen abrufen (total und ungelesen pro Ordner)
getFolderCounts: async (id: number) => {
const res = await api.get<ApiResponse<{
inbox: number;
inboxUnread: number;
sent: number;
sentUnread: number;
trash: number;
trashUnread: number;
}>>(`/stressfrei-emails/${id}/folder-counts`);
return res.data;
},
};
// Cached Email API (E-Mail-Client)
export interface EmailFilterParams {
accountId?: number;
folder?: 'INBOX' | 'SENT';
limit?: number;
offset?: number;
// Suche / Filter (alle AND-verknüpft)
search?: string;
fromFilter?: string;
toFilter?: string;
subjectFilter?: string;
bodyFilter?: string;
attachmentNameFilter?: string;
hasAttachments?: boolean;
isRead?: boolean;
isStarred?: boolean;
receivedFrom?: string; // ISO date
receivedTo?: string; // ISO date
}
export const cachedEmailApi = {
// E-Mails für Kunden abrufen
getForCustomer: async (customerId: number, options?: EmailFilterParams) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/customers/${customerId}/emails`, { params: options });
return res.data;
},
// E-Mails für Vertrag abrufen
getForContract: async (contractId: number, options?: { folder?: 'INBOX' | 'SENT'; limit?: number; offset?: number }) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/contracts/${contractId}/emails`, { params: options });
return res.data;
},
// Ordner-Anzahlen für Vertrag abrufen (zugeordnete E-Mails)
getContractFolderCounts: async (contractId: number) => {
const res = await api.get<ApiResponse<{
inbox: number;
inboxUnread: number;
sent: number;
sentUnread: number;
}>>(`/contracts/${contractId}/emails/folder-counts`);
return res.data;
},
// Mailbox-Konten eines Kunden abrufen
getMailboxAccounts: async (customerId: number) => {
const res = await api.get<ApiResponse<MailboxAccount[]>>(`/customers/${customerId}/mailbox-accounts`);
return res.data;
},
// Einzelne E-Mail abrufen (mit Body)
getById: async (id: number) => {
const res = await api.get<ApiResponse<CachedEmail>>(`/emails/${id}`);
return res.data;
},
// E-Mail-Thread abrufen
getThread: async (id: number) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/emails/${id}/thread`);
return res.data;
},
// Als gelesen/ungelesen markieren
markAsRead: async (id: number, isRead: boolean) => {
const res = await api.patch<ApiResponse<void>>(`/emails/${id}/read`, { isRead });
return res.data;
},
// Stern umschalten
toggleStar: async (id: number) => {
const res = await api.post<ApiResponse<{ isStarred: boolean }>>(`/emails/${id}/star`);
return res.data;
},
// Vertrag zuordnen
assignToContract: async (emailId: number, contractId: number) => {
const res = await api.post<ApiResponse<CachedEmail>>(`/emails/${emailId}/assign`, { contractId });
return res.data;
},
// Zuordnung aufheben
unassignFromContract: async (emailId: number) => {
const res = await api.delete<ApiResponse<CachedEmail>>(`/emails/${emailId}/assign`);
return res.data;
},
// E-Mail löschen (nur Admin)
delete: async (emailId: number) => {
const res = await api.delete<ApiResponse<void>>(`/emails/${emailId}`);
return res.data;
},
// Anhang-URL (view=true für inline anzeigen, sonst download)
// Hinweis: gibt die URL mit dem aktuellen Access-Token als Query-Param zurück,
// weil <iframe>/<a> keinen Authorization-Header senden können. Der Token läuft
// nach 15 min ab wenn Anhang dann geöffnet wird, kommt 401; UI muss in dem
// Fall die URL frisch holen.
getAttachmentUrl: (emailId: number, filename: string, view?: boolean) => {
const token = getAccessToken();
const encodedFilename = encodeURIComponent(filename);
const viewParam = view ? '&view=true' : '';
return `${api.defaults.baseURL}/emails/${emailId}/attachments/${encodedFilename}?token=${token || ''}${viewParam}`;
},
// Ungelesene E-Mails zählen
getUnreadCount: async (params: { customerId?: number; contractId?: number }) => {
const res = await api.get<ApiResponse<{ count: number }>>('/emails/unread-count', { params });
return res.data;
},
// ==================== PAPIERKORB ====================
// Papierkorb-E-Mails für Kunden abrufen
getTrash: async (customerId: number) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/customers/${customerId}/emails/trash`);
return res.data;
},
// Papierkorb-Anzahl für Kunden
getTrashCount: async (customerId: number) => {
const res = await api.get<ApiResponse<{ count: number }>>(`/customers/${customerId}/emails/trash/count`);
return res.data;
},
// E-Mail aus Papierkorb wiederherstellen
restore: async (emailId: number) => {
const res = await api.post<ApiResponse<void>>(`/emails/${emailId}/restore`);
return res.data;
},
// E-Mail endgültig löschen (aus Papierkorb)
permanentDelete: async (emailId: number) => {
const res = await api.delete<ApiResponse<void>>(`/emails/${emailId}/permanent`);
return res.data;
},
// ==================== ANHANG-SPEICHERUNG ====================
// Verfügbare Dokumenten-Ziele für Anhänge abrufen
getAttachmentTargets: async (emailId: number) => {
const res = await api.get<ApiResponse<AttachmentTargetsResponse>>(`/emails/${emailId}/attachment-targets`);
return res.data;
},
// Anhang in Dokumentenfeld speichern
saveAttachmentTo: async (emailId: number, filename: string, params: { entityType: string; entityId?: number; targetKey: string }) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ path: string; filename: string; originalName: string; size: number }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-to`,
params
);
return res.data;
},
// E-Mail als PDF speichern
saveEmailAsPdf: async (emailId: number, params: { entityType: string; entityId?: number; targetKey: string }) => {
const res = await api.post<ApiResponse<{ path: string; filename: string; size: number }>>(
`/emails/${emailId}/save-as-pdf`,
params
);
return res.data;
},
// E-Mail als Rechnung speichern (für Energieverträge)
saveEmailAsInvoice: async (emailId: number, params: { invoiceDate: string; invoiceType: string; notes?: string }) => {
const res = await api.post<ApiResponse<{ id: number; invoiceDate: string; invoiceType: string; documentPath: string }>>(
`/emails/${emailId}/save-as-invoice`,
params
);
return res.data;
},
// Anhang als Rechnung speichern (für Energieverträge)
saveAttachmentAsInvoice: async (emailId: number, filename: string, params: { invoiceDate: string; invoiceType: string; notes?: string }) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ id: number; invoiceDate: string; invoiceType: string; documentPath: string }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-as-invoice`,
params
);
return res.data;
},
saveAttachmentAsContractDocument: async (
emailId: number,
filename: string,
params: { documentType: string; notes?: string; deliveryDate?: string },
) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-as-contract-document`,
params,
);
return res.data;
},
};
// Contracts - Vertragsbaum für Kundenansicht
export interface ContractTreeNodeContract {
id: number;
contractNumber: string;
type: string;
status: string;
startDate: string | null;
endDate: string | null;
providerName: string | null;
tariffName: string | null;
previousContractId: number | null;
provider?: { id: number; name: string } | null;
tariff?: { id: number; name: string } | null;
contractCategory?: { id: number; name: string } | null;
customer?: { id: number; firstName: string; lastName: string; companyName: string | null; customerNumber: string } | null;
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
simCards: { phoneNumber: string | null; isMain: boolean }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}
export interface ContractTreeNode {
contract: ContractTreeNodeContract;
predecessors: ContractTreeNode[];
hasHistory: boolean;
}
export const contractApi = {
getAll: async (params?: { customerId?: number; type?: string; status?: string; search?: string; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
return res.data;
},
getTreeForCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', { params: { customerId, tree: 'true' } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Contract>>(`/contracts/${id}`);
return res.data;
},
create: async (data: Partial<Contract> & { [key: string]: unknown }) => {
const res = await api.post<ApiResponse<Contract>>('/contracts', data);
return res.data;
},
update: async (id: number, data: Partial<Contract> & { [key: string]: unknown }) => {
const res = await api.put<ApiResponse<Contract>>(`/contracts/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${id}`);
return res.data;
},
createFollowUp: async (id: number) => {
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/follow-up`);
return res.data;
},
createRenewal: async (id: number) => {
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/renewal`);
return res.data;
},
getPassword: async (id: number) => {
const res = await api.get<ApiResponse<{ password: string }>>(`/contracts/${id}/password`);
return res.data;
},
getSimCardCredentials: async (simCardId: number) => {
const res = await api.get<ApiResponse<{ pin: string | null; puk: string | null }>>(`/contracts/simcard/${simCardId}/credentials`);
return res.data;
},
getInternetCredentials: async (contractId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/contracts/${contractId}/internet-credentials`);
return res.data;
},
getSipCredentials: async (phoneNumberId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/contracts/phonenumber/${phoneNumberId}/sip-credentials`);
return res.data;
},
// Vertrags-Cockpit
getCockpit: async () => {
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
return res.data;
},
// Vertragsdokumente
getDocuments: async (contractId: number) => {
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
return res.data;
},
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string, deliveryDate?: string) => {
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', documentType);
if (notes) formData.append('notes', notes);
if (deliveryDate) formData.append('deliveryDate', deliveryDate);
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteDocument: async (contractId: number, documentId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/documents/${documentId}`);
return res.data;
},
// Folgezähler
addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => {
const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data);
return res.data;
},
removeContractMeter: async (contractId: number, contractMeterId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/contract-meter/${contractMeterId}`);
return res.data;
},
// Snooze: Vertrag zurückstellen
snooze: async (id: number, data: { nextReviewDate?: string; months?: number }) => {
const res = await api.patch<ApiResponse<{ id: number; contractNumber: string; nextReviewDate: string | null }>>(`/contracts/${id}/snooze`, data);
return res.data;
},
};
// Contract History (Vertragshistorie - nur intern)
export const contractHistoryApi = {
getByContract: async (contractId: number) => {
const res = await api.get<ApiResponse<ContractHistoryEntry[]>>(`/contracts/${contractId}/history`);
return res.data;
},
create: async (contractId: number, data: { title: string; description?: string }) => {
const res = await api.post<ApiResponse<ContractHistoryEntry>>(`/contracts/${contractId}/history`, data);
return res.data;
},
update: async (contractId: number, entryId: number, data: { title?: string; description?: string }) => {
const res = await api.put<ApiResponse<ContractHistoryEntry>>(`/contracts/${contractId}/history/${entryId}`, data);
return res.data;
},
delete: async (contractId: number, entryId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/history/${entryId}`);
return res.data;
},
};
// Contract Tasks (Aufgaben)
export const contractTaskApi = {
// Alle Tasks über alle Verträge (für Task-Liste & Dashboard)
getAll: async (params?: { status?: ContractTaskStatus; customerId?: number }) => {
const res = await api.get<ApiResponse<ContractTask[]>>('/tasks', { params });
return res.data;
},
// Task-Statistik (offene Aufgaben)
getStats: async () => {
const res = await api.get<ApiResponse<{ openCount: number }>>('/tasks/stats');
return res.data;
},
// Tasks für einen spezifischen Vertrag
getByContract: async (contractId: number, status?: ContractTaskStatus) => {
const res = await api.get<ApiResponse<ContractTask[]>>(`/contracts/${contractId}/tasks`, { params: { status } });
return res.data;
},
create: async (contractId: number, data: { title: string; description?: string; visibleInPortal?: boolean }) => {
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/tasks`, data);
return res.data;
},
update: async (taskId: number, data: { title?: string; description?: string; visibleInPortal?: boolean }) => {
const res = await api.put<ApiResponse<ContractTask>>(`/tasks/${taskId}`, data);
return res.data;
},
complete: async (taskId: number) => {
const res = await api.post<ApiResponse<ContractTask>>(`/tasks/${taskId}/complete`);
return res.data;
},
reopen: async (taskId: number) => {
const res = await api.post<ApiResponse<ContractTask>>(`/tasks/${taskId}/reopen`);
return res.data;
},
delete: async (taskId: number) => {
const res = await api.delete<ApiResponse<void>>(`/tasks/${taskId}`);
return res.data;
},
// Subtasks
createSubtask: async (taskId: number, title: string) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/tasks/${taskId}/subtasks`, { title });
return res.data;
},
// Kundenportal: Antwort auf eigenes Ticket
createReply: async (taskId: number, title: string) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/tasks/${taskId}/reply`, { title });
return res.data;
},
updateSubtask: async (subtaskId: number, title: string) => {
const res = await api.put<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}`, { title });
return res.data;
},
completeSubtask: async (subtaskId: number) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}/complete`);
return res.data;
},
reopenSubtask: async (subtaskId: number) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}/reopen`);
return res.data;
},
deleteSubtask: async (subtaskId: number) => {
const res = await api.delete<ApiResponse<void>>(`/subtasks/${subtaskId}`);
return res.data;
},
// Support-Ticket erstellen (für Kundenportal)
createSupportTicket: async (contractId: number, data: { title: string; description?: string }) => {
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/support-ticket`, data);
return res.data;
},
};
// App Settings
export const appSettingsApi = {
getPublic: async () => {
const res = await api.get<ApiResponse<Record<string, string>>>('/settings/public');
return res.data;
},
getAll: async () => {
const res = await api.get<ApiResponse<Record<string, string>>>('/settings');
return res.data;
},
update: async (settings: Record<string, string>) => {
const res = await api.put<ApiResponse<void>>('/settings', settings);
return res.data;
},
updateOne: async (key: string, value: string) => {
const res = await api.put<ApiResponse<void>>(`/settings/${key}`, { value });
return res.data;
},
};
// Backup & Restore
export interface BackupInfo {
name: string;
timestamp: string;
totalRecords: number;
tables: { table: string; count: number }[];
sizeBytes: number;
hasUploads: boolean;
uploadSizeBytes: number;
}
export const backupApi = {
list: async () => {
const res = await api.get<ApiResponse<BackupInfo[]>>('/settings/backups');
return res.data;
},
create: async () => {
const res = await api.post<ApiResponse<{ backupName: string }>>('/settings/backup');
return res.data;
},
restore: async (name: string) => {
const res = await api.post<ApiResponse<{ restoredRecords: number; restoredFiles: number }>>(`/settings/backup/${name}/restore`);
return res.data;
},
delete: async (name: string) => {
const res = await api.delete<ApiResponse<void>>(`/settings/backup/${name}`);
return res.data;
},
getDownloadUrl: (name: string) => {
return `/api/settings/backup/${name}/download`;
},
upload: async (file: File) => {
const formData = new FormData();
formData.append('backup', file);
const res = await api.post<ApiResponse<{ backupName: string }>>('/settings/backup/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
factoryReset: async () => {
// Server erzwingt confirm-Body als Schutz gegen versehentliche
// DB-Plättung (Pentest Runde 11). Der Confirm-String muss exakt
// dieser Wert sein.
const res = await api.post<ApiResponse<void>>('/settings/factory-reset', {
confirm: 'FACTORY-RESET-BESTAETIGT',
});
return res.data;
},
};
export interface BackupLogEntry {
id: number;
operation: 'CREATE' | 'RESTORE';
backupName: string | null;
success: boolean;
durationMs: number;
summary: string;
userEmail: string | null;
ipAddress: string | null;
createdAt: string;
}
export interface BackupLogDetail extends BackupLogEntry {
fullLog: string;
}
export const backupLogApi = {
list: async (operation: 'CREATE' | 'RESTORE') => {
const res = await api.get<ApiResponse<BackupLogEntry[]>>('/settings/backup-logs', {
params: { operation, limit: 50 },
});
return res.data;
},
get: async (id: number) => {
const res = await api.get<ApiResponse<BackupLogDetail>>(`/settings/backup-logs/${id}`);
return res.data;
},
};
// Rate-Limit-Verwaltung (Admin)
export interface ActiveRateLimit {
ipAddress: string;
email: string | null;
lastHit: string;
hitCount: number;
lastEndpoint: string | null;
limiters: string[];
}
export const rateLimitApi = {
getActive: async () => {
const res = await api.get<ApiResponse<ActiveRateLimit[]>>('/settings/rate-limits/active');
return res.data;
},
reset: async (body: { ipAddress: string; email?: string }) => {
const res = await api.post<ApiResponse<void>>('/settings/rate-limits/reset', body);
return res.data;
},
};
// Platforms
export const platformApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<SalesPlatform[]>>('/platforms', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<SalesPlatform>>(`/platforms/${id}`);
return res.data;
},
create: async (data: Partial<SalesPlatform>) => {
const res = await api.post<ApiResponse<SalesPlatform>>('/platforms', data);
return res.data;
},
update: async (id: number, data: Partial<SalesPlatform>) => {
const res = await api.put<ApiResponse<SalesPlatform>>(`/platforms/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/platforms/${id}`);
return res.data;
},
};
// Cancellation Periods
export const cancellationPeriodApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<CancellationPeriod[]>>('/cancellation-periods', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<CancellationPeriod>>(`/cancellation-periods/${id}`);
return res.data;
},
create: async (data: Partial<CancellationPeriod>) => {
const res = await api.post<ApiResponse<CancellationPeriod>>('/cancellation-periods', data);
return res.data;
},
update: async (id: number, data: Partial<CancellationPeriod>) => {
const res = await api.put<ApiResponse<CancellationPeriod>>(`/cancellation-periods/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/cancellation-periods/${id}`);
return res.data;
},
};
// Contract Durations
export const contractDurationApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<ContractDuration[]>>('/contract-durations', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<ContractDuration>>(`/contract-durations/${id}`);
return res.data;
},
create: async (data: Partial<ContractDuration>) => {
const res = await api.post<ApiResponse<ContractDuration>>('/contract-durations', data);
return res.data;
},
update: async (id: number, data: Partial<ContractDuration>) => {
const res = await api.put<ApiResponse<ContractDuration>>(`/contract-durations/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contract-durations/${id}`);
return res.data;
},
};
// Contract Categories (Vertragstypen)
export const contractCategoryApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<ContractCategory[]>>('/contract-categories', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<ContractCategory>>(`/contract-categories/${id}`);
return res.data;
},
create: async (data: Partial<ContractCategory>) => {
const res = await api.post<ApiResponse<ContractCategory>>('/contract-categories', data);
return res.data;
},
update: async (id: number, data: Partial<ContractCategory>) => {
const res = await api.put<ApiResponse<ContractCategory>>(`/contract-categories/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contract-categories/${id}`);
return res.data;
},
};
// Providers (Anbieter)
export const providerApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<Provider[]>>('/providers', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Provider>>(`/providers/${id}`);
return res.data;
},
create: async (data: Partial<Provider>) => {
const res = await api.post<ApiResponse<Provider>>('/providers', data);
return res.data;
},
update: async (id: number, data: Partial<Provider>) => {
const res = await api.put<ApiResponse<Provider>>(`/providers/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/providers/${id}`);
return res.data;
},
getTariffs: async (providerId: number, includeInactive = false) => {
const res = await api.get<ApiResponse<Tariff[]>>(`/providers/${providerId}/tariffs`, { params: { includeInactive } });
return res.data;
},
createTariff: async (providerId: number, data: Partial<Tariff>) => {
const res = await api.post<ApiResponse<Tariff>>(`/providers/${providerId}/tariffs`, data);
return res.data;
},
};
// Tariffs (Tarife)
export const tariffApi = {
getById: async (id: number) => {
const res = await api.get<ApiResponse<Tariff>>(`/tariffs/${id}`);
return res.data;
},
update: async (id: number, data: Partial<Tariff>) => {
const res = await api.put<ApiResponse<Tariff>>(`/tariffs/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/tariffs/${id}`);
return res.data;
},
};
// Upload
export const uploadApi = {
uploadBankCardDocument: async (bankCardId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/bank-cards/${bankCardId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
uploadIdentityDocument: async (documentId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/documents/${documentId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteBankCardDocument: async (bankCardId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/bank-cards/${bankCardId}`);
return res.data;
},
deleteIdentityDocument: async (documentId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/documents/${documentId}`);
return res.data;
},
uploadBusinessRegistration: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/business-registration`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteBusinessRegistration: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/business-registration`);
return res.data;
},
uploadCommercialRegister: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/commercial-register`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCommercialRegister: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/commercial-register`);
return res.data;
},
uploadPrivacyPolicy: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/privacy-policy`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deletePrivacyPolicy: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/privacy-policy`);
return res.data;
},
// Contract Documents
uploadCancellationLetter: async (contractId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-letter`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationLetter: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter`);
return res.data;
},
uploadCancellationConfirmation: async (contractId: number, file: File, confirmationDate?: string) => {
const formData = new FormData();
formData.append('document', file);
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationConfirmation: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-confirmation`);
return res.data;
},
uploadCancellationLetterOptions: async (contractId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-letter-options`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationLetterOptions: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter-options`);
return res.data;
},
uploadCancellationConfirmationOptions: async (contractId: number, file: File, confirmationDate?: string) => {
const formData = new FormData();
formData.append('document', file);
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationConfirmationOptions: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`);
return res.data;
},
};
// Users
export const userApi = {
getAll: async (params?: { search?: string; isActive?: boolean; roleId?: number; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<User[]>>('/users', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<User>>(`/users/${id}`);
return res.data;
},
create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => {
const res = await api.post<ApiResponse<User>>('/users', data);
return res.data;
},
update: async (id: number, data: Partial<User> & { password?: string; roleIds?: number[] }) => {
const res = await api.put<ApiResponse<User>>(`/users/${id}`, data);
return res.data;
},
// Passwort eines Users zurücksetzen (Admin-Funktion). Separat vom generischen
// Update, damit der Vorgang einen eigenen Audit-Eintrag bekommt.
setPassword: async (id: number, password: string) => {
const res = await api.post<ApiResponse<void>>(`/users/${id}/password`, { password });
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/users/${id}`);
return res.data;
},
getRoles: async () => {
const res = await api.get<ApiResponse<Role[]>>('/users/roles/list');
return res.data;
},
};
// Developer
export const developerApi = {
getSchema: async () => {
const res = await api.get<ApiResponse<any[]>>('/developer/schema');
return res.data;
},
getTableData: async (tableName: string, page = 1, limit = 50) => {
const res = await api.get<ApiResponse<any[]>>(`/developer/table/${tableName}`, { params: { page, limit } });
return res.data;
},
updateRow: async (tableName: string, id: string, data: Record<string, any>) => {
const res = await api.put<ApiResponse<any>>(`/developer/table/${tableName}/${id}`, data);
return res.data;
},
deleteRow: async (tableName: string, id: string) => {
const res = await api.delete<ApiResponse<void>>(`/developer/table/${tableName}/${id}`);
return res.data;
},
getReference: async (tableName: string) => {
const res = await api.get<ApiResponse<any[]>>(`/developer/reference/${tableName}`);
return res.data;
},
};
// Email Provider (für Stressfrei-Wechseln Provisionierung)
export interface EmailProviderConfig {
id: number;
name: string;
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
passwordEncrypted?: string;
domain: string;
defaultForwardEmail?: string;
// IMAP/SMTP-Server (für E-Mail-Client)
imapServer?: string;
imapPort?: number;
smtpServer?: string;
smtpPort?: number;
// Verschlüsselungs-Einstellungen
imapEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
smtpEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
// System-E-Mail für automatisierten Versand
systemEmailAddress?: string;
systemEmailPasswordEncrypted?: string;
// UI-Label für Kunden-E-Mail-Adressen (z.B. "Stressfrei-Wechseln")
customerEmailLabel?: string | null;
isActive: boolean;
isDefault: boolean;
createdAt: string;
updatedAt: string;
}
export interface EmailOperationResult {
success: boolean;
message?: string;
error?: string;
}
export const emailProviderApi = {
// Config CRUD
getConfigs: async () => {
const res = await api.get<ApiResponse<EmailProviderConfig[]>>('/email-providers/configs');
return res.data;
},
getConfig: async (id: number) => {
const res = await api.get<ApiResponse<EmailProviderConfig>>(`/email-providers/configs/${id}`);
return res.data;
},
createConfig: async (data: Partial<EmailProviderConfig> & { password?: string }) => {
const res = await api.post<ApiResponse<EmailProviderConfig>>('/email-providers/configs', data);
return res.data;
},
updateConfig: async (id: number, data: Partial<EmailProviderConfig> & { password?: string }) => {
const res = await api.put<ApiResponse<EmailProviderConfig>>(`/email-providers/configs/${id}`, data);
return res.data;
},
deleteConfig: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/email-providers/configs/${id}`);
return res.data;
},
// Email Operations
testConnection: async (options?: {
id?: number;
testData?: {
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
password?: string;
domain: string;
};
}) => {
const body = options?.testData
? { ...options.testData }
: options?.id
? { id: options.id }
: {};
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/test-connection', body);
return res.data;
},
testMailAccess: async (body: {
id?: number;
apiUrl?: string;
domain?: string;
systemEmailAddress?: string;
systemEmailPassword?: string;
imapEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
smtpEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
allowSelfSignedCerts?: boolean;
}) => {
const res = await api.post<ApiResponse<{
imap: { success: boolean; error?: string; server: string; port: number; encryption: string };
smtp: { success: boolean; error?: string; server: string; port: number; encryption: string };
user: string;
}>>('/email-providers/test-mail-access', body);
return res.data;
},
getDomain: async () => {
const res = await api.get<ApiResponse<{ domain: string | null }>>('/email-providers/domain');
return res.data;
},
getPublicSettings: async () => {
const res = await api.get<ApiResponse<{
domain: string | null;
customerEmailLabel: string;
customerEmailLabelIsCustom: boolean;
}>>('/email-providers/public-settings');
return res.data;
},
checkEmailExists: async (localPart: string) => {
const res = await api.get<ApiResponse<{ exists: boolean; email?: string }>>(`/email-providers/check/${localPart}`);
return res.data;
},
provisionEmail: async (localPart: string, customerEmail: string) => {
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/provision', { localPart, customerEmail });
return res.data;
},
deprovisionEmail: async (localPart: string) => {
const res = await api.delete<ApiResponse<EmailOperationResult>>(`/email-providers/deprovision/${localPart}`);
return res.data;
},
};
// ==================== AUDIT-LOGS ====================
export interface AuditLogSearchParams {
page?: number;
limit?: number;
userId?: number;
action?: string;
resourceType?: string;
sensitivity?: AuditSensitivity;
dataSubjectId?: number;
startDate?: string;
endDate?: string;
search?: string;
}
export const auditLogApi = {
search: async (params?: AuditLogSearchParams) => {
const res = await api.get<ApiResponse<AuditLog[]>>('/audit-logs', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<AuditLog>>(`/audit-logs/${id}`);
return res.data;
},
getByCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<AuditLog[]>>(`/audit-logs/customer/${customerId}`);
return res.data;
},
export: async (params?: AuditLogSearchParams & { format?: 'json' | 'csv' }) => {
const res = await api.get<ApiResponse<{ data: AuditLog[]; format: string }>>('/audit-logs/export', { params });
return res.data;
},
verifyIntegrity: async () => {
const res = await api.post<ApiResponse<{ valid: boolean; checkedCount: number; invalidEntries: number[]; message: string }>>('/audit-logs/verify');
return res.data;
},
rehash: async () => {
const res = await api.post<ApiResponse<{ rehashedCount: number }>>('/audit-logs/rehash');
return res.data;
},
getRetentionPolicies: async () => {
const res = await api.get<ApiResponse<AuditRetentionPolicy[]>>('/audit-logs/retention-policies');
return res.data;
},
updateRetentionPolicy: async (id: number, data: { retentionDays: number; isActive?: boolean }) => {
const res = await api.put<ApiResponse<AuditRetentionPolicy>>(`/audit-logs/retention-policies/${id}`, data);
return res.data;
},
runRetentionCleanup: async () => {
const res = await api.post<ApiResponse<{ deletedCount: number }>>('/audit-logs/cleanup');
return res.data;
},
};
// ==================== PDF TEMPLATES ====================
export const pdfTemplateApi = {
getAll: async () => {
const res = await api.get<ApiResponse<import('../types').PdfTemplate[]>>('/pdf-templates');
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<import('../types').PdfTemplate>>(`/pdf-templates/${id}`);
return res.data;
},
create: async (formData: FormData) => {
const res = await api.post<ApiResponse<import('../types').PdfTemplate & { pdfFields?: import('../types').PdfField[] }>>('/pdf-templates', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
update: async (id: number, data: Record<string, unknown>) => {
const res = await api.put<ApiResponse<import('../types').PdfTemplate>>(`/pdf-templates/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/pdf-templates/${id}`);
return res.data;
},
getFields: async (id: number) => {
const res = await api.get<ApiResponse<import('../types').PdfField[]>>(`/pdf-templates/${id}/fields`);
return res.data;
},
getCrmFields: async (maxPhoneFields?: number) => {
const res = await api.get<ApiResponse<import('../types').CrmField[]>>('/pdf-templates/crm-fields', { params: { maxPhoneFields } });
return res.data;
},
getRequiredInputs: async (templateId: number, contractId: number) => {
const res = await api.get<ApiResponse<{
needsStressfreiEmail: boolean;
stressfreiEmails: { id: number; email: string }[];
manualFields: { key: string; pdfFieldName: string }[];
}>>(`/pdf-templates/${templateId}/generate/${contractId}/inputs`);
return res.data;
},
generatePdf: async (templateId: number, contractId: number, extras?: { stressfreiEmailId?: number; manualValues?: Record<string, string> }) => {
const res = await api.post(`/pdf-templates/${templateId}/generate/${contractId}`, extras || {}, { responseType: 'blob' });
return res.data;
},
generateUrl: (templateId: number, contractId: number) =>
`/api/pdf-templates/${templateId}/generate/${contractId}`,
};
// ==================== EMAIL LOG ====================
export interface EmailLog {
id: number;
fromAddress: string;
toAddress: string;
subject: string;
context: string;
customerId?: number;
triggeredBy?: string;
smtpServer: string;
smtpPort: number;
smtpEncryption: string;
smtpUser: string;
success: boolean;
messageId?: string;
errorMessage?: string;
smtpResponse?: string;
sentAt: string;
}
// ==================== MONITORING ====================
export type SecurityEventType =
| 'LOGIN_FAILED' | 'LOGIN_SUCCESS' | 'RATE_LIMIT_HIT' | 'ACCESS_DENIED'
| 'SSRF_BLOCKED' | 'PASSWORD_RESET_REQUEST' | 'PASSWORD_RESET_CONFIRM'
| 'LOGOUT' | 'TOKEN_REJECTED' | 'PERMISSION_CHANGED' | 'SUSPICIOUS';
export type SecuritySeverity = 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
export interface SecurityEvent {
id: number;
type: SecurityEventType;
severity: SecuritySeverity;
message: string;
ipAddress: string | null;
userId: number | null;
customerId: number | null;
userEmail: string | null;
endpoint: string | null;
details: Record<string, unknown> | null;
alerted: boolean;
alertedAt: string | null;
createdAt: string;
}
export interface MonitoringSettings {
alertEmail: string;
digestEnabled: boolean;
lastDigestAt: string | null;
}
export const monitoringApi = {
getEvents: async (params?: { page?: number; limit?: number; type?: SecurityEventType | ''; severity?: SecuritySeverity | ''; search?: string; ip?: string; since?: string }) => {
const q = new URLSearchParams();
if (params?.page) q.set('page', String(params.page));
if (params?.limit) q.set('limit', String(params.limit));
if (params?.type) q.set('type', params.type);
if (params?.severity) q.set('severity', params.severity);
if (params?.search) q.set('search', params.search);
if (params?.ip) q.set('ip', params.ip);
if (params?.since) q.set('since', params.since);
const res = await api.get<ApiResponse<SecurityEvent[]> & {
pagination: { page: number; limit: number; total: number; totalPages: number };
stats: { byType: Record<string, number>; bySeverity: Record<string, number> };
}>(`/monitoring/events?${q}`);
return res.data;
},
getSettings: async () => {
const res = await api.get<ApiResponse<MonitoringSettings>>('/monitoring/settings');
return res.data;
},
updateSettings: async (data: { alertEmail?: string; digestEnabled?: boolean }) => {
const res = await api.put<ApiResponse<void>>('/monitoring/settings', data);
return res.data;
},
testAlert: async () => {
const res = await api.post<ApiResponse<void>>('/monitoring/test-alert');
return res.data;
},
runDigest: async () => {
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
return res.data;
},
clearEvents: async (olderThanDays?: number) => {
const q = olderThanDays ? `?olderThanDays=${olderThanDays}` : '';
const res = await api.delete<ApiResponse<{ deletedCount: number }>>(`/monitoring/events${q}`);
return res.data;
},
};
export const emailLogApi = {
getLogs: async (params?: { page?: number; limit?: number; success?: string; search?: string; context?: string }) => {
const query = new URLSearchParams();
if (params?.page) query.set('page', params.page.toString());
if (params?.limit) query.set('limit', params.limit.toString());
if (params?.success !== undefined) query.set('success', params.success);
if (params?.search) query.set('search', params.search);
if (params?.context) query.set('context', params.context);
const res = await api.get<ApiResponse<EmailLog[]> & { pagination: { page: number; limit: number; total: number; totalPages: number } }>(`/email-logs?${query}`);
return res.data;
},
getStats: async () => {
const res = await api.get<ApiResponse<{ total: number; success: number; failed: number; last24h: number }>>('/email-logs/stats');
return res.data;
},
getDetail: async (id: number) => {
const res = await api.get<ApiResponse<EmailLog>>(`/email-logs/${id}`);
return res.data;
},
};
// ==================== DSGVO ====================
export const gdprApi = {
// Dashboard
getDashboardStats: async () => {
const res = await api.get<ApiResponse<GDPRDashboardStats>>('/gdpr/dashboard');
return res.data;
},
// Datenexport (Art. 15)
exportCustomerData: async (customerId: number) => {
const res = await api.get<ApiResponse<unknown>>(`/gdpr/customer/${customerId}/export`);
return res.data;
},
// Löschanfragen
getDeletionRequests: async (params?: { status?: DeletionRequestStatus; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<DataDeletionRequest[]>>('/gdpr/deletions', { params });
return res.data;
},
getDeletionRequest: async (id: number) => {
const res = await api.get<ApiResponse<DataDeletionRequest>>(`/gdpr/deletions/${id}`);
return res.data;
},
createDeletionRequest: async (data: { customerId: number; requestSource: string; requestedBy: string }) => {
const res = await api.post<ApiResponse<DataDeletionRequest>>('/gdpr/deletions', data);
return res.data;
},
processDeletionRequest: async (id: number, data: { processedBy: string; action: 'complete' | 'partial' | 'reject'; retentionReason?: string }) => {
const res = await api.put<ApiResponse<DataDeletionRequest>>(`/gdpr/deletions/${id}/process`, data);
return res.data;
},
// Consent-Status prüfen (hat Kunde vollständig zugestimmt?)
checkConsentStatus: async (customerId: number) => {
const res = await api.get<ApiResponse<{ hasConsent: boolean; hasPaperConsent: boolean; hasOnlineConsent: boolean; consentDetails: { type: string; status: string }[]; consentHash: string | null }>>(`/gdpr/customer/${customerId}/consent-status`);
return res.data;
},
getMyConsentStatus: async () => {
const res = await api.get<ApiResponse<{ hasConsent: boolean; hasPaperConsent: boolean; hasOnlineConsent: boolean; consentDetails: { type: string; status: string }[]; consentHash: string | null }>>('/gdpr/my-consent-status');
return res.data;
},
// Einwilligungen
getCustomerConsents: async (customerId: number) => {
const res = await api.get<ApiResponse<CustomerConsent[]>>(`/gdpr/customer/${customerId}/consents`);
return res.data;
},
updateConsent: async (customerId: number, consentType: ConsentType, data: { status: ConsentStatus; source?: string }) => {
const res = await api.put<ApiResponse<CustomerConsent>>(`/gdpr/customer/${customerId}/consents/${consentType}`, data);
return res.data;
},
getConsentOverview: async () => {
const res = await api.get<ApiResponse<Record<string, { granted: number; withdrawn: number; pending: number }>>>('/gdpr/consents/overview');
return res.data;
},
// Datenschutzerklärung (Editor)
getPrivacyPolicy: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/privacy-policy');
return res.data;
},
updatePrivacyPolicy: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/privacy-policy', { html });
return res.data;
},
// Vollmacht-Vorlage
getAuthorizationTemplate: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/authorization-template');
return res.data;
},
updateAuthorizationTemplate: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/authorization-template', { html });
return res.data;
},
// Impressum
getImprint: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/imprint');
return res.data;
},
updateImprint: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/imprint', { html });
return res.data;
},
// Website-Datenschutzerklärung
getWebsitePrivacyPolicy: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/website-privacy-policy');
return res.data;
},
updateWebsitePrivacyPolicy: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/website-privacy-policy', { html });
return res.data;
},
// Consent-Link senden
sendConsentLink: async (customerId: number, channel: string) => {
const res = await api.post<ApiResponse<{ url: string; channel: string; hash: string }>>(`/gdpr/customer/${customerId}/send-consent-link`, { channel });
return res.data;
},
// Portal: Eigene Datenschutzseite
getMyPrivacy: async () => {
const res = await api.get<ApiResponse<{ privacyPolicyHtml: string; consents: CustomerConsent[] }>>('/gdpr/my-privacy');
return res.data;
},
getMyPrivacyPdfUrl: '/api/gdpr/my-privacy/pdf',
// Vollmachten (Admin)
getAuthorizations: async (customerId: number) => {
const res = await api.get<ApiResponse<RepresentativeAuthorization[]>>(`/gdpr/customer/${customerId}/authorizations`);
return res.data;
},
sendAuthorizationRequest: async (customerId: number, representativeId: number, channel: string) => {
const res = await api.post<ApiResponse<{ channel: string; portalUrl: string; messageText: string }>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/send`, { channel });
return res.data;
},
grantAuthorization: async (customerId: number, representativeId: number, data?: { source?: string; notes?: string }) => {
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/grant`, data || {});
return res.data;
},
withdrawAuthorization: async (customerId: number, representativeId: number) => {
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/withdraw`);
return res.data;
},
uploadAuthorizationDocument: async (customerId: number, representativeId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(
`/gdpr/customer/${customerId}/authorizations/${representativeId}/upload`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return res.data;
},
deleteAuthorizationDocument: async (customerId: number, representativeId: number) => {
const res = await api.delete<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/document`);
return res.data;
},
// Vollmachten (Portal)
getMyAuthorizations: async () => {
const res = await api.get<ApiResponse<RepresentativeAuthorization[]>>('/gdpr/my-authorizations');
return res.data;
},
toggleMyAuthorization: async (representativeId: number, grant: boolean) => {
const res = await api.put<ApiResponse<RepresentativeAuthorization>>(`/gdpr/my-authorizations/${representativeId}`, { grant });
return res.data;
},
getMyAuthorizationStatus: async () => {
const res = await api.get<ApiResponse<{ customerId: number; hasAuthorization: boolean }[]>>('/gdpr/my-authorization-status');
return res.data;
},
};
// ==================== PUBLIC API (kein Auth-Token) ====================
const publicAxios = axios.create({
baseURL: '/api/public',
headers: { 'Content-Type': 'application/json' },
});
export const publicApi = {
getConsentPage: async (hash: string) => {
const res = await publicAxios.get<ApiResponse<{
customer: { firstName: string; lastName: string; customerNumber: string };
consents: Array<{
consentType: string;
status: string;
label: string;
description: string;
grantedAt: string | null;
}>;
privacyPolicyHtml: string;
}>>(`/consent/${hash}`);
return res.data;
},
grantAllConsents: async (hash: string) => {
const res = await publicAxios.post<ApiResponse<void>>(`/consent/${hash}/grant`);
return res.data;
},
getConsentPdfUrl: (hash: string) => `/api/public/consent/${hash}/pdf`,
};
// ============ BIRTHDAY API ============
export interface BirthdayEntry {
customerId: number;
customerNumber: string;
name: string;
birthDate: string;
age: number;
daysUntil: number;
isToday: boolean;
isPast: boolean;
portalEnabled: boolean;
email?: string | null;
phone?: string | null;
}
export interface MyBirthdayCheck {
show: boolean;
isToday: boolean;
daysAgo: number;
firstName: string;
lastName: string;
salutation: string | null;
useInformalAddress: boolean;
age: number;
}
export const birthdayApi = {
getUpcoming: async (past: number = 7, future: number = 30) => {
const res = await api.get<ApiResponse<BirthdayEntry[]>>('/birthdays/upcoming', {
params: { past, future },
});
return res.data;
},
getMyBirthday: async () => {
const res = await api.get<ApiResponse<MyBirthdayCheck | null>>('/birthdays/my-birthday');
return res.data;
},
acknowledgeMyBirthday: async () => {
const res = await api.post<ApiResponse<void>>('/birthdays/my-birthday/acknowledge');
return res.data;
},
resetGreeting: async (customerId: number) => {
const res = await api.post<ApiResponse<void>>(`/birthdays/${customerId}/reset`);
return res.data;
},
sendGreeting: async (customerId: number, channel: 'email' | 'whatsapp' | 'telegram' | 'signal') => {
const res = await api.post<ApiResponse<{ channel: string; messageText: string }>>(
`/birthdays/${customerId}/send`,
{ channel },
);
return res.data;
},
};
export default api;