Many fixes

This commit is contained in:
root
2026-01-16 21:58:12 +03:00
parent b810a17649
commit 77f76c8c28
45 changed files with 3392 additions and 1243 deletions
+8 -1
View File
@@ -2,18 +2,25 @@
// CRM Module Routes
$routes->group('crm', ['filter' => 'org', 'namespace' => 'App\Modules\CRM\Controllers'], static function ($routes) {
$routes->group('crm', ['filter' => ['org', 'subscription:crm'], 'namespace' => 'App\Modules\CRM\Controllers'], static function ($routes) {
// Dashboard
$routes->get('/', 'DashboardController::index');
// Contacts
$routes->get('contacts', 'ContactsController::index');
$routes->get('contacts/table', 'ContactsController::contactsTable');
$routes->get('contacts/create', 'ContactsController::create');
$routes->post('contacts', 'ContactsController::store');
$routes->get('contacts/(:num)/edit', 'ContactsController::edit/$1');
$routes->post('contacts/(:num)', 'ContactsController::update/$1');
$routes->get('contacts/(:num)/delete', 'ContactsController::destroy/$1');
// Contacts AJAX API (for inline editing in Clients module)
$routes->post('contacts/list/(:num)', 'ContactsController::ajaxList/$1');
$routes->post('contacts/store', 'ContactsController::ajaxStore');
$routes->post('contacts/update/(:num)', 'ContactsController::ajaxUpdate/$1');
$routes->post('contacts/delete/(:num)', 'ContactsController::ajaxDelete/$1');
// Deals
$routes->group('deals', static function ($routes) {
@@ -17,24 +17,84 @@ class ContactsController extends BaseController
$this->clientModel = new ClientModel();
}
/**
* Конфигурация таблицы контактов
*/
protected function getContactsTableConfig(): array
{
$organizationId = $this->requireActiveOrg();
return [
'id' => 'contacts-table',
'url' => '/crm/contacts/table',
'model' => $this->contactModel,
'columns' => [
'id' => ['label' => 'ID', 'width' => '60px'],
'name' => ['label' => 'Имя'],
'email' => ['label' => 'Email', 'width' => '180px'],
'phone' => ['label' => 'Телефон', 'width' => '140px'],
'position' => ['label' => 'Должность', 'width' => '150px'],
'customer_name' => ['label' => 'Клиент'],
'created_at' => ['label' => 'Дата', 'width' => '100px'],
],
'searchable' => ['name', 'email', 'phone', 'position', 'customer_name'],
'sortable' => ['id', 'name', 'created_at'],
'defaultSort' => 'created_at',
'order' => 'desc',
'fieldMap' => [
'customer_name' => 'customers.name',
'name' => 'contacts.name',
'email' => 'contacts.email',
'phone' => 'contacts.phone',
'position' => 'contacts.position',
'created_at' => 'contacts.created_at',
'id' => 'contacts.id',
],
'scope' => function($builder) use ($organizationId) {
$builder->from('contacts')
->select('contacts.id, contacts.name, contacts.email, contacts.phone, contacts.position, contacts.created_at, contacts.deleted_at, customers.name as customer_name')
->join('organizations_clients customers', 'customers.id = contacts.customer_id', 'left')
->where('contacts.organization_id', $organizationId)
->where('contacts.deleted_at', null);
},
'actions' => ['label' => 'Действия', 'width' => '120px'],
'actionsConfig' => [
[
'label' => '',
'url' => '/crm/contacts/{id}/edit',
'icon' => 'fa-solid fa-pen',
'class' => 'btn-outline-primary',
'title' => 'Редактировать',
],
],
'emptyMessage' => 'Контактов пока нет',
'emptyIcon' => 'fa-solid fa-users',
];
}
/**
* Список контактов
*/
public function index()
{
$organizationId = $this->requireActiveOrg();
$contacts = $this->contactModel
->where('organization_id', $organizationId)
->orderBy('created_at', 'DESC')
->findAll();
$config = $this->getContactsTableConfig();
$tableHtml = $this->renderTable($config);
return $this->renderTwig('@CRM/contacts/index', [
'title' => 'Контакты',
'contacts' => $contacts,
'tableHtml' => $tableHtml,
'config' => $config,
]);
}
/**
* AJAX таблица контактов
*/
public function contactsTable()
{
return parent::table($this->getContactsTableConfig(), '/crm/contacts');
}
/**
* Форма создания контакта
*/
@@ -151,4 +211,190 @@ class ContactsController extends BaseController
return redirect()->to('/crm/contacts')->with('success', 'Контакт удалён');
}
// =========================================================================
// AJAX API для inline-редактирования в модуле Clients
// =========================================================================
/**
* Получить список контактов клиента (AJAX)
* GET /crm/contacts/list/{clientId}
*/
public function ajaxList(int $clientId)
{
$organizationId = $this->requireActiveOrg();
// Проверяем что клиент принадлежит организации
$client = $this->clientModel->forCurrentOrg()->find($clientId);
if (!$client) {
return $this->response->setJSON([
'success' => false,
'message' => 'Клиент не найден',
]);
}
$contacts = $this->contactModel
->where('organization_id', $organizationId)
->where('customer_id', $clientId)
->orderBy('name', 'ASC')
->findAll();
$items = array_map(function ($contact) {
return [
'id' => $contact->id,
'name' => $contact->name,
'email' => $contact->email,
'phone' => $contact->phone,
'position' => $contact->position,
];
}, $contacts);
return $this->response->setJSON([
'success' => true,
'items' => $items,
'total' => count($items),
]);
}
/**
* Создать контакт (AJAX)
* POST /crm/contacts/store
*/
public function ajaxStore()
{
$organizationId = $this->requireActiveOrg();
$customerId = $this->request->getPost('customer_id');
// Проверяем клиента если указан
if ($customerId) {
$client = $this->clientModel->forCurrentOrg()->find($customerId);
if (!$client) {
return $this->response->setJSON([
'success' => false,
'message' => 'Клиент не найден',
])->setStatusCode(422);
}
}
$data = [
'organization_id' => $organizationId,
'customer_id' => $customerId ?: null,
'name' => $this->request->getPost('name'),
'email' => $this->request->getPost('email') ?: null,
'phone' => $this->request->getPost('phone') ?: null,
'position' => $this->request->getPost('position') ?: null,
];
// Валидация
if (empty($data['name'])) {
return $this->response->setJSON([
'success' => false,
'message' => 'Имя контакта обязательно',
'errors' => ['name' => 'Имя контакта обязательно'],
])->setStatusCode(422);
}
$contactId = $this->contactModel->insert($data);
if (!$contactId) {
return $this->response->setJSON([
'success' => false,
'message' => 'Ошибка при создании контакта',
'errors' => $this->contactModel->errors(),
])->setStatusCode(422);
}
return $this->response->setJSON([
'success' => true,
'message' => 'Контакт создан',
'item' => [
'id' => $contactId,
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'],
'position' => $data['position'],
],
]);
}
/**
* Обновить контакт (AJAX)
* POST /crm/contacts/update/{id}
*/
public function ajaxUpdate(int $id)
{
$organizationId = $this->requireActiveOrg();
$contact = $this->contactModel->find($id);
if (!$contact || $contact->organization_id !== $organizationId) {
return $this->response->setJSON([
'success' => false,
'message' => 'Контакт не найден',
])->setStatusCode(404);
}
$data = [
'name' => $this->request->getPost('name'),
'email' => $this->request->getPost('email') ?: null,
'phone' => $this->request->getPost('phone') ?: null,
'position' => $this->request->getPost('position') ?: null,
];
// Валидация
if (empty($data['name'])) {
return $this->response->setJSON([
'success' => false,
'message' => 'Имя контакта обязательно',
'errors' => ['name' => 'Имя контакта обязательно'],
])->setStatusCode(422);
}
$result = $this->contactModel->update($id, $data);
if (!$result) {
return $this->response->setJSON([
'success' => false,
'message' => 'Ошибка при обновлении контакта',
'errors' => $this->contactModel->errors(),
])->setStatusCode(422);
}
return $this->response->setJSON([
'success' => true,
'message' => 'Контакт обновлён',
'item' => [
'id' => $id,
'name' => $data['name'],
'email' => $data['email'],
'phone' => $data['phone'],
'position' => $data['position'],
],
]);
}
/**
* Удалить контакт (AJAX)
* POST /crm/contacts/delete/{id}
*/
public function ajaxDelete(int $id)
{
$organizationId = $this->requireActiveOrg();
$contact = $this->contactModel->find($id);
if (!$contact || $contact->organization_id !== $organizationId) {
return $this->response->setJSON([
'success' => false,
'message' => 'Контакт не найден',
])->setStatusCode(404);
}
$this->contactModel->delete($id);
return $this->response->setJSON([
'success' => true,
'message' => 'Контакт удалён',
]);
}
}
@@ -74,7 +74,7 @@ class DealsController extends BaseController
'amount' => [
'label' => 'Сумма',
'width' => '15%',
'align' => 'text-end',
],
'client_name' => [
'label' => 'Клиент',
@@ -401,7 +401,14 @@ class DealsController extends BaseController
$result = $this->dealService->changeStage($dealId, $newStageId, $userId);
return $this->response->setJSON(['success' => $result]);
// Получаем новый CSRF токен для клиента
$csrfToken = csrf_hash();
$csrfHash = csrf_token();
return $this->response
->setHeader('X-CSRF-TOKEN', $csrfToken)
->setHeader('X-CSRF-HASH', $csrfHash)
->setJSON(['success' => $result]);
}
/**
-4
View File
@@ -29,10 +29,6 @@
<form method="POST" action="{{ actionUrl }}">
{{ csrf_field()|raw }}
{% if contact is defined %}
<input type="hidden" name="_method" value="PUT">
{% endif %}
<div class="mb-3">
<label for="name" class="form-label fw-bold">Имя *</label>
<input type="text"
+42 -65
View File
@@ -2,10 +2,14 @@
{% block title %}{{ title }}{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">{{ title }}</h1>
<h1 class="h3 mb-0"><i class="fa-solid fa-users text-info me-2"></i> {{ title }}</h1>
</div>
<a href="{{ site_url('/crm/contacts/create') }}" class="btn btn-primary">
<i class="fa-solid fa-plus me-2"></i>Добавить контакт
@@ -13,77 +17,50 @@
</div>
{# Сообщения #}
{% if success is defined %}
{% if session.success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success }}
{{ session.success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if session.error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ session.error }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Должность</th>
<th>Клиент</th>
<th class="text-end" style="width: 120px;">Действия</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr>
<td>
<div class="d-flex align-items-center gap-2">
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center"
style="width: 32px; height: 32px; min-width: 32px;">
<span class="text-white small">{{ contact.name|slice(0, 2)|upper }}</span>
</div>
<span class="fw-medium">{{ contact.name }}</span>
{% if contact.is_primary %}
<span class="badge bg-success" style="font-size: 0.7rem;">Основной</span>
{% endif %}
</div>
</td>
<td>{{ contact.email ?: '—' }}</td>
<td>{{ contact.phone ?: '—' }}</td>
<td>{{ contact.position ?: '—' }}</td>
<td>
{% if contact.customer_id %}
<span class="text-muted">{{ contact.customer_id }}</span>
{% else %}
<span class="text-muted">—</span>
{% endif %}
</td>
<td class="text-end">
<a href="{{ site_url('/crm/contacts/' ~ contact.id ~ '/edit') }}"
class="btn btn-outline-primary btn-sm" title="Редактировать">
<i class="fa-solid fa-pen"></i>
</a>
<form action="{{ site_url('/crm/contacts/' ~ contact.id) }}" method="POST" class="d-inline">
{{ csrf_field()|raw }}
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-outline-danger btn-sm"
onclick="return confirm('Удалить контакт?')" title="Удалить">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center py-4 text-muted">
Контактов пока нет
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{{ tableHtml|raw }}
{# CSRF токен для AJAX запросов #}
{{ csrf_field()|raw }}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ site_url('/assets/js/modules/DataTable.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.data-table').forEach(function(container) {
const id = container.id;
const url = container.dataset.url;
const perPage = parseInt(container.dataset.perPage) || 10;
if (window.dataTables && window.dataTables[id]) {
return;
}
const table = new DataTable(id, {
url: url,
perPage: perPage
});
window.dataTables = window.dataTables || {};
window.dataTables[id] = table;
});
});
</script>
{% endblock %}
+2 -2
View File
@@ -5,7 +5,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">CRM</h1>
<h1 class="h3 mb-0"><i class="fa-solid fa-chart-line text-primary me-2"></i>CRM</h1>
<p class="text-muted mb-0">Управление продажами и клиентами</p>
</div>
</div>
@@ -66,7 +66,7 @@
</a>
</div>
<div class="col-md-3">
<a href="{{ site_url('/crm/clients') }}" class="card shadow-sm text-decoration-none h-100">
<a href="{{ site_url('/clients') }}" class="card shadow-sm text-decoration-none h-100">
<div class="card-body">
<div class="d-flex align-items-center gap-3">
<div class="bg-success bg-opacity-10 rounded p-3">
+1 -19
View File
@@ -45,31 +45,13 @@
showNavigation: true,
showLegend: true,
legend: calendarLegend,
eventComponent: '@Deals/calendar_event.twig'
eventComponent: '@CRM/deals/calendar_event.twig'
}) }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.calendar-event {
display: block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
background-color: #f3f4f6;
border-left: 3px solid #6b7280;
border-radius: 0.25rem;
text-decoration: none;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.calendar-event:hover {
background-color: #e5e7eb;
}
.calendar-events-more {
padding: 0.125rem 0.5rem;
}
-4
View File
@@ -29,10 +29,6 @@
<form method="POST" action="{{ actionUrl }}">
{{ csrf_field()|raw }}
{% if deal is defined %}
<input type="hidden" name="_method" value="PUT">
{% endif %}
<div class="mb-3">
<label for="title" class="form-label fw-bold">Название сделки *</label>
<input type="text"
+1 -1
View File
@@ -5,7 +5,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">{{ title }}</h1>
<h1 class="h3 mb-0"><i class="fa-solid fa-file-contract text-primary me-2"></i>{{ title }}</h1>
<p class="text-muted mb-0">
Всего: {{ items|length }} |
Открыто: {{ stats.open_count }} на {{ stats.open_total|number_format(0, ',', ' ') }} ₽
+1 -1
View File
@@ -34,7 +34,7 @@
</a>
</li>
</ul>
{{ csrf_field()|raw }}
{# Канбан доска #}
{{ include('@components/kanban/kanban.twig', {
columns: kanbanColumns,
+1 -2
View File
@@ -5,7 +5,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">{{ title }}</h1>
<h1 class="h3 mb-0"><i class="fa-solid fa-list-check text-warning me-2"></i> {{ title }}</h1>
<p class="text-muted mb-0">Настройка воронки продаж</p>
</div>
<a href="{{ site_url('/crm/deals') }}" class="btn btn-outline-secondary">
@@ -137,7 +137,6 @@
<div class="modal-content">
<form id="editForm" method="POST">
{{ csrf_field()|raw }}
<input type="hidden" name="_method" value="PUT">
<div class="modal-header">
<h5 class="modal-title">Редактировать этап</h5>
+9 -4
View File
@@ -5,14 +5,17 @@ namespace App\Modules\Clients\Controllers;
use App\Controllers\BaseController;
use App\Modules\Clients\Models\ClientModel;
use App\Services\AccessService;
use App\Services\ModuleSubscriptionService;
class Clients extends BaseController
{
protected ClientModel $clientModel;
protected ModuleSubscriptionService $subscriptionService;
public function __construct()
{
$this->clientModel = new ClientModel();
$this->subscriptionService = service('moduleSubscription');
}
public function index()
@@ -98,8 +101,9 @@ class Clients extends BaseController
}
$data = [
'title' => 'Добавить клиента',
'client' => null,
'title' => 'Добавить клиента',
'client' => null,
'crm_active' => $this->subscriptionService->isModuleActive('crm'),
];
return $this->renderTwig('@Clients/form', $data);
@@ -154,8 +158,9 @@ class Clients extends BaseController
}
$data = [
'title' => 'Редактировать клиента',
'client' => $client,
'title' => 'Редактировать клиента',
'client' => $client,
'crm_active' => $this->subscriptionService->isModuleActive('crm'),
];
return $this->renderTwig('@Clients/form', $data);
+96 -45
View File
@@ -3,7 +3,7 @@
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="col-lg-10">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex align-items-center">
@@ -19,55 +19,106 @@
<div class="card-body">
{{ forms.form_open(client ? base_url('/clients/update/' ~ client.id) : base_url('/clients/create')) }}
<div class="mb-3">
<label for="name" class="form-label fw-bold">Имя / Название *</label>
<input type="text" name="name" id="name" class="form-control {{ errors.name ? 'is-invalid' : '' }}"
value="{{ old.name ?? client.name ?? '' }}" required autofocus>
{% if errors.name %}
<div class="invalid-feedback">{{ errors.name }}</div>
{% endif %}
<div class="form-text">ФИО клиента или название компании</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" id="email" class="form-control {{ errors.email ? 'is-invalid' : '' }}"
value="{{ old.email ?? client.email ?? '' }}">
{% if errors.email %}
<div class="invalid-feedback">{{ errors.email }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Телефон</label>
<input type="tel" name="phone" id="phone" class="form-control {{ errors.phone ? 'is-invalid' : '' }}"
value="{{ old.phone ?? client.phone ?? '' }}">
{% if errors.phone %}
<div class="invalid-feedback">{{ errors.phone }}</div>
{% endif %}
</div>
</div>
<div class="mb-4">
<label for="notes" class="form-label">Заметки</label>
<textarea name="notes" id="notes" rows="4" class="form-control {{ errors.notes ? 'is-invalid' : '' }}"
placeholder="Дополнительная информация о клиенте...">{{ old.notes ?? client.notes ?? '' }}</textarea>
{% if errors.notes %}
<div class="invalid-feedback">{{ errors.notes }}</div>
{% endif %}
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ base_url('/clients') }}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-check me-2"></i>
{{ client ? 'Сохранить изменения' : 'Добавить клиента' }}
{# Табы #}
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-main" type="button">
<i class="fa-solid fa-building me-2"></i>Основное
</button>
</li>
{% if crm_active %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-contacts" type="button">
<i class="fa-solid fa-users me-2"></i>Контакты
<span class="badge bg-primary ms-1" id="contacts-count">0</span>
</button>
</li>
{% endif %}
</ul>
{# Содержимое табов #}
<div class="tab-content">
{# Таб "Основное" #}
<div class="tab-pane fade show active" id="tab-main" role="tabpanel">
<div class="mb-3">
<label for="name" class="form-label fw-bold">Имя / Название *</label>
<input type="text" name="name" id="name" class="form-control {{ errors.name ? 'is-invalid' : '' }}"
value="{{ old.name ?? client.name ?? '' }}" required autofocus>
{% if errors.name %}
<div class="invalid-feedback">{{ errors.name }}</div>
{% endif %}
<div class="form-text">ФИО клиента или название компании</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" name="email" id="email" class="form-control {{ errors.email ? 'is-invalid' : '' }}"
value="{{ old.email ?? client.email ?? '' }}">
{% if errors.email %}
<div class="invalid-feedback">{{ errors.email }}</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Телефон</label>
<input type="tel" name="phone" id="phone" class="form-control {{ errors.phone ? 'is-invalid' : '' }}"
value="{{ old.phone ?? client.phone ?? '' }}">
{% if errors.phone %}
<div class="invalid-feedback">{{ errors.phone }}</div>
{% endif %}
</div>
</div>
<div class="mb-4">
<label for="notes" class="form-label">Заметки</label>
<textarea name="notes" id="notes" rows="4" class="form-control {{ errors.notes ? 'is-invalid' : '' }}"
placeholder="Дополнительная информация о клиенте...">{{ old.notes ?? client.notes ?? '' }}</textarea>
{% if errors.notes %}
<div class="invalid-feedback">{{ errors.notes }}</div>
{% endif %}
</div>
</div>
{# Таб "Контакты" (только при активном CRM) #}
{% if crm_active %}
<div class="tab-pane fade" id="tab-contacts" role="tabpanel">
<input type="hidden" name="customer_id" value="{{ client.id }}">
{# Скрипт инициализации контактов подключаем в конце #}
<div id="contacts-container"
data-client-id="{{ client.id }}"
data-api-url="{{ base_url('/crm/contacts') }}"
data-csrf-token="{{ csrf_hash }}">
{# Таблица контактов загружается через AJAX #}
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="text-muted mt-2">Загрузка контактов...</p>
</div>
</div>
</div>
{% endif %}
</div>
<div class="d-flex justify-content-end gap-2 pt-3 border-top mt-4">
<a href="{{ base_url('/clients') }}" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-check me-2"></i>
{{ client ? 'Сохранить изменения' : 'Добавить клиента' }}
</button>
</div>
{{ forms.form_close() }}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{# Inline-редактирование контактов #}
{% if crm_active %}
<script src="{{ base_url('/assets/js/modules/contacts.js') }}"></script>
{% endif %}
{% endblock %}
+1 -1
View File
@@ -3,7 +3,7 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">{{ title }}</h1>
<h1 class="h3 mb-0"><i class="fa-solid fa-building text-success me-2"></i> {{ title }}</h1>
<p class="text-muted mb-0">Управление клиентами вашей организации</p>
</div>
<div class="d-flex gap-2">