Spaces:
Running
Running
User commited on
Commit ·
8c88a89
1
Parent(s): 2358ba3
add: language switch
Browse files- TaskTrackingSystem.Shared/Localization/AppLocalization.cs +615 -0
- TaskTrackingSystem.Shared/Models/Issue/CreateIssueDto.cs +4 -3
- TaskTrackingSystem.Shared/Models/Issue/UpdateIssueDto.cs +4 -3
- TaskTrackingSystem.Shared/Result.cs +8 -7
- TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +45 -40
- TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +8 -8
- TaskTrackingSystem.WebApp/Components/Pages/Features/Audit/AuditLogs.razor +13 -13
- TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/Login.razor +14 -14
- TaskTrackingSystem.WebApp/Components/Pages/Features/Auth/ResetPassword.razor +12 -12
- TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor +48 -48
- TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor +76 -76
- TaskTrackingSystem.WebApp/Components/Pages/Features/Projects/ProjectAssign.razor +26 -26
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/Components/ReportToolbar.razor +4 -4
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/EmployeeReport.razor +27 -27
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/IssueReport.razor +80 -80
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/OverdueTasksReport.razor +37 -37
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/ProjectProgressReport.razor +20 -20
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TaskReport.razor +139 -123
- TaskTrackingSystem.WebApp/Components/Pages/Features/Reports/TimeTrackingReport.razor +18 -18
- TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/TaskAssign.razor +20 -20
- TaskTrackingSystem.WebApp/Components/Pages/Features/Users/Users.razor +57 -57
- TaskTrackingSystem.WebApp/Components/Partial/ConfirmDialog.razor +4 -4
- TaskTrackingSystem.WebApp/Components/Partial/EmptyState.razor +2 -2
- TaskTrackingSystem.WebApp/Components/Partial/NotificationBell.razor +12 -117
- TaskTrackingSystem.WebApp/Components/_Imports.razor +1 -1
- 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
|
| 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 |
-
_ =>
|
| 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 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
|
| 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.
|
| 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>
|
| 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">
|
| 19 |
-
<p class="text-slate-500 mt-1">
|
| 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
|
| 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
|
| 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">
|
| 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
|
| 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">
|
| 100 |
-
<option value="@AppTaskStatus.InProgress">
|
| 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="
|
| 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 ? "
|
| 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 |
-
|
| 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
|
| 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
|
| 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
|
| 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="">
|
| 71 |
-
<option value="@AppTaskStatus.Todo">
|
| 72 |
-
<option value="@AppTaskStatus.InProgress">
|
| 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="">
|
| 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">
|
| 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 |
-
|
| 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
|
| 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">
|
| 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
|
| 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">
|
| 322 |
-
<option value="@AppTaskStatus.InProgress">
|
| 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 ? "
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 ? "
|
| 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 &
|
| 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 &
|
| 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">
|
| 75 |
-
<option value="2">
|
| 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">
|
| 60 |
-
<option value="2">
|
| 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 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 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) =>
|
| 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) =>
|
| 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[] { (
|
| 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(
|
| 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(
|
| 350 |
-
new(
|
| 351 |
-
new(
|
| 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 &
|
| 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 &
|
| 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 |
-
|
| 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">
|
| 77 |
-
<option value="2">
|
| 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">
|
| 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">
|
| 78 |
-
<option value="2">
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
| 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.
|
| 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.
|
| 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(
|
| 400 |
-
new(
|
| 401 |
-
new(
|
| 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(
|
| 408 |
-
new(
|
| 409 |
-
new(
|
| 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.
|
| 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 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 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" =>
|
| 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 |
-
|
| 662 |
-
|
| 663 |
-
$"Search: {(string.IsNullOrWhiteSpace(searchQuery) ? "All tasks" : searchQuery)}",
|
| 664 |
-
$"Period: {filterPeriod}",
|
| 665 |
-
$"Status: {filterStatus switch { "0" => "All", "1" =>
|
| 666 |
-
$"Project: {(filterProjectId > 0 ? projects.FirstOrDefault(p => p.Id == filterProjectId)?.Name ?? "Selected project" : "All projects")}",
|
| 667 |
-
$"Generated: {DisplayFormats.Date(DateTime.Today)}"
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 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 &
|
| 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 &
|
| 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">
|
| 117 |
-
<option value="InProgress">
|
| 118 |
-
<option value="Done">
|
| 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">
|
| 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">
|
| 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 ? "
|
| 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
|
| 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
|
| 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 =>
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 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 |
}
|