@eintrek/erp-shell 0.1.31 → 0.1.33

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
@@ -1214,6 +1214,33 @@ function AccessDenied({ title = "ขออภัย, คุณไม่มีส
1214
1214
  return (jsxRuntime.jsx("div", { className: wrapperClass, children: jsxRuntime.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: jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-8 text-center sm:flex-row sm:text-left", children: [jsxRuntime.jsxs("div", { className: "relative shrink-0", children: [jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full bg-destructive/20 blur-2xl" }), jsxRuntime.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: jsxRuntime.jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full border border-destructive/40 bg-destructive/10", children: jsxRuntime.jsx(lucideReact.Ban, { className: "h-8 w-8 text-destructive", strokeWidth: 2.5 }) }) })] }), jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [jsxRuntime.jsx("h1", { className: "text-xl font-bold text-foreground sm:text-2xl", children: title }), message ? (jsxRuntime.jsx("p", { className: "mt-3 whitespace-pre-line text-sm leading-relaxed text-muted-foreground sm:text-base", children: message })) : null, jsxRuntime.jsxs("div", { className: "mt-6 flex flex-col gap-3 sm:flex-row", children: [jsxRuntime.jsxs(erpTheme.Button, { type: "button", onClick: () => go(homeTarget), className: "w-full sm:w-auto", children: [jsxRuntime.jsx(lucideReact.Home, { className: "mr-2 h-4 w-4" }), homeLabel] }), supportHref ? (jsxRuntime.jsxs(erpTheme.Button, { type: "button", variant: "outline", onClick: () => go(supportHref), className: "w-full sm:w-auto", children: [jsxRuntime.jsx(lucideReact.Headset, { className: "mr-2 h-4 w-4" }), supportLabel] })) : null] })] })] }) }) }));
1215
1215
  }
1216
1216
 
1217
+ const EMPLOYEE_RECORD_REQUIRED = "EMPLOYEE_RECORD_REQUIRED";
1218
+ const EMPLOYEE_RECORD_REQUIRED_EVENT = "erp:employee-record-required";
1219
+ /**
1220
+ * Shown when the API refuses every request because the signed-in account has
1221
+ * no employee record in this organisation.
1222
+ *
1223
+ * A gate rather than a banner. Leave, attendance, approvals and payslips all
1224
+ * identify a person by that record, so once the server started enforcing it
1225
+ * every call fails — and without something that says so, the app becomes a
1226
+ * screen of error toasts with no explanation and no way forward.
1227
+ */
1228
+ function EmployeeRequiredGate({ platformBaseUrl, }) {
1229
+ const [blocked, setBlocked] = React.useState(false);
1230
+ const params = navigation.useParams();
1231
+ const code = params?.code ?? "";
1232
+ React.useEffect(() => {
1233
+ const onBlocked = () => setBlocked(true);
1234
+ window.addEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
1235
+ return () => window.removeEventListener(EMPLOYEE_RECORD_REQUIRED_EVENT, onBlocked);
1236
+ }, []);
1237
+ if (!blocked)
1238
+ return null;
1239
+ const base = platformBaseUrl?.replace(/\/$/, "") ?? "";
1240
+ const createHref = code ? `${base}/${code}/employee/create` : `${base}/`;
1241
+ return (jsxRuntime.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-background/95 p-6 backdrop-blur-sm", children: jsxRuntime.jsxs("div", { className: "w-full max-w-md space-y-5 text-center", children: [jsxRuntime.jsx(lucideReact.IdCard, { className: "mx-auto h-12 w-12 text-muted-foreground" }), jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.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" }), jsxRuntime.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" })] }), jsxRuntime.jsxs("div", { className: "flex flex-wrap justify-center gap-2", children: [jsxRuntime.jsx(erpTheme.Button, { asChild: true, children: jsxRuntime.jsxs("a", { href: createHref, children: [jsxRuntime.jsx(lucideReact.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"] }) }), jsxRuntime.jsx(erpTheme.Button, { variant: "outline", onClick: () => window.location.reload(), children: "\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48" })] }), jsxRuntime.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" })] }) }));
1242
+ }
1243
+
1217
1244
  const shellClassName = "flex min-w-0 flex-1 flex-col gap-6 overflow-x-hidden";
