User commited on
Commit
121df90
·
1 Parent(s): f0d69b8

fix: Paginations, Numbering and Serializations

Browse files
TaskTrackingSystem.WebApp/Components/Pages/AuditLogs.razor ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/audit-logs"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @using TaskTrackingSystem.Shared.Models.AuditLog
5
+ @using TaskTrackingSystem.WebApp.Components.Partial
6
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
7
+ @attribute [Authorize(Roles = "Admin")]
8
+ @inject ApiClientService ApiClient
9
+ @inject IJSRuntime JS
10
+ @inject AuthenticationStateProvider AuthStateProvider
11
+
12
+ <PageTitle>Admin Audit Logs</PageTitle>
13
+
14
+ <div class="space-y-6">
15
+ <!-- Page Header -->
16
+ <div class="flex items-center justify-between">
17
+ <div>
18
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">System Audit Logs</h2>
19
+ <p class="text-slate-500 mt-1">Monitor administrator actions and security events.</p>
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Search Panel -->
24
+ <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
25
+ <div class="flex items-center gap-3">
26
+ <div class="relative flex-1 max-w-md">
27
+ <input @bind="searchInput" @bind:event="oninput" @onkeyup="HandleKeyUp" type="text" placeholder="Search by user, action, module, or details..."
28
+ class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
29
+ </div>
30
+ <button @onclick="ApplySearch" 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">
31
+ <i data-lucide="search" class="h-4 w-4 mr-2"></i>
32
+ Search
33
+ </button>
34
+ <button @onclick="ResetSearch" 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">
35
+ <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
36
+ Reset
37
+ </button>
38
+ </div>
39
+ </div>
40
+
41
+ @if (isLoading)
42
+ {
43
+ <LoadingSpinner Message="Loading audit logs..." />
44
+ }
45
+ else
46
+ {
47
+ <!-- Logs Table -->
48
+ <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
49
+ <table class="w-full">
50
+ <thead>
51
+ <tr class="border-b border-slate-100 bg-slate-50/60">
52
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
53
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Timestamp</th>
54
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
55
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Module</th>
56
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Action</th>
57
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th>
58
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">IP Address</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody class="divide-y divide-slate-100 font-sans">
62
+ @if (PagedLogs.Any())
63
+ {
64
+ @foreach (var entry in PagedLogs.Select((log, index) => (log, index)))
65
+ {
66
+ var log = entry.log;
67
+ <tr class="hover:bg-slate-50/50 transition-colors">
68
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td>
69
+ <td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top">
70
+ @log.CreatedAt.ToString("dd-MM-yyyy HH:mm:ss")
71
+ </td>
72
+ <td class="px-6 py-4 align-top">
73
+ <div class="flex items-center gap-2">
74
+ <span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold shrink-0">
75
+ @(log.UserFullName.Length > 0 ? log.UserFullName[0].ToString().ToUpper() : "S")
76
+ </span>
77
+ <div>
78
+ <p class="text-xs font-semibold text-slate-800">@log.UserFullName</p>
79
+ <p class="text-[10px] text-slate-400">@@@log.Username</p>
80
+ </div>
81
+ </div>
82
+ </td>
83
+ <td class="px-6 py-4 align-top">
84
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 text-slate-700 border border-slate-200">
85
+ @log.Module
86
+ </span>
87
+ </td>
88
+ <td class="px-6 py-4 align-top">
89
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-bold @GetActionBadge(log.Action)">
90
+ @log.Action
91
+ </span>
92
+ </td>
93
+ <td class="px-6 py-4 text-xs text-slate-700 whitespace-normal break-words align-top">
94
+ @log.Description
95
+ </td>
96
+ <td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top">
97
+ @(string.IsNullOrWhiteSpace(log.IpAddress) ? "N/A" : log.IpAddress)
98
+ </td>
99
+ </tr>
100
+ }
101
+ }
102
+ else
103
+ {
104
+ <EmptyState ColSpan="7" Icon="history" Title="No audit logs" Subtitle="No security events or administrator operations were found." />
105
+ }
106
+ </tbody>
107
+ </table>
108
+
109
+ <!-- Pagination -->
110
+ <Pagination CurrentPage="currentPage"
111
+ TotalPages="TotalPages"
112
+ PageSize="pageSize"
113
+ TotalCount="FilteredLogs.Count()"
114
+ OnPageChanged="HandlePageChanged"
115
+ OnPageSizeChanged="HandlePageSizeChanged" />
116
+ </div>
117
+ }
118
+ </div>
119
+
120
+ @code {
121
+ private bool isLoading = true;
122
+ private List<AuditLogDto> auditLogs = new();
123
+ private string searchInput = "";
124
+ private string searchQuery = "";
125
+
126
+ // Pagination state
127
+ private int currentPage = 1;
128
+ private int pageSize = 15;
129
+
130
+ protected override async Task OnInitializedAsync()
131
+ {
132
+ await LoadLogs();
133
+ }
134
+
135
+ protected override async Task OnAfterRenderAsync(bool firstRender)
136
+ {
137
+ await JS.InvokeVoidAsync("initIcons");
138
+ }
139
+
140
+ private async Task LoadLogs()
141
+ {
142
+ isLoading = true;
143
+ var client = ApiClient.CreateClient();
144
+ try
145
+ {
146
+ var url = "AuditLog";
147
+ if (!string.IsNullOrWhiteSpace(searchQuery))
148
+ {
149
+ url += $"?search={Uri.EscapeDataString(searchQuery)}";
150
+ }
151
+ auditLogs = await client.GetFromJsonAsync<List<AuditLogDto>>(url, Serialization.CaseInsensitive) ?? new();
152
+ }
153
+ catch (Exception ex)
154
+ {
155
+ Console.WriteLine($"Load audit logs error: {ex.Message}");
156
+ }
157
+ finally
158
+ {
159
+ isLoading = false;
160
+ }
161
+ }
162
+
163
+ private void ApplySearch()
164
+ {
165
+ searchQuery = searchInput;
166
+ currentPage = 1;
167
+ _ = LoadLogs();
168
+ }
169
+
170
+ private void ResetSearch()
171
+ {
172
+ searchInput = "";
173
+ searchQuery = "";
174
+ currentPage = 1;
175
+ _ = LoadLogs();
176
+ }
177
+
178
+ private void HandleKeyUp(KeyboardEventArgs e)
179
+ {
180
+ if (e.Key == "Enter")
181
+ {
182
+ ApplySearch();
183
+ }
184
+ }
185
+
186
+ private IEnumerable<AuditLogDto> FilteredLogs => auditLogs.OrderByDescending(l => l.CreatedAt);
187
+
188
+ private int TotalPages => (int)Math.Ceiling((double)FilteredLogs.Count() / pageSize);
189
+
190
+ private IEnumerable<AuditLogDto> PagedLogs =>
191
+ FilteredLogs.Skip((currentPage - 1) * pageSize).Take(pageSize);
192
+
193
+ private void HandlePageChanged(int page)
194
+ {
195
+ currentPage = page;
196
+ }
197
+
198
+ private void HandlePageSizeChanged(int size)
199
+ {
200
+ pageSize = size;
201
+ currentPage = 1;
202
+ }
203
+
204
+ private string GetActionBadge(string action)
205
+ {
206
+ return action.ToLower() switch
207
+ {
208
+ "create" => "bg-emerald-50 text-emerald-700 border border-emerald-200",
209
+ "update" => "bg-amber-50 text-amber-700 border border-amber-200",
210
+ "delete" => "bg-rose-50 text-rose-700 border border-rose-200",
211
+ "assignaccess" => "bg-violet-50 text-violet-700 border border-violet-200",
212
+ _ => "bg-blue-50 text-blue-700 border border-blue-200"
213
+ };
214
+ }
215
+ }
TaskTrackingSystem.WebApp/Components/Pages/Backlog.razor ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/tasks/backlog"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @using TaskTrackingSystem.Shared
5
+ @using TaskTrackingSystem.Shared.Models.Task
6
+ @using TaskTrackingSystem.Shared.Models.Project
7
+ @using TaskTrackingSystem.Shared.Models.User
8
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
9
+ @attribute [Authorize]
10
+ @inject ApiClientService ApiClient
11
+ @inject NavigationManager Navigation
12
+ @inject IJSRuntime JS
13
+ @inject AuthenticationStateProvider AuthStateProvider
14
+ @inject MenuAuthorizationService MenuAuthorization
15
+
16
+ <PageTitle>Task Backlog</PageTitle>
17
+
18
+ <div class="space-y-6">
19
+ <!-- Page Header -->
20
+ <div class="flex items-center justify-between">
21
+ <div>
22
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">Task Backlog</h2>
23
+ <p class="text-slate-500 mt-1">Track backlog tasks, update status, and collaborate inline where allowed.</p>
24
+ </div>
25
+ </div>
26
+
27
+ @if (isLoading)
28
+ {
29
+ <LoadingSpinner Message="Loading backlog..." />
30
+ }
31
+ else
32
+ {
33
+ <div class="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-4 gap-6 items-start">
34
+ <!-- Left Column: Backlog Task List -->
35
+ <div class="lg:col-span-1 xl:col-span-1 bg-white border border-slate-200 rounded-2xl p-4 shadow-sm flex flex-col h-[700px]">
36
+ <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 mb-3 flex items-center gap-2">
37
+ <i data-lucide="layers" class="h-4 w-4"></i> Tasks List
38
+ </h3>
39
+
40
+ <!-- Search & Filters -->
41
+ <div class="space-y-2.5 mb-4 shrink-0">
42
+ <div class="relative">
43
+ <input @bind="searchInput" @bind:event="oninput" type="text" placeholder="Search tasks..."
44
+ class="w-full rounded-lg border border-slate-200 pl-8 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-violet-600 focus:border-transparent transition-all" />
45
+ <i data-lucide="search" class="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
46
+ </div>
47
+
48
+ <div class="grid grid-cols-2 gap-2">
49
+ <select @bind="filterStatusId" class="rounded-lg border border-slate-200 px-2 py-1.5 text-[11px] text-slate-700 bg-white focus:outline-none focus:ring-1 focus:ring-violet-600 transition-all">
50
+ <option value="0">All Status</option>
51
+ <option value="1">To Do</option>
52
+ <option value="2">In Progress</option>
53
+ <option value="3">Done</option>
54
+ </select>
55
+
56
+ <select @bind="filterPriorityId" class="rounded-lg border border-slate-200 px-2 py-1.5 text-[11px] text-slate-700 bg-white focus:outline-none focus:ring-1 focus:ring-violet-600 transition-all">
57
+ <option value="0">All Priority</option>
58
+ <option value="1">Low</option>
59
+ <option value="2">Medium</option>
60
+ <option value="3">High</option>
61
+ </select>
62
+ </div>
63
+
64
+ <select @bind="filterProjectId" class="w-full rounded-lg border border-slate-200 px-2 py-1.5 text-[11px] text-slate-700 bg-white focus:outline-none focus:ring-1 focus:ring-violet-600 transition-all">
65
+ <option value="0">All Projects</option>
66
+ @foreach (var p in projects)
67
+ {
68
+ <option value="@p.Id">@p.Name</option>
69
+ }
70
+ </select>
71
+ </div>
72
+
73
+ <!-- Grouped Scrollable Tasks list -->
74
+ <div class="flex-1 overflow-y-auto space-y-4 pr-1 scrollbar-thin">
75
+ @if (projects.Any())
76
+ {
77
+ var displayedAnyTask = false;
78
+
79
+ @foreach (var proj in projects.Where(p => filterProjectId == 0 || p.Id == filterProjectId))
80
+ {
81
+ var projTasks = FilteredTasks.Where(t => t.ProjectId == proj.Id).ToList();
82
+ @if (projTasks.Any())
83
+ {
84
+ displayedAnyTask = true;
85
+ <div>
86
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center justify-between border-b border-slate-100 pb-1">
87
+ <span class="truncate pr-2" title="@proj.Name">@proj.Name</span>
88
+ <span class="px-1.5 py-0.5 rounded-full bg-slate-100 text-slate-500 font-semibold font-sans text-[8px]">@projTasks.Count</span>
89
+ </div>
90
+ <div class="space-y-2">
91
+ @foreach (var task in projTasks)
92
+ {
93
+ <button @onclick="() => SelectTask(task.Id)"
94
+ class="w-full text-left p-3 rounded-xl border transition-all flex flex-col gap-1.5 @(selectedTaskId == task.Id ? "bg-violet-50/70 border-violet-200 shadow-sm" : "bg-slate-50/50 border-slate-100 hover:bg-slate-50 hover:border-slate-200")">
95
+ <div class="flex items-center justify-between gap-2">
96
+ <span class="text-[9px] font-mono text-slate-400">TASK-#@task.Id</span>
97
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-semibold @GetPriorityBadge(task.PriorityId)">
98
+ @GetPriorityLabel(task.PriorityId)
99
+ </span>
100
+ </div>
101
+ <h4 class="text-xs font-bold text-slate-800 line-clamp-2 @(task.StatusId == 3 ? "line-through text-slate-400" : "")">
102
+ @task.Title
103
+ </h4>
104
+ <div class="flex items-center justify-between gap-2 mt-0.5">
105
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[8px] font-semibold @GetStatusBadge(task.StatusId)">
106
+ @GetStatusLabel(task.StatusId)
107
+ </span>
108
+ <span class="text-[9px] text-slate-400 shrink-0 font-sans">
109
+ Due @DisplayFormats.Date(task.DueDate)
110
+ </span>
111
+ </div>
112
+ </button>
113
+ }
114
+ </div>
115
+ </div>
116
+ }
117
+ }
118
+
119
+ @if (!displayedAnyTask)
120
+ {
121
+ <div class="text-center py-12 text-slate-400 space-y-2">
122
+ <i data-lucide="check-square" class="h-8 w-8 mx-auto text-slate-300"></i>
123
+ <p class="text-xs">No tasks match filters.</p>
124
+ </div>
125
+ }
126
+ }
127
+ else
128
+ {
129
+ <div class="text-center py-12 text-slate-400 space-y-2">
130
+ <i data-lucide="folder-open" class="h-8 w-8 mx-auto text-slate-300"></i>
131
+ <p class="text-xs">No projects found.</p>
132
+ </div>
133
+ }
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Right Column: Task Details Panel -->
138
+ <div class="lg:col-span-2 xl:col-span-3">
139
+ @if (selectedTaskId.HasValue)
140
+ {
141
+ <div class="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm min-h-[600px]">
142
+ <TaskDetails TaskId="selectedTaskId.Value" ViewMode="Embedded" OnTaskUpdated="RefreshTasksList" />
143
+ </div>
144
+ }
145
+ else
146
+ {
147
+ <div class="bg-white border border-slate-200 rounded-2xl p-12 shadow-sm text-center flex flex-col items-center justify-center min-h-[600px] text-slate-400">
148
+ <i data-lucide="layers" class="h-12 w-12 text-slate-300 mb-3 animate-pulse"></i>
149
+ <h4 class="text-sm font-bold text-slate-700">Select a task from the backlog</h4>
150
+ <p class="text-xs text-slate-400 mt-1 max-w-sm">Click any task on the left list to view details, update status, post comments, or upload file attachments inline.</p>
151
+ </div>
152
+ }
153
+ </div>
154
+ </div>
155
+ }
156
+ </div>
157
+
158
+ @code {
159
+ private bool isLoading = true;
160
+ private bool isAdmin = false;
161
+ private bool isManager = false;
162
+ private long currentUserId = 0;
163
+
164
+ private List<TaskDto> taskList = new();
165
+ private List<ProjectDto> projects = new();
166
+ private long? selectedTaskId;
167
+
168
+ // Filters state
169
+ private string searchInput = "";
170
+ private string searchQuery => searchInput.Trim().ToLower();
171
+ private long filterProjectId = 0;
172
+ private long filterStatusId = 0;
173
+ private long filterPriorityId = 0;
174
+
175
+ protected override async Task OnInitializedAsync()
176
+ {
177
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
178
+ var user = authState.User;
179
+ isAdmin = user.IsInRole("Admin");
180
+ isManager = user.IsInRole("Manager");
181
+ var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
182
+ if (idStr != null) long.TryParse(idStr, out currentUserId);
183
+
184
+ await LoadData();
185
+ }
186
+
187
+ protected override async Task OnAfterRenderAsync(bool firstRender)
188
+ {
189
+ await JS.InvokeVoidAsync("initIcons");
190
+ }
191
+
192
+ private async Task LoadData()
193
+ {
194
+ isLoading = true;
195
+ var client = ApiClient.CreateClient();
196
+ try
197
+ {
198
+ var tasksTask = client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive);
199
+ var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
200
+ await System.Threading.Tasks.Task.WhenAll(tasksTask, projectsTask);
201
+
202
+ var allTasks = tasksTask.Result ?? new();
203
+ // Non-admin / non-manager: only show tasks assigned to them
204
+ taskList = (!(isAdmin || isManager) && currentUserId > 0)
205
+ ? allTasks.Where(t => t.AssignedTo == currentUserId).ToList()
206
+ : allTasks;
207
+
208
+ projects = projectsTask.Result ?? new();
209
+
210
+ // Auto-select first task if list is not empty
211
+ var filtered = FilteredTasks.ToList();
212
+ if (filtered.Any())
213
+ {
214
+ selectedTaskId = filtered.First().Id;
215
+ }
216
+ }
217
+ catch (Exception ex)
218
+ {
219
+ Console.WriteLine($"Backlog load error: {ex.Message}");
220
+ }
221
+ finally
222
+ {
223
+ isLoading = false;
224
+ }
225
+ }
226
+
227
+ private async Task RefreshTasksList()
228
+ {
229
+ // Re-load tasks from API when updated inline
230
+ var client = ApiClient.CreateClient();
231
+ try
232
+ {
233
+ var allTasks = await client.GetFromJsonAsync<List<TaskDto>>("Task", Serialization.CaseInsensitive) ?? new();
234
+ taskList = (!(isAdmin || isManager) && currentUserId > 0)
235
+ ? allTasks.Where(t => t.AssignedTo == currentUserId).ToList()
236
+ : allTasks;
237
+ }
238
+ catch (Exception ex)
239
+ {
240
+ Console.WriteLine($"Refresh tasks error: {ex.Message}");
241
+ }
242
+ }
243
+
244
+ private void SelectTask(long taskId)
245
+ {
246
+ selectedTaskId = taskId;
247
+ }
248
+
249
+ private IEnumerable<TaskDto> FilteredTasks
250
+ {
251
+ get
252
+ {
253
+ var result = taskList.AsEnumerable();
254
+ if (!string.IsNullOrWhiteSpace(searchQuery))
255
+ {
256
+ result = result.Where(t => t.Title.ToLower().Contains(searchQuery)
257
+ || (t.Description != null && t.Description.ToLower().Contains(searchQuery)));
258
+ }
259
+ if (filterProjectId > 0)
260
+ {
261
+ result = result.Where(t => t.ProjectId == filterProjectId);
262
+ }
263
+ if (filterStatusId > 0)
264
+ {
265
+ result = result.Where(t => t.StatusId == filterStatusId);
266
+ }
267
+ if (filterPriorityId > 0)
268
+ {
269
+ result = result.Where(t => t.PriorityId == filterPriorityId);
270
+ }
271
+ return result;
272
+ }
273
+ }
274
+
275
+ private string GetStatusLabel(long statusId) => statusId switch
276
+ {
277
+ 1 => "To Do",
278
+ 2 => "In Progress",
279
+ 3 => "Done",
280
+ _ => "Unknown"
281
+ };
282
+
283
+ private string GetStatusBadge(long statusId) => statusId switch
284
+ {
285
+ 1 => "bg-slate-100 text-slate-800 border border-slate-200",
286
+ 2 => "bg-blue-50 text-blue-700 border border-blue-100",
287
+ 3 => "bg-emerald-50 text-emerald-700 border border-emerald-100",
288
+ _ => "bg-slate-50 text-slate-600"
289
+ };
290
+
291
+ private string GetPriorityLabel(long priorityId) => priorityId switch
292
+ {
293
+ 1 => "Low",
294
+ 2 => "Medium",
295
+ 3 => "High",
296
+ _ => "Unknown"
297
+ };
298
+
299
+ private string GetPriorityBadge(long priorityId) => priorityId switch
300
+ {
301
+ 1 => "bg-slate-100 text-slate-600",
302
+ 2 => "bg-amber-50 text-amber-700 border border-amber-100",
303
+ 3 => "bg-rose-50 text-rose-700 border border-rose-100",
304
+ _ => "bg-slate-50 text-slate-600"
305
+ };
306
+ }
TaskTrackingSystem.WebApp/Components/Pages/EmployeeReport.razor CHANGED
@@ -131,6 +131,7 @@
131
  <table class="w-full">
