@eintrek/erp-shell 0.1.32 → 0.1.34

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.
@@ -0,0 +1,15 @@
1
+ export declare const EMPLOYEE_RECORD_REQUIRED = "EMPLOYEE_RECORD_REQUIRED";
2
+ export declare const EMPLOYEE_RECORD_REQUIRED_EVENT = "erp:employee-record-required";
3
+ /**
4
+ * Shown when the API refuses every request because the signed-in account has
5
+ * no employee record in this organisation.
6
+ *
7
+ * A gate rather than a banner. Leave, attendance, approvals and payslips all
8
+ * identify a person by that record, so once the server started enforcing it
9
+ * every call fails — and without something that says so, the app becomes a
10
+ * screen of error toasts with no explanation and no way forward.
11
+ */
12
+ export declare function EmployeeRequiredGate({ platformBaseUrl, }: {
13
+ /** Where employee records are managed, e.g. https://platform.antniti.com */
14
+ platformBaseUrl?: string;
15
+ }): import("react/jsx-runtime").JSX.Element | null;
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { SidebarHeader } from "./navigation/sidebar-header";
20
20
  export { AppSidebar, type AppSidebarProps } from "./navigation/app-sidebar";
21
21
  export { AppLayoutShell, type AppLayoutShellProps, type AppLayoutShellOrganization, } from "./navigation/app-layout-shell";
22
22
  export { AccessDenied, type AccessDeniedProps } from "./auth/access-denied";
23
+ export { EmployeeRequiredGate, EMPLOYEE_RECORD_REQUIRED, EMPLOYEE_RECORD_REQUIRED_EVENT, } from "./auth/employee-required-gate";
23
24
  export { PermissionGateShell, type PermissionGateShellProps, type PermissionGateSkeletonVariant, } from "./auth/permission-gate-shell";
24
25
  export { PermissionProvider, type PermissionProviderProps, type PermissionProbe, type UsePermissionHook, } from "./auth/permission-provider";
25
26
  export { getRequiredChildGroups, normalizePathnameForPermissionRules, resolveUiRoutePermission, type UiRoutePermissionLookup, } from "./auth/permission-utils";
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 } from 'lucide-react';
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';
9
9
  import { create } from 'zustand';
10
10
  import { persist, createJSONStorage } from 'zustand/middleware';
11
11
  import { useRouter, usePathname, useParams } from 'next/navigation';
