diff --git a/Mokap/crm.html b/Mokap/crm.html
index c8f9fd8..6ba7956 100644
--- a/Mokap/crm.html
+++ b/Mokap/crm.html
@@ -191,7 +191,46 @@ function renderDashboard(){
${renderRevenueChart()}
${renderUpcomingTasks()}
Все клиенты · ${total}
- ${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `${esc((p.client_name||'?')[0])}
${esc(p.client_name)}
${esc(p.niche)} · ${p.msg_count} сообщений
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)} `}).join("")||'Создайте первого клиента
'}`;
+ ${projects.map(p=>renderClientRow(p)).join("")||'Создайте первого клиента
'}`;
+}
+const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
+function clientStages(p){
+ const done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec];
+ const cnt=done.filter(Boolean).length;
+ let cur=done.findIndex(d=>!d); // первый незавершённый
+ const all=cur===-1;
+ const label=all?"Готово · ТЗ собрано":STAGE_DEFS[cur].name;
+ return {done,cnt,cur:all?STAGE_DEFS.length:cur,all,label};
+}
+function stepper(p){
+ const 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 3px rgba(16,185,129,.25);":"";
+ dots+=``;
+ if(i`;}
+ });
+ const col=st.all?"#047857":"#10B981";
+ return `${dots}
+ Этап: ${st.label} · ${st.cnt}/5
`;
+}
+function renderClientRow(p){
+ const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
+ const billing=(p.crm&&p.crm.billing_type)||"paid";
+ const bChip=billing==="free"
+ ?`🎁 Бесплатный`
+ :`💰 Платный`;
+ return `
+
${esc((p.client_name||'?')[0])}
+
+
${esc(p.client_name)}${bChip}
+
${esc(p.niche)} · ${p.msg_count} сообщений
+ ${stepper(p)}
+
+
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)}
+
`;
}
function renderRevenueChart(){
diff --git a/backend/elena_app.py b/backend/elena_app.py
index de5e9c4..567b173 100644
--- a/backend/elena_app.py
+++ b/backend/elena_app.py
@@ -952,6 +952,7 @@ def list_projects():
"token": r["token"], "client_name": r["client_name"] or "Без имени",
"niche": r["niche"] or "", "status": r["status"],
"created_at": r["created_at"], "msg_count": r["msg_count"],
+ "has_selection": latest_artifact(pid, "selection") is not None,
"has_canvas": latest_artifact(pid, "canvas") is not None,
"has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None,
"has_spec": latest_artifact(pid, "spec") is not None,
diff --git a/docs/crm.html b/docs/crm.html
index c8f9fd8..6ba7956 100644
--- a/docs/crm.html
+++ b/docs/crm.html
@@ -191,7 +191,46 @@ function renderDashboard(){
${renderRevenueChart()}
${renderUpcomingTasks()}
Все клиенты · ${total}
- ${projects.map(p=>{const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];return `${esc((p.client_name||'?')[0])}
${esc(p.client_name)}
${esc(p.niche)} · ${p.msg_count} сообщений
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)} `}).join("")||'Создайте первого клиента
'}`;
+ ${projects.map(p=>renderClientRow(p)).join("")||'Создайте первого клиента
'}`;
+}
+const STAGE_DEFS=[{key:"interview",name:"Интервью"},{key:"methods",name:"Методологии"},{key:"canvas",name:"Стратегия"},{key:"idef0",name:"Функции"},{key:"spec",name:"ТЗ"}];
+function clientStages(p){
+ const done=[p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec];
+ const cnt=done.filter(Boolean).length;
+ let cur=done.findIndex(d=>!d); // первый незавершённый
+ const all=cur===-1;
+ const label=all?"Готово · ТЗ собрано":STAGE_DEFS[cur].name;
+ return {done,cnt,cur:all?STAGE_DEFS.length:cur,all,label};
+}
+function stepper(p){
+ const 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 3px rgba(16,185,129,.25);":"";
+ dots+=``;
+ if(i`;}
+ });
+ const col=st.all?"#047857":"#10B981";
+ return `${dots}
+ Этап: ${st.label} · ${st.cnt}/5
`;
+}
+function renderClientRow(p){
+ const pc=pipeMap[(p.crm&&p.crm.pipeline)||"lead"];
+ const billing=(p.crm&&p.crm.billing_type)||"paid";
+ const bChip=billing==="free"
+ ?`🎁 Бесплатный`
+ :`💰 Платный`;
+ return `
+
${esc((p.client_name||'?')[0])}
+
+
${esc(p.client_name)}${bChip}
+
${esc(p.niche)} · ${p.msg_count} сообщений
+ ${stepper(p)}
+
+
${pc[1]}${money((p.crm&&p.crm.deal_amount)||0)}
+
`;
}
function renderRevenueChart(){