398 lines
18 KiB
Twig
398 lines
18 KiB
Twig
{% extends 'layouts/base.twig' %}
|
||
|
||
{% block title %}{{ title }} — Бизнес.Точка{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||
<div class="btn-group">
|
||
<a href="{{ base_url('/tasks') }}" class="btn btn-outline-secondary">
|
||
<i class="fa-solid fa-list me-2"></i>Список
|
||
</a>
|
||
<a href="{{ base_url('/tasks/kanban') }}" class="btn btn-outline-primary">
|
||
<i class="fa-solid fa-table-columns me-2"></i>Канбан
|
||
</a>
|
||
<a href="{{ base_url('/tasks/calendar') }}" class="btn btn-outline-primary">
|
||
<i class="fa-solid fa-calendar me-2"></i>Календарь
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">
|
||
{% if task.priority == 'urgent' %}
|
||
<span class="badge bg-danger me-2">Срочно</span>
|
||
{% elseif task.priority == 'high' %}
|
||
<span class="badge bg-warning text-dark me-2">Высокий</span>
|
||
{% elseif task.priority == 'low' %}
|
||
<span class="badge bg-secondary me-2">Низкий</span>
|
||
{% endif %}
|
||
{{ task.title }}
|
||
</h5>
|
||
<div>
|
||
{% if not task.completed_at %}
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/complete') }}" method="post" class="d-inline">
|
||
{{ csrf_field()|raw }}
|
||
<button type="submit" class="btn btn-success btn-sm">
|
||
<i class="fa-solid fa-check me-1"></i>Завершить
|
||
</button>
|
||
</form>
|
||
{% else %}
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/reopen') }}" method="post" class="d-inline">
|
||
{{ csrf_field()|raw }}
|
||
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||
<i class="fa-solid fa-rotate-left me-1"></i>Вернуть в работу
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
<a href="{{ base_url('/tasks/' ~ task.id ~ '/edit') }}" class="btn btn-outline-primary btn-sm">
|
||
<i class="fa-solid fa-pen me-1"></i>Редактировать
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<p class="mb-1"><strong>Статус:</strong></p>
|
||
<span class="badge" style="background-color: {{ task.column_color|default('#6B7280') }}">
|
||
{{ task.column_name|default('—') }}
|
||
</span>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<p class="mb-1"><strong>Приоритет:</strong></p>
|
||
<span class="badge {% if task.priority == 'urgent' %}bg-danger{% elseif task.priority == 'high' %}bg-warning text-dark{% elseif task.priority == 'low' %}bg-secondary{% else %}bg-info{% endif %}">
|
||
{{ task.priorityLabels[task.priority]|default(task.priority) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{% if task.description %}
|
||
<div class="mb-3">
|
||
<p class="mb-1"><strong>Описание:</strong></p>
|
||
<p class="text-muted">{{ task.description|nl2br }}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if task.assignees %}
|
||
<div class="mb-3">
|
||
<p class="mb-1"><strong>Исполнители:</strong></p>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
{% for assignee in task.assignees %}
|
||
<span class="badge bg-light text-dark border">
|
||
<i class="fa-solid fa-user me-1"></i>
|
||
{{ assignee.user_name|default(assignee.user_email) }}
|
||
{% if assignee.role == 'watcher' %}
|
||
(наблюдатель)
|
||
{% endif %}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{# Подзадачи #}
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">
|
||
<i class="fa-solid fa-list-check me-2"></i>Подзадачи
|
||
{% if task.subtasks_count %}
|
||
<span class="badge bg-secondary ms-2">{{ task.subtasks_completed }}/{{ task.subtasks_count }}</span>
|
||
{% endif %}
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if task.subtasks %}
|
||
<ul class="list-group list-group-flush mb-3" id="subtasks-list">
|
||
{% for subtask in task.subtasks %}
|
||
<li class="list-group-item d-flex align-items-center gap-2" data-subtask-id="{{ subtask.id }}">
|
||
<input type="checkbox" class="form-check-input subtask-checkbox"
|
||
{% if subtask.is_completed %}checked{% endif %}
|
||
onchange="toggleSubtask({{ task.id }}, {{ subtask.id }})">
|
||
<span class="{% if subtask.is_completed %}text-muted text-decoration-line-through{% endif %} flex-grow-1">
|
||
{{ subtask.title }}
|
||
</span>
|
||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||
onclick="deleteSubtask({{ task.id }}, {{ subtask.id }})"
|
||
title="Удалить">
|
||
<i class="fa-solid fa-times"></i>
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% else %}
|
||
<p class="text-muted text-center mb-3">Подзадач пока нет</p>
|
||
{% endif %}
|
||
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/subtasks') }}" method="post"
|
||
class="d-flex gap-2 subtask-form" onsubmit="addSubtask(event, {{ task.id }})">
|
||
{{ csrf_field()|raw }}
|
||
<input type="text" name="title" class="form-control"
|
||
placeholder="Добавить подзадачу..." required>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fa-solid fa-plus"></i>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{# Комментарии #}
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-light">
|
||
<h5 class="mb-0">Комментарии</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-muted text-center">Комментарии будут доступны в следующей версии</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<div class="card border-0 shadow-sm mb-3">
|
||
<div class="card-header bg-light">
|
||
<h6 class="mb-0">Детали</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="mb-2">
|
||
<i class="fa-regular fa-calendar me-2 text-muted"></i>
|
||
<strong>Срок:</strong>
|
||
{% if task.due_date %}
|
||
{{ task.due_date|date('d.m.Y') }}
|
||
{% if task.due_date < date('now') and not task.completed_at %}
|
||
<span class="text-danger">(просрочено)</span>
|
||
{% endif %}
|
||
{% else %}
|
||
не указан
|
||
{% endif %}
|
||
</p>
|
||
<p class="mb-2">
|
||
<i class="fa-regular fa-user me-2 text-muted"></i>
|
||
<strong>Автор:</strong> {{ task.created_by_name|default('—') }}
|
||
</p>
|
||
<p class="mb-2">
|
||
<i class="fa-regular fa-clock me-2 text-muted"></i>
|
||
<strong>Создано:</strong> {{ task.created_at|date('d.m.Y H:i') }}
|
||
</p>
|
||
{% if task.completed_at %}
|
||
<p class="mb-0 text-success">
|
||
<i class="fa-solid fa-check me-2"></i>
|
||
<strong>Завершено:</strong> {{ task.completed_at|date('d.m.Y H:i') }}
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-light">
|
||
<h6 class="mb-0">Действия</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="d-grid gap-2">
|
||
{% if not task.completed_at %}
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/complete') }}" method="post">
|
||
{{ csrf_field()|raw }}
|
||
<button type="submit" class="btn btn-success w-100">
|
||
<i class="fa-solid fa-check me-2"></i>Отметить как выполненное
|
||
</button>
|
||
</form>
|
||
{% else %}
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/reopen') }}" method="post">
|
||
{{ csrf_field()|raw }}
|
||
<button type="submit" class="btn btn-outline-secondary w-100">
|
||
<i class="fa-solid fa-rotate-left me-2"></i>Вернуть в работу
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
<a href="{{ base_url('/tasks/' ~ task.id ~ '/edit') }}" class="btn btn-outline-primary">
|
||
<i class="fa-solid fa-pen me-2"></i>Редактировать
|
||
</a>
|
||
<form action="{{ base_url('/tasks/' ~ task.id ~ '/delete') }}" method="post"
|
||
onsubmit="return confirm('Вы уверены, что хотите удалить задачу?')">
|
||
{{ csrf_field()|raw }}
|
||
<button type="submit" class="btn btn-outline-danger w-100">
|
||
<i class="fa-solid fa-trash me-2"></i>Удалить задачу
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
{{ parent() }}
|
||
<script>
|
||
function addSubtask(event, taskId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const input = form.querySelector('input[name="title"]');
|
||
const title = input.value.trim();
|
||
|
||
if (!title) return;
|
||
|
||
fetch(`/tasks/${taskId}/subtasks`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: 'title=' + encodeURIComponent(title)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Находим контейнер для подзадач
|
||
let list = document.getElementById('subtasks-list');
|
||
|
||
// Если списка ещё нет (был пустой), создаём его
|
||
if (!list) {
|
||
const cardBody = form.closest('.card-body');
|
||
const emptyMessage = cardBody.querySelector('p.text-muted.text-center');
|
||
|
||
// Создаём новый список подзадач
|
||
const newList = document.createElement('ul');
|
||
newList.className = 'list-group list-group-flush mb-3';
|
||
newList.id = 'subtasks-list';
|
||
|
||
// Вставляем перед формой
|
||
cardBody.insertBefore(newList, form);
|
||
|
||
// Удаляем сообщение "Подзадач пока нет"
|
||
if (emptyMessage) {
|
||
emptyMessage.remove();
|
||
}
|
||
|
||
list = newList;
|
||
}
|
||
|
||
// Создаём новую подзадачу
|
||
const newItem = document.createElement('li');
|
||
newItem.className = 'list-group-item d-flex align-items-center gap-2';
|
||
newItem.setAttribute('data-subtask-id', data.subtask_id);
|
||
newItem.innerHTML = `
|
||
<input type="checkbox" class="form-check-input subtask-checkbox" onchange="toggleSubtask(${taskId}, ${data.subtask_id})">
|
||
<span class="flex-grow-1">${title}</span>
|
||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteSubtask(${taskId}, ${data.subtask_id})" title="Удалить">
|
||
<i class="fa-solid fa-times"></i>
|
||
</button>
|
||
`;
|
||
|
||
// Добавляем в список
|
||
list.appendChild(newItem);
|
||
|
||
// Очищаем поле ввода
|
||
input.value = '';
|
||
|
||
// Обновляем счётчик
|
||
updateSubtasksCount();
|
||
} else {
|
||
alert(data.error || 'Ошибка при создании подзадачи');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Ошибка при создании подзадачи');
|
||
});
|
||
}
|
||
|
||
function updateSubtasksCount() {
|
||
const list = document.getElementById('subtasks-list');
|
||
if (!list) return;
|
||
|
||
const items = list.querySelectorAll('li');
|
||
const count = items.length;
|
||
const completed = list.querySelectorAll('li input:checked').length;
|
||
|
||
// Обновляем счётчик на заголовке карточки
|
||
const badge = document.querySelector('.card-header h5 .badge');
|
||
if (badge) {
|
||
badge.textContent = `${completed}/${count}`;
|
||
}
|
||
}
|
||
|
||
function toggleSubtask(taskId, subtaskId) {
|
||
fetch(`/tasks/${taskId}/subtasks/${subtaskId}/toggle`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: ''
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (!data.success) {
|
||
alert(data.error || 'Ошибка');
|
||
} else {
|
||
// Находим чекбокс и обновляем состояние
|
||
const checkbox = document.querySelector(`[data-subtask-id="${subtaskId}"] .subtask-checkbox`);
|
||
const span = checkbox.nextElementSibling;
|
||
|
||
if (checkbox.checked) {
|
||
checkbox.checked = false;
|
||
span.classList.remove('text-muted', 'text-decoration-line-through');
|
||
} else {
|
||
checkbox.checked = true;
|
||
span.classList.add('text-muted', 'text-decoration-line-through');
|
||
}
|
||
|
||
// Обновляем счётчик
|
||
updateSubtasksCount();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
});
|
||
}
|
||
|
||
function deleteSubtask(taskId, subtaskId) {
|
||
if (!confirm('Удалить подзадачу?')) return;
|
||
|
||
fetch(`/tasks/${taskId}/subtasks/${subtaskId}/delete`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: ''
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (!data.success) {
|
||
alert(data.error || 'Ошибка');
|
||
} else {
|
||
// Удаляем из DOM
|
||
const item = document.querySelector(`li[data-subtask-id="${subtaskId}"]`);
|
||
if (item) {
|
||
item.remove();
|
||
|
||
// Если список пуст, показываем сообщение
|
||
const list = document.getElementById('subtasks-list');
|
||
if (list && list.children.length === 0) {
|
||
list.remove();
|
||
|
||
// Добавляем сообщение "Подзадач пока нет"
|
||
const cardBody = item.closest('.card-body');
|
||
const message = document.createElement('p');
|
||
message.className = 'text-muted text-center mb-3';
|
||
message.textContent = 'Подзадач пока нет';
|
||
cardBody.appendChild(message);
|
||
}
|
||
|
||
// Обновляем счётчик
|
||
updateSubtasksCount();
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
});
|
||
}
|
||
</script>
|
||
{% endblock %}
|