Spaces:
Running
Running
| @inherits LayoutComponentBase | |
| @implements IDisposable | |
| @using System.Security.Claims | |
| @inject TaskTrackingSystem.WebApp.Localization.UiLanguageService Language | |
| <div class="app-shell flex min-h-screen bg-slate-50 text-slate-950 dark:bg-slate-950 dark:text-slate-50"> | |
| <button type="button" | |
| class="sidebar-scrim" | |
| onclick="document.body.classList.remove('tts-sidebar-open');" | |
| aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")"></button> | |
| <aside class="app-sidebar sticky top-0 z-40 flex h-screen w-64 shrink-0 flex-col overflow-hidden border-r border-slate-800 bg-slate-950 text-white"> | |
| <div class="app-sidebar-brand flex min-h-20 items-center gap-3 px-5 py-4"> | |
| <span class="app-sidebar-logo inline-flex h-10 w-10 items-center justify-center rounded-lg text-white shadow-sm"> | |
| <i data-lucide="clipboard-check" class="h-5 w-5"></i> | |
| </span> | |
| <span class="app-sidebar-title flex flex-col leading-tight"> | |
| <strong class="text-lg font-bold tracking-normal text-white">Taskify</strong> | |
| <span class="mt-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">@AppLocalization.Text("common.workspace", "Workspace")</span> | |
| </span> | |
| <button type="button" | |
| class="control-button mobile-sidebar-close ml-auto hidden h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/10 text-white hover:bg-white/15 hover:text-white lg:hidden" | |
| onclick="document.body.classList.remove('tts-sidebar-open');" | |
| aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")"> | |
| <i data-lucide="x" class="h-4 w-4"></i> | |
| </button> | |
| </div> | |
| <div class="flex-1 overflow-y-auto pb-28"> | |
| <NavMenu @rendermode="new InteractiveServerRenderMode(prerender: false)" /> | |
| </div> | |
| <div class="app-sidebar-user absolute inset-x-0 bottom-0 z-20 shrink-0 border-t border-white/10 bg-slate-950/95 p-3 backdrop-blur"> | |
| <AuthorizeView> | |
| <Authorized> | |
| <div class="flex items-center rounded-lg border border-white/10 bg-white/[0.04] p-2 transition-colors hover:bg-white/[0.08]"> | |
| <span class="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white font-semibold text-sm shrink-0"> | |
| @(context.User.Identity?.Name?.Length > 0 ? context.User.Identity.Name[0].ToString().ToUpper() : "U") | |
| </span> | |
| <div class="ml-3 flex min-w-0 flex-col overflow-hidden"> | |
| <span class="max-w-[130px] truncate text-sm font-semibold text-white">@context.User.Identity?.Name</span> | |
| <form id="logout-form" method="post" action="/account/logout" class="mt-0.5"> | |
| <AntiforgeryToken /> | |
| <button type="button" onclick="document.body.classList.remove('tts-sidebar-open'); document.getElementById('logout-confirm-dialog').style.display='flex';" class="text-xs hover:opacity-90 font-semibold text-left flex items-center gap-1 transition-all text-slate-300"> | |
| <i data-lucide="log-out" class="h-3 w-3"></i> | |
| @Language.Texts.SignOut | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </Authorized> | |
| <NotAuthorized> | |
| <a href="/login" class="flex items-center justify-center rounded-lg bg-white/10 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-white/15"> | |
| @Language.Texts.SignIn | |
| </a> | |
| </NotAuthorized> | |
| </AuthorizeView> | |
| </div> | |
| </aside> | |
| <main class="app-main flex min-w-0 flex-1 flex-col lg:ml-4"> | |
| <header class="app-topbar flex min-h-16 items-center justify-between gap-3 border-b border-slate-200 bg-slate-50/95 px-4 text-slate-900 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100"> | |
| <div class="flex min-w-0 items-center gap-3"> | |
| <button type="button" | |
| class="control-button mobile-sidebar-toggle hidden h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 hover:text-teal-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white" | |
| onclick="document.body.classList.add('tts-sidebar-open');" | |
| aria-label="@AppLocalization.Text("common.openNavigation", "Open navigation")"> | |
| <i data-lucide="menu" class="h-4 w-4"></i> | |
| </button> | |
| <div class="app-topbar-title min-w-0"> | |
| <h1 class="truncate text-base font-bold tracking-normal text-slate-950 dark:text-slate-50">@CurrentPageTitle</h1> | |
| <p class="hidden truncate text-xs font-medium text-slate-500 dark:text-slate-400 sm:block">@CurrentPageDescription</p> | |
| </div> | |
| </div> | |
| <div class="flex shrink-0 items-center gap-2 sm:gap-3"> | |
| <AuthorizeView> | |
| <Authorized> | |
| <FirebaseTokenSync /> | |
| </Authorized> | |
| </AuthorizeView> | |
| <NotificationBell /> | |
| <LanguageToggle /> | |
| <ThemeToggle /> | |
| </div> | |
| </header> | |
| <div class="app-content flex-1 overflow-y-auto p-5 lg:p-6"> | |
| @if (isAuthorizedPage) | |
| { | |
| @Body | |
| } | |
| else | |
| { | |
| <div class="flex flex-col items-center justify-center py-20 px-4"> | |
| <div class="rounded-full p-4 mb-4" style="color: var(--brand-accent); background-color: var(--status-warning-bg);"> | |
| <i data-lucide="shield-alert" class="h-12 w-12"></i> | |
| </div> | |
| <h3 class="text-xl font-bold" style="color: var(--text-primary);">@Language.Texts.AccessDenied</h3> | |
| <p class="text-center mt-2 max-w-sm text-sm" style="color: var(--text-muted);"> | |
| @Language.Texts.AccessDeniedDescription | |
| </p> | |
| <a href="@dashboardHref" class="mt-6 inline-flex items-center rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors shadow-sm hover:opacity-90" style="background-color: var(--brand-primary);"> | |
| <i data-lucide="arrow-left" class="h-4 w-4 mr-2"></i> | |
| @Language.Texts.BackToDashboard | |
| </a> | |
| </div> | |
| } | |
| </div> | |
| </main> | |
| <div id="logout-confirm-dialog" class="fixed inset-0 z-[80] flex items-center justify-center bg-slate-950/45 px-4 backdrop-blur-sm" style="display: none;"> | |
| <div class="w-full max-w-sm overflow-hidden rounded-lg border border-slate-100 bg-white shadow-lg dark:border-slate-800 dark:bg-slate-900"> | |
| <div class="p-6 text-center"> | |
| <div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full" style="background-color: var(--status-warning-bg); color: var(--status-warning-text);"> | |
| <i data-lucide="log-out" class="h-7 w-7"></i> | |
| </div> | |
| <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-50">@Language.Texts.SignOutTitle</h3> | |
| <p class="mt-2 text-sm text-slate-500 dark:text-slate-400">@Language.Texts.SignOutDescription</p> | |
| </div> | |
| <div class="flex items-center justify-center space-x-3 border-t border-slate-100 bg-slate-50/50 px-6 py-4 dark:border-slate-800 dark:bg-slate-950/40"> | |
| <button type="button" onclick="document.getElementById('logout-confirm-dialog').style.display='none';" class="flex-1 rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"> | |
| @Language.Texts.Cancel | |
| </button> | |
| <button type="button" onclick="document.getElementById('logout-form').submit();" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors" style="background-color: var(--brand-accent);"> | |
| @Language.Texts.ConfirmSignOut | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| @code { | |
| [Inject] private NavigationManager Navigation { get; set; } = default!; | |
| [Inject] private IJSRuntime JS { get; set; } = default!; | |
| [Inject] private MenuAuthorizationService MenuAuthorization { get; set; } = default!; | |
| [Inject] private Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider AuthStateProvider { get; set; } = default!; | |
| private bool isAuthorizedPage = true; | |
| private string dashboardHref = "/dashboard"; | |
| private bool IsDashboardPage => | |
| string.IsNullOrEmpty(Navigation.ToBaseRelativePath(Navigation.Uri)) || | |
| Navigation.ToBaseRelativePath(Navigation.Uri).Split('?')[0].Trim('/') is "dashboard" or "Home" or ""; | |
| private string CurrentRoute => Navigation.ToBaseRelativePath(Navigation.Uri).Split('?')[0].Trim('/'); | |
| private string CurrentPageTitle | |
| { | |
| get | |
| { | |
| if (CurrentRoute.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) && | |
| CurrentRoute.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return AppLocalization.PageTitle("kanbanBoard", "Kanban Board"); | |
| } | |
| if (CurrentRoute.StartsWith("tasks/", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return AppLocalization.PageTitle("taskDetails", "Task Details"); | |
| } | |
| return CurrentRoute switch | |
| { | |
| "" or "dashboard" or "Home" => AppLocalization.PageTitle("dashboard", "Dashboard"), | |
| "dashboard/manager" => AppLocalization.PageTitle("managerDashboard", "Manager Dashboard"), | |
| "dashboard/employee" => AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard"), | |
| "projects" or "projects/all" => AppLocalization.PageTitle("projects", "Projects"), | |
| "projects/assign" => AppLocalization.PageTitle("projectAssignment", "Project Assignment"), | |
| "tasks" or "tasks/all" => AppLocalization.PageTitle("tasks", "Tasks"), | |
| "board" => AppLocalization.PageTitle("kanbanBoard", "Kanban Board"), | |
| "tasks/assign" => AppLocalization.PageTitle("taskAssignment", "Task Assignment"), | |
| "issues" or "issues/all" => AppLocalization.PageTitle("issues", "Issues"), | |
| "issues/add" => AppLocalization.PageTitle("addIssue", "Add Issue"), | |
| "reports/tasks" => AppLocalization.PageTitle("taskReport", "Task Report"), | |
| "reports/time" => AppLocalization.PageTitle("timeTracking", "Time Tracking"), | |
| "reports/projects" => AppLocalization.PageTitle("projectProgress", "Project Progress"), | |
| "reports/overdue" => AppLocalization.PageTitle("overdueTasks", "Overdue Tasks"), | |
| "reports/employees" => AppLocalization.PageTitle("employeeReport", "Employee Report"), | |
| "roles" => AppLocalization.PageTitle("rolesAccess", "Roles & Access"), | |
| "users" => AppLocalization.PageTitle("users", "Users"), | |
| "audit-logs" => AppLocalization.PageTitle("auditLogs", "Audit Logs"), | |
| _ => AppLocalization.Text("common.workspace", "Workspace") | |
| }; | |
| } | |
| } | |
| private string CurrentPageDescription => CurrentPageTitle switch | |
| { | |
| var title when title == AppLocalization.PageTitle("dashboard", "Dashboard") => AppLocalization.PageDescription("dashboard", "A quick overview of work, risk, and progress."), | |
| var title when title == AppLocalization.PageTitle("managerDashboard", "Manager Dashboard") => AppLocalization.PageDescription("managerDashboard", "Track projects, people, and delivery health."), | |
| var title when title == AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard") => AppLocalization.PageDescription("employeeDashboard", "Focus on your tasks, due dates, and progress."), | |
| var title when title == AppLocalization.PageTitle("projects", "Projects") => AppLocalization.PageDescription("projects", "Manage project plans, timelines, and assignments."), | |
| var title when title == AppLocalization.PageTitle("projectAssignment", "Project Assignment") => AppLocalization.PageDescription("projectAssignment", "Assign team members to the right projects."), | |
| var title when title == AppLocalization.PageTitle("tasks", "Tasks") => AppLocalization.PageDescription("tasks", "Review, filter, and update work items."), | |
| var title when title == AppLocalization.PageTitle("kanbanBoard", "Kanban Board") => AppLocalization.PageDescription("kanbanBoard", "Move work through status columns."), | |
| var title when title == AppLocalization.PageTitle("taskAssignment", "Task Assignment") => AppLocalization.PageDescription("taskAssignment", "Assign tasks to project members."), | |
| var title when title == AppLocalization.PageTitle("issues", "Issues") => AppLocalization.PageDescription("issues", "Track blockers, notes, and daily work progress."), | |
| var title when title == AppLocalization.PageTitle("addIssue", "Add Issue") => AppLocalization.PageDescription("addIssue", "Record a new issue against a task."), | |
| var title when title == AppLocalization.PageTitle("rolesAccess", "Roles & Access") => AppLocalization.PageDescription("rolesAccess", "Control user roles, menus, and permissions."), | |
| var title when title == AppLocalization.PageTitle("users", "Users") => AppLocalization.PageDescription("users", "Manage people, status, and account details."), | |
| var title when title == AppLocalization.PageTitle("auditLogs", "Audit Logs") => AppLocalization.PageDescription("auditLogs", "Review system activity and changes."), | |
| var title when title.Contains(AppLocalization.Text("page.taskReport", "Task Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.taskReport.desc", "Analyze work data and export reports."), | |
| var title when title.Contains(AppLocalization.Text("page.timeTracking", "Time Tracking"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.timeTracking.desc", "Review effort by task, project, and assignee."), | |
| var title when title.Contains(AppLocalization.Text("page.projectProgress", "Project Progress"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.projectProgress.desc", "Monitor delivery health across active projects."), | |
| var title when title.Contains(AppLocalization.Text("page.overdueTasks", "Overdue Tasks"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.overdueTasks.desc", "Focus on delayed work that needs attention."), | |
| var title when title.Contains(AppLocalization.Text("page.employeeReport", "Employee Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.employeeReport.desc", "See how work is distributed across people."), | |
| var title when title.Contains(AppLocalization.Text("page.issueReport", "Issue Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.issueReport.desc", "Track daily execution, backlog health, and issue-level effort."), | |
| _ => AppLocalization.Text("common.workspace", "Workspace") | |
| }; | |
| protected override async Task OnInitializedAsync() | |
| { | |
| Language.Changed += OnLanguageChanged; | |
| Navigation.LocationChanged += OnLocationChanged; | |
| var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri); | |
| await UpdateAuthorizationAsync(relativePath); | |
| } | |
| private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e) | |
| { | |
| UpdateAuthorization(); | |
| } | |
| private void UpdateAuthorization() | |
| { | |
| var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri); | |
| _ = UpdateAuthorizationAsync(relativePath); | |
| } | |
| private void OnLanguageChanged() | |
| { | |
| _ = InvokeAsync(StateHasChanged); | |
| } | |
| private async Task UpdateAuthorizationAsync(string relativePath) | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| var user = authState.User; | |
| if (user.Identity?.IsAuthenticated != true) | |
| { | |
| if (!MenuAuthorizationService.IsPublicRoute(relativePath)) | |
| { | |
| var returnPath = new Uri(Navigation.Uri).AbsolutePath; | |
| Navigation.NavigateTo($"/login?ReturnUrl={Uri.EscapeDataString(returnPath)}"); | |
| } | |
| isAuthorizedPage = MenuAuthorizationService.IsPublicRoute(relativePath); | |
| await InvokeAsync(StateHasChanged); | |
| return; | |
| } | |
| var menus = await MenuAuthorization.GetUserMenusAsync(user); | |
| dashboardHref = ResolveDashboardHref(menus); | |
| var allowed = MenuAuthorization.IsRouteAllowed(user, relativePath); | |
| isAuthorizedPage = allowed; | |
| await InvokeAsync(StateHasChanged); | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| public void Dispose() | |
| { | |
| Navigation.LocationChanged -= OnLocationChanged; | |
| Language.Changed -= OnLanguageChanged; | |
| } | |
| private static string ResolveDashboardHref(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus) | |
| { | |
| foreach (var menu in menus) | |
| { | |
| var found = ResolveDashboardHref(menu); | |
| if (!string.IsNullOrWhiteSpace(found)) | |
| { | |
| return found; | |
| } | |
| } | |
| return "/dashboard"; | |
| } | |
| private static string? ResolveDashboardHref(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu) | |
| { | |
| if (!string.IsNullOrWhiteSpace(menu.MenuUrl) && | |
| menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return menu.MenuUrl; | |
| } | |
| foreach (var subMenu in menu.SubMenus) | |
| { | |
| var found = ResolveDashboardHref(subMenu); | |
| if (!string.IsNullOrWhiteSpace(found)) | |
| { | |
| return found; | |
| } | |
| } | |
| return null; | |
| } | |
| } | |