132
  <thead>
133
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
134
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th>
135
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
136
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
@@ -141,9 +142,11 @@
141
  <tbody class="divide-y divide-slate-100">
142
  @if (PagedList.Any())
143
  {
144
- @foreach (var emp in PagedList)
145
  {
 
146
  <tr class="hover:bg-slate-50/50 transition-colors">
 
147
  <td class="px-6 py-4">
148
  <div class="flex items-center space-x-3">
149
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
@@ -182,7 +185,7 @@
182
  else
183
  {
184
  <tr>
185
- <td colspan="5" class="px-6 py-12 text-center">
186
  <p class="text-sm text-slate-400">No employee data found matching criteria.</p>
187
  </td>
188
  </tr>
@@ -361,7 +364,7 @@
361
  });
362
  }
363
 
364
- displayList = metrics.OrderByDescending(m => m.Efficiency).ToList();
365
  }
366
 
367
  private async Task DownloadExcel()
 
131
  <table class="w-full">
132
  <thead>
133
  <tr class="border-b border-slate-100 bg-slate-50/60">
134
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
135
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Employee</th>
136
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
137
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed Tasks</th>
 
142
  <tbody class="divide-y divide-slate-100">
143
  @if (PagedList.Any())
144
  {
145
+ @foreach (var entry in PagedList.Select((emp, index) => (emp, index)))
146
  {
147
+ var emp = entry.emp;
148
  <tr class="hover:bg-slate-50/50 transition-colors">
149
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
150
  <td class="px-6 py-4">
151
  <div class="flex items-center space-x-3">
152
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
 
185
  else
186
  {
187
  <tr>
188
+ <td colspan="6" class="px-6 py-12 text-center">
189
  <p class="text-sm text-slate-400">No employee data found matching criteria.</p>
190
  </td>
191
  </tr>
 
364
  });
365
  }
366
 
367
+ displayList = metrics.OrderBy(m => m.FullName).ToList();
368
  }
369
 
370
  private async Task DownloadExcel()
TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor CHANGED
@@ -1,5 +1,4 @@
1
  @page "/projects/{ProjectId:long}/tasks"
2
- @page "/board"
3
  @using Microsoft.AspNetCore.Authorization
4
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
5
  @attribute [Microsoft.AspNetCore.Authorization.Authorize]
@@ -307,7 +306,7 @@
307
  }
308
  }
309
 
310
- private void GoBack() => Navigation.NavigateTo("/projects");
311
 
312
  private void OpenCreateTaskModal()
313
  {
@@ -383,7 +382,7 @@
383
  if (response.IsSuccessStatusCode)
384
  {
385
  CloseTaskModal();
386
- Navigation.NavigateTo("/tasks");
387
  }
388
  else
389
  {
 
1
  @page "/projects/{ProjectId:long}/tasks"
 
2
  @using Microsoft.AspNetCore.Authorization
3
  @rendermode @(new InteractiveServerRenderMode(prerender: false))
4
  @attribute [Microsoft.AspNetCore.Authorization.Authorize]
 
306
  }
307
  }
308
 
309
+ private void GoBack() => Navigation.NavigateTo("/projects/all");
310
 
311
  private void OpenCreateTaskModal()
312
  {
 
382
  if (response.IsSuccessStatusCode)
383
  {
384
  CloseTaskModal();
385
+ Navigation.NavigateTo("/tasks/all");
386
  }
387
  else
388
  {
TaskTrackingSystem.WebApp/Components/Pages/OverdueTasksReport.razor CHANGED
@@ -132,6 +132,7 @@
132
  <table class="w-full">
133
  <thead>
134
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
135
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
136
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
137
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
@@ -145,12 +146,14 @@
145
  <tbody class="divide-y divide-slate-100">
146
  @if (PagedTasks.Any())
147
  {
148
- @foreach (var task in PagedTasks)
149
  {
 
150
  var daysOverdue = (DateTime.Today - task.DueDate).Days;
151
  var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
152
  var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
153
  <tr class="hover:bg-slate-50/50 transition-colors">
 
154
  <td class="px-6 py-4">
155
  <div>
156
  <p class="text-sm font-semibold text-slate-900">@task.Title</p>
@@ -239,7 +242,7 @@
239
  }
240
  else
241
  {
242
- <EmptyState ColSpan="7" Icon="check-circle" Title="No overdue tasks" Subtitle="Great! No tasks are overdue or at risk based on the current filters." />
243
  }
244
  </tbody>
245
  </table>
@@ -348,7 +351,7 @@
348
  if (appliedPriority > 0)
349
  result = result.Where(t => t.PriorityId == appliedPriority);
350
 
351
- return result.OrderByDescending(t => (DateTime.Today - t.DueDate).Days);
352
  }
353
  }
354
 
 
132
  <table class="w-full">
133
  <thead>
134
  <tr class="border-b border-slate-100 bg-slate-50/60">
135
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
136
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
137
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
138
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
 
146
  <tbody class="divide-y divide-slate-100">
147
  @if (PagedTasks.Any())
148
  {
149
+ @foreach (var entry in PagedTasks.Select((task, index) => (task, index)))
150
  {
151
+ var task = entry.task;
152
  var daysOverdue = (DateTime.Today - task.DueDate).Days;
153
  var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo);
154
  var project = allProjects.FirstOrDefault(p => p.Id == task.ProjectId);
155
  <tr class="hover:bg-slate-50/50 transition-colors">
156
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td>
157
  <td class="px-6 py-4">
158
  <div>
159
  <p class="text-sm font-semibold text-slate-900">@task.Title</p>
 
242
  }
243
  else
244
  {
245
+ <EmptyState ColSpan="8" Icon="check-circle" Title="No overdue tasks" Subtitle="Great! No tasks are overdue or at risk based on the current filters." />
246
  }
247
  </tbody>
248
  </table>
 
351
  if (appliedPriority > 0)
352
  result = result.Where(t => t.PriorityId == appliedPriority);
353
 
354
+ return result.OrderBy(t => t.Title).ThenBy(t => t.DueDate);
355
  }
356
  }
357
 
TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor CHANGED
@@ -432,7 +432,7 @@
432
  statusMessage = "Project assignments saved successfully!";
433
  successMessage = "Assignment saved successfully!";
434
  isError = false;
435
- Navigation.NavigateTo("/projects");
436
  }
437
  else
438
  {
 
432
  statusMessage = "Project assignments saved successfully!";
433
  successMessage = "Assignment saved successfully!";
434
  isError = false;
435
+ Navigation.NavigateTo("/projects/all");
436
  }
437
  else
438
  {
TaskTrackingSystem.WebApp/Components/Pages/ProjectProgressReport.razor CHANGED
@@ -172,6 +172,7 @@
172
  <table class="w-full">
173
  <thead>
174
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
175
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project Name</th>
176
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
177
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
@@ -183,9 +184,11 @@
183
  <tbody class="divide-y divide-slate-100">
184
  @if (PagedList.Any())
185
  {
186
- @foreach (var proj in PagedList)
187
  {
 
188
  <tr class="hover:bg-slate-50/50 transition-colors">
 
189
  <td class="px-6 py-4 text-sm font-semibold text-slate-900">@proj.ProjectName</td>
190
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.StartDate)</td>
191
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.EndDate)</td>
@@ -205,7 +208,7 @@
205
  else
206
  {
207
  <tr>
208
- <td colspan="6" class="px-6 py-12 text-center">
209
  <p class="text-sm text-slate-400">No project data found matching criteria.</p>
210
  </td>
211
  </tr>
@@ -403,7 +406,7 @@
403
  });
404
  }
405
 
406
- displayList = metrics.OrderByDescending(m => m.Progress).ToList();
407
 
408
  // Calculate card statistics
409
  totalActiveProjects = displayList.Count;
 
172
  <table class="w-full">
173
  <thead>
174
  <tr class="border-b border-slate-100 bg-slate-50/60">
175
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
176
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project Name</th>
177
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
178
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
 
184
  <tbody class="divide-y divide-slate-100">
