Commit Graph

136 Commits

Author SHA1 Message Date
Дмитрий 277800149f @
fix(sales): строгое разделение двух ролей начальника — экраны «мои» больше не показывают отдел

Начальник продаёт сам и руководит отделом. Правило «начальник видит всё»
применялось к ЧЕЛОВЕКУ, а не к экрану, поэтому разделы меню врали:

- «Потенциальные клиенты» показывали все карточки отдела — точную копию
  «Воронки отдела» (это владелец и заметил);
- «Мои клиенты» и «Сводка» — всех клиентов отдела;
- «Привязать клиента» — очередь заявок отдела, причём в ЧУЖОМ формате
  ({pending, history} вместо {data}), форма получала не те данные.

Новое правило: роли разделяются по ЭКРАНУ. Любой запрос по умолчанию отдаёт
только личное — включая начальника. Весь отдел выдаётся только по явному
?scope=department, и просят его только экраны раздела НАЧАЛЬНИК. Менеджеру
параметр ничего не даёт: проверка по роли, не по параметру.

ownedTenantIds теперь ВСЕГДА личные привязки (тип сузился с ?array до array),
добавлен visibleTenantIds для области видимости запроса.

Правом начальника осталось открыть ЛЮБУЮ карточку — кандидата и клиента:
иначе из «Воронки отдела» не открылась бы карточка чужого менеджера.
Ограничены только списки, не доступ к записи.

Следствие: у начальника сейчас 0 своих кандидатов и клиентов, поэтому его
личные экраны станут пустыми — это правильно, а не поломка.

Pest 272/272, Vitest зелёный, vue-tsc чист, Larastan без своих ошибок.
Спека — §23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 15:59:56 +03:00
Дмитрий e9edb206c8 @
feat(sales): кабинет начальника — единый период (без 500) + пополнения баланса

Экраны начальника отстали от вчерашних правок кабинета менеджера (§16–§21).

1. Живой баг: «Произвольный» период до выбора дат ронял запрос в 500 на пяти
   точках из шести (сводка отдела, доход, результативность, выплаты, тарифы).
   Разбор периода вынесен в трейт ResolvesSalesPeriod: 422 вместо падения,
   период по умолчанию d30 вместо this — как показывает сам PeriodPicker.

2. Пополнения баланса (topup_rub) добавлены в dashboard/overview (kpi +
   строки менеджеров) и managers/performance. На экранах: плашка «Пополнили
   баланс» в сводке отдела и колонка «Пополнили ₽» в обеих таблицах
   результативности. Из подписей убрано «(мес)» — период больше не месяц.

3. «Воронка отдела» правок не потребовала: она рендерит те же доску и карточку,
   что экран менеджера, а карточка сама грузит журнал и сохраняет контакты.

Тесты: SalesPeriodRequestTest проходит по всем шести точкам сразу.
Pest 263/263, Vitest 1420/1420, vue-tsc чист.
Спека — §22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 15:20:44 +03:00
Дмитрий 6416ec3789 @
feat(sales): периоды сегодня/вчера/7/30 дней календарём, пополнения за период, скролл доски наверх

Замечания владельца по сводке и доске.

ПЕРИОДЫ:
- Резолвер понимает today/yesterday/d7/d30; «7 дней» = сегодня и 6 предыдущих.
  Месячные this/prev/prev2 сервер принимает по-прежнему.
- По умолчанию 30 дней.
- ПОЧИНЕНО: выбор «Произвольный» без дат ронял запрос (500). Теперь понятный 422,
  а на фронте период применяется только когда отмечены ОБЕ даты.
- Произвольный выбирается календарём-диапазоном, не руками; порядок дат неважен.

ПОПОЛНЕНИЯ ЗА ПЕРИОД (переиспользован готовый topupsRub):
- Сводка: плашка «Пополнили баланс» рядом с «Σ баланс».
- Мои клиенты: колонка «Пополнил».
  Заодно даёт число, которое видимо меняется при смене периода.

ДОСКА: горизонтальная полоса прокрутки поднята НАД колонками (колонки высокие,
системная полоса уезжала за экран); синхронизация в обе стороны, ResizeObserver
на приезжающие карточки.

Спека §21 (включая §21.4 — как по коду заполняется «Требуют внимания»).
Гейты: Pest 257/257 sales+unit, Vitest 1417, vue-tsc чисто, Larastan 0 в своих. TDD.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 14:29:18 +03:00
Дмитрий 1e0442facf @
feat(sales): результат разговора и его содержание — одной записью в журнале

Владелец: «объедини в одну запись: договорились… сверху, внизу текст».
Было две записи подряд — в ленте читалось как два события, хотя разговор один.

- БД v8.75: sales_prospect_notes.title VARCHAR(500) NULL — что решили.
- Одна запись kind=note: title = «Договорились на созвон 20.07.2026 17:35»,
  body = краткое содержание. Есть результат без содержания — как раньше,
  одна запись kind=stage без заголовка. Ручная заметка — без заголовка.
- В ленте заголовок строкой сверху, под ним текст.
- Старые парные записи задним числом не сливаем — историю не переписываем.

Спека §20.2. Гейты: Pest 240/240 sales, Vitest 64/64 воронка, vue-tsc чисто,
Larastan 0 в своих файлах. TDD RED→GREEN.
🪤 Тесты DOM для v-dialog: контент уезжает телепортом в body — искать через
document.body.querySelector, а не wrapper.find.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 14:00:55 +03:00
Дмитрий 138550207a @
feat(sales): содержание разговора — вместе с результатом; недозвону дата перезвона

