@eintrek/erp-shell 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,11 +34,10 @@ export interface PermissionProviderProps {
34
34
  * into the host's `usePermission` probe, and renders one of three states:
35
35
  *
36
36
  * 1. while rules or probe are still loading → centered spinner card
37
- * 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
38
- * user roles for debugging
37
+ * 2. probe says no access → centered "ไม่มีสิทธิ์" card
39
38
  * 3. otherwise → children
40
39
  *
41
- * The host wraps `<PermissionProvider>` around its page chrome (typically
42
- * via `AppLayoutShell`'s `permissionProvider` slot).
40
+ * Role codes are intentionally not shown on the deny screen (they are
41
+ * internal identifiers like `fin_controller` and confuse end users).
43
42
  */
44
43
  export declare function PermissionProvider({ children, usePermission, fallbackRulesMap, }: PermissionProviderProps): import("react/jsx-runtime").JSX.Element | null;
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { useSidebarToggle } from "./navigation/use-sidebar-toggle";
13
13
  export { TapMenu } from "./navigation/tap-menu";
14
14
  export { NavTools } from "./navigation/nav-tools";
15
15
  export { AppBreadcrumb } from "./navigation/app-breadcrumb";
16
- export { NavUser, type NavUserProps } from "./navigation/nav-user";
16
+ export { NavUser, type NavUserProps, type NavUserSystemLink } from "./navigation/nav-user";
17
17
  export { NavMain, type NavMainProps } from "./navigation/nav-main";
18
18
  export { OrganizationSwitcher, type OrganizationSwitcherProps, type OrganizationLike, } from "./navigation/organization-switcher";
19
19
  export { SidebarHeader } from "./navigation/sidebar-header";
package/dist/index.esm.js CHANGED
@@ -5,7 +5,7 @@ import { useSession, signOut } from 'next-auth/react';
5
5
  import { useQueryClient, MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
6
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
7
7
  import { cn, Button, Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage, Avatar, AvatarImage, AvatarFallback, Collapsible, CollapsibleTrigger, CollapsibleContent, Badge, Skeleton, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, Card, CardHeader, CardContent } from '@eintrek/erp-theme';
8
- import { PanelLeftClose, Menu, ChevronRight, Building2, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
8
+ import { PanelLeftClose, Menu, ChevronRight, Building2, ExternalLink, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, AlertCircle } from 'lucide-react';
9
9
  import { create } from 'zustand';
10
10
  import { persist, createJSONStorage } from 'zustand/middleware';
11
11
  import { useRouter, usePathname, useParams } from 'next/navigation';
@@ -740,7 +740,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
740
740
  })] }) }));
741
741
  }
742
742
 
743
- function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", } = {}) {
743
+ function isExternalHref(href) {
744
+ return /^https?:\/\//i.test(href) || href.startsWith("//");
745
+ }
746
+ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, } = {}) {
744
747
  const { data: session, status } = useSession();
745
748
  const router = useRouter();
746
749
  const { theme, setTheme } = useTheme();
@@ -792,6 +795,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
792
795
  if (profileHref)
793
796
  router.push(profileHref);
794
797
  };
798
+ const handleSystemLink = (href) => {
799
+ setIsOpen(false);
800
+ if (isExternalHref(href)) {
801
+ window.location.assign(href);
802
+ return;
803
+ }
804
+ router.push(href);
805
+ };
795
806
  const handleThemeChange = (newTheme) => {
796
807
  setTheme(newTheme);
797
808
  setShowThemeMenu(false);
@@ -824,6 +835,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
824
835
  }
825
836
  if (!user)
826
837
  return null;
