сумма] — факт за текущий месяц (реальные 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'); ?>
,
100; ?> Бюджет % потрачено = 0; ?> 0 ? '+' : '') . number_format($month_delta, 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; ?>
title, ENT_QUOTES, 'UTF-8') ?>
type_acc] ?? '' ?>
summa, 2, ',', ' ') ?> currency, ENT_QUOTES, 'UTF-8') ?>
1 currency, ENT_QUOTES, 'UTF-8') ?> = ₽
≈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="id ?>" data-title="title, ENT_QUOTES, 'UTF-8') ?>" data-type="type_acc, ENT_QUOTES, 'UTF-8') ?>" data-summa="summa, ENT_QUOTES, 'UTF-8') ?>" data-currency="currency, ENT_QUOTES, 'UTF-8') ?>" data-description="description ?? ''), ENT_QUOTES, 'UTF-8') ?>" data-color="color ?? ''), ENT_QUOTES, 'UTF-8') ?>" data-privacy="privacy ?? 'shared', ENT_QUOTES, 'UTF-8') ?>" data-include="include_in_total ? '1' : '' ?>" data-goal-target="target_summa, ENT_QUOTES, 'UTF-8') : '' ?>" data-goal-deadline="deadline, ENT_QUOTES, 'UTF-8') : '' ?>"> %
title, ENT_QUOTES, 'UTF-8') ?>
из currency, ENT_QUOTES, 'UTF-8') ?> summa, 0, ',', ' ') ?> 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 '
'; }; ?>
Доходы
Бюджет на