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(); } 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(); } }