Spaces:
Running
Running
User commited on
Commit ·
086aaf2
1
Parent(s): 5723c42
add: view archived tasks button
Browse files
TaskTrackingSystem.WebApi/Features/Task/TaskController.cs
CHANGED
|
@@ -30,6 +30,13 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 30 |
return Ok(tasks);
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
[HttpGet("{id}")]
|
| 34 |
public async Task<ActionResult<TaskDto>> GetTask(long id)
|
| 35 |
{
|
|
|
|
| 30 |
return Ok(tasks);
|
| 31 |
}
|
| 32 |
|
| 33 |
+
[HttpGet("archived")]
|
| 34 |
+
public async Task<ActionResult<IEnumerable<TaskDto>>> GetArchivedTasks()
|
| 35 |
+
{
|
| 36 |
+
var tasks = await _taskService.GetArchivedTasksAsync(User.GetRoleName(), User.GetUserId());
|
| 37 |
+
return Ok(tasks);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
[HttpGet("{id}")]
|
| 41 |
public async Task<ActionResult<TaskDto>> GetTask(long id)
|
| 42 |
{
|
TaskTrackingSystem.WebApi/Features/Task/TaskService.cs
CHANGED
|
@@ -50,6 +50,34 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 50 |
}).ToListAsync();
|
| 51 |
}
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
public async Task<TaskDto?> GetTaskByIdAsync(long id, string roleName, long currentUserId)
|
| 54 |
{
|
| 55 |
var task = await BuildAccessibleTaskQuery(roleName, currentUserId)
|
|
@@ -338,9 +366,9 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 338 |
return Result<int>.Success(tasksToArchive.Count);
|
| 339 |
}
|
| 340 |
|
| 341 |
-
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
|
| 342 |
{
|
| 343 |
-
var query = _db.Tasks.Where(t => t.IsDeleted != true && !t.IsArchived);
|
| 344 |
|
| 345 |
if (IsAdmin(roleName))
|
| 346 |
{
|
|
|
|
| 50 |
}).ToListAsync();
|
| 51 |
}
|
| 52 |
|
| 53 |
+
public async Task<IEnumerable<TaskDto>> GetArchivedTasksAsync(string roleName, long currentUserId)
|
| 54 |
+
{
|
| 55 |
+
return await BuildAccessibleTaskQuery(roleName, currentUserId, includeArchived: true)
|
| 56 |
+
.Where(t => t.IsArchived)
|
| 57 |
+
.OrderByDescending(t => t.UpdatedAt ?? t.CreatedAt ?? DateTime.UtcNow)
|
| 58 |
+
.Select(t => new TaskDto
|
| 59 |
+
{
|
| 60 |
+
Id = t.Id,
|
| 61 |
+
Title = t.Title,
|
| 62 |
+
Description = t.Description,
|
| 63 |
+
ProjectId = t.ProjectId,
|
| 64 |
+
StatusId = t.StatusId,
|
| 65 |
+
PriorityId = t.PriorityId,
|
| 66 |
+
AssignedTo = t.AssignedTo,
|
| 67 |
+
AssignedBy = t.AssignedBy,
|
| 68 |
+
EstimatedHours = t.EstimatedHours,
|
| 69 |
+
ActualHours = t.ActualHours,
|
| 70 |
+
DueDate = t.DueDate,
|
| 71 |
+
CreatedAt = t.CreatedAt ?? DateTime.UtcNow,
|
| 72 |
+
IsArchived = t.IsArchived,
|
| 73 |
+
CompletedAt = t.TaskHistories
|
| 74 |
+
.Where(th => th.NewStatusId == AppTaskStatus.Done)
|
| 75 |
+
.OrderBy(th => th.CreatedAt)
|
| 76 |
+
.Select(th => th.CreatedAt)
|
| 77 |
+
.FirstOrDefault()
|
| 78 |
+
}).ToListAsync();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
public async Task<TaskDto?> GetTaskByIdAsync(long id, string roleName, long currentUserId)
|
| 82 |
{
|
| 83 |
var task = await BuildAccessibleTaskQuery(roleName, currentUserId)
|
|
|
|
| 366 |
return Result<int>.Success(tasksToArchive.Count);
|
| 367 |
}
|
| 368 |
|
| 369 |
+
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId, bool includeArchived = false)
|
| 370 |
{
|
| 371 |
+
var query = _db.Tasks.Where(t => t.IsDeleted != true && (includeArchived || !t.IsArchived));
|
| 372 |
|
| 373 |
if (IsAdmin(roleName))
|
| 374 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor
CHANGED
|
@@ -37,14 +37,18 @@
|
|
| 37 |
<option value="@p.Id">@p.Name</option>
|
| 38 |
}
|
| 39 |
</select>
|
| 40 |
-
@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
{
|
| 42 |
<button @onclick="RequestArchiveDoneConfirmation" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
|
| 43 |
<i data-lucide="archive" class="h-4 w-4 mr-2 text-slate-500"></i>
|
| 44 |
Archive Done
|
| 45 |
</button>
|
| 46 |
}
|
| 47 |
-
@if (canCreateTask)
|
| 48 |
{
|
| 49 |
<button @onclick="OpenCreateTaskModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 50 |
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
|
@@ -73,8 +77,11 @@
|
|
| 73 |
else
|
| 74 |
{
|
| 75 |
<!-- Kanban Columns -->
|
| 76 |
-
<div class="
|
| 77 |
-
@
|
|
|
|
|
|
|
|
|
|
| 78 |
{
|
| 79 |
var tasksInColumn = VisibleTasks.Where(t => t.StatusId == column.StatusId).ToList();
|
| 80 |
<div @key="column.StatusId" class="flex flex-col rounded-xl border border-slate-200 bg-slate-50/50 overflow-hidden min-h-[400px]">
|
|
@@ -88,7 +95,7 @@
|
|
| 88 |
</div>
|
| 89 |
|
| 90 |
<!-- Cards -->
|
| 91 |
-
<div class="flex-1 p-3 space-y-3 overflow-y-auto transition-colors @(dropTargetStatus == column.StatusId ? "bg-slate-100" : "")"
|
| 92 |
@ondrop="() => HandleDrop(column.StatusId)"
|
| 93 |
@ondragenter="() => HandleDragEnter(column.StatusId)"
|
| 94 |
@ondragleave="HandleDragLeave"
|
|
@@ -98,9 +105,9 @@
|
|
| 98 |
{
|
| 99 |
@foreach (var task in tasksInColumn)
|
| 100 |
{
|
| 101 |
-
<div @key="task.Id" class="select-none rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow @(canUpdateTask ? "cursor-grab active:cursor-grabbing" : "cursor-default")"
|
| 102 |
data-task-id="@task.Id"
|
| 103 |
-
draggable="@(canUpdateTask
|
| 104 |
@ondragstart="() => HandleDragStart(task)"
|
| 105 |
@ondragend="HandleDragEnd">
|
| 106 |
<div class="flex items-start justify-between">
|
|
@@ -113,6 +120,10 @@
|
|
| 113 |
}
|
| 114 |
<div class="flex items-center justify-between mt-3 pt-3 border-t border-slate-50">
|
| 115 |
<span class="text-xs text-slate-400">Due @DisplayFormats.Date(task.DueDate)</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
}
|
|
@@ -222,6 +233,7 @@
|
|
| 222 |
private bool isManager = false;
|
| 223 |
private bool canCreateTask = false;
|
| 224 |
private bool canUpdateTask = false;
|
|
|
|
| 225 |
private long currentUserId = 0;
|
| 226 |
private ProjectDto? project;
|
| 227 |
private bool showTaskModal = false;
|
|
@@ -268,6 +280,11 @@
|
|
| 268 |
? tasks.Where(t => t.ProjectId == selectedProjectId)
|
| 269 |
: tasks;
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
private Task HandleDragStart(TaskDto task)
|
| 272 |
{
|
| 273 |
if (!canUpdateTask)
|
|
@@ -275,6 +292,11 @@
|
|
| 275 |
return Task.CompletedTask;
|
| 276 |
}
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
draggedTask = task;
|
| 279 |
dropTargetStatus = null;
|
| 280 |
return Task.CompletedTask;
|
|
@@ -348,7 +370,7 @@
|
|
| 348 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 349 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
| 350 |
|
| 351 |
-
var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive);
|
| 352 |
var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
|
| 353 |
await System.Threading.Tasks.Task.WhenAll(tasksTask, projectsTask);
|
| 354 |
|
|
@@ -415,6 +437,15 @@
|
|
| 415 |
|
| 416 |
private void GoBack() => Navigation.NavigateTo("/projects/all");
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
private void OnProjectFilterChanged(ChangeEventArgs e)
|
| 419 |
{
|
| 420 |
selectedProjectId = long.TryParse(e.Value?.ToString(), out var value) ? value : 0;
|
|
|
|
| 37 |
<option value="@p.Id">@p.Name</option>
|
| 38 |
}
|
| 39 |
</select>
|
| 40 |
+
<button @onclick="ToggleArchivedView" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
|
| 41 |
+
<i data-lucide="@(showArchivedTasks ? "layout-grid" : "archive")" class="h-4 w-4 mr-2 text-slate-500"></i>
|
| 42 |
+
@(showArchivedTasks ? "Back to Board" : "Archived Tasks")
|
| 43 |
+
</button>
|
| 44 |
+
@if (canUpdateTask && !showArchivedTasks)
|
| 45 |
{
|
| 46 |
<button @onclick="RequestArchiveDoneConfirmation" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
|
| 47 |
<i data-lucide="archive" class="h-4 w-4 mr-2 text-slate-500"></i>
|
| 48 |
Archive Done
|
| 49 |
</button>
|
| 50 |
}
|
| 51 |
+
@if (canCreateTask && !showArchivedTasks)
|
| 52 |
{
|
| 53 |
<button @onclick="OpenCreateTaskModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 54 |
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
|
|
|
| 77 |
else
|
| 78 |
{
|
| 79 |
<!-- Kanban Columns -->
|
| 80 |
+
<div class="mb-2 text-sm text-slate-500">
|
| 81 |
+
@(showArchivedTasks ? "Viewing archived tasks." : "Drag tasks between columns to update their status.")
|
| 82 |
+
</div>
|
| 83 |
+
<div class="grid grid-cols-1 @(showArchivedTasks ? "md:grid-cols-1" : "md:grid-cols-3") gap-6">
|
| 84 |
+
@foreach (var column in VisibleColumns)
|
| 85 |
{
|
| 86 |
var tasksInColumn = VisibleTasks.Where(t => t.StatusId == column.StatusId).ToList();
|
| 87 |
<div @key="column.StatusId" class="flex flex-col rounded-xl border border-slate-200 bg-slate-50/50 overflow-hidden min-h-[400px]">
|
|
|
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<!-- Cards -->
|
| 98 |
+
<div class="flex-1 p-3 space-y-3 overflow-y-auto transition-colors @((!showArchivedTasks && dropTargetStatus == column.StatusId) ? "bg-slate-100" : "")"
|
| 99 |
@ondrop="() => HandleDrop(column.StatusId)"
|
| 100 |
@ondragenter="() => HandleDragEnter(column.StatusId)"
|
| 101 |
@ondragleave="HandleDragLeave"
|
|
|
|
| 105 |
{
|
| 106 |
@foreach (var task in tasksInColumn)
|
| 107 |
{
|
| 108 |
+
<div @key="task.Id" class="select-none rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow @((canUpdateTask && !showArchivedTasks) ? "cursor-grab active:cursor-grabbing" : "cursor-default")"
|
| 109 |
data-task-id="@task.Id"
|
| 110 |
+
draggable="@((canUpdateTask && !showArchivedTasks).ToString().ToLowerInvariant())"
|
| 111 |
@ondragstart="() => HandleDragStart(task)"
|
| 112 |
@ondragend="HandleDragEnd">
|
| 113 |
<div class="flex items-start justify-between">
|
|
|
|
| 120 |
}
|
| 121 |
<div class="flex items-center justify-between mt-3 pt-3 border-t border-slate-50">
|
| 122 |
<span class="text-xs text-slate-400">Due @DisplayFormats.Date(task.DueDate)</span>
|
| 123 |
+
@if (showArchivedTasks)
|
| 124 |
+
{
|
| 125 |
+
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-slate-500">Archived</span>
|
| 126 |
+
}
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
}
|
|
|
|
| 233 |
private bool isManager = false;
|
| 234 |
private bool canCreateTask = false;
|
| 235 |
private bool canUpdateTask = false;
|
| 236 |
+
private bool showArchivedTasks = false;
|
| 237 |
private long currentUserId = 0;
|
| 238 |
private ProjectDto? project;
|
| 239 |
private bool showTaskModal = false;
|
|
|
|
| 280 |
? tasks.Where(t => t.ProjectId == selectedProjectId)
|
| 281 |
: tasks;
|
| 282 |
|
| 283 |
+
private IEnumerable<KanbanColumn> VisibleColumns =>
|
| 284 |
+
showArchivedTasks
|
| 285 |
+
? columns.Where(column => column.StatusId == AppTaskStatus.Done)
|
| 286 |
+
: columns;
|
| 287 |
+
|
| 288 |
private Task HandleDragStart(TaskDto task)
|
| 289 |
{
|
| 290 |
if (!canUpdateTask)
|
|
|
|
| 292 |
return Task.CompletedTask;
|
| 293 |
}
|
| 294 |
|
| 295 |
+
if (showArchivedTasks)
|
| 296 |
+
{
|
| 297 |
+
return Task.CompletedTask;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
draggedTask = task;
|
| 301 |
dropTargetStatus = null;
|
| 302 |
return Task.CompletedTask;
|
|
|
|
| 370 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 371 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
| 372 |
|
| 373 |
+
var tasksTask = client.GetFromJsonAsync<List<TaskDto>>(showArchivedTasks ? "Task/archived" : "Task", Serialization.CaseInsensitive);
|
| 374 |
var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
|
| 375 |
await System.Threading.Tasks.Task.WhenAll(tasksTask, projectsTask);
|
| 376 |
|
|
|
|
| 437 |
|
| 438 |
private void GoBack() => Navigation.NavigateTo("/projects/all");
|
| 439 |
|
| 440 |
+
private async Task ToggleArchivedView()
|
| 441 |
+
{
|
| 442 |
+
showArchivedTasks = !showArchivedTasks;
|
| 443 |
+
draggedTask = null;
|
| 444 |
+
dropTargetStatus = null;
|
| 445 |
+
boardError = null;
|
| 446 |
+
await LoadTasks();
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
private void OnProjectFilterChanged(ChangeEventArgs e)
|
| 450 |
{
|
| 451 |
selectedProjectId = long.TryParse(e.Value?.ToString(), out var value) ? value : 0;
|