Spaces:
Runtime error
Runtime error
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Issues /AddIssue.razor
Download TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor from Yuruyuruu/task-tracking-system: direct link, hf CLI and curl.
- Browser
- Download file 39.3 kB
-
https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor
- Command line
-
hf download hf://spaces/Yuruyuruu/task-tracking-system/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor
-
curl -L -o AddIssue.razor https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Issues/AddIssue.razor
39.3 kB
| @page "/issues/add" | |
| @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("addIssue", "Add Issue")</PageTitle> | |
| <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.Text("common.addIssue", "Add Issue")</h2> | |
| <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> | |
| </div> | |
| </div> | |
| @if (isLoading) | |
| { | |
| <LoadingSpinner Message="@AppLocalization.Text("common.loadingTasksAndUsers", "Loading tasks and users...")" /> | |
| } | |
| else | |
| { | |
| <div class="w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="md:col-span-2"> | |
| <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> | |
| <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"> | |
| <option value="0">@AppLocalization.Text("common.selectTask", "Select task...")</option> | |
| @foreach (var task in tasks) | |
| { | |
| <option value="@task.Id">@Content.GetTaskTitle(task)</option> | |
| } | |
| </select> | |
| @if (isTaskError) | |
| { | |
| <p class="mt-1 text-xs text-red-500">@taskErrorMessage</p> | |
| } | |
| </div> | |
| <div class="md:col-span-2"> | |
| <BilingualEditorTabs BurmeseComplete="@(!string.IsNullOrWhiteSpace(formTitleMy))" | |
| EnglishComplete="@(!string.IsNullOrWhiteSpace(formTitle))" | |
| BurmeseAiGenerated="@burmeseGenerated" | |
| EnglishAiGenerated="@englishGenerated" | |
| BurmeseNeedsReview="@burmeseNeedsReview" | |
| EnglishNeedsReview="@englishNeedsReview"> | |
| <BurmeseContent> | |
| <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 @(isTitleError && string.IsNullOrWhiteSpace(formTitle) ? "border-red-500" : "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" /> | |
| <label class="mt-4 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" /> | |
| <label class="mt-4 block text-sm font-medium text-slate-700 mb-1.5">နှောင့်နှေးရခြင်းအကြောင်း</label> | |
| <input @bind="formDelayReasonMy" 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="@formDelayReasonMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueDelayToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formDelayReasonMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formDelayReason))" Generated="GenerateIssueEnglishDelay" /> | |
| <label class="mt-4 block text-sm font-medium text-slate-700 mb-1.5">ပိတ်ဆို့ထားသူ / မှီခိုမှု</label> | |
| <input @bind="formBlockedByMy" 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="@formBlockedByMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveIssueBlockedByToEnglish" /> | |
| <BilingualTranslationAction SourceText="@formBlockedByMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formBlockedBy))" Generated="GenerateIssueEnglishBlockedBy" /> | |
| </BurmeseContent> | |
| <EnglishContent> | |
| <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.whatDidYouWorkOn", "What did you work on?")" class="w-full rounded-lg border @(isTitleError && string.IsNullOrWhiteSpace(formTitleMy) ? "border-red-500" : "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" /> | |
| <label class="mt-4 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.addShortNote", "Add a short note about the work you did...")" 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" /> | |
| <label class="mt-4 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" /> | |
| <label class="mt-4 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" /> | |
| </EnglishContent> | |
| </BilingualEditorTabs> | |
| @if (isTitleError) { <p class="mt-1 text-xs text-red-500">@titleErrorMessage</p> } | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.estimatedHours", "Estimated Hours")</label> | |
| <input @bind="formEstimatedHours" type="number" step="0.25" min="0" | |
| 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" | |
| placeholder="@AppLocalization.Text("issue.exampleEstimatedHours", "e.g. 4")" /> | |
| </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" | |
| placeholder="@AppLocalization.Text("issue.exampleActualHours", "e.g. 7")" /> | |
| <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> | |
| </div> | |
| <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> | |
| <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> | |
| <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> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.project", "Project")</label> | |
| <input value="@SelectedProjectLabel" readonly | |
| class="w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 focus:outline-none" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.assignee", "Assignee")</label> | |
| <div class="relative"> | |
| <button type="button" | |
| @onclick="ToggleAssigneeDropdown" | |
| class="flex h-11 w-full items-center justify-between rounded-lg border border-slate-200 bg-white px-3 text-left text-sm text-slate-900 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-violet-600"> | |
| <span class="truncate @(formAssignedTo.HasValue ? "font-medium text-slate-900" : "text-slate-400")">@SelectedAssigneeLabel</span> | |
| <i data-lucide="chevron-down" class="ml-3 h-4 w-4 shrink-0 text-slate-400"></i> | |
| </button> | |
| @if (isAssigneeDropdownOpen) | |
| { | |
| <div class="absolute bottom-full left-0 z-30 mb-2 w-full rounded-2xl border border-slate-200 bg-white p-3 shadow-lg"> | |
| <div class="flex items-center justify-end"> | |
| <button type="button" | |
| @onclick="CloseAssigneeDropdown" | |
| 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" | |
| aria-label="@AppLocalization.Text("common.close", "Close")"> | |
| <i data-lucide="x" class="h-4 w-4"></i> | |
| </button> | |
| </div> | |
| <div class="relative"> | |
| <input @bind="assigneeSearchInput" @bind:event="oninput" | |
| @onfocus="EnsureAssigneeDropdownOpen" | |
| type="text" | |
| placeholder="@AppLocalization.Text("common.searchProjectMembers", "Search project members...")" | |
| 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" /> | |
| <i data-lucide="search" class="absolute left-3 top-2.5 h-4 w-4 text-slate-400"></i> | |
| </div> | |
| @if (isLoadingProjectMembers) | |
| { | |
| <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"> | |
| @AppLocalization.Text("common.loadingProjectMembers", "Loading project members...") | |
| </div> | |
| } | |
| else | |
| { | |
| <div class="mt-3 max-h-56 overflow-y-auto space-y-2 pr-1"> | |
| <button type="button" | |
| @onclick="() => SetFormAssignee(null)" | |
| class="flex w-full items-center gap-3 rounded-xl border px-3 py-3 text-left transition-colors @(formAssignedTo.HasValue ? "border-slate-200 bg-white hover:bg-slate-50" : "border-violet-600 bg-violet-50/40")"> | |
| <span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-500 text-xs font-bold shrink-0"> | |
| <i data-lucide="user-minus" class="h-4 w-4"></i> | |
| </span> | |
| <div class="min-w-0 flex-1"> | |
| <p class="text-sm font-semibold text-slate-900">@AppLocalization.Text("common.unassigned", "Unassigned")</p> | |
| </div> | |
| </button> | |
| @foreach (var user in FilteredProjectMembers) | |
| { | |
| var isSelected = formAssignedTo == user.Id; | |
| <button type="button" | |
| @onclick="() => SetFormAssignee(user.Id)" | |
| class="flex w-full items-center gap-3 rounded-xl border px-3 py-3 text-left transition-colors @(isSelected ? "border-violet-600 bg-violet-50/40" : "border-slate-100 bg-white hover:border-slate-200 hover:bg-slate-50")"> | |
| <span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0"> | |
| @(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "") | |
| </span> | |
| <div class="min-w-0 flex-1"> | |
| <p class="truncate text-sm font-semibold text-slate-900">@Content.GetUserFullName(user)</p> | |
| <p class="truncate text-xs text-slate-500">@@@user.Username</p> | |
| </div> | |
| </button> | |
| } | |
| @if (!FilteredProjectMembers.Any()) | |
| { | |
| <div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500"> | |
| @AppLocalization.Text("common.noMembersMatchSearch", "No members match the current search.") | |
| </div> | |
| } | |
| </div> | |
| } | |
| </div> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex items-center justify-end gap-3"> | |
| <button @onclick="ResetForm" 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> | |
| <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"> | |
| @(isSaving ? AppLocalization.Text("common.loadingSpinner", "Loading...") : AppLocalization.Text("common.addIssue", "Add Issue")) | |
| </button> | |
| </div> | |
| @if (!string.IsNullOrWhiteSpace(errorMessage)) | |
| { | |
| <p class="mt-4 text-sm text-red-600">@errorMessage</p> | |
| } | |
| </div> | |
| } | |
| </div> | |
| <ConfirmDialog IsVisible="showConfirmDialog" | |
| Title="@AppLocalization.Text("common.addIssueTitle", "Add Issue?")" | |
| Message="@AppLocalization.Text("issue.saveAddConfirmMessage", "Are you sure you want to save this issue?")" | |
| ConfirmText="@AppLocalization.Text("issue.yesAdd", "Yes, Add")" | |
| Icon="circle-plus" | |
| IconBgClass="bg-emerald-50" | |
| IconTextClass="text-emerald-600" | |
| ButtonClass="bg-violet-600 hover:bg-violet-700" | |
| OnConfirm="ConfirmSave" | |
| OnCancel="CloseConfirmDialog" /> | |
| @code { | |
| private bool isLoading = true; | |
| private bool isSaving; | |
| private bool showConfirmDialog; | |
| private bool isTaskError; | |
| private bool isTitleError; | |
| private string taskErrorMessage = ""; | |
| private string titleErrorMessage = ""; | |
| private string? errorMessage; | |
| private List<TaskDto> tasks = new(); | |
| private List<ProjectDto> projects = new(); | |
| private List<UserDto> projectMembers = new(); | |
| private long currentUserId; | |
| private long formTaskId; | |
| private string formTitle = ""; | |
| private string? formTitleMy; | |
| private string? formDescription; | |
| private string? formDescriptionMy; | |
| private bool burmeseGenerated; | |
| private bool englishGenerated; | |
| private bool burmeseNeedsReview; | |
| private bool englishNeedsReview; | |
| private long? formAssignedTo; | |
| private decimal? formEstimatedHours; | |
| 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 string assigneeSearchInput = ""; | |
| private bool isAssigneeDropdownOpen; | |
| private bool isLoadingProjectMembers; | |
| 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 ?? string.Empty; 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 TaskDto? SelectedTask => tasks.FirstOrDefault(t => t.Id == formTaskId); | |
| private string SelectedTaskLabel => SelectedTask == null ? AppLocalization.Text("common.noTaskSelectedForIssue", "No task selected") : Content.GetTaskTitle(SelectedTask); | |
| private string SelectedProjectLabel => SelectedTask == null | |
| ? AppLocalization.Text("common.noProject", "No project") | |
| : (projects.FirstOrDefault(p => p.Id == SelectedTask.ProjectId) is { } project ? Content.GetProjectName(project) : $"{AppLocalization.Text("common.project", "Project")} #{AppLocalization.Number(SelectedTask.ProjectId)}"); | |
| private string SelectedAssigneeLabel => formAssignedTo.HasValue | |
| ? (projectMembers.FirstOrDefault(u => u.Id == formAssignedTo.Value) is { } user ? Content.GetUserFullName(user) : AppLocalization.Text("common.selectedMember", "Selected member")) | |
| : AppLocalization.Text("common.unassigned", "Unassigned"); | |
| private IEnumerable<UserDto> FilteredProjectMembers | |
| { | |
| get | |
| { | |
| var result = projectMembers.Where(u => u.IsActive); | |
| if (!string.IsNullOrWhiteSpace(assigneeSearchInput)) | |
| { | |
| var search = assigneeSearchInput.Trim(); | |
| result = result.Where(u => | |
| $"{u.FirstName} {u.LastName} {u.FirstNameMy} {u.LastNameMy}".Contains(search, StringComparison.OrdinalIgnoreCase) || | |
| u.Username.Contains(search, StringComparison.OrdinalIgnoreCase)); | |
| } | |
| return result.OrderBy(u => u.FirstName).ThenBy(u => u.LastName); | |
| } | |
| } | |
| protected override async Task OnInitializedAsync() | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| var user = authState.User; | |
| var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; | |
| if (idStr != null) | |
| { | |
| long.TryParse(idStr, out currentUserId); | |
| } | |
| await LoadDataAsync(user); | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task LoadDataAsync(System.Security.Claims.ClaimsPrincipal user) | |
| { | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| 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 Task.WhenAll(tasksTask, projectsTask, usersTask); | |
| tasks = tasksTask.Result ?? new(); | |
| projects = projectsTask.Result ?? new(); | |
| var allUsers = usersTask.Result ?? new(); | |
| projectMembers = allUsers.Where(u => u.IsActive).ToList(); | |
| formTaskId = tasks.FirstOrDefault()?.Id ?? 0; | |
| await LoadProjectMembersForTaskAsync(formTaskId); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Load issue data error: {ex.Message}"); | |
| errorMessage = AppLocalization.Text("common.loadFailed", "Unable to load the requested data. Please try again."); | |
| } | |
| finally | |
| { | |
| isLoading = false; | |
| } | |
| } | |
| private async Task OnTaskChanged(ChangeEventArgs e) | |
| { | |
| if (long.TryParse(e.Value?.ToString(), out var taskId)) | |
| { | |
| formTaskId = taskId; | |
| await LoadProjectMembersForTaskAsync(formTaskId); | |
| if (formTaskId > 0) | |
| { | |
| formAssignedTo = currentUserId; | |
| } | |
| } | |
| } | |
| private async Task LoadProjectMembersForTaskAsync(long taskId) | |
| { | |
| isLoadingProjectMembers = true; | |
| projectMembers.Clear(); | |
| var task = tasks.FirstOrDefault(t => t.Id == taskId); | |
| if (task == null) | |
| { | |
| formAssignedTo = null; | |
| isLoadingProjectMembers = false; | |
| 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(); | |
| if (projectMembers.Any(u => u.Id == currentUserId)) | |
| { | |
| formAssignedTo = currentUserId; | |
| } | |
| else if (formAssignedTo.HasValue && !projectMembers.Any(u => u.Id == formAssignedTo.Value)) | |
| { | |
| formAssignedTo = projectMembers.FirstOrDefault()?.Id; | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Failed to load project members: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| isLoadingProjectMembers = false; | |
| } | |
| } | |
| private void ToggleAssigneeDropdown() | |
| { | |
| isAssigneeDropdownOpen = !isAssigneeDropdownOpen; | |
| if (isAssigneeDropdownOpen) | |
| { | |
| assigneeSearchInput = ""; | |
| } | |
| } | |
| private void CloseAssigneeDropdown() | |
| { | |
| isAssigneeDropdownOpen = false; | |
| } | |
| private void EnsureAssigneeDropdownOpen() | |
| { | |
| isAssigneeDropdownOpen = true; | |
| } | |
| private void SetFormAssignee(long? userId) | |
| { | |
| formAssignedTo = userId; | |
| isAssigneeDropdownOpen = false; | |
| assigneeSearchInput = ""; | |
| } | |
| private void RequestSaveConfirmation() | |
| { | |
| isTaskError = false; | |
| isTitleError = false; | |
| taskErrorMessage = ""; | |
| titleErrorMessage = ""; | |
| if (formTaskId <= 0) | |
| { | |
| isTaskError = true; | |
| taskErrorMessage = AppLocalization.Text("validation.selectTaskRequired", "Please select a task."); | |
| return; | |
| } | |
| if (string.IsNullOrWhiteSpace(formTitle) && string.IsNullOrWhiteSpace(formTitleMy)) | |
| { | |
| isTitleError = true; | |
| titleErrorMessage = AppLocalization.Text("validation.issueTitleRequired", "Issue title is required."); | |
| return; | |
| } | |
| if (formDueDate.Date < formStartDate.Date) | |
| { | |
| errorMessage = AppLocalization.Text("issue.dateRangeInvalid", "The due date cannot be earlier than the start date."); | |
| return; | |
| } | |
| showConfirmDialog = true; | |
| } | |
| private void CloseConfirmDialog() | |
| { | |
| showConfirmDialog = false; | |
| } | |
| private async Task ConfirmSave() | |
| { | |
| showConfirmDialog = false; | |
| await SaveIssueAsync(); | |
| } | |
| private async Task SaveIssueAsync() | |
| { | |
| if (isSaving) | |
| { | |
| return; | |
| } | |
| if (formTaskId <= 0) | |
| { | |
| errorMessage = AppLocalization.Text("validation.selectTaskRequired", "Please select a task."); | |
| return; | |
| } | |
| if (string.IsNullOrWhiteSpace(formTitle) && string.IsNullOrWhiteSpace(formTitleMy)) | |
| { | |
| errorMessage = AppLocalization.Text("validation.issueTitleRequired", "Issue title is required."); | |
| return; | |
| } | |
| if (formDueDate.Date < formStartDate.Date) | |
| { | |
| errorMessage = AppLocalization.Text("issue.dateRangeInvalid", "The due date cannot be earlier than the start date."); | |
| return; | |
| } | |
| isSaving = true; | |
| errorMessage = null; | |
| try | |
| { | |
| var client = ApiClient.CreateClient(); | |
| var dto = new CreateIssueDto | |
| { | |
| TaskId = formTaskId, | |
| Title = formTitle.Trim(), | |
| TitleMy = formTitleMy, | |
| Description = formDescription, | |
| DescriptionMy = formDescriptionMy, | |
| AssignedTo = formAssignedTo, | |
| EstimatedHours = formEstimatedHours, | |
| 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.PostAsJsonAsync("Issue", dto); | |
| var result = await response.Content.ReadFromJsonAsync<Result<IssueDto>>(Serialization.CaseInsensitive); | |
| if (response.IsSuccessStatusCode && result?.IsSuccess == true) | |
| { | |
| Navigation.NavigateTo("/issues/all?created=true"); | |
| } | |
| else | |
| { | |
| errorMessage = AppLocalization.Text("issue.failedCreate", "Failed to create issue. Please try again."); | |
| Console.WriteLine($"Create issue rejected: {result?.ErrorMessage ?? response.ReasonPhrase}"); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| errorMessage = AppLocalization.Text("issue.failedCreate", "Failed to create issue. Please try again."); | |
| Console.WriteLine($"Create issue error: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| isSaving = false; | |
| } | |
| } | |
| private void ResetForm() | |
| { | |
| formTaskId = tasks.FirstOrDefault()?.Id ?? 0; | |
| formTitle = ""; | |
| formTitleMy = null; | |
| formDescription = null; | |
| formDescriptionMy = null; | |
| burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false; | |
| formAssignedTo = currentUserId; | |
| formEstimatedHours = null; | |
| formActualHours = 7m; | |
| formStartDate = DateTime.Today; | |
| formDueDate = DateTime.Today; | |
| formStatusId = AppTaskStatus.Todo; | |
| formPriorityId = TaskPriority.Medium; | |
| formDelayReason = null; | |
| formDelayReasonMy = null; | |
| formIsBlocked = false; | |
| formBlockedBy = null; | |
| formBlockedByMy = null; | |
| formEscalationLevel = 0; | |
| taskErrorMessage = ""; | |
| titleErrorMessage = ""; | |
| isTaskError = false; | |
| isTitleError = false; | |
| assigneeSearchInput = ""; | |
| isAssigneeDropdownOpen = false; | |
| _ = LoadProjectMembersForTaskAsync(formTaskId); | |
| } | |
| } | |