1036 lines
82 KiB
HTML
1036 lines
82 KiB
HTML
<!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)"> </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)"> </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)"> </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="{"$preview":{"width":1440,"height":900},"defaultScreen":{"editor":"enum","default":"dashboard","tsType":"string","options":["dashboard","plan","analytics"],"section":"Прототип"},"defaultDevice":{"editor":"enum","default":"desktop","tsType":"string","options":["desktop","tablet","phone"],"section":"Прототип"}}">
|
||
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>
|