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

262 lines
7.9 KiB
Plaintext

@using MudBlazor
@using Tasks.Module.Models
@using Microsoft.AspNetCore.Components.Web
<div class="task-item @(Task.IsCompleted ? "completed" : "")" style="@GetDepthStyle()" @oncontextmenu:preventDefault="true" @oncontextmenu="(e) => OnContextMenu(e)">
<div class="flex items-center gap-2 py-1">
@* Expand/Collapse Button *@
@if (Task.Children.Any())
{
<MudIconButton Icon="@(IsCollapsed ? Icons.Material.Filled.ChevronRight : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
OnClick="HandleToggleCollapse"
Class="pa-0" />
}
else
{
<div style="width: 24px;"></div>
}
@* Checkbox *@
<MudCheckBox T="bool"
Value="@Task.IsCompleted"
ValueChanged="HandleToggle"
Color="Color.Success"
Dense="true"
Class="ma-0 pa-0" />
@* Task Title *@
<MudText Typo="Typo.body2"
Class="@(Task.IsCompleted ? "strikethrough text-slate-400 flex-grow-1" : "flex-grow-1")">
@Task.Title
</MudText>
@* Priority Badge *@
<MudChip Size="Size.Small"
T="string"
Label="true"
Class="@GetPriorityClass()"
Style="height: 20px; font-size: 10px; padding: 0 6px;">
@GetPriorityLabel()
</MudChip>
@* Due Date *@
@if (Task.DueDate.HasValue)
{
<MudText Typo="Typo.caption" Color="@GetDueDateColor()" Class="@(Task.IsCompleted ? "text-slate-400" : "")">
@Task.DueDate.Value.ToString("MMM dd")
</MudText>
}
@* Sub-task count *@
@if (Task.Children.Any())
{
<MudText Typo="Typo.caption" Color="Color.Default" Class="text-slate-500">
@GetTotalChildrenCount() sub
</MudText>
}
@* Actions *@
<div class="task-actions">
<MudIconButton Icon="@Icons.Material.Filled.Add"
Size="Size.Small"
OnClick="HandleAddChild"
Title="Add sub-task"
Class="pa-0" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="HandleDelete"
Title="Delete"
Class="pa-0" />
</div>
</div>
</div>
@* Context Menu *@
@if (_contextMenuOpen)
{
<div Style="@($"position: fixed; left: {_contextMenuX}px; top: {_contextMenuY}px; z-index: 2000;")">
<MudPaper Elevation="4" Class="pa-2" Style="min-width: 160px;">
<MudStack Spacing="0">
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.Add" OnClick="@(() => HandleContextAction("addChild"))">
Add Sub-task
</MudButton>
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.Edit" OnClick="@(() => HandleContextAction("edit"))">
Edit
</MudButton>
<MudDivider Class="my-1" />
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(() => HandleContextAction("delete"))">
Delete
</MudButton>
</MudStack>
</MudPaper>
</div>
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1999;" @onclick="CloseContextMenu" @oncontextmenu:preventDefault="true"></div>
}
@* Recursive Children *@
@if (Task.Children.Any() && !IsCollapsed)
{
<div style="padding-left: 20px;">
@foreach (var child in Task.Children)
{
<TaskCard Task="child"
OnToggle="OnToggle"
OnDelete="OnDelete"
OnUpdate="OnUpdate"
OnAddChild="OnAddChild"
CollapsedTasks="CollapsedTasks"
ToggleCollapse="ToggleCollapse"
Depth="Depth + 1" />
}
</div>
}
<style>
.task-item {
padding: 2px 8px;
margin: 2px 0;
border-radius: 4px;
background-color: var(--mud-palette-background-grey, #f5f5f5);
cursor: context-menu;
}
.task-item:hover {
background-color: var(--mud-palette-action-default-hover, #e0e0e0);
}
.task-item.completed {
background-color: transparent;
opacity: 0.7;
}
.task-actions {
opacity: 0;
transition: opacity 0.2s;
}
.task-item:hover .task-actions {
opacity: 1;
}
</style>
@code {
[Parameter] public TodoItemDto Task { get; set; } = new();
[Parameter] public EventCallback<string> OnToggle { get; set; }
[Parameter] public EventCallback<string> OnDelete { get; set; }
[Parameter] public EventCallback<TodoItemDto> OnUpdate { get; set; }
[Parameter] public EventCallback<TodoItemDto> OnAddChild { get; set; }
[Parameter] public HashSet<string> CollapsedTasks { get; set; } = new();
[Parameter] public EventCallback<string> ToggleCollapse { get; set; }
[Parameter] public int Depth { get; set; } = 0;
private bool _contextMenuOpen = false;
private double _contextMenuX = 0;
private double _contextMenuY = 0;
private bool IsCollapsed => CollapsedTasks.Contains(Task.Id);
private int GetTotalChildrenCount() => CountChildren(Task);
private int CountChildren(TodoItemDto task)
{
int count = task.Children.Count;
foreach (var child in task.Children)
count += CountChildren(child);
return count;
}
private void OnContextMenu(MouseEventArgs args)
{
_contextMenuX = args.ClientX;
_contextMenuY = args.ClientY;
_contextMenuOpen = true;
}
private void CloseContextMenu()
{
_contextMenuOpen = false;
}
private async Task HandleContextAction(string action)
{
CloseContextMenu();
switch (action)
{
case "addChild":
await HandleAddChild();
break;
case "edit":
await HandleEdit();
break;
case "delete":
await HandleDelete();
break;
}
}
private async Task HandleEdit()
{
if (OnUpdate.HasDelegate)
await OnUpdate.InvokeAsync(Task);
}
private async Task HandleToggle(bool isCompleted)
{
if (OnToggle.HasDelegate)
await OnToggle.InvokeAsync(Task.Id);
}
private async Task HandleToggleCollapse()
{
if (ToggleCollapse.HasDelegate)
await ToggleCollapse.InvokeAsync(Task.Id);
}
private async Task HandleDelete()
{
if (OnDelete.HasDelegate)
await OnDelete.InvokeAsync(Task.Id);
}
private async Task HandleAddChild()
{
if (OnAddChild.HasDelegate)
await OnAddChild.InvokeAsync(Task);
}
private string GetDepthStyle() => "";
private string GetPriorityClass()
{
return Task.Priority switch
{
2 => "bg-red-100 text-red-700",
1 => "bg-yellow-100 text-yellow-700",
_ => "bg-green-100 text-green-700"
};
}
private string GetPriorityLabel()
{
return Task.Priority switch
{
2 => "H",
1 => "M",
_ => "L"
};
}
private Color GetDueDateColor()
{
if (Task.IsCompleted)
return Color.Default;
if (IsDueOverdue())
return Color.Error;
return Color.Default;
}
private bool IsDueOverdue()
{
return Task.DueDate.HasValue && Task.DueDate.Value < DateTime.Now && !Task.IsCompleted;
}
}