User commited on
Commit
8c88a89
·
1 Parent(s): 2358ba3

add: language switch

Browse files
Files changed (26) hide show
  1. TaskTrackingSystem.Shared/Localization/AppLocalization.cs +615 -0
  2. TaskTrackingSystem.Shared/Models/Issue/CreateIssueDto.cs +4 -3
  3. TaskTrackingSystem.Shared/Models/Issue/UpdateIssueDto.cs +4 -3
  4. TaskTrackingSystem.Shared/Result.cs +8 -7
  5. TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +45 -40
  6. TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +8 -8
  7. TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor +13 -13
  8. TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Login.razor +14 -14
  9. TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/ResetPassword.razor +12 -12
  10. TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor +48 -48
  11. TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor +76 -76
  12. TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/ProjectAssign.razor +26 -26
  13. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/Components/ReportToolbar.razor +4 -4
  14. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor +27 -27
  15. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/IssueReport.razor +80 -80
  16. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor +37 -37
  17. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor +20 -20
  18. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor +139 -123
  19. TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor +18 -18
  20. TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskAssign.razor +20 -20
  21. TaskTrackingSystem.WebApp/Components/Pages/Features/Users/Users.razor +57 -57
  22. TaskTrackingSystem.WebApp/Components/Partial/ConfirmDialog.razor +4 -4
  23. TaskTrackingSystem.WebApp/Components/Partial/EmptyState.razor +2 -2
  24. TaskTrackingSystem.WebApp/Components/Partial/NotificationBell.razor +12 -117
  25. TaskTrackingSystem.WebApp/Components/_Imports.razor +1 -1
  26. TaskTrackingSystem.WebApp/Localization/UiLanguageService.cs +42 -41
TaskTrackingSystem.Shared/Localization/AppLocalization.cs ADDED
@@ -0,0 +1,615 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Globalization;
2
+ using TaskTrackingSystem.Shared.Enums;
3
+
4
+ namespace TaskTrackingSystem.Shared.Localization;
5
+
6
+ public static class AppLocalization
7
+ {
8
+ private static readonly IReadOnlyDictionary<string, string> English = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
9
+ {
10
+ ["common.workspace"] = "Workspace",
11
+ ["common.signIn"] = "Sign In",
12
+ ["common.signOut"] = "Sign Out",
13
+ ["common.signOutTitle"] = "Sign Out?",
14
+ ["common.signOutDescription"] = "Are you sure you want to sign out of Taskify?",
15
+ ["common.cancel"] = "Cancel",
16
+ ["common.confirm"] = "Confirm",
17
+ ["common.confirmAction"] = "Confirm",
18
+ ["common.areYouSure"] = "Are you sure?",
19
+ ["common.cannotUndo"] = "This action cannot be undone.",
20
+ ["common.yes"] = "Yes",
21
+ ["common.no"] = "No",
22
+ ["common.save"] = "Save",
23
+ ["common.create"] = "Create",
24
+ ["common.update"] = "Update",
25
+ ["common.delete"] = "Delete",
26
+ ["common.confirmSignOut"] = "Yes, Sign Out",
27
+ ["common.listView"] = "List View",
28
+ ["common.chartView"] = "Chart View",
29
+ ["common.search"] = "Search",
30
+ ["common.reset"] = "Reset",
31
+ ["common.clear"] = "Clear",
32
+ ["common.all"] = "All",
33
+ ["common.loading"] = "Loading...",
34
+ ["common.close"] = "Close",
35
+ ["common.openNavigation"] = "Open navigation",
36
+ ["common.closeNavigation"] = "Close navigation",
37
+ ["common.index"] = "No.",
38
+ ["common.exportExcel"] = "Export Excel",
39
+ ["common.exportPdf"] = "Export PDF",
40
+ ["common.actions"] = "Actions",
41
+ ["common.status"] = "Status",
42
+ ["common.priority"] = "Priority",
43
+ ["common.assignee"] = "Assignee",
44
+ ["common.project"] = "Project",
45
+ ["common.task"] = "Task",
46
+ ["common.taskProject"] = "Task / Project",
47
+ ["common.issue"] = "Issue",
48
+ ["common.user"] = "User",
49
+ ["common.role"] = "Role",
50
+ ["common.module"] = "Module",
51
+ ["common.description"] = "Description",
52
+ ["common.date"] = "Date",
53
+ ["common.startDate"] = "Start Date",
54
+ ["common.endDate"] = "End Date",
55
+ ["common.dueDate"] = "Due Date",
56
+ ["common.due"] = "Due",
57
+ ["common.hours"] = "Hours",
58
+ ["common.risk"] = "Risk",
59
+ ["common.blocked"] = "Blocked",
60
+ ["common.overdue"] = "Overdue",
61
+ ["common.done"] = "Done",
62
+ ["common.inProgress"] = "In Progress",
63
+ ["common.todo"] = "To Do",
64
+ ["common.resolved"] = "Resolved",
65
+ ["common.clearStatus"] = "Clear",
66
+ ["common.none"] = "None",
67
+ ["common.unassigned"] = "Unassigned",
68
+ ["common.noDescription"] = "No description",
69
+ ["common.noneRecorded"] = "None recorded",
70
+ ["common.na"] = "N/A",
71
+ ["common.noAccessItems"] = "No access items are available for this role.",
72
+ ["common.accessDenied"] = "Access Denied",
73
+ ["common.accessDeniedDescription"] = "You do not have permission to access this page. Please contact your system administrator.",
74
+ ["common.backToDashboard"] = "Back to Dashboard",
75
+ ["common.noDataYet"] = "No data yet",
76
+ ["common.createFirstEntry"] = "Create your first entry to get started.",
77
+ ["common.loadingAuditLogs"] = "Loading audit logs...",
78
+ ["common.loadingUsers"] = "Loading users...",
79
+ ["common.loadingProjects"] = "Loading projects...",
80
+ ["common.loadingTasks"] = "Loading tasks...",
81
+ ["common.searchPlaceholder"] = "Search by name, email, username...",
82
+ ["common.searchTasks"] = "Search Task",
83
+ ["common.searchByUser"] = "Search by user, action, module, or details...",
84
+ ["common.searchByTitle"] = "Search by title or project...",
85
+ ["common.searchByTitleOrProject"] = "Search by title or project...",
86
+ ["common.searchProjects"] = "Search projects...",
87
+ ["common.searchTeamMembers"] = "Search team members...",
88
+ ["common.searchProjectMembers"] = "Search project members...",
89
+ ["common.searchIssues"] = "Search issue title, description or assignee...",
90
+ ["common.searchByIssueAssignee"] = "Search Issues / Assignee",
91
+ ["common.loadingSpinner"] = "Loading...",
92
+ ["common.loadingIssues"] = "Loading issues...",
93
+ ["common.loadingTasksAndUsers"] = "Loading tasks and users...",
94
+ ["common.loadingProjectsAndUsers"] = "Loading projects and users...",
95
+ ["common.loadingProjectMembers"] = "Loading project members...",
96
+ ["common.loadingMembers"] = "Loading members...",
97
+ ["common.noResults"] = "No results found.",
98
+ ["common.notApplicable"] = "Not applicable",
99
+ ["common.selectAll"] = "Select All",
100
+ ["common.deselectAll"] = "Deselect All",
101
+ ["common.selected"] = "selected",
102
+ ["common.viewDetails"] = "View details",
103
+ ["common.currentLanguage"] = "Switch language",
104
+ ["common.languageEnglish"] = "English",
105
+ ["common.languageBurmese"] = "Burmese",
106
+ ["common.allTasks"] = "All tasks",
107
+ ["common.allProjects"] = "All projects",
108
+ ["common.allRoles"] = "All Roles",
109
+ ["common.noProjectsFound"] = "No projects found.",
110
+ ["common.noTasksFound"] = "No tasks found.",
111
+ ["common.noTeamMembersFound"] = "No team members match your search criteria.",
112
+ ["common.noMembersMatchSearch"] = "No members match the current search.",
113
+ ["common.noTaskSelected"] = "No Task Selected",
114
+ ["common.noProjectSelected"] = "No Project Selected",
115
+ ["common.noProject"] = "No Project",
116
+ ["common.currentlyAssigningTask"] = "Currently Assigning Task",
117
+ ["common.currentlyManaging"] = "Currently Managing",
118
+ ["common.selectProject"] = "Select Project",
119
+ ["common.selectTask"] = "Select task...",
120
+ ["common.issueTitle"] = "Issue Title",
121
+ ["common.issueDescription"] = "Description",
122
+ ["common.blockedStatus"] = "Blocked Status",
123
+ ["common.escalationLevel"] = "Escalation Level",
124
+ ["common.blockedBy"] = "Blocked By",
125
+ ["common.delayReason"] = "Delay Reason",
126
+ ["common.actualHours"] = "Actual Hours",
127
+ ["common.estimatedHours"] = "Estimated Hours",
128
+ ["common.assignedToMeOnly"] = "Assigned to me only",
129
+ ["common.saveAssignment"] = "Save Assignment",
130
+ ["common.saveAssignmentChanges"] = "Save Assignment Changes",
131
+ ["common.saveChanges"] = "Save Changes",
132
+ ["common.addIssue"] = "Add Issue",
133
+ ["common.addIssueTitle"] = "Add Issue?",
134
+ ["common.editIssue"] = "Edit Issue",
135
+ ["common.noTaskSelectedForIssue"] = "No task selected",
136
+ ["common.unassignedMembers"] = "Unassigned",
137
+ ["common.noMembersMatchSearch"] = "No members match the current search.",
138
+ ["common.removeOwnerFromTask"] = "Remove owner from this task",
139
+
140
+ ["page.dashboard"] = "Dashboard",
141
+ ["page.managerDashboard"] = "Manager Dashboard",
142
+ ["page.employeeDashboard"] = "Employee Dashboard",
143
+ ["page.projects"] = "Projects",
144
+ ["page.projectAssignment"] = "Project Assignment",
145
+ ["page.tasks"] = "Tasks",
146
+ ["page.kanbanBoard"] = "Kanban Board",
147
+ ["page.taskAssignment"] = "Task Assignment",
148
+ ["page.taskDetails"] = "Task Details",
149
+ ["page.issues"] = "Issues",
150
+ ["page.addIssue"] = "Add Issue",
151
+ ["page.taskReport"] = "Task Report",
152
+ ["page.timeTracking"] = "Time Tracking",
153
+ ["page.projectProgress"] = "Project Progress",
154
+ ["page.overdueTasks"] = "Overdue Tasks",
155
+ ["page.employeeReport"] = "Employee Report",
156
+ ["page.rolesAccess"] = "Roles & Access",
157
+ ["page.users"] = "Users",
158
+ ["page.auditLogs"] = "Audit Logs",
159
+ ["page.issueReport"] = "Issue Report",
160
+ ["page.projectList"] = "Project List",
161
+ ["page.taskList"] = "Task List",
162
+ ["page.taskBacklog"] = "Task Backlog",
163
+ ["page.roleLayouts"] = "Role Layouts",
164
+
165
+ ["page.dashboard.desc"] = "A quick overview of work, risk, and progress.",
166
+ ["page.managerDashboard.desc"] = "Track projects, people, and delivery health.",
167
+ ["page.employeeDashboard.desc"] = "Focus on your tasks, due dates, and progress.",
168
+ ["page.projects.desc"] = "Manage project plans, timelines, and assignments.",
169
+ ["page.projectAssignment.desc"] = "Assign team members to the right projects.",
170
+ ["page.tasks.desc"] = "Review, filter, and update work items.",
171
+ ["page.kanbanBoard.desc"] = "Move work through status columns.",
172
+ ["page.taskAssignment.desc"] = "Assign tasks to project members.",
173
+ ["page.taskDetails.desc"] = "Review task progress, comments, and updates.",
174
+ ["page.issues.desc"] = "Track blockers, notes, and daily work progress.",
175
+ ["page.addIssue.desc"] = "Record a new issue against a task.",
176
+ ["page.taskReport.desc"] = "Analyze task activity and export task reports.",
177
+ ["page.timeTracking.desc"] = "Review effort by task, project, and assignee.",
178
+ ["page.projectProgress.desc"] = "Monitor delivery health across active projects.",
179
+ ["page.overdueTasks.desc"] = "Focus on delayed work that needs attention.",
180
+ ["page.employeeReport.desc"] = "See how work is distributed across people.",
181
+ ["page.rolesAccess.desc"] = "Control user roles, menus, and permissions.",
182
+ ["page.users.desc"] = "Manage people, status, and account details.",
183
+ ["page.auditLogs.desc"] = "Review system activity and changes.",
184
+ ["page.issueReport.desc"] = "Track daily execution, backlog health, and issue-level effort.",
185
+ ["page.projectList.desc"] = "Review project details and progress at a glance.",
186
+ ["page.taskList.desc"] = "Review, filter, and update task records.",
187
+ ["page.taskBacklog.desc"] = "See unassigned and queued work items.",
188
+ ["page.roleLayouts.desc"] = "Review access group layouts and permissions.",
189
+
190
+ ["menu.DASHBOARD"] = "Dashboard",
191
+ ["menu.DASHBOARD_ADMIN"] = "Dashboard",
192
+ ["menu.DASHBOARD_MANAGER"] = "Dashboard",
193
+ ["menu.DASHBOARD_EMPLOYEE"] = "Dashboard",
194
+ ["menu.PROJECTS"] = "Projects",
195
+ ["menu.PROJECTS_LIST"] = "Project List",
196
+ ["menu.PROJECTS_ASSIGN"] = "Project Assign",
197
+ ["menu.TASKS"] = "Tasks",
198
+ ["menu.TASKS_LIST"] = "Task List",
199
+ ["menu.TASKS_BOARD"] = "Kanban Board",
200
+ ["menu.TASKS_ASSIGN"] = "Task Assign",
201
+ ["menu.TASKS_BACKLOG"] = "Task Backlog",
202
+ ["menu.ISSUES_ADD"] = "Add Issue",
203
+ ["menu.ISSUES_LIST"] = "Issue List",
204
+ ["menu.REPORTS"] = "Reports",
205
+ ["menu.REPORTS_TASKS"] = "Task Report",
206
+ ["menu.REPORTS_TIMESHEET"] = "Time Tracking",
207
+ ["menu.REPORTS_OVERDUE"] = "Overdue Tasks",
208
+ ["menu.REPORTS_EMPLOYEES"] = "Employee Report",
209
+ ["menu.REPORTS_PROJECTS"] = "Project Progress",
210
+ ["menu.REPORTS_ISSUES"] = "Issue Report",
211
+ ["menu.AUDIT_LOGS"] = "Audit Logs",
212
+ ["menu.USERS"] = "Users",
213
+ ["menu.ROLES"] = "Roles",
214
+ ["menu.DASHBOARD_WIDGETS"] = "Role Layouts",
215
+
216
+ ["status.todo"] = "To Do",
217
+ ["status.inprogress"] = "In Progress",
218
+ ["status.done"] = "Done",
219
+ ["status.overdue"] = "Overdue",
220
+ ["status.blocked"] = "Blocked",
221
+ ["status.clear"] = "Clear",
222
+ ["status.resolved"] = "Resolved",
223
+ ["status.uncompleted"] = "Uncompleted",
224
+ ["priority.low"] = "Low",
225
+ ["priority.medium"] = "Medium",
226
+ ["priority.high"] = "High",
227
+
228
+ ["action.search"] = "Search",
229
+ ["action.reset"] = "Reset",
230
+ ["action.exportExcel"] = "Export Excel",
231
+ ["action.exportPdf"] = "Export PDF",
232
+ ["action.edit"] = "Edit",
233
+ ["action.delete"] = "Delete",
234
+ ["action.add"] = "Add",
235
+ ["action.create"] = "Create",
236
+ ["action.update"] = "Update",
237
+ ["action.save"] = "Save",
238
+ ["action.assign"] = "Assign",
239
+ ["action.viewTasks"] = "View Tasks",
240
+ ["action.archiveDone"] = "Archive Done",
241
+ ["action.clear"] = "Clear",
242
+
243
+ ["result.loading"] = "Loading...",
244
+ ["result.saved"] = "Saved successfully.",
245
+ ["result.created"] = "Created successfully.",
246
+ ["result.updated"] = "Updated successfully.",
247
+ ["result.deleted"] = "Deleted successfully.",
248
+
249
+ ["report.periodFilter"] = "Period",
250
+ ["report.daily"] = "Daily (Today)",
251
+ ["report.monthly"] = "Monthly (This Month)",
252
+ ["report.yearly"] = "Yearly (This Year)",
253
+ ["report.customRange"] = "Custom Range",
254
+ ["report.selectedProject"] = "Selected project",
255
+ ["report.openIssues"] = "Open issues",
256
+ ["report.lastActivity"] = "Last activity",
257
+ ["report.noTasksFound"] = "No Tasks Found",
258
+ ["report.noTasksFoundDesc"] = "There are no tasks matching your filters. Try adjusting your search query or filters.",
259
+ ["report.noIssuesFound"] = "No issues found",
260
+ ["report.noIssuesFoundDesc"] = "No issues matched the current report filters.",
261
+ ["report.healthCritical"] = "Critical",
262
+ ["report.healthAtRisk"] = "At Risk",
263
+ ["report.healthGood"] = "Healthy",
264
+ ["report.delay"] = "Delay",
265
+ ["report.blockedBy"] = "Blocked by",
266
+ ["report.noEffortEntries"] = "No effort entries",
267
+ ["report.noEffortEntriesDesc"] = "No timesheet effort entries matched your filters.",
268
+ ["report.estimatedVsActual"] = "Estimated vs actual effort",
269
+ ["report.utilizationGauge"] = "Utilization gauge",
270
+ ["report.estimateAccuracy"] = "Estimate accuracy",
271
+ ["report.effortByProject"] = "Effort by project",
272
+ ["report.effortLeaderboard"] = "Effort leaderboard",
273
+ ["report.workloadHeatmap"] = "Workload heatmap",
274
+ ["report.capacitySupportList"] = "Capacity support list",
275
+ ["report.completedVsOpenIssues"] = "Completed vs open issues",
276
+ ["report.overdueSupport"] = "Overdue support",
277
+ ["report.completionPattern"] = "Completion pattern",
278
+ ["report.projectHealthMatrix"] = "Project health matrix",
279
+ ["report.completionVsRisk"] = "Completion vs risk",
280
+ ["report.timelineSummary"] = "Timeline summary",
281
+ ["report.openOverdueBlockingIssues"] = "Open, overdue, and blocking issues by project",
282
+ ["report.topAtRiskProjects"] = "Top at-risk projects",
283
+ ["report.keyInsights"] = "Key Insights",
284
+
285
+ ["validation.startDateRequired"] = "Start date is required.",
286
+ ["validation.dueDateRequired"] = "Due date is required.",
287
+ ["validation.dueDateBeforeStart"] = "Due date cannot be earlier than start date.",
288
+ ["validation.projectNameRequired"] = "Project name is required.",
289
+ ["validation.taskTitleRequired"] = "Task title is required.",
290
+ ["validation.selectProjectRequired"] = "Please select a project.",
291
+ ["validation.roleNameRequired"] = "Role name is required.",
292
+ ["validation.passwordMinLength"] = "Password must be at least 6 characters.",
293
+ ["validation.usernameMinLength"] = "Username must be at least 3 characters long.",
294
+ ["validation.usernameNoSpaces"] = "Username cannot contain spaces.",
295
+ ["validation.usernameInvalidCharacters"] = "Username cannot contain spaces, and can only contain letters, numbers, underscores (_), and periods (.)",
296
+ ["validation.passwordMinLengthRule"] = "Password must be at least 8 characters long.",
297
+ ["validation.passwordComplexityRule"] = "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character."
298
+ };
299
+
300
+ private static readonly IReadOnlyDictionary<string, string> Burmese = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
301
+ {
302
+ ["common.workspace"] = "အလုပ်ခွင်",
303
+ ["common.signIn"] = "အကောင့်ဝင်ရန်",
304
+ ["common.signOut"] = "အကောင့်ထွက်ရန်",
305
+ ["common.signOutTitle"] = "အကောင့်ထွက်မလား?",
306
+ ["common.signOutDescription"] = "Taskify မှ အကောင့်ထွက်ရန် သေချာပါသလား?",
307
+ ["common.cancel"] = "မလုပ်တော့ပါ",
308
+ ["common.confirm"] = "အတည်ပြုမည်",
309
+ ["common.confirmAction"] = "အတည်ပြုမည်",
310
+ ["common.areYouSure"] = "သင် သေချာပါသလား?",
311
+ ["common.cannotUndo"] = "ဤလုပ်ဆောင်ချက်ကို ပြန်ပြင်မရပါ။",
312
+ ["common.yes"] = "ဟုတ်ကဲ့",
313
+ ["common.no"] = "မဟုတ်ပါ",
314
+ ["common.save"] = "သိမ်းမည်",
315
+ ["common.create"] = "ဖန်တီးမည်",
316
+ ["common.update"] = "အပ်ဒိတ်လုပ်မည်",
317
+ ["common.delete"] = "ဖျက်မည်",
318
+ ["common.confirmSignOut"] = "ဟုတ်ကဲ့၊ အကောင့်ထွက်မည်",
319
+ ["common.listView"] = "စာရင်းမြင်ကွင်း",
320
+ ["common.chartView"] = "ဇယားမြင်ကွင်း",
321
+ ["common.search"] = "ရှာမည်",
322
+ ["common.reset"] = "ပြန်လည်သတ်မှတ်မည်",
323
+ ["common.clear"] = "ရှင်းမည်",
324
+ ["common.all"] = "အားလုံး",
325
+ ["common.loading"] = "ဖွင့်နေသည်...",
326
+ ["common.close"] = "ပိတ်မည်",
327
+ ["common.openNavigation"] = "မီနူးဖွင့်ရန်",
328
+ ["common.closeNavigation"] = "မီနူးပိတ်ရန်",
329
+ ["common.index"] = "နံပါတ်",
330
+ ["common.exportExcel"] = "Excel ထုတ်မည်",
331
+ ["common.exportPdf"] = "PDF ထုတ်မည်",
332
+ ["common.actions"] = "လုပ်ဆောင်ချက်များ",
333
+ ["common.status"] = "အခြေအနေ",
334
+ ["common.priority"] = "ဦးစားပေး",
335
+ ["common.assignee"] = "တာဝန်ယူသူ",
336
+ ["common.project"] = "စီမံကိန်း",
337
+ ["common.task"] = "လုပ်ငန်း",
338
+ ["common.taskProject"] = "လုပ်ငန်း / စီမံကိန်း",
339
+ ["common.issue"] = "ပြဿနာ",
340
+ ["common.user"] = "အသုံးပြုသူ",
341
+ ["common.role"] = "အခန်းကဏ္ဍ",
342
+ ["common.module"] = "မော်ဂျူး",
343
+ ["common.description"] = "ဖော်ပြချက်",
344
+ ["common.date"] = "ရက်စွဲ",
345
+ ["common.startDate"] = "စတင်ရက်",
346
+ ["common.endDate"] = "ပြီးဆုံးရက်",
347
+ ["common.dueDate"] = "ပြီးဆုံးရန်ရက်",
348
+ ["common.due"] = "ပြီးဆုံးရက်",
349
+ ["common.hours"] = "နာရီ",
350
+ ["common.risk"] = "အန္တရာယ်",
351
+ ["common.blocked"] = "ပိတ်ဆို့ထားသည်",
352
+ ["common.overdue"] = "နောက်ကျနေသည်",
353
+ ["common.done"] = "ပြီးဆုံး",
354
+ ["common.inProgress"] = "ဆောင်ရွက်နေသည်",
355
+ ["common.todo"] = "လုပ်ရန်",
356
+ ["common.resolved"] = "ဖြေရှင်းပြီး",
357
+ ["common.clearStatus"] = "ရှင်းလင်း",
358
+ ["common.none"] = "မရှိ",
359
+ ["common.unassigned"] = "မချမှတ်ရသေး",
360
+ ["common.noDescription"] = "ဖော်ပြချက်မရှိ",
361
+ ["common.noneRecorded"] = "မှတ်တမ်းမရှိ",
362
+ ["common.na"] = "မသက်ဆိုင်ပါ",
363
+ ["common.noAccessItems"] = "ဤအခန်းကဏ္ဍအတွက် ဝင်ရောက်ခွင့်မရှိသေးပါ။",
364
+ ["common.accessDenied"] = "ဝင်ရောက်ခွင့် မရှိပါ",
365
+ ["common.accessDeniedDescription"] = "ဤစာမျက်နှာသို့ ဝင်ရောက်ရန် သင်၌ ခွင့်ပြုချက်မရှိပါ။ စနစ်စီမံသူကို ဆက်သွယ်ပါ။",
366
+ ["common.backToDashboard"] = "ဒက်ရှ်ဘုတ်သို့ ပြန်သွားမည်",
367
+ ["common.noDataYet"] = "အချက်အလက် မရှိသေးပါ",
368
+ ["common.createFirstEntry"] = "စတင်ရန် ပထမဆုံး အချက်အလက်ကို ဖန်တီးပါ။",
369
+ ["common.loadingAuditLogs"] = "မှတ်တမ်းများ ဖွင့်နေသည်...",
370
+ ["common.loadingUsers"] = "အသုံးပြုသူများ ဖွင့်နေသည်...",
371
+ ["common.loadingProjects"] = "စီမံကိန်းများ ဖွင့်နေသည်...",
372
+ ["common.loadingTasks"] = "လုပ်ငန်းများ ဖွင့်နေသည်...",
373
+ ["common.searchPlaceholder"] = "အမည်၊ အီးမေးလ်၊ အသုံးပြုသူအမည်ဖြင့် ရှာပါ...",
374
+ ["common.searchTasks"] = "အလုပ်ရှာရန်",
375
+ ["common.searchByTitleOrProject"] = "ခေါင်းစဉ် သို့မဟုတ် စီမံကိန်းဖြင့် ရှာပါ...",
376
+ ["common.searchByUser"] = "အသုံးပြုသူ၊ လုပ်ဆောင်ချက်၊ မော်ဂျူး သို့မဟုတ် အသေးစိတ်ဖြင့် ရှာပါ...",
377
+ ["common.searchByTitle"] = "ခေါင်းစဉ် သို့မဟုတ် စီမံကိန်းအမည်ဖြင့် ရှာပါ...",
378
+ ["common.searchProjects"] = "စီမံကိန်းများ ရှာပါ...",
379
+ ["common.searchTeamMembers"] = "အဖွဲ့ဝင်များ ရှာပါ...",
380
+ ["common.searchProjectMembers"] = "စီမံကိန်းအဖွဲ့ဝင်များ ရှာပါ...",
381
+ ["common.searchIssues"] = "ပြဿနာ ခေါင်းစဉ်၊ ဖော်ပြချက် သို့မဟုတ် တာဝန်ယူသူဖြင့် ရှာပါ...",
382
+ ["common.searchByIssueAssignee"] = "ပြဿနာ / တာဝန်ယူသူ ရှာရန်",
383
+ ["common.loadingSpinner"] = "ဖွင့်နေသည်...",
384
+ ["common.loadingIssues"] = "ပြဿနာများ ဖွင့်နေသည်...",
385
+ ["common.loadingTasksAndUsers"] = "လုပ်ငန်းများနှင့် အသုံးပြုသူများ ဖွင့်နေသည်...",
386
+ ["common.loadingProjectsAndUsers"] = "စီမံကိန်းများနှင့် အသုံးပြုသူများ ဖွင့်နေသည်...",
387
+ ["common.loadingProjectMembers"] = "စီမံကိန်းအဖွဲ့ဝင်များ ဖွင့်နေသည်...",
388
+ ["common.loadingMembers"] = "အဖွဲ့ဝင်များ ဖွင့်နေသည်...",
389
+ ["common.noResults"] = "ရလဒ်မတွေ့ပါ။",
390
+ ["common.notApplicable"] = "မသက်ဆိုင်ပါ",
391
+ ["common.selectAll"] = "အားလုံးရွေးမည်",
392
+ ["common.deselectAll"] = "အားလုံးဖြုတ်မည်",
393
+ ["common.selected"] = "ရွေးထားသည်",
394
+ ["common.viewDetails"] = "အသေးစိတ်ကြည့်မည်",
395
+ ["common.currentLanguage"] = "ဘာသာစကားပြောင်းရန်",
396
+ ["common.languageEnglish"] = "အင်္ဂလိပ်",
397
+ ["common.languageBurmese"] = "မြန်မာ",
398
+ ["common.allTasks"] = "အလုပ်အားလုံး",
399
+ ["common.allProjects"] = "စီမံကိန်းအားလုံး",
400
+ ["common.allRoles"] = "အခန်းကဏ္ဍအားလုံး",
401
+ ["common.noProjectsFound"] = "စီမံကိန်းမတွေ့ပါ။",
402
+ ["common.noTasksFound"] = "လုပ်ငန်းမတွေ့ပါ။",
403
+ ["common.noTeamMembersFound"] = "သင်ရှာဖွေသည့် အဖွဲ့ဝင် မရှိပါ။",
404
+ ["common.noMembersMatchSearch"] = "လက်ရှိရှာဖွေမှုနှင့် ကိုက်ညီသည့် အဖွဲ့ဝင် မရှိပါ။",
405
+ ["common.noTaskSelected"] = "လုပ်ငန်း မရွေးထားပါ",
406
+ ["common.noProjectSelected"] = "စီမံကိန်း မရွေးထားပါ",
407
+ ["common.noProject"] = "စီမံကိန်း မရှိပါ",
408
+ ["common.currentlyAssigningTask"] = "လက်ရှိ ချထားနေသော လုပ်ငန်း",
409
+ ["common.currentlyManaging"] = "လက်ရှိ စီမံနေသည်",
410
+ ["common.selectProject"] = "စီမံကိန်း ရွေးရန်",
411
+ ["common.selectTask"] = "လုပ်ငန်း ရွေးရန်...",
412
+ ["common.issueTitle"] = "ပြဿနာ ခေါင်းစဉ်",
413
+ ["common.issueDescription"] = "ဖော်ပြချက်",
414
+ ["common.blockedStatus"] = "ပိတ်ဆို့မှု အခြေအနေ",
415
+ ["common.escalationLevel"] = "အဆင့်တိုးမှု အဆင့်",
416
+ ["common.blockedBy"] = "ပိတ်ဆို့နေသူ",
417
+ ["common.delayReason"] = "နှောင့်နှေးရသည့် အကြောင်းရင်း",
418
+ ["common.actualHours"] = "တကယ့် နာရီ",
419
+ ["common.estimatedHours"] = "ခန့်မှန်း နာရီ",
420
+ ["common.assignedToMeOnly"] = "ကျွန်ုပ်ထံပဲ ချထားထားသည်",
421
+ ["common.saveAssignment"] = "တာဝန်ချမှု သိမ်းမည်",
422
+ ["common.saveAssignmentChanges"] = "တာဝန်ချမှု ပြောင်းလဲချက်များ သိမ်းမည်",
423
+ ["common.saveChanges"] = "ပြောင်းလဲချက်များ သိမ်းမည်",
424
+ ["common.addIssue"] = "ပြဿနာ ထည့်မည်",
425
+ ["common.addIssueTitle"] = "ပြဿနာ ထည့်မည်လား?",
426
+ ["common.editIssue"] = "ပြဿနာ ပြင်မည်",
427
+ ["common.removeOwnerFromTask"] = "ဤလုပ်ငန်းမှ ပိုင်ရှင်ကို ဖယ်ရှားမည်",
428
+
429
+ ["page.dashboard"] = "ဒက်ရှ်ဘုတ်",
430
+ ["page.managerDashboard"] = "မန်နေဂျာ ဒက်ရှ်ဘုတ်",
431
+ ["page.employeeDashboard"] = "ဝန်ထမ်း ဒက်ရှ်ဘုတ်",
432
+ ["page.projects"] = "စီမံကိန်းများ",
433
+ ["page.projectAssignment"] = "စီမံကိန်း ခွဲဝေခြင်း",
434
+ ["page.tasks"] = "လုပ်ငန်းများ",
435
+ ["page.kanbanBoard"] = "ကန်ဘန်းဘုတ်",
436
+ ["page.taskAssignment"] = "လုပ်ငန်း ခွဲဝေခြင်း",
437
+ ["page.taskDetails"] = "လုပ်ငန်း အသေးစိတ်",
438
+ ["page.issues"] = "ပြဿနာများ",
439
+ ["page.addIssue"] = "ပြဿနာ ထည့်ရန်",
440
+ ["page.taskReport"] = "လုပ်ငန်း အစီရင်ခံစာ",
441
+ ["page.timeTracking"] = "အချိန်မှတ်တမ်း",
442
+ ["page.projectProgress"] = "စီမံကိန်း တိုးတက်မှု",
443
+ ["page.overdueTasks"] = "နောက်ကျ လုပ်ငန်းများ",
444
+ ["page.employeeReport"] = "ဝန်ထမ်း အစီရင်ခံစာ",
445
+ ["page.rolesAccess"] = "အခန်းကဏ္ဍနှင့် ဝင်ခွင့်",
446
+ ["page.users"] = "အသုံးပြုသူများ",
447
+ ["page.auditLogs"] = "စနစ်မှတ်တမ်းများ",
448
+ ["page.issueReport"] = "ပြဿနာ အစီရင်ခံစာ",
449
+ ["page.projectList"] = "စီမံကိန်းစာရင်း",
450
+ ["page.taskList"] = "လုပ်ငန်းစာရင်း",
451
+ ["page.taskBacklog"] = "လုပ်ငန်း နောက်ခံစာရင်း",
452
+ ["page.roleLayouts"] = "အခန်းကဏ္ဍ အပြင်အဆင်များ",
453
+
454
+ ["page.dashboard.desc"] = "အလုပ်များ၊ အန္တရာယ်များနှင့် တိုးတက်မှုများကို တစ်ချက်ကြည့်နိုင်သည်။",
455
+ ["page.managerDashboard.desc"] = "စီမံကိန်းများ၊ လူအင်အားနှင့် ပေးပို့မှုအခြေအနေကို စောင့်ကြည့်ပါ။",
456
+ ["page.employeeDashboard.desc"] = "သင့်လုပ်ငန်းများ၊ သတ်မှတ်ရက်နှင့် တိုးတက်မှုများကို အာရုံစိုက်ပါ။",
457
+ ["page.projects.desc"] = "စီမံကိန်းအစီအစဉ်၊ အချိန်ဇယားနှင့် ခွဲဝေမှုများကို စီမံပါ။",
458
+ ["page.projectAssignment.desc"] = "အဖွဲ့ဝင်များကို သင့်တော်သော စီမံကိန်းများသို့ ခွဲဝေပါ။",
459
+ ["page.tasks.desc"] = "လုပ်ငန်းများကို ကြည့်ရှု၊ စစ်ထုတ်၊ အပ်ဒိတ်လုပ်ပါ။",
460
+ ["page.kanbanBoard.desc"] = "အလုပ်များကို အခြေအနေကော်လံများအတိုင်း ရွှေ့ပါ။",
461
+ ["page.taskAssignment.desc"] = "လုပ်ငန်းများကို စီမံကိန်းအဖွဲ့ဝင်များသို့ ခွဲဝေပါ။",
462
+ ["page.taskDetails.desc"] = "လုပ်ငန်းတိုးတက်မှု၊ မှတ်ချက်များနှင့် အပ်ဒိတ်များကို ကြည့်ရှုပါ။",
463
+ ["page.issues.desc"] = "ပိတ်ဆို့မှုများ၊ မှတ်စုများနှင့် နေ့စဉ် အလုပ်တိုးတက်မှုကို ခြေရာခံပါ။",
464
+ ["page.addIssue.desc"] = "လုပ်ငန်းတစ်ခုအတွက် ပြဿနာအသစ်ကို မှတ်တမ်းတင်ပါ။",
465
+ ["page.taskReport.desc"] = "လုပ်ငန်းလှုပ်ရှားမှုကို ခွဲခြမ်းပြီး အစီရင်ခံစာများ ထုတ်ပါ။",
466
+ ["page.timeTracking.desc"] = "လုပ်ငန်း၊ စီမံကိန်းနှင့် တာဝန်ယူသူအလိုက် အားထုတ်မှုကို ကြည့်ရှုပါ။",
467
+ ["page.projectProgress.desc"] = "လက်ရှိ စီမံကိန်းများ၏ ပေးပို့မှုအခြေအနေကို စောင့်ကြည့်ပါ။",
468
+ ["page.overdueTasks.desc"] = "အရေးပေါ် အာရုံစိုက်ရန် လိုသည့် နောက်ကျလုပ်ငန်းများကို ကြည့်ပါ။",
469
+ ["page.employeeReport.desc"] = "အလုပ်တာဝန်များ လူတစ်ဦးချင်းစီအလိုက် မည်သို့ ဖြန့်ဝေထားသည်ကို ကြည့်ပါ။",
470
+ ["page.rolesAccess.desc"] = "အသုံးပြုသူအခန်းကဏ္ဍ၊ မီနူးများနှင့် ခွင့်ပြုချက်များကို ထိန်းချုပ်ပါ။",
471
+ ["page.users.desc"] = "လူများ၊ အခြေအနေများနှင့် အကောင့်အသေးစိတ်များကို စီမံပါ။",
472
+ ["page.auditLogs.desc"] = "စနစ်လှုပ်ရှားမှုများနှင့် ပြောင်းလဲမှုများကို ပြန်လည်ကြည့်ရှုပါ။",
473
+ ["page.issueReport.desc"] = "နေ့စဉ် ဆောင်ရွက်မှု၊ နောက်ကျမှုနှင့် ပြဿနာအလိုက် အားထုတ်မှုကို ခြေရာခံပါ။",
474
+ ["page.projectList.desc"] = "စီမံကိန်းအသေးစိတ်နှင့် တိုးတက်မှုကို တစ်ချက်ကြည့်ပါ။",
475
+ ["page.taskList.desc"] = "လုပ်ငန်းမှတ်တမ်းများကို ကြည့်ရှု၊ စစ်ထုတ်၊ အပ်ဒိတ်လုပ်ပါ။",
476
+ ["page.taskBacklog.desc"] = "မခွဲဝေသေးသော နှင့် စောင့်ဆိုင်းနေသော အလုပ်များကို ကြည့်ပါ။",
477
+ ["page.roleLayouts.desc"] = "ဝင်ခွင့်အုပ်စု ပုံစံများနှင့် ခွင့်ပြုချက်များကို ကြည့်ပါ။",
478
+
479
+ ["menu.DASHBOARD"] = "ဒက်ရှ်ဘုတ်",
480
+ ["menu.DASHBOARD_ADMIN"] = "ဒက်ရှ်ဘုတ်",
481
+ ["menu.DASHBOARD_MANAGER"] = "ဒက်ရှ်ဘုတ်",
482
+ ["menu.DASHBOARD_EMPLOYEE"] = "ဒက်ရှ်ဘုတ်",
483
+ ["menu.PROJECTS"] = "စီမံကိန်းများ",
484
+ ["menu.PROJECTS_LIST"] = "စီမံကိန်းစာရင်း",
485
+ ["menu.PROJECTS_ASSIGN"] = "စီမံကိန်း ခွဲဝေခြင်း",
486
+ ["menu.TASKS"] = "လုပ်ငန်းများ",
487
+ ["menu.TASKS_LIST"] = "လုပ်ငန်းစာရင်း",
488
+ ["menu.TASKS_BOARD"] = "ကန်ဘန်းဘုတ်",
489
+ ["menu.TASKS_ASSIGN"] = "လုပ်ငန်း ခွဲဝေခြင်း",
490
+ ["menu.TASKS_BACKLOG"] = "လုပ်ငန်း နောက်ခံစာရင်း",
491
+ ["menu.ISSUES_ADD"] = "ပြဿနာ ထည့်ရန်",
492
+ ["menu.ISSUES_LIST"] = "ပြဿနာ စာရင်း",
493
+ ["menu.REPORTS"] = "အစီရင်ခံစာများ",
494
+ ["menu.REPORTS_TASKS"] = "လုပ်ငန်း အစီရင်ခံစာ",
495
+ ["menu.REPORTS_TIMESHEET"] = "အချိန်မှတ်တမ်း",
496
+ ["menu.REPORTS_OVERDUE"] = "နောက်ကျ လုပ်ငန်းများ",
497
+ ["menu.REPORTS_EMPLOYEES"] = "ဝန်ထမ်း အစီရင်ခံစာ",
498
+ ["menu.REPORTS_PROJECTS"] = "စီမံကိန်း တိုးတက်မှု",
499
+ ["menu.REPORTS_ISSUES"] = "ပြဿနာ အစီရင်ခံစာ",
500
+ ["menu.AUDIT_LOGS"] = "စနစ်မှတ်တမ်းများ",
501
+ ["menu.USERS"] = "အသုံးပြုသူများ",
502
+ ["menu.ROLES"] = "အခန်းကဏ္ဍများ",
503
+ ["menu.DASHBOARD_WIDGETS"] = "အခန်းကဏ္ဍ အပြင်အဆင်များ",
504
+
505
+ ["status.todo"] = "လုပ်ရန်",
506
+ ["status.inprogress"] = "ဆောင်ရွက်နေသည်",
507
+ ["status.done"] = "ပြီးဆုံး",
508
+ ["status.overdue"] = "နောက်ကျ",
509
+ ["status.blocked"] = "ပိတ်ဆို့",
510
+ ["status.clear"] = "ရှင်း",
511
+ ["status.resolved"] = "ဖြေရှင်းပြီး",
512
+ ["status.uncompleted"] = "မပြီးသေး",
513
+ ["priority.low"] = "နိမ့်",
514
+ ["priority.medium"] = "အလယ်",
515
+ ["priority.high"] = "မြင့်",
516
+
517
+ ["action.search"] = "ရှာမည်",
518
+ ["action.reset"] = "ပြန်လည်သတ်မှတ်မည်",
519
+ ["action.exportExcel"] = "Excel ထုတ်မည်",
520
+ ["action.exportPdf"] = "PDF ထုတ်မည်",
521
+ ["action.edit"] = "ပြင်မည်",
522
+ ["action.delete"] = "ဖျက်မည်",
523
+ ["action.add"] = "ထည့်မည်",
524
+ ["action.create"] = "ဖန်တီးမည်",
525
+ ["action.update"] = "အပ်ဒိတ်လုပ်မည်",
526
+ ["action.save"] = "သိမ်းမည်",
527
+ ["action.assign"] = "ခွဲဝေမည်",
528
+ ["action.viewTasks"] = "လုပ်ငန်းများ ကြည့်မည်",
529
+ ["action.archiveDone"] = "ပြီးဆုံးများ သိမ်းဆည်းမည်",
530
+ ["action.clear"] = "ရှင်းမည်",
531
+
532
+ ["result.loading"] = "ဖွင့်နေသည်...",
533
+ ["result.saved"] = "အောင်မြင်စွာ သိမ်းပြီးပါပြီ။",
534
+ ["result.created"] = "အောင်မြင်စွာ ဖန်တီးပြီးပါပြီ။",
535
+ ["result.updated"] = "အောင်မြင်စွာ အပ်ဒိတ်ပြီးပါပြီ။",
536
+ ["result.deleted"] = "အောင်မြင်စွာ ဖျက်ပြီးပါပြီ။",
537
+ ["report.periodFilter"] = "ကာလ",
538
+ ["report.daily"] = "နေ့စဉ် (ယနေ့)",
539
+ ["report.monthly"] = "လစဉ် (ယခုလ)",
540
+ ["report.yearly"] = "နှစ်စဉ် (ယခုနှစ်)",
541
+ ["report.customRange"] = "စိတ်ကြိုက်အပိုင်းအခြား",
542
+ ["report.selectedProject"] = "ရွေးထားသော စီမံကိန်း",
543
+ ["report.openIssues"] = "ဖွင့်ထားသော ပြဿနာများ",
544
+ ["report.lastActivity"] = "နောက်ဆုံး လုပ်ဆောင်ချက်",
545
+ ["report.noTasksFound"] = "အလုပ်မတွေ့ပါ",
546
+ ["report.noTasksFoundDesc"] = "လက်ရှိစစ်ထုတ်မှုများနှင့် ကိုက်ညီသော အလုပ် မရှိပါ။ ရှာဖွေမှု သို့မဟုတ် စစ်ထုတ်ချက်များကို ပြင်ဆင်ကြည့်ပါ။",
547
+ ["report.noIssuesFound"] = "ပြဿနာမတွေ့ပါ",
548
+ ["report.noIssuesFoundDesc"] = "လက်ရှိအစီရင်ခံ စစ်ထုတ်ချက်များနှင့် ကိုက်ညီသော ပြဿနာ မရှိပါ။",
549
+ ["report.healthCritical"] = "အရေးကြီး",
550
+ ["report.healthAtRisk"] = "အန္တရာယ်ရှိ",
551
+ ["report.healthGood"] = "ကောင်းမွန်",
552
+ ["report.delay"] = "နှောင့်နှေးချက်",
553
+ ["report.blockedBy"] = "ပိတ်ဆို့နေသည်မှာ",
554
+
555
+ ["validation.startDateRequired"] = "စတင်ရက် လိုအပ်ပါသည်။",
556
+ ["validation.dueDateRequired"] = "ပြီးဆုံးရက် လိုအပ်ပါသည်။",
557
+ ["validation.dueDateBeforeStart"] = "ပြီးဆုံးရက်သည် စတင်ရက်ထက် စော၍ မရပါ။",
558
+ ["validation.projectNameRequired"] = "စီမံကိန်းအမည် လိုအပ်ပါသည်။",
559
+ ["validation.taskTitleRequired"] = "လုပ်ငန်းခေါင်းစဉ် လိုအပ်ပါသည်။",
560
+ ["validation.selectProjectRequired"] = "စီမံကိန်းတစ်ခုကို ရွေးပါ။",
561
+ ["validation.roleNameRequired"] = "အခန်းကဏ္ဍအမည် လိုအပ်ပါသည်။",
562
+ ["validation.passwordMinLength"] = "စကားဝှက်မှာ အနည်းဆုံး ၆ လုံး ရှိရမည်။",
563
+ ["validation.usernameMinLength"] = "အသုံးပြုသူအမည်မှာ အနည်းဆုံး ၃ လုံး ရှိရမည်။",
564
+ ["validation.usernameNoSpaces"] = "အသုံးပြုသူအမည်တွင် space မပါရပါ။",
565
+ ["validation.usernameInvalidCharacters"] = "အသုံးပြုသူအမည်တွင် space မပါရပါ၊ စာလုံး၊ နံပါတ်၊ underscore (_) နှင့် dot (.) သာ အသုံးပြုနိုင်သည်။",
566
+ ["validation.passwordMinLengthRule"] = "စကားဝှက်မှာ အနည်းဆုံး ၈ လုံး ရှိရမည်။",
567
+ ["validation.passwordComplexityRule"] = "စကားဝှက်တွင် အနည်းဆုံး uppercase တစ်လုံး၊ lowercase တစ်လုံး၊ နံပါတ်တစ်လုံးနှင့် အထူးလက္ခဏာတစ်လုံး ပါရမည်။"
568
+ };
569
+
570
+ public static bool IsBurmeseCulture => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Equals("my", StringComparison.OrdinalIgnoreCase);
571
+
572
+ public static string Text(string key, string fallback = "")
573
+ {
574
+ var catalog = IsBurmeseCulture ? Burmese : English;
575
+ if (catalog.TryGetValue(key, out var value))
576
+ {
577
+ return value;
578
+ }
579
+
580
+ if ((IsBurmeseCulture ? English : Burmese).TryGetValue(key, out value))
581
+ {
582
+ return value;
583
+ }
584
+
585
+ return string.IsNullOrWhiteSpace(fallback) ? key : fallback;
586
+ }
587
+
588
+ public static string Menu(string menuCode, string fallback = "") => Text($"menu.{menuCode}", fallback);
589
+
590
+ public static string PageTitle(string key, string fallback = "") => Text($"page.{key}", fallback);
591
+
592
+ public static string PageDescription(string key, string fallback = "") => Text($"page.{key}.desc", fallback);
593
+
594
+ public static string StatusLabel(AppTaskStatus status) => status switch
595
+ {
596
+ AppTaskStatus.InProgress => Text("status.inprogress", "In Progress"),
597
+ AppTaskStatus.Done => Text("status.done", "Done"),
598
+ _ => Text("status.todo", "To Do")
599
+ };
600
+
601
+ public static string PriorityLabel(TaskPriority priority) => priority switch
602
+ {
603
+ TaskPriority.High => Text("priority.high", "High"),
604
+ TaskPriority.Medium => Text("priority.medium", "Medium"),
605
+ _ => Text("priority.low", "Low")
606
+ };
607
+
608
+ public static string EscalationLabel(int level) => level switch
609
+ {
610
+ 1 => IsBurmeseCulture ? "နိမ့်" : "Low",
611
+ 2 => IsBurmeseCulture ? "အလယ်" : "Medium",
612
+ 3 => IsBurmeseCulture ? "မြင့်" : "High",
613
+ _ => Text("common.none", "None")
614
+ };
615
+ }
TaskTrackingSystem.Shared/Models/Issue/CreateIssueDto.cs CHANGED
@@ -2,6 +2,7 @@ using System;
2
  using System.Collections.Generic;
