Przejście w internecie na addons kompaktowe

This commit is contained in:
dm
2025-12-15 12:32:21 +01:00
parent 3b654a8841
commit dc07fa083e
2 changed files with 164 additions and 75 deletions

View File

@@ -43,7 +43,7 @@ function normalizeAddons(addons) {
.map((a) => ({
id: String(a.id),
nazwa: String(a.nazwa),
typ: String(a.typ || "checkbox"),
typ: String(a.typ ?? a.type ?? "checkbox"),
ilosc: !!a.ilosc,
min: a.min != null ? Number(a.min) : 0,
max: a.max != null ? Number(a.max) : 10,
@@ -53,6 +53,7 @@ function normalizeAddons(addons) {
}));
}
/** ✅ Sekcja-akordeon (jak w internet modal) */
function SectionAccordion({ title, right, open, onToggle, children }) {
return (
<div class={`f-accordion-item f-section-acc ${open ? "is-open" : ""}`}>
@@ -78,8 +79,8 @@ export default function InternetAddonsModal({
onClose,
plan,
phoneCards = [], // telefon/cards.yaml -> cards[]
addons = [], // internet-swiatlowodowy/addons.yaml -> dodatki[]
phoneCards = [],
addons = [],
cenaOpis = "zł/mies.",
}) {
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
@@ -118,22 +119,24 @@ export default function InternetAddonsModal({
setSelectedPhoneId(null);
setSelectedQty({});
setOpenSections({ internet: true, phone: false, addons: false, summary: false });
}, [isOpen, plan]);
}, [isOpen, plan?.id]);
if (!isOpen || !plan) return null;
const basePrice = Number(plan.price_monthly || 0);
const phonePrice = (() => {
const phonePrice = useMemo(() => {
if (!selectedPhoneId) return 0;
const p = phonePlans.find((p) => String(p.id) === String(selectedPhoneId));
const p = phonePlans.find((x) => String(x.id) === String(selectedPhoneId));
return Number(p?.price_monthly || 0);
})();
}, [selectedPhoneId, phonePlans]);
const addonsPrice = addonsList.reduce((sum, a) => {
const addonsPrice = useMemo(() => {
return addonsList.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0);
return sum + qty * Number(a.cena || 0);
}, 0);
}, [selectedQty, addonsList]);
const totalMonthly = basePrice + phonePrice + addonsPrice;
@@ -158,6 +161,91 @@ export default function InternetAddonsModal({
setSelectedQty((prev) => ({ ...prev, [id]: safe }));
};
// ======================
// ✅ zapis do localStorage (jak w Jambox)
// ======================
const LS_KEY = "fuz_offer_config_v1";
function buildOfferPayload() {
const phone = selectedPhoneId
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
: null;
const addonsChosen = addonsList
.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return null;
return { id: a.id, nazwa: a.nazwa, qty, unit: Number(a.cena || 0) };
})
.filter(Boolean);
return {
createdAt: new Date().toISOString(),
// ważne: plan internetowy jako "pkg" żeby kontakt miał wspólny format
pkg: { id: plan?.id ?? null, name: plan?.name ?? "", price: basePrice },
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
// tu nie ma dekodera i tv-addons
decoder: null,
tvAddons: [],
addons: addonsChosen,
totals: {
base: basePrice,
phone: phonePrice,
decoder: 0,
tv: 0,
addons: addonsPrice,
total: totalMonthly,
currencyLabel: cenaOpis,
},
};
}
function moneyWithLabel(v) {
return `${money(v)} ${cenaOpis}`;
}
function buildOfferMessage(payload) {
const lines = [];
lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
lines.push("");
// ✅ WSZYSTKIE linie jak w podsumowaniu (wspólny standard)
lines.push(`Pakiet: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
lines.push(`Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
lines.push(`Dekoder: —`);
lines.push(`Dodatki TV: —`);
lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
if (payload?.phone) {
lines.push("");
lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
}
if (Array.isArray(payload?.addons) && payload.addons.length) {
lines.push("");
lines.push("Dodatkowe usługi:");
for (const it of payload.addons) {
lines.push(`- ${it.nazwa} x${it.qty} @ ${moneyWithLabel(it.unit)}`);
}
}
return lines.join("\n");
}
function saveOfferToLocalStorage() {
try {
const payload = buildOfferPayload();
payload.message = buildOfferMessage(payload);
localStorage.setItem(LS_KEY, JSON.stringify(payload));
} catch {}
}
// ======================
return (
<div class="f-modal-overlay" onClick={onClose}>
<button
@@ -172,10 +260,7 @@ export default function InternetAddonsModal({
</button>
<div
class="f-modal-panel f-modal-panel--compact"
onClick={(e) => e.stopPropagation()}
>
<div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
<div class="f-modal-inner">
<h2 class="f-modal-title">{plan.name} konfiguracja usług</h2>
@@ -204,7 +289,7 @@ export default function InternetAddonsModal({
</SectionAccordion>
</div>
{/* ✅ TELEFON */}
{/* ✅ TELEFON — identyczny wygląd jak w TV (f-radio-*) */}
<div class="f-modal-section">
<SectionAccordion
title="Usługa telefoniczna"
@@ -219,76 +304,62 @@ export default function InternetAddonsModal({
{phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p>
) : (
<div class="f-modal-phone-list f-accordion">
<div class="f-radio-list">
{/* brak telefonu */}
<div class="f-accordion-item f-accordion-item--no-phone">
<button
type="button"
class="f-accordion-header"
onClick={() => handlePhoneSelect(null)}
>
<span class="f-accordion-header-left">
<label class={`f-radio-item ${selectedPhoneId === null ? "is-selected" : ""}`}>
<div class="f-radio-check">
<input
type="radio"
name="phone-plan"
checked={selectedPhoneId === null}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(null);
}}
onClick={(e) => e.stopPropagation()}
onChange={() => handlePhoneSelect(null)}
/>
<span class="f-modal-phone-name">Nie potrzebuję telefonu</span>
</span>
<span class="f-modal-phone-price">0,00 {cenaOpis}</span>
</button>
</div>
<div class="f-radio-main">
<div class="f-radio-name">Nie potrzebuję telefonu</div>
</div>
<div class="f-radio-price">0,00 {cenaOpis}</div>
</label>
{/* pakiety */}
{phonePlans.map((p) => {
const isSelected = String(selectedPhoneId) === String(p.id);
return (
<div class="f-accordion-item" key={p.id}>
<button
type="button"
class="f-accordion-header"
onClick={() => handlePhoneSelect(p.id)}
>
<span class="f-accordion-header-left">
<div class="f-radio-block" key={p.id}>
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`}>
<div class="f-radio-check">
<input
type="radio"
name="phone-plan"
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(p.id);
}}
onClick={(e) => e.stopPropagation()}
onChange={() => handlePhoneSelect(p.id)}
/>
<span class="f-modal-phone-name">{p.name}</span>
</span>
</div>
<span class="f-modal-phone-price">
<div class="f-radio-main">
<div class="f-radio-name">{p.name}</div>
</div>
<div class="f-radio-price">
{money(p.price_monthly)} {cenaOpis}
</span>
</button>
</div>
</label>
{/* pokazuj parametry tylko dla wybranego (czytelniej) */}
{isSelected && p.features?.length > 0 && (
<div class="f-accordion-body">
{/* ✅ detale ZAWSZE widoczne (jak w TV) */}
{p.features?.length > 0 && (
<div class="f-radio-details">
<ul class="f-card-features">
{p.features
.filter(
(f) =>
!String(f.label || "")
.toLowerCase()
.includes("aktyw"),
(f) => !String(f.label || "").toLowerCase().includes("aktyw"),
)
.map((f, idx) => (
<li class="f-card-row" key={idx}>
<span class="f-card-label">{f.label}</span>
<span class="f-card-value">
{formatFeatureValue(f.value)}
</span>
<span class="f-card-value">{formatFeatureValue(f.value)}</span>
</li>
))}
</ul>
@@ -409,8 +480,9 @@ export default function InternetAddonsModal({
>
<div class="f-summary">
<div class="f-summary-list">
{/* ✅ WSZYSTKIE linie jak w Jambox (standard) */}
<div class="f-summary-row">
<span>Internet</span>
<span>Pakiet</span>
<span>{money(basePrice)} {cenaOpis}</span>
</div>
@@ -420,7 +492,17 @@ export default function InternetAddonsModal({
</div>
<div class="f-summary-row">
<span>Dodatki</span>
<span>Dekoder</span>
<span></span>
</div>
<div class="f-summary-row">
<span>Dodatki TV</span>
<span></span>
</div>
<div class="f-summary-row">
<span>Dodatkowe usługi</span>
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
</div>
@@ -428,18 +510,25 @@ export default function InternetAddonsModal({
<span>Łącznie</span>
<span>{money(totalMonthly)} {cenaOpis}</span>
</div>
<a
href="/kontakt"
class="btn btn-primary w-full mt-4"
onClick={() => saveOfferToLocalStorage()}
>
Wyślij zapytanie z tym wyborem
</a>
</div>
</div>
</SectionAccordion>
</div>
{/* ✅ zawsze widoczne w rogu */}
{/* ✅ pływająca suma (ten sam styl co w Jambox, jeśli już masz CSS) */}
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<div class="f-floating-total-inner">
<span class="f-floating-total-label">Suma</span>
<span class="f-floating-total-value">
{money(totalMonthly)} {cenaOpis}
</span>
<div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna">
<span class="f-floating-total-unit">Razem</span>
<span class="f-floating-total-amount">{money(totalMonthly)}</span>
<span class="f-floating-total-unit">{cenaOpis}</span>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "preact/hooks";
import Markdown from "../Markdown.jsx";
import OffersSwitches from "../OffersSwitches.jsx";
import InternetAddonsModal from "./InternetAddonsModal.jsx";
import InternetAddonsModal from "./InternetAddonsModalCompact.jsx";
import "../../styles/offers/offers-table.css";
function formatMoney(amount, currency = "PLN") {