add invoice
This commit is contained in:
parent
1e9b2c702b
commit
a25e41d820
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
234
src/pages/api/admin/invoices/send-bulk.ts
Normal file
234
src/pages/api/admin/invoices/send-bulk.ts
Normal 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.' });
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user