3
  using System.ComponentModel.DataAnnotations;
4
  using TaskTrackingSystem.Shared.Enums;
 
5
  using AppTaskStatus = TaskTrackingSystem.Shared.Enums.AppTaskStatus;
6
 
7
  namespace TaskTrackingSystem.Shared.Models.Issue
@@ -54,17 +55,17 @@ namespace TaskTrackingSystem.Shared.Models.Issue
54
  {
55
  if (StartDate == default)
56
  {
57
- yield return new ValidationResult("Start date is required.", new[] { nameof(StartDate) });
58
  }
59
 
60
  if (DueDate == default)
61
  {
62
- yield return new ValidationResult("Due date is required.", new[] { nameof(DueDate) });
63
  }
64
 
65
  if (StartDate != default && DueDate != default && DueDate.Date < StartDate.Date)
66
  {
67
- yield return new ValidationResult("Due date cannot be earlier than start date.", new[] { nameof(DueDate), nameof(StartDate) });
68
  }
69
  }
70
  }
 
2
  using System.Collections.Generic;
3
  using System.ComponentModel.DataAnnotations;
4
  using TaskTrackingSystem.Shared.Enums;
5
+ using TaskTrackingSystem.Shared.Localization;
6
  using AppTaskStatus = TaskTrackingSystem.Shared.Enums.AppTaskStatus;
7
 
8
  namespace TaskTrackingSystem.Shared.Models.Issue
 
55
  {
56
  if (StartDate == default)
57
  {
58
+ yield return new ValidationResult(AppLocalization.Text("validation.startDateRequired", "Start date is required."), new[] { nameof(StartDate) });
59
  }
60
 
61
  if (DueDate == default)
62
  {
63
+ yield return new ValidationResult(AppLocalization.Text("validation.dueDateRequired", "Due date is required."), new[] { nameof(DueDate) });
64
  }
65
 
66
  if (StartDate != default && DueDate != default && DueDate.Date < StartDate.Date)
67
  {
68
+ yield return new ValidationResult(AppLocalization.Text("validation.dueDateBeforeStart", "Due date cannot be earlier than start date."), new[] { nameof(DueDate), nameof(StartDate) });
69
  }
70
  }
71
  }
TaskTrackingSystem.Shared/Models/Issue/UpdateIssueDto.cs CHANGED
@@ -2,6 +2,7 @@ using System;
2
  using System.Collections.Generic;
3
  using System.ComponentModel.DataAnnotations;
4
  using TaskTrackingSystem.Shared.Enums;
 
5
  using AppTaskStatus = TaskTrackingSystem.Shared.Enums.AppTaskStatus;
6
 
7
  namespace TaskTrackingSystem.Shared.Models.Issue
@@ -50,17 +51,17 @@ namespace TaskTrackingSystem.Shared.Models.Issue
50
  {
51
  if (StartDate == default)
52
  {
53
- yield return new ValidationResult("Start date is required.", new[] { nameof(StartDate) });
54
  }
55
 
56
  if (DueDate == default)
57
  {
58
- yield return new ValidationResult("Due date is required.", new[] { nameof(DueDate) });
59
  }
60
 
61
  if (StartDate != default && DueDate != default && DueDate.Date < StartDate.Date)
62
  {
63
- yield return new ValidationResult("Due date cannot be earlier than start date.", new[] { nameof(DueDate), nameof(StartDate) });
64
  }
65
  }
66
  }
 
2
  using System.Collections.Generic;
3
  using System.ComponentModel.DataAnnotations;
4
  using TaskTrackingSystem.Shared.Enums;
5
+ using TaskTrackingSystem.Shared.Localization;
6
  using AppTaskStatus = TaskTrackingSystem.Shared.Enums.AppTaskStatus;
7
 
8
  namespace TaskTrackingSystem.Shared.Models.Issue
 
51
  {
52
  if (StartDate == default)
53
  {
54
+ yield return new ValidationResult(AppLocalization.Text("validation.startDateRequired", "Start date is required."), new[] { nameof(StartDate) });
55
  }
56
 
57
  if (DueDate == default)
58
  {
59
+ yield return new ValidationResult(AppLocalization.Text("validation.dueDateRequired", "Due date is required."), new[] { nameof(DueDate) });
60
  }
61
 
62
  if (StartDate != default && DueDate != default && DueDate.Date < StartDate.Date)
63
  {
64
+ yield return new ValidationResult(AppLocalization.Text("validation.dueDateBeforeStart", "Due date cannot be earlier than start date."), new[] { nameof(DueDate), nameof(StartDate) });
65
  }
66
  }
67
  }
TaskTrackingSystem.Shared/Result.cs CHANGED
@@ -1,4 +1,5 @@
1
  using System;
 
2
 
3
  namespace TaskTrackingSystem.Shared
4
  {
@@ -66,7 +67,7 @@ namespace TaskTrackingSystem.Shared
66
  public const string ProjectNameRequired = "Project name is required.";
67
  public const string FailedToUpdateProject = "Failed to update project.";
68
  public const string FailedToCreateProject = "Failed to create project.";
69
- public static string ProjectNotFound(long id) => $"Project with ID {id} not found.";
70
  public const string UserIdsCannotBeNull = "User IDs cannot be null.";
71
  public static string InvalidUserIds(string ids) => $"The following user IDs are invalid or deleted: {ids}";
72
  public static string UserNotProjectMember(long userId, long projectId) => $"User with ID {userId} is not a member of project with ID {projectId}.";
@@ -76,23 +77,23 @@ namespace TaskTrackingSystem.Shared
76
  public const string SelectProjectRequired = "Please select a project.";
77
  public const string FailedToUpdateTask = "Failed to update task.";
78
  public const string FailedToCreateTask = "Failed to create task.";
79
- public static string TaskNotFound(long id) => $"Task with ID {id} not found.";
80
- public static string TaskNotFoundOrDeleted(long id) => $"Task with ID {id} not found or already deleted.";
81
 
82
  // Role/Permission
83
  public const string RoleNameRequired = "Role name is required.";
84
  public const string FailedToUpdateRole = "Failed to update role.";
85
  public const string FailedToCreateRole = "Failed to create role.";
86
- public static string RoleNotFound(long id) => $"Role with ID {id} not found.";
87
- public const string AccessCodesCannotBeNull = "Access codes cannot be null.";
88
- public static string InvalidAccessCodes(string ids) => $"The following access codes are invalid or deleted: {ids}";
89
 
90
  // User
91
  public const string FillAllFields = "Please fill in all required fields.";
92
  public const string FailedToUpdateUser = "Failed to update user.";
93
  public const string PasswordMinLength = "Password must be at least 6 characters.";
94
  public const string FailedToCreateUser = "Failed to create user.";
95
- public static string UserNotFound(long id) => $"User with ID {id} not found.";
96
 
97
  // Register
98
  public const string ParseRegistrationResponseFailed = "Failed to parse registration response.";
 
1
  using System;
2
+ using TaskTrackingSystem.Shared.Localization;
3
 
4
  namespace TaskTrackingSystem.Shared
5
  {
 
67
  public const string ProjectNameRequired = "Project name is required.";
68
  public const string FailedToUpdateProject = "Failed to update project.";
69
  public const string FailedToCreateProject = "Failed to create project.";
70
+ public static string ProjectNotFound(long id) => AppLocalization.Text("result.projectNotFound", $"Project with ID {id} not found.").Replace("{id}", id.ToString());
71
  public const string UserIdsCannotBeNull = "User IDs cannot be null.";
72
  public static string InvalidUserIds(string ids) => $"The following user IDs are invalid or deleted: {ids}";
73
  public static string UserNotProjectMember(long userId, long projectId) => $"User with ID {userId} is not a member of project with ID {projectId}.";
 
77
  public const string SelectProjectRequired = "Please select a project.";
78
  public const string FailedToUpdateTask = "Failed to update task.";
79
  public const string FailedToCreateTask = "Failed to create task.";
80
+ public static string TaskNotFound(long id) => AppLocalization.Text("result.taskNotFound", $"Task with ID {id} not found.").Replace("{id}", id.ToString());
81
+ public static string TaskNotFoundOrDeleted(long id) => AppLocalization.Text("result.taskNotFoundOrDeleted", $"Task with ID {id} not found or already deleted.").Replace("{id}", id.ToString());
82
 
83
  // Role/Permission
84
  public const string RoleNameRequired = "Role name is required.";
85
  public const string FailedToUpdateRole = "Failed to update role.";
86
  public const string FailedToCreateRole = "Failed to create role.";
87
+ public static string RoleNotFound(long id) => AppLocalization.Text("result.roleNotFound", $"Role with ID {id} not found.").Replace("{id}", id.ToString());
88
+ public static string AccessCodesCannotBeNull => AppLocalization.Text("result.accessCodesCannotBeNull", "Access codes cannot be null.");
89
+ public static string InvalidAccessCodes(string ids) => AppLocalization.Text("result.invalidAccessCodes", $"The following access codes are invalid or deleted: {ids}").Replace("{ids}", ids);
90
 
91
  // User
92
  public const string FillAllFields = "Please fill in all required fields.";
93
  public const string FailedToUpdateUser = "Failed to update user.";
94
  public const string PasswordMinLength = "Password must be at least 6 characters.";
95
  public const string FailedToCreateUser = "Failed to create user.";
96
+ public static string UserNotFound(long id) => AppLocalization.Text("result.userNotFound", $"User with ID {id} not found.").Replace("{id}", id.ToString());
97
 
98
  // Register
99
  public const string ParseRegistrationResponseFailed = "Failed to parse registration response.";
TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor CHANGED
@@ -7,7 +7,7 @@
7
  <button type="button"
8
  class="sidebar-scrim"
9
  onclick="document.body.classList.remove('tts-sidebar-open');"
10
- aria-label="Close navigation"></button>
11
 
12
  <aside class="app-sidebar fixed inset-y-0 left-0 z-[60] flex w-72 -translate-x-full flex-col overflow-hidden border-r border-slate-800 bg-slate-950 text-white transition-transform duration-200 lg:static lg:z-auto lg:min-h-screen lg:translate-x-0 lg:shrink-0">
13
  <div class="app-sidebar-brand flex min-h-20 items-center gap-3 px-5 py-4">
@@ -16,12 +16,12 @@
16
  </span>
17
  <span class="app-sidebar-title flex flex-col leading-tight">
18
  <strong class="text-lg font-bold tracking-normal text-white">Taskify</strong>
19
- <span class="mt-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">Workspace</span>
20
  </span>
21
  <button type="button"
22
  class="control-button mobile-sidebar-close ml-auto inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/10 text-white hover:bg-white/15 hover:text-white lg:hidden"
23
  onclick="document.body.classList.remove('tts-sidebar-open');"
24
- aria-label="Close navigation">
25
  <i data-lucide="x" class="h-4 w-4"></i>
26
  </button>
27
  </div>
@@ -64,7 +64,7 @@
64
  <button type="button"
65
  class="control-button mobile-sidebar-toggle inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 hover:text-blue-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white lg:hidden"
66
  onclick="document.body.classList.add('tts-sidebar-open');"
67
- aria-label="Open navigation">
68
  <i data-lucide="menu" class="h-4 w-4"></i>
69
  </button>
70
  <div class="app-topbar-title min-w-0">
@@ -151,56 +151,61 @@
151
  if (CurrentRoute.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) &&
152
  CurrentRoute.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase))
153
  {
154
- return "Kanban Board";
155
  }
156
 
157
  if (CurrentRoute.StartsWith("tasks/", StringComparison.OrdinalIgnoreCase))
158
  {
159
- return "Task Details";
160
  }
161
 
162
  return CurrentRoute switch
163
  {
164
- "" or "dashboard" or "Home" => "Dashboard",
165
- "dashboard/manager" => "Manager Dashboard",
166
- "dashboard/employee" => "Employee Dashboard",
167
- "projects" or "projects/all" => "Projects",
168
- "projects/assign" => "Project Assignment",
169
- "tasks" or "tasks/all" => "Tasks",
170
- "board" => "Kanban Board",
171
- "tasks/assign" => "Task Assignment",
172
- "issues" or "issues/all" => "Issues",
173
- "issues/add" => "Add Issue",
174
- "reports/tasks" => "Task Report",
175
- "reports/time" => "Time Tracking",
176
- "reports/projects" => "Project Progress",
177
- "reports/overdue" => "Overdue Tasks",
178
- "reports/employees" => "Employee Report",
179
- "roles" => "Roles & Access",
180
- "users" => "Users",
181
- "audit-logs" => "Audit Logs",
182
- _ => Language.Texts.Workspace
183
  };
184
  }
185
  }
186
 
187
  private string CurrentPageDescription => CurrentPageTitle switch
188
  {
189
- "Dashboard" => "A quick overview of work, risk, and progress.",
190
- "Manager Dashboard" => "Track projects, people, and delivery health.",
191
- "Employee Dashboard" => "Focus on your tasks, due dates, and progress.",
192
- "Projects" => "Manage project plans, timelines, and assignments.",
193
- "Project Assignment" => "Assign team members to the right projects.",
194
- "Tasks" => "Review, filter, and update work items.",
195
- "Kanban Board" => "Move work through status columns.",
196
- "Task Assignment" => "Assign tasks to project members.",
197
- "Issues" => "Track blockers, notes, and daily work progress.",
198
- "Add Issue" => "Record a new issue against a task.",
199
- "Roles & Access" => "Control user roles, menus, and permissions.",
200
- "Users" => "Manage people, status, and account details.",
201
- "Audit Logs" => "Review system activity and changes.",
202
- _ when CurrentPageTitle.Contains("Report", StringComparison.OrdinalIgnoreCase) => "Analyze work data and export reports.",
203
- _ => "Workspace"
 
 
 
 
 
204
  };
205
 
206
  protected override async Task OnInitializedAsync()
 
7
  <button type="button"
8
  class="sidebar-scrim"
9
  onclick="document.body.classList.remove('tts-sidebar-open');"
10
+ aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")"></button>
11
 
12
  <aside class="app-sidebar fixed inset-y-0 left-0 z-[60] flex w-72 -translate-x-full flex-col overflow-hidden border-r border-slate-800 bg-slate-950 text-white transition-transform duration-200 lg:static lg:z-auto lg:min-h-screen lg:translate-x-0 lg:shrink-0">
13
  <div class="app-sidebar-brand flex min-h-20 items-center gap-3 px-5 py-4">
 
16
  </span>
17
  <span class="app-sidebar-title flex flex-col leading-tight">
18
  <strong class="text-lg font-bold tracking-normal text-white">Taskify</strong>
19
+ <span class="mt-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">@AppLocalization.Text("common.workspace", "Workspace")</span>
20
  </span>
21
  <button type="button"
22
  class="control-button mobile-sidebar-close ml-auto inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/10 text-white hover:bg-white/15 hover:text-white lg:hidden"
23
  onclick="document.body.classList.remove('tts-sidebar-open');"
24
+ aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")">
25
  <i data-lucide="x" class="h-4 w-4"></i>
26
  </button>
27
  </div>
 
64
  <button type="button"
65
  class="control-button mobile-sidebar-toggle inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 hover:text-blue-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white lg:hidden"
66
  onclick="document.body.classList.add('tts-sidebar-open');"
67
+ aria-label="@AppLocalization.Text("common.openNavigation", "Open navigation")">
68
  <i data-lucide="menu" class="h-4 w-4"></i>
69
  </button>
70
  <div class="app-topbar-title min-w-0">
 
151
  if (CurrentRoute.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) &&
152
  CurrentRoute.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase))
153
  {
154
+ return AppLocalization.PageTitle("kanbanBoard", "Kanban Board");
155
  }
156
 
157
  if (CurrentRoute.StartsWith("tasks/", StringComparison.OrdinalIgnoreCase))
158
  {
159
+ return AppLocalization.PageTitle("taskDetails", "Task Details");
160
  }
161
 
162
  return CurrentRoute switch
163
  {
164
+ "" or "dashboard" or "Home" => AppLocalization.PageTitle("dashboard", "Dashboard"),
165
+ "dashboard/manager" => AppLocalization.PageTitle("managerDashboard", "Manager Dashboard"),
166
+ "dashboard/employee" => AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard"),
167
+ "projects" or "projects/all" => AppLocalization.PageTitle("projects", "Projects"),
168
+ "projects/assign" => AppLocalization.PageTitle("projectAssignment", "Project Assignment"),
169
+ "tasks" or "tasks/all" => AppLocalization.PageTitle("tasks", "Tasks"),
170
+ "board" => AppLocalization.PageTitle("kanbanBoard", "Kanban Board"),
171
+ "tasks/assign" => AppLocalization.PageTitle("taskAssignment", "Task Assignment"),
172
+ "issues" or "issues/all" => AppLocalization.PageTitle("issues", "Issues"),
173
+ "issues/add" => AppLocalization.PageTitle("addIssue", "Add Issue"),
174
+ "reports/tasks" => AppLocalization.PageTitle("taskReport", "Task Report"),
175
+ "reports/time" => AppLocalization.PageTitle("timeTracking", "Time Tracking"),
176
+ "reports/projects" => AppLocalization.PageTitle("projectProgress", "Project Progress"),
177
+ "reports/overdue" => AppLocalization.PageTitle("overdueTasks", "Overdue Tasks"),
178
+ "reports/employees" => AppLocalization.PageTitle("employeeReport", "Employee Report"),
179
+ "roles" => AppLocalization.PageTitle("rolesAccess", "Roles & Access"),
180
+ "users" => AppLocalization.PageTitle("users", "Users"),
181
+ "audit-logs" => AppLocalization.PageTitle("auditLogs", "Audit Logs"),
182
+ _ => AppLocalization.Text("common.workspace", "Workspace")
183
  };
184
  }
185
  }
186
 
187
  private string CurrentPageDescription => CurrentPageTitle switch