185
  @if (PagedList.Any())
186
  {
187
+ @foreach (var entry in PagedList.Select((proj, index) => (proj, index)))
188
  {
189
+ var proj = entry.proj;
190
  <tr class="hover:bg-slate-50/50 transition-colors">
191
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
192
  <td class="px-6 py-4 text-sm font-semibold text-slate-900">@proj.ProjectName</td>
193
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.StartDate)</td>
194
  <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(proj.EndDate)</td>
 
208
  else
209
  {
210
  <tr>
211
+ <td colspan="7" class="px-6 py-12 text-center">
212
  <p class="text-sm text-slate-400">No project data found matching criteria.</p>
213
  </td>
214
  </tr>
 
406
  });
407
  }
408
 
409
+ displayList = metrics.OrderBy(m => m.ProjectName).ToList();
410
 
411
  // Calculate card statistics
412
  totalActiveProjects = displayList.Count;
TaskTrackingSystem.WebApp/Components/Pages/Projects.razor CHANGED
@@ -60,6 +60,7 @@
60
  <table class="w-full">
61
  <thead>
62
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
63
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Name</th>
64
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
65
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
@@ -70,9 +71,11 @@
70
  <tbody class="divide-y divide-slate-100">
71
  @if (PagedProjects.Any())
72
  {
73
- @foreach (var project in PagedProjects)
74
  {
 
75
  <tr class="hover:bg-slate-50/50 transition-colors">
 
76
  <td class="px-6 py-4">
77
  <button @onclick="() => NavigateToKanban(project.Id)" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">
78
  @project.Name
@@ -118,7 +121,7 @@
118
  }
119
  else
120
  {
121
- <EmptyState ColSpan="5" Icon="folder-kanban" Title="No projects yet" Subtitle="@(canCreateProject ? "Create your first project to get started." : "You have not been assigned to any projects.")" />
122
  }
123
  </tbody>
124
  </table>
 
60
  <table class="w-full">
61
  <thead>
62
  <tr class="border-b border-slate-100 bg-slate-50/60">
63
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
64
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Name</th>
65
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Start Date</th>
66
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">End Date</th>
 
71
  <tbody class="divide-y divide-slate-100">
72
  @if (PagedProjects.Any())
73
  {
74
+ @foreach (var entry in PagedProjects.Select((project, index) => (project, index)))
75
  {
76
+ var project = entry.project;
77
  <tr class="hover:bg-slate-50/50 transition-colors">
78
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
79
  <td class="px-6 py-4">
80
  <button @onclick="() => NavigateToKanban(project.Id)" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">
81
  @project.Name
 
121
  }
122
  else
123
  {
124
+ <EmptyState ColSpan="6" Icon="folder-kanban" Title="No projects yet" Subtitle="@(canCreateProject ? "Create your first project to get started." : "You have not been assigned to any projects.")" />
125
  }
126
  </tbody>
127
  </table>
TaskTrackingSystem.WebApp/Components/Pages/Roles.razor CHANGED
@@ -59,6 +59,7 @@
59
  <table class="w-full">
60
  <thead>
61
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
62
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Role</th>
63
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th>
64
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Created</th>
@@ -68,9 +69,11 @@
68
  <tbody class="divide-y divide-slate-100">
69
  @if (PagedRoles.Any())
70
  {
71
- @foreach (var role in PagedRoles)
72
  {
 
73
  <tr class="hover:bg-slate-50/50 transition-colors">
 
74
  <td class="px-6 py-4">
75
  <div class="flex items-center space-x-3">
76
  <span class="flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600">
@@ -111,7 +114,7 @@
111
  }
112
  else
113
  {
114
- <EmptyState ColSpan="4" Icon="shield-check" Title="No roles yet" Subtitle="Create your first role to get started." />
115
  }
116
  </tbody>
117
  </table>
 
59
  <table class="w-full">
60
  <thead>
61
  <tr class="border-b border-slate-100 bg-slate-50/60">
62
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
63
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Role</th>
64
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Description</th>
65
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Created</th>
 
69
  <tbody class="divide-y divide-slate-100">
70
  @if (PagedRoles.Any())
71
  {
72
+ @foreach (var entry in PagedRoles.Select((role, index) => (role, index)))
73
  {
74
+ var role = entry.role;
75
  <tr class="hover:bg-slate-50/50 transition-colors">
76
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
77
  <td class="px-6 py-4">
78
  <div class="flex items-center space-x-3">
79
  <span class="flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600">
 
114
  }
115
  else
116
  {
117
+ <EmptyState ColSpan="5" Icon="shield-check" Title="No roles yet" Subtitle="Create your first role to get started." />
118
  }
119
  </tbody>
120
  </table>
TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor CHANGED
@@ -504,7 +504,7 @@
504
  statusMessage = "Task assignee updated successfully!";
505
  successMessage = "Assignment saved successfully!";
506
  isError = false;
507
- Navigation.NavigateTo("/tasks");
508
  }
509
  else
510
  {
 
504
  statusMessage = "Task assignee updated successfully!";
505
  successMessage = "Assignment saved successfully!";
506
  isError = false;
507
+ Navigation.NavigateTo("/tasks/all");
508
  }
509
  else
510
  {
TaskTrackingSystem.WebApp/Components/Pages/TaskDetails.razor ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/tasks/{TaskId:long}/details"
2
+ @using Microsoft.AspNetCore.Authorization
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @using Microsoft.AspNetCore.Components.Forms
5
+ @using TaskTrackingSystem.Shared
6
+ @using TaskTrackingSystem.Shared.Models.Task
7
+ @using TaskTrackingSystem.Shared.Models.Comment
8
+ @using TaskTrackingSystem.Shared.Models.Attachment
9
+ @using TaskTrackingSystem.Shared.Models.Project
10
+ @using TaskTrackingSystem.Shared.Models.User
11
+ @using TaskTrackingSystem.WebApp.Components.Partial
12
+ @rendermode @(new InteractiveServerRenderMode(prerender: false))
13
+ @attribute [Authorize]
14
+ @inject ApiClientService ApiClient
15
+ @inject IJSRuntime JS
16
+ @inject NavigationManager Navigation
17
+ @inject AuthenticationStateProvider AuthStateProvider
18
+ @inject MenuAuthorizationService MenuAuthorization
19
+
20
+ <PageTitle>Task Details</PageTitle>
21
+
22
+ <div class="space-y-6">
23
+ @if (ViewMode != "Embedded")
24
+ {
25
+ <!-- Breadcrumbs & Actions -->
26
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
27
+ <div class="flex items-center space-x-2 text-sm text-slate-500">
28
+ <a href="/dashboard" class="hover:text-violet-600 transition-colors">Workspace</a>
29
+ <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i>
30
+ <a href="/tasks" class="hover:text-violet-600 transition-colors">Tasks</a>
31
+ <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i>
32
+ <span class="text-slate-800 font-medium">Task Details</span>
33
+ </div>
34
+ <a href="/tasks" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
35
+ <i data-lucide="arrow-left" class="h-4 w-4 mr-2 text-slate-500"></i>
36
+ Back to Tasks
37
+ </a>
38
+ </div>
39
+ }
40
+
41
+ @if (isLoading)
42
+ {
43
+ <LoadingSpinner Message="Loading task details..." />
44
+ }
45
+ else if (task == null)
46
+ {
47
+ <div class="rounded-2xl border border-slate-200 bg-white p-12 text-center">
48
+ <i data-lucide="alert-circle" class="h-10 w-10 mx-auto text-red-500 mb-3"></i>
49
+ <h4 class="text-lg font-bold text-slate-800">Task Not Found</h4>
50
+ <p class="text-sm text-slate-400 mt-1">The task you are looking for does not exist or has been deleted.</p>
51
+ </div>
52
+ }
53
+ else
54
+ {
55
+ <!-- Task Header Card -->
56
+ <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
57
+ <div class="flex flex-wrap items-start justify-between gap-4">
58
+ <div class="space-y-2 flex-1 min-w-0">
59
+ <div class="flex flex-wrap items-center gap-2">
60
+ <span class="text-xs font-bold font-mono text-slate-400">TASK-#@task.Id</span>
61
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetStatusBadge(task.StatusId)">
62
+ @GetStatusLabel(task.StatusId)
63
+ </span>
64
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetPriorityBadge(task.PriorityId)">
65
+ @GetPriorityLabel(task.PriorityId)
66
+ </span>
67
+ @if (project != null)
68
+ {
69
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-violet-50 text-violet-700 border border-violet-100">
70
+ <i data-lucide="folder" class="h-3 w-3 mr-1.5"></i>
71
+ @project.Name
72
+ </span>
73
+ }
74
+ </div>
75
+ @if (isEditingTitle)
76
+ {
77
+ <div class="flex items-center gap-2 max-w-xl mt-1">
78
+ <input @bind="tempTitle" type="text" class="flex-1 rounded-lg border border-slate-200 px-3 py-1 text-lg font-bold text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600" />
79
+ <button @onclick="CancelEditTitle" class="px-2.5 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 rounded-lg transition-colors">Cancel</button>
80
+ <button @onclick="SaveTitle" disabled="@isSavingTitle" class="px-2.5 py-1 text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-lg transition-colors shadow-sm">Save</button>
81
+ </div>
82
+ }
83
+ else
84
+ {
85
+ <h2 class="text-2xl font-bold text-slate-900 leading-tight flex items-center gap-2 group">
86
+ @task.Title
87
+ @if (hasTaskManagePermission)
88
+ {
89
+ <button @onclick="StartEditTitle" class="opacity-0 group-hover:opacity-100 p-1 text-slate-400 hover:text-violet-600 transition-all rounded-lg hover:bg-slate-50" title="Edit title">
90
+ <i data-lucide="pencil" class="h-4 w-4"></i>
91
+ </button>
92
+ }
93
+ </h2>
94
+ }
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
100
+ <!-- Left Grid: Task Details & Description -->
101
+ <div class="lg:col-span-2 space-y-6">
102
+ <!-- Description Card -->
103
+ <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm space-y-4">
104
+ <div class="flex items-center justify-between">
105
+ <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2">
106
+ <i data-lucide="align-left" class="h-4 w-4"></i> Description
107
+ </h3>
108
+ @if (hasTaskManagePermission && !isEditingDescription)
109
+ {
110
+ <button @onclick="StartEditDescription" class="inline-flex items-center text-xs font-semibold text-violet-600 hover:text-violet-800 transition-colors">
111
+ <i data-lucide="pencil" class="h-3 w-3 mr-1"></i> Edit
112
+ </button>
113
+ }
114
+ </div>
115
+ @if (isEditingDescription)
116
+ {
117
+ <div class="space-y-3">
118
+ <textarea @bind="tempDescription" rows="4" class="w-full text-sm rounded-xl border border-slate-200 p-3 text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea>
119
+ <div class="flex items-center gap-2 justify-end">
120
+ <button @onclick="CancelEditDescription" class="px-3 py-1.5 text-xs font-medium text-slate-500 hover:bg-slate-100 rounded-lg transition-colors">Cancel</button>
121
+ <button @onclick="SaveDescription" disabled="@isSavingDescription" class="px-3 py-1.5 text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-lg transition-colors shadow-sm">
122
+ @(isSavingDescription ? "Saving..." : "Save")
123
+ </button>
124
+ </div>
125
+ </div>
126
+ }
127
+ else
128
+ {
129
+ <div class="text-sm text-slate-700 leading-relaxed min-h-[120px] whitespace-pre-wrap">
130
+ @(string.IsNullOrWhiteSpace(task.Description) ? "No description provided for this task." : task.Description)
131
+ </div>
132
+ }
133
+ </div>
134
+
135
+ <!-- Properties Grid (Pristine grid alignment in uniform 2-column layout) -->
136
+ <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
137
+ <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2 mb-5">
138
+ <i data-lucide="info" class="h-4 w-4"></i> Properties
139
+ </h3>
140
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-5 text-sm font-sans">
141
+ <!-- Status Property (Free for everyone) -->
142
+ <div class="flex flex-col gap-1.5">
143
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Status</span>
144
+ <select value="@task.StatusId" @onchange="OnStatusChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm">
145
+ <option value="1">To Do</option>
146
+ <option value="2">In Progress</option>
147
+ <option value="3">Done</option>
148
+ </select>
149
+ </div>
150
+
151
+ <!-- Priority Property -->
152
+ <div class="flex flex-col gap-1.5">
153
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Priority</span>
154
+ @if (hasTaskManagePermission)
155
+ {
156
+ <select value="@task.PriorityId" @onchange="OnPriorityChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm">
157
+ <option value="1">Low</option>
158
+ <option value="2">Medium</option>
159
+ <option value="3">High</option>
160
+ </select>
161
+ }
162
+ else
163
+ {
164
+ <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none">
165
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetPriorityBadge(task.PriorityId)">
166
+ @GetPriorityLabel(task.PriorityId)
167
+ </span>
168
+ </div>
169
+ }
170
+ </div>
171
+
172
+ <!-- Assignee Property -->
173
+ <div class="flex flex-col gap-1.5">
174
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Assignee</span>
175
+ @if (hasTaskManagePermission)
176
+ {
177
+ <select value="@(task.AssignedTo ?? 0)" @onchange="OnAssigneeChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm">
178
+ <option value="0">Unassigned</option>
179
+ @foreach (var u in allUsers)
180
+ {
181
+ <option value="@u.Id">@u.FirstName @u.LastName</option>
182
+ }
183
+ </select>
184
+ }
185
+ else
186
+ {
187
+ <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none gap-2">
188
+ @if (assignee != null)
189
+ {
190
+ <span class="flex h-5 w-5 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold">
191
+ @(assignee.FirstName.Length > 0 ? assignee.FirstName[0].ToString().ToUpper() : "")
192
+ </span>
193
+ <span class="font-medium text-slate-700">@assignee.FirstName @assignee.LastName</span>
194
+ }
195
+ else
196
+ {
197
+ <span class="text-slate-400 italic">Unassigned</span>
198
+ }
199
+ </div>
200
+ }
201
+ </div>
202
+
203
+ <!-- Due Date Property -->
204
+ <div class="flex flex-col gap-1.5">
205
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Due Date</span>
206
+ @if (hasTaskManagePermission)
207
+ {
208
+ <input type="date" value="@task.DueDate.ToString("yyyy-MM-dd")" @onchange="OnDueDateChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm" />
209
+ }
210
+ else
211
+ {
212
+ <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none font-medium text-slate-700">
213
+ <i data-lucide="calendar" class="h-4 w-4 mr-2 text-slate-400"></i>
214
+ @DisplayFormats.Date(task.DueDate)
215
+ </div>
216
+ }
217
+ </div>
218
+
219
+ <!-- Estimated Hours Property -->
220
+ <div class="flex flex-col gap-1.5">
221
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Estimated Hours</span>
222
+ @if (hasTaskManagePermission)
223
+ {
224
+ <div class="relative flex items-center w-full">
225
+ <input type="number" step="0.5" min="0" max="999" value="@task.EstimatedHours" @onchange="OnEstimatedHoursChanged" placeholder="0.0" class="h-9 rounded-lg border border-slate-200 pl-3 pr-8 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm" />
226
+ <span class="absolute right-3 text-xs text-slate-400 pointer-events-none">hr</span>
227
+ </div>
228
+ }
229
+ else
230
+ {
231
+ <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none font-medium text-slate-700">
232
+ <i data-lucide="clock" class="h-4 w-4 mr-2 text-slate-400"></i>
233
+ @(task.EstimatedHours.HasValue ? $"{task.EstimatedHours} hr" : "Not set")
234
+ </div>
235
+ }
236
+ </div>
237
+
238
+ <!-- Created At Property -->
239
+ <div class="flex flex-col gap-1.5">
240
+ <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Created At</span>
241
+ <div class="h-9 flex items-center px-1 border-b border-transparent text-sm text-slate-800 cursor-default select-none font-medium text-slate-700">
242
+ <i data-lucide="calendar-plus" class="h-4 w-4 mr-2 text-slate-400"></i>
243
+ @task.CreatedAt.ToString("dd-MM-yyyy HH:mm")
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <!-- Right Grid: Collaboration Hub (Comments & Attachments) -->
251
+ <div class="space-y-6">
252
+ <div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[580px]">
253
+ <!-- Hub Tabs -->
254
+ <div class="flex border-b border-slate-100 bg-slate-50/50 p-1">
255
+ <button @onclick='() => SetTab("comments")' class="flex-1 py-2 rounded-lg text-xs font-bold flex items-center justify-center gap-2 transition-all @(currentTab == "comments" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
256
+ <i data-lucide="message-square" class="h-4 w-4"></i>
257
+ <span>Comments (@comments.Count)</span>
258
+ </button>
259
+ <button @onclick='() => SetTab("files")' class="flex-1 py-2 rounded-lg text-xs font-bold flex items-center justify-center gap-2 transition-all @(currentTab == "files" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
260
+ <i data-lucide="paperclip" class="h-4 w-4"></i>
261
+ <span>Files (@attachments.Count)</span>
262
+ </button>
263
+ </div>
264
+
265
+ <!-- Hub Body -->
266
+ <div class="flex-1 overflow-y-auto p-4 min-h-0">
267
+ @if (currentTab == "comments")
268
+ {
269
+ <!-- Comments List -->
270
+ <div class="space-y-4 font-sans">
271
+ @if (comments.Any())
272
+ {
273
+ @foreach (var c in comments)
274
+ {
275
+ <div class="flex items-start gap-3">
276
+ <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0 mt-0.5">
277
+ @(c.UserFullName.Length > 0 ? c.UserFullName[0].ToString().ToUpper() : "U")
278
+ </span>
279
+ <div class="flex-1 min-w-0 bg-slate-50 rounded-xl p-3 border border-slate-100">
280
+ <div class="flex items-center justify-between gap-2">
281
+ <span class="text-xs font-bold text-slate-800 truncate">@c.UserFullName</span>
282
+ <span class="text-[9px] text-slate-400 shrink-0 font-sans">@c.CreatedAt.ToString("g")</span>
283
+ </div>
284
+ <p class="text-xs text-slate-400 mt-0.5">@c.UserRoleName</p>
285
+ <div class="text-xs text-slate-700 mt-2 whitespace-pre-wrap break-words">@c.Message</div>
286
+ </div>
287
+ @if (c.UserId == currentUserId || isAdmin)
288
+ {
289
+ <button @onclick="() => ConfirmDeleteComment(c.Id)" class="p-1 text-slate-300 hover:text-rose-500 rounded-lg transition-colors shrink-0" title="Delete comment">
290
+ <i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
291
+ </button>
292
+ }
293
+ </div>
294
+ }
295
+ }
296
+ else
297
+ {
298
+ <div class="text-center py-12 text-slate-400 space-y-2">
299
+ <i data-lucide="messages-square" class="h-8 w-8 mx-auto text-slate-300"></i>
300
+ <p class="text-xs">No comments posted yet.</p>
301
+ <p class="text-[10px]">Start the conversation below!</p>
302
+ </div>
303
+ }
304
+ </div>
305
+ }
306
+ else
307
+ {
308
+ <!-- Attachments List -->
309
+ <div class="space-y-3 font-sans">
310
+ @if (attachments.Any())
311
+ {
312
+ @foreach (var file in attachments)
313
+ {
314
+ <div class="flex items-center justify-between p-2.5 rounded-xl border border-slate-200 bg-white hover:bg-slate-50/50 transition-colors gap-3">
315
+ <div class="flex items-center gap-2.5 min-w-0">
316
+ <span class="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 text-slate-500 shrink-0">
317
+ <i data-lucide="@GetFileIcon(file.FileType)" class="h-4 w-4"></i>
318
+ </span>
319
+ <div class="min-w-0">
320
+ <p class="text-xs font-semibold text-slate-800 truncate" title="@file.FileName">@file.FileName</p>
321
+ <p class="text-[10px] text-slate-400 mt-0.5 font-sans">@FormatBytes(file.FileSizeInBytes) • by @file.CreatedByName</p>
322
+ </div>
323
+ </div>
324
+ <div class="flex items-center gap-1.5 shrink-0">
325
+ <a href="@($"{ApiClient.BaseUrl}/api/TaskDetails/attachments/{file.Id}/download")" target="_blank" class="p-1.5 text-slate-400 hover:text-violet-600 hover:bg-violet-50 rounded-lg transition-all" title="Download">
326
+ <i data-lucide="download" class="h-4 w-4"></i>
327
+ </a>
328
+ @if (file.CreatedBy == currentUserId || isAdmin)
329
+ {
330
+ <button @onclick="() => ConfirmDeleteAttachment(file.Id)" class="p-1.5 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all" title="Delete file">
331
+ <i data-lucide="trash-2" class="h-4 w-4"></i>
332
+ </button>
333
+ }
334
+ </div>
335
+ </div>
336
+ }
337
+ }
338
+ else
339
+ {
340
+ <div class="text-center py-8 text-slate-400 space-y-2">
341
+ <i data-lucide="file-up" class="h-8 w-8 mx-auto text-slate-300"></i>
342
+ <p class="text-xs">No attachments found.</p>
343
+ </div>
344
+ }
345
+ </div>
346
+ }
347
+ </div>
348
+
349
+ <!-- Hub Footer (Inputs) (Free for everyone) -->
350
+ <div class="p-4 border-t border-slate-100 bg-slate-50/50">
351
+ @if (currentTab == "comments")
352
+ {
353
+ <div class="flex items-start gap-2">
354
+ <div class="flex-1">
355
+ <textarea @bind="newCommentMessage" rows="2" placeholder="Write a comment..." class="w-full rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" disabled="@isSavingComment"></textarea>
356
+ @if (!string.IsNullOrEmpty(commentError))
357
+ {
358
+ <p class="text-[10px] text-red-500 mt-1 font-sans">@commentError</p>
359
+ }
360
+ </div>
361
+ <button @onclick="PostComment" disabled="@(isSavingComment || string.IsNullOrWhiteSpace(newCommentMessage))" class="rounded-xl bg-violet-600 text-white p-2.5 hover:bg-violet-700 disabled:opacity-50 transition-all shadow-sm shrink-0">
362
+ <i data-lucide="send" class="h-4 w-4"></i>
363
+ </button>
364
+ </div>
365
+ }
366
+ else
367
+ {
368
+ <div>
369
+ <InputFile OnChange="OnInputFileChange" class="hidden" id="fileUpload" disabled="@isUploading" />
370
+ <label for="fileUpload" class="cursor-pointer flex flex-col items-center justify-center border-2 border-dashed border-slate-200 rounded-xl p-4 hover:border-violet-500 hover:bg-violet-50/20 transition-all">
371
+ @if (isUploading)
372
+ {
373
+ <LoadingSpinner Message="Uploading file..." />
374
+ }
375
+ else
376
+ {
377
+ <i data-lucide="upload-cloud" class="h-7 w-7 text-slate-400 mb-1.5"></i>
378
+ <span class="text-xs font-semibold text-slate-600">Click to upload file</span>
379
+ <span class="text-[9px] text-slate-400 mt-0.5">Maximum size: 10MB</span>
380
+ }
381
+ </label>
382
+ @if (!string.IsNullOrEmpty(uploadError))
383
+ {
384
+ <p class="text-[10px] text-red-500 mt-1.5 font-sans">@uploadError</p>
385
+ }
386
+ </div>
387
+ }
388
+ </div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ }
393
+ </div>
394
+
395
+ @* Confirm Actions dialog *@
396
+ <ConfirmDialog IsVisible="showConfirm"
397
+ Title="@confirmTitle"
398
+ Message="@confirmMessage"
399
+ ConfirmText="Yes, Delete"
400
+ Icon="trash-2"
401
+ IconBgClass="bg-rose-50"
402
+ IconTextClass="text-rose-500"
403
+ ButtonClass="bg-rose-600 hover:bg-rose-700"
404
+ OnConfirm="ExecuteConfirmAction"
405
+ OnCancel="CancelConfirm" />
406
+
407
+ @code {
408
+ [Parameter] public long TaskId { get; set; }
409
+ [Parameter] public string? ViewMode { get; set; }
410
+ [Parameter] public EventCallback OnTaskUpdated { get; set; }
411
+
412
+ private bool isLoading = true;
413
+ private TaskDto? task;
414
+ private ProjectDto? project;
415
+ private UserDto? assignee;
416
+ private List<CommentDto> comments = new();
417
+ private List<FileAttachmentDto> attachments = new();
418
+ private List<UserDto> allUsers = new();
419
+
420
+ private bool isAdmin = false;
421
+ private bool canEditTask = false;
422
+ private bool hasTaskManagePermission = false;
423
+ private long currentUserId = 0;
424
+ private string userRoleName = "Employee";
425
+
426
+ private List<string> scopes = new();
427
+
428
+ private string currentTab = "comments";
429
+
430
+ // Comments Form
431
+ private string newCommentMessage = "";
432
+ private bool isSavingComment = false;
433
+ private string? commentError;
434
+
435
+ // Attachments Form
436
+ private bool isUploading = false;
437
+ private string? uploadError;
438
+
439
+ // Confirm dialog
440
+ private bool showConfirm = false;
441
+ private string confirmTitle = "";
442
+ private string confirmMessage = "";
443
+ private Func<Task>? confirmAction;
444
+
445
+ // Inline edit states
446
+ private bool isEditingDescription = false;
447
+ private bool isSavingDescription = false;
448
+ private string tempDescription = "";
449
+
450
+ private bool isEditingTitle = false;
451
+ private bool isSavingTitle = false;
452
+ private string tempTitle = "";
453
+
454
+ private long lastLoadedTaskId = 0;
455
+
456
+ protected override async Task OnInitializedAsync()
457
+ {
458
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
459
+ var user = authState.User;
460
+ isAdmin = user.IsInRole("Admin");
461
+
462
+ // Task details editing is driven by role permissions so future roles can be granted access.
463
+ canEditTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Update");
464
+ hasTaskManagePermission = canEditTask;
465
+
466
+ var roleClaim = user.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value;
467
+ userRoleName = roleClaim ?? "Employee";
468
+
469
+ var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
470
+ if (idStr != null) long.TryParse(idStr, out currentUserId);
471
+ }
472
+
473
+ protected override async Task OnParametersSetAsync()
474
+ {
475
+ if (TaskId > 0 && TaskId != lastLoadedTaskId)
476
+ {
477
+ lastLoadedTaskId = TaskId;
478
+ assignee = null;
479
+ await LoadAllData();
480
+ }
481
+ }
482
+
483
+ protected override async Task OnAfterRenderAsync(bool firstRender)
484
+ {
485
+ await JS.InvokeVoidAsync("initIcons");
486
+ }
487
+
488
+ private void SetTab(string tab)
489
+ {
490
+ currentTab = tab;
491
+ _ = InvokeAsync(async () => {
492
+ await Task.Delay(50);
493
+ await JS.InvokeVoidAsync("initIcons");
494
+ });
495
+ }
496
+
497
+ private async Task LoadAllData()
498
+ {
499
+ isLoading = true;
500
+ var client = ApiClient.CreateClient();
501
+ try
502
+ {
503
+ var taskUrl = $"Task/{TaskId}";
504
+ var commentsUrl = $"TaskDetails/tasks/{TaskId}/comments";
505
+ var attachmentsUrl = $"TaskDetails/tasks/{TaskId}/attachments";
506
+ var usersUrl = "User";
507
+
508
+ var taskResponseTask = client.GetAsync(taskUrl);
509
+ var commentsResponseTask = client.GetAsync(commentsUrl);
510
+ var attachmentsResponseTask = client.GetAsync(attachmentsUrl);
511
+ var usersResponseTask = client.GetAsync(usersUrl);
512
+
513
+ await Task.WhenAll(taskResponseTask, commentsResponseTask, attachmentsResponseTask, usersResponseTask);
514
+
515
+ var taskResponse = taskResponseTask.Result;
516
+ if (taskResponse.IsSuccessStatusCode)
517
+ {
518
+ task = await taskResponse.Content.ReadFromJsonAsync<TaskDto>(Serialization.CaseInsensitive);
519
+ }
520
+
521
+ var commentsResponse = commentsResponseTask.Result;
522
+ if (commentsResponse.IsSuccessStatusCode)
523
+ {
524
+ comments = await commentsResponse.Content.ReadFromJsonAsync<List<CommentDto>>(Serialization.CaseInsensitive) ?? new();
525
+ }
526
+
527
+ var attachmentsResponse = attachmentsResponseTask.Result;
528
+ if (attachmentsResponse.IsSuccessStatusCode)
529
+ {
530
+ attachments = await attachmentsResponse.Content.ReadFromJsonAsync<List<FileAttachmentDto>>(Serialization.CaseInsensitive) ?? new();
531
+ }
532
+
533
+ var usersResponse = usersResponseTask.Result;
534
+ if (usersResponse.IsSuccessStatusCode)
535
+ {
536
+ allUsers = await usersResponse.Content.ReadFromJsonAsync<List<UserDto>>(Serialization.CaseInsensitive) ?? new();
537
+ }
538
+
539
+ if (task != null)
540
+ {
541
+ // Fetch project details
542
+ var projResponse = await client.GetAsync($"Project/{task.ProjectId}");
543
+ if (projResponse.IsSuccessStatusCode)
544
+ {
545
+ project = await projResponse.Content.ReadFromJsonAsync<ProjectDto>(Serialization.CaseInsensitive);
546
+ }
547
+
548
+ // Set assignee details
549
+ if (task.AssignedTo.HasValue)
550
+ {
551
+ assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo.Value);
552
+ }
553
+
554
+ CalculateScopes();
555
+ }
556
+ }
557
+ catch (Exception ex)
558
+ {
559
+ Console.WriteLine($"Load task details error: {ex.Message}");
560
+ }
561
+ finally
562
+ {
563
+ isLoading = false;
564
+ }
565
+ }
566
+
567
+ private void CalculateScopes()
568
+ {
569
+ scopes = new List<string>();
570
+
571
+ if (task == null) return;
572
+
573
+ if (hasTaskManagePermission)
574
+ {
575
+ scopes.Add("task:all");
576
+ scopes.Add("task:write");
577
+ scopes.Add("comment:write");
578
+ }
579
+ else
580
+ {
581
+ scopes.Add("task:read");
582
+ // Comments stay available to everyone who can view the task.
583
+ scopes.Add("comment:write");
584
+ }
585
+ }
586
+
587
+ // ─── INLINE MUTATIONS ───────────────────────────────────────────────────
588
+
589
+ private async Task OnStatusChanged(ChangeEventArgs e)
590
+ {
591
+ if (task == null) return;
592
+ if (long.TryParse(e.Value?.ToString(), out var newStatusId))
593
+ {
594
+ var client = ApiClient.CreateClient();
595
+ try
596
+ {
597
+ var response = await client.PatchAsync($"Task/{task.Id}/status?statusId={newStatusId}", null);
598
+ if (response.IsSuccessStatusCode)
599
+ {
600
+ task.StatusId = newStatusId;
601
+ await OnTaskUpdated.InvokeAsync();
602
+ await JS.InvokeVoidAsync("initIcons");
603
+ }
604
+ }
605
+ catch (Exception ex)
606
+ {
607
+ Console.WriteLine($"Update status error: {ex.Message}");
608
+ }
609
+ }
610
+ }
611
+
612
+ private async Task OnPriorityChanged(ChangeEventArgs e)
613
+ {
614
+ if (long.TryParse(e.Value?.ToString(), out var newPriorityId))
615
+ {
616
+ await UpdateTaskProperty(dto => dto.PriorityId = newPriorityId);
617
+ }
618
+ }
619
+
620
+ private async Task OnAssigneeChanged(ChangeEventArgs e)
621
+ {
622
+ if (long.TryParse(e.Value?.ToString(), out var assigneeId))
623
+ {
624
+ var newAssigneeId = assigneeId > 0 ? (long?)assigneeId : null;
625
+ await UpdateTaskProperty(dto => dto.AssignedTo = newAssigneeId);
626
+ assignee = newAssigneeId.HasValue ? allUsers.FirstOrDefault(u => u.Id == newAssigneeId.Value) : null;
627
+ }
628
+ }
629
+
630
+ private async Task OnDueDateChanged(ChangeEventArgs e)
631
+ {
632
+ if (DateTime.TryParse(e.Value?.ToString(), out var newDueDate))
633
+ {
634
+ await UpdateTaskProperty(dto => dto.DueDate = newDueDate);
635
+ }
636
+ }
637
+
638
+ private async Task OnEstimatedHoursChanged(ChangeEventArgs e)
639
+ {
640
+ var input = e.Value?.ToString();
641
+ decimal? newEstimatedHours = null;
642
+ if (!string.IsNullOrWhiteSpace(input) && decimal.TryParse(input, out var val))
643
+ {
644
+ newEstimatedHours = val;
645
+ }
646
+ await UpdateTaskProperty(dto => dto.EstimatedHours = newEstimatedHours);
647
+ }
648
+
649
+ private async Task UpdateTaskProperty(Action<UpdateTaskDto> applyChange)
650
+ {
651
+ if (task == null) return;
652
+
653
+ var dto = new UpdateTaskDto
654
+ {
655
+ Title = task.Title,
656
+ Description = task.Description,
657
+ StatusId = task.StatusId,
658
+ PriorityId = task.PriorityId,
659
+ DueDate = task.DueDate,
660
+ AssignedTo = task.AssignedTo,
661
+ AssignedBy = task.AssignedBy,
662
+ EstimatedHours = task.EstimatedHours
663
+ };
664
+
665
+ applyChange(dto);
666
+
667
+ var client = ApiClient.CreateClient();
668
+ try
669
+ {
670
+ var response = await client.PutAsJsonAsync($"Task/{task.Id}", dto);
671
+ if (response.IsSuccessStatusCode)
672
+ {
673
+ task.Title = dto.Title;
674
+ task.Description = dto.Description;
675
+ task.StatusId = dto.StatusId;
676
+ task.PriorityId = dto.PriorityId;
677
+ task.DueDate = dto.DueDate;
678
+ task.AssignedTo = dto.AssignedTo;
679
+ task.EstimatedHours = dto.EstimatedHours;
680
+
681
+ await OnTaskUpdated.InvokeAsync();
682
+ await JS.InvokeVoidAsync("initIcons");
683
+ }
684
+ }
685
+ catch (Exception ex)
686
+ {
687
+ Console.WriteLine($"Update task property error: {ex.Message}");
688
+ }
689
+ }
690
+
691
+ private void StartEditDescription()
692
+ {
693
+ if (task == null) return;
694
+ tempDescription = task.Description ?? "";
695
+ isEditingDescription = true;
696
+ }
697
+
698
+ private void CancelEditDescription()
699
+ {
700
+ isEditingDescription = false;
701
+ }
702
+
703
+ private async Task SaveDescription()
704
+ {
705
+ isSavingDescription = true;
706
+ await UpdateTaskProperty(dto => dto.Description = tempDescription);
707
+ isEditingDescription = false;
708
+ isSavingDescription = false;
709
+ }
710
+
711
+ private void StartEditTitle()
712
+ {
713
+ if (task == null) return;
714
+ tempTitle = task.Title;
715
+ isEditingTitle = true;
716
+ }
717
+
718
+ private void CancelEditTitle()
719
+ {
720
+ isEditingTitle = false;
721
+ }
722
+
723
+ private async Task SaveTitle()
724
+ {
725
+ if (string.IsNullOrWhiteSpace(tempTitle)) return;
726
+ isSavingTitle = true;
727
+ await UpdateTaskProperty(dto => dto.Title = tempTitle.Trim());
728
+ isEditingTitle = false;
729
+ isSavingTitle = false;
730
+ }
731
+
732
+ // ─── COMMENTS ACTIONS ───────────────────────────────────────────────────
733
+
734
+ private async Task PostComment()
735
+ {
736
+ if (string.IsNullOrWhiteSpace(newCommentMessage)) return;
737
+
738
+ isSavingComment = true;
739
+ commentError = null;
740
+ try
741
+ {
742
+ var client = ApiClient.CreateClient();
743
+ var response = await client.PostAsJsonAsync($"TaskDetails/tasks/{TaskId}/comments", new CreateCommentDto { Message = newCommentMessage });
744
+ var res = await response.Content.ReadFromJsonAsync<Result<CommentDto>>(Serialization.CaseInsensitive);
745
+
746
+ if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
747
+ {
748
+ comments.Add(res.Value);
749
+ newCommentMessage = "";
750
+ await JS.InvokeVoidAsync("initIcons");
751
+ }
752
+ else
753
+ {
754
+ commentError = res?.ErrorMessage ?? "Failed to post comment.";
755
+ }
756
+ }
757
+ catch (Exception ex)
758
+ {
759
+ commentError = ex.Message;
760
+ }
761
+ finally
762
+ {
763
+ isSavingComment = false;
764
+ }
765
+ }
766
+
767
+ private void ConfirmDeleteComment(long commentId)
768
+ {
769
+ confirmTitle = "Delete Comment?";
770
+ confirmMessage = "Are you sure you want to delete this comment? This action cannot be undone.";
771
+ confirmAction = () => DeleteComment(commentId);
772
+ showConfirm = true;
773
+ }
774
+
775
+ private async Task DeleteComment(long commentId)
776
+ {
777
+ var client = ApiClient.CreateClient();
778
+ try
779
+ {
780
+ var response = await client.DeleteAsync($"TaskDetails/comments/{commentId}");
781
+ var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
782
+ if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
783
+ {
784
+ var idx = comments.FindIndex(c => c.Id == commentId);
785
+ if (idx >= 0) comments.RemoveAt(idx);
786
+ }
787
+ }
788
+ catch (Exception ex)
789
+ {
790
+ Console.WriteLine($"Delete comment error: {ex.Message}");
791
+ }
792
+ }
793
+
794
+ // ─── ATTACHMENTS ACTIONS ────────────────────────────────────────────────
795
+
796
+ private async Task OnInputFileChange(InputFileChangeEventArgs e)
797
+ {
798
+ var file = e.File;
799
+ if (file == null) return;
800
+
801
+ isUploading = true;
802
+ uploadError = null;
803
+
804
+ try
805
+ {
806
+ var content = new MultipartFormDataContent();
807
+ var fileContent = new StreamContent(file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024));
808
+ fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType);
809
+ content.Add(fileContent, "file", file.Name);
810
+
811
+ var client = ApiClient.CreateClient();
812
+ var response = await client.PostAsync($"TaskDetails/tasks/{TaskId}/attachments", content);
813
+ var res = await response.Content.ReadFromJsonAsync<Result<FileAttachmentDto>>(Serialization.CaseInsensitive);
814
+
815
+ if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
816
+ {
817
+ attachments.Add(res.Value);
818
+ await JS.InvokeVoidAsync("initIcons");
819
+ }
820
+ else
821
+ {
822
+ uploadError = res?.ErrorMessage ?? "Failed to upload file.";
823
+ }
824
+ }
825
+ catch (Exception ex)
826
+ {
827
+ uploadError = ex.Message;
828
+ }
829
+ finally
830
+ {
831
+ isUploading = false;
832
+ }
833
+ }
834
+
835
+ private void ConfirmDeleteAttachment(long attachmentId)
836
+ {
837
+ confirmTitle = "Delete File?";
838
+ confirmMessage = "Are you sure you want to delete this file attachment?";
839
+ confirmAction = () => DeleteAttachment(attachmentId);
840
+ showConfirm = true;
841
+ }
842
+
843
+ private async Task DeleteAttachment(long attachmentId)
844
+ {
845
+ var client = ApiClient.CreateClient();
846
+ try
847
+ {
848
+ var response = await client.DeleteAsync($"TaskDetails/attachments/{attachmentId}");
849
+ var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
850
+ if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
851
+ {
852
+ var idx = attachments.FindIndex(f => f.Id == attachmentId);
853
+ if (idx >= 0) attachments.RemoveAt(idx);
854
+ }
855
+ }
856
+ catch (Exception ex)
857
+ {
858
+ Console.WriteLine($"Delete file error: {ex.Message}");
859
+ }
860
+ }
861
+
862
+ // ─── CONFIRM DIALOG LOGIC ───────────────────────────────────────────────
863
+
864
+ private async Task ExecuteConfirmAction()
865
+ {
866
+ showConfirm = false;
867
+ if (confirmAction != null)
868
+ {
869
+ await confirmAction();
870
+ }
871
+ confirmAction = null;
872
+ }
873
+
874
+ private void CancelConfirm()
875
+ {
876
+ showConfirm = false;
877
+ confirmAction = null;
878
+ }
879
+
880
+ // ─── DISPLAY UTILITIES ──────────────────────────────────────────────────
881
+
882
+ private string GetStatusLabel(long statusId) => statusId switch
883
+ {
884
+ 1 => "To Do",
885
+ 2 => "In Progress",
886
+ 3 => "Done",
887
+ _ => "Unknown"
888
+ };
889
+
890
+ private string GetStatusBadge(long statusId) => statusId switch
891
+ {
892
+ 1 => "bg-slate-100 text-slate-800 border border-slate-200",
893
+ 2 => "bg-blue-50 text-blue-700 border border-blue-100",
894
+ 3 => "bg-emerald-50 text-emerald-700 border border-emerald-100",
895
+ _ => "bg-slate-50 text-slate-600"
896
+ };
897
+
898
+ private string GetPriorityLabel(long priorityId) => priorityId switch
899
+ {
900
+ 1 => "Low",
901
+ 2 => "Medium",
902
+ 3 => "High",
903
+ _ => "Unknown"
904
+ };
905
+
906
+ private string GetPriorityBadge(long priorityId) => priorityId switch
907
+ {
908
+ 1 => "bg-slate-100 text-slate-600",
909
+ 2 => "bg-amber-50 text-amber-700 border border-amber-100",
910
+ 3 => "bg-rose-50 text-rose-700 border border-rose-100",
911
+ _ => "bg-slate-50 text-slate-600"
912
+ };
913
+
914
+ private string GetFileIcon(string fileType)
915
+ {
916
+ if (fileType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) return "image";
917
+ if (fileType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) return "file-text";
918
+ if (fileType.Contains("word") || fileType.Contains("officedocument.wordprocessingml")) return "file-text";
919
+ if (fileType.Contains("excel") || fileType.Contains("officedocument.spreadsheetml")) return "file-spreadsheet";
920
+ return "file";
921
+ }
922
+
923
+ private string FormatBytes(long bytes)
924
+ {
925
+ string[] suffix = { "B", "KB", "MB", "GB", "TB" };
926
+ int i;
927
+ double dblBytes = bytes;
928
+ for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024)
929
+ {
930
+ dblBytes = bytes / 1024.0;
931
+ }
932
+ return $"{dblBytes:0.##} {suffix[i]}";
933
+ }
934
+ }
TaskTrackingSystem.WebApp/Components/Pages/TaskReport.razor CHANGED
@@ -247,6 +247,7 @@
247
  <table class="w-full">
