@bytebrand/fe-ui-core-autobahn 1.0.79 → 1.0.81
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 +14 -13
- package/cards/cardParts.tsx +7 -5
- package/compat/CookieModal/modals/ManageCookieModal/ManageCookieModal.tsx +8 -6
- package/compat/MdField/MdField.tsx +29 -5
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +6 -2
- package/locales/en/autobahn.json +6 -2
- package/package.json +1 -1
- package/theme/md3.css +176 -11
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/FinanceCalculator.tsx +133 -39
- package/vdp/VdpModules.tsx +184 -20
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
|
+
};
|
|
@@ -196,27 +196,87 @@ export const ToggleRow: React.FunctionComponent<{
|
|
|
196
196
|
className?: string;
|
|
197
197
|
/** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
|
|
198
198
|
staticNote?: string;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
199
|
+
/** when set, the info icon becomes clickable and opens a popover with this body (e.g. the
|
|
200
|
+
* Rundum-Sorglos-Paket contents) — "Mit Schlussrate" rows omit this, keeping their icon inert. */
|
|
201
|
+
tooltip?: React.ReactNode;
|
|
202
|
+
/** aria-label for the popover's close button — required together with `tooltip`. */
|
|
203
|
+
tooltipCloseLabel?: string;
|
|
204
|
+
}> = ({ label, price, checked, disabled, onChange, className, staticNote, tooltip, tooltipCloseLabel }) => {
|
|
205
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
className={className}
|
|
209
|
+
style={{
|
|
210
|
+
display: 'flex',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
justifyContent: staticNote ? undefined : 'space-between',
|
|
213
|
+
gap: 10,
|
|
214
|
+
position: tooltip ? 'relative' : undefined,
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<span
|
|
218
|
+
className="md-title-sm"
|
|
219
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 5, cursor: tooltip ? 'pointer' : undefined }}
|
|
220
|
+
role={tooltip ? 'button' : undefined}
|
|
221
|
+
tabIndex={tooltip ? 0 : undefined}
|
|
222
|
+
onClick={tooltip ? (e) => { e.stopPropagation(); setTooltipOpen((v) => !v); } : undefined}
|
|
223
|
+
>
|
|
224
|
+
{label}
|
|
225
|
+
{tooltip && <Icon name="info" size={13} color="var(--md-on-surface-variant)" />}
|
|
226
|
+
</span>
|
|
227
|
+
{tooltip && tooltipOpen && (
|
|
228
|
+
<div className="pp-uvp-tooltip fin-toggle-tooltip" onClick={(e) => e.stopPropagation()}>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
className="pp-uvp-tooltip-close fin-toggle-tooltip-close"
|
|
232
|
+
aria-label={tooltipCloseLabel}
|
|
233
|
+
onClick={(e) => {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
setTooltipOpen(false);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
239
|
+
<path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
<div className="pp-uvp-tooltip-body fin-toggle-tooltip-body">{tooltip}</div>
|
|
243
|
+
</div>
|
|
217
244
|
)}
|
|
245
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
|
|
246
|
+
{price && (
|
|
247
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
|
|
248
|
+
{price}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
{staticNote ? (
|
|
252
|
+
<span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
|
|
253
|
+
{staticNote}
|
|
254
|
+
</span>
|
|
255
|
+
) : (
|
|
256
|
+
<Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
218
259
|
</div>
|
|
219
|
-
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** body content for the "Rundum-Sorglos-Paket" ToggleRow tooltip — intro sentence, bold heading,
|
|
264
|
+
* then the package's contents as a bullet list. Shared by every addServicesToggle ToggleRow
|
|
265
|
+
* (FinanceCalculator's own card + VdpModules' modal/mobile-sheet instances). */
|
|
266
|
+
export const AddServicesTooltipBody: React.FunctionComponent<{ t: TranslationFunction }> = ({ t }) => (
|
|
267
|
+
<>
|
|
268
|
+
<p style={{ margin: '0 0 8px' }}>{t('autobahn:finance.addServicesTooltipIntro')}</p>
|
|
269
|
+
<p style={{ fontFamily: 'Arial', fontWeight: 700, fontSize: 10, lineHeight: '13px', color: '#0E2238', margin: '0 0 4px' }}>
|
|
270
|
+
{t('autobahn:finance.addServicesTooltipHeading')}
|
|
271
|
+
</p>
|
|
272
|
+
<ul style={{ margin: 0, padding: '0 0 0 14px' }}>
|
|
273
|
+
{t('autobahn:finance.addServicesTooltipItems')
|
|
274
|
+
.split('\n')
|
|
275
|
+
.map((item) => (
|
|
276
|
+
<li key={item}>{item}</li>
|
|
277
|
+
))}
|
|
278
|
+
</ul>
|
|
279
|
+
</>
|
|
220
280
|
);
|
|
221
281
|
|
|
222
282
|
export interface FinanceCalculatorProps {
|
|
@@ -278,6 +338,12 @@ export interface FinanceCalculatorProps {
|
|
|
278
338
|
phone?: string;
|
|
279
339
|
/** primary CTA click (Angebot sichern). */
|
|
280
340
|
onSecure?: () => void;
|
|
341
|
+
/** true once the customer already has an offer request/checkout in progress for this car —
|
|
342
|
+
* turns the primary CTA green (see .fin-cta-secured) instead of the default filled style. */
|
|
343
|
+
secured?: boolean;
|
|
344
|
+
/** overrides the primary CTA label while `secured` is true (e.g. "Zur Bestellung"). Ignored
|
|
345
|
+
* when `secured` is false — the CTA keeps its own default label in that case. */
|
|
346
|
+
secureLabel?: string;
|
|
281
347
|
/** secondary CTA click (phone). */
|
|
282
348
|
onCall?: () => void;
|
|
283
349
|
/** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
|
|
@@ -374,6 +440,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
374
440
|
available,
|
|
375
441
|
phone = '0800 40 30 182',
|
|
376
442
|
onSecure,
|
|
443
|
+
secured,
|
|
444
|
+
secureLabel,
|
|
377
445
|
onCall,
|
|
378
446
|
onShowDetails,
|
|
379
447
|
onOpenAdjust,
|
|
@@ -393,10 +461,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
393
461
|
// price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
|
|
394
462
|
const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
|
|
395
463
|
const evalTier = tierIdx >= 0 ? tierIdx : 1;
|
|
396
|
-
// both green tiers ('top' and 'good') collapse onto one "Top-Preis" claim (same convention as
|
|
397
|
-
// CarCard's PriceEvalCol) — footnote 4 carries the disclaimer text.
|
|
398
464
|
const isGoodTier = TIER_KEYS[evalTier] === 'good';
|
|
399
|
-
|
|
465
|
+
// each tier shows its own real label now (Top/Guter/Fairer/Erhöht/Hoher Preis) — no more
|
|
466
|
+
// collapsing 'good' onto the "Top-Preis" claim. Pill styling (colour/class/footnote) unchanged.
|
|
467
|
+
const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
|
|
400
468
|
const evalPillColor = isGoodTier ? TIER_COLORS[0] : TIER_COLORS[evalTier];
|
|
401
469
|
// condition summary (current selection) + its own details link — financing: "0 € Anzahlung ·
|
|
402
470
|
// 60 Monate · Finanzierungsdetails"; leasing: "25.000 € Sonderzahlung · 60 Mon. · 10.000
|
|
@@ -456,8 +524,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
456
524
|
>
|
|
457
525
|
{k === 'cash' ? (
|
|
458
526
|
<>
|
|
459
|
-
{/*
|
|
460
|
-
<span className="hide-mobile">{t(
|
|
527
|
+
{/* cash tab is "Kaufen" on both phone and desktop now — same key both spans. */}
|
|
528
|
+
<span className="hide-mobile">{t('autobahn:card.price.buy')}</span>
|
|
461
529
|
<span className="show-mobile" style={{ justifyContent: 'center' }}>
|
|
462
530
|
{t('autobahn:card.price.buy')}
|
|
463
531
|
</span>
|
|
@@ -636,7 +704,10 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
636
704
|
3
|
|
637
705
|
</span>
|
|
638
706
|
{uvpTooltipOpen && (
|
|
639
|
-
<div
|
|
707
|
+
<div
|
|
708
|
+
className={'pp-uvp-tooltip' + (mode === 'leasing' ? ' pp-uvp-tooltip--leasing' : '')}
|
|
709
|
+
onClick={(e) => e.stopPropagation()}
|
|
710
|
+
>
|
|
640
711
|
<button
|
|
641
712
|
type="button"
|
|
642
713
|
className="pp-uvp-tooltip-close"
|
|
@@ -737,17 +808,29 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
737
808
|
</button>
|
|
738
809
|
{adjust &&
|
|
739
810
|
(isCash ? (
|
|
740
|
-
|
|
741
|
-
<
|
|
742
|
-
|
|
743
|
-
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
811
|
+
<>
|
|
812
|
+
<div style={{ marginTop: 4 }}>
|
|
813
|
+
<CalcRow label={t('autobahn:finance.paymentLabel')} last={!available && !(addServicesCost != null && onToggleAddServices)}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
814
|
+
{available && (
|
|
815
|
+
<CalcRow label={t('autobahn:finance.availabilityLabel')} last={!(addServicesCost != null && onToggleAddServices)}>
|
|
816
|
+
{t('autobahn:finance.availabilityValue')}
|
|
817
|
+
</CalcRow>
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
{addServicesCost != null && onToggleAddServices && (
|
|
821
|
+
<ToggleRow
|
|
822
|
+
className="fin-toggle-addservices fin-toggle-addservices--cash"
|
|
823
|
+
label={t('autobahn:finance.addServicesToggle')}
|
|
824
|
+
price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
|
|
825
|
+
checked={!!withAddServices}
|
|
826
|
+
disabled={addServicesDisabled}
|
|
827
|
+
onChange={onToggleAddServices}
|
|
828
|
+
staticNote={t('autobahn:finance.addServicesIncluded')}
|
|
829
|
+
tooltip={<AddServicesTooltipBody t={t} />}
|
|
830
|
+
tooltipCloseLabel={t('autobahn:modal.close')}
|
|
831
|
+
/>
|
|
749
832
|
)}
|
|
750
|
-
|
|
833
|
+
</>
|
|
751
834
|
) : (
|
|
752
835
|
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
|
|
753
836
|
<div style={{ marginBottom: 15 }}>
|
|
@@ -808,6 +891,8 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
808
891
|
checked={!!withAddServices}
|
|
809
892
|
disabled={addServicesDisabled}
|
|
810
893
|
onChange={onToggleAddServices}
|
|
894
|
+
tooltip={<AddServicesTooltipBody t={t} />}
|
|
895
|
+
tooltipCloseLabel={t('autobahn:modal.close')}
|
|
811
896
|
/>
|
|
812
897
|
)}
|
|
813
898
|
</div>
|
|
@@ -815,8 +900,16 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
815
900
|
</div>
|
|
816
901
|
|
|
817
902
|
{/* REQUEST CTA */}
|
|
818
|
-
<Button
|
|
819
|
-
|
|
903
|
+
<Button
|
|
904
|
+
variant="filled"
|
|
905
|
+
block
|
|
906
|
+
size="lg"
|
|
907
|
+
icon="shield-check"
|
|
908
|
+
className={secured ? 'fin-cta-secured' : undefined}
|
|
909
|
+
style={{ marginTop: 14 }}
|
|
910
|
+
onClick={onSecure}
|
|
911
|
+
>
|
|
912
|
+
{secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
|
|
820
913
|
</Button>
|
|
821
914
|
{/* phone CTA — touch only (hidden on desktop via .fin-call) */}
|
|
822
915
|
<div className="fin-call">
|
|
@@ -848,11 +941,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
848
941
|
<Button
|
|
849
942
|
variant="filled"
|
|
850
943
|
size="lg"
|
|
944
|
+
className={secured ? 'fin-cta-secured' : undefined}
|
|
851
945
|
style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: 18 }}
|
|
852
946
|
onClick={onSecure}
|
|
853
947
|
>
|
|
854
|
-
<DocCheckIcon size={23} />
|
|
855
|
-
{t('autobahn:finance.secureOffer')}
|
|
948
|
+
{secured ? <Icon name="shield-check" size={18} /> : <DocCheckIcon size={23} />}
|
|
949
|
+
{secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
|
|
856
950
|
</Button>
|
|
857
951
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
|
858
952
|
<Button variant="outlined" size="lg" icon="phone" style={{ width: 112, height: 50 }} onClick={onCall}>
|