343 lines
14 KiB
HTML
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>
|