248
  <thead>
249
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
250
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
251
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
252
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
@@ -258,9 +259,11 @@
258
  <tbody class="divide-y divide-slate-100">
259
  @if (PagedList.Any())
260
  {
261
- @foreach (var t in PagedList)
262
  {
 
263
  <tr class="hover:bg-slate-50/50 transition-colors">
 
264
  <td class="px-6 py-3 text-sm font-medium text-slate-900">@t.Title</td>
265
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
266
  <td class="px-6 py-3">
@@ -277,7 +280,7 @@
277
  else
278
  {
279
  <tr>
280
- <td colspan="6" class="px-6 py-16 text-center bg-slate-50/20">
281
  <div class="flex flex-col items-center justify-center space-y-4">
282
  <svg class="w-24 h-24 text-slate-300" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
283
  <rect x="25" y="15" width="70" height="90" rx="8" stroke="currentColor" stroke-width="3" stroke-dasharray="6 4" />
@@ -366,7 +369,8 @@
366
  private IEnumerable<TaskReportDto> FilteredList =>
367
  string.IsNullOrWhiteSpace(searchQuery)
368
  ? taskReports
369
- : taskReports.Where(t => t.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) || t.ProjectName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase));
 
370
 
371
  private int TotalPages => (int)Math.Ceiling((double)FilteredList.Count() / pageSize);
372
 
 
247
  <table class="w-full">
248
  <thead>
249
  <tr class="border-b border-slate-100 bg-slate-50/60">
250
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
251
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
252
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
253
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
 
259
  <tbody class="divide-y divide-slate-100">
260
  @if (PagedList.Any())
261
  {
262
+ @foreach (var entry in PagedList.Select((t, index) => (t, index)))
263
  {
264
+ var t = entry.t;
265
  <tr class="hover:bg-slate-50/50 transition-colors">
266
+ <td class="px-6 py-3 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
267
  <td class="px-6 py-3 text-sm font-medium text-slate-900">@t.Title</td>
268
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
269
  <td class="px-6 py-3">
 
280
  else
281
  {
282
  <tr>
283
+ <td colspan="7" class="px-6 py-16 text-center bg-slate-50/20">
284
  <div class="flex flex-col items-center justify-center space-y-4">
285
  <svg class="w-24 h-24 text-slate-300" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
286
  <rect x="25" y="15" width="70" height="90" rx="8" stroke="currentColor" stroke-width="3" stroke-dasharray="6 4" />
 
369
  private IEnumerable<TaskReportDto> FilteredList =>
370
  string.IsNullOrWhiteSpace(searchQuery)
371
  ? taskReports
372
+ : taskReports.Where(t => t.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) || t.ProjectName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase))
373
+ .OrderBy(t => t.Title);
374
 
375
  private int TotalPages => (int)Math.Ceiling((double)FilteredList.Count() / pageSize);
376
 
TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor CHANGED
@@ -22,22 +22,6 @@
22
  <p class="text-slate-500 mt-1">@(isAdmin ? "View and manage all tasks across projects." : "Your assigned tasks.")</p>
23
  </div>
24
  <div class="flex items-center space-x-4">
25
- <!-- View Toggle Button Group -->
26
- <div class="flex items-center space-x-1 bg-slate-100 p-1 rounded-xl border border-slate-200">
27
- <button @onclick='() => SetView("list")' class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center space-x-1.5 @(currentView == "list" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
28
- <i data-lucide="list" class="h-3.5 w-3.5"></i>
29
- <span>List View</span>
30
- </button>
31
- <button @onclick='() => SetView("backlog")' class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center space-x-1.5 @(currentView == "backlog" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
32
- <i data-lucide="layers" class="h-3.5 w-3.5"></i>
33
- <span>Backlog View</span>
34
- </button>
35
- <button @onclick='() => SetView("board")' class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center space-x-1.5 @(currentView == "board" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
36
- <i data-lucide="kanban" class="h-3.5 w-3.5"></i>
37
- <span>Board View</span>
38
- </button>
39
- </div>
40
-
41
  @if (canCreateTask)
42
  {
43
  <button @onclick="OpenCreateModal" 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">
@@ -96,249 +80,101 @@
96
  </div>
97
  </div>
98
 
99
- @if (currentView == "backlog")
100
- {
101
- <div class="space-y-6 mt-4">
102
- @foreach (var proj in projects.Where(p => filterProjectId == 0 || p.Id == filterProjectId))
103
- {
104
- var projTasks = FilteredTasks.Where(t => t.ProjectId == proj.Id).ToList();
105
- @if (projTasks.Any())
 
 
 
 
 
 
 
 
 
 
106
  {
107
- <div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
108
- <!-- Project Header Row -->
109
- <div class="flex items-center justify-between px-6 py-4 bg-slate-50 border-b border-slate-100">
110
- <div class="flex items-center space-x-3">
111
- <span class="flex h-7 w-7 items-center justify-center rounded-lg bg-violet-100 text-violet-600 font-bold text-xs">P</span>
112
- <h3 class="font-bold text-sm text-slate-800">@proj.Name</h3>
113
- <span class="px-2.5 py-0.5 rounded-full bg-slate-200/80 text-[10px] text-slate-500 font-semibold font-sans">@projTasks.Count tasks</span>
114
- </div>
115
- @if (canCreateTask)
116
- {
117
- <button @onclick="() => OpenCreateModalForProject(proj.Id)" class="inline-flex items-center text-xs font-semibold text-emerald-600 hover:text-emerald-800 transition-colors">
118
- <i data-lucide="plus" class="h-3 w-3 mr-1"></i> Add Task
119
- </button>
120
- }
121
- </div>
122
-
123
- <!-- Tasks List (Vertical Backlog rows) -->
124
- <div class="divide-y divide-slate-100">
125
- @foreach (var task in projTasks)
126
- {
127
- <div class="flex flex-col sm:flex-row sm:items-center justify-between p-4 hover:bg-slate-50/40 transition-colors gap-3">
128
- <!-- Left section -->
129
- <div class="flex items-start space-x-3 flex-1 min-w-0">
130
- <input type="checkbox" checked="@(task.StatusId == 3)" @onchange="() => ToggleTaskStatus(task)" disabled="@(!canUpdateTask)" class="h-4 w-4 rounded border-slate-300 text-violet-600 accent-violet-600 focus:ring-violet-500 mt-1 cursor-pointer shrink-0 disabled:cursor-not-allowed disabled:opacity-50" />
131
- <div class="min-w-0">
132
- <div class="flex items-center space-x-2">
133
- <span class="text-[10px] font-bold text-slate-400 font-mono tracking-wider font-semibold">TASK-#@task.Id</span>
134
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-semibold @GetStatusBadge(task.StatusId)">
135
- @GetStatusLabel(task.StatusId)
136
- </span>
137
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[9px] font-semibold @GetPriorityBadge(task.PriorityId)">
138
- @GetPriorityLabel(task.PriorityId)
139
- </span>
140
- </div>
141
- <h4 class="text-sm font-bold text-slate-800 mt-1 truncate @(task.StatusId == 3 ? "line-through text-slate-400" : "")">@task.Title</h4>
142
- @if (!string.IsNullOrEmpty(task.Description))
143
- {
144
- <p class="text-xs text-slate-400 mt-0.5 truncate max-w-xl">@task.Description</p>
145
- }
146
- </div>
147
- </div>
148
-
149
- <!-- Right section -->
150
- <div class="flex items-center justify-between sm:justify-end gap-4 shrink-0">
151
- <span class="text-xs text-slate-400 bg-slate-100 px-2 py-1 rounded-lg">
152
- Due @DisplayFormats.Date(task.DueDate)
153
  </span>
154
-
155
- <div class="flex items-center space-x-1">
156
- @if (canEditTask)
157
- {
158
- <button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
159
- <i data-lucide="pencil" class="h-3.5 w-3.5"></i>
160
- </button>
161
- }
162
- @if (canDeleteTask)
163
- {
164
- <button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
165
- <i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
166
- </button>
167
- }
168
- </div>
169
  </div>
170
- </div>
171
- }
172
- </div>
173
- </div>
174
- }
175
- }
176
- @if (!taskList.Any(t => filterProjectId == 0 || t.ProjectId == filterProjectId))
177
- {
178
- <div class="rounded-2xl border border-dashed border-slate-200 bg-white p-12 text-center">
179
- <i data-lucide="check-square" class="h-10 w-10 mx-auto text-slate-300 mb-3"></i>
180
- <h4 class="text-sm font-bold text-slate-800">No tasks in backlog</h4>
181
- <p class="text-xs text-slate-400 mt-1">Select another project or create your first task!</p>
182
- </div>
183
- }
184
- </div>
185
- }
186
- else if (currentView == "board")
187
- {
188
- <!-- Board View: Kanban Column view -->
189
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-4">
190
- @foreach (var column in columns)
191
- {
192
- var tasksInColumn = FilteredTasks.Where(t => t.StatusId == column.StatusId).ToList();
193
- <div class="flex flex-col rounded-xl border border-slate-200 bg-slate-50/50 overflow-hidden min-h-[450px]">
194
- <!-- Column Header -->
195
- <div class="flex items-center justify-between px-4 py-3 bg-white border-b border-slate-100">
196
- <div class="flex items-center space-x-2">
197
- <span class="h-2.5 w-2.5 rounded-full @column.DotColor"></span>
198
- <h3 class="text-sm font-semibold text-slate-900">@column.Name</h3>
199
- </div>
200
- <span class="flex h-6 w-6 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-600">@tasksInColumn.Count</span>
201
- </div>
202
-
203
- <!-- Cards -->
204
- <div class="flex-1 p-3 space-y-3 overflow-y-auto"
205
- @ondrop="() => HandleDrop(column.StatusId)"
206
- @ondragover="HandleDragOver"
207
- @ondragover:preventDefault="true">
208
- @if (tasksInColumn.Any())
209
- {
210
- @foreach (var task in tasksInColumn)
211
- {
212
- <div class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-all @(canUpdateTask ? "cursor-grab active:cursor-grabbing" : "cursor-default")"
213
- draggable="@canUpdateTask"
214
- @ondragstart="() => HandleDragStart(task)">
215
- <div class="flex items-start justify-between">
216
- <h4 class="text-sm font-semibold text-slate-900">@task.Title</h4>
217
- <span class="text-[10px] px-2 py-0.5 rounded-full font-medium @GetPriorityBadge(task.PriorityId)">@GetPriorityLabel(task.PriorityId)</span>
218
- </div>
219
- @if (!string.IsNullOrEmpty(task.Description))
220
- {
221
- <p class="text-xs text-slate-505 mt-1.5 line-clamp-2">@task.Description</p>
222
- }
223
- <div class="flex items-center justify-between mt-3 pt-3 border-t border-slate-50">
224
- <span class="text-[10px] text-slate-400">Due @DisplayFormats.Date(task.DueDate)</span>
225
- @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
226
- @if (user != null)
227
- {
228
- <span class="flex h-5 w-5 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[8px] font-bold" title="@user.FirstName @user.LastName">
229
- @(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
230
- </span>
231
- }
232
- </div>
233
- </div>
234
- }
235
- }
236
- else
237
- {
238
- <div class="flex items-center justify-center py-8 h-full">
239
- <p class="text-xs text-slate-400">Drop tasks here</p>
240
- </div>
241
- }
242
- </div>
243
- </div>
244
- }
245
- </div>
246
- }
247
- else
248
- {
249
- <!-- Tasks Table (List View) -->
250
- <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden mt-4">
251
- <table class="w-full">
252
- <thead>
253
- <tr class="border-b border-slate-100 bg-slate-50/60">
254
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Title</th>
255
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
256
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
257
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
258
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
259
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
260
- <th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
261
- </tr>
262
- </thead>
263
- <tbody class="divide-y divide-slate-100">
264
- @if (PagedTasks.Any())
265
- {
266
- @foreach (var task in PagedTasks)
267
- {
268
- <tr class="hover:bg-slate-50/50 transition-colors">
269
- <td class="px-6 py-4">
270
- <span class="text-sm font-semibold text-slate-900">@task.Title</span>
271
- @if (!string.IsNullOrEmpty(task.Description))
272
- {
273
- <p class="text-xs text-slate-400 mt-0.5 truncate max-w-xs">@task.Description</p>
274
- }
275
- </td>
276
- <td class="px-6 py-4">
277
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(task.StatusId)">
278
- @GetStatusLabel(task.StatusId)
279
- </span>
280
- </td>
281
- <td class="px-6 py-4">
282
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(task.PriorityId)">
283
- @GetPriorityLabel(task.PriorityId)
284
- </span>
285
- </td>
286
- <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(task.DueDate)</td>
287
- <td class="px-6 py-4 text-sm text-slate-500">
288
- @{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); }
289
- @(proj?.Name ?? $"#{task.ProjectId}")
290
- </td>
291
- <td class="px-6 py-4 text-sm text-slate-500 font-medium">
292
- @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
293
- @if (user != null)
294
  {
295
- <div class="flex items-center gap-2">
296
- <span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold">
297
- @(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
298
- </span>
299
- <span class="text-xs text-slate-700 font-semibold">@user.FirstName @user.LastName</span>
300
- </div>
301
  }
302
- else
303
  {
304
- <span class="text-xs text-slate-400 italic">Unassigned</span>
 
 
305
  }
306
- </td>
307
- <td class="px-6 py-4 text-right">
308
- <div class="flex items-center justify-end space-x-2">
309
- @if (canEditTask)
310
- {
311
- <button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
312
- <i data-lucide="pencil" class="h-4 w-4"></i>
313
- </button>
314
- }
315
- @if (canDeleteTask)
316
- {
317
- <button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
318
- <i data-lucide="trash-2" class="h-4 w-4"></i>
319
- </button>
320
- }
321
- </div>
322
- </td>
323
- </tr>
324
- }
325
- }
326
- else
327
- {
328
- <EmptyState ColSpan="6" Icon="check-square" Title="No tasks yet" Subtitle="Create your first task to get started." />
329
  }
330
- </tbody>
331
- </table>
332
-
333
- <!-- Pagination -->
334
- <Pagination CurrentPage="currentPage"
335
- TotalPages="TotalPages"
336
- PageSize="pageSize"
337
- TotalCount="FilteredTasks.Count()"
338
- OnPageChanged="HandlePageChanged"
339
- OnPageSizeChanged="HandlePageSizeChanged" />
340
- </div>
341
- }
 
 
 
 
342
  }
343
  </div>
344
 
@@ -497,9 +333,6 @@
497
  private List<ProjectDto> projects = new();
498
  private List<UserDto> allUsers = new();
499
 
500
- // Backlog vs List vs Board View state
501
- private string currentView = "list";
502
-
503
  private string? successMessage;
504
 
505
  // Search and Filters state
@@ -516,71 +349,6 @@
516
  private int currentPage = 1;
517
  private int pageSize = 10;
518
 
519
- private List<KanbanColumn> columns = new()
520
- {
521
- new(1, "To Do", "bg-slate-400"),
522
- new(2, "In Progress", "bg-blue-500"),
523
- new(3, "Done", "bg-emerald-500")
524
- };
525
-
526
- private TaskDto? draggedTask;
527
-
528
- private void SetView(string view)
529
- {
530
- currentView = view;
531
- StateHasChanged();
532
- }
533
-
534
- private void HandleDragStart(TaskDto task)
535
- {
536
- draggedTask = task;
537
- }
538
-
539
- private void HandleDragOver()
540
- {
541
- // Allow dropping
542
- }
543
-
544
- private async Task HandleDrop(long newStatusId)
545
- {
546
- if (!canUpdateTask)
547
- {
548
- draggedTask = null;
549
- return;
550
- }
551
-
552
- if (draggedTask != null && draggedTask.StatusId != newStatusId)
553
- {
554
- await MoveTask(draggedTask.Id, newStatusId);
555
- }
556
- draggedTask = null;
557
- }
558
-
559
- private async Task MoveTask(long taskId, long newStatusId)
560
- {
561
- if (!canUpdateTask)
562
- {
563
- return;
564
- }
565
-
566
- var client = ApiClient.CreateClient();
567
- try
568
- {
569
- var response = await client.PatchAsync($"Task/{taskId}/status?statusId={newStatusId}", null);
570
- if (response.IsSuccessStatusCode)
571
- {
572
- var task = taskList.FirstOrDefault(t => t.Id == taskId);
573
- if (task != null) task.StatusId = newStatusId;
574
- await LoadTasks();
575
- StateHasChanged();
576
- }
577
- }
578
- catch (Exception ex)
579
- {
580
- Console.WriteLine($"Move task error: {ex.Message}");
581
- }
582
- }
583
-
584
  private void ApplySearch()
585
  {
586
  searchQuery = searchInput;
@@ -603,36 +371,6 @@
603
  currentPage = 1;
604
  }
605
 
606
- private void OpenCreateModalForProject(long projectId)
607
- {
608
- OpenCreateModal();
609
- formProjectId = projectId;
610
- }
611
-
612
- private async Task ToggleTaskStatus(TaskDto task)
613
- {
614
- if (!canUpdateTask)
615
- {
616
- return;
617
- }
618
-
619
- var newStatusId = task.StatusId == 3 ? 1 : 3;
620
- var client = ApiClient.CreateClient();
621
- try
622
- {
623
- var response = await client.PatchAsync($"Task/{task.Id}/status?statusId={newStatusId}", null);
624
- if (response.IsSuccessStatusCode)
625
- {
626
- task.StatusId = newStatusId;
627
- await LoadTasks();
628
- }
629
- }
630
- catch (Exception ex)
631
- {
632
- Console.WriteLine($"Toggle task error: {ex.Message}");
633
- }
634
- }
635
-
636
  private IEnumerable<TaskDto> FilteredTasks
637
  {
638
  get
@@ -656,7 +394,9 @@
656
  {
657
  result = result.Where(t => t.PriorityId == filterPriorityId);
658
  }
659
- return result;
 
 
660
  }
661
  }
662
 
@@ -932,7 +672,6 @@
932
 
933
  CloseModal();
934
  await LoadTasks();
935
- currentView = "list";
936
  successMessage = isEditing ? "Task updated successfully!" : "Task created successfully!";
937
  }
938
  catch (Exception ex)
@@ -958,7 +697,6 @@
958
  return;
959
  }
960
  await LoadTasks();
961
- currentView = "list";
962
  if (currentPage > TotalPages && TotalPages > 0)
963
  currentPage = TotalPages;
964
  successMessage = "Task deleted successfully!";
@@ -1001,5 +739,4 @@
1001
  _ => "—"
1002
  };
1003
 
1004
- private sealed record KanbanColumn(long StatusId, string Name, string DotColor);
1005
  }
 
22
  <p class="text-slate-500 mt-1">@(isAdmin ? "View and manage all tasks across projects." : "Your assigned tasks.")</p>
23
  </div>
24
  <div class="flex items-center space-x-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  @if (canCreateTask)
26
  {
27
  <button @onclick="OpenCreateModal" 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">
 
80
  </div>
81
  </div>
82
 
83
+ <!-- Tasks Table (List View) -->
84
+ <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden mt-4">
85
+ <table class="w-full">
86
+ <thead>
87
+ <tr class="border-b border-slate-100 bg-slate-50/60">
88
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
89
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Title</th>
90
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
91
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
92
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
93
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
94
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
95
+ <th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody class="divide-y divide-slate-100">
99
+ @if (PagedTasks.Any())
100
  {
101
+ @foreach (var entry in PagedTasks.Select((task, index) => (task, index)))
102
+ {
103
+ var task = entry.task;
104
+ <tr class="hover:bg-slate-50/50 transition-colors">
105
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
106
+ <td class="px-6 py-4">
107
+ <a href="/tasks/@task.Id/details" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">@task.Title</a>
108
+ @if (!string.IsNullOrEmpty(task.Description))
109
+ {
110
+ <p class="text-xs text-slate-400 mt-0.5 truncate max-w-xs">@task.Description</p>
111
+ }
112
+ </td>
113
+ <td class="px-6 py-4">
114
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(task.StatusId)">
115
+ @GetStatusLabel(task.StatusId)
116
+ </span>
117
+ </td>
118
+ <td class="px-6 py-4">
119
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(task.PriorityId)">
120
+ @GetPriorityLabel(task.PriorityId)
121
+ </span>
122
+ </td>
123
+ <td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(task.DueDate)</td>
124
+ <td class="px-6 py-4 text-sm text-slate-500">
125
+ @{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); }
126
+ @(proj?.Name ?? $"#{task.ProjectId}")
127
+ </td>
128
+ <td class="px-6 py-4 text-sm text-slate-500 font-medium">
129
+ @{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
130
+ @if (user != null)
131
+ {
132
+ <div class="flex items-center gap-2">
133
+ <span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold">
134
+ @(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
 
 
 
 
 
 
 
 
 
 
 
 
135
  </span>
136
+ <span class="text-xs text-slate-700 font-semibold">@user.FirstName @user.LastName</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
138
+ }
139
+ else
140
+ {
141
+ <span class="text-xs text-slate-400 italic">Unassigned</span>
142
+ }
143
+ </td>
144
+ <td class="px-6 py-4 text-right">
145
+ <div class="flex items-center justify-end space-x-2">
146
+ @if (canEditTask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  {
148
+ <button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
149
+ <i data-lucide="pencil" class="h-4 w-4"></i>
150
+ </button>
 
 
 
151
  }
152
+ @if (canDeleteTask)
153
  {
154
+ <button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
155
+ <i data-lucide="trash-2" class="h-4 w-4"></i>
156
+ </button>
157
  }
158
+ </div>
159
+ </td>
160
+ </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
+ }
163
+ else
164
+ {
165
+ <EmptyState ColSpan="7" Icon="check-square" Title="No tasks yet" Subtitle="Create your first task to get started." />
166
+ }
167
+ </tbody>
168
+ </table>
169
+
170
+ <!-- Pagination -->
171
+ <Pagination CurrentPage="currentPage"
172
+ TotalPages="TotalPages"
173
+ PageSize="pageSize"
174
+ TotalCount="FilteredTasks.Count()"
175
+ OnPageChanged="HandlePageChanged"
176
+ OnPageSizeChanged="HandlePageSizeChanged" />
177
+ </div>
178
  }
179
  </div>
180
 
 
333
  private List<ProjectDto> projects = new();
334
  private List<UserDto> allUsers = new();
335
 
 
 
 
336
  private string? successMessage;
337
 
338
  // Search and Filters state
 
349
  private int currentPage = 1;
350
  private int pageSize = 10;
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  private void ApplySearch()
353
  {
354
  searchQuery = searchInput;
 
371
  currentPage = 1;
372
  }
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  private IEnumerable<TaskDto> FilteredTasks
375
  {
376
  get
 
394
  {
395
  result = result.Where(t => t.PriorityId == filterPriorityId);
396
  }
397
+ return result
398
+ .OrderBy(t => t.Title)
399
+ .ThenBy(t => t.DueDate);
400
  }
401
  }
402
 
 
672
 
673
  CloseModal();
674
  await LoadTasks();
 
675
  successMessage = isEditing ? "Task updated successfully!" : "Task created successfully!";
676
  }
677
  catch (Exception ex)
 
697
  return;
698
  }
699
  await LoadTasks();
 
700
  if (currentPage > TotalPages && TotalPages > 0)
701
  currentPage = TotalPages;
702
  successMessage = "Task deleted successfully!";
 
739
  _ => "—"
740
  };
