feat: Latest production version with interior scene and glass
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>
This commit is contained in:
234
actions/send-quote.ts
Normal file
234
actions/send-quote.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
"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.",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user