@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.
package/dist/index.js CHANGED
@@ -759,7 +759,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
759
759
  })] }) }));
760
760
  }
761
761
 
762
- function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", } = {}) {
762
+ function isExternalHref(href) {
763
+ return /^https?:\/\//i.test(href) || href.startsWith("//");
764
+ }
765
+ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, } = {}) {
763
766
  const { data: session, status } = react.useSession();
764
767
  const router = navigation.useRouter();
765
768
  const { theme, setTheme } = nextThemes.useTheme();
@@ -811,6 +814,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
811
814
  if (profileHref)
812
815
  router.push(profileHref);
813
816
  };
817
+ const handleSystemLink = (href) => {
818
+ setIsOpen(false);
819
+ if (isExternalHref(href)) {
820
+ window.location.assign(href);
821
+ return;
822
+ }
823
+ router.push(href);
824
+ };
814
825
  const handleThemeChange = (newTheme) => {
815
826
  setTheme(newTheme);
816
827
  setShowThemeMenu(false);
@@ -843,6 +854,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
843
854
  }
844
855
  if (!user)
845
856
  return null;
857
+ const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
846
858
  const dropdownContent = isOpen ? (jsxRuntime.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 && (jsxRuntime.jsx("div", { className: "p-4 border-b border-sidebar-border bg-sidebar-accent/30", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [organization.logoUrl ? (
847
859
  // eslint-disable-next-line @next/next/no-img-element
848
860
  jsxRuntime.jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
@@ -850,7 +862,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
850
862
  // sidebar; drop back to the generic icon's empty slot.
851
863
  onError: e => {
852
864
  e.currentTarget.style.display = "none";
853
- } })) : (jsxRuntime.jsx(lucideReact.Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsxRuntime.jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxRuntime.jsxs("div", { className: "p-2", children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { onClick: () => {
865
+ } })) : (jsxRuntime.jsx(lucideReact.Building2, { className: "h-5 w-5 text-sidebar-foreground/70 flex-shrink-0" })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-sm sm:truncate", children: organization.name }), organization.description && (jsxRuntime.jsx("div", { className: "text-xs text-sidebar-foreground/70 truncate mt-0.5", children: organization.description }))] })] }) })), jsxRuntime.jsxs("div", { className: "p-2", children: [visibleSystemLinks.length > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [visibleSystemLinks.map(link => (jsxRuntime.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 ?? jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: link.label })] }, `${link.label}:${link.href}`))), jsxRuntime.jsx("div", { className: "my-1 border-t border-sidebar-border" })] })) : null, jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { onClick: () => {
854
866
  setShowThemeMenu(!showThemeMenu);
855
867
  setShowLanguageMenu(false);
856
868
  }, 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: [jsxRuntime.jsx(lucideReact.Palette, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E18\u0E35\u0E21" }), jsxRuntime.jsx(lucideReact.ChevronUp, { className: `h-4 w-4 transition-transform ${showThemeMenu ? "rotate-180" : ""}` })] }), showThemeMenu && (jsxRuntime.jsx("div", { className: "mt-1 ml-4 space-y-1", children: [
@@ -1284,12 +1296,11 @@ function getRequiredChildGroups(pathname, rulesMap) {
1284
1296
  * into the host's `usePermission` probe, and renders one of three states:
1285
1297
  *
1286
1298
  * 1. while rules or probe are still loading → centered spinner card
1287
- * 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
1288
- * user roles for debugging
1299
+ * 2. probe says no access → centered "ไม่มีสิทธิ์" card
1289
1300
  * 3. otherwise → children
1290
1301
  *
1291
- * The host wraps `<PermissionProvider>` around its page chrome (typically
1292
- * via `AppLayoutShell`'s `permissionProvider` slot).
1302
+ * Role codes are intentionally not shown on the deny screen (they are
1303
+ * internal identifiers like `fin_controller` and confuse end users).
1293
1304
  */
1294
1305
  function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1295
1306
  const pathname = navigation.usePathname();
@@ -1303,7 +1314,7 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1303
1314
  const requiredRoles = React.useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
1304
1315
  // Hook must run unconditionally (rules of hooks); we just ignore the
1305
1316
  // result on the fast path below.
1306
- const { hasPermission, isLoading, userRoles } = usePermission({
1317
+ const { hasPermission, isLoading } = usePermission({
1307
1318
  requiredRoles,
1308
1319
  });
1309
1320
  // Fast path — no rule covers this pathname, anyone can see it. Skip the
@@ -1324,13 +1335,83 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1324
1335
  return null;
1325
1336
  }
1326
1337
  if (!hasPermission) {
1327
- return (jsxRuntime.jsx(erpTheme.Card, { className: "p-6", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxRuntime.jsxs("div", { className: "space-y-1", children: [jsxRuntime.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" }), jsxRuntime.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 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.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:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: requiredRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-muted px-2 py-0.5 text-xs font-mono", children: role }, idx))) })] })), userRoles.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-3 space-y-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 justify-center", children: userRoles.map((role, idx) => (jsxRuntime.jsx("span", { className: "rounded bg-primary/10 px-2 py-0.5 text-xs font-mono text-primary", children: role }, idx))) })] }))] })] }) }));
1338
+ return (jsxRuntime.jsx(erpTheme.Card, { className: "p-6", children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-3 text-center", children: [jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-8 w-8 text-destructive" }), jsxRuntime.jsxs("div", { className: "space-y-1", children: [jsxRuntime.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" }), jsxRuntime.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" })] })] }) }));
1328
1339
  }
