import { useEffect, useMemo, useState } from "preact/hooks"; import useDraggableFloating from "../hooks/useDraggableFloating.js"; 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, tid: String(a.tid), })); } 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 floating = useDraggableFloating("fuz_floating_total_pos_tv_v1"); 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 */}
{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 */}
{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 */}
{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 */}
{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"} } open={openSections.addons} onToggle={() => toggleSection("addons")} > {addonsList.length === 0 ? (

Brak usług dodatkowych.

) : (
{addonsList.map((a) => renderAddonRow(a, false))}
)}
{/* PODSUMOWANIE */}
{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
e.stopPropagation()} >
Razem {money(totalMonthly)} {cenaOpis}
); }