Initial commit: Tasks.Module
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
@page "/tasks/categories"
|
||||
@using Tasks.Module.Models
|
||||
@using Tasks.Module.Services
|
||||
@using Tasks.Module.Components.Dialogs
|
||||
@inject ICategoryService CategoryService
|
||||
@inject IDialogService Dialog
|
||||
@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">
|
||||
<h1 class="text-3xl font-bold text-slate-800">Categories</h1>
|
||||
<p class="text-slate-500 mt-1">Manage your task categories</p>
|
||||
</div>
|
||||
|
||||
@* Toolbar *@
|
||||
<div class="bg-white rounded-2xl shadow-md p-4 mb-4">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<MudTextField T="string"
|
||||
@bind-Value:get="searchText"
|
||||
@bind-Value:set="OnSearchChanged"
|
||||
Placeholder="Search categories..."
|
||||
DisableUnderLine="true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Style="max-width: 300px;" />
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddRootCategory">
|
||||
Add Category
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Categories Tree *@
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="flex justify-center py-12">
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (filteredCategories.Count == 0 && string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Large" Color="Color.Default" Class="mb-4" />
|
||||
<h3 class="text-lg font-semibold text-slate-700">No categories yet</h3>
|
||||
<p class="text-slate-500 mt-2">Click "Add Category" to create your first category</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg">
|
||||
@foreach (var category in filteredCategories)
|
||||
{
|
||||
<CategoryTreeItem Category="category"
|
||||
OnAddChild="AddChildCategory"
|
||||
OnDelete="DeleteCategory"
|
||||
OnUpdate="UpdateCategory"
|
||||
SearchText="searchText" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<CategoryDto> categories = new();
|
||||
private List<CategoryDto> filteredCategories = new();
|
||||
private bool isLoading = true;
|
||||
private string searchText = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
categories = await CategoryService.GetHierarchyAsync();
|
||||
ApplyFilter();
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
filteredCategories = categories;
|
||||
}
|
||||
else
|
||||
{
|
||||
filteredCategories = FilterTree(categories, searchText.ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
private List<CategoryDto> FilterTree(List<CategoryDto> cats, string search)
|
||||
{
|
||||
var result = new List<CategoryDto>();
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
var filteredChildren = FilterTree(cat.Children, search);
|
||||
if (cat.Name.ToLower().Contains(search) || filteredChildren.Count > 0)
|
||||
{
|
||||
var copy = new CategoryDto
|
||||
{
|
||||
Id = cat.Id,
|
||||
Name = cat.Name,
|
||||
Description = cat.Description,
|
||||
ParentId = cat.ParentId,
|
||||
CreatedAt = cat.CreatedAt,
|
||||
UpdatedAt = cat.UpdatedAt,
|
||||
Children = filteredChildren
|
||||
};
|
||||
result.Add(copy);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnSearchChanged(string value)
|
||||
{
|
||||
searchText = value;
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private async Task AddRootCategory()
|
||||
{
|
||||
await AddCategory(null);
|
||||
}
|
||||
|
||||
private async Task AddCategory(string? parentId)
|
||||
{
|
||||
var dto = new CreateCategoryDto { ParentId = parentId };
|
||||
|
||||
var parameters = new DialogParameters<CreateCategoryDialog>
|
||||
{
|
||||
{ x => x.Category, dto },
|
||||
{ x => x.IsChild, parentId != null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await Dialog.ShowAsync<CreateCategoryDialog>("Add Category", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
await CategoryService.CreateAsync(dto);
|
||||
await LoadCategories();
|
||||
Snackbar.Add("Category created", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddChildCategory(CategoryDto parent)
|
||||
{
|
||||
await AddCategory(parent.Id);
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(CategoryDto category)
|
||||
{
|
||||
var childCount = category.Children.Count;
|
||||
var (success, hasChildren, _, _) = await CategoryService.DeleteAsync(category.Id, false);
|
||||
|
||||
if (!success && hasChildren)
|
||||
{
|
||||
var confirmed = await Dialog.ShowMessageBox(
|
||||
"Delete Category",
|
||||
$"\"{category.Name}\" has {childCount} child category(ries). Delete all?",
|
||||
yesText: "Delete All", cancelText: "Cancel");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
var result = await CategoryService.DeleteAsync(category.Id, true);
|
||||
if (result.success)
|
||||
{
|
||||
Snackbar.Add("Category deleted", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (success)
|
||||
{
|
||||
Snackbar.Add("Category deleted", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCategory(CategoryDto category)
|
||||
{
|
||||
var dto = new UpdateCategoryDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Description = category.Description,
|
||||
ParentId = category.ParentId
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CreateCategoryDialog>
|
||||
{
|
||||
{ x => x.Category, dto },
|
||||
{ x => x.IsUpdate, true },
|
||||
{ x => x.IsChild, category.ParentId != null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await Dialog.ShowAsync<CreateCategoryDialog>("Update Category", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
await CategoryService.UpdateAsync(dto);
|
||||
Snackbar.Add("Category updated", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
@using Tasks.Module.Models
|
||||
@using Tasks.Module.Services
|
||||
|
||||
<MudExpansionPanel @bind-IsExpanded="_expanded" Class="category-item">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center justify-space-between" style="width: 100%;">
|
||||
<div class="d-flex align-center">
|
||||
@if (Category.Children.Count > 0)
|
||||
{
|
||||
<span class="expand-chevron" style="cursor: pointer; padding: 4px;"
|
||||
@onclick:stopPropagation="true"
|
||||
@onclick="@(() => { _expanded = !_expanded; })">
|
||||
<MudIcon Icon="@(_expanded ? Icons.Material.Filled.KeyboardArrowDown : Icons.Material.Filled.KeyboardArrowRight)"
|
||||
Size="Size.Small"
|
||||
Class="mr-1"
|
||||
Style="width: 20px; display: block;" />
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="width: 32px;"></div>
|
||||
}
|
||||
<MudIcon Icon="@Icons.Material.Filled.Folder" Color="Color.Default" Class="mr-2" />
|
||||
<span class="task-title">@HighlightText(Category.Name)</span>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-1">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mr-2">
|
||||
@Category.Children.Count children
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Size="Size.Small"
|
||||
Title="Add sub-category"
|
||||
OnClick="() => OnAddChild.InvokeAsync(Category)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Title="Edit"
|
||||
OnClick="() => OnUpdate.InvokeAsync(Category)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
Title="Delete"
|
||||
OnClick="() => OnDelete.InvokeAsync(Category)" />
|
||||
</div>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<div class="pl-4">
|
||||
@foreach (var child in Category.Children)
|
||||
{
|
||||
<CategoryTreeItem Category="child"
|
||||
OnAddChild="OnAddChild"
|
||||
OnDelete="OnDelete"
|
||||
OnUpdate="OnUpdate"
|
||||
SearchText="SearchText" />
|
||||
}
|
||||
</div>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public CategoryDto Category { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CategoryDto> OnAddChild { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CategoryDto> OnDelete { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CategoryDto> OnUpdate { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string SearchText { get; set; } = string.Empty;
|
||||
|
||||
private bool _expanded = false;
|
||||
|
||||
private RenderFragment HighlightText(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchText))
|
||||
{
|
||||
return @<span>@text</span>;
|
||||
}
|
||||
|
||||
var parts = text.Split(new[] { SearchText }, StringSplitOptions.None);
|
||||
|
||||
return@<text>
|
||||
@{
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
@parts[i]
|
||||
if (i < parts.Length - 1)
|
||||
{
|
||||
<mark class="search-highlight">@SearchText</mark>
|
||||
}
|
||||
}
|
||||
}
|
||||
</text>;
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--mud-palette-lines-default);
|
||||
}
|
||||
.category-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.search-highlight {
|
||||
background-color: yellow;
|
||||
padding: 0 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,208 @@
|
||||
@page "/tasks/tags"
|
||||
@using Tasks.Module.Models
|
||||
@using Tasks.Module.Services
|
||||
@using Tasks.Module.Components.Dialogs
|
||||
@inject ITagService TagService
|
||||
@inject IDialogService Dialog
|
||||
@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">
|
||||
<h1 class="text-3xl font-bold text-slate-800">Tags</h1>
|
||||
<p class="text-slate-500 mt-1">Manage your task tags</p>
|
||||
</div>
|
||||
|
||||
@* Toolbar *@
|
||||
<div class="bg-white rounded-2xl shadow-md p-4 mb-4">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<MudTextField T="string"
|
||||
@bind-Value:get="searchText"
|
||||
@bind-Value:set="OnSearchChanged"
|
||||
Placeholder="Search tags..."
|
||||
DisableUnderLine="true"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Style="max-width: 300px;" />
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddTag">
|
||||
Add Tag
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Tags Grid *@
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="flex justify-center py-12">
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (filteredTags.Count == 0 && string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalOffer" Size="Size.Large" Color="Color.Default" Class="mb-4" />
|
||||
<h3 class="text-lg font-semibold text-slate-700">No tags yet</h3>
|
||||
<p class="text-slate-500 mt-2">Click "Add Tag" to create your first tag</p>
|
||||
</div>
|
||||
}
|
||||
else if (filteredTags.Count == 0)
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.SearchOff" Size="Size.Large" Color="Color.Default" Class="mb-4" />
|
||||
<h3 class="text-lg font-semibold text-slate-700">No tags found</h3>
|
||||
<p class="text-slate-500 mt-2">Try a different search term</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
@foreach (var tag in filteredTags)
|
||||
{
|
||||
<MudChip T="string"
|
||||
Color="Color.Secondary"
|
||||
Size="Size.Medium"
|
||||
OnClick="() => EditTag(tag)"
|
||||
OnClose="() => DeleteTag(tag)">
|
||||
@(string.IsNullOrEmpty(tag.Color) ? "" : "● ")
|
||||
@tag.Name
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<TagDto> tags = new();
|
||||
private List<TagDto> filteredTags = new();
|
||||
private bool isLoading = true;
|
||||
private string searchText = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadTags();
|
||||
}
|
||||
|
||||
private async Task LoadTags()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
tags = await TagService.GetAllAsync();
|
||||
ApplyFilter();
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
filteredTags = tags;
|
||||
}
|
||||
else
|
||||
{
|
||||
filteredTags = tags.Where(t => t.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchChanged(string value)
|
||||
{
|
||||
searchText = value;
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private async Task AddTag()
|
||||
{
|
||||
var dto = new CreateTagDto();
|
||||
|
||||
var parameters = new DialogParameters<CreateTagDialog>
|
||||
{
|
||||
{ x => x.Tag, dto }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await Dialog.ShowAsync<CreateTagDialog>("Add Tag", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
await TagService.CreateAsync(dto);
|
||||
Snackbar.Add("Tag created", Severity.Success);
|
||||
await LoadTags();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditTag(TagDto tag)
|
||||
{
|
||||
var dto = new UpdateTagDto
|
||||
{
|
||||
Id = tag.Id,
|
||||
Name = tag.Name,
|
||||
Color = tag.Color
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CreateTagDialog>
|
||||
{
|
||||
{ x => x.Tag, dto },
|
||||
{ x => x.IsUpdate, true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await Dialog.ShowAsync<CreateTagDialog>("Edit Tag", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
try
|
||||
{
|
||||
await TagService.UpdateAsync(dto);
|
||||
Snackbar.Add("Tag updated", Severity.Success);
|
||||
await LoadTags();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteTag(TagDto tag)
|
||||
{
|
||||
var confirmed = await Dialog.ShowMessageBox(
|
||||
"Delete Tag",
|
||||
$"Delete \"{tag.Name}\"? It will be removed from all tasks.",
|
||||
yesText: "Delete", cancelText: "Cancel");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await TagService.DeleteAsync(tag.Id);
|
||||
Snackbar.Add("Tag deleted", Severity.Success);
|
||||
await LoadTags();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
@page "/tasks/taskssheet/{tasksSheetId}"
|
||||
@using Tasks.Module.Models
|
||||
@using Tasks.Module.Services
|
||||
@inject IFolderService FolderService
|
||||
@inject ITodoService TodoService
|
||||
@inject IDialogService Dialog
|
||||
|
||||
<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">@_tasksSheetName</h1>
|
||||
<p class="text-slate-500 mt-1">Manage tasks in this sheet</p>
|
||||
</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... (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>
|
||||
|
||||
@* Tasks List *@
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="flex justify-center py-12">
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (!_tasksSheetFound)
|
||||
{
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<h3 class="text-lg font-semibold text-slate-700">Tasks sheet not found</h3>
|
||||
<MudButton Href="/tasks" Color="Color.Primary" Class="mt-4">Go to Tasks</MudButton>
|
||||
</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 tasksSheetId { get; set; } = string.Empty;
|
||||
|
||||
private string _tasksSheetName = string.Empty;
|
||||
private bool _loading = true;
|
||||
private bool _tasksSheetFound = false;
|
||||
private CreateTodoItemDto editTask = new();
|
||||
private string currentTaskId = string.Empty;
|
||||
private string quickAddTitle = string.Empty;
|
||||
private HashSet<string> collapsedTasks = new();
|
||||
private List<TodoItemDto> _tasks = new();
|
||||
private string _lastTasksSheetId = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (tasksSheetId != _lastTasksSheetId)
|
||||
{
|
||||
_lastTasksSheetId = tasksSheetId;
|
||||
collapsedTasks.Clear();
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadTasksSheet()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var tasksSheet = await FolderService.GetTasksSheetByIdAsync(tasksSheetId);
|
||||
if (tasksSheet != null)
|
||||
{
|
||||
_tasksSheetName = tasksSheet.Title;
|
||||
_tasks = tasksSheet.Tasks;
|
||||
_tasksSheetFound = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tasksSheetFound = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
ParentId = null
|
||||
};
|
||||
|
||||
await FolderService.AddTaskToTasksSheetAsync(tasksSheetId, newTask);
|
||||
quickAddTitle = string.Empty;
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
|
||||
private void ToggleCollapse(string taskId)
|
||||
{
|
||||
if (collapsedTasks.Contains(taskId))
|
||||
collapsedTasks.Remove(taskId);
|
||||
else
|
||||
collapsedTasks.Add(taskId);
|
||||
}
|
||||
|
||||
private async Task ToggleTask(string id)
|
||||
{
|
||||
await FolderService.ToggleTaskInTasksSheetAsync(id);
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
|
||||
private async Task DeleteTask(string id)
|
||||
{
|
||||
await FolderService.DeleteTaskInTasksSheetAsync(id);
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
|
||||
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 FolderService.AddTaskToTasksSheetAsync(tasksSheetId, editTask);
|
||||
await LoadTasksSheet();
|
||||
}
|
||||
}
|
||||
|
||||
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 LoadTasksSheet();
|
||||
}
|
||||
}
|
||||
|
||||
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 LoadTasksSheet();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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