Files
portal/web/v8/deals-final.html
T
Дмитрий 55a34af986 feat(deals): redesign groundwork — spec, plan, mockups + sidebar nav cleanup
Deals page redesign: design spec + implementation plan (Phase A page redesign,
Phase B 14->5 status funnel) + v8 HTML mockups (variants comparison + final).
AppSidebar: remove Импорт данных / Отчёты nav links (routes stay reachable by
direct URL); AppLayout.spec updated to 6 nav items. stylelint --fix on mockups;
cspell-words += deals-redesign terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 03:42:39 +03:00

315 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Сделки — финальный дизайн · Лидерра</title>
<style>
:root{
--teal:#0F6E56; --ivory:#F6F3EC; --noir:#012019; --surface:#ffffff;
--line:#E6E2D6; --line-strong:#d4cfbe; --muted:#6b6356; --ink:#081319; --radius:10px;
}
*{box-sizing:border-box;}
body{margin:0;background:var(--ivory);color:var(--ink);font-family:'Inter','Segoe UI',system-ui,sans-serif;font-size:14px;line-height:1.5;}
.mono{font-family:'JetBrains Mono',ui-monospace,monospace;font-feature-settings:'tnum';}
h1{font-size:23px;margin:0 0 4px;letter-spacing:-.02em;}
h2{font-size:19px;margin:0 0 2px;letter-spacing:-.015em;}
.wrap{max-width:1320px;margin:0 auto;padding:24px 28px 64px;}
.intro{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px 20px;margin-bottom:16px;}
.intro p{margin:5px 0;color:var(--muted);font-size:13px;}
.intro b{color:var(--ink);}
.intro ul{margin:6px 0;padding-left:20px;color:var(--muted);font-size:13px;}
.note{background:#fff8ec;border:1px solid #ecdcb8;border-left:3px solid #c8821a;border-radius:6px;padding:10px 14px;margin-top:10px;color:#7a5a1a;font-size:12.5px;}
.page{background:var(--ivory);border:1px solid var(--line);border-radius:var(--radius);padding:22px;}
.pagehead{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:14px;}
.stats{color:var(--muted);font-size:13px;display:flex;gap:8px;flex-wrap:wrap;}
.stats .num{color:var(--teal);font-weight:600;}
.stats .sep{opacity:.4;}
.btn{padding:7px 13px;border-radius:8px;border:1px solid var(--line-strong);background:var(--surface);cursor:pointer;font:inherit;font-size:13px;color:var(--ink);}
.btn.primary{background:var(--teal);color:#ffffff;border-color:var(--teal);}
/* export panel */
.export{
display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:var(--surface);
border:1px solid var(--line);border-radius:var(--radius);padding:11px 14px;margin-bottom:12px;
}
.export .lbl{font-size:13px;color:var(--muted);}
.export input[type="date"]{
border:1px solid var(--line);border-radius:7px;padding:6px 9px;font:inherit;font-size:13px;color:var(--ink);
}
.export .dash{color:var(--muted);}
.export .sp{flex:1;}
.export .ok{font-size:12px;color:var(--teal);}
/* filters */
.filterbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px;}
.fld{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font-size:13px;color:var(--muted);display:inline-flex;align-items:center;gap:6px;}
.fld.search{min-width:220px;flex:1;}
.fld .caret{opacity:.5;font-size:10px;}
.perpage{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--muted);margin-bottom:12px;}
.pp-btn{border:1px solid var(--line);background:var(--surface);border-radius:6px;padding:4px 10px;cursor:pointer;font:inherit;font-size:13px;color:var(--muted);}
.pp-btn.on{background:var(--teal);color:#ffffff;border-color:var(--teal);}
/* master-detail body: список + панель сделки сбоку */
.deals-body{display:flex;gap:14px;align-items:flex-start;}
.deals-body .tbl-card{flex:1;min-width:0;}
/* table */
.tbl-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);overflow-x:auto;}
table{width:100%;border-collapse:collapse;}
th{text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);padding:11px 12px;border-bottom:1px solid var(--line);background:#fbfaf6;white-space:nowrap;}
td{padding:11px 12px;border-bottom:1px solid var(--line);vertical-align:middle;}
tr.row{cursor:pointer;}
tr.row:hover{background:#fbfaf6;}
tr.row.selected{background:rgba(15,110,86,.07);}
.phone{font-family:'JetBrains Mono',monospace;font-weight:500;}
.proj{font-weight:500;}
.ty{font-size:12px;color:var(--muted);}
.city{font-size:13px;}
.inline{font-size:12px;color:var(--muted);}
.inline.set{color:var(--ink);font-size:13px;}
.rem{display:inline-flex;align-items:center;gap:5px;font-size:12px;}
.rem.due{color:#9a6b16;}
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:14px;font-size:12px;font-weight:500;white-space:nowrap;}
.pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.85;}
/* панель сделки («легенда») — справа, не поверх списка */
.detail-panel{width:360px;flex-shrink:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);display:none;}
.detail-panel.open{display:block;}
.dp-head{display:flex;justify-content:space-between;align-items:center;padding:13px 16px;border-bottom:1px solid var(--line);}
.dp-head .t{font-weight:600;font-size:15px;}
.dp-close{background:none;border:0;font-size:16px;cursor:pointer;color:var(--muted);line-height:1;}
.dp-body{padding:14px 16px;}
.dp-phone{font-family:'JetBrains Mono',monospace;font-size:17px;font-weight:600;margin-bottom:8px;}
.dp-status{margin-bottom:12px;}
.dp-row{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid var(--line);font-size:13px;}
.dp-row .k{color:var(--muted);flex-shrink:0;}
.dp-row .v{text-align:right;}
.dp-hist{margin-top:14px;}
.dp-hist .hh{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-bottom:7px;}
.dp-hist .ev{font-size:12px;color:var(--muted);padding:3px 0 3px 10px;border-left:2px solid var(--line);margin-bottom:4px;}
/* footer / pagination */
.tfoot{display:flex;align-items:center;justify-content:flex-end;gap:12px;flex-wrap:wrap;padding:12px 4px 0;}
.pager{display:flex;align-items:center;gap:4px;}
.pg{border:1px solid var(--line);background:var(--surface);border-radius:6px;min-width:30px;height:30px;cursor:pointer;font:inherit;font-size:13px;color:var(--ink);}
.pg.on{background:var(--noir);color:#ffffff;border-color:var(--noir);}
.pg:disabled{opacity:.4;cursor:default;}
</style>
</head>
<body>
<div class="wrap">
<div class="intro">
<h1>Страница «Сделки» — финальный дизайн</h1>
<p>Вариант A с правками. Лиды поступают только от поставщика crm.bp по заказанным в проектах источникам; ручного создания и корзины нет.</p>
<ul>
<li>Колонка <b>Телефон</b> — только номер (ФИО убрано).</li>
<li>Колонка <b>Источник</b> — проект + тип сигнала; B1/B2/B3 не показываются.</li>
<li><b>5 статусов:</b> Новая сделка → Просмотрено → В работе → Сделка / Не реализовано.</li>
<li>Колонка «Ответственный» убрана; «Отгружен» → <b>«Поставлен»</b>.</li>
<li>Добавлена колонка <b>Город</b>.</li>
<li>Экспорт за период поставки в Excel и CSV; выбор числа строк 10 / 20 / 50.</li>
<li><b>Клик по строке</b> открывает панель сделки справа — список <b>сжимается влево</b>, панель не перекрывает его (повторный клик / ✕ — закрыть).</li>
</ul>
<div class="note">
<b>Город — источник данных не определён.</b> crm.bp город/регион не передаёт ни в вебхуке
(<span class="mono">SupplierWebhookController</span> принимает только vid/project/phone/time/tag/phones),
ни в CSV-импорте; доработку поставщик делать не будет. Источник заполнения колонки заказчик
определит отдельно — вопрос записан, напоминание на 18.05. В макете — мок-данные.
</div>
<div class="note" style="background:#eef4f1;border-color:#cfe0d9;border-left-color:var(--teal);color:#2a5145;">
<b>Каскад от 5 статусов.</b> Сжатие воронки 14 → 5 статусов потянет за собой: <span class="mono">lead_statuses</span>/воронку в схеме, стор <span class="mono">leadStatuses</span>, страницу <b>Канбан</b> (колонки), фильтр и массовую смену статуса, маппинг статусов из импорта поставщика. Это отдельный backend-эпик — здесь только макет.
</div>
</div>
<div class="page">
<div class="pagehead">
<div>
<h2>Сделки</h2>
<div class="stats"><span><span class="num">+3</span> новых сегодня</span><span class="sep">·</span><span><span class="num">14</span> за период</span><span class="sep">·</span><span><span class="num">4</span> в работе</span></div>
</div>
<button class="btn">↻ Обновить</button>
</div>
<!-- Экспорт по датам поставки -->
<div class="export">
<span class="lbl">Поставлены с</span>
<input type="date" id="from" value="2026-05-10" />
<span class="dash"></span>
<input type="date" id="to" value="2026-05-17" />
<button class="btn" id="exp-xlsx">⤓ Экспорт в Excel</button>
<button class="btn" id="exp-csv">⤓ Экспорт в CSV</button>
<span class="sp"></span>
<span class="ok" id="exp-msg"></span>
</div>
<!-- Фильтры -->
<div class="filterbar">
<span class="fld search">🔍 Поиск по телефону…</span>
<span class="fld">Статус <span class="caret"></span></span>
<span class="fld">Проект <span class="caret"></span></span>
<span class="fld">Город <span class="caret"></span></span>
</div>
<div class="perpage">Показывать по:
<button class="pp-btn on" data-pp="10">10</button>
<button class="pp-btn" data-pp="20">20</button>
<button class="pp-btn" data-pp="50">50</button>
</div>
<!-- Список + панель сделки (master-detail) -->
<div class="deals-body">
<div class="tbl-card">
<table id="tbl">
<thead><tr>
<th style="width:32px;"><input type="checkbox" /></th>
<th>Телефон</th><th>Источник</th><th>Город</th><th>Статус</th>
<th>Напоминание</th><th>Комментарий</th><th>Поставлен</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
<aside class="detail-panel" id="panel">
<div class="dp-head">
<span class="t">Сделка</span>
<button class="dp-close" id="panel-close" aria-label="Закрыть"></button>
</div>
<div class="dp-body" id="panel-body"></div>
</aside>
</div>
<div class="tfoot">
<div class="pager" id="pager"></div>
</div>
</div>
</div>
<script>
/* лиды, поставленные crm.bp (мок-данные; город — из источника, который заказчик определит) */
const LEADS = [
{phone:'+7 916 234-56-78', project:'Окна-Люкс', type:'Звонки', city:'Москва', status:'Новая сделка', reminder:'', comment:'', shipped:'17.05 09:12'},
{phone:'+7 903 887-12-09', project:'Двери-Маркет', type:'Сайт', city:'Санкт-Петербург', status:'В работе', reminder:'сегодня 16:00', comment:'Перезвонить после обеда', shipped:'17.05 08:40'},
{phone:'+7 928 220-71-44', project:'Ремонт-Сервис',type:'Звонки', city:'Казань', status:'Новая сделка', reminder:'', comment:'', shipped:'17.05 10:05'},
{phone:'+7 925 110-44-87', project:'Кухни-Про', type:'СМС', city:'Подольск', status:'Просмотрено', reminder:'', comment:'', shipped:'16.05 19:20'},
{phone:'+7 999 002-77-31', project:'Окна-Люкс', type:'Звонки', city:'Краснодар', status:'Сделка', reminder:'', comment:'Счёт оплачен', shipped:'16.05 14:05'},
{phone:'+7 911 556-23-90', project:'Ремонт-Сервис',type:'Сайт', city:'Санкт-Петербург', status:'В работе', reminder:'сегодня 12:30', comment:'Готов к замеру', shipped:'16.05 11:50'},
{phone:'+7 962 334-88-21', project:'Кухни-Про', type:'СМС', city:'Ростов-на-Дону', status:'Не реализовано', reminder:'', comment:'Дорого для клиента', shipped:'15.05 16:30'},
{phone:'+7 905 778-31-02', project:'Двери-Маркет', type:'Сайт', city:'Екатеринбург', status:'Просмотрено', reminder:'', comment:'', shipped:'15.05 10:15'},
{phone:'+7 952 661-09-37', project:'Кухни-Про', type:'СМС', city:'Новосибирск', status:'В работе', reminder:'завтра 11:00', comment:'Ждёт КП', shipped:'14.05 09:30'},
{phone:'+7 916 445-90-12', project:'Окна-Люкс', type:'Звонки', city:'Москва', status:'Сделка', reminder:'', comment:'', shipped:'14.05 13:00'},
{phone:'+7 919 003-55-18', project:'Двери-Маркет', type:'Сайт', city:'Москва', status:'Не реализовано', reminder:'', comment:'Не тот регион', shipped:'13.05 18:45'},
{phone:'+7 909 871-22-60', project:'Окна-Люкс', type:'Звонки', city:'Химки', status:'В работе', reminder:'', comment:'', shipped:'13.05 12:10'},
{phone:'+7 977 445-83-21', project:'Ремонт-Сервис',type:'Сайт', city:'Сочи', status:'Просмотрено', reminder:'', comment:'', shipped:'12.05 15:25'},
{phone:'+7 913 556-90-04', project:'Кухни-Про', type:'СМС', city:'Новосибирск', status:'Сделка', reminder:'', comment:'Оплачено', shipped:'11.05 11:40'}
];
/* 5 статусов воронки */
const ST = {
'Новая сделка':['#e3f0ec','#0F6E56'],
'Просмотрено':['#e8ecf2','#4a5b73'],
'В работе':['#fbeede','#9a6b16'],
'Сделка':['#e0f0e2','#2f7d3a'],
'Не реализовано':['#efe9e9','#9a5a52']
};
function el(tag,cls,txt){const e=document.createElement(tag);if(cls)e.className=cls;if(txt!=null)e.textContent=txt;return e;}
function td(child){const c=el('td');if(child!=null)child instanceof Node?c.appendChild(child):(c.textContent=child);return c;}
function pillEl(s){const c=ST[s]||['#eee','#555'];const e=el('span','pill',s);e.style.background=c[0];e.style.color=c[1];return e;}
let perPage=10, page=1;
/* панель сделки («легенда») */
function dpRow(k,v){
const r=el('div','dp-row');
r.appendChild(el('span','k',k));
r.appendChild(v instanceof Node?(()=>{const w=el('span','v');w.appendChild(v);return w;})():el('span','v',v));
return r;
}
function openPanel(l,rowEl){
document.querySelectorAll('tr.row.selected').forEach(r=>r.classList.remove('selected'));
if(rowEl)rowEl.classList.add('selected');
const body=document.getElementById('panel-body');
body.textContent='';
body.appendChild(el('div','dp-phone',l.phone));
body.appendChild((()=>{const d=el('div','dp-status');d.appendChild(pillEl(l.status));return d;})());
body.appendChild(dpRow('Источник', l.project+' · '+l.type));
body.appendChild(dpRow('Город', l.city));
body.appendChild(dpRow('Поставлен', l.shipped));
body.appendChild(dpRow('Напоминание', l.reminder||'—'));
body.appendChild(dpRow('Комментарий', l.comment||'—'));
const hist=el('div','dp-hist');
hist.appendChild(el('div','hh','История'));
hist.appendChild(el('div','ev','Поставлен от crm.bp — '+l.shipped));
hist.appendChild(el('div','ev','Статус: '+l.status));
if(l.comment)hist.appendChild(el('div','ev','Комментарий: '+l.comment));
body.appendChild(hist);
document.getElementById('panel').classList.add('open');
}
function closePanel(){
document.getElementById('panel').classList.remove('open');
document.querySelectorAll('tr.row.selected').forEach(r=>r.classList.remove('selected'));
}
document.getElementById('panel-close').addEventListener('click',closePanel);
function render(){
const total=LEADS.length, pages=Math.max(1,Math.ceil(total/perPage));
if(page>pages)page=pages;
const slice=LEADS.slice((page-1)*perPage, (page-1)*perPage+perPage);
const tb=document.querySelector('#tbl tbody');
tb.textContent='';
slice.forEach(l=>{
const tr=el('tr','row');
const cb=document.createElement('input');cb.type='checkbox';
cb.addEventListener('click',e=>e.stopPropagation());
tr.appendChild(td(cb));
tr.appendChild(td(el('span','phone',l.phone)));
const srcTd=el('td');
srcTd.appendChild(el('div','proj',l.project));
srcTd.appendChild(el('div','ty',l.type));
tr.appendChild(srcTd);
tr.appendChild(td(el('span','city',l.city)));
tr.appendChild(td(pillEl(l.status)));
tr.appendChild(td(l.reminder?el('span','rem due','⏰ '+l.reminder):el('span','inline','—')));
tr.appendChild(td(l.comment?el('span','inline set',l.comment):el('span','inline','—')));
tr.appendChild(td(el('span','inline',l.shipped)));
tr.addEventListener('click',()=>openPanel(l,tr));
tb.appendChild(tr);
});
const pager=document.getElementById('pager');
pager.textContent='';
const prev=el('button','pg','');prev.disabled=page===1;prev.onclick=()=>{page--;render();};
pager.appendChild(prev);
for(let p=1;p<=pages;p++){
const b=el('button','pg'+(p===page?' on':''),String(p));
b.onclick=()=>{page=p;render();};
pager.appendChild(b);
}
const next=el('button','pg','');next.disabled=page===pages;next.onclick=()=>{page++;render();};
pager.appendChild(next);
}
document.querySelectorAll('.pp-btn').forEach(b=>b.addEventListener('click',()=>{
document.querySelectorAll('.pp-btn').forEach(x=>x.classList.remove('on'));
b.classList.add('on');
perPage=Number(b.dataset.pp); page=1; render();
}));
function expMsg(fmt){
const from=document.getElementById('from').value, to=document.getElementById('to').value;
document.getElementById('exp-msg').textContent='Сформирован '+fmt+' за '+from+' — '+to+' (макет)';
}
document.getElementById('exp-xlsx').addEventListener('click',()=>expMsg('Excel'));
document.getElementById('exp-csv').addEventListener('click',()=>expMsg('CSV'));
render();
</script>
</body>
</html>