"use strict";
(function () {
const STORAGE_KEY = "ya_stats_business_finance_v4";
const LEGACY_KEYS = [
"ya_stats_business_finance_v3",
"ya_stats_business_finance_v2",
"ya_stats_business_finance_v1",
];
const HISTORY_LIMIT = 6;
let cachedSubscription = null;
let historyCache = null;
let historyLoadWarning = "";
const MONTH_NAMES = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
function core() {
return (window.YAStatsCabinet && window.YAStatsCabinet.core) || {};
}
function formatMoney(value) {
if (typeof core().formatMoney === "function") return core().formatMoney(value);
return Number(value || 0).toLocaleString("ru-RU") + " ₽";
}
function formatPercent(value) {
if (typeof core().formatPercent === "function") return core().formatPercent(value);
return Number(value || 0).toLocaleString("ru-RU", {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}) + "%";
}
function escapeHtml(value) {
if (typeof core().escapeHtml === "function") return core().escapeHtml(value);
return String(value || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function toNumber(value) {
const parsed = Number(value || 0);
return Number.isFinite(parsed) ? parsed : 0;
}
function currentMonthKey() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
}
function getProfileTariffCode() {
const candidates = [
cachedSubscription,
window.__yaBusinessFinanceSubscription,
];
const subscription = candidates.find((item) => item && typeof item === "object") || {};
return String(
subscription.feature_tariff_code
|| subscription.tariff_code
|| subscription.tariff_name
|| ""
).trim().toLowerCase();
}
function hasBusinessFinanceAccess() {
return ["pro", "business"].includes(getProfileTariffCode());
}
async function ensureBusinessFinanceSubscription() {
try {
cachedSubscription = null;
const response = await fetch("/auth/site/settings", {
credentials: "same-origin",
cache: "no-store",
});
if (response.ok) {
const data = await response.json();
cachedSubscription = data.subscription || null;
window.__yaBusinessFinanceSubscription = cachedSubscription;
return cachedSubscription;
}
} catch (error) {
return null;
}
return null;
}
function getBusinessFinanceUpgradeHtml() {
return `
Этот раздел доступен только на тарифах Pro и Business .
Перейдите на более высокий тариф, чтобы сохранять финансы бизнеса по месяцам, сравнивать историю и скачивать PDF-отчеты.
`;
}
function normalizeMonthKey(value) {
if (typeof value === "string" && /^\d{4}-\d{2}$/.test(value)) return value;
return currentMonthKey();
}
function monthLabel(monthKey) {
const normalized = normalizeMonthKey(monthKey);
const [year, month] = normalized.split("-");
const index = Math.max(0, Math.min(11, Number(month) - 1));
return `${MONTH_NAMES[index]} ${year}`;
}
function buildMonthOptions() {
const options = [];
const cursor = new Date();
cursor.setDate(1);
for (let index = 0; index < HISTORY_LIMIT; index += 1) {
const key = `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`;
options.push({ value: key, label: monthLabel(key) });
cursor.setMonth(cursor.getMonth() + 1);
}
return options;
}
function normalizeRecord(item) {
const month = normalizeMonthKey(item && item.month);
const revenue = Math.round(toNumber(item && item.revenue) * 100) / 100;
const planRevenue = Math.round(toNumber(item && item.planRevenue) * 100) / 100;
const planSpend = Math.round(toNumber(item && item.planSpend) * 100) / 100;
const payroll = Math.round(toNumber(item && item.payroll) * 100) / 100;
const rent = Math.round(toNumber(item && item.rent) * 100) / 100;
const taxes = Math.round(toNumber(item && item.taxes) * 100) / 100;
const adSpend = Math.round(toNumber(item && item.adSpend) * 100) / 100;
const otherSpend = Math.round(toNumber(item && item.otherSpend) * 100) / 100;
const note = String(item && item.note || "").trim();
const totalSpend = Math.round((payroll + rent + taxes + adSpend + otherSpend) * 100) / 100;
const profit = Math.round((revenue - totalSpend) * 100) / 100;
const roi = totalSpend > 0 ? Math.round(((profit / totalSpend) * 100) * 10) / 10 : null;
const adShare = revenue > 0 ? Math.round(((adSpend / revenue) * 100) * 10) / 10 : null;
const payrollShare = revenue > 0 ? Math.round(((payroll / revenue) * 100) * 10) / 10 : null;
return {
month,
label: monthLabel(month),
revenue,
planRevenue,
planSpend,
payroll,
rent,
taxes,
adSpend,
otherSpend,
note,
totalSpend,
profit,
roi,
adShare,
payrollShare,
breakEvenRevenue: totalSpend,
updatedAt: item && item.updatedAt ? item.updatedAt : new Date().toISOString(),
};
}
function sortHistory(items) {
return items.slice().sort((left, right) => String(right.month).localeCompare(String(left.month)));
}
function readStorage() {
try {
const current = window.localStorage.getItem(STORAGE_KEY);
if (current) return JSON.parse(current);
for (const key of LEGACY_KEYS) {
const legacy = window.localStorage.getItem(key);
if (legacy) return JSON.parse(legacy);
}
} catch (error) {
return [];
}
return [];
}
function loadHistory() {
if (!Array.isArray(historyCache)) return [];
return sortHistory(historyCache.map(normalizeRecord)).slice(0, HISTORY_LIMIT);
}
function saveHistory(items) {
const next = sortHistory(items.map(normalizeRecord)).slice(0, HISTORY_LIMIT);
historyCache = next;
return next;
}
async function fetchHistoryFromServer() {
const response = await fetch("/auth/site/business-finance", {
credentials: "same-origin",
cache: "no-store",
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.detail || "Не удалось загрузить финансы бизнеса.");
}
const items = Array.isArray(data.items) ? data.items : [];
return saveHistory(items);
}
async function persistHistory(items) {
const next = sortHistory(items.map(normalizeRecord)).slice(0, HISTORY_LIMIT);
const response = await fetch("/auth/site/business-finance", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ items: next }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.detail || "Не удалось сохранить финансы бизнеса.");
}
historyCache = Array.isArray(data.items) ? sortHistory(data.items.map(normalizeRecord)).slice(0, HISTORY_LIMIT) : next;
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(historyCache));
} catch (error) {
// local cache is optional; server is the source of truth
}
return historyCache;
}
async function ensureHistoryLoaded() {
if (Array.isArray(historyCache)) return historyCache;
const legacy = readStorage();
historyLoadWarning = "";
try {
const serverItems = await fetchHistoryFromServer();
if (serverItems.length) {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(serverItems));
} catch (error) {
// ignore optional browser cache write errors
}
return serverItems;
}
} catch (error) {
if (Array.isArray(legacy) && legacy.length) {
historyLoadWarning = "Серверное хранилище пока недоступно. Показываю данные из этого браузера.";
historyCache = sortHistory(legacy.map(normalizeRecord)).slice(0, HISTORY_LIMIT);
return historyCache;
}
historyLoadWarning = String(error && error.message || "Не удалось загрузить финансы бизнеса.");
historyCache = [];
return historyCache;
}
if (Array.isArray(legacy) && legacy.length) {
try {
return await persistHistory(legacy);
} catch (error) {
historyLoadWarning = "Форма открыта, но серверное сохранение пока недоступно. Старые данные показаны локально.";
historyCache = sortHistory(legacy.map(normalizeRecord)).slice(0, HISTORY_LIMIT);
return historyCache;
}
}
historyCache = [];
return historyCache;
}
function getRecord(month) {
const key = normalizeMonthKey(month);
return loadHistory().find((item) => item.month === key) || normalizeRecord({ month: key });
}
async function upsertRecord(payload) {
const record = normalizeRecord(payload);
const history = loadHistory().filter((item) => item.month !== record.month);
history.push(record);
return persistHistory(history);
}
function buildPlanFact(record) {
const revenueGap = record.planRevenue > 0 ? Math.round((record.revenue - record.planRevenue) * 100) / 100 : null;
const spendGap = record.planSpend > 0 ? Math.round((record.totalSpend - record.planSpend) * 100) / 100 : null;
let revenueStatus = "План не задан";
let revenueTone = "text-yellow-400";
if (record.planRevenue > 0) {
revenueStatus = revenueGap >= 0 ? "План по выручке выполняется" : "Выручка ниже плана";
revenueTone = revenueGap >= 0 ? "text-green-400" : "text-red-400";
}
let spendStatus = "Лимит не задан";
let spendTone = "text-yellow-400";
if (record.planSpend > 0) {
spendStatus = spendGap <= 0 ? "Расходы в лимите" : "Расходы выше лимита";
spendTone = spendGap <= 0 ? "text-green-400" : "text-red-400";
}
return {
revenueGap,
spendGap,
revenueStatus,
revenueTone,
spendStatus,
spendTone,
};
}
function buildExpenseNorm(record, previousRecord) {
const revenueGrowth = previousRecord && previousRecord.revenue > 0
? ((record.revenue - previousRecord.revenue) / previousRecord.revenue) * 100
: null;
const adGrowth = previousRecord && previousRecord.adSpend > 0
? ((record.adSpend - previousRecord.adSpend) / previousRecord.adSpend) * 100
: null;
const payrollGrowth = previousRecord && previousRecord.payroll > 0
? ((record.payroll - previousRecord.payroll) / previousRecord.payroll) * 100
: null;
const reasons = [];
if (record.revenue > 0 && record.totalSpend > record.revenue) {
reasons.push("все расходы уже выше выручки");
}
if (record.adShare != null && record.adShare > 35) {
reasons.push("реклама забирает слишком большую долю выручки");
}
if (record.payrollShare != null && record.payrollShare > 45) {
reasons.push("зарплаты съедают слишком большую долю выручки");
}
if (revenueGrowth != null && adGrowth != null && adGrowth > revenueGrowth + 10) {
reasons.push("реклама растет быстрее выручки");
}
if (revenueGrowth != null && payrollGrowth != null && payrollGrowth > revenueGrowth + 10) {
reasons.push("зарплаты растут быстрее выручки");
}
if (record.revenue <= 0 && record.totalSpend > 0) {
return {
label: "Нужны данные",
toneClass: "text-yellow-400",
description: "Пока нет выручки, поэтому нельзя честно оценить норму расходов.",
};
}
if (!reasons.length) {
return {
label: "В норме",
toneClass: "text-green-400",
description: "Пока расходы выглядят здорово относительно выручки. Перегрева не видно.",
};
}
return {
label: "Проверить",
toneClass: "text-red-400",
description: `Сейчас стоит проверить: ${reasons.join(", ")}.`,
};
}
function buildSummary(record, previousRecord, expenseNorm) {
const lines = [];
if (record.revenue <= 0 && record.totalSpend > 0) {
lines.push("Пока внесены только расходы. Добавьте выручку, чтобы увидеть реальный итог бизнеса.");
} else if (record.profit >= 0) {
lines.push(`Сейчас бизнес остается в плюсе примерно на ${formatMoney(record.profit)} после всех расходов.`);
} else {
lines.push(`Сейчас бизнес в минусе примерно на ${formatMoney(Math.abs(record.profit))}. Стоит пересмотреть расходы или поднять выручку.`);
}
lines.push(`Точка безубыточности на этот месяц: ${formatMoney(record.breakEvenRevenue)}.`);
lines.push(`Норма расходов: ${expenseNorm.label}. ${expenseNorm.description}`);
if (record.adShare != null) {
lines.push(`Реклама занимает около ${formatPercent(record.adShare)} от выручки за месяц.`);
}
if (record.note) {
lines.push(`Комментарий к месяцу: ${record.note}`);
}
if (previousRecord) {
const revenueDiff = record.revenue - previousRecord.revenue;
const profitDiff = record.profit - previousRecord.profit;
lines.push(
revenueDiff >= 0
? `Выручка выше прошлого месяца на ${formatMoney(revenueDiff)}.`
: `Выручка ниже прошлого месяца на ${formatMoney(Math.abs(revenueDiff))}.`
);
lines.push(
profitDiff >= 0
? `Финальный результат лучше прошлого месяца на ${formatMoney(profitDiff)}.`
: `Финальный результат хуже прошлого месяца на ${formatMoney(Math.abs(profitDiff))}.`
);
} else {
lines.push("Как только появится второй месяц, раздел начнет показывать сравнение с прошлым периодом.");
}
return lines;
}
function buildChart(history) {
if (!history.length) {
return `История пока пустая. Сохраните хотя бы один месяц, и здесь появится сравнение выручки, расходов и структуры затрат.
`;
}
const ordered = history.slice().reverse();
const maxValue = Math.max(1, ...ordered.flatMap((item) => [item.revenue, item.totalSpend, item.planRevenue, item.planSpend]));
return `
${ordered.map((item) => {
const revenueWidth = Math.max(3, Math.round((item.revenue / maxValue) * 100));
const totalSpendWidth = Math.max(3, Math.round((item.totalSpend / maxValue) * 100));
const payrollWidth = item.totalSpend > 0 ? (item.payroll / item.totalSpend) * 100 : 0;
const rentWidth = item.totalSpend > 0 ? (item.rent / item.totalSpend) * 100 : 0;
const taxesWidth = item.totalSpend > 0 ? (item.taxes / item.totalSpend) * 100 : 0;
const adWidth = item.totalSpend > 0 ? (item.adSpend / item.totalSpend) * 100 : 0;
const otherWidth = item.totalSpend > 0 ? (item.otherSpend / item.totalSpend) * 100 : 0;
return `
${escapeHtml(item.label)}
${item.profit >= 0 ? "Плюс" : "Минус"} ${escapeHtml(formatMoney(item.profit))}
Выручка
${escapeHtml(formatMoney(item.revenue))}${item.planRevenue > 0 ? ` / план ${escapeHtml(formatMoney(item.planRevenue))}` : ""}
Все расходы
${escapeHtml(formatMoney(item.totalSpend))}${item.planSpend > 0 ? ` / лимит ${escapeHtml(formatMoney(item.planSpend))}` : ""}
Зарплаты ${escapeHtml(formatMoney(item.payroll))}
Аренда ${escapeHtml(formatMoney(item.rent))}
Налоги ${escapeHtml(formatMoney(item.taxes))}
Реклама ${escapeHtml(formatMoney(item.adSpend))}
Прочее ${escapeHtml(formatMoney(item.otherSpend))}
`;
}).join("")}
`;
}
function buildHistoryRows(history) {
if (!history.length) {
return `Пока нет сохраненных месяцев для сравнения.
`;
}
return history.map((item) => `
${escapeHtml(item.label)}
Зарплаты ${escapeHtml(formatMoney(item.payroll))} • Аренда ${escapeHtml(formatMoney(item.rent))} • Налоги ${escapeHtml(formatMoney(item.taxes))} • Реклама ${escapeHtml(formatMoney(item.adSpend))} • Прочее ${escapeHtml(formatMoney(item.otherSpend))}
${item.note ? `
${escapeHtml(item.note)}
` : ""}
${escapeHtml(formatMoney(item.profit))}
${item.roi != null ? `ROI ${escapeHtml(formatPercent(item.roi))}` : "ROI пока не считается"}
`).join("");
}
function buildPdfHtml(record, history, expenseNorm, planFact) {
const generatedAt = new Date().toLocaleString("ru-RU");
const categories = [
{ name: "Зарплаты", value: record.payroll, color: "#38bdf8" },
{ name: "Аренда", value: record.rent, color: "#8b5cf6" },
{ name: "Налоги", value: record.taxes, color: "#22c55e" },
{ name: "Реклама", value: record.adSpend, color: "#ffcc00" },
{ name: "Прочее", value: record.otherSpend, color: "#fb7185" },
];
const positiveCategories = categories.filter((item) => item.value > 0);
const total = Math.max(record.totalSpend, 1);
let cursor = 0;
const piePaths = positiveCategories.map((item) => {
const ratio = item.value / total;
const start = cursor;
const end = cursor + ratio;
cursor = end;
const largeArc = end - start > 0.5 ? 1 : 0;
const startAngle = (start * Math.PI * 2) - (Math.PI / 2);
const endAngle = (end * Math.PI * 2) - (Math.PI / 2);
const x1 = 100 + (84 * Math.cos(startAngle));
const y1 = 100 + (84 * Math.sin(startAngle));
const x2 = 100 + (84 * Math.cos(endAngle));
const y2 = 100 + (84 * Math.sin(endAngle));
const path = `M 100 100 L ${x1.toFixed(3)} ${y1.toFixed(3)} A 84 84 0 ${largeArc} 1 ${x2.toFixed(3)} ${y2.toFixed(3)} Z`;
return ` `;
}).join("");
return `
Финансы бизнеса — ${escapeHtml(record.label)}
YA
YA Stats Web
Финансы бизнеса
Отчет сформирован ${escapeHtml(generatedAt)}
${escapeHtml(record.label)} • Красивый фирменный отчет по выручке, расходам, плану и итоговому результату бизнеса.
Выручка ${escapeHtml(formatMoney(record.revenue))}
Все расходы ${escapeHtml(formatMoney(record.totalSpend))}
Чистый итог ${escapeHtml(formatMoney(record.profit))}
ROI ${record.roi != null ? escapeHtml(formatPercent(record.roi)) : "—"}
Ключевые выводы
Точка безубыточности
${escapeHtml(formatMoney(record.breakEvenRevenue))}
Минимальная выручка за месяц, чтобы бизнес не ушел в минус.
Норма расходов
${escapeHtml(expenseNorm.label)}
${escapeHtml(expenseNorm.description)}
План-факт месяца
План по выручке
${escapeHtml(formatMoney(record.planRevenue || 0))}
${escapeHtml(planFact.revenueStatus)}
Лимит расходов
${escapeHtml(formatMoney(record.planSpend || 0))}
${escapeHtml(planFact.spendStatus)}
Диаграмма расходов
${piePaths || ' '}
Расходы
${escapeHtml(formatMoney(record.totalSpend))}
${categories.map((item) => {
const share = record.totalSpend > 0 ? (item.value / record.totalSpend) * 100 : 0;
return `
${escapeHtml(item.name)}
${escapeHtml(formatPercent(share))} от всех расходов
${escapeHtml(formatMoney(item.value))}
`;
}).join("")}
Комментарий месяца
${escapeHtml(record.note || "Комментарий к месяцу пока не заполнен.")}
История за 6 месяцев
${history.map((item) => `
${escapeHtml(item.label)}
Выручка ${escapeHtml(formatMoney(item.revenue))} • Расходы ${escapeHtml(formatMoney(item.totalSpend))} • Итог ${escapeHtml(formatMoney(item.profit))}
${item.note ? `
${escapeHtml(item.note)}
` : ""}
`).join("")}
`;
}
function buildCurrentPayload() {
const history = loadHistory();
const monthValue = document.getElementById("business-finance-month") ? document.getElementById("business-finance-month").value : currentMonthKey();
const record = normalizeRecord({
month: monthValue,
label: monthLabel(monthValue),
revenue: document.getElementById("business-finance-revenue") ? document.getElementById("business-finance-revenue").value : 0,
planRevenue: document.getElementById("business-finance-plan-revenue") ? document.getElementById("business-finance-plan-revenue").value : 0,
planSpend: document.getElementById("business-finance-plan-spend") ? document.getElementById("business-finance-plan-spend").value : 0,
payroll: document.getElementById("business-finance-payroll") ? document.getElementById("business-finance-payroll").value : 0,
rent: document.getElementById("business-finance-rent") ? document.getElementById("business-finance-rent").value : 0,
taxes: document.getElementById("business-finance-taxes") ? document.getElementById("business-finance-taxes").value : 0,
adSpend: document.getElementById("business-finance-ad-spend") ? document.getElementById("business-finance-ad-spend").value : 0,
otherSpend: document.getElementById("business-finance-other-spend") ? document.getElementById("business-finance-other-spend").value : 0,
note: document.getElementById("business-finance-note") ? document.getElementById("business-finance-note").value : "",
});
const previous = history.find((item) => item.month < record.month) || null;
return {
record,
history,
expenseNorm: buildExpenseNorm(record, previous),
planFact: buildPlanFact(record),
filename: `ya-stats-business-finance-${record.month || "month"}.pdf`,
};
}
async function downloadPdf() {
const payload = buildCurrentPayload();
const response = await fetch("/auth/site/business-finance-pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || "Не удалось скачать PDF.");
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = payload.filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => window.URL.revokeObjectURL(url), 1000);
}
function renderModal(selectedMonth) {
const month = normalizeMonthKey(selectedMonth);
const history = loadHistory();
const current = getRecord(month);
const previous = history.find((item) => item.month < month) || null;
const expenseNorm = buildExpenseNorm(current, previous);
const planFact = buildPlanFact(current);
const summaryLines = buildSummary(current, previous, expenseNorm);
const modal = document.getElementById("business-modal");
const body = document.getElementById("business-modal-body");
if (!modal || !body) return;
const kicker = modal.querySelector(".text-yellow-500");
const title = modal.querySelector("h3");
if (kicker) kicker.textContent = "Финансы бизнеса";
if (title) title.textContent = "Расходы, выручка и итог по месяцам";
if (!hasBusinessFinanceAccess()) {
body.innerHTML = getBusinessFinanceUpgradeHtml();
return;
}
body.innerHTML = `
Этот раздел живет отдельно от Яндекс Директа. Вы сами вносите расходы и выручку, а кабинет показывает, остается бизнес в плюсе или уже проседает.
Месяц для заполнения
Скачать PDF
${buildMonthOptions().map((option) => `${escapeHtml(option.label)} `).join("")}
Комментарий к месяцу
Сохранить месяц
Все расходы
${formatMoney(current.totalSpend)}
Чистый итог
${formatMoney(current.profit)}
ROI
${current.roi != null ? formatPercent(current.roi) : "—"}
Доля рекламы
${current.adShare != null ? formatPercent(current.adShare) : "—"}
Точка безубыточности
${formatMoney(current.breakEvenRevenue)}
Сколько выручки нужно за месяц, чтобы просто выйти в ноль.
Норма расходов
${escapeHtml(expenseNorm.label)}
${escapeHtml(expenseNorm.description)}
План-факт по выручке
${escapeHtml(planFact.revenueStatus)}
${current.planRevenue > 0 ? `Факт ${escapeHtml(formatMoney(current.revenue))} против плана ${escapeHtml(formatMoney(current.planRevenue))}.` : "Задайте план по выручке, чтобы видеть отклонение."}
План-факт по расходам
${escapeHtml(planFact.spendStatus)}
${current.planSpend > 0 ? `Факт ${escapeHtml(formatMoney(current.totalSpend))} против лимита ${escapeHtml(formatMoney(current.planSpend))}.` : "Задайте лимит расходов, чтобы видеть перерасход."}
График за последние 6 месяцев
Здесь видно не только выручку и все расходы, но и из чего именно эти расходы состоят.
${buildChart(history)}
Что это значит простыми словами
${summaryLines.map((item) => `
${escapeHtml(item)}
`).join("")}
Сравнение по месяцам
${buildHistoryRows(history)}
Что смотреть в первую очередь
Если точка безубыточности выше вашей обычной выручки, бизнесу тяжело держаться в плюсе. Значит, надо или резать расходы, или поднимать продажи.
Если раздел показывает статус “Проверить”, первым делом смотрите, что именно растет быстрее выручки: реклама, зарплаты или прочие траты.
Комментарий к месяцу помогает потом не гадать, почему цифры изменились: акция, новый сотрудник, сезонность, аренда, налоги.
`;
const statusBox = document.getElementById("business-finance-status");
if (statusBox && historyLoadWarning) {
statusBox.textContent = historyLoadWarning;
statusBox.classList.remove("hidden");
}
document.getElementById("business-finance-month").addEventListener("change", (event) => {
renderModal(event.target.value);
});
document.getElementById("business-finance-save-btn").addEventListener("click", async () => {
const monthValue = document.getElementById("business-finance-month").value;
const statusBox = document.getElementById("business-finance-status");
try {
if (statusBox) {
statusBox.textContent = "";
statusBox.classList.add("hidden");
}
await upsertRecord({
month: monthValue,
revenue: document.getElementById("business-finance-revenue").value,
planRevenue: document.getElementById("business-finance-plan-revenue").value,
planSpend: document.getElementById("business-finance-plan-spend").value,
payroll: document.getElementById("business-finance-payroll").value,
rent: document.getElementById("business-finance-rent").value,
taxes: document.getElementById("business-finance-taxes").value,
adSpend: document.getElementById("business-finance-ad-spend").value,
otherSpend: document.getElementById("business-finance-other-spend").value,
note: document.getElementById("business-finance-note").value,
});
renderModal(monthValue);
} catch (error) {
if (statusBox) {
statusBox.textContent = String(error && error.message || "Не удалось сохранить месяц.");
statusBox.classList.remove("hidden");
}
}
});
document.getElementById("business-finance-pdf-btn").addEventListener("click", async () => {
const statusBox = document.getElementById("business-finance-status");
try {
if (statusBox) {
statusBox.textContent = "";
statusBox.classList.add("hidden");
}
await downloadPdf();
} catch (error) {
window.alert(String(error && error.message || "Не удалось скачать PDF."));
}
});
const originalPdfButton = document.getElementById("business-finance-pdf-btn");
if (originalPdfButton) {
const cleanPdfButton = originalPdfButton.cloneNode(true);
originalPdfButton.replaceWith(cleanPdfButton);
cleanPdfButton.addEventListener("click", async () => {
const statusBox = document.getElementById("business-finance-status");
if (statusBox) {
statusBox.textContent = "";
statusBox.classList.add("hidden");
}
try {
await downloadPdf();
} catch (error) {
let message = String(error && error.message || "Не удалось скачать PDF.");
if (message.includes("Not Found") || message.includes("404")) {
message = "PDF-модуль пока не опубликован на сервере. Нужно обновить только маршрут финансового PDF.";
} else if (message.includes("403")) {
message = "PDF по финансам бизнеса доступен только на тарифах Pro и Business.";
}
if (statusBox) {
statusBox.textContent = message;
statusBox.classList.remove("hidden");
}
}
});
}
}
async function openStandaloneBusinessFinanceModal() {
if (typeof window.openModal === "function") {
window.openModal("business-modal");
}
await ensureBusinessFinanceSubscription();
try {
await ensureHistoryLoaded();
} catch (error) {
historyLoadWarning = String(error && error.message || "Не удалось загрузить финансы бизнеса.");
historyCache = Array.isArray(historyCache) ? historyCache : [];
}
renderModal(currentMonthKey());
}
function bindButtons() {
["open-business-finance-btn", "tool-business-btn"].forEach((id) => {
const node = document.getElementById(id);
if (!node || node.dataset.businessFinanceBound === "1") return;
node.dataset.businessFinanceBound = "1";
node.addEventListener("click", openStandaloneBusinessFinanceModal);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindButtons, { once: true });
} else {
bindButtons();
}
window.openStandaloneBusinessFinanceModal = openStandaloneBusinessFinanceModal;
window.openBusinessMetricsModal = openStandaloneBusinessFinanceModal;
})();