Files
TaskTracker/TaskTracker.Api/Pages/Partials/_TaskDetail.cshtml

324 lines
15 KiB
Plaintext

@using TaskTracker.Api.Pages
@using TaskTracker.Core.Enums
@model TaskTracker.Core.Entities.WorkTask
@{
var statusColors = new Dictionary<WorkTaskStatus, (string Color, string Label)>
{
[WorkTaskStatus.Pending] = ("#64748b", "Pending"),
[WorkTaskStatus.Active] = ("#3b82f6", "Active"),
[WorkTaskStatus.Paused] = ("#eab308", "Paused"),
[WorkTaskStatus.Completed] = ("#22c55e", "Completed"),
[WorkTaskStatus.Abandoned] = ("#ef4444", "Abandoned"),
};
var (statusColor, statusLabel) = statusColors[Model.Status];
var catColor = BoardModel.GetCategoryColor(Model.Category);
var elapsed = BoardModel.FormatElapsed(Model.StartedAt, Model.CompletedAt);
double? progressPercent = null;
if (Model.EstimatedMinutes.HasValue && Model.StartedAt.HasValue)
{
var start = Model.StartedAt.Value;
var end = Model.CompletedAt ?? DateTime.UtcNow;
var elapsedMins = (end - start).TotalMinutes;
progressPercent = Math.Min(100, (elapsedMins / Model.EstimatedMinutes.Value) * 100);
}
var isTerminal = Model.Status is WorkTaskStatus.Completed or WorkTaskStatus.Abandoned;
}
<!-- Overlay -->
<div class="detail-overlay" onclick="closeDetailPanel()"></div>
<!-- Panel -->
<div class="detail-panel">
<!-- Header -->
<div class="detail-header">
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;">
<div class="inline-edit" id="edit-title" style="flex: 1; min-width: 0;">
<h2 class="detail-title inline-edit-display inline-edit-display--title"
onclick="startEdit('title')">@Model.Title</h2>
<form class="inline-edit-form" style="display:none"
hx-put="/board?handler=UpdateTask&id=@Model.Id"
hx-target="#detail-panel"
hx-swap="innerHTML">
<input type="text" name="title" value="@Model.Title"
class="inline-edit-input"
onblur="this.form.requestSubmit()"
onkeydown="if(event.key==='Escape'){cancelEdit('title');event.stopPropagation()}"
onkeypress="if(event.key==='Enter'){this.form.requestSubmit();event.preventDefault()}" />
</form>
</div>
<button class="detail-close" onclick="closeDetailPanel()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<!-- Status badge + Category -->
<div style="display: flex; align-items: center; gap: 8px; margin-top: 12px;">
<span class="badge badge--@statusLabel.ToLower()">@statusLabel</span>
<div class="inline-edit" id="edit-category">
@if (!string.IsNullOrEmpty(Model.Category))
{
<span class="inline-edit-display inline-edit-display--field"
onclick="startEdit('category')"
style="font-size: 12px; display: inline-flex; align-items: center; gap: 4px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: @catColor; flex-shrink: 0;"></span>
@Model.Category
</span>
}
else
{
<span class="inline-edit-display inline-edit-display--field"
onclick="startEdit('category')"
style="font-size: 12px; color: var(--color-text-tertiary);">
+ category
</span>
}
<form class="inline-edit-form" style="display:none"
hx-put="/board?handler=UpdateTask&id=@Model.Id"
hx-target="#detail-panel"
hx-swap="innerHTML">
<input type="text" name="category" value="@(Model.Category ?? "")"
class="inline-edit-input" placeholder="Category"
onblur="this.form.requestSubmit()"
onkeydown="if(event.key==='Escape'){cancelEdit('category');event.stopPropagation()}"
onkeypress="if(event.key==='Enter'){this.form.requestSubmit();event.preventDefault()}" />
</form>
</div>
</div>
</div>
<!-- Scrollable body -->
<div class="detail-body">
<!-- Description -->
<div class="detail-section">
<h3 class="detail-section-label">Description</h3>
<div class="inline-edit" id="edit-description">
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="inline-edit-display inline-edit-display--field"
onclick="startEdit('description')"
style="font-size: 13px; line-height: 1.6; white-space: pre-wrap;">@Model.Description</div>
}
else
{
<div class="inline-edit-display inline-edit-display--field"
onclick="startEdit('description')"
style="font-size: 13px; color: var(--color-text-tertiary);">
Click to add description...
</div>
}
<form class="inline-edit-form" style="display:none"
hx-put="/board?handler=UpdateTask&id=@Model.Id"
hx-target="#detail-panel"
hx-swap="innerHTML">
<textarea name="description" rows="4"
class="inline-edit-input"
onkeydown="if(event.key==='Escape'){cancelEdit('description');event.stopPropagation()}"
style="resize: vertical;">@(Model.Description ?? "")</textarea>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<button type="submit" class="btn btn--sm btn--primary">Save</button>
<button type="button" class="btn btn--sm btn--ghost" onclick="cancelEdit('description')">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Time section -->
<div class="detail-section">
<h3 class="detail-section-label">Time</h3>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; justify-content: space-between; font-size: 13px;">
<span style="color: var(--color-text-secondary);">Elapsed</span>
<span style="color: var(--color-text-primary); font-variant-numeric: tabular-nums;">@elapsed</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 13px;">
<span style="color: var(--color-text-secondary);">Estimate</span>
<div class="inline-edit" id="edit-estimate" style="text-align: right;">
<span class="inline-edit-display inline-edit-display--field"
onclick="startEdit('estimate')"
style="color: var(--color-text-primary); font-variant-numeric: tabular-nums; padding: 2px 8px;">
@(Model.EstimatedMinutes.HasValue ? $"{Model.EstimatedMinutes}m" : "--")
</span>
<form class="inline-edit-form" style="display:none"
hx-put="/board?handler=UpdateTask&id=@Model.Id"
hx-target="#detail-panel"
hx-swap="innerHTML">
<input type="number" name="estimatedMinutes"
value="@(Model.EstimatedMinutes?.ToString() ?? "")"
class="inline-edit-input" placeholder="minutes"
style="width: 100px; text-align: right;"
onblur="this.form.requestSubmit()"
onkeydown="if(event.key==='Escape'){cancelEdit('estimate');event.stopPropagation()}"
onkeypress="if(event.key==='Enter'){this.form.requestSubmit();event.preventDefault()}" />
</form>
</div>
</div>
@if (progressPercent.HasValue)
{
var isOver = progressPercent.Value >= 100;
<div class="progress-bar">
<div class="progress-bar-fill @(isOver ? "progress-bar-fill--over" : "")"
style="width: @progressPercent.Value.ToString("F0")%"></div>
</div>
}
</div>
</div>
<!-- Subtasks -->
<div class="detail-section">
<partial name="Partials/_SubtaskList" model="Model" />
</div>
<!-- Notes -->
<div class="detail-section">
<partial name="Partials/_NotesList" model="Model" />
</div>
</div>
<!-- Action buttons (sticky bottom) -->
@if (!isTerminal)
{
<div class="detail-actions">
@switch (Model.Status)
{
case WorkTaskStatus.Pending:
<button hx-put="/board?handler=Start&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--primary btn--full"
onclick="closeDetailPanel()">
Start
</button>
<button hx-delete="/board?handler=Abandon&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--danger btn--full"
onclick="closeDetailPanel()">
Abandon
</button>
break;
case WorkTaskStatus.Active:
<button hx-put="/board?handler=Pause&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--amber btn--full"
onclick="closeDetailPanel()">
Pause
</button>
<button hx-put="/board?handler=Complete&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--emerald btn--full"
onclick="closeDetailPanel()">
Complete
</button>
<button hx-delete="/board?handler=Abandon&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--danger btn--full"
onclick="closeDetailPanel()">
Abandon
</button>
break;
case WorkTaskStatus.Paused:
<button hx-put="/board?handler=Resume&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--primary btn--full"
onclick="closeDetailPanel()">
Resume
</button>
<button hx-put="/board?handler=Complete&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--emerald btn--full"
onclick="closeDetailPanel()">
Complete
</button>
<button hx-delete="/board?handler=Abandon&id=@Model.Id"
hx-target="#kanban-board"
hx-swap="innerHTML"
class="btn btn--danger btn--full"
onclick="closeDetailPanel()">
Abandon
</button>
break;
}
</div>
}
</div>
<script>
// Open the detail panel when this partial is loaded
(function() {
var overlay = document.querySelector('.detail-overlay');
var panel = document.querySelector('.detail-panel');
// Use requestAnimationFrame to ensure the DOM is painted before adding classes
requestAnimationFrame(function() {
if (overlay) overlay.classList.add('detail-overlay--open');
if (panel) panel.classList.add('detail-panel--open');
});
})();
function closeDetailPanel() {
var overlay = document.querySelector('.detail-overlay');
var panel = document.querySelector('.detail-panel');
if (overlay) overlay.classList.remove('detail-overlay--open');
if (panel) panel.classList.remove('detail-panel--open');
// Clear the panel content after the transition
setTimeout(function() {
var container = document.getElementById('detail-panel');
if (container) container.innerHTML = '';
}, 300);
}
function startEdit(field) {
var wrapper = document.getElementById('edit-' + field);
if (!wrapper) return;
var display = wrapper.querySelector('.inline-edit-display');
var form = wrapper.querySelector('.inline-edit-form');
if (display) display.style.display = 'none';
if (form) {
form.style.display = '';
var input = form.querySelector('input, textarea');
if (input) {
input.focus();
if (input.type === 'text' || input.tagName === 'TEXTAREA') {
input.setSelectionRange(input.value.length, input.value.length);
}
}
}
}
function cancelEdit(field) {
var wrapper = document.getElementById('edit-' + field);
if (!wrapper) return;
var display = wrapper.querySelector('.inline-edit-display');
var form = wrapper.querySelector('.inline-edit-form');
if (display) display.style.display = '';
if (form) form.style.display = 'none';
}
// Close panel on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var panel = document.querySelector('.detail-panel--open');
if (panel) {
closeDetailPanel();
e.stopPropagation();
}
}
});
</script>