166 lines
6.2 KiB
Twig
166 lines
6.2 KiB
Twig
{#
|
||
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>
|