Владелец после первого показа карточки: «куда звонить убери; договорились на
созвон — ниже краткое содержание, и оно копится в Историю разговоров, свежие
в начало; недозвон — поставь дату следующего перезвона».

- Блок «Куда звонить» убран: дублировал строку «Телефон общий» из данных фирмы.
- «Краткое содержание разговора» переехало последним полем в блок «Результат
  разговора» (было отдельное поле с кнопкой слева — два места для одного
  действия). Уходит параметром summary вместе с результатом, ложится в журнал
  отдельной записью kind=note. Пустое — не пишем.
- Порядок внутри одного сохранения: сначала автозапись про этап, затем
  содержание → у него больший id и в ленте оно оказывается НАД этапом.
- Недозвон получил необязательное поле «Когда перезвонить»: пишется в
  next_call_at (видно на плитке) и дописывается в автозапись журнала.

Спека §20. Гейты: Pest 236/236 sales, Vitest 59/59 воронка, vue-tsc чисто,
Larastan 0 в своих файлах. TDD RED→GREEN.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 13:20:27 +03:00
Дмитрий 41fbc4491d @
feat(sales): журнал разговоров по кандидату + переделка обоих окон воронки

Владелец: «краткое содержание разговора писать, чтобы не потерять историю»
и «переделай обоих дизайн, очень не практично, спроси у перплексити».

ЖУРНАЛ (БД v8.74, таблица sales_prospect_notes, append-only):
- kind=note — менеджер написал руками; kind=stage — автозапись о переезде
  по стадии (взял в работу, созвон, недозвон, отказ, регистрация).
  Повторное открытие карточки журнал не засоряет.
- GET/POST /api/sales/prospects/{id}/notes; лента свежими сверху, грузится
  при открытии карточки, а не вместе с доской.
- PATCH /prospects/{id} БОЛЬШЕ НЕ принимает notes: он молча затирал прошлую
  запись — ровно та потеря истории, ради которой журнал и появился.
  Нашёл rls-reviewer, закрыто тестом. Старые notes перенесены в журнал.

ДИЗАЙН (по разбору Pipedrive/HubSpot/Salesforce через Perplexity):
- Карточка 1100px, две колонки. Слева «что за фирма» + история разговоров
  с полем «о чём поговорили». Справа зона действия: «Куда звонить» (номер
  крупно, ссылкой tel:), «Результат разговора» своим фоном, контактные лица.
  Кнопки внизу окна. Пустая история объясняет, что делать.
- Форма создания разбита на разделы: Компания / Контактные лица / Заметка;
  Юрлицо и Город в одну строку; обязательных полей по-прежнему два.

Спека §19. Гейты: Pest 231/231 sales, Vitest 60/60 воронка, vue-tsc чисто,
Larastan 0 в своих файлах, rls-reviewer PASS. TDD RED→GREEN.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 13:04:02 +03:00
Дмитрий 68428f16c6 @
feat(sales): карточка кандидата — девять полей владельца + правка контактных лиц

Владелец задал список полей карточки и попросил дать менеджеру записывать
контактных лиц прямо в карточке: поиск отдаёт 5-7 ПРЕДПОЛАГАЕМЫХ номеров
директора, живой из них выясняется обзвоном; а если директор перенаправил
к маркетологу — менеджер фиксирует и его, людей может быть 2-3.

- Карточка показывает РОВНО девять полей в его порядке: Юрлицо, Сайт, ИНН фирмы,
  Адрес, Телефон общий, Каналы, Бюджет, Директор, Тел. директора (предполагаемый).
  Убраны Оценка, Достоверность, Реклама, Запросы в Директе, ОГРН, Статус юрлица,
  Личный ИНН директора, Почта директора — данные остаются в payload.
- PATCH /api/sales/prospects/{id}/contacts — список заменяется целиком, пустой
  стирает всех; права как у update() (менеджер свои, начальник любые); главный
  телефон карточки из поиска не трогается.
- ProspectContactsEditor.vue + utils/prospectContacts.ts — один редактор контактов
  на диалог создания и карточку; диалог создания переведён на него.
- Закрывает хвост §16: у 24 живых карточек из поиска контактных лиц не было,
  теперь дозаполняются руками.

Спека §18. Гейты: Pest 223/223 sales, Vitest 196 файлов / 1397 тестов,
vue-tsc чисто, Larastan 0 в своих файлах. TDD RED→GREEN на каждом шаге.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 12:29:04 +03:00
Дмитрий 1011a37e77 @
feat(sales): кандидат — ИНН обязателен, юрлицо отдельно от бренда, контактные лица

Замечание владельца по форме «Добавить кандидата»: телефонов бывает много, нужны
ФИО и должность человека, ИНН обязателен, бренд и юрлицо — разные вещи, юрлицо
подтягивать по ИНН через ДаData.

- БД v8.73: sales_prospects.legal_name VARCHAR(500) + contacts JSONB DEFAULT [].
  phone остаётся «главным телефоном»; inn в БД по-прежнему nullable — карточки
  из поиска и 24 живые строки могут быть без него.
