Przebudowa strony
This commit is contained in:
365
src/islands/Internet/InternetAddonsModal.jsx
Normal file
365
src/islands/Internet/InternetAddonsModal.jsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function InternetAddonsModal({ isOpen, onClose, plan }) {
|
||||
const [phonePlans, setPhonePlans] = useState([]);
|
||||
const [addons, setAddons] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||
const [selectedAddons, setSelectedAddons] = useState([]);
|
||||
|
||||
// który pakiet telefoniczny jest rozwinięty
|
||||
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||||
|
||||
// czy akordeon internetu (fiber) jest rozwinięty
|
||||
const [baseOpen, setBaseOpen] = useState(true);
|
||||
|
||||
// reset wyborów po otwarciu nowego planu
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setSelectedPhoneId(null);
|
||||
setSelectedAddons([]);
|
||||
setOpenPhoneId(null);
|
||||
setBaseOpen(true);
|
||||
}, [isOpen, plan]);
|
||||
|
||||
// ładowanie danych
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
// telefon
|
||||
const phoneRes = await fetch("/api/phone/plans");
|
||||
if (!phoneRes.ok) throw new Error(`HTTP ${phoneRes.status} (phone)`);
|
||||
|
||||
const phoneJson = await phoneRes.json();
|
||||
const phoneData = Array.isArray(phoneJson.data) ? phoneJson.data : [];
|
||||
|
||||
// dodatki
|
||||
const addonsRes = await fetch("/api/internet/addons");
|
||||
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`);
|
||||
|
||||
const addonsJson = await addonsRes.json();
|
||||
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : [];
|
||||
|
||||
if (!cancelled) {
|
||||
setPhonePlans(phoneData);
|
||||
setAddons(addonsData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd ładowania danych do InternetAddonsModal:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować danych dodatkowych usług.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen || !plan) return null;
|
||||
|
||||
const basePrice = plan.price_monthly || 0;
|
||||
|
||||
const phonePrice = (() => {
|
||||
if (!selectedPhoneId) return 0;
|
||||
const p = phonePlans.find((p) => p.id === selectedPhoneId);
|
||||
return p?.price_monthly || 0;
|
||||
})();
|
||||
|
||||
const addonsPrice = selectedAddons.reduce((sum, sel) => {
|
||||
const addon = addons.find((a) => a.id === sel.addonId);
|
||||
if (!addon) return sum;
|
||||
const opt = addon.options.find((o) => o.id === sel.optionId);
|
||||
if (!opt) return sum;
|
||||
return sum + (opt.price || 0);
|
||||
}, 0);
|
||||
|
||||
const totalMonthly = basePrice + phonePrice + addonsPrice;
|
||||
|
||||
const handleAddonToggle = (addonId, optionId) => {
|
||||
setSelectedAddons((prev) => {
|
||||
const exists = prev.some(
|
||||
(x) => x.addonId === addonId && x.optionId === optionId
|
||||
);
|
||||
if (exists) {
|
||||
return prev.filter(
|
||||
(x) => !(x.addonId === addonId && x.optionId === optionId)
|
||||
);
|
||||
} else {
|
||||
return [...prev, { addonId, optionId }];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const togglePhoneOpen = (id) => {
|
||||
setOpenPhoneId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="fuz-modal-overlay" onClick={onClose}>
|
||||
<button
|
||||
class="fuz-modal-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="fuz-modal-panel fuz-modal-panel--compact"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="fuz-modal-inner">
|
||||
<h2 class="fuz-modal-title">Konfiguracja usług dodatkowych</h2>
|
||||
|
||||
{/* INTERNET (fiber) jako akordeon */}
|
||||
<div class="fuz-modal-section">
|
||||
<div class={`fuz-accordion-item ${baseOpen ? "is-open" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => setBaseOpen((prev) => !prev)}
|
||||
>
|
||||
<span class="fuz-modal-phone-name">{plan.name}</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{basePrice.toFixed(2)} zł/mies.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{baseOpen && plan.features && plan.features.length > 0 && (
|
||||
<div class="fuz-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">{f.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p>Ładowanie danych...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
{/* Sekcja: wybór telefonu (akordeon + opcja bez telefonu) */}
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Usługa telefoniczna</h3>
|
||||
{phonePlans.length === 0 ? (
|
||||
<p>Brak dostępnych pakietów telefonicznych.</p>
|
||||
) : (
|
||||
<div class="fuz-modal-phone-list fuz-accordion">
|
||||
{/* OPCJA: brak telefonu */}
|
||||
<div class="fuz-accordion-item fuz-accordion-item--no-phone">
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => {
|
||||
setSelectedPhoneId(null);
|
||||
setOpenPhoneId(null);
|
||||
}}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={selectedPhoneId === null}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPhoneId(null);
|
||||
setOpenPhoneId(null);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="fuz-modal-phone-name">
|
||||
Nie potrzebuję telefonu
|
||||
</span>
|
||||
</span>
|
||||
<span class="fuz-modal-phone-price">0,00 zł/mies.</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* LISTA PAKIETÓW TELEFONICZNYCH */}
|
||||
{phonePlans.map((p) => {
|
||||
const isSelected = selectedPhoneId === p.id;
|
||||
const isOpen = openPhoneId === p.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`fuz-accordion-item ${
|
||||
isOpen ? "is-open" : ""
|
||||
}`}
|
||||
key={p.id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="fuz-accordion-header"
|
||||
onClick={() => togglePhoneOpen(p.id)}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedPhoneId(p.id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="fuz-modal-phone-name">
|
||||
{p.name}
|
||||
</span>
|
||||
</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{p.price_monthly.toFixed(2)} zł/mies.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div class="fuz-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">
|
||||
{f.value}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sekcja: dodatki internetowe */}
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Dodatkowe usługi</h3>
|
||||
{addons.length === 0 ? (
|
||||
<p>Brak dodatkowych usług.</p>
|
||||
) : (
|
||||
<div class="fuz-addon-list">
|
||||
{addons.map((addon) =>
|
||||
addon.options.map((opt) => {
|
||||
const checked = selectedAddons.some(
|
||||
(x) =>
|
||||
x.addonId === addon.id &&
|
||||
x.optionId === opt.id
|
||||
);
|
||||
|
||||
return (
|
||||
<label
|
||||
class="fuz-addon-item"
|
||||
key={`${addon.id}-${opt.id}`}
|
||||
>
|
||||
<div class="fuz-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
handleAddonToggle(addon.id, opt.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-main">
|
||||
<div class="fuz-addon-name">{addon.name}</div>
|
||||
{addon.description && (
|
||||
<div class="fuz-addon-desc">
|
||||
{addon.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-price">
|
||||
{opt.price.toFixed(2)} zł/mies.
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Podsumowanie */}
|
||||
<div class="fuz-modal-section fuz-summary">
|
||||
<h3>Podsumowanie miesięczne</h3>
|
||||
|
||||
<div class="fuz-summary-list">
|
||||
<div class="fuz-summary-row">
|
||||
<span>Internet</span>
|
||||
<span>{basePrice.toFixed(2)} zł/mies.</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Telefon</span>
|
||||
<span>
|
||||
{phonePrice
|
||||
? `${phonePrice.toFixed(2)} zł/mies.`
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Dodatki</span>
|
||||
<span>
|
||||
{addonsPrice
|
||||
? `${addonsPrice.toFixed(2)} zł/mies.`
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-total">
|
||||
<span>Łącznie</span>
|
||||
<span>{totalMonthly.toFixed(2)} zł/mies.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-table.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
import InternetAddonsModal from "./InternetAddonsModal.jsx"; // 🔹 dostosuj ścieżkę, jeśli inna
|
||||
|
||||
export default function InternetDbOffersCards({
|
||||
title = "Oferty Internetu FUZ",
|
||||
}) {
|
||||
const [selected, setSelected] = useState({});
|
||||
const [labels, setLabels] = useState({});
|
||||
const [labels, setLabels] = useState({});
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// 🔹 stan modala z dodatkami
|
||||
const [addonsModalOpen, setAddonsModalOpen] = useState(false);
|
||||
const [activePlan, setActivePlan] = useState(null);
|
||||
|
||||
// nasłuchuj globalnego eventu z OffersSwitches
|
||||
useEffect(() => {
|
||||
function handler(e) {
|
||||
@@ -18,7 +23,7 @@ export default function InternetDbOffersCards({
|
||||
setSelected(detail.selected);
|
||||
}
|
||||
if (detail.labels) {
|
||||
setLabels(detail.labels);
|
||||
setLabels(detail.labels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +70,11 @@ export default function InternetDbOffersCards({
|
||||
};
|
||||
}, [buildingCode, contractCode]);
|
||||
|
||||
const contractLabel = labels.umowa || "";
|
||||
const contractLabel = labels.umowa || "";
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
|
||||
{loading && <p>Ładowanie ofert...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
@@ -79,26 +85,45 @@ export default function InternetDbOffersCards({
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
contractLabel={contractLabel}
|
||||
onConfigureAddons={() => {
|
||||
setActivePlan(plan);
|
||||
setAddonsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🔹 Modal z usługami dodatkowymi (internet + telefon + addon’y) */}
|
||||
<InternetAddonsModal
|
||||
isOpen={addonsModalOpen}
|
||||
onClose={() => setAddonsModalOpen(false)}
|
||||
plan={activePlan}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OfferCard({ plan, contractLabel }) {
|
||||
function OfferCard({ plan, contractLabel, onConfigureAddons }) {
|
||||
const basePrice = plan.price_monthly;
|
||||
const installPrice = plan.price_installation;
|
||||
|
||||
const featureRows = (plan.features || []).filter(
|
||||
const allFeatures = plan.features || [];
|
||||
|
||||
// 🔹 to są inne cechy (bez umowy i instalacji)
|
||||
const featureRows = allFeatures.filter(
|
||||
(f) => f.id !== "umowa_info" && f.id !== "instalacja"
|
||||
);
|
||||
|
||||
// 🔹 cecha opisująca umowę (z backendu)
|
||||
const contractFeature = allFeatures.find((f) => f.id === "umowa_info");
|
||||
|
||||
// 🔹 tekst, który faktycznie pokażemy w wierszu "Umowa"
|
||||
const effectiveContract =
|
||||
contractLabel || contractFeature?.value || contractFeature?.label || "—";
|
||||
|
||||
return (
|
||||
<div class={`f-card ${plan.popular ? "f-card-popular" : ""}`}>
|
||||
{/* {plan.popular && <div class="f-card-badge">Najczęściej wybierany</div>} */}
|
||||
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{plan.name}</div>
|
||||
<div class="f-card-price">{basePrice} zł/mies.</div>
|
||||
@@ -123,7 +148,7 @@ function OfferCard({ plan, contractLabel }) {
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Umowa</span>
|
||||
<span class="f-card-value">{contractLabel}</span>
|
||||
<span class="f-card-value">{effectiveContract}</span>
|
||||
</li>
|
||||
|
||||
<li class="f-card-row">
|
||||
@@ -133,6 +158,14 @@ function OfferCard({ plan, contractLabel }) {
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary mt-4"
|
||||
onClick={onConfigureAddons}
|
||||
>
|
||||
Skonfiguruj usługi dodatkowe
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user