Bicycle/App/view/Index/index.html
2026-08-15 10:39:22 +03:00

675 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Bicycle
* @description index.html — дашборд/«Обзор» (главная страница после входа). Дизайн — по
* согласованному макету (тёмная тема, room-card, dock-навбар), рендерится
* внутри layout.html через Controller::render().
*
* @var array $user Текущий авторизованный пользователь (без пароля)
* @var object[] $accounts Счета пользователя (accounts, не архивные)
* @var object[] $archived_accounts Архивные счета пользователя (is_delete=1)
* @var float $total Сумма счетов с include_in_total=1 в рублях
* @var float $total_all Сумма всех (не архивных) счетов в рублях, без фильтра include_in_total
* @var array $fact_month [categorie_id => сумма] — факт за текущий месяц (реальные 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');
?>
<svg width="0" height="0" style="position:absolute">
<defs>
<symbol id="ic-wallet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="6" width="20" height="14" rx="2"/>
<path d="M16 12h4v4h-4a2 2 0 0 1 0-4Z"/>
</symbol>
<symbol id="ic-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="5" width="20" height="14" rx="2"/>
<line x1="2" y1="10" x2="22" y2="10"/>
</symbol>
<symbol id="ic-move" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="5 9 2 12 5 15"/>
<polyline points="9 5 12 2 15 5"/>
<polyline points="15 19 12 22 9 19"/>
<polyline points="19 9 22 12 19 15"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<line x1="12" y1="2" x2="12" y2="22"/>
</symbol>
<symbol id="ic-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</symbol>
<symbol id="ic-dots" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="5" r="1.6"/>
<circle cx="12" cy="12" r="1.6"/>
<circle cx="12" cy="19" r="1.6"/>
</symbol>
<symbol id="ic-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>
</symbol>
<symbol id="ic-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
</symbol>
<symbol id="ic-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 10.5 12 3l9 7.5"/>
<path d="M5 9.5V20a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V9.5"/>
</symbol>
<symbol id="ic-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2 2 12l9 9 11-11V2Z"/>
<circle cx="7.5" cy="7.5" r="1.5"/>
</symbol>
<symbol id="ic-swap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 7h11l-3-3"/>
<path d="M17 17H6l3 3"/>
</symbol>
<symbol id="ic-target" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<circle cx="12" cy="12" r="5"/>
<circle cx="12" cy="12" r="1.5"/>
</symbol>
<symbol id="ic-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="5" rx="1"/>
<path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9"/>
<line x1="10" y1="13" x2="14" y2="13"/>
</symbol>
<symbol id="ic-undo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 10h10a5 5 0 0 1 0 10H8"/>
<path d="M8 5 3 10l5 5"/>
</symbol>
<symbol id="ic-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2"/>
<path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="2" y1="13" x2="22" y2="13"/>
</symbol>
<symbol id="ic-bolt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8Z"/>
</symbol>
<symbol id="ic-bank" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 10 12 4l9 6"/>
<path d="M5 10v9"/>
<path d="M10 10v9"/>
<path d="M14 10v9"/>
<path d="M19 10v9"/>
<path d="M3 21h18"/>
</symbol>
<symbol id="ic-car" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 17V12l2-5h10l2 5v5"/>
<line x1="5" y1="12" x2="19" y2="12"/>
<circle cx="7" cy="17" r="2"/>
<circle cx="17" cy="17" r="2"/>
</symbol>
<symbol id="ic-coin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="12" y1="6" x2="12" y2="18"/>
<path d="M9 15c0 1.1 1.3 2 3 2s3-.9 3-2-1.3-1.5-3-2-3-.9-3-2 1.3-2 3-2 3 .9 3 2"/>
</symbol>
</defs>
</svg>
<div class="dash-header">
<div class="dash-kicker"><?= $weekdays[(int)date('N')] ?>, <?= (int)date('j') ?> <?= $month_names_gen[(int)date('n')] ?></div>
<div class="dash-tags">
<?php $over = ($pct_expense ?? 0) > 100; ?>
<span class="tag" style="background:color-mix(in srgb, <?= $over ? 'var(--red)' : 'oklch(0.80 0.18 90)' ?> 20%, transparent);color:<?= $over ? 'var(--red)' : 'oklch(0.85 0.14 90)' ?>">Бюджет <?= $pct_expense ?? 0 ?>% потрачено</span>
<?php $delta_pos = $month_delta >= 0; ?>
<span class="tag" style="background:color-mix(in srgb, <?= $delta_pos ? 'var(--positive)' : 'var(--red)' ?> 20%, transparent);color:<?= $delta_pos ? 'var(--positive-text)' : 'var(--red)' ?>"><?= $delta_pos ? '↗' : '↘' ?> <?= ($delta_pos && $month_delta > 0 ? '+' : '') . number_format($month_delta, 0, ',', ' ') ?> ₽ за месяц</span>
</div>
</div>
<div class="balance-block">
<div class="balance-label">Доступно сейчас</div>
<div class="balance num"><?= number_format($total, 0, ',', ' ') ?> ₽</div>
<div class="balance-total num">Всего по всем счетам <span><?= number_format($total_all, 0, ',', ' ') ?> ₽</span></div>
</div>
<div class="section-label">
<span>Счета</span>
<div style="display:flex;gap:0.5rem">
<button type="button" class="btn-add js-account-reorder-toggle" title="Изменить порядок счетов"><svg class="ico"><use href="#ic-move"/></svg></button>
<button type="button" class="btn-add js-account-archive-open" title="Архив счетов"><svg class="ico"><use href="#ic-archive"/></svg></button>
<button type="button" class="btn-add js-account-add" title="Добавить счёт"><svg class="ico"><use href="#ic-plus"/></svg></button>
</div>
</div>
<?php if (empty($gridAccounts)): ?>
<div class="empty-state">Пока нет ни одного счёта — нажмите «+», чтобы добавить первый</div>
<?php else: ?>
<div class="grid-cards">
<?php foreach ($gridAccounts as $account): ?>
<?php
$accent = $account->color ?: ($type_accents[$account->type_acc] ?? 'var(--blue)');
$goal = $goalsByAccount[(int)$account->id] ?? null;
$fx_rate = $account->currency !== 'RUB' ? ($rates[$account->currency] ?? null) : null;
?>
<div class="room-card"
style="--card-accent:<?= htmlspecialchars($accent, ENT_QUOTES, 'UTF-8') ?>"
data-id="<?= (int)$account->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') : '' ?>">
<div class="room-card-head">
<span class="room-icon"><svg class="ico"><use href="#<?= $type_icons[$account->type_acc] ?? 'ic-wallet' ?>"/></svg></span>
<div class="room-menu">
<button type="button" class="room-menu-btn">
<svg class="ico"><use href="#ic-dots"/></svg>
</button>
<div class="room-menu-list">
<a href="#" class="js-account-edit"><svg class="ico ico-sm"><use href="#ic-pencil"/></svg>Редактировать</a>
<a href="#" class="danger js-account-delete"><svg class="ico ico-sm"><use href="#ic-trash"/></svg>Удалить</a>
</div>
</div>
</div>
<div>
<div class="room-title"><?= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="room-sub"><?= $type_labels[$account->type_acc] ?? '' ?></div>
</div>
<div class="room-value num">
<?= number_format((float)$account->summa, 2, ',', ' ') ?>
<?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php if ($fx_rate !== null): ?>
<div class="card-meta" style="font-size:11px">1 <?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?> = <?= number_format($fx_rate, 2, ',', ' ') ?> ₽</div>
<div class="card-meta" style="font-size:11px">≈<?= number_format((float)$account->summa * $fx_rate, 0, ',', ' ') ?> ₽</div>
<?php endif; ?>
<div class="acc-dot"><span></span>Обновлён сегодня</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($savingsAccounts)): ?>
<div class="section-label"><span>Копилки и цели</span></div>
<div class="grid-cards-goals" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin-bottom:2rem">
<?php foreach ($savingsAccounts as $account): ?>
<?php
$goal = $goalsByAccount[(int)$account->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);
}
?>
<div class="room-card goal-card" style="padding:16px 18px;flex-direction:row;align-items:center;gap:14px;--card-accent:<?= htmlspecialchars($accent, ENT_QUOTES, 'UTF-8') ?>;position:relative"
data-id="<?= (int)$account->id ?>"
<?php if ($goal): ?>data-goal-id="<?= (int)$goal->id ?>"<?php endif; ?>
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') : '' ?>">
<?php if ($goal): ?>
<svg width="52" height="52" viewBox="0 0 52 52" style="flex-shrink:0">
<circle cx="26" cy="26" r="<?= $r ?>" fill="none" stroke="var(--surface-3)" stroke-width="5"/>
<circle cx="26" cy="26" r="<?= $r ?>" fill="none" stroke="<?= htmlspecialchars($accent, ENT_QUOTES, 'UTF-8') ?>" stroke-width="5" stroke-linecap="round"
stroke-dasharray="<?= round($circumference, 2) ?>" stroke-dashoffset="<?= round($offset, 2) ?>"
transform="rotate(-90 26 26)"/>
<text x="26" y="30" text-anchor="middle" font-size="13" fill="var(--ink)"><?= round($pct) ?>%</text>
</svg>
<?php else: ?>
<span class="room-icon" style="width:52px;height:52px;flex-shrink:0"><i class="bi bi-piggy-bank" style="font-size:1.4em"></i></span>
<?php endif; ?>
<div style="flex:1">
<div class="card-title" style="font-size:15px"><?= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="card-meta" style="font-size:12px">
<?php if ($goal): ?>
<span class="num" style="color:var(--ink)"><?= number_format($saved, 0, ',', ' ') ?></span>
из
<span class="num"><?= number_format($target, 0, ',', ' ') ?> <?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?></span>
<?php else: ?>
<span class="num" style="color:var(--ink)"><?= number_format((float)$account->summa, 0, ',', ' ') ?> <?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
</div>
<div class="room-menu">
<button type="button" class="room-menu-btn"><svg class="ico"><use href="#ic-dots"/></svg></button>
<div class="room-menu-list">
<a href="#" class="js-account-edit"><svg class="ico ico-sm"><use href="#ic-pencil"/></svg>Редактировать</a>
<?php if ($goal): ?>
<a href="#" class="danger js-goal-delete"><svg class="ico ico-sm"><use href="#ic-trash"/></svg>Удалить цель</a>
<?php else: ?>
<a href="#" class="danger js-account-delete"><svg class="ico ico-sm"><use href="#ic-trash"/></svg>Удалить</a>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
// Строки плана+факта для секции типа $type (income|expense): у каждой настоящей группы
// (App\Repositories\CategoryGroupRepository — самостоятельная сущность, не статья с детьми) —
// заголовок-плашка + строки по её статьям; статьи без группы ($tree[0]) — сразу строками, без
// плашки. Факт — реальные transactions за месяц ($fact_month, 0 пока операций нет — это
// корректное значение, не заглушка). Замыкание, не именованная функция — шаблон может быть
// include'ится не один раз за процесс (например в тестах), именованная функция дала бы
// "Cannot redeclare".
$renderBudgetList = static function (array $groups, array $tree, array $plan_month, array $fact_month, string $type): void {
$type_groups = array_filter($groups, static fn ($g) => $g->type === $type);
$ungrouped = array_filter($tree[0] ?? [], static fn ($c) => $c->type === $type);
if ($type_groups === [] && $ungrouped === []) {
echo '<div class="empty-state">Нет статей — добавьте их на странице «Статьи»</div>';
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 '<div class="stat-row">'
. '<span class="stat-name">' . htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') . '</span>'
. '<div>'
. '<div class="budget-bar"><div class="budget-bar-fill" style="width:' . $bar_width . '%;background:' . $bar_color . '"></div></div>'
. '<div class="stat-caption num">' . number_format($fact, 0, ',', ' ') . ' из ' . number_format($plan, 0, ',', ' ') . ' ₽</div>'
. '</div>'
. '<div style="text-align:right"><span class="num" style="color:' . $pct_color . '">' . $pct . '%</span></div>'
. '</div>';
};
echo '<div class="card elev-md pm-wrap">';
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 '<div class="budget-group-head" style="--card-accent:var(--blue)">'
. '<span class="budget-group-name">' . htmlspecialchars($group->title, ENT_QUOTES, 'UTF-8') . '</span>'
. '<span class="budget-group-sum num">' . number_format($head_plan, 0, ',', ' ') . ' ₽</span>'
. '</div>';
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 '</div>';
};
?>
<div class="section-label"><span>Доходы</span></div>
<?php $renderBudgetList($groups, $tree, $plan_month, $fact_month, 'income'); ?>
<div class="section-label"><span>Бюджет на <?= $month_names[$month] ?? '' ?></span></div>
<?php $renderBudgetList($groups, $tree, $plan_month, $fact_month, 'expense'); ?>
<div class="modal fade" id="account-modal" tabindex="-1" data-bs-theme="dark">
<div class="modal-dialog modal-dialog-scrollable" style="max-width:520px">
<div class="modal-content" style="background:var(--surface);color:var(--ink);border-radius:var(--radius-lg)">
<div class="modal-header" style="border-color:var(--surface-3);align-items:flex-start">
<div style="display:flex;gap:12px;align-items:flex-start">
<span class="dialog-icon"><svg class="ico" style="width:1.3em;height:1.3em"><use href="#ic-wallet"/></svg></span>
<div>
<h5 class="modal-title" id="account-modal-title">Новый счёт</h5>
<div class="dialog-sub">Расчётный, накопительный, наличные или кредит</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<form id="account-form" method="post">
<?= CSRF::field() ?>
<input type="hidden" name="id">
<div class="modal-body">
<div id="account-form-error" class="alert-error" style="display:none"></div>
<div class="dialog-section-label" style="margin-top:0">Тип счёта</div>
<div class="pm-tiles-grid" id="acc-type-tiles" style="grid-template-columns:1fr 1fr">
<div class="pm-tile selected" data-type="bank" data-color="oklch(0.80 0.18 90)" data-icon="ic-card">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.80 0.18 90) 20%,transparent);color:oklch(0.80 0.18 90)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-card"/></svg></span>
<div><div class="pm-tile-title">Расчётный</div><div class="pm-tile-desc">Повседневные траты и доходы</div></div>
</div>
<div class="pm-tile" data-type="cash" data-color="var(--positive)" data-icon="ic-wallet">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,var(--positive) 20%,transparent);color:var(--positive)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-wallet"/></svg></span>
<div><div class="pm-tile-title">Наличные</div><div class="pm-tile-desc">Деньги на руках</div></div>
</div>
<div class="pm-tile" data-type="savings" data-color="oklch(0.78 0.17 230)" data-icon="ic-bank">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.78 0.17 230) 20%,transparent);color:oklch(0.78 0.17 230)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-bank"/></svg></span>
<div><div class="pm-tile-title">Накопительный</div><div class="pm-tile-desc">Копилка с целью</div></div>
</div>
<div class="pm-tile" data-type="credit" data-color="oklch(0.72 0.19 25)" data-icon="ic-card">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.72 0.19 25) 20%,transparent);color:oklch(0.72 0.19 25)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-card"/></svg></span>
<div><div class="pm-tile-title">Кредит / Карта</div><div class="pm-tile-desc">Кредит, ипотека, кредитка</div></div>
</div>
</div>
<input type="hidden" name="type_acc" id="acc-type-hidden" value="bank">
<div class="dialog-section-label">Параметры</div>
<div style="display:flex;gap:10px;align-items:center;position:relative">
<div style="position:relative">
<button type="button" id="acc-color-swatch" style="background:oklch(0.80 0.18 90);width:44px;height:44px;border-radius:50%;border:2px solid var(--divider);cursor:pointer;flex-shrink:0"></button>
<div id="acc-color-picker" class="card elev-lg" style="display:none;position:absolute;top:52px;left:0;z-index:20;padding:12px;gap:8px;width:170px">
<div style="font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:color-mix(in srgb, var(--ink) 55%, transparent)">Цвет</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px" id="acc-color-swatches">
<?php foreach ([
'oklch(0.72 0.19 25)', 'var(--positive)', 'oklch(0.78 0.17 230)', 'oklch(0.80 0.18 90)',
'oklch(0.72 0.19 330)', 'oklch(0.75 0.16 200)', 'oklch(0.74 0.18 55)', 'oklch(0.76 0.17 290)',
] as $swatch): ?>
<button type="button" class="acc-color-swatch-opt" data-color="<?= htmlspecialchars($swatch, ENT_QUOTES, 'UTF-8') ?>"
style="width:28px;height:28px;border-radius:50%;background:<?= htmlspecialchars($swatch, ENT_QUOTES, 'UTF-8') ?>;border:2px solid transparent;cursor:pointer;padding:0"></button>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="field" style="flex:1;margin:0">
<label>Название</label>
<input class="input" type="text" name="title" maxlength="128" placeholder="Напр. Основная карта" required>
</div>
<span id="acc-icon-preview" style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb,oklch(0.80 0.18 90) 20%,transparent);color:oklch(0.80 0.18 90);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ico" style="width:1.05em;height:1.05em"><use href="#ic-card" id="acc-icon-preview-use"/></svg>
</span>
<input type="hidden" name="color" id="acc-color-hidden" value="">
</div>
<div style="display:flex;gap:10px;margin-top:10px">
<div class="field" style="flex:1;margin:0">
<label>Сумма</label>
<input class="input" type="number" step="0.01" min="0" name="summa" placeholder="0" required>
</div>
<div style="width:110px">
<div class="field" style="margin:0">
<label>Валюта</label>
<select class="input" name="currency">
<?php foreach (Currency::LIST as $code => $name): ?>
<option value="<?= $code ?>"<?= $code === 'RUB' ? ' selected' : '' ?>><?= $code ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div id="account-rate-field" style="display:none;margin-top:10px">
<div class="field" style="margin:0">
<label>Ставка, % годовых</label>
<input class="input" type="number" step="0.01" min="0" name="rate_percent" placeholder="0">
</div>
</div>
<div id="account-goal-fields" style="display:none;margin-top:10px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="field" style="margin:0">
<label>Цель накопления</label>
<input class="input" type="number" step="0.01" min="0" name="goal_target_summa" placeholder="100 000">
</div>
<div class="field" style="margin:0">
<label>Дата цели</label>
<input class="input" type="date" name="goal_deadline">
</div>
</div>
</div>
<div class="field" style="margin-top:10px">
<label>Описание (необязательно)</label>
<input class="input" type="text" name="description" maxlength="255">
</div>
<div style="margin-top:16px">
<div class="dialog-section-label" style="margin:0 0 8px">Общий баланс</div>
<div class="seg">
<label class="seg-opt"><input type="radio" name="include_in_total" value="1" checked>Учитывать</label>
<label class="seg-opt"><input type="radio" name="include_in_total" value="0">Не учитывать</label>
</div>
</div>
</div>
<div class="modal-footer" style="border-color:var(--surface-3)">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="credit-modal" tabindex="-1" data-bs-theme="dark">
<div class="modal-dialog modal-dialog-scrollable" style="max-width:560px">
<div class="modal-content" style="background:var(--surface);color:var(--ink);border-radius:var(--radius-lg)">
<div class="modal-header" style="border-color:var(--surface-3);align-items:flex-start">
<div style="display:flex;gap:12px;align-items:flex-start">
<span class="dialog-icon"><svg class="ico" style="width:1.3em;height:1.3em"><use href="#ic-bolt"/></svg></span>
<div>
<h5 class="modal-title">Новый кредит</h5>
<div class="dialog-sub">Раздел «Кредиты» · банковский кредит или карта</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="alert-error" style="background:color-mix(in srgb, var(--accent) 12%, transparent);border-color:color-mix(in srgb, var(--accent) 30%, transparent);color:var(--accent-tint);margin-bottom:14px">
Раздел «Кредиты» пока в разработке — форма показывает, как это будет выглядеть, но
ничего не сохраняет.
</div>
<div class="dialog-section-label" style="margin-top:0">Что добавляем</div>
<div class="pm-tiles-grid" id="credit-mode-tiles" style="grid-template-columns:1fr 1fr">
<div class="pm-tile selected" data-mode="new">
<span class="pm-check">✓</span>
<div><div class="pm-tile-title">Новый кредит</div><div class="pm-tile-desc">Только что оформил</div></div>
</div>
<div class="pm-tile" data-mode="existing">
<span class="pm-check">✓</span>
<div><div class="pm-tile-title">Уже выплачиваю</div><div class="pm-tile-desc">Завести по остатку</div></div>
</div>
</div>
<div class="dialog-section-label">Тип кредита</div>
<div class="pm-tiles-grid" id="credit-type-tiles" style="grid-template-columns:1fr 1fr">
<div class="pm-tile selected" data-credit-type="consumer">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.80 0.18 90) 20%,transparent);color:oklch(0.80 0.18 90)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-card"/></svg></span>
<div><div class="pm-tile-title">Потребительский</div><div class="pm-tile-desc">Обычный кредит с графиком платежей</div></div>
</div>
<div class="pm-tile" data-credit-type="mortgage">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.72 0.19 25) 20%,transparent);color:oklch(0.72 0.19 25)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-home"/></svg></span>
<div><div class="pm-tile-title">Ипотека</div><div class="pm-tile-desc">Долгий кредит под залог жилья</div></div>
</div>
<div class="pm-tile" data-credit-type="auto">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.72 0.19 25) 20%,transparent);color:oklch(0.72 0.19 25)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-car"/></svg></span>
<div><div class="pm-tile-title">Автокредит</div><div class="pm-tile-desc">Кредит на покупку автомобиля</div></div>
</div>
<div class="pm-tile" data-credit-type="card">
<span class="pm-check">✓</span>
<span class="pm-tile-icon" style="background:color-mix(in srgb,oklch(0.80 0.18 90) 20%,transparent);color:oklch(0.80 0.18 90)"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#ic-coin"/></svg></span>
<div><div class="pm-tile-title">Кредитная карта</div><div class="pm-tile-desc">Лимит, льготный период, мин. платёж</div></div>
</div>
</div>
<div id="credit-existing-hint" style="display:none;font-size:12px;color:var(--muted);margin:8px 0 0;padding:10px 12px;background:var(--surface-2);border-radius:var(--radius-md)">
Заводим уже идущий кредит — укажите остаток долга и дату, на которую он верен.
Историю пересчитывать не нужно.
</div>
<div class="dialog-section-label">Основные параметры</div>
<div class="field"><label>Название</label><input class="input" type="text" placeholder="Напр. Тачка"></div>
<div id="credit-card-fields" style="display:none">
<div style="display:flex;gap:10px;margin-top:10px">
<div class="field" style="flex:1;margin:0"><label>Кредитный лимит *</label><input class="input" type="number" min="0"></div>
<div style="width:110px"><div class="field" style="margin:0"><label>Валюта</label>
<select class="input"><?php foreach (Currency::LIST as $code => $name): ?><option value="<?= $code ?>"<?= $code === 'RUB' ? ' selected' : '' ?>><?= $code ?></option><?php endforeach; ?></select>
</div></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>Льготный период, дней</label><input class="input" type="number" min="0"></div>
<div class="field" style="margin:0"><label>Ставка годовая, % *</label><input class="input" type="number" min="0" step="0.01"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>Мин. платёж</label><input class="input" type="number" min="0"></div>
<div class="field" style="margin:0"><label>Текущий баланс карты</label><input class="input" type="number" min="0"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>День платежа</label><input class="input" type="number" min="1" max="31"></div>
<div class="field" style="margin:0"><label>Следующий платёж</label><input class="input" type="date"></div>
</div>
</div>
<div id="credit-noncard-fields">
<div style="display:flex;gap:10px;margin-top:10px">
<div class="field" style="flex:1;margin:0"><label>Сумма кредита *</label><input class="input" type="number" min="0"></div>
<div style="width:110px"><div class="field" style="margin:0"><label>Валюта</label>
<select class="input"><?php foreach (Currency::LIST as $code => $name): ?><option value="<?= $code ?>"<?= $code === 'RUB' ? ' selected' : '' ?>><?= $code ?></option><?php endforeach; ?></select>
</div></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>Ставка годовая, % *</label><input class="input" type="number" min="0" step="0.01"></div>
<div class="field" style="margin:0"><label>Срок, мес.</label><input class="input" type="number" min="0"></div>
<div class="field" style="margin:0"><label>Ежемесячный платёж</label><input class="input" type="number" min="0"></div>
</div>
<div id="credit-existing-fields" style="display:none;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>Остаток долга *</label><input class="input" type="number" min="0"></div>
<div class="field" style="margin:0"><label>Остаток актуален на *</label><input class="input" type="date"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
<div class="field" style="margin:0"><label>День платежа</label><input class="input" type="number" min="1" max="31"></div>
<div class="field" style="margin:0"><label>Следующий платёж</label><input class="input" type="date"></div>
</div>
<div class="field" style="margin-top:10px">
<label>Способ расчёта</label>
<select class="input"><option>Аннуитет</option><option>Дифференцированный</option></select>
</div>
</div>
<div class="field" style="margin-top:10px"><label>Дата выдачи</label><input class="input" type="date"></div>
</div>
<div class="modal-footer" style="border-color:var(--surface-3);align-items:center;justify-content:space-between">
<div class="card-meta" style="font-size:11px" id="credit-min-hint">Минимум нужно: <strong style="color:var(--ink)">название, сумма, ставка и срок или платёж</strong></div>
<div style="display:flex;gap:8px">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Создать кредит</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="archive-modal" tabindex="-1" data-bs-theme="dark">
<div class="modal-dialog">
<div class="modal-content" style="background:var(--surface-2);color:var(--ink);">
<div class="modal-header" style="border-color:var(--surface-3)">
<h5 class="modal-title">Архив счетов</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<?php if (empty($archived_accounts)): ?>
<div class="empty-state">Архив пуст</div>
<?php else: ?>
<?php foreach ($archived_accounts as $account): ?>
<div class="archive-row" data-id="<?= (int)$account->id ?>" data-title="<?= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?>">
<div>
<div class="room-title"><?= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="room-sub num">
<?= number_format((float)$account->summa, 2, ',', ' ') ?>
<?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-light js-account-restore" title="Восстановить">
<svg class="ico"><use href="#ic-undo"/></svg>
Восстановить
</button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>