@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.js CHANGED
@@ -783,13 +783,27 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
783
783
  if (segments.length === 0) {
784
784
  return jsxRuntime.jsx("h1", { className: "text-xl font-semibold", children: homeLabel });
785
785
  }
786
- return (jsxRuntime.jsx(erpTheme.Breadcrumb, { children: jsxRuntime.jsxs(erpTheme.BreadcrumbList, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbItem, { className: "hidden md:block", children: jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors", children: rootLabel }) }) }), segments.map((segment, index) => {
786
+ return (jsxRuntime.jsx(erpTheme.Breadcrumb, { children: jsxRuntime.jsxs(erpTheme.BreadcrumbList, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-white/80 hover:text-white transition-colors", children: rootLabel }) }) }), segments.map((segment, index) => {
787
787
  const href = "/" + segments.slice(0, index + 1).join("/");
788
788
  const isLast = index === segments.length - 1;
789
- const label = labels[segment] || segment;
790
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbSeparator, { className: "hidden md:block text-white/80", children: jsxRuntime.jsx(lucideReact.ChevronRight, { strokeWidth: 2, className: "h-4 w-4" }) }), jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: isLast ? (jsxRuntime.jsx(erpTheme.BreadcrumbPage, { className: "font-thai text-[14px] font-normal text-white/95 leading-none", children: decodeURIComponent(label) })) : (jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.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}`));
789
+ // A record id is not a place. Falling back to the raw segment put a
790
+ // 36-character uuid in the trail, which pushed everything before it
791
+ // off a phone and told the reader nothing they could use.
792
+ const label = labels[segment] ?? (isRecordID(segment) ? "รายละเอียด" : segment);
793
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbSeparator, { className: "text-white/80", children: jsxRuntime.jsx(lucideReact.ChevronRight, { strokeWidth: 2, className: "h-4 w-4" }) }), jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: isLast ? (jsxRuntime.jsx(erpTheme.BreadcrumbPage, { className: "font-thai text-[14px] font-normal text-white/95 leading-none", children: decodeURIComponent(label) })) : (jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.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}`));
791
794
  })] }) }));
792
795
  }
796
+ /**
797
+ * Whether a path segment is a record identifier rather than a page.
798
+ *
799
+ * Covers the uuid the APIs issue and the long opaque ids that show up in
800
+ * document routes. Deliberately narrow: a real page called "settings" must
801
+ * never be mistaken for one of these.
802
+ */
803
+ function isRecordID(segment) {
804
+ return (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment) ||
805
+ /^[0-9a-f]{24,}$/i.test(segment));
806
+ }
793
807
 
794
808
  function isExternalHref(href) {
795
809
  return /^https?:\/\//i.test(href) || href.startsWith("//");
@@ -921,6 +935,115 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
921
935
  reactDom.createPortal(dropdownContent, document.body)] }) }));
922
936
  }
923
937
 
