Files
fuz-site/src/islands/Internet/InternetAddonsModal.jsx

416 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from "preact/hooks";
import "../../styles/modal.css";
import "../../styles/offers-table.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(".", ",");
}
/**
* Mapuje YAML telefonu (cards.yaml) na format używany w modalu:
* { id, name, price_monthly, features: [{label, value}] }
*/
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,
})),
}));
}
/**
* Dodatki z YAML:
* { id, nazwa, typ, ilosc, min, max, krok, opis, cena }
*/
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 || "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: Number(a.cena ?? 0),
}));
}
export default function InternetAddonsModal({
isOpen,
onClose,
plan,
// ✅ nowe: z YAML
phoneCards = [], // telefon/cards.yaml -> cards[]
addons = [], // internet-swiatlowodowy/addons.yaml -> dodatki[]
cenaOpis = "zł/mies.",
}) {
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
const [error, setError] = useState("");
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
// zamiast selectedAddons (DB) -> mapka ilości
// { public_ip: 1, ip_v4_extra: 3 }
const [selectedQty, setSelectedQty] = useState({});
// akordeony
const [openPhoneId, setOpenPhoneId] = useState(null);
const [baseOpen, setBaseOpen] = useState(true);
// reset wyborów po otwarciu / zmianie planu
useEffect(() => {
if (!isOpen) return;
setError("");
setSelectedPhoneId(null);
setSelectedQty({});
setOpenPhoneId(null);
setBaseOpen(true);
}, [isOpen, plan]);
if (!isOpen || !plan) return null;
const basePrice = Number(plan.price_monthly || 0);
const phonePrice = (() => {
if (!selectedPhoneId) return 0;
const p = phonePlans.find((p) => String(p.id) === String(selectedPhoneId));
return Number(p?.price_monthly || 0);
})();
const addonsPrice = addonsList.reduce((sum, a) => {
const qty = Number(selectedQty[a.id] || 0);
return sum + qty * Number(a.cena || 0);
}, 0);
const totalMonthly = basePrice + phonePrice + 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 }));
};
return (
<div class="f-modal-overlay" onClick={onClose}>
<button
class="f-modal-close"
type="button"
aria-label="Zamknij"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
>
</button>
<div
class="f-modal-panel f-modal-panel--compact"
onClick={(e) => e.stopPropagation()}
>
<div class="f-modal-inner">
<h2 class="f-modal-title">{plan.name} konfiguracja usług</h2>
{/* INTERNET (fiber) jako akordeon */}
<div class="f-modal-section">
<div class={`f-accordion-item ${baseOpen ? "is-open" : ""}`}>
<button
type="button"
class="f-accordion-header"
onClick={() => setBaseOpen((prev) => !prev)}
>
<span class="f-modal-phone-name">{plan.name}</span>
<span class="f-modal-phone-price">
{money(basePrice)} {cenaOpis}
</span>
</button>
{baseOpen && plan.features && plan.features.length > 0 && (
<div class="f-accordion-body">
<ul class="f-card-features">
{plan.features.map((f, idx) => (
<li class="f-card-row" key={idx}>
<span class="f-card-label">{f.label}</span>
<span class="f-card-value">
{formatFeatureValue(f.value)}
</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
{error && <p class="text-red-600">{error}</p>}
{/* Telefon */}
<div class="f-modal-section">
<h3>Usługa telefoniczna</h3>
{phonePlans.length === 0 ? (
<p>Brak dostępnych pakietów telefonicznych.</p>
) : (
<div class="f-modal-phone-list f-accordion">
{/* brak telefonu */}
<div class="f-accordion-item f-accordion-item--no-phone">
<button
type="button"
class="f-accordion-header"
onClick={() => handlePhoneSelect(null)}
>
<span class="f-accordion-header-left">
<input
type="radio"
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>
{phonePlans.map((p) => {
const isSelected = String(selectedPhoneId) === String(p.id);
const isOpen = String(openPhoneId) === String(p.id);
return (
<div
class={`f-accordion-item ${isOpen ? "is-open" : ""}`}
key={p.id}
>
<button
type="button"
class="f-accordion-header"
onClick={() => handlePhoneSelect(p.id)}
>
<span class="f-accordion-header-left">
<input
type="radio"
name="phone-plan"
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handlePhoneSelect(p.id);
}}
onClick={(e) => e.stopPropagation()}
/>
<span class="f-modal-phone-name">{p.name}</span>
</span>
<span class="f-modal-phone-price">
{money(p.price_monthly)} {cenaOpis}
</span>
</button>
{isOpen && (
<div class="f-accordion-body">
{p.features && p.features.length > 0 && (
<ul class="f-card-features">
{p.features
.filter(
(f) =>
!String(f.label || "")
.toLowerCase()
.includes("aktyw"),
)
.map((f, idx) => (
<li class="f-card-row" key={idx}>
<span class="f-card-label">{f.label}</span>
<span class="f-card-value">
{formatFeatureValue(f.value)}
</span>
</li>
))}
</ul>
)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
{/* Dodatki internetowe */}
<div class="f-modal-section">
<h3>Dodatkowe usługi</h3>
{addonsList.length === 0 ? (
<p>Brak dodatkowych usług.</p>
) : (
<div class="f-addon-list">
{addonsList.map((a) => {
const qty = Number(selectedQty[a.id] || 0);
const isQty = a.typ === "quantity" || a.ilosc === true;
if (!isQty) {
const checked = qty > 0;
return (
<label class="f-addon-item" key={a.id}>
<div class="f-addon-checkbox">
<input
type="checkbox"
checked={checked}
onChange={() => toggleCheckboxAddon(a.id)}
/>
</div>
<div class="f-addon-main">
<div class="f-addon-name">{a.nazwa}</div>
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
</div>
<div class="f-addon-price">
{money(a.cena)} {cenaOpis}
</div>
</label>
);
}
// quantity
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 * Number(a.cena || 0);
return (
<div class="f-addon-item f-addon-item--qty" key={a.id}>
{/* slot na checkbox (dla wyrównania kolumn) */}
<div class="f-addon-checkbox" aria-hidden="true"></div>
<div class="f-addon-main">
<div class="f-addon-name">{a.nazwa}</div>
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
</div>
{/* licznik ilości bliżej prawej */}
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
<button
type="button"
class="btn btn-outline"
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
disabled={qty <= min}
>
</button>
<span class="f-addon-qty-value">{qty}</span>
<button
type="button"
class="btn btn-outline"
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
disabled={qty >= max}
>
+
</button>
</div>
{/* cena po prawej + suma pod spodem */}
<div class="f-addon-price">
<div>
{money(a.cena)} {cenaOpis}
</div>
<div class="f-addon-price-total">
{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Podsumowanie */}
<div class="f-modal-section f-summary">
<h3>Podsumowanie miesięczne</h3>
<div class="f-summary-list">
<div class="f-summary-row">
<span>Internet</span>
<span>{money(basePrice)} {cenaOpis}</span>
</div>
<div class="f-summary-row">
<span>Telefon</span>
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-row">
<span>Dodatki</span>
<span>{addonsPrice ? `${money(addonsPrice)} ${cenaOpis}` : "—"}</span>
</div>
<div class="f-summary-total">
<span>Łącznie</span>
<span>{money(totalMonthly)} {cenaOpis}</span>
</div>
</div>
</div>
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<div class="f-floating-total-inner">
<span class="f-floating-total-label">Suma</span>
<span class="f-floating-total-value">
{money(totalMonthly)} {cenaOpis}
</span>
</div>
</div>
</div>
</div>
</div>
);
}