@eintrek/erp-shell 0.1.38 → 0.1.40

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.d.ts CHANGED
@@ -14,6 +14,8 @@ export { TapMenu } from "./navigation/tap-menu";
14
14
  export { NavTools } from "./navigation/nav-tools";
15
15
  export { AppBreadcrumb } from "./navigation/app-breadcrumb";
16
16
  export { NavUser, type NavUserProps, type NavUserSystemLink } from "./navigation/nav-user";
17
+ export { buildSystemLinks, type BuildSystemLinksOptions, type ErpAppId, } from "./navigation/app-registry";
18
+ export { PLAN_FEATURES, hasPlanFeature, type PlanFeatureKey, } from "./lib/plan-features";
17
19
  export { NavMain, type NavMainProps } from "./navigation/nav-main";
18
20
  export { OrganizationSwitcher, type OrganizationSwitcherProps, type OrganizationLike, } from "./navigation/organization-switcher";
19
21
  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, LayoutGrid, ExternalLink, Loader2, LogOut, ChevronDown, ChevronsUpDown, Plus, Ban, Home, Headset, ContactRound, UserPlus } from 'lucide-react';
8
+ import { PanelLeftClose, Menu, ChevronRight, Building2, Palette, ChevronUp, Sun, Moon, Monitor, Globe, User, LayoutGrid, ExternalLink, Loader2, LogOut, Calculator, Warehouse, FolderKanban, Users, ChevronDown, ChevronsUpDown, Plus, Ban, Home, Headset, ContactRound, UserPlus } 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';
@@ -764,13 +764,27 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
764
764
  if (segments.length === 0) {
765
765
  return jsx("h1", { className: "text-xl font-semibold", children: homeLabel });
766
766
  }
