@eintrek/erp-shell 0.1.21 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @eintrek/erp-shell
2
2
 
3
- Shared frontend platform for ERP apps (erp-accounting, erp-hr, …).
3
+ Shared frontend platform for ERP apps (erp-accounting, erp-hr, erp-platform, …).
4
4
 
5
5
  Holds the cross-app glue that was previously duplicated in every consumer:
6
6
  HTTP client, auth/token plumbing, organization-id context, app chrome
@@ -81,7 +81,8 @@ Shell now owns the **shapes + helpers** for navigation; apps own the data.
81
81
  slot for app narrowing), `Tool`, `Company`, `NavigationData`.
82
82
  - Helpers: `canAccessNavPermissionRoute`, `filterNavigationToolsByPermission`,
83
83
  `filterNavigationModulesByPermission`, `getActiveModule(pathname, modules)`,
84
- `getActiveSubModule(pathname, modules)`, `buildUrlWithCode(code, url)`.
84
+ `getActiveSubModule(pathname, modules)`, `buildUrlWithCode(code, url)`
85
+ (absolute `http(s)://` URLs pass through; `{code}` placeholder is replaced).
85
86
  - Components: `NavTools` (reads Tool[] + buildUrlWithCode internally),
86
87
  `AppBreadcrumb` (accepts `labels` prop so per-app Thai overrides stay local).
87
88
 
@@ -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';
@@ -692,8 +692,15 @@ function getActiveSubModule(pathname, modules) {
692
692
  }
693
693
  return null;
694
694
  }
695
- /** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`. */
695
+ /** Prepend `code` (org code) to a relative URL so `/sales` → `/O25.../sales`.
696
+ * Absolute `http(s)://` URLs are left intact; optional `{code}` placeholder is replaced.
697
+ */
696
698
  function buildUrlWithCode(code, url) {
699
+ if (/^https?:\/\//i.test(url)) {
700
+ if (!code)
701
+ return url.split("{code}").join("");
702
+ return url.split("{code}").join(code);
703
+ }
697
704
  if (!code)
698
705
  return url;
699
706
  const cleanUrl = url.startsWith("/") ? url.slice(1) : url;
@@ -733,7 +740,10 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
733
740
  })] }) }));
734
741
  }
735
742
 
736
- 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, } = {}) {
737
747
  const { data: session, status } = useSession();
738
748
  const router = useRouter();
739
749
  const { theme, setTheme } = useTheme();
@@ -785,6 +795,14 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
785
795
  if (profileHref)
786
796
  router.push(profileHref);
787
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
+ };
788
806
  const handleThemeChange = (newTheme) => {
789
807
  setTheme(newTheme);
790
808
  setShowThemeMenu(false);
@@ -817,6 +835,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
817
835
  }
818
836
  if (!user)
819
837
  return null;
838
+ const visibleSystemLinks = (systemLinks ?? []).filter(link => link.href && link.label);
820
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 ? (
821
840
  // eslint-disable-next-line @next/next/no-img-element
822
841
  jsx("img", { src: organization.logoUrl, alt: organization.name ?? "โลโก้บริษัท", className: "h-8 w-8 flex-shrink-0 rounded object-contain bg-white",
@@ -824,7 +843,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
824
843
  // sidebar; drop back to the generic icon's empty slot.
825
844
  onError: e => {
826
845
  e.currentTarget.style.display = "none";
827
- } })) : (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: () => {
828
847
  setShowThemeMenu(!showThemeMenu);
829
848
  setShowLanguageMenu(false);
830
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: [
@@ -840,7 +859,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
840
859
  { code: "zh", label: "中文" },
841
860
  { code: "ja", label: "日本語" },
842
861
  ].map(({ code, label }) => (jsxs("button", { onClick: () => handleLanguageChange(code), className: "flex w-full items-center gap-3 px-3 py-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsx("span", { className: "flex-1 text-left", children: label }), organization?.language === code && (jsx("div", { className: "h-2 w-2 rounded-full bg-primary" }))] }, code))) }))] }), profileHref && (jsxs("button", { onClick: handleProfile, 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(User, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E2A\u0E48\u0E27\u0E19\u0E15\u0E31\u0E27" })] })), jsxs("button", { onClick: handleSelectOrganization, 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(Building2, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E18\u0E38\u0E23\u0E01\u0E34\u0E08" })] }), jsx("button", { onClick: handleSignOut, disabled: isSigningOut, 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 disabled:opacity-50 disabled:cursor-not-allowed mt-1", children: isSigningOut ? (jsxs(Fragment$1, { children: [jsx(Loader2, { className: "h-4 w-4 animate-spin" }), jsx("span", { className: "flex-1 text-left", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A..." })] })) : (jsxs(Fragment$1, { children: [jsx(LogOut, { className: "h-4 w-4" }), jsx("span", { className: "flex-1 text-left", children: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A" })] })) })] })] })) : null;