741
 
 
742
  }
TaskTrackingSystem.WebApp/Components/Pages/TimeTrackingReport.razor CHANGED
@@ -146,6 +146,7 @@
146
  <table class="w-full">
147
  <thead>
148
  <tr class="border-b border-slate-100 bg-slate-50/60 font-sans">
 
149
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Employee</th>
150
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
151
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
@@ -158,10 +159,12 @@
158
  <tbody class="divide-y divide-slate-100">
159
  @if (reportData.EmployeeSummary.Any())
160
  {
161
- @foreach (var emp in reportData.EmployeeSummary)
162
  {
 
163
  var pct = emp.CompletionPercentage;
164
  <tr class="hover:bg-slate-50/50 transition-colors">
 
165
  <td class="px-6 py-4">
166
  <div class="flex items-center gap-3">
167
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
@@ -194,10 +197,16 @@
194
  }
195
  else
196
  {
197
- <EmptyState ColSpan="7" Icon="clock" Title="No timesheet data" Subtitle="No tasks found with the current filters." />
198
  }
199
  </tbody>
200
  </table>
 
 
 
 
 
 
201
  </div>
202
  }
203
 
@@ -211,6 +220,7 @@
211
  <table class="w-full">
212
  <thead>
213
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
214
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
215
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
216
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
@@ -222,10 +232,12 @@
222
  <tbody class="divide-y divide-slate-100">