@@ -1195,6 +1195,33 @@ function AccessDenied({ title = "ขออภัย, คุณไม่มีส
1195
1195
  return (jsx("div", { className: wrapperClass, children: jsx("div", { className: "relative w-full max-w-3xl overflow-hidden rounded-2xl border border-border/60 bg-card/60 p-8 shadow-sm backdrop-blur sm:p-10", children: jsxs("div", { className: "flex flex-col items-center gap-8 text-center sm:flex-row sm:text-left", children: [jsxs("div", { className: "relative shrink-0", children: [jsx("div", { className: "absolute inset-0 rounded-full bg-destructive/20 blur-2xl" }), jsx("div", { className: "relative flex h-28 w-28 items-center justify-center rounded-3xl border border-border/60 bg-gradient-to-br from-muted/40 to-background shadow-inner", children: jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-destructive/40 bg-destructive/10", children: jsx(Ban, { className: "h-8 w-8 text-destructive", strokeWidth: 2.5 }) }) })] }), jsxs("div", { className: "min-w-0 flex-1", children: [jsx("h1", { className: "text-xl font-bold text-foreground sm:text-2xl", children: title }), message ? (jsx("p", { className: "mt-3 whitespace-pre-line text-sm leading-relaxed text-muted-foreground sm:text-base", children: message })) : null, jsxs("div", { className: "mt-6 flex flex-col gap-3 sm:flex-row", children: [jsxs(Button, { type: "button", onClick: () => go(homeTarget), className: "w-full sm:w-auto", children: [jsx(Home, { className: "mr-2 h-4 w-4" }), homeLabel] }), supportHref ? (jsxs(Button, { type: "button", variant: "outline", onClick: () => go(supportHref), className: "w-full sm:w-auto", children: [jsx(Headset, { className: "mr-2 h-4 w-4" }), supportLabel] })) : null] })] })] }) }) }));
1196
1196
  }
1197
1197
 
1198
+ const EMPLOYEE_RECORD_REQUIRED = "EMPLOYEE_RECORD_REQUIRED";
1199
+ const EMPLOYEE_RECORD_REQUIRED_EVENT = "erp:employee-record-required";
1200
+ /**
1201
+ * Shown when the API refuses every request because the signed-in account has
1202
+ * no employee record in this organisation.
1203
+ *
1204
+ * A gate rather than a banner. Leave, attendance, approvals and payslips all
1205
+ * identify a person by that record, so once the server started enforcing it
1206
+ * every call fails — and without something that says so, the app becomes a
1207
+ * screen of error toasts with no explanation and no way forward.
1208
+ */
1209
+ function EmployeeRequiredGate({ platformBaseUrl, }) {
1210
+ const [blocked, setBlocked] = useState(false);
1211
+ const params = useParams();
1212
+ const code = params?.code ?? "";
1213
+ useEffect(() => {
1214
+ const onBlocked = () => setBlocked(true);
1215
+ window.addEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
1216
+ return () => window.removeEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
1217
+ }, []);
1218
+ if (!blocked)
1219
+ return null;
1220
+ const base = platformBaseUrl?.replace(/\/$/, "") ?? "";
1221
+ const createHref = code ? `${base}/${code}/employee/create` : `${base}/`;
1222
+ return (jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-background/95 p-6 backdrop-blur-sm", children: jsxs("div", { className: "w-full max-w-md space-y-5 text-center", children: [jsx(ContactRound, { className: "mx-auto h-12 w-12 text-muted-foreground" }), jsxs("div", { className: "space-y-2", children: [jsx("h1", { className: "text-lg font-semibold tracking-tight", children: "\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E19\u0E35\u0E49\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E1C\u0E39\u0E01\u0E01\u0E31\u0E1A\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19" }), jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E25\u0E32 \u0E25\u0E07\u0E40\u0E27\u0E25\u0E32 \u0E40\u0E1A\u0E34\u0E01\u0E40\u0E07\u0E34\u0E19 \u0E41\u0E25\u0E30\u0E01\u0E32\u0E23\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34 \u0E23\u0E30\u0E1A\u0E38\u0E15\u0E31\u0E27\u0E04\u0E19\u0E08\u0E32\u0E01\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19 \u0E08\u0E36\u0E07\u0E22\u0E31\u0E07\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19\u0E43\u0E19\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23\u0E19\u0E35\u0E49\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49" })] }), jsxs("div", { className: "flex flex-wrap justify-center gap-2", children: [jsx(Button, { asChild: true, children: jsxs("a", { href: createHref, children: [jsx(UserPlus, { className: "mr-1 h-4 w-4" }), "\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E02\u0E49\u0E2D\u0E21\u0E39\u0E25\u0E1E\u0E19\u0E31\u0E01\u0E07\u0E32\u0E19"] }) }), jsx(Button, { variant: "outline", onClick: () => window.location.reload(), children: "\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48" })] }), jsx("p", { className: "text-xs text-muted-foreground", children: "\u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C\u0E41\u0E01\u0E49\u0E44\u0E02 \u0E43\u0E2B\u0E49\u0E41\u0E08\u0E49\u0E07\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] }) }));
1223
+ }
1224
+
1198
1225
  const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
1199
1226
  /**
1200
1227
  * Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
@@ -1860,6 +1887,15 @@ instance.interceptors.response.use(response => response, async (error) => {
1860
1887
  await redirectToLogout();
1861
1888
  return Promise.reject(toApiError(error));
1862
1889
  }
1890
+ // The server refuses every organisation-scoped call when the account has
1891
+ // no employee record. Announce it once so a gate can explain it, instead
1892
+ // of letting each failed query raise its own toast.
1893
+ if (error.response?.status === 403) {
1894
+ const body = error.response.data;
1895
+ if (body?.code === "EMPLOYEE_RECORD_REQUIRED" && typeof window !== "undefined") {
1896
+ window.dispatchEvent(new CustomEvent("erp:employee-record-required"));
1897
+ }
1898
+ }
1863
1899
  return Promise.reject(toApiError(error));
1864
1900
  });
1865
1901
 
@@ -1902,5 +1938,5 @@ const getAxiosErrorMessage = (error) => {
1902
1938
  return String(error);
1903
1939
  };
1904
1940
 
1905
- export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, 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 };
1941
+ export { AccessDenied, ApiError, AppBreadcrumb, AppLayoutShell, AppSidebar, 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 };
1906
1942
  //# sourceMappingURL=index.esm.js.map