Spaces:
Runtime error
Runtime error
Download TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor from Yuruyuruu/task-tracking-system: direct link, hf CLI and curl.
- Browser
- Download file 53.2 kB
-
https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor
- Command line
-
hf download hf://spaces/Yuruyuruu/task-tracking-system/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor
-
curl -L -o Tasks.razor https://huggingface.co/spaces/Yuruyuruu/task-tracking-system/resolve/main/TaskTrackingSystem.WebApp/Components/Pages/Features/Tasks/Tasks.razor
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); | |
| } | |