Dorabiamy funkcjonalnosci w TV
This commit is contained in:
@@ -141,13 +141,11 @@ function OfferCard({ plan, contractLabel, onConfigureAddons }) {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Umowa – już tylko z przełącznika */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Umowa</span>
|
||||
<span class="f-card-value">{effectiveContract}</span>
|
||||
</li>
|
||||
|
||||
{/* Aktywacja – z pola plan.price_installation */}
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/offers/offers-table.css"; //
|
||||
|
||||
const BUILDING_MAP = {
|
||||
jednorodzinny: 1,
|
||||
wielorodzinny: 2,
|
||||
};
|
||||
|
||||
const CONTRACT_MAP = {
|
||||
"24m": 1,
|
||||
bezterminowa: 2,
|
||||
};
|
||||
|
||||
export default function JamboxBasePackages({
|
||||
source = "PLUS",
|
||||
title,
|
||||
selected = {},
|
||||
}) {
|
||||
const [packages, setPackages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// wyliczamy kody na podstawie tego samego selected, którego używasz w OffersCards
|
||||
const buildingCode = useMemo(() => {
|
||||
const val = selected?.budynek;
|
||||
return BUILDING_MAP[val] ?? 1; // domyślnie jednorodzinny
|
||||
}, [selected?.budynek]);
|
||||
|
||||
const contractCode = useMemo(() => {
|
||||
const val = selected?.umowa;
|
||||
return CONTRACT_MAP[val] ?? 1; // domyślnie 24m
|
||||
}, [selected?.umowa]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (source && source !== "ALL") {
|
||||
params.set("source", source);
|
||||
}
|
||||
if (buildingCode) {
|
||||
params.set("building", String(buildingCode));
|
||||
}
|
||||
if (contractCode) {
|
||||
params.set("contract", String(contractCode));
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const url = `/api/jambox/base-packages${query ? `?${query}` : ""}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!cancelled) {
|
||||
setPackages(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Błąd pobierania pakietów JAMBOX:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować pakietów JAMBOX.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [source, buildingCode, contractCode]);
|
||||
|
||||
const effectiveTitle =
|
||||
title ||
|
||||
(source === "PLUS"
|
||||
? "Pakiety podstawowe JAMBOX PLUS"
|
||||
: source === "EVIO"
|
||||
? "Pakiety podstawowe JAMBOX EVIO"
|
||||
: "Pakiety podstawowe JAMBOX");
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p>Ładowanie pakietów...</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p class="text-red-600">{error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!packages.length) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<h2 class="f-offers-title">{effectiveTitle}</h2>
|
||||
<p>Brak pakietów do wyświetlenia.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
{effectiveTitle && <h2 class="f-offers-title">{effectiveTitle}</h2>}
|
||||
|
||||
<div class={`f-offers-grid f-count-${packages.length}`}>
|
||||
{packages.map((pkg) => (
|
||||
<JamboxPackageCard key={`${pkg.source}-${pkg.tid}`} pkg={pkg} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function JamboxPackageCard({ pkg }) {
|
||||
const updatedDate = pkg.updated_at
|
||||
? new Date(pkg.updated_at).toLocaleDateString("pl-PL")
|
||||
: "-";
|
||||
|
||||
const hasPrice = pkg.price_monthly != null;
|
||||
const hasInstall = pkg.price_installation != null;
|
||||
|
||||
return (
|
||||
<div class="f-card">
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{pkg.name}</div>
|
||||
|
||||
{/* TU zamiast JAMBOX PLUS/EVIO pokazujemy cenę */}
|
||||
<div class="f-card-price">
|
||||
{hasPrice
|
||||
? `${pkg.price_monthly} zł/mies.`
|
||||
: pkg.source === "PLUS"
|
||||
? "JAMBOX PLUS"
|
||||
: "JAMBOX EVIO"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{/* ID / slug jako techniczne info */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">ID pakietu</span>
|
||||
<span class="f-card-value">{pkg.tid}</span>
|
||||
</li>
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Slug</span>
|
||||
<span class="f-card-value">{pkg.slug || "—"}</span>
|
||||
</li>
|
||||
|
||||
{/* nowa linia: cena instalacji z bazy */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
{hasInstall ? `${pkg.price_installation} zł` : "—"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{/* opcjonalnie informacja o źródle pakietu */}
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Platforma</span>
|
||||
<span class="f-card-value">
|
||||
{pkg.source === "PLUS" ? "JAMBOX PLUS" : "JAMBOX EVIO"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Ostatnia aktualizacja</span>
|
||||
<span class="f-card-value">{updatedDate}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useState } from "preact/hooks";
|
||||
|
||||
import OffersSwitches from "./Offers/OffersSwitches.jsx";
|
||||
import OffersCards from "./Offers/OffersCards.jsx"; // <-- WAŻNE!!
|
||||
import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
// import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
|
||||
|
||||
export default function OffersIsland({ data }) {
|
||||
const switches = data.przelaczniki ?? [];
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function RangeForm() {
|
||||
let timeoutStreet = null;
|
||||
|
||||
async function fetchCitySuggestions(q) {
|
||||
const res = await fetch(`/api/cities-autocomplete?q=${encodeURIComponent(q)}`);
|
||||
const res = await fetch(`/api/range/cities-autocomplete?q=${encodeURIComponent(q)}`);
|
||||
setCitySuggest(await res.json());
|
||||
setHighlightIndex(-1);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export default function RangeForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/has-streets?city=${encodeURIComponent(currentCity)}`);
|
||||
const res = await fetch(`/api/range/has-streets?city=${encodeURIComponent(currentCity)}`);
|
||||
const { hasStreets } = await res.json();
|
||||
|
||||
if (!hasStreets) {
|
||||
@@ -126,7 +126,7 @@ export default function RangeForm() {
|
||||
|
||||
async function fetchStreetSuggestions(q, c) {
|
||||
const res = await fetch(
|
||||
`/api/streets-autocomplete?city=${encodeURIComponent(c)}&q=${encodeURIComponent(q)}`
|
||||
`/api/range/streets-autocomplete?city=${encodeURIComponent(c)}&q=${encodeURIComponent(q)}`
|
||||
);
|
||||
setStreetSuggest(await res.json());
|
||||
setStreetHighlightIndex(-1);
|
||||
@@ -207,7 +207,7 @@ export default function RangeForm() {
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch("/api/search", {
|
||||
const res = await fetch("/api/range/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ city, street, number }),
|
||||
|
||||
410
src/islands/jambox/JamboxAddonsModal.jsx
Normal file
410
src/islands/jambox/JamboxAddonsModal.jsx
Normal file
@@ -0,0 +1,410 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function JamboxAddonsModal({ isOpen, onClose, pkg }) {
|
||||
const [phonePlans, setPhonePlans] = useState([]);
|
||||
const [addons, setAddons] = useState([]);
|
||||
|
||||
const [tvAddons, setTvAddons] = useState([]);
|
||||
const [selectedTvAddonTids, setSelectedTvAddonTids] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||||
const [selectedAddonIds, setSelectedAddonIds] = useState([]);
|
||||
|
||||
// akordeony
|
||||
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||||
const [baseOpen, setBaseOpen] = useState(true);
|
||||
|
||||
const formatFeatureValue = (val) => {
|
||||
if (val === true || val === "true") return "✓";
|
||||
if (val === false || val === "false" || val == null) return "✕";
|
||||
return val;
|
||||
};
|
||||
|
||||
const handlePhoneSelect = (id) => {
|
||||
if (id === null) {
|
||||
setSelectedPhoneId(null);
|
||||
setOpenPhoneId(null);
|
||||
return;
|
||||
}
|
||||
setSelectedPhoneId(id);
|
||||
setOpenPhoneId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const toggleAddon = (addonId) => {
|
||||
setSelectedAddonIds((prev) =>
|
||||
prev.includes(addonId)
|
||||
? prev.filter((x) => x !== addonId)
|
||||
: [...prev, addonId]
|
||||
);
|
||||
};
|
||||
|
||||
// reset po otwarciu / zmianie pakietu
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setSelectedPhoneId(null);
|
||||
setSelectedAddonIds([]);
|
||||
setOpenPhoneId(null);
|
||||
setBaseOpen(true);
|
||||
setError("");
|
||||
setSelectedTvAddonTids([]);
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
// load danych
|
||||
useEffect(() => {
|
||||
if (!isOpen || !pkg?.id) 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 JAMBOX (dla pakietu)
|
||||
const addonsRes = await fetch(`/api/jambox/addons?packageId=${pkg.id}`);
|
||||
if (!addonsRes.ok) throw new Error(`HTTP ${addonsRes.status} (addons)`);
|
||||
const addonsJson = await addonsRes.json();
|
||||
const addonsData = Array.isArray(addonsJson.data) ? addonsJson.data : [];
|
||||
|
||||
const tvRes = await fetch(`/api/jambox/tv-addons?packageId=${pkg.id}`);
|
||||
if (!tvRes.ok) throw new Error(`HTTP ${tvRes.status} (tv-addons)`);
|
||||
|
||||
const tvJson = await tvRes.json();
|
||||
const tvData = Array.isArray(tvJson.data) ? tvJson.data : [];
|
||||
|
||||
if (!cancelled) {
|
||||
setPhonePlans(phoneData);
|
||||
setAddons(addonsData);
|
||||
setTvAddons(tvData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd ładowania danych do JamboxAddonsModal:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować danych dodatkowych usług.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
if (!isOpen || !pkg) return null;
|
||||
|
||||
const basePrice = Number(pkg.price_monthly || 0);
|
||||
|
||||
const phonePrice = useMemo(() => {
|
||||
if (!selectedPhoneId) return 0;
|
||||
const p = phonePlans.find((x) => x.id === selectedPhoneId);
|
||||
return Number(p?.price_monthly || 0);
|
||||
}, [selectedPhoneId, phonePlans]);
|
||||
|
||||
// UWAGA: backend może zwrócić { id, price } albo { addon_id, price }
|
||||
const addonsPrice = useMemo(() => {
|
||||
return selectedAddonIds.reduce((sum, addonId) => {
|
||||
const a = addons.find((x) => (x.id ?? x.addon_id) === addonId);
|
||||
return sum + Number(a?.price || 0);
|
||||
}, 0);
|
||||
}, [selectedAddonIds, addons]);
|
||||
|
||||
|
||||
|
||||
const tvAddonsPrice = useMemo(() => {
|
||||
return selectedTvAddonTids.reduce((sum, tid) => {
|
||||
const a = tvAddons.find((x) => Number(x.tid) === Number(tid));
|
||||
return sum + Number(a?.price || 0);
|
||||
}, 0);
|
||||
}, [selectedTvAddonTids, tvAddons]);
|
||||
|
||||
const totalMonthly = basePrice + phonePrice + addonsPrice + tvAddonsPrice;
|
||||
|
||||
const toggleTvAddon = (tid) => {
|
||||
const t = Number(tid);
|
||||
setSelectedTvAddonTids((prev) =>
|
||||
prev.includes(t) ? prev.filter((x) => x !== t) : [...prev, t]
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{/* PAKIET JAMBOX jako akordeon (jak internet w Twoim modalu) */}
|
||||
<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">{pkg.name}</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{baseOpen && pkg.features && pkg.features.length > 0 && (
|
||||
<div class="fuz-accordion-body">
|
||||
<ul class="f-card-features">
|
||||
{pkg.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>
|
||||
|
||||
{loading && <p>Ładowanie danych...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Pakiety dodatkowe TV</h3>
|
||||
|
||||
{tvAddons.length === 0 ? (
|
||||
<p>Brak pakietów dodatkowych TV dla tego pakietu.</p>
|
||||
) : (
|
||||
<div class="fuz-addon-list">
|
||||
{tvAddons.map((a) => {
|
||||
const tid = Number(a.tid);
|
||||
const checked = selectedTvAddonTids.includes(tid);
|
||||
const priceNum = Number(a.price || 0);
|
||||
|
||||
return (
|
||||
<label class="fuz-addon-item" key={`tv-${tid}`}>
|
||||
<div class="fuz-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleTvAddon(tid)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-main">
|
||||
<div class="fuz-addon-name">{a.name}</div>
|
||||
{/* jeśli chcesz pokazać typ/kind */}
|
||||
<div class="fuz-addon-desc">{a.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-price">
|
||||
{Number.isFinite(priceNum) ? `${priceNum.toFixed(2)} zł/mies.` : "—"}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TELEFON (identycznie jak w InternetAddonsModal) */}
|
||||
<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={() => handlePhoneSelect(null)}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={selectedPhoneId === null}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(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>
|
||||
|
||||
{/* PAKIETY TELEFONU */}
|
||||
{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={() => handlePhoneSelect(p.id)}
|
||||
>
|
||||
<span class="fuz-accordion-header-left">
|
||||
<input
|
||||
type="radio"
|
||||
name="phone-plan"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePhoneSelect(p.id);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="fuz-modal-phone-name">{p.name}</span>
|
||||
</span>
|
||||
<span class="fuz-modal-phone-price">
|
||||
{Number(p.price_monthly || 0).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>
|
||||
|
||||
{/* DODATKI JAMBOX (checkbox, cena z jambox_package_addon_options.price) */}
|
||||
<div class="fuz-modal-section">
|
||||
<h3>Dodatkowe usługi</h3>
|
||||
|
||||
{addons.length === 0 ? (
|
||||
<p>Brak usług dodatkowych dla tego pakietu.</p>
|
||||
) : (
|
||||
<div class="fuz-addon-list">
|
||||
{addons.map((a) => {
|
||||
const addonId = a.id ?? a.addon_id; // defensywnie
|
||||
const checked = selectedAddonIds.includes(addonId);
|
||||
|
||||
return (
|
||||
<label class="fuz-addon-item" key={addonId}>
|
||||
<div class="fuz-addon-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleAddon(addonId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-main">
|
||||
<div class="fuz-addon-name">{a.name}</div>
|
||||
{a.description && (
|
||||
<div class="fuz-addon-desc">{a.description}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="fuz-addon-price">
|
||||
{Number(a.price || 0).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>Pakiet</span>
|
||||
<span>
|
||||
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="fuz-summary-row">
|
||||
<span>Pakiety TV</span>
|
||||
<span>
|
||||
{tvAddonsPrice ? `${tvAddonsPrice.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>
|
||||
);
|
||||
}
|
||||
154
src/islands/jambox/JamboxChannelsModal.jsx
Normal file
154
src/islands/jambox/JamboxChannelsModal.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
// src/islands/JamboxChannelsModal.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/modal.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function JamboxChannelsModal({ isOpen, onClose, pkg }) {
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
|
||||
const filtered = !q
|
||||
? channels
|
||||
: channels.filter((ch) =>
|
||||
(ch.name || "").toLowerCase().includes(q)
|
||||
);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !pkg?.id) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadChannels() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setChannels([]);
|
||||
setQuery("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ packageId: String(pkg.id) });
|
||||
const res = await fetch(`/api/jambox/channels?${params.toString()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) {
|
||||
setChannels(Array.isArray(json.data) ? json.data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Błąd pobierania listy kanałów:", err);
|
||||
if (!cancelled) {
|
||||
setError("Nie udało się załadować listy kanałów.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadChannels();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen, pkg?.id]);
|
||||
|
||||
if (!isOpen || !pkg) return null;
|
||||
|
||||
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--channels"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="">
|
||||
<h2 class="fuz-modal-title">Kanały w pakiecie {pkg.name}</h2>
|
||||
|
||||
<div class="jmb-search">
|
||||
<input
|
||||
class="jmb-search-input"
|
||||
type="search"
|
||||
value={query}
|
||||
onInput={(e) => setQuery(e.currentTarget.value)}
|
||||
placeholder="Szukaj kanału po nazwie…"
|
||||
aria-label="Szukaj kanału po nazwie"
|
||||
/>
|
||||
{query && (
|
||||
<button class="jmb-search-clear" type="button" onClick={() => setQuery("")}>
|
||||
Wyczyść
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && !error && (
|
||||
<div class="jmb-search-meta">
|
||||
Wyniki: <strong>{filtered.length}</strong> / {channels.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p>Ładowanie kanałów...</p>}
|
||||
{error && <p class="text-red-600">{error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<>
|
||||
{filtered.length === 0 ? (
|
||||
<p>Brak kanałów spełniających kryteria.</p>
|
||||
) : (
|
||||
<div class="">
|
||||
<div class="f-section-channel">
|
||||
{filtered.map((ch) => (
|
||||
<div class="jmb-channel-card" key={ch.number}>
|
||||
<div class="jmb-channel-inner">
|
||||
{/* FRONT */}
|
||||
<div class="jmb-channel-face jmb-channel-front">
|
||||
{ch.logo_url && (
|
||||
<img
|
||||
src={ch.logo_url}
|
||||
alt={ch.name}
|
||||
class="jmb-channel-logo"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div class="jmb-channel-name">{ch.name}</div>
|
||||
<div class="jmb-channel-number">kanał {ch.number}</div>
|
||||
</div>
|
||||
|
||||
{/* BACK */}
|
||||
<div class="jmb-channel-face jmb-channel-back">
|
||||
<div class="jmb-channel-back-title">{ch.name}</div>
|
||||
<div
|
||||
class="jmb-channel-desc"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: ch.description || "<em>Brak opisu kanału.</em>",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
187
src/islands/jambox/OffersJamboxCards.jsx
Normal file
187
src/islands/jambox/OffersJamboxCards.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
// src/islands/JamboxBasePackages.jsx
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
import JamboxChannelsModal from "./JamboxChannelsModal.jsx";
|
||||
import JamboxAddonsModal from "./JamboxAddonsModal.jsx";
|
||||
|
||||
export default function JamboxBasePackages({ source = "ALL" }) {
|
||||
const [selected, setSelected] = useState({});
|
||||
const [packages, setPackages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [channelsModalOpen, setChannelsModalOpen] = useState(false);
|
||||
const [addonsModalOpen, setAddonsModalOpen] = useState(false);
|
||||
|
||||
const [activePackage, setActivePackage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.fuzSwitchState) {
|
||||
const { selected: sel } = window.fuzSwitchState;
|
||||
if (sel) setSelected(sel);
|
||||
}
|
||||
|
||||
function handler(e) {
|
||||
const detail = e.detail || {};
|
||||
if (detail.selected) setSelected(detail.selected);
|
||||
}
|
||||
|
||||
window.addEventListener("fuz:switch-change", handler);
|
||||
return () => window.removeEventListener("fuz:switch-change", handler);
|
||||
}, []);
|
||||
|
||||
const buildingCode = Number(selected.budynek) || 1;
|
||||
const contractCode = Number(selected.umowa) || 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!buildingCode || !contractCode) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set("source", String(source || "ALL"));
|
||||
params.set("building", String(buildingCode));
|
||||
params.set("contract", String(contractCode));
|
||||
|
||||
const res = await fetch(`/api/jambox/base-packages?${params.toString()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const json = await res.json();
|
||||
if (!cancelled) setPackages(Array.isArray(json.data) ? json.data : []);
|
||||
} catch (err) {
|
||||
console.error("Błąd pobierania pakietów JAMBOX:", err);
|
||||
if (!cancelled) setError("Nie udało się załadować pakietów JAMBOX.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [buildingCode, contractCode, source]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p>Ładowanie pakietów...</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p class="text-red-600">{error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!packages.length) {
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<p>Brak pakietów do wyświetlenia.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section class="f-offers">
|
||||
<div class={`f-offers-grid f-count-${packages.length || 1}`}>
|
||||
{packages.map((pkg) => (
|
||||
<JamboxPackageCard
|
||||
key={pkg.id}
|
||||
pkg={pkg}
|
||||
onShowChannels={() => {
|
||||
setActivePackage(pkg);
|
||||
setChannelsModalOpen(true);
|
||||
}}
|
||||
onConfigureAddons={() => {
|
||||
setActivePackage(pkg);
|
||||
setAddonsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<JamboxChannelsModal
|
||||
isOpen={channelsModalOpen}
|
||||
onClose={() => setChannelsModalOpen(false)}
|
||||
pkg={activePackage}
|
||||
/>
|
||||
|
||||
<JamboxAddonsModal
|
||||
isOpen={addonsModalOpen}
|
||||
onClose={() => setAddonsModalOpen(false)}
|
||||
pkg={activePackage}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function JamboxPackageCard({ pkg, onShowChannels, onConfigureAddons }) {
|
||||
const basePrice = pkg.price_monthly;
|
||||
const installPrice = pkg.price_installation;
|
||||
|
||||
const featureRows = pkg.features || [];
|
||||
const hasPrice = basePrice != null;
|
||||
|
||||
return (
|
||||
<div class="f-card">
|
||||
<div class="f-card-header">
|
||||
<div class="f-card-name">{pkg.name}</div>
|
||||
|
||||
<div class="f-card-price">
|
||||
{hasPrice
|
||||
? `${basePrice} zł/mies.`
|
||||
: pkg.source === "PLUS"
|
||||
? "JAMBOX PLUS"
|
||||
: "JAMBOX EVIO"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="f-card-features">
|
||||
{featureRows.map((f, idx) => {
|
||||
let val = f.value;
|
||||
let display;
|
||||
|
||||
if (val === true || val === "true") display = "✓";
|
||||
else if (val === false || val === "false" || val == null) display = "✕";
|
||||
else display = val;
|
||||
|
||||
return (
|
||||
<li class="f-card-row" key={idx}>
|
||||
<span class="f-card-label">{f.label}</span>
|
||||
<span class="f-card-value">{display}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li class="f-card-row">
|
||||
<span class="f-card-label">Aktywacja</span>
|
||||
<span class="f-card-value">
|
||||
{installPrice != null ? `${installPrice} zł` : "—"}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button type="button" class="btn btn-primary mt-2" onClick={onShowChannels}>
|
||||
Pokaż listę kanałów
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary mt-2"
|
||||
onClick={onConfigureAddons}
|
||||
>
|
||||
Skonfiguruj usługi dodatkowe
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-table.css";
|
||||
import "../../styles/offers/offers-table.css";
|
||||
|
||||
export default function PhoneDbOffersCards({
|
||||
title = "Telefonia stacjonarna FUZ",
|
||||
Reference in New Issue
Block a user