767
- return (jsx(Breadcrumb, { children: jsxs(BreadcrumbList, { children: [jsx(BreadcrumbItem, { className: "hidden md:block", children: jsx(BreadcrumbLink, { asChild: true, children: jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors", children: rootLabel }) }) }), segments.map((segment, index) => {
767
+ return (jsx(Breadcrumb, { children: jsxs(BreadcrumbList, { children: [jsx(BreadcrumbItem, { children: jsx(BreadcrumbLink, { asChild: true, children: jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors", children: rootLabel }) }) }), segments.map((segment, index) => {
768
768
  const href = "/" + segments.slice(0, index + 1).join("/");
769
769
  const isLast = index === segments.length - 1;
770
- const label = labels[segment] || segment;
771
- return (jsxs(Fragment, { children: [jsx(BreadcrumbSeparator, { className: "hidden md:block text-white/80", children: jsx(ChevronRight, { strokeWidth: 2, className: "h-4 w-4" }) }), jsx(BreadcrumbItem, { children: isLast ? (jsx(BreadcrumbPage, { className: "font-thai text-[14px] font-normal text-white/95 leading-none", children: decodeURIComponent(label) })) : (jsx(BreadcrumbLink, { asChild: true, children: jsx(Link, { href: href, className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors leading-none", children: decodeURIComponent(label) }) })) })] }, `${segment}-${index}`));
770
+ // A record id is not a place. Falling back to the raw segment put a
771
+ // 36-character uuid in the trail, which pushed everything before it
772
+ // off a phone and told the reader nothing they could use.
773
+ const label = labels[segment] ?? (isRecordID(segment) ? "รายละเอียด" : segment);
774
+ return (jsxs(Fragment, { children: [jsx(BreadcrumbSeparator, { className: "text-white/80", children: jsx(ChevronRight, { strokeWidth: 2, className: "h-4 w-4" }) }), jsx(BreadcrumbItem, { children: isLast ? (jsx(BreadcrumbPage, { className: "font-thai text-[14px] font-normal text-white/95 leading-none", children: decodeURIComponent(label) })) : (jsx(BreadcrumbLink, { asChild: true, children: jsx(Link, { href: href, className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors leading-none", children: decodeURIComponent(label) }) })) })] }, `${segment}-${index}`));
772
775
  })] }) }));
773
776
  }
777
+ /**
778
+ * Whether a path segment is a record identifier rather than a page.
779
+ *
780
+ * Covers the uuid the APIs issue and the long opaque ids that show up in
781
+ * document routes. Deliberately narrow: a real page called "settings" must
782
+ * never be mistaken for one of these.
783
+ */
784
+ function isRecordID(segment) {
785
+ return (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment) ||
786
+ /^[0-9a-f]{24,}$/i.test(segment));
787
+ }
774
788
 
775
789
  function isExternalHref(href) {
776
790
  return /^https?:\/\//i.test(href) || href.startsWith("//");
@@ -902,6 +916,115 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
902
916
  createPortal(dropdownContent, document.body)] }) }));
903
917
  }
904
918
 
919
+ /**
920
+ * Plan feature keys (billable entitlements). Mirror of platform-apis plan.go.
921
+ *
922
+ * This lives in the shell because every app gates on the same keys, and five
923
+ * private copies of the same list drift: the app-switcher list did exactly
924
+ * that before it moved here.
925
+ */
926
+ const PLAN_FEATURES = {
927
+ ORG_CHART: "org_chart",
928
+ APPROVAL_WORKFLOW: "approval_workflow",
929
+ API_ACCESS: "api_access",
930
+ SSO: "sso",
931
+ BASIC_ACCOUNTING_HR: "basic_accounting_hr",
932
+ MODULE_PLATFORM: "module.platform",
933
+ MODULE_HR: "module.hr",
934
+ MODULE_ACCOUNTING: "module.accounting",
935
+ MODULE_SALES: "module.sales",
936
+ MODULE_PURCHASING: "module.purchasing",
937
+ MODULE_INVENTORY: "module.inventory",
938
+ MODULE_PROJECT: "module.project",
939
+ MODULE_SECRETARIAT: "module.secretariat",
940
+ };
941
+ /** True when plan features include key (with legacy basic_accounting_hr alias). */
942
+ function hasPlanFeature(features, feature) {
943
+ if (!features?.length)
944
+ return false;
945
+ if (features.includes(feature))
946
+ return true;
947
+ if ((feature === PLAN_FEATURES.MODULE_HR ||
948
+ feature === PLAN_FEATURES.MODULE_ACCOUNTING) &&
949
+ features.includes(PLAN_FEATURES.BASIC_ACCOUNTING_HR)) {
950
+ return true;
951
+ }
952
+ if (feature === PLAN_FEATURES.BASIC_ACCOUNTING_HR &&
953
+ features.includes(PLAN_FEATURES.MODULE_HR) &&
954
+ features.includes(PLAN_FEATURES.MODULE_ACCOUNTING)) {
955
+ return true;
956
+ }
957
+ return false;
958
+ }
959
+
960
+ function normaliseBase(value, fallback) {
961
+ return (value ?? fallback).replace(/\/$/, "");
962
+ }
963
+ /**
964
+ * One list, in display order, for the whole suite.
965
+ *
966
+ * It used to be copied into every app's own nav-user.tsx, which is how Invenx
967
+ * shipped without a link to Plannex: five copies, one of them edited by hand.
968
+ * Adding the sixth app is now a change here plus a version bump.
969
+ */
970
+ const APPS = [
971
+ {
972
+ id: "platform",
973
+ label: "แพลตฟอร์ม",
974
+ icon: jsx(Building2, { className: "h-4 w-4 shrink-0" }),
975
+ feature: null,
976
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLATFORM_URL, "https://platform.antniti.com"),
977
+ },
978
+ {
979
+ id: "accounx",
980
+ label: "Accounx",
981
+ icon: jsx(Calculator, { className: "h-4 w-4 shrink-0" }),
982
+ feature: PLAN_FEATURES.MODULE_ACCOUNTING,
983
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_ACCOUNTING_URL, "https://accounx.antniti.com"),
984
+ },
985
+ {
986
+ id: "invenx",
987
+ label: "Invenx",
988
+ icon: jsx(Warehouse, { className: "h-4 w-4 shrink-0" }),
989
+ feature: PLAN_FEATURES.MODULE_INVENTORY,
990
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_INVENX_URL, "https://invenx.antniti.com"),
991
+ },
992
+ {
993
+ id: "plannex",
994
+ label: "Plannex",
995
+ icon: jsx(FolderKanban, { className: "h-4 w-4 shrink-0" }),
996
+ feature: PLAN_FEATURES.MODULE_PROJECT,
997
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLANNEX_URL, "https://plannex.antniti.com"),
998
+ },
999
+ {
1000
+ id: "peoplx",
1001
+ label: "Peoplx",
1002
+ icon: jsx(Users, { className: "h-4 w-4 shrink-0" }),
1003
+ feature: PLAN_FEATURES.MODULE_HR,
1004
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_HR_URL, "https://peoplx.antniti.com"),
1005
+ },
1006
+ ];
1007
+ /**
1008
+ * Builds the cross-app link list for <NavUser systemLinks>.
1009
+ *
1010
+ * The current app is always present even when the org's plan no longer covers
1011
+ * it — the user is looking at it, so hiding the link would only make the menu
1012
+ * disagree with the screen.
1013
+ */
1014
+ function buildSystemLinks({ orgCode, activeApp, features, }) {
1015
+ if (!orgCode)
1016
+ return [];
1017
+ return APPS.filter(app => app.id === activeApp ||
1018
+ app.feature === null ||
1019
+ hasPlanFeature(features, app.feature)).map(app => ({
1020
+ label: app.label,
1021
+ icon: app.icon,
1022
+ // Same-app link stays relative so it never leaves the current origin.
1023
+ href: app.id === activeApp ? `/${orgCode}` : `${app.baseUrl()}/${orgCode}`,
1024
+ ...(app.id === activeApp ? { active: true } : {}),
1025
+ }));
1026
+ }
1027
+
905
1028
  // Helper function to check if pathname matches a URL pattern
906
1029
  // This prevents false matches like /reports/sales matching /sales
907
1030
  // Handles paths with or without [code] prefix
@@ -1997,5 +2120,5 @@ const getAxiosErrorMessage = (error) => {
1997
2120
  return String(error);
1998
2121
  };
1999
2122
 
2000
- export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, EMPLOYEE_RECORD_OK_EVENT, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, EmployeeRequiredGate, FeatureFlagsProvider, HeaderBackground, NavMain, NavTools, NavUser, OrganizationSwitcher, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, instance as axiosInstance, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, decodeJWT, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, isLikelyTransientNetworkError, isPathActive, normalizePathnameForPermissionRules, resolveUiRoutePermission, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
2123
+ export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, EMPLOYEE_RECORD_OK_EVENT, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, EmployeeRequiredGate, FeatureFlagsProvider, HeaderBackground, NavMain, NavTools, NavUser, OrganizationSwitcher, PLAN_FEATURES, PermissionGateShell, PermissionProvider, PermissionRulesProvider, ReactQueryProvider, SessionBackedProfileCacheSeeder, SessionRefreshTrigger, SidebarContent, SidebarFooter, SidebarHeader, SidebarProvider, SidebarTrigger, TapMenu, ThemeProvider, instance as axiosInstance, buildSystemLinks, buildUrlWithCode, canAccessNavPermissionRoute, clearAccessTokenCache, clearOrganizationId, decodeJWT, filterNavigationModulesByPermission, filterNavigationToolsByPermission, getAccessToken, getActiveModule, getActiveSubModule, getAxiosErrorMessage, getOrganizationId, getQueryClientConfig, getRequiredChildGroups, hasPlanFeature, isLikelyTransientNetworkError, isPathActive, normalizePathnameForPermissionRules, resolveUiRoutePermission, runGlobalRequestPrecheck, runWithTransientRetry, setOrganizationId, toApiError, unwrapApiResponse, updateOrganizationId, useClearOrgOnUserChange, useFeatureFlag, useFeatureFlags, useInvalidateQueriesOnOrgChange, useKeycloakRoles, useOrganizationIdState, usePermissionRules, useSessionRefresh, useSidebar, useSidebarToggle };
2001
2124
  //# sourceMappingURL=index.esm.js.map