This commit is contained in:
Lars Nolden
2026-08-13 21:52:39 +02:00
parent 33690b30a3
commit 9806427b16
16 changed files with 603 additions and 342 deletions
+333 -289
View File
@@ -1,8 +1,6 @@
import * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight, Euro, Plus, X } from "lucide-react"
import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
@@ -11,12 +9,18 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Checklist } from "@/components/gebos/checklist"
import { DonutIllustration } from "@/components/gebos/illustrations"
import { IconBadge } from "@/components/gebos/icon-tile"
import { Container, Section, SectionHeading } from "@/components/gebos/section"
import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb"
import { icons, vectors } from "@/components/gebos/site-assets"
import {
AssetRender,
renders,
type RenderAsset,
} from "@/components/gebos/site-assets"
import { cn } from "@/lib/utils"
import { useAnimatedNumber } from "@/lib/use-animated-number"
type Heat = "heizkoerper" | "fussbodenheizung" | "gemischt" | "unbekannt"
@@ -24,8 +28,9 @@ type Building = {
id: number
adresse: string
heat: Heat
wohnungen: number
zimmer: number
/** "" while the field is being edited the estimate counts it as 0. */
wohnungen: number | ""
zimmer: number | ""
}
const HEAT_OPTIONS: { value: Heat; label: string }[] = [
@@ -39,9 +44,15 @@ const INITIAL_BUILDINGS: Building[] = [
{ id: 1, adresse: "", heat: "heizkoerper", wohnungen: 12, zimmer: 3 },
]
/** Grundpreise je Wohnung / Monat Selbstmontage, wie in der Klarpreis-Liste. */
const UVI_MONTHLY = 10.5
const INSPECTION_MONTHLY = 2.2
const RWM_ONE_TIME = 42
/** Aufschlag je Monatsposition, wenn GebOS montiert Platzhalter bis zum finalen Preismodell. */
const INSTALL_MONTHLY_SURCHARGE = 2
/** RWM-Hardware je Wohnung einmalig, inklusive Montage wie in der Klarpreis-Liste. */
const RWM_ONE_TIME_INSTALLED = 42
/** Selbstmontage durch Kunde oder Fachbetrieb Platzhalter bis zum finalen Preismodell. */
const RWM_ONE_TIME_SELF = 30
function formatEuro(value: number) {
return value.toLocaleString("de-DE", {
@@ -50,22 +61,55 @@ function formatEuro(value: number) {
})
}
/**
* Keeps a partially edited number field empty instead of snapping it to 0, so
* Backspace clears the input like any other field.
*/
function parseAmount(raw: string): number | "" {
if (raw.trim() === "") return ""
const value = Number(raw)
return Number.isFinite(value) ? Math.max(0, value) : ""
}
function parseCount(raw: string): number | "" {
const value = parseAmount(raw)
return value === "" ? "" : Math.round(value)
}
/** Leaving the field settles an empty or too-small value at the minimum. */
function settle(value: number | "", min: number) {
return value === "" || value < min ? min : value
}
/** glossy numerals for the three configurator steps */
type Step = 1 | 2 | 3
const STEP_BADGES: Record<Step, RenderAsset> = {
1: renders.glassBadge1,
2: renders.glassBadge2,
3: renders.glassBadge3,
}
function StepCard({
step,
title,
children,
}: {
step: number
step: Step
title: string
children: React.ReactNode
}) {
return (
<Card className="gap-0 p-6 sm:p-7">
<div className="flex items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-extrabold text-primary-foreground">
{step}
</span>
<AssetRender
render={STEP_BADGES[step]}
className="flex size-11 shrink-0 items-center justify-center"
imgClassName="w-11"
loading="eager"
/>
<h3 className="text-lg font-extrabold tracking-tight text-navy">
<span className="sr-only">Schritt {step}: </span>
{title}
</h3>
</div>
@@ -74,10 +118,152 @@ function StepCard({
)
}
/**
* Selectable service tile (step 1) — the active option carries a brand border.
*/
function ServiceOption({
checked,
onCheckedChange,
title,
description,
}: {
checked: boolean
onCheckedChange: (checked: boolean) => void
title: string
description: string
}) {
return (
<label
className={cn(
"flex cursor-pointer items-start gap-3 rounded-xl border p-4 transition-colors",
checked
? "border-primary bg-brand-50/60 shadow-pill"
: "border-border bg-card hover:border-brand-200 hover:bg-brand-50/40"
)}
>
<Checkbox
checked={checked}
onCheckedChange={(v) => onCheckedChange(v === true)}
className="mt-0.5"
/>
<span>
<span className="block text-sm font-bold text-navy">{title}</span>
<span className="block text-sm text-muted-foreground">{description}</span>
</span>
</label>
)
}
/**
* Leistungspaket in "Was ist in dem Preis enthalten?". A package the user did
* not pick in step 1 is priced out, so it is shown as struck-out and labelled
* "Nicht enthalten" instead of merely dimmed.
*/
function PackageCard({
assets,
title,
items,
included,
}: {
/** drawn side by side in the header, first one leading */
assets: RenderAsset[]
title: string
items: string[]
included: boolean
}) {
return (
<Card
className={cn(
"gap-0 p-6 transition-colors sm:p-8",
included ? "" : "border-dashed border-border bg-muted/40 shadow-none"
)}
>
<div className="flex flex-wrap items-center gap-x-4 gap-y-3">
<div className="flex shrink-0 items-center gap-1">
{assets.map((asset) => (
<AssetRender
key={asset.webp}
render={asset}
alt=""
className="flex size-20 items-center justify-center"
imgClassName={cn("w-20", !included && "opacity-40 grayscale")}
/>
))}
</div>
<h3
className={cn(
"text-xl font-extrabold tracking-tight",
included ? "text-navy" : "text-muted-foreground"
)}
>
{title}
</h3>
<Badge
variant={included ? "default" : "muted"}
className="px-3 py-1.5 sm:ml-auto"
>
{included ? <Check aria-hidden="true" /> : <X aria-hidden="true" />}
{included ? "Enthalten" : "Nicht enthalten"}
</Badge>
</div>
<Checklist
className="mt-5"
tone={included ? "included" : "excluded"}
items={items}
/>
{included ? null : (
<p className="mt-5 text-sm text-muted-foreground">
Leistung in Schritt 1 auswählen, um sie in den Preis aufzunehmen.
</p>
)}
</Card>
)
}
/**
* One position of the price indication: figure, cadence and whether the
* position can be passed on to tenants (umlagefähig). Wording follows the
* Klarpreis price list.
*/
function PriceRow({
label,
amount,
unit,
note,
allocatable,
}: {
label: string
amount: number
unit: string
note?: string
allocatable: boolean
}) {
const shown = useAnimatedNumber(amount)
return (
<div>
<p className="text-sm font-bold text-navy">{label}</p>
<p className="mt-1 text-3xl font-extrabold tracking-tight text-navy tabular-nums">
ca. {formatEuro(shown)}
{"\u00A0"}
</p>
<p className="mt-0.5 text-sm text-muted-foreground">{unit}</p>
{note ? (
<p className="mt-0.5 text-xs text-muted-foreground">{note}</p>
) : null}
<Badge variant={allocatable ? "default" : "destructive"} className="mt-3">
{allocatable ? <CircleCheck /> : <Ban />}
{allocatable ? "Umlagefähig" : "Nicht umlagefähig"}
</Badge>
</div>
)
}
export default function ConfiguratorPage() {
const [uvi, setUvi] = React.useState(true)
const [rwm, setRwm] = React.useState(true)
const [buildings, setBuildings] = React.useState<Building[]>(INITIAL_BUILDINGS)
const [install, setInstall] = React.useState(true)
const [email, setEmail] = React.useState("")
const nextId = React.useRef(INITIAL_BUILDINGS.length + 1)
@@ -96,18 +282,20 @@ export default function ConfiguratorPage() {
setBuildings((prev) => (prev.length > 1 ? prev.filter((b) => b.id !== id) : prev))
}
const totalBuildings = buildings.length
const totalUnits = buildings.reduce((sum, b) => sum + (b.wohnungen || 0), 0)
const sensors = buildings.reduce(
(sum, b) => sum + Math.round((b.wohnungen || 0) * (b.zimmer || 0) * 0.92),
0
)
const smokeDetectors = buildings.reduce(
(sum, b) => sum + Math.round((b.wohnungen || 0) * ((b.zimmer || 0) + 0.7)),
0
)
const monthly =
(uvi ? UVI_MONTHLY : 0) + (uvi || rwm ? INSPECTION_MONTHLY : 0)
const installSurcharge = install ? INSTALL_MONTHLY_SURCHARGE : 0
const uviMonthly = UVI_MONTHLY + installSurcharge
const inspectionMonthly = INSPECTION_MONTHLY + installSurcharge
const installNote = install
? `inklusive ${formatEuro(INSTALL_MONTHLY_SURCHARGE)}\u00A0€ Installationsanteil`
: undefined
const rwmRatePerUnit = install ? RWM_ONE_TIME_INSTALLED : RWM_ONE_TIME_SELF
/* RWM hardware is billed once per Wohnung the estimate is the sum. */
const rwmOneTimeTotal = rwm ? rwmRatePerUnit * totalUnits : 0
/* the UVI package shows the heat distribution it has to measure */
const heatAsset = buildings.some((b) => b.heat === "fussbodenheizung")
? renders.floorHeating
: renders.radiator
return (
<Container>
@@ -122,55 +310,45 @@ export default function ConfiguratorPage() {
/>
<SectionHeading
title="Konfigurator"
lead="In 4 Schritten zur Preisindikation."
lead="In 3 Schritten zur Preisindikation."
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-6 right-0 hidden items-end lg:flex"
>
<img src={vectors.euroOrb} alt="" className="w-40" />
<img
src={vectors.consumptionRing}
alt=""
className="-mb-3 -ml-6 w-44"
/>
</div>
</div>
<div className="mt-10 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_380px]">
{/* Left column: steps */}
<div className="flex flex-col gap-6">
<StepCard step={1} title="Leistungen wählen">
<div className="flex flex-col gap-4">
<label className="flex cursor-pointer items-start gap-3">
<Checkbox
checked={uvi}
onCheckedChange={(v) => setUvi(v === true)}
className="mt-0.5"
/>
<span>
<span className="block text-sm font-bold text-navy">UVI</span>
<span className="block text-sm text-muted-foreground">
Unterjährige Verbrauchsinformation inklusive Messtechnik
</span>
</span>
</label>
<label className="flex cursor-pointer items-start gap-3">
<Checkbox
checked={rwm}
onCheckedChange={(v) => setRwm(v === true)}
className="mt-0.5"
/>
<span>
<span className="block text-sm font-bold text-navy">
Rauchwarnmelder
</span>
<span className="block text-sm text-muted-foreground">
Rauchwarnmelder inklusive digitaler Dienstleistungen
</span>
</span>
</label>
<div className="flex flex-col gap-3">
<ServiceOption
checked={uvi}
onCheckedChange={setUvi}
title="UVI"
description="Unterjährige Verbrauchsinformation inklusive Messtechnik"
/>
<ServiceOption
checked={rwm}
onCheckedChange={setRwm}
title="Rauchwarnmelder"
description="Rauchwarnmelder inklusive digitaler Dienstleistungen"
/>
</div>
<Separator className="my-6" />
<p className="text-sm font-bold text-navy">Installation</p>
<Tabs
className="mt-3"
value={install ? "mit" : "ohne"}
onValueChange={(v) => setInstall(v === "mit")}
>
<TabsList>
<TabsTrigger value="mit">Mit Installation</TabsTrigger>
<TabsTrigger value="ohne">Ohne Installation</TabsTrigger>
</TabsList>
</Tabs>
<p className="mt-3 text-sm text-muted-foreground">
{install
? "GebOS organisiert die Montage Geräte- und Monatspreise enthalten die Installation."
: "Sie selbst, Ihr Hausmeister oder Ihr Fachbetrieb montiert die Geräte."}
</p>
</StepCard>
<StepCard step={2} title="Gebäude angeben">
@@ -243,10 +421,12 @@ export default function ConfiguratorPage() {
value={b.wohnungen}
onChange={(e) =>
updateBuilding(b.id, {
wohnungen: Math.max(
0,
Math.round(Number(e.target.value) || 0)
),
wohnungen: parseCount(e.target.value),
})
}
onBlur={() =>
updateBuilding(b.id, {
wohnungen: settle(b.wohnungen, 1),
})
}
/>
@@ -260,7 +440,12 @@ export default function ConfiguratorPage() {
value={b.zimmer}
onChange={(e) =>
updateBuilding(b.id, {
zimmer: Math.max(0, Number(e.target.value) || 0),
zimmer: parseAmount(e.target.value),
})
}
onBlur={() =>
updateBuilding(b.id, {
zimmer: settle(b.zimmer, 1),
})
}
/>
@@ -274,46 +459,12 @@ export default function ConfiguratorPage() {
className="self-start"
onClick={addBuilding}
>
<Plus /> Gebäude hinzufügen
<Plus /> weiteres Gebäude hinzufügen
</Button>
</div>
</StepCard>
<StepCard step={3} title="Preis sehen">
<p className="text-sm text-muted-foreground">
Sofortige erste Preisindikation.
</p>
<Accordion type="single" collapsible className="mt-3">
<AccordionItem value="annahmen" className="border-b-0">
<AccordionTrigger className="text-sm font-bold text-navy">
Wie wurde dieser Preis berechnet?
</AccordionTrigger>
<AccordionContent className="text-sm text-muted-foreground">
<p>
Für diese Schätzung gehen wir aufgrund Ihrer Angaben von
ungefähr{" "}
<strong className="text-navy">
{sensors} Heizsensoren und {smokeDetectors}{" "}
Rauchwarnmeldern
</strong>{" "}
aus.
</p>
<p className="mt-2">
Bei Gebäuden mit durchschnittlich drei Zimmern verwenden
wir eine typische Geräteausstattung als
Berechnungsgrundlage.
</p>
<p className="mt-2">
Die endgültige Anzahl wird bei der detaillierten
Konfiguration der Wohnungen bestimmt und ersetzt diese
Schätzwerte.
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
</StepCard>
<StepCard step={4} title="Konfiguration speichern">
<StepCard step={3} title="Konfiguration speichern">
<form
onSubmit={(e) => e.preventDefault()}
className="flex flex-col gap-3"
@@ -332,81 +483,69 @@ export default function ConfiguratorPage() {
Konfiguration speichern &amp; fortsetzen
</Button>
<p className="text-xs text-muted-foreground">
Sie erhalten einen persönlichen Link (gebos.de/konfiguration/)
zu Ihrer Konfiguration sofort weitermachen, später
zurückkehren oder den Link intern weitergeben.
Sie erhalten einen persönlichen Link
zu Ihrer Konfiguration. Damit kannst du
sofort weitermachen, später zurückkehren
oder den Link intern weitergeben.
</p>
</form>
</StepCard>
<div className="flex flex-wrap gap-3">
<Button type="button">Jetzt Preis berechnen</Button>
<Button variant="outline" asChild>
<Link to="/so-funktionierts">So funktioniert der Konfigurator</Link>
</Button>
</div>
</div>
{/* Right column: sticky summary */}
{/* Right column: sticky price indication */}
<div className="lg:sticky lg:top-24">
<Card className="gap-0 p-6 shadow-card-lg sm:p-7">
<h3 className="text-base font-extrabold tracking-tight text-navy">
Ihre vorläufige Konfiguration
</h3>
<div className="mt-4 flex flex-col gap-1">
<div className="text-2xl font-extrabold tracking-tight text-navy">
{totalBuildings} Gebäude
</div>
<div className="text-2xl font-extrabold tracking-tight text-navy">
{totalUnits} {totalUnits === 1 ? "Wohnung" : "Wohnungen"}
</div>
<Card className="gap-0 overflow-hidden p-0 shadow-card-lg">
<div className="flex items-center justify-between gap-3 bg-brand-gradient p-6 text-white sm:p-7">
<p className="text-xs font-bold tracking-[0.14em] uppercase">
Geschätzter Preis
</p>
<IconBadge variant="glass" size="lg">
<Euro />
</IconBadge>
</div>
{uvi || rwm ? (
<>
<Separator className="my-4" />
<p className="text-xs font-bold tracking-[0.14em] text-muted-foreground uppercase">
Voraussichtlich benötigt
</p>
<ul className="mt-2 flex flex-col gap-1.5 text-sm text-navy">
<div className="p-6 sm:p-7">
{uvi || rwm ? (
<div className="flex flex-col gap-5">
{uvi ? (
<li className="font-semibold">
ca. {sensors} Heizsensoren
</li>
<PriceRow
label="UVI-Komplettbetrieb"
amount={uviMonthly}
unit="je Wohnung / Monat"
note={installNote}
allocatable
/>
) : null}
{rwm ? (
<li className="font-semibold">
ca. {smokeDetectors} Rauchwarnmelder
</li>
<>
{uvi ? <Separator /> : null}
<PriceRow
label="Ferninspektion Rauchwarnmelder"
amount={inspectionMonthly}
unit="je Wohnung / Monat"
note={installNote}
allocatable
/>
<Separator />
<PriceRow
label="Rauchwarnmelder-Geräte"
amount={rwmOneTimeTotal}
unit="einmalig gesamt"
note={`${totalUnits} ${totalUnits === 1 ? "Wohnung" : "Wohnungen"} × ${formatEuro(rwmRatePerUnit)}\u00A0€ · ${install ? "inklusive Montage" : "Selbstmontage"}`}
allocatable={false}
/>
</>
) : null}
</ul>
</>
) : null}
<div className="mt-5 rounded-xl bg-brand-gradient p-4 text-white shadow-band">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-2xl font-extrabold tracking-tight">
ca. {formatEuro(monthly)}{"\u00A0"}
</div>
<div className="text-sm text-white/80">
je Wohnung / Monat
</div>
</div>
<IconBadge variant="glass" size="lg">
<Euro />
</IconBadge>
</div>
{rwm ? (
<div className="mt-3 border-t border-white/20 pt-2 text-xs text-white/80">
+ {formatEuro(RWM_ONE_TIME)}{"\u00A0"} je Wohnung einmalig
(RWM-Hardware)
</div>
) : null}
</div>
<div className="mt-5 flex items-start gap-4">
<DonutIllustration className="w-16 shrink-0" />
<p className="text-xs leading-relaxed text-muted-foreground">
) : (
<p className="text-sm text-muted-foreground">
Wählen Sie in Schritt 1 mindestens eine Leistung, um eine
Preisindikation zu erhalten.
</p>
)}
<p className="mt-6 text-xs leading-relaxed text-muted-foreground">
{install
? "Preise inklusive Montage durch GebOS. "
: "Preise ohne Montage Installation durch Sie oder Ihren Fachbetrieb. "}
Die erste Berechnung ist eine unverbindliche Preisindikation.
Der genaue Preis ergibt sich aus der vollständigen
Objektkonfiguration.
@@ -420,131 +559,36 @@ export default function ConfiguratorPage() {
{/* Was ist in der Schätzung enthalten? */}
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
title="Was ist in der Schätzung enthalten?"
lead="Je nach gewähltem Paket beispielsweise:"
title="Was ist in dem Preis enthalten?"
/>
<div className="mt-8 grid gap-6 lg:grid-cols-2">
<Card
className={
"gap-0 p-6 transition-opacity sm:p-8" +
(uvi ? "" : " opacity-50")
}
>
<div className="flex items-center gap-4">
<img src={icons.uvi} alt="" className="size-14" />
<h3 className="text-xl font-extrabold tracking-tight text-navy">
UVI
</h3>
</div>
<Checklist
className="mt-5"
items={[
"geschätzte benötigte Messtechnik",
"GebOS Plattform",
"Datenerfassung",
"Unterjährige Verbrauchsinformation",
"laufender digitaler Betrieb",
]}
/>
</Card>
<Card
className={
"gap-0 p-6 transition-opacity sm:p-8" +
(rwm ? "" : " opacity-50")
}
>
<div className="flex items-center gap-4">
<img src={icons.smokeAlarm} alt="" className="size-14" />
<h3 className="text-xl font-extrabold tracking-tight text-navy">
Rauchwarnmelder
</h3>
</div>
<Checklist
className="mt-5"
items={[
"geschätzte Anzahl Rauchwarnmelder",
"Verwaltung der Geräte",
"gebuchte digitale Dienstleistungen",
]}
/>
</Card>
<PackageCard
assets={[renders.gateway, heatAsset]}
title="UVI"
included={uvi}
items={[
"geschätzte benötigte Messtechnik",
"GebOS Plattform",
"Datenerfassung",
"Unterjährige Verbrauchsinformation",
"laufender digitaler Betrieb",
]}
/>
<PackageCard
assets={[renders.smokeAlarm]}
title="Rauchwarnmelder"
included={rwm}
items={[
"geschätzte Anzahl Rauchwarnmelder",
"Verwaltung der Geräte",
"gebuchte digitale Dienstleistungen",
install
? "Montage und Eigentumsübertragung"
: "Eigentumsübertragung Montage durch Sie",
]}
/>
</div>
</Section>
{/* Stufe 2 So geht es weiter */}
<Section className="pt-0 pb-16 sm:pt-0 sm:pb-20 lg:pt-0 lg:pb-24">
<SectionHeading
eyebrow="So geht es weiter"
title="Erst schätzen. Dann genau konfigurieren."
lead="Der vollständige Konfigurator beginnt nicht wieder bei null. Die bereits erfassten Informationen werden übernommen und die bisherigen Annahmen schrittweise durch echte Daten ersetzt."
/>
<div className="mt-8 flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2.5">
<Badge variant="muted" className="px-3.5 py-1.5 text-sm">
Geschätzt: 72 Heizsensoren
</Badge>
<ArrowRight className="size-4 text-primary" aria-hidden="true" />
<Badge className="px-3.5 py-1.5 text-sm">
Konfiguriert: 68 Heizsensoren
</Badge>
</div>
<div className="flex flex-wrap items-center gap-2.5">
<Badge variant="muted" className="px-3.5 py-1.5 text-sm">
Geschätzt: 96 Rauchwarnmelder
</Badge>
<ArrowRight className="size-4 text-primary" aria-hidden="true" />
<Badge className="px-3.5 py-1.5 text-sm">
Konfiguriert: 103 Rauchwarnmelder
</Badge>
</div>
</div>
<Card className="mt-10 max-w-2xl gap-0 p-6 sm:p-8">
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
Finales Ergebnis
</p>
<h3 className="mt-2 text-xl font-extrabold tracking-tight text-navy">
Ihre GebOS-Konfiguration
</h3>
<p className="mt-1 text-sm font-semibold text-navy">
3 Gebäude · 26 Wohnungen
</p>
<Separator className="my-4" />
<dl className="flex flex-col gap-3 text-sm">
<div>
<dt className="font-bold text-navy">UVI</dt>
<dd className="text-muted-foreground">
68 Heizsensoren / Messgeräte, weitere benötigte Messtechnik
</dd>
</div>
<div>
<dt className="font-bold text-navy">Rauchwarnmelder</dt>
<dd className="text-muted-foreground">103 Rauchwarnmelder</dd>
</div>
<div>
<dt className="font-bold text-navy">Infrastruktur</dt>
<dd className="text-muted-foreground">
technisch ausgelegte Gateway- und Kommunikationsinfrastruktur
</dd>
</div>
</dl>
<Separator className="my-4" />
<p className="text-sm leading-relaxed text-muted-foreground">
Ihr Preis ist dann keine grobe Hochrechnung auf Basis von
Durchschnittswerten mehr, sondern ein Preis auf Grundlage der
tatsächlichen Objektkonfiguration.
</p>
<div className="mt-5">
<Button type="button">Angebot anfordern</Button>
</div>
</Card>
<p className="mt-8 max-w-2xl text-sm leading-relaxed text-muted-foreground">
Ihre Daten werden im gesamten Prozess weiterverwendet:
Preisschätzung Konfiguration Bestellung Gerätevorbereitung
Installation laufender Betrieb.
</p>
</Section>
</Container>
)
}