diff --git a/src/islands/jambox/JamboxAddonsModal.jsx b/src/islands/jambox/JamboxAddonsModal.jsx index 217c444..2581108 100644 --- a/src/islands/jambox/JamboxAddonsModal.jsx +++ b/src/islands/jambox/JamboxAddonsModal.jsx @@ -37,6 +37,7 @@ function normalizeDecoders(list) { .map((d) => ({ id: String(d.id), nazwa: String(d.nazwa), + opis: d.opis ? String(d.opis) : "", cena: Number(d.cena ?? 0), })); } @@ -55,17 +56,12 @@ function normalizeAddons(addons) { max: a.max != null ? Number(a.max) : 10, krok: a.krok != null ? Number(a.krok) : 1, opis: a.opis ? String(a.opis) : "", - // addons.yaml -> number albo {default, by_name} - // tv-addons.yaml -> [{pakiety, 12m, bezterminowo}] cena: a.cena ?? 0, })); } function normKey(s) { - return String(s || "") - .trim() - .toLowerCase() - .replace(/\s+/g, " "); + return String(s || "").trim().toLowerCase().replace(/\s+/g, " "); } /** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */ @@ -75,14 +71,12 @@ function pickTvVariant(addon, pkgName) { const wanted = normKey(pkgName); - // 1) po nazwie pakietu for (const row of c) { const pk = row?.pakiety; if (!Array.isArray(pk)) continue; if (pk.some((p) => normKey(p) === wanted)) return row; } - // 2) fallback "*" for (const row of c) { const pk = row?.pakiety; if (!Array.isArray(pk)) continue; @@ -92,27 +86,22 @@ function pickTvVariant(addon, pkgName) { return null; } -/** TV: czy addon w ogóle dostępny dla pakietu */ function isTvAddonAvailableForPkg(addon, pkg) { if (!pkg) return false; const v = pickTvVariant(addon, String(pkg?.name ?? "")); return !!v; } -/** TV: czy ma dwie ceny (12m/bezterminowo) */ function hasTvTermPricing(addon, pkg) { const c = addon?.cena; if (!Array.isArray(c)) return false; - // sprawdzamy wariant dla konkretnego pakietu (bo może się różnić) const v = pickTvVariant(addon, String(pkg?.name ?? "")); if (!v || typeof v !== "object") return false; - // ✅ radio tylko jeśli są OBIE ceny return v["12m"] != null && v.bezterminowo != null; } - /** * ✅ cena jednostkowa: * - addons.yaml: number / string / legacy {default, by_name} @@ -121,11 +110,9 @@ function hasTvTermPricing(addon, pkg) { function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) { const c = addon?.cena; - // addons.yaml: liczba / liczba jako string if (typeof c === "number") return 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)) { const v = pickTvVariant(addon, String(pkg?.name ?? "")); if (!v) return 0; @@ -133,14 +120,11 @@ function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) { const t = term || "12m"; if (v[t] != null) return Number(v[t]) || 0; - // fallback if (v.bezterminowo != null) return Number(v.bezterminowo) || 0; if (v["12m"] != null) return Number(v["12m"]) || 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") { const name = String(pkg?.name ?? ""); const wanted = normKey(name); @@ -158,12 +142,32 @@ function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) { return 0; } +/** ✅ Sekcja-akordeon (jak w internet modal) */ +function SectionAccordion({ title, right, open, onToggle, children }) { + return ( +
+ + + {open &&
{children}
} +
+ ); +} + export default function JamboxAddonsModal({ isOpen, onClose, pkg, - // ✅ YAML phoneCards = [], tvAddons = [], addons = [], @@ -178,7 +182,6 @@ export default function JamboxAddonsModal({ const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]); - // ✅ TV: pokazujemy tylko dostępne dla pkg.name const tvAddonsVisible = useMemo(() => { if (!pkg) return []; return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg)); @@ -188,27 +191,55 @@ export default function JamboxAddonsModal({ const [selectedPhoneId, setSelectedPhoneId] = useState(null); const [openPhoneId, setOpenPhoneId] = useState(null); - // dekoder (radio) const [selectedDecoderId, setSelectedDecoderId] = useState(null); - // checkbox/quantity: { [id]: qty } const [selectedQty, setSelectedQty] = useState({}); - // ✅ TV: term per dodatek (12m / bezterminowo) const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" } - // akordeon pakietu bazowego - const [baseOpen, setBaseOpen] = useState(true); + // ✅ 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({}); - setBaseOpen(true); + + 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)) || @@ -216,6 +247,7 @@ export default function JamboxAddonsModal({ setSelectedDecoderId(d0 ? String(d0.id) : null); }, [isOpen, pkg?.id, decodersList]); + if (!isOpen || !pkg) return null; const basePrice = Number(pkg.price_monthly || 0); @@ -232,7 +264,6 @@ export default function JamboxAddonsModal({ return Number(d?.cena || 0); }, [selectedDecoderId, decodersList]); - // ✅ TV: suma liczona tylko po widocznych (czyli dostępnych) const tvAddonsPrice = useMemo(() => { return tvAddonsVisible.reduce((sum, a) => { const qty = Number(selectedQty[a.id] || 0); @@ -246,7 +277,6 @@ export default function JamboxAddonsModal({ }, 0); }, [selectedQty, tvAddonsVisible, tvTerm, pkg]); - // zwykłe dodatki (addons.yaml) – stara logika (multiroom itp.) const addonsOnlyPrice = useMemo(() => { return addonsList.reduce((sum, a) => { const qty = Number(selectedQty[a.id] || 0); @@ -285,10 +315,9 @@ export default function JamboxAddonsModal({ const qty = Number(selectedQty[a.id] || 0); const isQty = a.typ === "quantity" || a.ilosc === true; - // TV: term i cena -const termPricing = isTv && hasTvTermPricing(a, pkg); -const term = tvTerm[a.id] || "12m"; -const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null); + const termPricing = isTv && hasTvTermPricing(a, pkg); + const term = tvTerm[a.id] || "12m"; + const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null); if (!isQty) { return ( @@ -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 (
- - {baseOpen && pkg.features && pkg.features.length > 0 && ( -
- -
+ {money(basePrice)} {cenaOpis}} + open={openSections.base} + onToggle={() => toggleSection("base")} + > + {pkg.features?.length ? ( + + ) : ( +

Brak szczegółów pakietu.

)} -
+ - {/* ✅ DEKODER (radio) — NAD TV ADDONS */} + {/* ✅ DEKODER (sekcja) */}
-

Wybór dekodera

+ + {decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"} + + } + open={openSections.decoder} + onToggle={() => toggleSection("decoder")} + > + +{decodersList.length === 0 ? ( +

Brak dostępnych dekoderów.

+) : ( +
+ {decodersList.map((d) => { + const isSelected = String(selectedDecoderId) === String(d.id); - {decodersList.length === 0 ? ( -

Brak dostępnych dekoderów.

- ) : ( -
- {decodersList.map((d) => { - const isSelected = String(selectedDecoderId) === String(d.id); - - return ( -
- -
- ); - })} -
- )} + return ( +
diff --git a/src/islands/jambox/JamboxAddonsModalCompact.jsx b/src/islands/jambox/JamboxAddonsModalCompact.jsx deleted file mode 100644 index 2581108..0000000 --- a/src/islands/jambox/JamboxAddonsModalCompact.jsx +++ /dev/null @@ -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 ( -
- - - {open &&
{children}
} -
- ); -} - -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 ( - - ); - } - - 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 ( -
- - -
-
{a.nazwa}
- {a.opis &&
{a.opis}
} -
- -
e.stopPropagation()}> - - - {qty} - - -
- -
-
- {money(unit)} {cenaOpis} -
-
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
-
-
- ); - }; - - // --------- - 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 ( -
- - -
e.stopPropagation()}> -
-

{pkg.name} — konfiguracja usług

- - {/* ✅ PAKIET (sekcja) */} -
- {money(basePrice)} {cenaOpis}} - open={openSections.base} - onToggle={() => toggleSection("base")} - > - {pkg.features?.length ? ( -
    - {pkg.features.map((f, idx) => ( -
  • - {f.label} - {formatFeatureValue(f.value)} -
  • - ))} -
- ) : ( -

Brak szczegółów pakietu.

- )} -
-
- - {/* ✅ DEKODER (sekcja) */} -
- - {decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"} - - } - open={openSections.decoder} - onToggle={() => toggleSection("decoder")} - > - -{decodersList.length === 0 ? ( -

Brak dostępnych dekoderów.

-) : ( -
- {decodersList.map((d) => { - const isSelected = String(selectedDecoderId) === String(d.id); - - return ( - - ); - })} -
-)} - - - -
-
- - {/* ✅ TV ADDONS (sekcja) */} -
- - {tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"} - - } - open={openSections.tv} - onToggle={() => toggleSection("tv")} - > - {tvAddonsVisible.length === 0 ? ( -

Brak pakietów dodatkowych TV.

- ) : ( -
{tvAddonsVisible.map((a) => renderAddonRow(a, true))}
- )} -
-
- - {/* ✅ TELEFON (sekcja) */} -
- - {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"} - - } - open={openSections.phone} - onToggle={() => toggleSection("phone")} - > - {phonePlans.length === 0 ? ( -

Brak dostępnych pakietów telefonicznych.

-) : ( -
- {/* brak telefonu */} - - - {/* pakiety */} - {phonePlans.map((p) => { - const isSelected = String(selectedPhoneId) === String(p.id); - - return ( -
- - - {/* ✅ detale ZAWSZE widoczne */} - {p.features?.length > 0 && ( -
-
    - {p.features - .filter( - (f) => - !String(f.label || "").toLowerCase().includes("aktyw"), - ) - .map((f, idx) => ( -
  • - {f.label} - {formatFeatureValue(f.value)} -
  • - ))} -
-
- )} -
- ); - })} -
- -)} - -
-
- - {/* ✅ DODATKI (sekcja) */} -
- - {addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"} - - } - open={openSections.addons} - onToggle={() => toggleSection("addons")} - > - {addonsList.length === 0 ? ( -

Brak usług dodatkowych.

- ) : ( -
{addonsList.map((a) => renderAddonRow(a, false))}
- )} -
-
- - {/* ✅ PODSUMOWANIE (sekcja) */} -
- {money(totalMonthly)} {cenaOpis}} - open={openSections.summary} - onToggle={() => toggleSection("summary")} - > -
-
-
- Pakiet - {money(basePrice)} {cenaOpis} -
- -
- Telefon - {phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"} -
- -
- Dekoder - {decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"} -
- -
- Dodatki TV - {tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"} -
- -
- Dodatkowe usługi - {addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"} -
- -
- Łącznie - {money(totalMonthly)} {cenaOpis} - -
- - saveOfferToLocalStorage()} -> - Wyślij zapytanie z tym wyborem - - -
-
-
-
- - {/* ✅ pływająca suma jak w internecie */} - {/*
e.stopPropagation()}> -
- Suma - - {money(totalMonthly)} {cenaOpis} - -
-
*/} -
e.stopPropagation()}> -
- - Razem - - - {money(totalMonthly)} - - - {cenaOpis} - -
-
- -
-
-
- ); -} diff --git a/src/islands/jambox/JamboxCards.jsx b/src/islands/jambox/JamboxCards.jsx index efdcc97..844c1f4 100644 --- a/src/islands/jambox/JamboxCards.jsx +++ b/src/islands/jambox/JamboxCards.jsx @@ -3,7 +3,7 @@ import "../../styles/addons.css"; import OffersSwitches from "../Switches.jsx"; import JamboxChannelsModal from "./JamboxChannelsModal.jsx"; -import JamboxAddonsModal from "./JamboxAddonsModalCompact.jsx"; +import JamboxAddonsModal from "./JamboxAddonsModal.jsx"; import Markdown from "../Markdown.jsx"; function formatMoney(amount, currency = "PLN") {