(() => { "use strict"; const colors = ["#2563eb", "#0f9f6e", "#7a5af8", "#f79009", "#06aed5", "#e0528d", "#64748b", "#84cc16"]; const BUSINESS_TIME_ZONE = "Asia/Bangkok"; const I18N = window.ARRI18n; const $ = (selector) => document.querySelector(selector); let csrf = ""; let lastData = null; function escapeHtml(value) { return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } function monthNow() { const parts = Object.fromEntries(new Intl.DateTimeFormat("en-CA", { timeZone: BUSINESS_TIME_ZONE, year: "numeric", month: "2-digit", }).formatToParts(new Date()).map((part) => [part.type, part.value])); return `${parts.year}-${parts.month}`; } function integer(value) { return I18N?.formatInteger(value) || new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }).format(Number(value || 0)); } function money(value, compact = true) { if (I18N) return I18N.formatMoney(value, compact); const amount = Number(value || 0); return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY", maximumFractionDigits: 0 }).format(amount); } function redirectToLogin() { window.location.replace("/login?next=%2Fh5"); } async function api(path, options = {}) { const allowUnauthorized = Boolean(options.allowUnauthorized); const requestOptions = { ...options }; delete requestOptions.allowUnauthorized; const headers = new Headers(options.headers || {}); if (options.method && options.method !== "GET") headers.set("X-ARR-CSRF", csrf); const response = await fetch(path, { ...requestOptions, headers, credentials: "same-origin" }); if (response.status === 401) { if (allowUnauthorized) return null; redirectToLogin(); throw new Error(I18N?.errorMessage("SESSION_INVALID", "登录状态已失效") || "登录状态已失效"); } let payload; try { payload = await response.json(); } catch (_) { throw new Error(I18N?.t("error.unrecognized_response") || "服务返回了无法识别的结果"); } if (!response.ok || !payload.ok) { const error = payload?.error || {}; throw new Error(I18N?.errorMessage(error.code, error.message) || error.message || I18N?.t("error.database_read_unavailable") || "数据读取失败"); } return payload.data; } function notify(message) { const node = $("#h5-message"); node.textContent = I18N?.text(message) || message; node.classList.add("visible"); window.setTimeout(() => node.classList.remove("visible"), 3000); } function reset(message) { ["#h5-rooms", "#h5-revenue", "#h5-nights", "#h5-companies", "#h5-donut-total"].forEach((id) => { $(id).textContent = "—"; }); const copy = I18N?.text(message) || message; $("#h5-ranking").innerHTML = `
${escapeHtml(copy)}
`; $("#h5-matrix").innerHTML = `${escapeHtml(copy)}
`; $("#h5-legend").innerHTML = ""; $("#h5-donut").style.background = "#d8e0ec"; $("#h5-data-range").textContent = I18N?.t("bi.data_range", { start: "—", end: "—" }) || "数据范围:—"; } function dataRange(data) { const start = data.min_arrival_date ? (I18N?.formatDate(data.min_arrival_date) || data.min_arrival_date) : "—"; const end = data.max_arrival_date ? (I18N?.formatDate(data.max_arrival_date) || data.max_arrival_date) : "—"; return I18N?.t("bi.data_range", { start, end }) || `数据范围:${start} – ${end}`; } function renderSales(channels) { const sorted = [...channels].sort((a, b) => Number(b.totals.total_price) - Number(a.totals.total_price)); const total = sorted.reduce((sum, item) => sum + Number(item.totals.total_price || 0), 0); const maximum = Math.max(1, ...sorted.map((item) => Number(item.totals.total_price || 0))); let cursor = 0; const stops = sorted.map((item, index) => { const start = cursor; cursor += total ? Number(item.totals.total_price || 0) / total * 100 : 0; return `${colors[index % colors.length]} ${start.toFixed(3)}% ${cursor.toFixed(3)}%`; }); $("#h5-donut").style.background = total ? `conic-gradient(${stops.join(",")})` : "#d8e0ec"; $("#h5-donut-total").textContent = money(total); $("#h5-legend").innerHTML = sorted.map((item, index) => ` ${escapeHtml(item.worksheet)}${total ? (Number(item.totals.total_price || 0) / total * 100).toFixed(1) : "0.0"}%`).join(""); [...document.querySelectorAll("#h5-legend .mobile-legend-item i")].forEach((marker) => { marker.style.backgroundColor = marker.dataset.color; }); $("#h5-ranking").innerHTML = sorted.length ? sorted.map((item) => `${escapeHtml(I18N?.t("bi.empty_sales") || "暂无销售数据")}
`; [...document.querySelectorAll("#h5-ranking .track i")].forEach((bar) => { bar.style.width = `${Number(bar.dataset.width || 0)}%`; }); } function renderMatrix(data) { const rooms = data.overall.room_types.map((item) => item.room_type); if (!rooms.length || !data.channels.length) { $("#h5-matrix").innerHTML = `${escapeHtml(I18N?.t("bi.empty") || "暂无渠道与房型数据")}
`; return; } const rows = data.channels.map((channel) => { const values = new Map(channel.room_types.map((item) => [item.room_type, item.rooms_sold])); return `| ${escapeHtml(channelLabel)} | ${rooms.map((room) => `${escapeHtml(room)} | `).join("")}${escapeHtml(totalLabel)} |
|---|---|---|
| ${escapeHtml(totalLabel)} | ${rooms.map((room) => `${integer(totals.get(room) || 0)} | `).join("")}${integer(data.overall.totals.rooms_sold)} |