add CRM module start
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user