@eintrek/erp-shell 0.1.37 → 0.1.39

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
@@ -921,6 +921,115 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
921
921
  reactDom.createPortal(dropdownContent, document.body)] }) }));
922
922
  }
923
923
 
924
+ /**
925
+ * Plan feature keys (billable entitlements). Mirror of platform-apis plan.go.
926
+ *
927
+ * This lives in the shell because every app gates on the same keys, and five
928
+ * private copies of the same list drift: the app-switcher list did exactly
929
+ * that before it moved here.
930
+ */
931
+ const PLAN_FEATURES = {
932
+ ORG_CHART: "org_chart",
933
+ APPROVAL_WORKFLOW: "approval_workflow",
934
+ API_ACCESS: "api_access",
935
+ SSO: "sso",
936
+ BASIC_ACCOUNTING_HR: "basic_accounting_hr",
937
+ MODULE_PLATFORM: "module.platform",
938
+ MODULE_HR: "module.hr",
939
+ MODULE_ACCOUNTING: "module.accounting",
940
+ MODULE_SALES: "module.sales",
941
+ MODULE_PURCHASING: "module.purchasing",
942
+ MODULE_INVENTORY: "module.inventory",
943
+ MODULE_PROJECT: "module.project",
944
+ MODULE_SECRETARIAT: "module.secretariat",
945
+ };
946
+ /** True when plan features include key (with legacy basic_accounting_hr alias). */
947
+ function hasPlanFeature(features, feature) {
948
+ if (!features?.length)
949
+ return false;
950
+ if (features.includes(feature))
951
+ return true;
952
+ if ((feature === PLAN_FEATURES.MODULE_HR ||
953
+ feature === PLAN_FEATURES.MODULE_ACCOUNTING) &&
954
+ features.includes(PLAN_FEATURES.BASIC_ACCOUNTING_HR)) {
955
+ return true;
956
+ }
957
+ if (feature === PLAN_FEATURES.BASIC_ACCOUNTING_HR &&
958
+ features.includes(PLAN_FEATURES.MODULE_HR) &&
959
+ features.includes(PLAN_FEATURES.MODULE_ACCOUNTING)) {
960
+ return true;
961
+ }
962
+ return false;
963
+ }
964
+
965
+ function normaliseBase(value, fallback) {
966
+ return (value ?? fallback).replace(/\/$/, "");
967
+ }
968
+ /**
969
+ * One list, in display order, for the whole suite.
970
+ *
971
+ * It used to be copied into every app's own nav-user.tsx, which is how Invenx
972
+ * shipped without a link to Plannex: five copies, one of them edited by hand.
973
+ * Adding the sixth app is now a change here plus a version bump.
974
+ */
975
+ const APPS = [
976
+ {
977
+ id: "platform",
978
+ label: "แพลตฟอร์ม",
979
+ icon: jsxRuntime.jsx(lucideReact.Building2, { className: "h-4 w-4 shrink-0" }),
980
+ feature: null,
981
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLATFORM_URL, "https://platform.antniti.com"),
982
+ },
983
+ {
984
+ id: "accounx",
985
+ label: "Accounx",
986
+ icon: jsxRuntime.jsx(lucideReact.Calculator, { className: "h-4 w-4 shrink-0" }),
987
+ feature: PLAN_FEATURES.MODULE_ACCOUNTING,
988
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_ACCOUNTING_URL, "https://accounx.antniti.com"),
989
+ },
990
+ {
991
+ id: "invenx",
992
+ label: "Invenx",
993
+ icon: jsxRuntime.jsx(lucideReact.Warehouse, { className: "h-4 w-4 shrink-0" }),
994
+ feature: PLAN_FEATURES.MODULE_INVENTORY,
995
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_INVENX_URL, "https://invenx.antniti.com"),
996
+ },
997
+ {
998
+ id: "plannex",
999
+ label: "Plannex",
1000
+ icon: jsxRuntime.jsx(lucideReact.FolderKanban, { className: "h-4 w-4 shrink-0" }),
1001
+ feature: PLAN_FEATURES.MODULE_PROJECT,
1002
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_PLANNEX_URL, "https://plannex.antniti.com"),
1003
+ },
1004
+ {
1005
+ id: "peoplx",
1006
+ label: "Peoplx",
1007
+ icon: jsxRuntime.jsx(lucideReact.Users, { className: "h-4 w-4 shrink-0" }),
1008
+ feature: PLAN_FEATURES.MODULE_HR,
1009
+ baseUrl: () => normaliseBase(process.env.NEXT_PUBLIC_HR_URL, "https://peoplx.antniti.com"),
1010
+ },
1011
+ ];
1012
+ /**
1013
+ * Builds the cross-app link list for <NavUser systemLinks>.
1014
+ *
1015
+ * The current app is always present even when the org's plan no longer covers
1016
+ * it — the user is looking at it, so hiding the link would only make the menu
1017
+ * disagree with the screen.
1018
+ */
1019
+ function buildSystemLinks({ orgCode, activeApp, features, }) {
1020
+ if (!orgCode)
1021
+ return [];
1022
+ return APPS.filter(app => app.id === activeApp ||
1023
+ app.feature === null ||
1024
+ hasPlanFeature(features, app.feature)).map(app => ({
1025
+ label: app.label,
1026
+ icon: app.icon,
1027
+ // Same-app link stays relative so it never leaves the current origin.
1028
+ href: app.id === activeApp ? `/${orgCode}` : `${app.baseUrl()}/${orgCode}`,
1029
+ ...(app.id === activeApp ? { active: true } : {}),
1030
+ }));
1031
+ }
1032
+
924
1033
  // Helper function to check if pathname matches a URL pattern
