@eintrek/erp-shell 0.1.21 → 0.1.22

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
 
package/dist/index.esm.js CHANGED
@@ -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;
@@ -840,7 +847,7 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
840
847
  { code: "zh", label: "中文" },
841
848
  { code: "ja", label: "日本語" },
842
849
  ].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" &&
850
+ 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
851
  createPortal(dropdownContent, document.body)] }) }));
845
852
  }
846
853
 
@@ -919,6 +926,17 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, })
919
926
  }) }) }));
920
927
  }
921
928
 
929
+ function OrgMark({ logoUrl, size = "md", }) {
930
+ const [failed, setFailed] = useState(false);
931
+ const box = size === "sm"
932
+ ? "flex size-6 items-center justify-center rounded-md border overflow-hidden bg-background"
933
+ : "flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground overflow-hidden";
934
+ const icon = size === "sm" ? "size-3.5 shrink-0" : "size-4";
935
+ if (logoUrl && !failed) {
936
+ 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) }) }));
937
+ }
938
+ return (jsx("div", { className: box, children: jsx(Building2, { className: icon }) }));
939
+ }
922
940
  function OrganizationSwitcher({ activeOrganization, isContextLoading = false, organizations, isLoadingOrgs = false, setOrganizationCookie, registerOrganizationHref = "/register-organization", className, }) {
923
941
  const { isMobile } = useSidebarToggle();
924
942
  const router = useRouter();
@@ -964,10 +982,10 @@ function OrganizationSwitcher({ activeOrganization, isContextLoading = false, or
964
982
  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
983
  }
966
984
  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 || "ไม่มีองค์กร" })] }) }) }));
985
+ 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
986
  }
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" })] })] })] }) }) }));
987
+ 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) &&
988
+ "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
989
  }
972
990
 
973
991
  function SidebarHeader({ children, className }) {
@@ -1419,7 +1437,9 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
1419
1437
  if (mapped) {
1420
1438
  const existing = queryClient.getQueryData(queryKey);
1421
1439
  if (!existing) {
1422
- queryClient.setQueryData(queryKey, mapped);
1440
+ // updatedAt: 0 → immediately stale so observers still GET /myprofile
1441
+ // for fields the JWT seed omits (avatar/signature paths).
1442
+ queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
1423
1443
  }
1424
1444
  else {
1425
1445
  // Only overwrite when the mapped id changed — we don't know T's shape
@@ -1427,7 +1447,7 @@ function SessionBackedProfileCacheSeeder({ queryKey, mapper, }) {
1427
1447
  const existingId = existing?.id;
1428
1448
  const mappedId = mapped?.id;
1429
1449
  if (existingId !== mappedId) {
1430
- queryClient.setQueryData(queryKey, mapped);
1450
+ queryClient.setQueryData(queryKey, mapped, { updatedAt: 0 });
1431
1451
  }
1432
1452
  }
1433
1453
  }