HH/templates/projects/task_detail.html
2026-07-11 19:24:28 +03:00

219 lines
11 KiB
HTML

{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{{ task.title }} - PX360{% endblock %}
{% block content %}
<div class="max-w-5xl mx-auto px-4 py-6">
<!-- Back link -->
<a href="{% url 'projects:project_detail' pk=project.pk %}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-navy mb-4">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Back to project" %}
</a>
<!-- Task header -->
<div class="page-header-gradient mb-6">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs uppercase tracking-wide opacity-80 mb-1">{{ project.name }}</p>
<h1 class="text-2xl font-bold">{{ task.title }}</h1>
</div>
<div class="flex items-center gap-2">
{% if can_edit %}
<a href="{% url 'projects:task_edit' project_pk=project.pk task_pk=task.pk %}" class="px-3 py-1.5 bg-white/15 hover:bg-white/25 rounded-lg text-sm font-semibold transition inline-flex items-center gap-1">
<i data-lucide="pencil" class="w-4 h-4"></i> {% trans "Edit" %}
</a>
{% endif %}
</div>
</div>
<div class="flex flex-wrap items-center gap-4 mt-4 text-sm">
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold uppercase
{% if task.status == 'completed' %}bg-green-100 text-green-800
{% elif task.status == 'active' %}bg-blue-100 text-blue-800
{% elif task.status == 'pending' %}bg-amber-100 text-amber-800
{% elif task.status == 'cancelled' %}bg-slate-100 text-slate-600
{% else %}bg-slate-100 text-slate-600{% endif %}">
{{ task.get_status_display }}
</span>
<span class="inline-flex items-center gap-1">
<i data-lucide="user" class="w-4 h-4"></i>
{{ task.assigned_to.get_full_name|default:"-" }}
</span>
{% if task.due_date %}
<span class="inline-flex items-center gap-1 {% if task.due_date < now|date:'Y-m-d' and task.status != 'completed' %}text-red-300 font-semibold{% endif %}">
<i data-lucide="calendar" class="w-4 h-4"></i>
{% trans "Due" %}: {{ task.due_date|date:"Y-m-d" }}
</span>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Description -->
<div class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100">
<h3 class="font-bold text-navy mb-2 flex items-center gap-2">
<i data-lucide="align-left" class="w-5 h-5 text-blue"></i>
{% trans "Description" %}
</h3>
<p class="text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
{{ task.description|default:"-" }}
</p>
</div>
<!-- Notes -->
<div>
{% include "partials/notes_panel.html" with notes=notes content_type_id=content_type_id object_id=object_id %}
</div>
</div>
<!-- Attachments -->
<div class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100 mt-6">
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
<i data-lucide="paperclip" class="w-5 h-5 text-blue"></i>
{% trans "Attachments" %}
<span id="attachmentCount" class="text-xs font-normal text-slate-400">({{ attachments|length }})</span>
</h3>
{% if can_contribute %}
<form id="attachmentForm" class="mb-5" enctype="multipart/form-data">
{% csrf_token %}
<div class="flex flex-col sm:flex-row gap-2">
<input type="file" name="file" id="attachmentFile" required
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip"
class="text-sm file:mr-3 file:py-2 file:px-3 file:rounded-lg file:border-0 file:bg-navy file:text-white file:font-semibold file:cursor-pointer border border-slate-200 rounded-lg w-full">
<input type="text" name="description" placeholder="{% trans 'Optional description...' %}"
class="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-navy/20 outline-none">
<button type="submit" class="px-4 py-2 bg-navy text-white rounded-lg font-semibold hover:bg-blue transition text-sm inline-flex items-center gap-1 whitespace-nowrap">
<i data-lucide="upload" class="w-4 h-4"></i> {% trans "Upload" %}
</button>
</div>
<p id="attachmentError" class="text-red-500 text-xs mt-2 hidden"></p>
</form>
{% else %}
<p class="text-xs text-slate-400 italic mb-4">
{% trans "Only project contributors can upload attachments." %}
</p>
{% endif %}
<div id="attachmentList" class="space-y-2">
{% for att in attachments %}
<div id="att-{{ att.id }}" class="flex items-center justify-between gap-3 border border-slate-100 rounded-xl p-3 bg-slate-50">
<div class="flex items-center gap-3 min-w-0">
<div class="w-9 h-9 rounded-lg bg-navy/10 flex items-center justify-center shrink-0">
<i data-lucide="file-text" class="w-4 h-4 text-navy"></i>
</div>
<div class="min-w-0">
<a href="{{ att.file.url }}" target="_blank" class="text-sm font-semibold text-navy hover:underline truncate block">
{{ att.filename }}
</a>
<p class="text-xs text-slate-400">
{{ att.uploaded_by.get_full_name|default:"-" }} · {{ att.created_at|date:"M d, Y h:i A" }}
{% if att.file_size %} · {{ att.file_size|filesizeformat }}{% endif %}
{% if att.description %} · {{ att.description }}{% endif %}
</p>
</div>
</div>
{% if can_contribute %}
<button type="button" onclick="deleteAttachment('{{ att.id }}')"
class="w-8 h-8 rounded-lg text-slate-400 hover:bg-red-50 hover:text-red-600 transition shrink-0"
title="{% trans 'Delete' %}">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
{% endif %}
</div>
{% empty %}
<div id="attachmentEmpty" class="text-center py-8">
<i data-lucide="paperclip" class="w-12 h-12 mx-auto text-slate-300 mb-3"></i>
<p class="text-slate text-sm">{% trans "No attachments yet" %}</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const form = document.getElementById('attachmentForm');
const fileList = document.getElementById('attachmentList');
const emptyEl = document.getElementById('attachmentEmpty');
const countEl = document.getElementById('attachmentCount');
const errorEl = document.getElementById('attachmentError');
const uploadUrl = "{% url 'projects:task_attachment_upload' task_pk=task.pk %}";
function fmtSize(bytes) {
if (!bytes) return '';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; }
return bytes.toFixed(bytes >= 10 || i === 0 ? 0 : 1) + ' ' + units[i];
}
function updateCount(n) {
if (countEl) countEl.textContent = '(' + n + ')';
if (emptyEl) emptyEl.style.display = n === 0 ? '' : 'none';
}
function rowHtml(a) {
return '<div id="att-' + a.id + '" class="flex items-center justify-between gap-3 border border-slate-100 rounded-xl p-3 bg-slate-50">'
+ '<div class="flex items-center gap-3 min-w-0">'
+ '<div class="w-9 h-9 rounded-lg bg-navy/10 flex items-center justify-center shrink-0"><i data-lucide="file-text" class="w-4 h-4 text-navy"></i></div>'
+ '<div class="min-w-0">'
+ '<a href="' + a.file_url + '" target="_blank" class="text-sm font-semibold text-navy hover:underline truncate block">' + escapeHtml(a.filename) + '</a>'
+ '<p class="text-xs text-slate-400">' + escapeHtml(a.uploaded_by) + ' · ' + escapeHtml(a.uploaded_at)
+ (a.file_size ? ' · ' + fmtSize(a.file_size) : '')
+ (a.description ? ' · ' + escapeHtml(a.description) : '') + '</p>'
+ '</div></div>'
+ '<button type="button" onclick="deleteAttachment(\'' + a.id + '\')" class="w-8 h-8 rounded-lg text-slate-400 hover:bg-red-50 hover:text-red-600 transition shrink-0" title="{% trans 'Delete' %}"><i data-lucide="trash-2" class="w-4 h-4"></i></button>'
+ '</div>';
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : s;
return d.innerHTML;
}
if (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
if (errorEl) { errorEl.textContent = ''; errorEl.classList.add('hidden'); }
const data = new FormData(form);
fetch(uploadUrl, { method: 'POST', body: data, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(r => r.json())
.then(data => {
if (data.success) {
fileList.insertAdjacentHTML('afterbegin', rowHtml(data.attachment));
updateCount(data.attachment_count);
form.reset();
if (window.lucide) lucide.createIcons();
} else {
if (errorEl) { errorEl.textContent = data.error || 'Upload failed'; errorEl.classList.remove('hidden'); }
}
})
.catch(() => {
if (errorEl) { errorEl.textContent = 'Upload failed'; errorEl.classList.remove('hidden'); }
});
});
}
window.deleteAttachment = function (id) {
if (!confirm("{% trans 'Delete this attachment?' %}")) return;
const url = "{% url 'projects:task_attachment_delete' task_pk=task.pk attachment_pk='00000000-0000-0000-0000-000000000000' %}".replace('00000000-0000-0000-0000-000000000000', id);
fetch(url, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value }})
.then(r => r.json())
.then(data => {
if (data.success) {
const row = document.getElementById('att-' + id);
if (row) row.remove();
updateCount(data.attachment_count);
} else {
alert(data.error || 'Delete failed');
}
});
};
})();
</script>
{% endblock %}