Przejście w internecie na addons kompaktowe

This commit is contained in:
dm
2025-12-15 12:32:21 +01:00
parent 3b654a8841
commit dc07fa083e
2 changed files with 164 additions and 75 deletions

View File

@@ -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(() => {
const qty = Number(selectedQty[a.id] || 0); return addonsList.reduce((sum, a) => {
return sum + qty * Number(a.cena || 0); const qty = Number(selectedQty[a.id] || 0);
}, 0); return sum + qty * Number(a.cena || 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" <input
class="f-accordion-header" type="radio"
onClick={() => handlePhoneSelect(null)} name="phone-plan"
> checked={selectedPhoneId === null}
<span class="f-accordion-header-left"> onChange={() => handlePhoneSelect(null)}
<input />
type="radio" </div>
name="phone-plan"
checked={selectedPhoneId === null}
onChange={(e) => {
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 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>

View File

@@ -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") {