Initial commit: Tasks.Module

This commit is contained in:
Saidamin Kattan
2026-08-07 02:17:32 +03:00
commit 46aa82d0c7
97 changed files with 7443 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
@page "/tasks/view/{viewId}"
@using Tasks.Module.Models
@using Tasks.Module.Services
@inject IViewService ViewService
@inject ITodoService TodoService
@inject IDialogService Dialog
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<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">
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="/tasks" Color="Color.Default" Class="mb-4">
Back to Tasks
</MudButton>
<h1 class="text-3xl font-bold text-slate-800">@_viewName</h1>
<p class="text-slate-500 mt-1">Tasks matching this view's criteria</p>
</div>
@* View Criteria Summary *@
@if (_view != null && HasCriteria(_view))
{
<div class="bg-indigo-50 rounded-2xl p-4 mb-6 border border-indigo-100">
<div class="d-flex align-center mb-2">
<MudIcon Icon="@Icons.Material.Filled.FilterList" Color="Color.Primary" Class="mr-2" />
<MudText Typo="Typo.subtitle2" Color="Color.Primary">View Criteria</MudText>
</div>
<div class="d-flex flex-wrap gap-2">
@if (_view.Priority.HasValue)
{
<MudChip Size="Size.Small" T="string" Class="@GetPriorityChipClass(_view.Priority.Value)">
Priority: @GetPriorityLabel(_view.Priority.Value)
</MudChip>
}
@if (_view.IsCompleted.HasValue)
{
<MudChip Size="Size.Small" T="string" Color="@(_view.IsCompleted.Value ? Color.Success : Color.Warning)">
Status: @(_view.IsCompleted.Value ? "Completed" : "Pending")
</MudChip>
}
@if (!string.IsNullOrWhiteSpace(_view.ContainsText))
{
<MudChip Size="Size.Small" T="string" Color="Color.Info">
Contains: "@_view.ContainsText"
</MudChip>
}
@if (_view.DueDateFrom.HasValue)
{
<MudChip Size="Size.Small" T="string" Color="Color.Default">
Due from: @_view.DueDateFrom.Value.ToString("MMM dd")
</MudChip>
}
@if (_view.DueDateTo.HasValue)
{
<MudChip Size="Size.Small" T="string" Color="Color.Default">
Due to: @_view.DueDateTo.Value.ToString("MMM dd")
</MudChip>
}
</div>
</div>
}
@* Quick Add Input - Creates task with view criteria *@
<div class="bg-white rounded-2xl shadow-lg p-4 mb-6">
@if (!string.IsNullOrWhiteSpace(_view?.ContainsText))
{
<MudAlert Severity="Severity.Info" Class="mb-3" Icon="@Icons.Material.Filled.Info">
New tasks will be checked for text: "@_view.ContainsText"
</MudAlert>
}
<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 task with view criteria... (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">
<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.SearchOff" Size="Size.Large" Color="Color.Default" />
</div>
<h3 class="text-lg font-semibold text-slate-700">No tasks match this view</h3>
<p class="text-slate-500 mt-2">Add a task using the form above to create one with this view's criteria</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 {
[Parameter] public string viewId { get; set; } = string.Empty;
private TaskViewDto? _view;
private string _viewName = string.Empty;
private List<TodoItemDto> tasks = new();
private bool isLoading = true;
private CreateTodoItemDto editTask = new();
private string currentTaskId = string.Empty;
private string quickAddTitle = string.Empty;
private HashSet<string> collapsedTasks = new();
protected override async Task OnInitializedAsync()
{
await LoadView();
}
private async Task LoadView()
{
isLoading = true;
try
{
_view = await ViewService.GetViewByIdAsync(viewId);
if (_view != null)
{
_viewName = _view.Name;
tasks = await ViewService.GetTasksByViewAsync(viewId);
}
}
finally
{
isLoading = false;
}
}
private bool HasCriteria(TaskViewDto view)
{
return view.Priority.HasValue ||
view.IsCompleted.HasValue ||
!string.IsNullOrWhiteSpace(view.ContainsText) ||
view.DueDateFrom.HasValue ||
view.DueDateTo.HasValue;
}
private string GetPriorityLabel(int priority)
{
return priority switch
{
2 => "High",
1 => "Medium",
_ => "Low"
};
}
private string GetPriorityChipClass(int priority)
{
return priority switch
{
2 => "bg-red-100 text-red-700",
1 => "bg-yellow-100 text-yellow-700",
_ => "bg-green-100 text-green-700"
};
}
private bool TaskMatchesTextCriteria(string title)
{
if (string.IsNullOrWhiteSpace(_view?.ContainsText))
return true;
return title.Contains(_view.ContainsText, StringComparison.OrdinalIgnoreCase);
}
private async Task HandleQuickAdd(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(quickAddTitle))
{
await QuickAddTask();
}
}
private async Task QuickAddTask()
{
if (string.IsNullOrWhiteSpace(quickAddTitle))
return;
if (!TaskMatchesTextCriteria(quickAddTitle))
{
var confirmed = await Dialog.ShowMessageBox(
"Warning",
$"This task doesn't contain '{_view?.ContainsText}'. Adding it will not show in this view. Continue?",
yesText: "Yes, add it", cancelText: "No");
if (confirmed != true)
return;
}
var newTask = new CreateTodoItemDto
{
Title = quickAddTitle.Trim(),
Priority = 0
};
await ViewService.CreateTodoWithViewCriteriaAsync(viewId, newTask);
quickAddTitle = string.Empty;
await LoadView();
}
private void ToggleCollapse(string taskId)
{
if (collapsedTasks.Contains(taskId))
collapsedTasks.Remove(taskId);
else
collapsedTasks.Add(taskId);
}
private async Task ToggleTask(string id)
{
await TodoService.ToggleCompleteAsync(id);
await LoadView();
}
private async Task DeleteTask(string id)
{
await TodoService.DeleteTodoAsync(id);
await LoadView();
}
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 })
{
if (!TaskMatchesTextCriteria(editTask.Title))
{
var confirmed = await Dialog.ShowMessageBox(
"Warning",
$"This task doesn't contain '{_view?.ContainsText}'. Adding it will not show in this view. Continue?",
yesText: "Yes, add it", cancelText: "No");
if (confirmed != true)
return;
}
await ViewService.CreateTodoWithViewCriteriaAsync(viewId, editTask);
await LoadView();
}
}
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 })
{
if (!TaskMatchesTextCriteria(editTask.Title))
{
var confirmed = await Dialog.ShowMessageBox(
"Warning",
$"This task doesn't contain '{_view?.ContainsText}'. Adding it will not show in this view. Continue?",
yesText: "Yes, add it", cancelText: "No");
if (confirmed != true)
return;
}
await ViewService.CreateTodoWithViewCriteriaAsync(viewId, editTask);
await LoadView();
}
}
private async Task OpenUpdateDialog(TodoItemDto task)
{
var confirmed = await Dialog.ShowMessageBox(
"Edit Task Warning",
"Editing this task may remove it from this view if the changes don't match the view criteria. Continue?",
yesText: "Continue", cancelText: "Cancel");
if (confirmed != true)
return;
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 LoadView();
}
}
}