dealService = new DealService(); $this->stageService = new DealStageService(); $this->dealModel = new DealModel(); $this->stageModel = new DealStageModel(); $this->contactModel = new ContactModel(); $this->clientModel = new ClientModel(); } /** * Главная страница - список сделок (использует DataTable) */ public function index() { $organizationId = $this->requireActiveOrg(); return $this->renderTwig('@CRM/deals/index', [ 'title' => 'Сделки', 'tableHtml' => $this->renderTable($this->getTableConfig()), 'stats' => $this->dealService->getStats($organizationId), ]); } /** * AJAX endpoint для таблицы сделок */ public function table(?array $config = null, ?string $pageUrl = null) { return parent::table($this->getTableConfig(), '/crm/deals'); } /** * Конфигурация таблицы сделок для DataTable */ protected function getTableConfig(): array { $organizationId = $this->getActiveOrgId(); return [ 'id' => 'deals-table', 'url' => '/crm/deals/table', 'model' => $this->dealModel, 'columns' => [ 'title' => [ 'label' => 'Сделка', 'width' => '30%', ], 'stage_name' => [ 'label' => 'Этап', 'width' => '15%', ], 'amount' => [ 'label' => 'Сумма', 'width' => '15%', 'align' => 'text-end', ], 'client_name' => [ 'label' => 'Клиент', 'width' => '20%', ], 'expected_close_date' => [ 'label' => 'Срок', 'width' => '10%', ], ], 'searchable' => ['title', 'stage_name', 'client_name', 'amount'], 'sortable' => ['title', 'amount', 'expected_close_date', 'created_at', 'stage_name'], 'defaultSort' => 'created_at', 'order' => 'desc', 'actions' => ['label' => '', 'width' => '10%'], 'actionsConfig' => [ [ 'label' => '', 'url' => '/crm/deals/{id}', 'icon' => 'fa-solid fa-eye', 'class' => 'btn-outline-primary btn-sm', 'title' => 'Просмотр', ], [ 'label' => '', 'url' => '/crm/deals/{id}/edit', 'icon' => 'fa-solid fa-pen', 'class' => 'btn-outline-primary btn-sm', 'title' => 'Редактировать', 'type' => 'edit', ], ], 'emptyMessage' => 'Сделок пока нет', 'emptyIcon' => 'fa-solid fa-file-contract', 'emptyActionUrl' => '/crm/deals/new', 'emptyActionLabel' => 'Создать сделку', 'emptyActionIcon' => 'fa-solid fa-plus', 'can_edit' => true, 'can_delete' => true, // Field map for joined fields 'fieldMap' => [ 'stage_name' => 'ds.name', 'client_name' => 'oc.name', 'amount' => 'deals.amount', ], // Custom scope for JOINs and filtering 'scope' => function($builder) use ($organizationId) { $builder->from('deals') ->select('deals.id, deals.title, deals.amount, deals.currency, deals.expected_close_date, deals.created_at, deals.deleted_at, ds.name as stage_name, ds.color as stage_color, c.name as contact_name, oc.name as client_name, au.name as assigned_user_name, cb.name as created_by_name') ->join('deal_stages ds', 'deals.stage_id = ds.id', 'left') ->join('contacts c', 'deals.contact_id = c.id', 'left') ->join('organizations_clients oc', 'deals.company_id = oc.id', 'left') ->join('users au', 'deals.assigned_user_id = au.id', 'left') ->join('users cb', 'deals.created_by = cb.id', 'left') ->where('deals.organization_id', $organizationId) ->where('deals.deleted_at', null); }, ]; } /** * Канбан-доска сделок */ public function kanban() { $organizationId = $this->requireActiveOrg(); $stages = $this->stageService->getOrganizationStages($organizationId); $kanbanData = $this->dealService->getDealsForKanban($organizationId); // Формируем колонки для компонента $kanbanColumns = []; foreach ($stages as $stage) { $stageDeals = $kanbanData[$stage['id']]['deals'] ?? []; $kanbanColumns[] = [ 'id' => $stage['id'], 'name' => $stage['name'], 'color' => $stage['color'], 'items' => $stageDeals, 'total' => $kanbanData[$stage['id']]['total_amount'] ?? 0, ]; } return $this->renderTwig('@CRM/deals/kanban', [ 'title' => 'Сделки — Канбан', 'kanbanColumns' => $kanbanColumns, 'stats' => $this->dealService->getStats($organizationId), ]); } /** * Календарь сделок */ public function calendar() { $organizationId = $this->requireActiveOrg(); $month = $this->request->getGet('month') ?? date('Y-m'); $currentTimestamp = strtotime($month . '-01'); $daysInMonth = date('t', $currentTimestamp); $firstDayOfWeek = date('N', $currentTimestamp) - 1; $deals = $this->dealService->getDealsForCalendar($organizationId, $month); $eventsByDate = []; foreach ($deals as $deal) { if ($deal['expected_close_date']) { $dateKey = date('Y-m-d', strtotime($deal['expected_close_date'])); $eventsByDate[$dateKey][] = [ 'id' => $deal['id'], 'title' => $deal['title'], 'date' => $deal['expected_close_date'], 'stage_color' => $deal['stage_color'] ?? '#6B7280', 'url' => '/crm/deals/' . $deal['id'], ]; } } // Формируем легенду из этапов $stages = $this->stageService->getOrganizationStages($organizationId); $calendarLegend = array_map(function ($stage) { return [ 'name' => $stage['name'], 'color' => $stage['color'], ]; }, $stages); return $this->renderTwig('@CRM/deals/calendar', [ 'title' => 'Сделки — Календарь', 'calendarEvents' => array_map(function ($deal) { return [ 'id' => $deal['id'], 'title' => $deal['title'], 'date' => $deal['expected_close_date'], 'stage_color' => $deal['stage_color'] ?? '#6B7280', ]; }, $deals), 'eventsByDate' => $eventsByDate, 'calendarLegend' => $calendarLegend, 'currentMonth' => $month, 'monthName' => date('F Y', $currentTimestamp), 'daysInMonth' => $daysInMonth, 'firstDayOfWeek' => $firstDayOfWeek, 'prevMonth' => date('Y-m', strtotime('-1 month', $currentTimestamp)), 'nextMonth' => date('Y-m', strtotime('+1 month', $currentTimestamp)), 'today' => date('Y-m-d'), ]); } /** * Страница создания сделки */ public function create() { $organizationId = $this->requireActiveOrg(); $stageId = $this->request->getGet('stage_id'); // Получаем пользователей организации для поля "Ответственный" $orgUserModel = new \App\Models\OrganizationUserModel(); $orgUsers = $orgUserModel->getOrganizationUsers($organizationId); $users = []; foreach ($orgUsers as $user) { $users[$user['user_id']] = $user['user_name'] ?: $user['user_email']; } return $this->renderTwig('@CRM/deals/form', [ 'title' => 'Новая сделка', 'actionUrl' => '/crm/deals', 'stages' => $this->stageService->getOrganizationStages($organizationId), 'clients' => $this->clientModel->where('organization_id', $organizationId)->findAll(), 'contacts' => $this->contactModel->where('organization_id', $organizationId)->findAll(), 'users' => $users, 'stageId' => $stageId, 'currentUserId' => $this->getCurrentUserId(), ]); } /** * Сохранить новую сделку */ public function store() { $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); $data = [ 'organization_id' => $organizationId, 'title' => $this->request->getPost('title'), 'description' => $this->request->getPost('description'), 'amount' => $this->request->getPost('amount') ?? 0, 'currency' => $this->request->getPost('currency') ?? 'RUB', 'stage_id' => $this->request->getPost('stage_id'), 'contact_id' => $this->request->getPost('contact_id') ?: null, 'company_id' => $this->request->getPost('company_id') ?: null, 'assigned_user_id' => $this->request->getPost('assigned_user_id') ?: null, 'expected_close_date' => $this->request->getPost('expected_close_date') ?: null, ]; $dealId = $this->dealService->createDeal($data, $userId); if ($dealId) { return redirect()->to('/crm/deals')->with('success', 'Сделка успешно создана'); } return redirect()->back()->with('error', 'Ошибка при создании сделки')->withInput(); } /** * Просмотр сделки */ public function show(int $id) { $organizationId = $this->requireActiveOrg(); $deal = $this->dealService->getDealWithJoins($id, $organizationId); if (!$deal) { return redirect()->to('/crm/deals')->with('error', 'Сделка не найдена'); } return $this->renderTwig('@CRM/deals/show', [ 'title' => $deal['title'], 'deal' => (object) $deal, ]); } /** * Страница редактирования сделки */ public function edit(int $id) { $organizationId = $this->requireActiveOrg(); $deal = $this->dealService->getDealWithJoins($id, $organizationId); if (!$deal) { return redirect()->to('/crm/deals')->with('error', 'Сделка не найдена'); } // Получаем пользователей организации для поля "Ответственный" $orgUserModel = new \App\Models\OrganizationUserModel(); $orgUsers = $orgUserModel->getOrganizationUsers($organizationId); $users = []; foreach ($orgUsers as $user) { $users[$user['user_id']] = $user['user_name'] ?: $user['user_email']; } return $this->renderTwig('@CRM/deals/form', [ 'title' => 'Редактирование сделки', 'actionUrl' => "/crm/deals/{$id}", 'deal' => (object) $deal, 'stages' => $this->stageService->getOrganizationStages($organizationId), 'clients' => $this->clientModel->where('organization_id', $organizationId)->findAll(), 'contacts' => $this->contactModel->where('organization_id', $organizationId)->findAll(), 'users' => $users, 'currentUserId' => $this->getCurrentUserId(), ]); } /** * Обновить сделку */ public function update(int $id) { $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); $deal = $this->dealService->getDealWithJoins($id, $organizationId); if (!$deal) { return redirect()->to('/crm/deals')->with('error', 'Сделка не найдена'); } $data = [ 'title' => $this->request->getPost('title'), 'description' => $this->request->getPost('description'), 'amount' => $this->request->getPost('amount') ?? 0, 'currency' => $this->request->getPost('currency') ?? 'RUB', 'stage_id' => $this->request->getPost('stage_id'), 'contact_id' => $this->request->getPost('contact_id') ?: null, 'company_id' => $this->request->getPost('company_id') ?: null, 'assigned_user_id' => $this->request->getPost('assigned_user_id') ?: null, 'expected_close_date' => $this->request->getPost('expected_close_date') ?: null, ]; $result = $this->dealService->updateDeal($id, $data, $userId); if ($result) { return redirect()->to("/crm/deals/{$id}")->with('success', 'Сделка обновлена'); } return redirect()->back()->with('error', 'Ошибка при обновлении сделки')->withInput(); } /** * Удалить сделку */ public function destroy(int $id) { $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); $deal = $this->dealService->getDealWithJoins($id, $organizationId); if (!$deal) { return redirect()->to('/crm/deals')->with('error', 'Сделка не найдена'); } $this->dealService->deleteDeal($id, $userId); return redirect()->to('/crm/deals')->with('success', 'Сделка удалена'); } /** * API: перемещение сделки между этапами (drag-and-drop) */ public function moveStage() { $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); $dealId = $this->request->getPost('deal_id'); $newStageId = $this->request->getPost('stage_id'); $deal = $this->dealService->getDealWithJoins($dealId, $organizationId); if (!$deal) { return $this->response->setJSON(['success' => false, 'message' => 'Сделка не найдена']); } $result = $this->dealService->changeStage($dealId, $newStageId, $userId); return $this->response->setJSON(['success' => $result]); } /** * Управление этапами */ public function stages() { $organizationId = $this->requireActiveOrg(); $stages = $this->stageService->getOrganizationStages($organizationId); return $this->renderTwig('@CRM/deals/stages', [ 'title' => 'Этапы сделок', 'stages' => $stages, ]); } /** * Создать этап */ public function storeStage() { $organizationId = $this->requireActiveOrg(); $data = [ 'organization_id' => $organizationId, 'name' => $this->request->getPost('name'), 'color' => $this->request->getPost('color') ?? '#6B7280', 'type' => $this->request->getPost('type') ?? 'progress', 'probability' => $this->request->getPost('probability') ?? 0, ]; $stageId = $this->stageService->createStage($data); if ($stageId) { return redirect()->to('/crm/deals/stages')->with('success', 'Этап создан'); } return redirect()->back()->with('error', 'Ошибка при создании этапа')->withInput(); } /** * Обновить этап */ public function updateStage(int $id) { $organizationId = $this->requireActiveOrg(); $stage = $this->stageService->getStage($id); if (!$stage || $stage['organization_id'] !== $organizationId) { return redirect()->to('/crm/deals/stages')->with('error', 'Этап не найден'); } $data = [ 'name' => $this->request->getPost('name'), 'color' => $this->request->getPost('color'), 'type' => $this->request->getPost('type'), 'probability' => $this->request->getPost('probability'), ]; $this->stageService->updateStage($id, $data); return redirect()->to('/crm/deals/stages')->with('success', 'Этап обновлён'); } /** * Удалить этап */ public function destroyStage(int $id) { $organizationId = $this->requireActiveOrg(); $stage = $this->stageService->getStage($id); if (!$stage || $stage['organization_id'] !== $organizationId) { return redirect()->to('/crm/deals/stages')->with('error', 'Этап не найден'); } if (!$this->stageService->canDeleteStage($id)) { return redirect()->to('/crm/deals/stages')->with('error', 'Нельзя удалить этап, на котором есть сделки'); } $this->stageService->deleteStage($id); return redirect()->to('/crm/deals/stages')->with('success', 'Этап удалён'); } /** * API: получить контакты клиента */ public function getContactsByClient() { $organizationId = $this->requireActiveOrg(); $clientId = $this->request->getGet('client_id'); if (!$clientId) { return $this->response->setJSON(['success' => true, 'contacts' => []]); } // Проверяем что клиент принадлежит организации $client = $this->clientModel->where('organization_id', $organizationId)->find($clientId); if (!$client) { return $this->response->setJSON(['success' => false, 'message' => 'Клиент не найден']); } $contacts = $this->contactModel ->where('organization_id', $organizationId) ->where('customer_id', $clientId) ->findAll(); return $this->response->setJSON([ 'success' => true, 'contacts' => array_map(function($contact) { return [ 'id' => $contact->id, 'name' => $contact->name, 'email' => $contact->email, 'phone' => $contact->phone, ]; }, $contacts) ]); } }