@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 @AppLocalization.PageTitle("kanbanBoard", "Kanban Board")
@if (ProjectId > 0) { }

@AppLocalization.PageTitle("kanbanBoard", "Kanban Board")

@(project != null ? Content.GetProjectName(project) : (ProjectId > 0 ? $"{AppLocalization.Text("common.project", "Project")} #{ProjectId}" : AppLocalization.Text("common.allProjects", "All Projects")))

@if (canUpdateTask && !showArchivedTasks) { } @if (canCreateTask && !showArchivedTasks) { }
@if (!string.IsNullOrWhiteSpace(boardError)) {
@boardError
} @if (isLoading) {
@AppLocalization.Text("task.loadingBoard", "Loading board...")
} else {
@(showArchivedTasks ? AppLocalization.Text("task.viewingArchived", "Viewing archived tasks.") : AppLocalization.Text("task.dragHint", "Drag tasks between columns to update their status."))
@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);

@AppLocalization.StatusLabel(column.StatusId)

@tasksInColumn.Count
@if (pagedTasks.Any()) { @foreach (var task in pagedTasks) {

@Content.GetTaskTitle(task)

@GetPriorityLabel(task.PriorityId)
@if (!string.IsNullOrEmpty(Content.GetTaskDescription(task))) {

@Content.GetTaskDescription(task)

}
@AppLocalization.Text("task.due", "Due") @DisplayFormats.Date(task.DueDate) @if (showArchivedTasks) { @AppLocalization.Text("task.archived", "Archived") }
} } else {

@AppLocalization.Text("task.dropTasksHere", "Drop tasks here")

}
@if (tasksInColumn.Count == 0) { @AppLocalization.Text("task.noTasks", "No tasks") } else { var from = (columnPage - 1) * kanbanPageSize + 1; var to = Math.Min(columnPage * kanbanPageSize, tasksInColumn.Count); @from-@to @AppLocalization.Text("task.of", "of") @tasksInColumn.Count }
@columnPage / @columnTotalPages
}
}
@* ===== Create Task Modal ===== *@ @if (showTaskModal) {

@AppLocalization.Text("task.newTask", "New Task")

@if (!string.IsNullOrEmpty(taskError)) {

@taskError

}
} @* ===== Confirm Dialog ===== *@ @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? confirmAction; private List tasks = new(); private List projects = new(); private long selectedProjectId = 0; private int kanbanPageSize = 5; private Dictionary 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 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 VisibleTasks => selectedProjectId > 0 ? tasks.Where(t => t.ProjectId == selectedProjectId) : tasks; private IEnumerable 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>(showArchivedTasks ? "Task/archived" : "Task", Serialization.CaseInsensitive); var projectsTask = client.GetFromJsonAsync>("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>(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); }