151 lines
8.3 KiB
HTML
151 lines
8.3 KiB
HTML
<?php
|
||
/**
|
||
* @package Bicycle
|
||
* @description budgets.html — план бюджета на год по месяцам. Своя раскладка на каждое
|
||
* устройство (см. .claude/memory/бюджет_текущий_план.md → обсуждение дизайна):
|
||
* комп/планшет — карточки статьи с 12 месяцами (планшет — сеткой 6×2 вместо
|
||
* ряда, через container query); телефон — раскрывающиеся карточки-аккордeоны
|
||
* (все 12 месяцев по тапу). Ни один режим не даёт горизонтальный скролл.
|
||
* Факт (реальные transactions) здесь не показывается.
|
||
*
|
||
* @var array<int,object[]> $tree Активные статьи пользователя, сгруппированные по parent_id
|
||
* @var object[] $groups Верхнеуровневые группы (tree[0])
|
||
* @var array $plan [categorie_id => [month => ['plan_summ'=>float,'formula_pct_used'=>?float]]]
|
||
* @var int $year
|
||
*/
|
||
|
||
use System\Classes\CSRF;
|
||
|
||
$months = [
|
||
1 => 'Янв', 2 => 'Фев', 3 => 'Мар', 4 => 'Апр', 5 => 'Май', 6 => 'Июн',
|
||
7 => 'Июл', 8 => 'Авг', 9 => 'Сен', 10 => 'Окт', 11 => 'Ноя', 12 => 'Дек',
|
||
];
|
||
|
||
$sections = [
|
||
'income' => 'Доходы',
|
||
'expense' => 'Расходы',
|
||
];
|
||
|
||
// Категории, чей план считается автоматически по формуле связанной income-статьи —
|
||
// у них своя ячейка readonly (см. CategoriesController::parseFormulaLink()).
|
||
$formula_targets = [];
|
||
foreach ($tree as $items) {
|
||
foreach ($items as $c) {
|
||
if ($c->linked_expense_categorie_id) {
|
||
$formula_targets[(int)$c->linked_expense_categorie_id] = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Одна ячейка ввода — общая и для карточек (комп/планшет), и для аккордeона (телефон),
|
||
* чтобы имя поля/значение/readonly не разъезжались между двумя разметками одних данных.
|
||
*/
|
||
$planInput = static function (int $categorie_id, int $month, array $plan, bool $readonly) {
|
||
$value = $plan[$categorie_id][$month]['plan_summ'] ?? '';
|
||
echo '<input type="number" step="0.01" min="0" class="plan-input num" name="plan[' . $categorie_id . '][' . $month . ']"'
|
||
. ' value="' . htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8') . '"' . ($readonly ? ' readonly' : '') . '>';
|
||
};
|
||
|
||
$this->setScript('budgets.js');
|
||
?>
|
||
|
||
<div class="top-row">
|
||
<div>
|
||
<div class="hello">План бюджета на год</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||
<a class="btn btn-outline-light btn-sm" href="?year=<?= $year - 1 ?>">←</a>
|
||
<div class="balance num" style="font-size:2rem;"><?= $year ?></div>
|
||
<a class="btn btn-outline-light btn-sm" href="?year=<?= $year + 1 ?>">→</a>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="budgets-form" method="post" action="/budgets/save">
|
||
<?= CSRF::field() ?>
|
||
<input type="hidden" name="year" value="<?= $year ?>">
|
||
|
||
<?php foreach ($sections as $type => $label): ?>
|
||
<div class="section-label"><span><?= $label ?></span></div>
|
||
|
||
<?php $type_groups = array_filter($groups, static fn ($g) => $g->type === $type); ?>
|
||
|
||
<?php if (empty($type_groups)): ?>
|
||
<div class="empty-state">Нет статей — сначала добавьте их на странице «Статьи»</div>
|
||
<?php else: ?>
|
||
<div class="budget-zoom">
|
||
|
||
<!-- ---------- комп/планшет: карточки, 12 месяцев (планшет — сеткой 6×2) ---------- -->
|
||
<div class="budget-cards-wrap">
|
||
<?php foreach ($type_groups as $group): ?>
|
||
<?php $children = $tree[$group->id] ?? []; ?>
|
||
<div class="budget-plan-group">
|
||
<div class="budget-group-head" style="--card-accent:var(--blue)">
|
||
<span class="budget-group-name"><?= htmlspecialchars($group->title, ENT_QUOTES, 'UTF-8') ?></span>
|
||
</div>
|
||
<div class="budget-cards">
|
||
<?php foreach (($children !== [] ? $children : [$group]) as $category): ?>
|
||
<?php $is_target = isset($formula_targets[(int)$category->id]); ?>
|
||
<div class="budget-card">
|
||
<?php if ($children !== []): ?>
|
||
<div class="budget-card-name"><?= htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') ?></div>
|
||
<?php endif; ?>
|
||
<div class="budget-months-inline">
|
||
<?php foreach ($months as $m => $mlabel): ?>
|
||
<div class="budget-cell">
|
||
<div class="budget-cell-label"><?= $mlabel ?></div>
|
||
<?php $planInput((int)$category->id, $m, $plan, $is_target); ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- ---------- телефон: один аккордeон на статью верхнего уровня (группу или
|
||
самостоятельную статью), дети — подписями внутри того же аккордeона ---------- -->
|
||
<div class="budget-accordion">
|
||
<?php foreach ($type_groups as $group): ?>
|
||
<?php
|
||
$children = $tree[$group->id] ?? [];
|
||
$rows = $children !== [] ? $children : [$group];
|
||
$year_sum = 0.0;
|
||
foreach ($rows as $row_category) {
|
||
foreach ($months as $m => $_) {
|
||
$year_sum += (float)($plan[$row_category->id][$m]['plan_summ'] ?? 0);
|
||
}
|
||
}
|
||
?>
|
||
<details class="budget-acc-card">
|
||
<summary>
|
||
<span class="budget-acc-name"><?= htmlspecialchars($group->title, ENT_QUOTES, 'UTF-8') ?></span>
|
||
<span class="budget-acc-total num"><?= number_format($year_sum, 0, ',', ' ') ?> ₽/год</span>
|
||
</summary>
|
||
<?php foreach ($rows as $category): ?>
|
||
<?php $is_target = isset($formula_targets[(int)$category->id]); ?>
|
||
<?php if ($children !== []): ?>
|
||
<div class="budget-acc-caption"><?= htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') ?></div>
|
||
<?php endif; ?>
|
||
<div class="budget-acc-body">
|
||
<?php foreach ($months as $m => $mlabel): ?>
|
||
<div class="budget-acc-month">
|
||
<span><?= $mlabel ?></span>
|
||
<?php $planInput((int)$category->id, $m, $plan, $is_target); ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</details>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
|
||
<div id="budgets-form-error" class="alert alert-danger" style="display:none"></div>
|
||
</form>
|