This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Link, useSearchParams } from "react-router-dom"
|
import { Link, useSearchParams } from "react-router-dom"
|
||||||
import { Check, Plus, X } from "lucide-react"
|
import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react"
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
type RenderAsset,
|
type RenderAsset,
|
||||||
} from "@/components/solenos/site-assets"
|
} from "@/components/solenos/site-assets"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useAnimatedNumber } from "@/lib/use-animated-number"
|
||||||
|
|
||||||
type Heat = "heizkoerper" | "fussbodenheizung" | "gemischt" | "unbekannt"
|
type Heat = "heizkoerper" | "fussbodenheizung" | "gemischt" | "unbekannt"
|
||||||
|
|
||||||
@@ -49,6 +49,21 @@ const INITIAL_BUILDING: Building = {
|
|||||||
zimmer: 3,
|
zimmer: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Grundpreise je Wohnung / Monat – wie in der Klarpreis-Liste. */
|
||||||
|
const UVI_MONTHLY = 10.5
|
||||||
|
const INSPECTION_MONTHLY = 2.2
|
||||||
|
/** RWM-Hardware je Wohnung einmalig – ohne Montage, die eine eigene Position ist. */
|
||||||
|
const RWM_ONE_TIME = 30
|
||||||
|
/** Montage durch SolenOS: je neuem Melder und je Wohnung mit Messtechnik. */
|
||||||
|
const INSTALL_PER_RWM = 10
|
||||||
|
const INSTALL_PER_UNIT = 10
|
||||||
|
|
||||||
|
function formatEuro(value: number) {
|
||||||
|
return value.toLocaleString("de-DE", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps a partially edited number field empty instead of snapping it to 0, so
|
* Keeps a partially edited number field empty instead of snapping it to 0, so
|
||||||
@@ -167,7 +182,8 @@ function StepCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selectable service tile (step 1) — the active option carries a brand border.
|
* Selectable service tile (step 1) — the whole tile is the toggle, the switch
|
||||||
|
* mirrors the state. The active option carries a brand border.
|
||||||
*/
|
*/
|
||||||
function ServiceOption({
|
function ServiceOption({
|
||||||
id,
|
id,
|
||||||
@@ -183,30 +199,22 @@ function ServiceOption({
|
|||||||
description: string
|
description: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border p-4 transition-colors",
|
"flex cursor-pointer items-start justify-between gap-4 rounded-xl border p-4 transition-colors",
|
||||||
checked
|
checked
|
||||||
? "border-primary bg-brand-50/60 shadow-pill"
|
? "border-primary bg-brand-50/60 shadow-pill"
|
||||||
: "border-border bg-card hover:border-brand-200 hover:bg-brand-50/40"
|
: "border-border bg-card hover:border-brand-200 hover:bg-brand-50/40"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Checkbox
|
|
||||||
id={id}
|
|
||||||
checked={checked}
|
|
||||||
onCheckedChange={(value) => onCheckedChange(value === true)}
|
|
||||||
aria-describedby={`${id}-description ${id}-state`}
|
|
||||||
className="mt-0.5 size-6"
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Label
|
<span
|
||||||
htmlFor={id}
|
id={`${id}-title`}
|
||||||
className="cursor-pointer text-sm font-bold leading-snug text-navy"
|
className="text-sm font-bold leading-snug text-navy"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Label>
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
id={`${id}-state`}
|
id={`${id}-state`}
|
||||||
variant={checked ? "default" : "muted"}
|
variant={checked ? "default" : "muted"}
|
||||||
@@ -223,8 +231,15 @@ function ServiceOption({
|
|||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Switch
|
||||||
</div>
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
aria-labelledby={`${id}-title`}
|
||||||
|
aria-describedby={`${id}-description ${id}-state`}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,14 +296,56 @@ function PackageCard({
|
|||||||
/>
|
/>
|
||||||
{included ? null : (
|
{included ? null : (
|
||||||
<p className="mt-5 text-sm text-muted-foreground">
|
<p className="mt-5 text-sm text-muted-foreground">
|
||||||
Leistung in Schritt 1 auswählen, um sie in den Leistungsumfang
|
Leistung in Schritt 1 auswählen, um sie in den Preis aufzunehmen.
|
||||||
aufzunehmen.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</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.
|
||||||
|
*/
|
||||||
|
type Position = {
|
||||||
|
label: string
|
||||||
|
amount: number
|
||||||
|
unit: string
|
||||||
|
note?: string
|
||||||
|
allocatable: boolean
|
||||||
|
/** A deselected position stays on the card as a faint zero. */
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function PriceRow({
|
||||||
|
label,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
note,
|
||||||
|
allocatable,
|
||||||
|
active,
|
||||||
|
}: Position) {
|
||||||
|
const shown = useAnimatedNumber(amount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("transition-opacity", !active && "opacity-45")}>
|
||||||
|
<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() {
|
export default function ConfiguratorPage() {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
@@ -336,6 +393,77 @@ export default function ConfiguratorPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalUnits = buildings.reduce((sum, b) => sum + (b.wohnungen || 0), 0)
|
const totalUnits = buildings.reduce((sum, b) => sum + (b.wohnungen || 0), 0)
|
||||||
|
const unitWord = totalUnits === 1 ? "Wohnung" : "Wohnungen"
|
||||||
|
/*
|
||||||
|
* Melder je Wohnung: einer je Zimmer plus einer im Flur als Rettungsweg
|
||||||
|
* (DIN 14676). Die Hardware wird je Wohnung abgerechnet, die Montage je
|
||||||
|
* Gerät; die endgültige Anzahl ergibt sich aus der Detailkonfiguration.
|
||||||
|
*/
|
||||||
|
const rwmDevices = rwm
|
||||||
|
? Math.round(
|
||||||
|
buildings.reduce(
|
||||||
|
(sum, b) => sum + (b.wohnungen || 0) * ((b.zimmer || 0) + 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
/* UVI und Abrechnung teilen sich die Messtechnik – sie wird einmal montiert. */
|
||||||
|
const meteringUnits = uvi || billing ? totalUnits : 0
|
||||||
|
const installTotal =
|
||||||
|
rwmDevices * INSTALL_PER_RWM + meteringUnits * INSTALL_PER_UNIT
|
||||||
|
|
||||||
|
const installNotes: string[] = []
|
||||||
|
if (install && rwmDevices > 0) {
|
||||||
|
installNotes.push(
|
||||||
|
`${rwmDevices} Rauchwarnmelder × ${formatEuro(INSTALL_PER_RWM)}\u00A0€`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (install && meteringUnits > 0) {
|
||||||
|
installNotes.push(
|
||||||
|
`Messtechnik in ${meteringUnits} ${unitWord} × ${formatEuro(INSTALL_PER_UNIT)}\u00A0€`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every position stays on the card. A deselected one counts down to a faint
|
||||||
|
* zero, so a toggle reads as a change in the price instead of a row that
|
||||||
|
* vanishes.
|
||||||
|
*/
|
||||||
|
const positions: Position[] = [
|
||||||
|
{
|
||||||
|
label: "UVI-Komplettbetrieb",
|
||||||
|
amount: uvi ? UVI_MONTHLY : 0,
|
||||||
|
unit: "je Wohnung / Monat",
|
||||||
|
allocatable: true,
|
||||||
|
active: uvi,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ferninspektion Rauchwarnmelder",
|
||||||
|
amount: rwm ? INSPECTION_MONTHLY : 0,
|
||||||
|
unit: "je Wohnung / Monat",
|
||||||
|
allocatable: true,
|
||||||
|
active: rwm,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Rauchwarnmelder-Geräte",
|
||||||
|
/* Hardware is billed once per Wohnung – the estimate is the sum. */
|
||||||
|
amount: rwm ? RWM_ONE_TIME * totalUnits : 0,
|
||||||
|
unit: "einmalig gesamt",
|
||||||
|
note: rwm
|
||||||
|
? `${totalUnits} ${unitWord} × ${formatEuro(RWM_ONE_TIME)}\u00A0€`
|
||||||
|
: undefined,
|
||||||
|
allocatable: false,
|
||||||
|
active: rwm,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Montage durch SolenOS",
|
||||||
|
amount: install ? installTotal : 0,
|
||||||
|
unit: "einmalig gesamt",
|
||||||
|
note: installNotes.length > 0 ? installNotes.join(" · ") : undefined,
|
||||||
|
allocatable: false,
|
||||||
|
active: install && installTotal > 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -350,7 +478,7 @@ export default function ConfiguratorPage() {
|
|||||||
/>
|
/>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title="Konfigurator"
|
title="Konfigurator"
|
||||||
lead="Leistungen auswählen, Gebäudedaten online erfassen und die nächsten Schritte transparent planen."
|
lead="Leistungen auswählen, Gebäudedaten erfassen und sofort eine Preisindikation erhalten."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -382,22 +510,22 @@ export default function ConfiguratorPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-6" />
|
<Separator className="my-6" />
|
||||||
<div
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-start justify-between gap-4 rounded-xl border p-4 transition-colors",
|
"flex cursor-pointer items-start justify-between gap-4 rounded-xl border p-4 transition-colors",
|
||||||
install
|
install
|
||||||
? "border-primary bg-brand-50/60 shadow-pill"
|
? "border-primary bg-brand-50/60 shadow-pill"
|
||||||
: "border-border bg-card"
|
: "border-border bg-card hover:border-brand-200 hover:bg-brand-50/40"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Label
|
<span
|
||||||
htmlFor="installation"
|
id="installation-title"
|
||||||
className="cursor-pointer text-sm font-bold text-navy"
|
className="text-sm font-bold text-navy"
|
||||||
>
|
>
|
||||||
Montage durch SolenOS
|
Montage durch SolenOS
|
||||||
</Label>
|
</span>
|
||||||
<Badge variant={install ? "default" : "muted"}>
|
<Badge variant={install ? "default" : "muted"}>
|
||||||
{install ? "SolenOS montiert" : "Selbstmontage"}
|
{install ? "SolenOS montiert" : "Selbstmontage"}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -407,7 +535,7 @@ export default function ConfiguratorPage() {
|
|||||||
className="mt-1 text-sm text-muted-foreground"
|
className="mt-1 text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
{install
|
{install
|
||||||
? "SolenOS übernimmt Montage und Softwarekonfiguration. Der geprüfte Gesamtpreis folgt mit dem verbindlichen Angebot."
|
? "SolenOS übernimmt Montage und Softwarekonfiguration – die Montage erscheint als eigene Position."
|
||||||
: "Sie, Ihr Hausmeister oder Ihr Fachbetrieb montieren. Eine Softwarekonfiguration vor Ort ist nicht erforderlich."}
|
: "Sie, Ihr Hausmeister oder Ihr Fachbetrieb montieren. Eine Softwarekonfiguration vor Ort ist nicht erforderlich."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,10 +543,11 @@ export default function ConfiguratorPage() {
|
|||||||
id="installation"
|
id="installation"
|
||||||
checked={install}
|
checked={install}
|
||||||
onCheckedChange={setInstall}
|
onCheckedChange={setInstall}
|
||||||
|
aria-labelledby="installation-title"
|
||||||
aria-describedby="installation-description"
|
aria-describedby="installation-description"
|
||||||
className="mt-0.5"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
</StepCard>
|
</StepCard>
|
||||||
|
|
||||||
<StepCard step={2} title="Gebäudedaten online erfassen">
|
<StepCard step={2} title="Gebäudedaten online erfassen">
|
||||||
@@ -599,105 +728,52 @@ export default function ConfiguratorPage() {
|
|||||||
</StepCard>
|
</StepCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right column: sticky configuration summary */}
|
{/* Right column: sticky price indication */}
|
||||||
<div className="lg:sticky lg:top-24">
|
<div className="lg:sticky lg:top-24">
|
||||||
<Card className="gap-0 overflow-hidden p-0 shadow-card-lg">
|
<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">
|
<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">
|
<p className="text-xs font-bold tracking-[0.14em] uppercase">
|
||||||
Ihre Konfiguration
|
Geschätzter Preis
|
||||||
</p>
|
</p>
|
||||||
<IconBadge variant="glass" size="lg">
|
<IconBadge variant="glass" size="lg">
|
||||||
<Check />
|
<Euro />
|
||||||
</IconBadge>
|
</IconBadge>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 sm:p-7">
|
<div className="p-6 sm:p-7">
|
||||||
<p className="text-sm font-bold text-navy">
|
<p className="text-sm font-bold text-navy">
|
||||||
{buildings.length} Gebäude · {totalUnits}{" "}
|
{buildings.length} Gebäude · {totalUnits} {unitWord}
|
||||||
{totalUnits === 1 ? "Wohnung" : "Wohnungen"}
|
|
||||||
</p>
|
</p>
|
||||||
<Separator className="my-5" />
|
<Separator className="my-5" />
|
||||||
<div aria-live="polite">
|
<div className="flex flex-col gap-5" aria-live="polite">
|
||||||
<p className="text-xs font-bold tracking-wide text-navy uppercase">
|
{positions.map((position, index) => (
|
||||||
Gewählte Leistungen
|
<React.Fragment key={position.label}>
|
||||||
</p>
|
{index > 0 ? <Separator /> : null}
|
||||||
<ul className="mt-3 space-y-3 text-sm">
|
<PriceRow {...position} />
|
||||||
<li
|
</React.Fragment>
|
||||||
className={cn(
|
))}
|
||||||
"flex items-start gap-2",
|
|
||||||
uvi ? "text-navy" : "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{uvi ? (
|
|
||||||
<Check className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
||||||
) : (
|
|
||||||
<X className="mt-0.5 size-4 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
Monatliche Verbrauchsinformation
|
|
||||||
{uvi ? " – enthalten" : " – nicht enthalten"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"flex items-start gap-2",
|
|
||||||
billing ? "text-navy" : "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{billing ? (
|
|
||||||
<Check className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
||||||
) : (
|
|
||||||
<X className="mt-0.5 size-4 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
Heizkostenabrechnung
|
|
||||||
{billing ? " – enthalten" : " – nicht enthalten"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
"flex items-start gap-2",
|
|
||||||
rwm ? "text-navy" : "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{rwm ? (
|
|
||||||
<Check className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
||||||
) : (
|
|
||||||
<X className="mt-0.5 size-4 shrink-0" />
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
Rauchwarnmelder
|
|
||||||
{rwm ? " – enthalten" : " – nicht enthalten"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2 text-navy">
|
|
||||||
<Check className="mt-0.5 size-4 shrink-0 text-primary" />
|
|
||||||
<span>
|
|
||||||
{install
|
|
||||||
? "Montage durch SolenOS"
|
|
||||||
: "Montage durch Sie oder Ihren Fachbetrieb"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-5" />
|
<div className="mt-6 space-y-2 text-xs leading-relaxed text-muted-foreground">
|
||||||
<p className="text-sm font-bold text-navy">
|
{billing ? (
|
||||||
Geprüfter Gesamtpreis nach Detailkonfiguration
|
<p>
|
||||||
|
Die Heizkostenabrechnung wird mit dem verbindlichen
|
||||||
|
Angebot bepreist.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
) : null}
|
||||||
Wir veröffentlichen keine ungeprüften Preiswerte. Nach der
|
<p>
|
||||||
manuellen Prüfung erhalten Sie ein verbindliches
|
Die erste Berechnung ist eine unverbindliche
|
||||||
Gesamtangebot mit der erforderlichen Gateway-Infrastruktur,
|
Preisindikation. Der genaue Preis ergibt sich aus der
|
||||||
den gewählten Leistungen und der gewählten Montagevariante.
|
vollständigen Objektkonfiguration.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Vollständiger Leistungsumfang des späteren Gesamtangebots */}
|
{/* Was ist in dem Preis enthalten? */}
|
||||||
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
<Section className="pt-0 sm:pt-0 lg:pt-0">
|
||||||
<SectionHeading title="Was gehört in das Gesamtangebot?" />
|
<SectionHeading title="Was ist in dem Preis enthalten?" />
|
||||||
<div className="mt-8 grid gap-6 lg:grid-cols-3">
|
<div className="mt-8 grid gap-6 lg:grid-cols-3">
|
||||||
<PackageCard
|
<PackageCard
|
||||||
render={renders.gateway}
|
render={renders.gateway}
|
||||||
@@ -730,7 +806,7 @@ export default function ConfiguratorPage() {
|
|||||||
"Verwaltung und Ferninspektion der Geräte",
|
"Verwaltung und Ferninspektion der Geräte",
|
||||||
"Prüf- und Statusdokumentation",
|
"Prüf- und Statusdokumentation",
|
||||||
install
|
install
|
||||||
? "Montage durch SolenOS im verbindlichen Gesamtangebot"
|
? "Montage durch SolenOS als eigene Position"
|
||||||
: "Montage durch Sie oder Ihren Fachbetrieb",
|
: "Montage durch Sie oder Ihren Fachbetrieb",
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user