Spaces:
Runtime error
Runtime error
task-tracking-system / TaskTrackingSystem.WebApp /Components /Pages /Features /Tasks /KanbanBoard.razor
Download TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor from Yuruyuruu/task-tracking-system: direct link, hf CLI and curl.
- Browser
- Download file 33.7 kB
-
https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor
- Command line
-
hf download hf://spaces/Yuruyuruu/task-tracking-system/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor
-
curl -L -o KanbanBoard.razor https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/KanbanBoard.razor
33.7 kB
| @page "/board" | |
| @page "/projects/{ProjectId:long}/tasks" | |
| @using Microsoft.AspNetCore.Authorization | |
| @using TaskTrackingSystem.Shared.Models.Project | |
| @rendermode @(new InteractiveServerRenderMode(prerender: false)) | |
| @attribute [Microsoft.AspNetCore.Authorization.Authorize] | |
| @inject ApiClientService ApiClient | |
| @inject NavigationManager Navigation | |
| @inject IJSRuntime JS | |
| @inject AuthenticationStateProvider AuthStateProvider | |
| @inject MenuAuthorizationService MenuAuthorization | |
| <PageTitle>@AppLocalization.PageTitle("kanbanBoard", "Kanban Board")</PageTitle> | |
| <SuccessAlert Message="@successMessage" /> | |
| <div class="space-y-6"> | |
| <!-- Page Header --> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-4"> | |
| @if (ProjectId > 0) | |
| { | |
| <button @onclick="GoBack" class="rounded-lg p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all"> | |
| <i data-lucide="arrow-left" class="h-5 w-5"></i> | |
| </button> | |
| } | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("kanbanBoard", "Kanban Board")</h2> | |
| <p class="text-slate-500 mt-1 font-medium">@(project != null ? Content.GetProjectName(project) : (ProjectId > 0 ? $"{AppLocalization.Text("common.project", "Project")} #{ProjectId}" : AppLocalization.Text("common.allProjects", "All Projects")))</p> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap items-center gap-3"> | |
| <select value="@selectedProjectId" @onchange="OnProjectFilterChanged" class="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all"> | |
| <option value="0">@AppLocalization.Text("common.allProjects", "All Projects")</option> | |
| @foreach (var p in projects) | |
| { | |
| <option value="@p.Id">@Content.GetProjectName(p)</option> | |
| } | |
| </select> | |
| <button @onclick="ToggleArchivedView" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="@(showArchivedTasks ? "layout-grid" : "archive")" class="h-4 w-4 mr-2 text-slate-500"></i> | |
| @(showArchivedTasks ? AppLocalization.Text("task.backToBoard", "Back to Board") : AppLocalization.Text("task.archivedTasks", "Archived Tasks")) | |
| </button> | |
| @if (canUpdateTask && !showArchivedTasks) | |
| { | |
| <button @onclick="RequestArchiveDoneConfirmation" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm"> | |
| <i data-lucide="archive" class="h-4 w-4 mr-2 text-slate-500"></i> | |
| @AppLocalization.Text("action.archiveDone", "Archive Done") | |
| </button> | |
| } | |
| @if (canCreateTask && !showArchivedTasks) | |
| { | |
| <button @onclick="OpenCreateTaskModal" 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("task.newTask", "New Task") | |
| </button> | |
| } | |
| </div> | |
| </div> | |
| @if (!string.IsNullOrWhiteSpace(boardError)) | |
| { | |
| <div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700"> | |
| @boardError | |
| </div> | |
| } | |
| @if (isLoading) | |
| { | |
| <div class="flex items-center justify-center py-20"> | |
| <div class="flex items-center space-x-3 text-slate-500"> | |
| <svg class="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> | |
| <span class="text-sm font-medium">@AppLocalization.Text("task.loadingBoard", "Loading board...")</span> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| <!-- Kanban Columns --> | |
| <div class="mb-2 text-sm text-slate-500"> | |
| @(showArchivedTasks ? AppLocalization.Text("task.viewingArchived", "Viewing archived tasks.") : AppLocalization.Text("task.dragHint", "Drag tasks between columns to update their status.")) | |
| </div> | |
| <div class="grid grid-cols-1 @(showArchivedTasks ? "md:grid-cols-1" : "md:grid-cols-3") gap-6"> | |
| @foreach (var column in VisibleColumns) | |
| { | |
| var tasksInColumn = VisibleTasks.Where(t => t.StatusId == column.StatusId).ToList(); | |
| var columnPage = GetColumnPage(column.StatusId, tasksInColumn.Count); | |
| var pagedTasks = tasksInColumn | |
| .Skip((columnPage - 1) * kanbanPageSize) | |
| .Take(kanbanPageSize) | |
| .ToList(); | |
| var columnTotalPages = GetColumnTotalPages(tasksInColumn.Count); | |
| <div @key="column.StatusId" class="flex flex-col rounded-xl border border-slate-200 bg-slate-50/50 overflow-hidden min-h-[400px]"> | |
| <!-- Column Header --> | |
| <div class="flex items-center justify-between px-4 py-3 bg-white border-b border-slate-100"> | |
| <div class="flex items-center space-x-2"> | |
| <span class="h-2.5 w-2.5 rounded-full @column.DotColor"></span> | |
| <h3 class="text-sm font-semibold text-slate-900">@AppLocalization.StatusLabel(column.StatusId)</h3> | |
| </div> | |
| <span class="flex h-6 w-6 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-600">@tasksInColumn.Count</span> | |
| </div> | |
| <!-- Cards --> | |
| <div class="flex-1 p-3 space-y-3 overflow-y-auto transition-colors @((!showArchivedTasks && dropTargetStatus == column.StatusId) ? "bg-slate-100" : "")" | |
| @ondrop="() => HandleDrop(column.StatusId)" | |
| @ondragenter="() => HandleDragEnter(column.StatusId)" | |
| @ondragleave="HandleDragLeave" | |
| @ondragover="HandleDragOver" | |
| @ondragover:preventDefault="true"> | |
| @if (pagedTasks.Any()) | |
| { | |
| @foreach (var task in pagedTasks) | |
| { | |
| <div @key="task.Id" class="select-none rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow @((canUpdateTask && !showArchivedTasks) ? "cursor-grab active:cursor-grabbing" : "cursor-default")" | |
| data-task-id="@task.Id" | |
| draggable="@((canUpdateTask && !showArchivedTasks).ToString().ToLowerInvariant())" | |
| @ondragstart="() => HandleDragStart(task)" | |
| @ondragend="HandleDragEnd"> | |
| <div class="flex items-start justify-between"> | |
| <h4 class="text-sm font-semibold text-slate-900">@Content.GetTaskTitle(task)</h4> | |
| <span class="text-xs px-2 py-0.5 rounded-full font-medium @GetPriorityBadge(task.PriorityId)">@GetPriorityLabel(task.PriorityId)</span> | |
| </div> | |
| @if (!string.IsNullOrEmpty(Content.GetTaskDescription(task))) | |
| { | |
| <p class="text-xs text-slate-500 mt-1.5 line-clamp-2">@Content.GetTaskDescription(task)</p> | |
| } | |
| <div class="flex items-center justify-between mt-3 pt-3 border-t border-slate-50"> | |
| <span class="text-xs text-slate-400">@AppLocalization.Text("task.due", "Due") @DisplayFormats.Date(task.DueDate)</span> | |
| @if (showArchivedTasks) | |
| { | |
| <span class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-slate-500">@AppLocalization.Text("task.archived", "Archived")</span> | |
| } | |
| </div> | |
| </div> | |
| } | |
| } | |
| else | |
| { | |
| <div class="flex items-center justify-center py-8 h-full"> | |
| <p class="text-xs text-slate-400">@AppLocalization.Text("task.dropTasksHere", "Drop tasks here")</p> | |
| </div> | |
| } | |
| </div> | |
| <div class="border-t border-slate-100 bg-white px-3 py-2"> | |
| <div class="flex flex-wrap items-center justify-between gap-2"> | |
| <div class="text-[11px] text-slate-500"> | |
| @if (tasksInColumn.Count == 0) | |
| { | |
| <span>@AppLocalization.Text("task.noTasks", "No tasks")</span> | |
| } | |
| else | |
| { | |
| var from = (columnPage - 1) * kanbanPageSize + 1; | |
| var to = Math.Min(columnPage * kanbanPageSize, tasksInColumn.Count); | |
| <span><span class="font-semibold text-slate-700">@from-@to</span> @AppLocalization.Text("task.of", "of") <span class="font-semibold text-slate-700">@tasksInColumn.Count</span></span> | |
| } | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <select value="@kanbanPageSize" | |
| @onchange="HandleKanbanPageSizeChanged" | |
| class="rounded-lg border border-slate-200 bg-white px-2 py-1 text-[11px] text-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-600 transition-all"> | |
| <option value="5">5</option> | |
| <option value="10">10</option> | |
| <option value="20">20</option> | |
| </select> | |
| <button @onclick="() => ChangeColumnPage(column.StatusId, columnPage - 1)" | |
| disabled="@(columnPage <= 1)" | |
| class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-slate-50 hover:text-slate-700 disabled:cursor-not-allowed disabled:opacity-40" | |
| title="@AppLocalization.Text("task.previousPage", "Previous page")"> | |
| <i data-lucide="chevron-left" class="h-3.5 w-3.5"></i> | |
| </button> | |
| <span class="min-w-10 text-center text-[11px] font-semibold text-slate-600">@columnPage / @columnTotalPages</span> | |
| <button @onclick="() => ChangeColumnPage(column.StatusId, columnPage + 1)" | |
| disabled="@(columnPage >= columnTotalPages)" | |
| class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-slate-50 hover:text-slate-700 disabled:cursor-not-allowed disabled:opacity-40" | |
| title="@AppLocalization.Text("task.nextPage", "Next page")"> | |
| <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| } | |
| </div> | |
| @* ===== Create Task Modal ===== *@ | |
| @if (showTaskModal) | |
| { | |
| <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> | |
| <div class="bg-white rounded-2xl shadow-lg w-full max-w-lg mx-4 overflow-hidden"> | |
| <div class="flex items-center justify-between px-6 py-4 border-b border-slate-100"> | |
| <h3 class="text-lg font-semibold text-slate-900">@AppLocalization.Text("task.newTask", "New Task")</h3> | |
| <button @onclick="CloseTaskModal" 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="p-6 space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.project", "Project")</label> | |
| <select @bind="taskProjectId" disabled="@(ProjectId > 0)" 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-slate-900 focus:border-transparent transition-all disabled:bg-slate-50 disabled:text-slate-500"> | |
| <option value="0">@AppLocalization.Text("task.selectProject", "Select project...")</option> | |
| @foreach (var p in projects) | |
| { | |
| <option value="@p.Id">@Content.GetProjectName(p)</option> | |
| } | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("field.title", "Title")</label> | |
| <input @bind="taskTitleMy" 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:border-transparent transition-all" /> | |
| <input @bind="taskTitle" type="text" placeholder="@AppLocalization.Text("task.taskTitle", "Task 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:border-transparent transition-all" /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.description", "Description")</label> | |
| <textarea @bind="taskDescriptionMy" rows="2" 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:border-transparent transition-all"></textarea> | |
| <textarea @bind="taskDescription" rows="3" placeholder="@AppLocalization.Text("task.taskDescription", "Task description")" 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:border-transparent transition-all"></textarea> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label> | |
| <select @bind="taskPriorityId" 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-slate-900 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.dueDate", "Due Date")</label> | |
| <input @bind="taskDueDate" 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-slate-900 focus:border-transparent transition-all" /> | |
| </div> | |
| </div> | |
| @if (!string.IsNullOrEmpty(taskError)) | |
| { | |
| <div class="p-3 rounded-lg bg-red-50 border border-red-100"> | |
| <p class="text-sm text-red-600">@taskError</p> | |
| </div> | |
| } | |
| </div> | |
| <div class="flex items-center justify-end space-x-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50"> | |
| <button @onclick="CloseTaskModal" 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="RequestCreateConfirmation" 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.creating", "Creating...") : AppLocalization.Text("task.createTask", "Create Task")) | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| @* ===== Confirm Dialog ===== *@ | |
| <ConfirmDialog IsVisible="showConfirmDialog" | |
| Title="@confirmTitle" | |
| Message="@confirmMessage" | |
| ConfirmText="@confirmButtonText" | |
| Icon="@confirmIcon" | |
| IconBgClass="@confirmIconBg" | |
| IconTextClass="@confirmIconText" | |
| ButtonClass="@confirmButtonClass" | |
| OnConfirm="HandleConfirmAction" | |
| OnCancel="CloseConfirmDialog" /> | |
| @code { | |
| [Parameter] public long ProjectId { get; set; } | |
| private bool isLoading = true; | |
| private bool isAdmin = false; | |
| private bool isManager = false; | |
| private bool canCreateTask = false; | |
| private bool canUpdateTask = false; | |
| private bool showArchivedTasks = false; | |
| private long currentUserId = 0; | |
| private ProjectDto? project; | |
| private bool showTaskModal = false; | |
| private bool isSaving = false; | |
| private string? taskError; | |
| private string? boardError; | |
| // Confirm dialog state | |
| private bool showConfirmDialog = false; | |
| private string? successMessage; | |
| private string confirmTitle = ""; | |
| private string confirmMessage = ""; | |
| private string confirmButtonText = ""; | |
| private string confirmIcon = ""; | |
| private string confirmIconBg = ""; | |
| private string confirmIconText = ""; | |
| private string confirmButtonClass = ""; | |
| private Func<Task>? confirmAction; | |
| private List<TaskDto> tasks = new(); | |
| private List<ProjectDto> projects = new(); | |
| private long selectedProjectId = 0; | |
| private int kanbanPageSize = 5; | |
| private Dictionary<AppTaskStatus, int> columnPages = new(); | |
| // Task form | |
| private long taskProjectId; | |
| private string taskTitle = ""; | |
| private string? taskTitleMy; | |
| private string? taskDescription; | |
| private string? taskDescriptionMy; | |
| private TaskPriority taskPriorityId = TaskPriority.Medium; | |
| private DateTime taskDueDate = DateTime.Today.AddDays(7); | |
| private List<KanbanColumn> columns = new() | |
| { | |
| new(AppTaskStatus.Todo, "To Do", "bg-slate-400"), | |
| new(AppTaskStatus.InProgress, "In Progress", "bg-blue-500"), | |
| new(AppTaskStatus.Done, "Done", "bg-emerald-500") | |
| }; | |
| private TaskDto? draggedTask; | |
| private AppTaskStatus? dropTargetStatus; | |
| private IEnumerable<TaskDto> VisibleTasks => | |
| selectedProjectId > 0 | |
| ? tasks.Where(t => t.ProjectId == selectedProjectId) | |
| : tasks; | |
| private IEnumerable<KanbanColumn> VisibleColumns => | |
| showArchivedTasks | |
| ? columns.Where(column => column.StatusId == AppTaskStatus.Done) | |
| : columns; | |
| private int GetColumnTotalPages(int totalCount) => | |
| Math.Max(1, (int)Math.Ceiling(totalCount / (double)kanbanPageSize)); | |
| private int GetColumnPage(AppTaskStatus statusId, int totalCount) | |
| { | |
| var totalPages = GetColumnTotalPages(totalCount); | |
| if (!columnPages.TryGetValue(statusId, out var page)) | |
| { | |
| page = 1; | |
| } | |
| page = Math.Clamp(page, 1, totalPages); | |
| columnPages[statusId] = page; | |
| return page; | |
| } | |
| private void ChangeColumnPage(AppTaskStatus statusId, int page) | |
| { | |
| var totalCount = VisibleTasks.Count(t => t.StatusId == statusId); | |
| var totalPages = GetColumnTotalPages(totalCount); | |
| columnPages[statusId] = Math.Clamp(page, 1, totalPages); | |
| } | |
| private void HandleKanbanPageSizeChanged(ChangeEventArgs e) | |
| { | |
| if (int.TryParse(e.Value?.ToString(), out var size) && size > 0 && size != kanbanPageSize) | |
| { | |
| kanbanPageSize = size; | |
| ResetColumnPages(); | |
| } | |
| } | |
| private void ResetColumnPages() | |
| { | |
| columnPages.Clear(); | |
| } | |
| private Task HandleDragStart(TaskDto task) | |
| { | |
| if (!canUpdateTask) | |
| { | |
| return Task.CompletedTask; | |
| } | |
| if (showArchivedTasks) | |
| { | |
| return Task.CompletedTask; | |
| } | |
| draggedTask = task; | |
| dropTargetStatus = null; | |
| return Task.CompletedTask; | |
| } | |
| private void HandleDragOver() | |
| { | |
| // Allow dropping | |
| } | |
| private void HandleDragEnter(AppTaskStatus statusId) | |
| { | |
| if (draggedTask != null) | |
| { | |
| dropTargetStatus = statusId; | |
| } | |
| } | |
| private void HandleDragLeave() | |
| { | |
| dropTargetStatus = null; | |
| } | |
| private async Task HandleDrop(AppTaskStatus newStatusId) | |
| { | |
| var taskToMove = draggedTask; | |
| if (taskToMove != null && taskToMove.StatusId != newStatusId) | |
| { | |
| await MoveTask(taskToMove.Id, newStatusId); | |
| } | |
| draggedTask = null; | |
| dropTargetStatus = null; | |
| } | |
| private Task HandleDragEnd() | |
| { | |
| draggedTask = null; | |
| dropTargetStatus = null; | |
| return Task.CompletedTask; | |
| } | |
| protected override async Task OnInitializedAsync() | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| var user = authState.User; | |
| isAdmin = user.IsInRole("Admin"); | |
| isManager = user.IsInRole("Manager"); | |
| canCreateTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Create"); | |
| canUpdateTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Update"); | |
| selectedProjectId = ProjectId > 0 ? ProjectId : 0; | |
| await LoadTasks(); | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task LoadTasks() | |
| { | |
| isLoading = true; | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| var user = authState.User; | |
| isAdmin = user.IsInRole("Admin"); | |
| isManager = user.IsInRole("Manager"); | |
| var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; | |
| if (idStr != null) long.TryParse(idStr, out currentUserId); | |
| var tasksTask = client.GetFromJsonAsync<List<TaskDto>>(showArchivedTasks ? "Task/archived" : "Task", Serialization.CaseInsensitive); | |
| var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive); | |
| await System.Threading.Tasks.Task.WhenAll(tasksTask, projectsTask); | |
| var allTasks = tasksTask.Result ?? new(); | |
| projects = projectsTask.Result ?? new(); | |
| project = selectedProjectId > 0 ? projects.FirstOrDefault(p => p.Id == selectedProjectId) : null; | |
| // Filter for non-admins and non-managers | |
| tasks = (!(isAdmin || isManager) && currentUserId > 0) | |
| ? allTasks.Where(t => t.AssignedTo == currentUserId).ToList() | |
| : allTasks; | |
| ResetColumnPages(); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Load tasks error: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| isLoading = false; | |
| } | |
| } | |
| private async Task MoveTask(long taskId, AppTaskStatus newStatusId) | |
| { | |
| if (!canUpdateTask) | |
| { | |
| return; | |
| } | |
| var client = ApiClient.CreateClient(); | |
| var task = tasks.FirstOrDefault(t => t.Id == taskId); | |
| var previousStatus = task?.StatusId; | |
| if (task != null) | |
| { | |
| task.StatusId = newStatusId; | |
| boardError = null; | |
| StateHasChanged(); | |
| } | |
| try | |
| { | |
| var response = await client.PatchAsync($"Task/{taskId}/status?statusId={(long)newStatusId}", null); | |
| if (!response.IsSuccessStatusCode) | |
| { | |
| if (task != null && previousStatus.HasValue) | |
| { | |
| task.StatusId = previousStatus.Value; | |
| } | |
| boardError = AppLocalization.Text("task.failedMove", "Failed to move task. Please try again."); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| if (task != null && previousStatus.HasValue) | |
| { | |
| task.StatusId = previousStatus.Value; | |
| } | |
| await LoadTasks(); | |
| Console.WriteLine($"Move task error: {ex.Message}"); | |
| boardError = AppLocalization.Text("task.failedMove", "Failed to move task. Please try again."); | |
| } | |
| } | |
| private void GoBack() => Navigation.NavigateTo("/projects/all"); | |
| private async Task ToggleArchivedView() | |
| { | |
| showArchivedTasks = !showArchivedTasks; | |
| draggedTask = null; | |
| dropTargetStatus = null; | |
| boardError = null; | |
| ResetColumnPages(); | |
| await LoadTasks(); | |
| } | |
| private void OnProjectFilterChanged(ChangeEventArgs e) | |
| { | |
| selectedProjectId = long.TryParse(e.Value?.ToString(), out var value) ? value : 0; | |
| project = selectedProjectId > 0 ? projects.FirstOrDefault(p => p.Id == selectedProjectId) : null; | |
| taskProjectId = selectedProjectId; | |
| boardError = null; | |
| ResetColumnPages(); | |
| } | |
| private void OpenCreateTaskModal() | |
| { | |
| taskProjectId = selectedProjectId > 0 ? selectedProjectId : projects.FirstOrDefault()?.Id ?? 0; | |
| taskTitle = ""; | |
| taskTitleMy = null; | |
| taskDescription = null; | |
| taskDescriptionMy = null; | |
| taskPriorityId = TaskPriority.Medium; | |
| taskDueDate = DateTime.Today.AddDays(7); | |
| taskError = null; | |
| showTaskModal = true; | |
| } | |
| private void CloseTaskModal() | |
| { | |
| showTaskModal = false; | |
| taskError = null; | |
| } | |
| private void RequestCreateConfirmation() | |
| { | |
| if (string.IsNullOrWhiteSpace(taskTitle) && string.IsNullOrWhiteSpace(taskTitleMy)) | |
| { | |
| taskError = AppLocalization.Text("validation.taskTitleRequired", "Task title is required."); | |
| return; | |
| } | |
| if (taskProjectId == 0) | |
| { | |
| taskError = AppLocalization.Text("validation.selectProjectRequired", "Please select a project."); | |
| return; | |
| } | |
| confirmTitle = AppLocalization.Text("task.createTitle", "Create Task?"); | |
| confirmMessage = AppLocalization.Text("task.createConfirm", "Are you sure you want to create this task?"); | |
| confirmButtonText = AppLocalization.Text("action.create", "Create"); | |
| confirmIcon = "plus-circle"; | |
| confirmIconBg = "bg-emerald-50"; | |
| confirmIconText = "text-emerald-500"; | |
| confirmButtonClass = "bg-violet-600 hover:bg-violet-700"; | |
| confirmAction = CreateTask; | |
| showConfirmDialog = true; | |
| } | |
| private async Task HandleConfirmAction() | |
| { | |
| showConfirmDialog = false; | |
| if (confirmAction != null) | |
| await confirmAction(); | |
| } | |
| private void CloseConfirmDialog() | |
| { | |
| showConfirmDialog = false; | |
| } | |
| private async Task CreateTask() | |
| { | |
| if (string.IsNullOrWhiteSpace(taskTitle) && string.IsNullOrWhiteSpace(taskTitleMy)) | |
| { | |
| taskError = AppLocalization.Text("validation.taskTitleRequired", "Task title is required."); | |
| return; | |
| } | |
| isSaving = true; | |
| taskError = null; | |
| var client = ApiClient.CreateClient(); | |
| try | |
| { | |
| var dto = new CreateTaskDto | |
| { | |
| Title = taskTitle, | |
| TitleMy = taskTitleMy, | |
| Description = taskDescription, | |
| DescriptionMy = taskDescriptionMy, | |
| ProjectId = taskProjectId, | |
| StatusId = AppTaskStatus.Todo, | |
| PriorityId = taskPriorityId, | |
| DueDate = taskDueDate | |
| }; | |
| var response = await client.PostAsJsonAsync("Task", dto); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| CloseTaskModal(); | |
| await LoadTasks(); | |
| selectedProjectId = taskProjectId; | |
| project = projects.FirstOrDefault(p => p.Id == selectedProjectId); | |
| ResetColumnPages(); | |
| successMessage = AppLocalization.Text("task.created", "Task created successfully!"); | |
| } | |
| else | |
| { | |
| taskError = AppLocalization.Text("task.failedCreate", "Failed to create task."); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| taskError = ex.Message; | |
| } | |
| finally | |
| { | |
| isSaving = false; | |
| } | |
| } | |
| private void RequestArchiveDoneConfirmation() | |
| { | |
| var doneCount = VisibleTasks.Count(t => t.StatusId == AppTaskStatus.Done); | |
| if (doneCount == 0) | |
| { | |
| successMessage = AppLocalization.Text("task.noDoneToArchive", "No Done tasks to archive."); | |
| return; | |
| } | |
| confirmTitle = AppLocalization.Text("task.archiveDoneTitle", "Archive Done Tasks?"); | |
| confirmMessage = selectedProjectId > 0 | |
| ? AppLocalization.Text("task.archiveDoneInProject", "Archive {count} Done task(s) in this project? Archived tasks will be hidden from active views.").Replace("{count}", doneCount.ToString()) | |
| : AppLocalization.Text("task.archiveDoneAll", "Archive {count} Done task(s) across all visible projects? Archived tasks will be hidden from active views.").Replace("{count}", doneCount.ToString()); | |
| confirmButtonText = AppLocalization.Text("task.archive", "Archive"); | |
| confirmIcon = "archive"; | |
| confirmIconBg = "bg-slate-100"; | |
| confirmIconText = "text-slate-600"; | |
| confirmButtonClass = "bg-slate-900 hover:bg-slate-800"; | |
| confirmAction = ArchiveDoneTasks; | |
| showConfirmDialog = true; | |
| } | |
| private async Task ArchiveDoneTasks() | |
| { | |
| var client = ApiClient.CreateClient(); | |
| boardError = null; | |
| var url = selectedProjectId > 0 | |
| ? $"Task/archive-done?projectId={selectedProjectId}" | |
| : "Task/archive-done"; | |
| try | |
| { | |
| var response = await client.PatchAsync(url, null); | |
| var result = await response.Content.ReadFromJsonAsync<Result<int>>(Serialization.CaseInsensitive); | |
| if (response.IsSuccessStatusCode && result?.IsSuccess == true) | |
| { | |
| await LoadTasks(); | |
| successMessage = result.Value == 1 | |
| ? AppLocalization.Text("task.archivedOneDone", "Archived 1 Done task.") | |
| : AppLocalization.Text("task.archivedManyDone", "Archived {count} Done tasks.").Replace("{count}", (result?.Value ?? 0).ToString()); | |
| return; | |
| } | |
| boardError = result?.ErrorMessage ?? AppLocalization.Text("task.failedArchiveDone", "Failed to archive Done tasks."); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"Archive done tasks error: {ex.Message}"); | |
| boardError = AppLocalization.Text("task.failedArchiveDone", "Failed to archive Done tasks."); | |
| } | |
| } | |
| 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) => AppLocalization.PriorityLabel(priorityId); | |
| private record KanbanColumn(AppTaskStatus StatusId, string Name, string DotColor); | |
| } | |