188
  {
189
+ var title when title == AppLocalization.PageTitle("dashboard", "Dashboard") => AppLocalization.PageDescription("dashboard", "A quick overview of work, risk, and progress."),
190
+ var title when title == AppLocalization.PageTitle("managerDashboard", "Manager Dashboard") => AppLocalization.PageDescription("managerDashboard", "Track projects, people, and delivery health."),
191
+ var title when title == AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard") => AppLocalization.PageDescription("employeeDashboard", "Focus on your tasks, due dates, and progress."),
192
+ var title when title == AppLocalization.PageTitle("projects", "Projects") => AppLocalization.PageDescription("projects", "Manage project plans, timelines, and assignments."),
193
+ var title when title == AppLocalization.PageTitle("projectAssignment", "Project Assignment") => AppLocalization.PageDescription("projectAssignment", "Assign team members to the right projects."),
194
+ var title when title == AppLocalization.PageTitle("tasks", "Tasks") => AppLocalization.PageDescription("tasks", "Review, filter, and update work items."),
195
+ var title when title == AppLocalization.PageTitle("kanbanBoard", "Kanban Board") => AppLocalization.PageDescription("kanbanBoard", "Move work through status columns."),
196
+ var title when title == AppLocalization.PageTitle("taskAssignment", "Task Assignment") => AppLocalization.PageDescription("taskAssignment", "Assign tasks to project members."),
197
+ var title when title == AppLocalization.PageTitle("issues", "Issues") => AppLocalization.PageDescription("issues", "Track blockers, notes, and daily work progress."),
198
+ var title when title == AppLocalization.PageTitle("addIssue", "Add Issue") => AppLocalization.PageDescription("addIssue", "Record a new issue against a task."),
199
+ var title when title == AppLocalization.PageTitle("rolesAccess", "Roles & Access") => AppLocalization.PageDescription("rolesAccess", "Control user roles, menus, and permissions."),
200
+ var title when title == AppLocalization.PageTitle("users", "Users") => AppLocalization.PageDescription("users", "Manage people, status, and account details."),
201
+ var title when title == AppLocalization.PageTitle("auditLogs", "Audit Logs") => AppLocalization.PageDescription("auditLogs", "Review system activity and changes."),
202
+ var title when title.Contains(AppLocalization.Text("page.taskReport", "Task Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.taskReport.desc", "Analyze work data and export reports."),
203
+ var title when title.Contains(AppLocalization.Text("page.timeTracking", "Time Tracking"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.timeTracking.desc", "Review effort by task, project, and assignee."),
204
+ var title when title.Contains(AppLocalization.Text("page.projectProgress", "Project Progress"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.projectProgress.desc", "Monitor delivery health across active projects."),
205
+ var title when title.Contains(AppLocalization.Text("page.overdueTasks", "Overdue Tasks"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.overdueTasks.desc", "Focus on delayed work that needs attention."),
206
+ var title when title.Contains(AppLocalization.Text("page.employeeReport", "Employee Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.employeeReport.desc", "See how work is distributed across people."),
207
+ var title when title.Contains(AppLocalization.Text("page.issueReport", "Issue Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.issueReport.desc", "Track daily execution, backlog health, and issue-level effort."),
208
+ _ => AppLocalization.Text("common.workspace", "Workspace")
209
  };
210
 
211
  protected override async Task OnInitializedAsync()
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor CHANGED
@@ -21,7 +21,7 @@
21
  <button class="nav-dropdown-toggle flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-semibold text-slate-300 transition-colors hover:bg-white/10 hover:text-white @(IsMenuGroupExpanded(menu.MenuCode) ? "expanded bg-white/10 text-white" : "")"
22
  @onclick="() => ToggleMenuGroup(menu.MenuCode)">
23
  @RenderIcon(menu.Icon)
24
- <span class="flex-1 text-left">@menu.MenuName</span>
25
  <i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
26
  </button>
27
  @if (IsMenuGroupExpanded(menu.MenuCode))
@@ -33,7 +33,7 @@
33
  href="@MenuAuthorizationService.NormalizeMenuHref(sub.MenuUrl)"
34
  Match="@GetNavLinkMatch(sub.MenuUrl)">
35
  @RenderSmallIcon(sub.Icon)
36
- <span>@sub.MenuName</span>
37
  </NavLink>
38
  }
39
  </div>
@@ -45,7 +45,7 @@
45
  href="@MenuAuthorizationService.NormalizeMenuHref(menu.MenuUrl)"
46
  Match="@(string.IsNullOrWhiteSpace(menu.MenuUrl) || menu.MenuUrl.Trim('/') is "dashboard" or "" ? NavLinkMatch.All : NavLinkMatch.Prefix)">
47
  @RenderIcon(menu.Icon)
48
- <span>@menu.MenuName</span>
49
  </NavLink>
50
  }
51
  }
@@ -76,9 +76,9 @@
76
 
77
  if (user.Identity?.IsAuthenticated == true)
78
  {
79
- var rawMenus = await MenuAuthorization.GetUserMenusAsync(user);
80
- menus = ProcessMenus(rawMenus);
81
- BuildUrlHierarchy(menus);
82
 
83
  foreach (var menu in menus)
84
  {
@@ -110,10 +110,10 @@
110
  {
111
  return rawMenus
112
  .Where(menu => !string.Equals(menu.MenuCode, "ROLE_LAYOUTS", StringComparison.OrdinalIgnoreCase) &&
113
- !string.Equals(menu.MenuName, "Role Layouts", StringComparison.OrdinalIgnoreCase))
114
  .OrderBy(menu => string.Equals(menu.MenuCode, "DASHBOARD", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
115
  .ThenBy(menu => menu.OrderNo)
116
- .ThenBy(menu => menu.MenuName)
117
  .ToList();
118
  }
119
 
 
21
  <button class="nav-dropdown-toggle flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-semibold text-slate-300 transition-colors hover:bg-white/10 hover:text-white @(IsMenuGroupExpanded(menu.MenuCode) ? "expanded bg-white/10 text-white" : "")"
22
  @onclick="() => ToggleMenuGroup(menu.MenuCode)">
23
  @RenderIcon(menu.Icon)
24
+ <span class="flex-1 text-left">@AppLocalization.Menu(menu.MenuCode, menu.MenuName)</span>
25
  <i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
26
  </button>
27
  @if (IsMenuGroupExpanded(menu.MenuCode))
 
33
  href="@MenuAuthorizationService.NormalizeMenuHref(sub.MenuUrl)"
34
  Match="@GetNavLinkMatch(sub.MenuUrl)">
35
  @RenderSmallIcon(sub.Icon)
36
+ <span>@AppLocalization.Menu(sub.MenuCode, sub.MenuName)</span>
37
  </NavLink>
38
  }
39
  </div>
 
45
  href="@MenuAuthorizationService.NormalizeMenuHref(menu.MenuUrl)"
46
  Match="@(string.IsNullOrWhiteSpace(menu.MenuUrl) || menu.MenuUrl.Trim('/') is "dashboard" or "" ? NavLinkMatch.All : NavLinkMatch.Prefix)">
47
  @RenderIcon(menu.Icon)
48
+ <span>@AppLocalization.Menu(menu.MenuCode, menu.MenuName)</span>
49
  </NavLink>
50
  }
51
  }
 
76
 
77
  if (user.Identity?.IsAuthenticated == true)
78
  {
79
+ var rawMenus = await MenuAuthorization.GetUserMenusAsync(user);
80
+ menus = ProcessMenus(rawMenus);
81
+ BuildUrlHierarchy(menus);
82
 
83
  foreach (var menu in menus)
84
  {
 
110
  {
111
  return rawMenus
112
  .Where(menu => !string.Equals(menu.MenuCode, "ROLE_LAYOUTS", StringComparison.OrdinalIgnoreCase) &&
113
+ !string.Equals(menu.MenuCode, "DASHBOARD_WIDGETS", StringComparison.OrdinalIgnoreCase))
114
  .OrderBy(menu => string.Equals(menu.MenuCode, "DASHBOARD", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
115
  .ThenBy(menu => menu.OrderNo)
116
+ .ThenBy(menu => AppLocalization.Menu(menu.MenuCode, menu.MenuName))
117
  .ToList();
118
  }
119
 
TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor CHANGED
@@ -9,14 +9,14 @@
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
 
@@ -24,23 +24,23 @@
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
  {
@@ -50,11 +50,11 @@
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>
@@ -101,7 +101,7 @@
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>
 
9
  @inject IJSRuntime JS
10
  @inject AuthenticationStateProvider AuthStateProvider
11
 
12
+ <PageTitle>@AppLocalization.PageTitle("auditLogs", "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">@AppLocalization.PageTitle("auditLogs", "Audit Logs")</h2>
19
+ <p class="text-slate-500 mt-1">@AppLocalization.PageDescription("auditLogs", "Review system activity and changes.")</p>
20
  </div>
21
  </div>
22
 
 
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="@AppLocalization.Text("common.searchByUser", "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
+ @AppLocalization.Text("action.search", "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
+ @AppLocalization.Text("action.reset", "Reset")
37
  </button>
38
  </div>
39
  </div>
40
 
41
  @if (isLoading)
42
  {
43
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingAuditLogs", "Loading audit logs...")" />
44
  }
45
  else
46
  {
 
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">@AppLocalization.Text("common.date", "Timestamp")</th>
54
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.user", "User")</th>
55
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.module", "Module")</th>
56
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Action")</th>
57
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.description", "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>
 
101
  }
102
  else
103
  {
104
+ <EmptyState ColSpan="7" Icon="history" Title="@AppLocalization.Text("common.noResults", "No audit logs")" Subtitle="@AppLocalization.Text("common.noResults", "No security events or administrator operations were found.")" />
105
  }
106
  </tbody>
107
  </table>
TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Login.razor CHANGED
@@ -7,7 +7,7 @@
7
  @inject MenuAuthorizationService MenuAuthorization
8
  @inject IJSRuntime JS
9
 
10
- <PageTitle>Sign In - Taskify</PageTitle>
11
 
12
  <div class="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 sm:px-6 lg:px-8">
13
  <div class="w-full max-w-md space-y-7 rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
@@ -17,8 +17,8 @@
17
  <i data-lucide="flame" class="h-6 w-6 fill-current"></i>
18
  </span>
19
  </div>
20
- <h2 class="mt-5 text-center text-2xl font-bold text-slate-900">Sign in to Taskify</h2>
21
- <p class="mt-2 text-center text-sm text-slate-500">Continue to your work dashboard.</p>
22
  </div>
23
 
24
  <form method="post" action="/account/login" class="space-y-5">
@@ -27,8 +27,8 @@
27
 
28
  <div class="space-y-4">
29
  <div>
30
- <label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">Username or Email</label>
31
- <input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="Enter username or email"
32
  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-slate-900 focus:border-transparent transition-all" />
33
  </div>
34
  <div class="relative">
@@ -45,49 +45,49 @@
45
  <div class="flex items-center">
46
  <input id="rememberMe" name="RememberMe" type="checkbox" value="true"
47
  class="h-4 w-4 rounded border-slate-350 text-slate-900 focus:ring-slate-900 focus:outline-none" />
48
- <label for="rememberMe" class="ml-2 block text-sm text-slate-600 font-medium">Remember me</label>
49
  </div>
50
  </div>
51
 
52
  @if (ShowUnavailableError)
53
  {
54
  <div class="p-3 rounded-lg bg-amber-50 border border-amber-100">
55
- <p class="text-sm text-amber-800 text-center">Unable to reach the API server. Start <strong>TaskTrackingSystem.WebApi</strong> and try again.</p>
56
  </div>
57
  }
58
  else if (ShowError)
59
  {
60
  <div class="p-3 rounded-lg bg-red-50 border border-red-100">
61
- <p class="text-sm text-red-600 text-center">Invalid login credentials.</p>
62
  </div>
63
  }
64
 
65
  @if (ShowLoggedOutMessage)
66
  {
67
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
68
- <p class="text-sm text-emerald-700 text-center">You have been signed out successfully.</p>
69
  </div>
70
  }
71
  else if (ShowResetMessage)
72
  {
73
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
74
- <p class="text-sm text-emerald-700 text-center">Password updated successfully. Please sign in with your new password.</p>
75
  </div>
76
  }
77
 
78
  <p class="text-center text-sm text-slate-600">
79
- Need an account?
80
- <a href="/register" class="font-medium text-slate-900 hover:underline">Register</a>
81
  </p>
82
 
83
  <div>
84
  <button type="submit" class="group relative flex w-full justify-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700">
85
- Sign In
86
  </button>
87
  </div>
88
 
89
  <p class="text-center text-sm text-slate-600">
90
- <a href="/reset-password" class="font-medium text-slate-900 hover:underline">Reset password</a>
91
  </p>
92
  </form>
93
  </div>
 
7
  @inject MenuAuthorizationService MenuAuthorization
8
  @inject IJSRuntime JS
9
 
10
+ <PageTitle>@AppLocalization.Text("common.signIn", "Sign In") - Taskify</PageTitle>
11
 
12
  <div class="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 sm:px-6 lg:px-8">
13
  <div class="w-full max-w-md space-y-7 rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
 
17
  <i data-lucide="flame" class="h-6 w-6 fill-current"></i>
18
  </span>
19
  </div>
20
+ <h2 class="mt-5 text-center text-2xl font-bold text-slate-900">@AppLocalization.Text("common.signIn", "Sign In") - Taskify</h2>
21
+ <p class="mt-2 text-center text-sm text-slate-500">@AppLocalization.PageDescription("dashboard", "Continue to your work dashboard.")</p>
22
  </div>
23
 
24
  <form method="post" action="/account/login" class="space-y-5">
 
27
 
28
  <div class="space-y-4">
29
  <div>
30
+ <label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">@AppLocalization.Text("common.user", "Username or Email")</label>
31
+ <input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="@AppLocalization.Text("common.searchPlaceholder", "Enter username or email")"
32
  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-slate-900 focus:border-transparent transition-all" />
33
  </div>
34
  <div class="relative">
 
45
  <div class="flex items-center">
46
  <input id="rememberMe" name="RememberMe" type="checkbox" value="true"
47
  class="h-4 w-4 rounded border-slate-350 text-slate-900 focus:ring-slate-900 focus:outline-none" />
48
+ <label for="rememberMe" class="ml-2 block text-sm text-slate-600 font-medium">@AppLocalization.Text("common.rememberMe", "Remember me")</label>
49
  </div>
50
  </div>
51
 
52
  @if (ShowUnavailableError)
53
  {
54
  <div class="p-3 rounded-lg bg-amber-50 border border-amber-100">
55
+ <p class="text-sm text-amber-800 text-center">@AppLocalization.Text("result.apiUnavailable", "Unable to reach the API server. Start TaskTrackingSystem.WebApi and try again.")</p>
56
  </div>
57
  }
58
  else if (ShowError)
59
  {
60
  <div class="p-3 rounded-lg bg-red-50 border border-red-100">
61
+ <p class="text-sm text-red-600 text-center">@ResultMessages.InvalidCredentials</p>
62
  </div>
63
  }
64
 
65
  @if (ShowLoggedOutMessage)
66
  {
67
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
68
+ <p class="text-sm text-emerald-700 text-center">@AppLocalization.Text("result.signedOut", "You have been signed out successfully.")</p>
69
  </div>
70
  }
71
  else if (ShowResetMessage)
72
  {
73
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
74
+ <p class="text-sm text-emerald-700 text-center">@AppLocalization.Text("result.passwordUpdated", "Password updated successfully. Please sign in with your new password.")</p>
75
  </div>
76
  }
77
 
78
  <p class="text-center text-sm text-slate-600">
79
+ @AppLocalization.Text("auth.needAccount", "Need an account?")
80
+ <a href="/register" class="font-medium text-slate-900 hover:underline">@AppLocalization.Text("auth.register", "Register")</a>
81
  </p>
82
 
83
  <div>
84
  <button type="submit" class="group relative flex w-full justify-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700">
85
+ @AppLocalization.Text("common.signIn", "Sign In")
86
  </button>
87
  </div>
88
 
89
  <p class="text-center text-sm text-slate-600">
90
+ <a href="/reset-password" class="font-medium text-slate-900 hover:underline">@AppLocalization.Text("auth.resetPassword", "Reset password")</a>
91
  </p>
92
  </form>
93
  </div>
TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/ResetPassword.razor CHANGED
@@ -5,7 +5,7 @@
5
  @inject NavigationManager Navigation
6
  @inject IJSRuntime JS
7
 
8
- <PageTitle>Reset Password - Taskify</PageTitle>
9
 
10
  <div class="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 sm:px-6 lg:px-8">
11
  <div class="w-full max-w-md space-y-7 rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
@@ -15,9 +15,9 @@
15
  <i data-lucide="flame" class="h-6 w-6 fill-current"></i>
16
  </span>
17
  </div>
18
- <h2 class="mt-5 text-center text-2xl font-bold text-slate-900">Reset your password</h2>
19
  <p class="mt-2 text-center text-sm text-slate-600">
20
- Use the recovery code configured in the API to set a new password.
21
  </p>
22
  </div>
23
 
@@ -26,18 +26,18 @@
26
 
27
  <div class="space-y-4">
28
  <div>
29
- <label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">Username or Email</label>
30
- <input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="Enter username or email"
31
  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-slate-900 focus:border-transparent transition-all" />
32
  </div>
33
  <div>
34
- <label for="recoveryCode" class="block text-sm font-medium text-slate-700 mb-1">Recovery Code</label>
35
- <input id="recoveryCode" name="RecoveryCode" required placeholder="Enter recovery code"
36
  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-slate-900 focus:border-transparent transition-all" />
37
  </div>
38
  <div>
39
- <label for="newPassword" class="block text-sm font-medium text-slate-700 mb-1">New Password</label>
40
- <input id="newPassword" name="NewPassword" type="password" required placeholder="Min 8 characters"
41
  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-slate-900 focus:border-transparent transition-all" />
42
  </div>
43
  </div>
@@ -50,9 +50,9 @@
50
  }
51
 
52
  <div class="flex items-center justify-between">
53
- <a href="/login" class="text-sm font-medium text-violet-600 hover:underline">Back to sign in</a>
54
  <button type="submit" class="group relative flex justify-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700">
55
- Reset Password
56
  </button>
57
  </div>
58
  </form>
@@ -64,7 +64,7 @@
64
  private string? Error { get; set; }
65
 
66
  private bool ShowError => !string.IsNullOrWhiteSpace(Error);
67
- private string ErrorMessage => string.IsNullOrWhiteSpace(Error) ? "Unable to reset password." : Error;
68
 
69
  protected override async Task OnAfterRenderAsync(bool firstRender)
70
  {
 
5
  @inject NavigationManager Navigation
6
  @inject IJSRuntime JS
7
 
8
+ <PageTitle>@AppLocalization.Text("auth.resetPassword", "Reset Password") - Taskify</PageTitle>
9
 
10
  <div class="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 sm:px-6 lg:px-8">
11
  <div class="w-full max-w-md space-y-7 rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
 
15
  <i data-lucide="flame" class="h-6 w-6 fill-current"></i>
16
  </span>
17
  </div>
18
+ <h2 class="mt-5 text-center text-2xl font-bold text-slate-900">@AppLocalization.Text("auth.resetPassword", "Reset your password")</h2>
19
  <p class="mt-2 text-center text-sm text-slate-600">
20
+ @AppLocalization.Text("auth.resetPasswordHelp", "Use the recovery code configured in the API to set a new password.")
21
  </p>
22
  </div>
23
 
 
26
 
27
  <div class="space-y-4">
28
  <div>
29
+ <label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">@AppLocalization.Text("auth.usernameOrEmail", "Username or Email")</label>
30
+ <input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="@AppLocalization.Text("auth.enterUsernameOrEmail", "Enter username or email")"
31
  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-slate-900 focus:border-transparent transition-all" />
32
  </div>
33
  <div>
34
+ <label for="recoveryCode" class="block text-sm font-medium text-slate-700 mb-1">@AppLocalization.Text("auth.recoveryCode", "Recovery Code")</label>
35
+ <input id="recoveryCode" name="RecoveryCode" required placeholder="@AppLocalization.Text("auth.enterRecoveryCode", "Enter recovery code")"
36
  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-slate-900 focus:border-transparent transition-all" />
37
  </div>
38
  <div>
39
+ <label for="newPassword" class="block text-sm font-medium text-slate-700 mb-1">@AppLocalization.Text("auth.newPassword", "New Password")</label>
40
+ <input id="newPassword" name="NewPassword" type="password" required placeholder="@AppLocalization.Text("validation.passwordMinLengthRule", "Min 8 characters")"
41
  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-slate-900 focus:border-transparent transition-all" />
42
  </div>
43
  </div>
 
50
  }
51
 
52
  <div class="flex items-center justify-between">
53
+ <a href="/login" class="text-sm font-medium text-violet-600 hover:underline">@AppLocalization.Text("common.backToDashboard", "Back to sign in")</a>
54
  <button type="submit" class="group relative flex justify-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700">
55
+ @AppLocalization.Text("auth.resetPassword", "Reset Password")
56
  </button>
57
  </div>
58
  </form>
 
64
  private string? Error { get; set; }
65
 
66
  private bool ShowError => !string.IsNullOrWhiteSpace(Error);
67
+ private string ErrorMessage => string.IsNullOrWhiteSpace(Error) ? AppLocalization.Text("auth.unableToResetPassword", "Unable to reset password.") : Error;
68
 
69
  protected override async Task OnAfterRenderAsync(bool firstRender)
70
  {
TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor CHANGED
@@ -15,30 +15,30 @@
15
  @inject MenuAuthorizationService MenuAuthorization
16
  @inject NavigationManager Navigation
17
 
18
- <PageTitle>Add Issue</PageTitle>
19
 
20
  <SuccessAlert Message="@successMessage" />
21
 
22
  <div class="space-y-6">
23
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
24
  <div>
25
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Add Issue</h2>
26
- <p class="text-slate-500 mt-1">Create a one-day work issue for a task and record the hours you spent.</p>
27
  </div>
28
  </div>
29
 
30
  @if (isLoading)
31
  {
32
- <LoadingSpinner Message="Loading tasks, projects, and team members..." />
33
  }
34
  else
35
  {
36
  <div class="w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
37
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
38
  <div class="md:col-span-2">
39
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Task <span class="text-red-500">*</span></label>
40
  <select value="@formTaskId" @onchange="OnTaskChanged" class="w-full rounded-lg border @(isTaskError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-violet-600") px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:border-transparent transition-all">
41
- <option value="0">Select task...</option>
42
  @foreach (var task in tasks)
43
  {
44
  <option value="@task.Id">@task.Title</option>
@@ -51,8 +51,8 @@
51
  </div>
52
 
53
  <div class="md:col-span-2">
54
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Issue Title <span class="text-red-500">*</span></label>
55
- <input @bind="formTitle" type="text" placeholder="What did you work on?"
56
  class="w-full rounded-lg border @(isTitleError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-violet-600") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
57
  @if (isTitleError)
58
  {
@@ -61,94 +61,94 @@
61
  </div>
62
 
63
  <div class="md:col-span-2">
64
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Description</label>
65
- <textarea @bind="formDescription" rows="4" placeholder="Add a short note about the work you did..."
66
  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"></textarea>
67
  </div>
68
 
69
  <div>
70
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Estimated Hours</label>
71
  <input @bind="formEstimatedHours" type="number" step="0.25" min="0"
72
  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"
73
- placeholder="e.g. 4" />
74
  </div>
75
 
76
  <div>
77
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Actual Hours</label>
78
  <input @bind="formActualHours" type="number" step="0.25" min="0" max="7"
79
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"
80
- placeholder="e.g. 7" />
81
- <p class="mt-1 text-xs text-slate-500">Use the total hours you spent today. For your standard day, this is usually 7 hours.</p>
82
  </div>
83
 
84
  <div>
85
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Start Date</label>
86
  <input @bind="formStartDate" type="date"
87
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
88
  </div>
89
 
90
  <div>
91
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Due Date</label>
92
  <input @bind="formDueDate" type="date"
93
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
94
  </div>
95
 
96
  <div>
97
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Status</label>
98
  <select @bind="formStatusId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
99
- <option value="@AppTaskStatus.Todo">To Do</option>
100
- <option value="@AppTaskStatus.InProgress">In Progress</option>
101
- <option value="@AppTaskStatus.Done">Done</option>
102
  </select>
103
  </div>
104
 
105
  <div>
106
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Priority</label>
107
  <select @bind="formPriorityId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
108
- <option value="@TaskPriority.Low">Low</option>
109
- <option value="@TaskPriority.Medium">Medium</option>
110
- <option value="@TaskPriority.High">High</option>
111
  </select>
112
  </div>
113
 
114
  <div>
115
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked Status</label>
116
  <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
117
  <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
118
- <span>Blocked</span>
119
  </label>
120
  </div>
121
 
122
  <div>
123
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Escalation Level</label>
124
  <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
125
- <option value="0">None</option>
126
- <option value="1">Team Lead</option>
127
- <option value="2">PM</option>
128
- <option value="3">Manager</option>
129
  </select>
130
  </div>
131
 
132
  <div>
133
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked By</label>
134
- <input @bind="formBlockedBy" type="text" placeholder="Dependency, person, or team"
135
  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" />
136
  </div>
137
 
138
  <div>
139
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Delay Reason</label>
140
- <input @bind="formDelayReason" type="text" placeholder="Why is this issue delayed?"
141
  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" />
142
  </div>
143
 
144
  <div>
145
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Project</label>
146
  <input value="@SelectedProjectLabel" readonly
147
  class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 focus:outline-none" />
148
  </div>
149
 
150
  <div>
151
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Assignee</label>
152
  <div class="relative">
153
  <button type="button"
154
  @onclick="ToggleAssigneeDropdown"
@@ -164,7 +164,7 @@
164
  <button type="button"
165
  @onclick="CloseAssigneeDropdown"
166
  class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
167
- aria-label="Close assignee dropdown">
168
  <i data-lucide="x" class="h-4 w-4"></i>
169
  </button>
170
  </div>
@@ -172,7 +172,7 @@
172
  <input @bind="assigneeSearchInput"
173
  @onfocus="EnsureAssigneeDropdownOpen"
174
  type="text"
175
- placeholder="Search project members..."
176
  class="w-full rounded-xl border border-slate-200 bg-white py-2 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent" />
177
  <i data-lucide="search" class="absolute left-3 top-2.5 h-4 w-4 text-slate-400"></i>
178
  </div>
@@ -180,7 +180,7 @@
180
  @if (isLoadingProjectMembers)
181
  {
182
  <div class="mt-3 rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
183
- Loading project members...
184
  </div>
185
  }
186
  else
@@ -193,7 +193,7 @@
193
  <i data-lucide="user-minus" class="h-4 w-4"></i>
194
  </span>
195
  <div class="min-w-0 flex-1">
196
- <p class="text-sm font-semibold text-slate-900">Unassigned</p>
197
  </div>
198
  </button>
199
 
@@ -216,7 +216,7 @@
216
  @if (!FilteredProjectMembers.Any())
217
  {
218
  <div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
219
- No members match the current search.
220
  </div>
221
  }
222
  </div>
@@ -229,10 +229,10 @@
229
 
230
  <div class="mt-6 flex items-center justify-end gap-3">
231
  <button @onclick="ResetForm" 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">
232
- Reset
233
  </button>
234
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
235
- @(isSaving ? "Saving..." : "Add Issue")
236
  </button>
237
  </div>
238
 
@@ -245,9 +245,9 @@
245
  </div>
246
 
247
  <ConfirmDialog IsVisible="showConfirmDialog"
248
- Title="Add Issue?"
249
- Message="Are you sure you want to save this issue?"
250
- ConfirmText="Yes, Add"
251
  Icon="circle-plus"
252
  IconBgClass="bg-emerald-50"
253
  IconTextClass="text-emerald-600"
 
15
  @inject MenuAuthorizationService MenuAuthorization
16
  @inject NavigationManager Navigation
17
 
18
+ <PageTitle>@AppLocalization.PageTitle("addIssue", "Add Issue")</PageTitle>
19
 
20
  <SuccessAlert Message="@successMessage" />
21
 
22
  <div class="space-y-6">
23
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
24
  <div>
25
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("common.addIssue", "Add Issue")</h2>
26
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("issue.addIssueDescription", "Create a one-day work issue for a task and record the hours you spent.")</p>
27
  </div>
28
  </div>
29
 
30
  @if (isLoading)
31
  {
32
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingTasksAndUsers", "Loading tasks and users...")" />
33
  }
34
  else
35
  {
36
  <div class="w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
37
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
38
  <div class="md:col-span-2">
39
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.task", "Task") <span class="text-red-500">*</span></label>
40
  <select value="@formTaskId" @onchange="OnTaskChanged" class="w-full rounded-lg border @(isTaskError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-violet-600") px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:border-transparent transition-all">
41
+ <option value="0">@AppLocalization.Text("common.selectTask", "Select task...")</option>
42
  @foreach (var task in tasks)
43
  {
44
  <option value="@task.Id">@task.Title</option>
 
51
  </div>
52
 
53
  <div class="md:col-span-2">
54
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.issueTitle", "Issue Title") <span class="text-red-500">*</span></label>
55
+ <input @bind="formTitle" type="text" placeholder="@AppLocalization.Text("issue.whatDidYouWorkOn", "What did you work on?")"
56
  class="w-full rounded-lg border @(isTitleError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-violet-600") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
57
  @if (isTitleError)
58
  {
 
61
  </div>
62
 
63
  <div class="md:col-span-2">
64
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.description", "Description")</label>
65
+ <textarea @bind="formDescription" rows="4" placeholder="@AppLocalization.Text("issue.addShortNote", "Add a short note about the work you did...")"
66
  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"></textarea>
67
  </div>
68
 
69
  <div>
70
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.estimatedHours", "Estimated Hours")</label>
71
  <input @bind="formEstimatedHours" type="number" step="0.25" min="0"
72
  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"
73
+ placeholder="@AppLocalization.Text("issue.exampleEstimatedHours", "e.g. 4")" />
74
  </div>
75
 
76
  <div>
77
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.actualHours", "Actual Hours")</label>
78
  <input @bind="formActualHours" type="number" step="0.25" min="0" max="7"
79
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"
80
+ placeholder="@AppLocalization.Text("issue.exampleActualHours", "e.g. 7")" />
81
+ <p class="mt-1 text-xs text-slate-500">@AppLocalization.Text("issue.actualHoursHint", "Use the total hours you spent today. For your standard day, this is usually 7 hours.")</p>
82
  </div>
83
 
84
  <div>
85
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.startDate", "Start Date")</label>
86
  <input @bind="formStartDate" type="date"
87
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
88
  </div>
89
 
90
  <div>
91
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.dueDate", "Due Date")</label>
92
  <input @bind="formDueDate" type="date"
93
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
94
  </div>
95
 
96
  <div>
97
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
98
  <select @bind="formStatusId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
99
+ <option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
100
+ <option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
101
+ <option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
102
  </select>
103
  </div>
104
 
105
  <div>
106
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label>
107
  <select @bind="formPriorityId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
108
+ <option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option>
109
+ <option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option>
110
+ <option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option>
111
  </select>
112
  </div>
113
 
114
  <div>
115
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedStatus", "Blocked Status")</label>
116
  <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
117
  <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
118
+ <span>@AppLocalization.Text("status.blocked", "Blocked")</span>
119
  </label>
120
  </div>
121
 
122
  <div>
123
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.escalationLevel", "Escalation Level")</label>
124
  <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
125
+ <option value="0">@AppLocalization.Text("common.none", "None")</option>
126
+ <option value="1">@AppLocalization.Text("issue.teamLead", "Team Lead")</option>
127
+ <option value="2">@AppLocalization.Text("issue.pm", "PM")</option>
128
+ <option value="3">@AppLocalization.Text("issue.manager", "Manager")</option>
129
  </select>
130
  </div>
131
 
132
  <div>
133
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedBy", "Blocked By")</label>
134
+ <input @bind="formBlockedBy" type="text" placeholder="@AppLocalization.Text("issue.blockedByPlaceholder", "Dependency, person, or team")"
135
  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" />
136
  </div>
137
 
138
  <div>
139
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.delayReason", "Delay Reason")</label>
140
+ <input @bind="formDelayReason" type="text" placeholder="@AppLocalization.Text("issue.delayReasonPlaceholder", "Why is this issue delayed?")"
141
  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" />
142
  </div>
143
 
144
  <div>
145
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.project", "Project")</label>
146
  <input value="@SelectedProjectLabel" readonly
147
  class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 focus:outline-none" />
148
  </div>
149
 
150
  <div>
151
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.assignee", "Assignee")</label>
152
  <div class="relative">
153
  <button type="button"
154
  @onclick="ToggleAssigneeDropdown"
 
164
  <button type="button"
165
  @onclick="CloseAssigneeDropdown"
166
  class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
167
+ aria-label="@AppLocalization.Text("common.close", "Close")">
168
  <i data-lucide="x" class="h-4 w-4"></i>
169
  </button>
170
  </div>
 
172
  <input @bind="assigneeSearchInput"
173
  @onfocus="EnsureAssigneeDropdownOpen"
174
  type="text"
175
+ placeholder="@AppLocalization.Text("common.searchProjectMembers", "Search project members...")"
176
  class="w-full rounded-xl border border-slate-200 bg-white py-2 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent" />
177
  <i data-lucide="search" class="absolute left-3 top-2.5 h-4 w-4 text-slate-400"></i>
178
  </div>
 
180
  @if (isLoadingProjectMembers)
181
  {
182
  <div class="mt-3 rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
183
+ @AppLocalization.Text("common.loadingProjectMembers", "Loading project members...")
184
  </div>
185
  }
186
  else
 
193
  <i data-lucide="user-minus" class="h-4 w-4"></i>
194
  </span>
195
  <div class="min-w-0 flex-1">
196
+ <p class="text-sm font-semibold text-slate-900">@AppLocalization.Text("common.unassigned", "Unassigned")</p>
197
  </div>
198
  </button>
199
 
 
216
  @if (!FilteredProjectMembers.Any())
217
  {
218
  <div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
219
+ @AppLocalization.Text("common.noMembersMatchSearch", "No members match the current search.")
220
  </div>
221
  }
222
  </div>
 
229
 
230
  <div class="mt-6 flex items-center justify-end gap-3">
231
  <button @onclick="ResetForm" 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">
232
+ @AppLocalization.Text("action.reset", "Reset")
233
  </button>
234
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
235
+ @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.addIssue", "Add Issue"))
236
  </button>
237
  </div>
238
 
 
245
  </div>
246
 
247
  <ConfirmDialog IsVisible="showConfirmDialog"
248
+ Title="@AppLocalization.Text("common.addIssueTitle", "Add Issue?")"
249
+ Message="@AppLocalization.Text("issue.saveAddConfirmMessage", "Are you sure you want to save this issue?")"
250
+ ConfirmText="@AppLocalization.Text("issue.yesAdd", "Yes, Add")"
251
  Icon="circle-plus"
252
  IconBgClass="bg-emerald-50"
253
  IconTextClass="text-emerald-600"
TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor CHANGED
@@ -16,40 +16,40 @@
16
  @inject MenuAuthorizationService MenuAuthorization
17
  @inject NavigationManager Navigation
18
 
19
- <PageTitle>Issues</PageTitle>
20
 
21
  <SuccessAlert Message="@successMessage" />
22
 
23
  <div class="space-y-6">
24
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
25
  <div>
26
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Issues</h2>
27
- <p class="text-slate-500 mt-1">Track daily work items, update progress, and review what was done.</p>
28
  </div>
29
  @if (canCreateIssue)
30
  {
31
  <button @onclick="GoToAddIssue" 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">
32
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
33
- New Issue
34
  </button>
35
  }
36
  </div>
37
 
38
  @if (isLoading)
39
  {
40
- <LoadingSpinner Message="Loading issues..." />
41
  }
42
  else
43
  {
44
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
45
  <div class="grid grid-cols-1 lg:grid-cols-6 gap-3">
46
  <div class="lg:col-span-2">
47
- <input @bind="searchInput" type="text" placeholder="Search issues..."
48
  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" />
49
  </div>
50
  <div>
51
  <select @bind="taskIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
- <option value="0">All Tasks</option>
53
  @foreach (var task in tasks)
54
  {
55
  <option value="@task.Id">@task.Title</option>
@@ -58,7 +58,7 @@
58
  </div>
59
  <div>
60
  <select @bind="projectIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
61
- <option value="0">All Projects</option>
62
  @foreach (var project in projects)
63
  {
64
  <option value="@project.Id">@project.Name</option>
@@ -67,18 +67,18 @@
67
  </div>
68
  <div>
69
  <select @bind="statusIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
70
- <option value="">All Statuses</option>
71
- <option value="@AppTaskStatus.Todo">To Do</option>
72
- <option value="@AppTaskStatus.InProgress">In Progress</option>
73
- <option value="@AppTaskStatus.Done">Done</option>
74
  </select>
75
  </div>
76
  <div>
77
  <select @bind="priorityIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
78
- <option value="">All Priorities</option>
79
- <option value="@TaskPriority.Low">Low</option>
80
- <option value="@TaskPriority.Medium">Medium</option>
81
- <option value="@TaskPriority.High">High</option>
82
  </select>
83
  </div>
84
  </div>
@@ -86,15 +86,15 @@
86
  <div class="mt-3 flex flex-wrap items-center gap-3">
87
  <label class="inline-flex items-center gap-2 text-sm text-slate-700">
88
  <input type="checkbox" @bind="assignedOnly" class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
89
- Assigned to me only
90
  </label>
91
  <button @onclick="ApplySearch" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
92
  <i data-lucide="search" class="h-4 w-4 mr-2"></i>
93
- Search
94
  </button>
95
  <button @onclick="ResetSearch" 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">
96
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
97
- Reset
98
  </button>
99
  </div>
100
  </div>
@@ -115,15 +115,15 @@
115
  </colgroup>
116
  <thead>
117
  <tr class="border-b border-slate-100 bg-slate-50/60">
118
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task Code</th>
119
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issue</th>
120
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Hours</th>
121
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
122
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
123
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Dates</th>
124
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
125
- <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Risk</th>
126
- <th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
127
  </tr>
128
  </thead>
129
  <tbody class="divide-y divide-slate-100 text-[13px]">
@@ -134,7 +134,7 @@
134
  var issue = entry.issue;
135
  <tr class="hover:bg-slate-50/50 transition-colors align-top">
136
  <td class="px-5 py-4 text-left whitespace-nowrap">
137
- <span class="font-mono text-[11px] font-semibold tracking-wide text-indigo-700">
138
  @GetTaskCode(issue.TaskId)
139
  </span>
140
  </td>
@@ -144,7 +144,7 @@
144
  </button>
145
  </td>
146
  <td class="px-5 py-4 text-slate-600 whitespace-nowrap">
147
- <span class="font-medium text-slate-700">@(issue.ActualHours?.ToString("0.##") ?? "-") hr</span>
148
  </td>
149
  <td class="px-5 py-4">
150
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">
@@ -157,15 +157,15 @@
157
  </span>
158
  </td>
159
  <td class="px-5 py-4 text-sm text-slate-600 whitespace-nowrap">
160
- <span class="font-medium text-slate-700">Due: @issue.DueDate.ToString("MMM d, yyyy")</span>
161
  </td>
162
  <td class="px-5 py-4 text-sm text-slate-600 break-words">
163
- @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : "Unassigned")
164
  </td>
165
  <td class="px-5 py-4">
166
  @if (issue.IsBlocked)
167
  {
168
- <span class="inline-flex items-center rounded-full bg-rose-50 px-2 py-0.5 text-xs font-semibold text-rose-700">Blocked</span>
169
  }
170
  else if (issue.EscalationLevel > 0)
171
  {
@@ -173,20 +173,20 @@
173
  }
174
  else
175
  {
176
- <span class="text-xs text-slate-400">Clear</span>
177
  }
178
  </td>
179
  <td class="px-5 py-4 text-center">
180
  <div class="flex items-center justify-center gap-1">
181
  @if (canEditIssue)
182
  {
183
- <button @onclick="() => OpenEditModal(issue)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
184
  <i data-lucide="pencil" class="h-4 w-4"></i>
185
  </button>
186
  }
187
  @if (canDeleteIssue)
188
  {
189
- <button @onclick="() => RequestDeleteConfirmation(issue.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
190
  <i data-lucide="trash-2" class="h-4 w-4"></i>
191
  </button>
192
  }
@@ -197,7 +197,7 @@
197
  }
198
  else
199
  {
200
- <EmptyState ColSpan="9" Icon="file-pen-line" Title="No issues yet" Subtitle="Create your first issue to start tracking daily work." />
201
  }
202
  </tbody>
203
  </table>
@@ -219,8 +219,8 @@
219
  <div class="flex max-h-[calc(100vh-2rem)] w-full max-w-4xl flex-col overflow-hidden rounded-2xl bg-white shadow-xl sm:max-h-[calc(100vh-3rem)]">
220
  <div class="shrink-0 flex items-center justify-between px-6 py-4 border-b border-slate-100">
221
  <div>
222
- <h3 class="text-lg font-semibold text-slate-900">Edit Issue</h3>
223
- <p class="text-xs text-slate-500 mt-0.5">Issue #@editingId</p>
224
  </div>
225
  <button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
226
  <i data-lucide="x" class="h-5 w-5"></i>
@@ -230,64 +230,64 @@
230
  <div class="min-h-0 flex-1 overflow-y-auto p-6 space-y-4">
231
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
232
  <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
233
- <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Task</p>
234
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedTaskLabel</p>
235
  </div>
236
  <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
237
- <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">Project</p>
238
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedProjectLabel</p>
239
  </div>
240
  </div>
241
 
242
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
243
  <div>
244
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked Status</label>
245
  <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
246
  <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
247
- <span>Blocked</span>
248
  </label>
249
  </div>
250
  <div>
251
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Escalation Level</label>
252
  <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
253
- <option value="0">None</option>
254
- <option value="1">Team Lead</option>
255
- <option value="2">PM</option>
256
- <option value="3">Manager</option>
257
  </select>
258
  </div>
259
  </div>
260
 
261
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
262
  <div>
263
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Blocked By</label>
264
- <input @bind="formBlockedBy" type="text" placeholder="Dependency, person, or team"
265
  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" />
266
  </div>
267
  <div>
268
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Delay Reason</label>
269
- <input @bind="formDelayReason" type="text" placeholder="Why is this issue delayed?"
270
  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" />
271
  </div>
272
  </div>
273
 
274
  <div>
275
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Title <span class="text-red-500">*</span></label>
276
- <input @bind="formTitle" type="text" placeholder="Issue title"
277
  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" />
278
  </div>
279
 
280
  <div>
281
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Description</label>
282
- <textarea @bind="formDescription" rows="3" placeholder="Describe the work..."
283
  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"></textarea>
284
  </div>
285
 
286
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
287
  <div>
288
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Assignee</label>
289
  <select @bind="formAssignedTo" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
290
- <option value="">Unassigned</option>
291
  @foreach (var user in filteredProjectMembers)
292
  {
293
  <option value="@user.Id">@user.FirstName @user.LastName</option>
@@ -295,7 +295,7 @@
295
  </select>
296
  </div>
297
  <div>
298
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Actual Hours</label>
299
  <input @bind="formActualHours" type="number" step="0.25" min="0" max="7"
300
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
301
  </div>
@@ -303,12 +303,12 @@
303
 
304
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
305
  <div>
306
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Start Date</label>
307
  <input @bind="formStartDate" type="date"
308
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
309
  </div>
310
  <div>
311
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Due Date</label>
312
  <input @bind="formDueDate" type="date"
313
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
314
  </div>
@@ -316,19 +316,19 @@
316
 
317
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
318
  <div>
319
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Status</label>
320
  <select @bind="formStatusId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
321
- <option value="@AppTaskStatus.Todo">To Do</option>
322
- <option value="@AppTaskStatus.InProgress">In Progress</option>
323
- <option value="@AppTaskStatus.Done">Done</option>
324
  </select>
325
  </div>
326
  <div>
327
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Priority</label>
328
  <select @bind="formPriorityId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
329
- <option value="@TaskPriority.Low">Low</option>
330
- <option value="@TaskPriority.Medium">Medium</option>
331
- <option value="@TaskPriority.High">High</option>
332
  </select>
333
  </div>
334
  </div>
@@ -342,9 +342,9 @@
342
  </div>
343
 
344
  <div class="shrink-0 flex items-center justify-end gap-3 border-t border-slate-100 bg-white px-6 py-4 shadow-[0_-12px_24px_rgba(15,23,42,0.06)]">
345
- <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
346
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
347
- @(isSaving ? "Saving..." : "Update")
348
  </button>
349
  </div>
350
  </div>
@@ -352,9 +352,9 @@
352
  }
353
 
354
  <ConfirmDialog IsVisible="showDeleteConfirm"
355
- Title="Delete Issue?"
356
  Message="@deleteConfirmMessage"
357
- ConfirmText="Yes, Delete"
358
  Icon="trash-2"
359
  IconBgClass="bg-rose-50"
360
  IconTextClass="text-rose-600"
@@ -363,9 +363,9 @@
363
  OnCancel="CloseDeleteConfirm" />
364
 
365
  <ConfirmDialog IsVisible="showSaveConfirm"
366
- Title="Save Changes?"
367
- Message="Are you sure you want to save these issue changes?"
368
- ConfirmText="Yes, Save"
369
  Icon="save"
370
  IconBgClass="bg-violet-50"
371
  IconTextClass="text-violet-600"
 
16
  @inject MenuAuthorizationService MenuAuthorization
17
  @inject NavigationManager Navigation
18
 
19
+ <PageTitle>@AppLocalization.PageTitle("issues", "Issues")</PageTitle>
20
 
21
  <SuccessAlert Message="@successMessage" />
22
 
23
  <div class="space-y-6">
24
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
25
  <div>
26
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("issues", "Issues")</h2>
27
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("page.issues.desc", "Track daily work items, update progress, and review what was done.")</p>
28
  </div>
29
  @if (canCreateIssue)
30
  {
31
  <button @onclick="GoToAddIssue" 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">
32
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
33
+ @AppLocalization.Text("common.addIssue", "Add Issue")
34
  </button>
35
  }
36
  </div>
37
 
38
  @if (isLoading)
39
  {
40
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingIssues", "Loading issues...")" />
41
  }
42
  else
43
  {
44
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
45
  <div class="grid grid-cols-1 lg:grid-cols-6 gap-3">
46
  <div class="lg:col-span-2">
47
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchIssues", "Search issue title, description or assignee...")"
48
  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" />
49
  </div>
50
  <div>
51
  <select @bind="taskIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
+ <option value="0">@AppLocalization.Text("common.allTasks", "All tasks")</option>
53
  @foreach (var task in tasks)
54
  {
55
  <option value="@task.Id">@task.Title</option>
 
58
  </div>
59
  <div>
60
  <select @bind="projectIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
61
+ <option value="0">@AppLocalization.Text("common.allProjects", "All projects")</option>
62
  @foreach (var project in projects)
63
  {
64
  <option value="@project.Id">@project.Name</option>
 
67
  </div>
68
  <div>
69
  <select @bind="statusIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
70
+ <option value="">@AppLocalization.Text("common.all", "All")</option>
71
+ <option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
72
+ <option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
73
+ <option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
74
  </select>
75
  </div>
76
  <div>
77
  <select @bind="priorityIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
78
+ <option value="">@AppLocalization.Text("common.all", "All")</option>
79
+ <option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option>
80
+ <option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option>
81
+ <option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option>
82
  </select>
83
  </div>
84
  </div>
 
86
  <div class="mt-3 flex flex-wrap items-center gap-3">
87
  <label class="inline-flex items-center gap-2 text-sm text-slate-700">
88
  <input type="checkbox" @bind="assignedOnly" class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
89
+ @AppLocalization.Text("common.assignedToMeOnly", "Assigned to me only")
90
  </label>
91
  <button @onclick="ApplySearch" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
92
  <i data-lucide="search" class="h-4 w-4 mr-2"></i>
93
+ @AppLocalization.Text("action.search", "Search")
94
  </button>
95
  <button @onclick="ResetSearch" 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">
96
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
97
+ @AppLocalization.Text("action.reset", "Reset")
98
  </button>
99
  </div>
100
  </div>
 
115
  </colgroup>
116
  <thead>
117
  <tr class="border-b border-slate-100 bg-slate-50/60">
118
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.taskCode", "Task Code")</th>
119
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.issue", "Issue")</th>
120
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.hours", "Hours")</th>
121
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th>
122
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.priority", "Priority")</th>
123
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.date", "Date")</th>
124
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.assignee", "Assignee")</th>
125
+ <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.risk", "Risk")</th>
126
+ <th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Actions")</th>
127
  </tr>
128
  </thead>
129
  <tbody class="divide-y divide-slate-100 text-[13px]">
 
134
  var issue = entry.issue;
135
  <tr class="hover:bg-slate-50/50 transition-colors align-top">
136
  <td class="px-5 py-4 text-left whitespace-nowrap">
137
+ <span class="font-mono text-[11px] font-semibold tracking-wide text-indigo-700">
138
  @GetTaskCode(issue.TaskId)
139
  </span>
140
  </td>
 
144
  </button>
145
  </td>
146
  <td class="px-5 py-4 text-slate-600 whitespace-nowrap">
147
+ <span class="font-medium text-slate-700">@(issue.ActualHours?.ToString("0.##") ?? "-") @AppLocalization.Text("common.hoursAbbrev", "hr")</span>
148
  </td>
149
  <td class="px-5 py-4">
150
  <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">
 
157
  </span>
158
  </td>
159
  <td class="px-5 py-4 text-sm text-slate-600 whitespace-nowrap">
160
+ <span class="font-medium text-slate-700">@AppLocalization.Text("common.due", "Due"): @issue.DueDate.ToString("MMM d, yyyy")</span>
161
  </td>
162
  <td class="px-5 py-4 text-sm text-slate-600 break-words">
163
+ @(!string.IsNullOrWhiteSpace(issue.AssignedToName) ? issue.AssignedToName : AppLocalization.Text("common.unassigned", "Unassigned"))
164
  </td>
165
  <td class="px-5 py-4">
166
  @if (issue.IsBlocked)
167
  {
168
+ <span class="inline-flex items-center rounded-full bg-rose-50 px-2 py-0.5 text-xs font-semibold text-rose-700">@AppLocalization.Text("status.blocked", "Blocked")</span>
169
  }
170
  else if (issue.EscalationLevel > 0)
171
  {
 
173
  }
174
  else
175
  {
176
+ <span class="text-xs text-slate-400">@AppLocalization.Text("status.clear", "Clear")</span>
177
  }
178
  </td>
179
  <td class="px-5 py-4 text-center">
180
  <div class="flex items-center justify-center gap-1">
181
  @if (canEditIssue)
182
  {
183
+ <button @onclick="() => OpenEditModal(issue)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="@AppLocalization.Text("action.edit", "Edit")">
184
  <i data-lucide="pencil" class="h-4 w-4"></i>
185
  </button>
186
  }
187
  @if (canDeleteIssue)
188
  {
189
+ <button @onclick="() => RequestDeleteConfirmation(issue.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="@AppLocalization.Text("action.delete", "Delete")">
190
  <i data-lucide="trash-2" class="h-4 w-4"></i>
191
  </button>
192
  }
 
197
  }
198
  else
199
  {
200
+ <EmptyState ColSpan="9" Icon="file-pen-line" Title="@AppLocalization.Text("common.noResults", "No results found.")" Subtitle="@AppLocalization.Text("common.createFirstEntry", "Create your first entry to get started.")" />
201
  }
202
  </tbody>
203
  </table>
 
219
  <div class="flex max-h-[calc(100vh-2rem)] w-full max-w-4xl flex-col overflow-hidden rounded-2xl bg-white shadow-xl sm:max-h-[calc(100vh-3rem)]">
220
  <div class="shrink-0 flex items-center justify-between px-6 py-4 border-b border-slate-100">
221
  <div>
222
+ <h3 class="text-lg font-semibold text-slate-900">@AppLocalization.Text("common.editIssue", "Edit Issue")</h3>
223
+ <p class="text-xs text-slate-500 mt-0.5">#@editingId</p>
224
  </div>
225
  <button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
226
  <i data-lucide="x" class="h-5 w-5"></i>
 
230
  <div class="min-h-0 flex-1 overflow-y-auto p-6 space-y-4">
231
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
232
  <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
233
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">@AppLocalization.Text("common.task", "Task")</p>
234
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedTaskLabel</p>
235
  </div>
236
  <div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
237
+ <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">@AppLocalization.Text("common.project", "Project")</p>
238
  <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedProjectLabel</p>
239
  </div>
240
  </div>
241
 
242
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
243
  <div>
244
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedStatus", "Blocked Status")</label>
245
  <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700">
246
  <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
247
+ <span>@AppLocalization.Text("status.blocked", "Blocked")</span>
248
  </label>
249
  </div>
250
  <div>
251
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.escalationLevel", "Escalation Level")</label>
252
  <select @bind="formEscalationLevel" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
253
+ <option value="0">@AppLocalization.Text("common.none", "None")</option>
254
+ <option value="1">@AppLocalization.Text("issue.teamLead", "Team Lead")</option>
255
+ <option value="2">@AppLocalization.Text("issue.pm", "PM")</option>
256
+ <option value="3">@AppLocalization.Text("issue.manager", "Manager")</option>
257
  </select>
258
  </div>
259
  </div>
260
 
261
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
262
  <div>
263
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedBy", "Blocked By")</label>
264
+ <input @bind="formBlockedBy" type="text" placeholder="@AppLocalization.Text("issue.blockedByPlaceholder", "Dependency, person, or team")"
265
  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" />
266
  </div>
267
  <div>
268
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.delayReason", "Delay Reason")</label>
269
+ <input @bind="formDelayReason" type="text" placeholder="@AppLocalization.Text("issue.delayReasonPlaceholder", "Why is this issue delayed?")"
270
  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" />
271
  </div>
272
  </div>
273
 
274
  <div>
275
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.issueTitle", "Issue Title") <span class="text-red-500">*</span></label>
276
+ <input @bind="formTitle" type="text" placeholder="@AppLocalization.Text("common.issueTitle", "Issue Title")"
277
  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" />
278
  </div>
279
 
280
  <div>
281
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.issueDescription", "Description")</label>
282
+ <textarea @bind="formDescription" rows="3" placeholder="@AppLocalization.Text("issue.describeWork", "Describe the work...")"
283
  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"></textarea>
284
  </div>
285
 
286
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
287
  <div>
288
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.assignee", "Assignee")</label>
289
  <select @bind="formAssignedTo" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
290
+ <option value="">@AppLocalization.Text("common.unassigned", "Unassigned")</option>
291
  @foreach (var user in filteredProjectMembers)
292
  {
293
  <option value="@user.Id">@user.FirstName @user.LastName</option>
 
295
  </select>
296
  </div>
297
  <div>
298
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.actualHours", "Actual Hours")</label>
299
  <input @bind="formActualHours" type="number" step="0.25" min="0" max="7"
300
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
301
  </div>
 
303
 
304
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
305
  <div>
306
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.startDate", "Start Date")</label>
307
  <input @bind="formStartDate" type="date"
308
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
309
  </div>
310
  <div>
311
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.dueDate", "Due Date")</label>
312
  <input @bind="formDueDate" type="date"
313
  class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
314
  </div>
 
316
 
317
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
318
  <div>
319
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
320
  <select @bind="formStatusId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
321
+ <option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
322
+ <option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
323
+ <option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
324
  </select>
325
  </div>
326
  <div>
327
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label>
328
  <select @bind="formPriorityId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
329
+ <option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option>
330
+ <option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option>
331
+ <option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option>
332
  </select>
333
  </div>
334
  </div>
 
342
  </div>
343
 
344
  <div class="shrink-0 flex items-center justify-end gap-3 border-t border-slate-100 bg-white px-6 py-4 shadow-[0_-12px_24px_rgba(15,23,42,0.06)]">
345
+ <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">@AppLocalization.Text("common.cancel", "Cancel")</button>
346
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
347
+ @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.update", "Update"))
348
  </button>
349
  </div>
350
  </div>
 
352
  }
353
 
354
  <ConfirmDialog IsVisible="showDeleteConfirm"
355
+ Title="@AppLocalization.Text("issue.deleteIssue", "Delete Issue?")"
356
  Message="@deleteConfirmMessage"
357
+ ConfirmText="@AppLocalization.Text("issue.yesDelete", "Yes, Delete")"
358
  Icon="trash-2"
359
  IconBgClass="bg-rose-50"
360
  IconTextClass="text-rose-600"
 
363
  OnCancel="CloseDeleteConfirm" />
364
 
365
  <ConfirmDialog IsVisible="showSaveConfirm"
366
+ Title="@AppLocalization.Text("common.saveChanges", "Save Changes?")"
367
+ Message="@AppLocalization.Text("issue.saveConfirmMessage", "Are you sure you want to save these issue changes?")"
368
+ ConfirmText="@AppLocalization.Text("issue.yesSave", "Yes, Save")"
369
  Icon="save"
370
  IconBgClass="bg-violet-50"
371
  IconTextClass="text-violet-600"
TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/ProjectAssign.razor CHANGED
@@ -11,7 +11,7 @@
11
  @inject AuthenticationStateProvider AuthStateProvider
12
  @inject MenuAuthorizationService MenuAuthorization
13
 
14
- <PageTitle>Project Assignment</PageTitle>
15
 
16
  <SuccessAlert Message="@successMessage" />
17
 
@@ -19,14 +19,14 @@
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">Project Assignment</h2>
23
- <p class="text-slate-500 mt-1">Assign team members to projects to control their task visibility.</p>
24
  </div>
25
  </div>
26
 
27
  @if (isLoading)
28
  {
29
- <LoadingSpinner Message="Loading projects and users..." />
30
  }
31
  else
32
  {
@@ -34,18 +34,18 @@
34
  <!-- Left panel: Projects list -->
35
  <div class="lg:col-span-1 rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[600px]">
36
  <div class="p-4 border-b border-slate-100 bg-slate-50/60">
37
- <h3 class="font-bold text-slate-800 text-sm mb-3">Select Project</h3>
38
  <div class="relative flex gap-2">
39
  <div class="relative flex-1">
40
- <input @bind="projectSearchInput" type="text" placeholder="Search projects..."
41
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
42
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
43
  </div>
44
  <button @onclick="ApplyProjectSearch" class="inline-flex items-center rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-violet-700 transition-colors shrink-0">
45
- <i data-lucide="search" class="h-3.5 w-3.5 mr-1.5"></i> Search
46
  </button>
47
  <button @onclick="ResetProjectSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 transition-colors shrink-0">
48
- <i data-lucide="rotate-ccw" class="h-3.5 w-3.5 mr-1.5 text-slate-500"></i> Reset
49
  </button>
50
  </div>
51
  </div>
@@ -70,7 +70,7 @@
70
  }
71
  else
72
  {
73
- <div class="p-8 text-center text-slate-400 text-xs">No projects found.</div>
74
  }
75
  </div>
76
  </div>
@@ -83,8 +83,8 @@
83
  <div class="h-12 w-12 rounded-full bg-slate-100 flex items-center justify-center text-slate-400 mb-3">
84
  <i data-lucide="folder-kanban" class="h-6 w-6"></i>
85
  </div>
86
- <h4 class="text-sm font-bold text-slate-800">No Project Selected</h4>
87
- <p class="text-xs text-slate-500 mt-1 max-w-sm">Please select a project from the left panel to manage user assignments.</p>
88
  </div>
89
  }
90
  else
@@ -92,7 +92,7 @@
92
  <!-- Selected Project Header -->
93
  <div class="p-6 border-b border-slate-100 bg-slate-50 flex flex-col md:flex-row md:items-center justify-between gap-4">
94
  <div>
95
- <span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider">Currently Managing</span>
96
  <h3 class="text-xl font-bold text-slate-900">@SelectedProject.Name</h3>
97
  @if (!string.IsNullOrEmpty(SelectedProject.Description))
98
  {
@@ -110,22 +110,22 @@
110
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
111
  <div class="relative max-w-xs flex-1 flex gap-2">
112
  <div class="relative flex-1">
113
- <input @bind="userSearchInput" type="text" placeholder="Search team members..."
114
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
115
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
116
  </div>
117
  <button @onclick="ApplyUserSearch" class="inline-flex items-center rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-violet-700 transition-colors shrink-0">
118
- <i data-lucide="search" class="h-3.5 w-3.5 mr-1.5"></i> Search
119
  </button>
120
  <button @onclick="ResetUserSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 transition-colors shrink-0">
121
- <i data-lucide="rotate-ccw" class="h-3.5 w-3.5 mr-1.5 text-slate-500"></i> Reset
122
  </button>
123
  </div>
124
  @if (isAdminUser)
125
  {
126
  <div class="w-40 shrink-0">
127
  <select @bind="selectedRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
128
- <option value="0">All Roles</option>
129
  @foreach (var r in roles)
130
  {
131
  <option value="@r.Id">@r.Name</option>
@@ -136,15 +136,15 @@
136
  else
137
  {
138
  <div class="text-xs font-semibold text-violet-700 bg-violet-50 border border-violet-100 px-3 py-1.5 rounded-lg shrink-0">
139
- Employees only
140
  </div>
141
  }
142
  <div class="flex items-center gap-3 shrink-0">
143
- <button @onclick="SelectAllUsers" class="text-xs font-semibold text-violet-600 hover:text-violet-800 transition-colors">Select All</button>
144
  <span class="text-slate-300">|</span>
145
- <button @onclick="DeselectAllUsers" class="text-xs font-semibold text-slate-600 hover:text-slate-800 transition-colors">Deselect All</button>
146
  <span class="text-slate-300">|</span>
147
- <span class="text-xs font-bold text-slate-600">@SelectedUserIds.Count assigned</span>
148
  </div>
149
  </div>
150
 
@@ -153,7 +153,7 @@
153
  @if (isLoadingMembers)
154
  {
155
  <div class="flex justify-center py-12">
156
- <LoadingSpinner Message="Loading members..." />
157
  </div>
158
  }
159
  else
@@ -181,7 +181,7 @@
181
  }
182
  else
183
  {
184
- <div class="text-center text-slate-400 py-12 text-sm">No team members match your search criteria.</div>
185
  }
186
  }
187
  </div>
@@ -202,7 +202,7 @@
202
  <button @onclick="RequestSaveConfirmation" disabled="@(isSaving || isLoadingMembers)"
203
  class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
204
  <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
205
- @(isSaving ? "Saving changes..." : "Save Assignment Changes")
206
  </button>
207
  }
208
  </div>
@@ -213,9 +213,9 @@
213
  </div>
214
 
215
  <ConfirmDialog IsVisible="showConfirmDialog"
216
- Title="Save Assignment?"
217
- Message="Are you sure you want to save these project member assignments?"
218
- ConfirmText="Yes, Save"
219
  Icon="user-check"
220
  OnConfirm="ConfirmSaveAssignments"
221
  OnCancel="CloseConfirmDialog" />
 
11
  @inject AuthenticationStateProvider AuthStateProvider
12
  @inject MenuAuthorizationService MenuAuthorization
13
 
14
+ <PageTitle>@AppLocalization.PageTitle("projectAssignment", "Project Assignment")</PageTitle>
15
 
16
  <SuccessAlert Message="@successMessage" />
17
 
 
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">@AppLocalization.PageTitle("projectAssignment", "Project Assignment")</h2>
23
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("project.assignDescription", "Assign team members to projects to control their task visibility.")</p>
24
  </div>
25
  </div>
26
 
27
  @if (isLoading)
28
  {
29
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingProjectsAndUsers", "Loading projects and users...")" />
30
  }
31
  else
32
  {
 
34
  <!-- Left panel: Projects list -->
35
  <div class="lg:col-span-1 rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[600px]">
36
  <div class="p-4 border-b border-slate-100 bg-slate-50/60">
37
+ <h3 class="font-bold text-slate-800 text-sm mb-3">@AppLocalization.Text("common.selectProject", "Select Project")</h3>
38
  <div class="relative flex gap-2">
39
  <div class="relative flex-1">
40
+ <input @bind="projectSearchInput" type="text" placeholder="@AppLocalization.Text("common.searchProjects", "Search projects...")"
41
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
42
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
43
  </div>
44
  <button @onclick="ApplyProjectSearch" class="inline-flex items-center rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-violet-700 transition-colors shrink-0">
45
+ <i data-lucide="search" class="h-3.5 w-3.5 mr-1.5"></i> @AppLocalization.Text("action.search", "Search")
46
  </button>
47
  <button @onclick="ResetProjectSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 transition-colors shrink-0">
48
+ <i data-lucide="rotate-ccw" class="h-3.5 w-3.5 mr-1.5 text-slate-500"></i> @AppLocalization.Text("action.reset", "Reset")
49
  </button>
50
  </div>
51
  </div>
 
70
  }
71
  else
72
  {
73
+ <div class="p-8 text-center text-slate-400 text-xs">@AppLocalization.Text("common.noProjectsFound", "No projects found.")</div>
74
  }
75
  </div>
76
  </div>
 
83
  <div class="h-12 w-12 rounded-full bg-slate-100 flex items-center justify-center text-slate-400 mb-3">
84
  <i data-lucide="folder-kanban" class="h-6 w-6"></i>
85
  </div>
86
+ <h4 class="text-sm font-bold text-slate-800">@AppLocalization.Text("common.noProjectSelected", "No Project Selected")</h4>
87
+ <p class="text-xs text-slate-500 mt-1 max-w-sm">@AppLocalization.Text("project.selectProjectPrompt", "Please select a project from the left panel to manage user assignments.")</p>
88
  </div>
89
  }
90
  else
 
92
  <!-- Selected Project Header -->
93
  <div class="p-6 border-b border-slate-100 bg-slate-50 flex flex-col md:flex-row md:items-center justify-between gap-4">
94
  <div>
95
+ <span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider">@AppLocalization.Text("common.currentlyManaging", "Currently Managing")</span>
96
  <h3 class="text-xl font-bold text-slate-900">@SelectedProject.Name</h3>
97
  @if (!string.IsNullOrEmpty(SelectedProject.Description))
98
  {
 
110
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
111
  <div class="relative max-w-xs flex-1 flex gap-2">
112
  <div class="relative flex-1">
113
+ <input @bind="userSearchInput" type="text" placeholder="@AppLocalization.Text("common.searchTeamMembers", "Search team members...")"
114
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
115
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
116
  </div>
117
  <button @onclick="ApplyUserSearch" class="inline-flex items-center rounded-lg bg-violet-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-violet-700 transition-colors shrink-0">
118
+ <i data-lucide="search" class="h-3.5 w-3.5 mr-1.5"></i> @AppLocalization.Text("action.search", "Search")
119
  </button>
120
  <button @onclick="ResetUserSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50 transition-colors shrink-0">
121
+ <i data-lucide="rotate-ccw" class="h-3.5 w-3.5 mr-1.5 text-slate-500"></i> @AppLocalization.Text("action.reset", "Reset")
122
  </button>
123
  </div>
124
  @if (isAdminUser)
125
  {
126
  <div class="w-40 shrink-0">
127
  <select @bind="selectedRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
128
+ <option value="0">@AppLocalization.Text("common.allRoles", "All Roles")</option>
129
  @foreach (var r in roles)
130
  {
131
  <option value="@r.Id">@r.Name</option>
 
136
  else
137
  {
138
  <div class="text-xs font-semibold text-violet-700 bg-violet-50 border border-violet-100 px-3 py-1.5 rounded-lg shrink-0">
139
+ @AppLocalization.Text("project.employeesOnly", "Employees only")
140
  </div>
141
  }
142
  <div class="flex items-center gap-3 shrink-0">
143
+ <button @onclick="SelectAllUsers" class="text-xs font-semibold text-violet-600 hover:text-violet-800 transition-colors">@AppLocalization.Text("common.selectAll", "Select All")</button>
144
  <span class="text-slate-300">|</span>
145
+ <button @onclick="DeselectAllUsers" class="text-xs font-semibold text-slate-600 hover:text-slate-800 transition-colors">@AppLocalization.Text("common.deselectAll", "Deselect All")</button>
146
  <span class="text-slate-300">|</span>
147
+ <span class="text-xs font-bold text-slate-600">@SelectedUserIds.Count @AppLocalization.Text("common.selected", "selected")</span>
148
  </div>
149
  </div>
150
 
 
153
  @if (isLoadingMembers)
154
  {
155
  <div class="flex justify-center py-12">
156
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingMembers", "Loading members...")" />
157
  </div>
158
  }
159
  else
 
181
  }
182
  else
183
  {
184
+ <div class="text-center text-slate-400 py-12 text-sm">@AppLocalization.Text("common.noTeamMembersFound", "No team members match your search criteria.")</div>
185
  }
186
  }
187
  </div>
 
202
  <button @onclick="RequestSaveConfirmation" disabled="@(isSaving || isLoadingMembers)"
203
  class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
204
  <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
205
+ @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.saveAssignmentChanges", "Save Assignment Changes"))
206
  </button>
207
  }
208
  </div>
 
213
  </div>
214
 
215
  <ConfirmDialog IsVisible="showConfirmDialog"
216
+ Title="@AppLocalization.Text("project.saveAssignmentTitle", "Save Assignment?")"
217
+ Message="@AppLocalization.Text("project.saveAssignmentConfirm", "Are you sure you want to save these project member assignments?")"
218
+ ConfirmText="@AppLocalization.Text("issue.yesSave", "Yes, Save")"
219
  Icon="user-check"
220
  OnConfirm="ConfirmSaveAssignments"
221
  OnCancel="CloseConfirmDialog" />
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/Components/ReportToolbar.razor CHANGED
@@ -1,17 +1,17 @@
1
  <div class="flex items-center space-x-3">
2
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
3
  <button @onclick="() => OnViewChanged.InvokeAsync(false)" class="@ListButtonClass">
4
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
5
  </button>
6
  <button @onclick="() => OnViewChanged.InvokeAsync(true)" class="@ChartButtonClass">
7
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
8
  </button>
9
  </div>
10
  <button @onclick="OnExportExcel" 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">
11
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
12
  </button>
13
  <button @onclick="OnExportPdf" 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">
14
- <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
15
  </button>
16
  </div>
17
 
 
1
  <div class="flex items-center space-x-3">
2
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
3
  <button @onclick="() => OnViewChanged.InvokeAsync(false)" class="@ListButtonClass">
4
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
5
  </button>
6
  <button @onclick="() => OnViewChanged.InvokeAsync(true)" class="@ChartButtonClass">
7
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
8
  </button>
9
  </div>
10
  <button @onclick="OnExportExcel" 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">
11
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
12
  </button>
13
  <button @onclick="OnExportPdf" 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">
14
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
15
  </button>
16
  </div>
17
 
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor CHANGED
@@ -10,29 +10,29 @@
10
  @inject ApiClientService ApiClient
11
  @inject IJSRuntime JS
12
 
13
- <PageTitle>Employee Workload &amp; Performance</PageTitle>
14
 
15
  <div class="space-y-6">
16
  <!-- Page Header -->
17
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
18
  <div>
19
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Employee Workload &amp; Performance</h2>
20
- <p class="text-slate-500 mt-1">Monitor issue workload, execution health, and actual effort.</p>
21
  </div>
22
  <div class="flex items-center space-x-3">
23
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
24
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
25
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
26
  </button>
27
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
28
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
29
  </button>
30
  </div>
31
  <button @onclick="DownloadExcel" 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">
32
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
33
  </button>
34
  <button @onclick="DownloadPdf" 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="file-text" class="h-4 w-4 mr-2"></i> Export PDF
36
  </button>
37
  </div>
38
  </div>
@@ -41,13 +41,13 @@
41
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
42
  <div class="flex flex-wrap items-end gap-4">
43
  <div class="flex-1 min-w-[200px]">
44
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Employee</label>
45
- <input @bind="searchInput" type="text" placeholder="Search by name..." 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" />
46
  </div>
47
  <div>
48
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Period Filter</label>
49
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
50
- <option value="all">All Time</option>
51
  <option value="daily">Daily (Today)</option>
52
  <option value="monthly">Monthly (This Month)</option>
53
  <option value="yearly">Yearly (This Year)</option>
@@ -68,13 +68,13 @@
68
  }
69
 
70
  <div>
71
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Task Status</label>
72
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
73
- <option value="0">All Tasks</option>
74
- <option value="1">To Do</option>
75
- <option value="2">In Progress</option>
76
- <option value="3">Done</option>
77
- <option value="overdue">Overdue</option>
78
  </select>
79
  </div>
80
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
@@ -87,11 +87,11 @@
87
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
88
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
89
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
90
- <span>Assigned to me</span>
91
  </label>
92
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
93
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
94
- <span>Assigned to my team</span>
95
  </label>
96
  </div>
97
  </div>
@@ -103,7 +103,7 @@
103
  <div class="flex items-center justify-center py-20">
104
  <div class="flex items-center space-x-3 text-slate-500">
105
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
106
- <span class="text-sm font-medium">Loading data...</span>
107
  </div>
108
  </div>
109
  }
@@ -113,25 +113,25 @@
113
  {
114
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
115
  <div class="xl:col-span-7">
116
- <WorkloadHeatmap Title="Workload heatmap" Subtitle="Capacity signals across assigned work, open issues, overdue issues, and hours." Rows="EmployeeHeatmapRows" Columns="EmployeeHeatmapColumns" Cells="EmployeeHeatmapCells" Insight="Darker cells mean someone may need support or scope adjustment." />
117
  </div>
118
  <div class="xl:col-span-5">
119
- <ReportChartCard Title="Capacity support list" Subtitle="Highest support signals, phrased as capacity risk." Insight="Use this to route help and remove blockers, not to rank people negatively.">
120
- <TopRiskList Items="EmployeeCapacityRiskItems" EmptyText="No capacity risks in the current filter." />
121
  </ReportChartCard>
122
  </div>
123
  <div class="xl:col-span-5">
124
- <ReportChartCard Title="Completed vs open issues" Subtitle="Team execution balance by issue state." Value="@CompletedIssueTotal.ToString()" ValueLabel="completed tasks">
125
  <StackedProgressBar Segments="EmployeeCompletionSegments" AriaLabel="Completed versus open employee workload" />
126
  </ReportChartCard>
127
  </div>
128
  <div class="xl:col-span-4">
129
- <ReportChartCard Title="Overdue support" Subtitle="Employees with overdue issue load." Insight="Treat overdue counts as support requests.">
130
- <TopRiskList Items="EmployeeOverdueItems" EmptyText="No overdue support needs found." />
131
  </ReportChartCard>
132
  </div>
133
  <div class="xl:col-span-3">
134
- <TrendSparklineCard Title="Completion pattern" Subtitle="Completion rate across visible employees." Value="@($"{AverageCompletionRate:0}%")" ValueLabel="avg rate" Points="EmployeeCompletionSparkline" Color="#3b82f6" Insight="A quick view of whether completion is clustered or balanced." />
135
  </div>
136
  </div>
137
  }
 
10
  @inject ApiClientService ApiClient
11
  @inject IJSRuntime JS
12
 
13
+ <PageTitle>@AppLocalization.Text("report.employeeWorkloadTitle", "Employee Workload & Performance")</PageTitle>
14
 
15
  <div class="space-y-6">
16
  <!-- Page Header -->
17
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
18
  <div>
19
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("report.employeeWorkloadHeading", "Employee Workload & Performance")</h2>
20
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("report.employeeWorkloadDesc", "Monitor issue workload, execution health, and actual effort.")</p>
21
  </div>
22
  <div class="flex items-center space-x-3">
23
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
24
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
25
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
26
  </button>
27
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
28
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
29
  </button>
30
  </div>
31
  <button @onclick="DownloadExcel" 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">
32
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
33
  </button>
34
  <button @onclick="DownloadPdf" 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="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
36
  </button>
37
  </div>
38
  </div>
 
41
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
42
  <div class="flex flex-wrap items-end gap-4">
43
  <div class="flex-1 min-w-[200px]">
44
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.searchEmployee", "Search Employee")</label>
45
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchByName", "Search by name...")" 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" />
46
  </div>
47
  <div>
48
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.periodFilter", "Period Filter")</label>
49
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
50
+ <option value="all">@AppLocalization.Text("common.all", "All Time")</option>
51
  <option value="daily">Daily (Today)</option>
52
  <option value="monthly">Monthly (This Month)</option>
53
  <option value="yearly">Yearly (This Year)</option>
 
68
  }
69
 
70
  <div>
71
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Task Status")</label>
72
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
73
+ <option value="0">@AppLocalization.Text("common.allTasks", "All Tasks")</option>
74
+ <option value="1">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
75
+ <option value="2">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
76
+ <option value="3">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
77
+ <option value="overdue">@AppLocalization.Text("status.overdue", "Overdue")</option>
78
  </select>
79
  </div>
80
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
 
87
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
88
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
89
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
90
+ <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span>
91
  </label>
92
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
93
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
94
+ <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span>
95
  </label>
96
  </div>
97
  </div>
 
103
  <div class="flex items-center justify-center py-20">
104
  <div class="flex items-center space-x-3 text-slate-500">
105
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
106
+ <span class="text-sm font-medium">@AppLocalization.Text("common.loading", "Loading data...")</span>
107
  </div>
108
  </div>
109
  }
 
113
  {
114
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
115
  <div class="xl:col-span-7">
116
+ <WorkloadHeatmap Title="@AppLocalization.Text("report.workloadHeatmap", "Workload heatmap")" Subtitle="@AppLocalization.Text("report.workloadHeatmapDesc", "Capacity signals across assigned work, open issues, overdue issues, and hours.")" Rows="EmployeeHeatmapRows" Columns="EmployeeHeatmapColumns" Cells="EmployeeHeatmapCells" Insight="@AppLocalization.Text("report.workloadHeatmapInsight", "Darker cells mean someone may need support or scope adjustment.")" />
117
  </div>
118
  <div class="xl:col-span-5">
119
+ <ReportChartCard Title="@AppLocalization.Text("report.capacitySupportList", "Capacity support list")" Subtitle="@AppLocalization.Text("report.capacitySupportDesc", "Highest support signals, phrased as capacity risk.")" Insight="@AppLocalization.Text("report.capacitySupportInsight", "Use this to route help and remove blockers, not to rank people negatively.")">
120
+ <TopRiskList Items="EmployeeCapacityRiskItems" EmptyText="@AppLocalization.Text("report.noCapacityRisks", "No capacity risks in the current filter.")" />
121
  </ReportChartCard>
122
  </div>
123
  <div class="xl:col-span-5">
124
+ <ReportChartCard Title="@AppLocalization.Text("report.completedVsOpenIssues", "Completed vs open issues")" Subtitle="@AppLocalization.Text("report.teamExecutionBalance", "Team execution balance by issue state.")" Value="@CompletedIssueTotal.ToString()" ValueLabel="@AppLocalization.Text("report.completedTasks", "completed tasks")">
125
  <StackedProgressBar Segments="EmployeeCompletionSegments" AriaLabel="Completed versus open employee workload" />
126
  </ReportChartCard>
127
  </div>
128
  <div class="xl:col-span-4">
129
+ <ReportChartCard Title="@AppLocalization.Text("report.overdueSupport", "Overdue support")" Subtitle="@AppLocalization.Text("report.employeesWithOverdueLoad", "Employees with overdue issue load.")" Insight="@AppLocalization.Text("report.overdueSupportInsight", "Treat overdue counts as support requests.")">
130
+ <TopRiskList Items="EmployeeOverdueItems" EmptyText="@AppLocalization.Text("report.noOverdueSupport", "No overdue support needs found.")" />
131
  </ReportChartCard>
132
  </div>
133
  <div class="xl:col-span-3">
134
+ <TrendSparklineCard Title="@AppLocalization.Text("report.completionPattern", "Completion pattern")" Subtitle="@AppLocalization.Text("report.completionPatternDesc", "Completion rate across visible employees.")" Value="@($"{AverageCompletionRate:0}%")" ValueLabel="@AppLocalization.Text("report.avgRate", "avg rate")" Points="EmployeeCompletionSparkline" Color="#3b82f6" Insight="@AppLocalization.Text("report.completionPatternInsight", "A quick view of whether completion is clustered or balanced.")" />
135
  </div>
136
  </div>
137
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/IssueReport.razor CHANGED
@@ -9,28 +9,28 @@
9
  @inject ApiClientService ApiClient
10
  @inject IJSRuntime JS
11
 
12
- <PageTitle>Issue Report</PageTitle>
13
 
14
  <div class="space-y-6">
15
  <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
16
  <div>
17
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Issue Report</h2>
18
- <p class="mt-1 text-slate-500">Track daily execution, backlog health, and issue-level effort.</p>
19
  </div>
20
  <div class="flex flex-wrap items-center gap-3">
21
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
22
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
23
- <i data-lucide="list" class="mr-1 h-3.5 w-3.5"></i> List View
24
  </button>
25
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
26
- <i data-lucide="bar-chart-3" class="mr-1 h-3.5 w-3.5"></i> Chart View
27
  </button>
28
  </div>
29
  <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
30
- <i data-lucide="download" class="mr-2 h-4 w-4"></i> Export Excel
31
  </button>
32
  <button @onclick="DownloadPdf" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
33
- <i data-lucide="file-text" class="mr-2 h-4 w-4"></i> Export PDF
34
  </button>
35
  </div>
36
  </div>
@@ -38,14 +38,14 @@
38
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
39
  <div class="flex flex-wrap items-end gap-3">
40
  <div class="min-w-[220px] flex-1">
41
- <label class="mb-1.5 block text-xs font-medium text-slate-500">Search</label>
42
- <input @bind="searchInput" type="text" placeholder="Search issue, task, project, assignee..."
43
  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" />
44
  </div>
45
  <div>
46
- <label class="mb-1.5 block text-xs font-medium text-slate-500">Project</label>
47
  <select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
48
- <option value="0">All Projects</option>
49
  @foreach (var project in projects)
50
  {
51
  <option value="@project.Id">@project.Name</option>
@@ -53,40 +53,40 @@
53
  </select>
54
  </div>
55
  <div>
56
- <label class="mb-1.5 block text-xs font-medium text-slate-500">Status</label>
57
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
58
- <option value="0">All Statuses</option>
59
- <option value="1">To Do</option>
60
- <option value="2">In Progress</option>
61
- <option value="3">Done</option>
62
- <option value="overdue">Overdue</option>
63
- <option value="blocked">Blocked</option>
64
  </select>
65
  </div>
66
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-violet-700">
67
- <i data-lucide="search" class="mr-2 h-4 w-4"></i> Search
68
  </button>
69
  <button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
70
- <i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i> Reset
71
  </button>
72
  </div>
73
  </div>
74
 
75
  @if (isLoading)
76
  {
77
- <LoadingSpinner Message="Loading issue report..." />
78
  }
79
  else
80
  {
81
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
82
- @MetricCard("Total Issues", FilteredIssues.Count().ToString(), "file-pen-line", "violet")
83
- @MetricCard("Open Issues", OpenIssues.ToString(), "list-todo", "blue")
84
- @MetricCard("Overdue Issues", OverdueIssues.ToString(), "alarm-clock", "rose")
85
- @MetricCard("Blocked Issues", BlockedIssues.ToString(), "ban", "amber")
86
  </div>
87
 
88
  <div class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
89
- <h3 class="text-sm font-semibold text-slate-900">Key Insights</h3>
90
  <div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
91
  @foreach (var insight in Insights)
92
  {
@@ -100,30 +100,30 @@
100
  {
101
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
102
  <div class="xl:col-span-5">
103
- <ReportChartCard Title="Issue lifecycle funnel" Subtitle="How issues move from backlog to resolution." Insight="A large in-progress or blocked band is a daily execution signal.">
104
  <FunnelChart Items="IssueFunnelItems" />
105
  </ReportChartCard>
106
  </div>
107
  <div class="xl:col-span-4">
108
- <ReportChartCard Title="Issue status donut" Subtitle="Current status distribution, including overdue pressure." Insight="@IssueStatusInsight">
109
- <DonutStatusChart Items="IssueStatusItems" CenterLabel="issues" />
110
  </ReportChartCard>
111
  </div>
112
  <div class="xl:col-span-3">
113
- <RadialMetricCard Title="Blocked / overdue" Subtitle="Execution risk in the filtered issue set." Percent="@BlockedOverduePercent" Value="@((BlockedIssues + OverdueIssues).ToString())" ValueLabel="risk issues" CenterLabel="risk" Color="#ef4444" Insight="Focus standup discussion here first." />
114
  </div>
115
  <div class="xl:col-span-4">
116
- <ReportChartCard Title="Issues by project" Subtitle="Where issue volume is concentrated." Insight="Useful for project-level triage and planning health.">
117
- <TopRiskList Items="IssueProjectItems" EmptyText="No project issues found." />
118
  </ReportChartCard>
119
  </div>
120
  <div class="xl:col-span-4">
121
- <ReportChartCard Title="Issues by assignee" Subtitle="Support needs by assignee." Insight="Use as a support signal, especially when overdue or blocked items cluster.">
122
- <TopRiskList Items="IssueAssigneeItems" EmptyText="No assignee issues found." />
123
  </ReportChartCard>
124
  </div>
125
  <div class="xl:col-span-4">
126
- <ReportChartCard Title="Resolution aging" Subtitle="How close issues are to or past due date." Insight="Older overdue buckets should be cleared before new scope expands.">
127
  <AgingBucketChart Buckets="IssueAgingBuckets" />
128
  </ReportChartCard>
129
  </div>
@@ -144,13 +144,13 @@
144
  </colgroup>
145
  <thead>
146
  <tr class="border-b border-slate-100 bg-slate-50/60">
147
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Issue</th>
148
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Task / Project</th>
149
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Assignee</th>
150
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Status</th>
151
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Hours</th>
152
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Due</th>
153
- <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">Risk</th>
154
  </tr>
155
  </thead>
156
  <tbody class="divide-y divide-slate-100">
@@ -162,9 +162,9 @@
162
  @if (expandedIssues.Contains(issue.Id))
163
  {
164
  <div class="mt-2 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
165
- <div>@(string.IsNullOrWhiteSpace(issue.Description) ? "No description" : issue.Description)</div>
166
- <div class="mt-1">Delay: @(string.IsNullOrWhiteSpace(issue.DelayReason) ? "None recorded" : issue.DelayReason)</div>
167
- <div>Blocked by: @(string.IsNullOrWhiteSpace(issue.BlockedBy) ? "None" : issue.BlockedBy)</div>
168
  </div>
169
  }
170
  </td>
@@ -174,13 +174,13 @@
174
  </button>
175
  @if (expandedTaskDetails.Contains(issue.Id))
176
  {
177
- <div class="mt-2 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
178
- <div class="font-medium text-slate-800 break-words">@issue.TaskTitle</div>
179
- <div class="mt-1 break-words">@issue.ProjectName</div>
180
- </div>
181
  }
182
  </td>
183
- <td class="px-4 py-3 text-sm text-slate-600 break-words">@(issue.AssignedToName ?? "Unassigned")</td>
184
  <td class="px-4 py-3"><span class="inline-flex whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">@GetStatusLabel(issue.StatusId)</span></td>
185
  <td class="px-4 py-3 text-sm text-slate-700 whitespace-nowrap">@((issue.EstimatedHours ?? 0).ToString("N1")) / @((issue.ActualHours ?? 0).ToString("N1"))</td>
186
  <td class="px-4 py-3 text-sm whitespace-nowrap @(IsOverdue(issue) ? "font-semibold text-rose-600" : "text-slate-600")">@DisplayFormats.Date(issue.DueDate)</td>
@@ -189,7 +189,7 @@
189
  }
190
  @if (!PagedIssues.Any())
191
  {
192
- <EmptyState ColSpan="7" Icon="file-pen-line" Title="No issues found" Subtitle="No issues matched the current report filters." />
193
  }
194
  </tbody>
195
  </table>
@@ -310,14 +310,14 @@
310
 
311
  private static bool IsOverdue(IssueDto issue) => issue.StatusId != AppTaskStatus.Done && issue.DueDate.Date < DateTime.Today;
312
  private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}";
313
- private static string GetStatusLabel(AppTaskStatus status) => status == AppTaskStatus.InProgress ? "In Progress" : status == AppTaskStatus.Done ? "Done" : "To Do";
314
  private static string GetStatusBadge(AppTaskStatus status) => status == AppTaskStatus.Done ? "bg-emerald-50 text-emerald-700" : status == AppTaskStatus.InProgress ? "bg-blue-50 text-blue-700" : "bg-slate-100 text-slate-700";
315
- private static string GetPriorityLabel(TaskPriority priority) => priority == TaskPriority.High ? "High" : priority == TaskPriority.Medium ? "Medium" : "Low";
316
- private static string EscalationLabel(int level) => level switch { 1 => "Team Lead", 2 => "PM", 3 => "Manager", _ => "None" };
317
 
318
  private RenderFragment RiskBadge(IssueDto issue) => builder =>
319
  {
320
- var text = issue.IsBlocked ? "Blocked" : issue.EscalationLevel > 0 ? EscalationLabel(issue.EscalationLevel) : IsOverdue(issue) ? "Overdue" : "Clear";
321
  var cls = issue.IsBlocked || IsOverdue(issue) ? "bg-rose-50 text-rose-700" : issue.EscalationLevel > 0 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700";
322
  builder.OpenElement(0, "span");
323
  builder.AddAttribute(1, "class", $"rounded-full px-2 py-0.5 text-xs font-semibold {cls}");
@@ -325,31 +325,31 @@
325
  builder.CloseElement();
326
  };
327
 
328
- private IReadOnlyList<(string Label, int Value)> StatusChart => new[] { ("To Do", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Todo)), ("In Progress", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress)), ("Done", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done)), ("Overdue", OverdueIssues) };
329
  private IReadOnlyList<(string Label, int Value)> PriorityChart => Enum.GetValues<TaskPriority>().Select(p => (GetPriorityLabel(p), FilteredIssues.Count(i => i.PriorityId == p))).ToList();
330
- private IReadOnlyList<(string Label, int Value)> EmployeeChart => FilteredIssues.GroupBy(i => i.AssignedToName ?? "Unassigned").OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
331
  private IReadOnlyList<(string Label, int Value)> ProjectChart => FilteredIssues.GroupBy(i => i.ProjectName).OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
332
- private IEnumerable<string> Insights => new[] { $"{OverdueIssues} overdue issues need attention.", $"{BlockedIssues} issues are blocked.", $"Actual hours are {FilteredIssues.Sum(i => i.ActualHours ?? 0):N1} against {FilteredIssues.Sum(i => i.EstimatedHours ?? 0):N1} estimated." };
333
  private double BlockedOverduePercent => FilteredIssues.Any() ? (BlockedIssues + OverdueIssues) / (double)FilteredIssues.Count() * 100 : 0;
334
  private string IssueStatusInsight => OpenIssues > FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done)
335
- ? $"{OpenIssues} issues are still open across the selected scope."
336
- : "Resolved issues are leading this filtered set.";
337
 
338
  private List<ReportChartItem> IssueFunnelItems => new()
339
  {
340
- new("Created", FilteredIssues.Count(), "#7c3aed", "All reported issues"),
341
- new("Open", OpenIssues, "#3b82f6", "Unresolved issues"),
342
- new("In Progress", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress), "#06b6d4", "Currently being worked"),
343
- new("Blocked", BlockedIssues, "#f59e0b", "Needs unblock review"),
344
- new("Resolved", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done), "#10b981", "Completed issues")
345
  };
346
 
347
  private List<ReportChartItem> IssueStatusItems => new()
348
  {
349
- new("To Do", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Todo), "#94a3b8"),
350
- new("In Progress", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress), "#3b82f6"),
351
- new("Done", FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done), "#10b981"),
352
- new("Overdue", OverdueIssues, "#ef4444")
353
  };
354
 
355
  private List<TopRiskItem> IssueProjectItems => FilteredIssues
@@ -359,20 +359,20 @@
359
  var blocked = g.Count(i => i.IsBlocked);
360
  var overdue = g.Count(IsOverdue);
361
  var score = Math.Min(100, (g.Count() * 10) + (blocked * 25) + (overdue * 20));
362
- return new TopRiskItem(g.Key, $"{g.Count()} issues · {blocked} blocked", score, overdue > 0 ? $"{overdue} overdue" : $"{g.Count()} issues", overdue > 0 || blocked > 0 ? "rose" : "blue");
363
  })
364
  .OrderByDescending(item => item.Score)
365
  .Take(5)
366
  .ToList();
367
 
368
  private List<TopRiskItem> IssueAssigneeItems => FilteredIssues
369
- .GroupBy(i => i.AssignedToName ?? "Unassigned")
370
  .Select(g =>
371
  {
372
  var blocked = g.Count(i => i.IsBlocked);
373
  var overdue = g.Count(IsOverdue);
374
  var score = Math.Min(100, (g.Count() * 12) + (blocked * 22) + (overdue * 18));
375
- return new TopRiskItem(g.Key, $"{g.Count()} assigned issues", score, blocked > 0 ? $"{blocked} blocked" : $"{overdue} overdue", blocked > 0 || overdue > 0 ? "amber" : "blue", "Support signal");
376
  })
377
  .OrderByDescending(item => item.Score)
378
  .Take(5)
@@ -380,10 +380,10 @@
380
 
381
  private List<ReportChartItem> IssueAgingBuckets => new()
382
  {
383
- new("Due later", FilteredIssues.Count(i => !IsOverdue(i) && i.DueDate.Date > DateTime.Today.AddDays(3)), "#3b82f6"),
384
- new("Due soon", FilteredIssues.Count(i => !IsOverdue(i) && i.DueDate.Date <= DateTime.Today.AddDays(3)), "#f59e0b"),
385
- new("1-7d late", FilteredIssues.Count(i => IsOverdue(i) && (DateTime.Today - i.DueDate.Date).Days <= 7), "#fb7185"),
386
- new("8d+ late", FilteredIssues.Count(i => IsOverdue(i) && (DateTime.Today - i.DueDate.Date).Days > 7), "#ef4444")
387
  };
388
 
389
  private RenderFragment MetricCard(string label, string value, string icon, string color) => builder =>
@@ -396,15 +396,15 @@
396
 
397
  private async Task DownloadExcel()
398
  {
399
- var rows = FilteredIssues.Select(i => new object?[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? "Unassigned", GetStatusLabel(i.StatusId), GetPriorityLabel(i.PriorityId), i.EstimatedHours ?? 0, i.ActualHours ?? 0, i.DueDate, i.IsBlocked ? "Blocked" : "Clear", EscalationLabel(i.EscalationLevel), i.DelayReason ?? "" });
400
- var bytes = SpreadsheetReportBuilder.BuildTableWorkbook("Issue Report", "Issue Report", new[] { "Issue", "Task", "Project", "Assignee", "Status", "Priority", "Estimated", "Actual", "Due Date", "Blocked", "Escalation", "Delay Reason" }, rows, new[] { $"Generated: {DisplayFormats.Date(DateTime.Today)}" });
401
  await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(bytes));
402
  }
403
 
404
  private async Task DownloadPdf()
405
  {
406
- var rows = FilteredIssues.Select(i => new[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? "Unassigned", GetStatusLabel(i.StatusId), DisplayFormats.Date(i.DueDate), i.IsBlocked ? "Blocked" : "Clear" });
407
- var bytes = SimplePdfReportBuilder.BuildTableReport("Issue Report", new[] { $"Generated: {DisplayFormats.Date(DateTime.Today)}" }, new[] { "Issue", "Task", "Project", "Assignee", "Status", "Due Date", "Risk" }, rows);
408
  await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(bytes));
409
  }
410
 
 
9
  @inject ApiClientService ApiClient
10
  @inject IJSRuntime JS
11
 
12
+ <PageTitle>@AppLocalization.PageTitle("issueReport", "Issue Report")</PageTitle>
13
 
14
  <div class="space-y-6">
15
  <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
16
  <div>
17
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("issueReport", "Issue Report")</h2>
18
+ <p class="mt-1 text-slate-500">@AppLocalization.PageDescription("issueReport", "Track daily execution, backlog health, and issue-level effort.")</p>
19
  </div>
20
  <div class="flex flex-wrap items-center gap-3">
21
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
22
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
23
+ <i data-lucide="list" class="mr-1 h-3.5 w-3.5"></i> @AppLocalization.Text("common.listView", "List View")
24
  </button>
25
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900")">
26
+ <i data-lucide="bar-chart-3" class="mr-1 h-3.5 w-3.5"></i> @AppLocalization.Text("common.chartView", "Chart View")
27
  </button>
28
  </div>
29
  <button @onclick="DownloadExcel" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
30
+ <i data-lucide="download" class="mr-2 h-4 w-4"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
31
  </button>
32
  <button @onclick="DownloadPdf" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
33
+ <i data-lucide="file-text" class="mr-2 h-4 w-4"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
34
  </button>
35
  </div>
36
  </div>
 
38
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
39
  <div class="flex flex-wrap items-end gap-3">
40
  <div class="min-w-[220px] flex-1">
41
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">@AppLocalization.Text("common.search", "Search")</label>
42
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchIssues", "Search issue, task, project, assignee...")"
43
  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" />
44
  </div>
45
  <div>
46
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">@AppLocalization.Text("common.project", "Project")</label>
47
  <select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
48
+ <option value="0">@AppLocalization.Text("common.all", "All Projects")</option>
49
  @foreach (var project in projects)
50
  {
51
  <option value="@project.Id">@project.Name</option>
 
53
  </select>
54
  </div>
55
  <div>
56
+ <label class="mb-1.5 block text-xs font-medium text-slate-500">@AppLocalization.Text("common.status", "Status")</label>
57
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600">
58
+ <option value="0">@AppLocalization.Text("common.all", "All Statuses")</option>
59
+ <option value="1">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
60
+ <option value="2">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
61
+ <option value="3">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
62
+ <option value="overdue">@AppLocalization.Text("status.overdue", "Overdue")</option>
63
+ <option value="blocked">@AppLocalization.Text("status.blocked", "Blocked")</option>
64
  </select>
65
  </div>
66
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-violet-700">
67
+ <i data-lucide="search" class="mr-2 h-4 w-4"></i> @AppLocalization.Text("action.search", "Search")
68
  </button>
69
  <button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
70
+ <i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i> @AppLocalization.Text("action.reset", "Reset")
71
  </button>
72
  </div>
73
  </div>
74
 
75
  @if (isLoading)
76
  {
77
+ <LoadingSpinner Message="@AppLocalization.Text("report.loadingIssueReport", "Loading issue report...")" />
78
  }
79
  else
80
  {
81
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
82
+ @MetricCard(AppLocalization.Text("report.totalIssues", "Total Issues"), FilteredIssues.Count().ToString(), "file-pen-line", "violet")
83
+ @MetricCard(AppLocalization.Text("report.openIssues", "Open Issues"), OpenIssues.ToString(), "list-todo", "blue")
84
+ @MetricCard(AppLocalization.Text("report.overdueIssues", "Overdue Issues"), OverdueIssues.ToString(), "alarm-clock", "rose")
85
+ @MetricCard(AppLocalization.Text("report.blockedIssues", "Blocked Issues"), BlockedIssues.ToString(), "ban", "amber")
86
  </div>
87
 
88
  <div class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
89
+ <h3 class="text-sm font-semibold text-slate-900">@AppLocalization.Text("report.keyInsights", "Key Insights")</h3>
90
  <div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
91
  @foreach (var insight in Insights)
92
  {
 
100
  {
101
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
102
  <div class="xl:col-span-5">
103
+ <ReportChartCard Title="@AppLocalization.Text("report.issueLifecycleFunnel", "Issue lifecycle funnel")" Subtitle="@AppLocalization.Text("report.issueLifecycleFunnelDesc", "How issues move from backlog to resolution.")" Insight="@AppLocalization.Text("report.issueLifecycleInsight", "A large in-progress or blocked band is a daily execution signal.")">
104
  <FunnelChart Items="IssueFunnelItems" />
105
  </ReportChartCard>
106
  </div>
107
  <div class="xl:col-span-4">
108
+ <ReportChartCard Title="@AppLocalization.Text("report.issueStatusDonut", "Issue status donut")" Subtitle="@AppLocalization.Text("report.issueStatusDonutDesc", "Current status distribution, including overdue pressure.")" Insight="@IssueStatusInsight">
109
+ <DonutStatusChart Items="IssueStatusItems" CenterLabel="@AppLocalization.Text("common.issue", "issues")" />
110
  </ReportChartCard>
111
  </div>
112
  <div class="xl:col-span-3">
113
+ <RadialMetricCard Title="@AppLocalization.Text("report.blockedOverdue", "Blocked / overdue")" Subtitle="@AppLocalization.Text("report.executionRisk", "Execution risk in the filtered issue set.")" Percent="@BlockedOverduePercent" Value="@((BlockedIssues + OverdueIssues).ToString())" ValueLabel="@AppLocalization.Text("report.riskIssues", "risk issues")" CenterLabel="@AppLocalization.Text("report.risk", "risk")" Color="#ef4444" Insight="@AppLocalization.Text("report.focusStandup", "Focus standup discussion here first.")" />
114
  </div>
115
  <div class="xl:col-span-4">
116
+ <ReportChartCard Title="@AppLocalization.Text("report.issuesByProject", "Issues by project")" Subtitle="@AppLocalization.Text("report.issuesByProjectDesc", "Where issue volume is concentrated.")" Insight="@AppLocalization.Text("report.issuesByProjectInsight", "Useful for project-level triage and planning health.")">
117
+ <TopRiskList Items="IssueProjectItems" EmptyText="@AppLocalization.Text("common.noResults", "No project issues found.")" />
118
  </ReportChartCard>
119
  </div>
120
  <div class="xl:col-span-4">
121
+ <ReportChartCard Title="@AppLocalization.Text("report.issuesByAssignee", "Issues by assignee")" Subtitle="@AppLocalization.Text("report.issuesByAssigneeDesc", "Support needs by assignee.")" Insight="@AppLocalization.Text("report.issuesByAssigneeInsight", "Use as a support signal, especially when overdue or blocked items cluster.")">
122
+ <TopRiskList Items="IssueAssigneeItems" EmptyText="@AppLocalization.Text("common.noResults", "No assignee issues found.")" />
123
  </ReportChartCard>
124
  </div>
125
  <div class="xl:col-span-4">
126
+ <ReportChartCard Title="@AppLocalization.Text("report.resolutionAging", "Resolution aging")" Subtitle="@AppLocalization.Text("report.resolutionAgingDesc", "How close issues are to or past due date.")" Insight="@AppLocalization.Text("report.resolutionAgingInsight", "Older overdue buckets should be cleared before new scope expands.")">
127
  <AgingBucketChart Buckets="IssueAgingBuckets" />
128
  </ReportChartCard>
129
  </div>
 
144
  </colgroup>
145
  <thead>
146
  <tr class="border-b border-slate-100 bg-slate-50/60">
147
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.issue", "Issue")</th>
148
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.taskProject", "Task / Project")</th>
149
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.assignee", "Assignee")</th>
150
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.status", "Status")</th>
151
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.hours", "Hours")</th>
152
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.dueDate", "Due")</th>
153
+ <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-500">@AppLocalization.Text("common.risk", "Risk")</th>
154
  </tr>
155
  </thead>
156
  <tbody class="divide-y divide-slate-100">
 
162
  @if (expandedIssues.Contains(issue.Id))
163
  {
164
  <div class="mt-2 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
165
+ <div>@(string.IsNullOrWhiteSpace(issue.Description) ? AppLocalization.Text("common.noDescription", "No description") : issue.Description)</div>
166
+ <div class="mt-1">@AppLocalization.Text("report.delay", "Delay"): @(string.IsNullOrWhiteSpace(issue.DelayReason) ? AppLocalization.Text("common.noneRecorded", "None recorded") : issue.DelayReason)</div>
167
+ <div>@AppLocalization.Text("report.blockedBy", "Blocked by"): @(string.IsNullOrWhiteSpace(issue.BlockedBy) ? AppLocalization.Text("common.none", "None") : issue.BlockedBy)</div>
168
  </div>
169
  }
170
  </td>
 
174
  </button>
175
  @if (expandedTaskDetails.Contains(issue.Id))
176
  {
177
+ <div class="mt-2 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
178
+ <div class="font-medium text-slate-800 break-words">@issue.TaskTitle</div>
179
+ <div class="mt-1 break-words">@issue.ProjectName</div>
180
+ </div>
181
  }
182
  </td>
183
+ <td class="px-4 py-3 text-sm text-slate-600 break-words">@(issue.AssignedToName ?? AppLocalization.Text("common.unassigned", "Unassigned"))</td>
184
  <td class="px-4 py-3"><span class="inline-flex whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)">@GetStatusLabel(issue.StatusId)</span></td>
185
  <td class="px-4 py-3 text-sm text-slate-700 whitespace-nowrap">@((issue.EstimatedHours ?? 0).ToString("N1")) / @((issue.ActualHours ?? 0).ToString("N1"))</td>
186
  <td class="px-4 py-3 text-sm whitespace-nowrap @(IsOverdue(issue) ? "font-semibold text-rose-600" : "text-slate-600")">@DisplayFormats.Date(issue.DueDate)</td>
 
189
  }
190
  @if (!PagedIssues.Any())
191
  {
192
+ <EmptyState ColSpan="7" Icon="file-pen-line" Title="@AppLocalization.Text("report.noIssuesFound", "No issues found")" Subtitle="@AppLocalization.Text("report.noIssuesFoundDesc", "No issues matched the current report filters.")" />
193
  }
194
  </tbody>
195
  </table>
 
310
 
311
  private static bool IsOverdue(IssueDto issue) => issue.StatusId != AppTaskStatus.Done && issue.DueDate.Date < DateTime.Today;
312
  private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}";
313
+ private static string GetStatusLabel(AppTaskStatus status) => AppLocalization.StatusLabel(status);
314
  private static string GetStatusBadge(AppTaskStatus status) => status == AppTaskStatus.Done ? "bg-emerald-50 text-emerald-700" : status == AppTaskStatus.InProgress ? "bg-blue-50 text-blue-700" : "bg-slate-100 text-slate-700";
315
+ private static string GetPriorityLabel(TaskPriority priority) => AppLocalization.PriorityLabel(priority);
316
+ private static string EscalationLabel(int level) => level switch { 1 => AppLocalization.Text("report.teamLead", "Team Lead"), 2 => AppLocalization.Text("report.pm", "PM"), 3 => AppLocalization.Text("report.manager", "Manager"), _ => AppLocalization.Text("common.none", "None") };
317
 
318
  private RenderFragment RiskBadge(IssueDto issue) => builder =>
319
  {
320
+ var text = issue.IsBlocked ? AppLocalization.Text("status.blocked", "Blocked") : issue.EscalationLevel > 0 ? EscalationLabel(issue.EscalationLevel) : IsOverdue(issue) ? AppLocalization.Text("status.overdue", "Overdue") : AppLocalization.Text("status.clear", "Clear");
321
  var cls = issue.IsBlocked || IsOverdue(issue) ? "bg-rose-50 text-rose-700" : issue.EscalationLevel > 0 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700";
322
  builder.OpenElement(0, "span");
323
  builder.AddAttribute(1, "class", $"rounded-full px-2 py-0.5 text-xs font-semibold {cls}");
 
325
  builder.CloseElement();
326
  };
327
 
328
+ private IReadOnlyList<(string Label, int Value)> StatusChart => new[] { (AppLocalization.StatusLabel(AppTaskStatus.Todo), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Todo)), (AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress)), (AppLocalization.StatusLabel(AppTaskStatus.Done), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done)), (AppLocalization.Text("status.overdue", "Overdue"), OverdueIssues) };
329
  private IReadOnlyList<(string Label, int Value)> PriorityChart => Enum.GetValues<TaskPriority>().Select(p => (GetPriorityLabel(p), FilteredIssues.Count(i => i.PriorityId == p))).ToList();
330
+ private IReadOnlyList<(string Label, int Value)> EmployeeChart => FilteredIssues.GroupBy(i => i.AssignedToName ?? AppLocalization.Text("common.unassigned", "Unassigned")).OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
331
  private IReadOnlyList<(string Label, int Value)> ProjectChart => FilteredIssues.GroupBy(i => i.ProjectName).OrderByDescending(g => g.Count()).Take(5).Select(g => (g.Key, g.Count())).ToList();
332
+ private IEnumerable<string> Insights => new[] { $"{OverdueIssues} {AppLocalization.Text("report.overdueIssuesNeedAttention", "overdue issues need attention.")}", $"{BlockedIssues} {AppLocalization.Text("report.issuesAreBlocked", "issues are blocked.")}", $"{AppLocalization.Text("report.actualHoursAre", "Actual hours are")} {FilteredIssues.Sum(i => i.ActualHours ?? 0):N1} {AppLocalization.Text("report.against", "against")} {FilteredIssues.Sum(i => i.EstimatedHours ?? 0):N1} {AppLocalization.Text("report.estimated", "estimated.")}" };
333
  private double BlockedOverduePercent => FilteredIssues.Any() ? (BlockedIssues + OverdueIssues) / (double)FilteredIssues.Count() * 100 : 0;
334
  private string IssueStatusInsight => OpenIssues > FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done)
335
+ ? $"{OpenIssues} {AppLocalization.Text("report.issuesStillOpen", "issues are still open across the selected scope.")}"
336
+ : AppLocalization.Text("report.resolvedIssuesLeading", "Resolved issues are leading this filtered set.");
337
 
338
  private List<ReportChartItem> IssueFunnelItems => new()
339
  {
340
+ new(AppLocalization.Text("report.created", "Created"), FilteredIssues.Count(), "#7c3aed", AppLocalization.Text("report.allReportedIssues", "All reported issues")),
341
+ new(AppLocalization.Text("report.open", "Open"), OpenIssues, "#3b82f6", AppLocalization.Text("report.unresolvedIssues", "Unresolved issues")),
342
+ new(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress), "#06b6d4", AppLocalization.Text("report.currentlyBeingWorked", "Currently being worked")),
343
+ new(AppLocalization.Text("status.blocked", "Blocked"), BlockedIssues, "#f59e0b", AppLocalization.Text("report.needsUnblockReview", "Needs unblock review")),
344
+ new(AppLocalization.Text("status.resolved", "Resolved"), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done), "#10b981", AppLocalization.Text("report.completedIssues", "Completed issues"))
345
  };
