ignore folder

This commit is contained in:
2026-04-21 06:30:48 +07:00
commit ca00b36f19
70 changed files with 3871 additions and 0 deletions

View File

@ -0,0 +1,35 @@
"use client";
import { useApiStore } from "@/store/uiStore";
export default function AppToasts() {
const { toasts, removeToast } = useApiStore((s) => ({
toasts: s.toasts,
removeToast: s.removeToast
}));
return (
<div className="toast-stack">
{toasts.map((toast) => (
<div key={toast.id} className={`toast show align-items-center border-0 ${toastClass(toast.type)}`}>
<div className="d-flex">
<div className="toast-body text-white">{toast.message}</div>
<button
className="btn btn-sm btn-link text-white"
onClick={() => removeToast(toast.id)}
aria-label="dismiss"
>
×
</button>
</div>
</div>
))}
</div>
);
}
function toastClass(type: "success" | "error" | "info") {
if (type === "success") return "bg-success text-white";
if (type === "error") return "bg-danger text-white";
return "bg-info text-white";
}

View 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>
);
}