From ac1334c7845a1b3c2ef628ef565c4a659cb2f403 Mon Sep 17 00:00:00 2001 From: Egor Isaev Date: Fri, 14 Aug 2026 14:05:41 +0300 Subject: [PATCH] dev --- .claude/memory/MEMORY.md | 2 +- .../reference_dashboard_design_artifact.md | 94 +- App/Controller/IndexController.php | 41 +- App/Repositories/GoalRepository.php | 29 + App/Repositories/TransactionRepository.php | 31 + App/design_handoff_personal_finance/README.md | 34 + .../nocturne.css | 294 +++++ .../Личные финансы.dc.html | 1035 +++++++++++++++++ App/media/css/dashboard.css | 225 +++- App/view/Index/index.html | 116 +- App/view/layout.html | 24 +- 11 files changed, 1844 insertions(+), 81 deletions(-) create mode 100644 App/design_handoff_personal_finance/README.md create mode 100644 App/design_handoff_personal_finance/nocturne.css create mode 100644 App/design_handoff_personal_finance/Личные финансы.dc.html diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 0994a63..38c905d 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -19,4 +19,4 @@ - [Бюджет — актуальный план (2026)](бюджет_текущий_план.md) — что реально согласовано в разговоре; старый бюджет_проект_план.md — только источник идей, не требований - [Не переиспользовать память молча после сброса](feedback_no_silent_memory_reuse_after_reset.md) — после "снеси всё, начнём заново" каждое решение переподтверждается заново, даже если уже записано - [Не лезть в реализацию раньше времени](feedback_no_premature_implementation.md) — пока фича «Бюджет» обсуждается, решения только в план-файле, schema.sql/код не трогать без явного запроса -- [Макет дашборда (Claude Artifact)](reference_dashboard_design_artifact.md) — ссылка на согласованный визуальный дизайн (тёмная тема, room-card, dock-навбар), сверяться при работе над UI +- [Визуальный дизайн (Nocturne)](reference_dashboard_design_artifact.md) — актуальная палитра/токены проекта (App/media/css/dashboard.css), сверяться при работе над UI diff --git a/.claude/memory/reference_dashboard_design_artifact.md b/.claude/memory/reference_dashboard_design_artifact.md index 6ed8664..d3097a7 100644 --- a/.claude/memory/reference_dashboard_design_artifact.md +++ b/.claude/memory/reference_dashboard_design_artifact.md @@ -1,27 +1,85 @@ --- name: reference-dashboard-design-artifact -description: Ссылка на согласованный визуальный макет дашборда «Бюджет» (тёмная тема, room-card, dock-навбар) — сверяться при любой работе над UI +description: Визуальный дизайн проекта — палитра/токены Nocturne (2026-08-14, поверх старого макета room-card/dock-навбар) — сверяться при любой работе над UI metadata: type: reference --- -Макет дашборда согласован заранее (в другой сессии) как Claude Artifact: -`https://claude.ai/code/artifact/55e7f715-2ee7-45e2-acb7-82b883fcad02` («Бюджет — обзор (макет)»). +**Актуальная версия (2026-08-14).** Пользователь принёс новый hi-fi дизайн-хендофф — сгенерирован +внешним дизайн-инструментом («clode design»), лежит в `App/design_handoff_personal_finance/` +(`Личные финансы.dc.html`, `nocturne.css`, `README.md`). Система называется **Nocturne** (тёмная, +mono-accent, `--color-accent:#9184d9`, шрифт Inter в оригинале). Хендофф покрывает 4 экрана: +Дашборд, План на год, Транзакции, Аналитика — из них в проекте на момент разговора реально +существовали только первые два (Транзакции — есть только `TransactionRepository`, без +контроллера/view; Аналитика — не существует вообще). -Ключевые элементы дизайна оттуда, уже перенесённые в `App/media/css/dashboard.css` и -`App/view/Index/index.html`/`App/view/layout.html`: -- Тёмная тема (`--bg:#0b0b0f`, `--surface:#17171d`, акцентные цвета per-карточка через - `--card-accent`), inline SVG-иконки (не Bootstrap Icons) для навигации/типов счетов. -- Карточки-«комнаты» (`.room-card`) для счетов, кольца прогресса (`.ring`) для целей копилок, - списки `.budget-list`/`.budget-row` с прогресс-баром для план/факта по категориям. -- Нижний dock-навбар (`.dock`) вместо верхнего меню: Обзор/Счета/Категории/Операции/Цели. -- Верхний блок — «Доступно сейчас» + «Всего по всем счетам», строка stat-pills (дата/% бюджета/ - изменение за месяц/курс валюты) — в реализации пока упрощена (нет данных по бюджету/курсу). +**Решено с пользователем, что делаем сейчас (см. также [[budget-current-plan]] по фиче):** +1. **Только визуальная система** — токены/палитра/радиусы/тени/шрифт поверх уже существующих + экранов (Дашборд, Категории, План на год). Экраны Транзакции/Аналитика — не делаем сейчас, + это отдельный заход (нужен новый бэкенд: контроллеры, агрегирующие запросы для графиков). +2. **Категории остаются отдельной страницей `/categories`** — в хендоффе CRUD статей открывается + модалкой прямо со страницы «План на год» (кнопка «Статьи»), но пользователь явно попросил не + переносить, оставить как есть. +3. **Шрифт — не Inter из хендоффа, а SF Pro**, который уже лежит в проекте + (`App/media/fonts/`, `App/media/css/fonts.css`) — этот файл раньше нигде не подключался, + подключён в `layout.html` этим же заходом. -Макет — на русском, включает демо-данные (Кошелёк/Карта Тинькофф/Евро, цели «Отпуск»/«Остаток», -полный список категорий из [[бюджет_текущий_план]]) — не копировать демо-суммы как реальные, только -структуру/классы/цветовую схему. +**Как перенесено технически** — не копировали HTML хендоффа (там своя `sc-`-шаблонизация чужого +инструмента, не Bicycle), а перетащили только **значения** токенов Nocturne на уже существующие +CSS-переменные проекта в `App/media/css/dashboard.css`, чтобы не трогать разметку/JS: +- `--bg:#161826`, `--surface:#232532`, `--surface-2:#292b31`, `--surface-3:#3f424d`, + `--ink:#e9e9ed`, `--ink-soft:#cfd3e5`, `--muted:#9397ab`, `--divider` (16% ink) — вместо старой + чёрно-серой палитры (`--bg:#0b0b0f` и т.д.). +- Новый `--accent:#9184d9` (+ `--accent-tint:#d2cefd` для kicker-подписей типа «Доступно сейчас») — + единый акцент интерфейса (активный пункт dock, фокус форм, `.btn-primary`). Раньше активный dock + подсвечивался `--teal` — заменено на `--accent`. +- `--radius-sm/md/lg` (4/8/14px) и `--shadow-card`/`--shadow-pop` (hairline-обводка + мягкая тень — + фирменная деталь Nocturne) — применены ко всем карточкам/панелям/дропдаунам. +- `--sans`/`--font-heading` — теперь `"SF Pro Text"`/`"SF Pro Display"` вместо системного sans; + `--font-heading` навешан на заголовки/крупные суммы (`.balance`, `.room-title`, + `.budget-group-name`, `.section-label` и т.п.). +- Bootstrap-компоненты форм/модалок (`.btn-primary`, `.form-control`, `.form-select`, + `.form-check-input`, `.modal-content`) — довешаны CSS-переопределения под эту же палитру + (раньше были дефолтные цвета Bootstrap, не тронутые прошлым заходом). +- **Акцентные цвета per-тип счёта/категории** (`--teal/--green/--purple/--blue/--orange/--pink/ + --red/--amber/--gray`) — **не трогали**, оставлены как были: это уже прямые тёмные цвета вне + системы токенов (та же логика, что и в самом хендоффе — см. README.md проекта, «разноцветные + акценты... заданы прямыми oklch(), не входят в токены Nocturne»). +- `App/view/Login/login.html` — **не тронут**, у него свой независимый ` + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ Личные финансы +
+ + + +
+
+ + + +
+
+ +
+
+
+ +
+
+
Суббота, 28 февраля
+
+ 📅 Август 2026 + Бюджет 71% потрачено + ↗ +4 210 ₽ за месяц +
+
+ +
+
Доступно сейчас
+

66 450 ₽

+
Всего по всем счетам 441 950 ₽
+
+ +
+
Счета
+ +
+
+ +
+
+ + +
+ +
+ + +
+
+
+
{{acc.title}}
+
{{acc.subtitle}}
+
+
+
{{acc.amount}}
+ {{acc.rate}} +
+
{{acc.note}}
+
Обновлён сегодня
+
+
+
+ +
Копилки и цели
+
+ +
+ + + + {{g.pctLabel}} + +
+
{{g.title}}
+
{{g.savedLabel}} из {{g.targetLabel}}
+
+ + +
+ + +
+
+
+
+
+ +
Доходы
+
+ +
+ {{ir.title}} +
+
+
{{ir.amountLabel}}
+
+
+
{{ir.pctLabel}}
+
+
+
+
+ +
Бюджет на август
+
+ +
+
+ {{bg.title}} + {{bg.summaryLabel}} +
+ +
+ {{it.title}} +
+
+
{{it.amountLabel}}
+
+
+
{{it.pctLabel}}
+
+
+
+
+
+
+
+ +
+
+

План на {{year}} год

+
+ + + +
+
+ +
Доходы
+ +
+
+ {{group.title}} + {{group.sumLabel}} +
+
+
+ +
+
{{row.title}}
+
+ +
+ {{cell.label}} + +
+
+
+
+
+
+
+
+ + {{group.title}}{{group.yearLabel}} + +
+ +
+
{{row2.title}}
+
+ +
+ {{cell2.label}} + +
+
+
+
+
+
+
+
+
+
+
+ +
Расходы
+ +
+
+ {{group.title}} + {{group.sumLabel}} +
+
+
+ +
+
{{row.title}}
+
+ +
+ {{cell.label}} + +
+
+
+
+
+
+
+
+ + {{group.title}}{{group.yearLabel}} + +
+ +
+
{{row2.title}}
+
+ +
+ {{cell2.label}} + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+

Транзакции

+
+ +
+
+
{{tx.title}}
+
{{tx.dateLabel}} · {{tx.category}}
+
+
{{tx.amountLabel}}
+
+
+
+
+ +
+

Аналитика

+
+ +
+
Год к дате
+
Доходы и расходы по месяцам
+ + + + +
+ Доходы + Расходы +
+
+ +
+
Август
+
Структура расходов
+
+
+
+ Жильё · 33% + Продукты · 21% + Сбережения · 15% + Дети · 12% + Развлечения · 11% + Транспорт · 8% +
+
+
+ +
+
Август
+
План vs факт
+
+
Жильё54 200 ₽
+
Продукты38 900 ₽
+
Транспорт10 500 ₽
+
Развлечения21 300 ₽
+
Дети19 100 ₽
+
Сбережения25 000 ₽
+
+
+ +
+
Прогноз
+
Тренд до конца года
+ + + +

При текущем темпе к декабрю останется ≈ 980 000 ₽ сверх плана — этого хватит на обе цели накопления.

+
+ +
+
Год к году
+
Сравнение периодов
+
+
+
+
+
+
+
Q1Q2Q3Q4
+
+ 2025 + 2026 +
+
+ +
+
+ +
+ +
+
+ + + + + +
+
+
+
+ + +
+
+
+
+ +
+
Новый счёт
+
Расчётный, накопительный, наличные или кредит
+
+
+ +
+
+ +
Тип счёта
+
+ +
+ ✓ + +
{{at.title}}
{{at.desc}}
+
+
+
+ +
Параметры
+
+
+ + +
+
Цвет
+
+ + + +
+
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
Приватность
+
+
+
+
Общий баланс
+
+
+
+ +
+
Минимум нужно: название
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
Новый кредит
+
Раздел «Кредиты» · банковский кредит или карта
+
+
+ +
+
+ +
Что добавляем
+
+
Новый кредит
Только что оформил
+
Уже выплачиваю
Завести по остатку
+
+ +
+ +
+ ✓ + +
{{ct.title}}
{{ct.desc}}
+
+
+
+ + +
Заводим уже идущий кредит — укажите остаток долга и дату, на которую он верен. Историю пересчитывать не нужно.
+
+ +
Основные параметры
+
+ +
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
Приватность
+
+
+ +
+
Способ расчёта
+ +
+
+
+
+ +
+
Минимум нужно: {{creditMinHint}}
+
+ + +
+
+
+
+
+
+ + +
+
+
+
Статьи доходов и расходов
+ +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ + + +
+
+
+ + +
+
+
+ +
+
Доходы
+ +
+
+ +
+
+ {{g.title}} + + + + +
+ +
+ {{c.title}} + + + + +
+
+
+
+
+ +
+
Расходы
+ +
+
+ +
+
+ {{g2.title}} + + + + +
+ +
+ {{c2.title}} + + + + +
+
+
+
+
+ +
+
+
+
+ +
+ + + + + diff --git a/App/media/css/dashboard.css b/App/media/css/dashboard.css index 662efeb..0e48e4f 100644 --- a/App/media/css/dashboard.css +++ b/App/media/css/dashboard.css @@ -1,12 +1,21 @@ +/* Токены — палитра Nocturne (App/design_handoff_personal_finance/nocturne.css), перенесённая на + существующие имена переменных проекта, чтобы не переписывать разметку/JS (см. README хендоффа — + пересобрать в целевом окружении, не копировать HTML как есть). Акцентные цвета счетов/статей + (--teal/--green/--purple/...) — уже тёмная палитра под этот же принцип (прямые цвета, не токены + системы), оставлены как есть. */ :root { - --bg: #0b0b0f; - --surface: #17171d; - --surface-2: #1e1e26; - --surface-3: #24242d; - --ink: #f2f2f5; - --ink-soft: #c4c4cc; - --muted: #85858f; + --bg: #161826; + --surface: #232532; + --surface-2: #292b31; + --surface-3: #3f424d; + --ink: #e9e9ed; + --ink-soft: #cfd3e5; + --muted: #9397ab; --shadow: rgba(0, 0, 0, 0.5); + --divider: color-mix(in srgb, var(--ink) 16%, transparent); + + --accent: #9184d9; + --accent-tint: #d2cefd; --teal: #2dd4bf; --green: #34d399; @@ -18,7 +27,15 @@ --amber: #fbbf24; --gray: #6b7280; - --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --sans: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-heading: "SF Pro Display", var(--sans); + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 14px; + + --shadow-card: 0 0 0 1px var(--surface-3), 0 6px 18px var(--shadow); + --shadow-pop: 0 0 0 1px var(--surface-3), 0 16px 40px var(--shadow); } body { @@ -38,6 +55,37 @@ main { .ico { width: 1.05em; height: 1.05em; flex-shrink: 0; } +/* ---------- шапка: дата-плашка + теги (день недели/месяц/% бюджета) ---------- */ + +.dash-kicker { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} + +.dash-tags { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; + margin-top: 0.6rem; +} + +.tag { + display: inline-flex; + align-items: center; + font-size: 0.72rem; + letter-spacing: 0.01em; + padding: 0.2rem 0.65rem; + border-radius: var(--radius-sm); +} + +.tag-outline { + border: 1px solid var(--divider); + color: var(--ink-soft); +} + /* ---------- шапка: приветствие + баланс ---------- */ .top-row { @@ -59,11 +107,12 @@ main { font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; - color: var(--muted); + color: var(--accent-tint); margin-top: 0.6rem; } .balance { + font-family: var(--font-heading); font-size: 3rem; font-weight: 800; letter-spacing: -0.02em; @@ -92,6 +141,7 @@ main { display: flex; align-items: center; justify-content: space-between; + font-family: var(--font-heading); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.1em; @@ -113,10 +163,10 @@ main { .room-card { position: relative; background: var(--surface); - border-radius: 18px; + border-radius: var(--radius-lg); padding: 1rem 1.1rem 1.15rem; overflow: hidden; - box-shadow: 0 2px 10px var(--shadow); + box-shadow: var(--shadow-card); } .room-card::after { @@ -140,7 +190,7 @@ main { .room-icon { width: 2.1rem; height: 2.1rem; - border-radius: 10px; + border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; @@ -150,7 +200,7 @@ main { margin-bottom: 0.7rem; } -.room-title { font-size: 0.92rem; font-weight: 600; } +.room-title { font-family: var(--font-heading); font-size: 0.92rem; font-weight: 600; } .room-sub { font-size: 0.76rem; @@ -188,8 +238,8 @@ main { z-index: 5; min-width: 160px; background: var(--surface-2); - border-radius: 12px; - box-shadow: 0 8px 24px var(--shadow); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-pop); padding: 0.35rem; } @@ -200,7 +250,7 @@ main { align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem; - border-radius: 8px; + border-radius: var(--radius-md); font-size: 0.82rem; color: var(--ink-soft); } @@ -213,7 +263,7 @@ main { align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem; - border-radius: 8px; + border-radius: var(--radius-md); font-size: 0.82rem; color: var(--ink-soft); cursor: pointer; @@ -234,8 +284,8 @@ main { .budget-list { background: var(--surface); - border-radius: 16px; - box-shadow: 0 2px 10px var(--shadow); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); overflow: hidden; margin-bottom: 1.5rem; } @@ -248,7 +298,7 @@ main { gap: 1rem; } -.budget-row + .budget-row { border-top: 1px solid rgba(255, 255, 255, 0.06); } +.budget-row + .budget-row { border-top: 1px solid var(--divider); } .budget-group-head { display: flex; @@ -259,9 +309,10 @@ main { } .budget-group-head + .budget-row { border-top: none; } -.budget-row + .budget-group-head { border-top: 1px solid rgba(255, 255, 255, 0.06); } +.budget-row + .budget-group-head { border-top: 1px solid var(--divider); } .budget-group-name { + font-family: var(--font-heading); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.04em; @@ -275,11 +326,37 @@ main { .budget-plan-amount { font-size: 0.84rem; font-weight: 600; color: var(--ink-soft); white-space: nowrap; } +/* строка статьи план/факт — название сверху, бар+подпись снизу, % справа (см. дизайн-хендофф) */ + +.budget-row-main { flex: 1; min-width: 0; } + +.budget-bar { + height: 7px; + border-radius: 4px; + background: var(--surface-3); + overflow: hidden; + margin-top: 0.35rem; +} + +.budget-bar-fill { height: 100%; border-radius: 4px; } + +.budget-row-caption { font-size: 0.72rem; color: var(--muted); margin-top: 0.3rem; } + +.budget-row-side { flex-shrink: 0; font-size: 0.84rem; font-weight: 600; } + +/* ---------- копилки/цели: кольцо прогресса ---------- */ + +.goal-card { display: flex; flex-direction: row; align-items: center; gap: 0.9rem; } + +.goal-ring { flex-shrink: 0; } + +.goal-ring text { font-family: var(--sans); } + /* пустое состояние + кнопка добавить */ .empty-state { border: 1px dashed var(--surface-3); - border-radius: 18px; + border-radius: var(--radius-lg); padding: 2.5rem 1rem; text-align: center; color: var(--muted); @@ -298,7 +375,7 @@ main { cursor: pointer; } -.btn-add:hover { background: var(--blue); } +.btn-add:hover { background: var(--accent); } /* ---------- архив счетов (модалка) ---------- */ @@ -328,7 +405,7 @@ main { justify-content: space-between; gap: 1rem; background: var(--surface); - border-radius: 14px; + border-radius: var(--radius-lg); padding: 0.7rem 0.9rem; } @@ -339,7 +416,7 @@ main { min-width: 0; } -.category-group { font-weight: 600; } +.category-group { font-family: var(--font-heading); font-weight: 600; } .category-sub { margin-left: 1.6rem; @@ -367,12 +444,12 @@ main { .budget-card { background: var(--surface); - border-radius: 14px; + border-radius: var(--radius-lg); padding: 0.75rem 0.85rem; - box-shadow: 0 2px 8px var(--shadow); + box-shadow: var(--shadow-card); } -.budget-card-name { font-size: 0.84rem; font-weight: 600; margin-bottom: 0.5rem; } +.budget-card-name { font-family: var(--font-heading); font-size: 0.84rem; font-weight: 600; margin-bottom: 0.5rem; } .budget-months-inline { display: flex; gap: 0.35rem; } @@ -383,7 +460,7 @@ main { align-items: center; gap: 0.15rem; background: var(--surface-2); - border-radius: 8px; + border-radius: var(--radius-md); padding: 0.35rem 0.15rem; min-width: 0; } @@ -401,7 +478,7 @@ main { padding: 0; } -.plan-input:focus-visible { outline: 1px solid var(--teal); border-radius: 3px; } +.plan-input:focus-visible { outline: 1px solid var(--accent); border-radius: 3px; } .plan-input::-webkit-outer-spin-button, .plan-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .plan-input { -moz-appearance: textfield; } @@ -418,7 +495,7 @@ main { .budget-acc-card { background: var(--surface); - border-radius: 12px; + border-radius: var(--radius-lg); margin-bottom: 0.5rem; overflow: hidden; } @@ -428,6 +505,7 @@ main { align-items: center; gap: 0.6rem; padding: 0.7rem 0.9rem; + font-family: var(--font-heading); font-size: 0.84rem; font-weight: 600; cursor: pointer; @@ -462,7 +540,7 @@ main { justify-content: space-between; gap: 0.4rem; background: var(--surface-2); - border-radius: 8px; + border-radius: var(--radius-md); padding: 0.3rem 0.5rem; } @@ -509,9 +587,9 @@ main { a.dock-item:hover { color: var(--ink-soft); } .dock-item.active { - color: var(--teal); - background: color-mix(in srgb, var(--teal) 16%, transparent); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--teal) 35%, transparent); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 16%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent); } .dock-item.active .ico { transform: translateY(-1px); } @@ -521,3 +599,80 @@ a.dock-item:hover { color: var(--ink-soft); } .balance { font-size: 2.4rem; } .dock-item { padding: 0.5rem 0.65rem; } } + +/* ---------- Bootstrap: формы/кнопки/модалки под токены Nocturne ---------- + Разметка форм (модалки счетов/статей) — штатные Bootstrap-компоненты (.form-control, .btn-primary + и т.п.), меняем только их отрисовку под палитру/радиусы проекта, не трогая JS/структуру. */ + +.btn { border-radius: var(--radius-md); } + +.btn-primary { + background: var(--accent); + border-color: var(--accent); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active { + background: color-mix(in srgb, var(--accent) 85%, black) !important; + border-color: color-mix(in srgb, var(--accent) 85%, black) !important; +} + +.btn-outline-light { + border-color: var(--surface-3); + color: var(--ink-soft); +} + +.btn-outline-light:hover, +.btn-outline-light:focus, +.btn-outline-light:active { + background: var(--surface-3) !important; + border-color: var(--surface-3) !important; + color: var(--ink) !important; +} + +.btn-secondary { + background: transparent; + border-color: var(--surface-3); + color: var(--ink-soft); +} + +.btn-secondary:hover, +.btn-secondary:focus, +.btn-secondary:active { + background: var(--surface-3) !important; + border-color: var(--surface-3) !important; + color: var(--ink) !important; +} + +.form-control, +.form-select { + background: var(--bg); + border-color: var(--surface-3); + color: var(--ink); + border-radius: var(--radius-md); +} + +.form-control:focus, +.form-select:focus { + background: var(--bg); + border-color: var(--accent); + color: var(--ink); + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--accent) 25%, transparent); +} + +.form-check-input { + background-color: var(--bg); + border-color: var(--surface-3); +} + +.form-check-input:checked { + background-color: var(--accent); + border-color: var(--accent); +} + +.form-check-input:focus { + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--accent) 25%, transparent); +} + +.modal-content { border-radius: var(--radius-lg); } diff --git a/App/view/Index/index.html b/App/view/Index/index.html index 80b992b..ce27050 100644 --- a/App/view/Index/index.html +++ b/App/view/Index/index.html @@ -9,6 +9,10 @@ * @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 object[] $goals Цели пользователя + данные счёта-копилки (account_title/account_summa/account_currency) */ use App\Classes\Currency; @@ -26,6 +30,26 @@ $type_accents = [ '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'); ?> @@ -93,11 +117,21 @@ $this->setScript('accounts.js'); +
,
+
+ 📅 + + 100; ?> + Бюджет % потрачено + +
+
Здравствуйте,
Доступно сейчас
₽
+
Всего по всем счетам ₽
@@ -136,6 +170,7 @@ $this->setScript('accounts.js');
title, ENT_QUOTES, 'UTF-8') ?>
+
type_acc] ?? '' ?>
summa, 2, ',', ' ') ?> currency, ENT_QUOTES, 'UTF-8') ?> @@ -145,22 +180,48 @@ $this->setScript('accounts.js');
- 'январь', 2 => 'февраль', 3 => 'март', 4 => 'апрель', 5 => 'май', 6 => 'июнь', - 7 => 'июль', 8 => 'август', 9 => 'сентябрь', 10 => 'октябрь', 11 => 'ноябрь', 12 => 'декабрь', -]; + +
Копилки и цели
+
+ + account_summa; + $target = (float)$goal->target_summa; + $pct = $target > 0 ? min(100, $saved / $target * 100) : 0; + $r = 22; + $circumference = 2 * M_PI * $r; + $offset = $circumference * (1 - $pct / 100); + ?> +
+ + + + % + +
+
account_title, ENT_QUOTES, 'UTF-8') ?>
+
+ из + account_currency, ENT_QUOTES, 'UTF-8') ?> +
+
+
+ +
+ + $g->type === $type); if ($type_groups === []) { @@ -168,6 +229,26 @@ $renderBudgetList = static function (array $groups, array $tree, array $plan_mon 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) { @@ -186,13 +267,10 @@ $renderBudgetList = static function (array $groups, array $tree, array $plan_mon . '' . number_format($head_plan, 0, ',', ' ') . ' ₽' . '
'; + // Самостоятельная статья (без подкатегорий) — плашка выше уже единственная строка на неё + // (название+план), отдельного .budget-row для неё не рисуем (см. комментарий выше). foreach ($children as $category) { - $plan = $plan_month[$category->id] ?? 0.0; - - echo '
' - . '
' . htmlspecialchars($category->title, ENT_QUOTES, 'UTF-8') . '
' - . '
' . number_format($plan, 0, ',', ' ') . ' ₽
' - . '
'; + $renderRow($category, $plan_month[$category->id] ?? 0.0, $fact_month[$category->id] ?? 0.0, $type); } } @@ -201,10 +279,10 @@ $renderBudgetList = static function (array $groups, array $tree, array $plan_mon ?>
Доходы
- +
Бюджет на
- +