bp/app/Modules/Tasks/Views/tasks/show.twig

398 lines
18 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.

{% 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 %}