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