346
 
347
  private List<ReportChartItem> IssueStatusItems => new()
348
  {
349
+ new(AppLocalization.StatusLabel(AppTaskStatus.Todo), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Todo), "#94a3b8"),
350
+ new(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.InProgress), "#3b82f6"),
351
+ new(AppLocalization.StatusLabel(AppTaskStatus.Done), FilteredIssues.Count(i => i.StatusId == AppTaskStatus.Done), "#10b981"),
352
+ new(AppLocalization.Text("status.overdue", "Overdue"), OverdueIssues, "#ef4444")
353
  };
354
 
355
  private List<TopRiskItem> IssueProjectItems => FilteredIssues
 
359
  var blocked = g.Count(i => i.IsBlocked);
360
  var overdue = g.Count(IsOverdue);
361
  var score = Math.Min(100, (g.Count() * 10) + (blocked * 25) + (overdue * 20));
362
+ return new TopRiskItem(g.Key, $"{g.Count()} {AppLocalization.Text("common.issue", "issues")} · {blocked} {AppLocalization.Text("status.blocked", "blocked")}", score, overdue > 0 ? $"{overdue} {AppLocalization.Text("status.overdue", "overdue")}" : $"{g.Count()} {AppLocalization.Text("common.issue", "issues")}", overdue > 0 || blocked > 0 ? "rose" : "blue");
363
  })
