Compare commits

...
5 Commits
Author SHA1 Message Date
Lars Nolden 5c5cbd8f3a new configurator
Deploy Static Site / deploy (push) Successful in 10m3s
2026-08-19 22:00:56 +02:00
Lars Nolden 7cfb7fd5ac udpates from session with Guido
Deploy Static Site / deploy (push) Successful in 10m5s
2026-08-19 21:05:01 +02:00
Lars Nolden 467dae14a2 save
Deploy Static Site / deploy (push) Successful in 10m1s
2026-08-14 17:23:27 +02:00
Lars Nolden 44fe30a781 save
Deploy Static Site / deploy (push) Successful in 10m9s
2026-08-14 15:57:41 +02:00
Lars Nolden 4f250eb1c2 save 2026-08-14 15:36:10 +02:00
33 changed files with 3261 additions and 552 deletions
+4
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SolenOS Messdienstleistungen einfach gemacht</title>
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" />
<meta
name="description"
content="SolenOS Component Library shadcn-basierte Komponenten für die SolenOS Landing Pages."
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+17
View File
@@ -13,7 +13,11 @@ import HomePage from "@/pages/home"
import HowItWorksPage from "@/pages/how-it-works"
import KlarpreisPage from "@/pages/klarpreis"
import { SimplePage } from "@/pages/placeholder"
import HeizkostenabrechnungProductPage from "@/pages/products/heizkostenabrechnung"
import MesstechnikProductPage from "@/pages/products/messtechnik-infrastruktur"
import ProductsPage from "@/pages/products"
import RauchwarnmelderProductPage from "@/pages/products/rauchwarnmelder"
import UviProductPage from "@/pages/products/uvi"
import WissenPage from "@/pages/wissen"
import DatenschutzSicherheitPage from "@/pages/wissen/datenschutz-sicherheit"
import GesetzlicheAnforderungenPage from "@/pages/wissen/gesetzliche-anforderungen"
@@ -39,6 +43,19 @@ export default function App() {
<Route path="/" element={<HomePage />} />
<Route path="/so-funktionierts" element={<HowItWorksPage />} />
<Route path="/produkte" element={<ProductsPage />} />
<Route path="/produkte/uvi" element={<UviProductPage />} />
<Route
path="/produkte/heizkostenabrechnung"
element={<HeizkostenabrechnungProductPage />}
/>
<Route
path="/produkte/rauchwarnmelder"
element={<RauchwarnmelderProductPage />}
/>
<Route
path="/produkte/messtechnik-infrastruktur"
element={<MesstechnikProductPage />}
/>
<Route path="/fuer-wen" element={<ForWhomPage />} />
<Route
path="/fuer-wen/hauseigentuemer"
@@ -0,0 +1,704 @@
import * as React from "react"
import { ArrowRight, Plus } from "lucide-react"
import { Link } from "react-router-dom"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Checklist } from "@/components/solenos/checklist"
/**
* One entry of the grid. The same record feeds all three faces a card can
* show, so nothing has to be authored twice: teaser in the grid, single line
* in the rail, editorial spread once it is open.
*/
type ExpandingCard = {
/** stable identity React keys and the open-card state both key off it */
id: string
/** small mark, carried by the teaser and (shrunk) by the rail line */
media?: React.ReactNode
/** larger render; only the open card has the room for it */
visual?: React.ReactNode
title: string
text: string
/** detail bullets, revealed with the open card */
points?: string[]
/** deep link, rendered as the open card's action */
to?: string
/** defaults to "Mehr erfahren" */
linkLabel?: string
}
/** which side of the container the open card sits on the rail takes the other */
type Side = "left" | "right"
/**
* Where the minimised cards are allowed to gather. `auto` mirrors the pointer
* the picked card keeps its half, so the rail changes sides as you browse.
* Pinning it to one side trades that spatial cue for calm: picking out of the
* rail then shifts the column by a single slot instead of sending every card
* across the container.
*/
type RailSide = Side | "auto"
/** the face a card currently shows */
type Face = "brief" | "rail" | "open"
/** matches the `gap-5` of the static section grids this one grows out of */
const GRID_GAP = 20
/** the rail packs tighter than the grid, so a column of five still reads as one block */
const RAIL_GAP = 12
const RAIL_MIN_H = 68
const RAIL_MAX_H = 108
/** container widths, not viewport: the grid has to work in any column */
const TWO_COL = 560
const THREE_COL = 900
/** below this the open card stacks its render under the copy instead of beside it */
const SPREAD_MIN = 520
/** share of the open card reserved for its render */
const VISUAL_SHARE = 0.34
/** stand-in row height for the single render before the first measurement lands */
const FALLBACK_ROW_H = 232
type Frame = { x: number; y: number; w: number; h: number }
type Plan = {
/** absolute box of every card, in container coordinates */
frames: Frame[]
/** height the container has to hold for this state */
height: number
cols: number
/** open card next to a rail false while everything is a grid, or on one column */
split: boolean
/** width a teaser face is laid out at */
cardW: number
/** width an open card gets: the full row on one column, two thirds in the split */
openW: number
railW: number
/** padding of the open face the layout needs it to turn copy into a card height */
pad: number
/** width reserved for the open card's render, 0 while the card is too narrow for it */
visualW: number
/** open card wide enough to set copy beside its render */
spread: boolean
}
const EMPTY_PLAN: Plan = {
frames: [],
height: 0,
cols: 1,
split: false,
cardW: 0,
openW: 0,
railW: 0,
pad: 24,
visualW: 0,
spread: false,
}
const clamp = (value: number, low: number, high: number) =>
Math.min(high, Math.max(low, value))
const pad2 = (n: number) => String(n).padStart(2, "0")
/**
* The whole layout, as one pure function: every card gets an absolute frame, so
* a state change is a transform and a size to interpolate instead of a reflow.
* Grid, rail and open card all fall out of the same three numbers container
* width, measured teaser height, measured open-copy height.
*/
function planLayout({
count,
width,
teaserHeights,
copyHeights,
active,
side,
}: {
count: number
width: number
/** natural height of each teaser face, measured at grid card width */
teaserHeights: number[]
/** natural height of each open-card copy block, measured at open width */
copyHeights: number[]
active: number
side: Side
}): Plan {
if (count === 0 || width <= 0) return EMPTY_PLAN
const cols = width >= THREE_COL ? 3 : width >= TWO_COL ? 2 : 1
const cardW = (width - (cols - 1) * GRID_GAP) / cols
const rows = Math.ceil(count / cols)
/* only trust the measurement once every card has reported, so a half-mounted
grid never bakes a too-short row height into the open card's height */
const measured =
teaserHeights.length === count && teaserHeights.every((h) => h > 0)
? Math.max(...teaserHeights)
: 0
const rowH = measured || FALLBACK_ROW_H
const gridH = rows * rowH + (rows - 1) * GRID_GAP
const railW = (width - GRID_GAP) / 3
const railCount = Math.max(0, count - 1)
const split = active >= 0 && cols > 1 && railCount > 0
const openW = split ? width - railW - GRID_GAP : width
const spread = openW >= SPREAD_MIN
const pad = spread ? 32 : 24
const visualW = spread ? Math.round(openW * VISUAL_SHARE) : 0
const copyH = (i: number) => (copyHeights[i] ?? 0) + 2 * pad
const layout = { cols, split, cardW, openW, railW, pad, visualW, spread }
/* no rail: either the plain grid, or a single column where the open card has
nowhere to go but down */
if (!split) {
const frames: Frame[] = []
const height = (i: number) =>
i === active ? Math.max(copyH(i), rowH) : rowH
if (cols === 1) {
let y = 0
for (let i = 0; i < count; i++) {
frames.push({ x: 0, y, w: cardW, h: height(i) })
y += frames[i].h + GRID_GAP
}
return { ...layout, frames, height: y - GRID_GAP }
}
let bottom = 0
for (let i = 0; i < count; i++) {
const frame = {
x: (i % cols) * (cardW + GRID_GAP),
y: Math.floor(i / cols) * (rowH + GRID_GAP),
w: cardW,
h: height(i),
}
frames.push(frame)
bottom = Math.max(bottom, frame.y + frame.h)
}
return { ...layout, frames, height: Math.max(gridH, bottom) }
}
/* split: the open card keeps the grid's footprint, the rest becomes a column */
const railH = clamp(
(gridH - (railCount - 1) * RAIL_GAP) / railCount,
RAIL_MIN_H,
RAIL_MAX_H
)
const railTotal = railCount * railH + (railCount - 1) * RAIL_GAP
const height = Math.max(gridH, railTotal, copyH(active))
const openX = side === "left" ? 0 : railW + GRID_GAP
const railX = side === "left" ? openW + GRID_GAP : 0
const railTop = (height - railTotal) / 2
const frames: Frame[] = []
let slot = 0
for (let i = 0; i < count; i++) {
if (i === active) {
frames.push({ x: openX, y: 0, w: openW, h: height })
continue
}
frames.push({
x: railX,
y: railTop + slot * (railH + RAIL_GAP),
w: railW,
h: railH,
})
slot++
}
return { ...layout, frames, height }
}
type Metrics = { width: number; teaser: number[]; copy: number[] }
const sameNumbers = (a: number[], b: number[]) =>
a.length === b.length && a.every((v, i) => v === b[i])
const sameMetrics = (a: Metrics, b: Metrics) =>
a.width === b.width &&
sameNumbers(a.teaser, b.teaser) &&
sameNumbers(a.copy, b.copy)
/**
* Reads the container width plus the natural height of the two faces that have
* to size themselves from real copy. Both are laid out at the width they will
* have in their own state, so a measurement never depends on the state the grid
* is in and no card reflows while it travels.
*
* Re-reading on every width change keeps that honest: the pass runs in a layout
* effect, so the corrected heights are in before the browser paints.
*/
function useGridMetrics(count: number) {
const hostRef = React.useRef<HTMLDivElement | null>(null)
const teaserRefs = React.useRef<(HTMLElement | null)[]>([])
const copyRefs = React.useRef<(HTMLElement | null)[]>([])
const [metrics, setMetrics] = React.useState<Metrics>({
width: 0,
teaser: [],
copy: [],
})
React.useLayoutEffect(() => {
const host = hostRef.current
if (!host) return
const heights = (nodes: (HTMLElement | null)[]) =>
nodes
.slice(0, count)
.map((node) =>
node ? Math.ceil(node.getBoundingClientRect().height) : 0
)
const read = () =>
setMetrics((prev) => {
const next: Metrics = {
width: host.clientWidth,
teaser: heights(teaserRefs.current),
copy: heights(copyRefs.current),
}
return sameMetrics(prev, next) ? prev : next
})
read()
const observer = new ResizeObserver(read)
observer.observe(host)
for (const node of [...teaserRefs.current, ...copyRefs.current]) {
if (node) observer.observe(node)
}
return () => observer.disconnect()
/* width is a dep on purpose: a resize changes the width the faces are laid
out at, so their heights have to be read again in the same pre-paint pass */
}, [count, metrics.width])
return { hostRef, teaserRefs, copyRefs, metrics }
}
/** frames travel on ease-out quint: off immediately, settling without overshoot */
const FRAME_MOTION =
"transition-[transform,width,height] duration-[560ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
const FACE_MOTION =
"transition-[opacity,translate,scale] duration-300 ease-out motion-reduce:transition-none"
/** the leaving face clears out at once, the arriving one lets the box move first */
const faceMotion = (visible: boolean) =>
cn(
FACE_MOTION,
visible
? "opacity-100 translate-y-0 delay-[140ms]"
: "opacity-0 translate-y-1"
)
/** faces are decoration for the button underneath only their links take a click */
const FACE_BASE = "pointer-events-none absolute top-0 left-0 z-20"
/** grid face: mark, title, full copy the card as the static section had it */
function TeaserFace({
card,
width,
visible,
innerRef,
}: {
card: ExpandingCard
width: number
visible: boolean
innerRef: (node: HTMLElement | null) => void
}) {
return (
<div
ref={innerRef}
inert={!visible}
aria-hidden={!visible}
style={{ width }}
className={cn(
FACE_BASE,
"flex flex-col items-start gap-3 p-6",
faceMotion(visible)
)}
>
{card.media}
<h3 className="text-base font-bold text-balance text-navy">
{card.title}
</h3>
<p className="text-sm leading-relaxed text-pretty text-muted-foreground">
{card.text}
</p>
</div>
)
}
/** rail face: one line per card, the mark shrunk to keep the row scannable */
function RailFace({
card,
width,
visible,
}: {
card: ExpandingCard
width: number
visible: boolean
}) {
return (
<div
inert={!visible}
aria-hidden={!visible}
style={{ width }}
className={cn(
FACE_BASE,
"flex h-full items-center gap-3 pr-13 pl-5",
faceMotion(visible)
)}
>
<span className="flex size-9 shrink-0 items-center justify-center [&_img]:size-9 [&_picture]:size-9">
{card.media}
</span>
{/* long compounds ("Verbrauchsinformation") have to be allowed to break,
or the clamp never gets to put its ellipsis anywhere */}
<h3 className="line-clamp-2 min-w-0 text-[13px] leading-snug font-bold break-words hyphens-auto text-navy">
{card.title}
</h3>
</div>
)
}
/**
* Open face: index, title, the copy in full, the detail bullets and the deep
* link. Padding is inline because the layout has to add it to the measured copy
* height to know how tall the card must be on a single column.
*/
function OpenFace({
card,
index,
total,
plan,
visible,
panelId,
innerRef,
}: {
card: ExpandingCard
index: number
total: number
plan: Plan
visible: boolean
panelId: string
innerRef: (node: HTMLElement | null) => void
}) {
const { openW, pad, visualW, spread } = plan
return (
<div
id={panelId}
inert={!visible}
aria-hidden={!visible}
style={{
width: openW,
padding: pad,
paddingRight: visualW ? visualW + pad : pad,
}}
className={cn(
FACE_BASE,
"flex h-full flex-col justify-center",
faceMotion(visible)
)}
>
<div ref={innerRef} className="flex max-w-lg flex-col gap-3">
<p className="flex items-center gap-3 text-[11px] font-bold tracking-[0.16em] text-primary uppercase">
<span className="tabular-nums">{pad2(index + 1)}</span>
<span aria-hidden className="h-px w-8 bg-brand-200" />
<span className="tabular-nums text-brand-300">{pad2(total)}</span>
</p>
<h3
className={cn(
"font-extrabold tracking-tight text-balance text-navy",
spread ? "text-2xl" : "text-xl"
)}
>
{card.title}
</h3>
<p className="text-sm leading-relaxed text-pretty text-muted-foreground">
{card.text}
</p>
{card.points?.length ? (
<Checklist items={card.points} className="mt-1" />
) : null}
{card.to ? (
<Button
asChild
variant="link"
className="pointer-events-auto mt-1 h-auto self-start p-0 font-bold"
>
<Link to={card.to}>
{card.linkLabel ?? "Mehr erfahren"}
<ArrowRight />
</Link>
</Button>
) : null}
{!spread && card.visual ? (
<span className="mt-3 flex justify-center">{card.visual}</span>
) : null}
</div>
</div>
)
}
/**
* Toggle disc, borrowed from the FAQ list so both disclosures speak the same
* language: hairline teal plus that fills in and turns into an × once its card
* opens, and slides to the middle of the row while the card is in the rail.
*/
function CardToggleMark({ face }: { face: Face }) {
return (
<span
aria-hidden
className={cn(
"pointer-events-none absolute z-30 flex items-center justify-center rounded-full border border-brand-100 bg-card text-primary shadow-pill",
"transition-all duration-300 motion-reduce:transition-none",
face === "rail"
? "top-1/2 right-4 size-7 -translate-y-1/2"
: "top-5 right-5 size-8",
face === "open"
? "rotate-45 border-transparent bg-primary text-primary-foreground"
: "group-hover:border-brand-200 group-hover:bg-accent"
)}
>
<Plus className="size-4" strokeWidth={2.5} />
</span>
)
}
function GridCard({
card,
index,
total,
face,
frame,
plan,
delay,
animate,
panelId,
onToggle,
teaserRef,
copyRef,
}: {
card: ExpandingCard
index: number
total: number
face: Face
frame: Frame | undefined
plan: Plan
delay: number
animate: boolean
panelId: string
onToggle: () => void
teaserRef: (node: HTMLElement | null) => void
copyRef: (node: HTMLElement | null) => void
}) {
const open = face === "open"
return (
<div
className={cn(
"absolute top-0 left-0",
animate ? FRAME_MOTION : "transition-none",
open ? "z-20" : "z-10"
)}
style={{
transform: `translate3d(${frame?.x ?? 0}px, ${frame?.y ?? 0}px, 0)`,
width: frame?.w ?? 0,
height: frame?.h ?? 0,
transitionDelay: `${delay}ms`,
}}
>
<div
data-face={face}
className={cn(
"group relative size-full overflow-hidden rounded-2xl border bg-card",
"transition-[background-color,border-color,box-shadow,translate] duration-300 motion-reduce:transition-none",
"border-border/70 shadow-card",
"data-[face=brief]:hover:-translate-y-0.5 data-[face=brief]:hover:border-brand-200 data-[face=brief]:hover:shadow-card-lg",
"data-[face=rail]:hover:border-brand-200 data-[face=rail]:hover:bg-accent/40",
"data-[face=open]:border-brand-200 data-[face=open]:shadow-card-lg"
)}
>
<span
aria-hidden
className={cn(
"pointer-events-none absolute inset-0 bg-gradient-to-br from-brand-50 via-card to-card",
FACE_MOTION,
open ? "opacity-100" : "opacity-0"
)}
/>
{plan.visualW && card.visual ? (
<span
aria-hidden
style={{ width: plan.visualW }}
className={cn(
"pointer-events-none absolute inset-y-0 right-0 z-10 flex items-center justify-center p-6",
FACE_MOTION,
open ? "scale-100 opacity-100" : "scale-95 opacity-0"
)}
>
{card.visual}
</span>
) : null}
<button
type="button"
onClick={onToggle}
aria-expanded={open}
aria-controls={panelId}
aria-label={card.title}
className="absolute inset-0 z-10 cursor-pointer rounded-2xl outline-none focus-visible:inset-ring-[3px] focus-visible:inset-ring-ring/45"
/>
<TeaserFace
card={card}
width={plan.cardW}
visible={face === "brief"}
innerRef={teaserRef}
/>
<RailFace card={card} width={plan.railW} visible={face === "rail"} />
<OpenFace
card={card}
index={index}
total={total}
plan={plan}
visible={open}
panelId={panelId}
innerRef={copyRef}
/>
<CardToggleMark face={face} />
</div>
</div>
)
}
/**
* Card grid that unpacks in place. Clicking a card grows it to the full height
* of the grid it was part of, while the remaining cards gather into a column of
* a third of the width beside it by default on the side the open card came
* *from*, so the card you picked stays where you clicked it and everything else
* moves out of its way. Opening a card out of the rail therefore flips the
* composition; pass `railSide` to pin the column and keep the travel short.
*
* Every card is absolutely positioned from measured geometry, which keeps a
* state change to a transform plus a size: the faces are pre-laid out at the
* width they end up with, so no copy re-wraps mid-flight. Under two columns the
* split has no room, and the open card grows in place instead.
*/
function ExpandingCardGrid({
items,
railSide = "auto",
className,
}: {
items: ExpandingCard[]
/** side the minimised cards stack on; `auto` follows the card you clicked */
railSide?: RailSide
className?: string
}) {
const [open, setOpen] = React.useState<{ id: string; side: Side } | null>(
null
)
const { hostRef, teaserRefs, copyRefs, metrics } = useGridMetrics(
items.length
)
const uid = React.useId()
const active = open ? items.findIndex((item) => item.id === open.id) : -1
/* a pinned rail decides the composition outright; the pointer-driven side
stays in state either way, so switching back to `auto` resumes from the
half the reader last picked instead of jumping */
const pinned =
railSide === "auto" ? null : railSide === "left" ? "right" : "left"
const plan = planLayout({
count: items.length,
width: metrics.width,
teaserHeights: metrics.teaser,
copyHeights: metrics.copy,
active,
side: pinned ?? open?.side ?? "left",
})
/* transitions only after the measured geometry has been painted once, and
never for the reflow of a resize dragging a window edge should track the
pointer, not trail half a second behind it */
const [ready, setReady] = React.useState(false)
React.useEffect(() => setReady(true), [])
const painted = React.useRef(0)
const resized = painted.current !== metrics.width
/* written after the commit, not during the render, so React's double
invocation in development cannot swallow the flag */
React.useEffect(() => {
painted.current = metrics.width
})
const animate = ready && !resized
const toggle = (index: number) => {
if (index === active) {
setOpen(null)
return
}
const frame = plan.frames[index]
/* the card keeps its side: whichever half it is sitting in when clicked is
the half it opens into, so the rail forms on the other one unless a
pinned `railSide` overrules it above */
const onRight = frame
? frame.x + frame.w / 2 > metrics.width / 2 + 1
: false
setOpen({ id: items[index].id, side: onRight ? "right" : "left" })
}
let slot = 0
return (
<div
ref={hostRef}
onKeyDown={(event) => {
if (event.key === "Escape" && active >= 0) {
event.stopPropagation()
setOpen(null)
}
}}
style={{ height: plan.height || undefined }}
className={cn(
"relative",
animate
? "transition-[height] duration-[560ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
: "transition-none",
className
)}
>
{items.map((card, index) => {
const face: Face =
index === active ? "open" : plan.split ? "rail" : "brief"
/* the rail arrives as a cascade; everything else moves at once */
const delay = face === "rail" ? slot++ * 26 : 0
return (
<GridCard
key={card.id}
card={card}
index={index}
total={items.length}
face={face}
frame={plan.frames[index]}
plan={plan}
delay={delay}
animate={animate}
panelId={`${uid}-${card.id}`}
onToggle={() => toggle(index)}
teaserRef={(node) => {
teaserRefs.current[index] = node
}}
copyRef={(node) => {
copyRefs.current[index] = node
}}
/>
)
})}
</div>
)
}
export { ExpandingCardGrid }
export type { ExpandingCard }
@@ -26,8 +26,8 @@ function PriceCard({
cadence: React.ReactNode
price: React.ReactNode
unit: React.ReactNode
/** umlagefähig? tone drives soft-teal vs. soft-red pill */
allocation: { label: React.ReactNode; tone: "positive" | "negative" }
/** optional legal cost-allocation note, shown only after review */
allocation?: { label: React.ReactNode; tone: "positive" | "negative" }
description: React.ReactNode
media?: React.ReactNode
className?: string
@@ -47,13 +47,15 @@ function PriceCard({
{price}
</div>
<div className="mt-1.5 text-sm text-muted-foreground">{unit}</div>
<Badge
variant={allocation.tone === "positive" ? "default" : "destructive"}
className="mt-4"
>
{allocation.tone === "positive" ? <CircleCheck /> : <Ban />}
{allocation.label}
</Badge>
{allocation ? (
<Badge
variant={allocation.tone === "positive" ? "default" : "destructive"}
className="mt-4"
>
{allocation.tone === "positive" ? <CircleCheck /> : <Ban />}
{allocation.label}
</Badge>
) : null}
<Separator className="mt-5 mb-4" />
<p className="text-sm leading-relaxed text-muted-foreground">
{description}
@@ -1,4 +1,5 @@
import * as React from "react"
import { ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { IconBadge } from "@/components/solenos/icon-tile"
@@ -155,4 +156,66 @@ function StepRail({
)
}
export { ProcessSteps, NumberedList, StepRail }
/**
* Arrow-connected step flow on a card (home page, "So funktioniert's",
* steps 15): glass numeral, asset render, title and copy per column,
* hairline-divided, with a teal arrow sitting on each divider at render
* height. Arrows and dividers appear only once the columns sit in one row.
*/
function StepFlow({
steps,
className,
}: {
steps: ProcessStep[]
className?: string
}) {
return (
<ol
className={cn(
"grid grid-cols-1 gap-y-10 rounded-3xl bg-card p-5 shadow-card sm:grid-cols-2 sm:p-7 lg:gap-y-0",
steps.length >= 5 ? "lg:grid-cols-5" : "lg:grid-cols-4",
className
)}
>
{steps.map((step, i) => (
<li
key={i}
className={cn(
"flex flex-col items-center text-center",
/* dividers between side-by-side columns of the current layout */
i % 2 === 1 && "sm:border-l",
i > 0 && "lg:border-l"
)}
>
<GlassNumber n={i + 1} className="size-9" />
<div className="relative mt-7 flex h-32 w-full items-end justify-center gap-1">
{i > 0 ? (
<span
aria-hidden="true"
/* breaks the hairline it sits on, at render height */
className="absolute top-1/2 left-0 hidden -translate-x-1/2 -translate-y-1/2 bg-card px-1.5 text-primary lg:block"
>
<ArrowRight className="size-5" />
</span>
) : null}
{step.media ?? (
<IconBadge variant="outline" shape="squircle" size="xl">
{step.icon}
</IconBadge>
)}
</div>
<h3 className="mt-6 px-3 text-[0.9375rem] font-bold text-balance text-navy">
{step.title}
</h3>
{step.description ? (
<p className="mt-2.5 max-w-[14rem] px-2 text-[0.8125rem] leading-relaxed text-balance text-muted-foreground">
{step.description}
</p>
) : null}
</li>
))}
</ol>
)
}
export { ProcessSteps, NumberedList, StepFlow, StepRail }
@@ -8,6 +8,13 @@ const COLUMNS: { title: string; links: { to: string; label: string }[] }[] = [
title: "Produkt",
links: [
{ to: "/produkte", label: "Produkte" },
{ to: "/produkte/uvi", label: "Verbrauchsinformation (UVI)" },
{ to: "/produkte/heizkostenabrechnung", label: "Heizkostenabrechnung" },
{ to: "/produkte/rauchwarnmelder", label: "Rauchwarnmelder" },
{
to: "/produkte/messtechnik-infrastruktur",
label: "Messtechnik und Infrastruktur",
},
{ to: "/so-funktionierts", label: "So funktionierts" },
{ to: "/konfigurator", label: "Konfigurator" },
{ to: "/klarpreis", label: "SolenOS Klarpreis" },
@@ -1,5 +1,5 @@
import * as React from "react"
import { ChevronDown, Menu } from "lucide-react"
import { Check, ChevronDown, Menu } from "lucide-react"
import { Link, NavLink, matchPath, useLocation } from "react-router-dom"
import { cn } from "@/lib/utils"
@@ -22,17 +22,28 @@ const AUDIENCES = [
{ to: "/fuer-wen/installateure", label: "Installateure & Heizungsbauer" },
]
/** mirrors the product registry in `@/pages/products/product-page` */
const PRODUCTS = [
{ to: "/produkte/uvi", label: "Verbrauchsinformation (UVI)" },
{ to: "/produkte/heizkostenabrechnung", label: "Heizkostenabrechnung" },
{ to: "/produkte/rauchwarnmelder", label: "Rauchwarnmelder" },
{
to: "/produkte/messtechnik-infrastruktur",
label: "Messtechnik und Infrastruktur",
},
]
type NavItem = {
to: string
label: string
/** present ⇒ rendered as a dropdown; `to` stays the section root that marks it active */
menu?: typeof AUDIENCES
menu?: { to: string; label: string }[]
}
/** Source of truth for nav order desktop pill, dropdown and mobile sheet all read it. */
const NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Start" },
{ to: "/produkte", label: "Produkte" },
{ to: "/produkte", label: "Produkte", menu: PRODUCTS },
{ to: "/so-funktionierts", label: "So funktionierts" },
{ to: "/fuer-wen", label: "Für wen?", menu: AUDIENCES },
{ to: "/wissen", label: "Wissen" },
@@ -120,7 +131,33 @@ function useActivePill(activeKey: string | undefined) {
return { navRef, pill }
}
function AudienceMenu({ item, active }: { item: NavItem; active: boolean }) {
function MenuLink({
to,
label,
pathname,
}: {
to: string
label: string
pathname: string
}) {
const current = Boolean(matchPath({ path: to, end: true }, pathname))
return (
<DropdownMenuItem
asChild
className={cn(current && "bg-accent font-bold text-brand-800")}
>
<Link to={to} aria-current={current ? "page" : undefined}>
<span className="min-w-0 flex-1">{label}</span>
{current ? <Check aria-hidden="true" className="size-4" /> : null}
</Link>
</DropdownMenuItem>
)
}
function NavMenu({ item, active }: { item: NavItem; active: boolean }) {
const { pathname } = useLocation()
return (
<DropdownMenu>
<DropdownMenuTrigger
@@ -134,14 +171,10 @@ function AudienceMenu({ item, active }: { item: NavItem; active: boolean }) {
<ChevronDown className="size-3.5 opacity-60" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem asChild>
<Link to={item.to}>Übersicht</Link>
</DropdownMenuItem>
<MenuLink to={item.to} label="Übersicht" pathname={pathname} />
<DropdownMenuSeparator />
{item.menu?.map((a) => (
<DropdownMenuItem key={a.to} asChild>
<Link to={a.to}>{a.label}</Link>
</DropdownMenuItem>
{item.menu?.map((link) => (
<MenuLink key={link.to} {...link} pathname={pathname} />
))}
</DropdownMenuContent>
</DropdownMenu>
@@ -171,7 +204,7 @@ function DesktopNav() {
)}
{NAV_ITEMS.map((item) =>
item.menu ? (
<AudienceMenu key={item.to} item={item} active={item.to === activeKey} />
<NavMenu key={item.to} item={item} active={item.to === activeKey} />
) : (
<NavLink
key={item.to}
@@ -189,6 +222,8 @@ function DesktopNav() {
}
function MobileMenu() {
const { pathname } = useLocation()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -197,26 +232,25 @@ function MobileMenu() {
<span className="sr-only">Menü öffnen</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuContent align="end" className="w-72">
{NAV_ITEMS.map((item) =>
item.menu ? (
<React.Fragment key={item.to}>
<DropdownMenuSeparator />
<DropdownMenuLabel>{item.label}</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link to={item.to}>Übersicht</Link>
</DropdownMenuItem>
{item.menu.map((a) => (
<DropdownMenuItem key={a.to} asChild>
<Link to={a.to}>{a.label}</Link>
</DropdownMenuItem>
<MenuLink to={item.to} label="Übersicht" pathname={pathname} />
{item.menu.map((link) => (
<MenuLink key={link.to} {...link} pathname={pathname} />
))}
<DropdownMenuSeparator />
</React.Fragment>
) : (
<DropdownMenuItem key={item.to} asChild>
<Link to={item.to}>{item.label}</Link>
</DropdownMenuItem>
<MenuLink
key={item.to}
to={item.to}
label={item.label}
pathname={pathname}
/>
)
)}
<DropdownMenuSeparator />
@@ -246,9 +280,6 @@ function SiteHeader() {
<DesktopNav />
<div className="flex items-center gap-3">
{/* <Button asChild size="sm" className="hidden sm:inline-flex">
<Link to="/konfigurator">Preis berechnen</Link>
</Button> */}
<Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
<a href="https://app.gebos.online">Login</a>
</Button>
@@ -0,0 +1,69 @@
import type * as React from "react"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
import { IconBadge } from "@/components/solenos/icon-tile"
/** The paragraph of a regulation a property is cited against. */
type LegalSource = {
cite: string
href: string
}
/** One technical property: line icon, title, body, optional legal citation. */
type Topic = {
icon: React.ReactNode
title: string
body: string
source?: LegalSource
}
/** Cited source line, pinned to the card's baseline so rows stay aligned. */
function LegalSourceNote({ source }: { source: LegalSource }) {
return (
<p className="mt-auto pt-4 text-xs text-muted-foreground">
Quelle:{" "}
<a
href={source.href}
target="_blank"
rel="noreferrer"
className="font-medium text-brand-700 underline-offset-2 transition-colors hover:text-brand-800 hover:underline"
>
{source.cite}
</a>
</p>
)
}
/** Property grid: line icon over a navy title, body copy, optional citation. */
function TopicCards({
items,
className,
}: {
items: Topic[]
className?: string
}) {
return (
<div className={cn("mt-10 grid gap-5", className)}>
{items.map((item) => (
<Card key={item.title}>
<CardContent className="flex flex-1 flex-col">
<IconBadge variant="outline" shape="squircle" size="lg">
{item.icon}
</IconBadge>
<h3 className="mt-5 text-base font-bold text-balance text-navy">
{item.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
{item.body}
</p>
{item.source ? <LegalSourceNote source={item.source} /> : null}
</CardContent>
</Card>
))}
</div>
)
}
export { LegalSourceNote, TopicCards }
export type { LegalSource, Topic }
@@ -1,4 +1,6 @@
import * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { AssetRender, renders } from "@/components/solenos/site-assets"
@@ -6,6 +8,7 @@ import { AssetRender, renders } from "@/components/solenos/site-assets"
type TrustPanelItem = {
icon: React.ReactNode
label: React.ReactNode
to?: string
}
/**
@@ -14,6 +17,38 @@ type TrustPanelItem = {
* so the labels stay on one line next to the security render, with the
* follow-up link on the baseline underneath.
*/
function TrustProperty({ item }: { item: TrustPanelItem }) {
const content = (
<>
<span className="flex shrink-0 text-brand-600 [&_svg]:size-7">
{item.icon}
</span>
<span className="min-w-0 flex-1 text-[0.9375rem] leading-snug font-medium text-balance text-navy/75">
{item.label}
</span>
{item.to ? (
<ArrowRight
aria-hidden="true"
className="size-4 shrink-0 text-brand-600"
/>
) : null}
</>
)
const className =
"flex items-center gap-4 rounded-xl bg-card px-5 py-3.5 shadow-card"
return item.to ? (
<Link
to={item.to}
className={`${className} outline-none transition-colors hover:bg-brand-50 focus-visible:ring-[3px] focus-visible:ring-ring/40`}
>
{content}
</Link>
) : (
<div className={className}>{content}</div>
)
}
function TrustPanel({
title,
items,
@@ -44,17 +79,7 @@ function TrustPanel({
</h2>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{items.map((item, i) => (
<div
key={i}
className="bg-card shadow-card flex items-center gap-5 rounded-xl px-5 py-3.5"
>
<span className="text-brand-600 flex shrink-0 [&_svg]:size-7">
{item.icon}
</span>
<span className="text-navy/75 text-[0.9375rem] leading-snug font-medium text-balance">
{item.label}
</span>
</div>
<TrustProperty key={i} item={item} />
))}
</div>
</div>
+9
View File
@@ -0,0 +1,9 @@
/** gesetze-im-internet base for the Heizkostenverordnung. */
const HEIZKOSTENV = "https://www.gesetze-im-internet.de/heizkostenv"
/** Permalink to a single paragraph, e.g. `heizkostenv("6a")`. */
function heizkostenv(paragraph: string) {
return `${HEIZKOSTENV}/__${paragraph}.html`
}
export { heizkostenv }
@@ -29,6 +29,7 @@ function AudienceBreadcrumb({ current }: { current: string }) {
*/
function AudiencePage({
breadcrumb,
eyebrow,
title,
tagline,
lead,
@@ -40,6 +41,7 @@ function AudiencePage({
closing,
}: {
breadcrumb: string
eyebrow?: React.ReactNode
title: React.ReactNode
/** short bold subline under the H1 (e.g. "Bestellen. Montieren. Fertig.") */
tagline?: React.ReactNode
@@ -60,6 +62,7 @@ function AudiencePage({
<>
<PageHero
breadcrumb={<AudienceBreadcrumb current={breadcrumb} />}
eyebrow={eyebrow}
title={title}
lead={
<>
@@ -110,9 +110,9 @@ const SELF_INSTALL_STEPS = [
media: (
<AssetRender render={renders.houseSmallIso} imgClassName="w-28" />
),
title: "Gebäude angeben",
title: "Gebäudedaten online erfassen",
description:
"Daten zu Ihrem Gebäude und den Wohneinheiten in wenigen Schritten erfassen.",
"Daten zum Gebäude und zu den Wohneinheiten direkt online eingeben.",
},
{
media: (
@@ -121,29 +121,31 @@ const SELF_INSTALL_STEPS = [
<AssetRender render={renders.smokeAlarm} imgClassName="w-20" />
</>
),
title: "Passende Ausstattung zusammenstellen",
description: "Wir empfehlen die benötigte Ausstattung Sie wählen aus.",
title: "Benötigte Ausstattung bestimmen lassen",
description:
"SolenOS ermittelt und konfiguriert aus den Objektdaten die passende notwendige beziehungsweise gesetzliche Mindestausstattung.",
},
{
media: (
<AssetRender render={renders.shippingBoxClosed} imgClassName="w-36" />
),
title: "Vorkonfiguriert erhalten",
description: "Ihre Technik kommt vorkonfiguriert und einsatzbereit an.",
description:
"SolenOS prüft die Ausstattung vor dem Versand und ordnet die Geräte den vorgesehenen Wohnungen zu.",
},
{
media: <AssetRender render={renders.drillTool} imgClassName="w-32" />,
title: "Montieren",
description:
"Sie montieren mit Ihrem Handwerker oder selbst flexibel und unkompliziert.",
"Sie montieren selbst oder gemeinsam mit Ihrem eigenen Handwerker.",
},
{
media: (
<AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />
),
title: "SolenOS übernimmt",
title: "Betrieb übernehmen lassen",
description:
"Wir übernehmen Ablesung, Übertragung und Auswertung automatisch.",
"SolenOS übernimmt Einrichtung, Datenübertragung und laufenden Betrieb.",
},
]
@@ -165,13 +167,13 @@ const WHY_SOLENOS: BenefitGridItem[] = [
},
{
icon: <PortalGaugeIcon />,
title: "Alles an einem Ort",
text: "UVI, Abrechnung, Geräte und weitere Gebäudedienstleistungen werden zentral verwaltet.",
title: "Getrennte Portalrechte",
text: "Sie sehen Gebäude, Wohnungen, Geräte, Status und Verbrauchswerte an einer Stelle. Bewohner sehen ausschließlich Daten ihrer eigenen Wohnung und ihres eigenen Nutzungszeitraums.",
},
{
icon: <Clock strokeWidth={1.6} />,
title: "Weniger laufender Aufwand",
text: "Nach der Einrichtung werden Verbrauchsdaten automatisch erfasst und verarbeitet.",
title: "Keine regelmäßigen Ablesebesuche",
text: "Verbrauchsdaten werden fernabgelesen; Heizkostenabrechnungen werden im Portal erstellt und versandfertig bereitgestellt.",
},
]
@@ -187,7 +189,7 @@ const VARIANT_ROWS: ComparisonRow[] = [
label: "Installation",
cells: ["Kunde / eigener Fachbetrieb", "organisiert durch SolenOS"],
},
{ label: "Softwareeinrichtung", cells: ["vorbereitet durch SolenOS", "SolenOS"] },
{ label: "Softwarekonfiguration vor Ort", cells: ["nicht erforderlich durch SolenOS eingerichtet", "nicht erforderlich durch SolenOS eingerichtet"] },
{ label: "Laufender Betrieb", cells: ["SolenOS", "SolenOS"] },
{
label: "Kosten",
@@ -199,12 +201,12 @@ const FAQS: FaqItem[] = [
{
question: "Kann ich die Geräte wirklich selbst installieren?",
answer:
"Ja. Die Technik kommt vorkonfiguriert und objektbezogen bei Ihnen an. Montiert wird von Ihnen, Ihrem Handwerker oder wenn Sie die Komplettlösung wählen von SolenOS. Eine aufwendige Softwarekonfiguration vor Ort ist in keinem Fall erforderlich.",
"Ja. Die Technik kommt vorkonfiguriert und objektbezogen bei Ihnen an. Montiert wird von Ihnen, Ihrem Handwerker oder bei der Komplettlösung von SolenOS. Eine Softwarekonfiguration vor Ort ist in keinem Fall erforderlich.",
},
{
question: "Was kostet SolenOS für mein Gebäude?",
question: "Wann erhalte ich den verbindlichen Gesamtpreis?",
answer:
"Der Konfigurator gibt Ihnen mit wenigen Angaben zu Gebäude, Wohnungen und gewünschten Leistungen eine erste Preisindikation. Den genauen Preis erhalten Sie, sobald die tatsächlichen Gebäudedaten ergänzt sind.",
"Nach dem Schnellkonfigurator ergänzen Sie die Gebäudedetails und Fotos vorhandener Einbausituationen. SolenOS prüft die notwendige Ausstattung und erstellt anschließend das verbindliche Gesamtangebot.",
},
{
question: "Wer übernimmt Ablesung, Übertragung und Auswertung?",
@@ -214,7 +216,7 @@ const FAQS: FaqItem[] = [
{
question: "Was sehe ich im SolenOS Portal?",
answer:
"Ihre Gebäude, Wohnungen und Geräte an einer Stelle inklusive Status der Geräte und der laufenden Verbrauchswerte. Bewohner erhalten ihren eigenen Zugang zum Mieterportal.",
"Sie sehen Gebäude, Wohnungen, Geräte, Gerätestatus und Verbrauchswerte an einer Stelle. Bewohner erhalten einen eigenen Zugang und sehen ausschließlich Daten ihrer eigenen Wohnung und ihres eigenen Nutzungszeitraums. Heizkostenabrechnungen werden im Portal erstellt und versandfertig bereitgestellt.",
},
{
question: "Kann ich später weitere Gebäude oder Geräte ergänzen?",
@@ -227,15 +229,16 @@ export default function HauseigentuemerPage() {
return (
<AudiencePage
breadcrumb="Hauseigentümer"
eyebrow="Für Hauseigentümer"
title={
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
Messdienstleistungen für Ihr Mehrfamilienhaus. Einfach organisiert.
</span>
}
lead="UVI, Heizkostenabrechnung und Rauchwarnmelder mit vorkonfigurierter Technik selbst installieren oder als Komplettlösung beauftragen."
lead="Monatliche Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder mit vorkonfigurierter Technik selbst installieren oder als Komplettlösung beauftragen."
actions={
<Button asChild size="lg">
<Link to="/konfigurator">Preis für mein Gebäude berechnen</Link>
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
</Button>
}
visual={<HomeownerArt />}
@@ -267,12 +270,12 @@ export default function HauseigentuemerPage() {
<CtaBanner
icon={<Euro />}
title="Welche Lösung passt zu Ihrem Gebäude?"
description="Mit wenigen Angaben zur ersten Preisindikation oder direkt die Komplettlösung anfragen."
description="Leistungen und Gebäudedaten online erfassen oder direkt die Komplettlösung anfragen."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">
Preis für mein Gebäude berechnen
Konfiguration für mein Gebäude starten
</Link>
</Button>
<Button asChild variant="inverse" size="lg">
@@ -293,13 +296,7 @@ export default function HauseigentuemerPage() {
<OptionCard
title="Selbst installieren"
lead="Sie erhalten vorkonfigurierte Technik und installieren mit Ihrem Handwerker oder in Eigenregie."
takeaway={
<>
Sie organisieren die Montage.
<br />
SolenOS übernimmt den digitalen Betrieb.
</>
}
takeaway="Sie montieren selbst oder mit Ihrem Handwerker. Sobald die Geräte installiert sind, übernimmt SolenOS Einrichtung, Datenübertragung und laufenden Betrieb."
visual={
<AssetRender
render={renders.shippingBoxMeters}
@@ -308,24 +305,18 @@ export default function HauseigentuemerPage() {
}
action={
<Button asChild size="lg">
<Link to="/konfigurator?installation=0">Preis berechnen</Link>
<Link to="/konfigurator?installation=0">Konfiguration starten</Link>
</Button>
}
/>
<OptionCard
title="Komplett installieren lassen"
lead="Lehnen Sie sich zurück. SolenOS übernimmt Planung, Installation und Einrichtung inklusive Service."
takeaway={
<>
Ein Ansprechpartner. Alles aus einer Hand.
<br />
Komplettlösung mit voller Betreuung.
</>
}
lead="SolenOS übernimmt Installation und Einrichtung inklusive Service."
takeaway="Komplettlösung mit voller Betreuung."
visual={<FullServiceArt />}
action={
<Button asChild size="lg" variant="outline">
<Link to="/konfigurator?installation=1">Preis berechnen</Link>
<Link to="/konfigurator?installation=1">Konfiguration starten</Link>
</Button>
}
/>
@@ -67,9 +67,9 @@ const rolloutSteps = [
"Die Geräte werden passend zum jeweiligen Gebäude beziehungsweise zur jeweiligen Nutzungseinheit ausgeliefert.",
},
{
title: "Bestehende Monteure installieren",
title: "Vor Ort nur montieren",
description:
"Eigene Mitarbeiter, Hausmeister oder bestehende Installationspartner können die Montage übernehmen. Eine umfangreiche Softwarekonfiguration vor Ort ist nicht erforderlich.",
"Eigene Mitarbeiter, Hausmeister oder bestehende Installationspartner montieren ausschließlich die Geräte. SolenOS übernimmt die Softwarekonfiguration.",
},
{
title: "Bewohner einladen",
@@ -79,7 +79,7 @@ const rolloutSteps = [
]
const noEffortItems = [
{ icon: <Ban />, title: "Keine aufwendige Gerätekonfiguration." },
{ icon: <Ban />, title: "Keine Softwarekonfiguration vor Ort." },
{ icon: <X />, title: "Keine manuelle Anlage jedes einzelnen Gerätes." },
{
icon: <Unlock />,
@@ -135,7 +135,7 @@ const faqs: FaqItem[] = [
{
question: "Wie viel Aufwand entsteht für unser Team?",
answer:
"Die Gebäude- und Wohnungsdaten werden bei der Auftragserstellung einmal erfasst und anschließend weiterverwendet. Es entfällt die aufwendige Gerätekonfiguration und die manuelle Anlage jedes einzelnen Gerätes.",
"Die Gebäude- und Wohnungsdaten werden bei der Auftragserstellung einmal erfasst und anschließend weiterverwendet. Vor Ort werden nur die Geräte montiert; SolenOS übernimmt die Softwarekonfiguration und Geräteanlage.",
},
{
question: "Können unsere bestehenden Monteure installieren?",
@@ -188,12 +188,12 @@ export default function HausverwaltungenPage() {
<AudiencePage
breadcrumb="Hausverwaltungen"
title="Für Hausverwaltungen"
tagline="Skalieren Sie Ihre Bestände einfach."
lead="Rollout über viele Gebäude mit minimalem Aufwand von der Planung bis zum laufenden Betrieb."
tagline="Wachsende Bestände einfach betreuen."
lead="Gebäude strukturiert ausrollen und vor Ort ausschließlich die Geräte montieren. SolenOS übernimmt die Softwarekonfiguration und den laufenden Betrieb."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Portfolio kalkulieren</Link>
<Link to="/konfigurator">Portfolio konfigurieren</Link>
</Button>
<Button asChild size="lg" variant="outline">
<Link to="/kontakt">Rollout besprechen</Link>
@@ -204,11 +204,12 @@ export default function HausverwaltungenPage() {
features={[
{
icon: <Package />,
title: "Vorkonfigurierte Hardware",
title: "Zum Einbau vorkonfigurierte Messeinrichtungen",
},
{
icon: <Timer />,
title: "Wenig Aufwand vor Ort",
title: "Vor Ort nur montieren",
sub: "Keine Softwarekonfiguration vor Ort",
},
{
icon: <LayoutDashboard />,
@@ -216,7 +217,7 @@ export default function HausverwaltungenPage() {
},
{
icon: <TrendingUp />,
title: "Skalierbar für wachsende Portfolios",
title: "Für wachsende Portfolios",
},
]}
closing={
@@ -224,11 +225,11 @@ export default function HausverwaltungenPage() {
<CtaBanner
icon={<Euro />}
title="Wie sieht Ihr Portfolio aus?"
description="Erhalten Sie zunächst mit wenigen Angaben eine Preisindikation für mehrere Gebäude oder sprechen Sie mit uns über vorhandene Messtechnik und den geplanten Rollout."
description="Erfassen Sie Gebäude und Leistungen online oder sprechen Sie mit uns über vorhandene Messtechnik und den geplanten Rollout."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Portfolio kalkulieren</Link>
<Link to="/konfigurator">Portfolio konfigurieren</Link>
</Button>
<Button asChild variant="inverse" size="lg">
<Link to="/kontakt">Rollout besprechen</Link>
@@ -242,7 +243,7 @@ export default function HausverwaltungenPage() {
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
eyebrow="Skalierung"
eyebrow="Wachsende Bestände"
title="Weniger Projektaufwand pro Gebäude"
lead="Bei einem einzelnen Gebäude sind manuelle Arbeitsschritte überschaubar. Bei dutzenden Gebäuden werden sie zum Problem. SolenOS reduziert genau diese wiederkehrenden Aufgaben."
/>
@@ -21,7 +21,7 @@ import { AssetRender, renders, vectors } from "@/components/solenos/site-assets"
import { AudiencePage, AudienceClosing } from "@/pages/audiences/audience-page"
const KUNDEN_BEDARFE = [
"UVI",
"Monatliche Verbrauchsinformation (UVI)",
"Heizkostenabrechnung",
"fernauslesbare Messtechnik",
"Rauchwarnmelder",
@@ -42,7 +42,7 @@ const KEINE_HUERDEN = [
},
{
icon: <Wrench />,
title: "Keine komplizierte Softwareeinrichtung auf der Baustelle",
title: "Keinerlei Softwarekonfiguration vor Ort",
},
]
@@ -81,7 +81,7 @@ const FAQS: FaqItem[] = [
{
question: "Muss ich vor Ort Software konfigurieren?",
answer:
"Nein. Auf der Baustelle bleibt im Wesentlichen die Montage. Eine komplizierte Softwareeinrichtung am Gebäude ist nicht vorgesehen.",
"Nein. Vor Ort montieren Sie ausschließlich die vorgesehenen Geräte. Die gesamte Softwarekonfiguration übernimmt SolenOS.",
},
{
question: "Bleibt der Kunde mein Kunde?",
@@ -102,7 +102,7 @@ export default function InstallateurePage() {
</>
}
tagline="Bestellen. Montieren. Fertig."
lead="SolenOS für Ihre Kunden installieren ohne Partnerprogramm, verpflichtende Zertifizierung oder aufwendige Softwarekonfiguration."
lead="SolenOS für Ihre Kunden installieren ohne Partnerprogramm, verpflichtende Zertifizierung oder jegliche Softwarekonfiguration vor Ort."
actions={
<Button asChild size="lg">
<Link to="/konfigurator">Gebäude für meinen Kunden konfigurieren</Link>
@@ -140,12 +140,12 @@ export default function InstallateurePage() {
},
{
icon: <Package />,
title: "Vorkonfigurierte Hardware",
title: "Zum Einbau vorkonfigurierte Messeinrichtungen",
},
{
icon: <Wrench />,
title: "Einfache Installation",
sub: "Keine Softwarekonfiguration",
title: "Vor Ort nur montieren",
sub: "Keinerlei Softwarekonfiguration vor Ort",
},
{
icon: <Users />,
@@ -156,7 +156,7 @@ export default function InstallateurePage() {
closing={
<AudienceClosing
title="SolenOS bei einem Kunden einsetzen?"
lead="Stellen Sie mit wenigen Angaben zunächst die ungefähre Ausstattung und den Preis für das Gebäude zusammen."
lead="Erfassen Sie Leistungen und Gebäudedaten online. SolenOS bestimmt daraus die notwendige Ausstattung für das geprüfte Gesamtangebot."
actions={
<Button asChild size="lg">
<Link to="/konfigurator">
@@ -28,10 +28,10 @@ const platformRequirements = [
]
const whiteLabelItems = [
"eigenes Branding",
"Optionen für Ihr eigenes Logo im abgestimmten Umfang",
"eigene Kundenbeziehung",
"mandantenfähige Verwaltung",
"Endkundenportale",
"klar vereinbarte SolenOS- und Anbieterkennzeichnung",
"APIs und Datenexporte",
"Geräteverwaltung",
"technischer Support",
@@ -40,7 +40,7 @@ const whiteLabelItems = [
const platformCapabilities = [
{
title: "Gerätekompatibilität",
sub: "Einbindung unterschiedlicher Zähler- und Gerätetypen auf Basis geeigneter Standards.",
sub: "OMS-kompatible Geräte verschiedener Hersteller einbinden.",
},
{
title: "Gateway-Infrastruktur",
@@ -63,8 +63,8 @@ const platformCapabilities = [
sub: "Kunden und Bestände getrennt innerhalb einer gemeinsamen Plattform verwalten.",
},
{
title: "White Label",
sub: "SolenOS-Infrastruktur unter der Marke des Messdienstleisters einsetzen.",
title: "Eigenmarken-Option",
sub: "Logo und Kennzeichnung im gemeinsam festgelegten Umfang einsetzen.",
},
]
@@ -73,7 +73,7 @@ const businessComponents = [
"Plattformgebühren",
"Mengenstaffeln",
"wiederkehrende Gebühren",
"White-Label-Konditionen",
"Konditionen für Eigenmarken-Optionen",
"Support- und Servicelevel",
"mögliche Mindestmengen",
]
@@ -82,9 +82,9 @@ const stackFlow = ["Ihre Kunden", "Ihr Messdienst", "SolenOS Plattform"]
const faqs: FaqItem[] = [
{
question: "Können wir SolenOS unter unserer eigenen Marke anbieten?",
question: "Können wir SolenOS mit unserem eigenen Logo anbieten?",
answer:
"Ja. SolenOS ist als technische Grundlage für Dienstleistungen unter Ihrer Marke vorgesehen mit eigenem Branding, mandantenfähiger Verwaltung und Endkundenportalen in Ihrem Auftritt.",
"Optional kann SolenOS mit Ihrem eigenen Logo beziehungsweise unter Ihrer Eigenmarke eingesetzt werden. Für welche Oberflächen das gilt und wie SolenOS dort gekennzeichnet wird, legen wir vorab gemeinsam fest.",
},
{
question: "Behalten wir die Kundenbeziehung?",
@@ -104,7 +104,7 @@ const faqs: FaqItem[] = [
{
question: "Wie werden die Konditionen festgelegt?",
answer:
"Individuell beziehungsweise mengenabhängig mit anderen Geschäftsmodellen, Mengenstaffeln und gegebenenfalls White-Label-Konditionen als im öffentlichen Preiskonfigurator. Die konkreten Konditionen werden nicht öffentlich dargestellt.",
"Die Konditionen werden passend zu Leistungsumfang, Bestandsgröße, Support und vereinbarten Eigenmarken-Optionen gemeinsam festgelegt. Unbestätigte Tarife oder Mindestmengen werden nicht vorab zugesagt.",
},
]
@@ -138,7 +138,7 @@ export default function MessdienstleisterPage() {
breadcrumb="Messdienstleister"
title="Für Messdienstleister"
tagline="Ihre Services. Unsere Plattform."
lead="Nutzen Sie SolenOS als technische Grundlage für Ihren eigenen Messdienst skalierbar und zuverlässig."
lead="Nutzen Sie SolenOS als technische Grundlage für Ihren eigenen Messdienst für kleine Bestände und große Wohnungsportfolios."
actions={
<>
<Button asChild size="lg">
@@ -153,8 +153,8 @@ export default function MessdienstleisterPage() {
features={[
{
icon: <Tag />,
title: "White Label",
sub: "Ihr Branding, Ihre Kunden",
title: "Optional mit Ihrem eigenen Logo",
sub: "Unter Ihrer Eigenmarke im vereinbarten Umfang",
},
{
icon: <Code2 />,
@@ -163,8 +163,8 @@ export default function MessdienstleisterPage() {
},
{
icon: <Layers />,
title: "Skalierbare Plattform",
sub: "Für jedes Geschäftsmodell",
title: "Für wachsende Bestände",
sub: "Von kleinen Beständen bis zu großen Wohnungsportfolios",
},
{
icon: <ShieldCheck />,
@@ -233,8 +233,8 @@ export default function MessdienstleisterPage() {
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Ihre Marke"
title="White Label"
lead="SolenOS kann als technische Grundlage für Dienstleistungen unter Ihrer eigenen Marke eingesetzt werden. Je nach Geschäftsmodell können unter anderem relevant sein:"
title="Unter Ihrer Eigenmarke (White Label)"
lead="SolenOS kann optional mit Ihrem eigenen Logo eingesetzt werden. Welche Oberflächen die Eigenmarke tragen und wie SolenOS gekennzeichnet wird, vereinbaren wir vorab verbindlich."
/>
<div className="mt-8 grid max-w-3xl gap-x-10 gap-y-2.5 sm:grid-cols-2">
<Checklist items={whiteLabelItems.slice(0, 4)} />
@@ -267,8 +267,8 @@ export default function MessdienstleisterPage() {
<SectionHeading
align="center"
eyebrow="Konditionen"
title="Das Geschäftsmodell muss funktionieren"
lead="Eine Plattform für Messdienstleister ist nur interessant, wenn auch der wirtschaftliche Rahmen stimmt. Deshalb werden kommerzielle Konditionen individuell beziehungsweise mengenabhängig vereinbart mit anderen Geschäftsmodellen, Mengenstaffeln und gegebenenfalls White-Label-Konditionen als im öffentlichen Preiskonfigurator."
title="Konditionen passend zum vereinbarten Umfang"
lead="Leistungsumfang, Bestandsgröße, Support und Eigenmarken-Optionen bestimmen den wirtschaftlichen Rahmen. Die konkreten Konditionen werden gemeinsam festgelegt; die Website macht dazu keine unbestätigten Zusagen."
/>
<div className="mx-auto mt-8 max-w-2xl text-center">
<p className="text-xs font-bold text-muted-foreground">
@@ -291,8 +291,8 @@ export default function MessdienstleisterPage() {
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
title="Gemeinsam skalieren"
lead="SolenOS soll insbesondere Messdienstleistern ermöglichen, neue digitale Dienstleistungen anzubieten, ohne dafür eine vollständige eigene Hardware- und Softwareentwicklung aufbauen zu müssen. Der Messdienstleister behält dabei seine Kundenbeziehung und sein eigenes Geschäftsmodell."
title="Mit Ihren Beständen wachsen"
lead="SolenOS unterstützt kleine Bestände ebenso wie große Wohnungsportfolios. Messdienstleister können neue digitale Dienstleistungen anbieten, ohne dafür eine vollständige eigene Hardware- und Softwareentwicklung aufbauen zu müssen; die Kundenbeziehung bleibt bei ihnen."
/>
</Section>
+267 -129
View File
@@ -1,11 +1,10 @@
import * as React from "react"
import { useSearchParams } from "react-router-dom"
import { Link, useSearchParams } from "react-router-dom"
import { Ban, Check, CircleCheck, Euro, Plus, X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
@@ -50,15 +49,14 @@ const INITIAL_BUILDING: Building = {
zimmer: 3,
}
/** Grundpreise je Wohnung / Monat Selbstmontage, wie in der Klarpreis-Liste. */
/** Grundpreise je Wohnung / Monat wie in der Klarpreis-Liste. */
const UVI_MONTHLY = 10.5
const INSPECTION_MONTHLY = 2.2
/** Aufschlag je Monatsposition, wenn SolenOS 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
/** 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", {
@@ -90,13 +88,14 @@ function settle(value: number | "", min: number) {
/**
* Deep-link preset. Pages elsewhere on the site link into the configurator with
* parts of the form already answered, e.g. `/konfigurator?installation=0` for
* the self-install route. Supported params: `installation`, `uvi`, `rwm`,
* `adresse`, `heizung`, `wohnungen`, `zimmer`. Missing or unreadable values
* keep the page defaults; numbers below the field minimum settle at it, just
* as they do when typed into the form.
* the self-install route. Supported params: `installation`, `uvi`,
* `abrechnung`, `rwm`, `adresse`, `heizung`, `wohnungen`, `zimmer`. Missing or
* unreadable values keep the page defaults; numbers below the field minimum
* settle at it, just as they do when typed into the form.
*/
type Preset = {
uvi?: boolean
billing?: boolean
rwm?: boolean
install?: boolean
/** Seeds the single building the configurator starts with. */
@@ -149,6 +148,7 @@ function readPreset(params: URLSearchParams): Preset {
return {
uvi: presetFlag(params.get("uvi")),
billing: presetFlag(params.get("abrechnung")),
rwm: presetFlag(params.get("rwm")),
install: presetFlag(params.get("installation")),
building,
@@ -182,14 +182,17 @@ 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({
id,
checked,
onCheckedChange,
title,
description,
}: {
id: string
checked: boolean
onCheckedChange: (checked: boolean) => void
title: string
@@ -198,21 +201,44 @@ function ServiceOption({
return (
<label
className={cn(
"flex cursor-pointer items-start gap-3 rounded-xl border p-4 transition-colors",
"flex cursor-pointer items-start justify-between gap-4 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
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span
id={`${id}-title`}
className="text-sm font-bold leading-snug text-navy"
>
{title}
</span>
<Badge
id={`${id}-state`}
variant={checked ? "default" : "muted"}
aria-live="polite"
className="shrink-0"
>
{checked ? "Enthalten · abwählbar" : "Hinzufügen"}
</Badge>
</div>
<p
id={`${id}-description`}
className="mt-1 text-sm leading-relaxed text-muted-foreground"
>
{description}
</p>
</div>
<Switch
id={id}
checked={checked}
onCheckedChange={(v) => onCheckedChange(v === true)}
onCheckedChange={onCheckedChange}
aria-labelledby={`${id}-title`}
aria-describedby={`${id}-description ${id}-state`}
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>
)
}
@@ -282,23 +308,28 @@ function PackageCard({
* 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,
}: {
label: string
amount: number
unit: string
note?: string
allocatable: boolean
}) {
active,
}: Position) {
const shown = useAnimatedNumber(amount)
return (
<div>
<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)}
@@ -321,6 +352,7 @@ export default function ConfiguratorPage() {
const preset = React.useMemo(() => readPreset(searchParams), [searchParams])
const [uvi, setUvi] = React.useState(preset.uvi ?? true)
const [billing, setBilling] = React.useState(preset.billing ?? true)
const [rwm, setRwm] = React.useState(preset.rwm ?? true)
const [buildings, setBuildings] = React.useState<Building[]>(() => [
{ ...INITIAL_BUILDING, ...preset.building },
@@ -338,6 +370,7 @@ export default function ConfiguratorPage() {
if (appliedPreset.current === preset) return
appliedPreset.current = preset
setUvi(preset.uvi ?? true)
setBilling(preset.billing ?? true)
setRwm(preset.rwm ?? true)
setInstall(preset.install ?? true)
setBuildings([{ ...INITIAL_BUILDING, ...preset.building }])
@@ -360,15 +393,77 @@ export default function ConfiguratorPage() {
}
const totalUnits = buildings.reduce((sum, b) => sum + (b.wohnungen || 0), 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
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 (
<Container>
@@ -383,7 +478,7 @@ export default function ConfiguratorPage() {
/>
<SectionHeading
title="Konfigurator"
lead="In 3 Schritten zur Preisindikation."
lead="Leistungen auswählen, Gebäudedaten erfassen und sofort eine Preisindikation erhalten."
/>
</div>
@@ -393,50 +488,69 @@ export default function ConfiguratorPage() {
<StepCard step={1} title="Leistungen wählen">
<div className="flex flex-col gap-3">
<ServiceOption
id="service-uvi"
checked={uvi}
onCheckedChange={setUvi}
title="UVI"
description="Unterjährige Verbrauchsinformation inklusive Messtechnik"
title="Monatliche Verbrauchsinformation (UVI)"
description="Monatliche Verbrauchswerte und Vergleiche für jede Wohnung einschließlich der benötigten Messtechnik."
/>
<ServiceOption
id="service-billing"
checked={billing}
onCheckedChange={setBilling}
title="Heizkostenabrechnung"
description="Jährliche Heizkostenabrechnung einschließlich der gesetzlich vorgeschriebenen Pflichtangaben und Dokumente."
/>
<ServiceOption
id="service-smoke-alarms"
checked={rwm}
onCheckedChange={setRwm}
title="Rauchwarnmelder"
description="Rauchwarnmelder inklusive digitaler Dienstleistungen"
description="Rauchwarnmelder mit Ferninspektion, Geräteverwaltung sowie Prüf- und Statusdokumentation."
/>
</div>
<Separator className="my-6" />
<div
<label
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
? "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">
<Label
htmlFor="installation"
className="cursor-pointer text-sm font-bold text-navy"
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span
id="installation-title"
className="text-sm font-bold text-navy"
>
Montage durch SolenOS
</span>
<Badge variant={install ? "default" : "muted"}>
{install ? "SolenOS montiert" : "Selbstmontage"}
</Badge>
</div>
<p
id="installation-description"
className="mt-1 text-sm text-muted-foreground"
>
Installation durch SolenOS
</Label>
<p className="mt-1 text-sm text-muted-foreground">
{install
? "SolenOS organisiert die Montage Geräte- und Monatspreise enthalten die Installation."
: "Sie selbst, Ihr Hausmeister oder Ihr Fachbetrieb montiert die Geräte."}
? "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."}
</p>
</div>
<Switch
id="installation"
checked={install}
onCheckedChange={setInstall}
aria-labelledby="installation-title"
aria-describedby="installation-description"
className="mt-0.5"
/>
</div>
</label>
</StepCard>
<StepCard step={2} title="Gebäude angeben">
<StepCard step={2} title="Gebäudedaten online erfassen">
<div className="flex flex-col gap-5">
{buildings.map((b, i) => (
<div
@@ -549,10 +663,37 @@ export default function ConfiguratorPage() {
</div>
</StepCard>
<StepCard step={3} title="Konfiguration speichern">
<StepCard step={3} title="Persönlichen Link anfordern">
<div className="rounded-xl border border-brand-200 bg-brand-50/60 p-4">
<p className="text-sm font-bold text-navy">
So geht es nach der E-Mail weiter
</p>
<ol className="mt-3 list-decimal space-y-2 pl-5 text-sm leading-relaxed text-muted-foreground">
<li>
Wir speichern Ihre bisherige Auswahl und senden Ihnen einen
persönlichen Link.
</li>
<li>
Über den Link ergänzen Sie die Gebäudedetails und laden im
Portal Fotos vorhandener wasserführender Einbausituationen
hoch. Fotos ersetzen Maße und Typangaben, soweit sie eine
eindeutige Prüfung ermöglichen.
</li>
<li>
SolenOS prüft die Angaben und Fotos manuell, bestimmt die
endgültige Ausstattung und erstellt anschließend den genauen
Gesamtpreis beziehungsweise ein verbindliches Angebot.
</li>
</ol>
<p className="mt-3 text-xs leading-relaxed text-muted-foreground">
Beispiel für spätere Detailfragen: Kinderzimmer und regelmäßig
genutzte Gästezimmer zählen als Schlafräume; ein dauerhaftes
Büro nicht.
</p>
</div>
<form
onSubmit={(e) => e.preventDefault()}
className="flex flex-col gap-3"
onSubmit={(event) => event.preventDefault()}
className="mt-5 flex flex-col gap-3"
>
<div className="flex flex-col gap-1.5">
<Label htmlFor="config-email">E-Mail-Adresse</Label>
@@ -561,18 +702,28 @@ export default function ConfiguratorPage() {
type="email"
placeholder="name@unternehmen.de"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(event) => setEmail(event.target.value)}
aria-describedby="config-email-privacy"
required
/>
</div>
<Button type="submit" className="self-start">
Konfiguration speichern &amp; fortsetzen
</Button>
<p className="text-xs text-muted-foreground">
Sie erhalten einen persönlichen Link
zu Ihrer Konfiguration. Damit kannst du
sofort weitermachen, später zurückkehren
oder den Link intern weitergeben.
<p
id="config-email-privacy"
className="text-xs leading-relaxed text-muted-foreground"
>
Wir verwenden Ihre E-Mail-Adresse, um den persönlichen Link
zu versenden. Einzelheiten finden Sie in unseren{" "}
<Link
to="/wissen/datenschutz-sicherheit"
className="font-semibold text-primary underline underline-offset-2"
>
Hinweisen zu Datenschutz &amp; Sicherheit
</Link>
.
</p>
<Button type="submit" className="self-start">
Persönlichen Link anfordern
</Button>
</form>
</StepCard>
</div>
@@ -589,74 +740,61 @@ export default function ConfiguratorPage() {
</IconBadge>
</div>
<div className="p-6 sm:p-7">
{uvi || rwm ? (
<div className="flex flex-col gap-5">
{uvi ? (
<PriceRow
label="UVI-Komplettbetrieb"
amount={uviMonthly}
unit="je Wohnung / Monat"
note={installNote}
allocatable
/>
) : null}
{rwm ? (
<>
{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}
</div>
) : (
<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 SolenOS. "
: "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.
<p className="text-sm font-bold text-navy">
{buildings.length} Gebäude · {totalUnits} {unitWord}
</p>
<Separator className="my-5" />
<div className="flex flex-col gap-5" aria-live="polite">
{positions.map((position, index) => (
<React.Fragment key={position.label}>
{index > 0 ? <Separator /> : null}
<PriceRow {...position} />
</React.Fragment>
))}
</div>
<div className="mt-6 space-y-2 text-xs leading-relaxed text-muted-foreground">
{billing ? (
<p>
Die Heizkostenabrechnung wird mit dem verbindlichen
Angebot bepreist.
</p>
) : null}
<p>
Die erste Berechnung ist eine unverbindliche
Preisindikation. Der genaue Preis ergibt sich aus der
vollständigen Objektkonfiguration.
</p>
</div>
</div>
</Card>
</div>
</div>
</Section>
{/* Was ist in der Schätzung enthalten? */}
{/* Was ist in dem Preis enthalten? */}
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
title="Was ist in dem Preis enthalten?"
/>
<div className="mt-8 grid gap-6 lg:grid-cols-2">
<SectionHeading title="Was ist in dem Preis enthalten?" />
<div className="mt-8 grid gap-6 lg:grid-cols-3">
<PackageCard
render={renders.gateway}
title="UVI"
title="Monatliche Verbrauchsinformation (UVI)"
included={uvi}
items={[
"geschätzte benötigte Messtechnik",
"SolenOS Plattform",
"Datenerfassung",
"Unterjährige Verbrauchsinformation",
"laufender digitaler Betrieb",
"von SolenOS ermittelte notwendige Messtechnik",
"erforderliche Gateway- und Kommunikationsinfrastruktur",
"SolenOS Plattform, Datenerfassung und laufender digitaler Betrieb",
"monatliche Verbrauchswerte und Vergleiche für jede Wohnung",
]}
/>
<PackageCard
render={renders.glassMoney}
title="Heizkostenabrechnung"
included={billing}
items={[
"jährliche Heizkostenabrechnung im Online-Portal",
"Verteilung nach den Vorgaben der HeizkostenV",
"Berücksichtigung von Nutzerwechseln und Leerständen",
"Pflichtangaben nach § 6a Absatz 3 HeizkostenV",
]}
/>
<PackageCard
@@ -664,12 +802,12 @@ export default function ConfiguratorPage() {
title="Rauchwarnmelder"
included={rwm}
items={[
"geschätzte Anzahl Rauchwarnmelder",
"Verwaltung der Geräte",
"gebuchte digitale Dienstleistungen",
"von SolenOS ermittelte Anzahl Rauchwarnmelder",
"Verwaltung und Ferninspektion der Geräte",
"Prüf- und Statusdokumentation",
install
? "Montage und Eigentumsübertragung"
: "Eigentumsübertragung Montage durch Sie",
? "Montage durch SolenOS als eigene Position"
: "Montage durch Sie oder Ihren Fachbetrieb",
]}
/>
</div>
+9 -9
View File
@@ -62,11 +62,11 @@ const AUDIENCES: Record<
tab: "Hausverwaltungen",
tabIcon: <Building2 />,
headline: "Viele Gebäude. Ein einfacher Rollout.",
text: "Erfassen Sie Ihre Objekte einmal, erhalten Sie vorkonfigurierte Hardware und nutzen Sie bestehende Mitarbeiter oder Installationspartner für die Montage.",
text: "Erfassen Sie Ihre Objekte einmal, erhalten Sie vorkonfigurierte Messeinrichtungen und nutzen Sie bestehende Mitarbeiter oder Installationspartner ausschließlich für die Montage.",
bullets: [
"standardisierter Rollout",
"vorkonfigurierte Geräte",
"wenig Softwarearbeit vor Ort",
"keine Softwarekonfiguration vor Ort",
"zentrale Portfolioverwaltung",
"geringer administrativer Aufwand",
"schnelle Erweiterung um weitere Gebäude",
@@ -79,14 +79,14 @@ const AUDIENCES: Record<
tab: "Messdienstleister",
tabIcon: <LineChart />,
headline: "Ihre Messdienstleistung. Unsere Infrastruktur.",
text: "Nutzen Sie SolenOS als technische Grundlage für Ihre eigenen Dienstleistungen mit Hardware, Plattform, Gerätemanagement und optionalem White Label.",
text: "Nutzen Sie SolenOS als technische Grundlage für Ihre eigenen Dienstleistungen mit Hardware, Plattform, Gerätemanagement und optional mit Ihrem eigenen Logo (White Label).",
bullets: [
"technische Plattform",
"Gerätekompatibilität",
"Skalierbarkeit",
"OMS-kompatible Geräte verschiedener Hersteller",
"für kleine Bestände und große Wohnungsportfolios",
"APIs und Integrationen",
"White Label",
"individuelles Geschäftsmodell",
"Optionen für eigenes Logo und Eigenmarke",
"individuell vereinbarte Konditionen",
],
cta: "SolenOS für Messdienstleister",
href: "/fuer-wen/messdienstleister",
@@ -100,8 +100,8 @@ const AUDIENCES: Record<
bullets: [
"kein verpflichtender Partnerstatus",
"keine Mindestabnahme",
"vorkonfigurierte Hardware",
"keine aufwendige Softwarekonfiguration",
"zum Einbau vorkonfigurierte Messeinrichtungen",
"keinerlei Softwarekonfiguration vor Ort",
"bestehende Kundenbeziehung bleibt erhalten",
],
cta: "SolenOS für Installateure",
+184 -107
View File
@@ -14,9 +14,13 @@ import { Container, Section, SectionHeading } from "@/components/solenos/section
import { PageHero } from "@/components/solenos/page-hero"
import { HeroArt } from "@/components/solenos/hero-art"
import { InfoStrip } from "@/components/solenos/info-strip"
import { ProcessSteps, NumberedList } from "@/components/solenos/process-steps"
import { NumberedList, StepFlow } from "@/components/solenos/process-steps"
import { Checklist } from "@/components/solenos/checklist"
import { CtaBanner } from "@/components/solenos/cta-banner"
import {
ExpandingCardGrid,
type ExpandingCard,
} from "@/components/solenos/expanding-card-grid"
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
import { GermanyIcon } from "@/components/solenos/line-icons"
@@ -25,7 +29,6 @@ import {
AssetRender,
icons,
renders,
vectors,
type RenderAsset,
} from "@/components/solenos/site-assets"
@@ -68,32 +71,90 @@ const infoStripItems = [
},
]
const operationsScope = [
const operationsScope: ExpandingCard[] = [
{
id: "uvi",
media: <img src={icons.uvi} alt="" className="size-12" loading="lazy" />,
visual: (
<AssetRender
render={renders.uviGlassPane}
className="flex w-full justify-center"
imgClassName="w-full max-w-60"
/>
),
title: "Unterjährige Verbrauchsinformation",
text: "Verbrauchsdaten werden automatisch erfasst und die regelmäßigen Verbrauchsinformationen für die Bewohner bereitgestellt.",
points: [
"monatliche Erzeugung der Verbrauchsinformation",
"Bereitstellung im Mieterportal für jede Wohnung",
"Nutzerwechsel ohne Ablesetermin vor Ort",
],
to: "/produkte/uvi",
},
{
id: "heizkostenabrechnung",
media: (
<img src={icons.billing} alt="" className="size-12" loading="lazy" />
),
visual: (
<AssetRender
render={renders.glassMoney}
className="flex w-full justify-center"
imgClassName="w-full max-w-52"
/>
),
title: "Heizkostenabrechnung",
text: "Die erfassten Verbrauchsdaten bilden die Grundlage für die jährliche Heizkostenabrechnung.",
text: "Aus den erfassten Verbrauchsdaten wird die jährliche Heizkostenabrechnung im Online-Portal erstellt und bereitgestellt.",
points: [
"Verteilung der Wärme- und Warmwasserkosten nach HeizkostenV",
"Berücksichtigung von Nutzerwechseln und Leerständen",
"versandfertige Einzelabrechnung je Wohnung im Online-Portal",
"Pflichtangaben nach § 6a Absatz 3 HeizkostenV",
],
to: "/produkte/heizkostenabrechnung",
},
{
id: "rauchwarnmelder",
media: (
<img src={icons.smokeAlarm} alt="" className="size-12" loading="lazy" />
),
visual: (
<AssetRender
render={renders.glassSmokeDetector}
className="flex w-full justify-center"
imgClassName="w-full max-w-48"
/>
),
title: "Rauchwarnmelder",
text: "Rauchwarnmelder können gemeinsam mit der übrigen Gebäudetechnik geplant, installiert und zentral verwaltet werden.",
points: [
"Rauchwarnmelder inklusive Montage und Eigentumsübertragung",
"Bedarfsermittlung je Wohnung aus den Gebäudedaten",
"regelmäßige Ferninspektion und Störungsmeldungen",
],
to: "/produkte/rauchwarnmelder",
},
{
id: "messtechnik",
media: <img src={icons.radio} alt="" className="size-12" loading="lazy" />,
visual: (
<AssetRender
render={renders.heatMeter}
className="flex w-full justify-center"
imgClassName="w-full max-w-52"
/>
),
title: "Fernauslesbare Messtechnik",
text: "Wärme- und Wasserverbrauch werden über kompatible Geräte automatisch erfasst.",
points: [
"Wärmezähler, Heizkostenverteiler und Wasserzähler messen laufend je Wohnung",
"Geräte sind fernablesbar, also ohne Zugang zur Wohnung auslesbar",
"die Funkschnittstelle folgt einem offenen Standard (OMS)",
],
to: "/produkte/messtechnik-infrastruktur",
},
{
id: "gateway",
media: (
<AssetRender
render={renders.gateway}
@@ -101,81 +162,84 @@ const operationsScope = [
imgClassName="w-12"
/>
),
visual: (
<AssetRender
render={renders.glassGateway}
className="flex w-full justify-center"
imgClassName="w-full max-w-52"
/>
),
title: "Gateway und Datenübertragung",
text: "Die Messdaten werden zentral gesammelt und an SolenOS übertragen.",
points: [
"die erforderliche Gateway-Infrastruktur wird passend zum Objekt bestimmt",
"Übertragung per Mobilfunk, ohne Internetanschluss im Gebäude",
"TLS-verschlüsselte Übertragung an die Plattform",
],
to: "/produkte/messtechnik-infrastruktur",
linkLabel: "Messtechnik und Infrastruktur",
},
{
id: "portale",
media: (
<img src={icons.portal} alt="" className="size-12" loading="lazy" />
),
visual: (
<AssetRender
render={renders.laptopDashboard}
className="flex w-full justify-center"
imgClassName="w-full"
/>
),
title: "Portale und Verwaltung",
text: "Gebäude, Wohnungen, Geräte und Verbrauchsdaten werden zentral verwaltet. Bewohner erhalten Zugriff auf die für sie vorgesehenen Informationen.",
text: "Eigentümer betreuen Gebäude, Wohnungen, Geräte und Verbrauchsdaten zentral. Bewohner haben ihre persönliche Übersicht im Mieterportal.",
points: [
"Portalzugang für Eigentümer und Verwaltung",
"eigener Mieterportal-Zugang für jede Wohnung",
"ausschließlich Daten der eigenen Wohnung und des eigenen Nutzungszeitraums sichtbar",
],
},
]
function StepMedia({ src }: { src: string }) {
return (
<span className="flex size-16 items-center justify-center">
<img src={src} alt="" className="size-16" loading="lazy" />
</span>
)
}
const processSteps = [
{
media: (
<AssetRender
render={renders.building}
className="flex size-16 items-center justify-center"
imgClassName="w-16"
/>
),
title: "Gebäude anlegen & bestellen",
media: <AssetRender render={renders.building} imgClassName="w-52" />,
title: "Gebäudedaten online erfassen",
description:
"In nur 30 Minuten konfigurieren und bestellen.",
},
{
media: <StepMedia src={vectors.packageBox} />,
title: "Vorkonfiguriert liefern",
description: "Geräte werden für Ihr Objekt vorbereitet geliefert.",
"Adresse, Wohnungen und Heizart online angeben. SolenOS ermittelt daraus die notwendige Ausstattung und prüft sie vor dem Versand.",
},
{
media: (
<AssetRender
render={renders.heatMeter}
className="flex size-16 items-center justify-center"
imgClassName="w-16"
/>
<AssetRender render={renders.shippingBoxMeters} imgClassName="w-36" />
),
title: "Geräte montieren",
description: "Montage vor Ort ohne aufwendige Softwarekonfiguration.",
title: "Vorkonfiguriert erhalten",
description:
"Die geprüften Geräte werden bereits den vorgesehenen Wohnungen zugeordnet geliefert.",
},
{
media: <AssetRender render={renders.drillTool} imgClassName="w-32" />,
title: "Vor Ort montieren",
description:
"Selbst, mit dem eigenen Fachbetrieb oder durch SolenOS montieren lassen. Eine Softwarekonfiguration vor Ort ist nicht erforderlich.",
},
{
media: (
<AssetRender
render={renders.laptopDashboard}
className="flex size-16 items-center justify-center"
imgClassName="w-16"
/>
<AssetRender render={renders.laptopDashboard} imgClassName="w-44" />
),
title: "Bewohner einladen",
description: "Laden Sie die Bewohner in das SolenOS Portal ein.",
},
{
media: <StepMedia src={vectors.wirelessOrb} />,
title: "Automatischer Betrieb",
description: "SolenOS übernimmt Daten, Services und laufenden Betrieb.",
title: "Betrieb übernehmen lassen",
description:
"SolenOS übernimmt Einrichtung, Datenübertragung und laufenden Betrieb; Bewohner erhalten ihren Portalzugang.",
},
]
const lessEffort = [
{
title: "Vorkonfigurierte Hardware",
text: "Geräte werden passend zum jeweiligen Gebäude und zur vorgesehenen Nutzung vorbereitet.",
title: "Zum Einbau vorkonfigurierte Messeinrichtungen",
text: "SolenOS bestimmt die Ausstattung aus den Objektdaten, prüft sie und ordnet die Geräte vor dem Versand zu.",
},
{
title: "Keine aufwendige Einrichtung auf der Baustelle",
text: "Der Installateur kann sich auf die fachgerechte Montage konzentrieren.",
title: "Keine Softwarekonfiguration vor Ort",
text: "Vor Ort wird nur montiert. Eine Softwarekonfiguration ist nicht erforderlich.",
},
{
title: "Bestehende Fachbetriebe nutzen",
@@ -192,6 +256,7 @@ const systemTasks = [
media: <img src={icons.uvi} alt="" className="size-10" loading="lazy" />,
title: "UVI",
text: "Regelmäßige Verbrauchsinformation für Bewohner.",
to: "/produkte/uvi",
},
{
media: (
@@ -199,6 +264,7 @@ const systemTasks = [
),
title: "Heizkostenabrechnung",
text: "Verbrauchsdaten strukturiert erfassen und für die jährliche Abrechnung nutzen.",
to: "/produkte/heizkostenabrechnung",
},
{
media: (
@@ -206,11 +272,13 @@ const systemTasks = [
),
title: "Rauchwarnmelder",
text: "Rauchwarnmelder gemeinsam mit der übrigen Gebäudetechnik planen und verwalten.",
to: "/produkte/rauchwarnmelder",
},
{
media: <img src={icons.radio} alt="" className="size-10" loading="lazy" />,
title: "Messtechnik und Infrastruktur",
text: "Fernauslesbare Zähler, Sensoren und Kommunikationsinfrastruktur für den laufenden Betrieb.",
to: "/produkte/messtechnik-infrastruktur",
},
]
@@ -231,7 +299,7 @@ const audiences = [
"Ausstattung aus Gebäudedaten ermitteln",
"vorkonfigurierte Geräte erhalten",
"eigenen Fachbetrieb nutzen",
"laufenden Messbetrieb digital verwalten",
"laufenden Messbetrieb digital betreuen",
],
cta: "SolenOS für Hauseigentümer",
to: "/fuer-wen/hauseigentuemer",
@@ -276,7 +344,7 @@ const audiences = [
"Geräte- und Verbrauchsdaten",
"UVI",
"Heizkostenabrechnungsprozesse",
"White-Label-Optionen",
"Optionen für eigenes Logo und Eigenmarke",
],
cta: "SolenOS für Messdienstleister",
to: "/fuer-wen/messdienstleister",
@@ -332,11 +400,11 @@ const portfolioScopes = [
]
const priceFlow = [
{ title: "Wenige Angaben" },
{ title: "Erste Preisindikation" },
{ title: "Konfiguration speichern" },
{ title: "Gebäude vollständig konfigurieren" },
{ title: "Genauer Preis" },
{ title: "Leistungen auswählen" },
{ title: "Gebäudedaten online erfassen" },
{ title: "Persönlichen Link erhalten" },
{ title: "Details und Fotos im Portal ergänzen" },
{ title: "Geprüftes Gesamtangebot erhalten" },
]
const faqs: FaqItem[] = [
@@ -352,19 +420,19 @@ const faqs: FaqItem[] = [
"In vielen Fällen kann ein vorhandener geeigneter Fachbetrieb eingesetzt werden. Welche Arbeiten erforderlich sind, hängt von der jeweiligen Messtechnik und Einbausituation ab.",
},
{
question: "Muss vor Ort jedes Gerät einzeln in SolenOS eingerichtet werden?",
question: "Muss vor Ort Software konfiguriert werden?",
answer:
"Die Geräte werden soweit vorgesehen bereits vor dem Versand dem Gebäude und den geplanten Einheiten zugeordnet. Dadurch reduziert sich die Softwareeinrichtung vor Ort deutlich.",
"Nein. SolenOS ordnet die Geräte vor dem Versand dem Gebäude und den vorgesehenen Einheiten zu. Vor Ort wird nur montiert; eine Softwarekonfiguration ist nicht erforderlich.",
},
{
question: "Wie genau ist die erste Preisschätzung?",
question: "Wann erhalte ich den verbindlichen Gesamtpreis?",
answer:
"Die erste Schätzung basiert auf wenigen Gebäudedaten und Annahmen zur benötigten Ausstattung. Die vollständige Gebäudekonfiguration bestimmt anschließend die konkreten Geräte und den finalen Preis.",
"Nach dem Schnellkonfigurator ergänzen Sie Gebäudedetails und Fotos vorhandener Einbausituationen im Portal. SolenOS prüft die Angaben manuell, bestimmt die konkrete Ausstattung und erstellt anschließend das verbindliche Gesamtangebot.",
},
{
question: "Kann ich nur UVI buchen?",
question: "Welche Leistungen kann ich auswählen?",
answer:
"Ja. UVI und Rauchwarnmelder sind einzeln oder gemeinsam wählbar. Das UVI-Paket umfasst die dafür benötigte Messtechnik und die zugehörigen digitalen Dienstleistungen; die Heizkostenabrechnung baut auf denselben erfassten Verbrauchsdaten auf. Rauchwarnmelder können auch später ergänzt werden die bereits erfasste Gebäudestruktur wird dabei weiterverwendet.",
"Monatliche Verbrauchsinformation, Heizkostenabrechnung und Rauchwarnmelder lassen sich im Schnellkonfigurator unabhängig voneinander hinzufügen oder abwählen."
},
{
question: "Kann ich bestehende Messtechnik weiterverwenden?",
@@ -384,10 +452,27 @@ const faqs: FaqItem[] = [
]
const trustProperties = [
{ icon: <GermanyIcon />, label: "Hosting in Deutschland" },
{ icon: <ShieldCheck />, label: "AES-geschützte Messdaten" },
{ icon: <Lock />, label: "TLS-verschlüsselte Übertragung" },
{ icon: <Users />, label: "Rollenbasierte Zugriffsrechte" },
{
icon: <GermanyIcon />,
label: "Daten ausschließlich in Deutschland gehostet",
to: "/wissen/datenschutz-sicherheit",
},
{
icon: <ShieldCheck />,
label: "AES-Verschlüsselung schützt gespeicherte Messdaten",
to: "/wissen/datenschutz-sicherheit",
},
{
icon: <Lock />,
label: "TLS schützt Daten während der Übertragung",
to: "/wissen/datenschutz-sicherheit",
},
{
icon: <Users />,
label:
"Bewohner sehen nur Daten ihrer eigenen Wohnung und ihres eigenen Nutzungszeitraums",
to: "/wissen/datenschutz-sicherheit",
},
]
export default function HomePage() {
@@ -401,18 +486,18 @@ export default function HomePage() {
<span className="text-primary">einfach</span> gemacht.
</>
}
lead="Fernauslesbare Messtechnik, monatliche UVI, Heizkostenabrechnung und Rauchwarnmelder vorkonfiguriert geliefert, digital verwaltet und auf Wunsch komplett installiert."
lead="Fernauslesbare Messtechnik, monatliche Verbrauchsinformation (UVI), Heizkostenabrechnung und Rauchwarnmelder vorkonfiguriert geliefert, digital betreut und auf Wunsch komplett installiert."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link to="/so-funktionierts">So funktioniert SolenOS</Link>
</Button>
</>
}
note="Wenige Angaben · unverbindliche Kostenschätzung"
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
visual={<HeroArt />}
/>
@@ -452,41 +537,32 @@ export default function HomePage() {
align="center"
eyebrow="Das System"
title="Alles, was für den laufenden Messbetrieb benötigt wird"
lead="SolenOS verbindet Messtechnik, Datenerfassung und Software zu einem durchgängigen System. Statt einzelne Komponenten selbst zusammenzustellen und miteinander zu verbinden, erhalten Sie eine Lösung, bei der Bestellung, Hardware, Konfiguration und laufender Betrieb zusammenspielen."
lead="SolenOS verbindet Messtechnik, Datenerfassung und Software in einem durchgängigen System. Bestellung, Hardware, Konfiguration und laufender Betrieb greifen dabei ineinander."
/>
<ExpandingCardGrid
items={operationsScope}
railSide="right"
className="mt-12"
/>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{operationsScope.map((item) => (
<Card key={item.title} className="p-6 gap-0">
<CardContent className="flex h-full flex-col items-start gap-3 p-0">
{item.media}
<h3 className="text-base font-bold text-navy">
{item.title}
</h3>
<p className="text-sm leading-relaxed text-muted-foreground">
{item.text}
</p>
</CardContent>
</Card>
))}
</div>
</Section>
<Section>
<SectionHeading
align="center"
eyebrow="Der Prozess"
title="So funktioniert SolenOS"
lead="Von der Bestellung bis zum laufenden Betrieb SolenOS verbindet Bestellung, Konfiguration und Installation miteinander."
/>
<ProcessSteps steps={processSteps} className="mt-12" />
<div className="rounded-3xl bg-gradient-to-b from-secondary to-card px-6 py-10 sm:px-10 sm:py-12 lg:px-12">
<SectionHeading
eyebrow="So funktioniert's"
title="Online in vier Schritten zum laufenden Messbetrieb"
lead="Sie starten ohne Erstgespräch: Objektdaten online erfassen, die passende Ausstattung von SolenOS bestimmen lassen, vorkonfiguriert erhalten und montieren."
/>
</div>
<StepFlow steps={processSteps} className="mt-4" />
</Section>
<Section>
<SectionHeading
align="center"
eyebrow="Installation"
title="Weniger Aufwand vor Ort"
lead="Die Installation eines Messsystems sollte nicht daran scheitern, dass vor Ort komplizierte Software eingerichtet oder jedes Gerät manuell zugeordnet werden muss. SolenOS bereitet die digitale Struktur bereits vor der Installation vor."
title="Vor Ort nur montieren"
lead="Vor Ort wird nur montiert. Eine Softwarekonfiguration ist nicht erforderlich. SolenOS bereitet Gebäudestruktur und Gerätezuordnung vor dem Versand vor."
/>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{lessEffort.map((item) => (
@@ -529,7 +605,7 @@ export default function HomePage() {
variant="link"
className="h-auto shrink-0 self-start p-0 sm:self-center"
>
<Link to="/produkte">
<Link to={task.to}>
Mehr erfahren
<ArrowRight />
</Link>
@@ -602,11 +678,11 @@ export default function HomePage() {
<div>
<SectionHeading
eyebrow="Preis"
title="Was kostet SolenOS?"
lead="Mit dem SolenOS-Konfigurator erhalten Sie schnell einen ersten Preis und können sofort bestellen."
title="Wie entsteht der Gesamtpreis?"
lead="Im Konfigurator wählen Sie Leistungen und erfassen die grundlegenden Gebäudedaten online. Der verbindliche Gesamtpreis folgt nach der vollständigen Konfiguration und manuellen Prüfung."
/>
<p className="mt-10 text-sm leading-relaxed text-muted-foreground">
Für einen ersten Preis brauchen wir Grundlegende Informationen:
Für die erste Ermittlung der Ausstattung benötigen wir:
</p>
<Checklist
className="mt-4"
@@ -619,11 +695,12 @@ export default function HomePage() {
]}
/>
<p className="mt-4 text-sm leading-relaxed text-muted-foreground">
Auf Basis dieser Angaben schätzt SolenOS die voraussichtlich
benötigte Geräteanzahl und berechnet einen ungefähren Preis.
SolenOS bestimmt daraus den voraussichtlichen Gerätebedarf. Im
Folgeprozess ergänzen Sie Details und Fotos; danach erhalten Sie
den geprüften Gesamtpreis.
</p>
<Button asChild size="lg" className="mt-6">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
</div>
<div>
@@ -631,16 +708,16 @@ export default function HomePage() {
<CardContent className="p-0">
<div>
<h3 className="text-base font-bold text-navy">
In fünf Schritten zum genauen Preis
In fünf Schritten zum verbindlichen Gesamtangebot
</h3>
</div>
<NumberedList steps={priceFlow} className="mt-6" />
</CardContent>
</Card>
<p className="mt-5 text-xs leading-relaxed text-muted-foreground italic">
Die erste Berechnung ist eine unverbindliche Preisindikation.
Die tatsächliche Ausstattung und der genaue Preis ergeben sich
aus der vollständigen Objektkonfiguration.
SolenOS veröffentlicht keine ungeprüften Preiswerte. Das
Gesamtangebot enthält die gewählten Leistungen, erforderliche
Gateway-Infrastruktur und die gewählte Montagevariante.
</p>
</div>
</div>
@@ -719,7 +796,7 @@ export default function HomePage() {
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/so-funktionierts">So funktioniert SolenOS</Link>
+23 -22
View File
@@ -30,9 +30,9 @@ const steps = [
<img src={vectors.uviBadge} alt="" className="w-9" />
</StepMedia>
),
title: "Gebäude anlegen und bestellen",
title: "Gebäudedaten online erfassen",
description:
"Bei der Konfiguration werden Gebäude, Wohnungen und die benötigten Leistungen erfasst. Auf Basis dieser Daten wird automatisch ermittelt, welche Messtechnik, Rauchwarnmelder und weiteren Komponenten benötigt werden.",
"Sie erfassen Gebäude, Wohnungen und Leistungen online. SolenOS ermittelt daraus die notwendige Ausstattung und prüft sie vor dem Versand.",
},
{
media: (
@@ -51,13 +51,12 @@ const steps = [
<AssetRender render={renders.gateway} imgClassName="w-9" />
</StepMedia>
),
title: "Geräte montieren",
title: "Vor Ort nur montieren",
description: (
<>
Die Geräte müssen vor Ort im Wesentlichen nur noch fachgerecht montiert
werden.{" "}
Die vorgesehenen Geräte werden vor Ort fachgerecht montiert.{" "}
<strong className="font-bold text-navy">
Keine aufwendige Softwarekonfiguration bei der Installation.
Eine Softwarekonfiguration vor Ort ist nicht erforderlich.
</strong>
</>
),
@@ -86,13 +85,15 @@ const steps = [
const journey = [
"Besucher versteht SolenOS",
"identifiziert seinen Anwendungsfall",
"berechnet einen ungefähren Preis",
"speichert seine Konfiguration",
"ergänzt die tatsächlichen Gebäudedaten",
"erhält den genauen Preis",
"wählt Leistungen aus",
"erfasst Gebäudedaten online",
"erhält einen persönlichen Link",
"ergänzt Details und Fotos",
"SolenOS prüft die Ausstattung",
"erhält ein verbindliches Gesamtangebot",
"erteilt den Auftrag",
"SolenOS bereitet Gebäude und Geräte vor",
"vorkonfigurierte Hardware wird geliefert",
"vorkonfigurierte Technik wird geliefert",
"Montage vor Ort",
"automatischer laufender Betrieb",
]
@@ -101,22 +102,22 @@ const faqs: FaqItem[] = [
{
question: "Wo fange ich an?",
answer:
"Im Konfigurator. Dort erfassen Sie Gebäude, Wohnungen und die benötigten Leistungen und erhalten eine erste Preisindikation. Aus denselben Daten wird anschließend der Auftrag aufgebaut.",
"Im Konfigurator. Dort wählen Sie Leistungen und erfassen Gebäude sowie Wohnungen online. Die Angaben werden in die anschließende Detailkonfiguration übernommen.",
},
{
question: "Wie verbindlich ist der Preis aus dem Konfigurator?",
question: "Wann erhalte ich den verbindlichen Gesamtpreis?",
answer:
"Er ist eine Preisschätzung auf Basis Ihrer Angaben. Den genauen Preis erhalten Sie, sobald die tatsächlichen Gebäudedaten ergänzt und geprüft sind erst danach wird der Auftrag erteilt.",
"Nach dem Schnellkonfigurator ergänzen Sie Gebäudedetails und Fotos im Portal. SolenOS prüft die Einbausituationen und die notwendige Ausstattung manuell und erstellt anschließend das verbindliche Gesamtangebot.",
},
{
question: "Muss ich meine Gebäudedaten mehrfach erfassen?",
answer:
"Nein. Die einmal eingegebenen Gebäudedaten werden durch den gesamten Prozess weiterverwendet: von der Preisschätzung über die Gerätevorbereitung bis in den laufenden Betrieb.",
"Nein. Die einmal eingegebenen Gebäudedaten werden von der ersten Online-Erfassung über Gerätevorbereitung und Montage bis in den laufenden Betrieb weiterverwendet.",
},
{
question: "Wer montiert die Geräte?",
answer:
"Das entscheiden Sie: Sie selbst, Ihr Fachbetrieb oder SolenOS als Komplettlösung. Die Hardware kommt objektbezogen vorkonfiguriert an, eine aufwendige Softwarekonfiguration vor Ort entfällt.",
"Das entscheiden Sie: Sie selbst, Ihr Fachbetrieb oder SolenOS als Komplettlösung. Die Technik kommt objektbezogen vorkonfiguriert an; eine Softwarekonfiguration vor Ort ist nicht erforderlich.",
},
{
question: "Was passiert nach der Installation?",
@@ -182,8 +183,8 @@ export default function HowItWorksPage() {
sondern durch den gesamten Prozess weiterverwendet.
</p>
<p className="mt-6 font-mono text-xs tracking-wide text-muted-foreground uppercase">
Preisschätzung Konfiguration Bestellung Gerätevorbereitung
Installation Betrieb
Online-Erfassung Detailkonfiguration Gesamtangebot
Gerätevorbereitung Installation Betrieb
</p>
</div>
</Section>
@@ -194,7 +195,7 @@ export default function HowItWorksPage() {
align="center"
eyebrow="Häufige Fragen"
title="Fragen zum Ablauf"
lead="Von der ersten Preisindikation bis zum laufenden Betrieb die häufigsten Fragen zum SolenOS-Prozess."
lead="Von der Online-Erfassung bis zum laufenden Betrieb die häufigsten Fragen zum SolenOS-Prozess."
/>
<FaqAccordion className="mt-10" items={faqs} />
</Section>
@@ -203,11 +204,11 @@ export default function HowItWorksPage() {
<Section className="pt-0">
<CtaBanner
icon={<Calculator />}
title="Was kostet SolenOS für Ihr Gebäude?"
description="Mit wenigen Angaben erhalten Sie eine erste Preisindikation."
title="Konfiguration für Ihr Gebäude starten"
description="Leistungen auswählen, Gebäudedaten online erfassen und die geprüfte Ausstattung vorbereiten."
actions={
<Button variant="inverse" size="lg" asChild>
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
}
/>
+26 -36
View File
@@ -13,9 +13,9 @@ import {
} from "@/components/solenos/illustrations"
const trustItems = [
{ icon: ClipboardCheck, label: ["Klare", "Vertragsstruktur"] },
{ icon: ShieldCheck, label: ["Keine versteckten", "Gebühren"] },
{ icon: Users2, label: ["Rabatte für", "große Objekte"] },
{ icon: ClipboardCheck, label: ["Geprüfter", "Leistungsumfang"] },
{ icon: ShieldCheck, label: ["Verbindliches", "Gesamtangebot"] },
{ icon: Users2, label: ["Passend zum", "konkreten Objekt"] },
] as const
export default function KlarpreisPage() {
@@ -35,7 +35,7 @@ export default function KlarpreisPage() {
SolenOS <span className="text-primary">Klarpreis</span>
</>
}
lead="Drei sauber getrennte Leistungen. Einfache Preise. Keine Überraschungen."
lead="Ein vollständiger Leistungsumfang und ein verbindlicher Gesamtpreis nach Prüfung der konkreten Objektdaten."
extra={
<div className="inline-flex items-center gap-3 rounded-full border border-border bg-card px-4 py-2 text-sm text-muted-foreground shadow-pill">
<IconBadge variant="soft" size="sm">
@@ -57,20 +57,18 @@ export default function KlarpreisPage() {
<Section className="pt-0">
<div className="grid gap-6 lg:grid-cols-3">
<PriceCard
title="RWM-Hardware"
cadence="Einmalig"
price="42,00 €"
unit="je Wohnung einmalig"
allocation={{ label: "Nicht umlagefähig", tone: "negative" }}
description="Rauchwarnmelder inklusive Montage und Eigentumsübertragung."
title="Rauchwarnmelder"
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="Geräteanzahl und Montagevariante"
description="Benötigte Rauchwarnmelder, vereinbarte Montage und Eigentumsübertragung."
media={<SmokeDetectorIllustration />}
/>
<PriceCard
title="Ferninspektion"
cadence="Monatlich"
price="2,20 €"
unit="je Wohnung / Monat"
allocation={{ label: "Grundsätzlich umlagefähig", tone: "positive" }}
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="passend zum vereinbarten Leistungsumfang"
description="Regelmäßige Prüfung, Nachweise, Meldungen und Support."
media={
<div className="relative">
@@ -80,16 +78,11 @@ export default function KlarpreisPage() {
}
/>
<PriceCard
title={
<span className="text-[1.15rem] whitespace-nowrap">
UVI-Komplettbetrieb
</span>
}
cadence="Monatlich"
price="10,50 €"
unit="je Wohnung / Monat"
allocation={{ label: "Grundsätzlich umlagefähig", tone: "positive" }}
description="Sensoren, Daten, Portal und Verbrauchsinformation inklusive."
title="Messbetrieb und Abrechnung"
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="nach vollständiger Objektkonfiguration"
description="Je nach Auswahl: Messtechnik, erforderliche Gateway-Infrastruktur, Daten, Portale, Heizkostenabrechnung und monatliche Verbrauchsinformation (UVI)."
media={<SensorCubesIllustration />}
/>
</div>
@@ -102,16 +95,13 @@ export default function KlarpreisPage() {
</IconBadge>
<div>
<div className="font-semibold text-white/80">
Gesamtkosten laufend
</div>
<div className="mt-1 flex flex-wrap items-baseline gap-x-3">
<span className="text-4xl font-extrabold tracking-tight text-white">
12,70
</span>
<span className="text-sm text-white/75">
je Wohnung / Monat
</span>
Verbindlicher Gesamtpreis
</div>
<p className="mt-1 max-w-md text-sm leading-relaxed text-white/85">
Nach der vollständigen Objektkonfiguration und manuellen
Prüfung erhalten Sie ein Gesamtangebot statt ungeprüfter
Einzelpreise.
</p>
</div>
<Separator
orientation="vertical"
@@ -121,9 +111,9 @@ export default function KlarpreisPage() {
<IconBadge variant="glass" size="sm">
<Tag />
</IconBadge>
<p className="max-w-56 text-sm leading-snug text-white/80">
Alle laufenden Leistungen klar gebündelt ohne versteckte
Zusatzkosten.
<p className="max-w-64 text-sm leading-snug text-white/80">
Gewählte Leistungen, erforderliche Gateway-Infrastruktur und
Montagevariante werden gemeinsam ausgewiesen.
</p>
</div>
</div>
@@ -0,0 +1,338 @@
import { Link } from "react-router-dom"
import {
ArrowRight,
CalendarClock,
Euro,
FileCheck2,
Flame,
Layers,
Droplets,
UserRoundCog,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Checklist } from "@/components/solenos/checklist"
import {
ComparisonTable,
type ComparisonColumn,
type ComparisonRow,
} from "@/components/solenos/comparison-table"
import { CtaBanner } from "@/components/solenos/cta-banner"
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
import { EuroCircleIcon } from "@/components/solenos/line-icons"
import { NumberedList } from "@/components/solenos/process-steps"
import { Section, SectionHeading } from "@/components/solenos/section"
import {
AssetRender,
renders,
vectors,
} from "@/components/solenos/site-assets"
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
import { heizkostenv } from "@/lib/legal"
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
/**
* Hero art: the glass money render as the subject the costs being
* distributed with the consumption bars as their basis and the euro orb as
* the accent. Decorative; the copy carries the meaning.
*/
function BillingHeroArt() {
return (
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
<img
src={vectors.euroOrb}
alt=""
className="absolute top-[2%] right-[4%] w-[22%]"
/>
<AssetRender
render={renders.glassMoney}
alt=""
className="mx-auto w-[68%]"
loading="eager"
/>
<AssetRender
render={renders.barChartManyFull}
alt=""
className="absolute bottom-0 left-0 w-[32%]"
/>
</div>
)
}
const DISTRIBUTION: Topic[] = [
{
icon: <Flame />,
title: "Kosten der Wärmeversorgung",
body: "Mindestens 50 und höchstens 70 Prozent der Kosten werden nach dem erfassten Wärmeverbrauch verteilt, die übrigen nach Wohn- oder Nutzfläche beziehungsweise umbautem Raum.",
source: { cite: "§ 7 HeizkostenV", href: heizkostenv("7") },
},
{
icon: <Droplets />,
title: "Kosten der Warmwasserversorgung",
body: "Für die zentrale Warmwasserversorgung gilt derselbe Rahmen: 50 bis 70 Prozent nach erfasstem Warmwasserverbrauch, der Rest nach Wohn- oder Nutzfläche.",
source: { cite: "§ 8 HeizkostenV", href: heizkostenv("8") },
},
{
icon: <Layers />,
title: "Verbundene Anlagen",
body: "Erzeugt eine Anlage Wärme und Warmwasser gemeinsam, werden die einheitlich entstandenen Betriebskosten zuerst zwischen beiden Zwecken aufgeteilt anhand der auf die Warmwasserbereitung entfallenden Wärmemenge, die dafür mit einem Wärmezähler gemessen wird.",
source: { cite: "§ 9 HeizkostenV", href: heizkostenv("9") },
},
{
icon: <UserRoundCog />,
title: "Nutzungszeiträume und Nutzerwechsel",
body: "Weil die Ausstattung fernablesbar ist, werden Zwischenwerte zum Stichtag aus der laufenden Datenerfassung übernommen und die Verbräuche dem jeweiligen Nutzungszeitraum zugeordnet.",
},
]
const YEAR_FLOW = [
{
title: "Abrechnungszeitraum festlegen",
description:
"Zeitraum, Verteilungsmaßstäbe und Gebäudedaten werden im Portal hinterlegt.",
},
{
title: "Verbrauchsdaten laufen automatisch mit",
description:
"Die fernablesbare Messtechnik liefert Monatswerte dieselbe Datenbasis wie für die UVI.",
},
{
title: "Kosten des Abrechnungszeitraums erfassen",
description:
"Brennstoff- oder Wärmelieferkosten, Betriebsstrom, Wartung und Messdienstkosten werden zugeordnet.",
},
{
title: "Abrechnung erstellen und prüfen",
description:
"Verteilung nach den gewählten Maßstäben, inklusive Nutzerwechsel und Leerstand.",
},
{
title: "Dokumente bereitstellen",
description:
"Einzelabrechnungen und Pflichtangaben stehen für Eigentümer, Verwaltung und Bewohner im Portal bereit.",
},
]
const BASIS_COLUMNS: ComparisonColumn[] = [
{ title: "Mit SolenOS", highlight: true },
{ title: "Klassischer Ableseweg", check: false },
]
const BASIS_ROWS: ComparisonRow[] = [
{
label: "Datenerhebung",
cells: ["laufend fernabgelesen", "Ablesetermin je Wohnung"],
},
{
label: "Zugang zur Wohnung",
cells: ["nicht erforderlich", "erforderlich, inklusive Nachtermine"],
},
{
label: "Nutzerwechsel",
cells: ["Zwischenwerte aus der laufenden Erfassung", "Zwischenablesung vor Ort"],
},
{
label: "Unterjährige Information",
cells: ["monatlich aus derselben Datenbasis", "separat zu organisieren"],
},
{
label: "Datenbestand",
cells: [
"Gebäude, Wohnungen und Geräte einmalig strukturiert",
"je Leistung erneut zu übermitteln",
],
},
]
const INCLUDED = [
"Verteilung der Wärme- und Warmwasserkosten nach HeizkostenV",
"Berücksichtigung von Nutzerwechseln und Leerständen",
"Einzelabrechnung je Wohnung mit den Pflichtangaben nach § 6a Absatz 3",
"Bereitstellung der Dokumente im Portal für Verwaltung und Bewohner",
"Übernahme der Verbrauchsdaten aus dem laufenden Messbetrieb",
]
const NOT_INCLUDED = [
"Betriebskostenabrechnung des Gebäudes (kalte Betriebskosten)",
"Beschaffung von Brennstoff oder Wärmelieferung",
"Mietvertragliche Prüfung und Mahnwesen",
]
const FAQS: FaqItem[] = [
{
question: "Brauche ich für die Abrechnung einen Ablesetermin?",
answer:
"Nein. Die Werte stammen aus der fernablesbaren Messtechnik und werden zum Ende des Abrechnungszeitraums automatisch übernommen. Auch Zwischenwerte bei Nutzerwechseln entstehen ohne Termin in der Wohnung.",
},
{
question: "Welche Verteilungsmaßstäbe kann ich wählen?",
answer:
"Für die Wärmekosten sind mindestens 50 und höchstens 70 Prozent nach erfasstem Verbrauch zu verteilen (§ 7 Absatz 1 HeizkostenV); für Warmwasser gilt derselbe Rahmen (§ 8 Absatz 1). Die Wahl der Maßstäbe bleibt dem Gebäudeeigentümer überlassen und wird für künftige Abrechnungszeiträume im Portal hinterlegt (§ 6 Absatz 4).",
},
{
question: "Was passiert, wenn nicht verbrauchsabhängig abgerechnet wird?",
answer:
"Nach § 12 Absatz 1 HeizkostenV kann der Nutzer seinen Kostenanteil um 15 Prozent kürzen, soweit entgegen der Verordnung nicht verbrauchsabhängig abgerechnet wird. Fehlt die fernablesbare Ausstattung oder werden die Informationen nach § 6a nicht vollständig mitgeteilt, sind es 3 Prozent.",
},
{
question: "Kann ich die Abrechnung ohne UVI buchen?",
answer:
"Die Abrechnung setzt erfasste Verbrauchsdaten voraus. Läuft der Messbetrieb über SolenOS, ist die Datenbasis vorhanden und die Abrechnung nutzt sie direkt die monatliche Verbrauchsinformation entsteht dann aus demselben Datenbestand.",
},
{
question: "Wie sehen Bewohner ihre Abrechnung?",
answer:
"Die Einzelabrechnung wird im Mieterportal bereitgestellt und kann als Dokument ausgegeben werden. Bewohner sehen ausschließlich die Unterlagen ihres eigenen Nutzungsverhältnisses.",
},
]
export default function HeizkostenabrechnungProductPage() {
return (
<ProductPage
breadcrumb="Heizkostenabrechnung"
eyebrow="Produkt"
title={<>Heizkosten&shy;abrechnung</>}
tagline="Aus den Daten, die ohnehin laufen."
lead="Die jährliche Abrechnung der Heiz- und Warmwasserkosten entsteht aus denselben fernabgelesenen Verbrauchswerten wie die monatliche Verbrauchsinformation (UVI) verteilt nach den Maßstäben der Heizkostenverordnung, ohne Ablesetermin im Gebäude."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link to="/produkte/uvi">Verbrauchsinformation ansehen</Link>
</Button>
</>
}
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
visual={<BillingHeroArt />}
features={[
{
icon: <EuroCircleIcon />,
title: "Verbrauchsabhängig verteilt",
sub: "50 bis 70 Prozent nach Verbrauch, der Rest nach Fläche.",
},
{
icon: <CalendarClock strokeWidth={1.6} />,
title: "Nutzerwechsel inklusive",
sub: "Zwischenwerte zum Stichtag ohne Termin in der Wohnung.",
},
{
icon: <FileCheck2 strokeWidth={1.6} />,
title: "Mit Pflichtangaben",
sub: "Energieträger, Entgelte und Vergleichswerte nach § 6a.",
},
{
icon: <Layers strokeWidth={1.6} />,
title: "Ein Datenbestand",
sub: "Gebäude, Wohnungen und Geräte werden einmal strukturiert.",
},
]}
closing={
<Section className="pt-0 sm:pt-0 lg:pt-0">
<CtaBanner
icon={<Euro />}
title="Abrechnung und Messbetrieb aus einer Hand"
description="Wir gehen Abrechnungszeitraum, Verteilungsmaßstäbe und Ausstattung für Ihr Gebäude konkret durch."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/kontakt">Kontakt aufnehmen</Link>
</Button>
</>
}
/>
</Section>
}
>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Grundlagen"
title="Was in die Abrechnung einfließt"
lead="Die Heizkostenverordnung gibt vor, welche Kosten verbrauchsabhängig zu verteilen sind und in welchem Rahmen. SolenOS setzt diese Regeln auf die erfassten Werte des Gebäudes an."
/>
<TopicCards
items={DISTRIBUTION}
className="sm:grid-cols-2 lg:grid-cols-4"
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<div className="grid items-start gap-10 lg:grid-cols-2 lg:gap-14">
<div>
<SectionHeading
eyebrow="Ablauf"
title="Ein Abrechnungsjahr mit SolenOS"
lead="Die Abrechnung ist kein eigenes Projekt am Jahresende, sondern das Ergebnis des laufenden Messbetriebs."
/>
<Button asChild variant="outline" size="lg" className="mt-8">
<Link to="/so-funktionierts">
So funktioniert SolenOS
<ArrowRight />
</Link>
</Button>
</div>
<Card className="gap-0 p-6 sm:p-8">
<NumberedList steps={YEAR_FLOW} />
</Card>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Im Vergleich"
title="Woher die Abrechnungsdaten kommen"
lead="Der Unterschied liegt nicht in der Rechenvorschrift, sondern in der Datenbeschaffung."
/>
<ComparisonTable
className="mt-10"
columns={BASIS_COLUMNS}
rows={BASIS_ROWS}
labelHead="Abrechnungsgrundlage"
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Leistungsumfang"
title="Enthalten und nicht enthalten"
/>
<div className="mt-10 grid gap-6 lg:grid-cols-2">
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">Enthalten</h3>
<Checklist items={INCLUDED} className="mt-5" />
</Card>
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">
Nicht Teil der Heizkostenabrechnung
</h3>
<Checklist items={NOT_INCLUDED} className="mt-5" tone="excluded" />
<p className="mt-6 text-sm leading-relaxed text-muted-foreground">
Die Kosten der Verbrauchserfassung, ihrer Verwendung und der
Abrechnungs- und Verbrauchsinformationen gehören zu den
umlagefähigen Betriebskosten der Heizungsanlage.
</p>
<p className="mt-2 text-xs text-muted-foreground">
§ 7 Absatz 2 HeizkostenV
</p>
</Card>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
eyebrow="Häufige Fragen"
title="Fragen zur Heizkostenabrechnung"
/>
<FaqAccordion className="mt-10" items={FAQS} />
</Section>
<ProductCrossLinks current="/produkte/heizkostenabrechnung" />
</ProductPage>
)
}
@@ -1,3 +1,4 @@
import type * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight, Euro } from "lucide-react"
@@ -7,36 +8,30 @@ import { CtaBanner } from "@/components/solenos/cta-banner"
import { Container, Section, SectionHeading } from "@/components/solenos/section"
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
import { AssetRender, renders } from "@/components/solenos/site-assets"
import { PRODUCTS } from "@/pages/products/product-page"
const products = [
{
title: "UVI",
sub: "Monatliche Verbrauchsinformation",
description:
"Verbrauchsdaten werden automatisch erfasst und die regelmäßigen Verbrauchsinformationen für die Bewohner bereitgestellt.",
illustration: (
<div className="relative">
<AssetRender render={renders.gateway} imgClassName="w-28" />
</div>
),
},
{
title: "Heizkostenabrechnung",
sub: "Verbrauchsdaten korrekt abrechnen",
description:
"Verbrauchsdaten strukturiert erfassen und für die jährliche Abrechnung nutzen.",
illustration: <AssetRender render={renders.glassDonutShadow} alt="" className="w-28" />,
},
{
title: "Rauchwarnmelder",
sub: "Sicher überwacht, zentral verwaltet",
description:
"Rauchwarnmelder gemeinsam mit der übrigen Gebäudetechnik planen und verwalten.",
illustration: <AssetRender render={renders.smokeAlarm} imgClassName="w-28" />,
},
]
/**
* One illustration per product, keyed by route so the registry stays free of
* layout decisions. The first three products are the bookable services and get
* a card each; the fourth is the technical layer underneath them and closes the
* grid as a wide strip.
*/
const illustrations: Record<string, React.ReactNode> = {
"/produkte/uvi": (
<AssetRender render={renders.uviGlassPane} alt="" className="w-32" />
),
"/produkte/heizkostenabrechnung": (
<AssetRender render={renders.glassMoney} alt="" className="w-28" />
),
"/produkte/rauchwarnmelder": (
<AssetRender render={renders.glassSmokeDetector} alt="" className="w-24" />
),
}
export default function ProductsPage() {
const services = PRODUCTS.slice(0, 3)
const infrastructure = PRODUCTS[3]
return (
<>
<div className="bg-hero-glow">
@@ -44,34 +39,33 @@ export default function ProductsPage() {
<Section className="pt-6 pb-10 sm:pt-7 sm:pb-12 lg:pt-8 lg:pb-14">
<PageBreadcrumb
className="mb-4"
items={[
{ label: "Startseite", to: "/" },
{ label: "Produkte" },
]}
items={[{ label: "Startseite", to: "/" }, { label: "Produkte" }]}
/>
<SectionHeading
title="Unsere Produkte"
lead="Hier finden Sie eine Übersicht unserer Produkte für Messtechnik, Abrechnung und Verwaltung."
/>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{products.map((s) => (
<Card key={s.title} className="gap-0 p-6 sm:p-7">
{services.map((product) => (
<Card key={product.to} className="gap-0 p-6 sm:p-7">
<h3 className="text-xl font-extrabold tracking-tight text-navy">
{s.title}
{product.title}
</h3>
<p className="mt-1 text-sm text-muted-foreground">{s.sub}</p>
<p className="mt-1 text-sm text-muted-foreground">
{product.sub}
</p>
<div className="my-6 flex h-32 flex-1 items-center justify-center">
{s.illustration}
{illustrations[product.to]}
</div>
<p className="text-sm leading-relaxed text-muted-foreground">
{s.description}
{product.summary}
</p>
<Button
variant="link"
className="mt-3 self-start px-0"
asChild
>
<Link to="/produkte">
<Link to={product.to}>
Mehr erfahren
<ArrowRight />
</Link>
@@ -87,15 +81,14 @@ export default function ProductsPage() {
/>
<div className="min-w-0 flex-1">
<h3 className="text-sm font-bold text-navy">
Messtechnik und Infrastruktur
{infrastructure.title}
</h3>
<p className="mt-0.5 text-sm leading-relaxed text-muted-foreground">
Fernauslesbare Zähler, Sensoren und
Kommunikationsinfrastruktur für den laufenden Betrieb.
{infrastructure.summary}
</p>
</div>
<Button variant="link" className="shrink-0 px-0 sm:px-4" asChild>
<Link to="/produkte">
<Link to={infrastructure.to}>
Mehr erfahren
<ArrowRight />
</Link>
@@ -108,11 +101,11 @@ export default function ProductsPage() {
<Section className="pt-0">
<CtaBanner
icon={<Euro />}
title="Jetzt Preis für Ihr Gebäude berechnen"
description="In wenigen Minuten zur ersten Preisindikation."
title="Ausstattung für Ihr Gebäude konfigurieren"
description="Leistungen auswählen, Gebäudedaten online erfassen und die Prüfung vorbereiten."
actions={
<Button variant="inverse" size="lg" asChild>
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
}
/>
@@ -0,0 +1,396 @@
import { Link } from "react-router-dom"
import {
ArrowRight,
ChevronRight,
Droplets,
Flame,
Gauge,
Lock,
Puzzle,
RadioTower,
Router,
Users,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Checklist } from "@/components/solenos/checklist"
import { CtaBanner } from "@/components/solenos/cta-banner"
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
import {
GermanyIcon,
PreconfiguredIcon,
} from "@/components/solenos/line-icons"
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
import { StepFlow } from "@/components/solenos/process-steps"
import { Section, SectionHeading } from "@/components/solenos/section"
import {
AssetRender,
renders,
vectors,
} from "@/components/solenos/site-assets"
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
import { TrustPanel, TrustShieldArt } from "@/components/solenos/trust-panel"
import { heizkostenv } from "@/lib/legal"
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
const MESSEV_ANLAGE_7 =
"https://www.gesetze-im-internet.de/messev/anlage_7.html"
/**
* Hero art: the gateway as the subject the piece that ties the building
* together with the heat meter beside it and the wireless orb as the accent
* for the radio link between them. Decorative; the copy carries the meaning.
*/
function InfrastructureHeroArt() {
return (
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
<img
src={vectors.wirelessOrb}
alt=""
className="absolute top-[2%] right-[6%] w-[22%]"
/>
<AssetRender
render={renders.gateway}
className="mx-auto w-[62%]"
loading="eager"
/>
<AssetRender
render={renders.heatMeter}
className="absolute bottom-0 left-0 w-[34%]"
/>
</div>
)
}
const COMPONENTS: Topic[] = [
{
icon: <Flame />,
title: "Heizkostenverteiler",
body: "Elektronische Heizkostenverteiler erfassen an jedem Heizkörper den anteiligen Wärmeverbrauch. Sie sind keine Messgeräte im Sinne des Eichrechts und daher nicht eichpflichtig; getauscht werden sie nach Ablauf ihrer Nutzungsdauer.",
source: { cite: "§ 5 HeizkostenV", href: heizkostenv("5") },
},
{
icon: <Gauge />,
title: "Wärmezähler",
body: "Wo die Wärme je Einheit direkt gemessen wird etwa bei Fußbodenheizung oder Wohnungsstationen erfassen Wärmezähler die Wärmemenge in Kilowattstunden. Ihre Eichfrist beträgt sechs Jahre.",
source: { cite: "Anlage 7 MessEV", href: MESSEV_ANLAGE_7 },
},
{
icon: <Droplets />,
title: "Kalt- und Warmwasserzähler",
body: "Warmwasserzähler sind für die verbrauchsabhängige Abrechnung erforderlich, Kaltwasserzähler ergänzen die Erfassung. Für beide gilt eine Eichfrist von sechs Jahren.",
source: { cite: "Anlage 7 MessEV", href: MESSEV_ANLAGE_7 },
},
{
icon: <Router />,
title: "Gateway und Datenübertragung",
body: "Das Gateway empfängt die Funkdaten der Geräte im Gebäude, puffert sie und überträgt sie an SolenOS. Ein Zugang zu den einzelnen Wohnungen ist dafür nicht erforderlich.",
},
]
const DATA_PATH = [
{
media: <AssetRender render={renders.heatMeter} imgClassName="w-28" />,
title: "Gerät misst und funkt",
description:
"Zähler und Heizkostenverteiler senden ihre Werte per Funk; die Nutzdaten sind dabei AES-geschützt.",
},
{
media: <AssetRender render={renders.gateway} imgClassName="w-28" />,
title: "Gateway sammelt",
description:
"Die erforderliche Gateway-Infrastruktur empfängt die Funkpakete der angebundenen Geräte und wird von SolenOS passend zum Objekt bestimmt.",
},
{
media: (
<AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />
),
title: "Übertragung an SolenOS",
description:
"Die Daten werden TLS-verschlüsselt an die Plattform übertragen und dort verarbeitet.",
},
{
media: (
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
),
title: "Nutzung im Portal",
description:
"Aus denselben Werten entstehen Geräteüberwachung, monatliche UVI und Jahresabrechnung.",
},
]
const LIFECYCLE = [
{
device: "Wärmezähler",
interval: "Eichfrist 6 Jahre",
note: "Austausch oder Nacheichung vor Ablauf der Eichfrist",
},
{
device: "Kalt- und Warmwasserzähler",
interval: "Eichfrist 6 Jahre",
note: "seit der Novelle der MessEV einheitlich sechs Jahre",
},
{
device: "Elektronische Heizkostenverteiler",
interval: "nicht eichpflichtig",
note: "Austausch nach Ablauf der Nutzungsdauer des Geräts",
},
{
device: "Rauchwarnmelder",
interval: "Austausch nach 10 Jahren",
note: "jährliche Inspektion nach DIN 14676-1, per Ferninspektion möglich",
},
]
const REUSE_POSSIBLE = [
"Geräte sind fernablesbar, also ohne Zugang zur Wohnung auslesbar",
"die Funkschnittstelle folgt einem offenen Standard (OMS)",
"das Schlüsselmaterial der Geräte liegt vor",
"die Eichfrist der betroffenen Zähler ist nicht abgelaufen",
]
const REUSE_IMPOSSIBLE = [
"nicht fernablesbare Verbrauchserfassung",
"herstellergebundene Funkprotokolle ohne verfügbares Schlüsselmaterial",
"Zähler mit abgelaufener Eichfrist",
]
const TRUST_PROPERTIES = [
{ icon: <GermanyIcon />, label: "Hosting in Deutschland" },
{ icon: <Lock />, label: "AES-geschützte Funkdaten" },
{ icon: <RadioTower />, label: "TLS-Übertragung ab Gateway" },
{ icon: <Users />, label: "Rollenbasierte Zugriffsrechte" },
]
const FAQS: FaqItem[] = [
{
question: "Was kostet die Messtechnik?",
answer:
"SolenOS bestimmt die benötigten Sensoren und die erforderliche Gateway-Infrastruktur aus der vollständigen Gebäudekonfiguration. Beides wird im geprüften Gesamtangebot gemeinsam mit den gewählten Leistungen ausgewiesen; ungeprüfte Einzelpreise veröffentlichen wir nicht.",
},
{
question: "Kann ich vorhandene Zähler weiterverwenden?",
answer:
"Das hängt von den Geräten ab. Voraussetzung ist, dass sie fernablesbar sind, auf offenen Standards basieren und das Schlüsselmaterial vorliegt. Vorhandene Messtechnik wird deshalb bei der vollständigen Konfiguration je Gebäude und Einheit erfasst und geprüft.",
},
{
question: "Was bedeutet Interoperabilität konkret?",
answer:
"Nach § 5 Absatz 5 HeizkostenV dürfen seit dem 1. Dezember 2022 nur noch fernauslesbare Ausstattungen installiert werden, die mit Geräten gleicher Art anderer Hersteller interoperabel sind ein anderer Dienstleister muss sie also selbst fernablesen können. Das Schlüsselmaterial ist dem Gebäudeeigentümer kostenfrei zur Verfügung zu stellen.",
},
{
question: "Braucht das Gateway einen Internetanschluss im Gebäude?",
answer:
"Nein. Das Gateway überträgt die gesammelten Daten über Mobilfunk, sodass kein Anschluss im Haus und keine Mitnutzung eines fremden Netzes erforderlich ist.",
},
{
question: "Wird gemeldet, wenn ein Gerät ausfällt?",
answer:
"Ja. Ausbleibende Werte, Batteriewarnungen und Manipulationsmeldungen laufen im Portal auf, sodass ein Gerät vor der nächsten Abrechnung geprüft werden kann nicht erst am Jahresende.",
},
]
export default function MesstechnikProductPage() {
return (
<ProductPage
breadcrumb="Messtechnik und Infrastruktur"
eyebrow="Produkt"
title={
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
Messtechnik und Infrastruktur
</span>
}
tagline="Die Grundlage für alles andere."
lead="Zähler, Heizkostenverteiler und Gateway erfassen und übertragen die Werte, aus denen monatliche Verbrauchsinformation (UVI) und Heizkostenabrechnung entstehen fernablesbar, interoperabel und objektbezogen vorkonfiguriert geliefert."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Ausstattung ermitteln</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link to="/wissen/datenschutz-sicherheit">
Datenschutz & Sicherheit
</Link>
</Button>
</>
}
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
visual={<InfrastructureHeroArt />}
features={[
{
icon: <RadioTower strokeWidth={1.6} />,
title: "Fernablesbar",
sub: "Ablesung ohne Zugang zu den einzelnen Wohnungen.",
},
{
icon: <Puzzle strokeWidth={1.6} />,
title: "Interoperabel",
sub: "Offene Funkstandards, Schlüsselmaterial bleibt verfügbar.",
},
{
icon: <PreconfiguredIcon />,
title: "Vorkonfiguriert",
sub: "Geräte kommen Gebäude und Einheit zugeordnet an.",
},
{
icon: <GermanyIcon />,
title: "Daten in Deutschland",
sub: "Verschlüsselte Übertragung, Hosting in Falkenstein.",
},
]}
closing={
<Section className="pt-0 sm:pt-0 lg:pt-0">
<CtaBanner
icon={<Gauge />}
title="Welche Ausstattung braucht Ihr Gebäude?"
description="Aus Gebäudedaten, Wärmeverteilung und Wohnungszahl ermitteln wir Geräteart und Menge und daraus den Preis."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Ausstattung ermitteln</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/kontakt">Bestand prüfen lassen</Link>
</Button>
</>
}
/>
</Section>
}
>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Komponenten"
title="Was im Gebäude verbaut wird"
lead="Welche Geräte zum Einsatz kommen, hängt von der Wärmeverteilung und der Einbausituation ab. Zusammengestellt und vorbereitet wird die Ausstattung objektbezogen."
/>
<TopicCards
items={COMPONENTS}
className="sm:grid-cols-2 lg:grid-cols-4"
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Datenweg"
title="Vom Heizkörper bis ins Portal"
lead="Jeder Abschnitt hat seine eigene Schutzmaßnahme von der Funkstrecke in der Wohnung bis zur Verarbeitung auf der Plattform."
/>
<StepFlow steps={DATA_PATH} className="mt-10" />
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<NoticeBanner
title="Nachrüstung bis 31.12.2026 erforderlich"
source={{ cite: "§ 5 HeizkostenV", href: heizkostenv("5") }}
visual={<NoticeArt className="max-lg:hidden" />}
action={
<Button
asChild
variant="link"
className="h-auto p-0 text-[15px] font-bold text-brand-800 underline decoration-brand-500 decoration-2 underline-offset-[6px] hover:text-navy hover:decoration-brand-800"
>
<Link to="/wissen/gesetzliche-anforderungen">
Mehr erfahren
<ChevronRight />
</Link>
</Button>
}
>
Nicht fernablesbare Verbrauchserfassung muss grundsätzlich bis zum
31. Dezember 2026 durch Nachrüstung oder Austausch die Anforderungen
an Fernablesbarkeit und Interoperabilität erfüllen.
</NoticeBanner>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Bestand"
title="Was übernommen werden kann"
lead="Nicht jedes Gebäude beginnt bei null. Entscheidend ist, ob die vorhandenen Geräte fernablesbar und offen angebunden sind."
/>
<div className="mt-10 grid gap-6 lg:grid-cols-2">
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">
Weiterverwendung möglich, wenn
</h3>
<Checklist items={REUSE_POSSIBLE} className="mt-5" />
</Card>
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">
Austausch erforderlich bei
</h3>
<Checklist
items={REUSE_IMPOSSIBLE}
className="mt-5"
tone="excluded"
/>
<p className="mt-6 text-sm leading-relaxed text-muted-foreground">
Was übernommen werden kann, wirkt sich unmittelbar auf die
Geräteliste und damit auf den Preis aus. Der Bestand wird deshalb
bei der vollständigen Objektkonfiguration je Einheit erfasst.
</p>
</Card>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Fristen"
title="Eichfristen und Nutzungsdauern"
lead="Wann ein Gerät getauscht werden muss, ergibt sich aus dem Eichrecht oder aus seiner Nutzungsdauer. Fällige Termine werden je Gebäude im Portal geführt."
/>
<Card className="mt-10 gap-0 divide-y divide-border p-0">
{LIFECYCLE.map((item) => (
<div
key={item.device}
className="flex flex-col gap-1 p-5 sm:flex-row sm:items-center sm:gap-6 sm:px-7"
>
<h3 className="text-sm font-bold text-navy sm:w-72 sm:shrink-0">
{item.device}
</h3>
<p className="text-sm font-semibold text-primary sm:w-56 sm:shrink-0">
{item.interval}
</p>
<p className="text-sm leading-relaxed text-muted-foreground">
{item.note}
</p>
</div>
))}
</Card>
<p className="mt-5 text-xs leading-relaxed text-muted-foreground">
Eichfristen nach Anlage 7 der Mess- und Eichverordnung. Elektronische
Heizkostenverteiler unterliegen nicht dem Eichrecht.
</p>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<TrustPanel
title="Messdaten sind auf jedem Abschnitt geschützt"
items={TRUST_PROPERTIES}
visual={<TrustShieldArt />}
action={
<Button asChild variant="link" className="h-auto p-0 font-bold">
<Link to="/wissen/datenschutz-sicherheit">
Mehr zu Datenschutz & Sicherheit
<ArrowRight />
</Link>
</Button>
}
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
eyebrow="Häufige Fragen"
title="Fragen zu Messtechnik und Infrastruktur"
/>
<FaqAccordion className="mt-10" items={FAQS} />
</Section>
<ProductCrossLinks current="/produkte/messtechnik-infrastruktur" />
</ProductPage>
)
}
@@ -0,0 +1,199 @@
import * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { FeatureBand, type FeatureBandItem } from "@/components/solenos/feature-band"
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
import { PageHero } from "@/components/solenos/page-hero"
import { Container, Section, SectionHeading } from "@/components/solenos/section"
import { icons } from "@/components/solenos/site-assets"
type Product = {
to: string
/** short label for navigation and cross-links */
label: string
/** headline used on the overview cards */
title: string
/** one-line positioning under the card title */
sub: string
summary: string
/** baked-in disc icon, used wherever the four products are listed */
icon: string
}
/**
* The four products, in the order they are presented everywhere
* (overview grid, header menu, cross-links at the foot of each product page).
*/
const PRODUCTS: Product[] = [
{
to: "/produkte/uvi",
label: "Verbrauchsinformation (UVI)",
title: "Verbrauchsinformation (UVI)",
sub: "Monatlich für Bewohner",
summary:
"Verbrauchsdaten werden automatisch erfasst und die monatliche Verbrauchsinformation für die Bewohner erzeugt und bereitgestellt ohne Ablesetermin in der Wohnung.",
icon: icons.uvi,
},
{
to: "/produkte/heizkostenabrechnung",
label: "Heizkostenabrechnung",
title: "Heizkostenabrechnung",
sub: "Verbrauchsdaten korrekt abrechnen",
summary:
"Die jährliche Abrechnung entsteht aus denselben erfassten Verbrauchsdaten: Verteilung nach HeizkostenV, Nutzerwechsel berücksichtigt, Dokumente im Portal.",
icon: icons.billing,
},
{
to: "/produkte/rauchwarnmelder",
label: "Rauchwarnmelder",
title: "Rauchwarnmelder",
sub: "Sicher überwacht, zentral verwaltet",
summary:
"Ferninspizierbare Rauchwarnmelder inklusive Montage, jährlicher Inspektion nach DIN 14676-1 und lückenloser Dokumentation im Portal.",
icon: icons.smokeAlarm,
},
{
to: "/produkte/messtechnik-infrastruktur",
label: "Messtechnik und Infrastruktur",
title: "Messtechnik und Infrastruktur",
sub: "Die technische Grundlage",
summary:
"Fernablesbare Zähler und Heizkostenverteiler, Gateway und Datenübertragung vorkonfiguriert geliefert und laufend überwacht.",
icon: icons.radio,
},
]
function ProductIcon({
src,
className = "size-12",
}: {
src: string
className?: string
}) {
return <img src={src} alt="" className={className} loading="lazy" />
}
function ProductBreadcrumb({ current }: { current: string }) {
return (
<PageBreadcrumb
items={[
{ label: "Startseite", to: "/" },
{ label: "Produkte", to: "/produkte" },
{ label: current },
]}
/>
)
}
/**
* Shared product page shell: split PageHero with the
* Startseite → Produkte → product breadcrumb, a feature band summarising the
* product in four properties, the product's own sections, then the closing
* slot (CtaBanner).
*/
function ProductPage({
breadcrumb,
eyebrow,
title,
tagline,
lead,
actions,
note,
extra,
visual,
features,
children,
closing,
}: {
/** last breadcrumb crumb; also the product's own name */
breadcrumb: string
eyebrow?: React.ReactNode
title: React.ReactNode
/** short bold subline under the H1 */
tagline?: React.ReactNode
lead?: React.ReactNode
actions?: React.ReactNode
note?: React.ReactNode
extra?: React.ReactNode
visual?: React.ReactNode
features: FeatureBandItem[]
/** the product's spec sections between feature band and closing */
children?: React.ReactNode
closing?: React.ReactNode
}) {
return (
<>
<PageHero
breadcrumb={<ProductBreadcrumb current={breadcrumb} />}
eyebrow={eyebrow}
title={title}
lead={
<>
{tagline ? (
<span className="mb-2 block text-lg font-bold text-navy sm:text-xl">
{tagline}
</span>
) : null}
{lead}
</>
}
actions={actions}
note={note}
extra={extra}
visual={visual}
/>
<Container>
<Section className="pt-2 sm:pt-4 lg:pt-6">
<FeatureBand items={features} />
</Section>
{children}
{closing}
</Container>
</>
)
}
/**
* Closes a product page out with the three other products, so the four pages
* form one navigable set instead of dead ends.
*/
function ProductCrossLinks({ current }: { current: string }) {
const others = PRODUCTS.filter((product) => product.to !== current)
return (
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Weitere Produkte"
title="Läuft auf derselben Grundlage"
lead="Alle SolenOS-Leistungen nutzen dieselbe Gebäudestruktur und dieselbe Messtechnik einzeln buchbar, jederzeit erweiterbar."
/>
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{others.map((product) => (
<Card key={product.to} className="gap-0 p-6 sm:p-7">
<ProductIcon src={product.icon} />
<h3 className="mt-5 text-lg font-extrabold tracking-tight text-balance text-navy">
{product.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-pretty text-muted-foreground">
{product.summary}
</p>
<div className="mt-6 flex-1 content-end">
<Button asChild variant="outline">
<Link to={product.to}>
Mehr erfahren
<ArrowRight />
</Link>
</Button>
</div>
</Card>
))}
</div>
</Section>
)
}
export { PRODUCTS, ProductCrossLinks, ProductIcon, ProductPage }
export type { Product }
@@ -0,0 +1,341 @@
import { Link } from "react-router-dom"
import {
ArrowRight,
BellRing,
ClipboardCheck,
ScrollText,
ShieldCheck,
Timer,
Wifi,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Checklist } from "@/components/solenos/checklist"
import {
ComparisonTable,
type ComparisonColumn,
type ComparisonRow,
} from "@/components/solenos/comparison-table"
import { CtaBanner } from "@/components/solenos/cta-banner"
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
import { PortalGaugeIcon } from "@/components/solenos/line-icons"
import { PriceCard } from "@/components/solenos/price-card"
import { StepRail } from "@/components/solenos/process-steps"
import { Section, SectionHeading } from "@/components/solenos/section"
import {
AssetRender,
renders,
vectors,
} from "@/components/solenos/site-assets"
import { TopicCards, type Topic } from "@/components/solenos/topic-cards"
import { SmokeDetectorIllustration } from "@/components/solenos/illustrations"
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
/**
* Hero art: the glass smoke detector as the subject with the real device
* beside it and the check orb as the accent device plus verified operational
* readiness. Decorative; the copy carries the meaning.
*/
function SmokeAlarmHeroArt() {
return (
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
<img
src={vectors.checkOrb}
alt=""
className="absolute top-[4%] right-[6%] w-[20%]"
/>
<AssetRender
render={renders.glassSmokeDetector}
alt=""
className="mx-auto w-[64%]"
loading="eager"
/>
<AssetRender
render={renders.smokeAlarmAlpha}
className="absolute bottom-[2%] left-[2%] w-[30%]"
/>
</div>
)
}
const DUTIES: Topic[] = [
{
icon: <ScrollText />,
title: "Einbau und Betriebsbereitschaft",
body: "Rauchwarnmelder sind in Wohnungen nach den Bauordnungen der Länder vorgeschrieben. Wer für die Sicherstellung der Betriebsbereitschaft verantwortlich ist, regelt das jeweilige Landesrecht in den meisten Ländern der Eigentümer.",
},
{
icon: <ClipboardCheck />,
title: "Jährliche Inspektion",
body: "Die Anwendungsnorm DIN 14676-1 verlangt eine Inspektion mindestens einmal jährlich: Energieversorgung, Rauchzutrittsöffnungen und Umfeld des Melders werden geprüft.",
},
{
icon: <Wifi />,
title: "Ferninspektion nach Norm",
body: "Die DIN 14676-1 kennt neben der Vor-Ort-Inspektion die Teil- und die vollständige Ferninspektion. Geeignete Melder melden Verschmutzung, Batteriezustand und Demontage selbst ohne Termin in der Wohnung.",
},
{
icon: <Timer />,
title: "Austausch nach zehn Jahren",
body: "Rauchwarnmelder werden spätestens nach zehn Jahren Nutzungsdauer ausgetauscht. Verbaute Melder, Einbauorte und Austauschtermine sind im Portal je Wohnung dokumentiert.",
},
]
const ROLLOUT_STEPS = [
{
media: <AssetRender render={renders.houseSmallIso} imgClassName="w-24" />,
title: "Bedarf ermitteln",
description:
"Aus Wohnungen und Zimmerzahl ergibt sich, wie viele Melder je Einheit benötigt werden.",
},
{
media: (
<AssetRender render={renders.shippingBoxClosed} imgClassName="w-32" />
),
title: "Vorkonfiguriert erhalten",
description:
"Die Melder kommen bereits dem Gebäude und den geplanten Einheiten zugeordnet an.",
},
{
media: <AssetRender render={renders.drillTool} imgClassName="w-28" />,
title: "Montieren",
description:
"Montage durch Sie, Ihren Fachbetrieb oder als Komplettleistung durch SolenOS.",
},
{
media: <AssetRender render={renders.gateway} imgClassName="w-24" />,
title: "Anbinden",
description:
"Die Melder funken über dieselbe Gebäudeinfrastruktur wie die Verbrauchserfassung.",
},
{
media: (
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
),
title: "Ferninspektion läuft",
description:
"Status, Meldungen und Inspektionsnachweise stehen laufend im Portal bereit.",
},
]
const INSPECTION_COLUMNS: ComparisonColumn[] = [
{ title: "Ferninspektion", highlight: true },
{ title: "Vor-Ort-Inspektion", check: false },
]
const INSPECTION_ROWS: ComparisonRow[] = [
{
label: "Termin in der Wohnung",
cells: ["nicht erforderlich", "je Wohnung, inklusive Nachterminen"],
},
{
label: "Prüfung",
cells: ["laufend durch den Melder", "einmal jährlich vor Ort"],
},
{
label: "Störungen",
cells: ["werden gemeldet, sobald sie auftreten", "fallen beim Jahrestermin auf"],
},
{
label: "Nachweis",
cells: ["automatisch dokumentiert", "Protokoll je Begehung"],
},
{
label: "Aufwand für Bewohner",
cells: ["keine Anwesenheitspflicht", "Anwesenheit erforderlich"],
},
]
const INCLUDED = [
"Rauchwarnmelder inklusive Montage und Eigentumsübertragung",
"Bedarfsermittlung je Wohnung aus den Gebäudedaten",
"regelmäßige Ferninspektion und Störungsmeldungen",
"Inspektionsnachweise je Wohnung im Portal",
"Support für Rückfragen von Verwaltung und Bewohnern",
]
const FAQS: FaqItem[] = [
{
question: "Wie oft müssen Rauchwarnmelder geprüft werden?",
answer:
"Mindestens einmal jährlich. Die Anwendungsnorm DIN 14676-1 lässt dafür neben der Vor-Ort-Inspektion auch die Teil- und die vollständige Ferninspektion zu, sofern die eingesetzten Melder dafür geeignet sind.",
},
{
question: "Ersetzt die Ferninspektion die Vor-Ort-Prüfung vollständig?",
answer:
"Bei Meldern, die für die vollständige Ferninspektion geeignet sind, kann die Sicherstellung der Betriebsbereitschaft über die Gerätelebensdauer aus der Ferne erfolgen. Anlassbezogene Einsätze bleiben möglich etwa wenn ein Melder demontiert wurde, dauerhaft verschmutzt ist oder ausgetauscht werden muss.",
},
{
question: "Wer ist für die Rauchwarnmelder verantwortlich?",
answer:
"Einbau und Sicherstellung der Betriebsbereitschaft richten sich nach der Bauordnung des jeweiligen Landes. In den meisten Ländern trägt der Eigentümer die Verantwortung für die Betriebsbereitschaft; SolenOS liefert die Melder, die Inspektion und die Nachweise, mit denen diese Pflicht erfüllt werden kann.",
},
{
question: "Wann müssen die Melder ausgetauscht werden?",
answer:
"Spätestens nach zehn Jahren Nutzungsdauer. Einbaudatum und fälliger Austausch sind je Gerät und Wohnung im Portal hinterlegt, sodass der Austausch mit dem übrigen Gebäudebetrieb geplant werden kann.",
},
{
question: "Sind die Kosten umlagefähig?",
answer:
"Die laufenden Kosten der Ferninspektion sind grundsätzlich als Betriebskosten umlagefähig. Die einmaligen Kosten der Hardware inklusive Montage und Eigentumsübertragung sind es nicht sie sind Anschaffungskosten des Eigentümers.",
},
{
question: "Kann ich Rauchwarnmelder später ergänzen?",
answer:
"Ja. Läuft der Messbetrieb bereits über SolenOS, wird die vorhandene Gebäudestruktur weiterverwendet: Wohnungen und Einheiten sind erfasst, die Melder werden lediglich ergänzt und angebunden.",
},
]
export default function RauchwarnmelderProductPage() {
return (
<ProductPage
breadcrumb="Rauchwarnmelder"
eyebrow="Produkt"
title="Rauchwarnmelder"
tagline="Geprüft, ohne zu klingeln."
lead="Ferninspizierbare Rauchwarnmelder inklusive Montage, jährlicher Inspektion nach DIN 14676-1 und lückenloser Dokumentation über dieselbe Gebäudeinfrastruktur wie die Verbrauchserfassung."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link to="/klarpreis">Gesamtangebot ansehen</Link>
</Button>
</>
}
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
visual={<SmokeAlarmHeroArt />}
features={[
{
icon: <Wifi strokeWidth={1.6} />,
title: "Ferninspizierbar",
sub: "Prüfung ohne Termin und ohne Anwesenheit der Bewohner.",
},
{
icon: <BellRing strokeWidth={1.6} />,
title: "Störung fällt sofort auf",
sub: "Verschmutzung, Batterie und Demontage werden gemeldet.",
},
{
icon: <ClipboardCheck strokeWidth={1.6} />,
title: "Nachweis je Wohnung",
sub: "Inspektionen und Austauschfristen sind dokumentiert.",
},
{
icon: <PortalGaugeIcon />,
title: "Im selben Portal",
sub: "Melder stehen neben Zählern und Verbrauchsdaten.",
},
]}
closing={
<Section className="pt-0 sm:pt-0 lg:pt-0">
<CtaBanner
icon={<ShieldCheck />}
title="Rauchwarnmelder für Ihr Gebäude"
description="Bedarf, Montage und Ferninspektion in einem Schritt auch zusätzlich zu einem laufenden Messbetrieb."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/kontakt">Kontakt aufnehmen</Link>
</Button>
</>
}
/>
</Section>
}
>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Pflichten"
title="Was für Rauchwarnmelder gilt"
lead="Einbaupflicht, jährliche Inspektion und Austausch nach zehn Jahren sind gesetzt. Unterschiedlich ist nur, wie viel Aufwand daraus entsteht."
/>
<TopicCards items={DUTIES} className="sm:grid-cols-2 lg:grid-cols-4" />
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Schritt für Schritt"
title="Von der Bedarfsermittlung zur laufenden Inspektion"
/>
<StepRail steps={ROLLOUT_STEPS} className="mt-10" />
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Im Vergleich"
title="Ferninspektion oder Vor-Ort-Inspektion"
lead="Beide Verfahren erfüllen die Norm. Der Unterschied liegt im Aufwand für Verwaltung und Bewohner und darin, wie früh eine Störung auffällt."
/>
<ComparisonTable
className="mt-10"
columns={INSPECTION_COLUMNS}
rows={INSPECTION_ROWS}
labelHead="Inspektionsverfahren"
/>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Gesamtangebot"
title="Hardware, Ferninspektion und Montage transparent zusammenführen"
lead="Geräteanzahl, Inspektionsumfang und gewählte Montagevariante werden nach der vollständigen Objektkonfiguration geprüft und gemeinsam angeboten."
/>
<div className="mt-10 grid gap-6 lg:grid-cols-[1fr_1fr_1.1fr]">
<PriceCard
title="Rauchwarnmelder"
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="Geräteanzahl und Montagevariante"
description="Benötigte Rauchwarnmelder, vereinbarte Montage und Eigentumsübertragung."
media={<SmokeDetectorIllustration />}
/>
<PriceCard
title="Ferninspektion"
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="passend zum vereinbarten Leistungsumfang"
description="Regelmäßige Prüfung, Nachweise, Meldungen und Support."
media={
<div className="relative">
<SmokeDetectorIllustration />
<Wifi className="absolute top-1 -right-1 size-5 text-primary" />
</div>
}
/>
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">Enthalten</h3>
<Checklist items={INCLUDED} className="mt-5" />
<Button
asChild
variant="link"
className="mt-6 h-auto self-start p-0 font-bold"
>
<Link to="/konfigurator">
Gesamtangebot konfigurieren
<ArrowRight />
</Link>
</Button>
</Card>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
eyebrow="Häufige Fragen"
title="Fragen zu Rauchwarnmeldern"
/>
<FaqAccordion className="mt-10" items={FAQS} />
</Section>
<ProductCrossLinks current="/produkte/rauchwarnmelder" />
</ProductPage>
)
}
@@ -0,0 +1,378 @@
import { Link } from "react-router-dom"
import {
ArrowRight,
CalendarClock,
Euro,
FileCheck2,
RadioTower,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Checklist } from "@/components/solenos/checklist"
import { CtaBanner } from "@/components/solenos/cta-banner"
import { FaqAccordion, type FaqItem } from "@/components/solenos/faq-accordion"
import { PortalGaugeIcon } from "@/components/solenos/line-icons"
import { NoticeArt, NoticeBanner } from "@/components/solenos/notice-banner"
import { PriceCard } from "@/components/solenos/price-card"
import { StepRail } from "@/components/solenos/process-steps"
import { Section, SectionHeading } from "@/components/solenos/section"
import {
AssetRender,
renders,
vectors,
} from "@/components/solenos/site-assets"
import { heizkostenv } from "@/lib/legal"
import { ProductCrossLinks, ProductPage } from "@/pages/products/product-page"
/**
* Hero art: the UVI pane as the subject what the resident receives with
* the consumption bars behind it and the consumption ring as the accent.
* Decorative; the copy carries the meaning.
*/
function UviHeroArt() {
return (
<div className="relative mx-auto flex aspect-[1.3] w-full max-w-md items-center">
<img
src={vectors.consumptionRing}
alt=""
className="absolute top-0 right-[2%] w-[24%]"
/>
<AssetRender
render={renders.uviGlassPane}
alt=""
className="mx-auto w-[76%]"
loading="eager"
/>
<AssetRender
render={renders.barChart}
alt=""
className="absolute bottom-[2%] left-0 w-[28%]"
/>
</div>
)
}
const MONTHLY_STEPS = [
{
media: <AssetRender render={renders.heatMeter} imgClassName="w-24" />,
title: "Verbrauch erfassen",
description:
"Wärmezähler, Heizkostenverteiler und Wasserzähler messen laufend je Wohnung.",
},
{
media: <AssetRender render={renders.gateway} imgClassName="w-24" />,
title: "Werte übertragen",
description:
"Das Gateway sammelt die Funkdaten im Gebäude und überträgt sie verschlüsselt an SolenOS.",
},
{
media: <AssetRender render={renders.refreshLoopCloud} imgClassName="w-28" />,
title: "Daten prüfen und zuordnen",
description:
"Die Werte werden plausibilisiert und Wohnung, Gerät und Nutzungszeitraum zugeordnet.",
},
{
media: <AssetRender render={renders.uviGlassPane} alt="" className="w-28" />,
title: "Verbrauchsinformation erzeugen",
description:
"Monatswert, Vergleichswerte und die nach § 6a HeizkostenV geforderten Angaben werden aufbereitet.",
},
{
media: (
<AssetRender render={renders.laptopDashboard} imgClassName="w-40" />
),
title: "Bewohnern bereitstellen",
description:
"Bewohner rufen ihre Information im Mieterportal ab, Eigentümer sehen den Versandstatus.",
},
]
const MONTHLY_CONTENT = [
"Verbrauch des Nutzers im letzten Monat in Kilowattstunden",
"Vergleich mit dem Vormonat desselben Nutzers",
"Vergleich mit dem entsprechenden Monat des Vorjahres, soweit die Daten erhoben wurden",
"Vergleich mit dem Verbrauch eines normierten Durchschnittsnutzers derselben Nutzerkategorie",
]
const BILLING_CONTENT = [
"Anteil der eingesetzten Energieträger, bei Fernwärme zusätzlich Treibhausgasemissionen und Primärenergiefaktor",
"erhobene Steuern, Abgaben und Zölle",
"Entgelte für Messtechnik, Eichung, Ablesung und Abrechnung",
"grafischer Vergleich des witterungsbereinigten Energieverbrauchs mit dem vorherigen Abrechnungszeitraum",
"Kontaktinformationen zu Energieeffizienz und Hinweise zur Streitbeilegung",
]
const INCLUDED = [
"fernablesbare Messtechnik für Wärme und Warmwasser",
"Gateway, Datenübertragung und Geräteüberwachung",
"monatliche Erzeugung der Verbrauchsinformation",
"Bereitstellung im Mieterportal für jede Wohnung",
"Nutzerwechsel ohne Ablesetermin vor Ort",
"Portalzugang für Eigentümer und Verwaltung",
]
const NOT_INCLUDED = [
"Jahresabrechnung der Heiz- und Warmwasserkosten",
"Rauchwarnmelder und deren Inspektion",
"Betriebskostenabrechnung des gesamten Gebäudes",
]
const FAQS: FaqItem[] = [
{
question: "Ab wann muss die Verbrauchsinformation monatlich erfolgen?",
answer:
"Sobald fernablesbare Ausstattungen zur Verbrauchserfassung installiert sind, sind die Verbrauchsinformationen nach § 6a Absatz 1 Nummer 2 HeizkostenV seit dem 1. Januar 2022 monatlich mitzuteilen. Für Abrechnungszeiträume ab dem 1. Dezember 2021 galt zunächst ein mindestens vierteljährlicher bzw. halbjährlicher Rhythmus.",
},
{
question: "Wie erhalten die Bewohner ihre Verbrauchsinformation?",
answer:
"Über das Mieterportal. Jeder Bewohner erhält einen eigenen Zugang und sieht ausschließlich die Werte der eigenen Wohnung und des eigenen Nutzungszeitraums nicht die Daten von Vor- oder Nachmietern.",
},
{
question: "Was passiert, wenn ein Bewohner keinen digitalen Zugang nutzt?",
answer:
"Die Information wird im Portal bereitgestellt und kann als Dokument ausgegeben werden, sodass die Verwaltung sie auf dem für das Gebäude vereinbarten Weg zustellen kann. Die Pflicht trifft den Gebäudeeigentümer; SolenOS liefert die dafür benötigten Inhalte vollständig aufbereitet.",
},
{
question: "Muss für die UVI jemand in die Wohnung?",
answer:
"Nein. Fernablesbar heißt, dass die Ausstattung ohne Zugang zu den einzelnen Nutzeinheiten abgelesen werden kann (§ 5 Absatz 2 HeizkostenV). Auch bei einem Nutzerwechsel werden die Werte zum Stichtag aus der laufenden Datenerfassung übernommen.",
},
{
question: "Kann ich die UVI ohne Heizkostenabrechnung buchen?",
answer:
"Ja. Der UVI-Komplettbetrieb umfasst die benötigte Messtechnik und die zugehörigen digitalen Dienstleistungen. Die Heizkostenabrechnung baut auf denselben erfassten Verbrauchsdaten auf und kann jederzeit ergänzt werden.",
},
]
export default function UviProductPage() {
return (
<ProductPage
breadcrumb="Verbrauchsinformation (UVI)"
eyebrow="Produkt"
title={
<span className="block text-[2.25rem] text-pretty sm:text-[2.75rem]">
Unterjährige Verbrauchs&shy;information (UVI)
</span>
}
tagline="Monatlich. Automatisch. Nachweisbar."
lead="Sind fernablesbare Zähler installiert, muss jeder Nutzer monatlich über seinen Verbrauch informiert werden. SolenOS erzeugt diese Information aus den laufend erfassten Werten und stellt sie den Bewohnern im Mieterportal bereit."
actions={
<>
<Button asChild size="lg">
<Link to="/konfigurator">Konfiguration für mein Gebäude starten</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link to="/wissen/gesetzliche-anforderungen">
Gesetzliche Anforderungen
</Link>
</Button>
</>
}
note="Wenige Angaben · vollständige Prüfung im Folgeprozess"
extra={
<div
role="note"
className="rounded-2xl border border-brand-300 bg-brand-50/80 p-4 shadow-card"
>
<div className="flex items-start gap-3">
<Euro
aria-hidden="true"
className="mt-0.5 size-6 shrink-0 text-brand-700"
/>
<div>
<p className="font-extrabold text-navy">
Bis zu 3&nbsp;% Kürzungsrecht
</p>
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
Fehlt die monatliche Verbrauchsinformation oder ist sie
unvollständig, können Nutzer ihren Kostenanteil um 3&nbsp;%
kürzen.
</p>
</div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm font-semibold">
<Link
to="/wissen/gesetzliche-anforderungen"
className="inline-flex items-center gap-1 text-brand-800 underline decoration-brand-400 underline-offset-4"
>
Anforderungen im Detail
<ArrowRight className="size-4" />
</Link>
<a
href={heizkostenv("6a")}
className="text-muted-foreground underline underline-offset-4"
>
Quelle: § 6a HeizkostenV
</a>
</div>
</div>
}
visual={<UviHeroArt />}
features={[
{
icon: <CalendarClock strokeWidth={1.6} />,
title: "Monatlich automatisch",
sub: "Erfassung, Aufbereitung und Bereitstellung laufen ohne Zutun.",
},
{
icon: <RadioTower strokeWidth={1.6} />,
title: "Ohne Wohnungszugang",
sub: "Fernablesung statt Ablesetermin auch beim Nutzerwechsel.",
},
{
icon: <PortalGaugeIcon />,
title: "Eigener Bewohnerzugang",
sub: "Jede Wohnung sieht ausschließlich ihren eigenen Verbrauch.",
},
{
icon: <FileCheck2 strokeWidth={1.6} />,
title: "Pflichtangaben enthalten",
sub: "Monatswert und Vergleiche nach § 6a HeizkostenV.",
},
]}
closing={
<Section className="pt-0 sm:pt-0 lg:pt-0">
<CtaBanner
icon={<Euro />}
title="Verbrauchsinformation für Ihr Gebäude konfigurieren"
description="Gebäude und Leistungen online erfassen; SolenOS prüft anschließend Ausstattung und Gesamtpreis."
actions={
<>
<Button asChild variant="inverse" size="lg">
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/kontakt">Kontakt aufnehmen</Link>
</Button>
</>
}
/>
</Section>
}
>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<NoticeBanner
title="Monatliche Verbrauchsinformation ist Pflicht"
source={{ cite: "§ 6a HeizkostenV", href: heizkostenv("6a") }}
visual={<NoticeArt className="max-lg:hidden" />}
action={
<Button
asChild
variant="link"
className="h-auto p-0 text-[15px] font-bold text-brand-800 underline decoration-brand-500 decoration-2 underline-offset-[6px] hover:text-navy hover:decoration-brand-800"
>
<Link to="/wissen/gesetzliche-anforderungen">
Anforderungen im Detail
<ArrowRight />
</Link>
</Button>
}
>
Wo fernablesbare Ausstattungen installiert sind, sind die
Verbrauchsinformationen seit dem 1. Januar 2022 monatlich
mitzuteilen. Werden sie nicht oder nicht vollständig mitgeteilt,
können Nutzer ihren Kostenanteil um 3 % kürzen.
</NoticeBanner>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Inhalte"
title="Was die Bewohner erhalten"
lead="Die monatliche Information und die Angaben zur Jahresabrechnung sind in der HeizkostenV unterschiedlich geregelt. SolenOS bereitet beide Sätze aus derselben Datenbasis auf."
/>
<div className="mt-10 grid gap-6 lg:grid-cols-2">
<Card className="gap-0 p-6 sm:p-7">
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
Monatlich
</p>
<h3 className="mt-2 text-lg font-extrabold tracking-tight text-navy">
Verbrauchsinformation je Wohnung
</h3>
<Checklist items={MONTHLY_CONTENT} className="mt-5" />
<p className="mt-5 text-xs text-muted-foreground">
§ 6a Absatz 2 HeizkostenV
</p>
</Card>
<Card className="gap-0 p-6 sm:p-7">
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">
Zur Abrechnung
</p>
<h3 className="mt-2 text-lg font-extrabold tracking-tight text-navy">
Zusätzliche Informationen
</h3>
<Checklist items={BILLING_CONTENT} className="mt-5" />
<p className="mt-5 text-xs text-muted-foreground">
§ 6a Absatz 3 HeizkostenV
</p>
</Card>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Schritt für Schritt"
title="So entsteht die monatliche UVI"
lead="Vom Messwert in der Wohnung bis zur Information im Mieterportal ein Ablauf, der nach der Installation ohne weitere Eingriffe läuft."
/>
<StepRail steps={MONTHLY_STEPS} className="mt-10" />
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
eyebrow="Leistungsumfang"
title="Was im UVI-Betrieb enthalten ist"
/>
<div className="mt-10 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<Card className="gap-0 p-6 sm:p-7">
<h3 className="text-base font-bold text-navy">Enthalten</h3>
<Checklist items={INCLUDED} className="mt-5" />
<h3 className="mt-8 text-base font-bold text-navy">
Nicht im UVI-Betrieb enthalten
</h3>
<Checklist items={NOT_INCLUDED} className="mt-5" tone="excluded" />
<p className="mt-5 text-sm leading-relaxed text-muted-foreground">
Heizkostenabrechnung und Rauchwarnmelder sind eigene Leistungen
einzeln buchbar und jederzeit ergänzbar.
</p>
</Card>
<div>
<PriceCard
title="Monatliche Verbrauchsinformation (UVI)"
cadence="Gesamtangebot"
price="Nach Prüfung"
unit="nach vollständiger Objektkonfiguration"
description="Messtechnik, erforderliche Gateway-Infrastruktur, Daten, Portal und Verbrauchsinformation werden gemeinsam angeboten."
media={
<AssetRender
render={renders.uviGlassPane}
alt=""
className="w-full"
/>
}
/>
<Button asChild variant="link" className="mt-4 h-auto p-0 font-bold">
<Link to="/konfigurator">
Gesamtangebot konfigurieren
<ArrowRight />
</Link>
</Button>
</div>
</div>
</Section>
<Section className="pt-0 sm:pt-0 lg:pt-0">
<SectionHeading
align="center"
eyebrow="Häufige Fragen"
title="Fragen zur Verbrauchsinformation"
/>
<FaqAccordion className="mt-10" items={FAQS} />
</Section>
<ProductCrossLinks current="/produkte/uvi" />
</ProductPage>
)
}
@@ -11,10 +11,9 @@ import {
} from "@/components/solenos/site-assets"
import {
TopicCards,
WissenArticle,
WissenRelated,
type Topic,
} from "@/pages/wissen/wissen-article"
} from "@/components/solenos/topic-cards"
import { WissenArticle, WissenRelated } from "@/pages/wissen/wissen-article"
const safeguards: Topic[] = [
{
@@ -102,7 +101,7 @@ export default function DatenschutzSicherheitPage() {
<Link to="/kontakt">Kontakt aufnehmen</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
</>
}
@@ -11,19 +11,18 @@ import {
vectors,
} from "@/components/solenos/site-assets"
import {
HEIZKOSTENV,
TopicCards,
WissenArticle,
WissenRelated,
type Topic,
} from "@/pages/wissen/wissen-article"
} from "@/components/solenos/topic-cards"
import { heizkostenv } from "@/lib/legal"
import { WissenArticle, WissenRelated } from "@/pages/wissen/wissen-article"
const requirements: Topic[] = [
{
icon: <BarChart3 />,
title: "Unterjährige Verbrauchsinformation",
body: "Monatliche Verbrauchsinformationen auf Basis der erfassten Verbrauchs- und Ablesewerte zur Umsetzung der Anforderungen des § 6a HeizkostenV.",
source: { cite: "§ 6a HeizkostenV", href: `${HEIZKOSTENV}/__6a.html` },
source: { cite: "§ 6a HeizkostenV", href: heizkostenv("6a") },
},
{
icon: <RadioTower />,
@@ -34,7 +33,7 @@ const requirements: Topic[] = [
icon: <Puzzle />,
title: "Interoperable Messtechnik",
body: "Die eingesetzten Verbrauchserfassungsgeräte sind OMS-fähig. Soweit für die jeweilige Ausstattung anwendbar, erfüllt die gelieferte Messtechnik die Anforderungen an Interoperabilität und Stand der Technik nach § 5 HeizkostenV.",
source: { cite: "§ 5 HeizkostenV", href: `${HEIZKOSTENV}/__5.html` },
source: { cite: "§ 5 HeizkostenV", href: heizkostenv("5") },
},
]
@@ -127,7 +126,7 @@ export default function GesetzlicheAnforderungenPage() {
<Link to="/kontakt">Kontakt aufnehmen</Link>
</Button>
<Button asChild variant="inverseOutline" size="lg">
<Link to="/konfigurator">Preis berechnen</Link>
<Link to="/konfigurator">Konfiguration starten</Link>
</Button>
</>
}
@@ -2,76 +2,11 @@ import type * as React from "react"
import { Link } from "react-router-dom"
import { ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
import { IconBadge } from "@/components/solenos/icon-tile"
import { PageBreadcrumb } from "@/components/solenos/page-breadcrumb"
import { PageHero } from "@/components/solenos/page-hero"
import { Container, Section } from "@/components/solenos/section"
/** The paragraph of the HeizkostenV a property is cited against. */
type LegalSource = {
cite: string
href: string
}
/** One technical property: line icon, title, body, optional legal citation. */
type Topic = {
icon: React.ReactNode
title: string
body: string
source?: LegalSource
}
const HEIZKOSTENV = "https://www.gesetze-im-internet.de/heizkostenv"
/** Cited source line, pinned to the card's baseline so rows stay aligned. */
function LegalSourceNote({ source }: { source: LegalSource }) {
return (
<p className="mt-auto pt-4 text-xs text-muted-foreground">
Quelle:{" "}
<a
href={source.href}
target="_blank"
rel="noreferrer"
className="font-medium text-brand-700 underline-offset-2 transition-colors hover:text-brand-800 hover:underline"
>
{source.cite}
</a>
</p>
)
}
/** Property grid: line icon over a navy title, body copy, optional citation. */
function TopicCards({
items,
className,
}: {
items: Topic[]
className?: string
}) {
return (
<div className={cn("mt-10 grid gap-5", className)}>
{items.map((item) => (
<Card key={item.title}>
<CardContent className="flex flex-1 flex-col">
<IconBadge variant="outline" shape="squircle" size="lg">
{item.icon}
</IconBadge>
<h3 className="mt-5 text-base font-bold text-balance text-navy">
{item.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-pretty text-muted-foreground">
{item.body}
</p>
{item.source ? <LegalSourceNote source={item.source} /> : null}
</CardContent>
</Card>
))}
</div>
)
}
/**
* Shared shell for the /wissen articles: split PageHero with the
* Startseite → Wissen → article breadcrumb, then the article's own sections.
@@ -165,5 +100,4 @@ function WissenRelated({
)
}
export { HEIZKOSTENV, TopicCards, WissenArticle, WissenRelated }
export type { LegalSource, Topic }
export { WissenArticle, WissenRelated }