@optifye/dashboard-core 6.10.47 → 6.10.49

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
@@ -3947,6 +3947,10 @@ var AuthService = class {
3947
3947
  email: enrichedUser.email,
3948
3948
  role: enrichedUser.role,
3949
3949
  role_level: enrichedUser.role,
3950
+ scope_mode: enrichedUser.scope_mode,
3951
+ scope_generated_at: enrichedUser.scope_generated_at,
3952
+ scope_ttl_seconds: enrichedUser.scope_ttl_seconds,
3953
+ access_scope: enrichedUser.access_scope,
3950
3954
  company_id: enrichedUser.company_id,
3951
3955
  first_login_completed: enrichedUser.profile.first_login_completed,
3952
3956
  properties: {
@@ -8153,6 +8157,8 @@ var AuthProvider = ({ children }) => {
8153
8157
  const isFetchingRef = React26.useRef(false);
8154
8158
  const lastProcessedSessionRef = React26.useRef(null);
8155
8159
  const hasAuthenticatedRef = React26.useRef(false);
8160
+ const scopeFetchedAtRef = React26.useRef(0);
8161
+ const scopeTtlSecondsRef = React26.useRef(300);
8156
8162
  const fetchSession = React26.useCallback(async (supabaseSession, isReauth = false, allowRetry = true) => {
8157
8163
  if (isFetchingRef.current) {
8158
8164
  console.log("[AuthContext] Already fetching, skipping duplicate request");
@@ -8165,6 +8171,9 @@ var AuthProvider = ({ children }) => {
8165
8171
  }
8166
8172
  try {
8167
8173
  const enrichedUser = await AuthService.getSession(supabaseSession.access_token);
8174
+ const parsedScopeGeneratedAt = enrichedUser.scope_generated_at ? Date.parse(enrichedUser.scope_generated_at) : NaN;
8175
+ scopeFetchedAtRef.current = Number.isFinite(parsedScopeGeneratedAt) ? parsedScopeGeneratedAt : Date.now();
8176
+ scopeTtlSecondsRef.current = enrichedUser.scope_ttl_seconds || 300;
8168
8177
  const authUser = AuthService.toAuthUser(enrichedUser);
8169
8178
  setUser(authUser);
8170
8179
  setSession(supabaseSession);
@@ -8254,6 +8263,8 @@ var AuthProvider = ({ children }) => {
8254
8263
  setError(null);
8255
8264
  setShowOnboarding(false);
8256
8265
  hasAuthenticatedRef.current = false;
8266
+ scopeFetchedAtRef.current = 0;
8267
+ scopeTtlSecondsRef.current = 300;
8257
8268
  router$1.push("/login");
8258
8269
  } catch (err) {
8259
8270
  console.error("[AuthContext] Sign out error:", err);
@@ -8316,6 +8327,44 @@ var AuthProvider = ({ children }) => {
8316
8327
  clearInterval(intervalId);
8317
8328
  };
8318
8329
  }, [session, supabase]);
8330
+ React26.useEffect(() => {
8331
+ if (!session || !user) {
8332
+ return;
8333
+ }
8334
+ const refreshScopeIfExpired = async () => {
8335
+ if (!session || isFetchingRef.current) {
8336
+ return;
8337
+ }
8338
+ const ttlSeconds = user.scope_ttl_seconds || scopeTtlSecondsRef.current || 300;
8339
+ const fetchedAt = scopeFetchedAtRef.current || 0;
8340
+ const shouldRefresh = fetchedAt === 0 || Date.now() - fetchedAt >= ttlSeconds * 1e3;
8341
+ if (!shouldRefresh) {
8342
+ return;
8343
+ }
8344
+ try {
8345
+ console.log("[AuthContext] Scope TTL expired, refreshing session scope");
8346
+ await fetchSession(session, true);
8347
+ } catch (err) {
8348
+ console.error("[AuthContext] Scope refresh failed:", err);
8349
+ }
8350
+ };
8351
+ refreshScopeIfExpired();
8352
+ const intervalId = setInterval(refreshScopeIfExpired, 6e4);
8353
+ return () => clearInterval(intervalId);
8354
+ }, [session, user?.id, user?.scope_ttl_seconds, fetchSession]);
8355
+ React26.useEffect(() => {
8356
+ if (typeof window === "undefined") {
8357
+ return;
8358
+ }
8359
+ const handleScopeRefresh = () => {
8360
+ if (!session) {
8361
+ return;
8362
+ }
8363
+ void fetchSession(session, true);
8364
+ };
8365
+ window.addEventListener("rbac:refresh-scope", handleScopeRefresh);
8366
+ return () => window.removeEventListener("rbac:refresh-scope", handleScopeRefresh);
8367
+ }, [session, fetchSession]);
8319
8368
  React26.useEffect(() => {
8320
8369
  if (!supabase) {
8321
8370
  console.log("[AuthContext] No Supabase client, skipping auth initialization");
@@ -8384,7 +8433,8 @@ var AuthProvider = ({ children }) => {
8384
8433
  } else if (event === "TOKEN_REFRESHED") {
8385
8434
  console.log("[AuthContext] Token refreshed automatically");
8386
8435
  if (currentSession) {
8387
- setSession(currentSession);
8436
+ const isReauth = hasAuthenticatedRef.current;
8437
+ await fetchSession(currentSession, isReauth);
8388
8438
  const expiresAt = currentSession.expires_at;
8389
8439
  if (expiresAt) {
8390
8440
  const minutesUntilExpiry = Math.floor((expiresAt * 1e3 - Date.now()) / 6e4);
@@ -10532,12 +10582,16 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10532
10582
  const configuredLineIds = React26.useMemo(() => {
10533
10583
  return getConfiguredLineIds(entityConfig);
10534
10584
  }, [entityConfig]);
10585
+ const targetFactoryLineIds = React26.useMemo(() => {
10586
+ const sourceLineIds = userAccessibleLineIds !== void 0 ? userAccessibleLineIds : configuredLineIds;
10587
+ return Array.from(new Set((sourceLineIds || []).filter(Boolean)));
10588
+ }, [userAccessibleLineIds, configuredLineIds]);
10535
10589
  const { shiftConfig: staticShiftConfig } = useDashboardConfig();
10536
10590
  const {
10537
10591
  shiftConfigMap: multiLineShiftConfigMap,
10538
10592
  isLoading: isMultiLineShiftConfigLoading
10539
10593
  } = useMultiLineShiftConfigs(
10540
- isFactoryView ? configuredLineIds : [],
10594
+ isFactoryView ? targetFactoryLineIds : [],
10541
10595
  staticShiftConfig
10542
10596
  );
10543
10597
  const {
@@ -10632,7 +10686,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10632
10686
  return;
10633
10687
  }
10634
10688
  const isFactory = currentLineIdToUse === factoryViewId;
10635
- const targetLineIds = isFactory ? userAccessibleLineIds || configuredLineIds : [currentLineIdToUse];
10689
+ const targetLineIds = isFactory ? targetFactoryLineIds : [currentLineIdToUse];
10636
10690
  const targetLineIdsKey = targetLineIds.slice().sort().join(",");
10637
10691
  const usesShiftGroups = isFactory && shiftGroups.length > 0;
10638
10692
  const singleShiftDetails = usesShiftGroups ? null : shiftConfig ? getCurrentShift(defaultTimezone, shiftConfig) : shiftGroups.length === 1 ? { date: shiftGroups[0].date, shiftId: shiftGroups[0].shiftId } : getCurrentShift(defaultTimezone, staticShiftConfig);
@@ -10681,7 +10735,16 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10681
10735
  throw new Error("Backend URL is not configured. Please set NEXT_PUBLIC_BACKEND_URL in your environment.");
10682
10736
  }
10683
10737
  if (targetLineIds.length === 0) {
10684
- throw new Error("No target line IDs available for fetching metrics.");
10738
+ logDebug("[useDashboardMetrics] Skipping fetch: no target line IDs after scope filtering");
10739
+ setMetrics({
10740
+ workspaceMetrics: [],
10741
+ lineMetrics: [],
10742
+ metadata: void 0,
10743
+ efficiencyLegend: DEFAULT_EFFICIENCY_LEGEND
10744
+ });
10745
+ setMetricsLineId(requestLineId);
10746
+ lastFetchKeyRef.current = inFlightFetchKeyRef.current;
10747
+ return;
10685
10748
  }
10686
10749
  let allWorkspaceMetrics = [];
10687
10750
  let allLineMetrics = [];
@@ -10699,11 +10762,16 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10699
10762
  lineCount: group.lineIds.length
10700
10763
  }))
10701
10764
  });
10765
+ const targetLineIdSet = new Set(targetLineIds);
10702
10766
  const metricsPromises = shiftGroups.map(async (group) => {
10703
- const lineIdsParam = `line_ids=${group.lineIds.join(",")}`;
10767
+ const groupLineIds = group.lineIds.filter((lineGroupId) => targetLineIdSet.has(lineGroupId));
10768
+ if (groupLineIds.length === 0) {
10769
+ return null;
10770
+ }
10771
+ const lineIdsParam = `line_ids=${groupLineIds.join(",")}`;
10704
10772
  const url = `${apiUrl}/api/dashboard/metrics?${lineIdsParam}&date=${group.date}&shift_id=${group.shiftId}&company_id=${companyId}${forceParam}`;
10705
10773
  logDebug(`[useDashboardMetrics] \u{1F4CA} Fetching for shift ${group.shiftId} (${group.shiftName}):`, {
10706
- lineIds: group.lineIds,
10774
+ lineIds: groupLineIds,
10707
10775
  date: group.date
10708
10776
  });
10709
10777
  const response = await fetch(url, {
@@ -10727,7 +10795,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10727
10795
  throw new Error(`Invalid JSON response from backend`);
10728
10796
  }
10729
10797
  });
10730
- const results = await Promise.all(metricsPromises);
10798
+ const results = (await Promise.all(metricsPromises)).filter((result) => !!result);
10731
10799
  hasFlowBuffers = results.some((result) => result?.metadata?.has_flow_buffers);
10732
10800
  results.forEach((result) => {
10733
10801
  const idleMap = result?.metadata?.idle_time_vlm_by_line;
@@ -10905,6 +10973,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10905
10973
  shiftGroups,
10906
10974
  shiftGroupsKey,
10907
10975
  configuredLineIds,
10976
+ targetFactoryLineIds,
10908
10977
  isFactoryView,
10909
10978
  multiLineShiftConfigMap,
10910
10979
  staticShiftConfig,
@@ -10997,8 +11066,14 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
10997
11066
  logDebug("[useDashboardMetrics] Setting up group subscriptions:", {
10998
11067
  groupCount: currentShiftGroups.length
10999
11068
  });
11069
+ const targetFactoryLineIdsForSubscriptions = currentUserAccessibleLineIds !== void 0 ? currentUserAccessibleLineIds : currentConfiguredLineIds;
11070
+ const targetFactoryLineIdSet = new Set(
11071
+ (targetFactoryLineIdsForSubscriptions || []).filter(Boolean)
11072
+ );
11000
11073
  currentShiftGroups.forEach((group, index) => {
11001
- const groupLineIds = group.lineIds.filter((id3) => id3 && id3 !== factoryViewIdentifier);
11074
+ const groupLineIds = group.lineIds.filter(
11075
+ (id3) => id3 && id3 !== factoryViewIdentifier && targetFactoryLineIdSet.has(id3)
11076
+ );
11002
11077
  if (groupLineIds.length === 0) {
11003
11078
  logDebug("[useDashboardMetrics] Skipping group subscription: no line IDs after filtering", {
11004
11079
  groupIndex: index,
@@ -16177,6 +16252,7 @@ function useAccessControl() {
16177
16252
  function useTeamManagementPermissions() {
16178
16253
  const { user } = useAuth();
16179
16254
  const currentRole = user?.role_level;
16255
+ const isSuperAdminOptifye = currentRole === "optifye" && (user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin);
16180
16256
  return {
16181
16257
  /**
16182
16258
  * Can the current user assign lines to this user?
@@ -16232,7 +16308,10 @@ function useTeamManagementPermissions() {
16232
16308
  availableRolesToAssign: () => {
16233
16309
  if (!currentRole) return [];
16234
16310
  if (currentRole === "optifye") {
16235
- return ["owner", "it", "plant_head", "supervisor"];
16311
+ if (isSuperAdminOptifye) {
16312
+ return ["owner", "it", "plant_head", "supervisor"];
16313
+ }
16314
+ return ["it", "plant_head", "supervisor"];
16236
16315
  }
16237
16316
  if (currentRole === "owner") {
16238
16317
  return ["it", "plant_head", "supervisor"];
@@ -16250,7 +16329,12 @@ function useTeamManagementPermissions() {
16250
16329
  */
16251
16330
  canInviteRole: (role) => {
16252
16331
  if (!currentRole) return false;
16253
- if (currentRole === "optifye") return true;
16332
+ if (currentRole === "optifye") {
16333
+ if (role === "owner") {
16334
+ return isSuperAdminOptifye;
16335
+ }
16336
+ return true;
16337
+ }
16254
16338
  if (currentRole === "owner") {
16255
16339
  return ["it", "plant_head", "supervisor"].includes(role);
16256
16340
  }
@@ -16284,40 +16368,133 @@ function useTeamManagementPermissions() {
16284
16368
  }
16285
16369
  function useUserLineAccess(configLineIds) {
16286
16370
  const { user } = useAuth();
16371
+ const { lines: dbLines } = useLines();
16287
16372
  const { accessibleLineIds, defaultLineId } = React26.useMemo(() => {
16373
+ const dbLineIds = dbLines.map((line) => line.id);
16374
+ const dbLineIdSet = new Set(dbLineIds);
16375
+ const fallbackAllLineIds = configLineIds && configLineIds.length > 0 ? configLineIds : dbLineIds;
16376
+ const normalizeIds = (value) => {
16377
+ if (Array.isArray(value)) {
16378
+ return value.filter((id3) => typeof id3 === "string" && !!id3);
16379
+ }
16380
+ if (typeof value === "string" && value) {
16381
+ return [value];
16382
+ }
16383
+ return [];
16384
+ };
16385
+ const uniqueIds = (ids) => Array.from(new Set(ids.filter(Boolean)));
16386
+ const filterToEnabledDb = (lineIds, options = {}) => {
16387
+ const deduped = uniqueIds(lineIds);
16388
+ if (dbLineIdSet.size === 0) {
16389
+ return options.allowWhenDbUnavailable ? deduped : [];
16390
+ }
16391
+ return deduped.filter((lineId) => dbLineIdSet.has(lineId));
16392
+ };
16288
16393
  if (!user) {
16289
- return { accessibleLineIds: configLineIds || [], defaultLineId: configLineIds?.[0] };
16290
- }
16291
- if (user.role_level === "supervisor") {
16292
- const assignedLineIds = user.properties?.line_id || user.properties?.line_ids || [];
16293
- if (Array.isArray(assignedLineIds) && assignedLineIds.length > 0) {
16294
- console.log("[useUserLineAccess] Supervisor has assigned lines:", assignedLineIds);
16295
- return {
16296
- accessibleLineIds: assignedLineIds,
16297
- defaultLineId: assignedLineIds[0]
16298
- };
16394
+ const fallback = filterToEnabledDb(fallbackAllLineIds, { allowWhenDbUnavailable: true });
16395
+ return { accessibleLineIds: fallback, defaultLineId: fallback[0] };
16396
+ }
16397
+ const configSet = new Set(configLineIds || []);
16398
+ const hasScopePayload = !!user.access_scope || !!user.scope_mode;
16399
+ const role = user.role_level || user.role;
16400
+ const scopedLineIds = normalizeIds(user.access_scope?.line_ids);
16401
+ const scopedFactoryIds = normalizeIds(user.access_scope?.factory_ids);
16402
+ const properties = user.properties || {};
16403
+ const propertyLineIds = normalizeIds(properties?.line_ids || properties?.line_id);
16404
+ const propertyFactoryIds = normalizeIds(properties?.factory_ids || properties?.factory_id);
16405
+ const propertyCompanyId = typeof properties?.company_id === "string" ? properties.company_id : user.company_id;
16406
+ const scopedCompanyId = user.access_scope?.company_id || propertyCompanyId;
16407
+ const isExplicitSuperAdmin = user.scope_mode === "SUPER_ADMIN" || !!user.access_scope?.is_super_admin;
16408
+ const isLegacyOptifyeGlobal = !hasScopePayload && role === "optifye" && propertyLineIds.length === 0 && propertyFactoryIds.length === 0 && !propertyCompanyId;
16409
+ const lineFactoryMap = /* @__PURE__ */ new Map();
16410
+ dbLines.forEach((line) => {
16411
+ lineFactoryMap.set(line.id, line.factory_id);
16412
+ });
16413
+ const applyConfigFilter = (lineIds) => {
16414
+ const dedupedLineIds = uniqueIds(lineIds);
16415
+ if (!configLineIds || configLineIds.length === 0) {
16416
+ return dedupedLineIds;
16299
16417
  }
16300
- console.warn("[useUserLineAccess] Supervisor has NO assigned lines!", {
16301
- properties: user.properties,
16302
- hasLineId: !!user.properties?.line_id,
16303
- hasLineIds: !!user.properties?.line_ids
16418
+ const filtered = dedupedLineIds.filter((id3) => configSet.has(id3));
16419
+ return filtered.length > 0 ? filtered : dedupedLineIds;
16420
+ };
16421
+ const resolveFactoryScopedLines = (factoryIds) => {
16422
+ const factorySet = new Set(factoryIds);
16423
+ const baseLineIds = configLineIds && configLineIds.length > 0 ? configLineIds : dbLineIds;
16424
+ const filtered = baseLineIds.filter((lineId) => {
16425
+ const factoryId = lineFactoryMap.get(lineId);
16426
+ return !!factoryId && factorySet.has(factoryId);
16304
16427
  });
16428
+ if (filtered.length > 0) {
16429
+ return uniqueIds(filtered);
16430
+ }
16431
+ const fromDb = dbLines.filter((line) => !!line.factory_id && factorySet.has(line.factory_id)).map((line) => line.id);
16432
+ if (fromDb.length > 0) {
16433
+ return uniqueIds(fromDb);
16434
+ }
16435
+ return [];
16436
+ };
16437
+ if (isExplicitSuperAdmin || isLegacyOptifyeGlobal) {
16438
+ const all = filterToEnabledDb(fallbackAllLineIds, { allowWhenDbUnavailable: true });
16305
16439
  return {
16306
- accessibleLineIds: [],
16307
- defaultLineId: void 0
16440
+ accessibleLineIds: all,
16441
+ defaultLineId: all[0]
16308
16442
  };
16309
16443
  }
16310
- if (user.role_level === "plant_head") {
16444
+ if (scopedLineIds.length > 0) {
16445
+ const filtered = applyConfigFilter(scopedLineIds);
16446
+ const scoped = filterToEnabledDb(filtered.length > 0 ? filtered : uniqueIds(scopedLineIds));
16311
16447
  return {
16312
- accessibleLineIds: configLineIds || [],
16313
- defaultLineId: configLineIds?.[0]
16448
+ accessibleLineIds: scoped,
16449
+ defaultLineId: scoped[0]
16314
16450
  };
16315
16451
  }
16316
- return {
16317
- accessibleLineIds: configLineIds || [],
16318
- defaultLineId: configLineIds?.[0]
16319
- };
16320
- }, [user, configLineIds]);
16452
+ if (scopedFactoryIds.length > 0) {
16453
+ const scoped = filterToEnabledDb(resolveFactoryScopedLines(scopedFactoryIds));
16454
+ return {
16455
+ accessibleLineIds: scoped,
16456
+ defaultLineId: scoped[0]
16457
+ };
16458
+ }
16459
+ if (scopedCompanyId) {
16460
+ const dbCompanyLineIds = dbLines.filter((line) => !line.company_id || line.company_id === scopedCompanyId).map((line) => line.id);
16461
+ const companyScopedLines = configLineIds && configLineIds.length > 0 ? (() => {
16462
+ const dbCompanySet = new Set(dbCompanyLineIds);
16463
+ const filtered = configLineIds.filter((lineId) => dbCompanySet.has(lineId));
16464
+ if (filtered.length > 0) return filtered;
16465
+ return dbCompanyLineIds;
16466
+ })() : dbCompanyLineIds;
16467
+ const scoped = filterToEnabledDb(companyScopedLines);
16468
+ return {
16469
+ accessibleLineIds: scoped,
16470
+ defaultLineId: scoped[0]
16471
+ };
16472
+ }
16473
+ if (propertyLineIds.length > 0) {
16474
+ const filtered = applyConfigFilter(propertyLineIds);
16475
+ const scoped = filterToEnabledDb(filtered);
16476
+ return {
16477
+ accessibleLineIds: scoped,
16478
+ defaultLineId: scoped[0]
16479
+ };
16480
+ }
16481
+ if (propertyFactoryIds.length > 0) {
16482
+ const scoped = filterToEnabledDb(resolveFactoryScopedLines(propertyFactoryIds));
16483
+ return {
16484
+ accessibleLineIds: scoped,
16485
+ defaultLineId: scoped[0]
16486
+ };
16487
+ }
16488
+ if (role === "owner" || role === "it" || role === "optifye") {
16489
+ const companyScopedLines = scopedCompanyId ? dbLines.filter((line) => !line.company_id || line.company_id === scopedCompanyId).map((line) => line.id) : [];
16490
+ const scoped = filterToEnabledDb(companyScopedLines);
16491
+ return {
16492
+ accessibleLineIds: scoped,
16493
+ defaultLineId: scoped[0]
16494
+ };
16495
+ }
16496
+ return { accessibleLineIds: [], defaultLineId: void 0 };
16497
+ }, [user, configLineIds, dbLines]);
16321
16498
  return { accessibleLineIds, defaultLineId, user };
16322
16499
  }
16323
16500
  function useActiveLineId(queryLineId, configLineIds) {
@@ -16509,12 +16686,7 @@ var useSupervisorsByLineIds = (lineIds, options) => {
16509
16686
  const nextSupervisorsByLineId = /* @__PURE__ */ new Map();
16510
16687
  const nextAllSupervisorsMap = /* @__PURE__ */ new Map();
16511
16688
  if (useBackend && resolvedCompanyId) {
16512
- const searchParams = new URLSearchParams({
16513
- company_id: resolvedCompanyId
16514
- });
16515
- if (lineIdsKey) {
16516
- searchParams.set("line_ids", lineIdsKey);
16517
- }
16689
+ const searchParams = new URLSearchParams({ company_id: resolvedCompanyId });
16518
16690
  const data = await fetchBackendJson(supabase, `/api/dashboard/line-supervisors?${searchParams.toString()}`);
16519
16691
  (data?.supervisors || []).forEach((row) => {
16520
16692
  const assignedLineIds = Array.isArray(row.line_ids) ? row.line_ids : [];
@@ -28334,17 +28506,76 @@ function withAccessControl(WrappedComponent2, options = {}) {
28334
28506
  const WithAccessControlComponent = (props) => {
28335
28507
  const router$1 = router.useRouter();
28336
28508
  const { user, loading: authLoading } = useAuth();
28337
- const { canAccessPage, userRole } = useAccessControl();
28338
- requiredPath || router$1.pathname;
28509
+ const role = user?.role_level;
28510
+ const scope = user?.access_scope;
28511
+ const isSuperAdmin = !!scope?.is_super_admin || user?.scope_mode === "SUPER_ADMIN";
28512
+ const getBasePath = (path) => {
28513
+ const firstSegment = path.split("?")[0].split("/").filter(Boolean)[0];
28514
+ return firstSegment ? `/${firstSegment}` : "/";
28515
+ };
28516
+ const getIdFromPath = (path, key) => {
28517
+ const queryValue = router$1.query[key];
28518
+ if (typeof queryValue === "string") return queryValue;
28519
+ if (Array.isArray(queryValue) && queryValue.length > 0) return queryValue[0];
28520
+ const segments = path.split("?")[0].split("/").filter(Boolean);
28521
+ const keyIndex = segments.findIndex((segment) => segment.toLowerCase() === key.toLowerCase());
28522
+ if (keyIndex >= 0 && keyIndex < segments.length - 1) return segments[keyIndex + 1];
28523
+ return null;
28524
+ };
28525
+ const roleAccessMap = {
28526
+ optifye: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/improvement-center", "/tickets"],
28527
+ owner: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/improvement-center", "/tickets"],
28528
+ it: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/improvement-center", "/tickets"],
28529
+ plant_head: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/improvement-center", "/tickets"],
28530
+ supervisor: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/skus", "/help", "/health", "/profile", "/workspace", "/improvement-center", "/tickets"]
28531
+ };
28532
+ const canAccessPath = (path) => {
28533
+ if (!user || !role) return false;
28534
+ if (isSuperAdmin) return true;
28535
+ const basePath = getBasePath(path);
28536
+ const allowed = roleAccessMap[role] || [];
28537
+ if (!allowed.includes(basePath)) {
28538
+ return false;
28539
+ }
28540
+ const workspaceId = getIdFromPath(path, "workspace");
28541
+ if (workspaceId && scope?.workspace_ids?.length) {
28542
+ return scope.workspace_ids.includes(workspaceId);
28543
+ }
28544
+ const lineId = getIdFromPath(path, "kpis") || getIdFromPath(path, "line");
28545
+ if (lineId && scope?.line_ids?.length) {
28546
+ return scope.line_ids.includes(lineId);
28547
+ }
28548
+ const factoryId = getIdFromPath(path, "factory");
28549
+ if (factoryId && scope?.factory_ids?.length) {
28550
+ return scope.factory_ids.includes(factoryId);
28551
+ }
28552
+ return true;
28553
+ };
28554
+ const pathToCheck = requiredPath || router$1.asPath || router$1.pathname;
28339
28555
  if (authLoading) {
28340
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-screen w-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
28341
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4" }),
28342
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600", children: "Loading..." })
28343
- ] }) });
28556
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-screen items-center justify-center bg-slate-50", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "lg", message: "Loading Dashboard..." }) });
28344
28557
  }
28345
28558
  if (!user) {
28346
28559
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-screen w-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 mb-4", children: "Please log in to continue" }) }) });
28347
28560
  }
28561
+ const hasAccess = canAccessPath(pathToCheck);
28562
+ if (!hasAccess) {
28563
+ if (UnauthorizedComponent) {
28564
+ return /* @__PURE__ */ jsxRuntime.jsx(UnauthorizedComponent, {});
28565
+ }
28566
+ if (typeof window !== "undefined") {
28567
+ router$1.replace(redirectTo);
28568
+ }
28569
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-screen w-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
28570
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-2xl font-bold text-gray-900 mb-2", children: "Access Denied" }),
28571
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 mb-4", children: "You don't have permission to access this page." }),
28572
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-500", children: [
28573
+ "Your role: ",
28574
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: role || "Unknown" })
28575
+ ] }),
28576
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-2", children: "Redirecting to home page..." })
28577
+ ] }) });
28578
+ }
28348
28579
  return /* @__PURE__ */ jsxRuntime.jsx(WrappedComponent2, { ...props });
28349
28580
  };
28350
28581
  WithAccessControlComponent.displayName = `withAccessControl(${WrappedComponent2.displayName || WrappedComponent2.name || "Component"})`;
@@ -48658,9 +48889,29 @@ var SideNavBar = React26.memo(({
48658
48889
  }) => {
48659
48890
  const router$1 = router.useRouter();
48660
48891
  const { navigate } = useNavigation();
48661
- const { signOut } = useAuth();
48892
+ const { signOut, user } = useAuth();
48662
48893
  const entityConfig = useEntityConfig();
48663
48894
  const dashboardConfig = useDashboardConfig();
48895
+ const isSuperAdmin = user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin;
48896
+ const role = user?.role_level;
48897
+ const roleAccessMap = {
48898
+ optifye: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48899
+ owner: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48900
+ it: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48901
+ plant_head: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48902
+ supervisor: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/skus", "/help", "/health", "/profile", "/workspace", "/tickets", "/improvement-center"]
48903
+ };
48904
+ const getBasePath = React26.useCallback((path) => {
48905
+ const firstSegment = path.split("?")[0].split("/").filter(Boolean)[0];
48906
+ return firstSegment ? `/${firstSegment}` : "/";
48907
+ }, []);
48908
+ const canAccessPath = React26.useCallback((path) => {
48909
+ if (!role) return false;
48910
+ if (isSuperAdmin) return true;
48911
+ const basePath = getBasePath(path);
48912
+ const allowedPaths = roleAccessMap[role] || [];
48913
+ return allowedPaths.includes(basePath);
48914
+ }, [role, isSuperAdmin, getBasePath]);
48664
48915
  const lineId = entityConfig.defaultLineId || LINE_1_UUID;
48665
48916
  const skuEnabled = dashboardConfig?.skuConfig?.enabled || false;
48666
48917
  dashboardConfig?.supervisorConfig?.enabled || false;
@@ -48826,7 +49077,7 @@ var SideNavBar = React26.memo(({
48826
49077
  const settingsTriggerRef = React26.useRef(null);
48827
49078
  const settingsItems = React26.useMemo(() => {
48828
49079
  const items = [
48829
- {
49080
+ ...canAccessPath("/targets") ? [{
48830
49081
  key: "targets",
48831
49082
  label: "Targets",
48832
49083
  icon: outline.AdjustmentsHorizontalIcon,
@@ -48835,8 +49086,8 @@ var SideNavBar = React26.memo(({
48835
49086
  setIsSettingsOpen(false);
48836
49087
  },
48837
49088
  isActive: pathname === "/targets" || pathname.startsWith("/targets/")
48838
- },
48839
- {
49089
+ }] : [],
49090
+ ...canAccessPath("/shifts") ? [{
48840
49091
  key: "shifts",
48841
49092
  label: "Shifts",
48842
49093
  icon: outline.ClockIcon,
@@ -48845,8 +49096,8 @@ var SideNavBar = React26.memo(({
48845
49096
  setIsSettingsOpen(false);
48846
49097
  },
48847
49098
  isActive: pathname === "/shifts" || pathname.startsWith("/shifts/")
48848
- },
48849
- {
49099
+ }] : [],
49100
+ ...canAccessPath("/team-management") ? [{
48850
49101
  key: "teams",
48851
49102
  label: "Teams",
48852
49103
  icon: outline.UsersIcon,
@@ -48855,8 +49106,8 @@ var SideNavBar = React26.memo(({
48855
49106
  setIsSettingsOpen(false);
48856
49107
  },
48857
49108
  isActive: pathname === "/team-management" || pathname.startsWith("/team-management/")
48858
- },
48859
- {
49109
+ }] : [],
49110
+ ...canAccessPath("/profile") ? [{
48860
49111
  key: "profile",
48861
49112
  label: "Profile",
48862
49113
  icon: outline.UserCircleIcon,
@@ -48865,9 +49116,9 @@ var SideNavBar = React26.memo(({
48865
49116
  setIsSettingsOpen(false);
48866
49117
  },
48867
49118
  isActive: pathname === "/profile" || pathname.startsWith("/profile/")
48868
- }
49119
+ }] : []
48869
49120
  ];
48870
- if (ticketsEnabled) {
49121
+ if (ticketsEnabled && canAccessPath("/tickets")) {
48871
49122
  items.push({
48872
49123
  key: "tickets",
48873
49124
  label: "Tickets",
@@ -48879,18 +49130,20 @@ var SideNavBar = React26.memo(({
48879
49130
  isActive: pathname === "/tickets" || pathname.startsWith("/tickets/")
48880
49131
  });
48881
49132
  }
48882
- items.push({
48883
- key: "help",
48884
- label: "Help",
48885
- icon: outline.QuestionMarkCircleIcon,
48886
- onClick: () => {
48887
- handleHelpClick();
48888
- setIsSettingsOpen(false);
48889
- },
48890
- isActive: pathname === "/help" || pathname.startsWith("/help/")
48891
- });
49133
+ if (canAccessPath("/help")) {
49134
+ items.push({
49135
+ key: "help",
49136
+ label: "Help",
49137
+ icon: outline.QuestionMarkCircleIcon,
49138
+ onClick: () => {
49139
+ handleHelpClick();
49140
+ setIsSettingsOpen(false);
49141
+ },
49142
+ isActive: pathname === "/help" || pathname.startsWith("/help/")
49143
+ });
49144
+ }
48892
49145
  return items;
48893
- }, [handleTargetsClick, handleShiftsClick, handleTeamManagementClick, handleProfileClick, handleTicketsClick, handleHelpClick, pathname, ticketsEnabled]);
49146
+ }, [handleTargetsClick, handleShiftsClick, handleTeamManagementClick, handleProfileClick, handleTicketsClick, handleHelpClick, pathname, ticketsEnabled, canAccessPath]);
48894
49147
  const handleLogout = React26.useCallback(async () => {
48895
49148
  setIsSettingsOpen(false);
48896
49149
  try {
@@ -48928,7 +49181,7 @@ var SideNavBar = React26.memo(({
48928
49181
  }
48929
49182
  ) }),
48930
49183
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 w-full py-6 px-4 overflow-y-auto", children: [
48931
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6", children: /* @__PURE__ */ jsxRuntime.jsxs(
49184
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6", children: canAccessPath("/") && /* @__PURE__ */ jsxRuntime.jsxs(
48932
49185
  "button",
48933
49186
  {
48934
49187
  onClick: handleHomeClick,
@@ -48944,7 +49197,7 @@ var SideNavBar = React26.memo(({
48944
49197
  }
48945
49198
  ) }),
48946
49199
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
48947
- /* @__PURE__ */ jsxRuntime.jsxs(
49200
+ canAccessPath("/leaderboard") && /* @__PURE__ */ jsxRuntime.jsxs(
48948
49201
  "button",
48949
49202
  {
48950
49203
  onClick: handleLeaderboardClick,
@@ -48959,7 +49212,7 @@ var SideNavBar = React26.memo(({
48959
49212
  ]
48960
49213
  }
48961
49214
  ),
48962
- /* @__PURE__ */ jsxRuntime.jsxs(
49215
+ canAccessPath("/kpis") && /* @__PURE__ */ jsxRuntime.jsxs(
48963
49216
  "button",
48964
49217
  {
48965
49218
  onClick: handleKPIsClick,
@@ -48974,7 +49227,7 @@ var SideNavBar = React26.memo(({
48974
49227
  ]
48975
49228
  }
48976
49229
  ),
48977
- /* @__PURE__ */ jsxRuntime.jsxs(
49230
+ canAccessPath("/improvement-center") && /* @__PURE__ */ jsxRuntime.jsxs(
48978
49231
  "button",
48979
49232
  {
48980
49233
  onClick: handleImprovementClick,
@@ -48990,7 +49243,7 @@ var SideNavBar = React26.memo(({
48990
49243
  }
48991
49244
  ),
48992
49245
  showSupervisorManagement,
48993
- skuEnabled && true && /* @__PURE__ */ jsxRuntime.jsxs(
49246
+ skuEnabled && canAccessPath("/skus") && /* @__PURE__ */ jsxRuntime.jsxs(
48994
49247
  "button",
48995
49248
  {
48996
49249
  onClick: handleSKUsClick,
@@ -49005,7 +49258,7 @@ var SideNavBar = React26.memo(({
49005
49258
  ]
49006
49259
  }
49007
49260
  ),
49008
- /* @__PURE__ */ jsxRuntime.jsxs(
49261
+ canAccessPath("/health") && /* @__PURE__ */ jsxRuntime.jsxs(
49009
49262
  "button",
49010
49263
  {
49011
49264
  onClick: handleHealthClick,
@@ -49022,7 +49275,7 @@ var SideNavBar = React26.memo(({
49022
49275
  )
49023
49276
  ] })
49024
49277
  ] }),
49025
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full py-5 px-4 border-t border-gray-100 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
49278
+ settingsItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full py-5 px-4 border-t border-gray-100 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
49026
49279
  "button",
49027
49280
  {
49028
49281
  ref: settingsTriggerRef,
@@ -49060,7 +49313,7 @@ var SideNavBar = React26.memo(({
49060
49313
  };
49061
49314
  };
49062
49315
  return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "px-5 py-6", children: [
49063
- /* @__PURE__ */ jsxRuntime.jsxs(
49316
+ canAccessPath("/") && /* @__PURE__ */ jsxRuntime.jsxs(
49064
49317
  "button",
49065
49318
  {
49066
49319
  onClick: handleMobileNavClick(handleHomeClick),
@@ -49073,7 +49326,7 @@ var SideNavBar = React26.memo(({
49073
49326
  }
49074
49327
  ),
49075
49328
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 space-y-2", children: [
49076
- /* @__PURE__ */ jsxRuntime.jsxs(
49329
+ canAccessPath("/leaderboard") && /* @__PURE__ */ jsxRuntime.jsxs(
49077
49330
  "button",
49078
49331
  {
49079
49332
  onClick: handleMobileNavClick(handleLeaderboardClick),
@@ -49085,7 +49338,7 @@ var SideNavBar = React26.memo(({
49085
49338
  ]
49086
49339
  }
49087
49340
  ),
49088
- /* @__PURE__ */ jsxRuntime.jsxs(
49341
+ canAccessPath("/kpis") && /* @__PURE__ */ jsxRuntime.jsxs(
49089
49342
  "button",
49090
49343
  {
49091
49344
  onClick: handleMobileNavClick(handleKPIsClick),
@@ -49097,7 +49350,7 @@ var SideNavBar = React26.memo(({
49097
49350
  ]
49098
49351
  }
49099
49352
  ),
49100
- /* @__PURE__ */ jsxRuntime.jsxs(
49353
+ canAccessPath("/improvement-center") && /* @__PURE__ */ jsxRuntime.jsxs(
49101
49354
  "button",
49102
49355
  {
49103
49356
  onClick: handleMobileNavClick(handleImprovementClick),
@@ -49110,7 +49363,7 @@ var SideNavBar = React26.memo(({
49110
49363
  }
49111
49364
  ),
49112
49365
  showSupervisorManagement,
49113
- skuEnabled && true && /* @__PURE__ */ jsxRuntime.jsxs(
49366
+ skuEnabled && canAccessPath("/skus") && /* @__PURE__ */ jsxRuntime.jsxs(
49114
49367
  "button",
49115
49368
  {
49116
49369
  onClick: handleMobileNavClick(handleSKUsClick),
@@ -49122,7 +49375,7 @@ var SideNavBar = React26.memo(({
49122
49375
  ]
49123
49376
  }
49124
49377
  ),
49125
- /* @__PURE__ */ jsxRuntime.jsxs(
49378
+ canAccessPath("/health") && /* @__PURE__ */ jsxRuntime.jsxs(
49126
49379
  "button",
49127
49380
  {
49128
49381
  onClick: handleMobileNavClick(handleHealthClick),
@@ -49135,10 +49388,10 @@ var SideNavBar = React26.memo(({
49135
49388
  }
49136
49389
  )
49137
49390
  ] }),
49138
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 pt-6 border-t border-gray-100", children: [
49391
+ settingsItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 pt-6 border-t border-gray-100", children: [
49139
49392
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "px-5 mb-3 text-[10px] font-bold text-gray-400 uppercase tracking-widest", children: "Settings & Support" }),
49140
49393
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
49141
- /* @__PURE__ */ jsxRuntime.jsxs(
49394
+ canAccessPath("/targets") && /* @__PURE__ */ jsxRuntime.jsxs(
49142
49395
  "button",
49143
49396
  {
49144
49397
  onClick: handleMobileNavClick(handleTargetsClick),
@@ -49150,7 +49403,7 @@ var SideNavBar = React26.memo(({
49150
49403
  ]
49151
49404
  }
49152
49405
  ),
49153
- /* @__PURE__ */ jsxRuntime.jsxs(
49406
+ canAccessPath("/shifts") && /* @__PURE__ */ jsxRuntime.jsxs(
49154
49407
  "button",
49155
49408
  {
49156
49409
  onClick: handleMobileNavClick(handleShiftsClick),
@@ -49162,7 +49415,7 @@ var SideNavBar = React26.memo(({
49162
49415
  ]
49163
49416
  }
49164
49417
  ),
49165
- /* @__PURE__ */ jsxRuntime.jsxs(
49418
+ canAccessPath("/team-management") && /* @__PURE__ */ jsxRuntime.jsxs(
49166
49419
  "button",
49167
49420
  {
49168
49421
  onClick: handleMobileNavClick(handleTeamManagementClick),
@@ -49174,7 +49427,7 @@ var SideNavBar = React26.memo(({
49174
49427
  ]
49175
49428
  }
49176
49429
  ),
49177
- /* @__PURE__ */ jsxRuntime.jsxs(
49430
+ canAccessPath("/profile") && /* @__PURE__ */ jsxRuntime.jsxs(
49178
49431
  "button",
49179
49432
  {
49180
49433
  onClick: handleMobileNavClick(handleProfileClick),
@@ -49186,7 +49439,7 @@ var SideNavBar = React26.memo(({
49186
49439
  ]
49187
49440
  }
49188
49441
  ),
49189
- ticketsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
49442
+ ticketsEnabled && canAccessPath("/tickets") && /* @__PURE__ */ jsxRuntime.jsxs(
49190
49443
  "button",
49191
49444
  {
49192
49445
  onClick: handleMobileNavClick(handleTicketsClick),
@@ -49198,7 +49451,7 @@ var SideNavBar = React26.memo(({
49198
49451
  ]
49199
49452
  }
49200
49453
  ),
49201
- /* @__PURE__ */ jsxRuntime.jsxs(
49454
+ canAccessPath("/help") && /* @__PURE__ */ jsxRuntime.jsxs(
49202
49455
  "button",
49203
49456
  {
49204
49457
  onClick: handleMobileNavClick(handleHelpClick),
@@ -51458,9 +51711,19 @@ var InviteUserDialog = ({
51458
51711
  const [factorySearch, setFactorySearch] = React26.useState("");
51459
51712
  const [isSubmitting, setIsSubmitting] = React26.useState(false);
51460
51713
  const [error, setError] = React26.useState(null);
51714
+ const isSuperAdminOptifye = user?.role_level === "optifye" && (user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin);
51715
+ const canInviteOwner = isSuperAdminOptifye;
51461
51716
  const canInviteIT = user?.role_level === "owner" || user?.role_level === "optifye";
51462
51717
  const canInvitePlantHead = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "optifye";
51463
51718
  const canInviteSupervisor = ["owner", "it", "plant_head", "optifye"].includes(user?.role_level || "");
51719
+ const invitableRoles = React26.useMemo(() => {
51720
+ const roles = [];
51721
+ if (canInviteOwner) roles.push("owner");
51722
+ if (canInviteIT) roles.push("it");
51723
+ if (canInvitePlantHead) roles.push("plant_head");
51724
+ if (canInviteSupervisor) roles.push("supervisor");
51725
+ return roles;
51726
+ }, [canInviteOwner, canInviteIT, canInvitePlantHead, canInviteSupervisor]);
51464
51727
  const filteredLines = React26.useMemo(() => {
51465
51728
  const search = lineSearch.trim().toLowerCase();
51466
51729
  if (!search) return availableLines;
@@ -51492,6 +51755,14 @@ var InviteUserDialog = ({
51492
51755
  setError(null);
51493
51756
  }
51494
51757
  }, [isOpen]);
51758
+ React26.useEffect(() => {
51759
+ if (!isOpen || invitableRoles.length === 0) {
51760
+ return;
51761
+ }
51762
+ if (!invitableRoles.includes(selectedRole)) {
51763
+ setSelectedRole(invitableRoles[0]);
51764
+ }
51765
+ }, [isOpen, invitableRoles, selectedRole]);
51495
51766
  const validateEmail = (email2) => {
51496
51767
  const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$/;
51497
51768
  return emailRegex.test(email2);
@@ -51521,6 +51792,14 @@ var InviteUserDialog = ({
51521
51792
  setError("Please enter a valid email address");
51522
51793
  return;
51523
51794
  }
51795
+ if (!invitableRoles.includes(selectedRole)) {
51796
+ setError("You do not have permission to assign this role.");
51797
+ return;
51798
+ }
51799
+ if (selectedRole === "owner" && !canInviteOwner) {
51800
+ setError("Only super-admin Optifye users can create owner users.");
51801
+ return;
51802
+ }
51524
51803
  const companyId = entityConfig?.companyId || user?.properties?.company_id;
51525
51804
  if (!companyId) {
51526
51805
  setError("Company ID not found. Please refresh and try again.");
@@ -51701,6 +51980,33 @@ var InviteUserDialog = ({
51701
51980
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500", children: "*" })
51702
51981
  ] }),
51703
51982
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
51983
+ canInviteOwner && /* @__PURE__ */ jsxRuntime.jsxs(
51984
+ "label",
51985
+ {
51986
+ className: cn(
51987
+ "flex items-start gap-3 p-4 border-2 rounded-lg cursor-pointer transition-all duration-200",
51988
+ selectedRole === "owner" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
51989
+ ),
51990
+ children: [
51991
+ /* @__PURE__ */ jsxRuntime.jsx(
51992
+ "input",
51993
+ {
51994
+ type: "radio",
51995
+ name: "role",
51996
+ value: "owner",
51997
+ checked: selectedRole === "owner",
51998
+ onChange: (e) => setSelectedRole(e.target.value),
51999
+ className: "mt-1",
52000
+ disabled: isSubmitting
52001
+ }
52002
+ ),
52003
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
52004
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsx(RoleBadge_default, { role: "owner", size: "sm" }) }),
52005
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "Company owner access. Full management permissions for the selected company dashboard." })
52006
+ ] })
52007
+ ]
52008
+ }
52009
+ ),
51704
52010
  canInviteIT && /* @__PURE__ */ jsxRuntime.jsxs(
51705
52011
  "label",
51706
52012
  {
@@ -53572,11 +53878,32 @@ var UserManagementTable = ({
53572
53878
  setSortDirection("asc");
53573
53879
  }
53574
53880
  };
53881
+ const getFactoryIdsFromUser = (user) => {
53882
+ const properties = user.properties || {};
53883
+ const rawFactoryIds = properties.factory_ids ?? properties.factory_id;
53884
+ if (Array.isArray(rawFactoryIds)) {
53885
+ return rawFactoryIds.filter((factoryId) => typeof factoryId === "string" && factoryId.length > 0);
53886
+ }
53887
+ if (typeof rawFactoryIds === "string" && rawFactoryIds.length > 0) {
53888
+ return [rawFactoryIds];
53889
+ }
53890
+ return [];
53891
+ };
53575
53892
  const formatAssignments = (user) => {
53576
53893
  if (user.role_level === "owner" || user.role_level === "it") return "Company-wide";
53577
53894
  if (user.role_level === "optifye") return "All companies";
53578
- if (user.role_level === "plant_head" && user.assigned_factories) {
53579
- return user.assigned_factories.join(", ") || "No factories assigned";
53895
+ if (user.role_level === "plant_head") {
53896
+ if (user.assigned_factories && user.assigned_factories.length > 0) {
53897
+ return user.assigned_factories.join(", ");
53898
+ }
53899
+ const assignedFactoryIds = getFactoryIdsFromUser(user);
53900
+ if (assignedFactoryIds.length === 1) {
53901
+ return "1 factory assigned";
53902
+ }
53903
+ if (assignedFactoryIds.length > 1) {
53904
+ return `${assignedFactoryIds.length} factories assigned`;
53905
+ }
53906
+ return "No factories assigned";
53580
53907
  }
53581
53908
  if (user.role_level === "supervisor" && user.assigned_lines) {
53582
53909
  return user.assigned_lines.join(", ") || "No lines assigned";
@@ -53768,22 +54095,28 @@ var UserManagementTable = ({
53768
54095
  onUpdate: onLineAssignmentUpdate || (async () => {
53769
54096
  })
53770
54097
  }
53771
- ) : user.role_level === "plant_head" ? /* @__PURE__ */ jsxRuntime.jsx(
53772
- FactoryAssignmentDropdown,
53773
- {
53774
- userId: user.user_id,
53775
- currentFactoryIds: user.properties?.factory_ids || [],
53776
- availableFactories: (
53777
- // Filter factories to only show those from the target user's company
53778
- user.properties?.company_id ? availableFactories.filter(
53779
- (factory) => factory.company_id === user.properties?.company_id
53780
- ) : availableFactories
53781
- ),
53782
- canEdit: permissions.canAssignFactories(user) && !!onFactoryAssignmentUpdate,
53783
- onUpdate: onFactoryAssignmentUpdate || (async () => {
53784
- })
54098
+ ) : user.role_level === "plant_head" ? (() => {
54099
+ const canEditFactoryAssignments = permissions.canAssignFactories(user) && !!onFactoryAssignmentUpdate;
54100
+ if (!canEditFactoryAssignments) {
54101
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) });
53785
54102
  }
53786
- ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) }) }),
54103
+ return /* @__PURE__ */ jsxRuntime.jsx(
54104
+ FactoryAssignmentDropdown,
54105
+ {
54106
+ userId: user.user_id,
54107
+ currentFactoryIds: getFactoryIdsFromUser(user),
54108
+ availableFactories: (
54109
+ // Filter factories to only show those from the target user's company
54110
+ user.properties?.company_id ? availableFactories.filter(
54111
+ (factory) => factory.company_id === user.properties?.company_id
54112
+ ) : availableFactories
54113
+ ),
54114
+ canEdit: canEditFactoryAssignments,
54115
+ onUpdate: onFactoryAssignmentUpdate || (async () => {
54116
+ })
54117
+ }
54118
+ );
54119
+ })() : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) }) }),
53787
54120
  showUsageStats && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-6 py-4", children: user.role_level === "plant_head" || user.role_level === "supervisor" ? /* @__PURE__ */ jsxRuntime.jsx(
53788
54121
  "button",
53789
54122
  {
@@ -54948,52 +55281,18 @@ function HomeView({
54948
55281
  });
54949
55282
  return merged;
54950
55283
  }, [lineNames, dbLines]);
54951
- const isOwner = user?.role_level === "owner";
54952
- const isPlantHead = user?.role_level === "plant_head";
55284
+ const enabledLineIdSet = React26.useMemo(
55285
+ () => new Set(dbLines.filter((line) => line.enable).map((line) => line.id)),
55286
+ [dbLines]
55287
+ );
54953
55288
  const isSupervisor = user?.role_level === "supervisor";
54954
- const assignedLineIds = React26.useMemo(() => {
54955
- const rawLineIds = user?.properties?.line_id || user?.properties?.line_ids || [];
54956
- return Array.isArray(rawLineIds) ? rawLineIds : [];
54957
- }, [user]);
54958
- const assignedLineIdsInConfig = React26.useMemo(() => {
54959
- if (assignedLineIds.length === 0) {
54960
- return [];
54961
- }
54962
- return assignedLineIds.filter((id3) => allLineIds.includes(id3));
54963
- }, [assignedLineIds, allLineIds]);
54964
- const assignedFactoryIds = React26.useMemo(() => {
54965
- const rawFactoryIds = user?.properties?.factory_id || user?.properties?.factory_ids || [];
54966
- return Array.isArray(rawFactoryIds) ? rawFactoryIds : [];
54967
- }, [user]);
54968
- const lineFactoryMap = React26.useMemo(() => {
54969
- const map = /* @__PURE__ */ new Map();
54970
- dbLines.forEach((line) => {
54971
- map.set(line.id, line.factory_id);
54972
- });
54973
- return map;
54974
- }, [dbLines]);
54975
- const plantHeadLineIds = React26.useMemo(() => {
54976
- if (!isPlantHead || assignedFactoryIds.length === 0) {
54977
- return [];
54978
- }
54979
- const assignedFactoryIdSet = new Set(assignedFactoryIds);
54980
- return allLineIds.filter((lineId) => assignedFactoryIdSet.has(lineFactoryMap.get(lineId) || ""));
54981
- }, [isPlantHead, assignedFactoryIds, allLineIds, lineFactoryMap]);
54982
55289
  const visibleLineIds = React26.useMemo(() => {
54983
- if (isOwner) {
54984
- return allLineIds;
54985
- }
54986
- if (isPlantHead) {
54987
- const combinedLineIds = /* @__PURE__ */ new Set();
54988
- plantHeadLineIds.forEach((lineId) => combinedLineIds.add(lineId));
54989
- assignedLineIdsInConfig.forEach((lineId) => combinedLineIds.add(lineId));
54990
- return Array.from(combinedLineIds);
55290
+ const scoped = Array.from(new Set(allLineIds.filter(Boolean)));
55291
+ if (enabledLineIdSet.size === 0) {
55292
+ return scoped;
54991
55293
  }
54992
- if (isSupervisor) {
54993
- return assignedLineIdsInConfig;
54994
- }
54995
- return allLineIds;
54996
- }, [isOwner, isPlantHead, isSupervisor, allLineIds, plantHeadLineIds, assignedLineIdsInConfig]);
55294
+ return scoped.filter((lineId) => enabledLineIdSet.has(lineId));
55295
+ }, [allLineIds, enabledLineIdSet]);
54997
55296
  const fallbackLineId = visibleLineIds[0] || defaultLineId;
54998
55297
  const defaultHomeLineId = fallbackLineId;
54999
55298
  const availableLineIds = React26.useMemo(() => {
@@ -55020,7 +55319,7 @@ function HomeView({
55020
55319
  return defaultHomeLineId;
55021
55320
  });
55022
55321
  React26.useEffect(() => {
55023
- if (!user || !isSupervisor && !isPlantHead || availableLineIds.length === 0) {
55322
+ if (!user || availableLineIds.length === 0) {
55024
55323
  return;
55025
55324
  }
55026
55325
  try {
@@ -55034,13 +55333,14 @@ function HomeView({
55034
55333
  } catch (error) {
55035
55334
  console.warn("Failed to read line filter from sessionStorage:", error);
55036
55335
  }
55336
+ if (availableLineIds.includes(selectedLineId)) {
55337
+ return;
55338
+ }
55037
55339
  if (defaultHomeLineId !== selectedLineId) {
55038
55340
  setSelectedLineId(defaultHomeLineId);
55039
55341
  }
55040
55342
  }, [
55041
55343
  user,
55042
- isSupervisor,
55043
- isPlantHead,
55044
55344
  availableLineIds,
55045
55345
  defaultHomeLineId,
55046
55346
  selectedLineId,
@@ -55059,14 +55359,14 @@ function HomeView({
55059
55359
  const [diagnoses, setDiagnoses] = React26.useState([]);
55060
55360
  const timezone = useAppTimezone();
55061
55361
  const { shiftConfigMap: lineShiftConfigs } = useMultiLineShiftConfigs(
55062
- allLineIds,
55362
+ visibleLineIds,
55063
55363
  dashboardConfig?.shiftConfig
55064
55364
  );
55065
55365
  React26.useEffect(() => {
55066
55366
  const initDisplayNames = async () => {
55067
55367
  try {
55068
55368
  if (selectedLineId === factoryViewId) {
55069
- for (const lineId of allLineIds) {
55369
+ for (const lineId of visibleLineIds) {
55070
55370
  await preInitializeWorkspaceDisplayNames(lineId);
55071
55371
  }
55072
55372
  } else {
@@ -55079,7 +55379,7 @@ function HomeView({
55079
55379
  }
55080
55380
  };
55081
55381
  initDisplayNames();
55082
- }, [selectedLineId, factoryViewId, allLineIds]);
55382
+ }, [selectedLineId, factoryViewId, visibleLineIds]);
55083
55383
  const displayNameLineId = selectedLineId === factoryViewId ? void 0 : selectedLineId;
55084
55384
  const {
55085
55385
  displayNames: workspaceDisplayNames,
@@ -58830,8 +59130,32 @@ var KPIsOverviewView = ({
58830
59130
  const dbTimezone = useAppTimezone();
58831
59131
  const configuredTimezone = dbTimezone || dateTimeConfig.defaultTimezone || "UTC";
58832
59132
  const { startDate: monthStartDate, endDate: monthEndDateKey, monthEndDate } = getMonthDateInfo(configuredTimezone);
58833
- const isSupervisor = user?.role_level === "supervisor";
58834
- const assignedLineIdsForLeaderboard = isSupervisor ? lineIds : void 0;
59133
+ const isSuperAdmin = user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin;
59134
+ const scopedLineIds = React26__namespace.default.useMemo(
59135
+ () => Array.isArray(user?.access_scope?.line_ids) ? user.access_scope.line_ids.filter((lineId) => typeof lineId === "string" && lineId.length > 0) : [],
59136
+ [user?.access_scope?.line_ids]
59137
+ );
59138
+ const hasCanonicalScope = !!user?.scope_mode || !!user?.access_scope;
59139
+ const scopeRole = (user?.role_level || user?.role || "").toLowerCase();
59140
+ const isStrictLineScopedRole = scopeRole === "supervisor" || scopeRole === "plant_head";
59141
+ const resolvedAssignedLineIds = React26__namespace.default.useMemo(() => {
59142
+ if (isSuperAdmin) return [];
59143
+ if (scopedLineIds.length > 0) return scopedLineIds;
59144
+ if (lineIds && lineIds.length > 0) return lineIds;
59145
+ if (isStrictLineScopedRole && hasCanonicalScope) return [];
59146
+ return [];
59147
+ }, [isSuperAdmin, scopedLineIds, lineIds, isStrictLineScopedRole, hasCanonicalScope]);
59148
+ const assignedLineIdSet = React26__namespace.default.useMemo(
59149
+ () => new Set(resolvedAssignedLineIds),
59150
+ [resolvedAssignedLineIds]
59151
+ );
59152
+ const metricsLineIds = React26__namespace.default.useMemo(() => {
59153
+ if (isSuperAdmin) {
59154
+ return lineIds ?? [];
59155
+ }
59156
+ return resolvedAssignedLineIds;
59157
+ }, [isSuperAdmin, lineIds, resolvedAssignedLineIds]);
59158
+ const assignedLineIdsForLeaderboard = isSuperAdmin ? void 0 : resolvedAssignedLineIds;
58835
59159
  const leaderboardLinesForView = React26__namespace.default.useMemo(() => {
58836
59160
  const targetMode = viewType === "machine" ? "uptime" : "output";
58837
59161
  return leaderboardLines.filter((line) => (line.monitoring_mode ?? "output") === targetMode);
@@ -58883,7 +59207,7 @@ var KPIsOverviewView = ({
58883
59207
  error: metricsError
58884
59208
  } = useDashboardMetrics({
58885
59209
  lineId: factoryViewId,
58886
- userAccessibleLineIds: lineIds
59210
+ userAccessibleLineIds: metricsLineIds
58887
59211
  });
58888
59212
  const defaultKPIs = React26__namespace.default.useMemo(() => createDefaultKPIs(), []);
58889
59213
  const kpisByLineId = React26__namespace.default.useMemo(() => {
@@ -58937,16 +59261,16 @@ var KPIsOverviewView = ({
58937
59261
  console.log("[KPIsOverviewView] Fetching lines with lineIds filter:", lineIds);
58938
59262
  const allLines = await dashboardService.getAllLines();
58939
59263
  let filteredLines = allLines;
58940
- if (lineIds && lineIds.length > 0) {
58941
- filteredLines = allLines.filter((line) => lineIds.includes(line.id));
58942
- console.log("[KPIsOverviewView] Filtered lines:", {
59264
+ if (!isSuperAdmin) {
59265
+ filteredLines = allLines.filter((line) => assignedLineIdSet.has(line.id));
59266
+ console.log("[KPIsOverviewView] Applied scoped line filter:", {
58943
59267
  total: allLines.length,
58944
59268
  filtered: filteredLines.length,
58945
- lineIds,
59269
+ allowedLineIds: resolvedAssignedLineIds,
58946
59270
  filteredLineIds: filteredLines.map((l) => l.id)
58947
59271
  });
58948
59272
  } else {
58949
- console.log("[KPIsOverviewView] No lineIds filter, showing all lines:", allLines.length);
59273
+ console.log("[KPIsOverviewView] Super admin view, showing all lines:", allLines.length);
58950
59274
  }
58951
59275
  setLines(filteredLines);
58952
59276
  } catch (err) {
@@ -58957,34 +59281,36 @@ var KPIsOverviewView = ({
58957
59281
  }
58958
59282
  };
58959
59283
  fetchLines();
58960
- }, [supabase, dashboardConfig, lineIds]);
59284
+ }, [supabase, dashboardConfig, lineIds, isSuperAdmin, assignedLineIdSet, resolvedAssignedLineIds]);
58961
59285
  React26.useEffect(() => {
58962
59286
  let isMounted = true;
58963
59287
  const fetchLeaderboardLines = async () => {
58964
59288
  if (!supabase || !resolvedCompanyId) {
58965
- setLeaderboardLines(lines);
59289
+ setLeaderboardLines(lines.filter((line) => line.enable !== false));
58966
59290
  return;
58967
59291
  }
58968
59292
  setLeaderboardLinesLoading(true);
58969
59293
  try {
58970
- const linesService2 = new LinesService(supabase);
58971
- const companyLines = await linesService2.getLinesByCompanyId(resolvedCompanyId);
59294
+ const data = await fetchBackendJson(
59295
+ supabase,
59296
+ `/api/dashboard/leaderboard-lines?company_id=${encodeURIComponent(resolvedCompanyId)}`
59297
+ );
58972
59298
  if (!isMounted) return;
58973
- const transformed = companyLines.map((line) => ({
59299
+ const transformed = (data.lines || []).filter((line) => line.enable !== false).map((line) => ({
58974
59300
  id: line.id,
58975
- line_name: line.name,
58976
- factory_id: line.factoryId || "",
59301
+ line_name: line.line_name,
59302
+ factory_id: line.factory_id || "",
58977
59303
  factory_name: "N/A",
58978
- company_id: line.companyId,
59304
+ company_id: line.company_id,
58979
59305
  company_name: "",
58980
- enable: line.isActive ?? true,
58981
- monitoring_mode: line.monitoringMode ?? "output"
59306
+ enable: line.enable ?? true,
59307
+ monitoring_mode: line.monitoring_mode ?? "output"
58982
59308
  }));
58983
59309
  setLeaderboardLines(transformed);
58984
59310
  } catch (err) {
58985
59311
  console.error("[KPIsOverviewView] Failed to load leaderboard lines:", err);
58986
59312
  if (!isMounted) return;
58987
- setLeaderboardLines(lines);
59313
+ setLeaderboardLines(lines.filter((line) => line.enable !== false));
58988
59314
  } finally {
58989
59315
  if (isMounted) setLeaderboardLinesLoading(false);
58990
59316
  }
@@ -59065,11 +59391,10 @@ var KPIsOverviewView = ({
59065
59391
  lineMode: viewType === "machine" ? "uptime" : "output"
59066
59392
  });
59067
59393
  const nextMap = /* @__PURE__ */ new Map();
59394
+ targetLineIds.forEach((lineId) => nextMap.set(lineId, 0));
59068
59395
  entries.forEach((entry) => {
59069
59396
  const value = Number(entry.avg_efficiency);
59070
- if (Number.isFinite(value)) {
59071
- nextMap.set(entry.line_id, value);
59072
- }
59397
+ nextMap.set(entry.line_id, Number.isFinite(value) ? value : 0);
59073
59398
  });
59074
59399
  setTodayEfficiencyByLineId(nextMap);
59075
59400
  } catch (err) {
@@ -59152,6 +59477,9 @@ var KPIsOverviewView = ({
59152
59477
  };
59153
59478
  };
59154
59479
  const handleLineClick = (line, kpis) => {
59480
+ if (!isSuperAdmin && !assignedLineIdSet.has(line.id)) {
59481
+ return;
59482
+ }
59155
59483
  const trackProps = {
59156
59484
  line_id: line.id,
59157
59485
  line_name: line.line_name,
@@ -59664,6 +59992,7 @@ var MobileWorkspaceCard = React26.memo(({
59664
59992
  workspace,
59665
59993
  rank,
59666
59994
  cardClass,
59995
+ isClickable,
59667
59996
  onWorkspaceClick,
59668
59997
  getMedalIcon,
59669
59998
  efficiencyLabel
@@ -59676,8 +60005,8 @@ var MobileWorkspaceCard = React26.memo(({
59676
60005
  transition: {
59677
60006
  layout: { duration: 0.3, ease: "easeInOut" }
59678
60007
  },
59679
- onClick: () => onWorkspaceClick(workspace, rank),
59680
- className: `${cardClass} p-3 rounded-lg border shadow-sm active:scale-[0.98] cursor-pointer`,
60008
+ onClick: isClickable ? () => onWorkspaceClick(workspace, rank) : void 0,
60009
+ className: `${cardClass} p-3 rounded-lg border shadow-sm active:scale-[0.98] ${isClickable ? "cursor-pointer" : "cursor-not-allowed opacity-75"}`,
59681
60010
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
59682
60011
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
59683
60012
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
@@ -59699,13 +60028,14 @@ var MobileWorkspaceCard = React26.memo(({
59699
60028
  ] })
59700
60029
  }
59701
60030
  ), (prevProps, nextProps) => {
59702
- return prevProps.efficiencyLabel === nextProps.efficiencyLabel && prevProps.rank === nextProps.rank && prevProps.cardClass === nextProps.cardClass && prevProps.workspace.workspace_uuid === nextProps.workspace.workspace_uuid && prevProps.workspace.efficiency === nextProps.workspace.efficiency && prevProps.workspace.action_count === nextProps.workspace.action_count && prevProps.workspace.action_threshold === nextProps.workspace.action_threshold && prevProps.workspace.avg_cycle_time === nextProps.workspace.avg_cycle_time && prevProps.workspace.displayName === nextProps.workspace.displayName && prevProps.workspace.lineName === nextProps.workspace.lineName;
60031
+ return prevProps.efficiencyLabel === nextProps.efficiencyLabel && prevProps.rank === nextProps.rank && prevProps.cardClass === nextProps.cardClass && prevProps.isClickable === nextProps.isClickable && prevProps.workspace.workspace_uuid === nextProps.workspace.workspace_uuid && prevProps.workspace.efficiency === nextProps.workspace.efficiency && prevProps.workspace.action_count === nextProps.workspace.action_count && prevProps.workspace.action_threshold === nextProps.workspace.action_threshold && prevProps.workspace.avg_cycle_time === nextProps.workspace.avg_cycle_time && prevProps.workspace.displayName === nextProps.workspace.displayName && prevProps.workspace.lineName === nextProps.workspace.lineName;
59703
60032
  });
59704
60033
  MobileWorkspaceCard.displayName = "MobileWorkspaceCard";
59705
60034
  var DesktopWorkspaceRow = React26.memo(({
59706
60035
  workspace,
59707
60036
  index,
59708
60037
  rowClass,
60038
+ isClickable,
59709
60039
  onWorkspaceClick,
59710
60040
  getMedalIcon
59711
60041
  }) => /* @__PURE__ */ jsxRuntime.jsxs(
@@ -59715,8 +60045,8 @@ var DesktopWorkspaceRow = React26.memo(({
59715
60045
  layoutId: `row-${workspace.workspace_uuid}`,
59716
60046
  initial: false,
59717
60047
  transition: { layout: { duration: 0.3, ease: "easeInOut" } },
59718
- onClick: () => onWorkspaceClick(workspace, index + 1),
59719
- className: `${rowClass} hover:bg-gray-50/90 transition-colors duration-150 cursor-pointer group`,
60048
+ onClick: isClickable ? () => onWorkspaceClick(workspace, index + 1) : void 0,
60049
+ className: `${rowClass} transition-colors duration-150 ${isClickable ? "hover:bg-gray-50/90 cursor-pointer group" : "cursor-not-allowed opacity-75"}`,
59720
60050
  children: [
59721
60051
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2.5 sm:p-4 text-sm sm:text-base whitespace-nowrap group-hover:font-medium", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
59722
60052
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: index + 1 }),
@@ -59728,7 +60058,7 @@ var DesktopWorkspaceRow = React26.memo(({
59728
60058
  ]
59729
60059
  }
59730
60060
  ), (prevProps, nextProps) => {
59731
- return prevProps.index === nextProps.index && prevProps.rowClass === nextProps.rowClass && prevProps.workspace.workspace_uuid === nextProps.workspace.workspace_uuid && prevProps.workspace.efficiency === nextProps.workspace.efficiency && prevProps.workspace.displayName === nextProps.workspace.displayName && prevProps.workspace.lineName === nextProps.workspace.lineName;
60061
+ return prevProps.index === nextProps.index && prevProps.rowClass === nextProps.rowClass && prevProps.isClickable === nextProps.isClickable && prevProps.workspace.workspace_uuid === nextProps.workspace.workspace_uuid && prevProps.workspace.efficiency === nextProps.workspace.efficiency && prevProps.workspace.displayName === nextProps.workspace.displayName && prevProps.workspace.lineName === nextProps.workspace.lineName;
59732
60062
  });
59733
60063
  DesktopWorkspaceRow.displayName = "DesktopWorkspaceRow";
59734
60064
  var LeaderboardDetailView = React26.memo(({
@@ -59888,6 +60218,16 @@ var LeaderboardDetailView = React26.memo(({
59888
60218
  }
59889
60219
  return allLineIds;
59890
60220
  }, [entityConfig, userAccessibleLineIds]);
60221
+ const accessibleLineIdSet = React26.useMemo(
60222
+ () => new Set((userAccessibleLineIds || []).filter(Boolean)),
60223
+ [userAccessibleLineIds]
60224
+ );
60225
+ const canOpenWorkspace = React26.useCallback((workspaceLineId) => {
60226
+ if (!workspaceLineId) return false;
60227
+ if (!userAccessibleLineIds) return true;
60228
+ if (accessibleLineIdSet.size === 0) return false;
60229
+ return accessibleLineIdSet.has(workspaceLineId);
60230
+ }, [accessibleLineIdSet, userAccessibleLineIds]);
59891
60231
  const { hasUptime: lineModeHasUptime, hasOutput: lineModeHasOutput } = React26.useMemo(() => {
59892
60232
  if (!lines || lines.length === 0) {
59893
60233
  return { hasUptime: false, hasOutput: false };
@@ -60285,6 +60625,9 @@ var LeaderboardDetailView = React26.memo(({
60285
60625
  return `${startDate.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${endDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}`;
60286
60626
  }, [normalizedRange]);
60287
60627
  const handleWorkspaceClick = React26.useCallback((workspace, rank) => {
60628
+ if (!canOpenWorkspace(workspace.line_id)) {
60629
+ return;
60630
+ }
60288
60631
  trackCoreEvent("Workspace from Leaderboard Clicked", {
60289
60632
  workspace_name: workspace.workspace_name,
60290
60633
  workspace_id: workspace.workspace_uuid,
@@ -60317,7 +60660,7 @@ var LeaderboardDetailView = React26.memo(({
60317
60660
  const combinedParams = navParams ? `${navParams}&${contextParamString}` : `?${contextParamString}`;
60318
60661
  navigation.navigate(`/workspace/${workspace.workspace_uuid}${combinedParams}`);
60319
60662
  }
60320
- }, [onWorkspaceClick, navigation, date, shiftId]);
60663
+ }, [canOpenWorkspace, onWorkspaceClick, navigation, date, shiftId]);
60321
60664
  React26.useEffect(() => {
60322
60665
  workspacesLengthRef.current = activeEntries.length || 0;
60323
60666
  }, [activeEntries.length]);
@@ -60608,6 +60951,7 @@ var LeaderboardDetailView = React26.memo(({
60608
60951
  workspace: ws,
60609
60952
  rank,
60610
60953
  cardClass,
60954
+ isClickable: canOpenWorkspace(ws.line_id),
60611
60955
  onWorkspaceClick: stableHandleWorkspaceClick,
60612
60956
  getMedalIcon: stableGetMedalIcon,
60613
60957
  efficiencyLabel
@@ -60632,6 +60976,7 @@ var LeaderboardDetailView = React26.memo(({
60632
60976
  workspace: ws,
60633
60977
  index,
60634
60978
  rowClass,
60979
+ isClickable: canOpenWorkspace(ws.line_id),
60635
60980
  onWorkspaceClick: stableHandleWorkspaceClick,
60636
60981
  getMedalIcon: stableGetMedalIcon
60637
60982
  },
@@ -60642,7 +60987,7 @@ var LeaderboardDetailView = React26.memo(({
60642
60987
  ) })
60643
60988
  ] });
60644
60989
  }, (prevProps, nextProps) => {
60645
- return prevProps.lineId === nextProps.lineId && prevProps.date === nextProps.date && prevProps.shift === nextProps.shift && prevProps.line1Id === nextProps.line1Id && prevProps.line2Id === nextProps.line2Id && JSON.stringify(prevProps.lineNames) === JSON.stringify(nextProps.lineNames) && prevProps.className === nextProps.className;
60990
+ return prevProps.lineId === nextProps.lineId && prevProps.date === nextProps.date && prevProps.shift === nextProps.shift && prevProps.line1Id === nextProps.line1Id && prevProps.line2Id === nextProps.line2Id && JSON.stringify(prevProps.lineNames) === JSON.stringify(nextProps.lineNames) && JSON.stringify(prevProps.userAccessibleLineIds) === JSON.stringify(nextProps.userAccessibleLineIds) && prevProps.className === nextProps.className;
60646
60991
  });
60647
60992
  LeaderboardDetailView.displayName = "LeaderboardDetailView";
60648
60993
  var LeaderboardDetailViewWithDisplayNames = withAllWorkspaceDisplayNames(LeaderboardDetailView);
@@ -66403,9 +66748,38 @@ var TeamManagementView = ({
66403
66748
  optifye: 0
66404
66749
  });
66405
66750
  const [isAddUserDialogOpen, setIsAddUserDialogOpen] = React26.useState(false);
66751
+ const normalizeIds = React26.useCallback((value) => {
66752
+ if (Array.isArray(value)) {
66753
+ return value.filter((id3) => typeof id3 === "string" && id3.length > 0);
66754
+ }
66755
+ if (typeof value === "string" && value.length > 0) {
66756
+ return [value];
66757
+ }
66758
+ return [];
66759
+ }, []);
66760
+ const plantHeadFactoryIds = React26.useMemo(() => {
66761
+ if (user?.role_level !== "plant_head") return [];
66762
+ const scopedFactoryIds = normalizeIds(user?.access_scope?.factory_ids);
66763
+ if (scopedFactoryIds.length > 0) {
66764
+ return Array.from(new Set(scopedFactoryIds));
66765
+ }
66766
+ const propertyFactoryIds = normalizeIds(
66767
+ user?.properties?.factory_ids ?? user?.properties?.factory_id
66768
+ );
66769
+ if (propertyFactoryIds.length > 0) {
66770
+ return Array.from(new Set(propertyFactoryIds));
66771
+ }
66772
+ return entityConfig?.factoryId ? [entityConfig.factoryId] : [];
66773
+ }, [user, entityConfig?.factoryId, normalizeIds]);
66774
+ const notifyScopeRefresh = React26.useCallback(() => {
66775
+ if (typeof window !== "undefined") {
66776
+ window.dispatchEvent(new Event("rbac:refresh-scope"));
66777
+ }
66778
+ }, []);
66406
66779
  const canAddUsers = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "plant_head" || user?.role_level === "optifye";
66407
66780
  const canViewUsageStats = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "optifye";
66408
- const companyIdForUsage = entityConfig?.companyId || user?.properties?.company_id;
66781
+ const pageCompanyId = entityConfig?.companyId || user?.properties?.company_id;
66782
+ const companyIdForUsage = pageCompanyId;
66409
66783
  const usageDateRange = React26.useMemo(() => {
66410
66784
  const today = /* @__PURE__ */ new Date();
66411
66785
  const dayOfWeek = today.getDay();
@@ -66436,8 +66810,8 @@ var TeamManagementView = ({
66436
66810
  return acc;
66437
66811
  }, {});
66438
66812
  }, [usageData, usageDateRange.daysElapsed]);
66439
- const pageTitle = user?.role_level === "optifye" ? "Platform Users" : "Team Management";
66440
- const pageDescription = user?.role_level === "optifye" ? "Manage users across all companies" : user?.role_level === "owner" || user?.role_level === "it" ? "Manage team members and view their daily usage" : "Manage supervisors in your factory";
66813
+ const pageTitle = "Team Management";
66814
+ const pageDescription = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "optifye" ? "Manage team members and view their daily usage" : "Manage supervisors in your factory";
66441
66815
  const hasAccess = user ? user.role_level === void 0 || ["optifye", "owner", "it", "plant_head"].includes(user.role_level) : false;
66442
66816
  const loadData = React26.useCallback(async () => {
66443
66817
  if (!supabase) {
@@ -66445,20 +66819,16 @@ var TeamManagementView = ({
66445
66819
  setIsLoading(false);
66446
66820
  return;
66447
66821
  }
66448
- const isOptifyeUser = user?.role_level === "optifye";
66449
- if (!isOptifyeUser) {
66450
- const companyId2 = entityConfig?.companyId || user?.properties?.company_id;
66451
- if (!companyId2) {
66452
- setError("Company not found. Please contact your administrator.");
66453
- setIsLoading(false);
66454
- return;
66455
- }
66822
+ const companyId = pageCompanyId;
66823
+ if (!companyId) {
66824
+ setError("Company not found. Please contact your administrator.");
66825
+ setIsLoading(false);
66826
+ return;
66456
66827
  }
66457
- const companyId = entityConfig?.companyId || user?.properties?.company_id;
66458
66828
  console.log("[TeamManagementView] Loading data:", {
66459
66829
  role: user?.role_level,
66460
- isOptifye: isOptifyeUser,
66461
- companyId: isOptifyeUser ? "ALL" : companyId
66830
+ isOptifye: user?.role_level === "optifye",
66831
+ companyId
66462
66832
  });
66463
66833
  setIsLoading(true);
66464
66834
  setError(void 0);
@@ -66473,23 +66843,7 @@ var TeamManagementView = ({
66473
66843
  if (!backendUrl) {
66474
66844
  throw new Error("Backend URL is not configured. Please set NEXT_PUBLIC_BACKEND_URL in your environment.");
66475
66845
  }
66476
- if (isOptifyeUser) {
66477
- const { data: factories } = await supabase.from("factories").select("id, factory_name, company_id").order("factory_name");
66478
- const linesResponse = await fetch(`${backendUrl}/api/lines`, {
66479
- headers: {
66480
- "Authorization": `Bearer ${token}`,
66481
- "Content-Type": "application/json"
66482
- }
66483
- });
66484
- if (!linesResponse.ok) {
66485
- throw new Error(`Failed to fetch lines: ${linesResponse.statusText}`);
66486
- }
66487
- const linesData = await linesResponse.json();
66488
- const lines = linesData.lines || [];
66489
- setAvailableFactories(factories || []);
66490
- setAvailableLines(lines);
66491
- console.log("[TeamManagementView] Optifye - Loaded factories:", factories?.length, "lines:", lines?.length, "Sample lines:", lines?.slice(0, 3));
66492
- } else if ((user?.role_level === "owner" || user?.role_level === "it") && companyId) {
66846
+ if ((user?.role_level === "optifye" || user?.role_level === "owner" || user?.role_level === "it") && companyId) {
66493
66847
  const { data: factories } = await supabase.from("factories").select("id, factory_name, company_id").eq("company_id", companyId).order("factory_name");
66494
66848
  const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
66495
66849
  headers: {
@@ -66504,9 +66858,8 @@ var TeamManagementView = ({
66504
66858
  const lines = linesData.lines || [];
66505
66859
  setAvailableFactories(factories || []);
66506
66860
  setAvailableLines(lines);
66507
- console.log("[TeamManagementView] Owner/IT - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
66861
+ console.log("[TeamManagementView] Company-scoped team view - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
66508
66862
  } else if (user?.role_level === "plant_head") {
66509
- const plantHeadFactoryIds = user?.properties?.factory_ids || [];
66510
66863
  if (plantHeadFactoryIds.length > 0) {
66511
66864
  if (companyId) {
66512
66865
  const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
@@ -66559,35 +66912,46 @@ var TeamManagementView = ({
66559
66912
  setAvailableFactories([]);
66560
66913
  console.log("[TeamManagementView] Fallback - Company:", companyId, "Loaded lines:", lines?.length);
66561
66914
  }
66562
- if (isOptifyeUser) {
66563
- const [usersData, userStats] = await Promise.all([
66564
- userManagementService.getAllUsers(),
66565
- userManagementService.getAllUserStats()
66566
- ]);
66567
- setUsers(usersData);
66568
- setStats({
66569
- totalUsers: userStats.total,
66570
- owners: userStats.owners,
66571
- it: userStats.it,
66572
- plantHeads: userStats.plant_heads,
66573
- supervisors: userStats.supervisors,
66574
- optifye: userStats.optifye
66575
- });
66576
- } else {
66577
- const [usersData, userStats] = await Promise.all([
66578
- user?.role_level === "plant_head" && entityConfig?.factoryId ? userManagementService.getFactoryUsers(entityConfig.factoryId) : userManagementService.getCompanyUsers(companyId),
66579
- userManagementService.getUserStats(companyId)
66580
- ]);
66581
- setUsers(usersData);
66582
- setStats({
66583
- totalUsers: userStats.total,
66584
- owners: userStats.owners,
66585
- it: userStats.it,
66586
- plantHeads: userStats.plant_heads,
66587
- supervisors: userStats.supervisors,
66588
- optifye: 0
66589
- });
66590
- }
66915
+ const usersPromise = user?.role_level === "plant_head" ? (async () => {
66916
+ if (plantHeadFactoryIds.length === 0) {
66917
+ return [];
66918
+ }
66919
+ const results = await Promise.allSettled(
66920
+ plantHeadFactoryIds.map((factoryId) => userManagementService.getFactoryUsers(factoryId))
66921
+ );
66922
+ const successful = results.filter(
66923
+ (result) => result.status === "fulfilled"
66924
+ ).flatMap((result) => result.value || []);
66925
+ if (successful.length > 0) {
66926
+ const byUserId = /* @__PURE__ */ new Map();
66927
+ successful.forEach((u) => {
66928
+ if (u?.user_id) {
66929
+ byUserId.set(u.user_id, u);
66930
+ }
66931
+ });
66932
+ return Array.from(byUserId.values());
66933
+ }
66934
+ const firstRejected = results.find(
66935
+ (result) => result.status === "rejected"
66936
+ );
66937
+ if (firstRejected) {
66938
+ throw firstRejected.reason;
66939
+ }
66940
+ return [];
66941
+ })() : userManagementService.getCompanyUsers(companyId);
66942
+ const [usersData, userStats] = await Promise.all([
66943
+ usersPromise,
66944
+ userManagementService.getUserStats(companyId)
66945
+ ]);
66946
+ setUsers(usersData);
66947
+ setStats({
66948
+ totalUsers: userStats.total,
66949
+ owners: userStats.owners,
66950
+ it: userStats.it,
66951
+ plantHeads: userStats.plant_heads,
66952
+ supervisors: userStats.supervisors,
66953
+ optifye: 0
66954
+ });
66591
66955
  } catch (err) {
66592
66956
  console.error("Error loading team management data:", err);
66593
66957
  setError(err instanceof Error ? err.message : "Failed to load data");
@@ -66595,16 +66959,16 @@ var TeamManagementView = ({
66595
66959
  } finally {
66596
66960
  setIsLoading(false);
66597
66961
  }
66598
- }, [supabase, user, entityConfig]);
66962
+ }, [supabase, user, pageCompanyId, entityConfig?.factoryId, plantHeadFactoryIds]);
66599
66963
  React26.useEffect(() => {
66600
- const companyId = entityConfig?.companyId || user?.properties?.company_id;
66601
- const canLoad = hasAccess && user && (user.role_level === "optifye" || !!companyId);
66964
+ const companyId = pageCompanyId;
66965
+ const canLoad = hasAccess && user && !!companyId;
66602
66966
  if (canLoad) {
66603
66967
  loadData();
66604
66968
  } else if (!user) {
66605
66969
  setIsLoading(true);
66606
66970
  }
66607
- }, [hasAccess, loadData, user, entityConfig?.companyId]);
66971
+ }, [hasAccess, loadData, user, pageCompanyId]);
66608
66972
  const handleUserAdded = React26.useCallback(() => {
66609
66973
  loadData();
66610
66974
  }, [loadData]);
@@ -66618,12 +66982,13 @@ var TeamManagementView = ({
66618
66982
  updated_by: user.id
66619
66983
  });
66620
66984
  sonner.toast.success("User role updated successfully");
66985
+ notifyScopeRefresh();
66621
66986
  loadData();
66622
66987
  } catch (err) {
66623
66988
  console.error("Error updating user role:", err);
66624
66989
  sonner.toast.error("Failed to update user role");
66625
66990
  }
66626
- }, [supabase, user, loadData]);
66991
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66627
66992
  const handleRemoveUser = React26.useCallback(async (userId) => {
66628
66993
  if (!supabase || !user) return;
66629
66994
  try {
@@ -66646,12 +67011,13 @@ var TeamManagementView = ({
66646
67011
  assigned_by: user.id
66647
67012
  });
66648
67013
  sonner.toast.success("Line assignments updated successfully");
67014
+ notifyScopeRefresh();
66649
67015
  loadData();
66650
67016
  } catch (err) {
66651
67017
  console.error("Error updating line assignments:", err);
66652
67018
  sonner.toast.error("Failed to update line assignments");
66653
67019
  }
66654
- }, [supabase, user, loadData]);
67020
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66655
67021
  const handleFactoryAssignmentUpdate = React26.useCallback(async (userId, factoryIds) => {
66656
67022
  if (!supabase || !user) return;
66657
67023
  try {
@@ -66662,12 +67028,13 @@ var TeamManagementView = ({
66662
67028
  assigned_by: user.id
66663
67029
  });
66664
67030
  sonner.toast.success("Factory assignments updated successfully");
67031
+ notifyScopeRefresh();
66665
67032
  loadData();
66666
67033
  } catch (err) {
66667
67034
  console.error("Error updating factory assignments:", err);
66668
67035
  sonner.toast.error("Failed to update factory assignments");
66669
67036
  }
66670
- }, [supabase, user, loadData]);
67037
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66671
67038
  const handleProfileUpdate = React26.useCallback(async (userId, firstName, lastName, profilePhotoUrl) => {
66672
67039
  if (!supabase || !user) return;
66673
67040
  try {