364
  .OrderByDescending(item => item.Score)
365
  .Take(5)
366
  .ToList();
367
 
368
  private List<TopRiskItem> IssueAssigneeItems => FilteredIssues
369
+ .GroupBy(i => i.AssignedToName ?? AppLocalization.Text("common.unassigned", "Unassigned"))
370
  .Select(g =>
371
  {
372
  var blocked = g.Count(i => i.IsBlocked);
373
  var overdue = g.Count(IsOverdue);
374
  var score = Math.Min(100, (g.Count() * 12) + (blocked * 22) + (overdue * 18));
375
+ return new TopRiskItem(g.Key, $"{g.Count()} {AppLocalization.Text("report.assignedIssues", "assigned issues")}", score, blocked > 0 ? $"{blocked} {AppLocalization.Text("status.blocked", "blocked")}" : $"{overdue} {AppLocalization.Text("status.overdue", "overdue")}", blocked > 0 || overdue > 0 ? "amber" : "blue", AppLocalization.Text("report.supportSignal", "Support signal"));
376
  })
377
  .OrderByDescending(item => item.Score)
378
  .Take(5)
 
380
 
381
  private List<ReportChartItem> IssueAgingBuckets => new()
382
  {
383
+ new(AppLocalization.Text("report.dueLater", "Due later"), FilteredIssues.Count(i => !IsOverdue(i) && i.DueDate.Date > DateTime.Today.AddDays(3)), "#3b82f6"),
384
+ new(AppLocalization.Text("report.dueSoon", "Due soon"), FilteredIssues.Count(i => !IsOverdue(i) && i.DueDate.Date <= DateTime.Today.AddDays(3)), "#f59e0b"),
385
+ new(AppLocalization.Text("report.1to7Late", "1-7d late"), FilteredIssues.Count(i => IsOverdue(i) && (DateTime.Today - i.DueDate.Date).Days <= 7), "#fb7185"),
386
+ new(AppLocalization.Text("report.8PlusLate", "8d+ late"), FilteredIssues.Count(i => IsOverdue(i) && (DateTime.Today - i.DueDate.Date).Days > 7), "#ef4444")
387
  };
388
 
389
  private RenderFragment MetricCard(string label, string value, string icon, string color) => builder =>
 
396
 
397
  private async Task DownloadExcel()
398
  {
399
+ var rows = FilteredIssues.Select(i => new object?[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? AppLocalization.Text("common.unassigned", "Unassigned"), GetStatusLabel(i.StatusId), GetPriorityLabel(i.PriorityId), i.EstimatedHours ?? 0, i.ActualHours ?? 0, i.DueDate, i.IsBlocked ? AppLocalization.Text("status.blocked", "Blocked") : AppLocalization.Text("status.clear", "Clear"), EscalationLabel(i.EscalationLevel), i.DelayReason ?? "" });
400
+ var bytes = SpreadsheetReportBuilder.BuildTableWorkbook(AppLocalization.PageTitle("issueReport", "Issue Report"), AppLocalization.PageTitle("issueReport", "Issue Report"), new[] { AppLocalization.Text("common.issue", "Issue"), AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.assignee", "Assignee"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.priority", "Priority"), AppLocalization.Text("report.estimated", "Estimated"), AppLocalization.Text("report.actual", "Actual"), AppLocalization.Text("common.dueDate", "Due Date"), AppLocalization.Text("status.blocked", "Blocked"), AppLocalization.Text("report.escalation", "Escalation"), AppLocalization.Text("report.delayReason", "Delay Reason") }, rows, new[] { $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}" });
401
  await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Convert.ToBase64String(bytes));
402
  }
403
 
404
  private async Task DownloadPdf()
