bp/app/Views/components/kanban/kanban.twig

206 lines
8.1 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{#
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) {
console.log('Moving deal:', itemId, 'to stage:', newColumnId);
// Находим перетаскиваемую карточку
const draggedCard = document.querySelector(`.kanban-card[data-item-id="${itemId}"]`);
const sourceColumn = draggedCard ? draggedCard.closest('.kanban-cards-container') : null;
// AJAX перемещение - base.js автоматически добавит CSRF заголовок
fetch(moveUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'deal_id=' + itemId + '&stage_id=' + newColumnId
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (onMove) {
window[onMove](itemId, newColumnId, data);
} else if (draggedCard && sourceColumn) {
// Перемещаем карточку в новую колонку
const targetContainer = this;
targetContainer.appendChild(draggedCard);
// Обновляем счётчики колонок
updateColumnCounters(sourceColumn);
updateColumnCounters(targetContainer);
// Анимация успешного перемещения
draggedCard.style.transition = 'all 0.2s ease';
draggedCard.style.transform = 'scale(1.02)';
setTimeout(() => {
draggedCard.style.transform = '';
}, 200);
} else {
location.reload();
}
} else {
alert(data.message || 'Ошибка при перемещении');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при перемещении');
});
} else if (onMove) {
// Только callback без AJAX
window[onMove](itemId, newColumnId);
}
}
}
/**
* Обновление счётчиков колонки (количество карточек и сумма)
*/
function updateColumnCounters(container) {
const columnId = container.dataset.columnId;
const cards = container.querySelectorAll('.kanban-card');
const count = cards.length;
// Находим badge в заголовке колонки
const column = container.closest('.kanban-column');
if (column) {
const badge = column.querySelector('.badge');
if (badge) {
badge.textContent = count;
}
}
}
</script>