@page "/issues" @page "/issues/all" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using TaskTrackingSystem.Shared @using TaskTrackingSystem.Shared.Enums @using TaskTrackingSystem.Shared.Models.Issue @using TaskTrackingSystem.Shared.Models.Project @using TaskTrackingSystem.Shared.Models.Task @using TaskTrackingSystem.Shared.Models.User @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider @inject MenuAuthorizationService MenuAuthorization @inject NavigationManager Navigation @AppLocalization.PageTitle("issues", "Issues")

@AppLocalization.PageTitle("issues", "Issues")

@AppLocalization.Text("page.issues.desc", "Track daily work items, update progress, and review what was done.")

@if (canCreateIssue) { }
@if (isLoading) { } else {
@if (issues.Any()) { @foreach (var entry in issues.Select((issue, index) => (issue, index))) { var issue = entry.issue; } } else { }
@AppLocalization.Text("common.taskCode", "Task Code") @AppLocalization.Text("common.issue", "Issue") @AppLocalization.Text("common.hours", "Hours") @AppLocalization.Text("common.status", "Status") @AppLocalization.Text("common.priority", "Priority") @AppLocalization.Text("common.date", "Date") @AppLocalization.Text("common.assignee", "Assignee") @AppLocalization.Text("common.risk", "Risk") @AppLocalization.Text("common.actions", "Actions")
@GetTaskCode(issue.TaskId) @(issue.ActualHours.HasValue ? AppLocalization.Number(issue.ActualHours.Value) : "-") @AppLocalization.Text("common.hoursAbbrev", "hr") @GetStatusLabel(issue.StatusId) @GetPriorityLabel(issue.PriorityId) @DisplayFormats.Date(issue.DueDate) @(!string.IsNullOrWhiteSpace(Content.GetText(issue.AssignedToName, issue.AssignedToNameMy)) ? Content.GetText(issue.AssignedToName, issue.AssignedToNameMy) : AppLocalization.Text("common.unassigned", "Unassigned")) @if (issue.IsBlocked) { @AppLocalization.Text("status.blocked", "Blocked") } else if (issue.EscalationLevel > 0) { @GetEscalationLabel(issue.EscalationLevel) } else { @AppLocalization.Text("status.clear", "Clear") }
@if (canEditIssue) { } @if (canDeleteIssue) { }
}
@if (showModal) {

@AppLocalization.Text("common.editIssue", "Edit Issue")

#@AppLocalization.Number(editingId)

@AppLocalization.Text("common.task", "Task")

@SelectedTaskLabel

@AppLocalization.Text("common.project", "Project")

@SelectedProjectLabel

@if (!string.IsNullOrWhiteSpace(errorMessage)) {

@errorMessage

}
} @code { private bool isLoading = true; private bool isSaving; private bool canCreateIssue; private bool canEditIssue; private bool canDeleteIssue; private bool isAdmin; private bool showModal; private bool showDeleteConfirm; private bool showSaveConfirm; [Parameter, SupplyParameterFromQuery(Name = "created")] public bool Created { get; set; } private string? successMessage; private string? errorMessage; private string deleteConfirmMessage = AppLocalization.Text("issue.deleteConfirmMessage", "Are you sure you want to delete this issue?"); private long deleteIssueId; private long currentUserId; private List issues = new(); private List tasks = new(); private List projects = new(); private List projectMembers = new(); private int currentPage = 1; private int pageSize = 10; private int totalCount; private int totalPages; private string searchInput = ""; private long taskIdInput; private long projectIdInput; private AppTaskStatus? statusIdInput; private TaskPriority? priorityIdInput; private bool assignedOnly; private long editingId; private long formTaskId; private string formTitle = ""; private string? formTitleMy; private string? formDescription; private string? formDescriptionMy; private long? formAssignedTo; private decimal? formActualHours = 7m; private DateTime formStartDate = DateTime.Today; private DateTime formDueDate = DateTime.Today; private AppTaskStatus formStatusId = AppTaskStatus.Todo; private TaskPriority formPriorityId = TaskPriority.Medium; private string? formDelayReason; private string? formDelayReasonMy; private bool formIsBlocked; private string? formBlockedBy; private string? formBlockedByMy; private int formEscalationLevel; private bool burmeseGenerated; private bool englishGenerated; private bool burmeseNeedsReview; private bool englishNeedsReview; private string SelectedTaskLabel => tasks.FirstOrDefault(t => t.Id == formTaskId) is { } task ? Content.GetTaskTitle(task) : $"{AppLocalization.Text("common.task", "Task")} #{AppLocalization.Number(formTaskId)}"; private string SelectedProjectLabel => tasks.FirstOrDefault(t => t.Id == formTaskId) is { } task ? projects.FirstOrDefault(p => p.Id == task.ProjectId) is { } project ? Content.GetProjectName(project) : $"{AppLocalization.Text("common.project", "Project")} #{AppLocalization.Number(task.ProjectId)}" : AppLocalization.Text("issue.unknownProject", "Unknown project"); private IEnumerable filteredProjectMembers => projectMembers.Where(u => u.IsActive); private static string GetTaskCode(long taskId) => $"TSK-{AppLocalization.LocalizeDigits(taskId.ToString("D4", System.Globalization.CultureInfo.InvariantCulture))}"; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authState.User; isAdmin = user.IsInRole("Admin"); if (long.TryParse(user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var userId)) { currentUserId = userId; } canCreateIssue = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Issues_Create"); canEditIssue = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Issues_Update"); canDeleteIssue = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Issues_Delete"); if (Created) { successMessage = AppLocalization.Text("issue.created", "Issue created successfully!"); } var client = ApiClient.CreateClient(); try { var issuesTask = LoadIssuesPageAsync(client); var tasksTask = client.GetFromJsonAsync>("Task", Serialization.CaseInsensitive); var projectsTask = client.GetFromJsonAsync>("Project", Serialization.CaseInsensitive); var usersTask = client.GetFromJsonAsync>("User", Serialization.CaseInsensitive); await System.Threading.Tasks.Task.WhenAll(issuesTask, tasksTask, projectsTask, usersTask); tasks = tasksTask.Result ?? new(); projects = projectsTask.Result ?? new(); projectMembers = usersTask.Result?.Where(u => u.IsActive).OrderBy(u => u.FirstName).ThenBy(u => u.LastName).ToList() ?? new(); ApplyPageResult(issuesTask.Result); } catch (Exception ex) { errorMessage = AppLocalization.Text("issue.loadFailed", "Unable to load issues. Please try again."); Console.WriteLine($"Load issues error: {ex.Message}"); } finally { isLoading = false; } } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private async Task?> LoadIssuesPageAsync(HttpClient? client = null) { client ??= ApiClient.CreateClient(); return await client.GetFromJsonAsync>(BuildIssuesUrl(), Serialization.CaseInsensitive); } private void ApplyPageResult(PagedResult? result) { issues = result?.Items ?? new(); totalCount = result?.TotalCount ?? 0; totalPages = result?.TotalPages ?? 0; currentPage = result?.Page ?? currentPage; pageSize = result?.PageSize ?? pageSize; RemoveOrphanedIssues(); } private bool HasActiveParentRecords(IssueDto issue) { var task = tasks.FirstOrDefault(t => t.Id == issue.TaskId); return task != null && task.ProjectId == issue.ProjectId && projects.Any(project => project.Id == task.ProjectId); } private void RemoveOrphanedIssues() { var activeIssues = issues.Where(HasActiveParentRecords).ToList(); var removedCount = issues.Count - activeIssues.Count; if (removedCount == 0) { return; } issues = activeIssues; totalCount = Math.Max(0, totalCount - removedCount); totalPages = pageSize > 0 ? (int)Math.Ceiling(totalCount / (double)pageSize) : 0; } private async Task LoadIssues() { var client = ApiClient.CreateClient(); var result = await LoadIssuesPageAsync(client); ApplyPageResult(result); } private string BuildIssuesUrl() { var query = new List { $"page={currentPage}", $"limit={pageSize}" }; if (!string.IsNullOrWhiteSpace(searchInput)) { query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}"); } if (taskIdInput > 0) { query.Add($"taskId={taskIdInput}"); } if (projectIdInput > 0) { query.Add($"projectId={projectIdInput}"); } if (statusIdInput.HasValue) { query.Add($"statusId={(int)statusIdInput.Value}"); } if (priorityIdInput.HasValue) { query.Add($"priorityId={(int)priorityIdInput.Value}"); } if (assignedOnly) { query.Add("assignedOnly=true"); } return $"Issue?{string.Join("&", query)}"; } private async Task ApplySearch() { currentPage = 1; await LoadIssues(); } private async Task ResetSearch() { searchInput = ""; taskIdInput = 0; projectIdInput = 0; statusIdInput = null; priorityIdInput = null; assignedOnly = false; currentPage = 1; await LoadIssues(); } private async Task HandlePageChanged(int page) { currentPage = page; await LoadIssues(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadIssues(); } private void GoToAddIssue() { Navigation.NavigateTo("/issues/add"); } private async Task OpenEditModal(IssueDto issue) { if (!canEditIssue) { return; } // The page can still contain a DTO loaded before its task/project was deleted. // Never open an issue editor for an orphaned issue. if (!HasActiveParentRecords(issue)) { issues.RemoveAll(current => current.Id == issue.Id); totalCount = Math.Max(0, totalCount - 1); totalPages = pageSize > 0 ? (int)Math.Ceiling(totalCount / (double)pageSize) : 0; return; } editingId = issue.Id; formTaskId = issue.TaskId; formTitle = issue.Title; formTitleMy = issue.TitleMy; formDescription = issue.Description; formDescriptionMy = issue.DescriptionMy; formAssignedTo = issue.AssignedTo; formActualHours = issue.ActualHours; formStartDate = issue.StartDate; formDueDate = issue.DueDate; formStatusId = issue.StatusId; formPriorityId = issue.PriorityId; formDelayReason = issue.DelayReason; formDelayReasonMy = issue.DelayReasonMy; formIsBlocked = issue.IsBlocked; formBlockedBy = issue.BlockedBy; formBlockedByMy = issue.BlockedByMy; formEscalationLevel = issue.EscalationLevel; burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false; errorMessage = null; showModal = true; await LoadMembersForTaskAsync(issue.TaskId); } private void CloseModal() { showModal = false; errorMessage = null; editingId = 0; } private void MoveIssueTitleToEnglish() => formTitle = formTitleMy ?? string.Empty; private void MoveIssueTitleToBurmese() => formTitleMy = formTitle; private void MoveIssueDescriptionToEnglish() => formDescription = formDescriptionMy; private void MoveIssueDescriptionToBurmese() => formDescriptionMy = formDescription; private void MoveIssueDelayToEnglish() => formDelayReason = formDelayReasonMy; private void MoveIssueDelayToBurmese() => formDelayReasonMy = formDelayReason; private void MoveIssueBlockedByToEnglish() => formBlockedBy = formBlockedByMy; private void MoveIssueBlockedByToBurmese() => formBlockedByMy = formBlockedBy; private void MarkGenerated(bool english) { if (english) { englishGenerated = true; englishNeedsReview = true; } else { burmeseGenerated = true; burmeseNeedsReview = true; } } private Task GenerateIssueEnglishTitle(TranslationResult result) { formTitle = result.TranslatedText ?? string.Empty; MarkGenerated(true); return Task.CompletedTask; } private Task GenerateIssueBurmeseTitle(TranslationResult result) { formTitleMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; } private Task GenerateIssueEnglishDescription(TranslationResult result) { formDescription = result.TranslatedText; MarkGenerated(true); return Task.CompletedTask; } private Task GenerateIssueBurmeseDescription(TranslationResult result) { formDescriptionMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; } private Task GenerateIssueEnglishDelay(TranslationResult result) { formDelayReason = result.TranslatedText; MarkGenerated(true); return Task.CompletedTask; } private Task GenerateIssueBurmeseDelay(TranslationResult result) { formDelayReasonMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; } private Task GenerateIssueEnglishBlockedBy(TranslationResult result) { formBlockedBy = result.TranslatedText; MarkGenerated(true); return Task.CompletedTask; } private Task GenerateIssueBurmeseBlockedBy(TranslationResult result) { formBlockedByMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; } private async Task LoadMembersForTaskAsync(long taskId) { projectMembers.Clear(); var task = tasks.FirstOrDefault(t => t.Id == taskId); if (task == null) { return; } var client = ApiClient.CreateClient(); try { var membersResult = await client.GetFromJsonAsync>>($"Project/{task.ProjectId}/members", Serialization.CaseInsensitive); if (membersResult?.IsSuccess == true && membersResult.Value != null) { projectMembers = membersResult.Value.Where(u => u.IsActive).OrderBy(u => u.FirstName).ThenBy(u => u.LastName).ToList(); } } catch (Exception ex) { Console.WriteLine($"Failed to load issue assignees: {ex.Message}"); } } private void RequestSaveConfirmation() { showSaveConfirm = true; } private void CloseSaveConfirm() { showSaveConfirm = false; } private async Task ConfirmSave() { showSaveConfirm = false; await SaveIssueAsync(); } private async Task SaveIssueAsync() { if (editingId <= 0) { return; } isSaving = true; errorMessage = null; try { var client = ApiClient.CreateClient(); var dto = new UpdateIssueDto { Title = formTitle.Trim(), TitleMy = formTitleMy, Description = formDescription, DescriptionMy = formDescriptionMy, AssignedTo = formAssignedTo, ActualHours = formActualHours, DelayReason = formDelayReason, DelayReasonMy = formDelayReasonMy, IsBlocked = formIsBlocked, BlockedBy = formBlockedBy, BlockedByMy = formBlockedByMy, EscalationLevel = formEscalationLevel, StartDate = formStartDate, DueDate = formDueDate, StatusId = formStatusId, PriorityId = formPriorityId }; var response = await client.PutAsJsonAsync($"Issue/{editingId}", dto); if (response.IsSuccessStatusCode) { successMessage = AppLocalization.Text("issue.updated", "Issue updated successfully!"); showModal = false; await LoadIssues(); } else { errorMessage = AppLocalization.Text("issue.updateFailed", "Unable to update the issue. Please try again."); } } catch (Exception ex) { errorMessage = AppLocalization.Text("issue.updateFailed", "Unable to update the issue. Please try again."); Console.WriteLine($"Update issue error: {ex.Message}"); } finally { isSaving = false; } } private void RequestDeleteConfirmation(long issueId) { if (!canDeleteIssue) { return; } deleteIssueId = issueId; deleteConfirmMessage = AppLocalization.Text("issue.deleteConfirmMessage", "Are you sure you want to delete this issue?"); showDeleteConfirm = true; } private void CloseDeleteConfirm() { showDeleteConfirm = false; } private async Task ConfirmDelete() { showDeleteConfirm = false; try { var client = ApiClient.CreateClient(); var response = await client.DeleteAsync($"Issue/{deleteIssueId}"); if (response.IsSuccessStatusCode) { successMessage = AppLocalization.Text("issue.deleted", "Issue deleted successfully!"); await LoadIssues(); } else { errorMessage = AppLocalization.Text("issue.deleteFailed", "Unable to delete the issue. Please try again."); } } catch (Exception ex) { errorMessage = AppLocalization.Text("issue.deleteFailed", "Unable to delete the issue. Please try again."); Console.WriteLine($"Delete issue error: {ex.Message}"); } } private static async Task ReadErrorMessageAsync(HttpResponseMessage response) { var body = await response.Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(body)) { return $"Request failed with status {(int)response.StatusCode}."; } try { using var doc = System.Text.Json.JsonDocument.Parse(body); if (doc.RootElement.TryGetProperty("message", out var message)) { return message.GetString() ?? body; } } catch { } return body; } private string GetStatusBadge(AppTaskStatus statusId) => statusId switch { AppTaskStatus.Todo => "bg-slate-100 text-slate-700", AppTaskStatus.InProgress => "bg-blue-100 text-blue-700", AppTaskStatus.Done => "bg-emerald-100 text-emerald-700", _ => "bg-slate-100 text-slate-600" }; private string GetStatusLabel(AppTaskStatus statusId) => statusId switch { AppTaskStatus.Todo => AppLocalization.Text("status.todo", "To Do"), AppTaskStatus.InProgress => AppLocalization.Text("status.inprogress", "In Progress"), AppTaskStatus.Done => AppLocalization.Text("status.done", "Done"), _ => AppLocalization.Text("common.unknown", "Unknown") }; private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch { TaskPriority.Low => "bg-slate-100 text-slate-600", TaskPriority.Medium => "bg-amber-100 text-amber-700", TaskPriority.High => "bg-red-100 text-red-700", _ => "bg-slate-100 text-slate-600" }; private string GetPriorityLabel(TaskPriority priorityId) => priorityId switch { TaskPriority.Low => AppLocalization.Text("priority.low", "Low"), TaskPriority.Medium => AppLocalization.Text("priority.medium", "Medium"), TaskPriority.High => AppLocalization.Text("priority.high", "High"), _ => AppLocalization.Text("common.unknown", "Unknown") }; private static string GetEscalationLabel(int level) => AppLocalization.EscalationLabel(level); }