add invoice

This commit is contained in:
Elias Bennour 2025-05-31 17:29:06 +02:00
parent 1e9b2c702b
commit a25e41d820
4 changed files with 427 additions and 9 deletions

10
package-lock.json generated
View File

@ -14,6 +14,7 @@
"bcryptjs": "^3.0.2",
"next": "15.3.2",
"next-auth": "^4.24.11",
"nodemailer": "^6.10.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@ -5023,6 +5024,15 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",

View File

@ -15,6 +15,7 @@
"bcryptjs": "^3.0.2",
"next": "15.3.2",
"next-auth": "^4.24.11",
"nodemailer": "^6.10.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@ -22,6 +23,7 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",

View File

@ -4,7 +4,6 @@ import { useSession, signOut } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect, useState, Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
// import { useTheme } from '../../context/ThemeContext'; // Entfernt
// Typen für die Admin-Seite (Frontend)
interface AdminUser {
@ -58,6 +57,13 @@ interface SetApprovalStatusApiResponse {
approvedAt: Date | null;
}
interface SendInvoicesApiResponse {
message: string;
sentToCount: number;
errors: { userId: string; error: string }[];
}
const AdminPanelPage: NextPage = () => {
const { data: session, status } = useSession();
const router = useRouter();
@ -79,6 +85,17 @@ const AdminPanelPage: NextPage = () => {
const [isLoadingUserTransactions, setIsLoadingUserTransactions] = useState(false);
const [isTransactionsModalOpen, setIsTransactionsModalOpen] = useState(false);
const [isInvoiceModalOpen, setIsInvoiceModalOpen] = useState(false);
const [invoiceDeadline, setInvoiceDeadline] = useState<string>(() => {
const today = new Date();
today.setDate(today.getDate() + 14);
return today.toISOString().split('T')[0];
});
// paypalEmail und cashPaymentContacts States entfernt, da sie jetzt fest im Backend sind
const [isSendingInvoices, setIsSendingInvoices] = useState(false);
const [selectedUserIdsForInvoice, setSelectedUserIdsForInvoice] = useState<string[]>([]);
useEffect(() => {
if (status === 'loading') return;
@ -249,6 +266,60 @@ const AdminPanelPage: NextPage = () => {
}
};
const openInvoiceModal = (userIds: string[] = []) => {
setSelectedUserIdsForInvoice(userIds);
setIsInvoiceModalOpen(true);
};
const handleSendInvoices = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSendingInvoices(true);
setError(null);
setSuccessMessage(null);
const payload: { deadline: string; userIds?: string[] } = { // paypalEmail und cashPaymentContacts entfernt
deadline: invoiceDeadline,
};
if (selectedUserIdsForInvoice.length > 0) {
payload.userIds = selectedUserIdsForInvoice;
}
try {
const response = await fetch('/api/admin/invoices/send-bulk', { // API-Route bleibt gleich
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data: SendInvoicesApiResponse = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Fehler beim Senden der Rechnungs-E-Mails.');
}
setSuccessMessage(`${data.message} Fehler beim Senden an ${data.errors.length} Benutzer (Details in Konsole).`);
if (data.errors.length > 0) {
console.error("Fehler beim E-Mail-Versand für folgende Benutzer:", data.errors);
}
setIsInvoiceModalOpen(false);
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Ein unbekannter Fehler beim Senden der Rechnungs-E-Mails ist aufgetreten.');
}
console.error("Fehler Rechnungsversand:", err);
} finally {
setIsSendingInvoices(false);
}
};
const handleUserSelectionForInvoice = (userId: string, isSelected: boolean) => {
if (isSelected) {
setSelectedUserIdsForInvoice(prev => [...prev, userId]);
} else {
setSelectedUserIdsForInvoice(prev => prev.filter(id => id !== userId));
}
};
if (status === 'loading' || (status === 'authenticated' && session?.user?.role !== 'admin' && !isLoadingUsers) ) {
return (
@ -268,13 +339,12 @@ const AdminPanelPage: NextPage = () => {
return (
<div className="min-h-screen bg-gray-100 dark:bg-slate-900 p-4 sm:p-6 lg:p-8 transition-colors duration-300">
{/* Header angepasst für mobile Ansicht */}
<header className="mb-8 flex flex-col sm:flex-row sm:justify-between sm:items-center">
<div className="mb-4 sm:mb-0"> {/* Div für Admin Panel Text */}
<div className="mb-4 sm:mb-0">
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Admin Panel</h1>
<p className="text-gray-600 dark:text-gray-400">Benutzerverwaltung & Übersicht</p>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4"> {/* Container für Buttons */}
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4">
<button
onClick={() => router.push('/dashboard')}
className="w-full sm:w-auto px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@ -293,7 +363,6 @@ const AdminPanelPage: NextPage = () => {
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md dark:bg-red-900 dark:text-red-200 dark:border-red-700">{error}</div>}
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md dark:bg-green-900 dark:text-green-200 dark:border-green-700">{successMessage}</div>}
{/* Gesamtübersicht Sektion */}
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Gesamtübersicht</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@ -314,7 +383,31 @@ const AdminPanelPage: NextPage = () => {
</div>
</section>
{/* Benutzerliste */}
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Rechnungsversand</h2>
<div className="space-y-3 sm:space-y-0 sm:flex sm:space-x-3">
<button
onClick={() => openInvoiceModal()}
className="w-full sm:w-auto px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 dark:bg-orange-500 dark:hover:bg-orange-600"
>
Rechnung an alle (positiver Saldo)
</button>
<button
onClick={() => {
if (selectedUserIdsForInvoice.length === 0) {
setError("Bitte wähle zuerst Benutzer aus der Liste unten aus.");
return;
}
openInvoiceModal(selectedUserIdsForInvoice);
}}
disabled={selectedUserIdsForInvoice.length === 0}
className="w-full sm:w-auto px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 dark:bg-cyan-500 dark:hover:bg-cyan-600"
>
Rechnung an Ausgewählte ({selectedUserIdsForInvoice.length})
</button>
</div>
</section>
<section className="p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Benutzerübersicht</h2>
{isLoadingUsers ? (
@ -324,6 +417,9 @@ const AdminPanelPage: NextPage = () => {
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-slate-700">
<tr>
<th className="px-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<span className="sr-only">Alle auswählen</span>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">E-Mail</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Saldo</th>
@ -334,7 +430,17 @@ const AdminPanelPage: NextPage = () => {
</thead>
<tbody className="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{users.map((user) => (
<tr key={user.id} className="dark:hover:bg-slate-700/50">
<tr key={user.id} className={`dark:hover:bg-slate-700/50 ${selectedUserIdsForInvoice.includes(user.id) ? 'bg-blue-50 dark:bg-blue-900/30' : ''}`}>
<td className="px-1 py-4 whitespace-nowrap">
{parseFloat(user.balance || "0") > 0 && (
<input
type="checkbox"
className="h-4 w-4 text-indigo-600 border-gray-300 dark:border-gray-600 rounded focus:ring-indigo-500 dark:bg-slate-700 dark:focus:ring-indigo-600"
checked={selectedUserIdsForInvoice.includes(user.id)}
onChange={(e) => handleUserSelectionForInvoice(user.id, e.target.checked)}
/>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{user.name || 'N/A'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{user.email || 'N/A'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-medium">{user.balance ? `${user.balance}` : '0.00 €'}</td>
@ -379,7 +485,74 @@ const AdminPanelPage: NextPage = () => {
)}
</section>
{/* Modal für Saldo anpassen */}
{/* Modal für Rechnungsversand */}
<Transition appear show={isInvoiceModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => setIsInvoiceModalOpen(false)}>
<Transition.Child
as="div" className="fixed inset-0"
enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100"
leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0"
>
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm dark:bg-slate-900/80" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as="div"
className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 p-6 text-left align-middle shadow-xl transition-all"
enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95"
>
<Dialog.Panel>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
Rechnungs-E-Mails senden
</Dialog.Title>
<Dialog.Description className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{selectedUserIdsForInvoice.length > 0
? `E-Mail wird an ${selectedUserIdsForInvoice.length} ausgewählte Benutzer gesendet.`
: "E-Mail wird an alle Benutzer mit positivem Saldo gesendet."
}
<br/>Die PayPal-Adresse und Barzahlungskontakte sind fest hinterlegt.
</Dialog.Description>
<form onSubmit={handleSendInvoices} className="mt-4 space-y-4">
<div>
<label htmlFor="invoiceDeadline" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Zahlungsfrist</label>
<input
type="date"
id="invoiceDeadline"
value={invoiceDeadline}
onChange={(e) => setInvoiceDeadline(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 border border-gray-400 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-slate-700"
/>
</div>
{/* Eingabefelder für PayPal E-Mail und Barzahlungskontakte entfernt */}
<div className="mt-6 flex justify-end space-x-3">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-gray-200 dark:bg-slate-700 px-4 py-2 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
onClick={() => setIsInvoiceModalOpen(false)}
>
Abbrechen
</button>
<button
type="submit"
disabled={isSendingInvoices}
className="inline-flex justify-center rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 disabled:opacity-50 dark:bg-green-500 dark:hover:bg-green-600"
>
{isSendingInvoices ? 'Senden...' : 'E-Mails senden'}
</button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
{/* Andere Modals (Saldo anpassen, Transaktionsverlauf) bleiben unverändert */}
{/* ... Code für AdjustModal und TransactionsModal ... */}
<Transition appear show={isAdjustModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => setIsAdjustModalOpen(false)}>
<Transition.Child
@ -472,7 +645,6 @@ const AdminPanelPage: NextPage = () => {
</Dialog>
</Transition>
{/* Modal für Transaktionsverlauf */}
<Transition appear show={isTransactionsModalOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => setIsTransactionsModalOpen(false)}>
<Transition.Child

View File

@ -0,0 +1,234 @@
// Datei: pages/api/admin/invoices/send-bulk.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { Decimal } from '@prisma/client/runtime/library';
import nodemailer from 'nodemailer';
import {authOptions} from "@/pages/api/auth/[...nextauth]";
import prisma from "@/lib/prisma"
// FESTE KONSTANTEN
const FIXED_PAYPAL_EMAIL = "deine_feste_paypal_adresse@example.com"; // <-- HIER DEINE PAYPAL E-MAIL EINTRAGEN
const FIXED_CASH_PAYMENT_CONTACTS = "Thomas, Elias oder Paul"; // <-- HIER DEINE KONTAKTE EINTRAGEN
interface SendBulkInvoicesRequestBody {
deadline: string;
// paypalEmail: string; // Entfernt
// cashPaymentContacts: string; // Entfernt
userIds?: string[];
}
interface SuccessResponse {
message: string;
sentToCount: number;
errors: { userId: string; error: string }[];
}
interface ErrorResponse {
message: string;
}
// HTML E-Mail Vorlage
function createInvoiceHtml(
userName: string | null,
balance: Decimal,
deadline: string // ISO String
// paypalEmailText und cashPaymentContacts werden jetzt durch Konstanten ersetzt
): string {
const formattedDeadline = new Date(deadline).toLocaleDateString('de-DE', {
year: 'numeric', month: 'long', day: 'numeric'
});
const formattedBalance = balance.toFixed(2);
const paypalMeLink = `http://paypal.me/thomasqst/${formattedBalance}`;
return `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deine Strichlisten-Rechnung</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
h1 { color: #333; }
p { margin-bottom: 10px; }
.highlight { font-weight: bold; color: #d9534f; }
.footer { margin-top: 20px; font-size: 0.9em; color: #777; text-align: center; }
ul { list-style-type: none; padding: 0; }
li { margin-bottom: 8px; }
a { color: #007bff; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>Hallo ${userName || 'Benutzer'},</h1>
<p>dein aktueller Saldo auf der Strichliste beträgt: <span class="highlight">${formattedBalance} </span>.</p>
<p>Bitte begleiche diesen Betrag bis zum <strong>${formattedDeadline}</strong>.</p>
<h2>Zahlungsmöglichkeiten:</h2>
<ul>
<li><strong>PayPal:</strong> <a href="${paypalMeLink}" target="_blank">${paypalMeLink}</a> (oder sende an ${FIXED_PAYPAL_EMAIL})</li>
<li><strong>Bar an:</strong> ${FIXED_CASH_PAYMENT_CONTACTS}</li>
</ul>
<p>Vielen Dank!</p>
<p>Dein Strichlisten-Team</p>
<div class="footer">
<p>Dies ist eine automatisch generierte E-Mail.</p>
</div>
</div>
</body>
</html>
`;
}
// Nodemailer Transporter erstellen
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
tls: {
rejectUnauthorized: true,
}
});
async function sendInvoiceEmail(
userName: string | null,
userEmail: string | null,
balance: Decimal,
deadline: string
// paypalEmailText und cashPaymentContacts entfernt, da Konstanten verwendet werden
): Promise<{ success: boolean; error?: string }> {
if (!userEmail) {
return { success: false, error: "Keine E-Mail-Adresse für den Benutzer vorhanden." };
}
if (!process.env.SMTP_HOST || !process.env.SMTP_USER || !process.env.SMTP_PASS || !process.env.SMTP_FROM_EMAIL) {
console.error("SMTP-Konfiguration unvollständig. E-Mail-Versand übersprungen (simuliert).");
const formattedDeadlineSim = new Date(deadline).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
const formattedBalanceSim = balance.toFixed(2);
const paypalMeLinkSim = `http://paypal.me/thomasqst/${formattedBalanceSim}`;
console.log("----------------------------------------------------");
console.log(`SIMULIERTER E-Mail-Versand an: ${userEmail}`);
console.log(`Betreff: Deine Strichlisten-Rechnung - Saldo: ${formattedBalanceSim}`);
console.log(`Inhalt: Saldo ${formattedBalanceSim}€, Frist ${formattedDeadlineSim}, PayPal Link ${paypalMeLinkSim} (oder an ${FIXED_PAYPAL_EMAIL}), Bar bei ${FIXED_CASH_PAYMENT_CONTACTS}`);
console.log("----------------------------------------------------");
return { success: true };
}
const htmlContent = createInvoiceHtml(userName, balance, deadline); // Parameter entfernt
const mailOptions = {
from: `"${process.env.SMTP_FROM_NAME || 'Strichlisten Team'}" <${process.env.SMTP_FROM_EMAIL}>`,
to: userEmail,
subject: `Deine Strichlisten-Rechnung - Saldo: ${balance.toFixed(2)}`,
html: htmlContent,
};
try {
await transporter.sendMail(mailOptions);
console.log(`Rechnungs-E-Mail erfolgreich gesendet an: ${userEmail}`);
return { success: true };
} catch (e: unknown) {
console.error(`Fehler beim Senden der E-Mail an ${userEmail}:`, e);
if (!(e instanceof Error)) return { success: false, error: 'Unbekannter Fehler beim E-Mail-Versand' };
return { success: false, error: e.message || 'Unbekannter Fehler beim E-Mail-Versand' };
}
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<SuccessResponse | ErrorResponse>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
const session = await getServerSession(req, res, authOptions);
if (!session || !session.user || session.user.role !== 'admin') {
return res.status(403).json({ message: 'Zugriff verweigert. Nur für Administratoren.' });
}
const { deadline, userIds } = req.body as SendBulkInvoicesRequestBody; // paypalEmail und cashPaymentContacts entfernt
if (!deadline) { // Prüfung auf cashPaymentContacts entfernt
return res.status(400).json({ message: 'Frist ist erforderlich.' });
}
try {
new Date(deadline);
} catch (e) {
console.error('Ungültiges Datumsformat für die Frist:', e);
return res.status(400).json({ message: 'Ungültiges Datumsformat für die Frist.' });
}
try {
let usersToInvoice;
if (userIds && userIds.length > 0) {
usersToInvoice = await prisma.user.findMany({
where: {
id: { in: userIds },
balance: { gt: 0 },
isApproved: true,
},
select: { id: true, name: true, email: true, balance: true },
});
} else {
usersToInvoice = await prisma.user.findMany({
where: {
balance: { gt: 0 },
isApproved: true,
},
select: { id: true, name: true, email: true, balance: true },
});
}
if (usersToInvoice.length === 0) {
return res.status(200).json({ message: 'Keine passenden Benutzer für den Rechnungsversand gefunden.', sentToCount: 0, errors: [] });
}
let sentCount = 0;
const emailErrors: { userId: string; error: string }[] = [];
for (const user of usersToInvoice) {
if (user.balance) {
const result = await sendInvoiceEmail(
user.name,
user.email,
user.balance,
deadline
// Parameter für PayPal und Kontakte entfernt
);
if (result.success) {
sentCount++;
} else {
emailErrors.push({ userId: user.id, error: result.error || 'Unbekannter Fehler beim E-Mail-Versand' });
}
}
}
const targetAudience = userIds && userIds.length > 0 ? `${userIds.length} ausgewählte(n)` : "allen";
const finalMessage = emailErrors.length === 0
? `Rechnungs-E-Mails wurden erfolgreich an ${sentCount} von ${usersToInvoice.length} (${targetAudience}) Benutzer(n) gesendet.`
: `Rechnungs-E-Mails wurden an ${sentCount} von ${usersToInvoice.length} (${targetAudience}) Benutzer(n) gesendet. ${emailErrors.length} Fehler sind aufgetreten.`;
return res.status(200).json({
message: finalMessage,
sentToCount: sentCount,
errors: emailErrors,
});
} catch (error: unknown) {
console.error('Fehler beim Senden der Rechnungs-E-Mails:', error);
if (error instanceof Error) {
return res.status(500).json({ message: `Interner Serverfehler: ${error.message}` });
}
return res.status(500).json({ message: 'Ein unbekannter interner Serverfehler ist aufgetreten.' });
}
}