/
ANZh
/
Report
Обзор
Документация
Войти
/
ANZh
/
Report
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
1 111 строк
194 KB
ANZh
Create: index.html
24 июл 2026, 09:58
Верифицирован
24 июл 2026, 09:58
4ba8ef8
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <title>Дашборд · ГК Автор новой жизни · 30.06.2026</title> <link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js"></script> <style> :root{ --st:#DDE3E8;--st2:#c5d0d9;--blk:#161616;--blk3:#2a2a2a; --wh:#fff;--mu:#6a7b8a;--bo:rgba(22,22,22,.10);--bo2:rgba(22,22,22,.20); --good:#1a5e38;--warn:#7a5000;--bad:#7a1616; --fh:'Unbounded',sans-serif;--fb:'Inter',sans-serif; --sab:env(safe-area-inset-bottom,0px); } *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} body{background:var(--st);color:var(--blk);font-family:var(--fb);font-size:13px;line-height:1.5;-webkit-text-size-adjust:100%} /* HEADER */ .hdr{background:var(--blk);position:relative;overflow:hidden} .grad{position:absolute;inset:0;background:linear-gradient(180deg,#080e16 0%,#1d3542 45%,#365a58 70%,#5e8c72 100%);opacity:.93} .hdr-in{position:relative;z-index:2;display:flex;flex-direction:column;align-items:flex-start;padding:14px 18px 12px;gap:8px} .logo{height:26px;display:block;max-width:100%} .hr{text-align:left;width:100%} .hl{font-family:var(--fh);font-size:6px;font-weight:400;letter-spacing:.18em;text-transform:uppercase;color:rgba(255,255,255,.4);margin-bottom:2px} .ht{font-family:var(--fh);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:#fff;line-height:1.2} .hd{font-size:9px;letter-spacing:.05em;color:rgba(255,255,255,.8);margin-top:2px;font-family:var(--fh)} /* TABS */ .pbar{position:relative;z-index:2;background:#000;border-top:1px solid rgba(255,255,255,.15);display:flex;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch} .pbar::-webkit-scrollbar{display:none} .ptab{flex:0 0 auto;padding:12px 11px;font-family:var(--fh);font-size:8.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:rgba(255,255,255,.4);background:rgba(255,255,255,.06);border:none;border-bottom:3px solid transparent;cursor:pointer;white-space:nowrap;min-height:44px;display:flex;align-items:center;gap:5px;transition:all .15s} .ptab.active{background:#000;border-bottom-color:var(--st);color:#fff} .ptab .bs{font-size:8px;padding:1px 4px;border-radius:2px;background:rgba(255,255,255,.12);color:rgba(255,255,255,.5)} .ptab.del .bs{background:rgba(29,158,117,.3);color:#7aded2} .cbar{background:var(--blk3);display:flex;border-bottom:1px solid rgba(255,255,255,.08);overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch} .cbar::-webkit-scrollbar{display:none} .ctab{flex:1;padding:12px 4px;font-family:var(--fh);font-size:8.5px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;color:rgba(255,255,255,.4);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;min-height:44px;display:flex;align-items:center;justify-content:center;text-align:center;transition:all .15s} .ctab.active{color:var(--st);border-bottom-color:var(--st)} .wrap{padding:12px 12px calc(16px + var(--sab))} /* LAYOUT */ .g2{display:grid;grid-template-columns:1fr;gap:10px} .g3{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:10px} .g4{display:grid;grid-template-columns:1fr;gap:7px;margin-bottom:10px} /* CARD */ .card{background:var(--wh);border:1px solid var(--bo);padding:12px 13px 11px;position:relative} .card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--ac,transparent)} .clbl{font-family:var(--fh);font-size:8px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--mu);margin-bottom:6px} .v1{font-family:var(--fh);font-size:17px;font-weight:700;color:var(--blk);line-height:1} .v1 .u{font-size:11px;font-weight:400;color:var(--mu)} .rsub{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:6px;padding-top:6px;border-top:1px solid var(--bo)} /* BADGE */ .badge{display:inline-block;padding:2px 7px;font-size:11px;font-weight:600;font-family:var(--fh)} .bg{background:#d8f0e4;color:var(--good)}.bw{background:#f5e9d0;color:var(--warn)} .br{background:#f5d8d8;color:var(--bad)}.bi{background:#d8e8f5;color:#0c447c} .bn{background:#e8edf0;color:var(--mu)}.bd{background:rgba(29,158,117,.15);color:#0f6b4a} /* PANEL */ .pan{background:var(--wh);border:1px solid var(--bo);padding:12px 13px;margin-bottom:8px} .ptit{font-family:var(--fh);font-size:8px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--mu);margin-bottom:10px} .ch{position:relative;height:180px} .chsm{position:relative;height:140px} /* CYCLE */ .cycle-track{height:30px;background:var(--st);position:relative;overflow:hidden;margin-bottom:4px} .ce{position:absolute;top:0;bottom:0;left:0;background:rgba(22,22,22,.09)} .cs{position:absolute;top:0;height:10px;left:0} .cb{position:absolute;top:10px;height:10px;left:0} .ck{position:absolute;bottom:0;height:10px;left:0;background:#185fa5;opacity:.55} .cl{position:absolute;top:0;bottom:0;width:3px;background:rgba(22,22,22,.75);z-index:3;transform:translateX(-1px)} /* BARS */ .prow{margin-bottom:5px} .plbl{display:flex;justify-content:space-between;font-size:11px;color:var(--mu);margin-bottom:2px} .pval{font-weight:600;color:var(--blk)} .pbg{height:9px;background:var(--st);overflow:hidden} .pf{height:100%} /* NORM */ .nrow{display:flex;align-items:center;gap:6px;padding:6px 0;border-bottom:.5px solid var(--bo)} .nrow:last-child{border-bottom:none} .ndot{width:10px;height:10px;flex-shrink:0} .nname{font-size:11px;flex:1} .nval{font-size:11px;font-weight:600;min-width:36px;text-align:right} .ntrack{flex:0 0 75px;height:6px;background:var(--st);position:relative} .nbar{height:100%;position:absolute;left:0;top:0} .nrng{position:absolute;top:-2px;height:10px;opacity:.25} .nbdg{font-size:9px;font-weight:600;padding:2px 5px;font-family:var(--fh);white-space:nowrap} /* PF ROWS */ .pfrow{padding:8px 0;border-bottom:.5px solid var(--bo)} .pfrow:last-child{border-bottom:none} .pfl{display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:4px;flex-wrap:wrap;gap:4px} .pfn{font-weight:600} .pfbg{height:11px;background:var(--st);position:relative;overflow:visible;margin-top:14px} .pfb{height:100%;position:absolute;left:0;top:0} .pfm{display:flex;gap:12px;font-size:10px;color:var(--mu);margin-top:3px;flex-wrap:wrap} /* INT BARS */ .irow{margin-bottom:8px} .ihead{display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px} .itrack{height:11px;background:var(--st);position:relative;overflow:hidden} .ibar{height:100%;position:absolute;left:0;top:0} /* KPI */ .tscr{overflow-x:auto;-webkit-overflow-scrolling:touch;position:relative} .scrollhint{display:none;font-size:10px;color:var(--mu);margin:0 0 5px;text-align:right} .tpan{background:var(--wh);border:.5px solid var(--bo);min-width:0} .tscr .tpan{min-width:760px} .thd{display:grid;padding:6px 10px;background:var(--st2);border-bottom:.5px solid var(--bo2)} .thd span{font-family:var(--fh);font-size:7px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--blk3);text-align:center} .thd span:first-child{text-align:left} .trow{display:grid;padding:7px 10px;border-bottom:.5px solid var(--bo);align-items:center} .trow:last-child{border-bottom:none} .trow.grp{background:rgba(197,208,217,.25);padding:4px 10px} .tn{font-size:11px;font-weight:500} .tn.grp{font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--mu)} .tc{text-align:center;font-size:11px} .tc.mu{color:var(--mu);font-size:10px;font-style:italic;text-align:left} /* LG */ .lg{display:flex;gap:10px;margin-bottom:8px;flex-wrap:wrap} .lg span{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--mu)} .lgsq{width:9px;height:9px;border-radius:2px;flex-shrink:0} /* HR */ .hrbench{background:var(--st);padding:10px 12px;margin-top:8px;font-size:11px;line-height:1.7;border-left:3px solid var(--blk3)} /* INSIGHT */ .ins{padding:10px 13px;font-size:12px;line-height:1.65;margin-top:8px;border-left:3px solid transparent} .ins-g{background:#eef7f2;border-left-color:var(--good)} .ins-w{background:#fdf4e4;border-left-color:var(--warn)} .ins-b{background:#fceaea;border-left-color:var(--bad)} .ins-i{background:#e6f0fb;border-left-color:#185fa5} .ins-d{background:rgba(29,158,117,.08);border-left-color:#1a8c5e} .sep{height:.5px;background:var(--bo);margin:8px 0} .foot{background:var(--blk);border-top:1px solid rgba(255,255,255,.07);padding:10px 18px calc(10px + var(--sab));display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px} .foot span{font-family:var(--fh);font-size:7px;letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.25)} @media(min-width:580px){ .g3{grid-template-columns:repeat(3,1fr)} .g4{grid-template-columns:repeat(2,1fr)} .ntrack{flex:0 0 95px} .ctab{font-size:8px;padding:11px 6px} } @media(max-width:579px){ .ctab{flex:0 0 96px;font-size:8.5px} .ptab{font-size:8.5px} .pfrow{padding:11px 0} .pfm{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px;font-size:10px;line-height:1.35} .scrollhint{display:block} .thd span:first-child,.trow>.tn{position:sticky;left:0;z-index:2;background:var(--wh);box-shadow:8px 0 8px rgba(255,255,255,.9);padding-right:8px} .thd span:first-child{z-index:3;background:var(--st2);box-shadow:8px 0 8px rgba(197,208,217,.9)} .trow.grp>.tn{background:transparent;box-shadow:none} .badge{font-size:10px;padding:2px 5px} .tc{font-size:10px} } @media(min-width:640px){ .hdr-in{flex-direction:row;align-items:center;justify-content:space-between;padding:20px 32px 16px;gap:14px} .hr{text-align:right;width:auto} .logo{height:36px}.ht{font-size:16px} .ptab{padding:14px 18px;font-size:9px} .ctab{font-size:8.5px;padding:11px 10px} .wrap{padding:20px 28px 40px} .ch{height:220px}.chsm{height:170px} .ntrack{flex:0 0 105px} .g2{grid-template-columns:1fr 1fr} } @media(min-width:900px){ .hdr-in{padding:22px 40px 18px} .logo{height:40px} .wrap{padding:22px 36px 44px} .g4{grid-template-columns:repeat(4,1fr)} } </style> </head> <body> <div class="hdr"> <div class="grad"></div> <div class="hdr-in"> <img class="logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC9QAAAFoCAYAAAAMgFLmAAABCGlDQ1BJQ0MgUHJvZmlsZQAAeJxjYGA8wQAELAYMDLl5JUVB7k4KEZFRCuwPGBiBEAwSk4sLGHADoKpv1yBqL+viUYcLcKakFicD6Q9 … [Строка слишком длинная. Вы можете скачать файл] <div class="hr"> <div class="hl">Девелоперские проекты · управление портфелем</div> <div class="ht">Портфель проектов</div> <div class="hd">нарастающим итогом · 30.06.2026</div> </div> </div> <div class="pbar" id="pbar"></div> </div> <div class="cbar" id="cbar"></div> <div class="wrap" id="main"></div> <div class="foot"><span>Автор новой жизни · 2026</span><span>Управленческий дашборд</span></div> <script> const PROJS=[ {id:'all',label:'Портфель', short:'Портфель',col:'#DDE3E8',st:''}, {id:'a1', label:'Академия 1',short:'Акад.1', col:'#C0C0C0',st:'del',dlv:'сдан 2025'}, {id:'a2', label:'Академия 2',short:'Акад.2', col:'#808080',st:'', dlv:'авг 2027'}, {id:'ab1',label:'Арбери 1', short:'Арб.1', col:'#C0392B',st:'del',dlv:'апр 2026'}, {id:'ab2',label:'Арбери 2', short:'Арб.2', col:'#7B1A1A',st:'', dlv:'апр 2028'}, {id:'dob',label:'Доброе', short:'Доброе', col:'#9B7CC5',st:'', dlv:'апр 2028'}, {id:'tez',label:'Тезис', short:'Тезис', col:'#8B7355',st:'early',dlv:'сен 2030'}, {id:'ter',label:'Терра', short:'Терра', col:'#C17A40',st:'', dlv:'сен 2029'}, ]; // ═══ ДАННЫЕ (все нарастающим итогом 30.06.2026) ═══════════════════ const D = { names: ['Академия 2','Арбери 1','Арбери 2','Доброе','Тезис','Терра'], sh5: ['Акад.2','Арб.1','Арб.2','Доброе','Тезис','Терра'], col5: ['#808080','#C0392B','#7B1A1A','#9B7CC5','#8B7355','#C17A40'], dlv: ['авг 2027','апр 2026','апр 2028','апр 2028','сен 2030','сен 2029'], cycle: [68.9, 100.0, 47.6, 45.0, 5.6, 18.8], // фактические даты: Акад2 ноя.23-авг.27, Арб2 окт.24-апр.28, Доброе дек.24-апр.28, Тезис мар.26-сен.30, Терра сен.25-сен.29 // Источник: ПФ_по_проектам_на_3006, нарастающим итогом. Тезис добавлен с мая 2026. // Выручка и стройка план/факт (М руб) rv_p: [1697.89, 3786.61, 3123.29, 1549.89, 7110.79, 2009.73], rv_f: [1258.16, 3727.42, 2086.90, 994.27, 336.53, 604.38], bld_p: [1343.29, 2665.63, 2055.57, 1225.74, 4801.09, 1621.49], bld_f: [1035.56, 2563.64, 996.01, 568.24, 51.47, 410.43], ddu_p: [1615.92, 3372.38, 2873.76, 1495.93, 7110.79, 2009.73], ddu_f: [1258.16, 3402.57, 2086.85, 994.27, 336.53, 604.33], // Продажи = выручка факт / план, включая ДДУ, ДКП и прочие продажи. sp: [74.1, 98.4, 66.8, 64.2, 4.7, 30.1], area_sold: [73.9, 98.3, 69.3, 64.0, 7.5, 30.3], // Распроданность по площади, PDF "Распроданность по площади" st: [77.1, 96.2, 48.5, 46.4, 1.1, 25.3], bank_ready: [96.8, null, 80.5, 85.4, 5.7, 42.4], // строительная готовность по чек-листу, направленному в банк ko: [0.96, 1.02, 1.38, 1.38, 4.30, 1.19], gap: [-3.0, +2.3, +18.4, +17.8, +3.6, +4.8], // % по ПФ / выручка нарастающим (строка 190 ПФ файл 30.06) int_p: [8.97, 3.81, 3.47, 4.55, 7.87, 7.35], int_f: [10.09, 3.87, 3.40, 4.37, 0.08, 3.17], // Проценты абсолютные нарастающим М руб (строка 165 ПФ файл 30.06) int_abs_p: [152.24, 144.33, 108.28, 70.47, 559.81, 147.74], int_abs_f: [126.99, 144.34, 70.97, 43.44, 0.27, 19.14], // ПФ лимиты (финмодель СВОД деньги) и выборка нарастающим (факт) pf_lim: [1278, 2500, 1901, 1161, 4300, 1491], pf_drw: [1180.62, 2468, 1497.99, 783.38, 112.32, 435.19], pf_closed: [false, true, false, false, false, false], // Арб1 = погашен // Расходы % выручки нарастающим (ПФ 30.06) bl: [82.31, 68.78, 47.73, 57.15, 15.30, 67.91], cm: [5.22, 1.90, 3.06, 4.11, 4.68, 3.59], mg: [5.88, 3.47, 3.60, 4.12, 2.67, 5.24], tx: [2.92, 2.23, 2.38, 1.81, -0.50, 1.92], hr: { hc: [93,97,101,111,108,121], sal: [130699,138167,123496,145084,148742,107431], hired:[4,6,6,11,1,16], fired:[3,3,3,3,2,3], fot_pct:3.78, rev_emp:3.57 }, // Академия 1 — факт нарастающим из ПФ 30.06 (плана нет) a1: {rv_f:1043.69, bld_f:1037.79, int_abs_f:16.52, int_f_pct:1.58, bl:99.43, cm:3.17, mg:4.46, tx:2.77}, ins_cls: ['ins-b','ins-d','ins-w','ins-w','ins-i','ins-i'], note: [ 'Продажи 74.1%, фин. освоение стройки 77.1%, стройготовность банка 96.8%. Банк выше фин. освоения на 19.7 пп. % по ПФ: 10.09% — зона внимания. Лимит ПФ выбран на 92.4%, остаток 97М. Сдача август 2027.', 'Объект передан покупателям апрель 2026. ПФ полностью погашен (2 468М из 2 500М). Эскроу раскрыт. Проценты уплачены нарастающим: 144.3М = план. Показатели финальные.', 'Фин. освоение стройки 48.5%, стройготовность банка 80.5%. Банк выше фин. освоения на 32.0 пп. Продажи 66.8% опережают фин. освоение на 18.4 пп. % по ПФ 3.40% — в зоне «очень эффективно». Сдача апрель 2028.', 'Фин. освоение стройки 46.4%, стройготовность банка 85.4%. Банк выше фин. освоения на 39.0 пп. Продажи 64.2% опережают фин. освоение на 17.8 пп. % по ПФ 4.37% — в зоне нормы. Сдача апрель 2028.', 'Проект в начальной стадии продаж. Продажи 4.7%, фин. освоение стройки 1.1%, стройготовность банка 5.7%. ПФ выбрано 112М из 4 300М лимита. % по ПФ 0.08% — ПФ только открывается. Сдача сентябрь 2030.', 'Ранняя стадия. Продажи 30.1%, фин. освоение стройки 25.3%, стройготовность банка 42.4%. Банк выше фин. освоения на 17.1 пп. % по ПФ: факт 3.17% — в зоне «очень эффективно». Сдача сентябрь 2029.' ] }; const COST_META = [ {key:'bl', label:'Фин. стройка', col:'#2466a8', norm:[55,80]}, {key:'int_f', label:'% по ПФ', col:'#3e3490', norm:[5,8]}, {key:'cm', label:'Коммерч.',col:'#b84020', norm:[2,4]}, {key:'mg', label:'Управл.', col:'#9a6010', norm:[3,5]}, {key:'tx', label:'Налоги', col:'#505850', norm:[4,7]}, ]; // Доли выделенных желтым статей из Сводного отчёта ГК, янв-июнь 2026 const MONTHLY_RATIOS = { months: ['Январь','Февраль','Март','Апрель','Май','Июнь'], series: [ {label:'ФОТ / расходы всего', shortLabel:'ФОТ', data:[3.4, 7.2, 4.7, 5.3, 5.8, 3.8], col:'#2466a8'}, {label:'Накладные / продажи', shortLabel:'Накладн.', data:[1.1, 0.4, 0.8, 1.6, 0.7, 0.8], col:'#1a8c5e'}, {label:'Налоги / продажи', shortLabel:'Налоги', data:[3.2,-0.9, 3.4, 3.8, 0.9, 0.8], col:'#6a6a6a'}, {label:'Маркетинг и реклама / продажи', shortLabel:'Маркетинг', data:[1.7, 1.5, 2.9, 1.7, 2.2, 3.3], col:'#d4302a'}, {label:'АН и регистрация / продажи', shortLabel:'АН/рег.', data:[3.4, 1.6, 0.9, 1.4, 0.6, 1.7], col:'#9a4fc7'}, {label:'Кредиты и займы / продажи', shortLabel:'Кредиты', data:[11.0, 4.0, 4.0, 4.0, 5.0, 4.0], col:'#e08214'}, ] }; const rv_f_tot = D.rv_f.reduce((a,b)=>a+b,0); const rv_p_tot = D.rv_p.reduce((a,b)=>a+b,0); const bld_f_tot= D.bld_f.reduce((a,b)=>a+b,0); const bld_p_tot= D.bld_p.reduce((a,b)=>a+b,0); // ═══ HELPERS ══════════════════════════════════════════════════════ function fmtM(v){return Math.round(v).toLocaleString('ru-RU')+' М'} function fmtM2(v){return Number(v).toFixed(2)+'М'} function sgn(v){return v>0?'+':''} function normSt(v,[mn,mx]){ if(v<=mx&&v>=mn) return{cls:'bg',label:'в норме',bar:'#1a5e38'}; if(v>mx&&v<=mx*1.5) return{cls:'bw',label:'выше',bar:'#7a5000'}; if(v>mx*1.5) return{cls:'br',label:'превыш.',bar:'#7a1616'}; return{cls:'bi',label:'ниже нормы',bar:'#185fa5'}; } function bCls(v,type){ if(type==='gap') return Math.abs(v)<=5?'bg':Math.abs(v)<=15?'bw':'br'; if(type==='ko') return v<0.9?'br':v<=1.2?'bg':v<=1.4?'bw':'br'; if(type==='int') return v>12?'br':v>8?'bw':v>=3?'bg':'bi'; // <3 ранняя стадия, 3-8 норма, 8-12 напряженно, >12 опасно if(type==='rem') return v<10?'br':v<25?'bw':'bg'; if(type==='cm') return v>5?'br':v>4?'bw':'bg'; if(type==='mg') return v>6?'br':v>5?'bw':'bg'; if(type==='bl') return v>90?'br':v>=55&&v<=85?'bg':v>=40?'bw':'br'; if(type==='tx') return v<2?'br':v<4?'bw':v<=7?'bg':'bi'; // <2 риск, 2-4 повышенный, 4-7 норма, >10 белая if(type==='bank') return v>=80?'bg':v>=50?'bw':'bi'; if(type==='bank_gap') return v>=25?'bw':v>=0?'bg':'br'; return 'bn'; } function badge(v,type,fn){return`<span class="badge ${bCls(v,type)}">${fn(v)}</span>`;} function valBadge(v,type,fn){return v==null?'<span class="badge bn">н/д</span>':badge(v,type,fn);} function koNote(i){ const early=D.st[i]<10?' При фин. стройке <10% коэффициент нестабилен из-за малой базы.':''; return `0.9–1.2 баланс, 1.2–1.4 внимание, >1.4 риск.${early}`; } const CH={}; function dc(id){if(CH[id]){try{CH[id].destroy()}catch(e){}delete CH[id];}} let curP='all',curT=0; // Прямое соответствие id проекта -> индекс в массиве D const P2I = {a2:0, ab1:1, ab2:2, dob:3, tez:4, ter:5}; const TABS=['Темп / цикл','Расходы','ПФ и проценты','HR','KPI']; // ═══ TABS BUILD ════════════════════════════════════════════════════ function buildTabs(){ const mob=window.innerWidth<480; document.getElementById('pbar').innerHTML=PROJS.map(p=>` <button class="ptab${p.id===curP?' active':''}${p.st==='del'?' del':''}" onclick="sP('${p.id}')"> ${mob?p.short:p.label}${p.dlv?`<span class="bs">${p.dlv}</span>`:''} </button>`).join(''); document.getElementById('cbar').innerHTML=TABS.map((t,i)=> `<button class="ctab${i===curT?' active':''}" onclick="sT(${i})">${t}</button>`).join(''); } function sP(id){curP=id;render();} function sT(t){curT=t;render();} // ═══ RENDER ════════════════════════════════════════════════════════ function buildSellStatus(i){ const actual = (D.rv_f[i]/D.rv_p[i]*100).toFixed(1); const expected = (D.cycle[i]*0.85).toFixed(1); const diff = (D.rv_f[i]/D.rv_p[i]*100) - D.cycle[i]*0.85; const diffFmt = (diff>=0?'+':'')+diff.toFixed(1)+' пп'; let cls, lbl; if(D.pf_closed[i]){cls='bd';lbl='проект сдан';} else if(diff>=0){cls='bg';lbl='выше ожидаемого';} else if(diff>=-10){cls='bw';lbl='ниже ожидаемого';} else{cls='br';lbl='существенно ниже';} return '<span style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +'<span>Продажи (выручка ф/п): <b style="color:var(--blk)">'+actual+'%</b></span>' +'<span style="color:var(--bo2)">|</span>' +'<span>Ожидаемая ('+D.cycle[i].toFixed(0)+'% × 85%): <b style="color:var(--blk)">'+expected+'%</b></span>' +'<span>Откл.: <span class="badge '+cls+'">'+diffFmt+' — '+lbl+'</span></span>' +'</span>'; } // Register datalabels plugin if available if(typeof ChartDataLabels !== 'undefined'){ Chart.register(ChartDataLabels); Chart.defaults.set('plugins.datalabels', {display:false}); } function render(){ buildTabs(); const w=document.getElementById('main'); const idx = (curP in P2I) ? P2I[curP] : -1; if(curP==='all'){ if(curT===0) w.innerHTML=tPaceAll(); else if(curT===1) w.innerHTML=tCostAll(); else if(curT===2) w.innerHTML=tPFAll(); else if(curT===3) w.innerHTML=tHR(); else w.innerHTML=tKpiAll(); } else if(curP==='a1'){ w.innerHTML=tA1(); } else { if(curT===0) w.innerHTML=tPaceOne(idx); else if(curT===1) w.innerHTML=tCostOne(idx); else if(curT===2) w.innerHTML=tPFOne(idx); else if(curT===3) w.innerHTML=tHR(); else w.innerHTML=tKpiOne(idx); } requestAnimationFrame(drawCharts); } // ═══ ТЕМП / ЦИКЛ ALL ══════════════════════════════════════════════ function tPaceAll(){ const rows=D.names.map((n,i)=>{ const exp=D.cycle[i],sp=D.sp[i],area=D.area_sold[i],st=D.st[i]; const bank=D.bank_ready[i],bankGap=bank==null?null:bank-st; const sd=sp-exp,bd=st-exp,bal=sp-st; const sc=Math.abs(sd)<8?'bg':Math.abs(sd)<18?'bw':'br'; const bc=Math.abs(bd)<8?'bg':Math.abs(bd)<18?'bw':'br'; const balc=Math.abs(bal)<5?'bg':Math.abs(bal)<15?'bw':'br'; return `<div style="margin-bottom:10px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px"> <span style="font-size:11px;font-weight:600;color:${D.col5[i]}">${n}</span> <span style="font-size:10px;color:var(--mu)">Сдача: ${D.dlv[i]} · ${exp.toFixed(0)}% цикла</span> </div> <div class="cycle-track"> <div class="ce" style="width:${Math.min(exp,100)}%"></div> <div class="cs" style="width:${Math.min(sp,100)}%;background:${D.col5[i]};opacity:.9"></div> <div class="cb" style="width:${Math.min(st,100)}%;background:${D.col5[i]};opacity:.5"></div> ${bank==null?'':`<div class="ck" style="width:${Math.min(bank,100)}%"></div>`} <div class="cl" style="left:${Math.min(exp,99.5)}%"></div> </div> <div style="display:flex;gap:8px;font-size:10px;flex-wrap:wrap"> <span>Продажи (выручка ф/п): <span class="badge ${sc}" style="font-size:9px">${sp.toFixed(1)}%</span></span> <span>Распроданность по S: <span class="badge bn" style="font-size:9px">${area.toFixed(1)}%</span></span> <span>Фин. стройка: <span class="badge ${bc}" style="font-size:9px">${st.toFixed(1)}%</span></span> <span>Банк: ${valBadge(bank,'bank',v=>v.toFixed(1)+'%')}</span> ${bankGap==null?'':`<span>Банк-фин.: ${badge(bankGap,'bank_gap',v=>'+'+v.toFixed(1)+' пп')}</span>`} </div> </div>`; }).join(''); return `<div class="pan"> <div class="ptit">Позиция в цикле · отклонение от ожидаемого</div> <div class="lg"> <span><span style="display:inline-block;width:14px;height:5px;background:rgba(22,22,22,.09)"></span>ожидаемый % для стадии</span> <span><span style="display:inline-block;width:14px;height:5px;background:#555;opacity:.9"></span>продажи (выручка ф/п, верх)</span> <span><span style="display:inline-block;width:14px;height:5px;background:#555;opacity:.5"></span>фин. стройка (середина)</span> <span><span style="display:inline-block;width:14px;height:5px;background:#185fa5;opacity:.55"></span>стройготовность банка (низ)</span> </div> ${rows} <div style="font-size:10px;color:var(--mu);margin-top:6px;line-height:1.5">Фин. стройка = СМР + прочие работы / план. Распроданность по S = проданная площадь / площадь проекта по PDF «Распроданность по площади». Стройготовность банка = данные чек-листа, направленного в банк. Разрыв банк-фин. показывает отличие технической готовности от финансового освоения.</div> <div style="margin-top:8px;background:var(--wh);border:.5px solid var(--bo);padding:10px 12px;font-size:11px;line-height:1.7"> <div style="font-family:var(--fh);font-size:7px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--mu);margin-bottom:8px">Бенчмарк РФ — наш.дом.рф, июнь 2026</div> <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px 12px;margin-bottom:10px"> <div><div style="color:var(--mu);font-size:10px">Распроданность РФ</div><div style="font-size:17px;font-weight:700">31%</div><div style="font-size:10px;color:var(--mu)">36 716 тыс.м²</div></div> <div><div style="color:var(--mu);font-size:10px">Стройготовность РФ</div><div style="font-size:17px;font-weight:700">44%</div><div style="font-size:10px;color:var(--mu)">119 739 тыс.м²</div></div> <div><div style="color:var(--mu);font-size:10px">Распр. / стройгот.</div><div style="font-size:17px;font-weight:700">70%</div><div style="font-size:10px;color:var(--mu)">по наш.дом.рф</div></div> <div><div style="color:var(--mu);font-size:10px">Распр. на вводе 2026</div><div style="font-size:17px;font-weight:700">54%</div><div style="font-size:10px;color:var(--mu)">стройгот. 83%</div></div> </div> <div style="font-size:10px;font-weight:600;color:var(--mu);margin-bottom:4px;letter-spacing:.04em">По году ввода: распроданность / стройготовность / отн.</div> <div style="display:grid;grid-template-columns:repeat(6,minmax(38px,1fr));gap:3px;text-align:center"> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2026</div><div style="font-weight:700;font-size:12px">54%</div><div style="font-size:10px;color:var(--mu)">83%</div><div style="font-size:10px;color:#185fa5">65%</div></div> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2027</div><div style="font-weight:700;font-size:12px">35%</div><div style="font-size:10px;color:var(--mu)">48%</div><div style="font-size:10px;color:#185fa5">73%</div></div> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2028</div><div style="font-weight:700;font-size:12px">19%</div><div style="font-size:10px;color:var(--mu)">22%</div><div style="font-size:10px;color:#185fa5">85%</div></div> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2029</div><div style="font-weight:700;font-size:12px">9%</div><div style="font-size:10px;color:var(--mu)">14%</div><div style="font-size:10px;color:#185fa5">65%</div></div> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2030</div><div style="font-weight:700;font-size:12px">6%</div><div style="font-size:10px;color:var(--mu)">11%</div><div style="font-size:10px;color:#185fa5">62%</div></div> <div style="background:var(--st);padding:4px 2px"><div style="font-size:9px;color:var(--mu)">2031+</div><div style="font-weight:700;font-size:12px">3%</div><div style="font-size:10px;color:var(--mu)">8%</div><div style="font-size:10px;color:#185fa5">42%</div></div> </div> <div style="margin-top:6px;font-size:10px;color:var(--mu)"><b>Жирный</b> — распроданность, серый — стройготовность, синий — отношение. Источник: наш.дом.рф, июнь 2026.</div> </div> </div> <div class="g2"> <div class="pan"><div class="ptit">Продажи (выручка ф/п)</div><div class="ch"><canvas id="chSp"></canvas></div></div> <div class="pan"><div class="ptit">Фин. освоение стройки % от плана</div><div class="ch"><canvas id="chSt"></canvas></div></div> </div> <div class="pan"><div class="ptit">Коэффициент опережения продаж (выручка ф/п / фин. стройка)</div> <div class="chsm"><canvas id="chKo"></canvas></div> <div style="font-size:10px;color:var(--mu);margin-top:5px">1.0 — баланс. >1 — продажи опережают фин. стройку. <1 — фин. стройка впереди продаж.</div> </div>`; } // ═══ ТЕМП ONE ════════════════════════════════════════════════════ function tPaceOne(i){ const sp=D.sp[i],st=D.st[i],gap=D.gap[i],ko=D.ko[i],exp=D.cycle[i]; const area=D.area_sold[i]; const bank=D.bank_ready[i],bankGap=bank==null?null:bank-st; const sd=sp-exp,bd=st-exp,bal=sp-st; const isDel=D.pf_closed[i]; return `${isDel?`<div class="ins ins-d">Объект передан покупателям апрель 2026. ПФ погашен. Эскроу раскрыт. Показатели финальные.</div>`:''} <div class="pan" style="margin-top:${isDel?'8':'0'}px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> <div class="ptit" style="margin-bottom:0">Позиция в цикле · сдача ${D.dlv[i]}</div> <span class="badge bn">${exp.toFixed(0)}% цикла пройдено</span> </div> <div class="cycle-track" style="height:30px;margin-bottom:6px"> <div class="ce" style="width:${Math.min(exp,100)}%"></div> <div class="cs" style="width:${Math.min(sp,100)}%;background:${D.col5[i]};opacity:.9"></div> <div class="cb" style="width:${Math.min(st,100)}%;background:${D.col5[i]};opacity:.5"></div> ${bank==null?'':`<div class="ck" style="width:${Math.min(bank,100)}%"></div>`} <div class="cl" style="left:${Math.min(exp,99.5)}%"></div> </div> <div style="display:flex;gap:14px;font-size:11px;flex-wrap:wrap"> <span>Продажи (выручка ф/п) <b>${sp.toFixed(1)}%</b> <span style="color:var(--mu)">(${sgn(sd)}${sd.toFixed(1)}пп к ожид.)</span></span> <span>Распроданность по S <b>${area.toFixed(1)}%</b></span> <span>Фин. стройка <b>${st.toFixed(1)}%</b> <span style="color:var(--mu)">(${sgn(bd)}${bd.toFixed(1)}пп)</span></span> <span>Банк ${valBadge(bank,'bank',v=>v.toFixed(1)+'%')}</span> ${bankGap==null?'':`<span>Банк-фин. ${badge(bankGap,'bank_gap',v=>'+'+v.toFixed(1)+' пп')}</span>`} </div> </div> <div class="g4"> <div class="card" style="--ac:#b2c4d2"><div class="clbl">Продажи (выручка ф/п)</div> <span class="v1">${sp.toFixed(1)}<span class="u">%</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">выручка факт / план</span></div></div> <div class="card" style="--ac:#6a7b8a"><div class="clbl">Распроданность по S</div> <span class="v1">${area.toFixed(1)}<span class="u">%</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">проданная площадь / площадь проекта</span></div></div> <div class="card" style="--ac:#161616"><div class="clbl">Фин. освоение стройки</div> <span class="v1">${st.toFixed(1)}<span class="u">%</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">от плана нарастающим</span></div></div> <div class="card" style="--ac:#185fa5"><div class="clbl">Стройготовность банка</div> <span class="v1">${bank==null?'н/д':bank.toFixed(1)}<span class="u">${bank==null?'':'%'}</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">чек-лист, направленный в банк</span></div></div> <div class="card" style="--ac:#9a6010;grid-column:1/-1"><div class="clbl">Баланс продаж и фин. стройки</div> <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap"> <span class="v1">${sgn(bal)}${bal.toFixed(1)}<span class="u">пп</span></span> <span class="badge ${Math.abs(bal)<=5?'bg':Math.abs(bal)<=15?'bw':'br'}">${Math.abs(bal)<=5?'в балансе':bal>0?'продажи опережают':'стройка опережает'}</span> <span style="font-size:11px;color:var(--mu)">Коэф. ${ko.toFixed(2)}</span> ${bankGap==null?'':`<span style="font-size:11px;color:var(--mu)">Банк-фин.: <b style="color:var(--blk)">${sgn(bankGap)}${bankGap.toFixed(1)} пп</b></span>`} </div></div> </div> <div class="pan"> <div class="prow"><div class="plbl"><span>Продажи (выручка ф/п)</span><span class="pval">${sp.toFixed(1)}%</span></div> <div class="pbg"><div class="pf" style="width:${Math.min(sp,100)}%;background:#2466a8"></div></div></div> <div class="prow"><div class="plbl"><span>Фин. освоение стройки (СМР+прочие факт/план)</span><span class="pval">${st.toFixed(1)}%</span></div> <div class="pbg"><div class="pf" style="width:${Math.min(st,100)}%;background:#161616"></div></div></div> ${bank==null?'':`<div class="prow"><div class="plbl"><span>Стройготовность банка (чек-лист)</span><span class="pval">${bank.toFixed(1)}%</span></div> <div class="pbg"><div class="pf" style="width:${Math.min(bank,100)}%;background:#185fa5;opacity:.65"></div></div></div>`} <div class="sep"></div> <div style="display:flex;gap:14px;font-size:11px;color:var(--mu);flex-wrap:wrap"> <span>Выручка: <b style="color:var(--blk)">${fmtM(D.rv_f[i])}</b> / ${fmtM(D.rv_p[i])}</span> <span>Фин. стройка: <b style="color:var(--blk)">${fmtM(D.bld_f[i])}</b> / ${fmtM(D.bld_p[i])}</span> ${bankGap==null?'':`<span>Разрыв банк-фин.: <b style="color:var(--blk)">${sgn(bankGap)}${bankGap.toFixed(1)} пп</b></span>`} </div> </div> <div style="font-size:10px;color:var(--mu);background:var(--wh);border:.5px solid var(--bo);padding:10px 12px;margin-top:8px;line-height:1.7"> ${ buildSellStatus(i) } </div> </div> <div class="ins ${D.ins_cls[i]}">${D.note[i]}</div>`; } // ═══ РАСХОДЫ ALL ══════════════════════════════════════════════════ function tCostAll(){ return `<div class="g2"> <div class="pan"> <div class="ptit">Структура расходов — % от выручки нарастающим, по проектам</div> <div class="lg">${COST_META.map(m=>`<span><span class="lgsq" style="background:${m.col}"></span>${m.label}</span>`).join('')}</div> <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px"> ${D.names.map((n,i)=>` <div style="text-align:center"> <div style="font-size:10px;font-weight:600;color:${D.col5[i]};margin-bottom:3px">${n}</div> <div style="position:relative;width:100%;max-width:130px;margin:0 auto;aspect-ratio:1"><canvas id="chCAd_${i}"></canvas></div> </div>`).join('')} </div> <div style="font-size:10px;color:var(--mu);margin-top:6px">% по ПФ = уплаченные проценты по ПФ / выручка нарастающим (строка 190 ПФ отчёта). Ориентир: 3–5% эффективно, 5–8% норма, 8–12% напряженно, >12% опасно.<br><b>Налоги:</b> норма рынка 4–7%, повышенный риск 2–4%, <2% — очень высокий риск по налоговому контролю. У всех проектов 2–3% — ожидаемо для фазы строительства при УСН: налоговая база признаётся при раскрытии эскроу.</div> </div> <div class="pan" style="display:flex;flex-direction:column"> <div class="ptit">Доля расходов в продажах по компании · динамика с начала 2026 года</div> <div style="position:relative;flex:1;min-height:420px"><canvas id="chMonthlyRatios"></canvas></div> <div style="font-size:10px;color:var(--mu);margin-top:6px">Источник: Сводный отчёт ГК, янв–июнь 2026. Продажи/выручка факт = заключение ДДУ + заключено ДКП.</div> </div> </div>`; } // ═══ РАСХОДЫ ONE ══════════════════════════════════════════════════ function tCostOne(i){ const vals=COST_META.map(m=>D[m.key][i]); const total=vals.reduce((a,b)=>a+b,0); const SCALE=95; const nr=COST_META.map((m,j)=>{ const v=vals[j],st=normSt(v,m.norm); return `<div class="nrow"> <div class="ndot" style="background:${m.col}"></div> <div class="nname">${m.label}</div> <div class="nval">${v.toFixed(2)}%</div> <div class="ntrack"> <div class="nrng" style="left:${(m.norm[0]/SCALE*100).toFixed(1)}%;width:${((m.norm[1]-m.norm[0])/SCALE*100).toFixed(1)}%;background:${st.bar}"></div> <div class="nbar" style="width:${Math.min(v/SCALE*100,100).toFixed(1)}%;background:${m.col}"></div> </div> <span class="nbdg badge ${st.cls}">${st.label}</span> </div>`;}).join(''); const leg=COST_META.map((m,j)=>`<div style="display:flex;align-items:center;gap:5px;margin-bottom:5px"> <div style="width:10px;height:10px;background:${m.col};flex-shrink:0"></div> <span style="font-size:11px;color:var(--mu);flex:1">${m.label}</span> <span style="font-size:12px;font-weight:600">${vals[j].toFixed(2)}%</span> </div>`).join(''); return `<div class="g2"> <div class="pan"><div class="ptit">Структура % выручки</div> <div style="display:flex;flex-direction:column;align-items:center"> <div style="position:relative;width:180px;height:180px"><canvas id="dn_${i}"></canvas></div> <div style="margin-top:10px;width:100%">${leg}</div> <div class="sep"></div> <div style="display:flex;justify-content:space-between;font-size:11px;width:100%"> <span style="color:var(--mu)">Итого</span><b>${total.toFixed(2)}%</b> </div> </div> </div> <div class="pan"><div class="ptit">Факт vs норматив</div> <div style="display:flex;gap:10px;font-size:10px;color:var(--mu);margin-bottom:8px"> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:16px;height:4px;background:rgba(26,94,56,.3)"></span>норма</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:16px;height:4px;background:#888"></span>факт</span> </div> ${nr} <div style="margin-top:6px;font-size:10px;color:var(--mu);line-height:1.5">% по ПФ = уплаченные проценты по ПФ / выручка нарастающим. <b style="color:var(--warn)">Низкая доля стройки в выручке</b> — признак отставания СМР, а не снижения себестоимости: показатель вырастет по мере наращивания темпов. Полоска — факт; затенённый диапазон — норматив.</div> </div> </div>`; } // ═══ ПФ И ПРОЦЕНТЫ ALL ════════════════════════════════════════════ function tPFAll(){ const pfRows=D.names.map((n,i)=>{ if(D.pf_closed[i]){ return `<div class="pfrow"> <div class="pfl"><span class="pfn" style="color:${D.col5[i]}">${n}</span> <span class="badge bd">ПФ погашен · эскроу раскрыт</span></div> <div style="font-size:10px;color:var(--mu)">Выбрано всего: ${D.pf_drw[i]}М из ${D.pf_lim[i]}М. Проценты уплачены нарастающим: <b>${D.int_abs_f[i]}М</b> факт = <b>${D.int_abs_p[i]}М</b> план.</div> </div>`; } const lim=D.pf_lim[i],drw=D.pf_drw[i]; const rem=lim-drw,rem_pct=(rem/lim*100).toFixed(0); const drw_pct=(drw/lim*100).toFixed(1); const ip=D.int_f[i],ipp=D.int_p[i]; const ia=D.int_abs_f[i],iap=D.int_abs_p[i]; return `<div class="pfrow"> <div class="pfl"> <span class="pfn" style="color:${D.col5[i]}">${n}</span> <span style="display:flex;gap:5px;flex-wrap:wrap"> ${badge(parseFloat(rem_pct),'rem',v=>'ост '+v+'% лимита')} <span class="badge ${bCls(ip,'int')}" title="% по ПФ / выручка нарастающим">% ПФ ф ${ip.toFixed(2)}% / п ${ipp.toFixed(2)}%</span> </span> </div> <div class="pfbg" style="position:relative"> <div class="pfb" style="width:${drw_pct}%;background:${D.col5[i]}"></div> <div style="position:absolute;left:${D.cycle[i].toFixed(1)}%;top:-6px;bottom:-6px;width:2px;background:rgba(22,22,22,0.65);z-index:2" title="Ожидаемая выборка для текущей стадии цикла: ${D.cycle[i].toFixed(0)}%"></div> <div style="position:absolute;left:${D.cycle[i].toFixed(1)}%;top:-18px;transform:translateX(-50%);font-size:8px;color:rgba(22,22,22,0.72);font-family:var(--fh);font-weight:700;line-height:10px;white-space:nowrap">${D.cycle[i].toFixed(0)}%</div> </div> <div class="pfm"> <span>Лимит: <b>${lim}М</b></span> <span>Выбрано: <b>${drw}М</b> (${drw_pct}%)</span> <span>Остаток: <b>${fmtM2(rem)}</b></span> <span>Проценты: ф <b>${ia}М</b> / п ${iap}М</span> </div> </div>`; }).join(''); return `<div class="pan"> <div class="ptit">Выборка лимита ПФ нарастающим · % по ПФ / выручка нарастающим</div> ${pfRows} <div style="margin-top:8px;font-size:10px;color:var(--mu);line-height:1.5;display:flex;align-items:center;gap:6px;flex-wrap:wrap"><span style="display:inline-flex;align-items:center;gap:3px"><span style="display:inline-block;width:2px;height:12px;background:rgba(22,22,22,0.55)"></span>вертикальная черта = ожидаемый % освоения лимита для текущей стадии цикла</span></div><div style="margin-top:4px;font-size:10px;color:var(--mu);line-height:1.5"><b>Остаток лимита:</b> норма >20%. <b>% по ПФ / выручка</b> = уплаченные проценты / доходы от осн. деятельности нарастающим (строка 190 ПФ файл 30.06). Норма 5–8%.</div> </div> <div class="pan"> <div class="ptit">% по ПФ / выручка — план vs факт нарастающим · нормативные границы</div> <div style="position:relative;height:230px"><canvas id="chIntBar"></canvas></div> <div style="display:flex;gap:12px;font-size:10px;color:var(--mu);margin-top:6px;flex-wrap:wrap"> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:4px;background:rgba(55,138,221,.5)"></span>план</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:4px;background:#555"></span>факт</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:0;border-top:2px dashed #1a8c5e"></span>3%</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:0;border-top:2px dashed #9a6010"></span>5%</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:0;border-top:2px dashed #b84020"></span>8%</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:14px;height:0;border-top:2px dashed #7a1616"></span>12%</span> </div> </div>`; } // ═══ ПФ ONE ════════════════════════════════════════════════════════ function tPFOne(i){ if(D.pf_closed[i]){ return `<div class="ins ins-d"> <b>Арбери 1 — ПФ полностью погашен. Эскроу раскрыт.</b><br> Лимит 2 500М, выбрано 2 468М (98.7%). Проценты уплачены нарастающим: <b>${D.int_abs_f[i]}М</b> факт = ${D.int_abs_p[i]}М план.<br> Январь 2026: начислено ~18.3М — это накопленные за период строительства проценты, погашены при раскрытии эскроу (стандартная практика ПФ в России). </div>`; } const lim=D.pf_lim[i],drw=D.pf_drw[i]; const rem=lim-drw,rem_pct=(rem/lim*100).toFixed(1); const drw_pct=(drw/lim*100).toFixed(1); const ip=D.int_f[i],ipp=D.int_p[i]; const ia=D.int_abs_f[i],iap=D.int_abs_p[i]; const MAX=15; const normMin=5,normMax=8; const norL=(normMin/MAX*100).toFixed(1),norW=((normMax-normMin)/MAX*100).toFixed(1); return ` <div class="g3"> <div class="card" style="--ac:#b2c4d2"><div class="clbl">Лимит ПФ</div> <span class="v1">${lim}<span class="u">М</span></span></div> <div class="card" style="--ac:#161616"><div class="clbl">Остаток лимита</div> <span class="v1">${rem.toFixed(2)}<span class="u">М</span></span> <div class="rsub">${badge(parseFloat(rem_pct),'rem',v=>v+'% от лимита')}</div></div> <div class="card" style="--ac:#9a6010;grid-column:1/-1"><div class="clbl">Проценты по ПФ уплачено нарастающим</div> <div style="display:flex;align-items:baseline;gap:12px;flex-wrap:wrap"> <span><span class="v1">${ia}<span class="u">М</span></span> <span style="font-size:11px;color:var(--mu)">факт</span></span> <span style="font-size:11px;color:var(--mu)">план: ${iap}М</span> <span class="badge ${bCls(ip,'int')}">% ПФ: ф ${ip.toFixed(2)}% / п ${ipp.toFixed(2)}%</span> </div></div> </div> <div class="pan"> <div class="ptit">Выборка лимита ПФ нарастающим</div> <div style="height:18px;background:var(--st);overflow:visible;position:relative;margin:18px 0 2px"> <div style="position:absolute;left:0;top:0;height:100%;width:${drw_pct}%;background:${D.col5[i]}"></div> <div style="position:absolute;left:${D.cycle[i].toFixed(1)}%;top:-7px;bottom:-7px;width:2px;background:rgba(22,22,22,0.65);z-index:2"></div> <div style="position:absolute;left:${D.cycle[i].toFixed(1)}%;top:-17px;transform:translateX(-50%);font-size:8px;color:rgba(22,22,22,0.72);font-family:var(--fh);font-weight:700;white-space:nowrap;line-height:10px">${D.cycle[i].toFixed(0)}% ожид.</div> </div> <div style="display:flex;justify-content:space-between;font-size:10px;color:var(--mu);margin-top:4px"> <span>Выбрано <b style="color:var(--blk)">${drw}М (${drw_pct}%)</b></span> <span></span> <span>Остаток <b style="color:var(--blk)">${fmtM2(rem)} (${rem_pct}%)</b></span> </div> </div> <div class="pan"> <div class="ptit">% по ПФ / выручка — план vs факт нарастающим</div> <div style="display:flex;gap:14px;font-size:10px;color:var(--mu);margin-bottom:10px"> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:16px;height:5px;background:rgba(55,138,221,.45)"></span>план ${ipp.toFixed(2)}%</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:16px;height:5px;background:${D.col5[i]}"></span>факт ${ip.toFixed(2)}%</span> <span style="display:flex;align-items:center;gap:3px"><span style="display:inline-block;width:16px;height:5px;background:rgba(26,94,56,.3)"></span>норма 5–8%</span> </div> <div style="position:relative;height:50px;margin-top:14px"> <div style="position:absolute;left:${norL}%;top:-12px;width:${norW}%;text-align:center;font:700 8px var(--fh);color:#1a5e38;letter-spacing:.02em;white-space:nowrap">НОРМА 5–8%</div> <div style="position:absolute;left:${norL}%;top:2px;transform:translateX(-50%);font:700 8px var(--fh);color:#1a5e38">5%</div> <div style="position:absolute;left:${(normMax/MAX*100).toFixed(1)}%;top:2px;transform:translateX(-50%);font:700 8px var(--fh);color:#1a5e38">8%</div> <div style="position:absolute;left:0;right:0;bottom:0;height:36px;background:var(--st);overflow:hidden"> <div style="position:absolute;left:${norL}%;width:${norW}%;height:100%;background:rgba(26,94,56,.24);border-left:2px solid rgba(26,94,56,.78);border-right:2px solid rgba(26,94,56,.78);box-sizing:border-box"></div> <div style="position:absolute;left:0;top:0;width:${Math.min(ipp/MAX*100,100).toFixed(1)}%;height:100%;background:rgba(55,138,221,.35)"></div> <div style="position:absolute;left:0;top:7px;width:${Math.min(ip/MAX*100,100).toFixed(1)}%;height:22px;background:${D.col5[i]}"></div> </div> </div> <div style="position:relative;height:14px;font-size:9px;color:var(--mu);margin-top:3px"> <span style="position:absolute;left:0;transform:translateX(0)">0%</span> <span style="position:absolute;left:${(5/MAX*100).toFixed(1)}%;transform:translateX(-50%)">5%</span> <span style="position:absolute;left:${(8/MAX*100).toFixed(1)}%;transform:translateX(-50%)">8%</span> <span style="position:absolute;left:${(12/MAX*100).toFixed(1)}%;transform:translateX(-50%)">12%</span> <span style="position:absolute;right:0;transform:translateX(0)">15%</span> </div> <div style="font-size:10px;color:var(--mu);margin-top:6px"><b>% по ПФ / выручка</b> = уплата процентов по ПФ нарастающим / доходы от осн. деятельности нарастающим. Источник: строка 190 ПФ-файла.</div> </div>`; } // ═══ HR ══════════════════════════════════════════════════════════ function tHR(){ const hr=D.hr; const lastIdx=hr.hc.length-1; const hcGrow=((hr.hc[lastIdx]-hr.hc[0])/hr.hc[0]*100).toFixed(1); const salGrow=((hr.sal[lastIdx]-hr.sal[0])/hr.sal[0]*100).toFixed(1); const firedTotal=hr.fired.reduce((a,b)=>a+b,0); const hcAvg=(hr.hc.reduce((a,b)=>a+b,0)/hr.hc.length); const turnoverYTD=(firedTotal/hcAvg*100).toFixed(1); const turnoverLast=(hr.fired[lastIdx]/((hr.hc[lastIdx-1]+hr.hc[lastIdx])/2)*100).toFixed(1); const months=['янв','фев','мар','апр','май','июн']; return `<div class="g3"> <div class="card" style="--ac:#2466a8"><div class="clbl">Численность (июнь)</div> <span class="v1">${hr.hc[lastIdx]}<span class="u">чел</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">янв ${hr.hc[0]} → июнь ${hr.hc[lastIdx]}</span> <span class="badge ${hcGrow>0?'bw':'bn'}">${hcGrow>0?'+':''}${hcGrow}% янв–июнь</span></div></div> <div class="card" style="--ac:#1a8c5e"><div class="clbl">Средняя зарплата (июнь)</div> <span class="v1">${(hr.sal[lastIdx]/1000).toFixed(0)}<span class="u">К/мес</span></span> <div class="rsub"><span class="badge bn">в рамках бенчмарка</span> <span style="font-size:10px;color:var(--mu)">янв ${(hr.sal[0]/1000).toFixed(0)}К → июнь ${(hr.sal[lastIdx]/1000).toFixed(0)}К (${salGrow>0?'+':''}${salGrow}% янв–июнь)</span></div></div> <div class="card" style="--ac:#9a6010;grid-column:1/-1"> <div class="clbl">ФОТ / выручка факт нарастающим · Выручка на сотрудника · Текучесть</div> <div style="display:flex;gap:16px;align-items:baseline;flex-wrap:wrap"> <span><span class="v1">${hr.fot_pct.toFixed(1)}<span class="u">%</span></span> <span class="badge bg">ФОТ/выручка норма 3–6%</span></span> <span><span class="v1">${hr.rev_emp.toFixed(1)}<span class="u">М/чел</span></span> <span style="font-size:10px;color:var(--mu)">выручка на сотрудника</span></span> <span><span class="v1" style="font-size:14px">${turnoverYTD}<span class="u">%</span></span> <span class="badge bw">текучесть янв–июнь</span></span> </div> </div> </div> <div class="pan"> <div class="ptit">Численность и средняя зарплата (янв–июнь 2026)</div> <div style="position:relative;height:160px"><canvas id="chHR"></canvas></div> </div> <div style="background:var(--wh);border:.5px solid var(--bo);padding:10px 12px;margin-bottom:8px;font-size:11px"> <div style="font-family:var(--fh);font-size:7px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--mu);margin-bottom:6px">Движение персонала янв–июнь 2026</div> <div style="display:grid;grid-template-columns:repeat(6,1fr);gap:4px;text-align:center;margin-bottom:8px"> ${['Январь','Февраль','Март','Апрель','Май','Июнь'].map((m,i)=>` <div style="background:var(--st);padding:5px 4px"> <div style="font-size:9px;color:var(--mu)">${m}</div> <div style="font-size:10px;color:var(--good)">+${D.hr.hired[i]}</div> <div style="font-size:10px;color:var(--bad)">−${D.hr.fired[i]}</div> <div style="font-size:11px;font-weight:600">${D.hr.hc[i]}</div> </div>`).join('')} </div> <div style="display:flex;gap:16px;font-size:11px;flex-wrap:wrap"> <span style="color:var(--good)">Принято итого: <b>${D.hr.hired.reduce((a,b)=>a+b,0)} чел</b></span> <span style="color:var(--bad)">Уволено итого: <b>${D.hr.fired.reduce((a,b)=>a+b,0)} чел</b></span> <span>Коэф. текучести янв–июнь: <b class="badge bw">${(D.hr.fired.reduce((a,b)=>a+b,0)/(D.hr.hc.reduce((a,b)=>a+b,0)/D.hr.hc.length)*100).toFixed(1)}%</b></span> <span>Коэф. текучести июнь: <b class="badge bg">${(D.hr.fired[D.hr.fired.length-1]/((D.hr.hc[D.hr.hc.length-2]+D.hr.hc[D.hr.hc.length-1])/2)*100).toFixed(1)}%</b></span> </div> </div> <div class="hrbench"> <b>Зарплатный бенчмарк · девелопер в регионах РФ · 2025–2026</b><br> Бэк-офис, ПТО, юристы, финансы: 70–110 тыс. руб./мес.<br> Менеджеры по продажам недвижимости (оклад + % от ДДУ): 90–200 тыс. руб./мес. — при активных продажах зарплата существенно выше базы за счёт бонуса.<br> <b>Сводный бенчмарк для компании с активными продажами (смешанный состав): 90–150 тыс. руб./мес.</b><br> Факт компании: 107–149 тыс. — <b style="color:var(--good)">в рамках бенчмарка</b> с учётом наличия отдела продаж на нескольких активных проектах.<br> <span style="font-size:10px;color:var(--mu)">Источник: hh.ru, аналитика рынка труда «Недвижимость/строительство», регионы РФ, 2025–2026.</span> </div> <div class="ins ins-i" style="margin-top:8px"> Численность янв→июнь: 93→121 (+30.1%). Апрель 145К — разовая премия за передачу Арбери 1. Июнь 107К — снижение средней зарплаты после майского пика. Нанято янв–июнь: 44 чел., уволено: 17 чел. Коэф. текучести янв–июнь: 16.2% (в июне 2.6%). ФОТ/выручка факт нарастающим: 3.78%. </div>`; } // ═══ KPI ALL ════════════════════════════════════════════════════ function tKpiAll(){ const G='minmax(120px,1.4fr) repeat(6,minmax(44px,1fr)) minmax(88px,1fr)'; const rows=[ {grp:'Темп и цикл'}, {name:'Продажи (выручка ф/п)',key:'sp',vals:D.rv_f.map((f,i)=>f/D.rv_p[i]*100),fn:v=>v.toFixed(1)+'%',note:'85% к сдаче'}, {name:'Фин. освоение стройки (СМР+прочие/план)',key:'st',vals:D.bld_f.map((f,i)=>f/D.bld_p[i]*100),fn:v=>v.toFixed(1)+'%',note:'Факт затрат СМР + прочие работы / план.'}, {name:'Разрыв (прод.−фин. стройка)',key:'gap',vals:D.gap,fn:v=>(v>0?'+':'')+v.toFixed(1)+' пп', note:'Ключевой индикатор. ±5пп = норма. Abs. % темпа без привязки к стадии цикла неинформативен.'}, {name:'Коэф. опережения',key:'ko',vals:D.ko,fn:v=>v.toFixed(2),note:'0.9–1.2 баланс, 1.2–1.4 внимание, >1.4 риск. При фин. стройке <10% показатель нестабилен.'}, {name:'Позиция в цикле',key:'none',vals:D.cycle,fn:v=>v.toFixed(0)+'%',note:'Справочно. Смотрите вкладку «Темп / цикл».'}, {grp:'Банк'}, {name:'Стройготовность банка',key:'bank',vals:D.bank_ready,fn:v=>v.toFixed(1)+'%',note:'Чек-лист строительной готовности, направленный в банк.'}, {name:'Разрыв банк-фин.',key:'bank_gap',vals:D.bank_ready.map((v,i)=>v==null?null:v-D.st[i]),fn:v=>sgn(v)+v.toFixed(1)+' пп',note:'Банк минус фин. освоение стройки. Большой плюс требует пояснения причин разрыва.'}, {grp:'Проектное финансирование'}, {name:'% по ПФ / выр. план',key:'int',vals:D.int_p,fn:v=>v.toFixed(2)+'%',note:'3–5% эффективно, 5–8% норма, 8–12% напряженно, >12% опасно.'}, {name:'% по ПФ / выр. факт',key:'int',vals:D.int_f,fn:v=>v.toFixed(2)+'%',note:'3–5% эффективно, 5–8% норма, 8–12% напряженно, >12% опасно. Целевой: 5–8%.'}, {grp:'Расходы'}, {name:'Коммерч. / выр.',key:'cm',vals:D.cm,fn:v=>v.toFixed(2)+'%',note:'Норма 2–4%.'}, {name:'Управл. / выр.',key:'mg',vals:D.mg,fn:v=>v.toFixed(2)+'%',note:'Норма 3–5%.'}, {name:'Фин. стройка % выр.',key:'bl',vals:D.bl,fn:v=>v.toFixed(2)+'%',note:'Норма 55–80%.'}, {name:'Налоги / выр.',key:'tx',vals:D.tx,fn:v=>v.toFixed(2)+'%',note:'4–7% — норма рынка. 2–4% повышенный риск ФНС. Низко в период стройки: УСН признаёт базу при раскрытии эскроу.'}, ]; let h=`<div class="scrollhint">Прокрутите таблицу вправо →</div><div class="tscr"><div class="tpan"> <div class="thd" style="grid-template-columns:${G}"> <span style="text-align:left">KPI (нарастающим)</span> <span style="text-align:center">Акад.2</span><span style="text-align:center">Арб.1</span><span style="text-align:center">Арб.2</span><span style="text-align:center">Доброе</span><span style="text-align:center">Тезис</span><span style="text-align:center">Терра</span> <span style="text-align:left">Ориентир / пояснение</span> </div>`; rows.forEach(row=>{ if(row.grp){ h+=`<div class="trow grp" style="grid-template-columns:${G}"><span class="tn grp" style="grid-column:1/-1">${row.grp}</span></div>`; return; } const {name,key,vals,fn,note}=row; h+=`<div class="trow" style="grid-template-columns:${G}"><span class="tn">${name}</span>`; vals.forEach((v,i)=>{ const isClose=(D.pf_closed[i]&&(key==='int')); h+=`<span class="tc">${isClose?`<span class="badge bd">погашен</span>`:valBadge(v,key,fn)}</span>`; }); h+=`<span class="tc mu" style="text-align:left;line-height:1.3">${note}</span></div>`; }); h+='</div></div>'; h+=`<div style="margin-top:8px;font-size:10px;color:var(--mu);background:var(--wh);border:1px solid var(--bo);padding:10px;line-height:1.5"> <b>Продажи и фин. стройка (абс. %):</b> сами по себе зависят от стадии проекта. Оценивайте через разрыв (прод.−фин. стройка), банковскую стройготовность и позицию в цикле (вкладка «Темп / цикл»). <br><b>Нормативы расходов и ПФ:</b> управленческие ориентиры, если рядом не указан внешний источник или утвержденный документ. </div> <div class="ins ins-d" style="margin-top:8px"><b>Академия 1 (сдан 2025):</b> ПФ погашен, эскроу раскрыт. Остаточные расходы.</div>`; return h; } // ═══ KPI ONE ════════════════════════════════════════════════════ function tKpiOne(i){ const G='1.6fr 1fr 0.8fr'; const isDel=D.pf_closed[i]; const rows=[ {grp:'Темп и цикл'}, {name:'Продажи (выручка ф/п)',key:'sp',val:D.rv_f[i]/D.rv_p[i]*100,fn:v=>v.toFixed(1)+'%',note:'85% к сдаче'}, {name:'Фин. освоение стройки (СМР+прочие/план)',key:'st',val:D.bld_f[i]/D.bld_p[i]*100,fn:v=>v.toFixed(1)+'%',note:'факт затрат / план'}, {name:'Разрыв (прод.−фин. стройка)',key:'gap',val:D.gap[i],fn:v=>(v>0?'+':'')+v.toFixed(1)+' пп',note:'±5пп'}, {name:'Коэф. опережения',key:'ko',val:D.ko[i],fn:v=>v.toFixed(2),note:koNote(i)}, {name:'Позиция в цикле',key:'none',val:D.cycle[i],fn:v=>v.toFixed(0)+'%',note:'справочно'}, {grp:'Банк'}, {name:'Стройготовность банка',key:'bank',val:D.bank_ready[i],fn:v=>v.toFixed(1)+'%',note:'чек-лист банка'}, {name:'Разрыв банк-фин.',key:'bank_gap',val:D.bank_ready[i]==null?null:D.bank_ready[i]-D.st[i],fn:v=>sgn(v)+v.toFixed(1)+' пп',note:'банк минус фин. стройка'}, {grp:'Проектное финансирование'}, {name:'% по ПФ / выр. план',key:'int',val:D.int_p[i],fn:v=>v.toFixed(2)+'%',note:'5–8% целевой'}, {name:'% по ПФ / выр. факт',key:'int',val:D.int_f[i],fn:v=>v.toFixed(2)+'%',note:'5–8% целевой'}, {grp:'Расходы'}, {name:'Коммерч. / выр.',key:'cm',val:D.cm[i],fn:v=>v.toFixed(2)+'%',note:'2–4%'}, {name:'Управл. / выр.',key:'mg',val:D.mg[i],fn:v=>v.toFixed(2)+'%',note:'3–5%'}, {name:'Фин. стройка % выр.',key:'bl',val:D.bl[i],fn:v=>v.toFixed(2)+'%',note:'55–80%'}, {name:'Налоги / выр.',key:'tx',val:D.tx[i],fn:v=>v.toFixed(2)+'%',note:'4–7% норма рынка'}, ]; let h=`<div class="tpan"><div class="thd" style="grid-template-columns:${G}"> <span style="text-align:left">KPI (нарастающим)</span><span style="text-align:center">Значение</span><span style="text-align:left;border-left:1px solid var(--bo2);padding-left:8px">Ориентир</span></div>`; rows.forEach(row=>{ if(row.grp){ h+=`<div class="trow grp" style="grid-template-columns:${G}"><span class="tn grp" style="grid-column:1/-1">${row.grp}</span></div>`; return; } const {name,key,val,fn,note}=row; const v=(isDel&&key==='int')?`<span class="badge bd">погашен</span>`:valBadge(val,key,fn); h+=`<div class="trow" style="grid-template-columns:${G}"> <span class="tn">${name}</span><span class="tc" style="text-align:center">${v}</span> <span class="tc mu" style="text-align:left;border-left:1px solid var(--bo);padding-left:8px">${note}</span></div>`; }); h+='</div>'; h+=`<div style="margin-top:8px;font-size:10px;color:var(--mu);background:var(--wh);border:1px solid var(--bo);padding:10px;line-height:1.5"> <b>Нормативы расходов и ПФ:</b> управленческие ориентиры, если рядом не указан внешний источник или утвержденный документ. </div>`; h+=`<div class="ins ${D.ins_cls[i]}" style="margin-top:8px">${D.note[i]}</div>`; return h; } // ═══ АКАДЕМИЯ 1 ═══════════════════════════════════════════════ function tA1(){ const a=D.a1; return `<div class="ins ins-d"> <b>Академия 1 — проект сдан в 2025 году. Данные плана отсутствуют.</b><br> Источник: ПФ_по_проектам_на_3006. Все показатели нарастающим итогом.</div> <div class="g3" style="margin-top:8px"> <div class="card" style="--ac:#888780"><div class="clbl">Выручка нарастающим</div> <span class="v1">${a.rv_f.toFixed(1)}<span class="u">М</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">плана нет (сдан)</span></div></div> <div class="card" style="--ac:#444"><div class="clbl">Фин. стройка нарастающим</div> <span class="v1">${a.bld_f.toFixed(1)}<span class="u">М</span></span> <div class="rsub"><span style="font-size:10px;color:var(--mu)">${a.bl.toFixed(2)}% выручки</span></div></div> <div class="card" style="--ac:#3e3490;grid-column:1/-1"><div class="clbl">% по ПФ / выручка нарастающим</div> <span class="v1">${a.int_f_pct.toFixed(2)}<span class="u">%</span></span> <div class="rsub"><span class="badge bg">норма 5–8%</span> <span style="font-size:10px;color:var(--mu)">уплачено процентов ${a.int_abs_f.toFixed(2)}М</span></div></div> </div> <div class="pan"> <div class="ptit">Расходы % выручки нарастающим (нет плана)</div> ${[['Фин. стройка (СМР+прочие)',a.bl,'bl'],['Коммерческие',a.cm,'cm'],['Управленческие',a.mg,'mg'],['Налоги',a.tx,'tx'],['% по ПФ',a.int_f_pct,'int']].map(([n,v,key])=>` <div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:.5px solid var(--bo);font-size:11px"> <span style="color:var(--mu)">${n}</span> <span class="badge ${bCls(v,key)}">${v.toFixed(2)}%</span> </div>`).join('')} <div style="display:flex;justify-content:space-between;padding:8px 0;font-size:11px;font-weight:600;border-top:1px solid var(--bo2);margin-top:4px"> <span>Итого расходов</span> <span>${(a.bl+a.cm+a.mg+a.tx+a.int_f_pct).toFixed(2)}%</span> </div> </div> <div class="ins ins-d">ПФ погашен. Эскроу раскрыт. Доля стройки 99.43% — значительное превышение фактической себестоимости: фактический убыток по проекту. % по ПФ ${a.int_f_pct.toFixed(2)}% — низкие: выборка ПФ перекрывалась остатком эскроу, ПФ погашен.</div>`; } // ═══ CHARTS ═════════════════════════════════════════════════════ function drawCharts(){ const idx = (curP in P2I) ? P2I[curP] : -1; if(curT===3){drawHRChart();return;} if(curP==='all'){ if(curT===0){drawBar('chSp',D.sp);drawBar('chSt',D.st);drawKo();} else if(curT===1){D.names.forEach((_,i)=>drawDonut(i,'chCAd_'+i));drawMonthlyRatios();} else if(curT===2) drawIntBar(); } else if(curP!=='a1'){ if(curT===1) drawDonut(idx); else if(curT===2) drawIntBar(); } } function drawMonthlyRatios(){ const el=document.getElementById('chMonthlyRatios'); if(!el) return; dc('chMonthlyRatios'); const endLabelsPlugin={ id:'endLabels', afterDatasetsDraw(chart){ const {ctx}=chart; const MIN_GAP=13; // минимальный зазор между подписями по Y, px // Собираем конечные точки всех видимых линий let pts=chart.data.datasets.map((ds,i)=>{ const meta=chart.getDatasetMeta(i); if(meta.hidden) return null; const lastPoint=meta.data[meta.data.length-1]; if(!lastPoint) return null; return {y:lastPoint.y, x:lastPoint.x, color:ds.borderColor, label:ds.shortLabel||ds.label.split(' / ')[0]}; }).filter(Boolean); // Сортируем по Y и раздвигаем коллизии (sweep сверху вниз) pts.sort((a,b)=>a.y-b.y); for(let k=1;k<pts.length;k++){ if(pts[k].y - pts[k-1].y < MIN_GAP){ pts[k].y = pts[k-1].y + MIN_GAP; } } ctx.save(); ctx.font='600 9px Inter,sans-serif'; ctx.textBaseline='middle'; pts.forEach(p=>{ ctx.fillStyle=p.color; ctx.fillText(p.label,p.x+8,p.y); }); ctx.restore(); } }; CH['chMonthlyRatios']=new Chart(el,{type:'line', data:{labels:MONTHLY_RATIOS.months, datasets:MONTHLY_RATIOS.series.map(s=>({ label:s.label,shortLabel:s.shortLabel,data:s.data,borderColor:s.col,backgroundColor:s.col, borderWidth:2,pointRadius:2.5,pointHoverRadius:5,tension:.25,fill:false }))}, plugins:[endLabelsPlugin], options:{responsive:true,maintainAspectRatio:false, layout:{padding:{right:88}}, plugins:{ legend:{display:false}, tooltip:{backgroundColor:'#161616',titleColor:'#fff',bodyColor:'rgba(255,255,255,.7)',padding:8,cornerRadius:0, callbacks:{label:c=>' '+c.dataset.label+': '+c.parsed.y.toFixed(1)+'%'}} }, scales:{ x:{grid:{display:false},ticks:{font:{size:9},color:'#6a7b8a'}}, y:{grid:{color:'rgba(0,0,0,.07)'},ticks:{stepSize:1,font:{size:9},color:'#6a7b8a',callback:v=>v+'%'}} }} }); } function drawIntBar(){ const el=document.getElementById('chIntBar'); if(!el) return; dc('chIntBar'); const active=D.names.filter((_,i)=>!D.pf_closed[i]); const idxs=D.names.map((_,i)=>i).filter(i=>!D.pf_closed[i]); const planData=idxs.map(i=>D.int_p[i]); const factData=idxs.map(i=>D.int_f[i]); const colors=idxs.map(i=>D.col5[i]); const normPlugin={ id:'normLines', afterDraw(chart){ const {ctx,chartArea:{left,right,top,bottom},scales:{y}}=chart; [[3,'rgba(26,94,56,.85)','3% эффективно'],[5,'rgba(154,96,16,.85)','5% норма'], [8,'rgba(184,64,32,.85)','8% напряженно'],[12,'rgba(122,22,22,.85)','12% опасно']].forEach(([v,c,lbl])=>{ const yp=y.getPixelForValue(v); ctx.save(); ctx.beginPath();ctx.moveTo(left,yp);ctx.lineTo(right,yp); ctx.strokeStyle=c;ctx.lineWidth=1.5;ctx.setLineDash([6,4]);ctx.stroke(); ctx.fillStyle=c;ctx.font='600 9px Inter,sans-serif'; ctx.fillText(lbl,right-ctx.measureText(lbl).width-4,yp-3); ctx.restore(); }); } }; CH['chIntBar']=new Chart(el,{type:'bar', data:{labels:active, datasets:[ {label:'план %',data:planData,backgroundColor:colors.map(c=>c+'55'),borderColor:colors,borderWidth:1.5,borderRadius:2}, {label:'факт %',data:factData,backgroundColor:colors,borderRadius:2}, ]}, plugins:[normPlugin], options:{responsive:true,maintainAspectRatio:false, plugins:{ legend:{position:'top',labels:{font:{size:10},boxWidth:10,padding:8}}, datalabels:{display:true,anchor:'end',align:'top', font:{size:10,weight:'600'},color:'#fff', formatter:(v,ctx)=>v.toFixed(2)+'%'} }, scales:{ x:{grid:{display:false},ticks:{font:{size:10},color:'#6a7b8a',maxRotation:0}}, y:{max:14,min:0,grid:{color:'rgba(0,0,0,.07)'}, ticks:{font:{size:9},color:'#6a7b8a',callback:v=>v+'%'}} }} }); } const bOpts=max=>({responsive:true,maintainAspectRatio:false, plugins:{legend:{display:false},tooltip:{backgroundColor:'#161616',titleColor:'#fff',bodyColor:'rgba(255,255,255,.7)',padding:8,cornerRadius:0}}, scales:{x:{grid:{display:false},ticks:{font:{size:9},color:'#6a7b8a',maxRotation:0}}, y:{max,min:0,grid:{color:'rgba(0,0,0,.07)'},ticks:{font:{size:9},color:'#6a7b8a',callback:v=>v+'%'}}}}); function drawBar(id,data){ const el=document.getElementById(id); if(!el) return; dc(id); CH[id]=new Chart(el,{type:'bar',data:{labels:D.sh5,datasets:[{data,backgroundColor:D.col5,borderRadius:0}]},options:bOpts(115)}); } function drawKo(){ const el=document.getElementById('chKo'); if(!el) return; dc('chKo'); const koMax=Math.max(1.6,...D.ko)*1.12; const koNormPlugin={ id:'koNormLines', afterDraw(chart){ const {ctx,chartArea:{left,right},scales:{y}}=chart; [[0.9,'rgba(26,94,56,.85)','0.9'],[1.2,'rgba(26,94,56,.85)','1.2'],[1.4,'rgba(154,96,16,.85)','1.4']].forEach(([v,c,lbl])=>{ const yp=y.getPixelForValue(v); ctx.save(); ctx.beginPath();ctx.moveTo(left,yp);ctx.lineTo(right,yp); ctx.strokeStyle=c;ctx.lineWidth=1;ctx.setLineDash([5,4]);ctx.stroke(); ctx.fillStyle=c;ctx.font='600 9px Inter,sans-serif'; ctx.fillText(lbl,right-ctx.measureText(lbl).width-4,yp-3); ctx.restore(); }); } }; CH['chKo']=new Chart(el,{type:'bar', data:{labels:D.sh5,datasets:[{data:D.ko,backgroundColor:D.col5,borderRadius:0}]}, plugins:[koNormPlugin], options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'#161616',titleColor:'#fff',bodyColor:'rgba(255,255,255,.7)',padding:8,cornerRadius:0}}, scales:{x:{grid:{display:false},ticks:{font:{size:9},color:'#6a7b8a',maxRotation:0}}, y:{min:0,max:koMax,grid:{color:'rgba(0,0,0,.07)'},ticks:{font:{size:9},color:'#6a7b8a'}}}}}); } function drawStacked(){ const el=document.getElementById('chCA'); if(!el) return; dc('chCA'); CH['chCA']=new Chart(el,{type:'bar', data:{labels:D.sh5,datasets:[ {label:'_',data:D.names.map(()=>1.5),backgroundColor:D.col5,stack:'a',borderRadius:0,barPercentage:1,categoryPercentage:1}, ...COST_META.map(m=>({label:m.label,data:D[m.key],backgroundColor:m.col,stack:'a',borderRadius:0})) ]}, options:{responsive:true,maintainAspectRatio:false, plugins:{legend:{position:'bottom',labels:{font:{size:10},boxWidth:10,padding:8,filter:it=>it.text!=='_'}}}, scales:{x:{grid:{display:false},stacked:true,ticks:{font:{size:9},color:'#6a7b8a',maxRotation:0}}, y:{stacked:true,max:112,grid:{color:'rgba(0,0,0,.07)'},ticks:{font:{size:9},color:'#6a7b8a',callback:v=>v+'%'}}}}}); } function drawDonut(i,customId){ const id=customId||('dn_'+i); const el=document.getElementById(id); if(!el) return; dc(id); CH[id]=new Chart(el,{type:'doughnut', data:{labels:COST_META.map(m=>m.label), datasets:[{data:COST_META.map(m=>D[m.key][i]),backgroundColor:COST_META.map(m=>m.col),borderColor:'#DDE3E8',borderWidth:2,hoverOffset:6}]}, options:{responsive:true,maintainAspectRatio:true,cutout:'58%', plugins:{ legend:{display:false}, datalabels:{display:true,color:'#fff',font:{size:9,weight:'600'}, formatter:(v)=>v>=3?v.toFixed(1)+'%':''}, tooltip:{backgroundColor:'#161616',titleColor:'#fff',bodyColor:'rgba(255,255,255,.7)',padding:8,cornerRadius:0,callbacks:{label:c=>' '+c.label+': '+c.parsed.toFixed(2)+'%'}}}}}); } function drawHRChart(){ const el=document.getElementById('chHR'); if(!el) return; dc('chHR'); CH['chHR']=new Chart(el,{type:'bar', data:{labels:['Январь','Февраль','Март','Апрель','Май','Июнь'], datasets:[ {label:'Численность, чел.',data:D.hr.hc,backgroundColor:'#2466a8',borderRadius:0,yAxisID:'y'}, {label:'Ср. зарплата, тыс. руб.',data:D.hr.sal.map(v=>Math.round(v/1000)),backgroundColor:'rgba(186,117,23,.7)',borderRadius:0,yAxisID:'y2'}, ]}, options:{responsive:true,maintainAspectRatio:false, plugins:{ legend:{position:'top',labels:{font:{size:10},boxWidth:10,padding:8}}, datalabels:{display:true,anchor:'center',align:'center', font:{size:10,weight:'600'}, color:'#ffffff', formatter:(v,ctx)=>ctx.datasetIndex===0?v+'':v+'К', textShadowBlur:3,textShadowColor:'rgba(0,0,0,.6)'} }, scales:{ x:{grid:{display:false},ticks:{font:{size:11},color:'#6a7b8a'}}, y:{position:'left',grid:{color:'rgba(0,0,0,.07)'},ticks:{font:{size:10},color:'#6a7b8a'},title:{display:true,text:'Чел.',font:{size:9}}}, y2:{position:'right',grid:{display:false},ticks:{font:{size:10},color:'#6a7b8a'},title:{display:true,text:'Тыс. руб.',font:{size:9}}}, }}} ); } render(); </script> </body> </html>