mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 23:24:47 +00:00
909 lines
92 KiB
HTML
909 lines
92 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Кабинет клиента · @wasrusgen1 | КОНСАЛТИНГ</title>
|
||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@700;800&display=swap" rel="stylesheet">
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
:root{--primary:#047857;--dark:#064E3B;--mid:#10B981;--light:#ECFDF5;--sb:#0F0F1A;--bg:#F5F6F8;--white:#fff;--border:#E5E7EB;--text:#1A1A2E;--muted:#6B7280;--subtle:#F9FAFB}
|
||
html,body{height:100%;overflow:hidden}
|
||
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
|
||
/* Header */
|
||
.hdr{height:54px;background:var(--dark);display:flex;align-items:center;padding:0 20px;gap:12px;flex-shrink:0}
|
||
.hdr-ic{width:30px;height:30px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;font-size:16px;color:#fff}
|
||
.hdr-t{font-family:'Montserrat';font-weight:700;font-size:14px;color:rgba(255,255,255,.6);display:flex;align-items:center;gap:9px;letter-spacing:-.2px}
|
||
.hdr-sep{width:1.5px;height:15px;background:rgba(255,255,255,.25);flex-shrink:0}
|
||
.hdr-t b{font-weight:800;color:#fff}
|
||
.hdr-client{font-size:13px;color:rgba(255,255,255,.6);margin-left:6px}
|
||
.hdr-r{margin-left:auto}
|
||
.elena-chip{display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);border-radius:100px;padding:5px 13px 5px 6px}
|
||
.elena-av{width:24px;height:24px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-family:'Montserrat';font-weight:800;font-size:11px;color:#fff}
|
||
.elena-nm{font-size:12px;font-weight:600;color:rgba(255,255,255,.85)}
|
||
.elena-dot{width:7px;height:7px;background:var(--mid);border-radius:50%;box-shadow:0 0 4px var(--mid)}
|
||
/* Layout */
|
||
.layout{flex:1;display:flex;overflow:hidden}
|
||
.sb{width:240px;flex-shrink:0;background:var(--sb);display:flex;flex-direction:column}
|
||
.sb-nav{flex:1;padding:14px 0;overflow-y:auto}
|
||
.sb-cap{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.2);padding:10px 18px 6px}
|
||
.si{display:flex;align-items:flex-start;gap:11px;padding:10px 18px;cursor:pointer;border-left:2px solid transparent;transition:background .12s}
|
||
.si:hover:not(.locked){background:rgba(255,255,255,.04)}
|
||
.si.active{background:rgba(4,120,87,.14);border-left-color:var(--primary)}
|
||
.si.locked{opacity:.4;cursor:default}
|
||
.si-num{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}
|
||
.si.done .si-num{background:rgba(16,185,129,.15);color:var(--mid);font-size:14px}
|
||
.si.active .si-num{background:var(--primary);color:#fff;box-shadow:0 4px 10px rgba(4,120,87,.4)}
|
||
.si:not(.active):not(.done) .si-num{background:rgba(255,255,255,.06);color:rgba(255,255,255,.4)}
|
||
.si.locked .si-num{background:rgba(255,255,255,.05);color:rgba(255,255,255,.2);font-size:11px}
|
||
.si-body{flex:1;min-width:0}
|
||
.si-lbl{font-size:10px;color:rgba(255,255,255,.25);font-weight:600;letter-spacing:.04em;text-transform:uppercase}
|
||
.si-name{font-size:13px;font-weight:600;color:rgba(255,255,255,.7);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.si.active .si-name{color:#fff}
|
||
.si-sub{font-size:11px;color:rgba(255,255,255,.3);margin-top:1px}
|
||
.sb-foot{padding:14px;border-top:1px solid rgba(255,255,255,.06)}
|
||
.pb-box{background:rgba(255,255,255,.04);border-radius:10px;padding:12px}
|
||
.pb-row{display:flex;justify-content:space-between;margin-bottom:6px}
|
||
.pb-l{font-size:12px;font-weight:600;color:rgba(255,255,255,.45)}
|
||
.pb-p{font-size:12px;font-weight:700;color:var(--mid)}
|
||
.pb-track{height:4px;background:rgba(255,255,255,.08);border-radius:2px;overflow:hidden}
|
||
.pb-fill{height:100%;background:linear-gradient(90deg,var(--primary),var(--mid));border-radius:2px;transition:width .4s}
|
||
/* Main */
|
||
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
|
||
.sv{display:none;flex:1;flex-direction:column;overflow:hidden}
|
||
.sv.active{display:flex}
|
||
.hero{background:var(--white);border-bottom:1px solid var(--border);padding:18px 26px;display:flex;align-items:center;gap:16px;flex-shrink:0}
|
||
.hero-ic{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,var(--dark),var(--primary));display:flex;align-items:center;justify-content:center;font-size:21px;flex-shrink:0;color:#fff}
|
||
.hero-tag{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--primary);margin-bottom:3px}
|
||
.hero-h{font-size:18px;font-weight:700;color:var(--text)}
|
||
.hero-d{font-size:13px;color:var(--muted);margin-top:2px}
|
||
.hero-r{margin-left:auto}
|
||
.btn{font-family:'Inter';font-weight:600;border:none;cursor:pointer;border-radius:10px;display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
|
||
.btn-p{background:var(--primary);color:#fff;font-size:13px;padding:10px 18px}
|
||
.btn-p:hover{background:var(--dark)}.btn-p:disabled{opacity:.5;cursor:default}
|
||
.scroll{flex:1;overflow-y:auto;overflow-anchor:none}
|
||
/* Chat */
|
||
.chat{padding:24px 26px;display:flex;flex-direction:column;gap:14px}
|
||
/* Спросить Елену — док на этапах 3-5 */
|
||
.askdock{display:none;border-top:1px solid var(--border);background:var(--white);flex:0 0 auto}
|
||
.askdock.show{display:block}
|
||
.askdock-head{display:flex;align-items:center;gap:8px;padding:11px 18px;cursor:pointer;font-size:13px;font-weight:700;color:var(--primary);user-select:none}
|
||
.askdock-head .ad-sub{font-weight:500;color:#9ca3af;font-size:12px}
|
||
.askdock-head .ad-chev{margin-left:auto;transition:transform .2s}
|
||
.askdock.open .ad-chev{transform:rotate(180deg)}
|
||
.askdock-body{display:none;border-top:1px solid var(--border)}
|
||
.askdock.open .askdock-body{display:block}
|
||
.askdock-thread{max-height:240px;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px}
|
||
.askdock-thread:empty{display:none}
|
||
.askdock-inbar{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--border)}
|
||
.askdock-inbar textarea{flex:1;border:1px solid var(--border);border-radius:10px;padding:9px 12px;font:inherit;font-size:13px;resize:none;max-height:90px;outline:none}
|
||
.askdock-inbar textarea:focus{border-color:var(--primary)}
|
||
.am{display:flex;gap:8px;font-size:13px;line-height:1.45}
|
||
.am .am-av{width:24px;height:24px;border-radius:50%;flex:0 0 24px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff}
|
||
.am.u{flex-direction:row-reverse}
|
||
.am.u .am-av{background:#64748b}
|
||
.am.e .am-av{background:var(--primary)}
|
||
.am .am-bb{background:#f1f5f9;border-radius:10px;padding:7px 11px;max-width:86%}
|
||
.am.u .am-bb{background:#e2e8f0}
|
||
.am-dev{font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:4px 8px;margin-top:5px;font-weight:600}
|
||
.msg{display:flex;gap:10px;max-width:80%}
|
||
.msg.user{align-self:flex-end;flex-direction:row-reverse}
|
||
.av{width:30px;height:30px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#fff}
|
||
.av.e{background:var(--primary)}.av.u{background:#6366F1}
|
||
.bb{padding:11px 15px;border-radius:14px;font-size:14px;line-height:1.55;white-space:pre-wrap}
|
||
.bb.in{background:var(--white);border:1px solid var(--border);border-top-left-radius:4px}
|
||
.bb.out{background:var(--primary);color:#fff;border-top-right-radius:4px}
|
||
.typing{display:flex;gap:4px;padding:13px 16px;background:var(--white);border:1px solid var(--border);border-radius:14px;border-top-left-radius:4px;width:fit-content}
|
||
.typing span{width:7px;height:7px;border-radius:50%;background:#cbd5e1;animation:b 1.2s infinite}
|
||
.typing span:nth-child(2){animation-delay:.2s}.typing span:nth-child(3){animation-delay:.4s}
|
||
@keyframes b{0%,80%,100%{transform:scale(.7);opacity:.4}40%{transform:scale(1);opacity:1}}
|
||
.inbar{padding:14px 26px;border-top:1px solid var(--border);background:var(--white);display:flex;gap:10px;align-items:flex-end;flex-shrink:0}
|
||
.inp{flex:1;border:1.5px solid var(--border);border-radius:12px;padding:11px 15px;font-size:14px;font-family:'Inter';resize:none;outline:none;max-height:120px;line-height:1.5}
|
||
.inp:focus{border-color:var(--mid);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
|
||
.icon-btn{width:44px;height:44px;border-radius:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative}
|
||
.mic{background:var(--light);border:1.5px solid rgba(4,120,87,.2);color:var(--primary)}
|
||
.mic:hover{background:#D1FAE5}
|
||
.mic.rec{background:#FEF2F2;border-color:#EF4444;color:#EF4444}
|
||
.mic.rec::after{content:'';position:absolute;inset:-4px;border-radius:14px;border:2px solid #EF4444;animation:mp 1.3s ease-out infinite}
|
||
@keyframes mp{0%{transform:scale(1);opacity:.7}100%{transform:scale(1.25);opacity:0}}
|
||
.send{background:var(--primary);border:none;color:#fff}.send:hover{background:var(--dark)}.send:disabled{opacity:.4}
|
||
.mic-hint{position:absolute;bottom:56px;right:0;background:var(--sb);color:#fff;font-size:12px;padding:6px 12px;border-radius:8px;white-space:nowrap;display:none}
|
||
.mic-hint.show{display:block}
|
||
/* Analysis panes */
|
||
.pad{padding:24px 26px}
|
||
.run-card{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:28px;text-align:center;max-width:520px;margin:8px auto 16px}
|
||
.run-ic{font-size:34px;margin-bottom:12px}
|
||
.run-t{font-family:'Montserrat';font-weight:800;font-size:19px;color:var(--text);margin-bottom:8px}
|
||
.run-d{font-size:14px;color:var(--muted);line-height:1.5;margin-bottom:18px}
|
||
.an-tabs{display:flex;gap:8px;margin-bottom:18px}
|
||
.an-tab{padding:9px 16px;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;background:var(--white);border:1.5px solid var(--border);color:var(--muted)}
|
||
.an-tab.active{background:var(--primary);border-color:var(--primary);color:#fff}
|
||
.an-pane{display:none}.an-pane.active{display:block}
|
||
/* Canvas */
|
||
.canvas-grid{display:grid;grid-template-columns:repeat(5,1fr);grid-auto-rows:minmax(84px,auto);gap:8px}
|
||
.cv{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:11px}
|
||
.cv-h{font-size:10px;font-weight:700;text-transform:uppercase;color:var(--primary);margin-bottom:6px;display:flex;justify-content:space-between}
|
||
.cv-h span{color:#9ca3af}
|
||
.cv li{font-size:11.5px;color:#374151;line-height:1.35;list-style:none;padding-left:10px;position:relative;margin-bottom:3px}
|
||
.cv li::before{content:'';position:absolute;left:2px;top:7px;width:4px;height:4px;border-radius:50%;background:var(--mid)}
|
||
.cv-note{font-size:10px;color:#92400e;margin-top:6px;font-style:italic}
|
||
.cv.vp{grid-column:3;grid-row:span 2;border-color:var(--primary);border-width:1.5px}.cv.kp{grid-row:span 2}.cv.cs{grid-column:5;grid-row:span 2}
|
||
.cv-ins{grid-column:1/-1;background:var(--sb);color:#fff;border-radius:11px;padding:14px 18px;font-size:13px;line-height:1.5}
|
||
.cv-ins b{color:var(--mid)}
|
||
/* IDEF0 */
|
||
.idef-lbl{font-size:11px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin:14px 0 8px}
|
||
.idef{margin-bottom:12px}
|
||
.idef-c,.idef-m{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;padding:4px 0}
|
||
.idef-c{border-bottom:2px dashed #cbd5e1}.idef-m{border-top:2px dashed #cbd5e1}
|
||
.idef-mid{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:stretch}
|
||
.idef-i,.idef-o{display:flex;flex-direction:column;gap:4px;justify-content:center;max-width:140px}
|
||
.idef-fn{background:var(--white);border:2px solid var(--primary);border-radius:10px;padding:11px 9px;text-align:center;font-size:12.5px;font-weight:700;color:var(--dark);display:flex;flex-direction:column;gap:3px;justify-content:center}
|
||
.idef-fn b{font-size:10px;color:var(--primary);font-family:'Montserrat'}.idef-fn i{font-style:normal;font-size:10px;font-weight:700}
|
||
.ar{font-size:10px;line-height:1.3;padding:3px 7px;border-radius:6px;background:#F1F5F9;color:#475569}
|
||
.idef-c .ar{background:#FEF3C7;color:#92400E}.idef-c .ar.nomiss{background:#FEF2F2;color:#DC2626;border:1px dashed #FECACA}
|
||
.idef-i .ar{background:#EFF6FF;color:#1E40AF}.idef-o .ar{background:#ECFDF5;color:#047857}.idef-o .ar.dead{background:#FEF2F2;color:#DC2626}
|
||
.idef-m .ar{background:#F5F3FF;color:#6D28D9}
|
||
.blk{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:10px}
|
||
.blk-pain{font-size:12px;color:#92400e;padding-left:14px;position:relative;margin-bottom:3px;line-height:1.4}
|
||
.blk-pain::before{content:'•';position:absolute;left:3px;color:#f59e0b}
|
||
/* Spec */
|
||
.spec-h{font-family:'Montserrat';font-weight:800;font-size:16px;color:var(--text);margin:18px 0 12px;display:flex;align-items:center;gap:8px}
|
||
.spec-h .pl{width:24px;height:24px;border-radius:7px;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800}
|
||
.mod{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:15px;margin-bottom:10px}
|
||
.mod-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}
|
||
.mod-node{font-size:10px;font-weight:700;color:var(--primary);background:var(--light);padding:2px 7px;border-radius:5px}
|
||
.mod-name{font-size:14px;font-weight:700}
|
||
.scr{font-size:11px;background:#EFF6FF;color:#1E40AF;border:1px solid #BFDBFE;border-radius:6px;padding:3px 9px;display:inline-block;margin:3px 3px 0 0}
|
||
.mod-data{font-size:12px;color:#374151;margin-top:6px}
|
||
.ent{background:var(--white);border:1px solid var(--border);border-radius:11px;padding:13px;margin-bottom:8px}
|
||
.ent-name{font-size:13px;font-weight:700;font-family:'Montserrat';margin-bottom:8px}
|
||
.ent-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:5px;margin-bottom:8px}
|
||
.fld{font-size:11px;background:var(--bg);border-radius:6px;padding:4px 8px}.fld em{font-style:normal;color:#6366F1;font-size:10px}
|
||
.ent-ex{font-size:10.5px;color:#6b7280;font-family:monospace;background:var(--bg);border-radius:6px;padding:6px 9px;line-height:1.4}
|
||
/* Docs */
|
||
.pf-l{display:block;font-size:13px;font-weight:600;color:var(--text);margin-bottom:7px}
|
||
.pf-i{width:100%;border:1.5px solid var(--border);border-radius:10px;padding:11px 14px;font-size:14px;font-family:'Inter';outline:none}
|
||
.pf-i:focus{border-color:var(--mid);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
|
||
.pf-t{width:100%;min-height:120px;border:1.5px solid var(--border);border-radius:10px;padding:11px 14px;font-size:14px;font-family:'Inter';outline:none;resize:vertical;line-height:1.5}
|
||
.pf-t:focus{border-color:var(--mid);box-shadow:0 0 0 3px rgba(16,185,129,.1)}
|
||
.drop{border:2px dashed #CBD5E1;border-radius:14px;padding:32px;text-align:center;background:var(--subtle);max-width:520px;margin:8px auto}
|
||
.drop-ic{font-size:30px;margin-bottom:8px}
|
||
.spin{display:inline-block;animation:sp 1s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
|
||
.pay-banner{display:none;background:linear-gradient(135deg,var(--dark),var(--primary));color:#fff;padding:12px 26px;align-items:center;gap:14px;flex-shrink:0}
|
||
.pay-banner.show{display:flex}
|
||
.pay-banner b{font-size:16px;font-family:Montserrat}
|
||
.pay-btn{margin-left:auto;background:#fff;color:var(--primary);border:none;border-radius:9px;padding:9px 20px;font-weight:700;font-size:13px;cursor:pointer;font-family:Inter}
|
||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(15,15,26,.6);z-index:100;align-items:center;justify-content:center}
|
||
.modal-bg.show{display:flex}
|
||
.modal{background:#fff;border-radius:16px;padding:26px;max-width:440px;width:90%}
|
||
.modal-h{font-family:Montserrat;font-weight:800;font-size:20px;margin-bottom:4px}
|
||
.modal-sub{font-size:14px;color:var(--muted);margin-bottom:20px}
|
||
.pay-opt{display:flex;align-items:center;gap:14px;border:1.5px solid var(--border);border-radius:12px;padding:15px;margin-bottom:10px;cursor:pointer;transition:all .15s}
|
||
.pay-opt:hover{border-color:var(--primary);background:var(--light)}
|
||
.pay-opt-ic{width:42px;height:42px;border-radius:10px;background:var(--light);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
|
||
.pay-opt-t{font-size:15px;font-weight:700}.pay-opt-d{font-size:12px;color:var(--muted)}
|
||
.modal-close{margin-top:8px;width:100%;background:transparent;border:none;color:var(--muted);font-size:13px;cursor:pointer;padding:8px}
|
||
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:4px}
|
||
.btn-s{background:var(--white);color:var(--primary);border:1.5px solid var(--primary);font-size:13px;padding:9px 16px}
|
||
.btn-s:hover{background:var(--light)}
|
||
.exp-bar{margin-top:22px;border:1.5px solid var(--border);border-radius:16px;padding:22px;background:var(--white)}
|
||
.exp-bar.locked{background:linear-gradient(135deg,#FFFBEB,#FEF3C7);border-color:#FDE68A}
|
||
.exp-h{font-family:'Montserrat';font-weight:800;font-size:16px;color:var(--text);margin-bottom:6px}
|
||
.exp-d{font-size:13px;color:var(--muted);line-height:1.5;margin-bottom:14px}
|
||
.exp-btns{display:flex;flex-wrap:wrap;gap:10px}
|
||
@media print{.sb,.hdr,.pb,.hero,.nav,.exp-bar,.modal,.mic,.inp-bar{display:none!important}.main,.sv,.scroll,.pad{display:block!important;overflow:visible!important;height:auto!important;margin:0!important;padding:0!important}body{background:#fff}}
|
||
/* ── Мобильный (Telegram на телефоне) ── */
|
||
.hdr-burger{display:none;background:none;border:none;color:#fff;font-size:22px;cursor:pointer;padding:0 4px;line-height:1;flex-shrink:0}
|
||
.sb-backdrop{display:none;position:fixed;inset:54px 0 0 0;background:rgba(0,0,0,.55);z-index:40}
|
||
.sb-backdrop.show{display:block}
|
||
@media(max-width:560px){.idea-label{display:none}}
|
||
@media(max-width:680px){
|
||
.hdr{padding:0 12px;gap:8px}
|
||
.hdr-burger{display:block}
|
||
.hdr-t{font-size:12px}
|
||
.hdr-client{display:none}
|
||
.elena-nm{display:none}
|
||
.sb{position:fixed;left:-260px;top:54px;bottom:0;z-index:50;width:240px;transition:left .25s ease;box-shadow:3px 0 18px rgba(0,0,0,.45)}
|
||
.sb.open{left:0}
|
||
.main{width:100%}
|
||
.hero{padding:13px 15px;gap:11px}
|
||
.hero-ic{width:38px;height:38px;font-size:17px}
|
||
.hero-h{font-size:16px}
|
||
.hero-d{font-size:12px}
|
||
.chat{padding:16px 13px;gap:11px}
|
||
.msg{max-width:90%}
|
||
.bb{font-size:13.5px}
|
||
.inbar{padding:10px 13px;gap:8px}
|
||
.icon-btn{width:40px;height:40px}
|
||
.run-card{padding:20px 16px;margin:8px 14px 16px}
|
||
.exp-bar{padding:16px 14px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="hdr">
|
||
<button class="hdr-burger" onclick="toggleSb()" aria-label="Меню"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
|
||
<div class="hdr-ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
|
||
<div class="hdr-t">@wasrusgen1<span class="hdr-sep"></span><b>КОНСАЛТИНГ</b></div>
|
||
<div class="hdr-client" id="hdrClient"></div>
|
||
<div class="hdr-r"><button onclick="openIdeas()" title="Предложить идею" style="display:inline-flex;align-items:center;gap:6px;background:rgba(16,185,129,.16);color:#34D399;border:1px solid rgba(16,185,129,.32);border-radius:9px;padding:6px 11px;font-size:12px;font-weight:700;font-family:Inter;cursor:pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg><span class="idea-label">Предложить идею</span></button><div class="elena-chip"><div class="elena-av">Е</div><div class="elena-nm">Елена</div><div class="elena-dot"></div></div></div>
|
||
</header>
|
||
<div id="testBanner" style="display:flex;align-items:center;gap:10px;padding:9px 16px;background:#ECFDF5;border-bottom:1px solid #A7F3D0;color:#065F46;font-size:12.5px;font-weight:500;line-height:1.45">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#047857" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M9 2v6.5L4.2 17a2 2 0 0 0 1.8 3h12a2 2 0 0 0 1.8-3L15 8.5V2"/><path d="M8 2h8"/><path d="M7.5 13h9"/></svg>
|
||
<span><b>Тестовая версия.</b> Сервис в режиме обкатки — данные могут обновляться, это нормально. Спасибо, что тестируете вместе с нами!</span>
|
||
<button onclick="var b=document.getElementById('testBanner');b.style.display='none';try{localStorage.setItem('cab_test_hidden','1')}catch(e){}" aria-label="Скрыть" style="margin-left:auto;flex-shrink:0;background:none;border:none;color:#047857;font-size:16px;line-height:1;cursor:pointer;padding:2px 4px">✕</button>
|
||
</div>
|
||
<div class="pay-banner" id="payBanner"><span style="display:inline-flex;align-items:center;gap:7px"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>К оплате: <b id="payAmount"></b></span><button class="pay-btn" onclick="showPayModal()">Оплатить →</button></div>
|
||
<div class="modal-bg" id="signModal">
|
||
<div class="modal" style="max-width:520px">
|
||
<div class="modal-h">Подписание договора</div>
|
||
<div class="modal-sub">Для продолжения подпишите документы простой электронной подписью</div>
|
||
<div style="background:var(--subtle);border:1.5px solid var(--border);border-radius:11px;padding:14px;margin-bottom:16px;font-size:13px;line-height:1.6">
|
||
Ознакомьтесь с документами:<br>
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#047857" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><a href="#" onclick="viewDoc('offer');return false" style="color:var(--primary);font-weight:600">Договор-оферта на консультационные услуги</a><br>
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#047857" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><a href="#" onclick="viewDoc('pep');return false" style="color:var(--primary);font-weight:600">Соглашение об использовании ПЭП</a><br>
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#047857" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><a href="#" onclick="viewDoc('pdn');return false" style="color:var(--primary);font-weight:600">Политика обработки персональных данных</a>
|
||
</div>
|
||
<label style="display:flex;align-items:flex-start;gap:10px;margin-bottom:16px;cursor:pointer">
|
||
<input type="checkbox" id="signAgree" style="width:18px;height:18px;margin-top:2px;flex-shrink:0">
|
||
<span style="font-size:13px;line-height:1.5">Я ознакомлен и принимаю условия Договора-оферты и Соглашения об использовании простой электронной подписи</span>
|
||
</label>
|
||
<div id="signStep1">
|
||
<input id="signId" placeholder="Телефон или email" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:11px 14px;font-size:14px;font-family:Inter;outline:none;margin-bottom:12px">
|
||
<button class="btn btn-p" style="width:100%;justify-content:center" onclick="signRequest()">Получить код подтверждения</button>
|
||
</div>
|
||
<div id="signStep2" style="display:none">
|
||
<div id="signCodeHint" style="font-size:12px;color:var(--primary);background:var(--light);border-radius:8px;padding:8px 12px;margin-bottom:12px"></div>
|
||
<input id="signCode" placeholder="Код из SMS/email" maxlength="4" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:11px 14px;font-size:18px;font-family:Inter;outline:none;margin-bottom:12px;text-align:center;letter-spacing:8px">
|
||
<button class="btn btn-p" style="width:100%;justify-content:center;gap:7px" onclick="signConfirm()"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>Подписать договор</button>
|
||
</div>
|
||
<button class="modal-close" onclick="document.getElementById('signModal').classList.remove('show')">Отмена</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-bg" id="docModal">
|
||
<div class="modal" style="max-width:720px;max-height:80vh;display:flex;flex-direction:column">
|
||
<div class="modal-h" id="docTitle" style="flex-shrink:0">Документ</div>
|
||
<div id="docText" style="flex:1;overflow-y:auto;font-size:12.5px;line-height:1.6;white-space:pre-wrap;color:#374151;margin:12px 0;padding-right:8px"></div>
|
||
<button class="btn btn-p" style="flex-shrink:0;justify-content:center" onclick="document.getElementById('docModal').classList.remove('show')">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-bg" id="payModal">
|
||
<div class="modal">
|
||
<div class="modal-h">Оплата консалтинга</div>
|
||
<div class="modal-sub">Сумма: <b id="modalAmount"></b> · выберите способ</div>
|
||
<div class="pay-opt" onclick="payVia('card')"><div class="pay-opt-ic" style="color:#047857"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg></div><div><div class="pay-opt-t">Банковская карта</div><div class="pay-opt-d">Visa, Mastercard, Мир · мгновенно</div></div></div>
|
||
<div class="pay-opt" onclick="payVia('sbp')"><div class="pay-opt-ic" style="color:#047857"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg></div><div><div class="pay-opt-t">СБП — по QR-коду</div><div class="pay-opt-d">Перевод через приложение банка</div></div></div>
|
||
<div class="pay-opt" onclick="payVia('cash')"><div class="pay-opt-ic" style="color:#047857"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12h.01M18 12h.01"/></svg></div><div><div class="pay-opt-t">Наличными</div><div class="pay-opt-d">При встрече с консультантом</div></div></div>
|
||
<button class="modal-close" onclick="document.getElementById('payModal').classList.remove('show')">Отмена</button>
|
||
</div>
|
||
</div>
|
||
<div class="layout">
|
||
<div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
|
||
<aside class="sb" id="sbNav">
|
||
<div class="sb-nav">
|
||
<div class="si" id="si0" onclick="go(0)" style="margin-bottom:4px"><div class="si-num" style="background:rgba(255,255,255,.1);color:rgba(255,255,255,.7)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><div class="si-body"><div class="si-name">Профиль компании</div><div class="si-sub">О вашей деятельности</div></div></div>
|
||
<div style="height:1px;background:rgba(255,255,255,.06);margin:4px 18px 8px"></div>
|
||
<div class="sb-cap">Путь к результату</div>
|
||
<div class="si active" id="si1" onclick="go(1)"><div class="si-num">1</div><div class="si-body"><div class="si-lbl">Этап 1</div><div class="si-name">Разговор с Еленой</div><div class="si-sub">Знакомство и диагностика</div></div></div>
|
||
<div class="si" id="si3" onclick="go(3)"><div class="si-num">2</div><div class="si-body"><div class="si-lbl">Этап 2</div><div class="si-name">Документы</div><div class="si-sub">Материалы</div></div></div>
|
||
<div class="si" id="si4" onclick="go(4)"><div class="si-num">3</div><div class="si-body"><div class="si-lbl">Этап 3</div><div class="si-name">Анализ</div><div class="si-sub">Модель бизнеса</div></div></div>
|
||
<div class="si" id="si5" onclick="go(5)"><div class="si-num">4</div><div class="si-body"><div class="si-lbl">Этап 4</div><div class="si-name">План</div><div class="si-sub">ТЗ на программу</div></div></div>
|
||
<div style="height:1px;background:rgba(255,255,255,.06);margin:8px 18px"></div>
|
||
<div class="si" id="si6" onclick="go(6)"><div class="si-num" style="background:rgba(4,120,87,.25);color:#10B981"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div class="si-body"><div class="si-name">Консультант</div><div class="si-sub">Связаться с Русланом</div></div></div>
|
||
</div>
|
||
<div class="sb-foot"><div class="pb-box"><div class="pb-row"><span class="pb-l">Прогресс</span><span class="pb-p" id="pbPct">20%</span></div><div class="pb-track"><div class="pb-fill" id="pbFill" style="width:20%"></div></div></div></div>
|
||
</aside>
|
||
<main class="main">
|
||
|
||
<!-- Профиль компании -->
|
||
<div class="sv" id="sv0">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><div><div class="hero-tag">Профиль компании</div><div class="hero-h">Расскажите о вашей деятельности</div><div class="hero-d">Базовая информация — Елена начнёт разговор уже зная контекст</div></div></div>
|
||
<div class="scroll"><div class="pad">
|
||
<div style="max-width:600px">
|
||
<div style="margin-bottom:18px"><label class="pf-l">Ваше имя или название компании</label><input class="pf-i" id="pfName" placeholder="Например: Игорь / ООО «Автодом»"></div>
|
||
<div style="margin-bottom:18px"><label class="pf-l">Сфера деятельности</label><input class="pf-i" id="pfNiche" placeholder="Например: автосервис, нутрициология, швейное производство"></div>
|
||
<div style="margin-bottom:22px"><label class="pf-l" style="display:flex;align-items:center;justify-content:space-between">Описание деятельности <button type="button" id="pfMic" class="mic" onclick="toggleMic('pfDesc','pfMic')" title="Надиктовать" style="width:30px;height:30px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button></label><textarea class="pf-t" id="pfDesc" placeholder="Чем занимаетесь, сколько человек, как сейчас всё устроено, что беспокоит. Можно надиктовать голосом"></textarea></div>
|
||
<button class="btn btn-p" id="pfSave" onclick="saveProfile()" style="padding:12px 24px;font-size:14px">Сохранить и начать с Еленой →</button>
|
||
</div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- Этап 1 — Разговор с Еленой (знакомство + диагностика) -->
|
||
<div class="sv active" id="sv1">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div><div class="hero-tag">Этап 1 из 4 · В процессе</div><div class="hero-h">Разговор с Еленой</div><div class="hero-d">Расскажите о бизнесе — Елена знакомится и сразу углубляется в детали. Текстом или голосом.</div></div></div>
|
||
<div id="startHint" style="margin:12px 16px 0;padding:11px 14px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:11px;font-size:13px;color:#065F46;line-height:1.5;display:flex;gap:9px;align-items:flex-start">
|
||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#047857" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:1px"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>
|
||
<div><b>С чего начать:</b> расскажите Елене о своём бизнесе — чем занимаетесь, сколько человек в команде, что сейчас болит. Можно <b>голосом</b> (кнопка микрофона) или текстом. Хватит 10–15 минут.</div>
|
||
<button onclick="var h=document.getElementById('startHint');h.style.display='none';try{localStorage.setItem('cab_hint_hidden','1')}catch(e){}" aria-label="Скрыть" style="flex-shrink:0;background:none;border:none;color:#047857;font-size:15px;line-height:1;cursor:pointer;padding:2px 4px">✕</button>
|
||
</div>
|
||
<div class="scroll"><div class="chat" id="chat"></div></div>
|
||
<div class="inbar">
|
||
<textarea class="inp" id="inp" rows="1" placeholder="Напишите или скажите Елене..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
|
||
<button class="icon-btn mic" id="micBtn" onclick="toggleMic()" title="Голосовой ввод"><div class="mic-hint" id="micHint">Слушаю... говорите</div><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="icon-btn send" id="sendBtn" onclick="sendMsg()"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Этап 3 — Документы -->
|
||
<div class="sv" id="sv3">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></div><div><div class="hero-tag">Этап 2 из 4</div><div class="hero-h">Документы</div><div class="hero-d">Загрузите материалы — Елена учтёт их в анализе</div></div></div>
|
||
<div class="scroll"><div class="pad">
|
||
<div class="drop" id="dropZone" onclick="document.getElementById('fileInp').click()">
|
||
<div class="drop-ic" style="color:#94A3B8"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></div>
|
||
<div style="font-size:14px;font-weight:600;margin-bottom:4px">Нажмите или перетащите документы</div>
|
||
<div style="font-size:12px;color:#9ca3af">PDF, Word, Excel, txt · прайс, оргструктура, отчёты</div>
|
||
<input type="file" id="fileInp" multiple style="display:none" onchange="handleFiles(this.files)">
|
||
</div>
|
||
<div id="docList" style="max-width:520px;margin:16px auto 0"></div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- Этап 4 — Анализ -->
|
||
<div class="sv" id="sv4">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></div><div><div class="hero-tag">Этап 3 из 4</div><div class="hero-h">Анализ бизнеса</div><div class="hero-d">Елена строит модель вашего бизнеса</div></div></div>
|
||
<div class="scroll"><div class="pad" id="anPad"></div></div>
|
||
</div>
|
||
|
||
<!-- Этап 5 — План -->
|
||
<div class="sv" id="sv5">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg></div><div><div class="hero-tag">Этап 4 из 4</div><div class="hero-h">План — ТЗ на программу</div><div class="hero-d">Проект системы для вашего бизнеса</div></div></div>
|
||
<div class="scroll"><div class="pad" id="specPad"></div></div>
|
||
</div>
|
||
|
||
<!-- Канал «Консультант» (живой) -->
|
||
<div class="sv" id="sv6">
|
||
<div class="hero"><div class="hero-ic"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div style="flex:1"><div class="hero-tag">Личный консультант</div><div class="hero-h">Связаться с Русланом</div><div class="hero-d">Вопросы по проекту, срокам, оплате — напишите напрямую</div></div></div>
|
||
<div class="scroll"><div class="chat" id="opChat"></div></div>
|
||
<div class="inbar">
|
||
<textarea class="inp" id="opInp" rows="1" placeholder="Напишите консультанту…" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();opSend()}"></textarea>
|
||
<button class="icon-btn mic" id="opMic" title="Голосом" onclick="toggleMic('opInp','opMic')"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="icon-btn send" id="opSendBtn" onclick="opSend()"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Спросить Елену — док на этапах 3-5 -->
|
||
<div class="askdock" id="askDock">
|
||
<div class="askdock-head" onclick="toggleAsk()"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;vertical-align:-3px"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>Спросить Елену <span class="ad-sub" id="adSub">об этом этапе</span><svg class="ad-chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></div>
|
||
<div class="askdock-body">
|
||
<div class="askdock-thread" id="askThread"></div>
|
||
<div class="askdock-inbar">
|
||
<textarea id="askInp" rows="1" placeholder="Спросите Елену об этом этапе…" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();askElena()}"></textarea>
|
||
<input type="file" id="askFile" multiple style="display:none" onchange="askAttach(this.files)">
|
||
<button class="icon-btn" id="askAttachBtn" title="Приложить документ" onclick="document.getElementById('askFile').click()" style="background:var(--light);border:1.5px solid var(--border);color:var(--primary)"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button>
|
||
<button class="icon-btn mic" id="askMic" title="Голосом" onclick="toggleMic('askInp','askMic')"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="icon-btn send" id="askSend" onclick="askElena()"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
const API=location.hostname.indexOf("wasrusgen1.ru")>=0?"/consulting":"https://claude-83-172-150-111.sslip.io/elena";
|
||
// Доступ: токен из ссылки-приглашения (?t=) имеет приоритет
|
||
const urlToken=new URLSearchParams(location.search).get("t");
|
||
if(urlToken)localStorage.setItem("cab_token",urlToken);
|
||
let token=urlToken||localStorage.getItem("cab_token"), state=null, cur=1;
|
||
const chat=document.getElementById("chat"), inp=document.getElementById("inp");
|
||
const PCTS={1:25,3:50,4:75,5:100}; // 4 шага для клиента (Этап 1 объединён)
|
||
function esc(s){return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}
|
||
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
|
||
/* ── Иконки по брендбуку: Lucide, stroke 1.75, fill none, round ── */
|
||
const ICONS={
|
||
user:'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||
message:'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
||
folder:'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
|
||
file:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
|
||
chart:'<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
||
activity:'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
|
||
process:'<circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 19.07a10 10 0 0 1 0-14.14"/>',
|
||
team:'<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
||
clipboard:'<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>',
|
||
idea:'<path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/>',
|
||
paperclip:'<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>',
|
||
card:'<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
|
||
phone:'<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>',
|
||
cash:'<rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12h.01M18 12h.01"/>',
|
||
printer:'<polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>',
|
||
download:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
||
lock:'<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||
calendar:'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>',
|
||
check:'<polyline points="20 6 9 17 4 12"/>',
|
||
alert:'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
||
mic:'<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>',
|
||
receipt:'<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"/><path d="M16 8H8M16 12H8M13 16H8"/>',
|
||
search:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
||
target:'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
||
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||
close:'<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||
checkCircle:'<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
||
sparkle:'<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/>',
|
||
gift:'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>',
|
||
dot:'<circle cx="12" cy="12" r="5"/>'
|
||
};
|
||
function ic(n,s,sw){s=s||20;return '<svg style="display:inline-block;vertical-align:middle" width="'+s+'" height="'+s+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="'+(sw||1.75)+'" stroke-linecap="round" stroke-linejoin="round">'+(ICONS[n]||'')+'</svg>';}
|
||
|
||
function toggleSb(){
|
||
const sb=document.getElementById('sbNav'), bd=document.getElementById('sbBackdrop');
|
||
const open=sb.classList.toggle('open'); bd.classList.toggle('show',open);
|
||
}
|
||
function closeSb(){
|
||
const sb=document.getElementById('sbNav'), bd=document.getElementById('sbBackdrop');
|
||
if(sb)sb.classList.remove('open'); if(bd)bd.classList.remove('show');
|
||
}
|
||
function go(n){
|
||
if(document.getElementById('si'+n).classList.contains('locked'))return;
|
||
if(window.innerWidth<=680)closeSb();
|
||
cur=n;
|
||
document.querySelectorAll('.sv').forEach(s=>s.classList.remove('active'));
|
||
document.getElementById('sv'+n).classList.add('active');
|
||
document.querySelectorAll('.si').forEach(s=>s.classList.remove('active'));
|
||
document.getElementById('si'+n).classList.add('active');
|
||
if(PCTS[n]!=null){document.getElementById('pbPct').textContent=PCTS[n]+'%';document.getElementById('pbFill').style.width=PCTS[n]+'%';}
|
||
if(n===1){renderChat1();requestAnimationFrame(scrollChatBottom);} // ре-рендер из общей беседы
|
||
if(n===6){renderOpChat();startOpPoll();}else{stopOpPoll();}
|
||
if(n===3)renderDocs();
|
||
if(n===4)renderAnalysis();
|
||
if(n===5)renderSpecPane();
|
||
// Telegram: нативная «Назад» + лёгкий хаптик
|
||
const _tg=window.__tg;
|
||
if(_tg){try{if(_tg.BackButton){if(n&&n!==1)_tg.BackButton.show();else _tg.BackButton.hide();}if(_tg.HapticFeedback)_tg.HapticFeedback.selectionChanged();}catch(e){}}
|
||
// Док «Спросить Елену» — только на этапах 3-5
|
||
const dock=document.getElementById("askDock");
|
||
if(dock){
|
||
if(n>=3&&n<=5){dock.classList.add("show");dock.classList.add("open");document.getElementById("adSub").textContent=STAGE_LBL[n]||"об этапе";
|
||
if(!dock.dataset.rendered){renderAskThread();dock.dataset.rendered="1";}}
|
||
else dock.classList.remove("show");
|
||
}
|
||
}
|
||
|
||
async function saveProfile(){
|
||
const name=document.getElementById("pfName").value.trim();
|
||
const niche=document.getElementById("pfNiche").value.trim();
|
||
const desc=document.getElementById("pfDesc").value.trim();
|
||
if(!name&&!desc){alert("Заполните хотя бы имя и описание");return}
|
||
const btn=document.getElementById("pfSave");btn.disabled=true;btn.innerHTML='<span class="spin">⏳</span> Елена знакомится...';
|
||
try{
|
||
const r=await fetch(`${API}/api/project/profile`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,client_name:name,niche,description:desc})});
|
||
const d=await r.json();
|
||
if(d.error){alert("Ошибка: "+d.error);btn.disabled=false;btn.textContent="Сохранить и начать →";return}
|
||
// Обновляем состояние и чат
|
||
const r2=await fetch(`${API}/api/project/${token}`);state=await r2.json();
|
||
renderAll();go(1);
|
||
}catch(e){alert("Ошибка: "+e.message);btn.disabled=false}
|
||
}
|
||
|
||
/* ── Voice (переиспользуемый — любое поле) ── */
|
||
let recog=null,recording=false,micTarget=null,micBtnEl=null;
|
||
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
|
||
if(SR){recog=new SR();recog.lang="ru-RU";recog.continuous=true;recog.interimResults=true;let base="";
|
||
recog.onresult=e=>{let fin="",intr="";for(let i=e.resultIndex;i<e.results.length;i++){const t=e.results[i][0].transcript;if(e.results[i].isFinal)fin+=t;else intr+=t}if(fin)base+=fin;if(micTarget){micTarget.value=(base+intr).trim();if(micTarget.tagName==='TEXTAREA'){micTarget.style.height="auto";micTarget.style.height=Math.min(micTarget.scrollHeight,120)+"px";}}};
|
||
recog.onstart=()=>{base=(micTarget&&micTarget.value)?micTarget.value+" ":""};
|
||
recog.onend=()=>{if(recording){try{recog.start()}catch(e){}}};
|
||
recog.onerror=e=>{if(e.error==="not-allowed"){alert("Разрешите доступ к микрофону");stopMic()}};
|
||
}
|
||
function toggleMic(targetId,btnId){
|
||
if(!recog){alert("Голосовой ввод работает в Chrome / Android. На iPhone — печатайте.");return}
|
||
const t=document.getElementById(targetId||'inp'), b=document.getElementById(btnId||'micBtn');
|
||
if(recording&&micTarget===t){stopMic();return;}
|
||
if(recording)stopMic();
|
||
micTarget=t;micBtnEl=b;startMic();
|
||
}
|
||
function startMic(){recording=true;if(micBtnEl)micBtnEl.classList.add("rec");if(micBtnEl&&micBtnEl.id==='micBtn'){const h=document.getElementById("micHint");if(h)h.classList.add("show");}try{recog.start()}catch(e){}}
|
||
function stopMic(){recording=false;if(micBtnEl)micBtnEl.classList.remove("rec");const h=document.getElementById("micHint");if(h)h.classList.remove("show");try{recog.stop()}catch(e){}if(micTarget)micTarget.focus();}
|
||
|
||
/* ── Chat ── */
|
||
// Скроллит реальный контейнер (.scroll), а не .chat (у него нет своего скролла). Мгновенно, без анимации.
|
||
function scrollChatBottom(){const sc=chat.parentElement;if(sc)sc.scrollTop=sc.scrollHeight;}
|
||
function addMsg(role,text){const m=document.createElement("div");m.className="msg "+(role==="user"?"user":"");m.innerHTML=`<div class="av ${role==='user'?'u':'e'}">${role==='user'?'Я':'Е'}</div><div class="bb ${role==='user'?'out':'in'}">${fmt(text)}</div>`;chat.appendChild(m);scrollChatBottom()}
|
||
function showTyping(){const t=document.createElement("div");t.className="msg";t.id="typing";t.innerHTML=`<div class="av e">Е</div><div class="typing"><span></span><span></span><span></span></div>`;chat.appendChild(t);scrollChatBottom()}
|
||
function hideTyping(){const t=document.getElementById("typing");if(t)t.remove()}
|
||
|
||
/* ── Канал «Консультант» (живой) ── */
|
||
function opMsg(role,text){const c=document.getElementById("opChat");if(!c)return;const m=document.createElement("div");m.className="msg "+(role==="user"?"user":"");m.innerHTML=`<div class="av ${role==='user'?'u':'e'}">${role==='user'?'Я':'Р'}</div><div class="bb ${role==='user'?'out':'in'}">${fmt(text)}</div>`;c.appendChild(m);const sc=c.parentElement;if(sc)sc.scrollTop=sc.scrollHeight;}
|
||
function renderOpChat(){const c=document.getElementById("opChat");if(!c)return;c.innerHTML="";const m=state&&state.operator_chat||[];if(!m.length){c.innerHTML='<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:24px 16px">Напишите консультанту — Руслан ответит здесь и пришлёт уведомление в Telegram.</div>';return;}m.forEach(x=>opMsg(x.role==="user"?"user":"elena",x.content));}
|
||
async function opSend(){const inp=document.getElementById("opInp");const t=inp.value.trim();if(!t)return;inp.value="";inp.style.height="auto";opMsg("user",t);state.operator_chat=state.operator_chat||[];state.operator_chat.push({role:"user",content:t});
|
||
try{const r=await fetch(`${API}/api/operator-chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:t})});const d=await r.json();if(d.messages)state.operator_chat=d.messages;}catch(e){opMsg("elena","Не доставлено: "+e.message);}
|
||
}
|
||
let opPollTimer=null;
|
||
async function opPollOnce(){try{const r=await fetch(`${API}/api/operator-chat?token=${encodeURIComponent(token)}`);const d=await r.json();if(d.messages&&d.messages.length!==(state.operator_chat||[]).length){state.operator_chat=d.messages;renderOpChat();}}catch(e){}}
|
||
function startOpPoll(){stopOpPoll();opPollOnce();opPollTimer=setInterval(opPollOnce,15000);}
|
||
function stopOpPoll(){if(opPollTimer){clearInterval(opPollTimer);opPollTimer=null;}}
|
||
/* ── Предложения клиента ── */
|
||
const SUG_ST={new:[ic('dot',12)+' Новое','#EFF6FF','#2563EB'],discussion:[ic('message',12)+' На обсуждении','#FEF3C7','#92400E'],accepted:[ic('checkCircle',12)+' Принято','#D1FAE5','#047857'],rejected:[ic('close',12)+' Отклонено','#FEE2E2','#B91C1C']};
|
||
function openIdeas(){
|
||
const sgs=state.suggestions||[];
|
||
const list=sgs.length?sgs.map(s=>{const st=SUG_ST[s.status]||SUG_ST.new;return `<div style="border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px"><div style="font-size:13px">${esc(s.text)}</div><div style="margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-size:11px;font-weight:700;color:${st[2]};background:${st[1]};padding:2px 8px;border-radius:6px">${st[0]}</span>${s.decision?`<span style="font-size:11px;color:#6B7280">— ${esc(s.decision)}</span>`:''}</div></div>`}).join(''):'<div style="font-size:13px;color:#9ca3af;text-align:center;padding:10px">Пока нет. Предложите первую идею — мы рассмотрим её.</div>';
|
||
const m=document.createElement('div');m.id='ideaModal';m.style.cssText='position:fixed;inset:0;background:rgba(15,15,26,.5);z-index:260;display:flex;align-items:center;justify-content:center;padding:18px';m.onclick=()=>m.remove();
|
||
m.innerHTML=`<div onclick="event.stopPropagation()" style="background:#fff;border-radius:16px;padding:22px;width:100%;max-width:420px;max-height:84vh;display:flex;flex-direction:column">
|
||
<div style="font-size:18px;font-weight:800;font-family:Montserrat,Inter;margin-bottom:4px;display:flex;align-items:center;gap:8px">${ic('idea',20)} Мои предложения</div>
|
||
<div style="font-size:12px;color:#6B7280;margin-bottom:12px">Ваши идеи по проекту. Консультант рассмотрит каждую и ответит решением.</div>
|
||
<div style="overflow-y:auto;flex:1;margin-bottom:12px">${list}</div>
|
||
<textarea id="ideaInp" rows="2" placeholder="Опишите идею или улучшение…" style="width:100%;border:1.5px solid var(--border);border-radius:10px;padding:10px 12px;font-size:13px;font-family:Inter;resize:vertical;outline:none;box-sizing:border-box"></textarea>
|
||
<div style="display:flex;gap:8px;margin-top:10px"><button onclick="document.getElementById('ideaModal').remove()" style="flex:1;padding:11px;background:#F1F5F9;color:#475569;border:none;border-radius:10px;font-weight:700;font-family:Inter;cursor:pointer">Закрыть</button><button id="ideaOk" onclick="submitIdea()" style="flex:1.4;padding:11px;background:#047857;color:#fff;border:none;border-radius:10px;font-weight:700;font-family:Inter;cursor:pointer">Предложить →</button></div>
|
||
</div>`;
|
||
document.body.appendChild(m);setTimeout(()=>{const e=document.getElementById('ideaInp');if(e)e.focus();},40);
|
||
}
|
||
async function submitIdea(){
|
||
const t=(document.getElementById('ideaInp').value||'').trim();if(!t)return;
|
||
const b=document.getElementById('ideaOk');b.disabled=true;b.textContent='Отправляю…';
|
||
try{const r=await fetch(`${API}/api/suggestion`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,text:t})});const d=await r.json();
|
||
if(d.suggestions)state.suggestions=d.suggestions;
|
||
const mm=document.getElementById('ideaModal');if(mm)mm.remove();openIdeas();
|
||
}catch(e){b.disabled=false;b.textContent='Предложить →';}
|
||
}
|
||
/* ── Спросить Елену (этапы 3-5) ── */
|
||
const STAGE_LBL={3:"о документах",4:"о стратегии и модели",5:"о ТЗ и плане"};
|
||
function toggleAsk(){document.getElementById("askDock").classList.toggle("open")}
|
||
function addAsk(role,text,dev){
|
||
const t=document.getElementById("askThread");if(!t)return;
|
||
const m=document.createElement("div");m.className="am "+(role==="user"?"u":"e");
|
||
m.innerHTML=`<div class="am-av">${role==="user"?"Я":"Е"}</div><div class="am-bb">${fmt(text)}${dev?'<div class="am-dev">'+ic('alert',12)+' Ваше пожелание зафиксировано — учтём при внедрении</div>':''}</div>`;
|
||
t.appendChild(m);t.scrollTop=t.scrollHeight;
|
||
}
|
||
function renderAskThread(){
|
||
const t=document.getElementById("askThread");if(!t)return;
|
||
t.innerHTML="";(state&&state.qa||[]).forEach(m=>addAsk(m.role==="user"?"user":"elena",m.content));
|
||
}
|
||
async function askAttach(files){
|
||
document.getElementById("askDock").classList.add("open");
|
||
for(const f of files){
|
||
addAsk("user","Файл: "+f.name);
|
||
try{
|
||
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
||
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
||
const d=await r.json();
|
||
if(d.error){addAsk("elena","Не удалось загрузить «"+f.name+"»: "+d.error);continue;}
|
||
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
||
addAsk("elena","Документ «"+f.name+"» приложен — учту его в ответах по проекту. Спрашивайте.");
|
||
}catch(e){addAsk("elena","Ошибка загрузки: "+e.message);}
|
||
}
|
||
document.getElementById("askFile").value="";
|
||
}
|
||
async function askElena(){
|
||
const inp=document.getElementById("askInp");const text=inp.value.trim();if(!text)return;
|
||
inp.value="";inp.style.height="auto";
|
||
document.getElementById("askDock").classList.add("open");
|
||
addAsk("user",text);
|
||
const btn=document.getElementById("askSend");btn.disabled=true;
|
||
const tp=document.createElement("div");tp.className="am e";tp.id="askTyping";tp.innerHTML='<div class="am-av">Е</div><div class="am-bb">…</div>';
|
||
document.getElementById("askThread").appendChild(tp);
|
||
try{
|
||
const r=await fetch(`${API}/api/ask`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:text,stage:String(cur)})});
|
||
const d=await r.json();
|
||
const x=document.getElementById("askTyping");if(x)x.remove();
|
||
addAsk("elena",d.reply||("Ошибка: "+(d.error||"?")),d.deviation_recorded);
|
||
state.qa=state.qa||[];state.qa.push({role:"user",content:text},{role:"assistant",content:d.reply||""});
|
||
}catch(e){const x=document.getElementById("askTyping");if(x)x.remove();addAsk("elena","Ошибка связи: "+e.message)}
|
||
btn.disabled=false;
|
||
}
|
||
|
||
async function init(){
|
||
// ── Telegram Mini App: полноценная интеграция ──
|
||
const tg=window.Telegram&&window.Telegram.WebApp;
|
||
if(tg){try{
|
||
tg.ready();tg.expand();
|
||
const sp=tg.initDataUnsafe&&tg.initDataUnsafe.start_param;if(sp&&!token){token=sp;localStorage.setItem("cab_token",token);}
|
||
// Тема под бренд: тёмная шапка, светлый фон
|
||
if(tg.setHeaderColor)tg.setHeaderColor('#0F0F1A');
|
||
if(tg.setBackgroundColor)tg.setBackgroundColor('#F5F6F8');
|
||
// Защита от случайного смахивания вниз (не терять диалог)
|
||
if(tg.enableClosingConfirmation)tg.enableClosingConfirmation();
|
||
// Нативная кнопка «Назад» Telegram → к разговору с Еленой
|
||
if(tg.BackButton){tg.BackButton.onClick(()=>go(1));}
|
||
window.__tg=tg;
|
||
}catch(e){}}
|
||
if(token){const r=await fetch(`${API}/api/project/${token}`);if(r.ok){state=await r.json();renderAll();
|
||
fillProfile();
|
||
// Если профиль не заполнен — открыть вкладку Профиль
|
||
if(!state.description && !state.client_name) go(0); else go(1);
|
||
return;}}
|
||
const _src=new URLSearchParams(location.search).get('src');
|
||
const _body=_src?{source:_src==='landing'?'Лендинг':_src}:{};
|
||
const r=await fetch(`${API}/api/project/new`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(_body)});
|
||
const d=await r.json();token=d.token;localStorage.setItem("cab_token",token);
|
||
state={messages:[],client_name:"",niche:"",description:""};
|
||
go(0); // новый клиент → сразу профиль
|
||
}
|
||
function fillProfile(){
|
||
if(state.client_name)document.getElementById("pfName").value=state.client_name;
|
||
if(state.niche)document.getElementById("pfNiche").value=state.niche;
|
||
if(state.description)document.getElementById("pfDesc").value=state.description;
|
||
}
|
||
|
||
/* ── Документы (Этап 3) ── */
|
||
function renderDocs(){
|
||
const dl=document.getElementById("docList");if(!dl)return;
|
||
const docs=state.documents||[];
|
||
dl.innerHTML=docs.length?docs.map(d=>{const u=`${API}/api/doc?token=${encodeURIComponent(token)}&name=${encodeURIComponent(d.filename)}`;return `<div style="display:flex;align-items:center;gap:10px;background:var(--white);border:1px solid var(--border);border-radius:10px;padding:11px 14px;margin-bottom:8px"><span style="color:#047857;display:inline-flex">${ic('file',18)}</span><div style="flex:1;min-width:0"><a href="${u}" target="_blank" rel="noopener" style="font-size:13px;font-weight:600;color:var(--primary);text-decoration:none">${esc(d.filename)}</a><div style="font-size:11px;color:#9ca3af">${(d.size/1024).toFixed(0)} КБ · учтён в анализе</div></div><a href="${u}&dl=1" title="Скачать" style="color:#9ca3af;text-decoration:none;display:inline-flex">${ic('download',16)}</a></div>`}).join(""):'<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:10px">Документов пока нет</div>';
|
||
}
|
||
async function handleFiles(files){
|
||
const dl=document.getElementById("docList");
|
||
for(const f of files){
|
||
const tmp=document.createElement("div");tmp.style.cssText="background:var(--white);border:1px solid var(--border);border-radius:10px;padding:11px 14px;margin-bottom:8px;font-size:13px";tmp.innerHTML=`<span class="spin">⏳</span> ${esc(f.name)} — загрузка...`;dl.prepend(tmp);
|
||
try{
|
||
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result);r.onerror=rej;r.readAsDataURL(f)});
|
||
const r=await fetch(`${API}/api/upload`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,filename:f.name,content:b64})});
|
||
const d=await r.json();
|
||
if(d.error){tmp.innerHTML=`${ic('alert',13)} ${esc(f.name)}: ${d.error}`;continue}
|
||
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
||
}catch(e){tmp.innerHTML=`${ic('alert',13)} ${esc(f.name)}: ${e.message}`;continue}
|
||
}
|
||
renderDocs();
|
||
}
|
||
function renderAll(){
|
||
if(state.client_name)document.getElementById("hdrClient").textContent="· "+state.client_name;
|
||
renderChat1();
|
||
unlockStages();
|
||
checkPayment();
|
||
}
|
||
function renderChat1(){const c=document.getElementById("chat");if(!c)return;c.innerHTML="";(state.messages||[]).forEach(m=>addMsg(m.role==="user"?"user":"elena",m.content));}
|
||
function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"}
|
||
function checkPayment(){
|
||
const crm=state.crm||{};const deal=crm.deal_amount||0;
|
||
const paid=(crm.payments||[]).reduce((s,p)=>s+(p.amount||0),0);
|
||
const left=deal-paid;
|
||
if(left>0){
|
||
document.getElementById("payAmount").textContent=money(left);
|
||
document.getElementById("modalAmount").textContent=money(left);
|
||
document.getElementById("payBanner").classList.add("show");
|
||
const btn=document.querySelector("#payBanner .pay-btn");
|
||
if(btn)btn.textContent=state.signed?"Оплатить →":"Подписать и оплатить →";
|
||
window.__payLeft=left;
|
||
}else document.getElementById("payBanner").classList.remove("show");
|
||
}
|
||
function showPayModal(){
|
||
// Оплата только после подписания договора
|
||
if(!state.signed){document.getElementById("signModal").classList.add("show");return}
|
||
document.getElementById("payModal").classList.add("show");
|
||
}
|
||
async function viewDoc(doc){
|
||
const dm=document.getElementById("docModal");
|
||
document.getElementById("docTitle").textContent="Загрузка...";document.getElementById("docText").textContent="";dm.classList.add("show");
|
||
try{const r=await fetch(`${API}/api/legal/${doc}`);const d=await r.json();
|
||
document.getElementById("docTitle").textContent={offer:"Договор-оферта",pep:"Соглашение об использовании ПЭП",pdn:"Политика обработки персональных данных"}[doc]||"Документ";
|
||
document.getElementById("docText").textContent=d.text||d.error||"Документ недоступен";
|
||
}catch(e){document.getElementById("docText").textContent="Ошибка: "+e.message}
|
||
}
|
||
async function signRequest(){
|
||
if(!document.getElementById("signAgree").checked){alert("Поставьте отметку о согласии с условиями");return}
|
||
const id=document.getElementById("signId").value.trim();
|
||
if(!id){alert("Укажите телефон или email");return}
|
||
try{const r=await fetch(`${API}/api/sign/request`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,identifier:id})});const d=await r.json();
|
||
if(d.error){alert("Ошибка: "+d.error);return}
|
||
document.getElementById("signStep1").style.display="none";
|
||
document.getElementById("signStep2").style.display="block";
|
||
document.getElementById("signCodeHint").textContent=d.demo_code?("ДЕМО-код: "+d.demo_code+" (в проде придёт по SMS/email)"):"Код отправлен на "+id;
|
||
}catch(e){alert("Ошибка: "+e.message)}
|
||
}
|
||
async function signConfirm(){
|
||
const code=document.getElementById("signCode").value.trim();
|
||
if(code.length<4){alert("Введите код");return}
|
||
try{const r=await fetch(`${API}/api/sign/confirm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,code,docs:["offer","pep"]})});const d=await r.json();
|
||
if(d.error){alert("Ошибка: "+d.error);return}
|
||
state.signed=true;
|
||
document.getElementById("signModal").classList.remove("show");
|
||
document.getElementById("signStep1").style.display="block";document.getElementById("signStep2").style.display="none";
|
||
alert("Договор подписан.\nПодписант: "+d.identifier);
|
||
document.getElementById("payModal").classList.add("show"); // сразу к оплате
|
||
}catch(e){alert("Ошибка: "+e.message)}
|
||
}
|
||
async function payVia(method){
|
||
const amount=window.__payLeft||0;
|
||
document.getElementById("payModal").classList.remove("show");
|
||
try{
|
||
const r=await fetch(`${API}/api/payment/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,amount,method,return_url:location.href})});
|
||
const d=await r.json();
|
||
if(d.error){alert("Ошибка: "+d.error);return}
|
||
if(method==="cash"){alert(d.instructions);return}
|
||
if(d.demo){alert("ДЕМО-режим: ЮKassa ещё не подключена.\n\n"+d.note);return}
|
||
if(d.qr){alert("СБП: отсканируйте QR в приложении банка\n\n"+(d.qr||""));return}
|
||
if(d.confirmation_url){location.href=d.confirmation_url;return}
|
||
}catch(e){alert("Ошибка: "+e.message)}
|
||
}
|
||
function unlockStages(){
|
||
// Этап 4 доступен если есть достаточно сообщений
|
||
const enough=state.messages.length>=3;
|
||
[3,4,5].forEach(n=>{const si=document.getElementById('si'+n);if(si&&(enough||n===3))si.classList.remove('locked');});
|
||
}
|
||
|
||
async function sendMsg(){
|
||
if(recording)stopMic();
|
||
const text=inp.value.trim();if(!text)return;
|
||
inp.value="";inp.style.height="auto";
|
||
addMsg("user",text);state.messages.push({role:"user",content:text});
|
||
document.getElementById("sendBtn").disabled=true;showTyping();
|
||
try{const r=await fetch(`${API}/api/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:text})});const d=await r.json();hideTyping();
|
||
addMsg("elena",d.reply||("Ошибка: "+(d.error||"?")));state.messages.push({role:"assistant",content:d.reply||""});unlockStages();
|
||
}catch(e){hideTyping();addMsg("elena","Ошибка связи: "+e.message)}
|
||
document.getElementById("sendBtn").disabled=false;
|
||
}
|
||
|
||
/* ── Analysis (Этап 4) ── */
|
||
let anTab="canvas";
|
||
function renderAnalysis(){
|
||
const pad=document.getElementById("anPad");
|
||
pad.innerHTML=`<div class="an-tabs"><div class="an-tab ${anTab==='canvas'?'active':''}" onclick="setAnTab('canvas')">${ic('chart',16)} Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">${ic('process',16)} Функции</div><div class="an-tab ${anTab==='org'?'active':''}" onclick="setAnTab('org')">${ic('team',16)} Организация</div></div><div id="anContent"></div>`;
|
||
renderAnContent();
|
||
}
|
||
function setAnTab(t){anTab=t;renderAnalysis()}
|
||
function renderAnContent(){
|
||
const c=document.getElementById("anContent");
|
||
if(anTab==='canvas'){
|
||
if(!state.canvas){c.innerHTML=runCard("canvas",ic('chart',30),"Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return}
|
||
c.innerHTML=renderCanvas(state.canvas);
|
||
}else if(anTab==='idef0'){
|
||
if(!state.model){c.innerHTML=runCard("model",ic('process',30),"Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return}
|
||
c.innerHTML=renderIdef(state.model);
|
||
}else{
|
||
c.innerHTML=renderOrg();
|
||
}
|
||
}
|
||
function renderOrg(){
|
||
if(!state.model)return runCard(null,ic('alert',30),"Сначала функции","Оргструктура строится из функциональной модели. Постройте модель на вкладке «Функции».","→ К функциям",()=>setAnTab('idef0'));
|
||
let h='';
|
||
if(!state.orgchart)h+=runCard("orgchart",ic('team',30),"Целевая оргструктура","Елена построит оргструктуру из модели: кто за что отвечает, подчинённость, штат, узкие места.","Построить оргструктуру →");
|
||
else h+=renderOrgChart(state.orgchart);
|
||
if(state.orgchart){
|
||
if(!state.jobs)h+=`<div style="height:14px"></div>`+runCard("jobs",ic('clipboard',30),"Должностные инструкции","По ролям: зоны ответственности, KPI, полномочия. С учётом ваших пожеланий (отклонений).","Собрать инструкции →");
|
||
else h+=`<div style="height:14px"></div>`+renderJobs(state.jobs);
|
||
}
|
||
return h;
|
||
}
|
||
function renderOrgChart(o){
|
||
const units=o.units||[];
|
||
let h=`<div style="background:var(--sb);color:#fff;border-radius:11px;padding:12px 16px;margin-bottom:14px;font-size:13px"><b style="color:var(--mid)">Вывод:</b> ${esc(o.insight||'')}</div>`;
|
||
h+=units.map(u=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:12px 14px;margin-bottom:8px">
|
||
<div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap">
|
||
<span style="font-size:14px;font-weight:800">${esc(u.role||'')}</span>
|
||
<span style="font-size:11px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 8px;border-radius:5px">${(+u.headcount||1)} чел.</span>
|
||
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
||
</div>
|
||
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
||
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">${ic('alert',12)} ${esc(u.note)}</div>`:''}
|
||
</div>`).join('');
|
||
return h;
|
||
}
|
||
function renderJobs(j){
|
||
const roles=j.roles||[];
|
||
return `<div style="font-size:13px;font-weight:800;margin-bottom:10px;display:flex;align-items:center;gap:7px">${ic('clipboard',17)} Должностные инструкции</div>`+roles.map(r=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:14px 16px;margin-bottom:10px">
|
||
<div style="font-size:14px;font-weight:800">${esc(r.role||'')}</div>
|
||
<div style="font-size:12px;color:#6b7280;margin:3px 0 9px">${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}</div>
|
||
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Зоны ответственности</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
||
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
||
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
||
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">${ic('alert',12)} Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
||
</div>`).join('');
|
||
}
|
||
function renderSpecPane(){
|
||
const pad=document.getElementById("specPad");
|
||
if(!state.spec){
|
||
if(!state.model){pad.innerHTML=runCard(null,ic('alert',30),"Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
||
pad.innerHTML=runCard("spec",ic('clipboard',30),"Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
||
pad.innerHTML=renderSpec(state.spec)+renderExportBar();
|
||
}
|
||
const EST_STAGES=[
|
||
{key:"interview", name:"Интервью", icon:ic('message',15), desc:"Диагностика «как есть» — карта проблем"},
|
||
{key:"methods", name:"Методологии", icon:ic('target',15), desc:"Подбор фреймворков под вашу задачу"},
|
||
{key:"canvas", name:"Стратегия", icon:ic('chart',15), desc:"Целевая модель процессов (TO-BE)"},
|
||
{key:"idef0", name:"Функции IDEF0", icon:ic('process',15), desc:"Декомпозиция функций + регламенты"},
|
||
{key:"org", name:"Организация", icon:ic('team',15), desc:"Оргструктура + должностные инструкции"},
|
||
{key:"spec", name:"Выдача ТЗ", icon:ic('clipboard',15), desc:"Готовое ТЗ к внедрению"},
|
||
];
|
||
function estStageDone(k){
|
||
if(k==="interview")return (state.messages||[]).length>0;
|
||
if(k==="methods") return !!state.selection;
|
||
if(k==="canvas") return !!state.canvas;
|
||
if(k==="idef0") return !!state.model;
|
||
if(k==="org") return !!(state.orgchart && state.jobs);
|
||
if(k==="spec") return !!state.spec;
|
||
return false;
|
||
}
|
||
function renderEstimateCard(){
|
||
const crm=state.crm||{};
|
||
const sp=crm.stage_prices;
|
||
if(!sp)return ''; // смета ещё не сформирована — не показываем
|
||
const pays=crm.stage_payments||{};
|
||
const due=crm.stage_due||{};
|
||
const fmtD=d=>{if(!d)return'';const a=d.split('-');return a[2]+'.'+a[1]+'.'+a[0].slice(2);};
|
||
const total=Object.values(sp).reduce((a,b)=>a+(+b||0),0);
|
||
const paid=Object.values(pays).reduce((s,p)=>s+(p.amount||0),0);
|
||
return `<div class="exp-bar" style="margin-bottom:14px">
|
||
<div class="exp-h" style="display:flex;align-items:center;gap:7px">${ic('receipt',18)} Смета проекта</div>
|
||
<div class="exp-d" style="margin-bottom:10px">Прозрачно: за что и сколько. Интервью — бесплатно, дальше помодульно. Готовое ТЗ — после полной оплаты.</div>
|
||
${EST_STAGES.map(s=>{
|
||
const price=+sp[s.key]||0, isFree=price<=0, isPaid=!!pays[s.key], done=estStageDone(s.key);
|
||
const dd=due[s.key]||'';
|
||
let tag='';
|
||
if(isFree)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">бесплатно</span>';
|
||
else if(isPaid)tag='<span style="font-size:10px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 7px;border-radius:5px">'+ic('check',11)+' оплачено</span>';
|
||
else if(done)tag='<span style="font-size:10px;font-weight:700;color:#92400E;background:#FEF3C7;padding:1px 7px;border-radius:5px">к оплате</span>';
|
||
else tag='<span style="font-size:10px;font-weight:700;color:#9CA3AF;background:#F3F4F6;padding:1px 7px;border-radius:5px">в работе</span>';
|
||
const dueChip=(dd&&!isPaid&&!isFree)?`<span style="font-size:10px;font-weight:600;color:#2563EB;background:#EFF6FF;padding:1px 7px;border-radius:5px">${ic('calendar',11)} до ${fmtD(dd)}</span>`:'';
|
||
return `<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid rgba(0,0,0,.06)">
|
||
<span style="font-size:15px">${s.icon}</span>
|
||
<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${s.name} ${tag} ${dueChip}</div><div style="font-size:11px;color:#6b7280;margin-top:1px">${s.desc}</div></div>
|
||
<span style="font-size:13px;font-weight:700;color:${isFree?'#9CA3AF':'#047857'};white-space:nowrap">${isFree?'0 ₽':money(price)}</span>
|
||
</div>`;
|
||
}).join('')}
|
||
<div style="display:flex;align-items:center;padding:11px 0 2px;border-top:2px solid rgba(0,0,0,.1);margin-top:3px">
|
||
<span style="font-size:13px;font-weight:800">Итого</span>
|
||
<span style="margin-left:auto;font-size:17px;font-weight:800;color:#047857">${money(total)}</span>
|
||
</div>
|
||
${paid>0?`<div style="font-size:11px;color:#6b7280;margin-top:4px">Оплачено: <b style="color:#047857">${money(paid)}</b> · остаток ${money(Math.max(0,total-paid))}</div>`:''}
|
||
</div>`;
|
||
}
|
||
function renderExportBar(){
|
||
if(state.unlocked){
|
||
return renderEstimateCard()+`<div class="exp-bar"><div class="exp-h" style="display:flex;align-items:center;gap:7px">${ic('crm',17)} Готовые документы</div>
|
||
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
||
<div class="exp-btns">
|
||
<button class="btn btn-p" onclick="printDoc()">${ic('printer',16)} Печать / PDF</button>
|
||
<button class="btn btn-s" onclick="downloadTZ()">${ic('download',16)} Скачать ТЗ</button>
|
||
<button class="btn btn-s" onclick="exportDev()">{ } Выгрузить для разработчика</button>
|
||
</div></div>`;
|
||
}
|
||
const debt=state.debt||0, deal=state.deal_amount||0;
|
||
const moneyStr=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: <b>${debt.toLocaleString('ru')} ₽</b>`:''}`:'';
|
||
return renderEstimateCard()+`<div class="exp-bar locked">
|
||
<div class="exp-h" style="display:flex;align-items:center;gap:7px">${ic('lock',17)} Документы готовы — доступны после оплаты</div>
|
||
<div class="exp-d">Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.<br>${moneyStr}</div>
|
||
<div class="exp-btns"><button class="btn btn-p" onclick="(window.showPayModal||function(){alert('Оплата скоро будет доступна')})()">Оплатить и забрать документы →</button></div>
|
||
</div>`;
|
||
}
|
||
function buildTZmd(s){
|
||
let m=`# ТЗ · ${state.client_name||''}\n# @wasrusgen1 | КОНСАЛТИНГ\n\n## A. Обзор\n${s.overview||''}\n\n## A. Роли\n`;
|
||
(s.roles||[]).forEach(r=>m+=`- **${r.name}** — ${r.does} (доступ: ${r.access})\n`);
|
||
m+=`\n## B. Модули\n`;(s.modules||[]).forEach(x=>{m+=`### ${x.name} [${x.source_node}]\n${x.purpose}\nЭкраны: ${(x.screens||[]).join(', ')}\nДанные: вход — ${x.inputs_data}; выход — ${x.outputs_data}\n`;(x.rules||[]).forEach(r=>m+=`- правило: ${r}\n`);m+=`\n`});
|
||
m+=`## C. Модель данных\n`;(s.entities||[]).forEach(e=>{m+=`### ${e.name}\n`;(e.fields||[]).forEach(f=>m+=`- ${f.field}: ${f.type}\n`);if((e.relations||[]).length)m+=`Связи: ${e.relations.join(' · ')}\n`;m+=`Пример: ${e.example}\n\n`});
|
||
if(state.orgchart&&(state.orgchart.units||[]).length){m+=`\n## D. Оргструктура\n${state.orgchart.insight||''}\n`;state.orgchart.units.forEach(u=>{m+=`- **${u.role}** (${(+u.headcount||1)} чел.)${u.reports_to&&u.reports_to!=='—'?` ↑ ${u.reports_to}`:''}${(u.owns_functions||[]).length?` — отвечает: ${u.owns_functions.join(', ')}`:''}${u.note?` (внимание: ${u.note})`:''}\n`})}
|
||
if(state.jobs&&(state.jobs.roles||[]).length){m+=`\n## E. Должностные инструкции\n`;state.jobs.roles.forEach(r=>{m+=`### ${r.role}\n${r.purpose||''}${r.reports_to?` (подчинение: ${r.reports_to})`:''}\n`;(r.responsibilities||[]).forEach(x=>m+=`- ${x}\n`);if((r.kpis||[]).length)m+=`KPI: ${r.kpis.join(' · ')}\n`;if((r.authority||[]).length)m+=`Полномочия: ${r.authority.join(' · ')}\n`;if(r.deviation_note)m+=`Внимание — учтено пожелание клиента: ${r.deviation_note}\n`;m+=`\n`})}
|
||
if((s.open_questions||[]).length){m+=`## Уточнить перед разработкой\n`;s.open_questions.forEach(q=>m+=`- ${q}\n`)}
|
||
return m;
|
||
}
|
||
function dl(name,text,type){const b=new Blob([text],{type:type||'text/plain;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),2000)}
|
||
function printDoc(){if(!state.unlocked)return alert('Доступно после оплаты');window.print()}
|
||
function downloadTZ(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.md`,buildTZmd(state.spec),'text/markdown;charset=utf-8')}
|
||
function exportDev(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.json`,JSON.stringify({client:state.client_name,niche:state.niche,model:state.model,orgchart:state.orgchart,jobs:state.jobs,spec:state.spec},null,2),'application/json')}
|
||
function runCard(stage,ic,t,d,btn,custom){
|
||
const id=stage?`id="rb-${stage}"`:'';
|
||
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
||
return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="btn btn-p" ${id} ${custom?'':onclick}>${btn}</button></div>`;
|
||
}
|
||
const BUILD={canvas:["build-canvas","canvas"],model:["build-model","model"],spec:["build-spec","spec"],orgchart:["build-orgchart","orgchart"],jobs:["build-jobs","jobs"]};
|
||
async function runBuild(stage){
|
||
const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`);
|
||
if(btn){btn.disabled=true;btn.innerHTML=`<span class="spin">⏳</span> Елена анализирует...`}
|
||
try{const r=await fetch(`${API}/api/${ep}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token})});const d=await r.json();
|
||
if(d.error){alert("Ошибка: "+d.error);if(btn){btn.disabled=false;btn.textContent="Повторить"}return}
|
||
state[key]=d[key];
|
||
if(stage==='spec')renderSpecPane();else renderAnContent();
|
||
}catch(e){alert("Ошибка: "+e.message);if(btn)btn.disabled=false}
|
||
}
|
||
|
||
function renderCanvas(c){const B=(k,cls,l)=>{const b=c[k];return `<div class="cv ${cls}"><div class="cv-h">${l}<span>${b.completeness}%</span></div><ul>${b.items.map(i=>`<li>${esc(i)}</li>`).join("")}</ul>${b.note?`<div class="cv-note">${esc(b.note)}</div>`:''}</div>`};
|
||
return `<div class="canvas-grid">${B("key_partners","kp","Партнёры")}${B("key_activities","ka","Активности")}${B("value_propositions","vp","Ценность")}${B("customer_relationships","cr","Отношения")}${B("customer_segments","cs","Сегменты")}${B("key_resources","kr","Ресурсы")}${B("channels","ch","Каналы")}${B("cost_structure","co","Издержки")}${B("revenue_streams","rev","Доходы")}<div class="cv-ins"><b>Вывод:</b> ${esc(c.insight)}</div></div>`}
|
||
function renderIdef(m){const box=(fn,ct,ins,outs,me,id,pct)=>{const C=(ct&&ct.length)?ct.map(c=>`<span class="ar">${esc(c.name)}</span>`).join(""):`<span class="ar nomiss">нет управления</span>`;const I=(ins||[]).map(a=>`<span class="ar">${esc(a.name)}</span>`).join("")||'<span class="ar">—</span>';const O=(outs||[]).map(a=>`<span class="ar ${a.target==='НИКУДА'?'dead':''}">${esc(a.name)}${a.target==='НИКУДА'?' ⊘':''}</span>`).join("")||'<span class="ar">—</span>';const M=(me||[]).map(x=>`<span class="ar">${esc(x.name)}</span>`).join("")||'<span class="ar">—</span>';return `<div class="idef"><div class="idef-c">${C}</div><div class="idef-mid"><div class="idef-i">${I}</div><div class="idef-fn">${id?`<b>${id}</b>`:''}${esc(fn)}${pct!=null?`<i style="color:${pct>=70?'#047857':pct>=45?'#F59E0B':'#EF4444'}">${pct}%</i>`:''}</div><div class="idef-o">${O}</div></div><div class="idef-m">${M}</div></div>`};
|
||
let h=`<div style="background:var(--sb);color:#fff;border-radius:11px;padding:12px 16px;margin-bottom:14px;font-size:13px"><b style="color:var(--mid)">Паттерн:</b> ${esc(m.business_pattern)}</div>`;
|
||
if(m.context){h+=`<div class="idef-lbl">A-0 Контекст</div>`+box(m.context.function,m.context.controls,m.context.inputs,m.context.outputs,m.context.mechanisms,"A0")}
|
||
h+=`<div class="idef-lbl">Декомпозиция · ${m.activities.length} функций</div>`;m.activities.forEach(a=>h+=box(a.function,a.controls,a.inputs,a.outputs,a.mechanisms,a.node_id,a.completeness));
|
||
if(m.arrow_issues&&m.arrow_issues.length){h+=`<div class="idef-lbl">Разрывы · ${m.arrow_issues.length}</div>`;m.arrow_issues.forEach(g=>{const col=g.severity==='critical'?'#DC2626':g.severity==='high'?'#92400E':'#1E40AF';h+=`<div class="blk" style="border-left:3px solid ${col};padding:11px 14px"><div style="font-size:10px;font-weight:700;color:#9ca3af">${esc(g.node_id)} · ${g.type}</div><div style="font-size:13px;font-weight:700">${esc(g.title)}</div><div style="font-size:12px;color:#6b7280;margin-top:3px">${esc(g.description)}</div></div>`})}
|
||
return h}
|
||
function renderSpec(s){let h=`<div class="spec-h"><span class="pl">A</span>Обзор</div><div class="blk">${esc(s.overview)}</div>`;
|
||
h+=`<div class="spec-h"><span class="pl">A</span>Роли (${s.roles.length})</div>`;s.roles.forEach(r=>h+=`<div class="blk" style="padding:12px 15px"><b>${esc(r.name)}</b> — ${esc(r.does)}<div style="font-size:11px;color:#9ca3af;margin-top:3px">Доступ: ${esc(r.access)}</div></div>`);
|
||
h+=`<div class="spec-h"><span class="pl">B</span>Модули (${s.modules.length})</div>`;s.modules.forEach(m=>h+=`<div class="mod"><div class="mod-top"><span class="mod-node">${esc(m.source_node)}</span><span class="mod-name">${esc(m.name)}</span></div><div style="font-size:12px;color:var(--muted)">${esc(m.purpose)}</div><div style="margin:6px 0">${m.screens.map(s=>`<span class="scr">${esc(s)}</span>`).join("")}</div><div class="mod-data">Вход: ${esc(m.inputs_data)} · Выход: ${esc(m.outputs_data)}</div>${m.rules.length?`<div style="margin-top:6px">${m.rules.map(r=>`<div class="blk-pain">${esc(r)}</div>`).join("")}</div>`:''}</div>`);
|
||
h+=`<div class="spec-h"><span class="pl">C</span>Модель данных (${s.entities.length} таблиц)</div>`;s.entities.forEach(e=>h+=`<div class="ent"><div class="ent-name">◆ ${esc(e.name)}</div><div class="ent-fields">${e.fields.map(f=>`<div class="fld"><b>${esc(f.field)}</b> <em>${esc(f.type)}</em></div>`).join("")}</div>${e.relations.length?`<div style="font-size:11px;color:#6b7280;margin-bottom:6px">Связи: ${e.relations.map(esc).join(" · ")}</div>`:''}<div class="ent-ex">${esc(e.example)}</div></div>`);
|
||
if(s.open_questions&&s.open_questions.length){h+=`<div class="blk" style="border-color:#FDE68A;background:#FFFBEB"><div style="font-weight:700;margin-bottom:8px">Уточнить перед разработкой</div>${s.open_questions.map(q=>`<div class="blk-pain">${esc(q)}</div>`).join("")}</div>`}
|
||
return h}
|
||
|
||
inp.addEventListener("input",()=>{inp.style.height="auto";inp.style.height=Math.min(inp.scrollHeight,120)+"px"});
|
||
try{ if(localStorage.getItem('cab_test_hidden')==='1'){var _tb=document.getElementById('testBanner');if(_tb)_tb.style.display='none';}
|
||
if(localStorage.getItem('cab_hint_hidden')==='1'){var _sh=document.getElementById('startHint');if(_sh)_sh.style.display='none';} }catch(e){}
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|