Bicycle/App/view/Index/index.html
Egor Isaev c9b81cdcd1 dev
2026-08-14 13:09:54 +03:00

308 lines
17 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 в рублях
*/
use App\Classes\Currency;
use System\Classes\CSRF;
$type_icons = [
'cash' => 'ic-wallet',
'bank' => 'ic-card',
'savings' => 'ic-piggy',
];
$type_accents = [
'cash' => 'var(--teal)',
'bank' => 'var(--blue)',
'savings' => 'var(--purple)',
];
$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-piggy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="9" width="18" height="9" rx="4.5"/>
<line x1="9" y1="13.5" x2="14" y2="13.5"/>
<circle cx="17" cy="6" r="2"/>
<line x1="17" y1="8" x2="17" y2="9"/>
</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>
</defs>
</svg>
<div class="top-row">
<div>
<div class="hello">Здравствуйте, <?= htmlspecialchars($user['full_name'] ?? $user['login'], ENT_QUOTES, 'UTF-8') ?></div>
<div class="balance-label">Доступно сейчас</div>
<div class="balance num"><?= number_format($total, 0, ',', ' ') ?> <span>₽</span></div>
</div>
</div>
<div class="section-label">
<span>Счета</span>
<div style="display:flex;gap:0.5rem;">
<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($accounts)): ?>
<div class="empty-state">Пока нет ни одного счёта — нажмите «+», чтобы добавить первый</div>
<?php else: ?>
<div class="grid-cards">
<?php foreach ($accounts as $account): ?>
<div class="room-card"
style="--card-accent:<?= $type_accents[$account->type_acc] ?? 'var(--blue)' ?>"
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-include="<?= $account->include_in_total ? '1' : '' ?>">
<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 class="room-title"><?= htmlspecialchars($account->title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="room-value num">
<?= number_format((float)$account->summa, 2, ',', ' ') ?>
<?= htmlspecialchars($account->currency, ENT_QUOTES, 'UTF-8') ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php
$month_names = [
1 => 'январь', 2 => 'февраль', 3 => 'март', 4 => 'апрель', 5 => 'май', 6 => 'июнь',
7 => 'июль', 8 => 'август', 9 => 'сентябрь', 10 => 'октябрь', 11 => 'ноябрь', 12 => 'декабрь',
];
$groups = $tree[0] ?? [];
// Строки плана для секции типа $type (income|expense): группа с подкатегориями — заголовком
// группы + строками по ним; группа без подкатегорий — одной строкой на саму группу (тот же
// приём, что и в App/view/Budgets/budgets.html). Показываем только план — факта (реальных
// transactions) в проекте ещё нет, а прогресс-бар, который всегда 0%, только запутывает
// (см. фидбек пользователя); добавим план/факт, когда появятся операции. Замыкание, не
// именованная функция — шаблон может быть include'ится не один раз за процесс (например
// в тестах), именованная функция дала бы "Cannot redeclare".
$renderBudgetList = static function (array $groups, array $tree, array $plan_month, string $type): void {
$type_groups = array_filter($groups, static fn ($g) => $g->type === $type);
if ($type_groups === []) {
echo '<div class="empty-state">Нет статей — добавьте их на странице «Статьи»</div>';
return;
}
echo '<div class="budget-list">';
foreach ($type_groups as $group) {
$children = $tree[$group->id] ?? [];
// Заголовок-плашка — ВСЕГДА, и у настоящей группы, и у самостоятельной статьи (тогда
// плашка — единственная строка на неё, показывает сразу и название, и сумму). Без этого
// самостоятельная статья визуально не отличалась от рядового члена предыдущей группы
// (см. фидбек пользователя — тонкой разницы фона недостаточно, нужна однозначная граница).
$head_plan = $children !== []
? array_sum(array_map(static fn ($c) => $plan_month[$c->id] ?? 0.0, $children))
: ($plan_month[$group->id] ?? 0.0);
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) {
$plan = $plan_month[$category->id] ?? 0.0;
echo '<div class="budget-row">'
. '<div class="budget-name">' . htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') . '</div>'
. '<div class="budget-plan-amount num">' . number_format($plan, 0, ',', ' ') . ' ₽</div>'
. '</div>';
}
}
echo '</div>';
};
?>
<div class="section-label"><span>Доходы</span></div>
<?php $renderBudgetList($groups, $tree, $plan_month, 'income'); ?>
<div class="section-label"><span>Бюджет на <?= $month_names[$month] ?? '' ?></span></div>
<?php $renderBudgetList($groups, $tree, $plan_month, 'expense'); ?>
<div class="modal fade" id="account-modal" tabindex="-1" data-bs-theme="dark">
<div class="modal-dialog">
<div class="modal-content" style="background:var(--surface-2);color:var(--ink);">
<form id="account-form" method="post">
<?= CSRF::field() ?>
<input type="hidden" name="id">
<div class="modal-header" style="border-color:var(--surface-3)">
<h5 class="modal-title" id="account-modal-title">Новый счёт</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div id="account-form-error" class="alert alert-danger" style="display:none"></div>
<div class="mb-3">
<label class="form-label">Название</label>
<input type="text" class="form-control" name="title" maxlength="128" required>
</div>
<div class="mb-3">
<label class="form-label">Тип</label>
<select class="form-select" name="type_acc">
<option value="cash">Кошелёк (наличные)</option>
<option value="bank">Банковский счёт</option>
<option value="savings">Копилка</option>
</select>
</div>
<div class="row">
<div class="col-7 mb-3">
<label class="form-label">Сумма</label>
<input type="number" step="0.01" min="0" class="form-control" name="summa" required>
</div>
<div class="col-5 mb-3">
<label class="form-label">Валюта</label>
<select class="form-select" name="currency">
<?php foreach (Currency::LIST as $code => $name): ?>
<option value="<?= $code ?>"<?= $code === 'RUB' ? ' selected' : '' ?> title="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"><?= $code ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mb-3" id="account-rate-group">
<div id="account-rate-field" style="display:none">
<label class="form-label">Ставка, % годовых</label>
<input type="number" step="0.01" min="0" class="form-control" name="rate_percent">
</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<input type="text" class="form-control" name="description" maxlength="255">
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="include_in_total" value="1" id="account-include" checked>
<label class="form-check-label" for="account-include">Учитывать в «Доступно сейчас»</label>
</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="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>