bitmine/templates/task_detail.html
2026-03-17 21:47:16 +03:00

343 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Задача #{{ task.id }} — {{ task.subject }}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container { max-width: 1200px; margin: 0 auto; }
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.card-header {
background: linear-gradient(135deg, #5c2d91 0%, #7b42a8 100%);
color: white;
padding: 24px;
}
.card-header h1 { font-size: 24px; margin-bottom: 8px; }
.task-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
}
.meta-item {
background: rgba(255,255,255,0.15);
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
}
.meta-label { opacity: 0.8; margin-right: 6px; }
.card-body { padding: 24px; }
.section { margin-bottom: 24px; }
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
}
.description {
background: #f8f9fa;
padding: 16px;
border-radius: 6px;
white-space: pre-wrap;
word-wrap: break-word;
}
.status-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
}
.status-new { background: #e3f2fd; color: #1976d2; }
.status-progress { background: #fff3e0; color: #f57c00; }
.status-resolved { background: #e8f5e9; color: #388e3c; }
.status-closed { background: #f5f5f5; color: #616161; }
.journal-entry {
border-left: 3px solid #ddd;
padding: 16px;
margin-bottom: 16px;
background: #fafafa;
}
.journal-entry.system {
border-left-color: #999;
background: #f0f0f0;
}
.journal-entry:last-child { margin-bottom: 0; }
.journal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 13px;
color: #666;
}
.journal-user { font-weight: 600; color: #333; }
.journal-notes {
background: white;
padding: 12px;
border-radius: 6px;
margin-top: 8px;
white-space: pre-wrap;
word-wrap: break-word;
}
.journal-details {
margin-top: 8px;
font-size: 12px;
color: #666;
}
.journal-details table { width: 100%; border-collapse: collapse; }
.journal-details td {
padding: 4px 8px;
border-bottom: 1px solid #eee;
}
.journal-details .old-value {
color: #c62828;
text-decoration: line-through;
}
.journal-details .new-value {
color: #388e3c;
font-weight: 500;
}
.attachments-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.attachment-card {
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s;
}
.attachment-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.attachment-preview {
height: 150px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.attachment-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.attachment-preview .file-icon { font-size: 48px; color: #999; }
.attachment-info { padding: 12px; }
.attachment-name {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
word-break: break-word;
}
.attachment-meta { font-size: 11px; color: #666; }
.attachment-link {
display: block;
margin-top: 8px;
color: #007bff;
text-decoration: none;
font-size: 12px;
}
.attachment-link:hover { text-decoration: underline; }
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4caf50;
transition: width 0.3s;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.info-item {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
}
.info-label { font-size: 12px; color: #666; margin-bottom: 4px; }
.info-value { font-weight: 500; color: #333; }
</style>
</head>
<body>
<div class="container">
<a href="/api/redmine/tasks" class="back-link">← Назад к списку задач</a>
<div class="card">
<div class="card-header">
<h1>#{{ task.id }} — {{ task.subject }}</h1>
<div class="task-meta">
<span class="status-badge status-{{ task.status_class }}">{{ task.status_name }}</span>
<span class="meta-item"><span class="meta-label">Проект:</span>{{ task.project_name }}</span>
<span class="meta-item"><span class="meta-label">Приоритет:</span>{{ task.priority_name }}</span>
</div>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Автор</div>
<div class="info-value">{{ task.author_name }}</div>
</div>
<div class="info-item">
<div class="info-label">Исполнитель</div>
<div class="info-value">{{ task.assigned_to_name }}</div>
</div>
<div class="info-item">
<div class="info-label">Создана</div>
<div class="info-value">{{ task.created_on }}</div>
</div>
<div class="info-item">
<div class="info-label">Обновлена</div>
<div class="info-value">{{ task.updated_on }}</div>
</div>
<div class="info-item">
<div class="info-label">Начало</div>
<div class="info-value">{{ task.start_date }}</div>
</div>
<div class="info-item">
<div class="info-label">Срок</div>
<div class="info-value">{{ task.due_date }}</div>
</div>
<div class="info-item">
<div class="info-label">Готовность</div>
<div class="info-value">{{ task.done_ratio }}%</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ task.done_ratio }}%"></div>
</div>
</div>
<div class="info-item">
<div class="info-label">Оценка (часы)</div>
<div class="info-value">{{ task.estimated_hours }}</div>
</div>
<div class="info-item">
<div class="info-label">Затрачено (часы)</div>
<div class="info-value">{{ task.spent_hours }}</div>
</div>
</div>
{% if !task.description.is_empty() %}
<div class="section">
<div class="section-title">Описание</div>
<div class="description">{{ task.description }}</div>
</div>
{% endif %}
{% if !task.attachments.is_empty() %}
<div class="section">
<div class="section-title">Вложения ({{ task.attachments.len() }})</div>
<div class="attachments-grid">
{% for attachment in task.attachments %}
<div class="attachment-card">
<a href="{{ attachment.content_url }}" target="_blank">
<div class="attachment-preview">
{% if attachment.is_image %}
<img src="{{ attachment.content_url }}" alt="{{ attachment.filename }}">
{% else %}
<span class="file-icon">📄</span>
{% endif %}
</div>
</a>
<div class="attachment-info">
<div class="attachment-name">{{ attachment.filename }}</div>
<div class="attachment-meta">
{% if attachment.filesize > 0 %}{{ attachment.filesize / 1024 }} КБ{% endif %}
{% if !attachment.author_name.is_empty() %} • {{ attachment.author_name }}{% endif %}
</div>
{% if !attachment.description.is_empty() %}
<div class="attachment-meta" style="margin-top: 4px;">
{{ attachment.description }}
</div>
{% endif %}
<a href="{{ attachment.content_url }}" class="attachment-link" target="_blank">Скачать →</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if !task.journals.is_empty() %}
<div class="section">
<div class="section-title">История изменений ({{ task.journals.len() }})</div>
{% for journal in task.journals %}
<div class="journal-entry {% if journal.notes.is_empty() && !journal.details.is_empty() %}system{% endif %}">
<div class="journal-header">
<span class="journal-user">{{ journal.user_name }}</span>
<span>{{ journal.created_on }}</span>
</div>
{% if !journal.notes.is_empty() %}
<div class="journal-notes">{{ journal.notes }}</div>
{% endif %}
{% if !journal.details.is_empty() %}
<div class="journal-details">
<table>
{% for detail in journal.details %}
<tr>
<td>
{% if !detail.name.is_empty() %}{{ detail.name }}{% else %}{{ detail.property }}{% endif %}
</td>
<td>
{% if !detail.old_value.is_empty() %}
<span class="old-value">{{ detail.old_value }}</span>
{% endif %}
{% if !detail.old_value.is_empty() && !detail.new_value.is_empty() %} → {% endif %}
{% if !detail.new_value.is_empty() %}
<span class="new-value">{{ detail.new_value }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if error.is_some() %}
<div class="section">
<div class="error" style="background: #ffebee; color: #c62828; padding: 16px; border-radius: 8px;">
{{ error.as_ref().unwrap() }}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</body>
</html>