User
Deploy localized dashboard fixes
3dd5c9a
Raw History Blame Contribute Delete
32 kB
@page "/projects"
@page "/projects/all"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject ApiClientService ApiClient
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject AuthenticationStateProvider AuthStateProvider
@inject MenuAuthorizationService MenuAuthorization
<PageTitle>@AppLocalization.PageTitle("projects", "Projects")</PageTitle>
<SuccessAlert Message="@successMessage" />
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("projects", "Projects")</h2>
<p class="text-slate-500 mt-1">@(isAdmin ? AppLocalization.Text("project.manageAll", "Manage all projects.") : AppLocalization.Text("project.yourAssigned", "Your assigned projects."))</p>
</div>
@if (canCreateProject)
{
<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("project.newProject", "New Project")
</button>
}
</div>
@if (isLoading)
{
<LoadingSpinner Message="@AppLocalization.Text("common.loadingProjects", "Loading projects...")" />
}
else
{
<!-- Search Panel -->
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
<div class="flex items-center gap-3">
<div class="relative flex-1 max-w-md">
<input @bind="searchInput" @bind:event="oninput" type="text" placeholder="@AppLocalization.Text("common.searchProjects", "Search projects...")"
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>
<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">
<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">
<i data-lucide="rotate-ccw" class="h-4 w-4 text-slate-500"></i>
</button>
</div>
</div>
<!-- Projects Table -->
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<table class="w-full">
<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("common.index", "No.")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("field.name", "Name")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.startDate", "Start Date")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.endDate", "End Date")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("field.created", "Created")</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 (PagedProjects.Any())
{
@foreach (var entry in PagedProjects.Select((project, index) => (project, index)))
{
var project = entry.project;
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
<td class="px-6 py-4">
<button @onclick="() => NavigateToKanban(project.Id)" class="text-sm font-semibold text-slate-900 hover:text-violet-600 transition-colors">
@Content.GetProjectName(project)
</button>
@if (!string.IsNullOrEmpty(Content.GetProjectDescription(project)))
{
<p class="text-xs text-slate-400 mt-0.5 truncate max-w-xs">@Content.GetProjectDescription(project)</p>
}
</td>
<td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(project.StartDate)</td>
<td class="px-6 py-4 text-sm text-slate-600">@DisplayFormats.Date(project.EndDate)</td>
<td class="px-6 py-4 text-sm text-slate-400">@DisplayFormats.Date(project.CreatedAt)</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
@if (canEditProject || canDeleteProject)
{
@if (canEditProject)
{
<button @onclick="() => OpenEditModal(project)" 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 (canDeleteProject)
{
<button @onclick="() => RequestDeleteConfirmation(project.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="6" Icon="folder-kanban" Title="@AppLocalization.Text("project.noProjectsYet", "No projects yet")" Subtitle="@(canCreateProject ? AppLocalization.Text("project.createFirstProject", "Create your first project to get started.") : AppLocalization.Text("project.noAssignedProjects", "You have not been assigned to any projects."))" />
}
</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("project.editProject", "Edit Project") : AppLocalization.Text("project.newProject", "New Project"))</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(formNameMy))"
EnglishComplete="@(!string.IsNullOrWhiteSpace(formName))"
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="formNameMy" type="text" placeholder="α€‘α€™α€Šα€Ί" class="w-full rounded-lg border @(isNameError && string.IsNullOrWhiteSpace(formName) ? "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="@formNameMy" ExpectedBurmese="true" MoveToOtherLanguage="MoveProjectNameToEnglish" />
<BilingualTranslationAction SourceText="@formNameMy" SourceLanguage="my" TargetLanguage="en" IsName="false" TargetHasText="@(!string.IsNullOrWhiteSpace(formName))" Generated="GenerateProjectEnglishName" />
<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="MoveProjectDescriptionToEnglish" />
<BilingualTranslationAction SourceText="@formDescriptionMy" SourceLanguage="my" TargetLanguage="en" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescription))" Generated="GenerateProjectEnglishDescription" />
</BurmeseContent>
<EnglishContent>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.name", "Name") <span class="text-red-500">*</span></label>
<input @bind="formName" type="text" placeholder="@AppLocalization.Text("project.projectName", "Project name")" class="w-full rounded-lg border @(isNameError && string.IsNullOrWhiteSpace(formNameMy) ? "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="@formName" ExpectedBurmese="false" MoveToOtherLanguage="MoveProjectNameToBurmese" />
<BilingualTranslationAction SourceText="@formName" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formNameMy))" Generated="GenerateProjectBurmeseName" />
<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("project.projectDescriptionOptional", "Project description (optional)")" 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="MoveProjectDescriptionToBurmese" />
<BilingualTranslationAction SourceText="@formDescription" SourceLanguage="en" TargetLanguage="my" TargetHasText="@(!string.IsNullOrWhiteSpace(formDescriptionMy))" Generated="GenerateProjectBurmeseDescription" />
</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.startDate", "Start Date") <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formStartDate" type="date" class="w-full rounded-lg border @(isDateError ? "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 pr-10" />
@if (isDateError)
{
<svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" 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>
}
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.endDate", "End Date") <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formEndDate" type="date" class="w-full rounded-lg border @(isDateError ? "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 pr-10" />
@if (isDateError)
{
<svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" 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>
}
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5 font-sans">@AppLocalization.Text("common.budgetedHours", "Budgeted Hours")</label>
<input @bind="formBudgetedHours" type="number" min="0" placeholder="@AppLocalization.Text("common.budgetedHoursOptional", "Budgeted hours (optional)")" 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 font-sans" />
</div>
@if (isDateError)
{
<p class="mt-1 text-xs text-red-500">@dateErrorMessage</p>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="p-3 rounded-lg bg-red-50 border border-red-100">
<p class="text-sm text-red-600">@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 {
private bool isLoading = true;
private bool isAdmin = false;
private bool canCreateProject = false;
private bool canEditProject = false;
private bool canDeleteProject = false;
private long currentUserId = 0;
private bool showModal = false;
private bool isEditing = false;
private bool isSaving = false;
private string? errorMessage;
// Confirm dialog state
private bool showConfirmDialog = false;
private string? successMessage;
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<ProjectDto> projects = new();
// Search state
private string searchInput = "";
// 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<ProjectDto> PagedProjects => projects;
// Form fields
private long editingId;
private string formName = "";
private string? formNameMy;
private string? formDescription;
private string? formDescriptionMy;
private bool burmeseGenerated;
private bool englishGenerated;
private bool burmeseNeedsReview;
private bool englishNeedsReview;
private DateTime formStartDate = DateTime.Today;
private DateTime formEndDate = DateTime.Today.AddMonths(1);
private int? formBudgetedHours;
// Validation states
private bool isNameError;
private string nameErrorMessage = "";
private bool isDateError;
private string dateErrorMessage = "";
private void MoveProjectNameToEnglish() { formName = formNameMy ?? string.Empty; }
private void MoveProjectNameToBurmese() { formNameMy = formName; }
private void MoveProjectDescriptionToEnglish() { formDescription = formDescriptionMy; }
private void MoveProjectDescriptionToBurmese() { formDescriptionMy = formDescription; }
private void MarkGenerated(bool english)
{
if (english) { englishGenerated = true; englishNeedsReview = true; }
else { burmeseGenerated = true; burmeseNeedsReview = true; }
}
private Task GenerateProjectEnglishName(TranslationResult result) { formName = result.TranslatedText ?? string.Empty; MarkGenerated(true); return Task.CompletedTask; }
private Task GenerateProjectBurmeseName(TranslationResult result) { formNameMy = result.TranslatedText; MarkGenerated(false); return Task.CompletedTask; }
private Task GenerateProjectEnglishDescription(TranslationResult result) { formDescription = result.TranslatedText ?? string.Empty; MarkGenerated(true); return Task.CompletedTask; }
private Task GenerateProjectBurmeseDescription(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");
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (idStr != null) long.TryParse(idStr, out currentUserId);
await LoadProjectAccessAsync(user);
await LoadProjects();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
private async Task LoadProjects()
{
isLoading = true;
var client = ApiClient.CreateClient();
try
{
var url = BuildProjectsUrl();
var result = await client.GetFromJsonAsync<PagedResult<ProjectDto>>(url, Serialization.CaseInsensitive);
projects = result?.Items ?? new();
totalCount = result?.TotalCount ?? 0;
totalPages = result?.TotalPages ?? 0;
currentPage = result?.Page ?? currentPage;
pageSize = result?.PageSize ?? pageSize;
}
catch (Exception ex)
{
Console.WriteLine($"Load projects error: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private string BuildProjectsUrl()
{
var query = new List<string>
{
$"page={currentPage}",
$"limit={pageSize}"
};
if (!string.IsNullOrWhiteSpace(searchInput))
{
query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}");
}
return $"Project?{string.Join("&", query)}";
}
private void NavigateToKanban(long projectId)
{
Navigation.NavigateTo($"/projects/{projectId}/tasks");
}
private async Task HandlePageChanged(int page) { currentPage = page; await LoadProjects(); }
private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadProjects(); }
private async Task ApplySearch()
{
currentPage = 1;
await LoadProjects();
}
private async Task ResetSearch()
{
searchInput = "";
currentPage = 1;
await LoadProjects();
}
private async Task LoadProjectAccessAsync(System.Security.Claims.ClaimsPrincipal user)
{
canCreateProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Create");
canEditProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Update");
canDeleteProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Delete");
}
private void ClearValidation()
{
isNameError = false;
nameErrorMessage = "";
isDateError = false;
dateErrorMessage = "";
errorMessage = null;
}
private void OpenCreateModal()
{
isEditing = false;
editingId = 0;
formName = "";
formNameMy = null;
formDescription = null;
formDescriptionMy = null;
burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false;
formStartDate = DateTime.Today;
formEndDate = DateTime.Today.AddMonths(1);
formBudgetedHours = null;
ClearValidation();
showModal = true;
}
private void OpenEditModal(ProjectDto project)
{
isEditing = true;
editingId = project.Id;
formName = project.Name;
formNameMy = project.NameMy;
formDescription = project.Description;
formDescriptionMy = project.DescriptionMy;
burmeseGenerated = englishGenerated = burmeseNeedsReview = englishNeedsReview = false;
formStartDate = project.StartDate;
formEndDate = project.EndDate;
formBudgetedHours = project.BudgetedHours;
ClearValidation();
showModal = true;
}
private void CloseModal()
{
showModal = false;
ClearValidation();
}
// ─── Assignment Modal ─────────────────────────────────────────────────────
private void RequestSaveConfirmation()
{
ClearValidation();
bool hasError = false;
if (string.IsNullOrWhiteSpace(formName) && string.IsNullOrWhiteSpace(formNameMy))
{
isNameError = true;
nameErrorMessage = AppLocalization.Text("validation.projectNameRequired", "Project name is required.");
hasError = true;
}
if (formEndDate < formStartDate)
{
isDateError = true;
dateErrorMessage = AppLocalization.Text("validation.endDateBeforeStart", "End date cannot be before start date.");
hasError = true;
}
if (hasError) return;
if (isEditing)
{
confirmTitle = AppLocalization.Text("project.updateTitle", "Update Project?");
confirmMessage = AppLocalization.Text("project.updateConfirm", "Are you sure you want to update this project?");
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("project.createTitle", "Create Project?");
confirmMessage = AppLocalization.Text("project.createConfirm", "Are you sure you want to create this project?");
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 = SaveProject;
showConfirmDialog = true;
}
private void RequestDeleteConfirmation(long id)
{
confirmTitle = AppLocalization.Text("project.deleteTitle", "Delete Project?");
confirmMessage = AppLocalization.Text("project.deleteConfirm", "Are you sure you want to delete this project? 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 = () => DeleteProject(id);
showConfirmDialog = true;
}
private async Task HandleConfirmAction()
{
showConfirmDialog = false;
if (confirmAction != null)
await confirmAction();
}
private void CloseConfirmDialog()
{
showConfirmDialog = false;
}
private async Task SaveProject()
{
if ((string.IsNullOrWhiteSpace(formName) && string.IsNullOrWhiteSpace(formNameMy)) || formEndDate < formStartDate)
{
return;
}
isSaving = true;
errorMessage = null;
var client = ApiClient.CreateClient();
try
{
if (isEditing)
{
var dto = new UpdateProjectDto
{
Name = formName,
NameMy = formNameMy,
Description = formDescription,
DescriptionMy = formDescriptionMy,
StartDate = formStartDate,
EndDate = formEndDate,
BudgetedHours = formBudgetedHours
};
var response = await client.PutAsJsonAsync($"Project/{editingId}", dto);
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToUpdateProject;
return;
}
}
else
{
var dto = new CreateProjectDto
{
Name = formName,
NameMy = formNameMy,
Description = formDescription,
DescriptionMy = formDescriptionMy,
StartDate = formStartDate,
EndDate = formEndDate,
CreatedById = currentUserId,
BudgetedHours = formBudgetedHours
};
var response = await client.PostAsJsonAsync("Project", dto);
var res = await response.Content.ReadFromJsonAsync<Result<ProjectDto>>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToCreateProject;
return;
}
// After creating, reload
await LoadProjects();
CloseModal();
successMessage = AppLocalization.Text("project.created", "Project created successfully!");
return;
}
CloseModal();
await LoadProjects();
successMessage = isEditing ? AppLocalization.Text("project.updated", "Project updated successfully!") : AppLocalization.Text("project.created", "Project created successfully!");
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isSaving = false;
}
}
private async Task DeleteProject(long id)
{
var client = ApiClient.CreateClient();
try
{
var response = await client.DeleteAsync($"Project/{id}");
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
await JS.InvokeVoidAsync("alert", res?.ErrorMessage ?? AppLocalization.Text("project.failedDelete", "Failed to delete project."));
return;
}
await LoadProjects();
if (currentPage > TotalPages && TotalPages > 0)
currentPage = TotalPages;
successMessage = AppLocalization.Text("project.deleted", "Project deleted successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"Delete error: {ex.Message}");
}
}
}