feat: Convert email date format from American to European (dd.mm.yyyy)
- Add formatDateGerman() helper function to convert yyyy-mm-dd to dd.mm.yyyy - Update all email templates (HTML and text versions) to use European date format - Apply formatting to booking pending, confirmed, and cancelled emails - Ensure consistent date display across all customer communications - Improve user experience with familiar German date format Changes: - email-templates.ts: Add date formatting to all HTML templates - bookings.ts: Add date formatting to all text email versions - Both files: Consistent European date format (dd.mm.yyyy) throughout
This commit is contained in:
BIN
public/AGB.pdf
Normal file
BIN
public/AGB.pdf
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@ export function BookingForm() {
|
|||||||
const [appointmentDate, setAppointmentDate] = useState("");
|
const [appointmentDate, setAppointmentDate] = useState("");
|
||||||
const [selectedSlotId, setSelectedSlotId] = useState<string>("");
|
const [selectedSlotId, setSelectedSlotId] = useState<string>("");
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
|
const [agbAccepted, setAgbAccepted] = useState(false);
|
||||||
|
|
||||||
const { data: treatments } = useQuery(
|
const { data: treatments } = useQuery(
|
||||||
queryClient.treatments.live.list.experimental_liveOptions()
|
queryClient.treatments.live.list.experimental_liveOptions()
|
||||||
@@ -38,6 +39,10 @@ export function BookingForm() {
|
|||||||
alert("Bitte fülle alle erforderlichen Felder aus");
|
alert("Bitte fülle alle erforderlichen Felder aus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!agbAccepted) {
|
||||||
|
alert("Bitte bestätige die Kenntnisnahme der Allgemeinen Geschäftsbedingungen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const slot = availableSlots.find((s) => s.id === selectedSlotId);
|
const slot = availableSlots.find((s) => s.id === selectedSlotId);
|
||||||
const appointmentTime = slot?.time || "";
|
const appointmentTime = slot?.time || "";
|
||||||
createBooking(
|
createBooking(
|
||||||
@@ -60,6 +65,7 @@ export function BookingForm() {
|
|||||||
setAppointmentDate("");
|
setAppointmentDate("");
|
||||||
setSelectedSlotId("");
|
setSelectedSlotId("");
|
||||||
setNotes("");
|
setNotes("");
|
||||||
|
setAgbAccepted(false);
|
||||||
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
|
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -88,7 +94,7 @@ export function BookingForm() {
|
|||||||
<option value="">Wähle eine Behandlung</option>
|
<option value="">Wähle eine Behandlung</option>
|
||||||
{treatments?.map((treatment) => (
|
{treatments?.map((treatment) => (
|
||||||
<option key={treatment.id} value={treatment.id}>
|
<option key={treatment.id} value={treatment.id}>
|
||||||
{treatment.name} - ${(treatment.price / 100).toFixed(2)} ({treatment.duration} min)
|
{treatment.name} - {(treatment.price / 100).toFixed(2)} € ({treatment.duration} Min)
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -196,6 +202,35 @@ export function BookingForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AGB Acceptance */}
|
||||||
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="agb-acceptance"
|
||||||
|
checked={agbAccepted}
|
||||||
|
onChange={(e) => setAgbAccepted(e.target.checked)}
|
||||||
|
className="mt-1 h-4 w-4 text-pink-600 focus:ring-pink-500 border-gray-300 rounded"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="agb-acceptance" className="text-sm font-medium text-gray-700 cursor-pointer">
|
||||||
|
Ich habe die <a
|
||||||
|
href="/AGB.pdf"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-pink-600 hover:text-pink-700 underline font-semibold"
|
||||||
|
>
|
||||||
|
Allgemeinen Geschäftsbedingungen (AGB)
|
||||||
|
</a> gelesen und akzeptiere diese *
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
📋 Die AGB enthalten wichtige Informationen zu Buchungsgebühren, Stornierungsregeln und unseren Serviceleistungen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
@@ -2,6 +2,12 @@ import { readFile } from "node:fs/promises";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
|
||||||
|
function formatDateGerman(dateString: string): string {
|
||||||
|
const [year, month, day] = dateString.split('-');
|
||||||
|
return `${day}.${month}.${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
let cachedLogoDataUrl: string | null = null;
|
let cachedLogoDataUrl: string | null = null;
|
||||||
|
|
||||||
async function getLogoDataUrl(): Promise<string | null> {
|
async function getLogoDataUrl(): Promise<string | null> {
|
||||||
@@ -47,9 +53,10 @@ async function renderBrandedEmail(title: string, bodyHtml: string): Promise<stri
|
|||||||
|
|
||||||
export async function renderBookingPendingHTML(params: { name: string; date: string; time: string }) {
|
export async function renderBookingPendingHTML(params: { name: string; date: string; time: string }) {
|
||||||
const { name, date, time } = params;
|
const { name, date, time } = params;
|
||||||
|
const formattedDate = formatDateGerman(date);
|
||||||
const inner = `
|
const inner = `
|
||||||
<p>Hallo ${name},</p>
|
<p>Hallo ${name},</p>
|
||||||
<p>wir haben deine Anfrage für <strong>${date}</strong> um <strong>${time}</strong> erhalten.</p>
|
<p>wir haben deine Anfrage für <strong>${formattedDate}</strong> um <strong>${time}</strong> erhalten.</p>
|
||||||
<p>Wir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.</p>
|
<p>Wir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.</p>
|
||||||
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
||||||
`;
|
`;
|
||||||
@@ -58,9 +65,10 @@ export async function renderBookingPendingHTML(params: { name: string; date: str
|
|||||||
|
|
||||||
export async function renderBookingConfirmedHTML(params: { name: string; date: string; time: string }) {
|
export async function renderBookingConfirmedHTML(params: { name: string; date: string; time: string }) {
|
||||||
const { name, date, time } = params;
|
const { name, date, time } = params;
|
||||||
|
const formattedDate = formatDateGerman(date);
|
||||||
const inner = `
|
const inner = `
|
||||||
<p>Hallo ${name},</p>
|
<p>Hallo ${name},</p>
|
||||||
<p>wir haben deinen Termin am <strong>${date}</strong> um <strong>${time}</strong> bestätigt.</p>
|
<p>wir haben deinen Termin am <strong>${formattedDate}</strong> um <strong>${time}</strong> bestätigt.</p>
|
||||||
<p>Wir freuen uns auf dich!</p>
|
<p>Wir freuen uns auf dich!</p>
|
||||||
<div style="background-color: #f8fafc; border-left: 4px solid #db2777; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
<div style="background-color: #f8fafc; border-left: 4px solid #db2777; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
||||||
<p style="margin: 0; font-weight: 600; color: #db2777;">📋 Wichtiger Hinweis:</p>
|
<p style="margin: 0; font-weight: 600; color: #db2777;">📋 Wichtiger Hinweis:</p>
|
||||||
@@ -73,9 +81,10 @@ export async function renderBookingConfirmedHTML(params: { name: string; date: s
|
|||||||
|
|
||||||
export async function renderBookingCancelledHTML(params: { name: string; date: string; time: string }) {
|
export async function renderBookingCancelledHTML(params: { name: string; date: string; time: string }) {
|
||||||
const { name, date, time } = params;
|
const { name, date, time } = params;
|
||||||
|
const formattedDate = formatDateGerman(date);
|
||||||
const inner = `
|
const inner = `
|
||||||
<p>Hallo ${name},</p>
|
<p>Hallo ${name},</p>
|
||||||
<p>dein Termin am <strong>${date}</strong> um <strong>${time}</strong> wurde abgesagt.</p>
|
<p>dein Termin am <strong>${formattedDate}</strong> um <strong>${time}</strong> wurde abgesagt.</p>
|
||||||
<p>Bitte buche einen neuen Termin. Bei Fragen helfen wir dir gerne weiter.</p>
|
<p>Bitte buche einen neuen Termin. Bei Fragen helfen wir dir gerne weiter.</p>
|
||||||
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
||||||
`;
|
`;
|
||||||
|
@@ -6,6 +6,12 @@ import { createKV as createAvailabilityKV } from "@/server/lib/create-kv";
|
|||||||
import { sendEmail, sendEmailWithAGB } from "@/server/lib/email";
|
import { sendEmail, sendEmailWithAGB } from "@/server/lib/email";
|
||||||
import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML } from "@/server/lib/email-templates";
|
import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML } from "@/server/lib/email-templates";
|
||||||
|
|
||||||
|
// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
|
||||||
|
function formatDateGerman(dateString: string): string {
|
||||||
|
const [year, month, day] = dateString.split('-');
|
||||||
|
return `${day}.${month}.${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
const BookingSchema = z.object({
|
const BookingSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
treatmentId: z.string(),
|
treatmentId: z.string(),
|
||||||
@@ -70,11 +76,12 @@ const create = os
|
|||||||
|
|
||||||
// Notify customer: request received (pending)
|
// Notify customer: request received (pending)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
const formattedDate = formatDateGerman(input.appointmentDate);
|
||||||
const html = await renderBookingPendingHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime });
|
const html = await renderBookingPendingHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime });
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: input.customerEmail,
|
to: input.customerEmail,
|
||||||
subject: "Deine Terminanfrage ist eingegangen",
|
subject: "Deine Terminanfrage ist eingegangen",
|
||||||
text: `Hallo ${input.customerName},\n\nwir haben deine Anfrage für ${input.appointmentDate} um ${input.appointmentTime} erhalten. Wir bestätigen deinen Termin in Kürze.\n\nLiebe Grüße\nStargirlnails Kiel`,
|
text: `Hallo ${input.customerName},\n\nwir haben deine Anfrage für ${formattedDate} um ${input.appointmentTime} erhalten. Wir bestätigen deinen Termin in Kürze.\n\nLiebe Grüße\nStargirlnails Kiel`,
|
||||||
html,
|
html,
|
||||||
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -145,20 +152,22 @@ const updateStatus = os
|
|||||||
// Email notifications on status changes
|
// Email notifications on status changes
|
||||||
try {
|
try {
|
||||||
if (input.status === "confirmed") {
|
if (input.status === "confirmed") {
|
||||||
|
const formattedDate = formatDateGerman(booking.appointmentDate);
|
||||||
const html = await renderBookingConfirmedHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
const html = await renderBookingConfirmedHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
||||||
await sendEmailWithAGB({
|
await sendEmailWithAGB({
|
||||||
to: booking.customerEmail,
|
to: booking.customerEmail,
|
||||||
subject: "Dein Termin wurde bestätigt - AGB im Anhang",
|
subject: "Dein Termin wurde bestätigt - AGB im Anhang",
|
||||||
text: `Hallo ${booking.customerName},\n\nwir haben deinen Termin am ${booking.appointmentDate} um ${booking.appointmentTime} bestätigt.\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nBis bald!\nStargirlnails Kiel`,
|
text: `Hallo ${booking.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${booking.appointmentTime} bestätigt.\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nBis bald!\nStargirlnails Kiel`,
|
||||||
html,
|
html,
|
||||||
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
||||||
});
|
});
|
||||||
} else if (input.status === "cancelled") {
|
} else if (input.status === "cancelled") {
|
||||||
|
const formattedDate = formatDateGerman(booking.appointmentDate);
|
||||||
const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime });
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: booking.customerEmail,
|
to: booking.customerEmail,
|
||||||
subject: "Dein Termin wurde abgesagt",
|
subject: "Dein Termin wurde abgesagt",
|
||||||
text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${booking.appointmentDate} um ${booking.appointmentTime} abgesagt. Bitte buche einen neuen Termin.\n\nLiebe Grüße\nStargirlnails Kiel`,
|
text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt. Bitte buche einen neuen Termin.\n\nLiebe Grüße\nStargirlnails Kiel`,
|
||||||
html,
|
html,
|
||||||
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user