Files
stalendeuren/components/offerte/step-summary.tsx
Ubuntu 3d788740cb 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>
2026-03-01 14:50:31 +00:00

290 lines
9.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { useConfiguratorStore } from "@/lib/store";
import { sendQuoteAction } from "@/actions/send-quote";
import { Button } from "@/components/ui/button";
import { Send, Check, Loader2, CheckCircle2, AlertCircle } from "lucide-react";
// ============================================
// LABEL MAPS
// ============================================
const DOOR_TYPE_LABELS: Record<string, string> = {
taats: "Taatsdeur",
scharnier: "Scharnierdeur",
paneel: "Vast Paneel",
};
const CONFIG_LABELS: Record<string, string> = {
enkele: "Enkele deur",
dubbele: "Dubbele deur",
};
const SIDE_PANEL_LABELS: Record<string, string> = {
geen: "Geen",
links: "Links",
rechts: "Rechts",
beide: "Beide zijden",
};
const FINISH_LABELS: Record<string, string> = {
zwart: "Mat Zwart",
brons: "Brons",
grijs: "Antraciet",
goud: "Goud",
beige: "Beige",
ral: "RAL Kleur",
};
const GLASS_COLOR_LABELS: Record<string, string> = {
helder: "Helder",
grijs: "Rookglas",
brons: "Bronsglas",
"mat-blank": "Mat Blank",
"mat-brons": "Mat Brons",
"mat-zwart": "Mat Zwart",
};
const HANDLE_LABELS: Record<string, string> = {
beugelgreep: "Beugelgreep",
hoekgreep: "Hoekgreep",
maangreep: "Maangreep",
ovaalgreep: "Ovaalgreep",
klink: "Deurklink",
"u-greep": "U-Greep",
geen: "Geen greep",
};
const PATTERN_LABELS: Record<string, string> = {
standard: "Standaard",
"dt9-rounded": "DT9 Afgerond",
"dt10-ushape": "DT10 U-vorm",
};
function formatPrice(amount: number): string {
return new Intl.NumberFormat("nl-NL", {
style: "currency",
currency: "EUR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
}
// ============================================
// COMPONENTS
// ============================================
function SummaryRow({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
return (
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-500">{label}</span>
<span className={`text-sm font-medium ${highlight ? "text-[#C4D668]" : "text-[#1A2E2E]"}`}>
{value}
</span>
</div>
);
}
function SummarySection({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-xl border border-gray-200 bg-white p-4">
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">{title}</h3>
<div className="divide-y divide-gray-100">{children}</div>
</div>
);
}
function PriceRow({ label, amount, bold }: { label: string; amount: number; bold?: boolean }) {
if (amount === 0) return null;
return (
<div className={`flex items-center justify-between py-1.5 ${bold ? "text-base" : "text-sm"}`}>
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-500"}>{label}</span>
<span className={bold ? "font-bold text-[#1A2E2E]" : "text-gray-700"}>
{formatPrice(amount)}
</span>
</div>
);
}
// ============================================
// MAIN COMPONENT
// ============================================
export function StepSummary() {
const store = useConfiguratorStore();
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [errorMsg, setErrorMsg] = useState("");
const {
doorType, gridType, doorConfig, sidePanel,
width, height, doorLeafWidth,
finish, glassColor, handle, frameSize, glassPattern,
extraOptions,
name, email, phone, note,
priceBreakdown, screenshotDataUrl,
} = store;
const canSubmit = name.length >= 2 && email.includes("@") && phone.length >= 10;
async function handleSubmit() {
if (!canSubmit) return;
setStatus("loading");
setErrorMsg("");
const result = await sendQuoteAction({
doorType, gridType, doorConfig, sidePanel,
width, height, doorLeafWidth,
finish, glassColor, handle, frameSize, glassPattern,
extraOptions,
name, email, phone, note,
totalPrice: priceBreakdown.totalPrice,
steelCost: priceBreakdown.steelCost,
glassCost: priceBreakdown.glassCost,
baseFee: priceBreakdown.baseFee,
mechanismSurcharge: priceBreakdown.mechanismSurcharge,
sidePanelSurcharge: priceBreakdown.sidePanelSurcharge,
handleCost: priceBreakdown.handleCost,
finishSurcharge: priceBreakdown.finishSurcharge,
screenshotDataUrl,
});
if (result.success) {
setStatus("success");
} else {
setStatus("error");
setErrorMsg(result.error || "Onbekende fout");
}
}
if (status === "success") {
return (
<div className="flex flex-col items-center py-8 text-center">
<div className="mb-4 flex size-16 items-center justify-center rounded-full bg-green-100">
<CheckCircle2 className="size-8 text-green-600" />
</div>
<h2 className="mb-2 text-xl font-bold text-[#1A2E2E]">Aanvraag Verstuurd!</h2>
<p className="mb-1 text-sm text-gray-600">
Bedankt {name}, uw offerte aanvraag is ontvangen.
</p>
<p className="text-sm text-gray-500">
We sturen een bevestiging naar {email}.
</p>
</div>
);
}
return (
<div className="space-y-4">
<div>
<h2 className="mb-1 text-xl font-bold text-[#1A2E2E]">Overzicht</h2>
<p className="text-sm text-gray-500">
Controleer uw configuratie en verstuur de aanvraag.
</p>
</div>
{/* Product Section */}
<SummarySection title="Product">
<SummaryRow label="Deurtype" value={DOOR_TYPE_LABELS[doorType] || doorType} />
<SummaryRow label="Verdeling" value={gridType} />
<SummaryRow label="Configuratie" value={CONFIG_LABELS[doorConfig] || doorConfig} />
<SummaryRow label="Zijpanelen" value={SIDE_PANEL_LABELS[sidePanel] || sidePanel} />
</SummarySection>
{/* Dimensions Section */}
<SummarySection title="Afmetingen">
<SummaryRow label="Wandopening (breedte)" value={`${width} mm`} />
<SummaryRow label="Hoogte" value={`${height} mm`} />
<SummaryRow label="Deurblad breedte" value={`${Math.round(doorLeafWidth)} mm`} />
</SummarySection>
{/* Style Section */}
<SummarySection title="Stijl">
<SummaryRow label="Afwerking" value={FINISH_LABELS[finish] || finish} />
<SummaryRow label="Glaskleur" value={GLASS_COLOR_LABELS[glassColor] || glassColor} />
<SummaryRow label="Greep" value={HANDLE_LABELS[handle] || handle} />
<SummaryRow label="Profielbreedte" value={`${frameSize} mm`} />
<SummaryRow label="Glaspatroon" value={PATTERN_LABELS[glassPattern] || glassPattern} />
</SummarySection>
{/* Extra Options */}
{extraOptions.length > 0 && (
<SummarySection title="Extra opties">
{extraOptions.map((opt) => (
<div key={opt} className="flex items-center gap-2 py-1.5">
<Check className="size-3.5 text-green-500" />
<span className="text-sm text-[#1A2E2E]">{opt}</span>
</div>
))}
</SummarySection>
)}
{/* Price Breakdown */}
<div className="rounded-xl border-2 border-[#1A2E2E] bg-gradient-to-b from-white to-gray-50 p-4">
<h3 className="mb-3 text-sm font-bold uppercase tracking-wider text-gray-400">
Indicatieprijs
</h3>
<div className="space-y-1">
<PriceRow label="Staal" amount={priceBreakdown.steelCost} />
<PriceRow label="Glas" amount={priceBreakdown.glassCost} />
<PriceRow label="Basiskost" amount={priceBreakdown.baseFee} />
<PriceRow label="Mechanisme toeslag" amount={priceBreakdown.mechanismSurcharge} />
<PriceRow label="Zijpaneel toeslag" amount={priceBreakdown.sidePanelSurcharge} />
<PriceRow label="Greep" amount={priceBreakdown.handleCost} />
<PriceRow label="Kleur toeslag" amount={priceBreakdown.finishSurcharge} />
</div>
<div className="mt-3 border-t-2 border-[#1A2E2E] pt-3">
<PriceRow label="Totaal (indicatie)" amount={priceBreakdown.totalPrice} bold />
</div>
<p className="mt-2 text-[10px] text-gray-400">
* Dit is een indicatieprijs. De definitieve prijs wordt bepaald na opmeting.
</p>
</div>
{/* Contact Summary */}
<SummarySection title="Contactgegevens">
<SummaryRow label="Naam" value={name || "—"} />
<SummaryRow label="E-mail" value={email || "—"} />
<SummaryRow label="Telefoon" value={phone || "—"} />
{note && <SummaryRow label="Opmerking" value={note} />}
</SummarySection>
{/* Validation Warning */}
{!canSubmit && (
<div className="flex items-start gap-2 rounded-lg bg-amber-50 p-3 text-sm text-amber-700">
<AlertCircle className="mt-0.5 size-4 shrink-0" />
<span>Vul eerst uw contactgegevens in (stap 5) om de aanvraag te versturen.</span>
</div>
)}
{/* Error Message */}
{status === "error" && (
<div className="flex items-start gap-2 rounded-lg bg-red-50 p-3 text-sm text-red-700">
<AlertCircle className="mt-0.5 size-4 shrink-0" />
<span>{errorMsg}</span>
</div>
)}
{/* Submit Button */}
<Button
size="lg"
onClick={handleSubmit}
disabled={!canSubmit || status === "loading"}
className="w-full bg-[#C4D668] text-[#1A2E2E] hover:bg-[#b5c75a] disabled:opacity-50"
>
{status === "loading" ? (
<>
<Loader2 className="size-4 animate-spin" />
Verzenden...
</>
) : (
<>
<Send className="size-4" />
Verzend Offerte Aanvraag
</>
)}
</Button>
</div>
);
}