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 `
- ${logo ? ` ` : `💅 `}
- ${title}
+ ${logo ? ` ` : `💅 `}
+ ${companyName}
+ ${title}
|
@@ -46,6 +50,29 @@ async function renderBrandedEmail(title, bodyHtml) {
+ ${(instagramProfile || tiktokProfile) ? `
+
+
Folge uns auf Social Media:
+
+
+ ` : ''}
© ${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!
+
+ 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,
+});