User
Deploy localized dashboard fixes
3dd5c9a
Raw History Blame Contribute Delete
53.2 kB
@page "/tasks"
@page "/tasks/all"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject ApiClientService ApiClient
@inject IJSRuntime JS
@inject AuthenticationStateProvider AuthStateProvider
@inject MenuAuthorizationService MenuAuthorization
@inject NavigationManager Navigation
<PageTitle>@AppLocalization.PageTitle("tasks", "Tasks")</PageTitle>
<SuccessAlert Message="@successMessage" />
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("tasks", "Tasks")</h2>
<p class="text-slate-500 mt-1">@(isAdmin ? AppLocalization.Text("task.manageAll", "View and manage all tasks across projects.") : AppLocalization.Text("task.yourAssigned", "Your assigned tasks."))</p>
</div>
<div class="flex items-center space-x-4">
@if (canCreateTask)
{
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
@AppLocalization.Text("task.newTask", "New Task")
</button>
}
</div>
</div>
@if (isLoading)
{
<LoadingSpinner Message="@AppLocalization.Text("common.loadingTasks", "Loading tasks...")" />
}
else
{
<!-- Search and Filters Panel -->
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<div class="flex flex-wrap items-center gap-4">
<div class="relative flex-1 min-w-[200px]">
<input @bind="searchInput" @bind:event="oninput" type="text" placeholder="@AppLocalization.Text("common.searchTasksByTitle", "Search tasks by title...")" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
</div>
<div class="w-44 shrink-0">
<select @bind="filterProjectIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
<option value="0">@AppLocalization.Text("common.allProjects", "All Projects")</option>
@foreach (var p in projects)
{
<option value="@p.Id">@Content.GetProjectName(p)</option>
}
</select>
</div>
<div class="w-40 shrink-0">
<select @bind="filterStatusIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
<option value="">@AppLocalization.Text("common.allStatuses", "All Statuses")</option>
<option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
<option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
<option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
</select>
</div>
<div class="w-40 shrink-0">
<select @bind="filterPriorityIdInput" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
<option value="">@AppLocalization.Text("common.allPriorities", "All Priorities")</option>
<option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option>
<option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option>
<option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option>
</select>
</div>
<button @onclick="ApplySearch" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm shrink-0">
<i data-lucide="search" class="h-4 w-4 mr-2"></i>
@AppLocalization.Text("action.search", "Search")
</button>
<button @onclick="ResetSearch" title="@AppLocalization.Text("action.reset", "Reset")" aria-label="@AppLocalization.Text("action.reset", "Reset")" class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white p-2.5 text-slate-700 hover:bg-slate-50 transition-colors shadow-sm shrink-0">
<i data-lucide="rotate-ccw" class="h-4 w-4 text-slate-500"></i>
</button>
</div>
</div>
<!-- Tasks Table (List View) -->
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden mt-4">
<table class="w-full table-fixed">
<colgroup>
<col class="w-[12%]" />
<col class="w-[22%]" />
<col class="w-[11%]" />
<col class="w-[11%]" />
<col class="w-[14%]" />
<col class="w-[16%]" />
<col class="w-[14%]" />
<col class="w-[10%]" />
</colgroup>
<thead>
<tr class="border-b border-slate-100 bg-slate-50/60">
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("field.taskCode", "Task Code")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.task", "Task")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.priority", "Priority")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.dueDate", "Due Date")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.project", "Project")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.assignee", "Assignee")</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Actions")</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@if (PagedTasks.Any())
{
@foreach (var entry in PagedTasks.Select((task, index) => (task, index)))
{
var task = entry.task;
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="px-6 py-4 text-left">
<span class="font-mono text-[11px] font-semibold text-indigo-700">@GetTaskCode(task.Id)</span>
</td>
<td class="px-6 py-4 text-left">
<a href="/tasks/@task.Id/details" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">@Content.GetTaskTitle(task)</a>
@if (!string.IsNullOrEmpty(Content.GetTaskDescription(task)))
{
<p class="mt-0.5 text-xs text-slate-400 truncate max-w-full">@Content.GetTaskDescription(task)</p>
}
</td>
<td class="px-6 py-4 text-left">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(task.StatusId)">
@GetStatusLabel(task.StatusId)
</span>
</td>
<td class="px-6 py-4 text-left">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(task.PriorityId)">
@GetPriorityLabel(task.PriorityId)
</span>
</td>
<td class="px-6 py-4 text-sm text-slate-600 text-left whitespace-nowrap">@DisplayFormats.Date(task.DueDate)</td>
<td class="px-6 py-4 text-sm text-slate-500 text-left break-words">
@{ var proj = projects.FirstOrDefault(p => p.Id == task.ProjectId); }
@(proj != null ? Content.GetProjectName(proj) : $"#{task.ProjectId}")
</td>
<td class="px-6 py-4 text-sm text-slate-500 font-medium text-left">
@{ var user = allUsers.FirstOrDefault(u => u.Id == task.AssignedTo); }
@if (user != null)
{
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold">
@(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
</span>
<span class="text-xs text-slate-700 font-semibold">@Content.GetUserFullName(user)</span>
</div>
}
else
{
<span class="text-xs text-slate-400 italic">@AppLocalization.Text("common.unassigned", "Unassigned")</span>
}
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
@if (canEditTask)
{
<button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="@AppLocalization.Text("action.edit", "Edit")">
<i data-lucide="pencil" class="h-4 w-4"></i>
</button>
}
@if (canDeleteTask)
{
<button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="@AppLocalization.Text("action.delete", "Delete")">
<i data-lucide="trash-2" class="h-4 w-4"></i>
</button>
}
</div>
</td>
</tr>
}
}
else
{
<EmptyState ColSpan="8" Icon="check-square" Title="@AppLocalization.Text("task.noTasksYet", "No tasks yet")" Subtitle="@AppLocalization.Text("task.createFirstTask", "Create your first task to get started.")" />
}
</tbody>
</table>
<!-- Pagination -->
<Pagination CurrentPage="currentPage"
TotalPages="TotalPages"
PageSize="pageSize"
TotalCount="TotalCount"
OnPageChanged="HandlePageChanged"
OnPageSizeChanged="HandlePageSizeChanged" />
</div>
}
</div>
@* ===== Create / Edit Modal ===== *@
@if (showModal)
{
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div class="bg-white rounded-2xl shadow-lg w-full max-w-lg mx-4 overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<h3 class="text-lg font-semibold text-slate-900">@(isEditing ? AppLocalization.Text("task.editTask", "Edit Task") : AppLocalization.Text("task.newTask", "New Task"))</h3>
<button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
<i data-lucide="x" class="h-5 w-5"></i>
</button>
</div>
<div class="p-6 space-y-4">
<BilingualEditorTabs BurmeseComplete="@(!string.IsNullOrWhiteSpace(formTitleMy))"
EnglishComplete="@(!string.IsNullOrWhiteSpace(formTitle))"
BurmeseAiGenerated="@burmeseGenerated"
EnglishAiGenerated="@englishGenerated"
BurmeseNeedsReview="@burmeseNeedsReview"
EnglishNeedsReview="@englishNeedsReview">
<BurmeseContent>
<label class="block text-sm font-medium text-slate-700 mb-1.5">ခေါင်းစဉ် <span class="text-red-500">*</span></label>
<input @bind="formTitleMy" type="text" placeholder="ခေါင်းစဉ်" class="w-full rounded-lg border @(isTitleError && string.IsNullOrWhiteSpace(formTitle) ? "border-red-500" : "border-slate-200") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
<BilingualFieldWarning Text="@formTitleMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveTaskTitleToEnglish" />
<BilingualTranslationAction SourceText="@formTitleMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formTitle))" Generated="GenerateTaskEnglishTitle" />
<label class="mt-4 block text-sm font-medium text-slate-700 mb-1.5">ဖော်ပြချက်</label>
<textarea @bind="formDescriptionMy" rows="3" placeholder="ဖော်ပြချက်" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea>
<BilingualFieldWarning Text="@formDescriptionMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveTaskDescriptionToEnglish" />
<BilingualTranslationAction SourceText="@formDescriptionMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescription))" Generated="GenerateTaskEnglishDescription" />
</BurmeseContent>
<EnglishContent>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("field.title", "Title") <span class="text-red-500">*</span></label>
<input @bind="formTitle" type="text" placeholder="@AppLocalization.Text("task.taskTitle", "Task title")" class="w-full rounded-lg border @(isTitleError && string.IsNullOrWhiteSpace(formTitleMy) ? "border-red-500" : "border-slate-200") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
<BilingualFieldWarning Text="@formTitle" ExpectedBurmese="false" MoveToOtherLanguage="MoveTaskTitleToBurmese" />
<BilingualTranslationAction SourceText="@formTitle" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formTitleMy))" Generated="GenerateTaskBurmeseTitle" />
<label class="mt-4 block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.description", "Description")</label>
<textarea @bind="formDescription" rows="3" placeholder="@AppLocalization.Text("task.taskDescription", "Task description")" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all"></textarea>
<BilingualFieldWarning Text="@formDescription" ExpectedBurmese="false" MoveToOtherLanguage="MoveTaskDescriptionToBurmese" />
<BilingualTranslationAction SourceText="@formDescription" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescriptionMy))" Generated="GenerateTaskBurmeseDescription" />
</EnglishContent>
</BilingualEditorTabs>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.project", "Project") <span class="text-red-500">*</span></label>
<select value="@formProjectId" @onchange="OnFormProjectChanged" class="w-full rounded-lg border @(isProjectError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:border-transparent transition-all">
<option value="0">@AppLocalization.Text("task.selectProject", "Select project...")</option>
@foreach (var p in projects)
{
<option value="@p.Id">@Content.GetProjectName(p)</option>
}
</select>
@if (isProjectError)
{
<p class="mt-1 text-xs text-red-500">@projectErrorMessage</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
<select @bind="formStatusId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
<option value="@AppTaskStatus.Todo">@AppLocalization.StatusLabel(AppTaskStatus.Todo)</option>
<option value="@AppTaskStatus.InProgress">@AppLocalization.StatusLabel(AppTaskStatus.InProgress)</option>
<option value="@AppTaskStatus.Done">@AppLocalization.StatusLabel(AppTaskStatus.Done)</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.priority", "Priority")</label>
<select @bind="formPriorityId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
<option value="@TaskPriority.Low">@AppLocalization.PriorityLabel(TaskPriority.Low)</option>
<option value="@TaskPriority.Medium">@AppLocalization.PriorityLabel(TaskPriority.Medium)</option>
<option value="@TaskPriority.High">@AppLocalization.PriorityLabel(TaskPriority.High)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.dueDate", "Due Date")</label>
<input @bind="formDueDate" type="date" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.assignee", "Assignee")</label>
<div class="relative">
<button type="button"
@onclick="ToggleAssigneeDropdown"
class="flex h-11 w-full items-center justify-between rounded-lg border border-slate-200 bg-white px-3 text-left text-sm text-slate-900 shadow-sm transition-colors hover:border-violet-300 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-900">
<span class="truncate @(formAssignedTo.HasValue ? "font-medium text-slate-900" : "text-slate-400")">@SelectedAssigneeLabel</span>
<i data-lucide="chevron-down" class="ml-3 h-4 w-4 shrink-0 text-slate-400"></i>
</button>
@if (isAssigneeDropdownOpen)
{
<div class="absolute bottom-full left-0 z-30 mb-2 w-full rounded-2xl border border-slate-200 bg-white p-3 shadow-lg">
<div class="flex items-center justify-end">
<button type="button"
@onclick="CloseAssigneeDropdown"
class="inline-flex h-7 w-7 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
aria-label="@AppLocalization.Text("common.close", "Close")">
<i data-lucide="x" class="h-4 w-4"></i>
</button>
</div>
<div class="relative">
<input @bind="assigneeSearchInput" @bind:event="oninput"
@onfocus="EnsureAssigneeDropdownOpen"
type="text"
placeholder="@AppLocalization.Text("common.searchProjectMembers", "Search project members...")"
class="w-full rounded-xl border border-slate-200 bg-white py-2 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent" />
<i data-lucide="search" class="absolute left-3 top-2.5 h-4 w-4 text-slate-400"></i>
</div>
@if (isLoadingProjectMembers)
{
<div class="mt-3 rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
@AppLocalization.Text("common.loadingProjectMembers", "Loading project members...")
</div>
}
else
{
<div class="mt-3 max-h-56 overflow-y-auto space-y-2 pr-1">
<button type="button"
@onclick="() => SetFormAssignee(null)"
class="flex w-full items-center gap-3 rounded-xl border px-3 py-3 text-left transition-colors @(formAssignedTo.HasValue ? "border-slate-200 bg-white hover:bg-slate-50" : "border-violet-600 bg-violet-50/40")">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-500 text-xs font-bold shrink-0">
<i data-lucide="user-minus" class="h-4 w-4"></i>
</span>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-slate-900">@AppLocalization.Text("common.unassigned", "Unassigned")</p>
</div>
</button>
@foreach (var user in FilteredProjectMembers)
{
var isSelected = formAssignedTo == user.Id;
<button type="button"
@onclick="() => SetFormAssignee(user.Id)"
class="flex w-full items-center gap-3 rounded-xl border px-3 py-3 text-left transition-colors @(isSelected ? "border-violet-600 bg-violet-50/40" : "border-slate-100 bg-white hover:border-slate-200 hover:bg-slate-50")">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0">
@(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-slate-900">@Content.GetUserFullName(user)</p>
<p class="truncate text-xs text-slate-500">@@@user.Username</p>
</div>
</button>
}
@if (!FilteredProjectMembers.Any())
{
<div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-center text-xs text-slate-500">
@AppLocalization.Text("common.noMembersMatchSearch", "No members match the current search.")
</div>
}
</div>
}
</div>
}
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="rounded-lg bg-rose-50 p-3 border border-rose-200">
<p class="text-xs text-rose-600 font-medium flex items-center gap-1.5">
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
@errorMessage
</p>
</div>
}
</div>
<div class="flex items-center justify-end space-x-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50">
<button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">@AppLocalization.Text("common.cancel", "Cancel")</button>
<button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
@(isSaving ? AppLocalization.Text("common.saving", "Saving...") : (isEditing ? AppLocalization.Text("action.update", "Update") : AppLocalization.Text("action.create", "Create")))
</button>
</div>
</div>
</div>
}
@* ===== Confirm Dialog ===== *@
<ConfirmDialog IsVisible="showConfirmDialog"
Title="@confirmTitle"
Message="@confirmMessage"
ConfirmText="@confirmButtonText"
Icon="@confirmIcon"
IconBgClass="@confirmIconBg"
IconTextClass="@confirmIconText"
ButtonClass="@confirmButtonClass"
OnConfirm="HandleConfirmAction"
OnCancel="CloseConfirmDialog" />
@code {
[Parameter]
[SupplyParameterFromQuery(Name = "taskId")]
public long? TaskIdParam { get; set; }
private bool isLoading = true;
private bool isAdmin = false;
private bool isManager = false;
private bool canCreateTask = false;
private bool canEditTask = false;
private bool canDeleteTask = false;
private bool canUpdateTask = false;
private long currentUserId = 0;
private bool showModal = false;
private bool isEditing = false;
private bool isSaving = false;
private bool isAssigneeDropdownOpen = false;
private string? errorMessage;
// Per-field validation
private bool isTitleError = false;
private string titleErrorMessage = "";
private bool isProjectError = false;
private string projectErrorMessage = "";
// Confirm dialog state
private bool showConfirmDialog = false;
private string confirmTitle = "";
private string confirmMessage = "";
private string confirmButtonText = "";
private string confirmIcon = "";
private string confirmIconBg = "";
private string confirmIconText = "";
private string confirmButtonClass = "";
private Func<Task>? confirmAction;
private List<TaskDto> taskList = new();
private List<ProjectDto> projects = new();
private List<UserDto> allUsers = new();
private List<UserDto> projectMembers = new();
private string? successMessage;
// Search and Filters state
private string searchInput = "";
private long filterProjectIdInput = 0;
private AppTaskStatus? filterStatusIdInput = null;
private TaskPriority? filterPriorityIdInput = null;
// Pagination state
private int currentPage = 1;
private int pageSize = 10;
private int totalCount = 0;
private int totalPages = 0;
private int TotalPages => totalPages;
private int TotalCount => totalCount;
private IEnumerable<TaskDto> PagedTasks => taskList;
// Form fields
private long editingId;
private string formTitle = "";
private string? formTitleMy;
private string? formDescription;
private string? formDescriptionMy;
private bool burmeseGenerated;
private bool englishGenerated;
private bool burmeseNeedsReview;
private bool englishNeedsReview;
private long formProjectId;
private AppTaskStatus formStatusId = AppTaskStatus.Todo;
private TaskPriority formPriorityId = TaskPriority.Medium;
private DateTime formDueDate = DateTime.Today.AddDays(7);
private long? formAssignedTo;
private string assigneeSearchInput = "";
private bool isLoadingProjectMembers;
private void MoveTaskTitleToEnglish() { formTitle = formTitleMy ?? string.Empty; }
private void MoveTaskTitleToBurmese() { formTitleMy = formTitle; }
private void MoveTaskDescriptionToEnglish() { formDescription = formDescriptionMy; }
private void MoveTaskDescriptionToBurmese() { formDescriptionMy = formDescription; }
private void MarkGenerated(bool english)
{
if (english) { englishGenerated = true; englishNeedsReview = true; }
else { burmeseGenerated = true; burmeseNeedsReview = true; }
}
private Task GenerateTaskEnglishTitle(TranslationResult result) { formTitle = result.TranslatedText ?? string.Empty; MarkGenerated(true); return Task.CompletedTask; }
private Task GenerateTaskBurmeseTitle(TranslationResult result) { formTitleMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; }
private Task GenerateTaskEnglishDescription(TranslationResult result) { formDescription = result.TranslatedText ?? string.Empty; MarkGenerated(true); return Task.CompletedTask; }
private Task GenerateTaskBurmeseDescription(TranslationResult result) { formDescriptionMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; }
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
isAdmin = user.IsInRole("Admin");
isManager = user.IsInRole("Manager");
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (idStr != null) long.TryParse(idStr, out currentUserId);
await LoadTaskAccessAsync(user);
var client = ApiClient.CreateClient();
try
{
var tasksTask = LoadTasksPageAsync(client);
var projectsTask = client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
var usersTask = client.GetFromJsonAsync<List<UserDto>>("User", Serialization.CaseInsensitive);
await System.Threading.Tasks.Task.WhenAll(tasksTask, projectsTask, usersTask);
taskList = tasksTask.Result?.Items ?? new();
totalCount = tasksTask.Result?.TotalCount ?? 0;
totalPages = tasksTask.Result?.TotalPages ?? 0;
currentPage = tasksTask.Result?.Page ?? currentPage;
pageSize = tasksTask.Result?.PageSize ?? pageSize;
projects = projectsTask.Result ?? new();
allUsers = usersTask.Result ?? new();
}
catch (Exception ex)
{
Console.WriteLine($"Load error: {ex.Message}");
}
finally
{
if (TaskIdParam.HasValue && TaskIdParam > 0)
{
await FocusTaskAsync(TaskIdParam.Value);
}
}
isLoading = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
private async Task<PagedResult<TaskDto>?> LoadTasksPageAsync(HttpClient? client = null)
{
client ??= ApiClient.CreateClient();
var response = await client.GetFromJsonAsync<PagedResult<TaskDto>>(BuildTasksUrl(), Serialization.CaseInsensitive);
if (response != null)
{
return response;
}
return null;
}
// Pagination handlers
private async Task HandlePageChanged(int page)
{
currentPage = page;
await LoadTasks();
}
private async Task HandlePageSizeChanged(int size)
{
pageSize = size;
currentPage = 1;
await LoadTasks();
}
private async Task ApplySearch()
{
currentPage = 1;
await LoadTasks();
}
private async Task ResetSearch()
{
searchInput = "";
filterProjectIdInput = 0;
filterStatusIdInput = null;
filterPriorityIdInput = null;
currentPage = 1;
await LoadTasks();
}
private async Task LoadTasks()
{
var client = ApiClient.CreateClient();
var result = await LoadTasksPageAsync(client);
taskList = result?.Items ?? new();
totalCount = result?.TotalCount ?? 0;
totalPages = result?.TotalPages ?? 0;
currentPage = result?.Page ?? currentPage;
pageSize = result?.PageSize ?? pageSize;
}
private string BuildTasksUrl()
{
var query = new List<string>
{
$"page={currentPage}",
$"limit={pageSize}"
};
if (!string.IsNullOrWhiteSpace(searchInput))
{
query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}");
}
if (filterProjectIdInput > 0)
{
query.Add($"projectId={filterProjectIdInput}");
}
if (filterStatusIdInput.HasValue)
{
query.Add($"statusId={(int)filterStatusIdInput.Value}");
}
if (filterPriorityIdInput.HasValue)
{
query.Add($"priorityId={(int)filterPriorityIdInput.Value}");
}
if (!(isAdmin || isManager) && currentUserId > 0)
{
query.Add("assignedOnly=true");
}
return $"Task?{string.Join("&", query)}";
}
private async Task FocusTaskAsync(long taskId)
{
try
{
var client = ApiClient.CreateClient();
var task = await client.GetFromJsonAsync<TaskDto>($"Task/{taskId}", Serialization.CaseInsensitive);
if (task == null)
{
return;
}
searchInput = task.Title;
currentPage = 1;
await LoadTasks();
}
catch (Exception ex)
{
Console.WriteLine($"Focus task error: {ex.Message}");
}
}
private async Task LoadTaskAccessAsync(System.Security.Claims.ClaimsPrincipal user)
{
canCreateTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Create");
canEditTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Update");
canDeleteTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Delete");
canUpdateTask = canEditTask;
}
private IEnumerable<UserDto> FilteredProjectMembers
{
get
{
var result = projectMembers.Where(u => u.IsActive);
if (!string.IsNullOrWhiteSpace(assigneeSearchInput))
{
var search = assigneeSearchInput.Trim();
result = result.Where(u =>
Content.GetUserFullName(u).Contains(search, StringComparison.OrdinalIgnoreCase) ||
$"{u.FirstName} {u.LastName} {u.FirstNameMy} {u.LastNameMy}".Contains(search, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrWhiteSpace(u.Username) && u.Username.Contains(search, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrWhiteSpace(u.Email) && u.Email.Contains(search, StringComparison.OrdinalIgnoreCase)));
}
return result
.OrderBy(u => u.FirstName)
.ThenBy(u => u.LastName)
.ToList();
}
}
private string SelectedAssigneeLabel
{
get
{
if (!formAssignedTo.HasValue)
{
return AppLocalization.Text("common.unassigned", "Unassigned");
}
var selectedUser = projectMembers.FirstOrDefault(u => u.Id == formAssignedTo.Value);
return selectedUser == null
? AppLocalization.Text("common.selected", "selected")
: Content.GetUserFullName(selectedUser);
}
}
private async Task OnFormProjectChanged(ChangeEventArgs e)
{
if (long.TryParse(e.Value?.ToString(), out var projectId))
{
formProjectId = projectId;
}
else
{
formProjectId = 0;
}
formAssignedTo = null;
assigneeSearchInput = "";
await LoadProjectMembersAsync(formProjectId);
}
private void SetFormAssignee(long? userId)
{
formAssignedTo = userId;
errorMessage = null;
isAssigneeDropdownOpen = false;
}
private void OpenAssigneeDropdown()
{
if (formProjectId <= 0)
{
return;
}
isAssigneeDropdownOpen = true;
}
private void ToggleAssigneeDropdown()
{
if (isAssigneeDropdownOpen)
{
CloseAssigneeDropdown();
return;
}
OpenAssigneeDropdown();
}
private void EnsureAssigneeDropdownOpen()
{
if (formProjectId <= 0)
{
return;
}
isAssigneeDropdownOpen = true;
}
private void CloseAssigneeDropdown()
{
isAssigneeDropdownOpen = false;
}
private async Task LoadProjectMembersAsync(long projectId)
{
projectMembers.Clear();
isLoadingProjectMembers = true;
try
{
if (projectId <= 0)
{
return;
}
var client = ApiClient.CreateClient();
var membersResult = await client.GetFromJsonAsync<Result<IEnumerable<UserDto>>>($"Project/{projectId}/members", Serialization.CaseInsensitive);
if (membersResult?.IsSuccess == true && membersResult.Value != null)
{
projectMembers = membersResult.Value
.Where(user => user.IsActive)
.OrderBy(user => user.FirstName)
.ThenBy(user => user.LastName)
.ToList();
if (formAssignedTo.HasValue && !projectMembers.Any(user => user.Id == formAssignedTo.Value))
{
formAssignedTo = null;
}
}
else
{
formAssignedTo = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load project members for task form: {ex.Message}");
formAssignedTo = null;
}
finally
{
isLoadingProjectMembers = false;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("initIcons");
}
}
private async Task OpenCreateModal()
{
isEditing = false;
editingId = 0;
formTitle = "";
formTitleMy = null;
formDescription = null;
formDescriptionMy = null;
burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false;
formProjectId = projects.FirstOrDefault()?.Id ?? 0;
formStatusId = AppTaskStatus.Todo;
formPriorityId = TaskPriority.Medium;
formDueDate = DateTime.Today.AddDays(7);
formAssignedTo = null;
assigneeSearchInput = "";
errorMessage = null;
isTitleError = false; titleErrorMessage = "";
isProjectError = false; projectErrorMessage = "";
showModal = true;
await LoadProjectMembersAsync(formProjectId);
}
private async Task OpenEditModal(TaskDto task)
{
isEditing = true;
editingId = task.Id;
formTitle = task.Title;
formTitleMy = task.TitleMy;
formDescription = task.Description;
formDescriptionMy = task.DescriptionMy;
burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false;
formProjectId = task.ProjectId;
formStatusId = task.StatusId;
formPriorityId = task.PriorityId;
formDueDate = task.DueDate;
formAssignedTo = task.AssignedTo;
assigneeSearchInput = "";
errorMessage = null;
isTitleError = false; titleErrorMessage = "";
isProjectError = false; projectErrorMessage = "";
showModal = true;
await LoadProjectMembersAsync(formProjectId);
}
private void CloseModal()
{
showModal = false;
errorMessage = null;
isTitleError = false; titleErrorMessage = "";
isProjectError = false; projectErrorMessage = "";
assigneeSearchInput = "";
isAssigneeDropdownOpen = false;
projectMembers.Clear();
isLoadingProjectMembers = false;
}
private void RequestSaveConfirmation()
{
isTitleError = false; titleErrorMessage = "";
isProjectError = false; projectErrorMessage = "";
bool hasError = false;
if (string.IsNullOrWhiteSpace(formTitle) && string.IsNullOrWhiteSpace(formTitleMy))
{
isTitleError = true;
titleErrorMessage = AppLocalization.Text("validation.taskTitleRequired", "Task title is required.");
hasError = true;
}
if (formProjectId == 0)
{
isProjectError = true;
projectErrorMessage = AppLocalization.Text("validation.selectProjectRequired", "Please select a project.");
hasError = true;
}
if (hasError) return;
if (isEditing)
{
confirmTitle = AppLocalization.Text("task.updateTitle", "Update Task?");
confirmMessage = AppLocalization.Text("task.updateConfirm", "Are you sure you want to update this task?");
confirmButtonText = AppLocalization.Text("action.update", "Update");
confirmIcon = "pencil";
confirmIconBg = "bg-amber-50";
confirmIconText = "text-amber-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
}
else
{
confirmTitle = AppLocalization.Text("task.createTitle", "Create Task?");
confirmMessage = AppLocalization.Text("task.createConfirm", "Are you sure you want to create this task?");
confirmButtonText = AppLocalization.Text("action.create", "Create");
confirmIcon = "plus-circle";
confirmIconBg = "bg-emerald-50";
confirmIconText = "text-emerald-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
}
confirmAction = SaveTask;
showConfirmDialog = true;
}
private void RequestDeleteConfirmation(long id)
{
confirmTitle = AppLocalization.Text("task.deleteTitle", "Delete Task?");
confirmMessage = AppLocalization.Text("task.deleteConfirm", "Are you sure you want to delete this task? This action cannot be undone.");
confirmButtonText = AppLocalization.Text("action.delete", "Delete");
confirmIcon = "trash-2";
confirmIconBg = "bg-rose-50";
confirmIconText = "text-rose-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
confirmAction = () => DeleteTask(id);
showConfirmDialog = true;
}
private async Task HandleConfirmAction()
{
showConfirmDialog = false;
if (confirmAction != null)
await confirmAction();
}
private void CloseConfirmDialog()
{
showConfirmDialog = false;
}
private async Task SaveTask()
{
if (string.IsNullOrWhiteSpace(formTitle) && string.IsNullOrWhiteSpace(formTitleMy))
{
errorMessage = ResultMessages.TaskTitleRequired;
return;
}
if (formProjectId == 0)
{
errorMessage = ResultMessages.SelectProjectRequired;
return;
}
isSaving = true;
errorMessage = null;
var client = ApiClient.CreateClient();
try
{
if (isEditing)
{
var dto = new UpdateTaskDto
{
Title = formTitle,
TitleMy = formTitleMy,
Description = formDescription,
DescriptionMy = formDescriptionMy,
StatusId = formStatusId,
PriorityId = formPriorityId,
DueDate = formDueDate,
AssignedTo = formAssignedTo
};
var response = await client.PutAsJsonAsync($"Task/{editingId}", dto);
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToUpdateTask;
return;
}
}
else
{
var dto = new CreateTaskDto
{
Title = formTitle,
TitleMy = formTitleMy,
Description = formDescription,
DescriptionMy = formDescriptionMy,
ProjectId = formProjectId,
StatusId = formStatusId,
PriorityId = formPriorityId,
DueDate = formDueDate,
AssignedTo = formAssignedTo
};
var response = await client.PostAsJsonAsync("Task", dto);
var res = await response.Content.ReadFromJsonAsync<Result<TaskDto>>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToCreateTask;
return;
}
}
CloseModal();
await LoadTasks();
successMessage = isEditing ? AppLocalization.Text("task.updated", "Task updated successfully!") : AppLocalization.Text("task.created", "Task created successfully!");
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isSaving = false;
}
}
private async Task DeleteTask(long id)
{
var client = ApiClient.CreateClient();
try
{
var response = await client.DeleteAsync($"Task/{id}");
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
await JS.InvokeVoidAsync("alert", res?.ErrorMessage ?? AppLocalization.Text("task.failedDelete", "Failed to delete task."));
return;
}
await LoadTasks();
if (currentPage > TotalPages && TotalPages > 0)
currentPage = TotalPages;
successMessage = AppLocalization.Text("task.deleted", "Task deleted successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"Delete error: {ex.Message}");
}
}
private string GetStatusBadge(AppTaskStatus statusId) => statusId switch
{
AppTaskStatus.Todo => "bg-slate-100 text-slate-700",
AppTaskStatus.InProgress => "bg-blue-100 text-blue-700",
AppTaskStatus.Done => "bg-emerald-100 text-emerald-700",
_ => "bg-slate-100 text-slate-600"
};
private string GetStatusLabel(AppTaskStatus statusId) => AppLocalization.StatusLabel(statusId);
private static string GetTaskCode(long taskId) => $"TSK-{taskId:D4}";
private string GetPriorityBadge(TaskPriority priorityId) => priorityId switch
{
TaskPriority.Low => "bg-slate-100 text-slate-600",
TaskPriority.Medium => "bg-amber-100 text-amber-700",
TaskPriority.High => "bg-red-100 text-red-700",
_ => "bg-slate-100 text-slate-600"
};
private string GetPriorityLabel(TaskPriority priorityId) => AppLocalization.PriorityLabel(priorityId);
}