838
+ const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
827
839
  const dropdownContent = isOpen ? (jsxs("div", { "data-dropdown": "nav-user", className: "fixed w-52 sm:w-60 md:w-80 rounded-lg bg-sidebar border border-sidebar-border shadow-xl z-50 overflow-hidden\n -translate-x-[65%] -translate-y-[110%] sm:translate-x-0 sm:-translate-y-full", style: { top: `${position.top}px`, left: `${position.left}px` }, children: [organization && (jsx("div", { className: "p-4 border-b border-sidebar-border bg-sidebar-accent/30", children: jsxs("div", { className: "flex items-center gap-3", children: [organization.logoUrl ? (
828
840
  // eslint-disable-next-line @next/next/no-img-element
829
841
  jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
@@ -831,7 +843,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
831
843
  // sidebar; drop back to the generic icon's empty slot.
832
844
  onError: e => {
833
845
  e.currentTarget.style.display = "none";
834
- } })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
846
+ } })) : (jsx(Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxs("div", { className: "p-2", children: [visibleSystemLinks.length > 0 ? (jsxs(Fragment$1, { children: [visibleSystemLinks.map(link => (jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [link.icon ?? jsx(ExternalLink, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: link.label })] }, `${link.label}:${link.href}`))), jsx("div", { className: "my-1 border-t border-sidebar-border" })] })) : null, jsxs("div", { className: "relative", children: [jsxs("button", { onClick: () => {
835
847
  setShowThemeMenu(!showThemeMenu);
836
848
  setShowLanguageMenu(false);
837
849
  }, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsx(Palette, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E18\u0E35\u0E21" }), jsx(ChevronUp, { className: `h-4 w-4 transition-transform ${showThemeMenu ? "rotate-180" : ""}` })] }), showThemeMenu && (jsx("div", { className: "mt-1 ml-4 space-y-1", children: [
@@ -1265,12 +1277,11 @@ function getRequiredChildGroups(pathname, rulesMap) {
1265
1277
  * into the host's `usePermission` probe, and renders one of three states:
1266
1278
  *
1267
1279
  * 1. while rules or probe are still loading → centered spinner card
1268
- * 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
1269
- * user roles for debugging
1280
+ * 2. probe says no access → centered "ไม่มีสิทธิ์" card
1270
1281
  * 3. otherwise → children
1271
1282
  *
1272
- * The host wraps `<PermissionProvider>` around its page chrome (typically
1273
- * via `AppLayoutShell`'s `permissionProvider` slot).
1283
+ * Role codes are intentionally not shown on the deny screen (they are
1284
+ * internal identifiers like `fin_controller` and confuse end users).
1274
1285
  */
1275
1286
  function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1276
1287
  const pathname = usePathname();
@@ -1284,7 +1295,7 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1284
1295
  const requiredRoles = useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
1285
1296
  // Hook must run unconditionally (rules of hooks); we just ignore the
1286
1297
  // result on the fast path below.
1287
- const { hasPermission, isLoading, userRoles } = usePermission({
1298
+ const { hasPermission, isLoading } = usePermission({
1288
1299
  requiredRoles,
1289
1300
  });
1290
1301
  // Fast path — no rule covers this pathname, anyone can see it. Skip the
@@ -1305,13 +1316,83 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1305
1316
  return null;
1306
1317
  }
1307
1318
  if (!hasPermission) {
1308
- return (jsx(Card, { className: "p-6", children: jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxs("div", { className: "space-y-1", children: [jsx("h3", { className: "text-lg font-semibold text-destructive", children: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49" }), requiredRoles.length > 0 && (jsxs("div", { className: "mt-3 space-y-1", children: [jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E17\u0E35\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23:" }), jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: requiredRoles.map((role, idx) => (jsx("span", { className: "rounded bg-muted px-2 py-0.5 text-xs font-mono", children: role }, idx))) })] })), userRoles.length > 0 && (jsxs("div", { className: "mt-3 space-y-1", children: [jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13:" }), jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: userRoles.map((role, idx) => (jsx("span", { className: "rounded bg-primary/10 px-2 py-0.5 text-xs font-mono text-primary", children: role }, idx))) })] }))] })] }) }));
1319
+ return (jsx(Card, { className: "p-6", children: jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxs("div", { className: "space-y-1", children: [jsx("h3", { className: "text-lg font-semibold text-destructive", children: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49 \u0E2B\u0E32\u0E01\u0E04\u0E34\u0E14\u0E27\u0E48\u0E32\u0E04\u0E27\u0E23\u0E40\u0E02\u0E49\u0E32\u0E16\u0E36\u0E07\u0E44\u0E14\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E15\u0E34\u0E14\u0E15\u0E48\u0E2D\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E23\u0E30\u0E1A\u0E1A\u0E02\u0E2D\u0E07\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] }) }));
1309
1320
  }
1310
1321
  return jsx(Fragment$1, { children: children });
1311
1322
  }
1312
1323
 
1324
+ /** Cookie name for the cross-subdomain theme choice. */
1325
+ const THEME_COOKIE = "erp-theme";
1326
+ /**
1327
+ * Parent domain to scope the cookie to, so every `*.antniti.com` app reads the
1328
+ * same value. Returns `null` on localhost / bare IPs (host-only cookie).
1329
+ */
1330
+ function sharedCookieDomain() {
1331
+ if (typeof window === "undefined")
1332
+ return null;
1333
+ const host = window.location.hostname;
1334
+ if (host === "localhost" || /^[0-9.]+$/.test(host))
1335
+ return null;
1336
+ const parts = host.split(".");
1337
+ if (parts.length < 2)
1338
+ return null;
1339
+ return "." + parts.slice(-2).join(".");
1340
+ }
1341
+ function readThemeCookie() {
1342
+ if (typeof document === "undefined")
1343
+ return null;
1344
+ const match = document.cookie.match(new RegExp("(?:^|; )" + THEME_COOKIE + "=([^;]*)"));
1345
+ return match ? decodeURIComponent(match[1]) : null;
1346
+ }
1347
+ function writeThemeCookie(value) {
1348
+ if (typeof document === "undefined")
1349
+ return;
1350
+ const domain = sharedCookieDomain();
1351
+ const parts = [
1352
+ `${THEME_COOKIE}=${encodeURIComponent(value)}`,
1353
+ "path=/",
1354
+ "max-age=31536000", // 1 year
1355
+ "SameSite=Lax",
1356
+ ];
1357
+ if (domain)
1358
+ parts.push(`domain=${domain}`);
1359
+ document.cookie = parts.join("; ");
1360
+ }
1361
+ /**
1362
+ * Bridges next-themes (per-origin localStorage) with a cookie scoped to the
1363
+ * parent domain, so the light/dark/system choice is shared across the
1364
+ * accounting / HR / platform subdomains. On load it adopts the shared cookie;
1365
+ * on every later change it writes the cookie back.
1366
+ */
1367
+ function ThemeCookieSync() {
1368
+ const { theme, setTheme } = useTheme();
1369
+ const skipFirstWrite = React.useRef(true);
1370
+ React.useEffect(() => {
1371
+ const cookie = readThemeCookie();
1372
+ if (cookie) {
1373
+ if (cookie !== theme)
1374
+ setTheme(cookie);
1375
+ }
1376
+ else if (theme) {
1377
+ writeThemeCookie(theme); // seed the shared cookie the first time
1378
+ }
1379
+ // Run once on mount — `theme` is intentionally not a dependency here.
1380
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1381
+ }, []);
1382
+ React.useEffect(() => {
1383
+ // Skip the initial render; only persist genuine changes (incl. the
1384
+ // setTheme() triggered by adopting the cookie above — harmless re-write).
1385
+ if (skipFirstWrite.current) {
1386
+ skipFirstWrite.current = false;
1387
+ return;
1388
+ }
1389
+ if (theme)
1390
+ writeThemeCookie(theme);
1391
+ }, [theme]);
1392
+ return null;
1393
+ }
1313
1394
  function ThemeProvider({ children, ...props }) {
1314
- return jsx(ThemeProvider$1, { ...props, children: children });
1395
+ return (jsxs(ThemeProvider$1, { ...props, children: [jsx(ThemeCookieSync, {}), children] }));
1315
1396
  }
1316
1397
 
1317
1398
  const defaultFlags = {