Includes room interior with floor, walls, glass you can see through, and all uncommitted production changes that were running live. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
235 lines
7.4 KiB
TypeScript
235 lines
7.4 KiB
TypeScript
"use server";
|
|
|
|
import { Resend } from "resend";
|
|
|
|
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
|
|
interface QuoteRequest {
|
|
// Product
|
|
doorType: string;
|
|
gridType: string;
|
|
doorConfig: string;
|
|
sidePanel: string;
|
|
|
|
// Dimensions
|
|
width: number;
|
|
height: number;
|
|
doorLeafWidth: number;
|
|
|
|
// Options
|
|
finish: string;
|
|
glassColor: string;
|
|
handle: string;
|
|
frameSize: number;
|
|
glassPattern: string;
|
|
|
|
// Extras
|
|
extraOptions: string[];
|
|
|
|
// Contact
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
note: string;
|
|
|
|
// Pricing
|
|
totalPrice: number;
|
|
steelCost: number;
|
|
glassCost: number;
|
|
baseFee: number;
|
|
mechanismSurcharge: number;
|
|
sidePanelSurcharge: number;
|
|
handleCost: number;
|
|
finishSurcharge: number;
|
|
|
|
// Screenshot
|
|
screenshotDataUrl: string | null;
|
|
}
|
|
|
|
const LABEL_MAP: Record<string, string> = {
|
|
taats: "Taatsdeur",
|
|
scharnier: "Scharnierdeur",
|
|
paneel: "Vast Paneel",
|
|
enkele: "Enkele deur",
|
|
dubbele: "Dubbele deur",
|
|
geen: "Geen",
|
|
links: "Links",
|
|
rechts: "Rechts",
|
|
beide: "Beide zijden",
|
|
zwart: "Mat Zwart",
|
|
brons: "Brons",
|
|
grijs: "Antraciet",
|
|
goud: "Goud",
|
|
beige: "Beige",
|
|
ral: "RAL Kleur",
|
|
helder: "Helder glas",
|
|
"mat-blank": "Mat Blank",
|
|
"mat-brons": "Mat Brons",
|
|
"mat-zwart": "Mat Zwart",
|
|
beugelgreep: "Beugelgreep",
|
|
hoekgreep: "Hoekgreep",
|
|
maangreep: "Maangreep",
|
|
ovaalgreep: "Ovaalgreep",
|
|
klink: "Deurklink",
|
|
"u-greep": "U-Greep",
|
|
standard: "Standaard",
|
|
"dt9-rounded": "DT9 Afgerond",
|
|
"dt10-ushape": "DT10 U-vorm",
|
|
};
|
|
|
|
function label(key: string): string {
|
|
return LABEL_MAP[key] || key;
|
|
}
|
|
|
|
function formatPrice(cents: number): string {
|
|
return new Intl.NumberFormat("nl-NL", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(cents);
|
|
}
|
|
|
|
function buildHtmlEmail(data: QuoteRequest): string {
|
|
const rows = [
|
|
["Deurtype", label(data.doorType)],
|
|
["Verdeling", data.gridType],
|
|
["Configuratie", label(data.doorConfig)],
|
|
["Zijpanelen", label(data.sidePanel)],
|
|
["", ""],
|
|
["Breedte (wandopening)", `${data.width} mm`],
|
|
["Hoogte", `${data.height} mm`],
|
|
["Deurblad breedte", `${Math.round(data.doorLeafWidth)} mm`],
|
|
["", ""],
|
|
["Afwerking", label(data.finish)],
|
|
["Glaskleur", label(data.glassColor)],
|
|
["Greep", label(data.handle)],
|
|
["Profielbreedte", `${data.frameSize} mm`],
|
|
["Glaspatroon", label(data.glassPattern)],
|
|
];
|
|
|
|
if (data.extraOptions.length > 0) {
|
|
rows.push(["", ""], ["Extra opties", data.extraOptions.join(", ")]);
|
|
}
|
|
|
|
const tableRows = rows
|
|
.filter(([k]) => k !== "")
|
|
.map(
|
|
([k, v]) =>
|
|
`<tr><td style="padding:8px 12px;border-bottom:1px solid #eee;color:#666;width:40%">${k}</td><td style="padding:8px 12px;border-bottom:1px solid #eee;font-weight:600">${v}</td></tr>`
|
|
)
|
|
.join("");
|
|
|
|
const priceRows = [
|
|
["Staal", formatPrice(data.steelCost)],
|
|
["Glas", formatPrice(data.glassCost)],
|
|
["Basiskost", formatPrice(data.baseFee)],
|
|
...(data.mechanismSurcharge > 0
|
|
? [["Mechanisme toeslag", formatPrice(data.mechanismSurcharge)]]
|
|
: []),
|
|
...(data.sidePanelSurcharge > 0
|
|
? [["Zijpaneel toeslag", formatPrice(data.sidePanelSurcharge)]]
|
|
: []),
|
|
...(data.handleCost > 0 ? [["Greep", formatPrice(data.handleCost)]] : []),
|
|
...(data.finishSurcharge > 0
|
|
? [["Kleur toeslag", formatPrice(data.finishSurcharge)]]
|
|
: []),
|
|
];
|
|
|
|
const priceTableRows = priceRows
|
|
.map(
|
|
([k, v]) =>
|
|
`<tr><td style="padding:6px 12px;color:#666">${k}</td><td style="padding:6px 12px;text-align:right">${v}</td></tr>`
|
|
)
|
|
.join("");
|
|
|
|
return `
|
|
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;background:#fff">
|
|
<div style="background:#1A2E2E;padding:24px;border-radius:12px 12px 0 0">
|
|
<h1 style="color:#C4D668;margin:0;font-size:22px">Nieuwe Offerte Aanvraag</h1>
|
|
<p style="color:#fff;margin:8px 0 0;font-size:14px">Via Proinn Configurator</p>
|
|
</div>
|
|
|
|
<div style="padding:24px;border:1px solid #eee;border-top:0">
|
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 4px">Contactgegevens</h2>
|
|
<p style="margin:0;font-size:15px"><strong>${data.name}</strong></p>
|
|
<p style="margin:4px 0;font-size:14px;color:#666">${data.email} | ${data.phone}</p>
|
|
${data.note ? `<p style="margin:8px 0 0;font-size:14px;color:#333;background:#f7f7f7;padding:12px;border-radius:8px">${data.note}</p>` : ""}
|
|
</div>
|
|
|
|
<div style="padding:24px;border:1px solid #eee;border-top:0">
|
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 12px">Configuratie</h2>
|
|
<table style="width:100%;border-collapse:collapse;font-size:14px">
|
|
${tableRows}
|
|
</table>
|
|
</div>
|
|
|
|
<div style="padding:24px;border:1px solid #eee;border-top:0;border-radius:0 0 12px 12px;background:#f9f9f9">
|
|
<h2 style="color:#1A2E2E;font-size:16px;margin:0 0 12px">Indicatieprijs</h2>
|
|
<table style="width:100%;border-collapse:collapse;font-size:14px">
|
|
${priceTableRows}
|
|
</table>
|
|
<div style="margin-top:12px;padding-top:12px;border-top:2px solid #1A2E2E;text-align:right">
|
|
<span style="font-size:12px;color:#666">Indicatieprijs totaal</span><br>
|
|
<span style="font-size:24px;font-weight:700;color:#1A2E2E">${formatPrice(data.totalPrice)}</span>
|
|
</div>
|
|
<p style="margin:8px 0 0;font-size:11px;color:#999">* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export async function sendQuoteAction(
|
|
data: QuoteRequest
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const html = buildHtmlEmail(data);
|
|
|
|
const attachments: Array<{ filename: string; content: string }> = [];
|
|
if (data.screenshotDataUrl) {
|
|
const base64Data = data.screenshotDataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
attachments.push({
|
|
filename: "deur-configuratie.png",
|
|
content: base64Data,
|
|
});
|
|
}
|
|
|
|
await resend.emails.send({
|
|
from: "Proinn Configurator <noreply@proinn.youztech.nl>",
|
|
to: ["info@proinn.nl"],
|
|
replyTo: data.email,
|
|
subject: `Offerte Aanvraag - ${data.name} - ${label(data.doorType)}`,
|
|
html,
|
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
});
|
|
|
|
// Send confirmation to customer
|
|
await resend.emails.send({
|
|
from: "Proinn <noreply@proinn.youztech.nl>",
|
|
to: [data.email],
|
|
subject: "Uw offerte aanvraag is ontvangen - Proinn",
|
|
html: `
|
|
<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto">
|
|
<h2 style="color:#1A2E2E">Bedankt voor uw aanvraag, ${data.name}!</h2>
|
|
<p>Wij hebben uw configuratie ontvangen en nemen zo snel mogelijk contact met u op.</p>
|
|
<p style="margin-top:16px;padding:16px;background:#f7f7f7;border-radius:8px">
|
|
<strong>Indicatieprijs:</strong> ${formatPrice(data.totalPrice)}<br>
|
|
<small style="color:#666">Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.</small>
|
|
</p>
|
|
<p style="color:#999;font-size:12px;margin-top:24px">
|
|
Proinn Stalen Deuren | proinn.nl
|
|
</p>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error("Failed to send quote email:", error);
|
|
return {
|
|
success: false,
|
|
error: "Er is iets misgegaan bij het versturen. Probeer het opnieuw.",
|
|
};
|
|
}
|
|
}
|