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>
290 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
}
|