mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 22:24:46 +00:00
580 lines
52 KiB
HTML
580 lines
52 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}
|
||
.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}
|
||
/* Chat */
|
||
.chat{padding:24px 26px;display:flex;flex-direction:column;gap:14px}
|
||
.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}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="hdr">
|
||
<div class="hdr-ic">@</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"><div class="elena-chip"><div class="elena-av">Е</div><div class="elena-nm">Елена</div><div class="elena-dot"></div></div></div>
|
||
</header>
|
||
<div class="pay-banner" id="payBanner"><span>💳 К оплате: <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>
|
||
📄 <a href="#" onclick="viewDoc('offer');return false" style="color:var(--primary);font-weight:600">Договор-оферта на консультационные услуги</a><br>
|
||
📄 <a href="#" onclick="viewDoc('pep');return false" style="color:var(--primary);font-weight:600">Соглашение об использовании ПЭП</a><br>
|
||
📄 <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" onclick="signConfirm()">✓ Подписать договор</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">💳</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">📱</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">💵</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">
|
||
<aside class="sb">
|
||
<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)">👤</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="si2" onclick="go(2)"><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="si3" onclick="go(3)"><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="si4" onclick="go(4)"><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 class="si" id="si5" onclick="go(5)"><div class="si-num">5</div><div class="si-body"><div class="si-lbl">Этап 5</div><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">👤</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">Описание деятельности</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">💬</div><div><div class="hero-tag">Этап 1 из 5 · В процессе</div><div class="hero-h">Знакомство</div><div class="hero-d">Расскажите Елене о вашем бизнесе — текстом или голосом</div></div></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>
|
||
|
||
<!-- Этап 2 — Диагностика -->
|
||
<div class="sv" id="sv2">
|
||
<div class="hero"><div class="hero-ic">🔍</div><div><div class="hero-tag">Этап 2 из 5</div><div class="hero-h">Диагностика</div><div class="hero-d">Елена уточняет детали — продолжайте разговор</div></div></div>
|
||
<div class="scroll"><div class="pad"><div class="run-card"><div class="run-ic">🔍</div><div class="run-t">Диагностика идёт в чате</div><div class="run-d">Елена задаёт уточняющие вопросы прямо в интервью (Этап 1). Когда данных достаточно — переходите к Анализу.</div><button class="btn btn-p" onclick="go(1)">← Вернуться к интервью</button></div></div></div>
|
||
</div>
|
||
|
||
<!-- Этап 3 — Документы -->
|
||
<div class="sv" id="sv3">
|
||
<div class="hero"><div class="hero-ic">📁</div><div><div class="hero-tag">Этап 3 из 5</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">📎</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">🧠</div><div><div class="hero-tag">Этап 4 из 5</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">🚀</div><div><div class="hero-tag">Этап 5 из 5</div><div class="hero-h">План — ТЗ на программу</div><div class="hero-d">Проект системы для вашего бизнеса</div></div></div>
|
||
<div class="scroll"><div class="pad" id="specPad"></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:20,2:40,3:60,4:80,5:100};
|
||
function esc(s){return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}
|
||
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
|
||
|
||
function go(n){
|
||
if(document.getElementById('si'+n).classList.contains('locked'))return;
|
||
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===3)renderDocs();
|
||
if(n===4)renderAnalysis();
|
||
if(n===5)renderSpecPane();
|
||
}
|
||
|
||
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;
|
||
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;inp.value=(base+intr).trim();inp.style.height="auto";inp.style.height=Math.min(inp.scrollHeight,120)+"px"};
|
||
recog.onstart=()=>{base=inp.value?inp.value+" ":""};
|
||
recog.onend=()=>{if(recording){try{recog.start()}catch(e){}}};
|
||
recog.onerror=e=>{if(e.error==="not-allowed"){alert("Разрешите доступ к микрофону");stopMic()}};
|
||
}
|
||
function toggleMic(){if(!recog){alert("Голосовой ввод работает в Chrome");return}recording?stopMic():startMic()}
|
||
function startMic(){recording=true;document.getElementById("micBtn").classList.add("rec");document.getElementById("micHint").classList.add("show");try{recog.start()}catch(e){}}
|
||
function stopMic(){recording=false;document.getElementById("micBtn").classList.remove("rec");document.getElementById("micHint").classList.remove("show");try{recog.stop()}catch(e){}inp.focus()}
|
||
|
||
/* ── Chat ── */
|
||
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);chat.scrollTop=chat.scrollHeight}
|
||
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);chat.scrollTop=chat.scrollHeight}
|
||
function hideTyping(){const t=document.getElementById("typing");if(t)t.remove()}
|
||
|
||
async function init(){
|
||
// Telegram Mini App: развернуть на весь экран + токен из start_param если нет в URL
|
||
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);}}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 r=await fetch(`${API}/api/project/new`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});
|
||
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=>`<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="font-size:18px">📄</span><div style="flex:1"><div style="font-size:13px;font-weight:600">${esc(d.filename)}</div><div style="font-size:11px;color:#9ca3af">${(d.size/1024).toFixed(0)} КБ · учтён в анализе</div></div><span style="color:var(--primary);font-weight:700;font-size:13px">✓</span></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=`❌ ${esc(f.name)}: ${d.error}`;continue}
|
||
state.documents=state.documents||[];state.documents.push({filename:d.filename,size:d.size});
|
||
}catch(e){tmp.innerHTML=`❌ ${esc(f.name)}: ${e.message}`;continue}
|
||
}
|
||
renderDocs();
|
||
}
|
||
function renderAll(){
|
||
if(state.client_name)document.getElementById("hdrClient").textContent="· "+state.client_name;
|
||
chat.innerHTML="";
|
||
state.messages.forEach(m=>addMsg(m.role==="user"?"user":"elena",m.content));
|
||
unlockStages();
|
||
checkPayment();
|
||
}
|
||
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;
|
||
[2,3,4,5].forEach(n=>{const si=document.getElementById('si'+n);if(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')">📊 Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">🔧 Функции</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","📊","Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return}
|
||
c.innerHTML=renderCanvas(state.canvas);
|
||
}else{
|
||
if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return}
|
||
c.innerHTML=renderIdef(state.model);
|
||
}
|
||
}
|
||
function renderSpecPane(){
|
||
const pad=document.getElementById("specPad");
|
||
if(!state.spec){
|
||
if(!state.model){pad.innerHTML=runCard(null,"⚠️","Сначала Анализ","ТЗ собирается из функциональной модели. Перейдите на Этап 4 и постройте модель.","← К анализу",()=>go(4));return}
|
||
pad.innerHTML=runCard("spec","📋","Техническое задание","Из модели бизнеса Елена спроектирует программу: роли, модули, экраны, данные.","Собрать ТЗ →");return}
|
||
pad.innerHTML=renderSpec(state.spec)+renderExportBar();
|
||
}
|
||
function renderExportBar(){
|
||
if(state.unlocked){
|
||
return `<div class="exp-bar"><div class="exp-h">📦 Готовые документы</div>
|
||
<div class="exp-d">Долг закрыт — документы и ТЗ доступны для печати и выгрузки.</div>
|
||
<div class="exp-btns">
|
||
<button class="btn btn-p" onclick="printDoc()">🖨 Печать / PDF</button>
|
||
<button class="btn btn-s" onclick="downloadTZ()">⬇ Скачать ТЗ</button>
|
||
<button class="btn btn-s" onclick="exportDev()">{ } Выгрузить для разработчика</button>
|
||
</div></div>`;
|
||
}
|
||
const debt=state.debt||0, deal=state.deal_amount||0;
|
||
const money=deal>0?`Сумма: ${deal.toLocaleString('ru')} ₽${debt>0?` · к оплате: <b>${debt.toLocaleString('ru')} ₽</b>`:''}`:'';
|
||
return `<div class="exp-bar locked">
|
||
<div class="exp-h">🔒 Документы готовы — доступны после оплаты</div>
|
||
<div class="exp-d">Вы видите полный результат на экране. Печатный документ и выгрузка ТЗ для разработчика открываются после закрытия оплаты.<br>${money}</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((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,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"]};
|
||
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"});
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|