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>
This commit is contained in:
@@ -53,14 +53,12 @@ const navGroups = computed<NavGroup[]>(() => [
|
||||
},
|
||||
{ title: 'Канбан', icon: 'mdi-view-column-outline', to: '/kanban' },
|
||||
{ title: 'Дашборд', icon: 'mdi-view-dashboard-outline', to: '/dashboard' },
|
||||
{ title: 'Импорт данных', icon: 'mdi-database-import-outline', to: '/import' },
|
||||
],
|
||||
},
|
||||
{
|
||||
eyebrow: 'Финансы',
|
||||
items: [
|
||||
{ title: 'Биллинг', icon: 'mdi-credit-card-outline', to: '/billing' },
|
||||
{ title: 'Отчёты', icon: 'mdi-chart-box-outline', to: '/reports' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -87,14 +87,16 @@ describe('AppLayout.vue', () => {
|
||||
expect(text).toContain('Команда');
|
||||
});
|
||||
|
||||
it('содержит все 6 nav-пунктов (Менеджеры+Напоминания убраны по требованию заказчика)', async () => {
|
||||
it('содержит 6 nav-пунктов (Импорт данных + Отчёты убраны по требованию заказчика)', async () => {
|
||||
const wrapper = await mountAppLayout();
|
||||
const text = wrapper.text();
|
||||
['Проекты', 'Сделки', 'Канбан', 'Дашборд', 'Биллинг', 'Отчёты', 'Настройки'].forEach((label) =>
|
||||
['Проекты', 'Сделки', 'Канбан', 'Дашборд', 'Биллинг', 'Настройки'].forEach((label) =>
|
||||
expect(text).toContain(label),
|
||||
);
|
||||
expect(text).not.toContain('Менеджеры');
|
||||
expect(text).not.toContain('Напоминания');
|
||||
expect(text).not.toContain('Импорт данных');
|
||||
expect(text).not.toContain('Отчёты');
|
||||
});
|
||||
|
||||
it('показывает счётчики только у пунктов с count', async () => {
|
||||
|
||||
@@ -1385,3 +1385,9 @@ ivotoby
|
||||
ребейз
|
||||
ребейзнута
|
||||
ребейзом
|
||||
|
||||
# Deals page redesign — spec/plan (2026-05-17)
|
||||
гейта
|
||||
дровер
|
||||
канбаном
|
||||
коммитить
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
# Редизайн страницы «Сделки» — дизайн
|
||||
|
||||
**Дата:** 2026-05-17
|
||||
**Статус:** дизайн утверждён заказчиком; спека на ревью.
|
||||
**Макет:** `web/v8/deals-final.html` (вариант A, финал). Сравнение вариантов — `web/v8/deals-variants.html`.
|
||||
|
||||
## 1. Контекст и проблема
|
||||
|
||||
Текущая страница «Сделки» (`resources/js/views/DealsView.vue`) построена как обычная CRM: ручное создание сделки, корзина, mock-данные, колонки «Менеджер»/«Стоимость». Это неверно: лиды поступают в портал **только** от поставщика crm.bp по заказанным в проектах источникам (B1/B2/B3 × тип сигнала). Сделку нельзя создать вручную и нельзя удалить — у неё есть только статус.
|
||||
|
||||
Конкретные ошибки текущей реализации:
|
||||
|
||||
- Кнопка «Новая сделка» + `NewDealDialog` — [DealsView.vue:542-551](../../../app/resources/js/views/DealsView.vue#L542-L551), [:741](../../../app/resources/js/views/DealsView.vue#L741).
|
||||
- Режим «Корзина» (soft-delete) — [DealsView.vue:524-532](../../../app/resources/js/views/DealsView.vue#L524-L532).
|
||||
- Mock-данные — `mockDeals.ts` ([DealsView.vue:18](../../../app/resources/js/views/DealsView.vue#L18)).
|
||||
- Колонки «Менеджер»/«Стоимость» и две дублирующие фильтр-панели — [DealsTable.vue:57-64](../../../app/resources/js/components/deals/DealsTable.vue#L57-L64), [DealsView.vue:559-687](../../../app/resources/js/views/DealsView.vue#L559-L687).
|
||||
|
||||
## 2. Утверждённый дизайн
|
||||
|
||||
Страница — **реестр лидов, поставленных crm.bp**. Ручного создания и корзины нет.
|
||||
|
||||
**Шапка:** заголовок «Сделки» + статистика + кнопка «Обновить».
|
||||
|
||||
**Панель экспорта:** диапазон дат поставки (`от` — `до`) + кнопки «Экспорт в Excel» и «Экспорт в CSV». Диапазон фильтрует таблицу и задаёт окно экспорта.
|
||||
|
||||
**Фильтры:** поиск по телефону, Статус, Проект, Город.
|
||||
|
||||
**Выбор числа строк:** 10 / 20 / 50 — между фильтрами и таблицей.
|
||||
|
||||
**Таблица** (колонки): чекбокс · Телефон · Источник · Город · Статус · Напоминание · Комментарий · Поставлен.
|
||||
|
||||
- **Телефон** — только номер (ФИО убрано).
|
||||
- **Источник** — название проекта + тип сигнала (Звонки/Сайт/СМС); B1/B2/B3 не показываются.
|
||||
- **Город** — см. §4 (открытый вопрос источника).
|
||||
- **Статус** — одна из 5 (см. §3).
|
||||
- Колонка «Ответственный» убрана; «Отгружен» переименован в «Поставлен».
|
||||
|
||||
**Панель сделки (master-detail):** клик по строке открывает панель с деталями сделки **справа от списка**. Список при этом **сжимается влево**, освобождая место — панель **не перекрывает** таблицу (в отличие от текущего overlay-дровера). Повторный клик / кнопка ✕ — закрыть. Содержимое панели: телефон, статус, источник (проект+тип), город, дата поставки, напоминание, комментарий, история сделки.
|
||||
|
||||
**Футер:** пагинация.
|
||||
|
||||
**Сайдбар** (отдельная правка, уже внесена): из nav убраны ссылки «Импорт данных» и «Отчёты» — `AppSidebar.vue`, тест `AppLayout.spec.ts` обновлён.
|
||||
|
||||
## 3. Воронка статусов: 14 → 5
|
||||
|
||||
Заказчик пересматривает воронку: вместо 14 статусов — **5**:
|
||||
|
||||
1. **Новая сделка** — присваивается при поступлении лида от crm.bp.
|
||||
2. **Просмотрено**
|
||||
3. **В работе**
|
||||
4. **Сделка** — успешно закрыта.
|
||||
5. **Не реализовано** — закрыта без результата.
|
||||
|
||||
Это самый крупный затрагиваемый блок (work-stream B, см. §6). Каскад:
|
||||
|
||||
- Схема: `lead_statuses` / воронка (`db/schema.sql` р-н строки 2076) — изменение справочника статусов; миграция с записью в `db/CHANGELOG_schema.md`.
|
||||
- `composables/leadStatuses.ts` + `stores/leadStatuses` — новый набор.
|
||||
- `mockDeals.ts` `DEALS_TABS` — срезы по статусам.
|
||||
- Страница **Канбан** — колонки строятся из статусов → 5 колонок вместо 14.
|
||||
- Фильтр по статусу + массовая смена статуса (`DealsBulkBar`, `DealBulkActionController@transition`, `dealsApi.transitionDeals`).
|
||||
- `StatusRuToSlugMapper` (Sprint 4, импорт исторических лидов) — маппинг русских статусов поставщика на slug'и пересматривается под 5.
|
||||
|
||||
**Предлагаемый маппинг старых 14 → новых 5** (на подтверждение в фазе плана):
|
||||
|
||||
| Старый | Новый |
|
||||
|---|---|
|
||||
| Новый | Новая сделка |
|
||||
| Просмотрено | Просмотрено |
|
||||
| Проработан, База, Недозвон, Переговоры, Ожидаем оплаты, Партнёрка, Тест-драйв, Горячий, На замену, Конечный недозвон | В работе |
|
||||
| Оплачено | Сделка |
|
||||
| Закрыто и не реализовано | Не реализовано |
|
||||
|
||||
Слуги новых статусов и точный маппинг — фиксируются в плане после явного подтверждения заказчиком.
|
||||
|
||||
## 4. Открытый вопрос — источник данных для колонки «Город»
|
||||
|
||||
crm.bp **не передаёт** город/регион ни по одному каналу ингеста:
|
||||
|
||||
- вебхук `SupplierWebhookController@receive` валидирует только `vid/project/phone/time/tag/phones`;
|
||||
- CSV-импорт (`Services/Import/*`) гео-полей не читает.
|
||||
|
||||
Поставщик доработку делать не будет. Из телефона регион не вывести (`PhonePrefixService` для мобильных 9XX → «неизвестно»). В схеме `deals.region_code` + `deals.city` есть (Биз-23), но заполнять нечем.
|
||||
|
||||
**Решение:** заказчик определит источник заполнения «Города» отдельно. До этого колонка в реализацию не берётся (рендерится пустой/`—`). Вопрос зафиксирован: memory `project_deals_page_redesign.md`, напоминание 18.05.
|
||||
|
||||
## 5. Затрагиваемые компоненты
|
||||
|
||||
**Frontend:**
|
||||
|
||||
- `views/DealsView.vue` — убрать `NewDealDialog`/«Новая сделка», trash-режим, дублирующую `.ld-filterbar`; перевести на реальный API; добавить панель экспорта по датам и выбор числа строк.
|
||||
- `components/deals/DealsTable.vue` — новый набор колонок.
|
||||
- `components/deals/DealsFilters.vue` — фильтры Статус/Проект/Город; убрать «Менеджер».
|
||||
- `components/deals/DealsBulkBar.vue` — массовая смена статуса на 5 значений.
|
||||
- `composables/mockDeals.ts` — снять mock-зависимость (реальные данные из API).
|
||||
- `composables/leadStatuses.ts` / `stores/leadStatuses` — 5 статусов.
|
||||
- `api/deals.ts` — список с фильтром по диапазону дат поставки, пагинацией `per_page`, полем города.
|
||||
- `components/deals/DealDetailDrawer.vue` — переделать из overlay-дровера в боковую панель master-detail: список сжимается, панель встаёт рядом (flex split-pane), не перекрывает таблицу. Содержимое карточки переиспользуется.
|
||||
|
||||
**Backend:**
|
||||
|
||||
- `DealController@index` — пагинация `per_page` (10/20/50), фильтр `received_at BETWEEN`, фильтр по городу/статусу/проекту.
|
||||
- `DealExportController` (`POST /api/deals/export`) — экспорт по диапазону дат, XLSX + CSV.
|
||||
- `db/schema.sql` — справочник статусов воронки 14→5 (+ `db/CHANGELOG_schema.md`).
|
||||
- `RouteSupplierLeadJob` — присваивает статус «Новая сделка» новым лидам (сейчас `'new'`).
|
||||
- `StatusRuToSlugMapper` — маппинг под 5 статусов.
|
||||
|
||||
## 6. Декомпозиция и порядок
|
||||
|
||||
Два work-stream'а:
|
||||
|
||||
- **A — редизайн страницы** (frontend + `DealController`/`DealExportController`): колонки, фильтры, экспорт, пагинация, удаление ручного создания/корзины. Не зависит от B при условии, что A временно работает на текущем наборе статусов.
|
||||
- **B — воронка 14→5** (схема + миграция + Канбан + маппинг импорта). Schema-change — гейт на явное подтверждение маппинга §3.
|
||||
|
||||
Рекомендация для плана: A и B — отдельными фазами; B стартует после подтверждения маппинга. Колонка «Город» — вне реализации до решения §4.
|
||||
|
||||
## 7. Вне scope
|
||||
|
||||
- Заполнение колонки «Город» данными — до решения по источнику (§4).
|
||||
- Inline-редактирование напоминания/комментария в строке (было в варианте B макета) — вариант A оставляет их read-only.
|
||||
- Содержимое карточки сделки (поля, история) не пересматривается — переиспользуется как есть; меняется только подача: overlay-дровер → боковая панель master-detail (§2, §5).
|
||||
|
||||
## 8. Тестирование
|
||||
|
||||
- TDD на каждый блок: Vitest (DealsView/DealsTable/DealsFilters/DealsBulkBar), Pest (`DealController@index` пагинация/фильтры, `DealExportController` диапазон дат, миграция статусов — db-тесты).
|
||||
- После изменения схемы статусов — db-тесты + smoke.
|
||||
- Регрессия Vitest + Pest перед коммитом.
|
||||
|
||||
## 9. Открытые вопросы для фазы плана
|
||||
|
||||
1. Источник данных для «Города» (§4) — ждём заказчика.
|
||||
2. Точные slug'и 5 статусов и маппинг старых 14 (§3) — подтвердить.
|
||||
3. ~~Судьба маршрутов `/import` и `/reports`~~ — **РЕШЕНО (2026-05-17): вариант А** — маршруты и вью остаются доступными по прямому URL; из nav-сайдбара ссылки убраны (уже сделано).
|
||||
@@ -0,0 +1,314 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,506 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Сделки — 4 варианта дизайна · Лидерра</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:24px;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 + tabs ---- */
|
||||
.intro{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:18px 22px;margin-bottom:18px;}
|
||||
.intro p{margin:6px 0;color:var(--muted);}
|
||||
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0 8px;}
|
||||
.tab{
|
||||
padding:9px 18px;border:1px solid var(--line-strong);background:var(--surface);
|
||||
border-radius:var(--radius);cursor:pointer;font:inherit;font-weight:600;color:var(--muted);
|
||||
transition:all .15s;
|
||||
}
|
||||
.tab:hover{border-color:var(--teal);}
|
||||
.tab.active{background:var(--teal);color:#ffffff;border-color:var(--teal);}
|
||||
.variant{display:none;}
|
||||
.variant.active{display:block;}
|
||||
.caption{
|
||||
background:#fffdf6;border:1px solid var(--line);border-left:3px solid var(--teal);
|
||||
border-radius:6px;padding:12px 16px;margin:14px 0 18px;color:var(--muted);font-size:13px;
|
||||
}
|
||||
.caption b{color:var(--ink);}
|
||||
|
||||
/* ---- mock page shell ---- */
|
||||
.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 14px;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);}
|
||||
.btn.ghost{background:transparent;border-color:transparent;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:240px;flex:1;}
|
||||
.fld .caret{opacity:.5;font-size:10px;}
|
||||
.taskchips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;}
|
||||
.taskchip{
|
||||
background:var(--surface);border:1px solid var(--line);border-radius:20px;padding:5px 12px;
|
||||
font-size:12px;color:var(--muted);cursor:pointer;
|
||||
}
|
||||
.taskchip b{color:var(--teal);font-family:'JetBrains Mono',monospace;}
|
||||
.taskchip.on{border-color:var(--teal);background:rgba(15,110,86,.06);color:var(--teal);}
|
||||
|
||||
/* ---- table ---- */
|
||||
.tbl-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;}
|
||||
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;
|
||||
}
|
||||
td{padding:11px 12px;border-bottom:1px solid var(--line);vertical-align:middle;}
|
||||
tr:last-child td{border-bottom:none;}
|
||||
tr.row:hover{background:#fbfaf6;}
|
||||
.lead{display:flex;align-items:center;gap:10px;}
|
||||
.ava{
|
||||
width:32px;height:32px;border-radius:50%;background:var(--teal);color:#ffffff;
|
||||
display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex-shrink:0;
|
||||
}
|
||||
.ava.sm{width:22px;height:22px;font-size:9px;background:var(--noir);}
|
||||
.lead .nm{font-weight:500;}
|
||||
.lead .ph{font-size:12px;color:var(--muted);}
|
||||
.star{color:var(--line-strong);cursor:pointer;font-size:15px;}
|
||||
.star.on{color:#d6a531;}
|
||||
.dots{color:var(--muted);cursor:pointer;letter-spacing:1px;}
|
||||
|
||||
/* status pill */
|
||||
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;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;}
|
||||
|
||||
/* source badge */
|
||||
.src{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border:1px solid var(--line);border-radius:7px;font-size:12px;background:var(--surface);}
|
||||
.src .dot{width:7px;height:7px;border-radius:50%;}
|
||||
.src .b{font-weight:600;}
|
||||
.src .ty{color:var(--muted);}
|
||||
.src.big{padding:6px 12px;font-size:13px;}
|
||||
|
||||
/* inline cells */
|
||||
.inline{font-size:12px;color:var(--muted);}
|
||||
.inline.set{color:var(--ink);}
|
||||
.inline-edit{border:1px dashed var(--line-strong);border-radius:6px;padding:3px 8px;font-size:12px;color:var(--muted);display:inline-block;cursor:text;}
|
||||
.rem{display:inline-flex;align-items:center;gap:5px;font-size:12px;}
|
||||
.rem.due{color:#9a6b16;}
|
||||
.price{font-family:'JetBrains Mono',monospace;font-weight:600;}
|
||||
|
||||
/* bulk bar */
|
||||
.bulkbar{display:flex;align-items:center;gap:10px;background:var(--noir);color:#ffffff;border-radius:var(--radius);padding:10px 16px;margin-bottom:12px;font-size:13px;}
|
||||
.bulkbar .btn{background:rgba(255,255,255,.1);color:#ffffff;border-color:rgba(255,255,255,.2);}
|
||||
.bulkbar .sp{flex:1;}
|
||||
|
||||
/* ---- flow / inbox cards ---- */
|
||||
.subtoggle{display:inline-flex;border:1px solid var(--line);border-radius:8px;background:var(--surface);padding:2px;margin-bottom:14px;}
|
||||
.subtoggle button{border:none;background:transparent;padding:6px 16px;border-radius:6px;cursor:pointer;font:inherit;font-size:13px;color:var(--muted);}
|
||||
.subtoggle button.on{background:rgba(1,32,25,.08);color:var(--noir);font-weight:600;}
|
||||
.subview{display:none;}
|
||||
.subview.on{display:block;}
|
||||
.feed{display:flex;flex-direction:column;gap:10px;}
|
||||
.lcard{
|
||||
background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
|
||||
padding:14px 16px;display:flex;align-items:center;gap:16px;
|
||||
}
|
||||
.lcard:hover{border-color:var(--line-strong);}
|
||||
.lcard .when{font-size:11px;color:var(--muted);width:84px;flex-shrink:0;}
|
||||
.lcard .main{flex:1;min-width:0;}
|
||||
.lcard .main .nm{font-weight:600;}
|
||||
.lcard .main .ph{font-size:12px;color:var(--muted);}
|
||||
.lcard .meta{display:flex;align-items:center;gap:14px;flex-shrink:0;}
|
||||
.lcard .resp{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);}
|
||||
|
||||
/* kanban */
|
||||
.kanban{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;}
|
||||
.kcol{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);min-width:212px;flex:1;}
|
||||
.kcol h4{margin:0;padding:11px 13px;font-size:12px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;}
|
||||
.kcol h4 span{color:var(--muted);font-family:'JetBrains Mono',monospace;}
|
||||
.kcol .body{padding:10px;display:flex;flex-direction:column;gap:8px;min-height:60px;}
|
||||
.kcard{border:1px solid var(--line);border-radius:8px;padding:9px 10px;}
|
||||
.kcard .nm{font-weight:500;font-size:13px;}
|
||||
.kcard .ph{font-size:11px;color:var(--muted);}
|
||||
.kcard .ft{display:flex;justify-content:space-between;align-items:center;margin-top:6px;}
|
||||
.legend{font-size:11px;color:var(--muted);margin-top:8px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="intro">
|
||||
<h1>Страница «Сделки» — 4 варианта</h1>
|
||||
<p>Лиды в портал поступают <b>только</b> от поставщика crm.bp по заказанным в проектах источникам (B1/B2/B3 × тип сигнала). Сделку нельзя создать вручную и нельзя удалить — у неё есть только статус. Во всех вариантах убраны кнопка «Новая сделка» и «Корзина».</p>
|
||||
<p style="margin-top:10px;"><b>A</b> — минимальная коррекция текущей таблицы · <b>B</b> — паритет с «Мои сделки» поставщика · <b>C</b> — Лидерра-нативный инбокс (Поток/Воронка) · <b>Гибрид</b> — паритет-таблица + фишки C.</p>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="a">Вариант A</button>
|
||||
<button class="tab" data-tab="b">Вариант B</button>
|
||||
<button class="tab" data-tab="c">Вариант C</button>
|
||||
<button class="tab" data-tab="h">Гибрид</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ VARIANT A ============ -->
|
||||
<section class="variant active" id="v-a">
|
||||
<div class="caption"><b>Вариант A — Минимальная коррекция.</b> Текущая таблица сохранена, исправлены только ошибки модели: убрано ручное создание и корзина, колонки приведены к смыслу «лид от поставщика», добавлена колонка «Источник». Самый быстрый путь, но страница остаётся обычной таблицей без inline-работы с напоминанием/комментарием.</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">128</span> всего</span><span class="sep">·</span><span><span class="num">41</span> в работе</span></div>
|
||||
</div>
|
||||
<button class="btn">↻ Обновить</button>
|
||||
</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>
|
||||
<span class="fld">Ответственный <span class="caret">▾</span></span>
|
||||
<span class="fld">Период <span class="caret">▾</span></span>
|
||||
</div>
|
||||
<div class="tbl-card">
|
||||
<table id="tbl-a">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ VARIANT B ============ -->
|
||||
<section class="variant" id="v-b">
|
||||
<div class="caption"><b>Вариант B — Паритет с поставщиком.</b> Точное зеркало страницы «Мои сделки» crm.bp: полный колоночный набор, звёздочка-избранное, ⋮-меню, <b>inline-редактирование напоминания и комментария прямо в строке</b>, задачные быстрофильтры, массовые «Присвоить статус» / «Установить комментарий». Операторы, знающие систему BG, сразу в ней ориентируются.</div>
|
||||
<div class="page">
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h2>Мои сделки</h2>
|
||||
<div class="stats"><span class="mono">128</span> сделок<span class="sep">·</span><span>показано 50</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filterbar">
|
||||
<span class="fld">Статусы <span class="caret">▾</span></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="taskchips">
|
||||
<span class="taskchip on">Дела на сегодня <b>3</b></span>
|
||||
<span class="taskchip">Просроченные <b>1</b></span>
|
||||
<span class="taskchip">Предстоящие <b>5</b></span>
|
||||
<span class="taskchip">Без задач <b>12</b></span>
|
||||
</div>
|
||||
<div class="bulkbar">
|
||||
<span>Выбрано: <b>2</b></span>
|
||||
<button class="btn">Присвоить статус ▾</button>
|
||||
<button class="btn">Установить комментарий</button>
|
||||
<span class="sp"></span>
|
||||
<button class="btn ghost" style="color:#ffffff;">Снять выбор</button>
|
||||
</div>
|
||||
<div class="tbl-card">
|
||||
<table id="tbl-b">
|
||||
<thead><tr>
|
||||
<th style="width:30px;"><input type="checkbox" /></th>
|
||||
<th style="width:26px;"></th>
|
||||
<th>Проект</th><th>Тег</th><th>Телефон</th><th>Ответственный</th>
|
||||
<th>Эл. почта</th><th>Напоминание</th><th>Комментарий</th><th>Статус</th>
|
||||
<th>Время отгрузки</th><th style="width:26px;"></th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ VARIANT C ============ -->
|
||||
<section class="variant" id="v-c">
|
||||
<div class="caption"><b>Вариант C — Лидерра-нативный инбокс.</b> Переосмысление под реселл-модель: переключатель <b>«Поток»</b> (хронологическая лента входящих лидов — видно, что лиды «капают» от поставщика) ↔ <b>«Воронка»</b> (канбан по стадиям). На карточке — крупный бейдж источника и цена лида (что списано тенанту). Лучше всего отражает «лиды по заказанным источникам», но это отдельный продуктовый заход.</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">41</span> в работе</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtoggle" data-sub="c">
|
||||
<button class="on" data-v="flow">⊞ Поток</button>
|
||||
<button data-v="kanban">▦ Воронка</button>
|
||||
</div>
|
||||
<div class="subview on" id="c-flow"><div class="feed" id="feed-c"></div></div>
|
||||
<div class="subview" id="c-kanban">
|
||||
<div class="kanban" id="kanban-c"></div>
|
||||
<div class="legend">Стадии группируют 14 статусов воронки. Карточки перетаскиваются между стадиями (drag & drop).</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ HYBRID ============ -->
|
||||
<section class="variant" id="v-h">
|
||||
<div class="caption"><b>Гибрид — паритет-таблица B + фишки C.</b> Основа — таблица паритета с inline-напоминанием/комментарием и звёздочкой, но: имя+телефон объединены в одну колонку «Лид», источник вынесен крупным бейджем, добавлена колонка <b>«Цена»</b> (списано тенанту из <span class="mono">lead_charges</span>), и есть переключатель «Таблица ↔ Поток». Сохраняет привычность BG и добавляет портально-специфичное.</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">128</span> всего</span><span class="sep">·</span><span>списано за месяц <span class="num mono">38 940 ₽</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtoggle" data-sub="h">
|
||||
<button class="on" data-v="table">▤ Таблица</button>
|
||||
<button data-v="flow">⊞ Поток</button>
|
||||
</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>
|
||||
<span class="fld">Период <span class="caret">▾</span></span>
|
||||
</div>
|
||||
<div class="taskchips">
|
||||
<span class="taskchip on">Сегодня <b>3</b></span>
|
||||
<span class="taskchip">Просрочено <b>1</b></span>
|
||||
<span class="taskchip">Без задач <b>12</b></span>
|
||||
</div>
|
||||
<div class="subview on" id="h-table">
|
||||
<div class="tbl-card">
|
||||
<table id="tbl-h">
|
||||
<thead><tr>
|
||||
<th style="width:30px;"><input type="checkbox" /></th>
|
||||
<th style="width:26px;"></th>
|
||||
<th>Лид</th><th>Источник</th><th>Статус</th><th>Ответственный</th>
|
||||
<th>Напоминание</th><th>Комментарий</th><th>Цена</th><th>Отгружен</th><th style="width:26px;"></th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subview" id="h-flow"><div class="feed" id="feed-h"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---- mock data: лиды, отгруженные поставщиком (хардкод-константы, не пользовательский ввод) ---- */
|
||||
const LEADS = [
|
||||
{name:'Алексей Громов', phone:'+7 916 234-56-78', project:'Окна-Люкс', tag:'OKNA', b:'B2', type:'Звонки', status:'Новый', resp:'А. Петрова', ri:'АП', email:'', reminder:'', comment:'', shipped:'12 мин назад', price:420},
|
||||
{name:'Марина Ковалёва', phone:'+7 903 887-12-09', project:'Двери-Маркет', tag:'DVERI', b:'B1', type:'Сайт', status:'Переговоры', resp:'И. Сидоров', ri:'ИС', email:'m.kov@mail.ru', reminder:'сегодня 16:00', comment:'Перезвонить после обеда', shipped:'1 ч назад', price:380},
|
||||
{name:'', phone:'+7 925 110-44-87', project:'Кухни-Про', tag:'KUHNI', b:'B3', type:'СМС', status:'Недозвон', resp:'А. Петрова', ri:'АП', email:'', reminder:'завтра 10:00', comment:'', shipped:'3 ч назад', price:260},
|
||||
{name:'Сергей Власов', phone:'+7 999 002-77-31', project:'Окна-Люкс', tag:'OKNA', b:'B2', type:'Звонки', status:'Ожидаем оплаты', resp:'М. Орлов', ri:'МО', email:'', reminder:'', comment:'Счёт выставлен', shipped:'5 ч назад', price:420},
|
||||
{name:'Гость #4471', phone:'+7 911 556-23-90', project:'Ремонт-Сервис', tag:'REM', b:'B1', type:'Сайт', status:'Горячий', resp:'И. Сидоров', ri:'ИС', email:'', reminder:'сегодня 12:30', comment:'Готов к замеру', shipped:'вчера', price:510},
|
||||
{name:'Ольга Дроздова', phone:'+7 962 334-88-21', project:'Кухни-Про', tag:'KUHNI', b:'B3', type:'СМС', status:'Оплачено', resp:'М. Орлов', ri:'МО', email:'olga.d@gmail.com', reminder:'', comment:'', shipped:'вчера', price:260},
|
||||
{name:'Павел Н.', phone:'+7 905 778-31-02', project:'Двери-Маркет', tag:'DVERI', b:'B1', type:'Сайт', status:'Просмотрено', resp:'', ri:'', email:'', reminder:'', comment:'', shipped:'вчера', price:380},
|
||||
{name:'Ирина Соколова', phone:'+7 916 445-90-12', project:'Окна-Люкс', tag:'OKNA', b:'B2', type:'Звонки', status:'Закрыто и не реализовано', resp:'А. Петрова', ri:'АП', email:'', reminder:'', comment:'Дорого для клиента', shipped:'2 дн назад', price:420}
|
||||
];
|
||||
|
||||
const ST = {
|
||||
'Новый':['#e3f0ec','#0F6E56'], 'Просмотрено':['#e8ecf2','#4a5b73'], 'Проработан':['#e9e7f5','#4a3f8a'],
|
||||
'База':['#ecebe6','#6b6356'], 'Недозвон':['#fbeede','#9a6b16'], 'Переговоры':['#e2ecf6','#2c5a86'],
|
||||
'Ожидаем оплаты':['#fce8d8','#b3601a'], 'Партнёрка':['#f0e6f3','#7a3f86'], 'Оплачено':['#e0f0e2','#2f7d3a'],
|
||||
'Закрыто и не реализовано':['#efe9e9','#8a5a5a'], 'Тест-драйв':['#e0f0f2','#2a7d86'], 'Горячий':['#fce4e2','#c0392b'],
|
||||
'На замену':['#fbeede','#9a6b16'], 'Конечный недозвон':['#e8e6e3','#5a544c']
|
||||
};
|
||||
const BCOL = {B1:'#0F6E56', B2:'#c8821a', B3:'#6a4a9c'};
|
||||
|
||||
/* helpers — build DOM via createElement, без innerHTML */
|
||||
function el(tag, cls, txt){ const e=document.createElement(tag); if(cls) e.className=cls; if(txt!=null) e.textContent=txt; return e; }
|
||||
|
||||
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;
|
||||
}
|
||||
function srcEl(l, big){
|
||||
const e = el('span','src'+(big?' big':''));
|
||||
const dot = el('span','dot'); dot.style.background=BCOL[l.b]; e.appendChild(dot);
|
||||
e.appendChild(el('span','b',l.b));
|
||||
e.appendChild(el('span','ty','· '+l.type));
|
||||
return e;
|
||||
}
|
||||
function avaEl(l, sm){
|
||||
const ini = (l.name||'?').replace('Гость #','Г').split(' ').map(p=>p[0]).join('').slice(0,2).toUpperCase();
|
||||
return el('span','ava'+(sm?' sm':''), ini);
|
||||
}
|
||||
function nameOf(l){ return l.name || 'Без имени'; }
|
||||
function respCellEl(l){
|
||||
if(!l.resp) return el('span','inline','—');
|
||||
const w = el('span','rem'); w.appendChild(avaEl(l,true)); w.appendChild(document.createTextNode(' '+l.resp));
|
||||
return w;
|
||||
}
|
||||
function remEditEl(l){
|
||||
if(l.reminder){
|
||||
const due = /сегодня|завтра/.test(l.reminder);
|
||||
return el('span','rem'+(due?' due':''),'⏰ '+l.reminder);
|
||||
}
|
||||
return el('span','inline-edit','+ напоминание');
|
||||
}
|
||||
function cmtEditEl(l){
|
||||
return l.comment ? el('span','inline set',l.comment) : el('span','inline-edit','+ комментарий');
|
||||
}
|
||||
function td(child){ const c=el('td'); if(child!=null){ child instanceof Node ? c.appendChild(child) : (c.textContent=child); } return c; }
|
||||
function cell(){ const c=el('td'); for(const n of arguments){ if(n!=null) c.appendChild(n instanceof Node?n:document.createTextNode(n)); } return c; }
|
||||
function checkbox(checked){ const i=el('input'); i.type='checkbox'; if(checked) i.checked=true; return i; }
|
||||
function starEl(on){ return el('span','star'+(on?' on':''), on?'★':'☆'); }
|
||||
function leadCell(l){
|
||||
const w=el('div','lead'); w.appendChild(avaEl(l));
|
||||
const d=el('div'); d.appendChild(el('div','nm',nameOf(l))); d.appendChild(el('div','ph mono',l.phone));
|
||||
w.appendChild(d); return w;
|
||||
}
|
||||
|
||||
/* ---- variant A table ---- */
|
||||
(function(){
|
||||
const tb=document.querySelector('#tbl-a tbody');
|
||||
LEADS.forEach(l=>{
|
||||
const tr=el('tr','row');
|
||||
tr.appendChild(td(checkbox()));
|
||||
tr.appendChild(td(leadCell(l)));
|
||||
const srcTd=el('td'); srcTd.appendChild(el('span','inline set',l.project)); srcTd.appendChild(el('br')); srcTd.appendChild(srcEl(l)); tr.appendChild(srcTd);
|
||||
tr.appendChild(td(pillEl(l.status)));
|
||||
tr.appendChild(td(respCellEl(l)));
|
||||
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)));
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- variant B table (паритет) ---- */
|
||||
(function(){
|
||||
const tb=document.querySelector('#tbl-b tbody');
|
||||
LEADS.forEach((l,i)=>{
|
||||
const tr=el('tr','row');
|
||||
tr.appendChild(td(checkbox(i<2)));
|
||||
tr.appendChild(td(starEl(i===4)));
|
||||
tr.appendChild(td(el('span','inline set',l.project)));
|
||||
tr.appendChild(td(el('span','mono',l.tag)));
|
||||
const ph=el('td'); ph.className='mono'; ph.textContent=l.phone; tr.appendChild(ph);
|
||||
tr.appendChild(td(respCellEl(l)));
|
||||
tr.appendChild(td(l.email?el('span','inline set',l.email):el('span','inline','—')));
|
||||
tr.appendChild(td(remEditEl(l)));
|
||||
tr.appendChild(td(cmtEditEl(l)));
|
||||
tr.appendChild(td(pillEl(l.status)));
|
||||
tr.appendChild(td(el('span','inline',l.shipped)));
|
||||
tr.appendChild(td(el('span','dots','⋮')));
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- hybrid table ---- */
|
||||
(function(){
|
||||
const tb=document.querySelector('#tbl-h tbody');
|
||||
LEADS.forEach((l,i)=>{
|
||||
const tr=el('tr','row');
|
||||
tr.appendChild(td(checkbox()));
|
||||
tr.appendChild(td(starEl(i===4)));
|
||||
tr.appendChild(td(leadCell(l)));
|
||||
const srcTd=el('td');
|
||||
const pj=el('div',null,l.project); pj.style.cssText='font-size:12px;margin-bottom:3px;color:var(--muted)';
|
||||
srcTd.appendChild(pj); srcTd.appendChild(srcEl(l)); tr.appendChild(srcTd);
|
||||
tr.appendChild(td(pillEl(l.status)));
|
||||
tr.appendChild(td(respCellEl(l)));
|
||||
tr.appendChild(td(remEditEl(l)));
|
||||
tr.appendChild(td(cmtEditEl(l)));
|
||||
tr.appendChild(td(el('span','price',l.price+' ₽')));
|
||||
tr.appendChild(td(el('span','inline',l.shipped)));
|
||||
tr.appendChild(td(el('span','dots','⋮')));
|
||||
tb.appendChild(tr);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- flow feed (C + hybrid) ---- */
|
||||
function buildFeed(target){
|
||||
LEADS.forEach(l=>{
|
||||
const card=el('div','lcard');
|
||||
card.appendChild(el('div','when mono',l.shipped));
|
||||
card.appendChild(srcEl(l,true));
|
||||
const main=el('div','main');
|
||||
main.appendChild(el('div','nm',nameOf(l)));
|
||||
main.appendChild(el('div','ph mono',l.phone+' · '+l.project));
|
||||
card.appendChild(main);
|
||||
const meta=el('div','meta');
|
||||
if(l.reminder) meta.appendChild(el('span','rem due','⏰ '+l.reminder));
|
||||
meta.appendChild(el('span','price',l.price+' ₽'));
|
||||
meta.appendChild(pillEl(l.status));
|
||||
meta.appendChild(l.resp?avaEl(l,true):el('span','inline','—'));
|
||||
card.appendChild(meta);
|
||||
target.appendChild(card);
|
||||
});
|
||||
}
|
||||
buildFeed(document.getElementById('feed-c'));
|
||||
buildFeed(document.getElementById('feed-h'));
|
||||
|
||||
/* ---- kanban (C) ---- */
|
||||
const STAGES = [
|
||||
['Новые',['Новый','Просмотрено']],
|
||||
['В работе',['Проработан','База','Недозвон']],
|
||||
['Переговоры',['Переговоры','Горячий','Тест-драйв']],
|
||||
['Оплата',['Ожидаем оплаты','Оплачено','Партнёрка']],
|
||||
['Закрыто',['Закрыто и не реализовано','Конечный недозвон','На замену']]
|
||||
];
|
||||
(function(){
|
||||
const k=document.getElementById('kanban-c');
|
||||
STAGES.forEach(([title,sts])=>{
|
||||
const cards=LEADS.filter(l=>sts.includes(l.status));
|
||||
const col=el('div','kcol');
|
||||
const h=el('h4'); h.appendChild(document.createTextNode(title+' ')); h.appendChild(el('span',null,String(cards.length)));
|
||||
col.appendChild(h);
|
||||
const body=el('div','body');
|
||||
if(!cards.length) body.appendChild(el('span','inline','пусто'));
|
||||
cards.forEach(l=>{
|
||||
const kc=el('div','kcard');
|
||||
kc.appendChild(el('div','nm',nameOf(l)));
|
||||
kc.appendChild(el('div','ph mono',l.phone));
|
||||
const ft=el('div','ft'); ft.appendChild(srcEl(l)); ft.appendChild(el('span','price',l.price+' ₽'));
|
||||
kc.appendChild(ft); body.appendChild(kc);
|
||||
});
|
||||
col.appendChild(body); k.appendChild(col);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- tab switching ---- */
|
||||
document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',()=>{
|
||||
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
||||
document.querySelectorAll('.variant').forEach(x=>x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
document.getElementById('v-'+t.dataset.tab).classList.add('active');
|
||||
window.scrollTo({top:0,behavior:'smooth'});
|
||||
}));
|
||||
|
||||
/* ---- sub-toggles (C: flow/kanban, H: table/flow) ---- */
|
||||
document.querySelectorAll('.subtoggle').forEach(grp=>{
|
||||
const sub=grp.dataset.sub;
|
||||
grp.querySelectorAll('button').forEach(b=>b.addEventListener('click',()=>{
|
||||
grp.querySelectorAll('button').forEach(x=>x.classList.remove('on'));
|
||||
b.classList.add('on');
|
||||
grp.parentElement.querySelectorAll('.subview').forEach(v=>v.classList.remove('on'));
|
||||
document.getElementById(sub+'-'+b.dataset.v).classList.add('on');
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user