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 (