'use client' import React, { useState, useMemo } from 'react' import type { FDServiceCalculatorBlock as Props } from '@/payload-types' import { fdCardRadius as cardRadius } from '@/utilities/fdTheme' const formatKr = (n: number) => Math.round(n).toLocaleString('sv-SE') + ' kr' /* ── Toggle switch ─────────────────────────────────────────────────────── */ function Toggle({ active, onToggle, label }: { active: boolean; onToggle: () => void; label: string }) { return ( ) } /* ── +/- stepper row ───────────────────────────────────────────────────── */ function ResourceRow({ label, value, onChange, min = 0, max = 999, step = 1, unit, priceLabel, isDark, }: { label: string; value: number; onChange: (v: number) => void min?: number; max?: number; step?: number; unit: string; priceLabel: string; isDark: boolean }) { return (
{label} {priceLabel}
onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))} className="w-20 text-center font-joey-medium text-fd-body-lg rounded-full px-2 py-1.5 border-2 bg-fd-surface-alt border-fd-navy/15 text-fd-navy dark:bg-white/10 dark:border-white/20 dark:text-white [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" style={{ appearance: 'textfield' }} aria-label={label} /> {unit}
) } /* ── Main component ────────────────────────────────────────────────────── */ export const FDServiceCalculatorBlockComponent: React.FC = ({ heading = 'Beräkna din kostnad', description, summaryHeading = 'Kostnadsöversikt', totalLabel = 'Totalt per månad', totalSuffix = 'exkl. moms', orderCtaText = 'Beställ', orderCtaLink = '/kontakt', contactCtaText = 'Frågor? Kontakta oss', contactCtaLink = '/kontakt', sectionBackground = 'white', optionGroups = [], resources = [], addons = [], fixedFees = [], discountPercent, discountLabel, anchorId, }) => { /* ── State: one selected index per option group ──────────────────────── */ const [selectedOptions, setSelectedOptions] = useState>(() => { const initial: Record = {} ;(optionGroups ?? []).forEach((_, i) => { initial[i] = 0 }) return initial }) /* ── State: one number per resource slider ───────────────────────────── */ const [resourceValues, setResourceValues] = useState>(() => { const initial: Record = {} ;(resources ?? []).forEach((r, i) => { initial[i] = r.defaultValue ?? 0 }) return initial }) /* ── State: toggle map for addons ────────────────────────────────────── */ const [addonToggles, setAddonToggles] = useState>({}) const toggleAddon = (i: number) => setAddonToggles((p) => ({ ...p, [i]: !p[i] })) const discount = (discountPercent ?? 0) / 100 const disc = (v: number) => v * (1 - discount) /* ── Derived costs ───────────────────────────────────────────────────── */ const costs = useMemo(() => { // Option groups const optionCosts = (optionGroups ?? []).map((group, gi) => { const selectedIdx = selectedOptions[gi] ?? 0 const option = group.options?.[selectedIdx] return disc(option?.price ?? 0) }) // Resources const resourceCosts = (resources ?? []).map((r, i) => { return disc((resourceValues[i] ?? 0) * (r.pricePerUnit ?? 0)) }) // Addons const addonCosts = (addons ?? []).map((a, i) => { return addonToggles[i] ? disc(a.price ?? 0) : 0 }) // Fixed fees (not discounted) const fixedTotal = (fixedFees ?? []).reduce((sum, f) => sum + (f.amount ?? 0), 0) const total = optionCosts.reduce((a, b) => a + b, 0) + resourceCosts.reduce((a, b) => a + b, 0) + addonCosts.reduce((a, b) => a + b, 0) + fixedTotal return { optionCosts, resourceCosts, addonCosts, fixedTotal, total } }, [selectedOptions, resourceValues, addonToggles, optionGroups, resources, addons, fixedFees, discount]) /* ── Theme ───────────────────────────────────────────────────────────── */ const isDark = sectionBackground === 'navy' const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt dark:bg-fd-navy' : 'bg-white dark:bg-fd-navy' const cardClass = isDark ? `bg-white/5 border-[5px] border-white/10 ${cardRadius}` : `bg-white border-[5px] border-fd-gray-light ${cardRadius} dark:bg-white/5 dark:border-white/10` const headingColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white' const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60' const sectionLabel = isDark ? 'text-white/40' : 'text-fd-navy/35 dark:text-white/40' const summaryLabel = isDark ? 'text-white' : 'text-fd-navy dark:text-white' const categoryLabel = isDark ? 'text-white/40' : 'text-fd-navy/40 dark:text-white/40' const dividerClass = isDark ? 'border-white/10' : 'border-fd-navy/8 dark:border-white/10' const optActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold' const optInactiveClass = isDark ? 'bg-transparent text-white/70 border-white/20 hover:border-white/50 font-joey' : 'bg-transparent text-fd-navy/70 border-fd-navy/20 hover:border-fd-navy/50 font-joey dark:text-white/70 dark:border-white/20 dark:hover:border-white/50' const discStr = discount > 0 ? ` (${discountPercent}% rabatt)` : '' const hasAddons = (addons ?? []).length > 0 const hasFixedFees = (fixedFees ?? []).length > 0 /* ── Build summary rows ──────────────────────────────────────────────── */ const summaryRows: { label: string; cost: number; category?: string }[] = [] // Option group costs ;(optionGroups ?? []).forEach((group, gi) => { const selectedIdx = selectedOptions[gi] ?? 0 const option = group.options?.[selectedIdx] if (option && (option.price ?? 0) > 0) { summaryRows.push({ label: `${group.groupLabel} (${option.label})`, cost: costs.optionCosts[gi], }) } }) // Resource costs ;(resources ?? []).forEach((r, i) => { const val = resourceValues[i] ?? 0 if (val > 0) { const template = r.summaryTemplate || `${r.label} ({value} {unit})` const label = template .replace('{value}', String(val)) .replace('{unit}', r.unit || '') summaryRows.push({ label, cost: costs.resourceCosts[i] }) } }) // Fixed fees ;(fixedFees ?? []).forEach((f) => { summaryRows.push({ label: f.label, cost: f.amount ?? 0 }) }) // Active addons const addonSummaryRows = (addons ?? []).flatMap((a, i) => addonToggles[i] ? [{ label: a.label, cost: costs.addonCosts[i] }] : [], ) const resolvedDiscountLabel = discountLabel ? discountLabel.replace('{percent}', String(discountPercent ?? 0)) : `${discountPercent}% rabatt på alla resurser` return (
{/* ── Header ──────────────────────────────────────────────────── */} {(heading || description) && (
{heading &&

{heading}

} {description &&

{description}

} {discount > 0 && ( {resolvedDiscountLabel} )}
)}
{/* ── Left: Configuration ─────────────────────────────────── */}
{/* Option groups */} {(optionGroups ?? []).map((group, gi) => (
0 ? 'mt-7' : ''}>

{group.groupLabel}

{group.options?.map((opt, oi) => ( ))}
))} {/* Resource sliders */} {(resources ?? []).length > 0 && (
{(resources ?? []).map((r, i) => ( setResourceValues((p) => ({ ...p, [i]: v }))} min={r.min ?? 0} max={r.max ?? 1000} step={r.step ?? 1} unit={r.unit || ''} priceLabel={`${r.pricePerUnit} kr/${r.unit}${discStr}`} isDark={isDark} /> ))}
)} {/* Fixed fees (display only) */} {hasFixedFees && (
{(fixedFees ?? []).map((f, i) => (
{f.label} {formatKr(f.amount ?? 0)}
))}
)} {/* Toggle addons */} {hasAddons && (

Tillvalstjänster

{(addons ?? []).map((addon, i) => (
{addon.label} {addon.price != null && ( {addon.price} kr/mån )} {addon.description && ( {addon.description} )}
toggleAddon(i)} label={addon.label} />
))}
)}
{/* ── Right: Summary ──────────────────────────────────────── */}

{summaryHeading}

{summaryRows.map((row, i) => (
{row.label} {formatKr(row.cost)}
))} {addonSummaryRows.length > 0 && ( <>
Tillval
{addonSummaryRows.map((row, i) => (
{row.label} {formatKr(row.cost)}
))} )} {summaryRows.length === 0 && addonSummaryRows.length === 0 && (

Konfigurera din tjänst för att se kostnaden.

)}
{totalLabel} {formatKr(costs.total)}
{totalSuffix && (

{totalSuffix}

)}
) }