405
  {
406
+ var rows = FilteredIssues.Select(i => new[] { i.Title, i.TaskTitle, i.ProjectName, i.AssignedToName ?? AppLocalization.Text("common.unassigned", "Unassigned"), GetStatusLabel(i.StatusId), DisplayFormats.Date(i.DueDate), i.IsBlocked ? AppLocalization.Text("status.blocked", "Blocked") : AppLocalization.Text("status.clear", "Clear") });
407
+ var bytes = SimplePdfReportBuilder.BuildTableReport(AppLocalization.PageTitle("issueReport", "Issue Report"), new[] { $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}" }, new[] { AppLocalization.Text("common.issue", "Issue"), AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.assignee", "Assignee"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.dueDate", "Due Date"), AppLocalization.Text("common.issue", "Risk") }, rows);
408
  await JS.InvokeVoidAsync("downloadFileFromBase64", $"IssueReport_{DateTime.Today:yyyyMMdd}.pdf", "application/pdf", Convert.ToBase64String(bytes));
409
  }
410
 
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor CHANGED
@@ -14,29 +14,29 @@
14
  @inject AuthenticationStateProvider AuthStateProvider
15
  @inject MenuAuthorizationService MenuAuthorization
16
 
17
- <PageTitle>Overdue &amp; Delayed Tasks Report</PageTitle>
18
 
19
  <div class="space-y-6">
20
  <!-- Page Header -->
21
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
22
  <div>
23
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Overdue &amp; Delayed Tasks</h2>
24
- <p class="text-slate-500 mt-1">Track tasks that have passed their due date or are at risk of delay.</p>
25
  </div>
26
  <div class="flex flex-wrap items-center gap-3">
27
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
28
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
29
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
30
  </button>
31
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
32
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
33
  </button>
34
  </div>
35
  <button @onclick="DownloadExcel" 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">
36
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
37
  </button>
38
  <button @onclick="DownloadPdf" 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">
39
- <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
40
  </button>
41
  </div>
42
  </div>
@@ -59,7 +59,7 @@
59
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
60
  <div class="flex items-center justify-between">
61
  <div>
62
- <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">High Priority Overdue</p>
63
  <p class="mt-1 text-3xl font-bold text-amber-700">@criticalCount</p>
64
  </div>
65
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
@@ -70,7 +70,7 @@
70
  <div class="rounded-xl border border-orange-100 bg-orange-50 p-5 shadow-sm">
71
  <div class="flex items-center justify-between">
72
  <div>
73
- <p class="text-xs font-semibold uppercase tracking-wider text-orange-500">Due Within 3 Days</p>
74
  <p class="mt-1 text-3xl font-bold text-orange-700">@dueSoonCount</p>
75
  </div>
76
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-100">
@@ -81,7 +81,7 @@
81
  <div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
82
  <div class="flex items-center justify-between">
83
  <div>
84
- <p class="text-xs font-semibold uppercase tracking-wider text-violet-500">Avg Days Overdue</p>
85
  <p class="mt-1 text-3xl font-bold text-violet-700">@avgDaysOverdue</p>
86
  </div>
87
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
@@ -96,8 +96,8 @@
96
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
97
  <div class="flex flex-wrap items-end gap-4">
98
  <div class="flex-1 min-w-[200px]">
99
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Tasks</label>
100
- <input @bind="searchInput" type="text" placeholder="Search by title, assignee, or project..."
101
  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" />
102
  </div>
103
  <div>
@@ -112,26 +112,26 @@
112
  <div>
113
  <label class="block text-xs font-medium text-slate-500 mb-1.5">Delay Type</label>
114
  <select @bind="filterDelayType" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
115
- <option value="all">All Delayed</option>
116
- <option value="overdue">Overdue (Past Due)</option>
117
- <option value="soon">Due Within 3 Days</option>
118
  </select>
119
  </div>
120
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
121
- <i data-lucide="search" class="h-4 w-4 mr-2"></i> Search
122
  </button>
123
  <button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 transition-colors shadow-sm">
124
- <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> Reset
125
  </button>
126
 
127
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
128
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
129
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
130
- <span>Assigned to me</span>
131
  </label>
132
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
133
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
134
- <span>Assigned to my team</span>
135
  </label>
136
  </div>
137
  </div>
@@ -139,7 +139,7 @@
139
 
140
  @if (isLoading)
141
  {
142
- <LoadingSpinner Message="Loading overdue tasks..." />
143
  }
144
  else
145
  {
@@ -147,25 +147,25 @@
147
  {
148
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
149
  <div class="xl:col-span-7">
150
- <ReportChartCard Title="Aging buckets" Subtitle="How long delayed work has been waiting." Insight="@AgingInsight">
151
  <AgingBucketChart Buckets="AgingBuckets" />
152
  </ReportChartCard>
153
  </div>
154
  <div class="xl:col-span-5">
155
- <RadialMetricCard Title="High-priority overdue" Subtitle="Critical work that needs immediate support." Percent="@HighPriorityOverduePercent" Value="@criticalCount.ToString()" ValueLabel="high priority" CenterLabel="critical" Color="#ef4444" Insight="Phrase these as capacity risks so teams can unblock work, not assign blame." />
156
  </div>
157
  <div class="xl:col-span-6">
158
- <ReportChartCard Title="Delays by project" Subtitle="Project areas where delay concentration is highest." Insight="Use this to rebalance planning pressure across projects.">
159
- <TopRiskList Items="DelayProjectItems" EmptyText="No project delay concentration found." />
160
  </ReportChartCard>
161
  </div>
162
  <div class="xl:col-span-6">
163
- <ReportChartCard Title="Delays by assignee" Subtitle="People who may need support or scope adjustment." Insight="Support signal only: high counts should trigger help, not blame.">
164
- <TopRiskList Items="DelayAssigneeItems" EmptyText="No assignee delay concentration found." />
165
  </ReportChartCard>
166
  </div>
167
  <div class="xl:col-span-12">
168
- <RiskMatrixCard Title="Severity vs days overdue" Subtitle="High priority and older delays sit in the upper-right risk zone." Points="OverdueRiskMatrix" Insight="The matrix combines priority severity with age of delay to show triage order." />
169
  </div>
170
  </div>
171
  }
@@ -173,21 +173,21 @@
173
  {
174
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
175
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
176
- <h3 class="text-sm font-semibold text-slate-700">
177
- Showing <span class="text-rose-600 font-bold">@(overdueReportPage?.TotalCount ?? 0)</span> delayed / overdue task(s)
178
  </h3>
179
  </div>
180
  <table class="w-full">
181
  <thead>
182
  <tr class="border-b border-slate-100 bg-slate-50/60">
183
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
184
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
185
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
186
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assignee</th>
187
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
188
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Days Overdue</th>
189
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issues</th>
190
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Delay / Escalation</th>
191
  </tr>
192
  </thead>
193
  <tbody class="divide-y divide-slate-100">
 
14
  @inject AuthenticationStateProvider AuthStateProvider
15
  @inject MenuAuthorizationService MenuAuthorization
16
 
17
+ <PageTitle>@AppLocalization.Text("report.overdueTasksPageTitle", "Overdue & Delayed Tasks Report")</PageTitle>
18
 
19
  <div class="space-y-6">
20
  <!-- Page Header -->
21
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
22
  <div>
23
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("report.overdueTasksHeading", "Overdue & Delayed Tasks")</h2>
24
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("report.overdueTasksDesc", "Track tasks that have passed their due date or are at risk of delay.")</p>
25
  </div>
26
  <div class="flex flex-wrap items-center gap-3">
27
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
28
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
29
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
30
  </button>
31
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
32
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
33
  </button>
34
  </div>
35
  <button @onclick="DownloadExcel" 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">
36
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
37
  </button>
38
  <button @onclick="DownloadPdf" 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">
39
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
40
  </button>
41
  </div>
42
  </div>
 
59
  <div class="rounded-xl border border-amber-100 bg-amber-50 p-5 shadow-sm">
60
  <div class="flex items-center justify-between">
61
  <div>
62
+ <p class="text-xs font-semibold uppercase tracking-wider text-amber-500">@AppLocalization.Text("report.highPriorityOverdue", "High Priority Overdue")</p>
63
  <p class="mt-1 text-3xl font-bold text-amber-700">@criticalCount</p>
64
  </div>
65
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100">
 
70
  <div class="rounded-xl border border-orange-100 bg-orange-50 p-5 shadow-sm">
71
  <div class="flex items-center justify-between">
72
  <div>
73
+ <p class="text-xs font-semibold uppercase tracking-wider text-orange-500">@AppLocalization.Text("report.dueWithin3Days", "Due Within 3 Days")</p>
74
  <p class="mt-1 text-3xl font-bold text-orange-700">@dueSoonCount</p>
75
  </div>
76
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-100">
 
81
  <div class="rounded-xl border border-violet-100 bg-violet-50 p-5 shadow-sm">
82
  <div class="flex items-center justify-between">
83
  <div>
84
+ <p class="text-xs font-semibold uppercase tracking-wider text-violet-500">@AppLocalization.Text("report.avgDaysOverdue", "Avg Days Overdue")</p>
85
  <p class="mt-1 text-3xl font-bold text-violet-700">@avgDaysOverdue</p>
86
  </div>
87
  <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-violet-100">
 
96
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
97
  <div class="flex flex-wrap items-end gap-4">
98
  <div class="flex-1 min-w-[200px]">
99
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchTasks", "Search Tasks")</label>
100
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchByTaskAssigneeProject", "Search by title, assignee, or project...")"
101
  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" />
102
  </div>
103
  <div>
 
112
  <div>
113
  <label class="block text-xs font-medium text-slate-500 mb-1.5">Delay Type</label>
114
  <select @bind="filterDelayType" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
115
+ <option value="all">@AppLocalization.Text("report.allDelayed", "All Delayed")</option>
116
+ <option value="overdue">@AppLocalization.Text("report.overduePastDue", "Overdue (Past Due)")</option>
117
+ <option value="soon">@AppLocalization.Text("report.dueWithin3Days", "Due Within 3 Days")</option>
118
  </select>
119
  </div>
120
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
121
+ <i data-lucide="search" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("action.search", "Search")
122
  </button>
123
  <button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50 transition-colors shadow-sm">
124
+ <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("action.reset", "Reset")
125
  </button>
126
 
127
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
128
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
129
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
130
+ <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span>
131
  </label>
132
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
133
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
134
+ <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span>
135
  </label>
136
  </div>
137
  </div>
 
139
 
140
  @if (isLoading)
141
  {
142
+ <LoadingSpinner Message="@AppLocalization.Text("report.loadingOverdueTasks", "Loading overdue tasks...")" />
143
  }
144
  else
145
  {
 
147
  {
148
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
149
  <div class="xl:col-span-7">
150
+ <ReportChartCard Title="@AppLocalization.Text("report.agingBuckets", "Aging buckets")" Subtitle="@AppLocalization.Text("report.agingBucketsDesc", "How long delayed work has been waiting.")" Insight="@AgingInsight">
151
  <AgingBucketChart Buckets="AgingBuckets" />
152
  </ReportChartCard>
153
  </div>
154
  <div class="xl:col-span-5">
155
+ <RadialMetricCard Title="@AppLocalization.Text("report.highPriorityOverdue", "High-priority overdue")" Subtitle="@AppLocalization.Text("report.highPriorityOverdueDesc", "Critical work that needs immediate support.")" Percent="@HighPriorityOverduePercent" Value="@criticalCount.ToString()" ValueLabel="@AppLocalization.Text("priority.high", "high priority")" CenterLabel="@AppLocalization.Text("report.critical", "critical")" Color="#ef4444" Insight="@AppLocalization.Text("report.overdueInsight", "Phrase these as capacity risks so teams can unblock work, not assign blame.")" />
156
  </div>
157
  <div class="xl:col-span-6">
158
+ <ReportChartCard Title="@AppLocalization.Text("report.delaysByProject", "Delays by project")" Subtitle="@AppLocalization.Text("report.delaysByProjectDesc", "Project areas where delay concentration is highest.")" Insight="@AppLocalization.Text("report.delaysByProjectInsight", "Use this to rebalance planning pressure across projects.")">
159
+ <TopRiskList Items="DelayProjectItems" EmptyText="@AppLocalization.Text("report.noProjectDelay", "No project delay concentration found.")" />
160
  </ReportChartCard>
161
  </div>
162
  <div class="xl:col-span-6">
163
+ <ReportChartCard Title="@AppLocalization.Text("report.delaysByAssignee", "Delays by assignee")" Subtitle="@AppLocalization.Text("report.delaysByAssigneeDesc", "People who may need support or scope adjustment.")" Insight="@AppLocalization.Text("report.delaysByAssigneeInsight", "Support signal only: high counts should trigger help, not blame.")">
164
+ <TopRiskList Items="DelayAssigneeItems" EmptyText="@AppLocalization.Text("report.noAssigneeDelay", "No assignee delay concentration found.")" />
165
  </ReportChartCard>
166
  </div>
167
  <div class="xl:col-span-12">
168
+ <RiskMatrixCard Title="@AppLocalization.Text("report.severityVsDaysOverdue", "Severity vs days overdue")" Subtitle="@AppLocalization.Text("report.severityVsDaysOverdueDesc", "High priority and older delays sit in the upper-right risk zone.")" Points="OverdueRiskMatrix" Insight="@AppLocalization.Text("report.severityMatrixInsight", "The matrix combines priority severity with age of delay to show triage order.")" />
169
  </div>
170
  </div>
171
  }
 
173
  {
174
  <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
175
  <div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
176
+ <h3 class="text-sm font-semibold text-slate-700">
177
+ @AppLocalization.Text("report.showingDelayedTasks", "Showing") <span class="text-rose-600 font-bold">@(overdueReportPage?.TotalCount ?? 0)</span> @AppLocalization.Text("report.delayedOrOverdueTasks", "delayed / overdue task(s)")
178
  </h3>
179
  </div>
180
  <table class="w-full">
181
  <thead>
182
  <tr class="border-b border-slate-100 bg-slate-50/60">
183
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.index", "No.")</th>
184
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.task", "Task")</th>
185
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.project", "Project")</th>
186
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.assignee", "Assignee")</th>
187
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.dueDate", "Due Date")</th>
188
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("report.daysOverdue", "Days Overdue")</th>
189
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.issue", "Issues")</th>
190
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("report.delayEscalation", "Delay / Escalation")</th>
191
  </tr>
192
  </thead>
193
  <tbody class="divide-y divide-slate-100">
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor CHANGED
@@ -12,29 +12,29 @@
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
- <PageTitle>Project Progress Report</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Project Progress</h2>
22
- <p class="text-slate-500 mt-1">View project health using both planning tasks and daily execution issues.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
- <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
38
  </button>
39
  </div>
40
  </div>
@@ -43,13 +43,13 @@
43
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
44
  <div class="flex flex-wrap items-end gap-4">
45
  <div class="flex-1 min-w-[200px]">
46
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Project</label>
47
- <input @bind="searchInput" type="text" placeholder="Search by name..." 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" />
48
  </div>
49
  <div>
50
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Period (Project Start Date)</label>
51
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
- <option value="all">All Time</option>
53
  <option value="daily">Daily (Today)</option>
54
  <option value="monthly">Monthly (This Month)</option>
55
  <option value="yearly">Yearly (This Year)</option>
@@ -70,13 +70,13 @@
70
  }
71
 
72
  <div>
73
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Task Status Completion</label>
74
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
75
- <option value="0">All Tasks</option>
76
- <option value="1">To Do</option>
77
- <option value="2">In Progress</option>
78
- <option value="3">Done</option>
79
- <option value="overdue">Overdue</option>
80
  </select>
81
  </div>
82
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
@@ -89,11 +89,11 @@
89
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
90
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
91
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
92
- <span>Assigned to me</span>
93
  </label>
94
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
95
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
96
- <span>Assigned to my team</span>
97
  </label>
98
  </div>
99
  </div>
@@ -105,7 +105,7 @@
105
  <div class="flex items-center justify-center py-20">
106
  <div class="flex items-center space-x-3 text-slate-500">
107
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
108
- <span class="text-sm font-medium">Loading data...</span>
109
  </div>
110
  </div>
111
  }
 
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
+ <PageTitle>@AppLocalization.Text("report.projectProgressTitle", "Project Progress Report")</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("report.projectProgressHeading", "Project Progress")</h2>
22
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("report.projectProgressDesc", "View project health using both planning tasks and daily execution issues.")</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
38
  </button>
39
  </div>
40
  </div>
 
43
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
44
  <div class="flex flex-wrap items-end gap-4">
45
  <div class="flex-1 min-w-[200px]">
46
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.searchProject", "Search Project")</label>
47
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchByName", "Search by name...")" 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" />
48
  </div>
49
  <div>
50
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.periodProjectStart", "Period (Project Start Date)")</label>
51
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
+ <option value="all">@AppLocalization.Text("common.all", "All Time")</option>
53
  <option value="daily">Daily (Today)</option>
54
  <option value="monthly">Monthly (This Month)</option>
55
  <option value="yearly">Yearly (This Year)</option>
 
70
  }
71
 
72
  <div>
73
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.taskStatusCompletion", "Task Status Completion")</label>
74
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
75
+ <option value="0">@AppLocalization.Text("common.allTasks", "All Tasks")</option>
76
+ <option value="1">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
77
+ <option value="2">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
78
+ <option value="3">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
79
+ <option value="overdue">@AppLocalization.Text("status.overdue", "Overdue")</option>
80
  </select>
81
  </div>
82
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
 
89
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
90
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
91
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
92
+ <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span>
93
  </label>
94
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
95
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
96
+ <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span>
97
  </label>
98
  </div>
99
  </div>
 
105
  <div class="flex items-center justify-center py-20">
106
  <div class="flex items-center space-x-3 text-slate-500">
107
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
108
+ <span class="text-sm font-medium">@AppLocalization.Text("common.loading", "Loading data...")</span>
109
  </div>
110
  </div>
111
  }
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor CHANGED
@@ -12,29 +12,29 @@
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
- <PageTitle>Task Report</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Task Report</h2>
22
- <p class="text-slate-500 mt-1">Detailed analysis and metrics of project tasks.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
- <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
38
  </button>
39
  </div>
40
  </div>
@@ -43,51 +43,51 @@
43
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
44
  <div class="flex flex-wrap items-end gap-4">
45
  <div class="flex-1 min-w-[200px]">
46
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Task</label>
47
- <input @bind="searchInput" type="text" placeholder="Search by title or project..." 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" />
48
  </div>
49
  <div>
50
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Period Filter</label>
51
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
- <option value="all">All Time</option>
53
- <option value="daily">Daily (Today)</option>
54
- <option value="monthly">Monthly (This Month)</option>
55
- <option value="yearly">Yearly (This Year)</option>
56
- <option value="custom">Custom Range</option>
57
  </select>
58
  </div>
59
 
60
  @if (filterPeriod == "custom")
61
  {
62
  <div>
63
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Start Date</label>
64
  <input @bind="filterStartDate" type="date" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
65
  </div>
66
  <div>
67
- <label class="block text-xs font-medium text-slate-500 mb-1.5">End Date</label>
68
  <input @bind="filterEndDate" type="date" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
69
  </div>
70
  }
71
 
72
  <div>
73
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Status</label>
74
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
75
- <option value="uncompleted">Uncompleted</option>
76
- <option value="">All</option>
77
- <option value="1">To Do</option>
78
- <option value="2">In Progress</option>
79
- <option value="3">Done</option>
80
- <option value="overdue">Overdue</option>
81
  </select>
82
  </div>
83
  <div class="relative min-w-[240px]">
84
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Project</label>
85
  <div class="relative">
86
  <input @bind="projectSearchInput"
87
  @onfocus="ShowProjectSuggestions"
88
  @onblur="HideProjectSuggestionsDelayed"
89
  type="text"
90
- placeholder="Search projects..."
91
  autocomplete="off"
92
  class="w-full rounded-lg border border-slate-200 px-3 py-2 pr-8 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
93
  @if (filterProjectId > 0 || !string.IsNullOrWhiteSpace(projectSearchInput))
@@ -95,7 +95,7 @@
95
  <button type="button"
96
  @onmousedown="ClearProjectFilter"
97
  class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
98
- title="Clear project filter">
99
  <i data-lucide="x" class="h-4 w-4"></i>
100
  </button>
101
  }
@@ -107,7 +107,7 @@
107
  <button type="button"
108
  @onmousedown="ClearProjectFilter"
109
  class="w-full px-3 py-2 text-left text-sm text-slate-600 hover:bg-slate-50 transition-colors">
110
- All Projects
111
  </button>
112
  </li>
113
  @foreach (var p in FilteredProjects.Take(10))
@@ -124,20 +124,20 @@
124
  }
125
  </div>
126
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
127
- <i data-lucide="search" class="h-4 w-4 mr-2"></i> Search
128
  </button>
129
  <button @onclick="ResetAllFilters" 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">
130
- <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> Reset
131
  </button>
132
 
133
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
134
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
135
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
136
- <span>Assigned to me</span>
137
  </label>
138
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
139
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
140
- <span>Assigned to my team</span>
141
  </label>
142
  </div>
143
  </div>
@@ -148,7 +148,7 @@
148
  <div class="flex items-center justify-center py-20">
149
  <div class="flex items-center space-x-3 text-slate-500">
150
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
151
- <span class="text-sm font-medium">Loading data...</span>
152
  </div>
153
  </div>
154
  }
@@ -158,7 +158,7 @@
158
  <div class="flex items-start gap-3">
159
  <i data-lucide="alert-triangle" class="mt-0.5 h-5 w-5 shrink-0"></i>
160
  <div>
161
- <p class="font-semibold">Task report could not load.</p>
162
  <p class="mt-1">@errorMessage</p>
163
  </div>
164
  </div>
@@ -167,10 +167,10 @@
167
  else
168
  {
169
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
170
- @TaskMetricCard("Total", FilteredList.Count().ToString(), "file-text", "violet")
171
- @TaskMetricCard("Completed", FilteredList.Count(t => t.StatusId == AppTaskStatus.Done).ToString(), "check-circle-2", "emerald")
172
- @TaskMetricCard("In Progress", FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress).ToString(), "timer", "blue")
173
- @TaskMetricCard("Overdue", FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToString(), "alarm-clock", "rose")
174
  </div>
175
 
176
  <InsightPanel Insights="TaskInsights.ToList()" />
@@ -179,26 +179,26 @@
179
  {
180
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
181
  <div class="xl:col-span-5">
182
- <ReportChartCard Title="Task status mix" Subtitle="Where execution currently sits across the task lifecycle." Insight="@TaskStatusInsight">
183
- <DonutStatusChart Items="TaskStatusItems" CenterLabel="tasks" />
184
  </ReportChartCard>
185
  </div>
186
  <div class="xl:col-span-4">
187
- <ReportChartCard Title="Priority distribution" Subtitle="Planning pressure by task priority." Value="@HighPriorityCount.ToString()" ValueLabel="high priority" Insight="@PriorityInsight">
188
  <StackedProgressBar Segments="PrioritySegments" AriaLabel="Task priority distribution" />
189
  </ReportChartCard>
190
  </div>
191
  <div class="xl:col-span-3">
192
- <TrendSparklineCard Title="Completion movement" Subtitle="Recently completed tasks by creation month." Value="@($"{CompletionRate:0}%")" ValueLabel="completion" Points="CompletionSparkline" Color="#10b981" Insight="@CompletionInsight" />
193
  </div>
194
  <div class="xl:col-span-6">
195
- <ReportChartCard Title="Top project risk" Subtitle="Projects with the highest task and issue pressure." Insight="Score combines task volume, open issues, overdue issues, and critical task health.">
196
- <TopRiskList Items="ProjectRiskItems" EmptyText="No project risk in this filtered set." />
197
  </ReportChartCard>
198
  </div>
199
  <div class="xl:col-span-6">
200
- <ReportChartCard Title="Tasks with most open issues" Subtitle="Execution blockers that may need issue cleanup before progress moves." Insight="Use this to decide where issue triage creates the fastest task movement.">
201
- <TopRiskList Items="TaskIssueRiskItems" EmptyText="No open issue pressure in this filtered set." />
202
  </ReportChartCard>
203
  </div>
204
  </div>
@@ -210,13 +210,13 @@
210
  <table class="w-full">
211
  <thead>
212
  <tr class="border-b border-slate-100 bg-slate-50/60">
213
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
214
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
215
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
216
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
217
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
218
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Health</th>
219
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Issues</th>
220
  </tr>
221
  </thead>
222
  <tbody class="divide-y divide-slate-100">
@@ -232,13 +232,13 @@
232
  </td>
233
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
234
  <td class="px-6 py-3">
235
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(t.StatusId)">@t.StatusName</span>
236
  </td>
237
  <td class="px-6 py-3">
238
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(t.PriorityId)">@t.PriorityName</span>
239
  </td>
240
  <td class="px-6 py-3">
241
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold @GetHealthBadge(t.TaskHealth)">@t.TaskHealth</span>
242
  </td>
243
  <td class="px-6 py-3 text-sm text-slate-600">@t.IssueSummary</td>
244
  </tr>
@@ -247,10 +247,10 @@
247
  <tr class="bg-slate-50/60">
248
  <td colspan="7" class="px-6 py-4">
249
  <div class="grid grid-cols-1 gap-3 text-sm text-slate-600 md:grid-cols-4">
250
- <div><span class="font-semibold text-slate-800">Assignee:</span> @(t.AssignedToUser ?? "Unassigned")</div>
251
- <div><span class="font-semibold text-slate-800">Due:</span> @DisplayFormats.Date(t.DueDate)</div>
252
- <div><span class="font-semibold text-slate-800">Open issues:</span> @t.OpenIssues</div>
253
- <div><span class="font-semibold text-slate-800">Last activity:</span> @DisplayFormats.Date(t.LastActivity)</div>
254
  </div>
255
  </td>
256
  </tr>
@@ -270,8 +270,8 @@
270
  <circle cx="85" cy="85" r="15" fill="white" stroke="#111827" stroke-width="3" />
271
  <path d="M80 85L90 85M85 80L85 90" stroke="#111827" stroke-width="3" stroke-linecap="round" />
272
  </svg>
273
- <h3 class="text-lg font-semibold text-slate-700">No Tasks Found</h3>
274
- <p class="text-sm text-slate-400 max-w-xs">There are no tasks matching your filters. Try adjusting your search query or filters.</p>
275
  </div>
276
  </td>
277
  </tr>
@@ -371,9 +371,9 @@
371
 
372
  private IEnumerable<string> TaskInsights => new[]
373
  {
374
- $"{FilteredList.Count(t => t.TaskHealth == "Critical")} critical tasks need attention.",
375
- $"{FilteredList.Sum(t => t.OpenIssues)} open issues are attached to reported tasks.",
376
- $"{FilteredList.Count(t => t.OverdueIssues > 0)} tasks have overdue issues."
377
  };
378
 
379
  private int FilteredTaskCount => FilteredList.Count();
@@ -383,30 +383,30 @@
383
  private double CompletionRate => FilteredTaskCount > 0 ? CompletedTaskCount / (double)FilteredTaskCount * 100 : 0;
384
 
385
  private string TaskStatusInsight => OpenTaskCount > CompletedTaskCount
386
- ? $"{OpenTaskCount} tasks are still moving through execution."
387
- : "Completed work is leading the current filtered set.";
388
 
389
  private string PriorityInsight => HighPriorityCount > 0
390
- ? $"{HighPriorityCount} high-priority tasks should stay visible during planning review."
391
- : "No high-priority tasks in the current filter.";
392
 
393
  private string CompletionInsight => CompletionRate >= 70
394
- ? "Completion is healthy for the current filter."
395
- : "Completion has room to improve; inspect open issues and overdue work.";
396
 
397
  private List<ReportChartItem> TaskStatusItems => new()
398
  {
399
- new("To Do", FilteredList.Count(t => t.StatusId == AppTaskStatus.Todo), "#94a3b8"),
400
- new("In Progress", FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress), "#3b82f6"),
401
- new("Done", CompletedTaskCount, "#10b981"),
402
- new("Overdue", FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), "#ef4444")
403
  };
404
 
405
  private List<StackedSegment> PrioritySegments => new()
406
  {
407
- new("High", HighPriorityCount, "#ef4444"),
408
- new("Medium", FilteredList.Count(t => t.PriorityId == TaskPriority.Medium), "#f59e0b"),
409
- new("Low", FilteredList.Count(t => t.PriorityId == TaskPriority.Low), "#3b82f6")
410
  };
411
 
412
  private List<SparklinePoint> CompletionSparkline => FilteredList
@@ -427,11 +427,11 @@
427
 
428
  return new TopRiskItem(
429
  g.Key,
430
- $"{g.Count()} tasks · {g.Sum(t => t.OpenIssues)} open issues",
431
  score,
432
- overdue > 0 ? $"{overdue} overdue" : $"{score:0} risk",
433
  tone,
434
- $"{g.Count(t => t.PriorityId == TaskPriority.High)} high-priority tasks");
435
  })
436
  .OrderByDescending(item => item.Score)
437
  .Take(5)
@@ -449,9 +449,9 @@
449
  t.Title,
450
  t.ProjectName,
451
  score,
452
- $"{t.OpenIssues + t.OverdueIssues} issues",
453
  tone,
454
- $"{t.StatusName} · {t.PriorityName}");
455
  })
456
  .ToList();
457
 
@@ -582,7 +582,7 @@
582
  }
583
  else if (fullTask.Result?.IsSuccess == false)
584
  {
585
- errorMessage = fullTask.Result.ErrorMessage ?? "The server returned an error while loading task report data.";
586
  }
587
 
588
  if (pagedTask.Result != null)
@@ -625,28 +625,28 @@
625
 
626
  private async Task DownloadExcel()
627
  {
628
- var rows = FilteredList.Select(t => new object?[]
629
- {
630
- t.Title,
631
- t.ProjectName,
632
- t.StatusName,
633
- t.PriorityName,
634
- t.AssignedToUser ?? "Unassigned",
635
- t.DueDate
636
- });
637
 
638
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
639
- "Task Report",
640
- "Task Report",
641
- new[] { "Task", "Project", "Status", "Priority", "Assigned To", "Due Date" },
642
  rows,
643
  new[]
644
  {
645
- $"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All tasks" : searchQuery)}",
646
- $"Period: {filterPeriod}",
647
- $"Status: {filterStatus switch { "0" => "All", "1" => "To Do", "2" => "In Progress", "3" => "Done", "overdue" => "Overdue", "uncompleted" => "Uncompleted", _ => filterStatus }}",
648
- $"Project: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? "Selected project" : "All projects")}",
649
- $"Generated: {DisplayFormats.Date(DateTime.Today)}"
650
  });
651
 
652
  await JS.InvokeVoidAsync(
@@ -658,29 +658,29 @@
658
 
659
  private async Task DownloadPdf()
660
  {
661
- var summaryLines = new List<string>
662
- {
663
- $"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All tasks" : searchQuery)}",
664
- $"Period: {filterPeriod}",
665
- $"Status: {filterStatus switch { "0" => "All", "1" => "To Do", "2" => "In Progress", "3" => "Done", "overdue" => "Overdue", "uncompleted" => "Uncompleted", _ => filterStatus }}",
666
- $"Project: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? "Selected project" : "All projects")}",
667
- $"Generated: {DisplayFormats.Date(DateTime.Today)}"
668
- };
669
-
670
- var rows = FilteredList.Select(t => new[]
671
- {
672
- t.Title,
673
- t.ProjectName,
674
- t.StatusName,
675
- t.PriorityName,
676
- t.AssignedToUser ?? "Unassigned",
677
- DisplayFormats.Date(t.DueDate)
678
- });
679
-
680
- var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
681
- "Task Report",
682
  summaryLines,
683
- new[] { "Task", "Project", "Status", "Priority", "Assigned To", "Due Date" },
684
  rows);
685
 
686
  await JS.InvokeVoidAsync(
@@ -706,6 +706,13 @@
706
  _ => "bg-slate-100 text-slate-600"
707
  };
708
 
 
 
 
 
 
 
 
709
  private static string GetHealthBadge(string health) => health switch
710
  {
711
  "Critical" => "bg-rose-50 text-rose-700",
@@ -713,6 +720,15 @@
713
  _ => "bg-emerald-50 text-emerald-700"
714
  };
715
 
 
 
 
 
 
 
 
 
 
716
  private RenderFragment TaskMetricCard(string label, string value, string icon, string color) => builder =>
717
  {
718
  builder.OpenElement(0, "div");
 
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
+ <PageTitle>@AppLocalization.PageTitle("taskReport", "Task Report")</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("taskReport", "Task Report")</h2>
22
+ <p class="text-slate-500 mt-1">@AppLocalization.PageDescription("taskReport", "Analyze task activity and export task reports.")</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
38
  </button>
39
  </div>
40
  </div>
 
43
  <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
44
  <div class="flex flex-wrap items-end gap-4">
45
  <div class="flex-1 min-w-[200px]">
46
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchTasks", "Search Task")</label>
47
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchByTitle", "Search by title or project...")" 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" />
48
  </div>
49
  <div>
50
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("report.periodFilter", "Period Filter")</label>
51
  <select @bind="filterPeriod" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
52
+ <option value="all">@AppLocalization.Text("common.all", "All Time")</option>
53
+ <option value="daily">@AppLocalization.Text("report.daily", "Daily (Today)")</option>
54
+ <option value="monthly">@AppLocalization.Text("report.monthly", "Monthly (This Month)")</option>
55
+ <option value="yearly">@AppLocalization.Text("report.yearly", "Yearly (This Year)")</option>
56
+ <option value="custom">@AppLocalization.Text("report.customRange", "Custom Range")</option>
57
  </select>
58
  </div>
59
 
60
  @if (filterPeriod == "custom")
61
  {
62
  <div>
63
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.startDate", "Start Date")</label>
64
  <input @bind="filterStartDate" type="date" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
65
  </div>
66
  <div>
67
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.endDate", "End Date")</label>
68
  <input @bind="filterEndDate" type="date" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
69
  </div>
70
  }
71
 
72
  <div>
73
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
74
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
75
+ <option value="uncompleted">@AppLocalization.Text("status.uncompleted", "Uncompleted")</option>
76
+ <option value="">@AppLocalization.Text("common.all", "All")</option>
77
+ <option value="1">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
78
+ <option value="2">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
79
+ <option value="3">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
80
+ <option value="overdue">@AppLocalization.Text("status.overdue", "Overdue")</option>
81
  </select>
82
  </div>
83
  <div class="relative min-w-[240px]">
84
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.project", "Project")</label>
85
  <div class="relative">
86
  <input @bind="projectSearchInput"
87
  @onfocus="ShowProjectSuggestions"
88
  @onblur="HideProjectSuggestionsDelayed"
89
  type="text"
90
+ placeholder="@AppLocalization.Text("common.searchProjects", "Search projects...")"
91
  autocomplete="off"
92
  class="w-full rounded-lg border border-slate-200 px-3 py-2 pr-8 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
93
  @if (filterProjectId > 0 || !string.IsNullOrWhiteSpace(projectSearchInput))
 
95
  <button type="button"
96
  @onmousedown="ClearProjectFilter"
97
  class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
98
+ title="@AppLocalization.Text("common.clear", "Clear project filter")">
99
  <i data-lucide="x" class="h-4 w-4"></i>
100
  </button>
101
  }
 
107
  <button type="button"
108
  @onmousedown="ClearProjectFilter"
109
  class="w-full px-3 py-2 text-left text-sm text-slate-600 hover:bg-slate-50 transition-colors">
110
+ @AppLocalization.Text("common.all", "All Projects")
111
  </button>
112
  </li>
113
  @foreach (var p in FilteredProjects.Take(10))
 
124
  }
125
  </div>
126
  <button @onclick="ApplyAllFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
127
+ <i data-lucide="search" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("action.search", "Search")
128
  </button>
129
  <button @onclick="ResetAllFilters" 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">
130
+ <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i> @AppLocalization.Text("action.reset", "Reset")
131
  </button>
132
 
133
  <div class="flex flex-wrap items-center gap-6 w-full border-t border-slate-100 pt-3 mt-1">
134
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
135
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
136
+ <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span>
137
  </label>
138
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
139
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
140
+ <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span>
141
  </label>
142
  </div>
143
  </div>
 
148
  <div class="flex items-center justify-center py-20">
149
  <div class="flex items-center space-x-3 text-slate-500">
150
  <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
151
+ <span class="text-sm font-medium">@AppLocalization.Text("common.loading", "Loading data...")</span>
152
  </div>
153
  </div>
154
  }
 
158
  <div class="flex items-start gap-3">
159
  <i data-lucide="alert-triangle" class="mt-0.5 h-5 w-5 shrink-0"></i>
160
  <div>
161
+ <p class="font-semibold">@AppLocalization.Text("report.couldNotLoad", "Task report could not load.")</p>
162
  <p class="mt-1">@errorMessage</p>
163
  </div>
164
  </div>
 
167
  else
168
  {
169
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
170
+ @TaskMetricCard(AppLocalization.Text("report.total", "Total"), FilteredList.Count().ToString(), "file-text", "violet")
171
+ @TaskMetricCard(AppLocalization.Text("status.done", "Completed"), FilteredList.Count(t => t.StatusId == AppTaskStatus.Done).ToString(), "check-circle-2", "emerald")
172
+ @TaskMetricCard(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress).ToString(), "timer", "blue")
173
+ @TaskMetricCard(AppLocalization.Text("status.overdue", "Overdue"), FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done).ToString(), "alarm-clock", "rose")
174
  </div>
175
 
176
  <InsightPanel Insights="TaskInsights.ToList()" />
 