843
- return (jsx("div", { className: "p-2", children: jsxs("div", { className: "relative", children: [jsxs("button", { ref: buttonRef, onClick: () => setIsOpen(!isOpen), className: "flex w-full items-center gap-2 p-2 rounded-md transition-colors", children: [jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [jsx(AvatarImage, { src: user.avatar, alt: user.name }), jsx(AvatarFallback, { className: "rounded-lg bg-[#E4525F] text-sidebar-primary-foreground w-8 h-8", children: getInitials(user.name) })] }), jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [jsx("span", { className: "truncate font-medium", title: user.name, children: user.name }), jsx("span", { className: "truncate text-xs text-sidebar-foreground/70", title: user.email, children: user.email })] }), jsx(ChevronUp, { className: `ml-auto size-4 transition-transform ${isOpen ? "rotate-180" : ""}` })] }), typeof window !== "undefined" &&
862
+ return (jsx("div", { className: "p-2", children: jsxs("div", { className: "relative", children: [jsxs("button", { ref: buttonRef, onClick: () => setIsOpen(!isOpen), className: "flex w-full items-center gap-2 p-2 rounded-md transition-colors", children: [jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [user.avatar ? (jsx(AvatarImage, { src: user.avatar, alt: user.name })) : null, jsx(AvatarFallback, { className: "rounded-lg bg-[#E4525F] text-sidebar-primary-foreground w-8 h-8", children: getInitials(user.name) })] }), jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [jsx("span", { className: "truncate font-medium", title: user.name, children: user.name }), jsx("span", { className: "truncate text-xs text-sidebar-foreground/70", title: user.email, children: user.email })] }), jsx(ChevronUp, { className: `ml-auto size-4 transition-transform ${isOpen ? "rotate-180" : ""}` })] }), typeof window !== "undefined" &&
844
863
  createPortal(dropdownContent, document.body)] }) }));
845
864
  }
846
865
 
@@ -919,6 +938,17 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, })
919
938
  }) }) }));
920
939
  }
921
940
 
941
+ function OrgMark({ logoUrl, size = "md", }) {
942
+ const [failed, setFailed] = useState(false);
943
+ const box = size === "sm"
944
+ ? "flex size-6 items-center justify-center rounded-md border overflow-hidden bg-background"
945
+ : "flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground overflow-hidden";
946
+ const icon = size === "sm" ? "size-3.5 shrink-0" : "size-4";
947
+ if (logoUrl && !failed) {
948
+ return (jsx("div", { className: cn(box, size === "md" && "bg-white"), children: jsx("img", { src: logoUrl, alt: "", className: "size-full object-contain p-0.5", onError: () => setFailed(true) }) }));
949
+ }
950
+ return (jsx("div", { className: box, children: jsx(Building2, { className: icon }) }));
951
+ }
922
952
  function OrganizationSwitcher({ activeOrganization, isContextLoading = false, organizations, isLoadingOrgs = false, setOrganizationCookie, registerOrganizationHref = "/register-organization", className, }) {
923
953
  const { isMobile } = useSidebarToggle();
924
954
  const router = useRouter();
@@ -964,10 +994,10 @@ function OrganizationSwitcher({ activeOrganization, isContextLoading = false, or
964
994
  return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx(Skeleton, { className: "size-8 rounded-lg" }), jsx(Skeleton, { className: "h-4 w-24" })] }) }) }));
965
995
  }
966
996
  if (organizations.length <= 1) {
967
- return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground", children: jsx(Building2, { className: "size-4" }) }), jsx("span", { className: "truncate font-semibold text-sm min-w-0 flex-1", children: activeOrganization?.name || "ไม่มีองค์กร" })] }) }) }));
997
+ return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsx(OrgMark, { logoUrl: activeOrganization?.logoUrl }), jsx("span", { className: "truncate font-semibold text-sm min-w-0 flex-1", children: activeOrganization?.name || "ไม่มีองค์กร" })] }) }) }));
968
998
  }
