@bytebrand/fe-ui-core-autobahn 1.0.76 → 1.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cards/CarCard.tsx +15 -15
- package/cards/cardParts.tsx +36 -2
- package/cards/index.ts +2 -0
- package/cards/types.ts +6 -0
- package/constants.ts +31 -31
- package/data/toAutobahnCar.ts +4 -0
- package/locales/de/autobahn.json +2 -2
- package/locales/en/autobahn.json +1 -1
- package/package.json +1 -1
- package/theme/md3.css +216 -83
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/FinanceCalculator.tsx +151 -29
- package/vdp/VdpModules.tsx +367 -51
package/utils/financeFormat.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
// Autobahn (auto.de redesign) — shared financing-number formatting.
|
|
2
|
-
//
|
|
3
|
-
// Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
|
|
4
|
-
// Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
|
|
5
|
-
// second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
|
|
6
|
-
// Node service.
|
|
7
|
-
//
|
|
8
|
-
// The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
|
|
9
|
-
// financing means monthly×term is NOT the total) and lives here once, not per-caller.
|
|
10
|
-
|
|
11
|
-
/** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
|
|
12
|
-
* toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
|
|
13
|
-
export function formatEur(n: number): string {
|
|
14
|
-
return n.toLocaleString('de-DE');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
|
|
18
|
-
export function formatPct(n: number): string {
|
|
19
|
-
return `${n.toFixed(2).replace('.', ',')} %`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface FinanceExampleFields {
|
|
23
|
-
price: number;
|
|
24
|
-
apr: number;
|
|
25
|
-
down: number;
|
|
26
|
-
/** Real config figure — falls back to price-down ONLY when genuinely absent. */
|
|
27
|
-
netLoanAmount?: number;
|
|
28
|
-
nominalRate?: number;
|
|
29
|
-
finalInstallment?: number;
|
|
30
|
-
grossAmount?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface FinanceExampleLabels {
|
|
34
|
-
vehiclePrice: string;
|
|
35
|
-
priceSuffix: string;
|
|
36
|
-
apr: string;
|
|
37
|
-
nominal: string;
|
|
38
|
-
net: string;
|
|
39
|
-
down: string;
|
|
40
|
-
final: string;
|
|
41
|
-
gross: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
|
|
46
|
-
* included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
|
|
47
|
-
* derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
|
|
48
|
-
* `exampleLine`, byte-for-byte).
|
|
49
|
-
*/
|
|
50
|
-
export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
|
|
51
|
-
const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
|
|
52
|
-
const parts: string[] = [
|
|
53
|
-
`${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
|
|
54
|
-
...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
|
|
55
|
-
...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
|
|
56
|
-
`${labels.net}: ${formatEur(netto)} €`,
|
|
57
|
-
`${labels.down}: ${formatEur(fields.down)} €`,
|
|
58
|
-
...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
|
|
59
|
-
...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
|
|
60
|
-
];
|
|
61
|
-
return `${footnoteMarker} ${parts.join('; ')};`;
|
|
62
|
-
}
|
|
1
|
+
// Autobahn (auto.de redesign) — shared financing-number formatting.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
|
|
4
|
+
// Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
|
|
5
|
+
// second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
|
|
6
|
+
// Node service.
|
|
7
|
+
//
|
|
8
|
+
// The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
|
|
9
|
+
// financing means monthly×term is NOT the total) and lives here once, not per-caller.
|
|
10
|
+
|
|
11
|
+
/** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
|
|
12
|
+
* toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
|
|
13
|
+
export function formatEur(n: number): string {
|
|
14
|
+
return n.toLocaleString('de-DE');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
|
|
18
|
+
export function formatPct(n: number): string {
|
|
19
|
+
return `${n.toFixed(2).replace('.', ',')} %`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FinanceExampleFields {
|
|
23
|
+
price: number;
|
|
24
|
+
apr: number;
|
|
25
|
+
down: number;
|
|
26
|
+
/** Real config figure — falls back to price-down ONLY when genuinely absent. */
|
|
27
|
+
netLoanAmount?: number;
|
|
28
|
+
nominalRate?: number;
|
|
29
|
+
finalInstallment?: number;
|
|
30
|
+
grossAmount?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FinanceExampleLabels {
|
|
34
|
+
vehiclePrice: string;
|
|
35
|
+
priceSuffix: string;
|
|
36
|
+
apr: string;
|
|
37
|
+
nominal: string;
|
|
38
|
+
net: string;
|
|
39
|
+
down: string;
|
|
40
|
+
final: string;
|
|
41
|
+
gross: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
|
|
46
|
+
* included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
|
|
47
|
+
* derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
|
|
48
|
+
* `exampleLine`, byte-for-byte).
|
|
49
|
+
*/
|
|
50
|
+
export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
|
|
51
|
+
const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
|
|
52
|
+
const parts: string[] = [
|
|
53
|
+
`${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
|
|
54
|
+
...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
|
|
55
|
+
...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
|
|
56
|
+
`${labels.net}: ${formatEur(netto)} €`,
|
|
57
|
+
`${labels.down}: ${formatEur(fields.down)} €`,
|
|
58
|
+
...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
|
|
59
|
+
...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
|
|
60
|
+
];
|
|
61
|
+
return `${footnoteMarker} ${parts.join('; ')};`;
|
|
62
|
+
}
|
package/utils/priceRating.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
|
|
2
|
-
//
|
|
3
|
-
// Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
|
|
4
|
-
// `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
|
|
5
|
-
// This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
|
|
6
|
-
// module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
|
|
7
|
-
// (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
|
|
8
|
-
// on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
|
|
9
|
-
// so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
|
|
10
|
-
import { priceRatings, priceRatingConfig } from '../constants/price';
|
|
11
|
-
import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
|
|
12
|
-
|
|
13
|
-
export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
|
|
14
|
-
const round = (value: number) => {
|
|
15
|
-
return Number.isFinite(pricePredicted)
|
|
16
|
-
? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
|
|
17
|
-
: '???';
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
if (currentPrice === 0) {
|
|
21
|
-
console.log('Empty current price');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
|
|
25
|
-
const { min, max } = priceRatingConfig[key];
|
|
26
|
-
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
27
|
-
}, {}); // tslint:disable-line
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
|
|
31
|
-
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
32
|
-
return predictablePrice;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
|
|
39
|
-
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
40
|
-
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
41
|
-
|
|
42
|
-
if (!isCorrectCurrentSalesPrice) return 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
|
|
46
|
-
|
|
47
|
-
const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
|
|
48
|
-
|
|
49
|
-
for (const key of Object.keys(ratingConfig)) {
|
|
50
|
-
const { min, max } = ratingConfig[key];
|
|
51
|
-
const TOP_PRICE = 5;
|
|
52
|
-
if (key === 'highPrice' && currentSalesPrice === max) return 1;
|
|
53
|
-
if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
|
|
54
|
-
if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return 0;
|
|
58
|
-
};
|
|
1
|
+
// Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
|
|
2
|
+
//
|
|
3
|
+
// Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
|
|
4
|
+
// `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
|
|
5
|
+
// This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
|
|
6
|
+
// module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
|
|
7
|
+
// (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
|
|
8
|
+
// on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
|
|
9
|
+
// so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
|
|
10
|
+
import { priceRatings, priceRatingConfig } from '../constants/price';
|
|
11
|
+
import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
|
|
12
|
+
|
|
13
|
+
export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
|
|
14
|
+
const round = (value: number) => {
|
|
15
|
+
return Number.isFinite(pricePredicted)
|
|
16
|
+
? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
|
|
17
|
+
: '???';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (currentPrice === 0) {
|
|
21
|
+
console.log('Empty current price');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
|
|
25
|
+
const { min, max } = priceRatingConfig[key];
|
|
26
|
+
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
27
|
+
}, {}); // tslint:disable-line
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
|
|
31
|
+
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
32
|
+
return predictablePrice;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
|
|
39
|
+
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
40
|
+
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
41
|
+
|
|
42
|
+
if (!isCorrectCurrentSalesPrice) return 0;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
|
|
46
|
+
|
|
47
|
+
const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
|
|
48
|
+
|
|
49
|
+
for (const key of Object.keys(ratingConfig)) {
|
|
50
|
+
const { min, max } = ratingConfig[key];
|
|
51
|
+
const TOP_PRICE = 5;
|
|
52
|
+
if (key === 'highPrice' && currentSalesPrice === max) return 1;
|
|
53
|
+
if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
|
|
54
|
+
if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 0;
|
|
58
|
+
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import React, { useState } from 'react';
|
|
17
17
|
import { TranslationFunction } from '../types';
|
|
18
18
|
|
|
19
|
-
import { Button, Icon } from '../ui';
|
|
19
|
+
import { Button, Icon, Switch } from '../ui';
|
|
20
20
|
import { TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
21
21
|
|
|
22
22
|
/** Payment mode segment keys. Mirrors the design's ['fin','leasing','cash']. */
|
|
@@ -184,6 +184,41 @@ export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, ch
|
|
|
184
184
|
</div>
|
|
185
185
|
);
|
|
186
186
|
|
|
187
|
+
/** full-width label(+optional trailing price) / Switch row — Switch itself is inline-flex
|
|
188
|
+
* (track+label), so the "label ...... price switch" full-row layout can't be expressed via
|
|
189
|
+
* Switch's `label` prop (which renders track-then-label, not a full-width justified row). */
|
|
190
|
+
export const ToggleRow: React.FunctionComponent<{
|
|
191
|
+
label: string;
|
|
192
|
+
price?: string;
|
|
193
|
+
checked: boolean;
|
|
194
|
+
disabled?: boolean;
|
|
195
|
+
onChange: (checked: boolean) => void;
|
|
196
|
+
className?: string;
|
|
197
|
+
/** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
|
|
198
|
+
staticNote?: string;
|
|
199
|
+
}> = ({ label, price, checked, disabled, onChange, className, staticNote }) => (
|
|
200
|
+
<div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: staticNote ? undefined : 'space-between', gap: 10 }}>
|
|
201
|
+
<span className="md-title-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
|
202
|
+
{label}
|
|
203
|
+
<Icon name="info" size={13} color="var(--md-on-surface-variant)" />
|
|
204
|
+
</span>
|
|
205
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
|
|
206
|
+
{price && (
|
|
207
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
|
|
208
|
+
{price}
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
{staticNote ? (
|
|
212
|
+
<span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
|
|
213
|
+
{staticNote}
|
|
214
|
+
</span>
|
|
215
|
+
) : (
|
|
216
|
+
<Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
|
|
187
222
|
export interface FinanceCalculatorProps {
|
|
188
223
|
/** active payment mode (controlled). */
|
|
189
224
|
mode: FinanceMode;
|
|
@@ -226,6 +261,19 @@ export interface FinanceCalculatorProps {
|
|
|
226
261
|
annualMileageStep?: number;
|
|
227
262
|
/** annual-mileage slider changed. */
|
|
228
263
|
onAnnualMileageChange?: (km: number) => void;
|
|
264
|
+
/** "Mit Schlussrate" toggle state (financing only; leasing has no balloon concept). Omit
|
|
265
|
+
* onToggleFinalInstallment to hide the row. */
|
|
266
|
+
withFinalInstallment?: boolean;
|
|
267
|
+
onToggleFinalInstallment?: (checked: boolean) => void;
|
|
268
|
+
/** "Rundum-Sorglos-Paket" toggle state (fin/leasing). Omit addServicesCost or
|
|
269
|
+
* onToggleAddServices to hide the row. */
|
|
270
|
+
withAddServices?: boolean;
|
|
271
|
+
addServicesCost?: number;
|
|
272
|
+
addServicesDisabled?: boolean;
|
|
273
|
+
onToggleAddServices?: (checked: boolean) => void;
|
|
274
|
+
/** Kaufen tab only — real carStore.isAvailable, drives the Verfügbarkeit row (replaces the
|
|
275
|
+
* static Lieferung row). Same source as VdpTrustRow's "Sofort verfügbar" badge. */
|
|
276
|
+
available?: boolean;
|
|
229
277
|
/** phone number shown on the secondary CTA (touch only). */
|
|
230
278
|
phone?: string;
|
|
231
279
|
/** primary CTA click (Angebot sichern). */
|
|
@@ -289,7 +337,7 @@ export const expanderBtnStyle: React.CSSProperties = {
|
|
|
289
337
|
display: 'flex',
|
|
290
338
|
alignItems: 'center',
|
|
291
339
|
justifyContent: 'space-between',
|
|
292
|
-
padding: '
|
|
340
|
+
padding: '0 0 2px',
|
|
293
341
|
color: 'var(--md-primary)',
|
|
294
342
|
fontWeight: 600,
|
|
295
343
|
fontSize: 14,
|
|
@@ -317,6 +365,13 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
317
365
|
annualMileageMax = 60000,
|
|
318
366
|
annualMileageStep = 5000,
|
|
319
367
|
onAnnualMileageChange,
|
|
368
|
+
withFinalInstallment,
|
|
369
|
+
onToggleFinalInstallment,
|
|
370
|
+
withAddServices,
|
|
371
|
+
addServicesCost,
|
|
372
|
+
addServicesDisabled,
|
|
373
|
+
onToggleAddServices,
|
|
374
|
+
available,
|
|
320
375
|
phone = '0800 40 30 182',
|
|
321
376
|
onSecure,
|
|
322
377
|
onCall,
|
|
@@ -332,6 +387,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
332
387
|
// whether the "UVP-Rate" footnote popover (fin/leasing) is open — same widget as CarCard's.
|
|
333
388
|
const [uvpTooltipOpen, setUvpTooltipOpen] = useState(false);
|
|
334
389
|
const isCash = mode === 'cash';
|
|
390
|
+
const isLease = mode === 'leasing';
|
|
335
391
|
const terms = termOptions && termOptions.length ? termOptions : range(termMin, termMax, termStep);
|
|
336
392
|
const showStrike = listPrice != null && listPrice > price;
|
|
337
393
|
// price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
|
|
@@ -414,14 +470,20 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
414
470
|
</div>
|
|
415
471
|
|
|
416
472
|
{/* 2-col grid: rate|pill (row 1) + condLine|bar (row 2) — fixed right column prevents wrapping */}
|
|
417
|
-
<div className=
|
|
473
|
+
<div className={'fin-price-grid' + (isCash ? ' fin-price-grid--cash' : '')}>
|
|
418
474
|
{/* col 1 row 1: hero rate — Figma: "329 €" all-big, then a small stacked column with
|
|
419
475
|
the struck old rate ABOVE "/mtl." (round 11; was a single baseline row) */}
|
|
420
476
|
<div className="fin-rate-main">
|
|
421
477
|
{/* mobile Kaufen (Figma round 14): struck list price ABOVE the price, right-aligned,
|
|
422
478
|
tight to the figure — replaces the Festpreis + UVP lines, which stay desktop-only */}
|
|
423
479
|
{isCash && showStrike && (
|
|
424
|
-
<div
|
|
480
|
+
<div
|
|
481
|
+
className="show-mobile"
|
|
482
|
+
style={{ justifyContent: 'flex-end', alignItems: 'baseline', gap: 4, lineHeight: 1, marginTop: 7 }}
|
|
483
|
+
role="button"
|
|
484
|
+
tabIndex={0}
|
|
485
|
+
onClick={() => setUvpTooltipOpen((v) => !v)}
|
|
486
|
+
>
|
|
425
487
|
<span style={{ fontSize: 13, color: 'var(--md-on-surface-variant)' }}>{t('autobahn:finance.uvp')}</span>
|
|
426
488
|
<span
|
|
427
489
|
className={`num pp-hero-old${
|
|
@@ -465,6 +527,50 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
465
527
|
</span>
|
|
466
528
|
</div>
|
|
467
529
|
)}
|
|
530
|
+
{/* desktop: same struck list price ABOVE the rate, same fin-rate-old-list styling +
|
|
531
|
+
clickable UVP badge/tooltip as the mobile sheet's own Kaufen block. */}
|
|
532
|
+
{isCash && showStrike && (
|
|
533
|
+
<div
|
|
534
|
+
className="hide-mobile"
|
|
535
|
+
style={{ display: 'flex', justifyContent: 'flex-end', lineHeight: 1, marginTop: 0, cursor: 'pointer' }}
|
|
536
|
+
role="button"
|
|
537
|
+
tabIndex={0}
|
|
538
|
+
onClick={() => setUvpTooltipOpen((v) => !v)}
|
|
539
|
+
>
|
|
540
|
+
<s className="num fin-rate-old fin-rate-old-list">
|
|
541
|
+
{eur(listPrice as number)} €
|
|
542
|
+
<span
|
|
543
|
+
className="pp-eyebrow-badge"
|
|
544
|
+
role="button"
|
|
545
|
+
tabIndex={0}
|
|
546
|
+
onClick={(e) => {
|
|
547
|
+
e.stopPropagation();
|
|
548
|
+
setUvpTooltipOpen((v) => !v);
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
2
|
|
552
|
+
</span>
|
|
553
|
+
{uvpTooltipOpen && (
|
|
554
|
+
<div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
className="pp-uvp-tooltip-close"
|
|
558
|
+
aria-label={t('autobahn:modal.close')}
|
|
559
|
+
onClick={(e) => {
|
|
560
|
+
e.stopPropagation();
|
|
561
|
+
setUvpTooltipOpen(false);
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
565
|
+
<path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
|
|
566
|
+
</svg>
|
|
567
|
+
</button>
|
|
568
|
+
<div className="pp-uvp-tooltip-body">{t('autobahn:card.price.uvpListTooltipBody')}</div>
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
</s>
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
468
574
|
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
|
469
575
|
{isCash ? (
|
|
470
576
|
<>
|
|
@@ -486,11 +592,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
486
592
|
</span>
|
|
487
593
|
<span
|
|
488
594
|
className="num hide-mobile"
|
|
489
|
-
style={{ fontSize:
|
|
595
|
+
style={{ fontSize: 45, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
|
|
490
596
|
>
|
|
491
|
-
{eur(price)}
|
|
597
|
+
{eur(price)} €<sup>1</sup>
|
|
492
598
|
</span>
|
|
493
|
-
<span className="md-body-sm hide-mobile" style={{ fontWeight: 600 }}>€</span>
|
|
494
599
|
</>
|
|
495
600
|
) : (
|
|
496
601
|
<>
|
|
@@ -511,7 +616,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
511
616
|
<span className="fin-rate-unit">
|
|
512
617
|
{/* struck old rate (REAL oldMonthlyInstallment; SRP strikes the same source) */}
|
|
513
618
|
{oldMonthly != null && (
|
|
514
|
-
|
|
619
|
+
<span
|
|
620
|
+
style={{ display: 'flex', flexDirection: 'column', cursor: 'pointer' }}
|
|
621
|
+
role="button"
|
|
622
|
+
tabIndex={0}
|
|
623
|
+
onClick={() => setUvpTooltipOpen((v) => !v)}
|
|
624
|
+
>
|
|
515
625
|
<span className="pp-eyebrow">
|
|
516
626
|
{t('autobahn:card.price.uvpRateLabel')}
|
|
517
627
|
<span
|
|
@@ -561,7 +671,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
561
671
|
>
|
|
562
672
|
{eur(Math.round(oldMonthly))} €
|
|
563
673
|
</s>
|
|
564
|
-
|
|
674
|
+
</span>
|
|
565
675
|
)}
|
|
566
676
|
<span className="fin-rate-example-suffix">
|
|
567
677
|
{t('autobahn:finance.perMonth')}
|
|
@@ -571,11 +681,6 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
571
681
|
)}
|
|
572
682
|
</div>
|
|
573
683
|
{isCash && <div className="md-label-sm hide-mobile">{t('autobahn:finance.fixedPrice')}</div>}
|
|
574
|
-
{isCash && showStrike && (
|
|
575
|
-
<div className="fin-kauf hide-mobile">
|
|
576
|
-
{t('autobahn:finance.uvp')} <s>{eur(listPrice as number)} €</s>
|
|
577
|
-
</div>
|
|
578
|
-
)}
|
|
579
684
|
</div>
|
|
580
685
|
{/* col 2: pill + eval bar grouped together (display:contents on desktop — no-op,
|
|
581
686
|
preserves the original rate|pill / cond|bar auto-placement; mobile turns this into
|
|
@@ -634,14 +739,18 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
634
739
|
(isCash ? (
|
|
635
740
|
<div style={{ marginTop: 4 }}>
|
|
636
741
|
<CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
637
|
-
<CalcRow label={t('autobahn:finance.warrantyLabel')}
|
|
638
|
-
|
|
639
|
-
{t('autobahn:finance.deliveryValue')}
|
|
742
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')} last={!available}>
|
|
743
|
+
{t('autobahn:finance.warrantyValue')}
|
|
640
744
|
</CalcRow>
|
|
745
|
+
{available && (
|
|
746
|
+
<CalcRow label={t('autobahn:finance.availabilityLabel')} last>
|
|
747
|
+
{t('autobahn:finance.availabilityValue')}
|
|
748
|
+
</CalcRow>
|
|
749
|
+
)}
|
|
641
750
|
</div>
|
|
642
751
|
) : (
|
|
643
|
-
<div style={{ display: 'flex', flexDirection: 'column',
|
|
644
|
-
<div>
|
|
752
|
+
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
|
|
753
|
+
<div style={{ marginBottom: 15 }}>
|
|
645
754
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
646
755
|
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
647
756
|
<span className="num" style={{ fontWeight: 700 }}>
|
|
@@ -658,7 +767,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
658
767
|
/>
|
|
659
768
|
</div>
|
|
660
769
|
{annualMileage != null && onAnnualMileageChange && (
|
|
661
|
-
<div>
|
|
770
|
+
<div style={{ marginBottom: 15 }}>
|
|
662
771
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
663
772
|
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
664
773
|
<span className="num" style={{ fontWeight: 700 }}>
|
|
@@ -675,19 +784,32 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
675
784
|
/>
|
|
676
785
|
</div>
|
|
677
786
|
)}
|
|
678
|
-
<div>
|
|
787
|
+
<div style={{ marginBottom: 15 }}>
|
|
679
788
|
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
680
789
|
{t('autobahn:finance.termSelection')}
|
|
681
790
|
</div>
|
|
682
|
-
<TermChips options={terms} value={term} onChange={onTermChange} t={t} />
|
|
683
|
-
</div>
|
|
684
|
-
<div>
|
|
685
|
-
<CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
|
|
686
|
-
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
687
|
-
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
688
|
-
{t('autobahn:finance.deliveryValue')}
|
|
689
|
-
</CalcRow>
|
|
791
|
+
<TermChips options={terms.filter((v) => v !== 12 && v !== 96)} value={term} onChange={onTermChange} t={t} />
|
|
690
792
|
</div>
|
|
793
|
+
{/* same Mit-Schlussrate / Rundum-Sorglos-Paket toggles as the mobile sheet / modal
|
|
794
|
+
desktop block — replaces the static Kaufpreis/Garantie/Lieferung rows. */}
|
|
795
|
+
{!isLease && onToggleFinalInstallment && (
|
|
796
|
+
<ToggleRow
|
|
797
|
+
className="fin-toggle-final"
|
|
798
|
+
label={t('autobahn:finance.finalInstallmentToggle')}
|
|
799
|
+
checked={!!withFinalInstallment}
|
|
800
|
+
onChange={onToggleFinalInstallment}
|
|
801
|
+
/>
|
|
802
|
+
)}
|
|
803
|
+
{addServicesCost != null && onToggleAddServices && (
|
|
804
|
+
<ToggleRow
|
|
805
|
+
className={'fin-toggle-addservices' + (isLease ? ' fin-toggle-addservices--leasing' : '')}
|
|
806
|
+
label={t('autobahn:finance.addServicesToggle')}
|
|
807
|
+
price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
|
|
808
|
+
checked={!!withAddServices}
|
|
809
|
+
disabled={addServicesDisabled}
|
|
810
|
+
onChange={onToggleAddServices}
|
|
811
|
+
/>
|
|
812
|
+
)}
|
|
691
813
|
</div>
|
|
692
814
|
))}
|
|
693
815
|
</div>
|