User
Deploy refreshed UI theme
be17a46
Raw
History Blame Contribute Delete
25.3 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>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">Team Member</p>
<h2 class="mt-2 text-3xl font-bold tracking-tight text-slate-950">My Work</h2>
<p class="mt-3 max-w-2xl text-sm text-slate-600">
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>
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>
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">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">In Progress</h3>
<p class="text-sm text-slate-500">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">@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">@task.Title</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">@task.DueDate.ToString("yyyy-MM-dd")</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">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">To Do & Upcoming</h3>
<p class="text-sm text-slate-500">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">@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">@task.Title</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">@task.DueDate.ToString("yyyy-MM-dd")</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">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">Overdue</h3>
<p class="text-sm text-slate-500">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">@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">@task.Title</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">@task.DueDate.ToString("yyyy-MM-dd")</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">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")">@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">+@((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">Related Tasks</h3>
<p class="mt-1 text-sm text-slate-500">Tasks marked on @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">@task.Title</div>
<div class="truncate text-xs text-slate-500">@GetProjectName(task.ProjectId)</div>
</div>
<div class="text-right text-xs text-slate-500">
<div>@task.DueDate.ToString("yyyy-MM-dd")</div>
<div class="@GetDueTextClass(task)">@GetDueLabel(task)</div>
</div>
</div>
}
}
else
{
<div class="px-6 py-10 text-sm text-slate-500">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 readonly string[] WeekdayLabels = ["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 => selectedDate.ToString("dddd, MMM d");
private string CalendarMonthLabel => selectedDate.ToString("MMMM yyyy");
private string CalendarTodayLabel => DateTime.Today.ToString("dddd, d");
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 = $"Failed to load dashboard data: {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", "active", inProgress.ToString(), "In Progress", "rounded-2xl bg-violet-700 p-3 text-white", "text-sm font-semibold text-violet-700"),
new("list-checks", "queued", todo.ToString(), "To Do", "rounded-2xl bg-cyan-600 p-3 text-white", "text-sm font-semibold text-cyan-600"),
new("calendar-clock", "soon", dueSoon.ToString(), "Due Soon", "rounded-2xl bg-amber-500 p-3 text-white", "text-sm font-semibold text-amber-600"),
new("circle-check-big", "done", completed.ToString(), "Completed", "rounded-2xl bg-emerald-600 p-3 text-white", "text-sm font-semibold text-emerald-600"),
new("triangle-alert", "late", overdue.ToString(), "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)
{
return projects.FirstOrDefault(project => project.Id == projectId)?.Name ?? $"Project #{projectId}";
}
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 "Done";
}
if (days < 0)
{
return $"{Math.Abs(days)}d ago";
}
if (days == 0)
{
return "Today";
}
return $"{days}d left";
}
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 ? "Due today" : $"{days}d ago";
}
private sealed record CalendarCellViewModel(
DateTime Date,
bool IsCurrentMonth,
bool IsSelected,
List<string> TaskDots,
int TaskCount);
}