969
- return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [jsx("div", { className: "flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground", children: jsx(Building2, { className: "size-4" }) }), jsx("span", { className: "truncate font-semibold text-sm flex-1 min-w-0 text-left", children: activeOrganization?.name || "เลือกองค์กร" }), isPending ? (jsx(Loader2, { className: "ml-auto size-4 animate-spin" })) : (jsx(ChevronsUpDown, { className: "ml-auto size-4" }))] }) }), jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" }), organizations.map(org => (jsxs(DropdownMenuItem, { onClick: () => handleOrganizationChange(org), disabled: isPending, className: cn("gap-2 p-2 cursor-pointer", String(activeOrganization?.id) === String(org.id) &&
970
- "bg-accent"), children: [jsx("div", { className: "flex size-6 items-center justify-center rounded-md border", children: jsx(Building2, { className: "size-3.5 shrink-0" }) }), jsx("span", { className: "truncate", children: org.name })] }, org.id))), jsx(DropdownMenuSeparator, {}), jsxs(DropdownMenuItem, { className: "gap-2 p-2 cursor-pointer", onClick: () => router.push(registerOrganizationHref), children: [jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-transparent", children: jsx(Plus, { className: "size-4" }) }), jsx("span", { className: "text-muted-foreground font-medium", children: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] })] }) }) }));
999
+ return (jsx(SidebarMenu, { className: className, children: jsx(SidebarMenuItem, { children: jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [jsx(OrgMark, { logoUrl: activeOrganization?.logoUrl }), jsx("span", { className: "truncate font-semibold text-sm flex-1 min-w-0 text-left", children: activeOrganization?.name || "เลือกองค์กร" }), isPending ? (jsx(Loader2, { className: "ml-auto size-4 animate-spin" })) : (jsx(ChevronsUpDown, { className: "ml-auto size-4" }))] }) }), jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" }), organizations.map(org => (jsxs(DropdownMenuItem, { onClick: () => handleOrganizationChange(org), disabled: isPending, className: cn("gap-2 p-2 cursor-pointer", String(activeOrganization?.id) === String(org.id) &&
1000
+ "bg-accent"), children: [jsx(OrgMark, { logoUrl: org.logoUrl, size: "sm" }), jsx("span", { className: "truncate", children: org.name })] }, org.id))), jsx(DropdownMenuSeparator, {}), jsxs(DropdownMenuItem, { className: "gap-2 p-2 cursor-pointer", onClick: () => router.push(registerOrganizationHref), children: [jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-transparent", children: jsx(Plus, { className: "size-4" }) }), jsx("span", { className: "text-muted-foreground font-medium", children: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] })] }) }) }));
971
1001
  }
972
1002
 
973
1003
  function SidebarHeader({ children, className }) {
@@ -1247,12 +1277,11 @@ function getRequiredChildGroups(pathname, rulesMap) {
1247
1277
  * into the host's `usePermission` probe, and renders one of three states:
1248
1278
  *
1249
1279
  * 1. while rules or probe are still loading → centered spinner card
1250
- * 2. probe says no access → centered "ไม่มีสิทธิ์" card with required vs.
1251
- * user roles for debugging
1280
+ * 2. probe says no access → centered "ไม่มีสิทธิ์" card
1252
1281
  * 3. otherwise → children
1253
1282
  *
1254
- * The host wraps `<PermissionProvider>` around its page chrome (typically
1255
- * 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).
1256
1285
  */
1257
1286
  function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1258
1287
  const pathname = usePathname();
@@ -1266,7 +1295,7 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1266
1295
  const requiredRoles = useMemo(() => getRequiredChildGroups(pathname, mergedMap), [pathname, mergedMap]);
1267
1296
  // Hook must run unconditionally (rules of hooks); we just ignore the
1268
1297
  // result on the fast path below.
1269
- const { hasPermission, isLoading, userRoles } = usePermission({
1298
+ const { hasPermission, isLoading } = usePermission({
1270
1299
  requiredRoles,
1271
1300
  });
1272
1301
  // Fast path — no rule covers this pathname, anyone can see it. Skip the
@@ -1287,13 +1316,83 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1287
1316
  return null;
1288
1317
  }
1289
1318
  if (!hasPermission) {
1290
- 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" })] })] }) }));
1291
1320
  }
1292
1321
  return jsx(Fragment$1, { children: children });
1293
1322
  }
1294
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
+ }
1295
1394
  function ThemeProvider({ children, ...props }) {
1296
- return jsx(ThemeProvider$1, { ...props, children: children });
1395
+ return (jsxs(ThemeProvider$1, { ...props, children: [jsx(ThemeCookieSync, {}), children] }));
1297
1396
  }
1298
1397
 
1299
1398
  const defaultFlags = {
@@ -1419,7 +1518,9 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
1419
1518
  if (mapped) {
1420
1519
  const existing = queryClient.getQueryData(queryKey);
1421
1520
  if (!existing) {
1422
- queryClient.setQueryData(queryKey, mapped);
1521
+ // updatedAt: 0 → immediately stale so observers still GET /myprofile
1522
+ // for fields the JWT seed omits (avatar/signature paths).
1523
+ queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
1423
1524
  }
1424
1525
  else {
1425
1526
  // Only overwrite when the mapped id changed — we don't know T's shape
@@ -1427,7 +1528,7 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
1427
1528
  const existingId = existing?.id;
1428
1529
  const mappedId = mapped?.id;
1429
1530
  if (existingId !== mappedId) {
1430
- queryClient.setQueryData(queryKey, mapped);
1531
+ queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
1431
1532
  }
1432
1533
  }
1433
1534
  }