397 lines
15 KiB
JavaScript
397 lines
15 KiB
JavaScript
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 : [];
|
|
|
|
// pakiety TV
|
|
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]);
|
|
|
|
// 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="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">Konfiguracja usług dodatkowych</h2>
|
|
|
|
{/* PAKIET JAMBOX 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">{pkg.name}</span>
|
|
<span class="f-modal-phone-price">
|
|
{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}
|
|
</span>
|
|
</button>
|
|
|
|
{baseOpen && pkg.features && pkg.features.length > 0 && (
|
|
<div class="f-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 && (
|
|
<>
|
|
{/* TV ADDONS */}
|
|
<div class="f-modal-section">
|
|
<h3>Pakiety dodatkowe TV</h3>
|
|
|
|
{tvAddons.length === 0 ? (
|
|
<p>Brak pakietów dodatkowych TV dla tego pakietu.</p>
|
|
) : (
|
|
<div class="f-addon-list">
|
|
{tvAddons.map((a) => {
|
|
const tid = Number(a.tid);
|
|
const checked = selectedTvAddonTids.includes(tid);
|
|
const priceNum = Number(a.price || 0);
|
|
|
|
return (
|
|
<label class="f-addon-item" key={`tv-${tid}`}>
|
|
<div class="f-addon-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={() => toggleTvAddon(tid)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="f-addon-main">
|
|
<div class="f-addon-name">{a.name}</div>
|
|
<div class="f-addon-desc">{a.description}</div>
|
|
</div>
|
|
|
|
<div class="f-addon-price">
|
|
{Number.isFinite(priceNum) ? `${priceNum.toFixed(2)} zł/mies.` : "—"}
|
|
</div>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 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 zł/mies.</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* pakiety telefonu */}
|
|
{phonePlans.map((p) => {
|
|
const isSelected = selectedPhoneId === p.id;
|
|
const isOpen = openPhoneId === 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">
|
|
{Number(p.price_monthly || 0).toFixed(2)} zł/mies.
|
|
</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">{f.value}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* DODATKI JAMBOX */}
|
|
<div class="f-modal-section">
|
|
<h3>Dodatkowe usługi</h3>
|
|
|
|
{addons.length === 0 ? (
|
|
<p>Brak usług dodatkowych dla tego pakietu.</p>
|
|
) : (
|
|
<div class="f-addon-list">
|
|
{addons.map((a) => {
|
|
const addonId = a.id ?? a.addon_id;
|
|
const checked = selectedAddonIds.includes(addonId);
|
|
const priceNum = Number(a.price || 0);
|
|
|
|
return (
|
|
<label class="f-addon-item" key={addonId}>
|
|
<div class="f-addon-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={() => toggleAddon(addonId)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="f-addon-main">
|
|
<div class="f-addon-name">{a.name}</div>
|
|
{a.description && <div class="f-addon-desc">{a.description}</div>}
|
|
</div>
|
|
|
|
<div class="f-addon-price">{priceNum.toFixed(2)} zł/mies.</div>
|
|
</label>
|
|
);
|
|
})}
|
|
</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>Pakiet</span>
|
|
<span>{basePrice ? `${basePrice.toFixed(2)} zł/mies.` : "—"}</span>
|
|
</div>
|
|
|
|
<div class="f-summary-row">
|
|
<span>Pakiety TV</span>
|
|
<span>{tvAddonsPrice ? `${tvAddonsPrice.toFixed(2)} zł/mies.` : "—"}</span>
|
|
</div>
|
|
|
|
<div class="f-summary-row">
|
|
<span>Telefon</span>
|
|
<span>{phonePrice ? `${phonePrice.toFixed(2)} zł/mies.` : "—"}</span>
|
|
</div>
|
|
|
|
<div class="f-summary-row">
|
|
<span>Dodatki</span>
|
|
<span>{addonsPrice ? `${addonsPrice.toFixed(2)} zł/mies.` : "—"}</span>
|
|
</div>
|
|
|
|
<div class="f-summary-total">
|
|
<span>Łącznie</span>
|
|
<span>{totalMonthly.toFixed(2)} zł/mies.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* FLOATING TOTAL (dymek jak czat) */}
|
|
<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">{totalMonthly.toFixed(2)} zł/mies.</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|