JamboxModal- usuniecie starego i zmiana nazwy Compact

This commit is contained in:
dm
2025-12-16 12:03:40 +01:00
parent 31ebf5c90f
commit 91b0518a63
3 changed files with 435 additions and 1072 deletions

View File

@@ -37,6 +37,7 @@ function normalizeDecoders(list) {
.map((d) => ({ .map((d) => ({
id: String(d.id), id: String(d.id),
nazwa: String(d.nazwa), nazwa: String(d.nazwa),
opis: d.opis ? String(d.opis) : "",
cena: Number(d.cena ?? 0), cena: Number(d.cena ?? 0),
})); }));
} }
@@ -55,17 +56,12 @@ function normalizeAddons(addons) {
max: a.max != null ? Number(a.max) : 10, max: a.max != null ? Number(a.max) : 10,
krok: a.krok != null ? Number(a.krok) : 1, krok: a.krok != null ? Number(a.krok) : 1,
opis: a.opis ? String(a.opis) : "", opis: a.opis ? String(a.opis) : "",
// addons.yaml -> number albo {default, by_name}
// tv-addons.yaml -> [{pakiety, 12m, bezterminowo}]
cena: a.cena ?? 0, cena: a.cena ?? 0,
})); }));
} }
function normKey(s) { function normKey(s) {
return String(s || "") return String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
} }
/** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */ /** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */
@@ -75,14 +71,12 @@ function pickTvVariant(addon, pkgName) {
const wanted = normKey(pkgName); const wanted = normKey(pkgName);
// 1) po nazwie pakietu
for (const row of c) { for (const row of c) {
const pk = row?.pakiety; const pk = row?.pakiety;
if (!Array.isArray(pk)) continue; if (!Array.isArray(pk)) continue;
if (pk.some((p) => normKey(p) === wanted)) return row; if (pk.some((p) => normKey(p) === wanted)) return row;
} }
// 2) fallback "*"
for (const row of c) { for (const row of c) {
const pk = row?.pakiety; const pk = row?.pakiety;
if (!Array.isArray(pk)) continue; if (!Array.isArray(pk)) continue;
@@ -92,27 +86,22 @@ function pickTvVariant(addon, pkgName) {
return null; return null;
} }
/** TV: czy addon w ogóle dostępny dla pakietu */
function isTvAddonAvailableForPkg(addon, pkg) { function isTvAddonAvailableForPkg(addon, pkg) {
if (!pkg) return false; if (!pkg) return false;
const v = pickTvVariant(addon, String(pkg?.name ?? "")); const v = pickTvVariant(addon, String(pkg?.name ?? ""));
return !!v; return !!v;
} }
/** TV: czy ma dwie ceny (12m/bezterminowo) */
function hasTvTermPricing(addon, pkg) { function hasTvTermPricing(addon, pkg) {
const c = addon?.cena; const c = addon?.cena;
if (!Array.isArray(c)) return false; if (!Array.isArray(c)) return false;
// sprawdzamy wariant dla konkretnego pakietu (bo może się różnić)
const v = pickTvVariant(addon, String(pkg?.name ?? "")); const v = pickTvVariant(addon, String(pkg?.name ?? ""));
if (!v || typeof v !== "object") return false; if (!v || typeof v !== "object") return false;
// ✅ radio tylko jeśli są OBIE ceny
return v["12m"] != null && v.bezterminowo != null; return v["12m"] != null && v.bezterminowo != null;
} }
/** /**
* ✅ cena jednostkowa: * ✅ cena jednostkowa:
* - addons.yaml: number / string / legacy {default, by_name} * - addons.yaml: number / string / legacy {default, by_name}
@@ -121,11 +110,9 @@ function hasTvTermPricing(addon, pkg) {
function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) { function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
const c = addon?.cena; const c = addon?.cena;
// addons.yaml: liczba / liczba jako string
if (typeof c === "number") return c; if (typeof c === "number") return c;
if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c); if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c);
// tv-addons.yaml: tablica wariantów [{pakiety, 12m, bezterminowo}]
if (Array.isArray(c)) { if (Array.isArray(c)) {
const v = pickTvVariant(addon, String(pkg?.name ?? "")); const v = pickTvVariant(addon, String(pkg?.name ?? ""));
if (!v) return 0; if (!v) return 0;
@@ -133,14 +120,11 @@ function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
const t = term || "12m"; const t = term || "12m";
if (v[t] != null) return Number(v[t]) || 0; if (v[t] != null) return Number(v[t]) || 0;
// fallback
if (v.bezterminowo != null) return Number(v.bezterminowo) || 0; if (v.bezterminowo != null) return Number(v.bezterminowo) || 0;
if (v["12m"] != null) return Number(v["12m"]) || 0; if (v["12m"] != null) return Number(v["12m"]) || 0;
return 0; return 0;
} }
// ✅ LEGACY: addons.yaml może mieć cenę zależną od pakietu:
// cena: { default: 19.9, by_name: { "Smart": 15.0, ... } }
if (c && typeof c === "object") { if (c && typeof c === "object") {
const name = String(pkg?.name ?? ""); const name = String(pkg?.name ?? "");
const wanted = normKey(name); const wanted = normKey(name);
@@ -158,12 +142,32 @@ function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
return 0; return 0;
} }
/** ✅ 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" : ""}`}>
<button type="button" class="f-accordion-header" onClick={onToggle}>
<span class="f-accordion-header-left">
<span class="f-modal-phone-name">{title}</span>
</span>
<span class="f-accordion-header-right">
{right}
<span class="f-acc-chevron" aria-hidden="true">
{open ? "▲" : "▼"}
</span>
</span>
</button>
{open && <div class="f-accordion-body">{children}</div>}
</div>
);
}
export default function JamboxAddonsModal({ export default function JamboxAddonsModal({
isOpen, isOpen,
onClose, onClose,
pkg, pkg,
// ✅ YAML
phoneCards = [], phoneCards = [],
tvAddons = [], tvAddons = [],
addons = [], addons = [],
@@ -178,7 +182,6 @@ export default function JamboxAddonsModal({
const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]); const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]);
// ✅ TV: pokazujemy tylko dostępne dla pkg.name
const tvAddonsVisible = useMemo(() => { const tvAddonsVisible = useMemo(() => {
if (!pkg) return []; if (!pkg) return [];
return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg)); return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg));
@@ -188,27 +191,55 @@ export default function JamboxAddonsModal({
const [selectedPhoneId, setSelectedPhoneId] = useState(null); const [selectedPhoneId, setSelectedPhoneId] = useState(null);
const [openPhoneId, setOpenPhoneId] = useState(null); const [openPhoneId, setOpenPhoneId] = useState(null);
// dekoder (radio)
const [selectedDecoderId, setSelectedDecoderId] = useState(null); const [selectedDecoderId, setSelectedDecoderId] = useState(null);
// checkbox/quantity: { [id]: qty }
const [selectedQty, setSelectedQty] = useState({}); const [selectedQty, setSelectedQty] = useState({});
// ✅ TV: term per dodatek (12m / bezterminowo)
const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" } const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" }
// akordeon pakietu bazowego // ✅ sekcje (jedna otwarta naraz)
const [baseOpen, setBaseOpen] = useState(true); const [openSections, setOpenSections] = useState({
base: true,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
});
const toggleSection = (key) => {
setOpenSections((prev) => {
const nextOpen = !prev[key];
return {
base: false,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
[key]: nextOpen,
};
});
};
// reset po otwarciu / zmianie pakietu // reset po otwarciu / zmianie pakietu
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
setSelectedPhoneId(null); setSelectedPhoneId(null);
setOpenPhoneId(null); setOpenPhoneId(null);
setSelectedDecoderId(null); setSelectedDecoderId(null);
setSelectedQty({}); setSelectedQty({});
setTvTerm({}); setTvTerm({});
setBaseOpen(true);
setOpenSections({
base: true,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
});
const d0 = const d0 =
(Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) || (Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) ||
@@ -216,6 +247,7 @@ export default function JamboxAddonsModal({
setSelectedDecoderId(d0 ? String(d0.id) : null); setSelectedDecoderId(d0 ? String(d0.id) : null);
}, [isOpen, pkg?.id, decodersList]); }, [isOpen, pkg?.id, decodersList]);
if (!isOpen || !pkg) return null; if (!isOpen || !pkg) return null;
const basePrice = Number(pkg.price_monthly || 0); const basePrice = Number(pkg.price_monthly || 0);
@@ -232,7 +264,6 @@ export default function JamboxAddonsModal({
return Number(d?.cena || 0); return Number(d?.cena || 0);
}, [selectedDecoderId, decodersList]); }, [selectedDecoderId, decodersList]);
// ✅ TV: suma liczona tylko po widocznych (czyli dostępnych)
const tvAddonsPrice = useMemo(() => { const tvAddonsPrice = useMemo(() => {
return tvAddonsVisible.reduce((sum, a) => { return tvAddonsVisible.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0); const qty = Number(selectedQty[a.id] || 0);
@@ -246,7 +277,6 @@ export default function JamboxAddonsModal({
}, 0); }, 0);
}, [selectedQty, tvAddonsVisible, tvTerm, pkg]); }, [selectedQty, tvAddonsVisible, tvTerm, pkg]);
// zwykłe dodatki (addons.yaml) stara logika (multiroom itp.)
const addonsOnlyPrice = useMemo(() => { const addonsOnlyPrice = useMemo(() => {
return addonsList.reduce((sum, a) => { return addonsList.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0); const qty = Number(selectedQty[a.id] || 0);
@@ -285,7 +315,6 @@ export default function JamboxAddonsModal({
const qty = Number(selectedQty[a.id] || 0); const qty = Number(selectedQty[a.id] || 0);
const isQty = a.typ === "quantity" || a.ilosc === true; const isQty = a.typ === "quantity" || a.ilosc === true;
// TV: term i cena
const termPricing = isTv && hasTvTermPricing(a, pkg); const termPricing = isTv && hasTvTermPricing(a, pkg);
const term = tvTerm[a.id] || "12m"; const term = tvTerm[a.id] || "12m";
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null); const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
@@ -383,6 +412,137 @@ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
); );
}; };
// ---------
const LS_KEY = "fuz_offer_config_v1";
function buildOfferPayload() {
const phone = selectedPhoneId
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
: null;
const decoder = selectedDecoderId
? decodersList.find((d) => String(d.id) === String(selectedDecoderId))
: null;
const tvChosen = tvAddonsVisible
.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return null;
const termPricing = hasTvTermPricing(a, pkg);
const term = tvTerm[a.id] || "12m";
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
return {
id: a.id,
nazwa: a.nazwa,
qty,
term: termPricing ? term : null,
unit,
};
})
.filter(Boolean);
const addonsChosen = addonsList
.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return null;
const unit = getAddonUnitPrice(a, pkg, null);
return { id: a.id, nazwa: a.nazwa, qty, unit };
})
.filter(Boolean);
return {
createdAt: new Date().toISOString(),
pkg: { id: pkg?.id ?? null, name: pkg?.name ?? "", price: basePrice },
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
decoder: decoder ? { id: decoder.id, name: decoder.nazwa, price: decoder.cena } : null,
tvAddons: tvChosen,
addons: addonsChosen,
totals: {
base: basePrice,
phone: phonePrice,
decoder: decoderPrice,
tv: tvAddonsPrice,
addons: addonsOnlyPrice,
total: totalMonthly,
currencyLabel: cenaOpis,
},
};
}
function saveOfferToLocalStorage() {
try {
const payload = buildOfferPayload();
localStorage.setItem(LS_KEY, JSON.stringify(payload));
} catch {}
}
//-- dopisane
function moneyWithLabel(v) {
return `${money(v)} ${cenaOpis}`;
}
function buildOfferMessage(payload) {
const lines = [];
// nagłówek
lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
lines.push("");
// ✅ WSZYSTKIE linie jak w podsumowaniu
lines.push(`Pakiet: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
lines.push(`Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
lines.push(`Dekoder: ${payload?.decoder ? moneyWithLabel(payload.totals.decoder) : "—"}`);
lines.push(`Dodatki TV: ${payload?.tvAddons?.length ? moneyWithLabel(payload.totals.tv) : "—"}`);
lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
// szczegóły (pozycje)
if (payload?.phone) {
lines.push("");
lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
}
if (payload?.decoder) {
lines.push("");
lines.push(`Dekoder: ${payload.decoder.name} (${moneyWithLabel(payload.decoder.price)})`);
}
if (Array.isArray(payload?.tvAddons) && payload.tvAddons.length) {
lines.push("");
lines.push("Pakiety dodatkowe TV:");
for (const it of payload.tvAddons) {
const termTxt = it.term ? `, ${it.term}` : "";
lines.push(
`- ${it.nazwa} x${it.qty}${termTxt} @ ${moneyWithLabel(it.unit)}`
);
}
}
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); // ✅ gotowy tekst
localStorage.setItem(LS_KEY, JSON.stringify(payload));
} catch {}
}
// ---------
return ( return (
<div class="f-modal-overlay" onClick={onClose}> <div class="f-modal-overlay" onClick={onClose}>
<button <button
@@ -399,24 +559,17 @@ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
<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"> <div class="f-modal-inner">
<h2 class="f-modal-title">Konfiguracja usług dodatkowych</h2> <h2 class="f-modal-title">{pkg.name} konfiguracja usług</h2>
{/* PAKIET jako akordeon */} {/* PAKIET (sekcja) */}
<div class="f-modal-section"> <div class="f-modal-section">
<div class={`f-accordion-item ${baseOpen ? "is-open" : ""}`}> <SectionAccordion
<button title={pkg.name}
type="button" right={<span class="f-modal-phone-price">{money(basePrice)} {cenaOpis}</span>}
class="f-accordion-header" open={openSections.base}
onClick={() => setBaseOpen((prev) => !prev)} onToggle={() => toggleSection("base")}
> >
<span class="f-modal-phone-name">{pkg.name}</span> {pkg.features?.length ? (
<span class="f-modal-phone-price">
{money(basePrice)} {cenaOpis}
</span>
</button>
{baseOpen && pkg.features && pkg.features.length > 0 && (
<div class="f-accordion-body">
<ul class="f-card-features"> <ul class="f-card-features">
{pkg.features.map((f, idx) => ( {pkg.features.map((f, idx) => (
<li class="f-card-row" key={idx}> <li class="f-card-row" key={idx}>
@@ -425,133 +578,149 @@ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
</li> </li>
))} ))}
</ul> </ul>
</div> ) : (
<p class="opacity-80">Brak szczegółów pakietu.</p>
)} )}
</div> </SectionAccordion>
</div> </div>
{/* ✅ DEKODER (radio) — NAD TV ADDONS */} {/* ✅ DEKODER (sekcja) */}
<div class="f-modal-section"> <div class="f-modal-section">
<h3>Wybór dekodera</h3> <SectionAccordion
title="Wybór dekodera"
right={
<span class="f-modal-phone-price">
{decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.decoder}
onToggle={() => toggleSection("decoder")}
>
{decodersList.length === 0 ? ( {decodersList.length === 0 ? (
<p>Brak dostępnych dekoderów.</p> <p>Brak dostępnych dekoderów.</p>
) : ( ) : (
<div class="f-modal-phone-list f-accordion"> <div class="f-radio-list">
{decodersList.map((d) => { {decodersList.map((d) => {
const isSelected = String(selectedDecoderId) === String(d.id); const isSelected = String(selectedDecoderId) === String(d.id);
return ( return (
<div class={`f-accordion-item ${isSelected ? "is-open" : ""}`} key={d.id}> <label class={`f-radio-item ${isSelected ? "is-selected" : ""}`} key={d.id}>
<button <div class="f-radio-check">
type="button"
class="f-accordion-header"
onClick={() => setSelectedDecoderId(String(d.id))}
>
<span class="f-accordion-header-left">
<input <input
type="radio" type="radio"
name="decoder" name="decoder"
checked={isSelected} checked={isSelected}
onChange={(e) => { onChange={() => setSelectedDecoderId(String(d.id))}
e.stopPropagation();
setSelectedDecoderId(String(d.id));
}}
onClick={(e) => e.stopPropagation()}
/> />
<span class="f-modal-phone-name">{d.nazwa}</span>
</span>
<span class="f-modal-phone-price">
{money(d.cena)} {cenaOpis}
</span>
</button>
</div> </div>
<div class="f-radio-main">
<div class="f-radio-name">{d.nazwa}</div>
{d.opis && <div class="f-addon-desc">{d.opis}</div>}
</div>
<div class="f-radio-price">
{money(d.cena)} {cenaOpis}
</div>
</label>
); );
})} })}
</div> </div>
)} )}
</SectionAccordion>
</div> </div>
{/* ✅ TV ADDONS (sekcja) */}
{/* TV ADDONS */}
<div class="f-modal-section"> <div class="f-modal-section">
<h3>Pakiety dodatkowe TV</h3> <SectionAccordion
title="Pakiety dodatkowe TV"
right={
<span class="f-modal-phone-price">
{tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.tv}
onToggle={() => toggleSection("tv")}
>
{tvAddonsVisible.length === 0 ? ( {tvAddonsVisible.length === 0 ? (
<p>Brak pakietów dodatkowych TV.</p> <p>Brak pakietów dodatkowych TV.</p>
) : ( ) : (
<div class="f-addon-list">{tvAddonsVisible.map((a) => renderAddonRow(a, true))}</div> <div class="f-addon-list">{tvAddonsVisible.map((a) => renderAddonRow(a, true))}</div>
)} )}
</SectionAccordion>
</div> </div>
{/* TELEFON */} {/* TELEFON (sekcja) */}
<div class="f-modal-section"> <div class="f-modal-section">
<h3>Usługa telefoniczna</h3> <SectionAccordion
title="Usługa telefoniczna"
right={
<span class="f-modal-phone-price">
{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.phone}
onToggle={() => toggleSection("phone")}
>
{phonePlans.length === 0 ? ( {phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p> <p>Brak dostępnych pakietów telefonicznych.</p>
) : ( ) : (
<div class="f-modal-phone-list f-accordion"> <div class="f-radio-list">
<div class="f-accordion-item f-accordion-item--no-phone"> {/* brak telefonu */}
<button <label class={`f-radio-item ${selectedPhoneId === null ? "is-selected" : ""}`}>
type="button" <div class="f-radio-check">
class="f-accordion-header"
onClick={() => handlePhoneSelect(null)}
>
<span class="f-accordion-header-left">
<input <input
type="radio" type="radio"
name="phone-plan" name="phone-plan"
checked={selectedPhoneId === null} checked={selectedPhoneId === null}
onChange={(e) => { onChange={() => handlePhoneSelect(null)}
e.stopPropagation();
handlePhoneSelect(null);
}}
onClick={(e) => e.stopPropagation()}
/> />
<span class="f-modal-phone-name">Nie potrzebuję telefonu</span>
</span>
<span class="f-modal-phone-price">0,00 {cenaOpis}</span>
</button>
</div> </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) => { {phonePlans.map((p) => {
const isSelected = String(selectedPhoneId) === String(p.id); const isSelected = String(selectedPhoneId) === String(p.id);
const isOpen = String(openPhoneId) === String(p.id);
return ( return (
<div class={`f-accordion-item ${isOpen ? "is-open" : ""}`} key={p.id}> <div class="f-radio-block" key={p.id}>
<button <label class={`f-radio-item ${isSelected ? "is-selected" : ""}`}>
type="button" <div class="f-radio-check">
class="f-accordion-header"
onClick={() => handlePhoneSelect(p.id)}
>
<span class="f-accordion-header-left">
<input <input
type="radio" type="radio"
name="phone-plan" name="phone-plan"
checked={isSelected} checked={isSelected}
onChange={(e) => { onChange={() => handlePhoneSelect(p.id)}
e.stopPropagation();
handlePhoneSelect(p.id);
}}
onClick={(e) => e.stopPropagation()}
/> />
<span class="f-modal-phone-name">{p.name}</span> </div>
</span>
<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} {money(p.price_monthly)} {cenaOpis}
</span> </div>
</button> </label>
{isOpen && ( {/* ✅ detale ZAWSZE widoczne */}
<div class="f-accordion-body">
{p.features?.length > 0 && ( {p.features?.length > 0 && (
<div class="f-radio-details">
<ul class="f-card-features"> <ul class="f-card-features">
{p.features {p.features
.filter( .filter(
(f) => !String(f.label || "").toLowerCase().includes("aktyw"), (f) =>
!String(f.label || "").toLowerCase().includes("aktyw"),
) )
.map((f, idx) => ( .map((f, idx) => (
<li class="f-card-row" key={idx}> <li class="f-card-row" key={idx}>
@@ -560,37 +729,51 @@ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
</li> </li>
))} ))}
</ul> </ul>
)}
</div> </div>
)} )}
</div> </div>
); );
})} })}
</div> </div>
)} )}
</SectionAccordion>
</div> </div>
{/* DODATKI (addons.yaml) */} {/* DODATKI (sekcja) */}
<div class="f-modal-section"> <div class="f-modal-section">
<h3>Dodatkowe usługi</h3> <SectionAccordion
title="Dodatkowe usługi"
right={
<span class="f-modal-phone-price">
{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.addons}
onToggle={() => toggleSection("addons")}
>
{addonsList.length === 0 ? ( {addonsList.length === 0 ? (
<p>Brak usług dodatkowych.</p> <p>Brak usług dodatkowych.</p>
) : ( ) : (
<div class="f-addon-list">{addonsList.map((a) => renderAddonRow(a, false))}</div> <div class="f-addon-list">{addonsList.map((a) => renderAddonRow(a, false))}</div>
)} )}
</SectionAccordion>
</div> </div>
{/* PODSUMOWANIE */} {/* PODSUMOWANIE (sekcja) */}
<div class="f-modal-section f-summary"> <div class="f-modal-section">
<h3>Podsumowanie miesięczne</h3> <SectionAccordion
title="Podsumowanie miesięczne"
right={<span class="f-modal-phone-price">{money(totalMonthly)} {cenaOpis}</span>}
open={openSections.summary}
onToggle={() => toggleSection("summary")}
>
<div class="f-summary">
<div class="f-summary-list"> <div class="f-summary-list">
<div class="f-summary-row"> <div class="f-summary-row">
<span>Pakiet</span> <span>Pakiet</span>
<span> <span>{money(basePrice)} {cenaOpis}</span>
{money(basePrice)} {cenaOpis}
</span>
</div> </div>
<div class="f-summary-row"> <div class="f-summary-row">
@@ -609,27 +792,52 @@ const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
</div> </div>
<div class="f-summary-row"> <div class="f-summary-row">
<span>Dodatki</span> <span>Dodatkowe usługi</span>
<span>{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}</span> <span>{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}</span>
</div> </div>
<div class="f-summary-total"> <div class="f-summary-total">
<span>Łącznie</span> <span>Łącznie</span>
<span> <span>{money(totalMonthly)} {cenaOpis}</span>
{money(totalMonthly)} {cenaOpis}
</span>
</div>
</div>
</div> </div>
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}> <a
href="/kontakt"
class="btn btn-primary w-full mt-4"
onClick={() => saveOfferToLocalStorage()}
>
Wyślij zapytanie z tym wyborem
</a>
</div>
</div>
</SectionAccordion>
</div>
{/* ✅ pływająca suma jak w internecie */}
{/* <div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<div class="f-floating-total-inner"> <div class="f-floating-total-inner">
<span class="f-floating-total-label">Suma</span> <span class="f-floating-total-label">Suma</span>
<span class="f-floating-total-value"> <span class="f-floating-total-value">
{money(totalMonthly)} {cenaOpis} {money(totalMonthly)} {cenaOpis}
</span> </span>
</div> </div>
</div> */}
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<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>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,845 +0,0 @@
import { useEffect, useMemo, useState } from "preact/hooks";
import "../../styles/modal.css";
import "../../styles/addons.css";
function formatFeatureValue(val) {
if (val === true || val === "true") return "✓";
if (val === false || val === "false" || val == null) return "✕";
return val;
}
function money(amount) {
const n = Number(amount || 0);
return n.toFixed(2).replace(".", ",");
}
/** telefon z YAML (phone/cards.yaml -> cards[]) => { id, name, price_monthly, features[] } */
function mapPhoneYamlToPlans(phoneCards) {
const list = Array.isArray(phoneCards) ? phoneCards : [];
return list
.filter((c) => c?.widoczny !== false)
.map((c, idx) => ({
id: String(c?.id ?? c?.nazwa ?? idx),
name: c?.nazwa ?? "—",
price_monthly: Number(c?.cena?.wartosc ?? 0),
features: (Array.isArray(c?.parametry) ? c.parametry : []).map((p) => ({
label: p.label,
value: p.value,
})),
}));
}
/** dekodery z YAML */
function normalizeDecoders(list) {
const arr = Array.isArray(list) ? list : [];
return arr
.filter((d) => d?.id && d?.nazwa)
.map((d) => ({
id: String(d.id),
nazwa: String(d.nazwa),
opis: d.opis ? String(d.opis) : "",
cena: Number(d.cena ?? 0),
}));
}
/** dodatki z YAML (tv-addons.yaml / addons.yaml) */
function normalizeAddons(addons) {
const list = Array.isArray(addons) ? addons : [];
return list
.filter((a) => a?.id && a?.nazwa)
.map((a) => ({
id: String(a.id),
nazwa: String(a.nazwa),
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,
krok: a.krok != null ? Number(a.krok) : 1,
opis: a.opis ? String(a.opis) : "",
cena: a.cena ?? 0,
}));
}
function normKey(s) {
return String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
}
/** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */
function pickTvVariant(addon, pkgName) {
const c = addon?.cena;
if (!Array.isArray(c)) return null;
const wanted = normKey(pkgName);
for (const row of c) {
const pk = row?.pakiety;
if (!Array.isArray(pk)) continue;
if (pk.some((p) => normKey(p) === wanted)) return row;
}
for (const row of c) {
const pk = row?.pakiety;
if (!Array.isArray(pk)) continue;
if (pk.some((p) => String(p).trim() === "*")) return row;
}
return null;
}
function isTvAddonAvailableForPkg(addon, pkg) {
if (!pkg) return false;
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
return !!v;
}
function hasTvTermPricing(addon, pkg) {
const c = addon?.cena;
if (!Array.isArray(c)) return false;
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
if (!v || typeof v !== "object") return false;
return v["12m"] != null && v.bezterminowo != null;
}
/**
* ✅ cena jednostkowa:
* - addons.yaml: number / string / legacy {default, by_name}
* - tv-addons.yaml: tablica wariantów
*/
function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
const c = addon?.cena;
if (typeof c === "number") return c;
if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c);
if (Array.isArray(c)) {
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
if (!v) return 0;
const t = term || "12m";
if (v[t] != null) return Number(v[t]) || 0;
if (v.bezterminowo != null) return Number(v.bezterminowo) || 0;
if (v["12m"] != null) return Number(v["12m"]) || 0;
return 0;
}
if (c && typeof c === "object") {
const name = String(pkg?.name ?? "");
const wanted = normKey(name);
const byName = c.by_name || c.byName || c.by_nazwa || c.byNazwa;
if (byName && typeof byName === "object" && name) {
for (const k of Object.keys(byName)) {
if (normKey(k) === wanted) return Number(byName[k]) || 0;
}
}
if (c.default != null) return Number(c.default) || 0;
}
return 0;
}
/** ✅ 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" : ""}`}>
<button type="button" class="f-accordion-header" onClick={onToggle}>
<span class="f-accordion-header-left">
<span class="f-modal-phone-name">{title}</span>
</span>
<span class="f-accordion-header-right">
{right}
<span class="f-acc-chevron" aria-hidden="true">
{open ? "▲" : "▼"}
</span>
</span>
</button>
{open && <div class="f-accordion-body">{children}</div>}
</div>
);
}
export default function JamboxAddonsModal({
isOpen,
onClose,
pkg,
phoneCards = [],
tvAddons = [],
addons = [],
decoders = [],
cenaOpis = "zł/mies.",
}) {
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
const tvAddonsList = useMemo(() => normalizeAddons(tvAddons), [tvAddons]);
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]);
const tvAddonsVisible = useMemo(() => {
if (!pkg) return [];
return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg));
}, [tvAddonsList, pkg]);
// wybory
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
const [openPhoneId, setOpenPhoneId] = useState(null);
const [selectedDecoderId, setSelectedDecoderId] = useState(null);
const [selectedQty, setSelectedQty] = useState({});
const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" }
// ✅ sekcje (jedna otwarta naraz)
const [openSections, setOpenSections] = useState({
base: true,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
});
const toggleSection = (key) => {
setOpenSections((prev) => {
const nextOpen = !prev[key];
return {
base: false,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
[key]: nextOpen,
};
});
};
// reset po otwarciu / zmianie pakietu
useEffect(() => {
if (!isOpen) return;
setSelectedPhoneId(null);
setOpenPhoneId(null);
setSelectedDecoderId(null);
setSelectedQty({});
setTvTerm({});
setOpenSections({
base: true,
decoder: false,
tv: false,
phone: false,
addons: false,
summary: false,
});
const d0 =
(Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) ||
(Array.isArray(decodersList) ? decodersList[0] : null);
setSelectedDecoderId(d0 ? String(d0.id) : null);
}, [isOpen, pkg?.id, decodersList]);
if (!isOpen || !pkg) return null;
const basePrice = Number(pkg.price_monthly || 0);
const phonePrice = useMemo(() => {
if (!selectedPhoneId) return 0;
const p = phonePlans.find((x) => String(x.id) === String(selectedPhoneId));
return Number(p?.price_monthly || 0);
}, [selectedPhoneId, phonePlans]);
const decoderPrice = useMemo(() => {
if (!selectedDecoderId) return 0;
const d = decodersList.find((x) => String(x.id) === String(selectedDecoderId));
return Number(d?.cena || 0);
}, [selectedDecoderId, decodersList]);
const tvAddonsPrice = useMemo(() => {
return tvAddonsVisible.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return sum;
const termPricing = hasTvTermPricing(a, pkg);
const term = tvTerm[a.id] || "12m";
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
return sum + qty * unit;
}, 0);
}, [selectedQty, tvAddonsVisible, tvTerm, pkg]);
const addonsOnlyPrice = useMemo(() => {
return addonsList.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0);
const unit = getAddonUnitPrice(a, pkg, null);
return sum + qty * unit;
}, 0);
}, [selectedQty, addonsList, pkg]);
const addonsPrice = tvAddonsPrice + addonsOnlyPrice;
const totalMonthly = basePrice + phonePrice + decoderPrice + addonsPrice;
const handlePhoneSelect = (id) => {
if (id === null) {
setSelectedPhoneId(null);
setOpenPhoneId(null);
return;
}
setSelectedPhoneId(id);
setOpenPhoneId((prev) => (String(prev) === String(id) ? null : id));
};
const toggleCheckboxAddon = (id) => {
setSelectedQty((prev) => {
const next = { ...prev };
next[id] = (next[id] || 0) > 0 ? 0 : 1;
return next;
});
};
const setQtyAddon = (id, qty, min, max) => {
const safe = Math.max(min, Math.min(max, qty));
setSelectedQty((prev) => ({ ...prev, [id]: safe }));
};
const renderAddonRow = (a, isTv = false) => {
const qty = Number(selectedQty[a.id] || 0);
const isQty = a.typ === "quantity" || a.ilosc === true;
const termPricing = isTv && hasTvTermPricing(a, pkg);
const term = tvTerm[a.id] || "12m";
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
if (!isQty) {
return (
<label class="f-addon-item" key={(isTv ? "tv-" : "a-") + a.id}>
<div class="f-addon-checkbox">
<input
type="checkbox"
checked={qty > 0}
onChange={() => toggleCheckboxAddon(a.id)}
/>
</div>
<div class="f-addon-main">
<div class="f-addon-name">{a.nazwa}</div>
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
{termPricing && (
<div class="mt-2 flex flex-wrap gap-3 text-sm" onClick={(e) => e.stopPropagation()}>
<label class="inline-flex items-center gap-2">
<input
type="radio"
name={`term-${a.id}`}
checked={(tvTerm[a.id] || "12m") === "12m"}
onChange={() => setTvTerm((p) => ({ ...p, [a.id]: "12m" }))}
/>
<span>12 miesięcy</span>
</label>
<label class="inline-flex items-center gap-2">
<input
type="radio"
name={`term-${a.id}`}
checked={(tvTerm[a.id] || "12m") === "bezterminowo"}
onChange={() => setTvTerm((p) => ({ ...p, [a.id]: "bezterminowo" }))}
/>
<span>Bezterminowo</span>
</label>
</div>
)}
</div>
<div class="f-addon-price">
{money(unit)} {cenaOpis}
</div>
</label>
);
}
const min = Number.isFinite(a.min) ? a.min : 0;
const max = Number.isFinite(a.max) ? a.max : 10;
const step = Number.isFinite(a.krok) ? a.krok : 1;
const lineTotal = qty * unit;
return (
<div class="f-addon-item f-addon-item--qty" key={(isTv ? "tvq-" : "aq-") + a.id}>
<div class="f-addon-checkbox" aria-hidden="true"></div>
<div class="f-addon-main">
<div class="f-addon-name">{a.nazwa}</div>
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
</div>
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
<button
type="button"
class="btn btn-outline"
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
disabled={qty <= min}
>
</button>
<span class="f-addon-qty-value">{qty}</span>
<button
type="button"
class="btn btn-outline"
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
disabled={qty >= max}
>
+
</button>
</div>
<div class="f-addon-price">
<div>
{money(unit)} {cenaOpis}
</div>
<div class="f-addon-price-total">{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}</div>
</div>
</div>
);
};
// ---------
const LS_KEY = "fuz_offer_config_v1";
function buildOfferPayload() {
const phone = selectedPhoneId
? phonePlans.find((p) => String(p.id) === String(selectedPhoneId))
: null;
const decoder = selectedDecoderId
? decodersList.find((d) => String(d.id) === String(selectedDecoderId))
: null;
const tvChosen = tvAddonsVisible
.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return null;
const termPricing = hasTvTermPricing(a, pkg);
const term = tvTerm[a.id] || "12m";
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
return {
id: a.id,
nazwa: a.nazwa,
qty,
term: termPricing ? term : null,
unit,
};
})
.filter(Boolean);
const addonsChosen = addonsList
.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
if (qty <= 0) return null;
const unit = getAddonUnitPrice(a, pkg, null);
return { id: a.id, nazwa: a.nazwa, qty, unit };
})
.filter(Boolean);
return {
createdAt: new Date().toISOString(),
pkg: { id: pkg?.id ?? null, name: pkg?.name ?? "", price: basePrice },
phone: phone ? { id: phone.id, name: phone.name, price: phone.price_monthly } : null,
decoder: decoder ? { id: decoder.id, name: decoder.nazwa, price: decoder.cena } : null,
tvAddons: tvChosen,
addons: addonsChosen,
totals: {
base: basePrice,
phone: phonePrice,
decoder: decoderPrice,
tv: tvAddonsPrice,
addons: addonsOnlyPrice,
total: totalMonthly,
currencyLabel: cenaOpis,
},
};
}
function saveOfferToLocalStorage() {
try {
const payload = buildOfferPayload();
localStorage.setItem(LS_KEY, JSON.stringify(payload));
} catch {}
}
//-- dopisane
function moneyWithLabel(v) {
return `${money(v)} ${cenaOpis}`;
}
function buildOfferMessage(payload) {
const lines = [];
// nagłówek
lines.push(`Wybrana oferta: ${payload?.pkg?.name || "—"}`);
lines.push("");
// ✅ WSZYSTKIE linie jak w podsumowaniu
lines.push(`Pakiet: ${moneyWithLabel(payload?.totals?.base ?? 0)}`);
lines.push(`Telefon: ${payload?.phone ? moneyWithLabel(payload.totals.phone) : "—"}`);
lines.push(`Dekoder: ${payload?.decoder ? moneyWithLabel(payload.totals.decoder) : "—"}`);
lines.push(`Dodatki TV: ${payload?.tvAddons?.length ? moneyWithLabel(payload.totals.tv) : "—"}`);
lines.push(`Dodatkowe usługi: ${payload?.addons?.length ? moneyWithLabel(payload.totals.addons) : "—"}`);
lines.push(`Łącznie: ${moneyWithLabel(payload?.totals?.total ?? 0)}`);
// szczegóły (pozycje)
if (payload?.phone) {
lines.push("");
lines.push(`Telefon: ${payload.phone.name} (${moneyWithLabel(payload.phone.price)})`);
}
if (payload?.decoder) {
lines.push("");
lines.push(`Dekoder: ${payload.decoder.name} (${moneyWithLabel(payload.decoder.price)})`);
}
if (Array.isArray(payload?.tvAddons) && payload.tvAddons.length) {
lines.push("");
lines.push("Pakiety dodatkowe TV:");
for (const it of payload.tvAddons) {
const termTxt = it.term ? `, ${it.term}` : "";
lines.push(
`- ${it.nazwa} x${it.qty}${termTxt} @ ${moneyWithLabel(it.unit)}`
);
}
}
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); // ✅ gotowy tekst
localStorage.setItem(LS_KEY, JSON.stringify(payload));
} catch {}
}
// ---------
return (
<div class="f-modal-overlay" onClick={onClose}>
<button
class="f-modal-close"
type="button"
aria-label="Zamknij"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
>
</button>
<div class="f-modal-panel f-modal-panel--compact" onClick={(e) => e.stopPropagation()}>
<div class="f-modal-inner">
<h2 class="f-modal-title">{pkg.name} konfiguracja usług</h2>
{/* ✅ PAKIET (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title={pkg.name}
right={<span class="f-modal-phone-price">{money(basePrice)} {cenaOpis}</span>}
open={openSections.base}
onToggle={() => toggleSection("base")}
>
{pkg.features?.length ? (
<ul class="f-card-features">
{pkg.features.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>
</li>
))}
</ul>
) : (
<p class="opacity-80">Brak szczegółów pakietu.</p>
)}
</SectionAccordion>
</div>
{/* ✅ DEKODER (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title="Wybór dekodera"
right={
<span class="f-modal-phone-price">
{decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.decoder}
onToggle={() => toggleSection("decoder")}
>
{decodersList.length === 0 ? (
<p>Brak dostępnych dekoderów.</p>
) : (
<div class="f-radio-list">
{decodersList.map((d) => {
const isSelected = String(selectedDecoderId) === String(d.id);
return (
<label class={`f-radio-item ${isSelected ? "is-selected" : ""}`} key={d.id}>
<div class="f-radio-check">
<input
type="radio"
name="decoder"
checked={isSelected}
onChange={() => setSelectedDecoderId(String(d.id))}
/>
</div>
<div class="f-radio-main">
<div class="f-radio-name">{d.nazwa}</div>
{d.opis && <div class="f-addon-desc">{d.opis}</div>}
</div>
<div class="f-radio-price">
{money(d.cena)} {cenaOpis}
</div>
</label>
);
})}
</div>
)}
</SectionAccordion>
</div>
{/* ✅ TV ADDONS (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title="Pakiety dodatkowe TV"
right={
<span class="f-modal-phone-price">
{tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.tv}
onToggle={() => toggleSection("tv")}
>
{tvAddonsVisible.length === 0 ? (
<p>Brak pakietów dodatkowych TV.</p>
) : (
<div class="f-addon-list">{tvAddonsVisible.map((a) => renderAddonRow(a, true))}</div>
)}
</SectionAccordion>
</div>
{/* ✅ TELEFON (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title="Usługa telefoniczna"
right={
<span class="f-modal-phone-price">
{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.phone}
onToggle={() => toggleSection("phone")}
>
{phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p>
) : (
<div class="f-radio-list">
{/* brak telefonu */}
<label class={`f-radio-item ${selectedPhoneId === null ? "is-selected" : ""}`}>
<div class="f-radio-check">
<input
type="radio"
name="phone-plan"
checked={selectedPhoneId === null}
onChange={() => handlePhoneSelect(null)}
/>
</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-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={() => handlePhoneSelect(p.id)}
/>
</div>
<div class="f-radio-main">
<div class="f-radio-name">{p.name}</div>
</div>
<div class="f-radio-price">
{money(p.price_monthly)} {cenaOpis}
</div>
</label>
{/* ✅ detale ZAWSZE widoczne */}
{p.features?.length > 0 && (
<div class="f-radio-details">
<ul class="f-card-features">
{p.features
.filter(
(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>
</li>
))}
</ul>
</div>
)}
</div>
);
})}
</div>
)}
</SectionAccordion>
</div>
{/* ✅ DODATKI (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title="Dodatkowe usługi"
right={
<span class="f-modal-phone-price">
{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}
</span>
}
open={openSections.addons}
onToggle={() => toggleSection("addons")}
>
{addonsList.length === 0 ? (
<p>Brak usług dodatkowych.</p>
) : (
<div class="f-addon-list">{addonsList.map((a) => renderAddonRow(a, false))}</div>
)}
</SectionAccordion>
</div>
{/* ✅ PODSUMOWANIE (sekcja) */}
<div class="f-modal-section">
<SectionAccordion
title="Podsumowanie miesięczne"
right={<span class="f-modal-phone-price">{money(totalMonthly)} {cenaOpis}</span>}
open={openSections.summary}
onToggle={() => toggleSection("summary")}
>
<div class="f-summary">
<div class="f-summary-list">
<div class="f-summary-row">
<span>Pakiet</span>
<span>{money(basePrice)} {cenaOpis}</span>
</div>
<div class="f-summary-row">
<span>Telefon</span>
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-row">
<span>Dekoder</span>
<span>{decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-row">
<span>Dodatki TV</span>
<span>{tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-row">
<span>Dodatkowe usługi</span>
<span>{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-total">
<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>
{/* ✅ pływająca suma jak w internecie */}
{/* <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>
</div> */}
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<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>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import "../../styles/addons.css";
import OffersSwitches from "../Switches.jsx"; import OffersSwitches from "../Switches.jsx";
import JamboxChannelsModal from "./JamboxChannelsModal.jsx"; import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
import JamboxAddonsModal from "./JamboxAddonsModalCompact.jsx"; import JamboxAddonsModal from "./JamboxAddonsModal.jsx";
import Markdown from "../Markdown.jsx"; import Markdown from "../Markdown.jsx";
function formatMoney(amount, currency = "PLN") { function formatMoney(amount, currency = "PLN") {