- store(): ИНН обязателен + контрольная сумма ФНС; повтор ИНН у того же
  менеджера — понятный 422 вместо 500 от уникального индекса; пустые контакты
  и телефоны отсекаются; phone = первый телефон первого контакта.
- POST /api/sales/prospects/lookup-inn — юрлицо/город по ИНН через готовый шов
  PartyLookup (тот же, что в «Реквизитах»). Ничего не сохраняет; ДаData молчит
  или упала — заводим руками.
- Диалог создания: ИНН* с кнопкой «Найти», бренд*, юрлицо, блок контактных лиц
  (+человек / +телефон). Карточка показывает юрлицо и контакты со ссылками tel:.

Спека §16, CHANGELOG_schema v8.73. Гейты: Pest 28/28, Vitest 38/38,
Larastan 0 в своих файлах, rls-reviewer PASS 4/4.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 11:39:48 +03:00
Дмитрий d640631d3f feat(sales): фронт — колонка «Взят в работу», бейдж происхождения, свои кандидаты
Колонка «Взят в работу» сразу после «Новые» (9 колонок). Открытие карточки из
«Новые» само шлёт action=opened; в карточке кнопка «Вернуть в Новые» (только на
этой стадии). Бейдж на карточке: «своя» / «от начальника». Новый диалог
«Добавить кандидата» (название обязательно, пустые поля → null) + кнопка на доске
менеджера. У начальника селект «Происхождение» (все/из поиска/свои).
Гейты: фронт 30/30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 10:57:15 +03:00
Дмитрий b7cfee0a0d feat(dashboard-ui): строка «Прокси автоподбора» — срок «до …», остаток дней, статус
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 08:31:12 +03:00
Дмитрий 3d4cdbbda3 feat(dashboard): sales_finder+xfetch+keyso — одна карточка «Поиск клиентов» 2026-07-16 17:17:13 +03:00
Дмитрий 9ecdd539bf feat(dashboard): фронт онлайн-балансов — фон при открытии + кнопка ⟳ + колонка «Проверено»
Фаза D плана 2026-07-16-external-services-online-monitoring:
- api: refreshDashboardBalances(service?) — POST фон/кнопка.
- AdminDashboardView: метки/иконки 5 новых сервисов; LIVENESS_ONLY += self_render,
  sales_finder; serviceStatus → «жив» когда ok без денежного баланса; фон
  refreshLiveBalances() при каждом открытии дашборда; кнопка ⟳ у каждого сервиса
  (per-row спиннер); колонка «Проверено» (checkedLabel).
Тесты: Vitest AdminDashboardView 17/17 (2 новых); type-check по своим файлам чист.

