Files
CRM-IKZ/app/Views/tasks/kanban.php
T
2026-07-14 08:19:56 +04:00

385 lines
15 KiB
PHP
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.
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
<h1 class="h3 mb-0">Канбан-доска</h1>
<div class="d-flex gap-2 flex-wrap">
<a href="/tasks" class="btn btn-outline-primary">Таблица</a>
<a href="/tasks/kanban" class="btn btn-outline-secondary">Канбан</a>
<a href="/tasks/create" class="btn btn-primary">+ Новая задача</a>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body">
<form method="get" action="/tasks/kanban" class="row g-2 align-items-end">
<div class="col-12 col-md-4">
<label class="form-label">Фильтр по статусу</label>
<select name="status" class="form-select">
<option value="">Все статусы</option>
<option value="NEW" <?= ($filterStatus ?? '') === 'NEW' ? 'selected' : '' ?>>Новая</option>
<option value="IN_PROGRESS" <?= ($filterStatus ?? '') === 'IN_PROGRESS' ? 'selected' : '' ?>>В работе</option>
<option value="REVIEW" <?= ($filterStatus ?? '') === 'REVIEW' ? 'selected' : '' ?>>На проверке</option>
<option value="DONE" <?= ($filterStatus ?? '') === 'DONE' ? 'selected' : '' ?>>Закрыта</option>
<option value="CANCELED" <?= ($filterStatus ?? '') === 'CANCELED' ? 'selected' : '' ?>>Отменена</option>
<option value="OVERDUE" <?= ($filterStatus ?? '') === 'OVERDUE' ? 'selected' : '' ?>>Просрочена</option>
</select>
</div>
<div class="col-12 col-md-auto">
<button type="submit" class="btn btn-primary">Применить</button>
<a href="/tasks/kanban" class="btn btn-outline-secondary">Сбросить</a>
</div>
</form>
</div>
</div>
<div class="kanban-wrapper">
<div class="kanban-board">
<?php foreach ($columns as $statusCode => $column): ?>
<div class="kanban-column" data-status="<?= htmlspecialchars($statusCode) ?>">
<div class="kanban-column-header <?= htmlspecialchars($column['header_class']) ?>">
<div class="fw-semibold">
<?= htmlspecialchars($column['title']) ?>
</div>
<span class="badge text-bg-light kanban-count">
<?= count($column['tasks']) ?>
</span>
</div>
<div class="kanban-column-body dropzone" data-status="<?= htmlspecialchars($statusCode) ?>">
<?php if (empty($column['tasks'])): ?>
<div class="kanban-empty">
Нет задач
</div>
<?php endif; ?>
<?php foreach ($column['tasks'] as $task): ?>
<?php
$deadline = taskDeadlineState($task['planned_at'] ?? null, $task['status'] ?? null);
echo '<pre style="background:#000;color:#0f0;font-size:11px">';
echo 'ID: ' . ($task['id'] ?? '') . PHP_EOL;
echo 'planned_at: ' . ($task['planned_at'] ?? 'EMPTY') . PHP_EOL;
echo 'deadline: ' . print_r($deadline, true);
echo '</pre>';
$commentCount = (int)($task['comment_count'] ?? 0);
$lastCommentId = (int)($task['last_comment_id'] ?? 0);
$lastReadCommentId = (int)($task['last_read_comment_id'] ?? 0);
$isUnread = $commentCount > 0 && $lastCommentId > $lastReadCommentId;
$badgeClass = $isUnread ? 'bg-primary' : 'bg-secondary';
$priorityClass = match ($task['priority']) {
'LOW' => 'text-bg-light',
'MEDIUM' => 'text-bg-secondary',
'HIGH' => 'text-bg-warning',
'CRITICAL' => 'text-bg-danger',
default => 'text-bg-secondary',
};
?>
<div class="kanban-card <?= htmlspecialchars($deadline['class'] ?? '') ?>"
draggable="true"
data-task-id="<?= (int)$task['id'] ?>"
data-current-status="<?= htmlspecialchars((string)$task['status']) ?>">
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
<a href="/tasks/show?id=<?= (int)$task['id'] ?>"
data-task-modal
data-task-id="<?= (int)$task['id'] ?>"
class="kanban-card-title text-decoration-none">
<?= htmlspecialchars((string)$task['name']) ?>
</a>
<span class="badge <?= $priorityClass ?>">
<?= htmlspecialchars(task_priority_label((string)$task['priority'])) ?>
</span>
</div>
<?php if (!empty($task['description'])): ?>
<div class="kanban-card-desc mb-2">
<?= htmlspecialchars(mb_strimwidth((string)$task['description'], 0, 140, '...')) ?>
</div>
<?php endif; ?>
<div class="small text-muted mb-2">
<div><strong>CRM:</strong> <?= htmlspecialchars((string)$task['crm_id']) ?></div>
<?php if (!empty($task['creator_name'])): ?>
<div><strong>Постановщик:</strong> <?= htmlspecialchars((string)$task['creator_name']) ?></div>
<?php endif; ?>
<?php if (!empty($task['assignee_name'])): ?>
<div><strong>Ответственный:</strong> <?= htmlspecialchars((string)$task['assignee_name']) ?></div>
<?php endif; ?>
<?php if (!empty($task['supplier'])): ?>
<div><strong>Поставщик:</strong> <?= htmlspecialchars((string)$task['supplier']) ?></div>
<?php endif; ?>
<?php if (!empty($task['delivery_date'])): ?>
<div><strong>Срок:</strong> <?= htmlspecialchars((string)$task['delivery_date']) ?></div>
<?php endif; ?>
<?php if (!empty($task['planned_at'])): ?>
<div>
<strong>Дата план:</strong>
<?= htmlspecialchars(date('d.m.Y H:i', strtotime((string)$task['planned_at']))) ?>
<?php if ($deadline): ?>
<span class="badge deadline-badge-<?= htmlspecialchars($deadline['code']) ?> ms-1">
<?= htmlspecialchars($deadline['text']) ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="d-flex justify-content-between align-items-center">
<span
id="comment-badge-<?= (int)$task['id'] ?>"
class="badge <?= $badgeClass ?>"
data-task-id="<?= (int)$task['id'] ?>"
data-last-read-comment-id="<?= $lastReadCommentId ?>"
data-last-comment-id="<?= $lastCommentId ?>"
>
Комментарии: <span class="comment-count"><?= $commentCount ?></span>
</span>
<?php
$isDone = (($task['status'] ?? '') === 'DONE');
?>
<span class="badge <?= $isDone ? 'text-bg-success' : 'text-bg-light' ?>">
<?= $isDone ? 'Выполнено' : 'Не выполнено' ?>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<style>
.kanban-wrapper {
overflow-x: auto;
padding-bottom: 8px;
}
.kanban-board {
display: flex;
gap: 16px;
align-items: flex-start;
min-width: max-content;
}
.kanban-column {
width: 320px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 14px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 220px);
}
.kanban-column-header {
padding: 12px 14px;
border-bottom: 1px solid rgba(255,255,255,.15);
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
position: sticky;
top: 0;
z-index: 2;
color: #fff;
}
.kanban-header-new {
background: #6c757d;
}
.kanban-header-progress {
background: #0d6efd;
}
.kanban-header-review {
background: #fd7e14;
}
.kanban-header-done {
background: #198754;
}
.kanban-header-canceled {
background: #495057;
}
.kanban-header-overdue {
background: #dc3545;
}
.kanban-column-body {
padding: 12px;
overflow-y: auto;
min-height: 120px;
transition: background-color .2s ease;
}
.kanban-column-body.drag-over {
background: #e9f2ff;
}
.kanban-card {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,.04);
margin-bottom: 12px;
cursor: grab;
}
.kanban-card:last-child {
margin-bottom: 0;
}
.kanban-card.dragging {
opacity: .55;
transform: rotate(1deg);
}
.kanban-card-title {
font-weight: 600;
color: #212529;
line-height: 1.3;
}
.kanban-card-title:hover {
color: #0d6efd;
}
.kanban-card-desc {
color: #6c757d;
font-size: 0.92rem;
line-height: 1.35;
}
.kanban-empty {
color: #6c757d;
text-align: center;
padding: 20px 10px;
border: 1px dashed #ced4da;
border-radius: 10px;
background: #fff;
}
@media (max-width: 768px) {
.kanban-column {
width: 280px;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
let draggedCard = null;
let originalStatus = null;
document.querySelectorAll('.kanban-card').forEach(card => {
card.addEventListener('dragstart', function () {
draggedCard = this;
originalStatus = this.getAttribute('data-current-status');
this.classList.add('dragging');
});
card.addEventListener('dragend', function () {
this.classList.remove('dragging');
document.querySelectorAll('.dropzone').forEach(zone => zone.classList.remove('drag-over'));
});
});
document.querySelectorAll('.dropzone').forEach(zone => {
zone.addEventListener('dragover', function (e) {
e.preventDefault();
this.classList.add('drag-over');
});
zone.addEventListener('dragleave', function () {
this.classList.remove('drag-over');
});
zone.addEventListener('drop', async function (e) {
e.preventDefault();
this.classList.remove('drag-over');
if (!draggedCard) return;
const newStatus = this.getAttribute('data-status');
const taskId = draggedCard.getAttribute('data-task-id');
if (!taskId || !newStatus || originalStatus === newStatus) {
return;
}
const oldZone = document.querySelector('.dropzone[data-status="' + originalStatus + '"]');
this.appendChild(draggedCard);
try {
const formData = new FormData();
formData.append('task_id', taskId);
formData.append('status', newStatus);
const response = await fetch('/tasks/change-status', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Ошибка смены статуса');
}
draggedCard.setAttribute('data-current-status', newStatus);
updateColumnCounts();
removeEmptyBlocks();
} catch (error) {
if (oldZone) {
oldZone.appendChild(draggedCard);
}
updateColumnCounts();
removeEmptyBlocks();
alert('Не удалось изменить статус');
}
});
});
function updateColumnCounts() {
document.querySelectorAll('.kanban-column').forEach(column => {
const cards = column.querySelectorAll('.kanban-card');
const countBadge = column.querySelector('.kanban-count');
if (countBadge) {
countBadge.textContent = cards.length;
}
});
}
function removeEmptyBlocks() {
document.querySelectorAll('.dropzone').forEach(zone => {
const cards = zone.querySelectorAll('.kanban-card');
const empty = zone.querySelector('.kanban-empty');
if (cards.length === 0) {
if (!empty) {
const div = document.createElement('div');
div.className = 'kanban-empty';
div.textContent = 'Нет задач';
zone.appendChild(div);
}
} else if (empty) {
empty.remove();
}
});
}
});
</script>