ignore folder
This commit is contained in:
111
components/layout/DashboardShell.tsx
Normal file
111
components/layout/DashboardShell.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePermissions } from "@/hooks/usePermissions";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { useTenantStore } from "@/store/tenantStore";
|
||||
import { useLocaleStore } from "@/store/uiStore";
|
||||
import { logout } from "@/services/auth";
|
||||
import { t } from "@/lib/locale";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/dashboard", label: "Dashboard", key: "dashboard", permission: "ALL" as const },
|
||||
{ href: "/dashboard/users", label: "Users", key: "users", permission: "USER_MANAGE_OR_ADMIN" as const },
|
||||
{ href: "/dashboard/roles", label: "Roles", key: "roles", permission: "ROLE_MANAGE_OR_ADMIN" as const },
|
||||
{ href: "/dashboard/workflow", label: "Workflow", key: "workflow", permission: "WORKFLOW_OR_ADMIN" as const },
|
||||
{ href: "/dashboard/audit", label: "Audit", key: "audit", permission: "ADMIN_ONLY" as const },
|
||||
{ href: "/dashboard/modules", label: "Modules", key: "modules", permission: "ADMIN_ONLY" as const },
|
||||
{ href: "/dashboard/settings", label: "Settings", key: "settings", permission: "ALL" as const }
|
||||
];
|
||||
|
||||
export default function DashboardShell({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const clearAuth = useAuthStore((s) => s.clearAuth);
|
||||
const { username, tenantId } = useAuthStore((s) => ({
|
||||
username: s.profile?.username ?? "Unknown",
|
||||
tenantId: s.tenantId
|
||||
}));
|
||||
const permissions = usePermissions();
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
const tenants = useTenantStore((s) => s.availableTenants);
|
||||
const currentTenant = useTenantStore((s) => s.tenantId);
|
||||
const setTenant = useTenantStore((s) => s.setTenantId);
|
||||
const setLocale = useLocaleStore((s) => s.setLocale);
|
||||
const authMode =
|
||||
process.env.NEXT_PUBLIC_AUTH_MODE?.toLowerCase?.() === "ldap" ? "LDAP" : "Local";
|
||||
|
||||
const visibleNavItems = navItems.filter((item) => {
|
||||
if (item.permission === "ALL") return true;
|
||||
if (item.permission === "USER_MANAGE_OR_ADMIN") return permissions.canManageUsers || permissions.isAdminOrManager;
|
||||
if (item.permission === "ROLE_MANAGE_OR_ADMIN") return permissions.canManageRoles || permissions.isAdminOrManager;
|
||||
if (item.permission === "WORKFLOW_OR_ADMIN") return permissions.canApproveWorkflow || permissions.isAdminOrManager;
|
||||
return permissions.isAdmin;
|
||||
});
|
||||
|
||||
const onTenantChange = (value: string) => {
|
||||
setTenant(value);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
clearAuth();
|
||||
router.replace("/(auth)/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<aside className="app-sidebar">
|
||||
<h2 className="text-white mb-4">UTMS Admin</h2>
|
||||
<p className="small text-white-50 mb-4">
|
||||
{t("tenant", locale)}: {tenantId || currentTenant}
|
||||
</p>
|
||||
<nav className="vstack gap-1">
|
||||
{visibleNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className={`text-decoration-none ${pathname === item.href ? "text-primary fw-bold" : "text-white"}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="app-content w-100">
|
||||
<header className="card page-card p-3 mb-3">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="text-muted">{t("welcome", locale)}, {username}</span>
|
||||
<span className="text-muted d-none d-sm-inline">| Mode: {authMode}</span>
|
||||
</div>
|
||||
<div className="d-flex gap-2">
|
||||
<select className="form-select" value={currentTenant ?? ""} onChange={(e) => onTenantChange(e.target.value)}>
|
||||
<option value="">Tenant</option>
|
||||
{tenants.map((tenant) => (
|
||||
<option key={tenant} value={tenant}>
|
||||
{tenant}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="form-select"
|
||||
value={locale}
|
||||
onChange={(e) => setLocale(e.target.value as "en" | "id")}
|
||||
>
|
||||
<option value="en">en-US</option>
|
||||
<option value="id">id-ID</option>
|
||||
</select>
|
||||
<button className="btn btn-outline-danger" onClick={handleLogout}>
|
||||
{t("logout", locale)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section className="pb-3">{children}</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user