925
1034
  // This prevents false matches like /reports/sales matching /sales
926
1035
  // Handles paths with or without [code] prefix
@@ -1908,7 +2017,19 @@ instance.interceptors.request.use(async (config) => {
1908
2017
  if (token) {
1909
2018
  config.headers.Authorization = `Bearer ${token}`;
1910
2019
  }
1911
- const organizationId = getOrgFromCookie() || getOrganizationId();
2020
+ // localStorage first, cookie second.
2021
+ //
2022
+ // Both are written by the same state machine, but not at the same moment:
2023
+ // setOrganizationId writes localStorage synchronously and then awaits a
2024
+ // server action for the cookie. Preferring the cookie meant every request
2025
+ // fired during that round trip carried the previous organisation — while
2026
+ // the query params, built from React state, already carried the new one.
2027
+ //
2028
+ // The backend scopes by this header and ignores organization_id in the
2029
+ // query, so those requests asked for organisation A and were handed B, and
2030
+ // the answer landed in the cache under A's key. No amount of cache clearing
2031
+ // reaches that: the entry is wrong, not stale.
2032
+ const organizationId = getOrganizationId() || getOrgFromCookie();
1912
2033
  if (organizationId) {
1913
2034
  config.headers["X-Organization-ID"] = organizationId;
1914
2035
  }
@@ -2023,6 +2144,7 @@ exports.NavMain = NavMain;
2023
2144
  exports.NavTools = NavTools;
2024
2145
  exports.NavUser = NavUser;
2025
2146
  exports.OrganizationSwitcher = OrganizationSwitcher;
2147
+ exports.PLAN_FEATURES = PLAN_FEATURES;
2026
2148
  exports.PermissionGateShell = PermissionGateShell;
2027
2149
  exports.PermissionProvider = PermissionProvider;
2028
2150
  exports.PermissionRulesProvider = PermissionRulesProvider;
@@ -2037,6 +2159,7 @@ exports.SidebarTrigger = SidebarTrigger;
2037
2159
  exports.TapMenu = TapMenu;
2038
2160
  exports.ThemeProvider = ThemeProvider;
2039
2161
  exports.axiosInstance = instance;
2162
+ exports.buildSystemLinks = buildSystemLinks;
2040
2163
  exports.buildUrlWithCode = buildUrlWithCode;
2041
2164
  exports.canAccessNavPermissionRoute = canAccessNavPermissionRoute;
2042
2165
  exports.clearAccessTokenCache = clearAccessTokenCache;
@@ -2051,6 +2174,7 @@ exports.getAxiosErrorMessage = getAxiosErrorMessage;
2051
2174
  exports.getOrganizationId = getOrganizationId;
2052
2175
  exports.getQueryClientConfig = getQueryClientConfig;
2053
2176
  exports.getRequiredChildGroups = getRequiredChildGroups;
2177
+ exports.hasPlanFeature = hasPlanFeature;
2054
2178
  exports.isLikelyTransientNetworkError = isLikelyTransientNetworkError;
2055
2179
  exports.isPathActive = isPathActive;
2056
2180
  exports.normalizePathnameForPermissionRules = normalizePathnameForPermissionRules;