Spaces:
Running
Running
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Issues /AddIssue.razor
| @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> | |
| <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.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">@task.Title</option> | |
| } | |
| </select> | |
| @if (isTaskError) | |
| { | |
| <p class="mt-1 text-xs text-red-500">@taskErrorMessage</p> | |
| } | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.issueTitle", "Issue Title") <span class="text-red-500">*</span></label> | |
| <input @bind="formTitle" type="text" placeholder="@AppLocalization.Text("issue.whatDidYouWorkOn", "What did you work on?")" | |
| class="w-full rounded-lg border @(isTitleError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-violet-600") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" /> | |
| @if (isTitleError) | |
| { | |
| <p class="mt-1 text-xs text-red-500">@titleErrorMessage</p> | |
| } | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.description", "Description")</label> | |
| <textarea @bind="formDescription" rows="4" 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> | |
| </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.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" /> | |
| </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" /> | |
| </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" | |
| @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">@user.FirstName @user.LastName</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" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| @AppLocalization.Text("action.reset", "Reset") | |
| </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 string? successMessage; | |
| 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? formDescription; | |
| 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 bool formIsBlocked; | |
| private string? formBlockedBy; | |
| private int formEscalationLevel; | |
| private string assigneeSearchInput = ""; | |
| private bool isAssigneeDropdownOpen; | |
| private bool isLoadingProjectMembers; | |
| private TaskDto? SelectedTask => tasks.FirstOrDefault(t => t.Id == formTaskId); | |
| private string SelectedTaskLabel => SelectedTask == null ? "No task selected" : SelectedTask.Title; | |
| private string SelectedProjectLabel => SelectedTask == null | |
| ? "No project" | |
| : (projects.FirstOrDefault(p => p.Id == SelectedTask.ProjectId)?.Name ?? $"Project #{SelectedTask.ProjectId}"); | |
| private string SelectedAssigneeLabel => formAssignedTo.HasValue | |
| ? (projectMembers.FirstOrDefault(u => u.Id == formAssignedTo.Value) is { } user ? $"{user.FirstName} {user.LastName}" : "Selected member") | |
| : "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}".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) | |
| { | |
| errorMessage = ex.Message; | |
| } | |
| 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 = "Please select a task."; | |
| return; | |
| } | |
| if (string.IsNullOrWhiteSpace(formTitle)) | |
| { | |
| isTitleError = true; | |
| titleErrorMessage = "Issue title is required."; | |
| return; | |
| } | |
| showConfirmDialog = true; | |
| } | |
| private void CloseConfirmDialog() | |
| { | |
| showConfirmDialog = false; | |
| } | |
| private async Task ConfirmSave() | |
| { | |
| showConfirmDialog = false; | |
| await SaveIssueAsync(); | |
| } | |
| private async Task SaveIssueAsync() | |
| { | |
| if (formTaskId <= 0) | |
| { | |
| errorMessage = "Please select a task."; | |
| return; | |
| } | |
| if (string.IsNullOrWhiteSpace(formTitle)) | |
| { | |
| errorMessage = "Issue title is required."; | |
| return; | |
| } | |
| isSaving = true; | |
| errorMessage = null; | |
| try | |
| { | |
| var client = ApiClient.CreateClient(); | |
| var dto = new CreateIssueDto | |
| { | |
| TaskId = formTaskId, | |
| Title = formTitle.Trim(), | |
| Description = formDescription, | |
| AssignedTo = formAssignedTo, | |
| EstimatedHours = formEstimatedHours, | |
| ActualHours = formActualHours, | |
| DelayReason = formDelayReason, | |
| IsBlocked = formIsBlocked, | |
| BlockedBy = formBlockedBy, | |
| 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 (result?.IsSuccess == true) | |
| { | |
| successMessage = "Issue created successfully!"; | |
| ResetForm(); | |
| } | |
| else | |
| { | |
| errorMessage = result?.ErrorMessage ?? "Failed to create issue."; | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| errorMessage = ex.Message; | |
| } | |
| finally | |
| { | |
| isSaving = false; | |
| } | |
| } | |
| private void ResetForm() | |
| { | |
| formTaskId = tasks.FirstOrDefault()?.Id ?? 0; | |
| formTitle = ""; | |
| formDescription = null; | |
| formAssignedTo = currentUserId; | |
| formEstimatedHours = null; | |
| formActualHours = 7m; | |
| formStartDate = DateTime.Today; | |
| formDueDate = DateTime.Today; | |
| formStatusId = AppTaskStatus.Todo; | |
| formPriorityId = TaskPriority.Medium; | |
| formDelayReason = null; | |
| formIsBlocked = false; | |
| formBlockedBy = null; | |
| formEscalationLevel = 0; | |
| taskErrorMessage = ""; | |
| titleErrorMessage = ""; | |
| isTaskError = false; | |
| isTitleError = false; | |
| assigneeSearchInput = ""; | |
| isAssigneeDropdownOpen = false; | |
| _ = LoadProjectMembersForTaskAsync(formTaskId); | |
| } | |
| } | |