ui: компактный дашборд — таблицы вместо карточек, степпер в строку

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-06-01 07:38:32 +03:00
parent 5b146fb7e5
commit 6828ecfeb3

View File

@ -37,12 +37,20 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0} .main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.scroll{flex:1;overflow-y:auto;padding:24px 28px} .scroll{flex:1;overflow-y:auto;padding:24px 28px}
/* Dashboard */ /* Dashboard */
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:22px} .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}
.kpi{background:var(--white);border:1.5px solid var(--border);border-radius:14px;padding:20px} .kpi{background:var(--white);border:1.5px solid var(--border);border-radius:10px;padding:13px 16px}
.kpi-v{font-family:'Montserrat';font-weight:800;font-size:30px;color:var(--ink);letter-spacing:-1px} .kpi-v{font-family:'Montserrat';font-weight:800;font-size:23px;color:var(--ink);letter-spacing:-.5px;line-height:1.1}
.kpi-l{font-size:12px;color:var(--muted);margin-top:4px} .kpi-l{font-size:11px;color:var(--muted);margin-top:3px}
.kpi-sub{font-size:11px;color:var(--primary);font-weight:600;margin-top:6px} .kpi-sub{font-size:10.5px;color:var(--primary);font-weight:600;margin-top:4px}
.sec-h{font-family:'Montserrat';font-weight:800;font-size:18px;color:var(--ink);margin-bottom:14px} .sec-h{font-family:'Montserrat';font-weight:800;font-size:15px;color:var(--ink);margin-bottom:9px;margin-top:6px}
.cl-row{display:flex;align-items:center;gap:12px;cursor:pointer;padding:10px 14px;border-top:1px solid var(--bg)}
.cl-row:first-child{border-top:none}
.cl-row:hover{background:#FAFBFC}
.mini-chip{font-size:10px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap;display:inline-block}
.tbl{background:var(--white);border:1px solid var(--border);border-radius:12px;padding:0;margin-bottom:14px;overflow:hidden}
.tbl-row{display:flex;align-items:center;gap:12px;cursor:pointer;padding:9px 14px;border-top:1px solid var(--bg)}
.tbl-row:first-child{border-top:none}
.tbl-row:hover{background:#FAFBFC}
/* Pipeline kanban */ /* Pipeline kanban */
.kanban{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;align-items:flex-start} .kanban{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;align-items:flex-start}
.kcol{flex:1;min-width:200px;background:#eef0f3;border-radius:12px;padding:10px} .kcol{flex:1;min-width:200px;background:#eef0f3;border-radius:12px;padding:10px}
@ -191,7 +199,7 @@ function renderDashboard(){
${renderRevenueChart()} ${renderRevenueChart()}
${renderUpcomingTasks()} ${renderUpcomingTasks()}
<div class="sec-h">Все клиенты · ${total}</div> <div class="sec-h">Все клиенты · ${total}</div>
${projects.map(p=>renderClientRow(p)).join("")||'<div class="empty">Создайте первого клиента</div>'}`; ${projects.length?`<div class="tbl">${projects.map(p=>renderClientRow(p)).join("")}</div>`:'<div class="empty">Создайте первого клиента</div>'}`;
} }
const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}]; const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
function clientStages(p){ function clientStages(p){
@ -216,20 +224,33 @@ function stepper(p){
return `<div style="display:flex;align-items:center;gap:0;margin-top:8px;max-width:360px">${dots}</div> return `<div style="display:flex;align-items:center;gap:0;margin-top:8px;max-width:360px">${dots}</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</div>`; <div style="font-size:11px;color:var(--muted);margin-top:4px">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</div>`;
} }
function stepperInline(p,st){
st=st||clientStages(p);
let dots="";
STAGE_DEFS.forEach((s,i)=>{
const isDone=st.done[i], isCur=!st.all&&i===st.cur;
const bg=isDone?"#047857":isCur?"#10B981":"#E5E7EB";
const ring=isCur?"box-shadow:0 0 0 2.5px rgba(16,185,129,.25);":"";
dots+=`<span title="${s.name}${isDone?' ✓':isCur?' — текущий':''}" style="width:9px;height:9px;border-radius:50%;background:${bg};${ring}flex-shrink:0;display:inline-block"></span>`;
if(i<STAGE_DEFS.length-1){const lc=st.done[i]?"#047857":"#E5E7EB";dots+=`<span style="height:2px;width:13px;background:${lc};display:inline-block"></span>`;}
});
const col=st.all?"#047857":"#10B981";
return `<div style="display:flex;align-items:center;gap:9px;margin-top:5px"><span style="display:flex;align-items:center">${dots}</span><span style="font-size:11px;color:var(--muted);white-space:nowrap">Этап: <b style="color:${col}">${st.label}</b> · ${st.cnt}/5</span></div>`;
}
function renderClientRow(p){ function renderClientRow(p){
const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"]; const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
const billing=(p.crm&&p.crm.billing_type)||"paid"; const billing=(p.crm&&p.crm.billing_type)||"paid";
const bChip=billing==="free" const bChip=billing==="free"
?`<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:2px 8px;border-radius:6px">🎁 Бесплатный</span>` ?`<span class="mini-chip" style="color:#6366F1;background:#EEF2FF">🎁 Беспл.</span>`
:`<span style="font-size:10px;font-weight:700;color:#047857;background:#ECFDF5;padding:2px 8px;border-radius:6px">💰 Платный</span>`; :`<span class="mini-chip" style="color:#047857;background:#ECFDF5">💰 Платный</span>`;
return `<div class="blk" style="display:flex;align-items:flex-start;gap:14px;cursor:pointer" onclick="openClient('${p.token}')"> return `<div class="cl-row" onclick="openClient('${p.token}')">
<div class="cl-av" style="width:36px;height:36px;border-radius:9px;font-size:14px;margin-top:2px">${esc((p.client_name||'?')[0])}</div> <div class="cl-av" style="width:32px;height:32px;border-radius:8px;font-size:13px;flex-shrink:0">${esc((p.client_name||'?')[0])}</div>
<div style="flex:1;min-width:0"> <div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-weight:700">${esc(p.client_name)}</span>${bChip}</div> <div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap"><span style="font-weight:700;font-size:13.5px">${esc(p.client_name)}</span>${bChip}<span style="font-size:11.5px;color:var(--muted)">${esc(p.niche)} · ${p.msg_count} сообщ.</span></div>
<div style="font-size:12px;color:var(--muted);margin-top:1px">${esc(p.niche)} · ${p.msg_count} сообщений</div> ${stepperInline(p)}
${stepper(p)}
</div> </div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px"><span style="font-size:11px;font-weight:700;color:${pc[2]};background:${pc[2]}1a;padding:4px 10px;border-radius:20px;white-space:nowrap">${pc[1]}</span><span style="font-weight:700;color:var(--primary)">${money((p.crm&&p.crm.deal_amount)||0)}</span></div> <span class="mini-chip" style="color:${pc[2]};background:${pc[2]}1a">${pc[1]}</span>
<span style="font-weight:800;color:var(--primary);min-width:80px;text-align:right;font-size:14px">${money((p.crm&&p.crm.deal_amount)||0)}</span>
</div>`; </div>`;
} }
@ -243,7 +264,7 @@ function renderRevenueChart(){
const lbl=k=>{const[y,m]=k.split("-");return MN[+m-1]+" "+y.slice(2)}; const lbl=k=>{const[y,m]=k.split("-");return MN[+m-1]+" "+y.slice(2)};
const totalRev=keys.reduce((s,k)=>s+months[k],0); const totalRev=keys.reduce((s,k)=>s+months[k],0);
return `<div class="sec-h">📈 Выручка по месяцам · ${money(totalRev)}</div> return `<div class="sec-h">📈 Выручка по месяцам · ${money(totalRev)}</div>
<div class="blk" style="padding:18px"><div style="display:flex;align-items:flex-end;gap:14px;height:140px">${keys.map(k=>`<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%"><div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:4px">${(months[k]/1000).toFixed(0)}к</div><div style="width:100%;max-width:48px;background:linear-gradient(180deg,#10B981,#047857);border-radius:6px 6px 0 0;height:${Math.max(6,months[k]/max*100)}%"></div><div style="font-size:11px;color:var(--muted);margin-top:6px">${lbl(k)}</div></div>`).join("")}</div></div>`; <div class="blk" style="padding:14px 16px;margin-bottom:14px"><div style="display:flex;align-items:flex-end;justify-content:flex-start;gap:18px;height:104px">${keys.map(k=>`<div style="flex:0 0 56px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%"><div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:4px">${(months[k]/1000).toFixed(0)}к</div><div style="width:100%;background:linear-gradient(180deg,#10B981,#047857);border-radius:6px 6px 0 0;height:${Math.max(8,months[k]/max*100)}%"></div><div style="font-size:11px;color:var(--muted);margin-top:6px">${lbl(k)}</div></div>`).join("")}</div></div>`;
} }
function renderUpcomingTasks(){ function renderUpcomingTasks(){
const today=new Date().toISOString().slice(0,10); const today=new Date().toISOString().slice(0,10);
@ -252,7 +273,7 @@ function renderUpcomingTasks(){
all.sort((a,b)=>(a.due||"9999")<(b.due||"9999")?-1:1); all.sort((a,b)=>(a.due||"9999")<(b.due||"9999")?-1:1);
const top=all.slice(0,6); const top=all.slice(0,6);
if(!top.length)return""; if(!top.length)return"";
return `<div class="sec-h">📌 Ближайшие задачи · ${all.length}</div>${top.map(t=>`<div class="blk" style="display:flex;align-items:center;gap:12px;cursor:pointer;padding:11px 14px" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted)">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;${t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':t.due<today?'просрочено':fmtDate(t.due)}</span>`:''}</div>`).join("")}`; return `<div class="sec-h">📌 Ближайшие задачи · ${all.length}</div><div class="tbl">${top.map(t=>`<div class="tbl-row" onclick="openClient('${t.token}')"><span style="font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.text)}</span><span style="font-size:12px;color:var(--muted);white-space:nowrap">${esc(t.client)}</span>${t.due?`<span style="font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;${t.due<today?'background:#FEF2F2;color:#DC2626':t.due===today?'background:#FEF3C7;color:#92400E':'background:#F1F5F9;color:#6B7280'}">${t.due===today?'сегодня':t.due<today?'просрочено':fmtDate(t.due)}</span>`:''}</div>`).join("")}</div>`;
} }
function renderPipeline(){ function renderPipeline(){
document.getElementById("view").innerHTML=`<div class="sec-h">Воронка продаж</div><div class="kanban">${PIPE.map(([k,name,col])=>{ document.getElementById("view").innerHTML=`<div class="sec-h">Воронка продаж</div><div class="kanban">${PIPE.map(([k,name,col])=>{