223
  @if (reportData.ProjectSummary.Any())
224
  {
225
- @foreach (var proj in reportData.ProjectSummary)
226
  {
 
227
  var pct = proj.CompletionPercentage;
228
  <tr class="hover:bg-slate-50/50 transition-colors">
 
229
  <td class="px-6 py-4">
230
  <div class="flex items-center gap-2">
231
  <i data-lucide="folder-kanban" class="h-4 w-4 text-violet-500"></i>
@@ -250,10 +262,16 @@
250
  }
251
  else
252
  {
253
- <EmptyState ColSpan="6" Icon="folder-kanban" Title="No project data" Subtitle="No tasks found with the current filters." />
254
  }
255
  </tbody>
256
  </table>
 
 
 
 
 
 
257
  </div>
258
  }
259
 
@@ -269,6 +287,7 @@
269
  <table class="w-full">
270
  <thead>
271
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
272
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Task</th>
273
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
274
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Assignee</th>
@@ -281,14 +300,16 @@
281
  <tbody class="divide-y divide-slate-100">
282
  @if (PagedDetailTasks.Any())
283
  {
284
- @foreach (var task in PagedDetailTasks)
285
  {
 
286
  var (statusLabel, statusClass) = task.StatusId switch {
287
  2 => ("In Progress", "bg-blue-50 text-blue-700"),
288
  3 => ("Done", "bg-emerald-50 text-emerald-700"),
289
  _ => ("To Do", "bg-slate-100 text-slate-600")
290
  };
291
  <tr class="hover:bg-slate-50/50 transition-colors">
 
292
  <td class="px-6 py-4">
293
  <p class="text-sm font-semibold text-slate-900">@task.Title</p>
294
  </td>
@@ -327,7 +348,7 @@
327
  }
328
  else
329
  {
330
- <EmptyState ColSpan="7" Icon="clock" Title="No tasks found" Subtitle="Try adjusting your filters." />
331
  }
332
  </tbody>
333
  </table>
@@ -360,13 +381,23 @@
360
  private long filterStatusId = 0;
361
  private long appliedStatusId = 0;
362
 
 
 
 
363
  // Pagination (detail view)
364
  private int detailPage = 1;
365
  private int pageSize = 10;
366
 
367
- private IEnumerable<TaskTimeDetailDto> FilteredDetailTasks => reportData.TaskDetail;
 
 
 
 
 
368
  private int DetailTotalPages => (int)Math.Ceiling((double)FilteredDetailTasks.Count() / pageSize);
369
  private IEnumerable<TaskTimeDetailDto> PagedDetailTasks => FilteredDetailTasks.Skip((detailPage - 1) * pageSize).Take(pageSize);
 
 
370
 
371
  protected override async Task OnInitializedAsync()
372
  {
@@ -421,6 +452,8 @@
421
  appliedSearch = searchInput;
422
  appliedProjectId = filterProjectId;
423
  appliedStatusId = filterStatusId;
 
 
424
  detailPage = 1;
425
  await LoadTimeTrackingData();
426
  }
@@ -430,12 +463,23 @@
430
  searchInput = ""; appliedSearch = "";
431
  filterProjectId = 0; appliedProjectId = 0;
432
  filterStatusId = 0; appliedStatusId = 0;
 
 
433
  detailPage = 1;
434
  await LoadTimeTrackingData();
435
  }
