Bicycle/App/design_handoff_personal_finance/Личные финансы.dc.html
Egor Isaev ac1334c784 dev
2026-08-14 14:05:41 +03:00

1036 lines
82 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet data-dc-atomics>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="nocturne.css">
<style>
body{margin:0}
a{color:var(--color-accent)}
a:hover{color:var(--color-accent-400)}
.pm-wrap{container-type:inline-size}
.pm-cards{display:flex;flex-direction:column;gap:6px}
.pm-acc-wrap{display:none}
.pm-row{display:flex;gap:6px}
.pm-cell{flex:1;min-width:0;background:var(--color-neutral-900);border-radius:6px;padding:6px 4px;display:flex;flex-direction:column;align-items:center;gap:2px}
.pm-label{font-size:9px;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 55%, transparent)}
.pm-input{width:100%;background:transparent;border:none;color:var(--color-text);text-align:center;font:inherit;font-size:11px;padding:0}
.pm-input:focus-visible{outline:1px solid var(--color-accent);border-radius:3px}
.pm-input::-webkit-outer-spin-button,.pm-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
@container (max-width: 980px){ .pm-row{display:grid;grid-template-columns:repeat(6,1fr)} }
.pm-group-head{display:flex;justify-content:space-between;align-items:baseline}
@container (max-width: 460px){ .pm-cards{display:none} .pm-acc-wrap{display:block} .pm-group-head{display:none} }
.pm-acc-card summary::-webkit-details-marker{display:none}
.ico{width:1.05em;height:1.05em;flex-shrink:0;display:inline-block}
.pm-tile{border:1.5px solid var(--color-divider);border-radius:10px;padding:12px 14px;cursor:pointer;display:flex;align-items:flex-start;gap:10px;background:var(--color-neutral-900);position:relative}
.pm-tile.selected{border-color:var(--color-accent);background:color-mix(in srgb, var(--color-accent) 12%, var(--color-neutral-900))}
.pm-check{position:absolute;top:10px;right:10px;width:16px;height:16px;border-radius:50%;background:var(--color-accent);display:none;align-items:center;justify-content:center;color:var(--color-bg);font-size:10px}
.pm-tile.selected .pm-check{display:flex}
.stat-row{display:flex;align-items:center;gap:16px;padding:12px 20px;border-bottom:1px solid var(--color-divider)}
.stat-row>*:first-child{width:160px;flex-shrink:0}
.stat-row>*:nth-child(2){flex:1;min-width:0}
.stat-row>*:last-child{width:90px;flex-shrink:0;text-align:right}
@container (max-width: 480px){
.stat-row{flex-wrap:wrap;padding:12px 16px;row-gap:6px}
.stat-row>*:first-child{width:auto;flex:1;order:1}
.stat-row>*:last-child{width:auto;order:2}
.stat-row>*:nth-child(2){width:100%;order:3}
}
.acc-card{position:relative;transition:transform .15s ease, box-shadow .15s ease}
.acc-card::after{content:"";position:absolute;left:16%;right:16%;bottom:0;height:2px;border-radius:0 0 2px 2px;background:linear-gradient(90deg,transparent,var(--card-accent,var(--color-accent)),transparent);opacity:.9}
.acc-card:hover{transform:translateY(-2px)}
</style>
</helmet>
<svg width="0" height="0" style="position:absolute">
<defs>
<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-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="20" x2="20" y2="20"/><rect x="6" y="12" width="3" height="8"/><rect x="13" y="7" width="3" height="13"/></symbol>
<symbol id="ic-analytics" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="14 7 21 7 21 14"/></symbol>
<symbol id="ic-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 7.04 4.29l.06.06A1.65 1.65 0 0 0 8.92 4.68 1.65 1.65 0 0 0 9.93 3.17V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.32 9c.14.36.4.66.74.85.34.19.72.27 1.09.24H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>
<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-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-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></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-car" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 11 6.5 6h11L19 11"/><rect x="3" y="11" width="18" height="6" rx="2"/><circle cx="7.5" cy="17" r="1.5"/><circle cx="16.5" cy="17" r="1.5"/></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"/><path d="M9.5 15V9.8c0-.6.5-1 1.3-1h1.3c1.4 0 2.4.7 2.4 1.9S13.5 12.6 12 12.6H9.7"/></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="M4 10 12 4l8 6"/><line x1="5" y1="10" x2="19" y2="10"/><line x1="5" y1="10" x2="5" y2="19"/><line x1="19" y1="10" x2="19" y2="19"/><line x1="3" y1="19" x2="21" y2="19"/><line x1="9" y1="13" x2="9" y2="17"/><line x1="15" y1="13" x2="15" y2="17"/></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="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol>
<symbol id="ic-bolt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 4 14 11 14 10 22 20 10 13 10 13 2"/></symbol>
<symbol id="ic-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="17" x2="20" y2="17"/><circle cx="9" cy="7" r="2"/><circle cx="15" cy="17" r="2"/></symbol>
</defs>
</svg>
<div style="min-height:100vh;background:var(--color-neutral-900);font-family:var(--font-body);color:var(--color-text)">
<div style="position:sticky;top:0;z-index:50;background:var(--color-bg);border-bottom:1px solid var(--color-divider);padding:14px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap">
<strong style="font-family:var(--font-heading);font-size:17px;font-weight:500">Личные финансы</strong>
<div class="seg">
<label class="seg-opt"><input type="radio" name="screen" checked="{{isDashboard}}" onChange="{{goDashboard}}"/>Дашборд</label>
<label class="seg-opt"><input type="radio" name="screen" checked="{{isPlan}}" onChange="{{goPlan}}"/>План на год</label>
<label class="seg-opt"><input type="radio" name="screen" checked="{{isAnalytics}}" onChange="{{goAnalytics}}"/>Аналитика</label>
</div>
<div class="seg">
<label class="seg-opt"><input type="radio" name="device" checked="{{isDesktop}}" onChange="{{goDesktop}}"/>Комп</label>
<label class="seg-opt"><input type="radio" name="device" checked="{{isTablet}}" onChange="{{goTablet}}"/>Планшет</label>
<label class="seg-opt"><input type="radio" name="device" checked="{{isPhone}}" onChange="{{goPhone}}"/>Телефон</label>
</div>
</div>
<div style="display:flex;justify-content:center;padding:36px 16px 60px">
<div style="{{frameStyle}}">
<div style="{{mainScrollStyle}}">
<div style="{{dashboardStyle}}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:14px;margin-bottom:26px">
<div style="font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Суббота, 28 февраля</div>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<span class="tag tag-outline">📅 Август 2026</span>
<span class="tag" style="background:color-mix(in srgb, oklch(0.80 0.18 90) 20%, transparent);color:oklch(0.85 0.14 90)">Бюджет 71% потрачено</span>
<span class="tag" style="background:color-mix(in srgb, oklch(0.72 0.13 155) 20%, transparent);color:oklch(0.78 0.13 155)">↗ +4 210 ₽ за месяц</span>
</div>
</div>
<div style="margin-bottom:32px">
<div style="font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--color-accent-300)">Доступно сейчас</div>
<h1 style="margin:6px 0 0;font-size:48px;letter-spacing:-0.02em">66 450 ₽</h1>
<div style="font-size:13px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin-top:4px">Всего по всем счетам <span class="num" style="color:var(--color-text)">441 950 ₽</span></div>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<h6 style="margin:0;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Счета</h6>
<button type="button" class="btn btn-ghost btn-icon" style="width:22px;height:22px;min-width:0" onClick="{{openAccountModal}}"><svg class="ico" style="width:0.9em;height:0.9em"><use href="#ic-plus"/></svg></button>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:32px">
<sc-for list="{{accounts}}" as="acc" hint-placeholder-count="4">
<div class="card elev-md acc-card" style="padding:18px 18px 20px;gap:10px;--card-accent:{{acc.accent}};position:relative">
<div style="display:flex;align-items:flex-start;justify-content:space-between">
<span style="width:38px;height:38px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb, {{acc.accent}} 18%, transparent);color:{{acc.accent}}"><svg class="ico" style="width:1.2em;height:1.2em"><use href="#{{acc.icon}}"/></svg></span>
<button type="button" class="btn btn-ghost btn-icon" style="width:26px;height:26px;min-width:0" onClick="{{acc.toggleMenu}}">⋮</button>
</div>
<sc-if value="{{acc.menuOpen}}" hint-placeholder-val="{{false}}">
<div class="card elev-lg" style="position:absolute;top:44px;right:14px;z-index:10;padding:6px;min-width:140px;gap:0">
<button type="button" class="btn btn-ghost" style="justify-content:flex-start;width:100%" onClick="{{acc.onEdit}}">Редактировать</button>
<button type="button" class="btn btn-ghost" style="justify-content:flex-start;width:100%;color:oklch(0.68 0.20 25)" onClick="{{acc.onDelete}}">Удалить</button>
</div>
</sc-if>
<div>
<div class="card-title" style="font-size:14px;font-weight:500;color:color-mix(in srgb, var(--color-text) 85%, transparent)">{{acc.title}}</div>
<div class="card-meta" style="font-size:11px;margin-top:1px">{{acc.subtitle}}</div>
</div>
<div style="display:flex;align-items:baseline;gap:8px">
<div class="num" style="font-size:22px;font-weight:600;color:{{acc.accent}}">{{acc.amount}}</div>
<sc-if value="{{acc.rate}}" hint-placeholder-val="{{false}}"><span style="font-size:10px;color:color-mix(in srgb, var(--color-text) 45%, transparent)">{{acc.rate}}</span></sc-if>
</div>
<sc-if value="{{acc.note}}" hint-placeholder-val="{{false}}"><div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 45%, transparent)">{{acc.note}}</div></sc-if>
<div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 45%, transparent);display:flex;align-items:center;gap:5px"><span style="width:6px;height:6px;border-radius:50%;background:{{acc.accent}};display:inline-block"></span>Обновлён сегодня</div>
</div>
</sc-for>
</div>
<h6 style="margin:0 0 12px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Копилки и цели</h6>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin-bottom:32px">
<sc-for list="{{goals}}" as="g" hint-placeholder-count="2">
<div class="card elev-md acc-card" style="padding:16px 18px;flex-direction:row;align-items:center;gap:14px;--card-accent:{{g.color}};position:relative">
<svg width="52" height="52" viewBox="0 0 52 52" style="flex-shrink:0">
<circle cx="26" cy="26" r="22" fill="none" stroke="var(--color-neutral-800)" stroke-width="5"/>
<circle cx="26" cy="26" r="22" fill="none" stroke="{{g.color}}" stroke-width="5" stroke-linecap="round" stroke-dasharray="{{g.circumference}}" stroke-dashoffset="{{g.dashOffset}}" transform="rotate(-90 26 26)"/>
<text x="26" y="30" text-anchor="middle" font-size="13" fill="var(--color-text)" font-family="var(--font-body)">{{g.pctLabel}}</text>
</svg>
<div style="flex:1">
<div class="card-title" style="font-size:15px">{{g.title}}</div>
<div class="card-meta" style="font-size:12px"><span class="num" style="color:var(--color-text)">{{g.savedLabel}}</span> из <span class="num">{{g.targetLabel}}</span></div>
</div>
<button type="button" class="btn btn-ghost btn-icon" style="width:26px;height:26px;min-width:0" onClick="{{g.toggleMenu}}">⋮</button>
<sc-if value="{{g.menuOpen}}" hint-placeholder-val="{{false}}">
<div class="card elev-lg" style="position:absolute;top:44px;right:14px;z-index:10;padding:6px;min-width:140px;gap:0">
<button type="button" class="btn btn-ghost" style="justify-content:flex-start;width:100%" onClick="{{g.onEdit}}">Редактировать</button>
<button type="button" class="btn btn-ghost" style="justify-content:flex-start;width:100%;color:oklch(0.68 0.20 25)" onClick="{{g.onDelete}}">Удалить</button>
</div>
</sc-if>
</div>
</sc-for>
</div>
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Доходы</h6>
<div class="card elev-md pm-wrap" style="padding:6px 0;margin-bottom:28px">
<sc-for list="{{incomeRows}}" as="ir" hint-placeholder-count="4">
<div class="stat-row">
<span style="font-size:13px;font-weight:600">{{ir.title}}</span>
<div>
<div style="height:7px;border-radius:4px;background:var(--color-neutral-800);overflow:hidden"><div style="{{ir.barStyle}}"></div></div>
<div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin-top:4px">{{ir.amountLabel}}</div>
</div>
<div style="text-align:right">
<div class="num" style="font-size:13px;font-weight:600;color:var(--color-accent-300)">{{ir.pctLabel}}</div>
</div>
</div>
</sc-for>
</div>
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Бюджет на август</h6>
<div class="card elev-md pm-wrap" style="padding:6px 0">
<sc-for list="{{budgetGroups}}" as="bg" hint-placeholder-count="2">
<div>
<div style="display:flex;justify-content:space-between;padding:10px 20px;background:var(--color-neutral-900)">
<span style="font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--color-accent-300)">{{bg.title}}</span>
<span style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{bg.summaryLabel}}</span>
</div>
<sc-for list="{{bg.items}}" as="it" hint-placeholder-count="2">
<div class="stat-row">
<span style="font-size:13px;font-weight:600">{{it.title}}</span>
<div>
<div style="height:7px;border-radius:4px;background:var(--color-neutral-800);overflow:hidden"><div style="{{it.barStyle}}"></div></div>
<div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin-top:4px">{{it.amountLabel}}</div>
</div>
<div style="text-align:right">
<div class="num" style="font-size:13px;font-weight:600;color:var(--color-accent-300)">{{it.pctLabel}}</div>
</div>
</div>
</sc-for>
</div>
</sc-for>
</div>
</div>
<div style="{{planStyle}}">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px">
<h1 style="margin:0;font-size:24px">План на {{year}} год</h1>
<div style="display:flex;align-items:center;gap:8px">
<button type="button" class="btn btn-secondary btn-icon" onClick="{{prevYear}}">‹</button>
<button type="button" class="btn btn-secondary btn-icon" onClick="{{nextYear}}">›</button>
<button type="button" class="btn btn-primary" onClick="{{openManage}}"><svg class="ico"><use href="#ic-gear"/></svg>Статьи</button>
</div>
</div>
<h6 style="margin:0 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Доходы</h6>
<sc-for list="{{incomePlanTree}}" as="group" hint-placeholder-count="2">
<div style="margin-bottom:14px">
<div class="pm-group-head" style="padding:8px 14px;background:var(--color-surface);border-radius:10px 10px 0 0">
<span style="font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--color-accent-300)">{{group.title}}</span>
<span style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{group.sumLabel}}</span>
</div>
<div class="pm-wrap">
<div class="pm-cards" style="padding:8px;background:var(--color-surface);border-radius:0 0 10px 10px">
<sc-for list="{{group.rows}}" as="row" hint-placeholder-count="1">
<div>
<sc-if value="{{group.showRowName}}" hint-placeholder-val="{{false}}"><div style="font-size:12px;font-weight:500;margin-bottom:4px">{{row.title}}</div></sc-if>
<div class="pm-row">
<sc-for list="{{row.cells}}" as="cell" hint-placeholder-count="12">
<div class="pm-cell">
<span class="pm-label">{{cell.label}}</span>
<input class="pm-input" type="number" defaultValue="{{cell.value}}"/>
</div>
</sc-for>
</div>
</div>
</sc-for>
</div>
<div class="pm-acc-wrap">
<details class="pm-acc-card" style="background:var(--color-surface);border-radius:10px;margin-bottom:6px;overflow:hidden">
<summary style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;font-size:13px;font-weight:500;cursor:pointer;list-style:none">
<span>{{group.title}}</span><span style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{group.yearLabel}}</span>
</summary>
<div style="padding:0 14px 12px">
<sc-for list="{{group.rows}}" as="row2" hint-placeholder-count="1">
<div>
<sc-if value="{{group.showRowName}}" hint-placeholder-val="{{false}}"><div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin:8px 0 4px">{{row2.title}}</div></sc-if>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<sc-for list="{{row2.cells}}" as="cell2" hint-placeholder-count="12">
<div style="display:flex;justify-content:space-between;align-items:center;background:var(--color-neutral-900);border-radius:8px;padding:5px 8px">
<span style="font-size:10px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{cell2.label}}</span>
<input class="pm-input" style="width:64px;text-align:right" type="number" defaultValue="{{cell2.value}}"/>
</div>
</sc-for>
</div>
</div>
</sc-for>
</div>
</details>
</div>
</div>
</div>
</sc-for>
<h6 style="margin:20px 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Расходы</h6>
<sc-for list="{{expensePlanTree}}" as="group" hint-placeholder-count="4">
<div style="margin-bottom:14px">
<div class="pm-group-head" style="padding:8px 14px;background:var(--color-surface);border-radius:10px 10px 0 0">
<span style="font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--color-accent-300)">{{group.title}}</span>
<span style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{group.sumLabel}}</span>
</div>
<div class="pm-wrap">
<div class="pm-cards" style="padding:8px;background:var(--color-surface);border-radius:0 0 10px 10px">
<sc-for list="{{group.rows}}" as="row" hint-placeholder-count="1">
<div>
<sc-if value="{{group.showRowName}}" hint-placeholder-val="{{false}}"><div style="font-size:12px;font-weight:500;margin-bottom:4px">{{row.title}}</div></sc-if>
<div class="pm-row">
<sc-for list="{{row.cells}}" as="cell" hint-placeholder-count="12">
<div class="pm-cell">
<span class="pm-label">{{cell.label}}</span>
<input class="pm-input" type="number" defaultValue="{{cell.value}}"/>
</div>
</sc-for>
</div>
</div>
</sc-for>
</div>
<div class="pm-acc-wrap">
<details class="pm-acc-card" style="background:var(--color-surface);border-radius:10px;margin-bottom:6px;overflow:hidden">
<summary style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;font-size:13px;font-weight:500;cursor:pointer;list-style:none">
<span>{{group.title}}</span><span style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{group.yearLabel}}</span>
</summary>
<div style="padding:0 14px 12px">
<sc-for list="{{group.rows}}" as="row2" hint-placeholder-count="1">
<div>
<sc-if value="{{group.showRowName}}" hint-placeholder-val="{{false}}"><div style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin:8px 0 4px">{{row2.title}}</div></sc-if>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<sc-for list="{{row2.cells}}" as="cell2" hint-placeholder-count="12">
<div style="display:flex;justify-content:space-between;align-items:center;background:var(--color-neutral-900);border-radius:8px;padding:5px 8px">
<span style="font-size:10px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">{{cell2.label}}</span>
<input class="pm-input" style="width:64px;text-align:right" type="number" defaultValue="{{cell2.value}}"/>
</div>
</sc-for>
</div>
</div>
</sc-for>
</div>
</details>
</div>
</div>
</div>
</sc-for>
</div>
<div style="{{transactionsStyle}}">
<h1 style="margin:0 0 20px;font-size:24px">Транзакции</h1>
<div class="card elev-md pm-wrap" style="padding:6px 0">
<sc-for list="{{transactions}}" as="tx" hint-placeholder-count="6">
<div class="stat-row">
<div style="width:auto;flex:1">
<div style="font-size:13px;font-weight:600">{{tx.title}}</div>
<div class="card-meta" style="font-size:11px;margin-top:2px">{{tx.dateLabel}} · {{tx.category}}</div>
</div>
<div class="num" style="width:auto;flex-shrink:0;font-size:14px;font-weight:600;color:{{tx.color}}">{{tx.amountLabel}}</div>
</div>
</sc-for>
</div>
</div>
<div style="{{analyticsStyle}}">
<h1 style="margin:0 0 20px;font-size:24px">Аналитика</h1>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px">
<div class="card elev-sm">
<div class="card-kicker">Год к дате</div>
<div class="card-title">Доходы и расходы по месяцам</div>
<svg viewBox="0 0 720 160" style="width:100%;height:auto;margin-top:6px">
<polyline points="20,90 80,80 140,85 200,75 260,65 320,70 380,78 440,75 500,60 560,65 620,68 680,55" fill="none" stroke="var(--color-accent-400)" stroke-width="2.5"/>
<polyline points="20,130 80,125 140,128 200,120 260,115 320,118 380,122 440,119 500,110 560,112 620,115 680,108" fill="none" stroke="var(--color-neutral-500)" stroke-width="2.5" stroke-dasharray="4 3"/>
</svg>
<div style="display:flex;gap:16px;font-size:11px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">
<span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--color-accent-400);margin-right:4px"></span>Доходы</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--color-neutral-500);margin-right:4px"></span>Расходы</span>
</div>
</div>
<div class="card elev-sm">
<div class="card-kicker">Август</div>
<div class="card-title">Структура расходов</div>
<div style="display:flex;align-items:center;gap:18px;margin-top:6px">
<div style="width:110px;height:110px;border-radius:50%;flex-shrink:0;background:conic-gradient(var(--color-accent-300) 0% 33%, var(--color-accent-400) 33% 54%, var(--color-accent-500) 54% 69%, var(--color-accent-600) 69% 81%, var(--color-accent-700) 81% 92%, var(--color-neutral-500) 92% 100%)"></div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:12px">
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-300);margin-right:6px"></span>Жильё · 33%</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-400);margin-right:6px"></span>Продукты · 21%</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-500);margin-right:6px"></span>Сбережения · 15%</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-600);margin-right:6px"></span>Дети · 12%</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-700);margin-right:6px"></span>Развлечения · 11%</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-neutral-500);margin-right:6px"></span>Транспорт · 8%</span>
</div>
</div>
</div>
<div class="card elev-sm">
<div class="card-kicker">Август</div>
<div class="card-title">План vs факт</div>
<div style="display:flex;flex-direction:column;gap:9px;margin-top:6px">
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Жильё</span><span class="num">54 200 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800);position:relative"><div style="position:absolute;inset:0;width:88%;border-radius:3px;box-shadow:inset 0 0 0 1px var(--color-accent-400)"></div><div style="height:100%;width:90%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Продукты</span><span class="num">38 900 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800)"><div style="height:100%;width:65%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Транспорт</span><span class="num">10 500 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800)"><div style="height:100%;width:18%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Развлечения</span><span class="num">21 300 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800)"><div style="height:100%;width:35%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Дети</span><span class="num">19 100 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800)"><div style="height:100%;width:32%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
<div><div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px"><span>Сбережения</span><span class="num">25 000 ₽</span></div><div style="height:6px;border-radius:3px;background:var(--color-neutral-800)"><div style="height:100%;width:41%;border-radius:3px;background:var(--color-accent-400)"></div></div></div>
</div>
</div>
<div class="card elev-sm">
<div class="card-kicker">Прогноз</div>
<div class="card-title">Тренд до конца года</div>
<svg viewBox="0 0 240 60" style="width:140px;height:auto;margin-top:6px">
<polyline points="0,45 40,40 80,42 120,30 160,25 200,18 240,12" fill="none" stroke="var(--color-accent-400)" stroke-width="2.5"/>
</svg>
<p class="card-body" style="margin-top:8px">При текущем темпе к декабрю останется ≈ 980 000 ₽ сверх плана — этого хватит на обе цели накопления.</p>
</div>
<div class="card elev-sm">
<div class="card-kicker">Год к году</div>
<div class="card-title">Сравнение периодов</div>
<div style="display:flex;align-items:flex-end;gap:18px;height:90px;margin-top:10px">
<div style="display:flex;align-items:flex-end;gap:4px;height:100%"><div style="width:14px;height:55%;background:var(--color-accent-700);border-radius:3px 3px 0 0"></div><div style="width:14px;height:62%;background:var(--color-accent-400);border-radius:3px 3px 0 0"></div></div>
<div style="display:flex;align-items:flex-end;gap:4px;height:100%"><div style="width:14px;height:60%;background:var(--color-accent-700);border-radius:3px 3px 0 0"></div><div style="width:14px;height:70%;background:var(--color-accent-400);border-radius:3px 3px 0 0"></div></div>
<div style="display:flex;align-items:flex-end;gap:4px;height:100%"><div style="width:14px;height:65%;background:var(--color-accent-700);border-radius:3px 3px 0 0"></div><div style="width:14px;height:78%;background:var(--color-accent-400);border-radius:3px 3px 0 0"></div></div>
<div style="display:flex;align-items:flex-end;gap:4px;height:100%"><div style="width:14px;height:68%;background:var(--color-accent-700);border-radius:3px 3px 0 0"></div><div style="width:14px;height:85%;background:var(--color-accent-400);border-radius:3px 3px 0 0"></div></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:10px;color:color-mix(in srgb, var(--color-text) 55%, transparent);margin-top:4px"><span>Q1</span><span>Q2</span><span>Q3</span><span>Q4</span></div>
<div style="display:flex;gap:16px;font-size:11px;margin-top:8px;color:color-mix(in srgb, var(--color-text) 60%, transparent)">
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-700);margin-right:4px"></span>2025</span>
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--color-accent-400);margin-right:4px"></span>2026</span>
</div>
</div>
</div>
</div>
</div>
<div style="display:flex;justify-content:center;padding:14px 0;background:transparent">
<div style="display:flex;align-items:center;gap:6px;padding:6px;border-radius:999px;background:var(--color-surface);box-shadow:var(--shadow-md)">
<button type="button" onClick="{{goDashboard}}" style="{{dockDashboardStyle}}"><svg class="ico" style="width:1.15rem;height:1.15rem"><use href="#ic-home"/></svg></button>
<button type="button" onClick="{{goPlan}}" style="{{dockPlanStyle}}"><svg class="ico" style="width:1.15rem;height:1.15rem"><use href="#ic-chart"/></svg></button>
<button type="button" onClick="{{goTransactions}}" style="{{dockTransactionsStyle}}"><svg class="ico" style="width:1.15rem;height:1.15rem"><use href="#ic-sliders"/></svg></button>
<button type="button" onClick="{{goAnalytics}}" style="{{dockAnalyticsStyle}}"><svg class="ico" style="width:1.15rem;height:1.15rem"><use href="#ic-analytics"/></svg></button>
<button type="button" onClick="{{openManage}}" style="{{dockManageStyle}}"><svg class="ico" style="width:1.15rem;height:1.15rem"><use href="#ic-gear"/></svg></button>
</div>
</div>
</div>
</div>
<sc-if value="{{accountModalOpen}}" hint-placeholder-val="{{false}}">
<div class="dialog-backdrop" style="position:fixed;z-index:110">
<div class="dialog" style="max-height:88vh;overflow-y:auto;width:520px;max-width:92vw">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
<div style="display:flex;gap:12px;align-items:flex-start">
<span style="width:40px;height:40px;border-radius:10px;background:color-mix(in srgb, var(--color-accent) 20%, transparent);color:var(--color-accent);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.3em;height:1.3em"><use href="#ic-bag"/></svg></span>
<div>
<div class="dialog-title" style="margin:0">Новый счёт</div>
<div class="card-meta" style="font-size:12px;margin-top:2px">Расчётный, накопительный, наличные или кредит</div>
</div>
</div>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{closeAccountModal}}">✕</button>
</div>
<div class="dialog-body">
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Тип счёта</h6>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px">
<sc-for list="{{accountTypeOptions}}" as="at" hint-placeholder-count="4">
<div class="pm-tile {{at.tileClass}}" onClick="{{at.onSelect}}">
<span class="pm-check">✓</span>
<span style="width:34px;height:34px;border-radius:8px;background:color-mix(in srgb, {{at.color}} 20%, transparent);color:{{at.color}};display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#{{at.icon}}"/></svg></span>
<div><div style="font-size:13px;font-weight:600">{{at.title}}</div><div class="card-meta" style="font-size:11px">{{at.desc}}</div></div>
</div>
</sc-for>
</div>
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Параметры</h6>
<div style="display:flex;gap:10px;align-items:center;position:relative">
<div style="position:relative">
<button type="button" onClick="{{toggleColorPicker}}" style="{{colorSwatchStyle}};width:44px;height:44px;border-radius:50%;border:2px solid var(--color-divider);cursor:pointer;flex-shrink:0"></button>
<sc-if value="{{colorPickerOpen}}" hint-placeholder-val="{{false}}">
<div class="card elev-lg" style="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(--color-text) 55%, transparent)">Цвет</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
<sc-for list="{{colorSwatches}}" as="cw" hint-placeholder-count="8">
<button type="button" onClick="{{cw.onSelect}}" style="width:28px;height:28px;border-radius:50%;background:{{cw.value}};border:2px solid {{cw.borderStyle}};cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--color-bg);font-size:11px">{{cw.check}}</button>
</sc-for>
</div>
</div>
</sc-if>
</div>
<div class="field" style="flex:1;margin:0"><label>Название</label><input class="input" placeholder="Напр. Основная карта"/></div>
<span style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb, {{selectedAccentColor}} 20%, transparent);color:{{selectedAccentColor}};display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.05em;height:1.05em"><use href="#{{selectedAccentIcon}}"/></svg></span>
</div>
<div style="display:flex;gap:12px;margin-top:12px">
<div class="field" style="flex:1;margin:0"><label>Начальный баланс</label><input class="input" type="number" placeholder="0"/></div>
<div style="width:110px">
<label style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">&nbsp;</label>
<select class="input" value="{{newAccCurrency}}" onChange="{{setNewAccCurrency}}"><option value="RUB">₽ RUB</option><option value="USD">$ USD</option><option value="EUR">€ EUR</option></select>
</div>
</div>
<sc-if value="{{isSavingsType}}" hint-placeholder-val="{{false}}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
<div class="field"><label>Цель накопления</label><input class="input" type="number" placeholder="100 000"/></div>
<div class="field"><label>Дата цели</label><input class="input" type="date"/></div>
</div>
</sc-if>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px">
<div>
<h6 style="margin:0 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Приватность</h6>
<div class="seg"><label class="seg-opt"><input type="radio" name="acc-priv" checked="{{isPrivShared}}" onChange="{{setPrivShared}}"/>Общий</label><label class="seg-opt"><input type="radio" name="acc-priv" checked="{{isPrivPersonal}}" onChange="{{setPrivPersonal}}"/>Личный</label></div>
</div>
<div>
<h6 style="margin:0 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Общий баланс</h6>
<div class="seg"><label class="seg-opt"><input type="radio" name="acc-incl" checked="{{isInclYes}}" onChange="{{setInclYes}}"/>Учитывать</label><label class="seg-opt"><input type="radio" name="acc-incl" checked="{{isInclNo}}" onChange="{{setInclNo}}"/>Не учитывать</label></div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:20px">
<div class="card-meta" style="font-size:11px">Минимум нужно: <strong style="color:var(--color-text)">название</strong></div>
<div style="display:flex;gap:8px">
<button type="button" class="btn btn-secondary" onClick="{{closeAccountModal}}">Отмена</button>
<button type="button" class="btn btn-primary" onClick="{{closeAccountModal}}">Создать счёт</button>
</div>
</div>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{creditModalOpen}}" hint-placeholder-val="{{false}}">
<div class="dialog-backdrop" style="position:fixed;z-index:110">
<div class="dialog" style="max-height:88vh;overflow-y:auto;width:560px;max-width:92vw">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
<div style="display:flex;gap:12px;align-items:flex-start">
<span style="width:40px;height:40px;border-radius:10px;background:color-mix(in srgb, var(--color-accent) 20%, transparent);color:var(--color-accent);display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.3em;height:1.3em"><use href="#ic-bolt"/></svg></span>
<div>
<div class="dialog-title" style="margin:0">Новый кредит</div>
<div class="card-meta" style="font-size:12px;margin-top:2px">Раздел «Кредиты» · банковский кредит или карта</div>
</div>
</div>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{closeCreditModal}}">✕</button>
</div>
<div class="dialog-body">
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Что добавляем</h6>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px">
<div class="pm-tile {{creditModeNewClass}}" onClick="{{setCreditModeNew}}"><div><div style="font-size:13px;font-weight:600">Новый кредит</div><div class="card-meta" style="font-size:11px">Только что оформил</div></div></div>
<div class="pm-tile {{creditModeExistingClass}}" onClick="{{setCreditModeExisting}}"><div><div style="font-size:13px;font-weight:600">Уже выплачиваю</div><div class="card-meta" style="font-size:11px">Завести по остатку</div></div></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px">
<sc-for list="{{creditTypeOptions}}" as="ct" hint-placeholder-count="4">
<div class="pm-tile {{ct.tileClass}}" onClick="{{ct.onSelect}}">
<span class="pm-check">✓</span>
<span style="width:34px;height:34px;border-radius:8px;background:color-mix(in srgb, {{ct.color}} 20%, transparent);color:{{ct.color}};display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.1em;height:1.1em"><use href="#{{ct.icon}}"/></svg></span>
<div><div style="font-size:13px;font-weight:600">{{ct.title}}</div><div class="card-meta" style="font-size:11px">{{ct.desc}}</div></div>
</div>
</sc-for>
</div>
<sc-if value="{{isCreditExisting}}" hint-placeholder-val="{{false}}">
<div style="background:color-mix(in srgb, var(--color-accent) 10%, var(--color-neutral-900));border-radius:10px;padding:10px 14px;font-size:12px;margin-bottom:16px">Заводим уже идущий кредит — укажите остаток долга и дату, на которую он верен. Историю пересчитывать не нужно.</div>
</sc-if>
<h6 style="margin:0 0 10px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Основные параметры</h6>
<div style="display:flex;gap:10px;align-items:center">
<button type="button" style="{{colorSwatchStyle}};width:44px;height:44px;border-radius:50%;border:2px solid var(--color-divider);flex-shrink:0"></button>
<div class="field" style="flex:1;margin:0"><label>Название</label><input class="input" placeholder="Напр. Тачка"/></div>
<span style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb, {{creditTypeColor}} 20%, transparent);color:{{creditTypeColor}};display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg class="ico" style="width:1.05em;height:1.05em"><use href="#{{creditTypeIcon}}"/></svg></span>
</div>
<sc-if value="{{isCreditCard}}" hint-placeholder-val="{{false}}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
<div style="display:flex;gap:8px">
<div class="field" style="flex:1;margin:0"><label>Кредитный лимит *</label><input class="input" type="number" placeholder="100 000"/></div>
<div style="width:90px"><label style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">&nbsp;</label><select class="input"><option>₽ RUB</option><option>$ USD</option><option>€ EUR</option></select></div>
</div>
<div class="field"><label>Льготный период, дней</label><input class="input" type="number" placeholder="120"/></div>
<div class="field"><label>Ставка годовая, % *</label><input class="input" type="number" placeholder="29,9"/></div>
<div class="field"><label>Мин. платёж</label><input class="input" type="number" placeholder="5 000"/></div>
<div class="field"><label>Текущий баланс карты</label><input class="input" type="number" placeholder="0"/></div>
<div class="field"><label>День платежа</label><input class="input" type="number" placeholder="10"/></div>
</div>
<div class="field" style="margin-top:12px"><label>Следующий платёж</label><input class="input" type="date"/></div>
</sc-if>
<sc-if value="{{isCreditNonCard}}" hint-placeholder-val="{{true}}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
<div style="display:flex;gap:8px">
<div class="field" style="flex:1;margin:0"><label>Сумма кредита *</label><input class="input" type="number" placeholder="1 000 000"/></div>
<div style="width:90px"><label style="font-size:11px;color:color-mix(in srgb, var(--color-text) 55%, transparent)">&nbsp;</label><select class="input"><option>₽ RUB</option><option>$ USD</option><option>€ EUR</option></select></div>
</div>
<div class="field"><label>Ставка годовая, % *</label><input class="input" type="number" placeholder="16,5"/></div>
<div class="field"><label>Срок, мес.</label><input class="input" type="number" placeholder="60"/></div>
<div class="field"><label>Ежемесячный платёж</label><input class="input" type="number" placeholder="35 000"/></div>
</div>
<sc-if value="{{isCreditExisting}}" hint-placeholder-val="{{false}}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
<div class="field"><label>Остаток долга *</label><input class="input" type="number" placeholder="750 000"/></div>
<div class="field"><label>Остаток актуален на *</label><input class="input" type="date"/></div>
</div>
</sc-if>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px">
<div class="field"><label>День платежа</label><input class="input" type="number" placeholder="10"/></div>
<div class="field"><label>Следующий платёж</label><input class="input" type="date"/></div>
</div>
</sc-if>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px">
<div>
<h6 style="margin:0 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Приватность</h6>
<div class="seg"><label class="seg-opt"><input type="radio" name="cr-priv" checked="{{isPrivShared}}" onChange="{{setPrivShared}}"/>Общий</label><label class="seg-opt"><input type="radio" name="cr-priv" checked="{{isPrivPersonal}}" onChange="{{setPrivPersonal}}"/>Личный</label></div>
</div>
<sc-if value="{{isCreditNonCard}}" hint-placeholder-val="{{true}}">
<div>
<h6 style="margin:0 0 8px;color:color-mix(in srgb, var(--color-text) 55%, transparent);letter-spacing:.06em;font-size:11px;text-transform:uppercase">Способ расчёта</h6>
<select class="input"><option>Аннуитет</option><option>Дифференцированный</option></select>
</div>
</sc-if>
</div>
<div class="field" style="margin-top:12px"><label>Дата выдачи</label><input class="input" type="date"/></div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:20px">
<div class="card-meta" style="font-size:11px">Минимум нужно: <strong style="color:var(--color-text)">{{creditMinHint}}</strong></div>
<div style="display:flex;gap:8px">
<button type="button" class="btn btn-secondary" onClick="{{closeCreditModal}}">Отмена</button>
<button type="button" class="btn btn-primary" onClick="{{closeCreditModal}}">Создать кредит</button>
</div>
</div>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{manageOpen}}" hint-placeholder-val="{{false}}">
<div class="dialog-backdrop" style="position:fixed;z-index:100">
<div class="dialog" style="max-height:82vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="dialog-title">Статьи доходов и расходов</div>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{closeManage}}">✕</button>
</div>
<div class="dialog-body">
<sc-if value="{{form}}" hint-placeholder-val="{{false}}">
<div class="card" style="background:var(--color-neutral-900);margin-bottom:16px">
<div class="field"><label>Название</label><input class="input" value="{{form.title}}" onChange="{{setFormTitle}}"/></div>
<div class="seg">
<label class="seg-opt"><input type="radio" name="ctype" checked="{{isFormIncome}}" onChange="{{setFormTypeIncome}}"/>Доход</label>
<label class="seg-opt"><input type="radio" name="ctype" checked="{{isFormExpense}}" onChange="{{setFormTypeExpense}}"/>Расход</label>
</div>
<div class="field" style="margin-top:8px">
<label>Группа</label>
<select class="input" value="{{form.parentId}}" onChange="{{setFormParent}}">
<option value="">— группа верхнего уровня —</option>
<sc-for list="{{formParentOptions}}" as="opt" hint-placeholder-count="2">
<option value="{{opt.id}}">{{opt.title}}</option>
</sc-for>
</select>
</div>
<sc-if value="{{isFormIncome}}" hint-placeholder-val="{{false}}">
<div class="field" style="margin-top:8px">
<label>Формула — связанная статья расхода</label>
<select class="input" value="{{form.linkedExpenseId}}" onChange="{{setFormLinked}}" style="margin-bottom:8px">
<option value="">— не привязано —</option>
<sc-for list="{{formExpenseOptions}}" as="eo" hint-placeholder-count="2">
<option value="{{eo.id}}">{{eo.title}}</option>
</sc-for>
</select>
<input class="input" type="number" min="0" max="100" step="0.01" placeholder="Процент" value="{{form.formulaPct}}" onChange="{{setFormPct}}"/>
</div>
</sc-if>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px">
<button type="button" class="btn btn-secondary" onClick="{{cancelForm}}">Отмена</button>
<button type="button" class="btn btn-primary" onClick="{{saveForm}}">Сохранить</button>
</div>
</div>
</sc-if>
<div style="display:flex;justify-content:space-between;align-items:center;margin:4px 0 8px">
<h6 style="margin:0;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Доходы</h6>
<button type="button" class="btn btn-ghost" onClick="{{openAddIncome}}"><svg class="ico"><use href="#ic-plus"/></svg>Добавить</button>
</div>
<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:16px">
<sc-for list="{{incomeManageTree}}" as="g" hint-placeholder-count="2">
<div>
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--color-surface);border-radius:10px;padding:8px 10px">
<span style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><svg class="ico"><use href="#ic-folder"/></svg>{{g.title}}</span>
<span style="display:flex;gap:2px">
<button type="button" class="btn btn-ghost btn-icon" onClick="{{g.onEdit}}"><svg class="ico"><use href="#ic-pencil"/></svg></button>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{g.onDelete}}"><svg class="ico"><use href="#ic-trash"/></svg></button>
</span>
</div>
<sc-for list="{{g.children}}" as="c" hint-placeholder-count="2">
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--color-neutral-900);border-radius:10px;padding:7px 10px 7px 26px;margin-top:3px">
<span style="font-size:13px">{{c.title}}</span>
<span style="display:flex;gap:2px">
<button type="button" class="btn btn-ghost btn-icon" onClick="{{c.onEdit}}"><svg class="ico"><use href="#ic-pencil"/></svg></button>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{c.onDelete}}"><svg class="ico"><use href="#ic-trash"/></svg></button>
</span>
</div>
</sc-for>
</div>
</sc-for>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin:4px 0 8px">
<h6 style="margin:0;color:color-mix(in srgb, var(--color-text) 55%, transparent)">Расходы</h6>
<button type="button" class="btn btn-ghost" onClick="{{openAddExpense}}"><svg class="ico"><use href="#ic-plus"/></svg>Добавить</button>
</div>
<div style="display:flex;flex-direction:column;gap:4px">
<sc-for list="{{expenseManageTree}}" as="g2" hint-placeholder-count="4">
<div>
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--color-surface);border-radius:10px;padding:8px 10px">
<span style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><svg class="ico"><use href="#ic-folder"/></svg>{{g2.title}}</span>
<span style="display:flex;gap:2px">
<button type="button" class="btn btn-ghost btn-icon" onClick="{{g2.onEdit}}"><svg class="ico"><use href="#ic-pencil"/></svg></button>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{g2.onDelete}}"><svg class="ico"><use href="#ic-trash"/></svg></button>
</span>
</div>
<sc-for list="{{g2.children}}" as="c2" hint-placeholder-count="2">
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--color-neutral-900);border-radius:10px;padding:7px 10px 7px 26px;margin-top:3px">
<span style="font-size:13px">{{c2.title}}</span>
<span style="display:flex;gap:2px">
<button type="button" class="btn btn-ghost btn-icon" onClick="{{c2.onEdit}}"><svg class="ico"><use href="#ic-pencil"/></svg></button>
<button type="button" class="btn btn-ghost btn-icon" onClick="{{c2.onDelete}}"><svg class="ico"><use href="#ic-trash"/></svg></button>
</span>
</div>
</sc-for>
</div>
</sc-for>
</div>
</div>
</div>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:900},&quot;defaultScreen&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;default&quot;:&quot;dashboard&quot;,&quot;tsType&quot;:&quot;string&quot;,&quot;options&quot;:[&quot;dashboard&quot;,&quot;plan&quot;,&quot;analytics&quot;],&quot;section&quot;:&quot;Прототип&quot;},&quot;defaultDevice&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;default&quot;:&quot;desktop&quot;,&quot;tsType&quot;:&quot;string&quot;,&quot;options&quot;:[&quot;desktop&quot;,&quot;tablet&quot;,&quot;phone&quot;],&quot;section&quot;:&quot;Прототип&quot;}}">
class Component extends DCLogic {
static rawCats() {
return [
[1,'Зарплата','income',null], [2,'Анна','income',1,120000], [3,'Игорь','income',1,95000],
[4,'Прочие доходы','income',null], [5,'Фриланс','income',4,30000],
[10,'Жильё','expense',null], [11,'Аренда','expense',10,45000], [12,'Коммуналка','expense',10,8000],
[13,'Продукты','expense',null,35000], [14,'Транспорт','expense',null,12000],
[15,'Развлечения','expense',null], [16,'Рестораны','expense',15,15000], [17,'Подписки','expense',15,3000],
[18,'Дети','expense',null,20000], [19,'Сбережения','expense',null,25000],
];
}
constructor(props) {
super(props);
this.state = {
screen: props.defaultScreen || 'dashboard',
transactions: null,
device: props.defaultDevice || 'desktop',
year: 2026,
manageOpen: false,
form: null,
nextId: 20,
accMenuOpenId: null,
deletedAccountIds: [],
goalMenuOpenId: null,
deletedGoalIds: [],
accountModalOpen: false,
newAccType: 'checking',
newAccColor: 'oklch(0.72 0.19 25)',
newAccCurrency: 'RUB',
colorPickerOpen: false,
privacy: 'personal',
includeInTotal: true,
creditModalOpen: false,
creditMode: 'new',
creditType: 'consumer',
categories: Component.rawCats().map(([id,title,type,parentId,plan]) => ({ id, title, type, parentId, ...(plan != null ? { monthlyPlan: Array(12).fill(plan) } : {}) })),
};
}
fmt(n) { return new Intl.NumberFormat('ru-RU').format(Math.round(n)); }
setScreen(s) { this.setState({ screen: s }); }
setDevice(d) { this.setState({ device: d }); }
prevYear() { this.setState(s => ({ year: s.year - 1 })); }
nextYear() { this.setState(s => ({ year: s.year + 1 })); }
openManage() { this.setState({ manageOpen: true }); }
closeManage() { this.setState({ manageOpen: false, form: null }); }
openAddIncome() { this.setState({ form: { id: null, title: '', type: 'income', parentId: '', linkedExpenseId: '', formulaPct: '' } }); }
openAddExpense() { this.setState({ form: { id: null, title: '', type: 'expense', parentId: '', linkedExpenseId: '', formulaPct: '' } }); }
openEditForm(cat) { this.setState({ form: { id: cat.id, title: cat.title, type: cat.type, parentId: cat.parentId ? String(cat.parentId) : '', linkedExpenseId: cat.linkedExpenseId ? String(cat.linkedExpenseId) : '', formulaPct: cat.formulaPct ?? '' } }); }
cancelForm() { this.setState({ form: null }); }
setFormTitle(e) { const v = e.target.value; this.setState(s => ({ form: { ...s.form, title: v } })); }
setFormParent(e) { const v = e.target.value; this.setState(s => ({ form: { ...s.form, parentId: v } })); }
setFormLinked(e) { const v = e.target.value; this.setState(s => ({ form: { ...s.form, linkedExpenseId: v } })); }
setFormPct(e) { const v = e.target.value; this.setState(s => ({ form: { ...s.form, formulaPct: v } })); }
setFormTypeIncome() { this.setState(s => ({ form: { ...s.form, type: 'income', parentId: '' } })); }
setFormTypeExpense() { this.setState(s => ({ form: { ...s.form, type: 'expense', parentId: '' } })); }
saveForm() {
const f = this.state.form;
if (!f || !f.title.trim()) return;
this.setState(s => {
let categories;
const linkFields = { linkedExpenseId: f.linkedExpenseId ? Number(f.linkedExpenseId) : null, formulaPct: f.formulaPct === '' ? null : Number(f.formulaPct) };
if (f.id) {
categories = s.categories.map(c => c.id === f.id ? { ...c, title: f.title, parentId: f.parentId ? Number(f.parentId) : null, ...linkFields } : c);
} else {
categories = [...s.categories, { id: s.nextId, title: f.title, type: f.type, parentId: f.parentId ? Number(f.parentId) : null, monthlyPlan: Array(12).fill(0), ...linkFields }];
}
return { categories, nextId: f.id ? s.nextId : s.nextId + 1, form: null };
});
}
openAccountModal() { this.setState({ accountModalOpen: true, newAccType: 'checking', newAccColor: 'oklch(0.72 0.19 25)', colorPickerOpen: false }); }
closeAccountModal() { this.setState({ accountModalOpen: false, colorPickerOpen: false }); }
setNewAccType(t) {
if (t === 'credit') { this.setState({ accountModalOpen: false, creditModalOpen: true, creditMode: 'new', creditType: 'consumer' }); return; }
this.setState({ newAccType: t });
}
toggleColorPicker() { this.setState(s => ({ colorPickerOpen: !s.colorPickerOpen })); }
setNewAccColor(c) { this.setState({ newAccColor: c, colorPickerOpen: false }); }
setNewAccCurrency(e) { this.setState({ newAccCurrency: e.target.value }); }
setPrivShared() { this.setState({ privacy: 'shared' }); }
setPrivPersonal() { this.setState({ privacy: 'personal' }); }
setInclYes() { this.setState({ includeInTotal: true }); }
setInclNo() { this.setState({ includeInTotal: false }); }
closeCreditModal() { this.setState({ creditModalOpen: false }); }
setCreditModeNew() { this.setState({ creditMode: 'new' }); }
setCreditModeExisting() { this.setState({ creditMode: 'existing' }); }
setCreditType(t) { this.setState({ creditType: t }); }
toggleAccMenu(id) { this.setState(s => ({ accMenuOpenId: s.accMenuOpenId === id ? null : id })); }
editAccountStub() { this.setState({ accMenuOpenId: null }); }
deleteAccount(id) { this.setState(s => ({ deletedAccountIds: [...s.deletedAccountIds, id], accMenuOpenId: null })); }
toggleGoalMenu(id) { this.setState(s => ({ goalMenuOpenId: s.goalMenuOpenId === id ? null : id })); }
editGoalStub() { this.setState({ goalMenuOpenId: null }); }
deleteGoal(id) { this.setState(s => ({ deletedGoalIds: [...s.deletedGoalIds, id], goalMenuOpenId: null })); }
deleteCategory(id) {
this.setState(s => ({ categories: s.categories.filter(c => c.id !== id && c.parentId !== id) }));
}
buildTransactions() {
const rows = [
['Зарплата Егор', '01 авг', 'Зарплата', 88000, 1],
['Пятёрочка', '02 авг', 'Продукты', -3400, -1],
['ЖКХ Люберцы', '05 авг', 'Жильё', -9600, -1],
['Кафе', '07 авг', 'Развлечения', -1850, -1],
['Перевод на копилку', '10 авг', 'Сбережения', -15000, -1],
['Фриланс', '12 авг', 'Прочие доходы', 24000, 1],
];
return rows.map(([title, dateLabel, category, amount]) => ({
title, dateLabel, category,
amountLabel: (amount > 0 ? '+' : '') + this.fmt(amount) + ' ₽',
color: amount > 0 ? 'oklch(0.72 0.13 155)' : 'var(--color-text)',
}));
}
buildIncomeRows() {
const palette = ['oklch(0.72 0.19 25)', 'oklch(0.72 0.13 155)', 'oklch(0.78 0.17 230)', 'oklch(0.80 0.18 90)'];
const rows = [
{ title: 'Зарплата Егор', plan: 95000, actual: 88000 },
{ title: 'Зарплата Лена', plan: 65000, actual: 65000 },
{ title: 'Квартира', plan: 40000, actual: 32000 },
{ title: 'Другие доходы', plan: 8000, actual: 11500 },
];
return rows.map((r, i) => {
const pct = Math.round(r.actual / r.plan * 100);
const color = palette[i % palette.length];
const over = pct > 100;
const filled = Math.min(pct, 100);
const fillColor = over ? 'oklch(0.68 0.20 25)' : 'oklch(0.72 0.13 155)';
return {
title: r.title, color,
amountLabel: this.fmt(r.actual) + ' ₽ из ' + this.fmt(r.plan) + ' ₽',
barStyle: { height: '100%', borderRadius: '4px', background: 'linear-gradient(90deg, ' + fillColor + ' 0%, ' + fillColor + ' ' + filled + '%, var(--color-neutral-700) ' + filled + '%, var(--color-neutral-700) 100%)' },
pctLabel: pct + '%', pctColor: over ? fillColor : color,
statusLabel: pct > 100 ? 'перевыполнено ' + this.fmt(r.actual - r.plan) + ' ₽' : 'план выполнен',
};
});
}
buildBudgetGroups() {
const palette = ['oklch(0.72 0.19 25)', 'oklch(0.72 0.13 155)', 'oklch(0.78 0.17 230)', 'oklch(0.80 0.18 90)', 'oklch(0.72 0.19 330)'];
const data = [
{ title: 'Инвестиции', items: [ { title: '10%', plan: 3000, actual: 1800 }, { title: 'К', plan: 3000, actual: 900 } ] },
{ title: 'Основные', items: [ { title: 'ЖКХ', plan: 12000, actual: 11400 }, { title: 'ЖКХ Люберцы', plan: 8000, actual: 9600 }, { title: 'Питание', plan: 25000, actual: 29800 }, { title: 'Связь+Инет', plan: 2500, actual: 2500 }, { title: 'Хозтовары', plan: 4000, actual: 1650 } ] },
];
let idx = 0;
return data.map(g => {
const items = g.items.map(it => {
const pct = Math.round(it.actual / it.plan * 100);
const left = it.plan - it.actual;
const color = palette[idx++ % palette.length];
const over = pct > 100;
const filled = Math.min(pct, 100);
const fillColor = over ? 'oklch(0.68 0.20 25)' : 'oklch(0.72 0.13 155)';
return {
title: it.title,
barStyle: { height: '100%', borderRadius: '4px', background: 'linear-gradient(90deg, ' + fillColor + ' 0%, ' + fillColor + ' ' + filled + '%, var(--color-neutral-700) ' + filled + '%, var(--color-neutral-700) 100%)' },
barColor: color,
amountLabel: this.fmt(it.actual) + ' ₽ из ' + this.fmt(it.plan) + ' ₽',
pctLabel: pct + '%',
pctColor: over ? fillColor : color,
statusLabel: left > 0 ? 'осталось ' + this.fmt(left) + ' ₽' : 'осталось 0 ₽',
};
});
const gPlan = g.items.reduce((a, i) => a + i.plan, 0);
const gActual = g.items.reduce((a, i) => a + i.actual, 0);
return { title: g.title, items, summaryLabel: this.fmt(gActual) + ' ₽ из ' + this.fmt(gPlan) + ' ₽ · ' + Math.round(gActual / gPlan * 100) + '%' };
});
}
buildManageTree(type) {
const { categories } = this.state;
return categories.filter(c => !c.parentId && c.type === type).map(g => ({
id: g.id, title: g.title,
onEdit: () => this.openEditForm(g), onDelete: () => this.deleteCategory(g.id),
children: categories.filter(c => c.parentId === g.id).map(ch => ({
id: ch.id, title: ch.title,
onEdit: () => this.openEditForm(ch), onDelete: () => this.deleteCategory(ch.id),
})),
}));
}
buildPlanTree(type) {
const { categories } = this.state;
const monthNames = ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
return categories.filter(c => !c.parentId && c.type === type).map(g => {
const children = categories.filter(c => c.parentId === g.id);
const source = children.length ? children : [g];
const rows = source.map(r => {
const plan = r.monthlyPlan || Array(12).fill(0);
const cells = monthNames.map((label, i) => ({ label, value: this.fmt(plan[i]) }));
return { id: r.id, title: r.title, cells, sum: plan.reduce((a, b) => a + b, 0) };
});
const groupSum = rows.reduce((a, r) => a + r.sum, 0);
return { id: g.id, title: g.title, showRowName: children.length > 0, rows, sumLabel: this.fmt(groupSum) + ' ₽', yearLabel: this.fmt(groupSum) + ' ₽/год' };
});
}
renderVals() {
const s = this.state;
const accountsRaw = [
{ id: 1, title: 'Кошелёк', subtitle: 'Наличные', icon: 'ic-wallet', accent: 'oklch(0.72 0.19 25)', amount: this.fmt(3200) + ' ₽' },
{ id: 2, title: 'Карта Тинькофф', subtitle: 'Счёт в банке', icon: 'ic-card', accent: 'oklch(0.72 0.13 155)', amount: this.fmt(47850) + ' ₽' },
{ id: 3, title: 'Евро', subtitle: 'Счёт в банке · EUR', icon: 'ic-piggy', accent: 'oklch(0.78 0.17 230)', amount: '€' + this.fmt(3000), note: '≈' + this.fmt(313500) + ' ₽', rate: '€1 = 104,50 ₽' },
];
const accounts = accountsRaw.filter(a => !s.deletedAccountIds.includes(a.id)).map(a => ({
...a,
menuOpen: s.accMenuOpenId === a.id,
toggleMenu: () => this.toggleAccMenu(a.id),
onEdit: () => this.editAccountStub(a),
onDelete: () => this.deleteAccount(a.id),
}));
const goalsRaw = [
{ id: 1, title: 'Отпуск', saved: 62000, target: 150000, kind: 'ring' },
{ id: 2, title: 'Остаток', saved: 15400, target: 15400, kind: 'flat' },
];
const circumference = 2 * Math.PI * 22;
const goalPalette = ['oklch(0.72 0.19 330)', 'oklch(0.72 0.13 155)'];
const goals = goalsRaw.filter(g => !s.deletedGoalIds.includes(g.id)).map((g, i) => {
const pct = g.kind === 'ring' ? g.saved / g.target : 1;
const color = goalPalette[i % goalPalette.length];
return {
...g, color, savedLabel: this.fmt(g.saved) + ' ₽', targetLabel: this.fmt(g.target) + ' ₽',
pctLabel: g.kind === 'ring' ? Math.round(pct * 100) + '%' : '',
circumference: circumference.toFixed(1),
dashOffset: (circumference * (1 - pct)).toFixed(1),
menuOpen: s.goalMenuOpenId === g.id,
toggleMenu: () => this.toggleGoalMenu(g.id),
onEdit: () => this.editGoalStub(),
onDelete: () => this.deleteGoal(g.id),
};
});
const fw = s.device === 'desktop' ? 1280 : s.device === 'tablet' ? 834 : 390;
const accentStyle = (active) => ({
color: active ? 'oklch(0.72 0.13 155)' : 'var(--color-neutral-500)',
background: active ? 'color-mix(in srgb, oklch(0.72 0.13 155) 20%, transparent)' : 'transparent',
border: 'none', width: '40px', height: '40px', borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
});
return {
year: s.year,
isDashboard: s.screen === 'dashboard', isPlan: s.screen === 'plan', isAnalytics: s.screen === 'analytics',
isDesktop: s.device === 'desktop', isTablet: s.device === 'tablet', isPhone: s.device === 'phone',
dashboardStyle: { display: s.screen === 'dashboard' ? 'block' : 'none' },
planStyle: { display: s.screen === 'plan' ? 'block' : 'none' },
analyticsStyle: { display: s.screen === 'analytics' ? 'block' : 'none' },
transactionsStyle: { display: s.screen === 'transactions' ? 'block' : 'none' },
transactions: this.buildTransactions(),
goTransactions: () => this.setScreen('transactions'),
dockTransactionsStyle: accentStyle(s.screen === 'transactions'),
dockManageStyle: accentStyle(false),
frameStyle: { width: fw + 'px', maxWidth: '100%', background: 'var(--color-bg)', borderRadius: '22px', boxShadow: 'var(--shadow-lg)', overflow: 'hidden', height: '840px', display: 'flex', flexDirection: 'column', margin: '0 auto' },
mainScrollStyle: { flex: '1', overflowY: 'auto', padding: s.device === 'phone' ? '18px 16px 24px' : '30px 34px 24px', background: 'radial-gradient(600px 300px at 8% 0%, color-mix(in srgb, oklch(0.72 0.19 330) 14%, transparent), transparent 70%), radial-gradient(500px 260px at 95% 10%, color-mix(in srgb, oklch(0.78 0.17 230) 12%, transparent), transparent 70%), radial-gradient(500px 300px at 50% 60%, color-mix(in srgb, oklch(0.72 0.13 155) 8%, transparent), transparent 70%), var(--color-bg)' },
goDashboard: () => this.setScreen('dashboard'), goPlan: () => this.setScreen('plan'), goAnalytics: () => this.setScreen('analytics'),
goDesktop: () => this.setDevice('desktop'), goTablet: () => this.setDevice('tablet'), goPhone: () => this.setDevice('phone'),
dockDashboardStyle: accentStyle(s.screen === 'dashboard'),
dockPlanStyle: accentStyle(s.screen === 'plan'),
dockAnalyticsStyle: accentStyle(s.screen === 'analytics'),
accounts, goals, incomeRows: this.buildIncomeRows(), budgetGroups: this.buildBudgetGroups(),
accountModalOpen: s.accountModalOpen,
openAccountModal: () => this.openAccountModal(),
closeAccountModal: () => this.closeAccountModal(),
colorPickerOpen: s.colorPickerOpen,
toggleColorPicker: () => this.toggleColorPicker(),
colorSwatchStyle: { background: s.newAccColor },
colorSwatches: ['oklch(0.72 0.19 25)', 'oklch(0.72 0.13 155)', '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)'].map(c => ({
value: c, onSelect: () => this.setNewAccColor(c), borderStyle: s.newAccColor === c ? 'var(--color-text)' : 'transparent', check: s.newAccColor === c ? '✓' : '',
})),
accountTypeOptions: [
{ id: 'checking', title: 'Расчётный', desc: 'Повседневные траты и доходы', icon: 'ic-card', color: 'oklch(0.80 0.18 90)' },
{ id: 'savings', title: 'Накопительный', desc: 'Копилка с целью', icon: 'ic-bank', color: 'oklch(0.78 0.17 230)' },
{ id: 'cash', title: 'Наличные', desc: 'Деньги на руках', icon: 'ic-wallet', color: 'oklch(0.72 0.13 155)' },
{ id: 'credit', title: 'Кредит / Карта', desc: 'Кредит, ипотека, кредитка', icon: 'ic-card', color: 'oklch(0.72 0.19 25)' },
].map(t => ({ ...t, tileClass: s.newAccType === t.id ? 'selected' : '', onSelect: () => this.setNewAccType(t.id) })),
isSavingsType: s.newAccType === 'savings',
newAccCurrency: s.newAccCurrency,
setNewAccCurrency: (e) => this.setNewAccCurrency(e),
isPrivShared: s.privacy === 'shared', isPrivPersonal: s.privacy === 'personal',
setPrivShared: () => this.setPrivShared(), setPrivPersonal: () => this.setPrivPersonal(),
isInclYes: s.includeInTotal, isInclNo: !s.includeInTotal,
setInclYes: () => this.setInclYes(), setInclNo: () => this.setInclNo(),
selectedAccentColor: (() => { const t = ['checking','savings','cash','credit'].includes(s.newAccType) ? s.newAccType : 'checking'; const m = { checking: ['ic-card','oklch(0.80 0.18 90)'], savings: ['ic-bank','oklch(0.78 0.17 230)'], cash: ['ic-wallet','oklch(0.72 0.13 155)'], credit: ['ic-card','oklch(0.72 0.19 25)'] }; return m[t][1]; })(),
selectedAccentIcon: (() => { const m = { checking: 'ic-card', savings: 'ic-bank', cash: 'ic-wallet', credit: 'ic-card' }; return m[s.newAccType] || 'ic-card'; })(),
creditModalOpen: s.creditModalOpen, closeCreditModal: () => this.closeCreditModal(),
creditModeNewClass: s.creditMode === 'new' ? 'selected' : '', creditModeExistingClass: s.creditMode === 'existing' ? 'selected' : '',
setCreditModeNew: () => this.setCreditModeNew(), setCreditModeExisting: () => this.setCreditModeExisting(),
isCreditExisting: s.creditMode === 'existing',
isCreditCard: s.creditType === 'card', isCreditNonCard: s.creditType !== 'card',
creditMinHint: s.creditType === 'card' ? 'название, лимит, ставка' : 'название, сумма, ставка и срок или платёж',
creditTypeOptions: [
{ id: 'consumer', title: 'Потребительский', desc: 'Обычный кредит с графиком платежей', icon: 'ic-card', color: 'oklch(0.80 0.18 90)' },
{ id: 'mortgage', title: 'Ипотека', desc: 'Долгий кредит под залог жилья', icon: 'ic-home', color: 'oklch(0.72 0.19 25)' },
{ id: 'auto', title: 'Автокредит', desc: 'Кредит на покупку автомобиля', icon: 'ic-car', color: 'oklch(0.72 0.19 25)' },
{ id: 'card', title: 'Кредитная карта', desc: 'Лимит, льготный период, мин. платёж', icon: 'ic-coin', color: 'oklch(0.80 0.18 90)' },
].map(t => ({ ...t, tileClass: s.creditType === t.id ? 'selected' : '', onSelect: () => this.setCreditType(t.id) })),
creditTypeIcon: ({ consumer: 'ic-card', mortgage: 'ic-home', auto: 'ic-car', card: 'ic-coin' })[s.creditType],
creditTypeColor: s.creditType === 'card' || s.creditType === 'consumer' ? 'oklch(0.80 0.18 90)' : 'oklch(0.72 0.19 25)',
prevYear: () => this.prevYear(), nextYear: () => this.nextYear(),
openManage: () => this.openManage(), closeManage: () => this.closeManage(),
manageOpen: s.manageOpen,
incomeManageTree: this.buildManageTree('income'), expenseManageTree: this.buildManageTree('expense'),
openAddIncome: () => this.openAddIncome(), openAddExpense: () => this.openAddExpense(),
form: s.form,
isFormIncome: !!s.form && s.form.type === 'income',
isFormExpense: !!s.form && s.form.type === 'expense',
formParentOptions: s.form ? s.categories.filter(c => !c.parentId && c.type === s.form.type && c.id !== s.form.id) : [],
formExpenseOptions: s.form ? s.categories.filter(c => c.type === 'expense') : [],
setFormTitle: (e) => this.setFormTitle(e), setFormParent: (e) => this.setFormParent(e),
setFormTypeIncome: () => this.setFormTypeIncome(), setFormTypeExpense: () => this.setFormTypeExpense(),
cancelForm: () => this.cancelForm(), saveForm: () => this.saveForm(),
incomePlanTree: this.buildPlanTree('income'), expensePlanTree: this.buildPlanTree('expense'),
};
}
}
</script>
</body>
</html>