Przebudowa stron na indywidualne karty , pobierane z bazy danych

This commit is contained in:
dm
2025-12-11 15:25:00 +01:00
parent 49c5beb362
commit 0cf7c45131
27 changed files with 1133 additions and 420 deletions

View File

@@ -0,0 +1,193 @@
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}` : "—"}
</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>
);
}

View File

@@ -1,19 +1,141 @@
import { useEffect, useState } from "preact/hooks";
// import "../../styles/offers/offers-switches.css";
export default function OffersSwitches({ switches, selected, onSwitch }) {
if (!switches.length) return null;
function buildLabels(switches, selected) {
const out = {};
for (const sw of switches || []) {
const currentId = selected[sw.id];
const opt = sw.opcje?.find((op) => String(op.id) === String(currentId));
if (opt) out[sw.id] = opt.nazwa;
}
return out;
}
export default function OffersSwitches(props) {
const { switches, selected, onSwitch } = props || {};
const isControlled =
Array.isArray(switches) &&
switches.length > 0 &&
typeof onSwitch === "function";
const [autoSwitches, setAutoSwitches] = useState([]);
const [autoSelected, setAutoSelected] = useState({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// AUTO: pobieramy konfigurację z API
useEffect(() => {
if (isControlled) return;
let cancelled = false;
async function load() {
setLoading(true);
setError("");
try {
const res = await fetch("/api/internet");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const sws = Array.isArray(json.data) ? json.data : [];
if (cancelled) return;
const initial = {};
for (const sw of sws) {
if (sw.domyslny != null) initial[sw.id] = sw.domyslny;
else if (sw.opcje?.length) initial[sw.id] = sw.opcje[0].id;
}
const labels = buildLabels(sws, initial);
setAutoSwitches(sws);
setAutoSelected(initial);
window.dispatchEvent(
new CustomEvent("fuz:switch-change", {
detail: {
id: null,
value: null,
selected: initial,
labels, // tu lecą etykiety z DB
},
}),
);
} catch (err) {
console.error("❌ Błąd pobierania switchy:", err);
if (!cancelled) setError("Nie udało się załadować przełączników.");
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [isControlled]);
const effectiveSwitches = isControlled ? switches : autoSwitches;
const effectiveSelected = isControlled ? selected || {} : autoSelected;
const handleClick = (id, value) => {
if (isControlled) {
onSwitch(id, value);
} else {
setAutoSelected((prev) => {
const next = { ...prev, [id]: value };
const labels = buildLabels(autoSwitches, next);
window.dispatchEvent(
new CustomEvent("fuz:switch-change", {
detail: {
id,
value,
selected: next,
labels, // etykiety po kliknięciu
},
}),
);
return next;
});
}
};
if (!isControlled && loading) {
return (
<div class="f-switches-wrapper">
<p>Ładowanie opcji przełączników...</p>
</div>
);
}
if (!isControlled && error) {
return (
<div class="f-switches-wrapper">
<p class="text-red-600">{error}</p>
</div>
);
}
if (!effectiveSwitches.length) return null;
return (
<div class="f-switches-wrapper">
{switches.map((sw) => (
{effectiveSwitches.map((sw) => (
<div class="f-switch-box">
<div class="f-switch-group">
{sw.opcje.map((op) => (
<button
type="button"
class={`f-switch ${selected[sw.id] === op.id ? "active" : ""
}`}
onClick={() => onSwitch(sw.id, op.id)}
class={`f-switch ${
String(effectiveSelected[sw.id]) === String(op.id)
? "active"
: ""
}`}
onClick={() => handleClick(sw.id, op.id)}
title={sw.title}
>
{op.nazwa}

View File

@@ -0,0 +1,138 @@
import { useEffect, useState } from "preact/hooks";
import "../styles/offers/offers-table.css";
export default function InternetDbOffersCards({
title = "Oferty Internetu FUZ",
}) {
const [selected, setSelected] = useState({});
const [labels, setLabels] = useState({});
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
// nasłuchuj globalnego eventu z OffersSwitches
useEffect(() => {
function handler(e) {
const detail = e.detail || {};
if (detail.selected) {
setSelected(detail.selected);
}
if (detail.labels) {
setLabels(detail.labels);
}
}
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({
building: String(buildingCode),
contract: String(contractCode),
});
const res = await fetch(`/api/internet/plans?${params.toString()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) {
setPlans(Array.isArray(json.data) ? json.data : []);
}
} catch (err) {
console.error("Błąd pobierania planów internetu:", err);
if (!cancelled) setError("Nie udało się załadować ofert.");
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [buildingCode, contractCode]);
const contractLabel = labels.umowa || "";
return (
<section class="f-offers">
{loading && <p>Ładowanie ofert...</p>}
{error && <p class="text-red-600">{error}</p>}
{!loading && !error && (
<div class={`f-offers-grid f-count-${plans.length || 1}`}>
{plans.map((plan) => (
<OfferCard
key={plan.id}
plan={plan}
contractLabel={contractLabel}
/>
))}
</div>
)}
</section>
);
}
function OfferCard({ plan, contractLabel }) {
const basePrice = plan.price_monthly;
const installPrice = plan.price_installation;
const featureRows = (plan.features || []).filter(
(f) => f.id !== "umowa_info" && f.id !== "instalacja"
);
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} /mies.</div>
</div>
<ul class="f-card-features">
{featureRows.map((f) => {
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">
<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">Umowa</span>
<span class="f-card-value">{contractLabel}</span>
</li>
<li class="f-card-row">
<span class="f-card-label">Aktywacja</span>
<span class="f-card-value">
{installPrice != null ? `${installPrice}` : "—"}
</span>
</li>
</ul>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { useState } from "preact/hooks";
import OffersSwitches from "./Offers/OffersSwitches.jsx";
import OffersCards from "./Offers/OffersTable.jsx"; // <-- WAŻNE!!
import OffersCards from "./Offers/OffersCards.jsx"; // <-- WAŻNE!!
import OffersExtraServices from "./Offers/OffersExtraServices.jsx";
export default function OffersIsland({ data }) {

View File

@@ -0,0 +1,77 @@
import { useEffect, useState } from "preact/hooks";
import "../styles/offers/offers-table.css";
export default function PhoneDbOffersCards({
title = "Telefonia stacjonarna FUZ",
}) {
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
async function load() {
setLoading(true);
try {
const res = await fetch("/api/phone/plans");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) {
setPlans(Array.isArray(json.data) ? json.data : []);
}
} catch (err) {
console.error("❌ Błąd pobierania planów telefonii:", err);
if (!cancelled) {
setError("Nie udało się załadować pakietów telefonicznych.");
}
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, []);
return (
<section class="f-offers">
{loading && <p>Ładowanie pakietów telefonicznych...</p>}
{error && <p class="text-red-600">{error}</p>}
{!loading && !error && (
<div class={`f-offers-grid f-count-${plans.length || 1}`}>
{plans.map((plan) => (
<PhoneOfferCard key={plan.id} plan={plan} />
))}
</div>
)}
</section>
);
}
function PhoneOfferCard({ plan }) {
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">{plan.price_monthly} /mies.</div>
</div>
<ul class="f-card-features">
{plan.features.map((f) => (
<li class="f-card-row">
<span class="f-card-label">{f.label}</span>
<span class="f-card-value">{f.value}</span>
</li>
))}
</ul>
</div>
);
}