-
pak222
2026-06-02
CRM-автоматизации: защита от дубликатов автозадач + cleanup команда. **Симптом:** user создал лид и быстро прокликал несколько этапов — получил **80+ дубликатов** автозадач. Корневая причина: автоматизации с `trigger_event=lead_stage_changed` и `condition=null` срабатывают на КАЖДУЮ смену этапа. При 5 stage_changed × 5 правил = 25 задач. У user-а получилось 80+ потому что user-кликал туда-обратно. **Фикс 1 — idempotency check в `CrmAutomationService::executeCreateTask`:** перед созданием LeadTask проверяем audit_logs (`action=crm_automation_executed`, `object_type=lead_task`, `created_at >= now()-24h`, `new_values LIKE %automation_id:N%`, `new_values LIKE %lead_id:M%`). Если есть запись — `return null` + Log::info(Pak222 CrmAutomation skipped duplicate) + AuditLog(crm_automation_skipped_duplicate). Метод теперь возвращает `?LeadTask` (раньше LeadTask). В `runAutomations` $stats[executed] не инкрементируется при null. **Фикс 2 — artisan команда `crm:dedupe-automation-tasks`** для cleanup существующих дубликатов. Опции: `--dry-run` (показать без изменений), `--hours=N` (окно default 720 = 30 дней). Логика: группирует audit-записи по ключу `(automation_id, lead_id, day)`, в группах с >1 LeadTask оставляет самый ранний id (наименьший), остальные → is_deleted=true с AuditLog `crm_automation_task_deduped`. Выводит line per group. Финальный counter дублей + удалённых. Log::info в конце. **Деплой:** `git pull && php artisan optimize:clear`. Для cleanup существующих: `php artisan crm:dedupe-automation-tasks --dry-run` (посмотреть) → `php artisan crm:dedupe-automation-tasks` (применить). **Без миграций.** Report: docs/reports/UPD_pak222_RESULTS.md.
-
pak221
2026-06-02
/chats — табы (Проекты / Департаменты / Личные) + красный bell-badge суммы непрочитанных. **#8a Табы вместо плоского списка.** Раньше pak123 группировал чаты по типу заголовком — при большом числе проектов всё в одну колонку непрактично. Pak221 — таб-навигация в sidebar: 3 таба «Проекты» (TYPE_PROJECT), «Департаменты» (TYPE_DEPARTMENT), «Личные» (TYPE_DIRECT + TYPE_COMPANY). При клике на таб — `?chat_tab=...` query, активный таб подсвечивается синим border-bottom. На каждой табе — красный counter-badge суммы непрочитанных в группе. Дефолтный таб — где есть unread (приоритет projects → direct → departments), иначе projects. Внутри активного таба — `chat-list-item` со старым layout (icon + name + last preview + per-chat unread badge). Empty-state в таблице — «В этой группе чатов нет». **#8b Красный bell-badge.** На иконке `fa-comments` в `header.blade.php` badge `#chatHeaderCount` получил `style="background:#dc2626 !important;color:#fff !important;font-weight:700"` (раньше использовал default `.badge` стиль). JS-polling pollChatUnread (создаёт badge динамически при появлении новых сообщений) тоже выставляет красный inline-style. Бросается в глаза как notification-bell. **Без миграций.** Report: docs/reports/UPD_pak221_RESULTS.md.
-
pak220
2026-06-02
Управление доступом (ACL): дружелюбные ошибки + диагностика. **Симптом:** при выдаче прав через UI приходила ошибка 500 без подсказки. **Корневая причина:** без точного trace не могу указать одну причину — поэтому усилил защиту по всему flow: `AclController::store` и `destroy` обёрнуты в try-catch с разделением на три ветки: (1) `AuthorizationException` → 403 + понятный message (пользователь не head dept); (2) `User::find($id) === null` → 422 «Пользователь не найден или удалён»; (3) `Throwable` → 500 с полным `Log::error(Pak220 ACL grant failed)` (user_id/target_id/object_type/object_id/permission/error_class/error_msg/trace до 2000 символов) и JSON с error_class + message. Раньше `User::findOrFail` бросал ModelNotFoundException → 404 без объяснения. **Деплой:** теперь user видит конкретное сообщение, и в `storage/logs/laravel-YYYY-MM-DD.log` всегда есть строка `Pak220 ACL grant failed` со stack trace — можем точечно починить корневую причину в следующем pak. Без миграций. Report: docs/reports/UPD_pak220_RESULTS.md.
-
pak219
2026-06-02
Project::scopeForUser расширен — член команды/responsible/Director-dept теперь видят проекты на /projects. **Симптом:** пользователь, добавленный в команду проекта, при заходе на /projects видел пустой список (или только проекты своего dept). На странице конкретного проекта `/projects/{id}` доступ работал — `ProjectPolicy::view` корректно покрывал team-membership. Расхождение между listing и detail-view. **Корневая причина:** `scopeForUser` упускал 3 case: (1) Director-департамент (isDirectorDept), (2) руководители с `projects.delete` permission, (3) `responsible_user_id` — owner проекта без departments не видел собственные проекты. **Фикс:** в начале scope — early-return для admin/Director/projects.delete (полный query без фильтра). В where-замыкании добавлен `->orWhere(responsible_user_id, $user->id)` рядом с team-membership и department-match. Без миграций. Report: docs/reports/UPD_pak219_RESULTS.md.
-
pak218
2026-06-02
/tasks/operational/{id}: редактирование задачи + @-mention autocomplete в комментариях. **#5 — редактирование без изменения даты.** Новый endpoint `PATCH /tasks/operational/{task}` через `GlobalTaskController::updateOperational`. Поля: name (required если передано), description (nullable), end_date (опц.). Спец-параметр `keep_date=1` (default) — означает «не трогать дедлайн», тогда end_date игнорируется. RBAC через canEdit (assignee/creator/admin/Director). UI: кнопка «Изменить» в page-actions (под @if canEdit), модалка `#editTaskModal` с полями: name input + description textarea + checkbox «Не изменять дедлайн» (default checked, отключает end_date input) + дата (disabled пока checkbox). **#4 — @-mention в composer.** Composer textarea получил `data-mention-pool` JSON со списком {id, name} участников задачи (assignee + creator, active). JS-обработчик подхватывает `@(\S{0,40})$` regex в позиции курсора, рендерит dropdown с лучшими 6 matches (substring), управление ↑/↓/Enter/Tab/Escape, click outside → close. mousedown на item (не click) — чтобы blur textarea не закрывал dd раньше pick. **Backend share:** mentionPool собирается в `show()` через `User::whereIn(...users where status=active)`. Передаётся в view как `$mentionPool`. **Без миграций.** Report: docs/reports/UPD_pak218_RESULTS.md.