179
  {
180
  <div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
181
  <div class="xl:col-span-5">
182
+ <ReportChartCard Title="@AppLocalization.Text("report.taskStatusMix", "Task status mix")" Subtitle="@AppLocalization.Text("report.taskStatusMixDesc", "Where execution currently sits across the task lifecycle.")" Insight="@TaskStatusInsight">
183
+ <DonutStatusChart Items="TaskStatusItems" CenterLabel="@AppLocalization.Text("common.task", "tasks")" />
184
  </ReportChartCard>
185
  </div>
186
  <div class="xl:col-span-4">
187
+ <ReportChartCard Title="@AppLocalization.Text("report.priorityDistribution", "Priority distribution")" Subtitle="@AppLocalization.Text("report.priorityDistributionDesc", "Planning pressure by task priority.")" Value="@HighPriorityCount.ToString()" ValueLabel="@AppLocalization.Text("priority.high", "high priority")" Insight="@PriorityInsight">
188
  <StackedProgressBar Segments="PrioritySegments" AriaLabel="Task priority distribution" />
189
  </ReportChartCard>
190
  </div>
191
  <div class="xl:col-span-3">
192
+ <TrendSparklineCard Title="@AppLocalization.Text("report.completionMovement", "Completion movement")" Subtitle="@AppLocalization.Text("report.completionMovementDesc", "Recently completed tasks by creation month.")" Value="@($"{CompletionRate:0}%")" ValueLabel="@AppLocalization.Text("report.completion", "completion")" Points="CompletionSparkline" Color="#10b981" Insight="@CompletionInsight" />
193
  </div>
194
  <div class="xl:col-span-6">
195
+ <ReportChartCard Title="@AppLocalization.Text("report.topProjectRisk", "Top project risk")" Subtitle="@AppLocalization.Text("report.topProjectRiskDesc", "Projects with the highest task and issue pressure.")" Insight="@AppLocalization.Text("report.topProjectRiskInsight", "Score combines task volume, open issues, overdue issues, and critical task health.")">
196
+ <TopRiskList Items="ProjectRiskItems" EmptyText="@AppLocalization.Text("common.noResults", "No project risk in this filtered set.")" />
197
  </ReportChartCard>
198
  </div>
199
  <div class="xl:col-span-6">
200
+ <ReportChartCard Title="@AppLocalization.Text("report.tasksWithMostIssues", "Tasks with most open issues")" Subtitle="@AppLocalization.Text("report.tasksWithMostIssuesDesc", "Execution blockers that may need issue cleanup before progress moves.")" Insight="@AppLocalization.Text("report.tasksWithMostIssuesInsight", "Use this to decide where issue triage creates the fastest task movement.")">
201
+ <TopRiskList Items="TaskIssueRiskItems" EmptyText="@AppLocalization.Text("common.noResults", "No open issue pressure in this filtered set.")" />
202
  </ReportChartCard>
203
  </div>
204
  </div>
 
210
  <table class="w-full">
211
  <thead>
212
  <tr class="border-b border-slate-100 bg-slate-50/60">
213
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.index", "No.")</th>
214
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.task", "Task")</th>
215
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.project", "Project")</th>
216
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th>
217
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.priority", "Priority")</th>
218
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("report.health", "Health")</th>
219
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.issue", "Issues")</th>
220
  </tr>
221
  </thead>
222
  <tbody class="divide-y divide-slate-100">
 
232
  </td>
233
  <td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
234
  <td class="px-6 py-3">
235
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(t.StatusId)">@AppLocalization.StatusLabel(t.StatusId)</span>
236
  </td>
237
  <td class="px-6 py-3">
238
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(t.PriorityId)">@AppLocalization.PriorityLabel(t.PriorityId)</span>
239
  </td>
240
  <td class="px-6 py-3">
241
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold @GetHealthBadge(t.TaskHealth)">@GetHealthLabel(t.TaskHealth)</span>
242
  </td>
243
  <td class="px-6 py-3 text-sm text-slate-600">@t.IssueSummary</td>
244
  </tr>
 
247
  <tr class="bg-slate-50/60">
248
  <td colspan="7" class="px-6 py-4">
249
  <div class="grid grid-cols-1 gap-3 text-sm text-slate-600 md:grid-cols-4">
250
+ <div><span class="font-semibold text-slate-800">@AppLocalization.Text("common.assignee", "Assignee"):</span> @(t.AssignedToUser ?? AppLocalization.Text("common.unassigned", "Unassigned"))</div>
251
+ <div><span class="font-semibold text-slate-800">@AppLocalization.Text("common.due", "Due"):</span> @DisplayFormats.Date(t.DueDate)</div>
252
+ <div><span class="font-semibold text-slate-800">@AppLocalization.Text("report.openIssues", "Open issues"):</span> @t.OpenIssues</div>
253
+ <div><span class="font-semibold text-slate-800">@AppLocalization.Text("report.lastActivity", "Last activity"):</span> @DisplayFormats.Date(t.LastActivity)</div>
254
  </div>
255
  </td>
256
  </tr>
 
270
  <circle cx="85" cy="85" r="15" fill="white" stroke="#111827" stroke-width="3" />
271
  <path d="M80 85L90 85M85 80L85 90" stroke="#111827" stroke-width="3" stroke-linecap="round" />
272
  </svg>
273
+ <h3 class="text-lg font-semibold text-slate-700">@AppLocalization.Text("report.noTasksFound", "No Tasks Found")</h3>
274
+ <p class="text-sm text-slate-400 max-w-xs">@AppLocalization.Text("report.noTasksFoundDesc", "There are no tasks matching your filters. Try adjusting your search query or filters.")</p>
275
  </div>
276
  </td>
277
  </tr>
 
371
 
372
  private IEnumerable<string> TaskInsights => new[]
373
  {
374
+ $"{FilteredList.Count(t => t.TaskHealth == "Critical")} {AppLocalization.Text("report.criticalTasksNeedAttention", "critical tasks need attention.")}",
375
+ $"{FilteredList.Sum(t => t.OpenIssues)} {AppLocalization.Text("report.openIssuesAttached", "open issues are attached to reported tasks.")}",
376
+ $"{FilteredList.Count(t => t.OverdueIssues > 0)} {AppLocalization.Text("report.tasksHaveOverdueIssues", "tasks have overdue issues.")}"
377
  };
378
 
379
  private int FilteredTaskCount => FilteredList.Count();
 
383
  private double CompletionRate => FilteredTaskCount > 0 ? CompletedTaskCount / (double)FilteredTaskCount * 100 : 0;
384
 
385
  private string TaskStatusInsight => OpenTaskCount > CompletedTaskCount
386
+ ? $"{OpenTaskCount} {AppLocalization.Text("report.tasksStillMoving", "tasks are still moving through execution.")}"
387
+ : AppLocalization.Text("report.completedWorkLeading", "Completed work is leading the current filtered set.");
388
 
389
  private string PriorityInsight => HighPriorityCount > 0
390
+ ? $"{HighPriorityCount} {AppLocalization.Text("report.highPriorityVisible", "high-priority tasks should stay visible during planning review.")}"
391
+ : AppLocalization.Text("report.noHighPriority", "No high-priority tasks in the current filter.");
392
 
393
  private string CompletionInsight => CompletionRate >= 70
394
+ ? AppLocalization.Text("report.completionHealthy", "Completion is healthy for the current filter.")
395
+ : AppLocalization.Text("report.completionImprove", "Completion has room to improve; inspect open issues and overdue work.");
396
 
397
  private List<ReportChartItem> TaskStatusItems => new()
398
  {
399
+ new(AppLocalization.StatusLabel(AppTaskStatus.Todo), FilteredList.Count(t => t.StatusId == AppTaskStatus.Todo), "#94a3b8"),
400
+ new(AppLocalization.StatusLabel(AppTaskStatus.InProgress), FilteredList.Count(t => t.StatusId == AppTaskStatus.InProgress), "#3b82f6"),
401
+ new(AppLocalization.StatusLabel(AppTaskStatus.Done), CompletedTaskCount, "#10b981"),
402
+ new(AppLocalization.Text("status.overdue", "Overdue"), FilteredList.Count(t => t.DueDate.Date < DateTime.Today && t.StatusId != AppTaskStatus.Done), "#ef4444")
403
  };
404
 
405
  private List<StackedSegment> PrioritySegments => new()
406
  {
407
+ new(AppLocalization.PriorityLabel(TaskPriority.High), HighPriorityCount, "#ef4444"),
408
+ new(AppLocalization.PriorityLabel(TaskPriority.Medium), FilteredList.Count(t => t.PriorityId == TaskPriority.Medium), "#f59e0b"),
409
+ new(AppLocalization.PriorityLabel(TaskPriority.Low), FilteredList.Count(t => t.PriorityId == TaskPriority.Low), "#3b82f6")
410
  };
411
 
412
  private List<SparklinePoint> CompletionSparkline => FilteredList
 
427
 
428
  return new TopRiskItem(
429
  g.Key,
430
+ $"{g.Count()} {AppLocalization.Text("common.task", "tasks")} · {g.Sum(t => t.OpenIssues)} {AppLocalization.Text("common.issue", "open issues")}",
431
  score,
432
+ overdue > 0 ? $"{overdue} {AppLocalization.Text("status.overdue", "overdue")}" : $"{score:0} {AppLocalization.Text("report.risk", "risk")}",
433
  tone,
434
+ $"{g.Count(t => t.PriorityId == TaskPriority.High)} {AppLocalization.Text("report.highPriorityTasks", "high-priority tasks")}");
435
  })
436
  .OrderByDescending(item => item.Score)
437
  .Take(5)
 
449
  t.Title,
450
  t.ProjectName,
451
  score,
452
+ $"{t.OpenIssues + t.OverdueIssues} {AppLocalization.Text("common.issue", "issues")}",
453
  tone,
454
+ $"{AppLocalization.StatusLabel(t.StatusId)} · {AppLocalization.PriorityLabel(t.PriorityId)}");
455
  })
456
  .ToList();
457
 
 
582
  }
583
  else if (fullTask.Result?.IsSuccess == false)
584
  {
585
+ errorMessage = fullTask.Result.ErrorMessage ?? AppLocalization.Text("report.loadError", "The server returned an error while loading task report data.");
586
  }
587
 
588
  if (pagedTask.Result != null)
 
625
 
626
  private async Task DownloadExcel()