938
+ /**
939
+ * Plan feature keys (billable entitlements). Mirror of platform-apis plan.go.
940
+ *
941
+ * This lives in the shell because every app gates on the same keys, and five
942
+ * private copies of the same list drift: the app-switcher list did exactly
943
+ * that before it moved here.
944
+ */
945
+ const PLAN_FEATURES = {
946
+ ORG_CHART: "org_chart",
947
+ APPROVAL_WORKFLOW: "approval_workflow",
948
+ API_ACCESS: "api_access",
949
+ SSO: "sso",
950
+ BASIC_ACCOUNTING_HR: "basic_accounting_hr",
951
+ MODULE_PLATFORM: "module.platform",
952
+ MODULE_HR: "module.hr",
953
+ MODULE_ACCOUNTING: "module.accounting",
954
+ MODULE_SALES: "module.sales",
955
+ MODULE_PURCHASING: "module.purchasing",
956
+ MODULE_INVENTORY: "module.inventory",
957
+ MODULE_PROJECT: "module.project",
958
+ MODULE_SECRETARIAT: "module.secretariat",
959
+ };
960
+ /** True when plan features include key (with legacy basic_accounting_hr alias). */
961
+ function hasPlanFeature(features, feature) {
962
+ if (!features?.length)
963
+ return false;
964
+ if (features.includes(feature))
965
+ return true;
966
+ if ((feature === PLAN_FEATURES.MODULE_HR ||
967
+ feature === PLAN_FEATURES.MODULE_ACCOUNTING) &&
968
+ features.includes(PLAN_FEATURES.BASIC_ACCOUNTING_HR)) {
969
+ return true;
970
+ }
971
+ if (feature === PLAN_FEATURES.BASIC_ACCOUNTING_HR &&
972
+ features.includes(PLAN_FEATURES.MODULE_HR) &&
973
+ features.includes(PLAN_FEATURES.MODULE_ACCOUNTING)) {
974
+ return true;
975
+ }
976
+ return false;
977
+ }
978
+
979
+ function normaliseBase(value, fallback) {
980
+ return (value ?? fallback).replace(/\/$/, "");
981
+ }
982
+ /**
983
+ * One list, in display order, for the whole suite.
984
+ *
985
+ * It used to be copied into every app's own nav-user.tsx, which is how Invenx
986
+ * shipped without a link to Plannex: five copies, one of them edited by hand.
987
+ * Adding the sixth app is now a change here plus a version bump.
988
+ */
989
+ const APPS = [
990
+ {
991
+ id: "platform",
992
+ label: "แพลตฟอร์ม",
993
+ icon: jsxRuntime.jsx(lucideReact.Building2, { className: "h-4 w-4 shrink-0" }),
994
+ feature: null,
995
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLATFORM_URL, "https://platform.antniti.com"),
996
+ },
997
+ {
998
+ id: "accounx",
999
+ label: "Accounx",
1000
+ icon: jsxRuntime.jsx(lucideReact.Calculator, { className: "h-4 w-4 shrink-0" }),
1001
+ feature: PLAN_FEATURES.MODULE_ACCOUNTING,
1002
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_ACCOUNTING_URL, "https://accounx.antniti.com"),
1003
+ },
1004
+ {
1005
+ id: "invenx",
1006
+ label: "Invenx",
1007
+ icon: jsxRuntime.jsx(lucideReact.Warehouse, { className: "h-4 w-4 shrink-0" }),
1008
+ feature: PLAN_FEATURES.MODULE_INVENTORY,
1009
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_INVENX_URL, "https://invenx.antniti.com"),
1010
+ },
1011
+ {
1012
+ id: "plannex",
1013
+ label: "Plannex",
1014
+ icon: jsxRuntime.jsx(lucideReact.FolderKanban, { className: "h-4 w-4 shrink-0" }),
1015
+ feature: PLAN_FEATURES.MODULE_PROJECT,
1016
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLANNEX_URL, "https://plannex.antniti.com"),
1017
+ },
1018
+ {
1019
+ id: "peoplx",
1020
+ label: "Peoplx",
1021
+ icon: jsxRuntime.jsx(lucideReact.Users, { className: "h-4 w-4 shrink-0" }),
1022
+ feature: PLAN_FEATURES.MODULE_HR,
1023
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_HR_URL, "https://peoplx.antniti.com"),
1024
+ },
1025
+ ];
1026
+ /**
1027
+ * Builds the cross-app link list for <NavUser systemLinks>.
1028
+ *
1029
+ * The current app is always present even when the org's plan no longer covers
1030
+ * it — the user is looking at it, so hiding the link would only make the menu
1031
+ * disagree with the screen.
1032
+ */
1033
+ function buildSystemLinks({ orgCode, activeApp, features, }) {
1034
+ if (!orgCode)
1035
+ return [];
1036
+ return APPS.filter(app => app.id === activeApp ||
1037
+ app.feature === null ||
1038
+ hasPlanFeature(features, app.feature)).map(app => ({
1039
+ label: app.label,
1040
+ icon: app.icon,
1041
+ // Same-app link stays relative so it never leaves the current origin.
1042
+ href: app.id === activeApp ? `/${orgCode}` : `${app.baseUrl()}/${orgCode}`,
1043
+ ...(app.id === activeApp ? { active: true } : {}),
1044
+ }));
1045
+ }
1046
+
924
1047
  // Helper function to check if pathname matches a URL pattern
925
1048
  // This prevents false matches like /reports/sales matching /sales
926
1049
  // Handles paths with or without [code] prefix
@@ -2035,6 +2158,7 @@ exports.NavMain = NavMain;
2035
2158
  exports.NavTools = NavTools;
2036
2159
  exports.NavUser = NavUser;
2037
2160
  exports.OrganizationSwitcher = OrganizationSwitcher;
2161
+ exports.PLAN_FEATURES = PLAN_FEATURES;
2038
2162
  exports.PermissionGateShell = PermissionGateShell;
2039
2163
  exports.PermissionProvider = PermissionProvider;
2040
2164
  exports.PermissionRulesProvider = PermissionRulesProvider;
@@ -2049,6 +2173,7 @@ exports.SidebarTrigger = SidebarTrigger;
2049
2173
  exports.TapMenu = TapMenu;
2050
2174
  exports.ThemeProvider = ThemeProvider;
2051
2175
  exports.axiosInstance = instance;
2176
+ exports.buildSystemLinks = buildSystemLinks;
2052
2177
  exports.buildUrlWithCode = buildUrlWithCode;
2053
2178
  exports.canAccessNavPermissionRoute = canAccessNavPermissionRoute;
2054
2179
  exports.clearAccessTokenCache = clearAccessTokenCache;
@@ -2063,6 +2188,7 @@ exports.getAxiosErrorMessage = getAxiosErrorMessage;
2063
2188
  exports.getOrganizationId = getOrganizationId;
2064
2189
  exports.getQueryClientConfig = getQueryClientConfig;
2065
2190
  exports.getRequiredChildGroups = getRequiredChildGroups;
2191
+ exports.hasPlanFeature = hasPlanFeature;
2066
2192
  exports.isLikelyTransientNetworkError = isLikelyTransientNetworkError;
2067
2193
  exports.isPathActive = isPathActive;
2068
2194
  exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;