436
 
 
 
 
437
  private void HandleDetailPageChanged(int page) => detailPage = page;
438
- private void HandlePageSizeChanged(int size) { pageSize = size; detailPage = 1; }
 
 
 
 
 
 
439
 
440
  private async Task DownloadExcel()
441
  {
 
146
  <table class="w-full">
147
  <thead>
148
  <tr class="border-b border-slate-100 bg-slate-50/60 font-sans">
149
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">No.</th>
150
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Employee</th>
151
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
152
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
 
159
  <tbody class="divide-y divide-slate-100">
160
  @if (reportData.EmployeeSummary.Any())
161
  {
162
+ @foreach (var entry in PagedEmployeeSummary.Select((emp, index) => (emp, index)))
163
  {
164
+ var emp = entry.emp;
165
  var pct = emp.CompletionPercentage;
166
  <tr class="hover:bg-slate-50/50 transition-colors">
167
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500 font-sans">@GetRowNo(userPage, entry.index)</td>
168
  <td class="px-6 py-4">
169
  <div class="flex items-center gap-3">
170
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold">
 
197
  }
198
  else
199
  {
200
+ <EmptyState ColSpan="8" Icon="clock" Title="No timesheet data" Subtitle="No tasks found with the current filters." />
201
  }
202
  </tbody>
203
  </table>
204
+ <Pagination CurrentPage="userPage"
205
+ TotalPages="UserTotalPages"
206
+ PageSize="pageSize"
207
+ TotalCount="reportData.EmployeeSummary.Count"
208
+ OnPageChanged="HandleUserPageChanged"
209
+ OnPageSizeChanged="HandlePageSizeChanged" />
210
  </div>
211
  }
212
 
 
220
  <table class="w-full">
221
  <thead>
222
  <tr class="border-b border-slate-100 bg-slate-50/60">
223
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">No.</th>
224
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
225
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Total Tasks</th>
226
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Completed</th>
 
232
  <tbody class="divide-y divide-slate-100">
233
  @if (reportData.ProjectSummary.Any())
234
  {
235
+ @foreach (var entry in PagedProjectSummary.Select((proj, index) => (proj, index)))
236
  {
237
+ var proj = entry.proj;
238
  var pct = proj.CompletionPercentage;
239
  <tr class="hover:bg-slate-50/50 transition-colors">
240
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500 font-sans">@GetRowNo(projectPage, entry.index)</td>
241
  <td class="px-6 py-4">
242
  <div class="flex items-center gap-2">
243
  <i data-lucide="folder-kanban" class="h-4 w-4 text-violet-500"></i>
 
262
  }
263
  else
264
  {
265
+ <EmptyState ColSpan="7" Icon="folder-kanban" Title="No project data" Subtitle="No tasks found with the current filters." />
266
  }
267
  </tbody>
268
  </table>
269
+ <Pagination CurrentPage="projectPage"
270
+ TotalPages="ProjectTotalPages"
271
+ PageSize="pageSize"
272
+ TotalCount="reportData.ProjectSummary.Count"
273
+ OnPageChanged="HandleProjectPageChanged"
274
+ OnPageSizeChanged="HandlePageSizeChanged" />
275
  </div>
276
  }
277
 
 
287
  <table class="w-full">
288
  <thead>
289
  <tr class="border-b border-slate-100 bg-slate-50/60">
290
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">No.</th>
291
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Task</th>
292
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Project</th>
293
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider font-sans">Assignee</th>
 
300
  <tbody class="divide-y divide-slate-100">
301
  @if (PagedDetailTasks.Any())
302
  {
303
+ @foreach (var entry in PagedDetailTasks.Select((task, index) => (task, index)))
304
  {
305
+ var task = entry.task;
306
  var (statusLabel, statusClass) = task.StatusId switch {
307
  2 => ("In Progress", "bg-blue-50 text-blue-700"),
308
  3 => ("Done", "bg-emerald-50 text-emerald-700"),
309
  _ => ("To Do", "bg-slate-100 text-slate-600")
310
  };
311
  <tr class="hover:bg-slate-50/50 transition-colors">
312
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500 font-sans">@GetRowNo(detailPage, entry.index)</td>
313
  <td class="px-6 py-4">
314
  <p class="text-sm font-semibold text-slate-900">@task.Title</p>
315
  </td>
 
348
  }
349
  else
350
  {
351
+ <EmptyState ColSpan="8" Icon="clock" Title="No tasks found" Subtitle="Try adjusting your filters." />
352
  }
353
  </tbody>
354
  </table>
 
381
  private long filterStatusId = 0;
382
  private long appliedStatusId = 0;
383
 
384
+ // Pagination (all list views)
385
+ private int userPage = 1;
386
+ private int projectPage = 1;
387
  // Pagination (detail view)
388
  private int detailPage = 1;
389
  private int pageSize = 10;
390
 
391
+ private IEnumerable<TaskTimeDetailDto> FilteredDetailTasks =>
392
+ reportData.TaskDetail.OrderBy(t => t.Title);
393
+ private IEnumerable<EmployeeTimeSummaryDto> PagedEmployeeSummary =>
394
+ reportData.EmployeeSummary.OrderBy(x => x.FullName).Skip((userPage - 1) * pageSize).Take(pageSize);
395
+ private IEnumerable<ProjectTimeSummaryDto> PagedProjectSummary =>
396
+ reportData.ProjectSummary.OrderBy(x => x.ProjectName).Skip((projectPage - 1) * pageSize).Take(pageSize);
397
  private int DetailTotalPages => (int)Math.Ceiling((double)FilteredDetailTasks.Count() / pageSize);
398
  private IEnumerable<TaskTimeDetailDto> PagedDetailTasks => FilteredDetailTasks.Skip((detailPage - 1) * pageSize).Take(pageSize);
399
+ private int UserTotalPages => (int)Math.Ceiling((double)reportData.EmployeeSummary.Count() / pageSize);
400
+ private int ProjectTotalPages => (int)Math.Ceiling((double)reportData.ProjectSummary.Count() / pageSize);
401
 
402
  protected override async Task OnInitializedAsync()
403
  {
 
452
  appliedSearch = searchInput;
453
  appliedProjectId = filterProjectId;
454
  appliedStatusId = filterStatusId;
455
+ userPage = 1;
456
+ projectPage = 1;
457
  detailPage = 1;
458
  await LoadTimeTrackingData();
459
  }
 
463
  searchInput = ""; appliedSearch = "";
464
  filterProjectId = 0; appliedProjectId = 0;
465
  filterStatusId = 0; appliedStatusId = 0;
466
+ userPage = 1;
467
+ projectPage = 1;
468
  detailPage = 1;
469
  await LoadTimeTrackingData();
470
  }
471
 
472
+ private int GetRowNo(int page, int index) => ((page - 1) * pageSize) + index + 1;
473
+ private void HandleUserPageChanged(int page) => userPage = page;
474
+ private void HandleProjectPageChanged(int page) => projectPage = page;
475
  private void HandleDetailPageChanged(int page) => detailPage = page;
476
+ private void HandlePageSizeChanged(int size)
477
+ {
478
+ pageSize = size;
479
+ userPage = 1;
480
+ projectPage = 1;
481
+ detailPage = 1;
482
+ }
483
 
484
  private async Task DownloadExcel()
485
  {
TaskTrackingSystem.WebApp/Components/Pages/Users.razor CHANGED
@@ -75,6 +75,7 @@
75
  <table class="w-full">
76
  <thead>
77
  <tr class="border-b border-slate-100 bg-slate-50/60">
 
78
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
79
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Email</th>
80
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Phone</th>
@@ -86,9 +87,11 @@
86
  <tbody class="divide-y divide-slate-100">
87
  @if (PagedUsers.Any())
88
  {
89
- @foreach (var user in PagedUsers)
90
  {
 
91
  <tr class="hover:bg-slate-50/50 transition-colors">
 
92
  <td class="px-6 py-4">
93
  <div class="flex items-center space-x-3">
94
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
@@ -134,7 +137,7 @@
134
  }
135
  else
136
  {
137
- <EmptyState ColSpan="6" Icon="users" Title="No users found" Subtitle="Try adjusting your search or create a new user." />
138
  }
139
  </tbody>
140
  </table>
@@ -361,7 +364,10 @@
361
  else if (filterStatus == "inactive")
362
  result = result.Where(u => !u.IsActive);
363
 
364
- return result;
 
 
 
365
  }
366
  }
367
 
 
75
  <table class="w-full">
76
  <thead>
77
  <tr class="border-b border-slate-100 bg-slate-50/60">
78
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
79
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
80
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Email</th>
81
  <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Phone</th>
 
87
  <tbody class="divide-y divide-slate-100">
88
  @if (PagedUsers.Any())
89
  {
90
+ @foreach (var entry in PagedUsers.Select((user, index) => (user, index)))
91
  {
92
+ var user = entry.user;
93
  <tr class="hover:bg-slate-50/50 transition-colors">
94
+ <td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
95
  <td class="px-6 py-4">
96
  <div class="flex items-center space-x-3">
97
  <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
 
137
  }
138
  else
139
  {
140
+ <EmptyState ColSpan="7" Icon="users" Title="No users found" Subtitle="Try adjusting your search or create a new user." />
141
  }
142
  </tbody>
143
  </table>
 
364
  else if (filterStatus == "inactive")
365
  result = result.Where(u => !u.IsActive);
366
 
367
+ return result
368
+ .OrderBy(u => u.FirstName)
369
+ .ThenBy(u => u.LastName)
370
+ .ThenBy(u => u.Username);
371
  }
372
  }
