Spaces:
Runtime error
Runtime error
Download TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor from Yuruyuruu/task-tracking-system: direct link, hf CLI and curl.
- Browser
- Download file 52.2 kB
-
https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor
- Command line
-
hf download hf://spaces/Yuruyuruu/task-tracking-system/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor
-
curl -L -o Issues.razor https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/Issues.razor
52.2 kB
| @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 | |
| <PageTitle>@AppLocalization.PageTitle("issues", "Issues")</PageTitle> | |
| <SuccessAlert Message="@successMessage" /> | |
| <div class="space-y-6"> | |
| <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("issues", "Issues")</h2> | |
| <p class="text-slate-500 mt-1">@AppLocalization.Text("page.issues.desc", "Track daily work items, update progress, and review what was done.")</p> | |
| </div> | |
| @if (canCreateIssue) | |
| { | |
| <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"> | |
| <i data-lucide="plus" class="h-4 w-4 mr-2"></i> | |
| @AppLocalization.Text("common.addIssue", "Add Issue") | |
| </button> | |
| } | |
| </div> | |
| @if (isLoading) | |
| { | |
| <LoadingSpinner Message="@AppLocalization.Text("common.loadingIssues", "Loading issues...")" /> | |
| } | |
| else | |
| { | |
| <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm"> | |
| <div class="grid grid-cols-1 lg:grid-cols-6 gap-3"> | |
| <div class="lg:col-span-2"> | |
| <input @bind="searchInput" @bind:event="oninput" type="text" placeholder="@AppLocalization.Text("common.searchIssues", "Search issue title, description or assignee...")" | |
| 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" /> | |
| </div> | |
| <div> | |
| <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"> | |
| <option value="0">@AppLocalization.Text("common.allTasks", "All tasks")</option> | |
| @foreach (var task in tasks) | |
| { | |
| <option value="@task.Id">@Content.GetTaskTitle(task)</option> | |
| } | |
| </select> | |
| </div> | |
| <div> | |
| <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"> | |
| <option value="0">@AppLocalization.Text("common.allProjects", "All projects")</option> | |
| @foreach (var project in projects) | |
| { | |
| <option value="@project.Id">@Content.GetProjectName(project)</option> | |
| } | |
| </select> | |
| </div> | |
| <div> | |
| <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"> | |
| <option value="">@AppLocalization.Text("common.all", "All")</option> | |
| <option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option> | |
| <option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option> | |
| <option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <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"> | |
| <option value="">@AppLocalization.Text("common.all", "All")</option> | |
| <option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option> | |
| <option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option> | |
| <option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-3 flex flex-wrap items-center gap-3"> | |
| <label class="inline-flex items-center gap-2 text-sm text-slate-700"> | |
| <input type="checkbox" @bind="assignedOnly" class="h-4 w-4 rounded border-slate-300 accent-violet-600" /> | |
| @AppLocalization.Text("common.assignedToMeOnly", "Assigned to me only") | |
| </label> | |
| <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"> | |
| <i data-lucide="search" class="h-4 w-4 mr-2"></i> | |
| @AppLocalization.Text("action.search", "Search") | |
| </button> | |
| <button @onclick="ResetSearch" title="@AppLocalization.Text("action.reset", "Reset")" aria-label="@AppLocalization.Text("action.reset", "Reset")" class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white p-2 text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="rotate-ccw" class="h-4 w-4 text-slate-500"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden"> | |
| <div class="overflow-hidden"> | |
| <table class="w-full table-fixed"> | |
| <colgroup> | |
| <col class="w-[12%]" /> | |
| <col class="w-[24%]" /> | |
| <col class="w-[11%]" /> | |
| <col class="w-[11%]" /> | |
| <col class="w-[12%]" /> | |
| <col class="w-[12%]" /> | |
| <col class="w-[12%]" /> | |
| <col class="w-[8%]" /> | |
| <col class="w-[10%]" /> | |
| </colgroup> | |
| <thead> | |
| <tr class="border-b border-slate-100 bg-slate-50/60"> | |
| <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> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.issue", "Issue")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.hours", "Hours")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.priority", "Priority")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.date", "Date")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.assignee", "Assignee")</th> | |
| <th class="px-5 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.risk", "Risk")</th> | |
| <th class="px-5 py-3 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Actions")</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-100 text-[13px]"> | |
| @if (issues.Any()) | |
| { | |
| @foreach (var entry in issues.Select((issue, index) => (issue, index))) | |
| { | |
| var issue = entry.issue; | |
| <tr class="hover:bg-slate-50/50 transition-colors align-top"> | |
| <td class="px-5 py-4 text-left whitespace-nowrap"> | |
| <span class="font-mono text-[11px] font-semibold tracking-wide text-indigo-700"> | |
| @GetTaskCode(issue.TaskId) | |
| </span> | |
| </td> | |
| <td class="px-5 py-4 text-left"> | |
| <button @onclick="() => OpenEditModal(issue)" class="min-w-0 text-left text-sm font-semibold leading-5 text-slate-900 hover:text-violet-600 transition-colors break-words"> | |
| @Content.GetIssueTitle(issue) | |
| </button> | |
| </td> | |
| <td class="px-5 py-4 text-slate-600 whitespace-nowrap"> | |
| <span class="font-medium text-slate-700">@(issue.ActualHours.HasValue ? AppLocalization.Number(issue.ActualHours.Value) : "-") @AppLocalization.Text("common.hoursAbbrev", "hr")</span> | |
| </td> | |
| <td class="px-5 py-4"> | |
| <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetStatusBadge(issue.StatusId)"> | |
| @GetStatusLabel(issue.StatusId) | |
| </span> | |
| </td> | |
| <td class="px-5 py-4"> | |
| <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium @GetPriorityBadge(issue.PriorityId)"> | |
| @GetPriorityLabel(issue.PriorityId) | |
| </span> | |
| </td> | |
| <td class="px-5 py-4 text-sm text-slate-600 whitespace-nowrap"> | |
| <span class="font-medium text-slate-700">@DisplayFormats.Date(issue.DueDate)</span> | |
| </td> | |
| <td class="px-5 py-4 text-sm text-slate-600 break-words"> | |
| @(!string.IsNullOrWhiteSpace(Content.GetText(issue.AssignedToName, issue.AssignedToNameMy)) ? Content.GetText(issue.AssignedToName, issue.AssignedToNameMy) : AppLocalization.Text("common.unassigned", "Unassigned")) | |
| </td> | |
| <td class="px-5 py-4"> | |
| @if (issue.IsBlocked) | |
| { | |
| <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> | |
| } | |
| else if (issue.EscalationLevel > 0) | |
| { | |
| <span class="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-700">@GetEscalationLabel(issue.EscalationLevel)</span> | |
| } | |
| else | |
| { | |
| <span class="text-xs text-slate-400">@AppLocalization.Text("status.clear", "Clear")</span> | |
| } | |
| </td> | |
| <td class="px-5 py-4 text-center"> | |
| <div class="flex items-center justify-center gap-1"> | |
| @if (canEditIssue) | |
| { | |
| <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")"> | |
| <i data-lucide="pencil" class="h-4 w-4"></i> | |
| </button> | |
| } | |
| @if (canDeleteIssue) | |
| { | |
| <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")"> | |
| <i data-lucide="trash-2" class="h-4 w-4"></i> | |
| </button> | |
| } | |
| </div> | |
| </td> | |
| </tr> | |
| } | |
| } | |
| else | |
| { | |
| <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.")" /> | |
| } | |
| </tbody> | |
| </table> | |
| </div> | |
| <Pagination CurrentPage="currentPage" | |
| TotalPages="totalPages" | |
| PageSize="pageSize" | |
| TotalCount="totalCount" | |
| OnPageChanged="HandlePageChanged" | |
| OnPageSizeChanged="HandlePageSizeChanged" /> | |
| </div> | |
| } | |
| </div> | |
| @if (showModal) | |
| { | |
| <div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/40 p-4 backdrop-blur-sm sm:p-6"> | |
| <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)]"> | |
| <div class="shrink-0 flex items-center justify-between px-6 py-4 border-b border-slate-100"> | |
| <div> | |
| <h3 class="text-lg font-semibold text-slate-900">@AppLocalization.Text("common.editIssue", "Edit Issue")</h3> | |
| <p class="text-xs text-slate-500 mt-0.5">#@AppLocalization.Number(editingId)</p> | |
| </div> | |
| <button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all"> | |
| <i data-lucide="x" class="h-5 w-5"></i> | |
| </button> | |
| </div> | |
| <div class="min-h-0 flex-1 overflow-y-auto p-6 space-y-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="rounded-xl border border-slate-200 bg-slate-50 p-3"> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">@AppLocalization.Text("common.task", "Task")</p> | |
| <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedTaskLabel</p> | |
| </div> | |
| <div class="rounded-xl border border-slate-200 bg-slate-50 p-3"> | |
| <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">@AppLocalization.Text("common.project", "Project")</p> | |
| <p class="mt-1 text-sm font-semibold text-slate-900">@SelectedProjectLabel</p> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedStatus", "Blocked Status")</label> | |
| <label class="flex h-10 items-center gap-2 rounded-lg border border-slate-200 px-3 text-sm text-slate-700"> | |
| <input type="checkbox" @bind="formIsBlocked" class="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" /> | |
| <span>@AppLocalization.Text("status.blocked", "Blocked")</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.escalationLevel", "Escalation Level")</label> | |
| <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"> | |
| <option value="0">@AppLocalization.Text("common.none", "None")</option> | |
| <option value="1">@AppLocalization.Text("issue.teamLead", "Team Lead")</option> | |
| <option value="2">@AppLocalization.Text("issue.pm", "PM")</option> | |
| <option value="3">@AppLocalization.Text("issue.manager", "Manager")</option> | |
| </select> | |
| </div> | |
| </div> | |
| <BilingualEditorTabs BurmeseComplete="@(!string.IsNullOrWhiteSpace(formTitleMy) || !string.IsNullOrWhiteSpace(formDescriptionMy))" | |
| EnglishComplete="@(!string.IsNullOrWhiteSpace(formTitle) || !string.IsNullOrWhiteSpace(formDescription))" | |
| BurmeseAiGenerated="@burmeseGenerated" EnglishAiGenerated="@englishGenerated" | |
| BurmeseNeedsReview="@burmeseNeedsReview" EnglishNeedsReview="@englishNeedsReview"> | |
| <BurmeseContent> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">ပိတ်ဆို့မှု</label> | |
| <input @bind="formBlockedByMy" type="text" placeholder="ပိတ်ဆို့ထားသူ / မှီခိုမှု" class="mb-2 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" /> | |
| <BilingualFieldWarning Text="@formBlockedByMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueBlockedByToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formBlockedByMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formBlockedBy))" Generated="GenerateIssueEnglishBlockedBy" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">နှောင့်နှေးရခြင်းအကြောင်း</label> | |
| <input @bind="formDelayReasonMy" type="text" placeholder="နှောင့်နှေးရခြင်းအကြောင်း" class="mb-2 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" /> | |
| <BilingualFieldWarning Text="@formDelayReasonMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueDelayToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formDelayReasonMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formDelayReason))" Generated="GenerateIssueEnglishDelay" /> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">ခေါင်းစဉ် <span class="text-red-500">*</span></label> | |
| <input @bind="formTitleMy" type="text" placeholder="ခေါင်းစဉ်" 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" /> | |
| <BilingualFieldWarning Text="@formTitleMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueTitleToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formTitleMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formTitle))" Generated="GenerateIssueEnglishTitle" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">ဖော်ပြချက်</label> | |
| <textarea @bind="formDescriptionMy" rows="3" placeholder="ဖော်ပြချက်" 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> | |
| <BilingualFieldWarning Text="@formDescriptionMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueDescriptionToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formDescriptionMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescription))" Generated="GenerateIssueEnglishDescription" /> | |
| </div> | |
| </BurmeseContent> | |
| <EnglishContent> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.blockedBy", "Blocked By")</label> | |
| <input @bind="formBlockedBy" type="text" placeholder="@AppLocalization.Text("issue.blockedByPlaceholder", "Dependency, person, or team")" 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" /> | |
| <BilingualFieldWarning Text="@formBlockedBy" ExpectedBurmese="false" MoveToOtherLanguage="MoveIssueBlockedByToBurmese" /> | |
| <BilingualTranslationAction SourceText="@formBlockedBy" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formBlockedByMy))" Generated="GenerateIssueBurmeseBlockedBy" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.delayReason", "Delay Reason")</label> | |
| <input @bind="formDelayReason" type="text" placeholder="@AppLocalization.Text("issue.delayReasonPlaceholder", "Why is this issue delayed?")" 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" /> | |
| <BilingualFieldWarning Text="@formDelayReason" ExpectedBurmese="false" MoveToOtherLanguage="MoveIssueDelayToBurmese" /> | |
| <BilingualTranslationAction SourceText="@formDelayReason" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formDelayReasonMy))" Generated="GenerateIssueBurmeseDelay" /> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("field.title", "Title") <span class="text-red-500">*</span></label> | |
| <input @bind="formTitle" type="text" placeholder="@AppLocalization.Text("issue.titlePlaceholder", "Issue title")" 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" /> | |
| <BilingualFieldWarning Text="@formTitle" ExpectedBurmese="false" MoveToOtherLanguage="MoveIssueTitleToBurmese" /> | |
| <BilingualTranslationAction SourceText="@formTitle" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formTitleMy))" Generated="GenerateIssueBurmeseTitle" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.description", "Description")</label> | |
| <textarea @bind="formDescription" rows="3" placeholder="@AppLocalization.Text("issue.descriptionPlaceholder", "Describe the work...")" 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> | |
| <BilingualFieldWarning Text="@formDescription" ExpectedBurmese="false" MoveToOtherLanguage="MoveIssueDescriptionToBurmese" /> | |
| <BilingualTranslationAction SourceText="@formDescription" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescriptionMy))" Generated="GenerateIssueBurmeseDescription" /> | |
| </div> | |
| </EnglishContent> | |
| </BilingualEditorTabs> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.assignee", "Assignee")</label> | |
| <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"> | |
| <option value="">@AppLocalization.Text("common.unassigned", "Unassigned")</option> | |
| @foreach (var user in filteredProjectMembers) | |
| { | |
| <option value="@user.Id">@Content.GetUserFullName(user)</option> | |
| } | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.actualHours", "Actual Hours")</label> | |
| <input @bind="formActualHours" type="number" step="0.25" min="0" max="7" | |
| 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" /> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.startDate", "Start Date")</label> | |
| <input @bind="formStartDate" type="date" | |
| 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" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.dueDate", "Due Date")</label> | |
| <input @bind="formDueDate" type="date" | |
| 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" /> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.status", "Status")</label> | |
| <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"> | |
| <option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option> | |
| <option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option> | |
| <option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label> | |
| <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"> | |
| <option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option> | |
| <option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option> | |
| <option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option> | |
| </select> | |
| </div> | |
| </div> | |
| @if (!string.IsNullOrWhiteSpace(errorMessage)) | |
| { | |
| <div class="rounded-lg border border-rose-100 bg-rose-50 p-3"> | |
| <p class="text-sm text-rose-600">@errorMessage</p> | |
| </div> | |
| } | |
| </div> | |
| <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)]"> | |
| <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> | |
| <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"> | |
| @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.update", "Update")) | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| <ConfirmDialog IsVisible="showDeleteConfirm" | |
| Title="@AppLocalization.Text("issue.deleteIssue", "Delete Issue?")" | |
| Message="@deleteConfirmMessage" | |
| ConfirmText="@AppLocalization.Text("issue.yesDelete", "Yes, Delete")" | |
| Icon="trash-2" | |
| IconBgClass="bg-rose-50" | |
| IconTextClass="text-rose-600" | |
| ButtonClass="bg-rose-600 hover:bg-rose-700" | |
| OnConfirm="ConfirmDelete" | |
| OnCancel="CloseDeleteConfirm" /> | |
| <ConfirmDialog IsVisible="showSaveConfirm" | |
| Title="@AppLocalization.Text("common.saveChanges", "Save Changes?")" | |
| Message="@AppLocalization.Text("issue.saveConfirmMessage", "Are you sure you want to save these issue changes?")" | |
| ConfirmText="@AppLocalization.Text("issue.yesSave", "Yes, Save")" | |
| Icon="save" | |
| IconBgClass="bg-violet-50" | |
| IconTextClass="text-violet-600" | |
| ButtonClass="bg-violet-600 hover:bg-violet-700" | |
| OnConfirm="ConfirmSave" | |
| OnCancel="CloseSaveConfirm" /> | |
| @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<IssueDto> issues = new(); | |
| private List<TaskDto> tasks = new(); | |
| private List<ProjectDto> projects = new(); | |
| private List<UserDto> 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<UserDto> 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<List<TaskDto>>("Task", Serialization.CaseInsensitive); | |
| var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive); | |
| var usersTask = client.GetFromJsonAsync<List<UserDto>>("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<PagedResult<IssueDto>?> LoadIssuesPageAsync(HttpClient? client = null) | |
| { | |
| client ??= ApiClient.CreateClient(); | |
| return await client.GetFromJsonAsync<PagedResult<IssueDto>>(BuildIssuesUrl(), Serialization.CaseInsensitive); | |
| } | |
| private void ApplyPageResult(PagedResult<IssueDto>? 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<string> | |
| { | |
| $"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<Result<IEnumerable<UserDto>>>($"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<string> 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); | |
| } | |