- Remove legacy wwwroot files (old index.html, app.css, api.js, page scripts) - Add Index page that redirects / to /board - Fix mapping delete button missing handler=Delete in URL - Add [IgnoreAntiforgeryToken] to AnalyticsModel for consistency - Remove duplicate JS from _TaskDetail.cshtml (already in app.js) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
13 KiB
Plaintext
263 lines
13 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>
|
|
|
|
<!-- Panel open/close, inline editing, and keyboard handling are in app.js -->
|