NB: larastan-хук исключён — чужой незакоммиченный файл в общей папке (параллельная сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:50:01 +03:00
Дмитрий 6ceec17439 feat(sales): Этап 3 — автожизнь воронки (регистрация + джоба стадий по деньгам)
Действие «Зарегистрировался» (PATCH action=registered): email→tenant, привязка
SalesClientAssignment со снимком тарифа, linked_tenant_id+stage=registered; клиент
занят другим → 422. Диалог карточки: пункт «Зарегистрировался» + поле e-mail.
SalesProspectsAdvanceJob (каждые 15 мин, pgsql_admin): по balance_transactions
считает стадию — Σtopup≥30000→user, >0→topped_up, есть расход при 0 topup→testing,
иначе registered; ручные/отказные стадии не трогает. Схема НЕ меняется.
Гейты: бэк 19/19, фронт 9/9, Larastan 0. 🪤 property $connection конфликтовал с
трейтом Queueable → переименовал в $dbConnection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:00:16 +03:00
Дмитрий 2f6e88e784 feat(sales): замечания владельца по воронке — богатая карточка, сортировка созвонов, счётчики фильтра
Три правки после демо Этапа 1 (все по TDD):
1. Карточка показывает ВСЕ данные поиска из payload (тип ProspectPayload 1:1 с
   dataclass Firm; computed infoRows рендерит юрлицо, директора+личный ИНН,
   контакты, бюджет Директа вилкой, каналы/коллтрекинг, оценку). Демо-сидер
   кладёт полный синтетический payload.
2. Начальнику отдаётся manager_counts; фильтр показывает «Имя (N)».
3. Колонка «Переговоры» сортируется по next_call_at ASC (просроченные сверху).

Гейты: бэк 12/12, фронт 19/19, Larastan 0. НЕ выкачено (ждём разрешения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:52:54 +03:00
Дмитрий ead1335242 feat(sales): фронт — API прогнозов + утилита стадий/просрочки
Этап 1 Task 9. PROSPECT_STAGES (8 колонок), stageMeta, isOverdue;
listProspects/updateProspect в api/sales.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:38:07 +03:00
Дмитрий 42e907c882 Merge gitea/main into feat/sales-finder — сведение с боевым перед выкатом
Ветка разошлась с боевым 28.06 (334 коммита в main / 215 у нас). Влито ВСЁ боевое:
автоподбор конкурентов, мобильный адаптив портала, свой учёт посетителей, мониторинг
внешних сервисов, фиксы поставщика/биллинга/бота/разборов.

ПОБАЙТОВАЯ СВЕРКА: из 1008 файлов, изменённых боевым, 989 совпадают точно;
19 отличаются — все с нашей законной работой (обе стороны внутри). Затёртых — 0.

24 конфликта разобраны вручную. Ключевое:
- VerifySupplierOrderJob — взята БОЕВАЯ версия (фикс инцидента 11-12.07: площадка
  берётся из src, а не из имени; наша была старой и вернула бы баг, терявший заявки).
- SyncSupplierProjectsJobTest — 15 боевых тестов + наш уникальный (limit-1 → только B1).
- routes/web, router/index, config/services, bootstrap/app — обе стороны сложены.
- NewProjectDialog — зелёные дни недели (наше) + мобильная раскладка (боевое).
- CHANGELOG схемы — номера версий столкнулись, наши перенумерованы в v8.67-v8.70.
- composer — обе зависимости (laravel-dompdf наш + geoip2 боевой).

Гейты: бэкенд 2907/2911 (0 падений), Larastan 0, фронт 1333/1333, сборка OK.
Baseline статанализа принял пре-существующий долг боевого кода (автоподбор/чат).
@mixin в 26 моделях — требование статанализа, dev-докблок, на рантайм не влияет.

Откат: git reset --hard pre-merge-main-20260714

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:49:28 +03:00
Дмитрий c0487ddaa5 fix(billing,ux): портал видит отмену платежа и перестаёт молчать в формах
Разбор живого клиента (стоматология, Красноярск, 14.07): 2 часа настраивал 26
проектов, получил 14 отказов в трёх формах и ушёл, не заплатив.

Деньги:
- отменённый шлюзом платёж больше не висит «ожидает» вечно: закрываем как failed
  с причиной (PaymentSettlementService — общий путь для webhook и крона);
- billing:reconcile-payments каждые 5 минут сам спрашивает шлюз про зависшие
  pending. Побочно страхует от ПОТЕРИ ДЕНЕГ: если webhook не дойдёт, оплаченный
  платёж всё равно зачислится;
- кабинет говорит правду: «Оплата не завершена» + «Оплатить снова» вместо
  «баланс обновится автоматически» (GET /api/billing/last-payment).

🔴 RLS-мина (поймана валидатором ДО выката): UPDATE при отмене шёл без
tenant-контекста → на проде тронул бы 0 строк, а портал рапортовал бы «отменено».
Тесты слепы (тестовая БД под postgres). Регресс-тест проверяет ПОРЯДОК:
SET LOCAL tenant ДО UPDATE. Тот же класс, что инциденты 07.07 и 12.07.

Формы (клиент бился и уходил):
- удаление проекта со сделками: причина показывается на месте + кнопка
  «Поставить на паузу» (раньше 422 улетал в никуда — 4 попытки впустую);
- создание проекта: ошибка по дням недели больше не молчит (у поля не было
  места для показа — 2 немых отказа);
- автоподбор «Добавить вручную»: показываем причину от сервера (был голый
  catch {}), длинные ссылки 2ГИС/Яндекс.Карт принимаются — трекинг-хвост срезаем
  сами. Воспроизведено тестом: именно длинная ссылка давала 3 отказа подряд.

Наблюдаемость: причины отказов пишутся в журнал (маршрут, tenant, ИМЕНА полей;
значений нет — 152-ФЗ). Уровень warning: на проде LOG_LEVEL=warning, info в
журнал не попадает вовсе. Робот-сверщик добавлен в реестр пульса.

Тесты: Pest 2475/2475, Vitest 1215/1215.
Выкачено на боевой 14.07.2026 ~13:00 МСК; сверка сразу закрыла 3 мёртвых платежа
(10 000 ₽, 5 000 ₽, 1 000 ₽).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:14:49 +03:00
Дмитрий b6b8d6e0d6 feat(visitors): раздел «Посетители» в админке — воронка, каналы, визиты, кабинет
Воронка подсвечивает красным шаг, где теряем больше всего людей. Гости с VPN
и хостингов показаны отдельным числом, а не подмешаны в конверсию. Город при
VPN честно помечается как недостоверный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:03:38 +03:00
Дмитрий d81cdaffac feat(autopodbor): кнопка «Собрать источники (N)» в подвале + яркий алерт баланса 2026-07-09 09:45:12 +03:00
Дмитрий 98614604af feat(сделки): колонки «Конкурент» и «Источник» со ссылками в автоподбор
Раздел «Сделки»: колонка «Источник» разбита на две — «Конкурент» (ссылка на
экран конкурента) и «Источник» (ссылка на «Настройки проекта»). В модалку
настроек добавлена кнопка пуск/стоп (срабатывает не закрывая окно).

- бэк DealController::index: батч-резолв конкурента+источника по
  deal.project_id → autopodbor_sources.created_project_id → competitor (без N+1)
- контракт ApiDeal/MockDeal + маппер: competitor_*/source_* поля
- DealsTable: 1 колонка → 2 (Конкурент/Источник), router-link, fallback без автоподбора
- AutopodborView: deep-link ?competitor=&project= открывает экран конкурента + модалку
- FieldCompetitorScreen: кнопка ⏸/▶ в модалке настроек + авто-открытие по ссылке

Тесты: бэк DealIndexTest +4 (31 ), фронт +новые (66 ). Дизайн не менялся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 07:56:30 +03:00
Дмитрий e9600fb3b1 feat(автоподбор): текстовые номера с сайта → «на сайте, проверьте» (unverified)
Второй пробел после фикса таймаута рендера: номера, написанные на сайте простым
текстом (не кликабельные tel:/schema, без коллтрекинга), терялись. Пример Центрофинанс:
8 800 200-00-10 («Телефон:» у юр. адреса), +7 931 106-54-50 (подвал) — движок брал
только tel:-ссылки.

- HtmlPhoneScanner: тело сканируем БЕЗ <script>/<style> (меньше шума из конфигов)
- CandidateBuilder: без трекера body-номера (не в code/visible) → kind 'text' «проверьте»
- SourceAggregator + DeepStudyCollector: text-only → phone_kind 'unverified' (не теряем,
  но и за 100% настоящий не выдаём); code/справочник перебивают → 'real'
- фронт: бейдж «⚠ на сайте — проверьте» (amber), сортировка ниже настоящих, легенда
- 7 тестов (scanner/builder/aggregator/DeepStudy); вся ветка 493/493, build ок

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:23:37 +03:00
Дмитрий a3fd8a6e9a feat(autopodbor): «Удалить» на актуализации — мягкое отклонение находки
Кнопка «Удалить» на карточках «На актуализацию» рядом с Добавить/Заменить.
Отклоняет ТОЛЬКО находку (новый сайт/адрес): фирма поля остаётся, находка → архив,
её новые ключи запоминаются у фирмы поля (новая колонка dismissed_actualize_keys jsonb).
Классификатор вычитает отклонённые ключи → та же находка больше не всплывает; реально
другой новый сайт даст новый ключ → покажется. Не глушим по имени, конкурента не убираем.

Бэк: миграция + модель-каст + ProposalClassifier (вычитание) + эндпоинт dismissActualize
+ роут. Фронт: api + store + кнопка «Удалить». Тесты: классификатор 8/8, ветка 185/185,
фронт 30/30. Canon-sync schema.sql (v8.62) — follow-up (миграция idempotent-guarded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 15:50:59 +03:00
Дмитрий 0f30edb2b6 Merge remote-tracking branch 'gitea/main' into worktree-avtopodbor
# Conflicts:
#	app/config/services.php
#	app/tests/Pest.php
2026-07-06 14:50:58 +03:00
Дмитрий 2a16f5b1fc feat(sales): пересмотр состава тарифов портала продаж — 2 вида
Решение заказчика 03.07.2026: два вида тарифа вместо трёх — суточный
оклад daily_salary и процент от пополнений topup_step. Убраны
percent_oborot и fix_per_client.

Миграция 2026_07_03_120000 меняет CHECK sales_tariffs.kind, освобождает
FK sales_users.current_tariff_id и sales_client_assignments.tariff_id,
удаляет тарифы уходящих видов. SalesEarningsService для уходящих видов
возвращает 0 по default-ветке. Снимки привязок не трогаются.

Обновлены контроллеры тарифов и дохода, сервисы Earnings и Metrics,
фронт SalesTariffsView и api/sales.ts, демо-сид, тесты бэка и фронта,
CHANGELOG схемы v8.61 и спека портала.

Не на проде: ветка feat/sales-portal-demo.

Проверка: Sales Feature 169/169, Vitest SalesTariffs 10/10, Larastan 0.

Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
2026-07-05 19:25:19 +03:00
Дмитрий 2aedb54dc5 feat(autopodbor): журнал слияний + возврат поглощённой карточки
Слияние конкурентов больше не «чёрная дыра» (баг «САПС/Метрополис»: карточка
исчезла без следа, вернуть было нечем). Теперь каждое слияние пишет событие в
autopodbor_merge_events со СНИМКОМ поглощённых карточек (имя/сайт/телефоны/
справочники), кто и когда слил.

- Таблица autopodbor_merge_events (+RLS tenant_isolation, паттерн 1:1 с sources,
  rls-ревью OK; nullOnDelete на survivor_id/user_id — история не рушится).
- AutopodborCompetitorMerger::merge($tenantId,$ids,$name,$userId) пишет событие.
- GET /field/merge-events — история; POST /field/merge-events/{id}/restore —
  возврат поглощённых как предложения (защита от повторного возврата).
- Фронт: панель «🕘 История слияний» + «↩ Вернуть карточку».

Бэк 428/428, фронт 12/12, Pint чистый, build ок. schema.sql канон-синк всех
таблиц автоподбора — предвыкатный TODO (вся фича migration-only в этой ветке).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:52:43 +03:00
Дмитрий f7bd3e39af fix(autopodbor): окно слияния показывает выживающую карточку и её имя по умолчанию
Баг «САПС/Метрополис»: клиент переименовал свежую карточку и слил — а выжила
ДРУГАЯ (с проектами/в поле), имя переименованной затёрлось вслепую, карточка
пропала. Теперь окно «Найти дубли» помечает карточку, которая ОСТАНЕТСЯ
(«· останется»), и берёт ЕЁ имя как имя объединённого по умолчанию — клиент
видит исход до подтверждения и не теряет карточку случайно.

Бэкенд (survivor-метка в duplicate-groups + AutopodborCompetitorMerger::survivorId)
уже в 853c5a00; здесь фронт: api-тип, окно, метка, тест. Фронт 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:32:36 +03:00
Дмитрий 1258b9bbed feat(autopodbor): кросс-ящик дублей поле↔предложения на экране поля (доведён до рабочего)
Кнопка «Найти и объединить дубли» на экране поля теперь ловит и предложения,
дублирующие карточки поля (scope=cross): счётчик учитывает их, окно помечает
«· предложение», объединение сливает предложение в карточку поля (выживает поле,
merger не трогали). Бэкенд оставляет только группы с ≥1 карточкой поля — пары
чисто-предложений на экране поля не мусорят.

Файл-статус сессии — docs/superpowers/findings/2026-07-05-avtopodbor-session-status.md.

Feature 154/154, фронт 32/32, Pint чистый, билд собран.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:19:38 +03:00
Дмитрий d4a32f16b8 fix(autopodbor): проект принадлежит ровно одному источнику и понятная причина отказа
Инвариант «один проект = один источник»: при переносе проекта на новый источник
снимаем привязку со всех прочих источников — проект больше не может висеть в двух
конкурентах сразу (жёсткий баг с дублями). Плюс разбор ошибки в автоподборе
показывает конкретную причину бэкенда вместо общей фразы «не удалось».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:02:40 +03:00
Дмитрий 8fef74f25e fix(autopodbor): индикатор сбора подхватывается при возврате на карточку
Клиент ушёл со страницы во время сбора и вернулся — индикатор пропадал (жил только
на опросе в памяти браузера), казалось, что процесс исчез.

- competitor-эндпоинт отдаёт active_run (идущий по конкуренту сбор) — тест
- loadCompetitor подхватывает active_run в currentRun (не затирая итог завершённого)
- onMounted возобновляет опрос, если сбор ещё идёт → индикатор оживает, дожидаемся итога
- тесты: active_run (бэк), loadCompetitor подхват/сохранение, подхват опроса на экране

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 06:40:10 +03:00
Дмитрий 002e2c91c4 feat(autopodbor): повторный сбор источников — фронт
Кнопка «🔄 Собрать источники ещё раз» вместо мёртвой «✓ Источники собраны».
Окно предупреждает, что повтор платный (даже если нового нет). Честный итог
после сбора (N новых / M ранее удалённых). Блок «ранее удалённые — вернуть?»
с кнопкой «Вернуть» (box archived→proposal).

- RunDto.result + collectResultMessage (чистый хелпер) + тесты
- store.restoreSource + тест
- компонент RefoundDeletedSources + тест
- FieldCompetitorScreen: кнопка-повтор, честный итог, блок «вернуть», текст окна

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 06:08:53 +03:00
Дмитрий ac27a59baa feat(autopodbor): гейт реквизитов при создании проекта и защита от дублей источника
#1 requisites_required — показываем короткую форму реквизитов прямо в экране
Конкурентного поля и повторяем создание проекта после сохранения.

#4 дубль источника — при добавлении проверяем, нет ли такого же источника у другого
конкурента или уже созданного по нему проекта. Отдаём клиенту выбор: убрать дубль или
перенести проект к новому источнику по идентификатору. Проект не пересоздаётся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 05:13:28 +03:00
Дмитрий e46d1354fd feat(autopodbor): показ очереди и этапа клиенту во время сбора (фронт)
Компонент RunCollectStatus: «вы в очереди №N» (пока прогон ждёт) или «этап N
из M: подпись» (пока идёт). Встроен в оба окна сбора (конкуренты/источники) и
экран загрузки ручного изучения. RunDto получил queue_position и progress.
Никаких фейковых процентов — только реальные этапы движка. 4 теста Vitest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:31:19 +03:00
Дмитрий 1cdaf9cbbb feat(autopodbor): при слиянии выбор имени объединённого из найденных
Диалог дублей: селект имени среди выбранных к слиянию, по умолчанию первое.
Merger и ручка принимают опциональное имя. Обе экрана.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:07:59 +03:00
Дмитрий 1435e168c4 feat(autopodbor): формы создания/правки конкурента через элементы - правка не теряет карточки
Бэк: хелпер CompetitorElements нормализует элементы и выводит легаси site_url/directory_urls
из ВСЕХ карточек. Ручки manual/update принимают elements.
Фронт: компонент CompetitorElementsEditor встроен в формы создания и правки на экранах
Поле и Предложения. Прежний баг - пересбор directory_urls из двух полей - закрыт.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:32:16 +03:00
Дмитрий f295351504 feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00
Дмитрий 2f384f098a feat(sales): экран «Менеджеры отдела» (фронт)
Task 7.1b — SalesManagersView (начальник, #page-managers) поверх
GET/POST /api/sales/managers.

- Форма «Новый менеджер» (имя, e-mail, пароль, роль) → POST → snackbar +
  очистка + перезагрузка; ошибки (дубль email) через extractSalesErrorMessage.
- Список менеджеров: роль-чип, клиентов, выплачено всего, статус
  (Активен/Отпуск).
- api/sales.ts: listSalesManagers/createSalesManager. Роут /sales/managers
  со заглушки на экран.

Vitest 6/6, фронт-набор 1089 без регрессий, ESLint чист.

Фаза 7 завершена: начальник создаёт менеджеров (логин+пароль) и видит
список отдела.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:40:59 +03:00
Дмитрий 5266b10066 feat(sales): результативность менеджеров (фронт)
Task 6.2b — SalesPerformanceView (начальник, #page-performance) поверх
GET /api/sales/managers/performance.

- Поиск по имени (debounce) + таблица: менеджер (имя/email), клиентов,
  активных, лидов пришло, оборот, выплачено, заработал, статус
  (Активен/Отпуск). HelpHint на Оборот/Заработал. Период из salesPeriod store.
- Роут /sales/performance со заглушки на экран.

Vitest 5/5, фронт-набор 1083 без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:23:46 +03:00
Дмитрий d114c01005 feat(sales): сводка отдела (фронт)
Task 6.1b — SalesBossOverviewView (начальник, #page-boss-overview) поверх
GET /api/sales/dashboard/overview.

- 5 KPI (менеджеров с active/vacation, клиентов, Σ баланс, оборот отдела,
  выплачено за период).
- 4 кликабельные плитки-алерта → переходы: счета→/sales/invoices,
  заявки→/sales/requests, проблемы баланса→/sales/performance,
  провести выплату→/sales/payouts.
- Мини-таблица результативности менеджеров (клиенты/лиды/оборот/выплачено/
  заработал) с HelpHint. Период из salesPeriod store.
- Роут /sales/boss со заглушки на экран.

Vitest 12/12, фронт-набор 1078 без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:07:10 +03:00
Дмитрий eab540504a feat(sales): экран «Счета» — список + отметка оплаты (фронт)
Task 5.1b — SalesInvoicesView (начальник, #page-invoices) поверх готовых
эндпоинтов /api/sales/invoices.

- Таблица счетов (дата/номер/клиент/плательщик/сумма/статус/оплатить до),
  поиск с debounce + фильтр статуса, чипы статусов.
- Кнопка «Отметить оплаченным» (только issued/overdue) → диалог
  подтверждения → POST mark-paid → snackbar + перезагрузка. Зачисление
  баланса и Акт формирует бэк (InvoicePaymentService).
- api/sales.ts: listSalesInvoices/markSalesInvoicePaid. Роут /sales/invoices
  со заглушки на экран.

Vitest 5/5, фронт-набор 1066 без регрессий, ESLint чист.

Фаза 5 завершена: начальник отмечает оплату счёта прямо в портале продаж.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:45:17 +03:00
Дмитрий a5defbb12b feat(sales): экраны выплат и «Мой доход» (фронт)
Task 4.2b — два экрана поверх готовых эндпоинтов.

- SalesPayoutsView (начальник, #page-payouts): форма «Провести выплату»
  (менеджер из remaining, сумма, дата, комментарий) → POST /payouts →
  snackbar + перезагрузка; таблица «Остаток к выплате по менеджерам»
  (начислено/выплачено/остаток за период); «Журнал всех выплат».
- SalesIncomeView (менеджер, #page-income): 4 KPI (оборот/начислено/
  выплачено всего/к выплате), таблица «Начислено по клиентам» с ИТОГО,
  «Журнал выплат мне» с ИТОГО.
- api/sales.ts: listSalesPayouts/getSalesPayoutsRemaining/createSalesPayout/
  getSalesIncome. Роуты /sales/payouts и /sales/income со заглушек на экраны.

Vitest 9/9, фронт-набор 1061 без регрессий, ESLint чист.

Фаза 4 завершена: начальник проводит выплаты, менеджер видит начисления
и выплаты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:29:32 +03:00
Дмитрий 6757bdfff7 feat(sales): показ дохода «Заработал» в списке/карточке/сводке (фронт)
Task 3.3b — три экрана Фазы 1 показывают живой earned_rub вместо
заглушки «—»:
- «Мои клиенты»: колонка «Заработал» = комиссия по клиенту (или «—»
  без привязки).
- Карточка клиента: KPI «Вы заработали (период)».
- Сводка: KPI «Я заработал» = суммарная комиссия за период.
Тип earned_rub расширен до number|null. Vitest-моки/ассерты обновлены
под реальные значения. Фронт-набор 1052 зелёных, ESLint чист.

Фаза 3 завершена: доход менеджера считается по тарифу каждого клиента
и виден во всех экранах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:50:24 +03:00
Дмитрий 12be6af1d5 feat(sales): экран «Тарифы менеджеров» (фронт)
Task 3.2b — SalesTariffsView поверх готового каталога тарифов.

- 3 семейства-конструктора: «процент от пополнений» (ступени from–to–%,
  +добавить период), «оклад+процент» (оклад→params.base_salary, ставка→
  params.rate), «фикс за клиента» (порог/вознаграждение). Создание — POST,
  правка — PUT.
- Секция «Текущий тариф менеджера»: select тарифа + оклад (для оклад+процент),
  метрики периода и оценка «Начислено» (estimated_earned_rub); сохранение —
  assign. Оклад уходит в base_salary_rub только для percent_oborot.
- Период из salesPeriod store, перезагрузка по смене. Роут /sales/tariffs
  переключён со заглушки.

Vitest 9/9, полный фронт-набор без регрессий, ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:10:20 +03:00
Дмитрий 6dbc6b5bb0 feat(sales): экраны привязки клиентов и заявок (фронт)
Task 2.3b портала отдела продаж — два Vue-экрана поверх готовых
эндпоинтов /api/sales/attachments:

- SalesAttachView (менеджер, #page-attach): форма заявки по логину
  (e-mail/ИНН), живой баннер результата (отправлено/не найден/ошибка),
  таблица «Мои заявки».
- SalesRequestsView (начальник, #page-requests): очередь на решение
  с проверкой «свободен / уже за: …», кнопки Подтвердить/Переназначить
  /Отклонить (переназначение = тот же approve на бэке), история, счётчик.
- api/sales.ts: submitAttachment / listMyAttachments / listAttachmentQueue
  / decideAttachment + типы.
- Роуты /sales/attach и /sales/requests переключены со stub на реальные экраны.

Vitest: SalesAttach 5 + SalesRequests 7 = 12 зелёных; полный фронт-набор
не сломан. ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:22:49 +03:00
Дмитрий 2cf1c25756 feat(sales): экран «Сводка менеджера» (фронт) — Фаза 1 закрыта
Task 1.5b: SalesOverviewView по демо #page-overview — 5 KPI (клиентов+разбивка статусов, Σ баланс, лидов пришло, оборот, я заработал=«—» до Фазы 3), зелёная плашка-пояснение, таблицы «Требуют внимания» и «Топ клиентов по лидам» (клик → карточка), период из стора. getSalesOverview в api/sales.ts. Vitest 12/12, полный фронт 1031 без регрессий. Фаза 1 (чтение менеджера) завершена. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:15:45 +03:00
Дмитрий 9a43754b59 feat(sales): экран карточки клиента (фронт)
Task 1.4b: SalesClientCardView по демо #page-client-card — 5 KPI (баланс+запас, проектов, пришло/цель, средняя цена лида, заработано=«—» до Фазы 3), 2 колонки: проекты+лиды по дням+последние лиды (телефон маскирован бэком), профиль+активность. HelpHint на Запас/Средняя цена/Заработано. 403 «клиент не закреплён за вами». getSalesClientCard в api/sales.ts. Vitest 8/8, lint/type-check 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:43:43 +03:00
Дмитрий b2699f0d84 feat(y360): баланс почты Яндекс 360 — ручной ввод + кнопка Пополнить
email — денежный сервис; сумма вписывается в админке «Система» (Yandex360BalanceStore),
светофор по порогам, кнопка «Открыть оплату»/«Пополнить» → admin.yandex.ru/products.
Робот-скрейпер отклонён (SPA Яндекса враждебен ботам + автопополнение защищает баланс).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:34:13 +03:00
Дмитрий d15a7cfd93 feat(биллинг-фронт): баланс на запуске — сообщения в рублях, метка «не запущен», группы
Фронт для правила «баланс блокирует запуск, а не создание»:
- ProjectLimitOverloadDialog переформулирован в «Проект не запущен — пополните
  ~X ₽ или уменьшите объём» (сумма в рублях topup_rub).
- NewProjectDialog: создание всегда успешно; при launch.deferred показывает
  сообщение (не блок). Update-лимита 409 → тот же диалог.
- Возобновление (toggle-active) ловит 409 balance_insufficient (стор не
  переключает is_active), показывает диалог; постоянная метка «Не запущен —
  не хватает баланса» на карточке/в панели по preflight_blocked_at/balance_blocked.
- BulkActionsBar: сводка «Запущено N, отложено M — не хватает баланса» на resume.
- Автоподбор: CreateScreen показывает сводку запуска (рубли); FieldCompetitor
  массовые pause/resume через общий POST /api/projects/bulk (наследует BULK_MAX,
  слепок-защиту, балансовый гейт).
- ProjectResource отдаёт preflight_blocked_at.

Новые/обновлённые фронт-тесты зелёные. Пред-существующий красный baseline
фронт-набора (ErrorView/Settings/Legal/… + ProjectsView v-show) не трогался.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:04:16 +03:00
Дмитрий 58869f8dbd feat(sales): экран «Мои клиенты» (фронт)
Task 1.3b: SalesClientsView — таблица 11 колонок по демо #page-clients (Клиент/Тип/Активность/Баланс/Запас/Проектов/Пришло/Оборот/Тариф/Заработал/Статус), бейдж типа лица, чипы статуса (Триал/Активен/Просрочка/Приостановлен), HelpHint на терминах, деньги ru-RU + JetBrains Mono, перезагрузка при смене периода, клик по строке → карточка. Заработал=«—» до Фазы 3. listSalesClients в api/sales.ts. Vitest 6/6, lint/type-check 0. Один эскейп на сессию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 06:40:56 +03:00
Дмитрий 49695cee8c feat автоподбор Stage 2 актуализация — новый сайт фирмы поля через Добавить/Заменить в форму проекта
Секция «На актуализацию»: дельта-карточка показывает текущие сайты и карточки фирмы поля + новый сайт кликабельными ссылками, кнопки Добавить и Заменить ведут через подтверждение «Создать проект?» в существующую форму CreateScreen с подставленным новым источником. Имя фирмы берётся из поля, не перезатирается.

Бэкенд: endpoint POST /competitors/{id}/actualize action=add|replace new_site. add заводит сайт-источник рядом со старым, replace архивирует старые сайт-источники и заводит новый, при активном проекте по старому сайту возвращает 409 manage_via_project. /proposals обогащён полем matched с текущими сайтами и карточками совпавшей фирмы.

По TDD. Бэкенд автоподбора 254/254, фронт автоподбора 64/64, ESLint чисто, Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:27:00 +03:00
Дмитрий 2834ff0f5a feat(автоподбор): сверка находок со состоянием клиента — 3 группы предложений + скрытые дубли, мягкое удаление в архив, убран вычет себя
Stage 1: опознавалка фирмы по сайту/карточкам, ProposalClassifier new/actualize/archived/hidden, box=archived, bulk удалить всех ранее удалённых. Backend 249/249, front autopodbor 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:11 +03:00