сумма] — факт за текущий месяц (реальные transactions)
* @var int|null $pct_expense % бюджета по расходам за месяц (факт/план), null — если план не задан
* @var float $month_delta Дельта за месяц: доход − расход (может быть 0)
* @var object[] $goals Цели пользователя + данные счёта-копилки (account_title/account_summa/account_currency)
* @var array $rates [currency => float|null] курс ЦБ РФ по не-RUB валютам, встреченным среди счетов
*/
use App\Classes\Currency;
use System\Classes\CSRF;
// Цель savings-счёта по account_id — для предзаполнения полей цели в модалке редактирования
// (см. App/media/js/accounts.js) и для рабочего "Редактировать" в ⋮-меню карточки цели.
$goalsByAccount = [];
foreach ($goals as $g) {
$goalsByAccount[(int)$g->account_id] = $g;
}
// Тип счёта — граница между «Счета» и «Копилки и цели», а не наличие цели: savings — это
// копилка по определению (см. бюджет_текущий_план.md), с целью или без, поэтому в «Счета»
// (обычные cash/bank счета) не попадает вообще ни один savings.
$gridAccounts = array_filter($accounts, static fn ($a) => $a->type_acc !== 'savings');
$savingsAccounts = array_filter($accounts, static fn ($a) => $a->type_acc === 'savings');
// Полные данные счёта по id — карточка копилки (цикл по $savingsAccounts ниже) несёт те же
// data-* атрибуты, что и room-card, чтобы «Редактировать» работало без поиска несуществующего
// (скрытого из сетки) room-card в DOM.
$accountsById = [];
foreach ($accounts as $a) {
$accountsById[(int)$a->id] = $a;
}
// 'savings' тут нет намеренно — savings-счета больше не попадают в эту сетку (см. $gridAccounts
// ниже, они целиком переехали в «Копилки и цели» с другой иконкой, bi-piggy-bank).
$type_icons = [
'cash' => 'ic-wallet',
'bank' => 'ic-card',
];
$type_accents = [
'cash' => 'var(--teal)',
'bank' => 'var(--blue)',
'savings' => 'var(--purple)',
];
$type_labels = [
'cash' => 'Наличные',
'bank' => 'Банковский счёт',
'savings' => 'Копилка',
];
$weekdays = [1 => 'Понедельник', 2 => 'Вторник', 3 => 'Среда', 4 => 'Четверг', 5 => 'Пятница', 6 => 'Суббота', 7 => 'Воскресенье'];
$month_names = [
1 => 'январь', 2 => 'февраль', 3 => 'март', 4 => 'апрель', 5 => 'май', 6 => 'июнь',
7 => 'июль', 8 => 'август', 9 => 'сентябрь', 10 => 'октябрь', 11 => 'ноябрь', 12 => 'декабрь',
];
$month_names_gen = [
1 => 'января', 2 => 'февраля', 3 => 'марта', 4 => 'апреля', 5 => 'мая', 6 => 'июня',
7 => 'июля', 8 => 'августа', 9 => 'сентября', 10 => 'октября', 11 => 'ноября', 12 => 'декабря',
];
$ucfirstMb = static fn (string $s): string => mb_strtoupper(mb_substr($s, 0, 1)) . mb_substr($s, 1);
$this->setScript('accounts.js');
?>
Доступно сейчас
= number_format($total, 0, ',', ' ') ?> ₽
Всего по всем счетам = number_format($total_all, 0, ',', ' ') ?> ₽
Пока нет ни одного счёта — нажмите «+», чтобы добавить первый
color ?: ($type_accents[$account->type_acc] ?? 'var(--blue)');
$goal = $goalsByAccount[(int)$account->id] ?? null;
$fx_rate = $account->currency !== 'RUB' ? ($rates[$account->currency] ?? null) : null;
?>
= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?>
= $type_labels[$account->type_acc] ?? '' ?>
= number_format((float)$account->summa, 2, ',', ' ') ?>
= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
1 = htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?> = = number_format($fx_rate, 2, ',', ' ') ?> ₽
≈= number_format((float)$account->summa * $fx_rate, 0, ',', ' ') ?> ₽
Обновлён сегодня
Копилки и цели
id] ?? null;
$accent = $account->color ?: 'var(--purple)';
$r = 22;
$circumference = 2 * M_PI * $r;
if ($goal) {
$saved = (float)$account->summa;
$target = (float)$goal->target_summa;
$pct = $target > 0 ? min(100, $saved / $target * 100) : 0;
$offset = $circumference * (1 - $pct / 100);
}
?>
data-goal-id="= (int)$goal->id ?>"
data-title="= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?>"
data-type="= htmlspecialchars($account->type_acc, ENT_QUOTES, 'UTF-8') ?>"
data-summa="= htmlspecialchars((string)$account->summa, ENT_QUOTES, 'UTF-8') ?>"
data-currency="= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>"
data-description="= htmlspecialchars((string)($account->description ?? ''), ENT_QUOTES, 'UTF-8') ?>"
data-color="= htmlspecialchars((string)($account->color ?? ''), ENT_QUOTES, 'UTF-8') ?>"
data-privacy="= htmlspecialchars($account->privacy ?? 'shared', ENT_QUOTES, 'UTF-8') ?>"
data-include="= $account->include_in_total ? '1' : '' ?>"
data-goal-target="= $goal ? htmlspecialchars((string)$goal->target_summa, ENT_QUOTES, 'UTF-8') : '' ?>"
data-goal-deadline="= $goal ? htmlspecialchars((string)$goal->deadline, ENT_QUOTES, 'UTF-8') : '' ?>">
= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?>
= number_format($saved, 0, ',', ' ') ?>
из
= number_format($target, 0, ',', ' ') ?> = htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
= number_format((float)$account->summa, 0, ',', ' ') ?> = htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
$g->type === $type);
$ungrouped = array_filter($tree[0] ?? [], static fn ($c) => $c->type === $type);
if ($type_groups === [] && $ungrouped === []) {
echo 'Нет статей — добавьте их на странице «Статьи»
';
return;
}
// Строка план+факт+прогресс-бар — и для статьи внутри группы, и для статьи без группы.
// Перерасход (факт > плана) для расходов подсвечивается красным — для доходов это, наоборот,
// хорошо (получено больше плана), там всегда акцентный цвет.
$renderRow = static function (object $category, float $plan, float $fact, string $type): void {
$pct = $plan > 0 ? round($fact / $plan * 100) : ($fact > 0 ? 100 : 0);
$bar_width = min(100, $pct);
$over = $type === 'expense' && $fact > $plan && $plan > 0;
$pct_color = $over ? 'var(--red)' : 'var(--accent-tint)';
$bar_color = $over ? 'var(--red)' : 'var(--accent)';
echo ''
. '
' . htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') . ''
. '
'
. '
'
. '
' . number_format($fact, 0, ',', ' ') . ' из ' . number_format($plan, 0, ',', ' ') . ' ₽
'
. '
'
. '
' . $pct . '%
'
. '
';
};
echo '';
foreach ($type_groups as $group) {
$children = $tree[$group->id] ?? [];
$head_plan = array_sum(array_map(static fn ($c) => $plan_month[$c->id] ?? 0.0, $children));
echo '
'
. '' . htmlspecialchars($group->title, ENT_QUOTES, 'UTF-8') . ''
. '' . number_format($head_plan, 0, ',', ' ') . ' ₽'
. '
';
foreach ($children as $category) {
$renderRow($category, $plan_month[$category->id] ?? 0.0, $fact_month[$category->id] ?? 0.0, $type);
}
}
foreach ($ungrouped as $category) {
$renderRow($category, $plan_month[$category->id] ?? 0.0, $fact_month[$category->id] ?? 0.0, $type);
}
echo '
';
};
?>
Доходы
Бюджет на = $month_names[$month] ?? '' ?>
Архив пуст
= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?>
= number_format((float)$account->summa, 2, ',', ' ') ?>
= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>