1329
1340
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
1330
1341
  }
1331
1342
 
1343
+ /** Cookie name for the cross-subdomain theme choice. */
1344
+ const THEME_COOKIE = "erp-theme";
1345
+ /**
1346
+ * Parent domain to scope the cookie to, so every `*.antniti.com` app reads the
1347
+ * same value. Returns `null` on localhost / bare IPs (host-only cookie).
1348
+ */
1349
+ function sharedCookieDomain() {
1350
+ if (typeof window === "undefined")
1351
+ return null;
1352
+ const host = window.location.hostname;
1353
+ if (host === "localhost" || /^[0-9.]+$/.test(host))
1354
+ return null;
1355
+ const parts = host.split(".");
1356
+ if (parts.length < 2)
1357
+ return null;
1358
+ return "." + parts.slice(-2).join(".");
1359
+ }
1360
+ function readThemeCookie() {
1361
+ if (typeof document === "undefined")
1362
+ return null;
1363
+ const match = document.cookie.match(new RegExp("(?:^|; )" + THEME_COOKIE + "=([^;]*)"));
1364
+ return match ? decodeURIComponent(match[1]) : null;
1365
+ }
1366
+ function writeThemeCookie(value) {
1367
+ if (typeof document === "undefined")
1368
+ return;
1369
+ const domain = sharedCookieDomain();
1370
+ const parts = [
1371
+ `${THEME_COOKIE}=${encodeURIComponent(value)}`,
1372
+ "path=/",
1373
+ "max-age=31536000", // 1 year
1374
+ "SameSite=Lax",
1375
+ ];
1376
+ if (domain)
1377
+ parts.push(`domain=${domain}`);
1378
+ document.cookie = parts.join("; ");
1379
+ }
1380
+ /**
1381
+ * Bridges next-themes (per-origin localStorage) with a cookie scoped to the
1382
+ * parent domain, so the light/dark/system choice is shared across the
1383
+ * accounting / HR / platform subdomains. On load it adopts the shared cookie;
1384
+ * on every later change it writes the cookie back.
1385
+ */
1386
+ function ThemeCookieSync() {
1387
+ const { theme, setTheme } = nextThemes.useTheme();
1388
+ const skipFirstWrite = React__namespace.useRef(true);
1389
+ React__namespace.useEffect(() => {
1390
+ const cookie = readThemeCookie();
1391
+ if (cookie) {
1392
+ if (cookie !== theme)
1393
+ setTheme(cookie);
1394
+ }
1395
+ else if (theme) {
1396
+ writeThemeCookie(theme); // seed the shared cookie the first time
1397
+ }
1398
+ // Run once on mount — `theme` is intentionally not a dependency here.
1399
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1400
+ }, []);
1401
+ React__namespace.useEffect(() => {
1402
+ // Skip the initial render; only persist genuine changes (incl. the
1403
+ // setTheme() triggered by adopting the cookie above — harmless re-write).
1404
+ if (skipFirstWrite.current) {
1405
+ skipFirstWrite.current = false;
1406
+ return;
1407
+ }
1408
+ if (theme)
1409
+ writeThemeCookie(theme);
1410
+ }, [theme]);
1411
+ return null;
1412
+ }
1332
1413
  function ThemeProvider({ children, ...props }) {
1333
- return jsxRuntime.jsx(nextThemes.ThemeProvider, { ...props, children: children });
1414
+ return (jsxRuntime.jsxs(nextThemes.ThemeProvider, { ...props, children: [jsxRuntime.jsx(ThemeCookieSync, {}), children] }));
1334
1415
  }
1335
1416
 
1336
1417
  const defaultFlags = {