Files
TaskTracker/TaskTracker.Api/Pages/Partials/_TaskDetail.cshtml
AJ Isaacs 6ea0e40d38 fix(web): address code review findings from Razor Pages migration
- 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>
2026-03-01 22:47:49 -05:00

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 -->