Spaces:
Running
Running
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Tasks /TaskDetails.razor
| @page "/tasks/{TaskId:long}/details" | |
| @using Microsoft.AspNetCore.Authorization | |
| @using Microsoft.AspNetCore.Components.Authorization | |
| @using Microsoft.AspNetCore.Components.Forms | |
| @using TaskTrackingSystem.Shared | |
| @using TaskTrackingSystem.Shared.Models.Task | |
| @using TaskTrackingSystem.Shared.Models.Comment | |
| @using TaskTrackingSystem.Shared.Models.Project | |
| @using TaskTrackingSystem.Shared.Models.User | |
| @using TaskTrackingSystem.WebApp.Components.Partial | |
| @rendermode @(new InteractiveServerRenderMode(prerender: false)) | |
| @attribute [Authorize] | |
| @inject ApiClientService ApiClient | |
| @inject IJSRuntime JS | |
| @inject NavigationManager Navigation | |
| @inject AuthenticationStateProvider AuthStateProvider | |
| @inject MenuAuthorizationService MenuAuthorization | |
| <PageTitle>Task Details</PageTitle> | |
| <div class="space-y-6"> | |
| @if (ViewMode != "Embedded") | |
| { | |
| <!-- Breadcrumbs & Actions --> | |
| <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> | |
| <div class="flex items-center space-x-2 text-sm text-slate-500"> | |
| <a href="/dashboard" class="hover:text-violet-600 transition-colors">Workspace</a> | |
| <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i> | |
| <a href="/tasks" class="hover:text-violet-600 transition-colors">Tasks</a> | |
| <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i> | |
| <span class="text-slate-800 font-medium">Task Details</span> | |
| </div> | |
| <a href="/tasks" 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"> | |
| <i data-lucide="arrow-left" class="h-4 w-4 mr-2 text-slate-500"></i> | |
| Back to Tasks | |
| </a> | |
| </div> | |
| } | |
| @if (isLoading) | |
| { | |
| <LoadingSpinner Message="Loading task details..." /> | |
| } | |
| else if (task == null) | |
| { | |
| <div class="rounded-2xl border border-slate-200 bg-white p-12 text-center"> | |
| <i data-lucide="alert-circle" class="h-10 w-10 mx-auto text-red-500 mb-3"></i> | |
| <h4 class="text-lg font-bold text-slate-800">Task Not Found</h4> | |
| <p class="text-sm text-slate-400 mt-1">The task you are looking for does not exist or has been deleted.</p> | |
| </div> | |
| } | |
| else | |
| { | |
| <!-- Task Header Card --> | |
| <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> | |
| <div class="flex flex-wrap items-start justify-between gap-4"> | |
| <div class="space-y-2 flex-1 min-w-0"> | |
| <div class="flex flex-wrap items-center gap-2"> | |
| <span class="text-xs font-bold font-mono text-slate-400">TASK-#@task.Id</span> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetStatusBadge(task.StatusId)"> | |
| @GetStatusLabel(task.StatusId) | |
| </span> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetPriorityBadge(task.PriorityId)"> | |
| @GetPriorityLabel(task.PriorityId) | |
| </span> | |
| @if (project != null) | |
| { | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-violet-50 text-violet-700 border border-violet-100"> | |
| <i data-lucide="folder" class="h-3 w-3 mr-1.5"></i> | |
| @project.Name | |
| </span> | |
| } | |
| </div> | |
| @if (isEditingTitle) | |
| { | |
| <div class="flex items-center gap-2 max-w-xl mt-1"> | |
| <input @bind="tempTitle" type="text" class="flex-1 rounded-lg border border-slate-200 px-3 py-1 text-lg font-bold text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600" /> | |
| <button @onclick="CancelEditTitle" class="px-2.5 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 rounded-lg transition-colors">Cancel</button> | |
| <button @onclick="SaveTitle" disabled="@isSavingTitle" class="px-2.5 py-1 text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-lg transition-colors shadow-sm">Save</button> | |
| </div> | |
| } | |
| else | |
| { | |
| <h2 class="text-xl font-bold text-slate-900 leading-tight flex items-center gap-2 group"> | |
| @task.Title | |
| @if (hasTaskManagePermission) | |
| { | |
| <button @onclick="StartEditTitle" class="opacity-0 group-hover:opacity-100 p-1 text-slate-400 hover:text-violet-600 transition-all rounded-lg hover:bg-slate-50" title="Edit title"> | |
| <i data-lucide="pencil" class="h-4 w-4"></i> | |
| </button> | |
| } | |
| </h2> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <!-- Left Grid: Task Details & Description --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Description Card --> | |
| <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm space-y-4"> | |
| <div class="flex items-center justify-between"> | |
| <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2"> | |
| <i data-lucide="align-left" class="h-4 w-4"></i> Description | |
| </h3> | |
| @if (hasTaskManagePermission && !isEditingDescription) | |
| { | |
| <button @onclick="StartEditDescription" class="inline-flex items-center text-xs font-semibold text-violet-600 hover:text-violet-800 transition-colors"> | |
| <i data-lucide="pencil" class="h-3 w-3 mr-1"></i> Edit | |
| </button> | |
| } | |
| </div> | |
| @if (isEditingDescription) | |
| { | |
| <div class="space-y-3"> | |
| <textarea @bind="tempDescription" rows="4" class="w-full text-sm rounded-xl border border-slate-200 p-3 text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea> | |
| <div class="flex items-center gap-2 justify-end"> | |
| <button @onclick="CancelEditDescription" class="px-3 py-1.5 text-xs font-medium text-slate-500 hover:bg-slate-100 rounded-lg transition-colors">Cancel</button> | |
| <button @onclick="SaveDescription" disabled="@isSavingDescription" class="px-3 py-1.5 text-xs font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-lg transition-colors shadow-sm"> | |
| @(isSavingDescription ? "Saving..." : "Save") | |
| </button> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| <div class="text-sm text-slate-700 leading-relaxed min-h-[120px] whitespace-pre-wrap"> | |
| @(string.IsNullOrWhiteSpace(task.Description) ? "No description provided for this task." : task.Description) | |
| </div> | |
| } | |
| </div> | |
| <!-- Properties Grid (Pristine grid alignment in uniform 2-column layout) --> | |
| <div class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> | |
| <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 flex items-center gap-2 mb-5"> | |
| <i data-lucide="info" class="h-4 w-4"></i> Properties | |
| </h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-5 text-sm font-sans"> | |
| <!-- Status Property (Free for everyone) --> | |
| <div class="flex flex-col gap-1.5"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Status</span> | |
| <select value="@task.StatusId" @onchange="OnStatusChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm"> | |
| <option value="@AppTaskStatus.Todo">To Do</option> | |
| <option value="@AppTaskStatus.InProgress">In Progress</option> | |
| <option value="@AppTaskStatus.Done">Done</option> | |
| </select> | |
| </div> | |
| <!-- Priority Property --> | |
| <div class="flex flex-col gap-1.5"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Priority</span> | |
| @if (hasTaskManagePermission) | |
| { | |
| <select value="@task.PriorityId" @onchange="OnPriorityChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm"> | |
| <option value="@TaskPriority.Low">Low</option> | |
| <option value="@TaskPriority.Medium">Medium</option> | |
| <option value="@TaskPriority.High">High</option> | |
| </select> | |
| } | |
| else | |
| { | |
| <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold @GetPriorityBadge(task.PriorityId)"> | |
| @GetPriorityLabel(task.PriorityId) | |
| </span> | |
| </div> | |
| } | |
| </div> | |
| <!-- Assignee Property --> | |
| <div class="flex flex-col gap-1.5"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Assignee</span> | |
| @if (hasTaskManagePermission) | |
| { | |
| <select value="@(task.AssignedTo ?? 0)" @onchange="OnAssigneeChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm"> | |
| <option value="0">Unassigned</option> | |
| @foreach (var u in allUsers) | |
| { | |
| <option value="@u.Id">@u.FirstName @u.LastName</option> | |
| } | |
| </select> | |
| } | |
| else | |
| { | |
| <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none gap-2"> | |
| @if (assignee != null) | |
| { | |
| <span class="flex h-5 w-5 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold"> | |
| @(assignee.FirstName.Length > 0 ? assignee.FirstName[0].ToString().ToUpper() : "") | |
| </span> | |
| <span class="font-medium text-slate-700">@assignee.FirstName @assignee.LastName</span> | |
| } | |
| else | |
| { | |
| <span class="text-slate-400 italic">Unassigned</span> | |
| } | |
| </div> | |
| } | |
| </div> | |
| <!-- Due Date Property --> | |
| <div class="flex flex-col gap-1.5"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Due Date</span> | |
| @if (hasTaskManagePermission) | |
| { | |
| <input type="date" value="@task.DueDate.ToString("yyyy-MM-dd")" @onchange="OnDueDateChanged" class="h-9 rounded-lg border border-slate-200 px-3 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent bg-white w-full transition-all cursor-pointer shadow-sm" /> | |
| } | |
| else | |
| { | |
| <div class="h-9 flex items-center px-1 text-sm text-slate-800 cursor-not-allowed select-none font-medium text-slate-700"> | |
| <i data-lucide="calendar" class="h-4 w-4 mr-2 text-slate-400"></i> | |
| @DisplayFormats.Date(task.DueDate) | |
| </div> | |
| } | |
| </div> | |
| <!-- Created At Property --> | |
| <div class="flex flex-col gap-1.5"> | |
| <span class="text-xs font-bold uppercase tracking-wider text-slate-400">Created At</span> | |
| <div class="h-9 flex items-center px-1 border-b border-transparent text-sm text-slate-800 cursor-default select-none font-medium text-slate-700"> | |
| <i data-lucide="calendar-plus" class="h-4 w-4 mr-2 text-slate-400"></i> | |
| @task.CreatedAt.ToString("dd-MM-yyyy HH:mm") | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Grid: Comments --> | |
| <div class="space-y-6"> | |
| <div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[580px]"> | |
| <!-- Header --> | |
| <div class="border-b border-slate-100 bg-slate-50/50 p-4"> | |
| <h3 class="text-sm font-bold text-slate-800 flex items-center gap-2"> | |
| <i data-lucide="message-square" class="h-4 w-4 text-violet-600"></i> | |
| Comments (@comments.Count) | |
| </h3> | |
| </div> | |
| <!-- Comments Body --> | |
| <div class="flex-1 overflow-y-auto p-4 min-h-0"> | |
| <div class="space-y-4 font-sans"> | |
| @if (comments.Any()) | |
| { | |
| @foreach (var c in comments) | |
| { | |
| <div class="flex items-start gap-3"> | |
| <span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0 mt-0.5"> | |
| @(c.UserFullName.Length > 0 ? c.UserFullName[0].ToString().ToUpper() : "U") | |
| </span> | |
| <div class="flex-1 min-w-0 bg-slate-50 rounded-xl p-3 border border-slate-100"> | |
| <div class="flex items-center justify-between gap-2"> | |
| <span class="text-xs font-bold text-slate-800 truncate">@c.UserFullName</span> | |
| <span class="text-[9px] text-slate-400 shrink-0 font-sans">@c.CreatedAt.ToString("g")</span> | |
| </div> | |
| <p class="text-xs text-slate-400 mt-0.5">@c.UserRoleName</p> | |
| <div class="text-xs text-slate-700 mt-2 whitespace-pre-wrap break-words">@c.Message</div> | |
| </div> | |
| @if (c.UserId == currentUserId || isAdmin) | |
| { | |
| <button @onclick="() => ConfirmDeleteComment(c.Id)" class="p-1 text-slate-300 hover:text-rose-500 rounded-lg transition-colors shrink-0" title="Delete comment"> | |
| <i data-lucide="trash-2" class="h-3.5 w-3.5"></i> | |
| </button> | |
| } | |
| </div> | |
| } | |
| } | |
| else | |
| { | |
| <div class="text-center py-12 text-slate-400 space-y-2"> | |
| <i data-lucide="messages-square" class="h-8 w-8 mx-auto text-slate-300"></i> | |
| <p class="text-xs">No comments posted yet.</p> | |
| <p class="text-[10px]">Start the conversation below!</p> | |
| </div> | |
| } | |
| </div> | |
| </div> | |
| <!-- Footer (Inputs) --> | |
| <div class="p-4 border-t border-slate-100 bg-slate-50/50"> | |
| <div class="flex items-start gap-2"> | |
| <div class="flex-1"> | |
| <textarea @bind="newCommentMessage" rows="2" placeholder="Write a comment..." class="w-full rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" disabled="@isSavingComment"></textarea> | |
| @if (!string.IsNullOrEmpty(commentError)) | |
| { | |
| <p class="text-[10px] text-red-500 mt-1 font-sans">@commentError</p> | |
| } | |
| </div> | |
| <button @onclick="PostComment" disabled="@(isSavingComment || string.IsNullOrWhiteSpace(newCommentMessage))" class="rounded-xl bg-violet-600 text-white p-2.5 hover:bg-violet-700 disabled:opacity-50 transition-all shadow-sm shrink-0"> | |
| <i data-lucide="send" class="h-4 w-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| <ConfirmDialog IsVisible="showConfirm" | |
| Title="@confirmTitle" | |
| Message="@confirmMessage" | |
| ConfirmText="Yes, Delete" | |
| Icon="trash-2" | |
| IconBgClass="bg-rose-50" | |
| IconTextClass="text-rose-500" | |
| ButtonClass="bg-rose-600 hover:bg-rose-700" | |
| OnConfirm="ExecuteConfirmAction" | |
| OnCancel="CancelConfirm" /> | |
| @code { | |
| [Parameter] public long TaskId { get; set; } | |
| [Parameter] public string? ViewMode { get; set; } | |
| [Parameter] public EventCallback OnTaskUpdated { get; set; } | |
| [SupplyParameterFromQuery(Name = "tab")] | |
| public string? TabQuery { get; set; } | |
| private Func<Task>? confirmAction; | |
| private bool isLoading = true; | |
| private TaskDto? task; | |
| private ProjectDto? project; | |
| private UserDto? assignee; | |
| private List<CommentDto> comments = new(); | |
| private List<UserDto> allUsers = new(); | |
| private bool isAdmin = false; | |
| private bool canEditTask = false; | |
| private bool hasTaskManagePermission = false; | |
| private long currentUserId = 0; | |
| private string userRoleName = "Employee"; | |
| private List<string> scopes = new(); | |
| // Comments Form | |
| private string newCommentMessage = ""; | |
| private bool isSavingComment = false; | |
| private string? commentError; | |
| // Confirm dialog | |
| private bool showConfirm = false; | |
| private string confirmTitle = ""; | |
| private string confirmMessage = ""; | |
| // Inline edit states | |
| private bool isEditingDescription = false; | |
| private bool isSavingDescription = false; | |
| private string tempDescription = ""; | |
| private bool isEditingTitle = false; | |
| private bool isSavingTitle = false; | |
| private string tempTitle = ""; | |
| private long lastLoadedTaskId = 0; | |
| protected override async Task OnInitializedAsync() | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| var user = authState.User; | |
| isAdmin = user.IsInRole("Admin"); | |
| // Task details editing is driven by role permissions so future roles can be granted access. | |
| canEditTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Update"); | |
| hasTaskManagePermission = canEditTask; | |
| var roleClaim = user.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value; | |
| userRoleName = roleClaim ?? "Employee"; | |
| var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; | |
| if (idStr != null) long.TryParse(idStr, out currentUserId); | |
| } | |
| protected override async Task OnParametersSetAsync() | |
| { | |
| if (TaskId > 0 && TaskId != lastLoadedTaskId) | |
| { | |
| lastLoadedTaskId = TaskId; | |
| assignee = null; | |
| await LoadAllData(); | |
| } | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task LoadAllData() | |
| { | |
| isLoading = true; | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var taskUrl = $"Task/{TaskId}"; | |
| var commentsUrl = $"TaskDetails/tasks/{TaskId}/comments"; | |
| var usersUrl = "User"; | |
| var taskResponseTask = client.GetAsync(taskUrl); | |
| var commentsResponseTask = client.GetAsync(commentsUrl); | |
| var usersResponseTask = client.GetAsync(usersUrl); | |
| await Task.WhenAll(taskResponseTask, commentsResponseTask, usersResponseTask); | |
| var taskResponse = taskResponseTask.Result; | |
| if (taskResponse.IsSuccessStatusCode) | |
| { | |
| task = await taskResponse.Content.ReadFromJsonAsync<TaskDto>(Serialization.CaseInsensitive); | |
| } | |
| var commentsResponse = commentsResponseTask.Result; | |
| if (commentsResponse.IsSuccessStatusCode) | |
| { | |
| comments = await commentsResponse.Content.ReadFromJsonAsync<List<CommentDto>>(Serialization.CaseInsensitive) ?? new(); | |
| } | |
| var usersResponse = usersResponseTask.Result; | |
| if (usersResponse.IsSuccessStatusCode) | |
| { | |
| allUsers = await usersResponse.Content.ReadFromJsonAsync<List<UserDto>>(Serialization.CaseInsensitive) ?? new(); | |
| } | |
| if (task != null) | |
| { | |
| // Fetch project details | |
| var projResponse = await client.GetAsync($"Project/{task.ProjectId}"); | |
| if (projResponse.IsSuccessStatusCode) | |
| { | |
| project = await projResponse.Content.ReadFromJsonAsync<ProjectDto>(Serialization.CaseInsensitive); | |
| } | |
| // Set assignee details | |
| if (task.AssignedTo.HasValue) | |
| { | |
| assignee = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo.Value); | |
| } | |
| CalculateScopes(); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Load task details error: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| isLoading = false; | |
| } | |
| } | |
| private void CalculateScopes() | |
| { | |
| scopes = new List<string>(); | |
| if (task == null) return; | |
| if (hasTaskManagePermission) | |
| { | |
| scopes.Add("task:all"); | |
| scopes.Add("task:write"); | |
| scopes.Add("comment:write"); | |
| } | |
| else | |
| { | |
| scopes.Add("task:read"); | |
| // Comments stay available to everyone who can view the task. | |
| scopes.Add("comment:write"); | |
| } | |
| } | |
| // βββ INLINE MUTATIONS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| private async Task OnStatusChanged(ChangeEventArgs e) | |
| { | |
| if (task == null) return; | |
| if (Enum.TryParse<AppTaskStatus>(e.Value?.ToString(), out var newStatus)) | |
| { | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var response = await client.PatchAsync($"Task/{task.Id}/status?statusId={(int)newStatus}", null); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| task.StatusId = newStatus; | |
| await OnTaskUpdated.InvokeAsync(); | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Update status error: {ex.Message}"); | |
| } | |
| } | |
| } | |
| private async Task OnPriorityChanged(ChangeEventArgs e) | |
| { | |
| if (Enum.TryParse<TaskPriority>(e.Value?.ToString(), out var newPriority)) | |
| { | |
| await UpdateTaskProperty(dto => dto.PriorityId = newPriority); | |
| } | |
| } | |
| private async Task OnAssigneeChanged(ChangeEventArgs e) | |
| { | |
| if (long.TryParse(e.Value?.ToString(), out var assigneeId)) | |
| { | |
| var newAssigneeId = assigneeId > 0 ? (long?)assigneeId : null; | |
| await UpdateTaskProperty(dto => dto.AssignedTo = newAssigneeId); | |
| assignee = newAssigneeId.HasValue ? allUsers.FirstOrDefault(u => u.Id == newAssigneeId.Value) : null; | |
| } | |
| } | |
| private async Task OnDueDateChanged(ChangeEventArgs e) | |
| { | |
| if (DateTime.TryParse(e.Value?.ToString(), out var newDueDate)) | |
| { | |
| await UpdateTaskProperty(dto => dto.DueDate = newDueDate); | |
| } | |
| } | |
| private async Task UpdateTaskProperty(Action<UpdateTaskDto> applyChange) | |
| { | |
| if (task == null) return; | |
| var dto = new UpdateTaskDto | |
| { | |
| Title = task.Title, | |
| Description = task.Description, | |
| StatusId = task.StatusId, | |
| PriorityId = task.PriorityId, | |
| DueDate = task.DueDate, | |
| AssignedTo = task.AssignedTo, | |
| AssignedBy = task.AssignedBy | |
| }; | |
| applyChange(dto); | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var response = await client.PutAsJsonAsync($"Task/{task.Id}", dto); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| task.Title = dto.Title; | |
| task.Description = dto.Description; | |
| task.StatusId = dto.StatusId; | |
| task.PriorityId = dto.PriorityId; | |
| task.DueDate = dto.DueDate; | |
| task.AssignedTo = dto.AssignedTo; | |
| await OnTaskUpdated.InvokeAsync(); | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Update task property error: {ex.Message}"); | |
| } | |
| } | |
| private void StartEditDescription() | |
| { | |
| if (task == null) return; | |
| tempDescription = task.Description ?? ""; | |
| isEditingDescription = true; | |
| } | |
| private void CancelEditDescription() | |
| { | |
| isEditingDescription = false; | |
| } | |
| private async Task SaveDescription() | |
| { | |
| isSavingDescription = true; | |
| await UpdateTaskProperty(dto => dto.Description = tempDescription); | |
| isEditingDescription = false; | |
| isSavingDescription = false; | |
| } | |
| private void StartEditTitle() | |
| { | |
| if (task == null) return; | |
| tempTitle = task.Title; | |
| isEditingTitle = true; | |
| } | |
| private void CancelEditTitle() | |
| { | |
| isEditingTitle = false; | |
| } | |
| private async Task SaveTitle() | |
| { | |
| if (string.IsNullOrWhiteSpace(tempTitle)) return; | |
| isSavingTitle = true; | |
| await UpdateTaskProperty(dto => dto.Title = tempTitle.Trim()); | |
| isEditingTitle = false; | |
| isSavingTitle = false; | |
| } | |
| // βββ COMMENTS ACTIONS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| private async Task PostComment() | |
| { | |
| if (string.IsNullOrWhiteSpace(newCommentMessage)) return; | |
| isSavingComment = true; | |
| commentError = null; | |
| try | |
| { | |
| var client = ApiClient.CreateClient(); | |
| var response = await client.PostAsJsonAsync($"TaskDetails/tasks/{TaskId}/comments", new CreateCommentDto { Message = newCommentMessage }); | |
| var res = await response.Content.ReadFromJsonAsync<Result<CommentDto>>(Serialization.CaseInsensitive); | |
| if (response.IsSuccessStatusCode && res != null && res.IsSuccess && res.Value != null) | |
| { | |
| comments.Add(res.Value); | |
| newCommentMessage = ""; | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| else | |
| { | |
| commentError = res?.ErrorMessage ?? "Failed to post comment."; | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| commentError = ex.Message; | |
| } | |
| finally | |
| { | |
| isSavingComment = false; | |
| } | |
| } | |
| private void ConfirmDeleteComment(long commentId) | |
| { | |
| confirmTitle = "Delete Comment?"; | |
| confirmMessage = "Are you sure you want to delete this comment? This action cannot be undone."; | |
| confirmAction = () => DeleteComment(commentId); | |
| showConfirm = true; | |
| } | |
| private async Task DeleteComment(long commentId) | |
| { | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var response = await client.DeleteAsync($"TaskDetails/comments/{commentId}"); | |
| var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive); | |
| if (response.IsSuccessStatusCode && res != null && res.IsSuccess) | |
| { | |
| var idx = comments.FindIndex(c => c.Id == commentId); | |
| if (idx >= 0) comments.RemoveAt(idx); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Delete comment error: {ex.Message}"); | |
| } | |
| } | |
| // --- CONFIRM DIALOG LOGIC ------------------------------------------------- | |
| private async Task ExecuteConfirmAction() | |
| { | |
| showConfirm = false; | |
| if (confirmAction != null) | |
| { | |
| await confirmAction(); | |
| } | |
| confirmAction = null; | |
| } | |
| private void CancelConfirm() | |
| { | |
| showConfirm = false; | |
| confirmAction = null; | |
| } | |
| // --- DISPLAY UTILITIES ---------------------------------------------------- | |
| private string GetStatusLabel(AppTaskStatus statusId) => statusId switch | |
| { | |
| AppTaskStatus.Todo => "To Do", | |
| AppTaskStatus.InProgress => "In Progress", | |
| AppTaskStatus.Done => "Done", | |
| _ => "Unknown" | |
| }; | |
| private string GetStatusBadge(AppTaskStatus statusId) => statusId switch | |
| { | |
| AppTaskStatus.Todo => "bg-slate-100 text-slate-800 border border-slate-200", | |
| AppTaskStatus.InProgress => "bg-blue-50 text-blue-700 border border-blue-100", | |
| AppTaskStatus.Done => "bg-emerald-50 text-emerald-700 border border-emerald-100", | |
| _ => "bg-slate-50 text-slate-600" | |
| }; | |
| private string GetPriorityLabel(TaskPriority priorityId) => priorityId switch | |
| { | |
| TaskPriority.Low => "Low", | |
| TaskPriority.Medium => "Medium", | |
| TaskPriority.High => "High", | |
| _ => "Unknown" | |
| }; | |
| private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch | |
| { | |
| TaskPriority.Low => "bg-slate-100 text-slate-600", | |
| TaskPriority.Medium => "bg-amber-50 text-amber-700 border border-amber-100", | |
| TaskPriority.High => "bg-rose-50 text-rose-700 border border-rose-100", | |
| _ => "bg-slate-50 text-slate-600" | |
| }; | |
| } | |