add CRM module start

This commit is contained in:
root
2026-01-15 07:09:36 +03:00
parent b14f293a45
commit b810a17649
40 changed files with 3807 additions and 14 deletions
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Modules\CRM\Models;
use CodeIgniter\Model;
use App\Models\Traits\TenantScopedModel;
class ContactModel extends Model
{
use TenantScopedModel;
protected $table = 'contacts';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = \App\Modules\CRM\Entities\Contact::class;
protected $useSoftDeletes = true;
protected $allowedFields = [
'organization_id',
'customer_id',
'name',
'email',
'phone',
'position',
'is_primary',
'notes',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
/**
* Получить контакты клиента
*/
public function getContactsForCustomer(int $customerId): array
{
return $this->where('customer_id', $customerId)->findAll();
}
/**
* Получить основной контакт клиента
*/
public function getPrimaryContact(int $customerId): ?object
{
return $this->where('customer_id', $customerId)
->where('is_primary', true)
->first();
}
/**
* Получить список контактов для выпадающего списка
*/
public function getContactsList(int $organizationId): array
{
$contacts = $this->where('organization_id', $organizationId)
->orderBy('name', 'ASC')
->findAll();
$list = [];
foreach ($contacts as $contact) {
$list[$contact->id] = $contact->name . ($contact->email ? " ({$contact->email})" : '');
}
return $list;
}
}
+229
View File
@@ -0,0 +1,229 @@
<?php
namespace App\Modules\CRM\Models;
use CodeIgniter\Model;
class DealModel extends Model
{
protected $table = 'deals';
protected $primaryKey = 'id';
protected $useSoftDeletes = true;
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
protected $returnType = 'array';
protected $allowedFields = [
'organization_id',
'contact_id',
'company_id',
'title',
'description',
'amount',
'currency',
'stage_id',
'assigned_user_id',
'expected_close_date',
'created_by',
];
/**
* Получить сделки с JOIN-ами для таблицы
*/
public function getForTable(int $organizationId): array
{
return $this->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,
ds.type as stage_type,
ds.probability as stage_probability,
c.name as contact_name,
c.email as contact_email,
oc.name as client_name,
au.name as assigned_user_name,
au.email as assigned_user_email,
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)
->orderBy('deals.created_at', 'DESC')
->findAll();
}
/**
* Получить сделки организации с фильтрами
*/
public function getDealsByOrganization(
int $organizationId,
?int $stageId = null,
?int $assignedUserId = null,
?string $search = null,
?string $dateFrom = null,
?string $dateTo = null
): array {
$builder = $this->where('organization_id', $organizationId);
if ($stageId) {
$builder->where('stage_id', $stageId);
}
if ($assignedUserId) {
$builder->where('assigned_user_id', $assignedUserId);
}
if ($search) {
$builder->groupStart()
->like('title', $search)
->orLike('description', $search)
->groupEnd();
}
if ($dateFrom) {
$builder->where('expected_close_date >=', $dateFrom);
}
if ($dateTo) {
$builder->where('expected_close_date <=', $dateTo);
}
return $builder->orderBy('created_at', 'DESC')->findAll();
}
/**
* Получить сделки, сгруппированные по этапам (для Канбана)
*/
public function getDealsGroupedByStage(int $organizationId): array
{
$deals = $this->select('deals.*, ds.name as stage_name, ds.color as stage_color, ds.type as stage_type, au.name as assigned_user_name')
->join('deal_stages ds', 'deals.stage_id = ds.id', 'left')
->join('users au', 'deals.assigned_user_id = au.id', 'left')
->join('contacts c', 'deals.contact_id = c.id', 'left')
->join('organizations_clients oc', 'deals.company_id = oc.id', 'left')
->where('deals.organization_id', $organizationId)
->where('deals.deleted_at', null)
->orderBy('ds.order_index', 'ASC')
->orderBy('deals.created_at', 'DESC')
->findAll();
$grouped = [];
foreach ($deals as $deal) {
$stageId = $deal['stage_id'] ?? 0;
if (!isset($grouped[$stageId])) {
$grouped[$stageId] = [
'stage_name' => $deal['stage_name'] ?? 'Без этапа',
'stage_color' => $deal['stage_color'] ?? '#6B7280',
'stage_type' => $deal['stage_type'] ?? 'progress',
'deals' => [],
'total_amount' => 0,
];
}
$grouped[$stageId]['deals'][] = $deal;
$grouped[$stageId]['total_amount'] += (float) $deal['amount'];
}
return $grouped;
}
/**
* Получить сделки для календаря (по дате закрытия)
*/
public function getDealsForCalendar(int $organizationId, string $month): array
{
return $this->select('deals.*, ds.color as stage_color, ds.name as stage_name')
->join('deal_stages ds', 'deals.stage_id = ds.id', 'left')
->where('deals.organization_id', $organizationId)
->where('deals.deleted_at', null)
->where('deals.expected_close_date >=', date('Y-m-01', strtotime($month)))
->where('deals.expected_close_date <=', date('Y-m-t', strtotime($month)))
->orderBy('expected_close_date', 'ASC')
->findAll();
}
/**
* Получить статистику по сделкам
*/
public function getDealStats(int $organizationId): array
{
$openDeals = $this->select('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
->where('organization_id', $organizationId)
->where('deleted_at', null)
->whereIn('stage_id', function($builder) use ($organizationId) {
return $builder->select('id')
->from('deal_stages')
->where('organization_id', $organizationId)
->whereIn('type', ['progress']);
})
->get()
->getRow();
$wonDeals = $this->select('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
->where('organization_id', $organizationId)
->where('deleted_at', null)
->whereIn('stage_id', function($builder) use ($organizationId) {
return $builder->select('id')
->from('deal_stages')
->where('organization_id', $organizationId)
->where('type', 'won');
})
->get()
->getRow();
$lostDeals = $this->select('COUNT(*) as count')
->where('organization_id', $organizationId)
->where('deleted_at', null)
->whereIn('stage_id', function($builder) use ($organizationId) {
return $builder->select('id')
->from('deal_stages')
->where('organization_id', $organizationId)
->where('type', 'lost');
})
->get()
->getRow();
return [
'open_count' => $openDeals->count,
'open_total' => $openDeals->total,
'won_count' => $wonDeals->count,
'won_total' => $wonDeals->total,
'lost_count' => $lostDeals->count,
];
}
/**
* Получить сделку по ID с JOIN-ами
*/
public function getWithJoins(int $dealId, int $organizationId): ?array
{
return $this->select('
deals.*,
ds.name as stage_name,
ds.color as stage_color,
ds.type as stage_type,
ds.probability as stage_probability,
c.name as contact_name,
c.email as contact_email,
oc.name as client_name,
au.name as assigned_user_name,
au.email as assigned_user_email
')
->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')
->where('deals.id', $dealId)
->where('deals.organization_id', $organizationId)
->first();
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace App\Modules\CRM\Models;
use CodeIgniter\Model;
class DealStageModel extends Model
{
protected $table = 'deal_stages';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'organization_id',
'name',
'color',
'order_index',
'type',
'probability',
];
protected $useTimestamps = false;
/**
* Получить все этапы организации
*/
public function getStagesByOrganization(int $organizationId): array
{
return $this->where('organization_id', $organizationId)
->orderBy('order_index', 'ASC')
->findAll();
}
/**
* Получить следующий порядковый номер для этапа
*/
public function getNextOrderIndex(int $organizationId): int
{
$max = $this->selectMax('order_index')
->where('organization_id', $organizationId)
->first();
return ($max['order_index'] ?? 0) + 1;
}
/**
* Создать этапы по умолчанию для новой организации
*/
public function createDefaultStages(int $organizationId): array
{
$defaultStages = [
[
'organization_id' => $organizationId,
'name' => 'Новый лид',
'color' => '#6B7280',
'order_index' => 1,
'type' => 'progress',
'probability' => 10,
],
[
'organization_id' => $organizationId,
'name' => 'Квалификация',
'color' => '#3B82F6',
'order_index' => 2,
'type' => 'progress',
'probability' => 25,
],
[
'organization_id' => $organizationId,
'name' => 'Предложение',
'color' => '#F59E0B',
'order_index' => 3,
'type' => 'progress',
'probability' => 50,
],
[
'organization_id' => $organizationId,
'name' => 'Переговоры',
'color' => '#8B5CF6',
'order_index' => 4,
'type' => 'progress',
'probability' => 75,
],
[
'organization_id' => $organizationId,
'name' => 'Успех',
'color' => '#10B981',
'order_index' => 5,
'type' => 'won',
'probability' => 100,
],
[
'organization_id' => $organizationId,
'name' => 'Провал',
'color' => '#EF4444',
'order_index' => 6,
'type' => 'lost',
'probability' => 0,
],
];
return $this->insertBatch($defaultStages);
}
/**
* Получить список этапов для выпадающего списка
*/
public function getStagesList(int $organizationId): array
{
$stages = $this->getStagesByOrganization($organizationId);
$list = [];
foreach ($stages as $stage) {
$list[$stage['id']] = $stage['name'];
}
return $list;
}
}