From db1a401230895fd1359b1482aa21fc01fb6f5955 Mon Sep 17 00:00:00 2001 From: elpatron Date: Wed, 8 Oct 2025 11:24:03 +0200 Subject: [PATCH] Build: Update server-dist artifacts for v0.1.4 --- server-dist/index.js | 3 ++ server-dist/lib/email-templates.js | 63 +++++++++++++++++++++++++++++- server-dist/lib/email.js | 25 +++++++----- server-dist/routes/client-entry.js | 2 +- server-dist/rpc/bookings.js | 61 ++++++++++++++++++++++++++++- server-dist/rpc/index.js | 2 + server-dist/rpc/social.js | 10 +++++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 server-dist/rpc/social.js diff --git a/server-dist/index.js b/server-dist/index.js index fa4d17b..f6580e4 100644 --- a/server-dist/index.js +++ b/server-dist/index.js @@ -55,6 +55,9 @@ if (process.env.NODE_ENV === 'production') { app.use('/assets/*', serveStatic({ root: './dist' })); } app.use('/favicon.png', serveStatic({ path: './public/favicon.png' })); +app.use('/AGB.pdf', serveStatic({ path: './public/AGB.pdf' })); +app.use('/icons/*', serveStatic({ root: './public' })); +app.use('/manifest.json', serveStatic({ path: './public/manifest.json' })); app.route("/rpc", rpcApp); app.route("/caldav", caldavApp); app.get("/*", clientEntry); diff --git a/server-dist/lib/email-templates.js b/server-dist/lib/email-templates.js index 89eb897..18cb214 100644 --- a/server-dist/lib/email-templates.js +++ b/server-dist/lib/email-templates.js @@ -28,13 +28,17 @@ async function renderBrandedEmail(title, bodyHtml) { const domain = process.env.DOMAIN || 'localhost:5173'; const protocol = domain.includes('localhost') ? 'http' : 'https'; const homepageUrl = `${protocol}://${domain}`; + const instagramProfile = process.env.INSTAGRAM_PROFILE; + const tiktokProfile = process.env.TIKTOK_PROFILE; + const companyName = process.env.COMPANY_NAME || 'Stargirlnails Kiel'; return `
@@ -46,6 +50,29 @@ async function renderBrandedEmail(title, bodyHtml) {
Zur Website
+ ${(instagramProfile || tiktokProfile) ? ` +
+

Folge uns auf Social Media:

+
+ ${instagramProfile ? ` + + + + + Instagram + + ` : ''} + ${tiktokProfile ? ` + + + + + TikTok + + ` : ''} +
+
+ ` : ''}
© ${new Date().getFullYear()} Stargirlnails Kiel • Professional Nail Care
@@ -256,3 +283,35 @@ export async function renderAdminRescheduleExpiredHTML(params) { `; return renderBrandedEmail("Abgelaufene Terminänderungsvorschläge", inner); } +export async function renderCustomerMessageHTML(params) { + const { customerName, message, appointmentDate, appointmentTime, treatmentName } = params; + const formattedDate = appointmentDate ? formatDateGerman(appointmentDate) : null; + const domain = process.env.DOMAIN || 'localhost:5173'; + const protocol = domain.includes('localhost') ? 'http' : 'https'; + const legalUrl = `${protocol}://${domain}/legal`; + const ownerName = process.env.OWNER_NAME || 'Stargirlnails Kiel'; + const inner = ` +

Hallo ${customerName},

+ ${(appointmentDate && appointmentTime && treatmentName) ? ` +
+

📅 Zu deinem Termin:

+
    +
  • Behandlung: ${treatmentName}
  • +
  • Datum: ${formattedDate}
  • +
  • Uhrzeit: ${appointmentTime}
  • +
+
+ ` : ''} +
+

💬 Nachricht von ${ownerName}:

+
${message.replace(/&/g, '&').replace(//g, '>')}
+
+

Bei Fragen oder Anliegen kannst du einfach auf diese E-Mail antworten – wir helfen dir gerne weiter!

+
+

📋 Rechtliche Informationen:

+

Weitere Informationen findest du in unserem Impressum und Datenschutz.

+
+

Liebe Grüße,
${ownerName}

+ `; + return renderBrandedEmail("Nachricht zu deinem Termin", inner); +} diff --git a/server-dist/lib/email.js b/server-dist/lib/email.js index 6d0a258..472865a 100644 --- a/server-dist/lib/email.js +++ b/server-dist/lib/email.js @@ -97,28 +97,33 @@ export async function sendEmail(params) { console.warn("Resend API key not configured. Skipping email send."); return { success: false }; } + const payload = { + from: params.from || DEFAULT_FROM, + to: Array.isArray(params.to) ? params.to : [params.to], + subject: params.subject, + text: params.text, + html: params.html, + cc: params.cc ? (Array.isArray(params.cc) ? params.cc : [params.cc]) : undefined, + bcc: params.bcc ? (Array.isArray(params.bcc) ? params.bcc : [params.bcc]) : undefined, + reply_to: params.replyTo ? (Array.isArray(params.replyTo) ? params.replyTo : [params.replyTo]) : undefined, + attachments: params.attachments, + }; + console.log(`Sending email via Resend: to=${JSON.stringify(payload.to)}, subject="${params.subject}"`); const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { "Authorization": `Bearer ${RESEND_API_KEY}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - from: params.from || DEFAULT_FROM, - to: Array.isArray(params.to) ? params.to : [params.to], - subject: params.subject, - text: params.text, - html: params.html, - cc: params.cc ? (Array.isArray(params.cc) ? params.cc : [params.cc]) : undefined, - bcc: params.bcc ? (Array.isArray(params.bcc) ? params.bcc : [params.bcc]) : undefined, - attachments: params.attachments, - }), + body: JSON.stringify(payload), }); if (!response.ok) { const body = await response.text().catch(() => ""); console.error("Resend send error:", response.status, body); return { success: false }; } + const responseData = await response.json().catch(() => ({})); + console.log("Resend email sent successfully:", responseData); return { success: true }; } export async function sendEmailWithAGB(params) { diff --git a/server-dist/routes/client-entry.js b/server-dist/routes/client-entry.js index 3f68228..8853a6b 100644 --- a/server-dist/routes/client-entry.js +++ b/server-dist/routes/client-entry.js @@ -24,5 +24,5 @@ export function clientEntry(c) { cssFiles = ["/assets/index-RdX4PbOO.css"]; } } - return c.html(_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { content: "width=device-width, initial-scale=1", name: "viewport" }), _jsx("title", { children: "Stargirlnails Kiel" }), _jsx("link", { rel: "icon", type: "image/png", href: "/favicon.png" }), cssFiles && cssFiles.map((css) => (_jsx("link", { rel: "stylesheet", href: css }, css))), process.env.NODE_ENV === 'production' ? (_jsx("script", { src: jsFile, type: "module" })) : (_jsxs(_Fragment, { children: [_jsx("script", { src: "/@vite/client", type: "module" }), _jsx("script", { src: jsFile, type: "module" })] }))] }), _jsx("body", { children: _jsx("div", { id: "root" }) })] })); + return c.html(_jsxs("html", { lang: "de", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { content: "width=device-width, initial-scale=1", name: "viewport" }), _jsx("meta", { name: "theme-color", content: "#ec4899" }), _jsx("meta", { name: "apple-mobile-web-app-capable", content: "yes" }), _jsx("meta", { name: "apple-mobile-web-app-status-bar-style", content: "default" }), _jsx("meta", { name: "apple-mobile-web-app-title", content: "Stargirlnails" }), _jsx("title", { children: "Stargirlnails Kiel" }), _jsx("link", { rel: "icon", type: "image/png", href: "/favicon.png" }), _jsx("link", { rel: "apple-touch-icon", href: "/icons/apple-touch-icon.png" }), _jsx("link", { rel: "manifest", href: "/manifest.json" }), cssFiles && cssFiles.map((css) => (_jsx("link", { rel: "stylesheet", href: css }, css))), process.env.NODE_ENV === 'production' ? (_jsx("script", { src: jsFile, type: "module" })) : (_jsxs(_Fragment, { children: [_jsx("script", { src: "/@vite/client", type: "module" }), _jsx("script", { src: jsFile, type: "module" })] }))] }), _jsx("body", { children: _jsx("div", { id: "root" }) })] })); } diff --git a/server-dist/rpc/bookings.js b/server-dist/rpc/bookings.js index 2d97b41..1a21505 100644 --- a/server-dist/rpc/bookings.js +++ b/server-dist/rpc/bookings.js @@ -3,7 +3,7 @@ import { z } from "zod"; import { randomUUID } from "crypto"; import { createKV } from "../lib/create-kv.js"; import { sendEmail, sendEmailWithAGBAndCalendar, sendEmailWithInspirationPhoto } from "../lib/email.js"; -import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML, renderAdminBookingNotificationHTML, renderBookingRescheduleProposalHTML, renderAdminRescheduleAcceptedHTML, renderAdminRescheduleDeclinedHTML } from "../lib/email-templates.js"; +import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML, renderAdminBookingNotificationHTML, renderBookingRescheduleProposalHTML, renderAdminRescheduleAcceptedHTML, renderAdminRescheduleDeclinedHTML, renderCustomerMessageHTML } from "../lib/email-templates.js"; import { createORPCClient } from "@orpc/client"; import { RPCLink } from "@orpc/client/fetch"; import { checkBookingRateLimit } from "../lib/rate-limiter.js"; @@ -745,4 +745,63 @@ export const router = { } }; }), + // Admin sendet Nachricht an Kunden + sendCustomerMessage: os + .input(z.object({ + sessionId: z.string(), + bookingId: z.string(), + message: z.string().min(1, "Nachricht darf nicht leer sein").max(5000, "Nachricht ist zu lang (max. 5000 Zeichen)"), + })) + .handler(async ({ input }) => { + await assertOwner(input.sessionId); + const booking = await kv.getItem(input.bookingId); + if (!booking) + throw new Error("Buchung nicht gefunden"); + // Check if booking has customer email + if (!booking.customerEmail) { + throw new Error("Diese Buchung hat keine E-Mail-Adresse. Bitte kontaktiere den Kunden telefonisch."); + } + // Check if booking is in the future + const today = new Date().toISOString().split("T")[0]; + const bookingDate = booking.appointmentDate; + if (bookingDate < today) { + throw new Error("Nachrichten können nur für zukünftige Termine gesendet werden."); + } + // Get treatment name for context + const treatment = await treatmentsKV.getItem(booking.treatmentId); + const treatmentName = treatment?.name || "Behandlung"; + // Prepare email with Reply-To header + const ownerName = process.env.OWNER_NAME || "Stargirlnails Kiel"; + const emailFrom = process.env.EMAIL_FROM || "Stargirlnails "; + const replyToEmail = process.env.ADMIN_EMAIL; + const formattedDate = formatDateGerman(bookingDate); + const html = await renderCustomerMessageHTML({ + customerName: booking.customerName, + message: input.message, + appointmentDate: bookingDate, + appointmentTime: booking.appointmentTime, + treatmentName: treatmentName, + }); + const textContent = `Hallo ${booking.customerName},\n\nZu deinem Termin:\nBehandlung: ${treatmentName}\nDatum: ${formattedDate}\nUhrzeit: ${booking.appointmentTime}\n\nNachricht von ${ownerName}:\n${input.message}\n\nBei Fragen oder Anliegen kannst du einfach auf diese E-Mail antworten – wir helfen dir gerne weiter!\n\nLiebe Grüße,\n${ownerName}`; + // Send email with BCC to admin for monitoring + // Note: Not using explicit 'from' or 'replyTo' to match behavior of other system emails + console.log(`Sending customer message to ${booking.customerEmail} for booking ${input.bookingId}`); + console.log(`Email config: from=${emailFrom}, replyTo=${replyToEmail}, bcc=${replyToEmail}`); + const emailResult = await sendEmail({ + to: booking.customerEmail, + subject: `Nachricht zu deinem Termin am ${formattedDate}`, + text: textContent, + html: html, + bcc: replyToEmail ? [replyToEmail] : undefined, + }); + if (!emailResult.success) { + console.error(`Failed to send customer message to ${booking.customerEmail}`); + throw new Error("E-Mail konnte nicht versendet werden. Bitte überprüfe die E-Mail-Konfiguration oder versuche es später erneut."); + } + console.log(`Successfully sent customer message to ${booking.customerEmail}`); + return { + success: true, + message: `Nachricht wurde erfolgreich an ${booking.customerEmail} gesendet.` + }; + }), }; diff --git a/server-dist/rpc/index.js b/server-dist/rpc/index.js index 6eded94..fbafedc 100644 --- a/server-dist/rpc/index.js +++ b/server-dist/rpc/index.js @@ -7,6 +7,7 @@ import { router as cancellation } from "./cancellation.js"; import { router as legal } from "./legal.js"; import { router as gallery } from "./gallery.js"; import { router as reviews } from "./reviews.js"; +import { router as social } from "./social.js"; export const router = { demo, treatments, @@ -17,4 +18,5 @@ export const router = { legal, gallery, reviews, + social, }; diff --git a/server-dist/rpc/social.js b/server-dist/rpc/social.js new file mode 100644 index 0000000..07cdedb --- /dev/null +++ b/server-dist/rpc/social.js @@ -0,0 +1,10 @@ +import { os } from "@orpc/server"; +const getSocialMedia = os.handler(async () => { + return { + tiktokProfile: process.env.TIKTOK_PROFILE, + instagramProfile: process.env.INSTAGRAM_PROFILE, + }; +}); +export const router = os.router({ + getSocialMedia, +});
- ${logo ? `Stargirlnails` : `
💅
`} -

${title}

+ ${logo ? `${companyName}` : `
💅
`} +
${companyName}
+

${title}