239 lines
7.8 KiB
Plaintext
239 lines
7.8 KiB
Plaintext
@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);
|
|
}
|
|
}
|
|
}
|
|
} |