627
  {
628
+ var rows = FilteredList.Select(t => new object?[]
629
+ {
630
+ t.Title,
631
+ t.ProjectName,
632
+ AppLocalization.StatusLabel(t.StatusId),
633
+ AppLocalization.PriorityLabel(t.PriorityId),
634
+ t.AssignedToUser ?? AppLocalization.Text("common.na", "Unassigned"),
635
+ t.DueDate
636
+ });
637
 
638
  var workbookBytes = SpreadsheetReportBuilder.BuildTableWorkbook(
639
+ AppLocalization.PageTitle("taskReport", "Task Report"),
640
+ AppLocalization.PageTitle("taskReport", "Task Report"),
641
+ new[] { AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.priority", "Priority"), AppLocalization.Text("common.assignee", "Assigned To"), AppLocalization.Text("common.dueDate", "Due Date") },
642
  rows,
643
  new[]
644
  {
645
+ $"{AppLocalization.Text("common.search", "Search")}: {(string.IsNullOrWhiteSpace(searchQuery) ? AppLocalization.Text("common.all", "All tasks") : searchQuery)}",
646
+ $"{AppLocalization.Text("report.periodFilter", "Period")}: {GetPeriodLabel(filterPeriod)}",
647
+ $"{AppLocalization.Text("common.status", "Status")}: {filterStatus switch { "0" => AppLocalization.Text("common.all", "All"), "1" => AppLocalization.StatusLabel(AppTaskStatus.Todo), "2" => AppLocalization.StatusLabel(AppTaskStatus.InProgress), "3" => AppLocalization.StatusLabel(AppTaskStatus.Done), "overdue" => AppLocalization.Text("status.overdue", "Overdue"), "uncompleted" => AppLocalization.Text("status.uncompleted", "Uncompleted"), _ => filterStatus }}",
648
+ $"{AppLocalization.Text("common.project", "Project")}: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? AppLocalization.Text("report.selectedProject", "Selected project") : AppLocalization.Text("common.all", "All projects"))}",
649
+ $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}"
650
  });
651
 
652
  await JS.InvokeVoidAsync(
 
658
 
659
  private async Task DownloadPdf()
660
  {
661
+ var summaryLines = new List<string>
662
+ {
663
+ $"{AppLocalization.Text("common.search", "Search")}: {(string.IsNullOrWhiteSpace(searchQuery) ? AppLocalization.Text("common.all", "All tasks") : searchQuery)}",
664
+ $"{AppLocalization.Text("report.periodFilter", "Period")}: {GetPeriodLabel(filterPeriod)}",
665
+ $"{AppLocalization.Text("common.status", "Status")}: {filterStatus switch { "0" => AppLocalization.Text("common.all", "All"), "1" => AppLocalization.StatusLabel(AppTaskStatus.Todo), "2" => AppLocalization.StatusLabel(AppTaskStatus.InProgress), "3" => AppLocalization.StatusLabel(AppTaskStatus.Done), "overdue" => AppLocalization.Text("status.overdue", "Overdue"), "uncompleted" => AppLocalization.Text("status.uncompleted", "Uncompleted"), _ => filterStatus }}",
666
+ $"{AppLocalization.Text("common.project", "Project")}: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? AppLocalization.Text("report.selectedProject", "Selected project") : AppLocalization.Text("common.all", "All projects"))}",
667
+ $"{AppLocalization.Text("common.date", "Generated")}: {DisplayFormats.Date(DateTime.Today)}"
668
+ };
669
+
670
+ var rows = FilteredList.Select(t => new[]
671
+ {
672
+ t.Title,
673
+ t.ProjectName,
674
+ AppLocalization.StatusLabel(t.StatusId),
675
+ AppLocalization.PriorityLabel(t.PriorityId),
676
+ t.AssignedToUser ?? AppLocalization.Text("common.na", "Unassigned"),
677
+ DisplayFormats.Date(t.DueDate)
678
+ });
679
+
680
+ var pdfBytes = SimplePdfReportBuilder.BuildTableReport(
681
+ AppLocalization.PageTitle("taskReport", "Task Report"),
682
  summaryLines,
683
+ new[] { AppLocalization.Text("common.task", "Task"), AppLocalization.Text("common.project", "Project"), AppLocalization.Text("common.status", "Status"), AppLocalization.Text("common.priority", "Priority"), AppLocalization.Text("common.assignee", "Assigned To"), AppLocalization.Text("common.dueDate", "Due Date") },
684
  rows);
685
 
686
  await JS.InvokeVoidAsync(
 
706
  _ => "bg-slate-100 text-slate-600"
707
  };
708
 
709
+ private static string GetHealthLabel(string health) => health switch
710
+ {
711
+ "Critical" => AppLocalization.Text("report.healthCritical", "Critical"),
712
+ "At Risk" => AppLocalization.Text("report.healthAtRisk", "At Risk"),
713
+ _ => AppLocalization.Text("report.healthGood", "Healthy")
714
+ };
715
+
716
  private static string GetHealthBadge(string health) => health switch
717
  {
718
  "Critical" => "bg-rose-50 text-rose-700",
 
720
  _ => "bg-emerald-50 text-emerald-700"
721
  };
722
 
723
+ private static string GetPeriodLabel(string period) => period switch
724
+ {
725
+ "daily" => AppLocalization.Text("report.daily", "Daily (Today)"),
726
+ "monthly" => AppLocalization.Text("report.monthly", "Monthly (This Month)"),
727
+ "yearly" => AppLocalization.Text("report.yearly", "Yearly (This Year)"),
728
+ "custom" => AppLocalization.Text("report.customRange", "Custom Range"),
729
+ _ => AppLocalization.Text("common.all", "All Time")
730
+ };
731
+
732
  private RenderFragment TaskMetricCard(string label, string value, string icon, string color) => builder =>
733
  {
734
  builder.OpenElement(0, "div");
TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor CHANGED
@@ -12,29 +12,29 @@
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
- <PageTitle>Time &amp; Effort Tracking Report</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Time &amp; Effort Tracking</h2>
22
- <p class="text-slate-500 mt-1">Review estimated hours, completion timelines, and resource effort distribution across issues.</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
- <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> List View
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
- <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> Chart View
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
- <i data-lucide="download" class="h-4 w-4 mr-2"></i> Export Excel
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
- <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> Export PDF
38
  </button>
39
  </div>
40
  </div>
@@ -95,14 +95,14 @@
95
  <div class="flex flex-col gap-4">
96
  <div class="flex flex-wrap items-end gap-4">
97
  <div class="flex-1 min-w-[200px]">
98
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Issues / Assignee</label>
99
- <input @bind="searchInput" type="text" placeholder="Search issue title, description or assignee..."
100
  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" />
101
  </div>
102
  <div>
103
  <label class="block text-xs font-medium text-slate-500 mb-1.5">Project</label>
104
  <select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
105
- <option value="0">All Projects</option>
106
  @foreach (var proj in allProjects)
107
  {
108
  <option value="@proj.Id">@proj.Name</option>
@@ -110,12 +110,12 @@
110
  </select>
111
  </div>
112
  <div>
113
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Issue Status</label>
114
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
115
- <option value="0">All Statuses</option>
116
- <option value="Todo">To Do</option>
117
- <option value="InProgress">In Progress</option>
118
- <option value="Done">Completed</option>
119
  </select>
120
  </div>
121
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
@@ -129,11 +129,11 @@
129
  <div class="flex flex-wrap items-center gap-6 border-t border-slate-100 pt-3">
130
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
131
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
132
- <span>Assigned to me</span>
133
  </label>
134
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
135
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
136
- <span>Assigned to my team</span>
137
  </label>
138
  </div>
139
  </div>
@@ -141,7 +141,7 @@
141
 
142
  @if (isLoading)
143
  {
144
- <LoadingSpinner Message="Loading timesheet data..." />
145
  }
146
  else
147
  {
 
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
+ <PageTitle>@AppLocalization.Text("report.timeTrackingTitle", "Time & Effort Tracking Report")</PageTitle>
16
 
17
  <div class="space-y-6">
18
  <!-- Page Header -->
19
  <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
20
  <div>
21
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.Text("report.timeTrackingHeading", "Time & Effort Tracking")</h2>
22
+ <p class="text-slate-500 mt-1">@AppLocalization.Text("report.timeTrackingDesc", "Review estimated hours, completion timelines, and resource effort distribution across issues.")</p>
23
  </div>
24
  <div class="flex items-center space-x-3">
25
  <div class="inline-flex rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm">
26
  <button @onclick="() => SetView(false)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(!isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
27
+ <i data-lucide="list" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.listView", "List View")
28
  </button>
29
  <button @onclick="() => SetView(true)" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold @(isChartView ? "bg-violet-600 text-white" : "text-slate-600 hover:text-slate-900") transition-colors">
30
+ <i data-lucide="bar-chart-3" class="h-3.5 w-3.5 mr-1"></i> @AppLocalization.Text("common.chartView", "Chart View")
31
  </button>
32
  </div>
33
  <button @onclick="DownloadExcel" 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">
34
+ <i data-lucide="download" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportExcel", "Export Excel")
35
  </button>
36
  <button @onclick="DownloadPdf" 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">
37
+ <i data-lucide="file-text" class="h-4 w-4 mr-2"></i> @AppLocalization.Text("common.exportPdf", "Export PDF")
38
  </button>
39
  </div>
40
  </div>
 
95
  <div class="flex flex-col gap-4">
96
  <div class="flex flex-wrap items-end gap-4">
97
  <div class="flex-1 min-w-[200px]">
98
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchByIssueAssignee", "Search Issues / Assignee")</label>
99
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchIssues", "Search issue title, description or assignee...")"
100
  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" />
101
  </div>
102
  <div>
103
  <label class="block text-xs font-medium text-slate-500 mb-1.5">Project</label>
104
  <select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
105
+ <option value="0">@AppLocalization.Text("common.allProjects", "All Projects")</option>
106
  @foreach (var proj in allProjects)
107
  {
108
  <option value="@proj.Id">@proj.Name</option>
 
110
  </select>
111
  </div>
112
  <div>
113
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Issue Status")</label>
114
  <select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
115
+ <option value="0">@AppLocalization.Text("common.all", "All Statuses")</option>
116
+ <option value="Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
117
+ <option value="InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
118
+ <option value="Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
119
  </select>
120
  </div>
121
  <button @onclick="ApplyFilters" class="inline-flex items-center rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
 
129
  <div class="flex flex-wrap items-center gap-6 border-t border-slate-100 pt-3">
130
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
131
  <input type="checkbox" @bind="filterAssignToMe" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
132
+ <span>@AppLocalization.Text("report.assignedToMe", "Assigned to me")</span>
133
  </label>
134
  <label class="flex items-center space-x-2 text-sm font-medium text-slate-700 cursor-pointer select-none">
135
  <input type="checkbox" @bind="filterAssignToMyTeam" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />
136
+ <span>@AppLocalization.Text("report.assignedToMyTeam", "Assigned to my team")</span>
137
  </label>
138
  </div>
139
  </div>
 
141
 
142
  @if (isLoading)
143
  {
144
+ <LoadingSpinner Message="@AppLocalization.Text("report.loadingTimesheet", "Loading timesheet data...")" />
145
  }
146
  else
147
  {
TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskAssign.razor CHANGED
@@ -12,7 +12,7 @@
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
- <PageTitle>Task Assignment</PageTitle>
16
 
17
  <SuccessAlert Message="@successMessage" />
18
 
@@ -20,14 +20,14 @@
20
  <!-- Page Header -->
21
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
22
  <div>
23
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Task Assignment</h2>
24
  <p class="text-slate-500 mt-1">Assign tasks to project members and track task owners.</p>
25
  </div>
26
  </div>
27
 
28
  @if (isLoading)
29
  {
30
- <LoadingSpinner Message="Loading tasks and users..." />
31
  }
32
  else
33
  {
@@ -61,7 +61,7 @@
61
  <h3 class="font-bold text-slate-800 text-sm mb-3">Tasks</h3>
62
  <div class="relative flex gap-2">
63
  <div class="relative flex-1">
64
- <input @bind="taskSearchInput" type="text" placeholder="Search tasks..."
65
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
66
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
67
  </div>
@@ -83,18 +83,18 @@
83
  <button @onclick="() => SelectTask(task)"
84
  class="w-full text-left p-4 hover:bg-slate-50/60 transition-colors flex flex-col gap-1.5 @(isSelected ? "bg-violet-50/60 border-l-4 border-violet-600 pl-3" : "")">
85
  <div class="flex items-center justify-between">
86
- <span class="text-xs font-mono font-bold text-slate-400">TASK-#@task.Id</span>
87
  <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-semibold @GetStatusBadge(task.StatusId)">
88
  @GetStatusLabel(task.StatusId)
89
  </span>
90
  </div>
91
  <span class="text-sm font-semibold text-slate-900 @(isSelected ? "text-violet-700" : "")">@task.Title</span>
92
  <div class="flex items-center justify-between text-[10px] text-slate-500">
93
- <span>@(proj?.Name ?? "No Project")</span>
94
  <span class="flex items-center">
95
  <i data-lucide="user" class="h-3 w-3 mr-1"></i>
96
  @{ var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
97
- @(assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : "Unassigned")
98
  </span>
99
  </div>
100
  </button>
@@ -102,7 +102,7 @@
102
  }
103
  else
104
  {
105
- <div class="p-8 text-center text-slate-400 text-xs">No tasks found.</div>
106
  }
107
  </div>
108
  </div>
@@ -116,7 +116,7 @@
116
  <i data-lucide="check-square" class="h-6 w-6"></i>
117
  </div>
118
  <h4 class="text-sm font-bold text-slate-800">No Task Selected</h4>
119
- <p class="text-xs text-slate-500 mt-1 max-w-sm">Select a task from the left panel to assign it to a team member.</p>
120
  </div>
121
  }
122
  else
@@ -124,8 +124,8 @@
124
  <!-- Selected Task Details -->
125
  <div class="p-6 border-b border-slate-100 bg-slate-50">
126
  <div class="flex items-center gap-2 mb-2">
127
- <span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider">Currently Assigning Task</span>
128
- <span class="text-[10px] font-mono font-semibold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">TASK-#@SelectedTask.Id</span>
129
  </div>
130
  <h3 class="text-xl font-bold text-slate-900">@SelectedTask.Title</h3>
131
  @if (!string.IsNullOrEmpty(SelectedTask.Description))
@@ -151,7 +151,7 @@
151
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
152
  <div class="relative max-w-xs flex-1 flex gap-2">
153
  <div class="relative flex-1">
154
- <input @bind="userSearchInput" type="text" placeholder="Search team members..."
155
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
156
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
157
  </div>
@@ -166,7 +166,7 @@
166
  {
167
  <div class="w-40 shrink-0">
168
  <select @bind="selectedRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
169
- <option value="0">All Roles</option>
170
  @foreach (var r in roles)
171
  {
172
  <option value="@r.Id">@r.Name</option>
@@ -181,7 +181,7 @@
181
  </div>
182
  }
183
  <div class="text-xs font-semibold text-slate-600 shrink-0">
184
- Selected: <span class="text-violet-700 font-bold">@(TargetAssigneeId.HasValue ? allUsers.FirstOrDefault(u => u.Id == TargetAssigneeId)?.Username ?? "Selected" : "Unassigned")</span>
185
  </div>
186
  </div>
187
 
@@ -197,8 +197,8 @@
197
  <i data-lucide="user-minus" class="h-4 w-4"></i>
198
  </span>
199
  <div class="min-w-0 flex-1">
200
- <p class="text-sm font-semibold text-slate-800">Unassigned</p>
201
- <p class="text-xs text-slate-400">Remove owner from this task</p>
202
  </div>
203
  </label>
204
 
@@ -237,7 +237,7 @@
237
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving"
238
  class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
239
  <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
240
- @(isSaving ? "Saving..." : "Save Assignment")
241
  </button>
242
  }
243
  </div>
@@ -248,9 +248,9 @@
248
  </div>
249
 
250
  <ConfirmDialog IsVisible="showConfirmDialog"
251
- Title="Save Assignment?"
252
- Message="Are you sure you want to save this task assignment?"
253
- ConfirmText="Yes, Save"
254
  Icon="user-check"
255
  OnConfirm="ConfirmSaveAssignment"
256
  OnCancel="CloseConfirmDialog" />
 
12
  @inject AuthenticationStateProvider AuthStateProvider
13
  @inject MenuAuthorizationService MenuAuthorization
14
 
15
+ <PageTitle>@AppLocalization.PageTitle("taskAssignment", "Task Assignment")</PageTitle>
16
 
17
  <SuccessAlert Message="@successMessage" />
18
 
 
20
  <!-- Page Header -->
21
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
22
  <div>
23
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("taskAssignment", "Task Assignment")</h2>
24
  <p class="text-slate-500 mt-1">Assign tasks to project members and track task owners.</p>
25
  </div>
26
  </div>
27
 
28
  @if (isLoading)
29
  {
30
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingTasksAndUsers", "Loading tasks and users...")" />
31
  }
32
  else
33
  {
 
61
  <h3 class="font-bold text-slate-800 text-sm mb-3">Tasks</h3>
62
  <div class="relative flex gap-2">
63
  <div class="relative flex-1">
64
+ <input @bind="taskSearchInput" type="text" placeholder="@AppLocalization.Text("common.searchTasks", "Search tasks...")"
65
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
66
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
67
  </div>
 
83
  <button @onclick="() => SelectTask(task)"
84
  class="w-full text-left p-4 hover:bg-slate-50/60 transition-colors flex flex-col gap-1.5 @(isSelected ? "bg-violet-50/60 border-l-4 border-violet-600 pl-3" : "")">
85
  <div class="flex items-center justify-between">
86
+ <span class="text-xs font-mono font-bold text-slate-400">TSK-@task.Id</span>
87
  <span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[9px] font-semibold @GetStatusBadge(task.StatusId)">
88
  @GetStatusLabel(task.StatusId)
89
  </span>
90
  </div>
91
  <span class="text-sm font-semibold text-slate-900 @(isSelected ? "text-violet-700" : "")">@task.Title</span>
92
  <div class="flex items-center justify-between text-[10px] text-slate-500">
93
+ <span>@(proj?.Name ?? AppLocalization.Text("common.noProject", "No Project"))</span>
94
  <span class="flex items-center">
95
  <i data-lucide="user" class="h-3 w-3 mr-1"></i>
96
  @{ var assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
97
+ @(assignee != null ? $"{assignee.FirstName} {assignee.LastName}" : AppLocalization.Text("common.unassigned", "Unassigned"))
98
  </span>
99
  </div>
100
  </button>
 
102
  }
103
  else
104
  {
105
+ <div class="p-8 text-center text-slate-400 text-xs">@AppLocalization.Text("common.noTasksFound", "No tasks found.")</div>
106
  }
107
  </div>
108
  </div>
 
116
  <i data-lucide="check-square" class="h-6 w-6"></i>
117
  </div>
118
  <h4 class="text-sm font-bold text-slate-800">No Task Selected</h4>
119
+ <p class="text-xs text-slate-500 mt-1 max-w-sm">@AppLocalization.Text("task.selectTaskPrompt", "Select a task from the left panel to assign it to a team member.")</p>
120
  </div>
121
  }
122
  else
 
124
  <!-- Selected Task Details -->
125
  <div class="p-6 border-b border-slate-100 bg-slate-50">
126
  <div class="flex items-center gap-2 mb-2">
127
+ <span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider">@AppLocalization.Text("common.currentlyAssigningTask", "Currently Assigning Task")</span>
128
+ <span class="text-[10px] font-mono font-semibold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">TSK-@SelectedTask.Id</span>
129
  </div>
130
  <h3 class="text-xl font-bold text-slate-900">@SelectedTask.Title</h3>
131
  @if (!string.IsNullOrEmpty(SelectedTask.Description))
 
151
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
152
  <div class="relative max-w-xs flex-1 flex gap-2">
153
  <div class="relative flex-1">
154
+ <input @bind="userSearchInput" type="text" placeholder="@AppLocalization.Text("common.searchTeamMembers", "Search team members...")"
155
  class="w-full rounded-lg border border-slate-200 pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
156
  <i data-lucide="search" class="absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400"></i>
157
  </div>
 
166
  {
167
  <div class="w-40 shrink-0">
168
  <select @bind="selectedRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
169
+ <option value="0">@AppLocalization.Text("common.allRoles", "All Roles")</option>
170
  @foreach (var r in roles)
171
  {
172
  <option value="@r.Id">@r.Name</option>
 
181
  </div>
182
  }
183
  <div class="text-xs font-semibold text-slate-600 shrink-0">
184
+ @AppLocalization.Text("common.selected", "Selected"): <span class="text-violet-700 font-bold">@(TargetAssigneeId.HasValue ? allUsers.FirstOrDefault(u => u.Id == TargetAssigneeId)?.Username ?? AppLocalization.Text("common.selected", "Selected") : AppLocalization.Text("common.unassigned", "Unassigned"))</span>
185
  </div>
186
  </div>
187
 
 
197
  <i data-lucide="user-minus" class="h-4 w-4"></i>
198
  </span>
199
  <div class="min-w-0 flex-1">
200
+ <p class="text-sm font-semibold text-slate-800">@AppLocalization.Text("common.unassigned", "Unassigned")</p>
201
+ <p class="text-xs text-slate-400">@AppLocalization.Text("common.removeOwnerFromTask", "Remove owner from this task")</p>
202
  </div>
203
  </label>
204
 
 
237
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving"
238
  class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
239
  <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
240
+ @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.saveAssignment", "Save Assignment"))
241
  </button>
242
  }
243
  </div>
 
248
  </div>
249
 
250
  <ConfirmDialog IsVisible="showConfirmDialog"
251
+ Title="@AppLocalization.Text("task.saveAssignmentTitle", "Save Assignment?")"
252
+ Message="@AppLocalization.Text("task.saveAssignmentConfirm", "Are you sure you want to save this task assignment?")"
253
+ ConfirmText="@AppLocalization.Text("issue.yesSave", "Yes, Save")"
254
  Icon="user-check"
255
  OnConfirm="ConfirmSaveAssignment"
256
  OnCancel="CloseConfirmDialog" />
TaskTrackingSystem.WebApp/Components/Pages/Features/Users/Users.razor CHANGED
@@ -8,7 +8,7 @@
8
  @inject AuthenticationStateProvider AuthStateProvider
9
  @inject MenuAuthorizationService MenuAuthorization
10
 
11
- <PageTitle>Users</PageTitle>
12
 
13
  <SuccessAlert Message="@successMessage" />
14
 
@@ -16,14 +16,14 @@
16
  <!-- Page Header -->
17
  <div class="flex items-center justify-between">
18
  <div>
19
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Users</h2>
20
- <p class="text-slate-500 mt-1">Manage system users and their roles.</p>
21
  </div>
22
  @if (canCreateUser)
23
  {
24
  <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">
25
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
26
- New User
27
  </button>
28
  }
29
  </div>
@@ -32,13 +32,13 @@
32
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
33
  <div class="flex flex-wrap items-end gap-4">
34
  <div class="flex-1 min-w-[240px]">
35
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Search Users</label>
36
- <input @bind="searchInput" type="text" placeholder="Search by name, email, username..." 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" />
37
  </div>
38
  <div>
39
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Role</label>
40
  <select @bind="filterRoleIdInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
41
- <option value="0">All Roles</option>
42
  @foreach (var r in roles)
43
  {
44
  <option value="@r.Id">@r.Name</option>
@@ -46,27 +46,27 @@
46
  </select>
47
  </div>
48
  <div>
49
- <label class="block text-xs font-medium text-slate-500 mb-1.5">Status</label>
50
  <select @bind="filterStatusInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
51
- <option value="all">All</option>
52
- <option value="active">Active</option>
53
- <option value="inactive">Inactive</option>
54
  </select>
55
  </div>
56
  <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">
57
  <i data-lucide="search" class="h-4 w-4 mr-2"></i>
58
- Search
59
  </button>
60
  <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">
61
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
62
- Reset
63
  </button>
64
  </div>
65
  </div>
66
 
67
  @if (isLoading)
68
  {
69
- <LoadingSpinner Message="Loading users..." />
70
  }
71
  else
72
  {
@@ -76,12 +76,12 @@
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>
82
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Role</th>
83
- <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
84
- <th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
85
  </tr>
86
  </thead>
87
  <tbody class="divide-y divide-slate-100">
@@ -113,20 +113,20 @@
113
  </td>
114
  <td class="px-6 py-4">
115
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @(user.IsActive ? "bg-emerald-50 text-emerald-700" : "bg-slate-100 text-slate-500")">
116
- @(user.IsActive ? "Active" : "Inactive")
117
  </span>
118
  </td>
119
  <td class="px-6 py-4 text-right">
120
  <div class="flex items-center justify-end space-x-2">
121
  @if (canEditUser)
122
  {
123
- <button @onclick="() => OpenEditModal(user)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
124
  <i data-lucide="pencil" class="h-4 w-4"></i>
125
  </button>
126
  }
127
  @if (canDeleteUser)
128
  {
129
- <button @onclick="() => RequestDeleteConfirmation(user.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
130
  <i data-lucide="trash-2" class="h-4 w-4"></i>
131
  </button>
132
  }
@@ -137,7 +137,7 @@
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>
@@ -159,7 +159,7 @@
159
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
160
  <div class="bg-white rounded-2xl shadow-lg w-full max-w-lg mx-4 overflow-hidden">
161
  <div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
162
- <h3 class="text-lg font-semibold text-slate-900">@(isEditing ? "Edit User" : "New User")</h3>
163
  <button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
164
  <i data-lucide="x" class="h-5 w-5"></i>
165
  </button>
@@ -167,26 +167,26 @@
167
  <div class="p-6 space-y-4">
168
  <div class="grid grid-cols-2 gap-4">
169
  <div>
170
- <label class="block text-sm font-medium text-slate-700 mb-1.5">First Name <span class="text-red-500">*</span></label>
171
  <div class="relative">
172
- <input @bind="formFirstName" type="text" placeholder="First name" class="w-full rounded-lg border @(isFirstNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
173
  @if (isFirstNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
174
  </div>
175
  @if (isFirstNameError) { <p class="mt-1 text-xs text-red-500">@firstNameErrorMsg</p> }
176
  </div>
177
  <div>
178
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Last Name <span class="text-red-500">*</span></label>
179
  <div class="relative">
180
- <input @bind="formLastName" type="text" placeholder="Last name" class="w-full rounded-lg border @(isLastNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
181
  @if (isLastNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
182
  </div>
183
  @if (isLastNameError) { <p class="mt-1 text-xs text-red-500">@lastNameErrorMsg</p> }
184
  </div>
185
  </div>
186
  <div>
187
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Username <span class="text-red-500">*</span></label>
188
  <div class="relative">
189
- <input @bind="formUsername" type="text" placeholder="username" class="w-full rounded-lg border @(isUsernameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
190
  @if (isUsernameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
191
  </div>
192
  @if (isUsernameError) { <p class="mt-1 text-xs text-red-500">@usernameErrorMsg</p> }
@@ -203,7 +203,7 @@
203
  {
204
  <div class="relative">
205
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Password <span class="text-red-500">*</span></label>
206
- <input @bind="formPassword" type="@(showPassword ? "text" : "password")" placeholder="Min 8 characters (A-Z, a-z, 0-9, special)" class="w-full rounded-lg border @(isPasswordError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
207
  <button type="button" @onclick="TogglePassword" class="absolute right-3 top-[34px] text-slate-400 hover:text-slate-600 focus:outline-none">
208
  @if (showPassword)
209
  {
@@ -219,11 +219,11 @@
219
  }
220
  <div class="grid grid-cols-2 gap-4">
221
  <div>
222
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Phone</label>
223
- <input @bind="formPhone" type="text" placeholder="Phone number" 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-slate-900 focus:border-transparent transition-all" />
224
  </div>
225
  <div>
226
- <label class="block text-sm font-medium text-slate-700 mb-1.5">Role</label>
227
  <select @bind="formRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
228
  @foreach (var r in roles)
229
  {
@@ -236,7 +236,7 @@
236
  {
237
  <div class="flex items-center space-x-3">
238
  <input @bind="formIsActive" type="checkbox" id="isActiveToggle" class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
239
- <label for="isActiveToggle" class="text-sm font-medium text-slate-700">Active</label>
240
  </div>
241
  }
242
 
@@ -251,9 +251,9 @@
251
  }
252
  </div>
253
  <div class="flex items-center justify-end space-x-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50">
254
- <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">Cancel</button>
255
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
256
- @(isSaving ? "Saving..." : (isEditing ? "Update" : "Create"))
257
  </button>
258
  </div>
259
  </div>
@@ -497,21 +497,21 @@
497
  isPasswordError = false; passwordErrorMsg = "";
498
  bool hasError = false;
499
 
500
- if (string.IsNullOrWhiteSpace(formFirstName)) { isFirstNameError = true; firstNameErrorMsg = "First name is required."; hasError = true; }
501
- if (string.IsNullOrWhiteSpace(formLastName)) { isLastNameError = true; lastNameErrorMsg = "Last name is required."; hasError = true; }
502
-
503
- if (string.IsNullOrWhiteSpace(formUsername)) { isUsernameError = true; usernameErrorMsg = "Username is required."; hasError = true; }
504
  else if (formUsername.Length < 3) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameMinLength; hasError = true; }
505
  else if (!System.Text.RegularExpressions.Regex.IsMatch(formUsername, @"^[a-zA-Z0-9._]+$")) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameInvalidCharacters; hasError = true; }
506
 
507
- if (string.IsNullOrWhiteSpace(formEmail)) { isEmailError = true; emailErrorMsg = "Email is required."; hasError = true; }
508
 
509
  if (!isEditing)
510
  {
511
  if (string.IsNullOrWhiteSpace(formPassword))
512
  {
513
  isPasswordError = true;
514
- passwordErrorMsg = "Password is required.";
515
  hasError = true;
516
  }
517
  else if (formPassword.Length < 8)
@@ -531,9 +531,9 @@
531
 
532
  if (isEditing)
533
  {
534
- confirmTitle = "Update User?";
535
- confirmMessage = "Are you sure you want to update this user?";
536
- confirmButtonText = "Update";
537
  confirmIcon = "pencil";
538
  confirmIconBg = "bg-amber-50";
539
  confirmIconText = "text-amber-500";
@@ -541,9 +541,9 @@
541
  }
542
  else
543
  {
544
- confirmTitle = "Create User?";
545
- confirmMessage = "Are you sure you want to create this user?";
546
- confirmButtonText = "Create";
547
  confirmIcon = "plus-circle";
548
  confirmIconBg = "bg-emerald-50";
549
  confirmIconText = "text-emerald-500";
@@ -555,9 +555,9 @@
555
 
556
  private void RequestDeleteConfirmation(long id)
557
  {
558
- confirmTitle = "Delete User?";
559
- confirmMessage = "Are you sure you want to delete this user? This action cannot be undone.";
560
- confirmButtonText = "Delete";
561
  confirmIcon = "trash-2";
562
  confirmIconBg = "bg-rose-50";
563
  confirmIconText = "text-rose-500";
@@ -615,7 +615,7 @@
615
  {
616
  if (string.IsNullOrWhiteSpace(formPassword))
617
  {
618
- errorMessage = "Password is required.";
619
  isSaving = false;
620
  return;
621
  }
@@ -652,7 +652,7 @@
652
 
653
  CloseModal();
654
  await LoadUsers();
655
- successMessage = isEditing ? "User updated successfully!" : "User created successfully!";
656
  }
657
  catch (Exception ex)
658
  {
@@ -670,7 +670,7 @@
670
  var currentUserIdStr = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
671
  if (currentUserIdStr != null && long.TryParse(currentUserIdStr, out var currentUserId) && currentUserId == id)
672
  {
673
- await JS.InvokeVoidAsync("alert", "You cannot delete your own account.");
674
  return;
675
  }
676
 
@@ -681,13 +681,13 @@
681
  var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
682
  if (res == null || !res.IsSuccess)
683
  {
684
- await JS.InvokeVoidAsync("alert", res?.ErrorMessage ?? "Failed to delete user.");
685
  return;
686
  }
687
  await LoadUsers();
688
  if (currentPage > TotalPages && TotalPages > 0)
689
  currentPage = TotalPages;
690
- successMessage = "User deleted successfully!";
691
  }
692
  catch (Exception ex)
693
  {
 
8
  @inject AuthenticationStateProvider AuthStateProvider
9
  @inject MenuAuthorizationService MenuAuthorization
10
 
11
+ <PageTitle>@AppLocalization.PageTitle("users", "Users")</PageTitle>
12
 
13
  <SuccessAlert Message="@successMessage" />
14
 
 
16
  <!-- Page Header -->
17
  <div class="flex items-center justify-between">
18
  <div>
19
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("users", "Users")</h2>
20
+ <p class="text-slate-500 mt-1">@AppLocalization.PageDescription("users", "Manage people, status, and account details.")</p>
21
  </div>
22
  @if (canCreateUser)
23
  {
24
  <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">
25
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
26
+ @AppLocalization.Text("common.newUser", "New User")
27
  </button>
28
  }
29
  </div>
 
32
  <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
33
  <div class="flex flex-wrap items-end gap-4">
34
  <div class="flex-1 min-w-[240px]">
35
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchUsers", "Search Users")</label>
36
+ <input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchPlaceholder", "Search by name, email, username...")" 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" />
37
  </div>
38
  <div>
39
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.role", "Role")</label>
40
  <select @bind="filterRoleIdInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
41
+ <option value="0">@AppLocalization.Text("common.all", "All Roles")</option>
42
  @foreach (var r in roles)
43
  {
44
  <option value="@r.Id">@r.Name</option>
 
46
  </select>
47
  </div>
48
  <div>
49
+ <label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
50
  <select @bind="filterStatusInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
51
+ <option value="all">@AppLocalization.Text("common.all", "All")</option>
52
+ <option value="active">@AppLocalization.Text("status.active", "Active")</option>
53
+ <option value="inactive">@AppLocalization.Text("status.inactive", "Inactive")</option>
54
  </select>
55
  </div>
56
  <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">
57
  <i data-lucide="search" class="h-4 w-4 mr-2"></i>
58
+ @AppLocalization.Text("action.search", "Search")
59
  </button>
60
  <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">
61
  <i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
62
+ @AppLocalization.Text("action.reset", "Reset")
63
  </button>
64
  </div>
65
  </div>
66
 
67
  @if (isLoading)
68
  {
69
+ <LoadingSpinner Message="@AppLocalization.Text("common.loadingUsers", "Loading users...")" />
70
  }
71
  else
72
  {
 
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">@AppLocalization.Text("common.user", "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>
82
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.role", "Role")</th>
83
+ <th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th>
84
+ <th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Actions")</th>
85
  </tr>
86
  </thead>
87
  <tbody class="divide-y divide-slate-100">
 
113
  </td>
114
  <td class="px-6 py-4">
115
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @(user.IsActive ? "bg-emerald-50 text-emerald-700" : "bg-slate-100 text-slate-500")">
116
+ @(user.IsActive ? AppLocalization.Text("status.active", "Active") : AppLocalization.Text("status.inactive", "Inactive"))
117
  </span>
118
  </td>
119
  <td class="px-6 py-4 text-right">
120
  <div class="flex items-center justify-end space-x-2">
121
  @if (canEditUser)
122
  {
123
+ <button @onclick="() => OpenEditModal(user)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="@AppLocalization.Text("action.edit", "Edit")">
124
  <i data-lucide="pencil" class="h-4 w-4"></i>
125
  </button>
126
  }
127
  @if (canDeleteUser)
128
  {
129
+ <button @onclick="() => RequestDeleteConfirmation(user.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="@AppLocalization.Text("action.delete", "Delete")">
130
  <i data-lucide="trash-2" class="h-4 w-4"></i>
131
  </button>
132
  }
 
137
  }
138
  else
139
  {
140
+ <EmptyState ColSpan="7" Icon="users" Title="@AppLocalization.Text("common.noResults", "No users found")" Subtitle="@AppLocalization.Text("common.createFirstEntry", "Try adjusting your search or create a new user.")" />
141
  }
142
  </tbody>
143
  </table>
 
159
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
160
  <div class="bg-white rounded-2xl shadow-lg w-full max-w-lg mx-4 overflow-hidden">
161
  <div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
162
+ <h3 class="text-lg font-semibold text-slate-900">@(isEditing ? AppLocalization.Text("user.editUser", "Edit User") : AppLocalization.Text("common.newUser", "New User"))</h3>
163
  <button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
164
  <i data-lucide="x" class="h-5 w-5"></i>
165
  </button>
 
167
  <div class="p-6 space-y-4">
168
  <div class="grid grid-cols-2 gap-4">
169
  <div>
170
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("user.firstName", "First Name") <span class="text-red-500">*</span></label>
171
  <div class="relative">
172
+ <input @bind="formFirstName" type="text" placeholder="@AppLocalization.Text("user.firstName", "First name")" class="w-full rounded-lg border @(isFirstNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
173
  @if (isFirstNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
174
  </div>
175
  @if (isFirstNameError) { <p class="mt-1 text-xs text-red-500">@firstNameErrorMsg</p> }
176
  </div>
177
  <div>
178
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("user.lastName", "Last Name") <span class="text-red-500">*</span></label>
179
  <div class="relative">
180
+ <input @bind="formLastName" type="text" placeholder="@AppLocalization.Text("user.lastName", "Last name")" class="w-full rounded-lg border @(isLastNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
181
  @if (isLastNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
182
  </div>
183
  @if (isLastNameError) { <p class="mt-1 text-xs text-red-500">@lastNameErrorMsg</p> }
184
  </div>
185
  </div>
186
  <div>
187
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.username", "Username") <span class="text-red-500">*</span></label>
188
  <div class="relative">
189
+ <input @bind="formUsername" type="text" placeholder="@AppLocalization.Text("common.username", "username")" class="w-full rounded-lg border @(isUsernameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
190
  @if (isUsernameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
191
  </div>
192
  @if (isUsernameError) { <p class="mt-1 text-xs text-red-500">@usernameErrorMsg</p> }
 
203
  {
204
  <div class="relative">
205
  <label class="block text-sm font-medium text-slate-700 mb-1.5">Password <span class="text-red-500">*</span></label>
206
+ <input @bind="formPassword" type="@(showPassword ? "text" : "password")" placeholder="@AppLocalization.Text("validation.passwordMinLengthRule", "Min 8 characters (A-Z, a-z, 0-9, special)")" class="w-full rounded-lg border @(isPasswordError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
207
  <button type="button" @onclick="TogglePassword" class="absolute right-3 top-[34px] text-slate-400 hover:text-slate-600 focus:outline-none">
208
  @if (showPassword)
209
  {
 
219
  }
220
  <div class="grid grid-cols-2 gap-4">
221
  <div>
222
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.phone", "Phone")</label>
223
+ <input @bind="formPhone" type="text" placeholder="@AppLocalization.Text("common.phoneNumber", "Phone number")" 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-slate-900 focus:border-transparent transition-all" />
224
  </div>
225
  <div>
226
+ <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.role", "Role")</label>
227
  <select @bind="formRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
228
  @foreach (var r in roles)
229
  {
 
236
  {
237
  <div class="flex items-center space-x-3">
238
  <input @bind="formIsActive" type="checkbox" id="isActiveToggle" class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
239
+ <label for="isActiveToggle" class="text-sm font-medium text-slate-700">@AppLocalization.Text("status.active", "Active")</label>
240
  </div>
241
  }
242
 
 
251
  }
252
  </div>
253
  <div class="flex items-center justify-end space-x-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50">
254
+ <button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">@AppLocalization.Text("common.cancel", "Cancel")</button>
255
  <button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
256
+ @(isSaving ? AppLocalization.Text("common.saving", "Saving...") : (isEditing ? AppLocalization.Text("action.update", "Update") : AppLocalization.Text("action.create", "Create")))
257
  </button>
258
  </div>
259
  </div>
 
497
  isPasswordError = false; passwordErrorMsg = "";
498
  bool hasError = false;
499
 
500
+ if (string.IsNullOrWhiteSpace(formFirstName)) { isFirstNameError = true; firstNameErrorMsg = AppLocalization.Text("validation.firstNameRequired", "First name is required."); hasError = true; }
501
+ if (string.IsNullOrWhiteSpace(formLastName)) { isLastNameError = true; lastNameErrorMsg = AppLocalization.Text("validation.lastNameRequired", "Last name is required."); hasError = true; }
502
+
503
+ if (string.IsNullOrWhiteSpace(formUsername)) { isUsernameError = true; usernameErrorMsg = AppLocalization.Text("validation.usernameRequired", "Username is required."); hasError = true; }
504
  else if (formUsername.Length < 3) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameMinLength; hasError = true; }
505
  else if (!System.Text.RegularExpressions.Regex.IsMatch(formUsername, @"^[a-zA-Z0-9._]+$")) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameInvalidCharacters; hasError = true; }
506
 
507
+ if (string.IsNullOrWhiteSpace(formEmail)) { isEmailError = true; emailErrorMsg = AppLocalization.Text("validation.emailRequired", "Email is required."); hasError = true; }
508
 
509
  if (!isEditing)
510
  {
511
  if (string.IsNullOrWhiteSpace(formPassword))
512
  {
513
  isPasswordError = true;
514
+ passwordErrorMsg = AppLocalization.Text("validation.passwordRequired", "Password is required.");
515
  hasError = true;
516
  }
517
  else if (formPassword.Length < 8)
 
531
 
532
  if (isEditing)
533
  {
534
+ confirmTitle = AppLocalization.Text("user.updateUser", "Update User?");
535
+ confirmMessage = AppLocalization.Text("user.updateUserConfirm", "Are you sure you want to update this user?");
536
+ confirmButtonText = AppLocalization.Text("action.update", "Update");
537
  confirmIcon = "pencil";
538
  confirmIconBg = "bg-amber-50";
539
  confirmIconText = "text-amber-500";
 
541
  }
542
  else
543
  {
544
+ confirmTitle = AppLocalization.Text("user.createUser", "Create User?");
545
+ confirmMessage = AppLocalization.Text("user.createUserConfirm", "Are you sure you want to create this user?");
546
+ confirmButtonText = AppLocalization.Text("action.create", "Create");
547
  confirmIcon = "plus-circle";
548
  confirmIconBg = "bg-emerald-50";
549
  confirmIconText = "text-emerald-500";
 
555
 
556
  private void RequestDeleteConfirmation(long id)
557
  {
558
+ confirmTitle = AppLocalization.Text("user.deleteUser", "Delete User?");
559
+ confirmMessage = AppLocalization.Text("user.deleteUserConfirm", "Are you sure you want to delete this user? This action cannot be undone.");
560
+ confirmButtonText = AppLocalization.Text("action.delete", "Delete");
561
  confirmIcon = "trash-2";
562
  confirmIconBg = "bg-rose-50";
563
  confirmIconText = "text-rose-500";
 
615
  {
616
  if (string.IsNullOrWhiteSpace(formPassword))
617
  {
618
+ errorMessage = AppLocalization.Text("validation.passwordRequired", "Password is required.");
619
  isSaving = false;
620
  return;
621
  }
 
652
 
653
  CloseModal();
654
  await LoadUsers();
655
+ successMessage = isEditing ? AppLocalization.Text("result.userUpdated", "User updated successfully!") : AppLocalization.Text("result.userCreated", "User created successfully!");
656
  }
657
  catch (Exception ex)
658
  {
 
670
  var currentUserIdStr = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
671
  if (currentUserIdStr != null && long.TryParse(currentUserIdStr, out var currentUserId) && currentUserId == id)
672
  {
673
+ await JS.InvokeVoidAsync("alert", AppLocalization.Text("user.cannotDeleteSelf", "You cannot delete your own account."));
674
  return;
675
  }
676
 
 
681
  var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
682
  if (res == null || !res.IsSuccess)
683
  {
684
+ await JS.InvokeVoidAsync("alert", res?.ErrorMessage ?? AppLocalization.Text("result.failedToDeleteUser", "Failed to delete user."));
685
  return;
686
  }
687
  await LoadUsers();
688
  if (currentPage > TotalPages && TotalPages > 0)
689
  currentPage = TotalPages;
690
+ successMessage = AppLocalization.Text("result.userDeleted", "User deleted successfully!");
691
  }
692
  catch (Exception ex)
693
  {
TaskTrackingSystem.WebApp/Components/Partial/ConfirmDialog.razor CHANGED
@@ -13,7 +13,7 @@
13
  @if (!HideCancel)
14
  {
15
  <button @onclick="OnCancel" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-colors border border-slate-200 bg-white hover:bg-slate-50" style="color: var(--text-muted);">
16
- Cancel
17
  </button>
18
  }
19
  <button @onclick="OnConfirm" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors @ButtonClass" style="@ConfirmButtonStyle">
@@ -26,9 +26,9 @@
26
 
27
  @code {
28
  [Parameter] public bool IsVisible { get; set; }
29
- [Parameter] public string Title { get; set; } = "Are you sure?";
30
- [Parameter] public string Message { get; set; } = "This action cannot be undone.";
31
- [Parameter] public string ConfirmText { get; set; } = "Confirm";
32
  [Parameter] public string Icon { get; set; } = "alert-triangle";
33
  [Parameter] public string IconBgClass { get; set; } = "";
34
  [Parameter] public string IconTextClass { get; set; } = "";
 
13
  @if (!HideCancel)
14
  {
15
  <button @onclick="OnCancel" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-colors border border-slate-200 bg-white hover:bg-slate-50" style="color: var(--text-muted);">
16
+ @AppLocalization.Text("common.cancel", "Cancel")
17
  </button>
18
  }
19
  <button @onclick="OnConfirm" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors @ButtonClass" style="@ConfirmButtonStyle">
 
26
 
27
  @code {
28
  [Parameter] public bool IsVisible { get; set; }
29
+ [Parameter] public string Title { get; set; } = AppLocalization.Text("common.areYouSure", "Are you sure?");
30
+ [Parameter] public string Message { get; set; } = AppLocalization.Text("common.cannotUndo", "This action cannot be undone.");
31
+ [Parameter] public string ConfirmText { get; set; } = AppLocalization.Text("common.confirmAction", "Confirm");
32
  [Parameter] public string Icon { get; set; } = "alert-triangle";
33
  [Parameter] public string IconBgClass { get; set; } = "";
34
  [Parameter] public string IconTextClass { get; set; } = "";
TaskTrackingSystem.WebApp/Components/Partial/EmptyState.razor CHANGED
@@ -18,8 +18,8 @@
18
  [Parameter] public string Icon { get; set; } = "inbox";
19
 
20
  /// <summary>Primary empty-state message.</summary>
21
- [Parameter] public string Title { get; set; } = "No data yet";
22
 
23
  /// <summary>Secondary hint text.</summary>
24
- [Parameter] public string Subtitle { get; set; } = "Create your first entry to get started.";
25
  }
 
18
  [Parameter] public string Icon { get; set; } = "inbox";
19
 
20
  /// <summary>Primary empty-state message.</summary>
21
+ [Parameter] public string Title { get; set; } = AppLocalization.Text("common.noDataYet", "No data yet");
22
 
23
  /// <summary>Secondary hint text.</summary>
24
+ [Parameter] public string Subtitle { get; set; } = AppLocalization.Text("common.createFirstEntry", "Create your first entry to get started.");
25
  }
TaskTrackingSystem.WebApp/Components/Partial/NotificationBell.razor CHANGED
@@ -23,21 +23,21 @@
23
  <div class="absolute right-0 z-50 mt-3 w-[380px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-lg border border-slate-200 bg-white shadow-lg">
24
  <div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-slate-50/80">
25
  <div>
26
- <p class="text-sm font-semibold text-slate-900">Notifications</p>
27
- <p class="text-xs text-slate-500">@UnreadCount unread</p>
28
  </div>
29
  <div class="flex items-center gap-2">
30
  @if (Notifications.Any(item => item.IsRead))
31
  {
32
  <button type="button" class="text-xs font-semibold text-rose-600 hover:text-rose-700" @onclick="RequestCleanupConfirmation">
33
- Clear read
34
  </button>
35
  }
36
  <button type="button" class="text-xs font-semibold text-violet-600 hover:text-violet-700" @onclick="RefreshAsync">
37
- Refresh
38
  </button>
39
  <button type="button" class="text-xs font-semibold text-slate-500 hover:text-slate-700" @onclick="Close">
40
- Close
41
  </button>
42
  </div>
43
  </div>
@@ -45,7 +45,7 @@
45
  <div class="max-h-[26rem] overflow-y-auto">
46
  @if (IsLoading)
47
  {
48
- <div class="p-4 text-sm text-slate-500">Loading notifications...</div>
49
  }
50
  else if (Notifications.Count == 0)
51
  {
@@ -53,8 +53,8 @@
53
  <div class="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-slate-400">
54
  <i data-lucide="inbox" class="h-5 w-5"></i>
55
  </div>
56
- <p class="text-sm font-medium text-slate-700">No notifications yet</p>
57
- <p class="text-xs text-slate-500 mt-1">Your task alerts will show up here.</p>
58
  </div>
59
  }
60
  else
@@ -92,16 +92,14 @@
92
  </div>
93
 
94
  <ConfirmDialog IsVisible="showCleanupConfirmDialog"
95
- Title="Clear read notifications?"
96
- Message="This will permanently remove all read notifications from your list."
97
- ConfirmText="Clear"
98
  Icon="trash-2"
99
  OnConfirm="ConfirmCleanupAsync"
100
  OnCancel="CloseCleanupConfirmDialog" />
101
 
102
  @code {
103
- private HubConnection? _hubConnection;
104
- private bool _hubHandlersRegistered;
105
  private bool IsOpen;
106
  private bool IsLoading;
107
  private bool showCleanupConfirmDialog;
@@ -115,99 +113,9 @@
115
 
116
  protected override async Task OnAfterRenderAsync(bool firstRender)
117
  {
118
- if (firstRender)
119
- {
120
- await EnsureHubConnectionAsync();
121
- }
122
  await JS.InvokeVoidAsync("initIcons");
123
  }
124
 
125
- private async Task EnsureHubConnectionAsync()
126
- {
127
- var authState = await AuthStateProvider.GetAuthenticationStateAsync();
128
- if (authState.User.Identity?.IsAuthenticated != true)
129
- {
130
- return;
131
- }
132
-
133
- if (_hubConnection != null)
134
- {
135
- return;
136
- }
137
-
138
- var token = ApiClient.GetCurrentToken(authState.User);
139
- if (string.IsNullOrWhiteSpace(token))
140
- {
141
- return;
142
- }
143
-
144
- _hubConnection = new HubConnectionBuilder()
145
- .WithUrl($"{ApiClient.BaseUrl}/hubs/notifications", options =>
146
- {
147
- options.AccessTokenProvider = () => Task.FromResult<string?>(ApiClient.GetCurrentToken() ?? token);
148
- })
149
- .WithAutomaticReconnect()
150
- .Build();
151
-
152
- RegisterHubHandlers();
153
-
154
- try
155
- {
156
- await _hubConnection.StartAsync();
157
- }
158
- catch (Exception ex)
159
- {
160
- Console.WriteLine($"[DEBUG NotificationBell] Hub connection failed: {ex.Message}");
161
- await _hubConnection.DisposeAsync();
162
- _hubConnection = null;
163
- _hubHandlersRegistered = false;
164
- }
165
- }
166
-
167
- private void RegisterHubHandlers()
168
- {
169
- if (_hubConnection == null || _hubHandlersRegistered)
170
- {
171
- return;
172
- }
173
-
174
- _hubConnection.On<NotificationDto>("NotificationCreated", async notification =>
175
- {
176
- await InvokeAsync(() =>
177
- {
178
- Notifications.Insert(0, notification);
179
- UnreadCount++;
180
- StateHasChanged();
181
- });
182
- });
183
-
184
- _hubConnection.On<int>("UnreadCountChanged", async unreadCount =>
185
- {
186
- await InvokeAsync(() =>
187
- {
188
- UnreadCount = unreadCount;
189
- StateHasChanged();
190
- });
191
- });
192
-
193
- _hubConnection.On<long, int>("NotificationRead", async (notificationId, unreadCount) =>
194
- {
195
- await InvokeAsync(() =>
196
- {
197
- var item = Notifications.FirstOrDefault(n => n.Id == notificationId);
198
- if (item != null)
199
- {
200
- item.IsRead = true;
201
- }
202
-
203
- UnreadCount = unreadCount;
204
- StateHasChanged();
205
- });
206
- });
207
-
208
- _hubHandlersRegistered = true;
209
- }
210
-
211
  private async Task ToggleOpenAsync()
212
  {
213
  IsOpen = !IsOpen;
@@ -227,7 +135,6 @@
227
  IsLoading = true;
228
  try
229
  {
230
- await EnsureHubConnectionAsync();
231
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
232
  if (authState.User.Identity?.IsAuthenticated != true)
233
  {
@@ -372,17 +279,5 @@
372
  return value.ToString("dd MMM, HH:mm");
373
  }
374
 
375
- public async ValueTask DisposeAsync()
376
- {
377
- if (_hubConnection != null)
378
- {
379
- try
380
- {
381
- await _hubConnection.DisposeAsync();
382
- }
383
- catch
384
- {
385
- }
386
- }
387
- }
388
  }
 
23
  <div class="absolute right-0 z-50 mt-3 w-[380px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-lg border border-slate-200 bg-white shadow-lg">
24
  <div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-slate-50/80">
25
  <div>
26
+ <p class="text-sm font-semibold text-slate-900">@AppLocalization.Text("notification.notifications", "Notifications")</p>
27
+ <p class="text-xs text-slate-500">@UnreadCount @AppLocalization.Text("notification.unread", "unread")</p>
28
  </div>
29
  <div class="flex items-center gap-2">
30
  @if (Notifications.Any(item => item.IsRead))
31
  {
32
  <button type="button" class="text-xs font-semibold text-rose-600 hover:text-rose-700" @onclick="RequestCleanupConfirmation">
33
+ @AppLocalization.Text("notification.clearRead", "Clear read")
34
  </button>
35
  }
36
  <button type="button" class="text-xs font-semibold text-violet-600 hover:text-violet-700" @onclick="RefreshAsync">
37
+ @AppLocalization.Text("common.refresh", "Refresh")
38
  </button>
39
  <button type="button" class="text-xs font-semibold text-slate-500 hover:text-slate-700" @onclick="Close">
40
+ @AppLocalization.Text("common.close", "Close")
41
  </button>
42
  </div>
43
  </div>
 
45
  <div class="max-h-[26rem] overflow-y-auto">
46
  @if (IsLoading)
47
  {
48
+ <div class="p-4 text-sm text-slate-500">@AppLocalization.Text("notification.loading", "Loading notifications...")</div>
49
  }
50
  else if (Notifications.Count == 0)
51
  {
 
53
  <div class="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-slate-400">
54
  <i data-lucide="inbox" class="h-5 w-5"></i>
55
  </div>
56
+ <p class="text-sm font-medium text-slate-700">@AppLocalization.Text("notification.noneYet", "No notifications yet")</p>
57
+ <p class="text-xs text-slate-500 mt-1">@AppLocalization.Text("notification.emptyState", "Your task alerts will show up here.")</p>
58
  </div>
59
  }
60
  else
 
92
  </div>
93
 
94
  <ConfirmDialog IsVisible="showCleanupConfirmDialog"
95
+ Title='@AppLocalization.Text("notification.clearReadTitle", "Clear read notifications?")'
96
+ Message='@AppLocalization.Text("notification.clearReadMessage", "This will permanently remove all read notifications from your list.")'
97
+ ConfirmText='@AppLocalization.Text("common.clear", "Clear")'
98
  Icon="trash-2"
99
  OnConfirm="ConfirmCleanupAsync"
100
  OnCancel="CloseCleanupConfirmDialog" />
101
 
102
  @code {
 
 
103
  private bool IsOpen;
104
  private bool IsLoading;
105
  private bool showCleanupConfirmDialog;
 
113
 
114
  protected override async Task OnAfterRenderAsync(bool firstRender)
115
  {
 
 
 
 
116
  await JS.InvokeVoidAsync("initIcons");
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  private async Task ToggleOpenAsync()
120
  {
121
  IsOpen = !IsOpen;
 
135
  IsLoading = true;
136
  try
137
  {
 
138
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
139
  if (authState.User.Identity?.IsAuthenticated != true)
140
  {
 
279
  return value.ToString("dd MMM, HH:mm");
280
  }
281
 
282
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
TaskTrackingSystem.WebApp/Components/_Imports.razor CHANGED
@@ -10,6 +10,7 @@
10
  @using TaskTrackingSystem.WebApp.Components
11
  @using TaskTrackingSystem.WebApp.Components.Partial
12
  @using TaskTrackingSystem.Shared
 
13
  @using TaskTrackingSystem.Shared.Models.User
14
  @using TaskTrackingSystem.Shared.Models.Project
15
  @using TaskTrackingSystem.Shared.Models.Task
@@ -20,7 +21,6 @@
20
  @using TaskTrackingSystem.Shared.Models.Notification
21
  @using TaskTrackingSystem.Shared.Models.Menu
22
  @using Microsoft.AspNetCore.Components.Authorization
23
- @using Microsoft.AspNetCore.SignalR.Client
24
  @using TaskTrackingSystem.Shared.Enums
25
  @using TaskTrackingSystem.WebApp.Components.Pages.Features.Dashboard
26
  @using TaskTrackingSystem.WebApp.Components.Pages.Features.Dashboard.Components
 
10
  @using TaskTrackingSystem.WebApp.Components
11
  @using TaskTrackingSystem.WebApp.Components.Partial
12
  @using TaskTrackingSystem.Shared
13
+ @using TaskTrackingSystem.Shared.Localization
14
  @using TaskTrackingSystem.Shared.Models.User
15
  @using TaskTrackingSystem.Shared.Models.Project
16
  @using TaskTrackingSystem.Shared.Models.Task
 
21
  @using TaskTrackingSystem.Shared.Models.Notification
22
  @using TaskTrackingSystem.Shared.Models.Menu
23
  @using Microsoft.AspNetCore.Components.Authorization
 
24
  @using TaskTrackingSystem.Shared.Enums
25
  @using TaskTrackingSystem.WebApp.Components.Pages.Features.Dashboard
26
  @using TaskTrackingSystem.WebApp.Components.Pages.Features.Dashboard.Components
TaskTrackingSystem.WebApp/Localization/UiLanguageService.cs CHANGED
@@ -1,4 +1,6 @@
 
1
  using Microsoft.JSInterop;
 
2
 
3
  namespace TaskTrackingSystem.WebApp.Localization;
4
 
@@ -39,9 +41,24 @@ public sealed class UiLanguageService
39
 
40
  public AppLanguage CurrentLanguage { get; private set; } = AppLanguage.English;
41
 
42
- public UiTextBundle Texts => CurrentLanguage == AppLanguage.Burmese
43
- ? BurmeseTexts
44
- : EnglishTexts;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  public event Action? Changed;
47
 
@@ -66,14 +83,27 @@ public sealed class UiLanguageService
66
 
67
  public string GetCode() => ToCode(CurrentLanguage);
68
 
69
- public static string ToCode(AppLanguage language) => language == AppLanguage.Burmese ? "my" : "en";
70
 
71
  public static AppLanguage FromCode(string? code) =>
72
- string.Equals(code, "my", StringComparison.OrdinalIgnoreCase) ? AppLanguage.Burmese : AppLanguage.English;
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  private void SetLanguageFromCode(string? code)
75
  {
76
  var next = FromCode(code);
 
 
77
  if (next == CurrentLanguage)
78
  {
79
  return;
@@ -83,41 +113,12 @@ public sealed class UiLanguageService
83
  Changed?.Invoke();
84
  }
85
 
86
- private static readonly UiTextBundle EnglishTexts = new()
87
- {
88
- Workspace = "Workspace",
89
- SignIn = "Sign In",
90
- SignOut = "Sign Out",
91
- SignOutTitle = "Sign Out?",
92
- SignOutDescription = "Are you sure you want to sign out of Taskify?",
93
- Cancel = "Cancel",
94
- ConfirmSignOut = "Yes, Sign Out",
95
- AccessDenied = "Access Denied",
96
- AccessDeniedDescription = "You do not have permission to access this page. Please contact your system administrator.",
97
- BackToDashboard = "Back to Dashboard",
98
- NoAccessItems = "No access items are available for this role.",
99
- AuditLogs = "Audit Logs",
100
- LanguageEnglish = "English",
101
- LanguageBurmese = "Burmese",
102
- LanguageTitle = "Switch language"
103
- };
104
-
105
- private static readonly UiTextBundle BurmeseTexts = new()
106
  {
107
- Workspace = "အလုပ်ခွင်",
108
- SignIn = "အကောင့်ဝင်မည်",
109
- SignOut = "အကောင့်ထွက်မည်",
110
- SignOutTitle = "အကောင့်ထွက်မလား?",
111
- SignOutDescription = "Taskify မှ အကောင့်ထွက်ရန် သေချာပါသလား?",
112
- Cancel = "မလုပ်တော့ပါ",
113
- ConfirmSignOut = "ဟုတ်ကဲ့၊ အကောင့်ထွက်မည်",
114
- AccessDenied = "ဝင်ခွင့်မရှိပါ",
115
- AccessDeniedDescription = "ဤစာမျက်နှာကို ဝင်ရောက်ခွင့် မရှိပါ။ စနစ်စီမံခန့်ခွဲသူကို ဆက်သွယ်ပါ။",
116
- BackToDashboard = "ဒက်ရှ်ဘုတ်သို့ ပြန်သွားမည်",
117
- NoAccessItems = "ဤအခန်းကဏ္ဍအတွက် ဝင်ခွင့်မရသည့် မီနူးမရှိပါ။",
118
- AuditLogs = "မှတ်တမ်းများ",
119
- LanguageEnglish = "အင်္ဂလိပ်",
120
- LanguageBurmese = "မြန်မာ",
121
- LanguageTitle = "ဘာသာစကားပြောင်းရန်"
122
- };
123
  }
 
1
+ using System.Globalization;
2
  using Microsoft.JSInterop;
3
+ using TaskTrackingSystem.Shared.Localization;
4
 
5
  namespace TaskTrackingSystem.WebApp.Localization;
6
 
 
41
 
42
  public AppLanguage CurrentLanguage { get; private set; } = AppLanguage.English;
43
 
44
+ public UiTextBundle Texts => new()
45
+ {
46
+ Workspace = AppLocalization.Text("common.workspace", "Workspace"),
47
+ SignIn = AppLocalization.Text("common.signIn", "Sign In"),
48
+ SignOut = AppLocalization.Text("common.signOut", "Sign Out"),
49
+ SignOutTitle = AppLocalization.Text("common.signOutTitle", "Sign Out?"),
50
+ SignOutDescription = AppLocalization.Text("common.signOutDescription", "Are you sure you want to sign out of Taskify?"),
51
+ Cancel = AppLocalization.Text("common.cancel", "Cancel"),
52
+ ConfirmSignOut = AppLocalization.Text("common.confirmSignOut", "Yes, Sign Out"),
53
+ AccessDenied = AppLocalization.Text("common.accessDenied", "Access Denied"),
54
+ AccessDeniedDescription = AppLocalization.Text("common.accessDeniedDescription", "You do not have permission to access this page. Please contact your system administrator."),
55
+ BackToDashboard = AppLocalization.Text("common.backToDashboard", "Back to Dashboard"),
56
+ NoAccessItems = AppLocalization.Text("common.noAccessItems", "No access items are available for this role."),
57
+ AuditLogs = AppLocalization.Text("page.auditLogs", "Audit Logs"),
58
+ LanguageEnglish = AppLocalization.Text("common.languageEnglish", "English"),
59
+ LanguageBurmese = AppLocalization.Text("common.languageBurmese", "Burmese"),
60
+ LanguageTitle = AppLocalization.Text("common.currentLanguage", "Switch language")
61
+ };
62
 
63
  public event Action? Changed;
64
 
 
83
 
84
  public string GetCode() => ToCode(CurrentLanguage);
85
 
86
+ public static string ToCode(AppLanguage language) => language == AppLanguage.Burmese ? "my-MM" : "en-US";
87
 
88
  public static AppLanguage FromCode(string? code) =>
89
+ string.Equals(code, "my", StringComparison.OrdinalIgnoreCase) ||
90
+ string.Equals(code, "my-MM", StringComparison.OrdinalIgnoreCase)
91
+ ? AppLanguage.Burmese
92
+ : AppLanguage.English;
93
+
94
+ public string T(string key, string fallback = "") => AppLocalization.Text(key, fallback);
95
+ public string Menu(string menuCode, string fallback = "") => AppLocalization.Menu(menuCode, fallback);
96
+ public string PageTitle(string key, string fallback = "") => AppLocalization.PageTitle(key, fallback);
97
+ public string PageDescription(string key, string fallback = "") => AppLocalization.PageDescription(key, fallback);
98
+ public string StatusLabel(TaskTrackingSystem.Shared.Enums.AppTaskStatus status) => AppLocalization.StatusLabel(status);
99
+ public string PriorityLabel(TaskTrackingSystem.Shared.Enums.TaskPriority priority) => AppLocalization.PriorityLabel(priority);
100
+ public string EscalationLabel(int level) => AppLocalization.EscalationLabel(level);
101
 
102
  private void SetLanguageFromCode(string? code)
103
  {
104
  var next = FromCode(code);
105
+ ApplyCulture(next);
106
+
107
  if (next == CurrentLanguage)
108
  {
109
  return;
 
113
  Changed?.Invoke();
114
  }
115
 
116
+ private static void ApplyCulture(AppLanguage language)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  {
118
+ var culture = language == AppLanguage.Burmese ? CultureInfo.GetCultureInfo("my-MM") : CultureInfo.GetCultureInfo("en-US");
119
+ CultureInfo.CurrentCulture = culture;
120
+ CultureInfo.CurrentUICulture = culture;
121
+ CultureInfo.DefaultThreadCurrentCulture = culture;
122
+ CultureInfo.DefaultThreadCurrentUICulture = culture;
123
+ }
 
 
 
 
 
 
 
 
 
 
124
  }