324 lines
15 KiB
Plaintext
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>
|