389 lines
14 KiB
Plaintext
389 lines
14 KiB
Plaintext
@page "/tasks"
|
|
@using Tasks.Module.Models
|
|
@using Tasks.Module.Services
|
|
@using Tasks.Module.Components.Dialogs
|
|
@inject ITodoService TodoService
|
|
@inject IDialogService Dialog
|
|
@inject IViewService ViewService
|
|
|
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
|
<div class="max-w-4xl mx-auto py-8 px-4">
|
|
@* Header *@
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-slate-800">My Tasks</h1>
|
|
<p class="text-slate-500 mt-1">Manage your tasks and sub-tasks</p>
|
|
</div>
|
|
|
|
@* Search and Filters *@
|
|
<div class="bg-white rounded-2xl shadow-md p-4 mb-4">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<MudTextField T="string"
|
|
@bind-Value:get="searchText"
|
|
@bind-Value:set="OnSearchChanged"
|
|
Placeholder="Search tasks..."
|
|
DisableUnderLine="true"
|
|
Adornment="Adornment.Start"
|
|
AdornmentIcon="@Icons.Material.Filled.Search"
|
|
Class="flex-grow"
|
|
Style="max-width: 400px;" />
|
|
<MudButton Variant="Variant.Text"
|
|
Color="Color.Default"
|
|
StartIcon="@(_showFilters ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
|
OnClick="ToggleFilters"
|
|
Class="rounded">
|
|
@(_showFilters ? "Hide Filters" : "Show Filters")
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
Color="Color.Default"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="CreateView"
|
|
Class="rounded">
|
|
Add View
|
|
</MudButton>
|
|
</div>
|
|
@if (_showFilters)
|
|
{
|
|
<div class="flex flex-wrap items-center gap-3 mt-3">
|
|
<MudSelect T="int?" Label="Priority" Value="selectedPriority" ValueChanged="OnPriorityChanged" Clearable="true" Class="min-w-[140px]">
|
|
<MudSelectItem T="int?" Value="null">All</MudSelectItem>
|
|
<MudSelectItem T="int?" Value="0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-2 h-2 bg-green-500 rounded-full"></span> Low
|
|
</div>
|
|
</MudSelectItem>
|
|
<MudSelectItem T="int?" Value="1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-2 h-2 bg-yellow-500 rounded-full"></span> Medium
|
|
</div>
|
|
</MudSelectItem>
|
|
<MudSelectItem T="int?" Value="2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-2 h-2 bg-red-500 rounded-full"></span> High
|
|
</div>
|
|
</MudSelectItem>
|
|
</MudSelect>
|
|
<MudSelect T="bool?" Label="Status" Value="selectedCompleted" ValueChanged="OnCompletedChanged" Clearable="true" Class="min-w-[140px]">
|
|
<MudSelectItem T="bool?" Value="null">All</MudSelectItem>
|
|
<MudSelectItem T="bool?" Value="false">
|
|
<div class="flex items-center gap-2">
|
|
<MudIcon Icon="@Icons.Material.Filled.Pending" Size="Size.Small" Color="Color.Warning" />
|
|
Pending
|
|
</div>
|
|
</MudSelectItem>
|
|
<MudSelectItem T="bool?" Value="true">
|
|
<div class="flex items-center gap-2">
|
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
|
|
Completed
|
|
</div>
|
|
</MudSelectItem>
|
|
</MudSelect>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@* Quick Add Input *@
|
|
<div class="bg-white rounded-2xl shadow-lg p-4 mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex-shrink-0 w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
|
|
<MudIcon Icon="@Icons.Material.Filled.Add" Color="Color.Primary" Size="Size.Small" />
|
|
</div>
|
|
<MudTextField T="string"
|
|
@bind-Value:get="quickAddTitle"
|
|
@bind-Value:set="(v) => { quickAddTitle = v; StateHasChanged(); }"
|
|
Placeholder="Add a task quickly... (press Enter)"
|
|
DisableUnderLine="true"
|
|
Class="flex-grow"
|
|
Adornment="Adornment.None"
|
|
OnKeyDown="HandleQuickAdd" />
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
OnClick="QuickAddTask"
|
|
Class="rounded-xl">
|
|
Add
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="OpenCreateDialog"
|
|
Class="rounded-xl">
|
|
Add Task
|
|
</MudButton>
|
|
</div>
|
|
</div>
|
|
|
|
@* Stats *@
|
|
@if (!isLoading && tasks.Any())
|
|
{
|
|
<div class="flex items-center gap-4 mb-4 text-sm">
|
|
<span class="text-slate-500">
|
|
@tasks.Count task@(tasks.Count != 1 ? "s" : "")
|
|
</span>
|
|
@{
|
|
var completed = tasks.Count(t => t.IsCompleted);
|
|
var pending = tasks.Count - completed;
|
|
}
|
|
@if (pending > 0)
|
|
{
|
|
<span class="px-2 py-1 bg-yellow-100 text-yellow-700 rounded-full text-xs">@pending pending</span>
|
|
}
|
|
@if (completed > 0)
|
|
{
|
|
<span class="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">@completed completed</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@* Tasks List *@
|
|
@if (isLoading)
|
|
{
|
|
<div class="flex justify-center py-12">
|
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
|
</div>
|
|
}
|
|
else if (tasks.Count == 0)
|
|
{
|
|
<div class="bg-white rounded-2xl shadow-lg p-12 text-center border border-slate-100">
|
|
<div class="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<MudIcon Icon="@Icons.Material.Filled.Assignment" Size="Size.Large" Color="Color.Default" />
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-slate-700">No tasks yet</h3>
|
|
<p class="text-slate-500 mt-2">Type a task above and press Enter to get started</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="space-y-3">
|
|
@foreach (var task in tasks)
|
|
{
|
|
<TaskCard Task="task"
|
|
OnToggle="ToggleTask"
|
|
OnDelete="DeleteTask"
|
|
OnUpdate="OpenUpdateDialog"
|
|
OnAddChild="OpenAddChildDialog"
|
|
CollapsedTasks="collapsedTasks"
|
|
ToggleCollapse="ToggleCollapse" />
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private List<TodoItemDto> tasks = new();
|
|
private List<TodoItemDto> allTasks = new();
|
|
private bool isLoading = true;
|
|
private int? selectedPriority;
|
|
private bool? selectedCompleted;
|
|
private CreateTodoItemDto editTask = new();
|
|
private string currentTaskId = string.Empty;
|
|
private string quickAddTitle = string.Empty;
|
|
private HashSet<string> collapsedTasks = new();
|
|
private string searchText = string.Empty;
|
|
private bool _showFilters = false;
|
|
|
|
private void ToggleFilters()
|
|
{
|
|
_showFilters = !_showFilters;
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadTasks();
|
|
}
|
|
|
|
private async Task LoadTasks()
|
|
{
|
|
isLoading = true;
|
|
try
|
|
{
|
|
var filter = new TodoFilterDto
|
|
{
|
|
Priority = selectedPriority,
|
|
IsCompleted = selectedCompleted
|
|
};
|
|
allTasks = await TodoService.GetAllTodosAsync(filter);
|
|
ApplyFilters();
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
private void ApplyFilters()
|
|
{
|
|
var filtered = allTasks.AsEnumerable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchText))
|
|
{
|
|
filtered = filtered.Where(t =>
|
|
t.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
|
(t.Description?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false));
|
|
}
|
|
|
|
tasks = filtered.ToList();
|
|
}
|
|
|
|
private void OnSearchChanged(string value)
|
|
{
|
|
searchText = value;
|
|
ApplyFilters();
|
|
}
|
|
|
|
private async Task HandleQuickAdd(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(quickAddTitle))
|
|
{
|
|
await QuickAddTask();
|
|
}
|
|
}
|
|
|
|
private async Task QuickAddTask()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(quickAddTitle))
|
|
return;
|
|
|
|
var newTask = new CreateTodoItemDto
|
|
{
|
|
Title = quickAddTitle.Trim(),
|
|
Priority = 0
|
|
};
|
|
|
|
await TodoService.CreateTodoAsync(newTask);
|
|
quickAddTitle = string.Empty;
|
|
await LoadTasks();
|
|
}
|
|
|
|
private void ToggleCollapse(string taskId)
|
|
{
|
|
if (collapsedTasks.Contains(taskId))
|
|
collapsedTasks.Remove(taskId);
|
|
else
|
|
collapsedTasks.Add(taskId);
|
|
}
|
|
|
|
private async Task OnPriorityChanged(int? value)
|
|
{
|
|
selectedPriority = value;
|
|
await LoadTasks();
|
|
}
|
|
|
|
private async Task OnCompletedChanged(bool? value)
|
|
{
|
|
selectedCompleted = value;
|
|
await LoadTasks();
|
|
}
|
|
|
|
private async Task ToggleTask(string id)
|
|
{
|
|
await TodoService.ToggleCompleteAsync(id);
|
|
await LoadTasks();
|
|
}
|
|
|
|
private async Task DeleteTask(string id)
|
|
{
|
|
await TodoService.DeleteTodoAsync(id);
|
|
await LoadTasks();
|
|
}
|
|
|
|
private async Task OpenCreateDialog()
|
|
{
|
|
editTask = new CreateTodoItemDto();
|
|
|
|
var parameters = new DialogParameters<CreateTaskDialog>
|
|
{
|
|
{ x => x.Task, editTask },
|
|
{ x => x.IsUpdate, false }
|
|
};
|
|
|
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
|
var dialog = await Dialog.ShowAsync<CreateTaskDialog>("Create Task", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (result is { Canceled: false })
|
|
{
|
|
await TodoService.CreateTodoAsync(editTask);
|
|
await LoadTasks();
|
|
}
|
|
}
|
|
|
|
private async Task OpenAddChildDialog(TodoItemDto parentTask)
|
|
{
|
|
editTask = new CreateTodoItemDto { ParentId = parentTask.Id };
|
|
|
|
var parameters = new DialogParameters<CreateTaskDialog>
|
|
{
|
|
{ x => x.Task, editTask },
|
|
{ x => x.IsUpdate, false },
|
|
{ x => x.IsChild, true },
|
|
{ x => x.ParentTitle, parentTask.Title }
|
|
};
|
|
|
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
|
var dialog = await Dialog.ShowAsync<CreateTaskDialog>("Add Sub-task", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (result is { Canceled: false })
|
|
{
|
|
await TodoService.CreateTodoAsync(editTask);
|
|
await LoadTasks();
|
|
}
|
|
}
|
|
|
|
private async Task OpenUpdateDialog(TodoItemDto task)
|
|
{
|
|
currentTaskId = task.Id;
|
|
editTask = new CreateTodoItemDto
|
|
{
|
|
Title = task.Title,
|
|
Description = task.Description,
|
|
Priority = task.Priority,
|
|
DueDate = task.DueDate,
|
|
ParentId = task.ParentId,
|
|
CategoryId = task.CategoryId,
|
|
TagIds = task.TagIds.ToList(),
|
|
FileId = task.FileId
|
|
};
|
|
|
|
var parameters = new DialogParameters<CreateTaskDialog>
|
|
{
|
|
{ x => x.Task, editTask },
|
|
{ x => x.IsUpdate, true }
|
|
};
|
|
|
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
|
var dialog = await Dialog.ShowAsync<CreateTaskDialog>("Update Task", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (result is { Canceled: false })
|
|
{
|
|
await TodoService.UpdateTodoAsync(new UpdateTodoItemDto
|
|
{
|
|
Id = currentTaskId,
|
|
Title = editTask.Title,
|
|
Description = editTask.Description,
|
|
Priority = editTask.Priority,
|
|
DueDate = editTask.DueDate,
|
|
IsCompleted = task.IsCompleted,
|
|
ParentId = editTask.ParentId,
|
|
CategoryId = editTask.CategoryId,
|
|
TagIds = editTask.TagIds,
|
|
FileId = editTask.FileId
|
|
});
|
|
await LoadTasks();
|
|
}
|
|
}
|
|
|
|
private async Task CreateView()
|
|
{
|
|
var parameters = new DialogParameters<CreateViewDialog>();
|
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
|
var dialog = await Dialog.ShowAsync<CreateViewDialog>("Create View", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (result is { Canceled: false } && result.Data is CreateTaskViewDto createDto)
|
|
{
|
|
await ViewService.CreateViewAsync(createDto);
|
|
}
|
|
}
|
|
} |