638 lines
21 KiB
JavaScript
638 lines
21 KiB
JavaScript
import { useEffect, useMemo, useState } from "preact/hooks";
|
||
import "../../styles/modal.css";
|
||
import "../../styles/addons.css";
|
||
|
||
function formatFeatureValue(val) {
|
||
if (val === true || val === "true") return "✓";
|
||
if (val === false || val === "false" || val == null) return "✕";
|
||
return val;
|
||
}
|
||
|
||
function money(amount) {
|
||
const n = Number(amount || 0);
|
||
return n.toFixed(2).replace(".", ",");
|
||
}
|
||
|
||
/** telefon z YAML (phone/cards.yaml -> cards[]) => { id, name, price_monthly, features[] } */
|
||
function mapPhoneYamlToPlans(phoneCards) {
|
||
const list = Array.isArray(phoneCards) ? phoneCards : [];
|
||
return list
|
||
.filter((c) => c?.widoczny !== false)
|
||
.map((c, idx) => ({
|
||
id: String(c?.id ?? c?.nazwa ?? idx),
|
||
name: c?.nazwa ?? "—",
|
||
price_monthly: Number(c?.cena?.wartosc ?? 0),
|
||
features: (Array.isArray(c?.parametry) ? c.parametry : []).map((p) => ({
|
||
label: p.label,
|
||
value: p.value,
|
||
})),
|
||
}));
|
||
}
|
||
|
||
/** dekodery z YAML */
|
||
function normalizeDecoders(list) {
|
||
const arr = Array.isArray(list) ? list : [];
|
||
return arr
|
||
.filter((d) => d?.id && d?.nazwa)
|
||
.map((d) => ({
|
||
id: String(d.id),
|
||
nazwa: String(d.nazwa),
|
||
cena: Number(d.cena ?? 0),
|
||
}));
|
||
}
|
||
|
||
/** dodatki z YAML (tv-addons.yaml / addons.yaml) */
|
||
function normalizeAddons(addons) {
|
||
const list = Array.isArray(addons) ? addons : [];
|
||
return list
|
||
.filter((a) => a?.id && a?.nazwa)
|
||
.map((a) => ({
|
||
id: String(a.id),
|
||
nazwa: String(a.nazwa),
|
||
typ: String(a.typ ?? a.type ?? "checkbox"),
|
||
ilosc: !!a.ilosc,
|
||
min: a.min != null ? Number(a.min) : 0,
|
||
max: a.max != null ? Number(a.max) : 10,
|
||
krok: a.krok != null ? Number(a.krok) : 1,
|
||
opis: a.opis ? String(a.opis) : "",
|
||
// addons.yaml -> number albo {default, by_name}
|
||
// tv-addons.yaml -> [{pakiety, 12m, bezterminowo}]
|
||
cena: a.cena ?? 0,
|
||
}));
|
||
}
|
||
|
||
function normKey(s) {
|
||
return String(s || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/\s+/g, " ");
|
||
}
|
||
|
||
/** TV: wybór wariantu ceny po pkg.name, albo fallback "*" */
|
||
function pickTvVariant(addon, pkgName) {
|
||
const c = addon?.cena;
|
||
if (!Array.isArray(c)) return null;
|
||
|
||
const wanted = normKey(pkgName);
|
||
|
||
// 1) po nazwie pakietu
|
||
for (const row of c) {
|
||
const pk = row?.pakiety;
|
||
if (!Array.isArray(pk)) continue;
|
||
if (pk.some((p) => normKey(p) === wanted)) return row;
|
||
}
|
||
|
||
// 2) fallback "*"
|
||
for (const row of c) {
|
||
const pk = row?.pakiety;
|
||
if (!Array.isArray(pk)) continue;
|
||
if (pk.some((p) => String(p).trim() === "*")) return row;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/** TV: czy addon w ogóle dostępny dla pakietu */
|
||
function isTvAddonAvailableForPkg(addon, pkg) {
|
||
if (!pkg) return false;
|
||
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
|
||
return !!v;
|
||
}
|
||
|
||
/** TV: czy ma dwie ceny (12m/bezterminowo) */
|
||
function hasTvTermPricing(addon, pkg) {
|
||
const c = addon?.cena;
|
||
if (!Array.isArray(c)) return false;
|
||
|
||
// sprawdzamy wariant dla konkretnego pakietu (bo może się różnić)
|
||
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
|
||
if (!v || typeof v !== "object") return false;
|
||
|
||
// ✅ radio tylko jeśli są OBIE ceny
|
||
return v["12m"] != null && v.bezterminowo != null;
|
||
}
|
||
|
||
|
||
/**
|
||
* ✅ cena jednostkowa:
|
||
* - addons.yaml: number / string / legacy {default, by_name}
|
||
* - tv-addons.yaml: tablica wariantów
|
||
*/
|
||
function getAddonUnitPrice(addon, pkg, term /* "12m"|"bezterminowo"|null */) {
|
||
const c = addon?.cena;
|
||
|
||
// addons.yaml: liczba / liczba jako string
|
||
if (typeof c === "number") return c;
|
||
if (typeof c === "string" && c.trim() !== "" && !Number.isNaN(Number(c))) return Number(c);
|
||
|
||
// tv-addons.yaml: tablica wariantów [{pakiety, 12m, bezterminowo}]
|
||
if (Array.isArray(c)) {
|
||
const v = pickTvVariant(addon, String(pkg?.name ?? ""));
|
||
if (!v) return 0;
|
||
|
||
const t = term || "12m";
|
||
if (v[t] != null) return Number(v[t]) || 0;
|
||
|
||
// fallback
|
||
if (v.bezterminowo != null) return Number(v.bezterminowo) || 0;
|
||
if (v["12m"] != null) return Number(v["12m"]) || 0;
|
||
return 0;
|
||
}
|
||
|
||
// ✅ LEGACY: addons.yaml może mieć cenę zależną od pakietu:
|
||
// cena: { default: 19.9, by_name: { "Smart": 15.0, ... } }
|
||
if (c && typeof c === "object") {
|
||
const name = String(pkg?.name ?? "");
|
||
const wanted = normKey(name);
|
||
|
||
const byName = c.by_name || c.byName || c.by_nazwa || c.byNazwa;
|
||
if (byName && typeof byName === "object" && name) {
|
||
for (const k of Object.keys(byName)) {
|
||
if (normKey(k) === wanted) return Number(byName[k]) || 0;
|
||
}
|
||
}
|
||
|
||
if (c.default != null) return Number(c.default) || 0;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
export default function JamboxAddonsModal({
|
||
isOpen,
|
||
onClose,
|
||
pkg,
|
||
|
||
// ✅ YAML
|
||
phoneCards = [],
|
||
tvAddons = [],
|
||
addons = [],
|
||
decoders = [],
|
||
|
||
cenaOpis = "zł/mies.",
|
||
}) {
|
||
const phonePlans = useMemo(() => mapPhoneYamlToPlans(phoneCards), [phoneCards]);
|
||
|
||
const tvAddonsList = useMemo(() => normalizeAddons(tvAddons), [tvAddons]);
|
||
const addonsList = useMemo(() => normalizeAddons(addons), [addons]);
|
||
|
||
const decodersList = useMemo(() => normalizeDecoders(decoders), [decoders]);
|
||
|
||
// ✅ TV: pokazujemy tylko dostępne dla pkg.name
|
||
const tvAddonsVisible = useMemo(() => {
|
||
if (!pkg) return [];
|
||
return tvAddonsList.filter((a) => isTvAddonAvailableForPkg(a, pkg));
|
||
}, [tvAddonsList, pkg]);
|
||
|
||
// wybory
|
||
const [selectedPhoneId, setSelectedPhoneId] = useState(null);
|
||
const [openPhoneId, setOpenPhoneId] = useState(null);
|
||
|
||
// dekoder (radio)
|
||
const [selectedDecoderId, setSelectedDecoderId] = useState(null);
|
||
|
||
// checkbox/quantity: { [id]: qty }
|
||
const [selectedQty, setSelectedQty] = useState({});
|
||
|
||
// ✅ TV: term per dodatek (12m / bezterminowo)
|
||
const [tvTerm, setTvTerm] = useState({}); // { [id]: "12m" | "bezterminowo" }
|
||
|
||
// akordeon pakietu bazowego
|
||
const [baseOpen, setBaseOpen] = useState(true);
|
||
|
||
// reset po otwarciu / zmianie pakietu
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
setSelectedPhoneId(null);
|
||
setOpenPhoneId(null);
|
||
setSelectedDecoderId(null);
|
||
setSelectedQty({});
|
||
setTvTerm({});
|
||
setBaseOpen(true);
|
||
|
||
const d0 =
|
||
(Array.isArray(decodersList) && decodersList.find((d) => Number(d.cena) === 0)) ||
|
||
(Array.isArray(decodersList) ? decodersList[0] : null);
|
||
|
||
setSelectedDecoderId(d0 ? String(d0.id) : null);
|
||
}, [isOpen, pkg?.id, decodersList]);
|
||
if (!isOpen || !pkg) return null;
|
||
|
||
const basePrice = Number(pkg.price_monthly || 0);
|
||
|
||
const phonePrice = useMemo(() => {
|
||
if (!selectedPhoneId) return 0;
|
||
const p = phonePlans.find((x) => String(x.id) === String(selectedPhoneId));
|
||
return Number(p?.price_monthly || 0);
|
||
}, [selectedPhoneId, phonePlans]);
|
||
|
||
const decoderPrice = useMemo(() => {
|
||
if (!selectedDecoderId) return 0;
|
||
const d = decodersList.find((x) => String(x.id) === String(selectedDecoderId));
|
||
return Number(d?.cena || 0);
|
||
}, [selectedDecoderId, decodersList]);
|
||
|
||
// ✅ TV: suma liczona tylko po widocznych (czyli dostępnych)
|
||
const tvAddonsPrice = useMemo(() => {
|
||
return tvAddonsVisible.reduce((sum, a) => {
|
||
const qty = Number(selectedQty[a.id] || 0);
|
||
if (qty <= 0) return sum;
|
||
|
||
const termPricing = hasTvTermPricing(a, pkg);
|
||
const term = tvTerm[a.id] || "12m";
|
||
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
|
||
|
||
return sum + qty * unit;
|
||
}, 0);
|
||
}, [selectedQty, tvAddonsVisible, tvTerm, pkg]);
|
||
|
||
// zwykłe dodatki (addons.yaml) – stara logika (multiroom itp.)
|
||
const addonsOnlyPrice = useMemo(() => {
|
||
return addonsList.reduce((sum, a) => {
|
||
const qty = Number(selectedQty[a.id] || 0);
|
||
const unit = getAddonUnitPrice(a, pkg, null);
|
||
return sum + qty * unit;
|
||
}, 0);
|
||
}, [selectedQty, addonsList, pkg]);
|
||
|
||
const addonsPrice = tvAddonsPrice + addonsOnlyPrice;
|
||
const totalMonthly = basePrice + phonePrice + decoderPrice + addonsPrice;
|
||
|
||
const handlePhoneSelect = (id) => {
|
||
if (id === null) {
|
||
setSelectedPhoneId(null);
|
||
setOpenPhoneId(null);
|
||
return;
|
||
}
|
||
setSelectedPhoneId(id);
|
||
setOpenPhoneId((prev) => (String(prev) === String(id) ? null : id));
|
||
};
|
||
|
||
const toggleCheckboxAddon = (id) => {
|
||
setSelectedQty((prev) => {
|
||
const next = { ...prev };
|
||
next[id] = (next[id] || 0) > 0 ? 0 : 1;
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const setQtyAddon = (id, qty, min, max) => {
|
||
const safe = Math.max(min, Math.min(max, qty));
|
||
setSelectedQty((prev) => ({ ...prev, [id]: safe }));
|
||
};
|
||
|
||
const renderAddonRow = (a, isTv = false) => {
|
||
const qty = Number(selectedQty[a.id] || 0);
|
||
const isQty = a.typ === "quantity" || a.ilosc === true;
|
||
|
||
// TV: term i cena
|
||
const termPricing = isTv && hasTvTermPricing(a, pkg);
|
||
const term = tvTerm[a.id] || "12m";
|
||
const unit = getAddonUnitPrice(a, pkg, termPricing ? term : null);
|
||
|
||
if (!isQty) {
|
||
return (
|
||
<label class="f-addon-item" key={(isTv ? "tv-" : "a-") + a.id}>
|
||
<div class="f-addon-checkbox">
|
||
<input
|
||
type="checkbox"
|
||
checked={qty > 0}
|
||
onChange={() => toggleCheckboxAddon(a.id)}
|
||
/>
|
||
</div>
|
||
|
||
<div class="f-addon-main">
|
||
<div class="f-addon-name">{a.nazwa}</div>
|
||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||
|
||
{termPricing && (
|
||
<div class="mt-2 flex flex-wrap gap-3 text-sm" onClick={(e) => e.stopPropagation()}>
|
||
<label class="inline-flex items-center gap-2">
|
||
<input
|
||
type="radio"
|
||
name={`term-${a.id}`}
|
||
checked={(tvTerm[a.id] || "12m") === "12m"}
|
||
onChange={() => setTvTerm((p) => ({ ...p, [a.id]: "12m" }))}
|
||
/>
|
||
<span>12 miesięcy</span>
|
||
</label>
|
||
|
||
<label class="inline-flex items-center gap-2">
|
||
<input
|
||
type="radio"
|
||
name={`term-${a.id}`}
|
||
checked={(tvTerm[a.id] || "12m") === "bezterminowo"}
|
||
onChange={() => setTvTerm((p) => ({ ...p, [a.id]: "bezterminowo" }))}
|
||
/>
|
||
<span>Bezterminowo</span>
|
||
</label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div class="f-addon-price">
|
||
{money(unit)} {cenaOpis}
|
||
</div>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
const min = Number.isFinite(a.min) ? a.min : 0;
|
||
const max = Number.isFinite(a.max) ? a.max : 10;
|
||
const step = Number.isFinite(a.krok) ? a.krok : 1;
|
||
const lineTotal = qty * unit;
|
||
|
||
return (
|
||
<div class="f-addon-item f-addon-item--qty" key={(isTv ? "tvq-" : "aq-") + a.id}>
|
||
<div class="f-addon-checkbox" aria-hidden="true"></div>
|
||
|
||
<div class="f-addon-main">
|
||
<div class="f-addon-name">{a.nazwa}</div>
|
||
{a.opis && <div class="f-addon-desc">{a.opis}</div>}
|
||
</div>
|
||
|
||
<div class="f-addon-qty" onClick={(e) => e.stopPropagation()}>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline"
|
||
onClick={() => setQtyAddon(a.id, qty - step, min, max)}
|
||
disabled={qty <= min}
|
||
>
|
||
−
|
||
</button>
|
||
|
||
<span class="f-addon-qty-value">{qty}</span>
|
||
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline"
|
||
onClick={() => setQtyAddon(a.id, qty + step, min, max)}
|
||
disabled={qty >= max}
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
|
||
<div class="f-addon-price">
|
||
<div>
|
||
{money(unit)} {cenaOpis}
|
||
</div>
|
||
<div class="f-addon-price-total">{qty > 0 ? `${money(lineTotal)} ${cenaOpis}` : "—"}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
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 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">
|
||
{money(basePrice)} {cenaOpis}
|
||
</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>
|
||
|
||
{/* ✅ DEKODER (radio) — NAD TV ADDONS */}
|
||
<div class="f-modal-section">
|
||
<h3>Wybór dekodera</h3>
|
||
|
||
{decodersList.length === 0 ? (
|
||
<p>Brak dostępnych dekoderów.</p>
|
||
) : (
|
||
<div class="f-modal-phone-list f-accordion">
|
||
{decodersList.map((d) => {
|
||
const isSelected = String(selectedDecoderId) === String(d.id);
|
||
|
||
return (
|
||
<div class={`f-accordion-item ${isSelected ? "is-open" : ""}`} key={d.id}>
|
||
<button
|
||
type="button"
|
||
class="f-accordion-header"
|
||
onClick={() => setSelectedDecoderId(String(d.id))}
|
||
>
|
||
<span class="f-accordion-header-left">
|
||
<input
|
||
type="radio"
|
||
name="decoder"
|
||
checked={isSelected}
|
||
onChange={(e) => {
|
||
e.stopPropagation();
|
||
setSelectedDecoderId(String(d.id));
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
/>
|
||
<span class="f-modal-phone-name">{d.nazwa}</span>
|
||
</span>
|
||
|
||
<span class="f-modal-phone-price">
|
||
{money(d.cena)} {cenaOpis}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
|
||
{/* TV ADDONS */}
|
||
<div class="f-modal-section">
|
||
<h3>Pakiety dodatkowe TV</h3>
|
||
{tvAddonsVisible.length === 0 ? (
|
||
<p>Brak pakietów dodatkowych TV.</p>
|
||
) : (
|
||
<div class="f-addon-list">{tvAddonsVisible.map((a) => renderAddonRow(a, true))}</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">
|
||
<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 {cenaOpis}</span>
|
||
</button>
|
||
</div>
|
||
|
||
{phonePlans.map((p) => {
|
||
const isSelected = String(selectedPhoneId) === String(p.id);
|
||
const isOpen = String(openPhoneId) === String(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">
|
||
{money(p.price_monthly)} {cenaOpis}
|
||
</span>
|
||
</button>
|
||
|
||
{isOpen && (
|
||
<div class="f-accordion-body">
|
||
{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">{formatFeatureValue(f.value)}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* DODATKI (addons.yaml) */}
|
||
<div class="f-modal-section">
|
||
<h3>Dodatkowe usługi</h3>
|
||
|
||
{addonsList.length === 0 ? (
|
||
<p>Brak usług dodatkowych.</p>
|
||
) : (
|
||
<div class="f-addon-list">{addonsList.map((a) => renderAddonRow(a, false))}</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>
|
||
{money(basePrice)} {cenaOpis}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="f-summary-row">
|
||
<span>Telefon</span>
|
||
<span>{phonePrice ? `${money(phonePrice)} ${cenaOpis}` : "—"}</span>
|
||
</div>
|
||
|
||
<div class="f-summary-row">
|
||
<span>Dekoder</span>
|
||
<span>{decoderPrice ? `${money(decoderPrice)} ${cenaOpis}` : "—"}</span>
|
||
</div>
|
||
|
||
<div class="f-summary-row">
|
||
<span>Dodatki TV</span>
|
||
<span>{tvAddonsPrice ? `${money(tvAddonsPrice)} ${cenaOpis}` : "—"}</span>
|
||
</div>
|
||
|
||
<div class="f-summary-row">
|
||
<span>Dodatki</span>
|
||
<span>{addonsOnlyPrice ? `${money(addonsOnlyPrice)} ${cenaOpis}` : "—"}</span>
|
||
</div>
|
||
|
||
<div class="f-summary-total">
|
||
<span>Łącznie</span>
|
||
<span>
|
||
{money(totalMonthly)} {cenaOpis}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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">
|
||
{money(totalMonthly)} {cenaOpis}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|