262 lines
7.9 KiB
Plaintext
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;
|
|
}
|
|
}
|