Dorabiamy funkcjonalnosci w TV

This commit is contained in:
dm
2025-12-12 14:58:13 +01:00
parent 0f17daee17
commit ee51e0816d
29 changed files with 1975 additions and 455 deletions

View 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 /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)} /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)} /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)} /mies.</span>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}