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:
Ubuntu
2026-03-01 14:50:31 +00:00
parent 748a5814e7
commit 3d788740cb
110 changed files with 162553 additions and 13070 deletions

234
actions/send-quote.ts Normal file
View 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.",
};
}
}