1218
1245
  /**
1219
1246
  * Shared layout for “กำลังตรวจสอบสิทธิ์” (skeleton) and “ไม่มีสิทธิ์” (message).
@@ -1368,7 +1395,9 @@ function DeniedCard() {
1368
1395
  * over the optional `fallbackRulesMap`), passes the resolved required roles
1369
1396
  * into the host's `usePermission` probe, and renders one of:
1370
1397
  *
1371
- * 1. rules / probe still loading → `null` (let Suspense / loading.tsx show)
1398
+ * 1. rules / probe still loading → `null` on FIRST load only (let Suspense /
1399
+ * loading.tsx show); once this subtree has been granted access, a later
1400
+ * loading state keeps the children mounted — see the note below
1372
1401
  * 2. no rule covers this path → deny (fail closed)
1373
1402
  * 3. rule is explicit public (`[]`) → children
1374
1403
  * 4. probe says no access → deny card
@@ -1388,7 +1417,25 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1388
1417
  const { hasPermission, isLoading } = usePermission({
1389
1418
  requiredRoles: lookup.roles,
1390
1419
  });
1391
- if (rulesLoading) {
1420
+ // Returning null unmounts the page. That is fine before anything has been
1421
+ // shown, and destructive afterwards: next-auth flips its status back to
1422
+ // "loading" on every session update() — one runs every 4 minutes — and a
1423
+ // rules hook that reports loading on that transient took the page down with
1424
+ // it. Users filling a long form watched everything they had typed vanish,
1425
+ // which reads as a spontaneous page refresh.
1426
+ //
1427
+ // So a loading state only blanks the subtree until access has been granted
1428
+ // once. After that the children stay mounted while the check re-settles; the
1429
+ // API still enforces access on every request, and a genuine loss of
1430
+ // permission surfaces as a denied response rather than a wiped form.
1431
+ //
1432
+ // The grant is remembered per pathname, not per mount. This provider lives in
1433
+ // the layout, so it survives every in-app navigation — a bare boolean would
1434
+ // carry one page's grant onto the next route and flash a page the user may
1435
+ // not be allowed to see while its own check is still running.
1436
+ const grantedForPathRef = React.useRef(null);
1437
+ const grantedHere = grantedForPathRef.current === pathname;
1438
+ if (rulesLoading && !grantedHere) {
1392
1439
  return null;
1393
1440
  }
1394
1441
  // Unknown route → deny. Only explicit `required_child_groups: []` is public.
@@ -1396,15 +1443,16 @@ function PermissionProvider({ children, usePermission, fallbackRulesMap, }) {
1396
1443
  return jsxRuntime.jsx(DeniedCard, {});
1397
1444
  }
1398
1445
  if (lookup.roles.length === 0) {
1446
+ grantedForPathRef.current = pathname;
1399
1447
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
1400
1448
  }
1401
- const isLoadingCombined = isLoading;
1402
- if (isLoadingCombined) {
1449
+ if (isLoading && !grantedHere) {
1403
1450
  return null;
1404
1451
  }
1405
- if (!hasPermission) {
1452
+ if (!hasPermission && !isLoading) {
1406
1453
  return jsxRuntime.jsx(DeniedCard, {});
1407
1454
  }
1455
+ grantedForPathRef.current = pathname;
1408
1456
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
1409
1457
  }
1410
1458
 
@@ -1858,6 +1906,15 @@ instance.interceptors.response.use(response => response, async (error) => {
1858
1906
  await redirectToLogout();
1859
1907
  return Promise.reject(toApiError(error));
1860
1908
  }
1909
+ // The server refuses every organisation-scoped call when the account has
1910
+ // no employee record. Announce it once so a gate can explain it, instead
1911
+ // of letting each failed query raise its own toast.
1912
+ if (error.response?.status === 403) {
1913
+ const body = error.response.data;
1914
+ if (body?.code === "EMPLOYEE_RECORD_REQUIRED" && typeof window !== "undefined") {
1915
+ window.dispatchEvent(new CustomEvent("erp:employee-record-required"));
1916
+ }
1917
+ }
1861
1918
  return Promise.reject(toApiError(error));
1862
1919
  });
1863
1920
 
@@ -1909,6 +1966,9 @@ exports.ApiError = ApiError;
1909
1966
  exports.AppBreadcrumb = AppBreadcrumb;
1910
1967
  exports.AppLayoutShell = AppLayoutShell;
1911
1968
  exports.AppSidebar = AppSidebar;
1969
+ exports.EMPLOYEE_RECORD_REQUIRED = EMPLOYEE_RECORD_REQUIRED;
1970
+ exports.EMPLOYEE_RECORD_REQUIRED_EVENT = EMPLOYEE_RECORD_REQUIRED_EVENT;
1971
+ exports.EmployeeRequiredGate = EmployeeRequiredGate;
1912
1972
  exports.FeatureFlagsProvider = FeatureFlagsProvider;
1913
1973
  exports.HeaderBackground = HeaderBackground;
1914
1974
  exports.NavMain = NavMain;