Initial commit: Tasks.Module
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user