From a25e41d8208263117b6967ae088b7a5616bda161 Mon Sep 17 00:00:00 2001 From: Elias Bennour Date: Sat, 31 May 2025 17:29:06 +0200 Subject: [PATCH] add invoice --- package-lock.json | 10 + package.json | 2 + src/pages/admin/index.tsx | 190 +++++++++++++++++- src/pages/api/admin/invoices/send-bulk.ts | 234 ++++++++++++++++++++++ 4 files changed, 427 insertions(+), 9 deletions(-) create mode 100644 src/pages/api/admin/invoices/send-bulk.ts diff --git a/package-lock.json b/package-lock.json index 7bf5931..f30f9db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 988d296..3ea67b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index e8bc6e0..6032f3a 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -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(() => { + 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([]); + + 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) => { + 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 (
- {/* Header angepasst für mobile Ansicht */}
-
{/* Div für Admin Panel Text */} +

Admin Panel

Benutzerverwaltung & Übersicht

-
{/* Container für Buttons */} +
+ +
+ +

Benutzerübersicht

{isLoadingUsers ? ( @@ -324,6 +417,9 @@ const AdminPanelPage: NextPage = () => { + @@ -334,7 +430,17 @@ const AdminPanelPage: NextPage = () => { {users.map((user) => ( - + + @@ -379,7 +485,74 @@ const AdminPanelPage: NextPage = () => { )} - {/* Modal für Saldo anpassen */} + {/* Modal für Rechnungsversand */} + + setIsInvoiceModalOpen(false)}> + +
+ +
+
+ + + + Rechnungs-E-Mails senden + + + {selectedUserIdsForInvoice.length > 0 + ? `E-Mail wird an ${selectedUserIdsForInvoice.length} ausgewählte Benutzer gesendet.` + : "E-Mail wird an alle Benutzer mit positivem Saldo gesendet." + } +
Die PayPal-Adresse und Barzahlungskontakte sind fest hinterlegt. +
+
+
+ + 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" + /> +
+ {/* Eingabefelder für PayPal E-Mail und Barzahlungskontakte entfernt */} +
+ + +
+ +
+
+
+
+
+
+ + {/* Andere Modals (Saldo anpassen, Transaktionsverlauf) bleiben unverändert */} + {/* ... Code für AdjustModal und TransactionsModal ... */} setIsAdjustModalOpen(false)}> { - {/* Modal für Transaktionsverlauf */} setIsTransactionsModalOpen(false)}> + + + + + Deine Strichlisten-Rechnung + + + +
+

Hallo ${userName || 'Benutzer'},

+

dein aktueller Saldo auf der Strichliste beträgt: ${formattedBalance} €.

+

Bitte begleiche diesen Betrag bis zum ${formattedDeadline}.

+

Zahlungsmöglichkeiten:

+
    +
  • PayPal: ${paypalMeLink} (oder sende an ${FIXED_PAYPAL_EMAIL})
  • +
  • Bar an: ${FIXED_CASH_PAYMENT_CONTACTS}
  • +
+

Vielen Dank!

+

Dein Strichlisten-Team

+ +
+ + + `; +} + +// 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 +) { + 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.' }); + } +}
+ Alle auswählen + Name E-Mail Saldo
+ {parseFloat(user.balance || "0") > 0 && ( + handleUserSelectionForInvoice(user.id, e.target.checked)} + /> + )} + {user.name || 'N/A'} {user.email || 'N/A'} {user.balance ? `${user.balance} €` : '0.00 €'}