User
Deploy localized dashboard fixes
3dd5c9a
Raw History Blame Contribute Delete
28.1 kB
@page "/dashboard/employee"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Authorize]
@inject ApiClientService ApiClient
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>@AppLocalization.Text("dashboard.employeeTitle", "Employee Dashboard")</PageTitle>
<div class="min-h-screen bg-slate-50 px-4 py-6 text-slate-900 sm:px-6 lg:px-8 dark:bg-slate-950 dark:text-slate-50">
<div class="mx-auto max-w-[1440px] space-y-6">
<div class="rounded-xl border border-slate-200 bg-white px-8 py-8 shadow-sm">
<div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.08em] text-violet-700">@AppLocalization.Text("dashboard.teamMember", "Team Member")</p>
<h2 class="mt-2 text-3xl font-bold tracking-tight text-slate-950">@AppLocalization.Text("dashboard.myWork", "My Work")</h2>
<p class="mt-3 max-w-2xl text-sm text-slate-600">
@AppLocalization.Text("dashboard.employeeIntro", "Tasks, deadlines, and your progress in one place.")
</p>
</div>
<div class="flex flex-wrap gap-3">
<a href="/tasks/all" class="inline-flex items-center rounded-2xl bg-violet-700 px-5 py-3 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-600">
<i data-lucide="check-square" class="mr-2 h-4 w-4"></i>
@AppLocalization.Text("dashboard.viewTasks", "View Tasks")
</a>
<a href="/reports/tasks" class="inline-flex items-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 shadow-sm transition-colors hover:bg-slate-50">
<i data-lucide="bar-chart-3" class="mr-2 h-4 w-4"></i>
@AppLocalization.Text("page.reports", "Reports")
</a>
</div>
</div>
</div>
@if (isLoading)
{
<div class="rounded-xl border border-slate-200 bg-white px-6 py-12 text-center shadow-sm">
<div class="mx-auto flex items-center justify-center gap-3 text-slate-500">
<svg class="h-5 w-5 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span class="text-sm font-medium">@AppLocalization.Text("dashboard.loadingDashboard", "Loading dashboard...")</span>
</div>
</div>
}
else if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="rounded-xl border border-rose-200 bg-rose-50 px-6 py-5 text-sm text-rose-700 shadow-sm">
@errorMessage
</div>
}
else
{
<EmployeeOverview Metrics="SummaryMetrics" />
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1.7fr)_minmax(280px,1fr)]">
<div class="space-y-5">
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="flex items-center justify-between gap-4 border-b border-slate-100 px-6 py-5">
<div class="flex items-center gap-3">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-violet-200 bg-violet-50 text-violet-700">
<i data-lucide="timer-reset" class="h-4 w-4"></i>
</span>
<div>
<h3 class="text-xl font-bold text-slate-950">@AppLocalization.Text("status.inprogress", "In Progress")</h3>
<p class="text-sm text-slate-500">@AppLocalization.Text("dashboard.tasksInProgressDesc", "Tasks currently being worked on")</p>
</div>
</div>
<span class="rounded-full bg-slate-100 px-3 py-1 text-sm font-semibold text-slate-600">@AppLocalization.Number(InProgressTasks.Count)</span>
</div>
<div class="max-h-[420px] divide-y divide-slate-100 overflow-y-auto">
@if (InProgressTasks.Any())
{
@foreach (var task in InProgressTasks)
{
<div class="flex items-center gap-4 px-6 py-5">
<span class="mt-1 h-2.5 w-2.5 rounded-full" style="background-color: @GetStatusColor(task.StatusId);"></span>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold text-slate-950">@Content.GetTaskTitle(task)</div>
<div class="truncate text-xs text-slate-500">@GetProjectName(task.ProjectId)</div>
</div>
<div class="text-right">
<div class="text-sm text-slate-500">@DisplayFormats.Date(task.DueDate)</div>
<div class="text-sm font-semibold @GetDueTextClass(task)">@GetDueLabel(task)</div>
</div>
</div>
}
}
else
{
<div class="px-6 py-10 text-sm text-slate-500">@AppLocalization.Text("dashboard.noTasksInProgress", "No tasks in progress right now.")</div>
}
</div>
</div>
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="flex items-center justify-between gap-4 border-b border-slate-100 px-6 py-5">
<div class="flex items-center gap-3">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-sky-200 bg-sky-50 text-sky-600">
<i data-lucide="clock-3" class="h-4 w-4"></i>
</span>
<div>
<h3 class="text-xl font-bold text-slate-950">@AppLocalization.Text("dashboard.todoUpcoming", "To Do & Upcoming")</h3>
<p class="text-sm text-slate-500">@AppLocalization.Text("dashboard.todoUpcomingDesc", "Tasks waiting for action or coming up soon")</p>
</div>
</div>
<span class="rounded-full bg-slate-100 px-3 py-1 text-sm font-semibold text-slate-600">@AppLocalization.Number(TodoUpcomingTasks.Count)</span>
</div>
<div class="max-h-[420px] divide-y divide-slate-100 overflow-y-auto">
@if (TodoUpcomingTasks.Any())
{
@foreach (var task in TodoUpcomingTasks)
{
<div class="flex items-center gap-4 px-6 py-5">
<span class="mt-1 h-2.5 w-2.5 rounded-full" style="background-color: @GetStatusColor(task.StatusId);"></span>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold text-slate-950">@Content.GetTaskTitle(task)</div>
<div class="truncate text-xs text-slate-500">@GetProjectName(task.ProjectId)</div>
</div>
<div class="text-right">
<div class="text-sm text-slate-500">@DisplayFormats.Date(task.DueDate)</div>
<div class="text-sm font-semibold @GetDueTextClass(task)">@GetDueLabel(task)</div>
</div>
</div>
}
}
else
{
<div class="px-6 py-10 text-sm text-slate-500">@AppLocalization.Text("dashboard.noUpcomingTasks", "No upcoming tasks found.")</div>
}
</div>
</div>
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="flex items-center justify-between gap-4 border-b border-rose-100 px-6 py-5">
<div class="flex items-center gap-3">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-rose-200 bg-rose-50 text-rose-600">
<i data-lucide="triangle-alert" class="h-4 w-4"></i>
</span>
<div>
<h3 class="text-xl font-bold text-slate-950">@AppLocalization.Text("status.overdue", "Overdue")</h3>
<p class="text-sm text-slate-500">@AppLocalization.Text("dashboard.overdueDesc", "Tasks past their due date")</p>
</div>
</div>
<span class="rounded-full bg-rose-50 px-3 py-1 text-sm font-semibold text-rose-600">@AppLocalization.Number(OverdueTasks.Count)</span>
</div>
<div class="max-h-[320px] divide-y divide-rose-100 overflow-y-auto">
@if (OverdueTasks.Any())
{
@foreach (var task in OverdueTasks)
{
<div class="flex items-center gap-4 px-6 py-5">
<span class="mt-1 inline-flex h-6 w-6 items-center justify-center rounded-full border border-rose-200 text-rose-500">
<i data-lucide="triangle-alert" class="h-3.5 w-3.5"></i>
</span>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold text-slate-950">@Content.GetTaskTitle(task)</div>
<div class="truncate text-xs text-slate-500">@GetProjectName(task.ProjectId)</div>
</div>
<div class="text-right">
<div class="text-sm text-slate-500">@DisplayFormats.Date(task.DueDate)</div>
<div class="text-sm font-semibold text-rose-600">@DaysOverdue(task)</div>
</div>
</div>
}
}
else
{
<div class="px-6 py-10 text-sm text-slate-500">@AppLocalization.Text("dashboard.noOverdueTasks", "No overdue tasks. Nice work.")</div>
}
</div>
</div>
</div>
<div class="space-y-5">
<div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<div class="flex items-start justify-between gap-4">
<div>
<div class="flex items-center gap-3">
<button type="button" @onclick="PrevMonth" class="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50">
<i data-lucide="chevron-left" class="h-4 w-4"></i>
</button>
<h3 class="text-xl font-bold text-slate-950 min-w-[120px]">@CalendarMonthLabel</h3>
<button type="button" @onclick="NextMonth" class="inline-flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50">
<i data-lucide="chevron-right" class="h-4 w-4"></i>
</button>
</div>
<p class="mt-2 text-sm text-slate-500">@CalendarTodayLabel</p>
</div>
</div>
<div class="mt-5 grid grid-cols-7 gap-2 text-center text-xs font-semibold uppercase tracking-wider text-slate-400">
@foreach (var day in WeekdayLabels)
{
<span>@day</span>
}
</div>
<div class="mt-3 grid grid-cols-7 gap-2">
@foreach (var cell in CalendarCells)
{
<button type="button" @onclick="() => SelectDate(cell.Date)" class="@GetCalendarCellClasses(cell)">
<div class="flex h-full flex-col justify-between">
<div class="text-sm font-semibold @(cell.IsCurrentMonth ? (cell.IsSelected ? "text-white" : "text-slate-900") : "text-slate-400")">@AppLocalization.Number(cell.Date.Day)</div>
<div class="flex items-center gap-1">
@foreach (var dot in cell.TaskDots.Take(3))
{
<span class="h-1.5 w-1.5 rounded-full" style="background-color: @dot;"></span>
}
@if (cell.TaskDots.Count > 3)
{
<span class="text-[10px] leading-none text-slate-400">+@AppLocalization.Number(cell.TaskDots.Count - 3)</span>
}
</div>
</div>
</button>
}
</div>
</div>
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div class="border-b border-slate-100 px-6 py-5">
<h3 class="text-xl font-bold text-slate-950">@AppLocalization.Text("dashboard.relatedTasks", "Related Tasks")</h3>
<p class="mt-1 text-sm text-slate-500">@AppLocalization.Text("dashboard.tasksMarkedOn", "Tasks marked on {date}").Replace("{date}", SelectedDateLabel)</p>
</div>
<div class="divide-y divide-slate-100">
@if (SelectedDateTasks.Any())
{
@foreach (var task in SelectedDateTasks)
{
<div class="flex items-center gap-4 px-6 py-4">
<span class="mt-1 h-2.5 w-2.5 rounded-full" style="background-color: @GetStatusColor(task.StatusId);"></span>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-semibold text-slate-950">@Content.GetTaskTitle(task)</div>
<div class="truncate text-xs text-slate-500">@GetProjectName(task.ProjectId)</div>
</div>
<div class="text-right text-xs text-slate-500">
<div>@DisplayFormats.Date(task.DueDate)</div>
<div class="@GetDueTextClass(task)">@GetDueLabel(task)</div>
</div>
</div>
}
}
else
{
<div class="px-6 py-10 text-sm text-slate-500">@AppLocalization.Text("dashboard.noTasksForDay", "No tasks are scheduled for this day.")</div>
}
</div>
</div>
</div>
</div>
}
</div>
</div>
@code {
private bool isLoading = true;
private string? errorMessage;
private long currentUserId;
private List<TaskDto> tasks = new();
private List<ProjectDto> projects = new();
private DateTime selectedDate = DateTime.Today;
private string[] WeekdayLabels => AppLocalization.IsBurmeseCulture
? ["တနင်္ဂနွေ", "α€α€”α€„α€Ία€Ήα€œα€¬", "ထင်္ဂါ", "α€—α€―α€’α€Ήα€“α€Ÿα€°α€Έ", "α€€α€Όα€¬α€žα€•α€α€±α€Έ", "α€žα€±α€¬α€€α€Όα€¬", "α€…α€”α€±"]
: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
private List<CalendarCellViewModel> CalendarCells => BuildCalendarCells();
private List<TaskDto> SelectedDateTasks => tasks
.Where(task => task.DueDate.Date == selectedDate.Date)
.OrderBy(task => task.StatusId)
.ThenBy(task => task.DueDate)
.ToList();
private string SelectedDateLabel => GetDisplayDateLabel(selectedDate);
private string CalendarMonthLabel => GetMonthYearLabel(selectedDate);
private string CalendarTodayLabel => GetDisplayDateLabel(DateTime.Today);
private string CurrentTimeLabel => DateTime.Now.ToString("h:mm tt");
private string TimeZoneLabel
{
get
{
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var hours = Math.Abs(offset.Hours);
var minutes = Math.Abs(offset.Minutes);
return minutes == 0 ? $"GMT{sign}{hours}" : $"GMT{sign}{hours}:{minutes:00}";
}
}
private List<TaskDto> InProgressTasks => tasks
.Where(task => task.StatusId == AppTaskStatus.InProgress)
.OrderBy(task => task.DueDate)
.ToList();
private List<TaskDto> TodoUpcomingTasks => tasks
.Where(task => task.StatusId == AppTaskStatus.Todo || (task.StatusId != AppTaskStatus.Done && task.DueDate.Date >= DateTime.Today))
.OrderBy(task => task.DueDate)
.ToList();
private List<TaskDto> OverdueTasks => tasks
.Where(task => task.DueDate.Date < DateTime.Today && task.StatusId != AppTaskStatus.Done)
.OrderBy(task => task.DueDate)
.ToList();
private List<DashboardMetric> SummaryMetrics => BuildSummaryMetrics();
protected override async Task OnInitializedAsync()
{
await LoadDashboardData();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
private async Task LoadDashboardData()
{
isLoading = true;
errorMessage = null;
try
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (idStr != null)
{
long.TryParse(idStr, out currentUserId);
}
var client = ApiClient.CreateClient();
var tasksResult = await client.GetFromJsonAsync<Result<List<TaskDto>>>($"User/{currentUserId}/tasks", Serialization.CaseInsensitive);
var projectsResult = await client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive);
if (tasksResult?.IsSuccess == true && tasksResult.Value != null)
{
tasks = tasksResult.Value.ToList();
}
else
{
tasks = new List<TaskDto>();
}
selectedDate = DateTime.Today;
projects = projectsResult ?? new List<ProjectDto>();
}
catch (Exception ex)
{
errorMessage = $"{AppLocalization.Text("common.error", "Error")}: {ex.Message}";
}
finally
{
isLoading = false;
}
}
private void SelectDate(DateTime date)
{
selectedDate = date.Date;
}
private void PrevMonth()
{
selectedDate = selectedDate.AddMonths(-1);
}
private void NextMonth()
{
selectedDate = selectedDate.AddMonths(1);
}
private List<DashboardMetric> BuildSummaryMetrics()
{
var inProgress = tasks.Count(task => task.StatusId == AppTaskStatus.InProgress);
var todo = tasks.Count(task => task.StatusId == AppTaskStatus.Todo);
var dueSoon = tasks.Count(task => task.StatusId != AppTaskStatus.Done && task.DueDate.Date >= DateTime.Today && task.DueDate.Date <= DateTime.Today.AddDays(3));
var completed = tasks.Count(task => task.StatusId == AppTaskStatus.Done);
var overdue = tasks.Count(task => task.DueDate.Date < DateTime.Today && task.StatusId != AppTaskStatus.Done);
return new List<DashboardMetric>
{
new("timer-reset", AppLocalization.Text("dashboard.active", "active"), AppLocalization.Number(inProgress), AppLocalization.Text("status.inprogress", "In Progress"), "rounded-2xl bg-violet-700 p-3 text-white", "text-sm font-semibold text-violet-700"),
new("list-checks", AppLocalization.Text("dashboard.queued", "queued"), AppLocalization.Number(todo), AppLocalization.Text("status.todo", "To Do"), "rounded-2xl bg-cyan-600 p-3 text-white", "text-sm font-semibold text-cyan-600"),
new("calendar-clock", AppLocalization.Text("dashboard.soon", "soon"), AppLocalization.Number(dueSoon), AppLocalization.Text("dashboard.dueSoon", "Due Soon"), "rounded-2xl bg-amber-500 p-3 text-white", "text-sm font-semibold text-amber-600"),
new("circle-check-big", AppLocalization.Text("dashboard.done", "done"), AppLocalization.Number(completed), AppLocalization.Text("status.done", "Completed"), "rounded-2xl bg-emerald-600 p-3 text-white", "text-sm font-semibold text-emerald-600"),
new("triangle-alert", AppLocalization.Text("dashboard.late", "late"), AppLocalization.Number(overdue), AppLocalization.Text("status.overdue", "Overdue"), "rounded-2xl bg-rose-600 p-3 text-white", "text-sm font-semibold text-rose-600")
};
}
private List<CalendarCellViewModel> BuildCalendarCells()
{
var firstOfMonth = new DateTime(selectedDate.Year, selectedDate.Month, 1);
var start = firstOfMonth.AddDays(-(int)firstOfMonth.DayOfWeek);
var end = start.AddDays(42);
var dateMap = tasks
.Where(task => task.DueDate.Date >= start.Date && task.DueDate.Date < end.Date)
.GroupBy(task => task.DueDate.Date)
.ToDictionary(group => group.Key, group => group.ToList());
var cells = new List<CalendarCellViewModel>();
for (var i = 0; i < 42; i++)
{
var date = start.AddDays(i);
dateMap.TryGetValue(date.Date, out var dayTasks);
dayTasks ??= new List<TaskDto>();
cells.Add(new CalendarCellViewModel(
date,
date.Month == selectedDate.Month,
date.Date == selectedDate.Date,
dayTasks.Select(task => GetStatusColor(task.StatusId)).ToList(),
dayTasks.Count));
}
return cells;
}
private string GetProjectName(long projectId)
{
var project = projects.FirstOrDefault(project => project.Id == projectId);
return project is null ? $"Project #{projectId}" : Content.GetProjectName(project);
}
private string GetStatusColor(AppTaskStatus statusId) => statusId switch
{
AppTaskStatus.Todo => "#22d3ee",
AppTaskStatus.InProgress => "#3b82f6",
AppTaskStatus.Done => "#10b981",
_ => "#94a3b8"
};
private string GetCalendarCellClasses(CalendarCellViewModel cell)
{
var classes = new List<string>
{
"min-h-[52px] rounded-2xl border p-2 text-left transition-colors"
};
if (cell.IsSelected)
{
classes.Add("border-violet-500 bg-violet-600");
}
else if (cell.IsCurrentMonth)
{
classes.Add("border-slate-200 bg-white hover:border-violet-200 hover:bg-slate-50");
}
else
{
classes.Add("border-slate-100 bg-slate-50 text-slate-400");
}
return string.Join(" ", classes);
}
private string GetDueLabel(TaskDto task)
{
var days = (task.DueDate.Date - DateTime.Today).Days;
if (task.StatusId == AppTaskStatus.Done)
{
return AppLocalization.Text("dashboard.done", "Done");
}
if (days < 0)
{
return AppLocalization.Text("dashboard.daysAgo", "{count}d ago").Replace("{count}", AppLocalization.Number(Math.Abs(days)));
}
if (days == 0)
{
return AppLocalization.Text("dashboard.today", "Today");
}
return AppLocalization.Text("dashboard.daysLeft", "{count}d left").Replace("{count}", AppLocalization.Number(days));
}
private string GetDueTextClass(TaskDto task)
{
var days = (task.DueDate.Date - DateTime.Today).Days;
if (task.StatusId == AppTaskStatus.Done)
{
return "text-emerald-600";
}
if (days < 0)
{
return "text-rose-600";
}
if (days <= 3)
{
return "text-amber-500";
}
return "text-sky-600";
}
private string DaysOverdue(TaskDto task)
{
var days = (DateTime.Today - task.DueDate.Date).Days;
return days <= 0
? AppLocalization.Text("dashboard.dueToday", "Due today")
: AppLocalization.Text("dashboard.daysAgo", "{count}d ago").Replace("{count}", AppLocalization.Number(days));
}
private string GetDisplayDateLabel(DateTime date) => AppLocalization.IsBurmeseCulture
? $"{AppLocalization.MonthName(date, true)} {AppLocalization.Number(date.Day)}"
: date.ToString("dddd, MMM d");
private string GetMonthYearLabel(DateTime date) => AppLocalization.IsBurmeseCulture
? $"{AppLocalization.Number(date.Year)} {AppLocalization.MonthName(date)}"
: date.ToString("MMMM yyyy");
private sealed record CalendarCellViewModel(
DateTime Date,
bool IsCurrentMonth,
bool IsSelected,
List<string> TaskDots,
int TaskCount);
}