brand(crm): пиктограммы по брендбуку (Lucide stroke 1.75) — навигация, вкладки, заголовки

This commit is contained in:
wasrusgen 2026-06-03 16:48:56 +03:00
parent c003faf931
commit d704bd84ef

View File

@ -168,8 +168,8 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
<div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div> <div class="sb-backdrop" id="sbBackdrop" onclick="toggleSb()"></div>
<aside class="sb" id="sbNav"> <aside class="sb" id="sbNav">
<div class="sb-nav"> <div class="sb-nav">
<div class="nav-item active" id="nav-dash" onclick="setView('dashboard')"><span class="ic">📊</span> Дашборд</div> <div class="nav-item active" id="nav-dash" onclick="setView('dashboard')"><span class="ic"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg></span> Дашборд</div>
<div class="nav-item" id="nav-pipe" onclick="setView('pipeline')"><span class="ic">🎯</span> Воронка</div> <div class="nav-item" id="nav-pipe" onclick="setView('pipeline')"><span class="ic"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg></span> Воронка</div>
</div> </div>
<button class="sb-new" onclick="newClient()">+ Новый клиент</button> <button class="sb-new" onclick="newClient()">+ Новый клиент</button>
<div class="sb-cap">Клиенты</div> <div class="sb-cap">Клиенты</div>
@ -186,6 +186,35 @@ const pipeMap=Object.fromEntries(PIPE.map(p=>[p[0],p]));
function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")} function esc(s){return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")} function fmt(s){return esc(s).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>")}
function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"} function money(n){return (n||0).toLocaleString("ru-RU")+" ₽"}
/* ── Иконки по брендбуку: 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"/>',
trend:'<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>',
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"/>',
wallet:'<path d="M20 12V8H6a2 2 0 0 1-2-2c0-1.1.9-2 2-2h12v4"/><path d="M4 6v12c0 1.1.9 2 2 2h14v-4"/><path d="M18 12a2 2 0 0 0 0 4h4v-4z"/>',
contact:'<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/>',
pin:'<line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/>',
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"/>',
target:'<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
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"/>',
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"/>',
funnel:'<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
dashboard:'<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>',
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"/>',
screen:'<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
send:'<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>'
};
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 o=sb.classList.toggle('open');bd.classList.toggle('show',o);} function toggleSb(){const sb=document.getElementById('sbNav'),bd=document.getElementById('sbBackdrop');const o=sb.classList.toggle('open');bd.classList.toggle('show',o);}
function closeSb(){const sb=document.getElementById('sbNav'),bd=document.getElementById('sbBackdrop');if(sb)sb.classList.remove('open');if(bd)bd.classList.remove('show');} function closeSb(){const sb=document.getElementById('sbNav'),bd=document.getElementById('sbBackdrop');if(sb)sb.classList.remove('open');if(bd)bd.classList.remove('show');}
@ -460,7 +489,7 @@ function renderUpcomingTasks(){
else if(f==='today')list=all.filter(t=>t.due===today); else if(f==='today')list=all.filter(t=>t.due===today);
const overdueN=all.filter(t=>t.due&&t.due<today).length; const overdueN=all.filter(t=>t.due&&t.due<today).length;
const filterChips=chips(f,[['all','Все'],['today','Сегодня'],['overdue',`Просроченные${overdueN?' '+overdueN:''}`]],'setTaskFilter'); const filterChips=chips(f,[['all','Все'],['today','Сегодня'],['overdue',`Просроченные${overdueN?' '+overdueN:''}`]],'setTaskFilter');
const head=secHead('tasks',`📌 Задачи · ${all.length}`,filterChips); const head=secHead('tasks',`${ic('pin',15)} Задачи · ${all.length}`,filterChips);
if(ui.collapsed.tasks)return head; if(ui.collapsed.tasks)return head;
const top=list.slice(0,10); const top=list.slice(0,10);
if(!top.length)return head+'<div class="tbl"><div class="tbl-row" style="color:#9CA3AF;font-size:12px;justify-content:center">Нет задач по фильтру</div></div>'; if(!top.length)return head+'<div class="tbl"><div class="tbl-row" style="color:#9CA3AF;font-size:12px;justify-content:center">Нет задач по фильтру</div></div>';
@ -507,7 +536,7 @@ async function kDrop(e,pipe){
await loadProjects();renderPipeline(); await loadProjects();renderPipeline();
} }
const MAINTABS=[{id:"deal",name:"Сделка",icon:"📇"},{id:"chat",name:"Чат с клиентом",icon:"💬"},{id:"pricing",name:"Ценообразование",icon:"💰"},{id:"payments",name:"Платежи",icon:"💳"},{id:"tasks",name:"Задачи",icon:"📌"},{id:"docs",name:"Документы",icon:"📎"},{id:"analysis",name:"Анализ",icon:"📊"},{id:"deviations",name:"Отклонения",icon:"⚠️"},{id:"suggestions",name:"Предложения",icon:"💡"}]; const MAINTABS=[{id:"deal",name:"Сделка",icon:ic('user',15)},{id:"chat",name:"Чат с клиентом",icon:ic('message',15)},{id:"pricing",name:"Ценообразование",icon:ic('wallet',15)},{id:"payments",name:"Платежи",icon:ic('card',15)},{id:"tasks",name:"Задачи",icon:ic('pin',15)},{id:"docs",name:"Документы",icon:ic('paperclip',15)},{id:"analysis",name:"Анализ",icon:ic('chart',15)},{id:"deviations",name:"Отклонения",icon:ic('alert',15)},{id:"suggestions",name:"Предложения",icon:ic('idea',15)}];
function renderClient(){ function renderClient(){
const crm=state.crm||{pipeline:"lead",deal_amount:0,paid_amount:0,contact:"",source:"",note:""}; const crm=state.crm||{pipeline:"lead",deal_amount:0,paid_amount:0,contact:"",source:"",note:""};
const billing=crm.billing_type||"paid"; const billing=crm.billing_type||"paid";
@ -533,10 +562,10 @@ function renderDocsTab(){
const docs=state.documents||[]; const docs=state.documents||[];
let h=`<div onclick="document.getElementById('opFile').click()" ondragover="event.preventDefault();this.style.background='#ECFDF5'" ondragleave="this.style.background='#fff'" ondrop="opDropFiles(event)" style="border:2px dashed var(--border);border-radius:12px;padding:20px;text-align:center;cursor:pointer;margin-bottom:14px;background:#fff;transition:background .15s"> let h=`<div onclick="document.getElementById('opFile').click()" ondragover="event.preventDefault();this.style.background='#ECFDF5'" ondragleave="this.style.background='#fff'" ondrop="opDropFiles(event)" style="border:2px dashed var(--border);border-radius:12px;padding:20px;text-align:center;cursor:pointer;margin-bottom:14px;background:#fff;transition:background .15s">
<input type="file" id="opFile" multiple style="display:none" onchange="opUpload(this.files)"> <input type="file" id="opFile" multiple style="display:none" onchange="opUpload(this.files)">
<div style="font-size:24px">📎</div><div style="font-size:13px;font-weight:700;color:#1A1A2E">Загрузить документ клиента</div> <div style="color:#94A3B8;display:flex;justify-content:center;margin-bottom:4px">${ic('paperclip',26)}</div><div style="font-size:13px;font-weight:700;color:#1A1A2E">Загрузить документ клиента</div>
<div style="font-size:11px;color:#9ca3af;margin-top:2px">PDF · Word · Excel · txt — перетащите или нажмите. Елена учтёт в анализе.</div></div>`; <div style="font-size:11px;color:#9ca3af;margin-top:2px">PDF · Word · Excel · txt — перетащите или нажмите. Елена учтёт в анализе.</div></div>`;
if(!docs.length)h+=`<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:8px">Документов пока нет</div>`; if(!docs.length)h+=`<div style="text-align:center;color:#cbd5e1;font-size:13px;padding:8px">Документов пока нет</div>`;
else h+=docs.map(d=>{const u=`${API}/api/doc?token=${encodeURIComponent(current)}&name=${encodeURIComponent(d.filename)}`;return `<div class="blk" style="display:flex;align-items:center;gap:10px;padding:11px 14px;margin-bottom:8px"><span style="font-size:18px">📄</span><div style="flex:1;min-width:0"><a href="${u}" target="_blank" rel="noopener" style="font-size:13px;font-weight:600;color:#047857;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;font-size:17px"></a></div>`}).join(''); else h+=docs.map(d=>{const u=`${API}/api/doc?token=${encodeURIComponent(current)}&name=${encodeURIComponent(d.filename)}`;return `<div class="blk" style="display:flex;align-items:center;gap: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:#047857;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('');
return h; return h;
} }
async function opUpload(files){ async function opUpload(files){
@ -653,7 +682,7 @@ function renderMainPanel(){
</div> </div>
<div style="display:flex;flex-direction:column;gap:14px"> <div style="display:flex;flex-direction:column;gap:14px">
<div class="blk" style="margin:0"> <div class="blk" style="margin:0">
<div style="font-size:13px;font-weight:700;margin-bottom:8px">💰 Финансы</div> <div style="font-size:13px;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:7px">${ic('wallet',16)} Финансы</div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px"><span style="color:var(--muted)">Смета</span><b>${deal>0?money(deal):'—'}</b></div> <div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px"><span style="color:var(--muted)">Смета</span><b>${deal>0?money(deal):'—'}</b></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Получено</span><b style="color:#047857">${money(paid)}</b></div> <div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Получено</span><b style="color:#047857">${money(paid)}</b></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Остаток</span><b style="color:${left>0?'#DC2626':'#047857'}">${money(left)}</b></div> <div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-top:1px solid var(--bg)"><span style="color:var(--muted)">Остаток</span><b style="color:${left>0?'#DC2626':'#047857'}">${money(left)}</b></div>
@ -710,12 +739,12 @@ async function setBilling(t){
// Этапы = 5 этапов анализа. Отмечаем оплату по мере прохождения. // Этапы = 5 этапов анализа. Отмечаем оплату по мере прохождения.
// Правило: ТЗ (печать + выгрузка) только после 100% оплаты. // Правило: ТЗ (печать + выгрузка) только после 100% оплаты.
const CLIENT_STAGES=[ const CLIENT_STAGES=[
{key:"interview", name:"Интервью", icon:"💬", desc:"Диагностика «как есть» — карта проблем"}, {key:"interview", name:"Интервью", icon:ic('message',15), desc:"Диагностика «как есть» — карта проблем"},
{key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"}, {key:"methods", name:"Методологии", icon:ic('target',15), desc:"Подбор фреймворков под вашу задачу"},
{key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"}, {key:"canvas", name:"Стратегия", icon:ic('chart',15), desc:"Целевая модель процессов (TO-BE)"},
{key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"}, {key:"idef0", name:"Функции IDEF0", icon:ic('process',15), desc:"Декомпозиция функций + регламенты"},
{key:"org", name:"Организация", icon:"🏢", desc:"Оргструктура + должностные инструкции"}, {key:"org", name:"Организация", icon:ic('team',15), desc:"Оргструктура + должностные инструкции"},
{key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"}, {key:"spec", name:"Выдача ТЗ", icon:ic('clipboard',15), desc:"Готовое ТЗ к внедрению"},
]; ];
// ── Смета: базовые ставки модулей (интервью = вход 0₽) ── // ── Смета: базовые ставки модулей (интервью = вход 0₽) ──
const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, org:10000, spec:5000}; const STAGE_BASE={interview:0, methods:8000, canvas:12000, idef0:15000, org:10000, spec:5000};
@ -910,7 +939,7 @@ function renderPaymentPlan(){
if(!sp){ if(!sp){
const cx=getComplexity(); const cx=getComplexity();
box.innerHTML=`<div class="blk" style="margin-bottom:14px"> box.innerHTML=`<div class="blk" style="margin-bottom:14px">
<div style="font-size:13px;font-weight:700;margin-bottom:6px">📋 Смета проекта</div> <div style="font-size:13px;font-weight:700;margin-bottom:6px">${ic('receipt',16)} Смета проекта</div>
<div style="font-size:12px;color:var(--muted);line-height:1.5;margin-bottom:14px">После диагностики сформируйте смету. Клиент увидит, <b>за что и сколько</b> платит. Интервью — бесплатный вход, дальше оплата помодульно. ТЗ выдаётся после полной оплаты.</div> <div style="font-size:12px;color:var(--muted);line-height:1.5;margin-bottom:14px">После диагностики сформируйте смету. Клиент увидит, <b>за что и сколько</b> платит. Интервью — бесплатный вход, дальше оплата помодульно. ТЗ выдаётся после полной оплаты.</div>
<div style="font-size:12px;font-weight:600;margin-bottom:8px">Сложность проекта (из интервью):</div> <div style="font-size:12px;font-weight:600;margin-bottom:8px">Сложность проекта (из интервью):</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px"> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
@ -932,7 +961,7 @@ function renderPaymentPlan(){
// первый ещё не оплаченный платный выполненный модуль — «доступен к оплате» // первый ещё не оплаченный платный выполненный модуль — «доступен к оплате»
box.innerHTML=`<div class="blk" style="margin-bottom:14px"> box.innerHTML=`<div class="blk" style="margin-bottom:14px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;flex-wrap:wrap">
<span style="font-size:13px;font-weight:700">📋 Смета проекта</span> <span style="font-size:13px;font-weight:700">${ic('receipt',16)} Смета проекта</span>
<span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span> <span style="font-size:11px;font-weight:700;color:var(--primary);background:#ECFDF5;padding:3px 9px;border-radius:6px">${CX_SHORT[cx]}</span>
<button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button> <button class="cp-btn cp-r" style="padding:4px 9px;margin-left:auto" onclick="buildEstimate()">↻ Пересчитать</button>
</div> </div>
@ -997,12 +1026,12 @@ function renderPricing(){
const billing=(state.crm||{}).billing_type||"paid"; const billing=(state.crm||{}).billing_type||"paid";
const freeNote=billing==="free"?`<div class="blk" style="background:#EEF2FF;border-color:#C7D2FE;padding:12px 14px;margin-bottom:14px;font-size:13px;line-height:1.5"><b style="color:#6366F1">🎁 Бесплатный клиент.</b> Расчёт цены ниже — справочно: показывает рыночную стоимость и аргументацию ценности. Решение об оплате остаётся на ваше усмотрение.</div>`:''; const freeNote=billing==="free"?`<div class="blk" style="background:#EEF2FF;border-color:#C7D2FE;padding:12px 14px;margin-bottom:14px;font-size:13px;line-height:1.5"><b style="color:#6366F1">🎁 Бесплатный клиент.</b> Расчёт цены ниже — справочно: показывает рыночную стоимость и аргументацию ценности. Решение об оплате остаётся на ваше усмотрение.</div>`:'';
const p=state.pricing; const p=state.pricing;
if(!p){box.innerHTML=freeNote+`<div class="run-card" style="margin:0 0 18px"><div class="run-ic">💰</div><div class="run-t">Ценовое предложение</div><div class="run-d">Елена оценит масштаб работы, проанализирует рынок и предложит пакеты с аргументами цены.</div><button class="run-btn" id="rb-pricing" onclick="buildPricing()">Рассчитать цену →</button></div>`;return} if(!p){box.innerHTML=freeNote+`<div class="run-card" style="margin:0 0 18px"><div class="run-ic">${ic('wallet',30)}</div><div class="run-t">Ценовое предложение</div><div class="run-d">Елена оценит масштаб работы, проанализирует рынок и предложит пакеты с аргументами цены.</div><button class="run-btn" id="rb-pricing" onclick="buildPricing()">Рассчитать цену →</button></div>`;return}
box.innerHTML=freeNote+pricingCardHTML(p); box.innerHTML=freeNote+pricingCardHTML(p);
} }
function pricingCardHTML(p){ function pricingCardHTML(p){
const SZ={micro:"Микро",small:"Малый",medium:"Средний",large:"Крупный"};const CX={low:"низкая",medium:"средняя",high:"высокая"}; const SZ={micro:"Микро",small:"Малый",medium:"Средний",large:"Крупный"};const CX={low:"низкая",medium:"средняя",high:"высокая"};
return `<div class="blk" style="margin-bottom:14px"><div style="display:flex;align-items:center;margin-bottom:10px"><b style="font-size:14px">💰 Ценовое предложение</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 10px" onclick="buildPricing()">↻ Пересчитать</button></div> return `<div class="blk" style="margin-bottom:14px"><div style="display:flex;align-items:center;margin-bottom:10px"><b style="font-size:14px;display:inline-flex;align-items:center;gap:7px">${ic('wallet',17)} Ценовое предложение</b><button class="cp-btn cp-r" style="margin-left:auto;padding:5px 10px" onclick="buildPricing()">↻ Пересчитать</button></div>
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap"><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Масштаб: <b>${SZ[p.scale.size]||p.scale.size}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Сложность: <b>${CX[p.scale.complexity]||p.scale.complexity}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">${esc(p.scale.effort_estimate)}</span></div> <div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap"><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Масштаб: <b>${SZ[p.scale.size]||p.scale.size}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">Сложность: <b>${CX[p.scale.complexity]||p.scale.complexity}</b></span><span style="font-size:12px;background:#F1F5F9;border-radius:6px;padding:4px 10px">${esc(p.scale.effort_estimate)}</span></div>
<div style="font-size:12px;color:var(--muted);margin-bottom:14px;padding:10px;background:var(--bg);border-radius:8px"><b>Рынок:</b> ${esc(p.market)}</div> <div style="font-size:12px;color:var(--muted);margin-bottom:14px;padding:10px;background:var(--bg);border-radius:8px"><b>Рынок:</b> ${esc(p.market)}</div>
<div style="display:grid;grid-template-columns:repeat(${p.packages.length},1fr);gap:10px">${p.packages.map((pk,i)=>`<div style="border:1.5px solid ${p.recommended&&p.recommended.indexOf(pk.name)>=0?'var(--primary)':'var(--border)'};border-radius:12px;padding:14px;position:relative">${p.recommended&&p.recommended.indexOf(pk.name)>=0?'<span style="position:absolute;top:-9px;left:14px;background:var(--primary);color:#fff;font-size:9px;font-weight:800;padding:2px 8px;border-radius:10px">РЕКОМЕНДУЮ</span>':''}<div style="font-size:14px;font-weight:800;font-family:Montserrat;margin-bottom:4px">${esc(pk.name)}</div><div style="font-size:22px;font-weight:800;color:var(--primary);margin-bottom:2px">${money(pk.price)}</div><div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${esc(pk.duration)}</div><div style="margin-bottom:8px">${pk.scope.map(s=>`<div style="font-size:11.5px;color:#374151;padding-left:14px;position:relative;margin-bottom:3px;line-height:1.35"><span style="position:absolute;left:2px;color:var(--mid)"></span>${esc(s)}</div>`).join("")}</div><div style="font-size:11px;color:#6b7280;font-style:italic;border-top:1px solid var(--bg);padding-top:6px">${esc(pk.argument)}</div><button class="cp-btn cp-a" style="width:100%;margin-top:8px" onclick="applyPrice(${pk.price})">Выбрать → ${money(pk.price)}</button></div>`).join("")}</div> <div style="display:grid;grid-template-columns:repeat(${p.packages.length},1fr);gap:10px">${p.packages.map((pk,i)=>`<div style="border:1.5px solid ${p.recommended&&p.recommended.indexOf(pk.name)>=0?'var(--primary)':'var(--border)'};border-radius:12px;padding:14px;position:relative">${p.recommended&&p.recommended.indexOf(pk.name)>=0?'<span style="position:absolute;top:-9px;left:14px;background:var(--primary);color:#fff;font-size:9px;font-weight:800;padding:2px 8px;border-radius:10px">РЕКОМЕНДУЮ</span>':''}<div style="font-size:14px;font-weight:800;font-family:Montserrat;margin-bottom:4px">${esc(pk.name)}</div><div style="font-size:22px;font-weight:800;color:var(--primary);margin-bottom:2px">${money(pk.price)}</div><div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${esc(pk.duration)}</div><div style="margin-bottom:8px">${pk.scope.map(s=>`<div style="font-size:11.5px;color:#374151;padding-left:14px;position:relative;margin-bottom:3px;line-height:1.35"><span style="position:absolute;left:2px;color:var(--mid)"></span>${esc(s)}</div>`).join("")}</div><div style="font-size:11px;color:#6b7280;font-style:italic;border-top:1px solid var(--bg);padding-top:6px">${esc(pk.argument)}</div><button class="cp-btn cp-a" style="width:100%;margin-top:8px" onclick="applyPrice(${pk.price})">Выбрать → ${money(pk.price)}</button></div>`).join("")}</div>
@ -1031,7 +1060,7 @@ function renderPayments(){
if(sb)sb.innerHTML=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`; if(sb)sb.innerHTML=`<div class="cc-fl">Оплата</div><div style="display:flex;align-items:center;gap:8px"><span style="font-size:14px;font-weight:700;color:${st[1]}">${money(paid)}</span>${deal>0?`<span style="font-size:11px;font-weight:700;color:${st[1]};background:${st[2]};padding:2px 8px;border-radius:6px">${st[0]}</span>`:''}</div>`;
const box=document.getElementById("paymentsBox");if(!box)return; const box=document.getElementById("paymentsBox");if(!box)return;
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px"> box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
<div style="display:flex;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px"><span style="font-size:13px;font-weight:700">💰 Платежи</span> <div style="display:flex;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px"><span style="font-size:13px;font-weight:700;display:inline-flex;align-items:center;gap:7px">${ic('card',16)} Платежи</span>
${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'} ${deal>0?`<span style="margin-left:auto;font-size:12px;color:var(--muted)">Сделка ${money(deal)} · Получено ${money(paid)} · <b style="color:${left>0?'#DC2626':'#047857'}">Остаток ${money(left)}</b></span>`:'<span style="margin-left:auto"></span>'}
${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''} ${left>0?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="remindBalance(${left})">⏰ Задача на остаток</button>`:''}
${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div> ${pays.length?`<button class="cp-btn cp-r" style="padding:5px 10px" onclick="exportPayments()">⬇ CSV</button>`:''}</div>
@ -1123,7 +1152,7 @@ function renderTasks(){
const tasks=state.tasks||[]; const tasks=state.tasks||[];
const today=new Date().toISOString().slice(0,10); const today=new Date().toISOString().slice(0,10);
box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px"> box.innerHTML=`<div style="background:var(--white);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:18px">
<div style="font-size:13px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px">📌 Задачи по клиенту</div> <div style="font-size:13px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px">${ic('pin',16)} Задачи по клиенту</div>
${tasks.map((t,i)=>`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><input type="checkbox" ${t.done?'checked':''} onchange="toggleTask(${i})" style="width:16px;height:16px;cursor:pointer"><span style="flex:1;font-size:13px;${t.done?'text-decoration:line-through;color:#9ca3af':''}">${esc(t.text)}</span>${t.due?`<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;${!t.done&&t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':fmtDate(t.due)}</span>`:''}<button onclick="delTask(${i})" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px"></button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Задач нет</div>'} ${tasks.map((t,i)=>`<div style="display:flex;align-items:center;gap:10px;padding:7px 0;border-top:1px solid var(--bg)"><input type="checkbox" ${t.done?'checked':''} onchange="toggleTask(${i})" style="width:16px;height:16px;cursor:pointer"><span style="flex:1;font-size:13px;${t.done?'text-decoration:line-through;color:#9ca3af':''}">${esc(t.text)}</span>${t.due?`<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;${!t.done&&t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':fmtDate(t.due)}</span>`:''}<button onclick="delTask(${i})" style="border:none;background:none;cursor:pointer;color:#cbd5e1;font-size:14px"></button></div>`).join("")||'<div style="font-size:12px;color:#cbd5e1;padding:4px">Задач нет</div>'}
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap"><input id="newTask" placeholder="Новая задача… или 🎤" style="flex:1;min-width:140px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter;outline:none" onkeydown="if(event.key==='Enter')addTask()">${micBtn('newTask',34)}<input id="newTaskDue" type="date" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" onclick="addTask()">+ Добавить</button></div> <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap"><input id="newTask" placeholder="Новая задача… или 🎤" style="flex:1;min-width:140px;border:1.5px solid var(--border);border-radius:8px;padding:8px 11px;font-size:13px;font-family:Inter;outline:none" onkeydown="if(event.key==='Enter')addTask()">${micBtn('newTask',34)}<input id="newTaskDue" type="date" style="border:1.5px solid var(--border);border-radius:8px;padding:8px;font-size:13px;font-family:Inter"><button class="cp-btn cp-a" onclick="addTask()">+ Добавить</button></div>
</div>`; </div>`;
@ -1233,7 +1262,7 @@ function renderJobs(j){
</div>`).join(''); </div>`).join('');
} }
const TABS=[{id:"interview",name:"Интервью",icon:"💬"},{id:"methods",name:"Методологии",icon:"🎯"},{id:"canvas",name:"Стратегия",icon:"📊"},{id:"idef0",name:"Функции",icon:"🔧"},{id:"org",name:"Организация",icon:"🏢"},{id:"spec",name:"ТЗ",icon:"📋"}]; const TABS=[{id:"interview",name:"Интервью",icon:ic('message',15)},{id:"methods",name:"Методологии",icon:ic('target',15)},{id:"canvas",name:"Стратегия",icon:ic('chart',15)},{id:"idef0",name:"Функции",icon:ic('process',15)},{id:"org",name:"Организация",icon:ic('team',15)},{id:"spec",name:"ТЗ",icon:ic('clipboard',15)}];
function approved(s){return state.approvals&&state.approvals[s]} function approved(s){return state.approvals&&state.approvals[s]}
async function approve(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:true})});state.approvals=state.approvals||{};state.approvals[s]=1;renderClient();} async function approve(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:true})});state.approvals=state.approvals||{};state.approvals[s]=1;renderClient();}
async function unapprove(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:false})});if(state.approvals)delete state.approvals[s];renderClient();} async function unapprove(s){await fetch(`${API}/api/project/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:current,stage:s,approved:false})});if(state.approvals)delete state.approvals[s];renderClient();}
@ -1244,20 +1273,20 @@ function runCard(s,ic,t,d,b){return `<div class="run-card"><div class="run-ic">$
function renderTab(){const c=document.getElementById("tabContent"); function renderTab(){const c=document.getElementById("tabContent");
if(activeTab==="interview"){c.innerHTML=`<div style="max-width:720px">${state.messages.map(m=>`<div style="display:flex;gap:9px;margin-bottom:12px;${m.role==='user'?'flex-direction:row-reverse':''}"><div class="cl-av" style="border-radius:50%;background:${m.role==='user'?'#6366F1':'#047857'}">${m.role==='user'?'К':'Е'}</div><div style="padding:10px 14px;border-radius:13px;font-size:13px;line-height:1.5;max-width:78%;white-space:pre-wrap;${m.role==='user'?'background:#047857;color:#fff':'background:#fff;border:1px solid #E5E7EB'}">${fmt(m.content)}</div></div>`).join("")||'<div class="empty" style="height:200px">Интервью не начато</div>'}</div>`;} if(activeTab==="interview"){c.innerHTML=`<div style="max-width:720px">${state.messages.map(m=>`<div style="display:flex;gap:9px;margin-bottom:12px;${m.role==='user'?'flex-direction:row-reverse':''}"><div class="cl-av" style="border-radius:50%;background:${m.role==='user'?'#6366F1':'#047857'}">${m.role==='user'?'К':'Е'}</div><div style="padding:10px 14px;border-radius:13px;font-size:13px;line-height:1.5;max-width:78%;white-space:pre-wrap;${m.role==='user'?'background:#047857;color:#fff':'background:#fff;border:1px solid #E5E7EB'}">${fmt(m.content)}</div></div>`).join("")||'<div class="empty" style="height:200px">Интервью не начато</div>'}</div>`;}
else if(activeTab==="methods"){if(!state.selection){c.innerHTML=runCard("methods","🎯","Подбор методологий","Елена предложит набор методологий под тип бизнеса.","Подобрать →");return}const s=state.selection;c.innerHTML=`<div class="blk"><div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Тип бизнеса</div><div style="font-size:16px;font-weight:700;margin-bottom:12px">${esc(s.business_type)}</div>${s.recommended.map(r=>`<div style="display:flex;gap:10px;padding:8px 0;border-top:1px solid #f1f5f9"><span style="font-size:16px">${r.use?'✅':'⬜'}</span><div style="flex:1"><b>${r.method.toUpperCase()}</b> <span style="font-size:11px;color:#9ca3af">[${r.depth}]</span><div style="font-size:12px;color:var(--muted)">${esc(r.reason)}</div></div></div>`).join("")}<div style="margin-top:12px;padding:10px;background:var(--light);border-radius:8px;font-size:13px">${esc(s.rationale)}</div></div>${cpBar("methods","Согласны с набором?")}`;} else if(activeTab==="methods"){if(!state.selection){c.innerHTML=runCard("methods","🎯","Подбор методологий","Елена предложит набор методологий под тип бизнеса.","Подобрать →");return}const s=state.selection;c.innerHTML=`<div class="blk"><div style="font-size:10px;font-weight:700;text-transform:uppercase;color:#9ca3af;margin-bottom:4px">Тип бизнеса</div><div style="font-size:16px;font-weight:700;margin-bottom:12px">${esc(s.business_type)}</div>${s.recommended.map(r=>`<div style="display:flex;gap:10px;padding:8px 0;border-top:1px solid #f1f5f9"><span style="font-size:16px">${r.use?'✅':'⬜'}</span><div style="flex:1"><b>${r.method.toUpperCase()}</b> <span style="font-size:11px;color:#9ca3af">[${r.depth}]</span><div style="font-size:12px;color:var(--muted)">${esc(r.reason)}</div></div></div>`).join("")}<div style="margin-top:12px;padding:10px;background:var(--light);border-radius:8px;font-size:13px">${esc(s.rationale)}</div></div>${cpBar("methods","Согласны с набором?")}`;}
else if(activeTab==="canvas"){if(!state.canvas){c.innerHTML=runCard("canvas","📊","Business Model Canvas","Стратегия — 9 блоков.","Построить →");return}c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегия верна?");} else if(activeTab==="canvas"){if(!state.canvas){c.innerHTML=runCard("canvas",ic('chart',30),"Business Model Canvas","Стратегия — 9 блоков.","Построить →");return}c.innerHTML=renderCanvas(state.canvas)+cpBar("canvas","Стратегия верна?");}
else if(activeTab==="idef0"){if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель IDEF0","Функции, входы/выходы, нормы, разрывы.","Построить →");return}c.innerHTML=renderIdef(state.model)+cpBar("idef0","Модель верна?");} else if(activeTab==="idef0"){if(!state.model){c.innerHTML=runCard("model",ic('process',30),"Функциональная модель IDEF0","Функции, входы/выходы, нормы, разрывы.","Построить →");return}c.innerHTML=renderIdef(state.model)+cpBar("idef0","Модель верна?");}
else if(activeTab==="org"){ else if(activeTab==="org"){
if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">⚠️</div><div class="run-t">Сначала IDEF0</div><div class="run-d">Оргструктура строится из функциональной модели.</div></div>`;return} if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">${ic('alert',30)}</div><div class="run-t">Сначала IDEF0</div><div class="run-d">Оргструктура строится из функциональной модели.</div></div>`;return}
let h=''; let h='';
if(!state.orgchart)h+=runCard("orgchart","🏢","Целевая оргструктура","Из модели: роли, штат, подчинённость, узкие места, конфликты интересов.","Построить оргструктуру →"); if(!state.orgchart)h+=runCard("orgchart",ic('team',30),"Целевая оргструктура","Из модели: роли, штат, подчинённость, узкие места, конфликты интересов.","Построить оргструктуру →");
else h+=renderOrgChart(state.orgchart)+`<div style="text-align:right;margin:8px 0 16px"><button class="run-btn" id="rb-orgchart" onclick="rerun('orgchart')" style="font-size:12px">↻ Пересобрать оргструктуру</button></div>`; else h+=renderOrgChart(state.orgchart)+`<div style="text-align:right;margin:8px 0 16px"><button class="run-btn" id="rb-orgchart" onclick="rerun('orgchart')" style="font-size:12px">↻ Пересобрать оргструктуру</button></div>`;
if(state.orgchart){ if(state.orgchart){
if(!state.jobs)h+=runCard("jobs","📋","Должностные инструкции","По ролям: ответственность, KPI, полномочия. С учётом отклонений клиента.","Собрать инструкции →"); if(!state.jobs)h+=runCard("jobs",ic('clipboard',30),"Должностные инструкции","По ролям: ответственность, KPI, полномочия. С учётом отклонений клиента.","Собрать инструкции →");
else h+=renderJobs(state.jobs)+`<div style="text-align:right;margin-top:8px"><button class="run-btn" id="rb-jobs" onclick="rerun('jobs')" style="font-size:12px">↻ Пересобрать инструкции</button></div>`; else h+=renderJobs(state.jobs)+`<div style="text-align:right;margin-top:8px"><button class="run-btn" id="rb-jobs" onclick="rerun('jobs')" style="font-size:12px">↻ Пересобрать инструкции</button></div>`;
} }
c.innerHTML=h; c.innerHTML=h;
} }
else if(activeTab==="spec"){if(!state.spec){if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">⚠️</div><div class="run-t">Сначала IDEF0</div><div class="run-d">ТЗ собирается из функциональной модели.</div></div>`;return}c.innerHTML=runCard("spec","📋","Техническое задание","Роли, модули, экраны, данные.","Собрать ТЗ →");return}c.innerHTML=renderSpecTab();} else if(activeTab==="spec"){if(!state.spec){if(!state.model){c.innerHTML=`<div class="run-card"><div class="run-ic">${ic('alert',30)}</div><div class="run-t">Сначала IDEF0</div><div class="run-d">ТЗ собирается из функциональной модели.</div></div>`;return}c.innerHTML=runCard("spec",ic('clipboard',30),"Техническое задание","Роли, модули, экраны, данные.","Собрать ТЗ →");return}c.innerHTML=renderSpecTab();}
} }
let specVariant="elena"; // elena | client — Phase 3 переключатель ТЗ let specVariant="elena"; // elena | client — Phase 3 переключатель ТЗ
function renderSpecTab(){ function renderSpecTab(){