373
 
TaskTrackingSystem.WebApp/Components/Partial/Pagination.razor CHANGED
@@ -10,7 +10,7 @@
10
  {
11
  var from = (CurrentPage - 1) * PageSize + 1;
12
  var to = Math.Min(CurrentPage * PageSize, TotalCount);
13
- <span>Showing <span class="font-semibold text-slate-700">@from@to</span> of <span class="font-semibold text-slate-700">@TotalCount</span> results</span>
14
  }
15
  </div>
16
 
@@ -46,7 +46,7 @@
46
  {
47
  if (p == -1)
48
  {
49
- <span class="inline-flex h-7 w-7 items-center justify-center text-xs text-slate-400"></span>
50
  }
51
  else
52
  {
 
10
  {
11
  var from = (CurrentPage - 1) * PageSize + 1;
12
  var to = Math.Min(CurrentPage * PageSize, TotalCount);
13
+ <span>Showing <span class="font-semibold text-slate-700">@from-@to</span> of <span class="font-semibold text-slate-700">@TotalCount</span> results</span>
14
  }
15
  </div>
16
 
 
46
  {
47
  if (p == -1)
48
  {
49
+ <span class="inline-flex h-7 w-7 items-center justify-center text-xs text-slate-400">...</span>
50
  }
51
  else
52
  {
TaskTrackingSystem.WebApp/MenuAuthorizationService.cs CHANGED
@@ -64,6 +64,12 @@ public class MenuAuthorizationService
64
  return true;
65
  }
66
 
 
 
 
 
 
 
67
  var roleKey = GetRoleCacheKey(user);
68
 
69
  if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessItems != null)
@@ -84,7 +90,28 @@ public class MenuAuthorizationService
84
  return true;
85
  }
86
 
87
- // Special case for Kanban Board page route parameter wildcard since MenuUrl is /board
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  if (cleanRelative.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) &&
89
  cleanRelative.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase))
90
  {
@@ -101,6 +128,24 @@ public class MenuAuthorizationService
101
  }
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  if (string.IsNullOrEmpty(cleanRelative) ||
105
  cleanRelative.Equals("dashboard", StringComparison.OrdinalIgnoreCase) ||
106
  cleanRelative.Equals("home", StringComparison.OrdinalIgnoreCase))
 
64
  return true;
65
  }
66
 
67
+ var cleanRelative = relativePath.Split('?')[0].Trim('/');
68
+ if (cleanRelative.Equals("audit-logs", StringComparison.OrdinalIgnoreCase))
69
+ {
70
+ return user.IsInRole("Admin");
71
+ }
72
+
73
  var roleKey = GetRoleCacheKey(user);
74
 
75
  if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessItems != null)
 
90
  return true;
91
  }
92
 
93
+ // Special case for Task Details page route parameter wildcard
94
+ if (cleanRelative.StartsWith("tasks/", StringComparison.OrdinalIgnoreCase) &&
95
+ cleanRelative.EndsWith("/details", StringComparison.OrdinalIgnoreCase))
96
+ {
97
+ var segments = cleanRelative.Split('/');
98
+ if (segments.Length == 3 && long.TryParse(segments[1], out _))
99
+ {
100
+ var hasTasksAccess = accessItems.Any(m =>
101
+ m.MenuCode.Equals("TASKS_LIST", StringComparison.OrdinalIgnoreCase) ||
102
+ m.MenuCode.Equals("TASKS_BOARD", StringComparison.OrdinalIgnoreCase) ||
103
+ m.MenuCode.Equals("TASKS_BACKLOG", StringComparison.OrdinalIgnoreCase) ||
104
+ m.SubMenus.Any(sm => sm.MenuCode.Equals("TASKS_LIST", StringComparison.OrdinalIgnoreCase) ||
105
+ sm.MenuCode.Equals("TASKS_BOARD", StringComparison.OrdinalIgnoreCase) ||
106
+ sm.MenuCode.Equals("TASKS_BACKLOG", StringComparison.OrdinalIgnoreCase)));
107
+ if (hasTasksAccess)
108
+ {
109
+ return true;
110
+ }
111
+ }
112
+ }
113
+
114
+ // Special case for project-specific Kanban board
115
  if (cleanRelative.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) &&
116
  cleanRelative.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase))
117
  {
 
128
  }
129
  }
130
 
131
+ // Special case for Task Backlog split-screen page
132
+ if (cleanRelative.Equals("tasks/backlog", StringComparison.OrdinalIgnoreCase))
133
+ {
134
+ var hasTasksAccess = accessItems.Any(m =>
135
+ m.MenuCode.Equals("TASKS_BACKLOG", StringComparison.OrdinalIgnoreCase) ||
136
+ m.SubMenus.Any(sm => sm.MenuCode.Equals("TASKS_BACKLOG", StringComparison.OrdinalIgnoreCase)));
137
+ if (hasTasksAccess)
138
+ {
139
+ return true;
140
+ }
141
+ }
142
+
143
+ // Disable global board access
144
+ if (cleanRelative.Equals("board", StringComparison.OrdinalIgnoreCase))
145
+ {
146
+ return false;
147
+ }
148
+
149
  if (string.IsNullOrEmpty(cleanRelative) ||
150
  cleanRelative.Equals("dashboard", StringComparison.OrdinalIgnoreCase) ||
151
  cleanRelative.Equals("home", StringComparison.OrdinalIgnoreCase))