using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using TaskTracker.Core.Entities; using TaskTracker.Core.Enums; using TaskTracker.Core.Interfaces; namespace TaskTracker.Api.Pages; public record ColumnViewModel(WorkTaskStatus Status, string Label, string Color, List Tasks); [IgnoreAntiforgeryToken] public class BoardModel : PageModel { private readonly ITaskRepository _taskRepo; public BoardModel(ITaskRepository taskRepo) { _taskRepo = taskRepo; } public List Columns { get; set; } = new(); public string? ActiveCategory { get; set; } public bool HasSubtasksFilter { get; set; } public List AllCategories { get; set; } = new(); private static readonly (WorkTaskStatus Status, string Label, string Color)[] ColumnConfig = [ (WorkTaskStatus.Pending, "Pending", "#64748b"), (WorkTaskStatus.Active, "Active", "#3b82f6"), (WorkTaskStatus.Paused, "Paused", "#eab308"), (WorkTaskStatus.Completed, "Completed", "#22c55e"), ]; public static readonly Dictionary CategoryColors = new() { ["Development"] = "#6366f1", ["Research"] = "#06b6d4", ["Communication"] = "#8b5cf6", ["DevOps"] = "#f97316", ["Documentation"] = "#14b8a6", ["Design"] = "#ec4899", ["Testing"] = "#3b82f6", ["General"] = "#64748b", ["Email"] = "#f59e0b", ["Engineering"] = "#6366f1", ["LaserCutting"] = "#ef4444", ["Unknown"] = "#475569", }; public static string GetCategoryColor(string? category) { if (string.IsNullOrEmpty(category)) return CategoryColors["Unknown"]; return CategoryColors.TryGetValue(category, out var color) ? color : CategoryColors["Unknown"]; } public static string FormatElapsed(DateTime? startedAt, DateTime? completedAt) { if (startedAt is null) return "--"; var start = startedAt.Value; var end = completedAt ?? DateTime.UtcNow; var mins = (int)(end - start).TotalMinutes; if (mins < 60) return $"{mins}m"; var hours = mins / 60; var remainder = mins % 60; if (hours < 24) return $"{hours}h {remainder}m"; var days = hours / 24; return $"{days}d {hours % 24}h"; } public async Task OnGetAsync(string? category, bool hasSubtasks = false) { ActiveCategory = category; HasSubtasksFilter = hasSubtasks; await LoadBoardDataAsync(); return Page(); } public async Task OnPutStartAsync(int id) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); var active = await _taskRepo.GetActiveTaskAsync(); if (active is not null && active.Id != id) { active.Status = WorkTaskStatus.Paused; await _taskRepo.UpdateAsync(active); } task.Status = WorkTaskStatus.Active; task.StartedAt ??= DateTime.UtcNow; await _taskRepo.UpdateAsync(task); return await ReturnBoardContentAsync(); } public async Task OnPutPauseAsync(int id) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); task.Status = WorkTaskStatus.Paused; await _taskRepo.UpdateAsync(task); return await ReturnBoardContentAsync(); } public async Task OnPutResumeAsync(int id) { var active = await _taskRepo.GetActiveTaskAsync(); if (active is not null) { active.Status = WorkTaskStatus.Paused; await _taskRepo.UpdateAsync(active); } var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); task.Status = WorkTaskStatus.Active; task.StartedAt ??= DateTime.UtcNow; await _taskRepo.UpdateAsync(task); return await ReturnBoardContentAsync(); } public async Task OnPutCompleteAsync(int id) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); var incompleteSubtasks = task.SubTasks .Count(st => st.Status != WorkTaskStatus.Completed && st.Status != WorkTaskStatus.Abandoned); if (incompleteSubtasks > 0) { return BadRequest($"Cannot complete: {incompleteSubtasks} subtask(s) still incomplete."); } task.Status = WorkTaskStatus.Completed; task.CompletedAt = DateTime.UtcNow; await _taskRepo.UpdateAsync(task); return await ReturnBoardContentAsync(); } public async Task OnDeleteAbandonAsync(int id) { await _taskRepo.DeleteAsync(id); return await ReturnBoardContentAsync(); } public async Task OnPostCreateTaskAsync(string title, string? category) { if (string.IsNullOrWhiteSpace(title)) return BadRequest("Title is required."); var task = new WorkTask { Title = title.Trim(), Category = category, Status = WorkTaskStatus.Pending, }; await _taskRepo.CreateAsync(task); return await ReturnBoardContentAsync(); } // Load task detail panel public async Task OnGetTaskDetailAsync(int id) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); return Partial("Partials/_TaskDetail", task); } // Update task fields (inline edit) public async Task OnPutUpdateTaskAsync(int id, string? title, string? description, string? category, int? estimatedMinutes) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); if (title is not null) task.Title = title; if (description is not null) task.Description = description; if (category is not null) task.Category = category; if (estimatedMinutes.HasValue) task.EstimatedMinutes = estimatedMinutes; await _taskRepo.UpdateAsync(task); return Partial("Partials/_TaskDetail", task); } // Add subtask public async Task OnPostAddSubtaskAsync(int id, string title) { var parent = await _taskRepo.GetByIdAsync(id); if (parent is null) return NotFound(); var subtask = new WorkTask { Title = title, ParentTaskId = id, Status = WorkTaskStatus.Pending }; await _taskRepo.CreateAsync(subtask); // Reload parent to get updated subtask list parent = await _taskRepo.GetByIdAsync(id); return Partial("Partials/_SubtaskList", parent); } // Complete subtask public async Task OnPutCompleteSubtaskAsync(int id) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); task.Status = WorkTaskStatus.Completed; task.CompletedAt = DateTime.UtcNow; await _taskRepo.UpdateAsync(task); // Return parent's subtask list if (task.ParentTaskId.HasValue) { var parent = await _taskRepo.GetByIdAsync(task.ParentTaskId.Value); if (parent is not null) return Partial("Partials/_SubtaskList", parent); } return Content(""); } // Add note public async Task OnPostAddNoteAsync(int id, string content) { var task = await _taskRepo.GetByIdAsync(id); if (task is null) return NotFound(); task.Notes.Add(new TaskNote { Content = content, Type = NoteType.General, CreatedAt = DateTime.UtcNow }); await _taskRepo.UpdateAsync(task); return Partial("Partials/_NotesList", task); } // Search tasks (for Ctrl+K modal) public async Task OnGetSearchAsync(string? q) { var allTasks = await _taskRepo.GetAllAsync(); List results; if (string.IsNullOrWhiteSpace(q)) { results = allTasks .Where(t => t.Status is WorkTaskStatus.Active or WorkTaskStatus.Paused or WorkTaskStatus.Pending) .OrderByDescending(t => t.Status == WorkTaskStatus.Active) .Take(8) .ToList(); } else { results = allTasks .Where(t => t.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || (t.Description?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false) || (t.Category?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false)) .Take(10) .ToList(); } return Partial("Partials/_SearchResults", results); } private async Task ReturnBoardContentAsync() { await LoadBoardDataAsync(); return Partial("Partials/_KanbanBoard", this); } private async Task LoadBoardDataAsync() { // Load all tasks with subtasks for category list var allTasks = await _taskRepo.GetAllAsync(includeSubTasks: true); // Collect all distinct categories from all tasks (unfiltered) AllCategories = allTasks .Where(t => !string.IsNullOrEmpty(t.Category)) .Select(t => t.Category!) .Distinct() .OrderBy(c => c) .ToList(); // Filter to top-level only var tasks = allTasks.Where(t => t.ParentTaskId == null).ToList(); // Apply category filter if (!string.IsNullOrEmpty(ActiveCategory)) { tasks = tasks.Where(t => string.Equals(t.Category, ActiveCategory, StringComparison.OrdinalIgnoreCase)).ToList(); } // Apply hasSubtasks filter if (HasSubtasksFilter) { tasks = tasks.Where(t => t.SubTasks.Count > 0).ToList(); } // Group by status into columns var tasksByStatus = tasks.ToLookup(t => t.Status); Columns = ColumnConfig .Select(cfg => new ColumnViewModel( cfg.Status, cfg.Label, cfg.Color, tasksByStatus[cfg.Status].ToList())) .ToList(); } }