add CRM module start
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<p class="text-muted mb-0">План закрытия сделок</p>
|
||||
</div>
|
||||
<a href="{{ site_url('/crm/deals/new') }}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus me-2"></i>Новая сделка
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Переключатель видов #}
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals') }}">
|
||||
<i class="fa-solid fa-list me-2"></i>Список
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals/kanban') }}">
|
||||
<i class="fa-solid fa-columns me-2"></i>Канбан
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ site_url('/crm/deals/calendar') }}">
|
||||
<i class="fa-solid fa-calendar me-2"></i>Календарь
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{# Календарь #}
|
||||
{{ include('@components/calendar/calendar.twig', {
|
||||
eventsByDate: eventsByDate,
|
||||
currentMonth: currentMonth,
|
||||
monthName: monthName,
|
||||
daysInMonth: daysInMonth,
|
||||
firstDayOfWeek: firstDayOfWeek,
|
||||
today: today,
|
||||
prevMonth: site_url('/crm/deals/calendar?month=' ~ prevMonth),
|
||||
nextMonth: site_url('/crm/deals/calendar?month=' ~ nextMonth),
|
||||
showNavigation: true,
|
||||
showLegend: true,
|
||||
legend: calendarLegend,
|
||||
eventComponent: '@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;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{#
|
||||
calendar_event.twig - Событие календаря для сделки
|
||||
#}
|
||||
<a href="{{ site_url('/crm/deals/' ~ event.id) }}"
|
||||
class="calendar-event"
|
||||
style="border-left-color: {{ event.stage_color|default('#6B7280') }}"
|
||||
title="{{ event.title }}">
|
||||
{{ event.title|length > 15 ? event.title|slice(0, 15) ~ '...' : event.title }}
|
||||
</a>
|
||||
@@ -0,0 +1,217 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<a href="{{ site_url('/crm/deals') }}" class="btn btn-outline-secondary">
|
||||
<i class="fa-solid fa-arrow-left me-2"></i>Назад
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Сообщения об ошибках #}
|
||||
{% if errors is defined and errors|length > 0 %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<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">
|
||||
<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"
|
||||
class="form-control"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ old.title|default(deal.title|default('')) }}"
|
||||
required
|
||||
placeholder="Например: Разработка сайта для ООО Ромашка">
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="amount" class="form-label fw-bold">Сумма</label>
|
||||
<div class="input-group">
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value="{{ old.amount|default(deal.amount|default(0)) }}">
|
||||
<select class="form-select" name="currency" style="max-width: 100px;">
|
||||
<option value="RUB" {{ (old.currency|default(deal.currency|default('RUB'))) == 'RUB' ? 'selected' : '' }}>₽</option>
|
||||
<option value="USD" {{ (old.currency|default(deal.currency|default(''))) == 'USD' ? 'selected' : '' }}>$</option>
|
||||
<option value="EUR" {{ (old.currency|default(deal.currency|default(''))) == 'EUR' ? 'selected' : '' }}>€</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="stage_id" class="form-label fw-bold">Этап *</label>
|
||||
<select class="form-select" id="stage_id" name="stage_id" required>
|
||||
<option value="">Выберите этап</option>
|
||||
{% for stage in stages %}
|
||||
<option value="{{ stage.id }}"
|
||||
{{ (old.stage_id|default(deal.stage_id|default(stageId|default('')))) == stage.id ? 'selected' : '' }}
|
||||
data-color="{{ stage.color }}">
|
||||
{{ stage.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label fw-bold">Описание</label>
|
||||
<textarea class="form-control"
|
||||
id="description"
|
||||
name="description"
|
||||
rows="4"
|
||||
placeholder="Детали сделки, условия, комментарии...">{{ old.description|default(deal.description|default('')) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="company_id" class="form-label fw-bold">Клиент *</label>
|
||||
<select class="form-select" id="company_id" name="company_id" required>
|
||||
<option value="">Выберите клиента</option>
|
||||
{% for client in clients %}
|
||||
<option value="{{ client.id }}"
|
||||
{{ (old.company_id|default(deal.company_id|default(''))) == client.id ? 'selected' : '' }}>
|
||||
{{ client.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="contact_id" class="form-label fw-bold">Контакт</label>
|
||||
<select class="form-select" id="contact_id" name="contact_id">
|
||||
<option value="">Сначала выберите клиента</option>
|
||||
{% for contact in contacts %}
|
||||
<option value="{{ contact.id }}"
|
||||
{{ (old.contact_id|default(deal.contact_id|default(''))) == contact.id ? 'selected' : '' }}>
|
||||
{{ contact.name }} {{ contact.position ? ' (' ~ contact.position ~ ')' : '' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="contacts-loading" class="text-muted small mt-1" style="display: none;">
|
||||
<i class="fa-solid fa-spinner fa-spin me-1"></i>Загрузка контактов...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="assigned_user_id" class="form-label fw-bold">Ответственный</label>
|
||||
<select class="form-select" id="assigned_user_id" name="assigned_user_id">
|
||||
<option value="">Не назначен</option>
|
||||
{% for userId, userName in users %}
|
||||
<option value="{{ userId }}"
|
||||
{{ (old.assigned_user_id|default(deal.assigned_user_id|default(currentUserId|default('')))) == userId ? 'selected' : '' }}>
|
||||
{{ userName }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="expected_close_date" class="form-label fw-bold">Ожидаемая дата закрытия</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="expected_close_date"
|
||||
name="expected_close_date"
|
||||
value="{{ old.expected_close_date|default(deal.expected_close_date|default('')) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 pt-3 border-top">
|
||||
<a href="{{ site_url('/crm/deals') }}" class="btn btn-outline-secondary">Отмена</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa-solid fa-check me-2"></i>
|
||||
{{ deal is defined ? 'Сохранить' : 'Создать сделку' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const companySelect = document.getElementById('company_id');
|
||||
const contactSelect = document.getElementById('contact_id');
|
||||
const contactsLoading = document.getElementById('contacts-loading');
|
||||
|
||||
// Функция загрузки контактов по клиенту
|
||||
function loadContacts(clientId) {
|
||||
if (!clientId) {
|
||||
contactSelect.innerHTML = '<option value="">Сначала выберите клиента</option>';
|
||||
contactSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
contactSelect.disabled = true;
|
||||
contactsLoading.style.display = 'block';
|
||||
|
||||
fetch(`/crm/deals/contacts-by-client?client_id=${clientId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
contactsLoading.style.display = 'none';
|
||||
|
||||
if (data.success && data.contacts.length > 0) {
|
||||
let options = '<option value="">Выберите контакт</option>';
|
||||
data.contacts.forEach(contact => {
|
||||
const label = contact.name + (contact.email ? ` (${contact.email})` : '');
|
||||
options += `<option value="${contact.id}">${label}</option>`;
|
||||
});
|
||||
contactSelect.innerHTML = options;
|
||||
contactSelect.disabled = false;
|
||||
} else {
|
||||
contactSelect.innerHTML = '<option value="">Нет контактов для этого клиента</option>';
|
||||
contactSelect.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка загрузки контактов:', error);
|
||||
contactsLoading.style.display = 'none';
|
||||
contactSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||
contactSelect.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Загружаем контакты если клиент уже выбран (редактирование)
|
||||
const selectedClientId = companySelect.value;
|
||||
if (selectedClientId) {
|
||||
const selectedContactId = contactSelect.value;
|
||||
if (selectedContactId) {
|
||||
// При редактировании сохраняем выбранный контакт
|
||||
contactSelect.dataset.selectedId = selectedContactId;
|
||||
}
|
||||
loadContacts(selectedClientId);
|
||||
}
|
||||
|
||||
// Обработчик изменения клиента
|
||||
companySelect.addEventListener('change', function() {
|
||||
loadContacts(this.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Всего: {{ items|length }} |
|
||||
Открыто: {{ stats.open_count }} на {{ stats.open_total|number_format(0, ',', ' ') }} ₽
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ site_url('/crm/deals/new') }}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus me-2"></i>Новая сделка
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Переключатель видов #}
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ site_url('/crm/deals') }}">
|
||||
<i class="fa-solid fa-list me-2"></i>Список
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals/kanban') }}">
|
||||
<i class="fa-solid fa-columns me-2"></i>Канбан
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals/calendar') }}">
|
||||
<i class="fa-solid fa-calendar me-2"></i>Календарь
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
{{ tableHtml|raw }}
|
||||
{# CSRF токен для AJAX запросов #}
|
||||
{{ csrf_field()|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ parent() }}
|
||||
<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 %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Открыто: {{ stats.open_count }} на {{ stats.open_total|number_format(0, ',', ' ') }} ₽ |
|
||||
Успешно: {{ stats.won_count }} на {{ stats.won_total|number_format(0, ',', ' ') }} ₽
|
||||
</p>
|
||||
</div>
|
||||
<a href="{{ site_url('/crm/deals/new') }}" class="btn btn-primary">
|
||||
<i class="fa-solid fa-plus me-2"></i>Новая сделка
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Переключатель видов #}
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals') }}">
|
||||
<i class="fa-solid fa-list me-2"></i>Список
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ site_url('/crm/deals/kanban') }}">
|
||||
<i class="fa-solid fa-columns me-2"></i>Канбан
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ site_url('/crm/deals/calendar') }}">
|
||||
<i class="fa-solid fa-calendar me-2"></i>Календарь
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{# Канбан доска #}
|
||||
{{ include('@components/kanban/kanban.twig', {
|
||||
columns: kanbanColumns,
|
||||
moveUrl: site_url('/crm/deals/move-stage'),
|
||||
addUrl: site_url('/crm/deals/new'),
|
||||
addLabel: 'Добавить',
|
||||
cardComponent: '@CRM/deals/kanban_card.twig'
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="/assets/css/modules/data-table.css">
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{#
|
||||
kanban_card.twig - Карточка сделки для Канбана
|
||||
|
||||
Используется как кастомный компонент карточки в kanban.twig
|
||||
#}
|
||||
<div class="card mb-2 kanban-card"
|
||||
draggable="true"
|
||||
data-item-id="{{ item.id }}"
|
||||
style="cursor: grab;">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<a href="{{ site_url('/crm/deals/' ~ item.id) }}" class="text-decoration-none">
|
||||
<strong class="text-dark">{{ item.title }}</strong>
|
||||
</a>
|
||||
<span class="badge bg-light text-dark">
|
||||
₽{{ item.amount|number_format(0, ',', ' ') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if item.contact_name or item.client_name %}
|
||||
<small class="text-muted d-block mb-2">
|
||||
<i class="fa-solid fa-user me-1"></i>
|
||||
{{ item.contact_name|default(item.client_name) }}
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{% if item.assigned_user_name %}
|
||||
<small class="text-muted">
|
||||
<i class="fa-solid fa-user-check me-1"></i>
|
||||
{{ item.assigned_user_name }}
|
||||
</small>
|
||||
{% else %}
|
||||
<small></small>
|
||||
{% endif %}
|
||||
|
||||
{% if item.expected_close_date %}
|
||||
<small class="{{ item.expected_close_date < date('today') ? 'text-danger' : 'text-muted' }}">
|
||||
<i class="fa-regular fa-calendar me-1"></i>
|
||||
{{ item.expected_close_date|date('d.m') }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,204 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ deal.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-4">
|
||||
<a href="{{ site_url('/crm/deals') }}" class="text-muted text-decoration-none">
|
||||
<i class="fa-solid fa-arrow-left me-2"></i>К списку сделок
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{# Основная информация #}
|
||||
<div class="col-lg-8">
|
||||
{# Заголовок и статус #}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||
<div>
|
||||
<span class="badge mb-2"
|
||||
style="background-color: {{ deal.stage_color }}20; color: {{ deal.stage_color }}; border: 1px solid {{ deal.stage_color }}40;">
|
||||
{{ deal.stage_name|default('Без этапа') }}
|
||||
</span>
|
||||
<h1 class="h3 mb-0">{{ deal.title }}</h1>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="h4 mb-0">{{ deal.amount|number_format(0, ',', ' ') }} {{ deal.currency }}</div>
|
||||
<small class="text-muted">Сумма сделки</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if deal.description %}
|
||||
<div class="mb-4">
|
||||
<h6 class="text-muted mb-2">Описание</h6>
|
||||
<p class="mb-0" style="white-space: pre-wrap;">{{ deal.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row text-sm">
|
||||
<div class="col-md-6">
|
||||
<span class="text-muted">Дата создания:</span>
|
||||
<span class="ms-2">{{ deal.created_at|date('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<span class="text-muted">Ожидаемое закрытие:</span>
|
||||
<span class="ms-2 {{ deal.is_overdue ? 'text-danger fw-bold' : '' }}">
|
||||
{{ deal.expected_close_date ? deal.expected_close_date|date('d.m.Y') : '—' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# История изменений #}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">История изменений</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if history is defined and history|length > 0 %}
|
||||
<div class="timeline">
|
||||
{% for item in history %}
|
||||
<div class="timeline-item d-flex gap-3 pb-3 {{ not loop.last ? 'border-bottom' : '' }}">
|
||||
<div class="timeline-icon bg-secondary rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 32px; height: 32px; min-width: 32px;">
|
||||
<span class="text-white small">{{ item.user_name|default('С')|slice(0, 2) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="fw-medium">{{ item.user_name|default('Система') }}</span>
|
||||
<small class="text-muted ms-2">{{ item.created_at|date('d.m.Y H:i') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 mt-1">
|
||||
<span class="{{ item.action_class }}">{{ item.action_label }}</span>
|
||||
{% if item.change_description %}
|
||||
— {{ item.change_description }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted text-center py-4 mb-0">Нет записей в истории</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Боковая панель #}
|
||||
<div class="col-lg-4">
|
||||
{# Клиент #}
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">Клиент</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if deal.contact_name %}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; min-width: 40px;">
|
||||
<span class="text-white">{{ deal.contact_name|slice(0, 2) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="fw-medium text-decoration-none">{{ deal.contact_name }}</a>
|
||||
{% if deal.contact_email %}
|
||||
<small class="d-block text-muted">{{ deal.contact_email }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elseif deal.client_name %}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="bg-success rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; min-width: 40px;">
|
||||
<span class="text-white">{{ deal.client_name|slice(0, 2) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="fw-medium text-decoration-none">{{ deal.client_name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Клиент не указан</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Ответственный #}
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">Ответственный</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if deal.assigned_user_name %}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="bg-secondary rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px; min-width: 40px;">
|
||||
<span class="text-white">{{ deal.assigned_user_name|slice(0, 2) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-medium">{{ deal.assigned_user_name }}</div>
|
||||
{% if deal.assigned_user_email %}
|
||||
<small class="text-muted">{{ deal.assigned_user_email }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Не назначен</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Вероятность #}
|
||||
{% if deal.stage_probability is defined and deal.stage_probability > 0 %}
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">Вероятность закрытия</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="progress flex-grow-1" style="height: 8px;">
|
||||
<div class="progress-bar bg-primary"
|
||||
role="progressbar"
|
||||
style="width: {{ deal.stage_probability }}%"></div>
|
||||
</div>
|
||||
<span class="fw-medium">{{ deal.stage_probability }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Действия #}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body d-flex gap-2">
|
||||
<a href="{{ site_url('/crm/deals/' ~ deal.id ~ '/edit') }}" class="btn btn-outline-primary flex-grow-1">
|
||||
<i class="fa-solid fa-pen me-1"></i>Редактировать
|
||||
</a>
|
||||
<form action="{{ site_url('/crm/deals/' ~ deal.id) }}" method="POST" class="flex-grow-1">
|
||||
{{ csrf_field()|raw }}
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button type="submit" class="btn btn-outline-danger w-100"
|
||||
onclick="return confirm('Удалить сделку?')">
|
||||
<i class="fa-solid fa-trash me-1"></i>Удалить
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.timeline {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,195 @@
|
||||
{% extends 'layouts/base.twig' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<p class="text-muted mb-0">Настройка воронки продаж</p>
|
||||
</div>
|
||||
<a href="{{ site_url('/crm/deals') }}" class="btn btn-outline-secondary">
|
||||
<i class="fa-solid fa-arrow-left me-2"></i>К сделкам
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Сообщения #}
|
||||
{% if success is defined %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error is defined %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
{{ error }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Форма добавления этапа #}
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Добавить этап</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ site_url('/crm/deals/stages') }}" method="POST" class="row align-items-end g-3">
|
||||
{{ csrf_field()|raw }}
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="name" class="form-label fw-bold">Название</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required
|
||||
placeholder="Название этапа">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="color" class="form-label fw-bold">Цвет</label>
|
||||
<input type="color" class="form-control form-control-color" id="color"
|
||||
name="color" value="#6B7280">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="type" class="form-label fw-bold">Тип</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="progress">В процессе</option>
|
||||
<option value="won">Успех</option>
|
||||
<option value="lost">Провал</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="probability" class="form-label fw-bold">Вероятность (%)</label>
|
||||
<input type="number" class="form-control" id="probability" name="probability"
|
||||
value="0" min="0" max="100">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fa-solid fa-plus me-1"></i>Добавить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Список этапов #}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Этапы</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4" style="width: 60px;">Порядок</th>
|
||||
<th>Этап</th>
|
||||
<th style="width: 120px;">Тип</th>
|
||||
<th style="width: 120px;">Вероятность</th>
|
||||
<th class="text-end pe-4" style="width: 200px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stage in stages %}
|
||||
<tr>
|
||||
<td class="ps-4 text-muted">{{ stage.order_index + 1 }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="rounded"
|
||||
style="width: 16px; height: 16px; background-color: {{ stage.color }};"></span>
|
||||
<span class="fw-medium">{{ stage.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ stage.type_class }}">
|
||||
{{ stage.type_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ stage.probability }}%</td>
|
||||
<td class="text-end pe-4">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="openEditModal({{ stage.id }}, '{{ stage.name }}', '{{ stage.color }}', '{{ stage.type }}', {{ stage.probability }})">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
{% if not stage.is_final %}
|
||||
<form action="{{ site_url('/crm/deals/stages/' ~ stage.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('Удалить этап?')">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-secondary btn-sm" disabled title="Нельзя удалить завершающий этап">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Модальное окно редактирования #}
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<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>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit_name" class="form-label fw-bold">Название</label>
|
||||
<input type="text" class="form-control" id="edit_name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_color" class="form-label fw-bold">Цвет</label>
|
||||
<input type="color" class="form-control form-control-color" id="edit_color" name="color">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_type" class="form-label fw-bold">Тип</label>
|
||||
<select class="form-select" id="edit_type" name="type">
|
||||
<option value="progress">В процессе</option>
|
||||
<option value="won">Успех</option>
|
||||
<option value="lost">Провал</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="edit_probability" class="form-label fw-bold">Вероятность (%)</label>
|
||||
<input type="number" class="form-control" id="edit_probability" name="probability"
|
||||
min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
function openEditModal(id, name, color, type, probability) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
const form = document.getElementById('editForm');
|
||||
|
||||
form.action = '{{ site_url('/crm/deals/stages/') }}' + id;
|
||||
document.getElementById('edit_name').value = name;
|
||||
document.getElementById('edit_color').value = color;
|
||||
document.getElementById('edit_type').value = type;
|
||||
document.getElementById('edit_probability').value = probability;
|
||||
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user