// services.js — page "Services" : répartition des coûts par service, tous
// tenants/providers confondus, avec sélecteur de période (7j/30j/1 an) et
// détail par tenant au clic sur un service.
let currentRange = 30;
let currentProvider = "all";
let currentServices = [];
let currentTotal = 0;
const SORT_COLUMNS = [
{ key: "service_name", label: "Service", numeric: false },
{ key: "amount", label: "Montant", numeric: true },
{ key: "pct", label: "Part", numeric: true },
];
let sortColumn = "amount";
let sortDirection = "desc";
async function loadServices(range, provider) {
const params = new URLSearchParams({ range });
if (provider !== "all") params.set("provider", provider);
const res = await fetch(`/api/services?${params}`);
if (!res.ok) throw new Error(`API services: HTTP ${res.status}`);
return res.json();
}
function periodTitle(data) {
if (data.mode === "monthly") return "12 derniers mois";
return rangeLabel(data.range_days);
}
function sortedServices() {
const sorted = [...currentServices];
sorted.sort((a, b) => {
const cmp =
sortColumn === "service_name"
? a.service_name.localeCompare(b.service_name, "fr", { sensitivity: "base" })
: a[sortColumn] - b[sortColumn];
return sortDirection === "asc" ? cmp : -cmp;
});
return sorted;
}
function sortIndicator(column) {
if (sortColumn !== column) return "";
return sortDirection === "asc" ? " ↑" : " ↓";
}
function renderServicesTable(data) {
document.getElementById("services-period-title").textContent = periodTitle(data);
currentServices = data.services;
currentTotal = data.total;
const wrap = document.getElementById("services-table-wrap");
if (currentServices.length === 0) {
wrap.innerHTML = `
Aucune donnée de coût sur cette période. Lance collect.py ou import_historical.py pour peupler le dashboard.
`;
return;
}
renderServicesTableRows();
}
function renderServicesTableRows() {
const wrap = document.getElementById("services-table-wrap");
const sorted = sortedServices();
const headerCells = SORT_COLUMNS.map((col) => {
const classes = ["sortable"];
if (col.numeric) classes.push("num");
if (sortColumn === col.key) classes.push(`sorted-${sortDirection}`);
return `${col.label}${sortIndicator(col.key)} | `;
}).join("");
const rows = sorted
.map(
(s) => `
| ${escapeHtml(s.service_name)} |
${formatEUR(s.amount)} |
${s.pct.toFixed(1)} % |
`
)
.join("");
wrap.innerHTML = `
${headerCells}
${rows}
| Total |
${formatEUR(currentTotal)} |
|
`;
wrap.querySelectorAll("tr.clickable").forEach((tr) => {
tr.addEventListener("click", () => openTenantDrawer(tr.dataset.service));
});
wrap.querySelectorAll("th.sortable").forEach((th) => {
th.addEventListener("click", () => {
const column = th.dataset.sort;
if (sortColumn === column) {
sortDirection = sortDirection === "asc" ? "desc" : "asc";
} else {
sortColumn = column;
sortDirection = column === "service_name" ? "asc" : "desc";
}
renderServicesTableRows();
});
});
}
async function reloadServices() {
try {
const data = await loadServices(currentRange, currentProvider);
renderServicesTable(data);
} catch (e) {
document.getElementById("error-banner").innerHTML = `
Erreur de chargement : ${escapeHtml(e.message)}
`;
}
}
function setupRangeToggle() {
const buttons = document.querySelectorAll("#range-toggle button");
buttons.forEach((btn) => {
btn.classList.toggle("active", Number(btn.dataset.range) === currentRange);
btn.addEventListener("click", () => {
currentRange = Number(btn.dataset.range);
buttons.forEach((b) => b.classList.toggle("active", b === btn));
reloadServices();
});
});
}
function setupProviderToggle() {
const buttons = document.querySelectorAll("#provider-toggle button");
buttons.forEach((btn) => {
btn.classList.toggle("active", btn.dataset.provider === currentProvider);
btn.addEventListener("click", () => {
currentProvider = btn.dataset.provider;
buttons.forEach((b) => b.classList.toggle("active", b === btn));
reloadServices();
});
});
}
// --- Drawer : détail par tenant d'un service ----------------------------
function openTenantDrawer(serviceName) {
document.getElementById("drawer-title").textContent = serviceName;
document.getElementById("drawer-sub").textContent = "Chargement…";
document.getElementById("drawer-body").innerHTML = `Chargement…
`;
document.getElementById("tenant-drawer").classList.add("open");
document.getElementById("tenant-drawer").setAttribute("aria-hidden", "false");
document.getElementById("drawer-backdrop").classList.add("open");
const params = new URLSearchParams({ range: currentRange });
if (currentProvider !== "all") params.set("provider", currentProvider);
fetch(`/api/services/${encodeURIComponent(serviceName)}/tenants?${params}`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => renderDrawerContent(data))
.catch((e) => {
document.getElementById("drawer-sub").textContent = "";
document.getElementById("drawer-body").innerHTML =
`Erreur de chargement : ${escapeHtml(e.message)}
`;
});
}
function renderDrawerContent(data) {
const subEl = document.getElementById("drawer-sub");
const bodyEl = document.getElementById("drawer-body");
subEl.textContent = data.mode === "monthly" ? "12 derniers mois" : rangeLabel(data.range_days);
if (!data.tenants.length) {
bodyEl.innerHTML = `Aucun tenant n'a de coût sur ce service pour cette période.
`;
return;
}
bodyEl.innerHTML = data.tenants
.map(
(t) => `
${providerLabel(t.provider)}
${escapeHtml(t.tenant_name)}
${t.pct.toFixed(1)} %
${formatEUR(t.amount)}
`
)
.join("");
bodyEl.querySelectorAll(".resource-row.clickable").forEach((row) => {
row.addEventListener("click", () => {
window.location.href = `/tenant/${row.dataset.provider}/${encodeURIComponent(row.dataset.name)}`;
});
});
}
function closeTenantDrawer() {
document.getElementById("tenant-drawer").classList.remove("open");
document.getElementById("tenant-drawer").setAttribute("aria-hidden", "true");
document.getElementById("drawer-backdrop").classList.remove("open");
}
function setupDrawer() {
document.getElementById("drawer-close").addEventListener("click", closeTenantDrawer);
document.getElementById("drawer-backdrop").addEventListener("click", closeTenantDrawer);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeTenantDrawer();
});
}
async function init() {
try {
const data = await loadServices(currentRange, currentProvider);
renderServicesTable(data);
setupRangeToggle();
setupProviderToggle();
setupDrawer();
} catch (e) {
document.getElementById("error-banner").innerHTML = `
Erreur de chargement : ${escapeHtml(e.message)}
`;
document.getElementById("services-table-wrap").innerHTML = "";
}
}
init();