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
@@ -0,0 +1,76 @@
{#
default_card.twig - Карточка по умолчанию для Канбан-компонента
Параметры:
- item: Объект элемента
- column: Объект колонки (для доступа к color и т.д.)
Ожидаемые поля в item:
- id: Идентификатор
- title: Заголовок
- url: Ссылка на просмотр (опционально)
- amount: Сумма для отображения (опционально)
- date: Дата для отображения (опционально)
- assignee: Ответственный (опционально)
- status: Статус для цветовой маркировки (опционально)
#}
<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">
{% if item.url %}
<a href="{{ item.url }}" class="text-decoration-none">
<strong class="text-dark">{{ item.title }}</strong>
</a>
{% else %}
<strong class="text-dark">{{ item.title }}</strong>
{% endif %}
{% if item.amount is defined and item.amount %}
<span class="badge bg-light text-dark">
₽{{ item.amount|number_format(0, ',', ' ') }}
</span>
{% endif %}
</div>
{# Дополнительная информация #}
{% if item.description is defined and item.description %}
<small class="text-muted d-block mb-2">
{{ item.description|length > 50 ? item.description|slice(0, 50) ~ '...' : item.description }}
</small>
{% endif %}
{# Нижняя панель #}
<div class="d-flex justify-content-between align-items-center">
{% if item.assignee is defined and item.assignee %}
<small class="text-muted">
<i class="fa-solid fa-user-check me-1"></i>
{{ item.assignee }}
</small>
{% else %}
<small></small>
{% endif %}
{% if item.date is defined and item.date %}
<small class="{{ item.isOverdue is defined and item.isOverdue ? 'text-danger' : 'text-muted' }}">
<i class="fa-regular fa-calendar me-1"></i>
{{ item.date|date('d.m') }}
</small>
{% endif %}
</div>
{# Теги/метки #}
{% if item.tags is defined and item.tags|length > 0 %}
<div class="mt-2 d-flex flex-wrap gap-1">
{% for tag in item.tags %}
<span class="badge" style="background-color: {{ tag.color }}20; color: {{ tag.color }}; font-size: 0.65rem;">
{{ tag.name }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
+165
View File
@@ -0,0 +1,165 @@
{#
kanban.twig - Универсальный компонент Канбан-доски
Параметры:
- columns: Массив колонок с данными
Пример:
columns: [
{
id: 1,
name: 'Колонка 1',
color: '#3B82F6',
items: [...],
total: 1000,
itemLabel: 'сделка' (опционально, для грамматики)
}
]
- cardComponent: Имя Twig компонента для рендеринга карточек (опционально)
- moveUrl: URL для API перемещения элементов (опционально)
- onMove: JavaScript функция callback при перемещении (опционально)
- emptyMessage: Сообщение при отсутствии элементов (опционально)
- addUrl: URL для добавления нового элемента (опционально)
- addLabel: Текст кнопки добавления (опционально)
#}
<div class="kanban-board overflow-auto pb-4">
<div class="d-flex gap-3" style="min-width: max-content;">
{% for column in columns %}
<div class="kanban-column" style="min-width: {{ column.width|default('320px') }}; min-height: 60vh;">
{# Заголовок колонки #}
<div class="card mb-2"
style="border-left: 4px solid {{ column.color }}; border-top: none; border-right: none; border-bottom: none;">
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ column.name }}</h6>
<span class="badge bg-secondary">{{ column.items|length }}</span>
</div>
{% if column.total is defined %}
<small class="text-muted">
₽{{ column.total|number_format(0, ',', ' ') }}
</small>
{% endif %}
</div>
</div>
{# Карточки #}
<div class="kanban-cards-container"
data-column-id="{{ column.id }}"
data-move-url="{{ moveUrl|default('') }}"
style="min-height: 50vh;"
{% if onMove is defined %}data-on-move="{{ onMove }}"{% endif %}>
{% if column.items is defined and column.items|length > 0 %}
{% for item in column.items %}
{% if cardComponent is defined %}
{{ include(cardComponent, {item: item, column: column}) }}
{% else %}
{{ include('@components/kanban/default_card.twig', {item: item, column: column}) }}
{% endif %}
{% endfor %}
{% endif %}
</div>
{# Кнопка добавления #}
{% if addUrl is defined or column.addUrl is defined %}
<a href="{{ column.addUrl|default(addUrl) }}?column_id={{ column.id }}"
class="btn btn-outline-secondary btn-sm w-100 mt-2">
<i class="fa-solid fa-plus me-1"></i>
{{ addLabel|default('Добавить') }}
</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
initKanban();
});
function initKanban() {
const cards = document.querySelectorAll('.kanban-card[draggable="true"]');
const containers = document.querySelectorAll('.kanban-cards-container');
cards.forEach(card => {
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
});
containers.forEach(container => {
container.addEventListener('dragover', handleDragOver);
container.addEventListener('drop', handleDrop);
container.addEventListener('dragenter', handleDragEnter);
container.addEventListener('dragleave', handleDragLeave);
});
}
function handleDragStart(e) {
this.classList.add('dragging');
e.dataTransfer.setData('text/plain', this.dataset.itemId);
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd() {
this.classList.remove('dragging');
document.querySelectorAll('.kanban-cards-container').forEach(col => {
col.classList.remove('bg-light');
});
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleDragEnter(e) {
e.preventDefault();
this.classList.add('bg-light');
}
function handleDragLeave() {
this.classList.remove('bg-light');
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove('bg-light');
const itemId = e.dataTransfer.getData('text/plain');
const newColumnId = this.dataset.columnId;
const moveUrl = this.dataset.moveUrl;
const onMove = this.dataset.onMove;
if (itemId && newColumnId) {
if (moveUrl) {
// AJAX перемещение
fetch(moveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'item_id=' + itemId + '&column_id=' + newColumnId
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (onMove) {
window[onMove](itemId, newColumnId, data);
} else {
location.reload();
}
} else {
alert(data.message || 'Ошибка при перемещении');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при перемещении');
});
} else if (onMove) {
// Только callback без AJAX
window[onMove](itemId, newColumnId);
}
}
}
</script>