Files
2026-08-07 02:17:32 +03:00

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);
}
}
}
}