@optifye/dashboard-core 6.10.47 → 6.10.48

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,8 +28506,52 @@ 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
28556
  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
28557
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4" }),
@@ -28345,6 +28561,24 @@ function withAccessControl(WrappedComponent2, options = {}) {
28345
28561
  if (!user) {
28346
28562
  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
28563
  }
28564
+ const hasAccess = canAccessPath(pathToCheck);
28565
+ if (!hasAccess) {
28566
+ if (UnauthorizedComponent) {
28567
+ return /* @__PURE__ */ jsxRuntime.jsx(UnauthorizedComponent, {});
28568
+ }
28569
+ if (typeof window !== "undefined") {
28570
+ router$1.replace(redirectTo);
28571
+ }
28572
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-screen w-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
28573
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-2xl font-bold text-gray-900 mb-2", children: "Access Denied" }),
28574
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 mb-4", children: "You don't have permission to access this page." }),
28575
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-500", children: [
28576
+ "Your role: ",
28577
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: role || "Unknown" })
28578
+ ] }),
28579
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-2", children: "Redirecting to home page..." })
28580
+ ] }) });
28581
+ }
28348
28582
  return /* @__PURE__ */ jsxRuntime.jsx(WrappedComponent2, { ...props });
28349
28583
  };
28350
28584
  WithAccessControlComponent.displayName = `withAccessControl(${WrappedComponent2.displayName || WrappedComponent2.name || "Component"})`;
@@ -48658,9 +48892,29 @@ var SideNavBar = React26.memo(({
48658
48892
  }) => {
48659
48893
  const router$1 = router.useRouter();
48660
48894
  const { navigate } = useNavigation();
48661
- const { signOut } = useAuth();
48895
+ const { signOut, user } = useAuth();
48662
48896
  const entityConfig = useEntityConfig();
48663
48897
  const dashboardConfig = useDashboardConfig();
48898
+ const isSuperAdmin = user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin;
48899
+ const role = user?.role_level;
48900
+ const roleAccessMap = {
48901
+ optifye: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48902
+ owner: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48903
+ it: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48904
+ plant_head: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/supervisor-management", "/skus", "/help", "/health", "/profile", "/workspace", "/factory-view", "/team-management", "/tickets", "/improvement-center"],
48905
+ supervisor: ["/", "/leaderboard", "/kpis", "/targets", "/shifts", "/skus", "/help", "/health", "/profile", "/workspace", "/tickets", "/improvement-center"]
48906
+ };
48907
+ const getBasePath = React26.useCallback((path) => {
48908
+ const firstSegment = path.split("?")[0].split("/").filter(Boolean)[0];
48909
+ return firstSegment ? `/${firstSegment}` : "/";
48910
+ }, []);
48911
+ const canAccessPath = React26.useCallback((path) => {
48912
+ if (!role) return false;
48913
+ if (isSuperAdmin) return true;
48914
+ const basePath = getBasePath(path);
48915
+ const allowedPaths = roleAccessMap[role] || [];
48916
+ return allowedPaths.includes(basePath);
48917
+ }, [role, isSuperAdmin, getBasePath]);
48664
48918
  const lineId = entityConfig.defaultLineId || LINE_1_UUID;
48665
48919
  const skuEnabled = dashboardConfig?.skuConfig?.enabled || false;
48666
48920
  dashboardConfig?.supervisorConfig?.enabled || false;
@@ -48826,7 +49080,7 @@ var SideNavBar = React26.memo(({
48826
49080
  const settingsTriggerRef = React26.useRef(null);
48827
49081
  const settingsItems = React26.useMemo(() => {
48828
49082
  const items = [
48829
- {
49083
+ ...canAccessPath("/targets") ? [{
48830
49084
  key: "targets",
48831
49085
  label: "Targets",
48832
49086
  icon: outline.AdjustmentsHorizontalIcon,
@@ -48835,8 +49089,8 @@ var SideNavBar = React26.memo(({
48835
49089
  setIsSettingsOpen(false);
48836
49090
  },
48837
49091
  isActive: pathname === "/targets" || pathname.startsWith("/targets/")
48838
- },
48839
- {
49092
+ }] : [],
49093
+ ...canAccessPath("/shifts") ? [{
48840
49094
  key: "shifts",
48841
49095
  label: "Shifts",
48842
49096
  icon: outline.ClockIcon,
@@ -48845,8 +49099,8 @@ var SideNavBar = React26.memo(({
48845
49099
  setIsSettingsOpen(false);
48846
49100
  },
48847
49101
  isActive: pathname === "/shifts" || pathname.startsWith("/shifts/")
48848
- },
48849
- {
49102
+ }] : [],
49103
+ ...canAccessPath("/team-management") ? [{
48850
49104
  key: "teams",
48851
49105
  label: "Teams",
48852
49106
  icon: outline.UsersIcon,
@@ -48855,8 +49109,8 @@ var SideNavBar = React26.memo(({
48855
49109
  setIsSettingsOpen(false);
48856
49110
  },
48857
49111
  isActive: pathname === "/team-management" || pathname.startsWith("/team-management/")
48858
- },
48859
- {
49112
+ }] : [],
49113
+ ...canAccessPath("/profile") ? [{
48860
49114
  key: "profile",
48861
49115
  label: "Profile",
48862
49116
  icon: outline.UserCircleIcon,
@@ -48865,9 +49119,9 @@ var SideNavBar = React26.memo(({
48865
49119
  setIsSettingsOpen(false);
48866
49120
  },
48867
49121
  isActive: pathname === "/profile" || pathname.startsWith("/profile/")
48868
- }
49122
+ }] : []
48869
49123
  ];
48870
- if (ticketsEnabled) {
49124
+ if (ticketsEnabled && canAccessPath("/tickets")) {
48871
49125
  items.push({
48872
49126
  key: "tickets",
48873
49127
  label: "Tickets",
@@ -48879,18 +49133,20 @@ var SideNavBar = React26.memo(({
48879
49133
  isActive: pathname === "/tickets" || pathname.startsWith("/tickets/")
48880
49134
  });
48881
49135
  }
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
- });
49136
+ if (canAccessPath("/help")) {
49137
+ items.push({
49138
+ key: "help",
49139
+ label: "Help",
49140
+ icon: outline.QuestionMarkCircleIcon,
49141
+ onClick: () => {
49142
+ handleHelpClick();
49143
+ setIsSettingsOpen(false);
49144
+ },
49145
+ isActive: pathname === "/help" || pathname.startsWith("/help/")
49146
+ });
49147
+ }
48892
49148
  return items;
48893
- }, [handleTargetsClick, handleShiftsClick, handleTeamManagementClick, handleProfileClick, handleTicketsClick, handleHelpClick, pathname, ticketsEnabled]);
49149
+ }, [handleTargetsClick, handleShiftsClick, handleTeamManagementClick, handleProfileClick, handleTicketsClick, handleHelpClick, pathname, ticketsEnabled, canAccessPath]);
48894
49150
  const handleLogout = React26.useCallback(async () => {
48895
49151
  setIsSettingsOpen(false);
48896
49152
  try {
@@ -48928,7 +49184,7 @@ var SideNavBar = React26.memo(({
48928
49184
  }
48929
49185
  ) }),
48930
49186
  /* @__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(
49187
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6", children: canAccessPath("/") && /* @__PURE__ */ jsxRuntime.jsxs(
48932
49188
  "button",
48933
49189
  {
48934
49190
  onClick: handleHomeClick,
@@ -48944,7 +49200,7 @@ var SideNavBar = React26.memo(({
48944
49200
  }
48945
49201
  ) }),
48946
49202
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
48947
- /* @__PURE__ */ jsxRuntime.jsxs(
49203
+ canAccessPath("/leaderboard") && /* @__PURE__ */ jsxRuntime.jsxs(
48948
49204
  "button",
48949
49205
  {
48950
49206
  onClick: handleLeaderboardClick,
@@ -48959,7 +49215,7 @@ var SideNavBar = React26.memo(({
48959
49215
  ]
48960
49216
  }
48961
49217
  ),
48962
- /* @__PURE__ */ jsxRuntime.jsxs(
49218
+ canAccessPath("/kpis") && /* @__PURE__ */ jsxRuntime.jsxs(
48963
49219
  "button",
48964
49220
  {
48965
49221
  onClick: handleKPIsClick,
@@ -48974,7 +49230,7 @@ var SideNavBar = React26.memo(({
48974
49230
  ]
48975
49231
  }
48976
49232
  ),
48977
- /* @__PURE__ */ jsxRuntime.jsxs(
49233
+ canAccessPath("/improvement-center") && /* @__PURE__ */ jsxRuntime.jsxs(
48978
49234
  "button",
48979
49235
  {
48980
49236
  onClick: handleImprovementClick,
@@ -48990,7 +49246,7 @@ var SideNavBar = React26.memo(({
48990
49246
  }
48991
49247
  ),
48992
49248
  showSupervisorManagement,
48993
- skuEnabled && true && /* @__PURE__ */ jsxRuntime.jsxs(
49249
+ skuEnabled && canAccessPath("/skus") && /* @__PURE__ */ jsxRuntime.jsxs(
48994
49250
  "button",
48995
49251
  {
48996
49252
  onClick: handleSKUsClick,
@@ -49005,7 +49261,7 @@ var SideNavBar = React26.memo(({
49005
49261
  ]
49006
49262
  }
49007
49263
  ),
49008
- /* @__PURE__ */ jsxRuntime.jsxs(
49264
+ canAccessPath("/health") && /* @__PURE__ */ jsxRuntime.jsxs(
49009
49265
  "button",
49010
49266
  {
49011
49267
  onClick: handleHealthClick,
@@ -49022,7 +49278,7 @@ var SideNavBar = React26.memo(({
49022
49278
  )
49023
49279
  ] })
49024
49280
  ] }),
49025
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full py-5 px-4 border-t border-gray-100 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
49281
+ 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
49282
  "button",
49027
49283
  {
49028
49284
  ref: settingsTriggerRef,
@@ -49060,7 +49316,7 @@ var SideNavBar = React26.memo(({
49060
49316
  };
49061
49317
  };
49062
49318
  return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "px-5 py-6", children: [
49063
- /* @__PURE__ */ jsxRuntime.jsxs(
49319
+ canAccessPath("/") && /* @__PURE__ */ jsxRuntime.jsxs(
49064
49320
  "button",
49065
49321
  {
49066
49322
  onClick: handleMobileNavClick(handleHomeClick),
@@ -49073,7 +49329,7 @@ var SideNavBar = React26.memo(({
49073
49329
  }
49074
49330
  ),
49075
49331
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 space-y-2", children: [
49076
- /* @__PURE__ */ jsxRuntime.jsxs(
49332
+ canAccessPath("/leaderboard") && /* @__PURE__ */ jsxRuntime.jsxs(
49077
49333
  "button",
49078
49334
  {
49079
49335
  onClick: handleMobileNavClick(handleLeaderboardClick),
@@ -49085,7 +49341,7 @@ var SideNavBar = React26.memo(({
49085
49341
  ]
49086
49342
  }
49087
49343
  ),
49088
- /* @__PURE__ */ jsxRuntime.jsxs(
49344
+ canAccessPath("/kpis") && /* @__PURE__ */ jsxRuntime.jsxs(
49089
49345
  "button",
49090
49346
  {
49091
49347
  onClick: handleMobileNavClick(handleKPIsClick),
@@ -49097,7 +49353,7 @@ var SideNavBar = React26.memo(({
49097
49353
  ]
49098
49354
  }
49099
49355
  ),
49100
- /* @__PURE__ */ jsxRuntime.jsxs(
49356
+ canAccessPath("/improvement-center") && /* @__PURE__ */ jsxRuntime.jsxs(
49101
49357
  "button",
49102
49358
  {
49103
49359
  onClick: handleMobileNavClick(handleImprovementClick),
@@ -49110,7 +49366,7 @@ var SideNavBar = React26.memo(({
49110
49366
  }
49111
49367
  ),
49112
49368
  showSupervisorManagement,
49113
- skuEnabled && true && /* @__PURE__ */ jsxRuntime.jsxs(
49369
+ skuEnabled && canAccessPath("/skus") && /* @__PURE__ */ jsxRuntime.jsxs(
49114
49370
  "button",
49115
49371
  {
49116
49372
  onClick: handleMobileNavClick(handleSKUsClick),
@@ -49122,7 +49378,7 @@ var SideNavBar = React26.memo(({
49122
49378
  ]
49123
49379
  }
49124
49380
  ),
49125
- /* @__PURE__ */ jsxRuntime.jsxs(
49381
+ canAccessPath("/health") && /* @__PURE__ */ jsxRuntime.jsxs(
49126
49382
  "button",
49127
49383
  {
49128
49384
  onClick: handleMobileNavClick(handleHealthClick),
@@ -49135,10 +49391,10 @@ var SideNavBar = React26.memo(({
49135
49391
  }
49136
49392
  )
49137
49393
  ] }),
49138
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 pt-6 border-t border-gray-100", children: [
49394
+ settingsItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 pt-6 border-t border-gray-100", children: [
49139
49395
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "px-5 mb-3 text-[10px] font-bold text-gray-400 uppercase tracking-widest", children: "Settings & Support" }),
49140
49396
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
49141
- /* @__PURE__ */ jsxRuntime.jsxs(
49397
+ canAccessPath("/targets") && /* @__PURE__ */ jsxRuntime.jsxs(
49142
49398
  "button",
49143
49399
  {
49144
49400
  onClick: handleMobileNavClick(handleTargetsClick),
@@ -49150,7 +49406,7 @@ var SideNavBar = React26.memo(({
49150
49406
  ]
49151
49407
  }
49152
49408
  ),
49153
- /* @__PURE__ */ jsxRuntime.jsxs(
49409
+ canAccessPath("/shifts") && /* @__PURE__ */ jsxRuntime.jsxs(
49154
49410
  "button",
49155
49411
  {
49156
49412
  onClick: handleMobileNavClick(handleShiftsClick),
@@ -49162,7 +49418,7 @@ var SideNavBar = React26.memo(({
49162
49418
  ]
49163
49419
  }
49164
49420
  ),
49165
- /* @__PURE__ */ jsxRuntime.jsxs(
49421
+ canAccessPath("/team-management") && /* @__PURE__ */ jsxRuntime.jsxs(
49166
49422
  "button",
49167
49423
  {
49168
49424
  onClick: handleMobileNavClick(handleTeamManagementClick),
@@ -49174,7 +49430,7 @@ var SideNavBar = React26.memo(({
49174
49430
  ]
49175
49431
  }
49176
49432
  ),
49177
- /* @__PURE__ */ jsxRuntime.jsxs(
49433
+ canAccessPath("/profile") && /* @__PURE__ */ jsxRuntime.jsxs(
49178
49434
  "button",
49179
49435
  {
49180
49436
  onClick: handleMobileNavClick(handleProfileClick),
@@ -49186,7 +49442,7 @@ var SideNavBar = React26.memo(({
49186
49442
  ]
49187
49443
  }
49188
49444
  ),
49189
- ticketsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
49445
+ ticketsEnabled && canAccessPath("/tickets") && /* @__PURE__ */ jsxRuntime.jsxs(
49190
49446
  "button",
49191
49447
  {
49192
49448
  onClick: handleMobileNavClick(handleTicketsClick),
@@ -49198,7 +49454,7 @@ var SideNavBar = React26.memo(({
49198
49454
  ]
49199
49455
  }
49200
49456
  ),
49201
- /* @__PURE__ */ jsxRuntime.jsxs(
49457
+ canAccessPath("/help") && /* @__PURE__ */ jsxRuntime.jsxs(
49202
49458
  "button",
49203
49459
  {
49204
49460
  onClick: handleMobileNavClick(handleHelpClick),
@@ -51458,9 +51714,19 @@ var InviteUserDialog = ({
51458
51714
  const [factorySearch, setFactorySearch] = React26.useState("");
51459
51715
  const [isSubmitting, setIsSubmitting] = React26.useState(false);
51460
51716
  const [error, setError] = React26.useState(null);
51717
+ const isSuperAdminOptifye = user?.role_level === "optifye" && (user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin);
51718
+ const canInviteOwner = isSuperAdminOptifye;
51461
51719
  const canInviteIT = user?.role_level === "owner" || user?.role_level === "optifye";
51462
51720
  const canInvitePlantHead = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "optifye";
51463
51721
  const canInviteSupervisor = ["owner", "it", "plant_head", "optifye"].includes(user?.role_level || "");
51722
+ const invitableRoles = React26.useMemo(() => {
51723
+ const roles = [];
51724
+ if (canInviteOwner) roles.push("owner");
51725
+ if (canInviteIT) roles.push("it");
51726
+ if (canInvitePlantHead) roles.push("plant_head");
51727
+ if (canInviteSupervisor) roles.push("supervisor");
51728
+ return roles;
51729
+ }, [canInviteOwner, canInviteIT, canInvitePlantHead, canInviteSupervisor]);
51464
51730
  const filteredLines = React26.useMemo(() => {
51465
51731
  const search = lineSearch.trim().toLowerCase();
51466
51732
  if (!search) return availableLines;
@@ -51492,6 +51758,14 @@ var InviteUserDialog = ({
51492
51758
  setError(null);
51493
51759
  }
51494
51760
  }, [isOpen]);
51761
+ React26.useEffect(() => {
51762
+ if (!isOpen || invitableRoles.length === 0) {
51763
+ return;
51764
+ }
51765
+ if (!invitableRoles.includes(selectedRole)) {
51766
+ setSelectedRole(invitableRoles[0]);
51767
+ }
51768
+ }, [isOpen, invitableRoles, selectedRole]);
51495
51769
  const validateEmail = (email2) => {
51496
51770
  const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$/;
51497
51771
  return emailRegex.test(email2);
@@ -51521,6 +51795,14 @@ var InviteUserDialog = ({
51521
51795
  setError("Please enter a valid email address");
51522
51796
  return;
51523
51797
  }
51798
+ if (!invitableRoles.includes(selectedRole)) {
51799
+ setError("You do not have permission to assign this role.");
51800
+ return;
51801
+ }
51802
+ if (selectedRole === "owner" && !canInviteOwner) {
51803
+ setError("Only super-admin Optifye users can create owner users.");
51804
+ return;
51805
+ }
51524
51806
  const companyId = entityConfig?.companyId || user?.properties?.company_id;
51525
51807
  if (!companyId) {
51526
51808
  setError("Company ID not found. Please refresh and try again.");
@@ -51701,6 +51983,33 @@ var InviteUserDialog = ({
51701
51983
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500", children: "*" })
51702
51984
  ] }),
51703
51985
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
51986
+ canInviteOwner && /* @__PURE__ */ jsxRuntime.jsxs(
51987
+ "label",
51988
+ {
51989
+ className: cn(
51990
+ "flex items-start gap-3 p-4 border-2 rounded-lg cursor-pointer transition-all duration-200",
51991
+ selectedRole === "owner" ? "border-amber-500 bg-amber-50" : "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
51992
+ ),
51993
+ children: [
51994
+ /* @__PURE__ */ jsxRuntime.jsx(
51995
+ "input",
51996
+ {
51997
+ type: "radio",
51998
+ name: "role",
51999
+ value: "owner",
52000
+ checked: selectedRole === "owner",
52001
+ onChange: (e) => setSelectedRole(e.target.value),
52002
+ className: "mt-1",
52003
+ disabled: isSubmitting
52004
+ }
52005
+ ),
52006
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
52007
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 mb-1", children: /* @__PURE__ */ jsxRuntime.jsx(RoleBadge_default, { role: "owner", size: "sm" }) }),
52008
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "Company owner access. Full management permissions for the selected company dashboard." })
52009
+ ] })
52010
+ ]
52011
+ }
52012
+ ),
51704
52013
  canInviteIT && /* @__PURE__ */ jsxRuntime.jsxs(
51705
52014
  "label",
51706
52015
  {
@@ -53572,11 +53881,32 @@ var UserManagementTable = ({
53572
53881
  setSortDirection("asc");
53573
53882
  }
53574
53883
  };
53884
+ const getFactoryIdsFromUser = (user) => {
53885
+ const properties = user.properties || {};
53886
+ const rawFactoryIds = properties.factory_ids ?? properties.factory_id;
53887
+ if (Array.isArray(rawFactoryIds)) {
53888
+ return rawFactoryIds.filter((factoryId) => typeof factoryId === "string" && factoryId.length > 0);
53889
+ }
53890
+ if (typeof rawFactoryIds === "string" && rawFactoryIds.length > 0) {
53891
+ return [rawFactoryIds];
53892
+ }
53893
+ return [];
53894
+ };
53575
53895
  const formatAssignments = (user) => {
53576
53896
  if (user.role_level === "owner" || user.role_level === "it") return "Company-wide";
53577
53897
  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";
53898
+ if (user.role_level === "plant_head") {
53899
+ if (user.assigned_factories && user.assigned_factories.length > 0) {
53900
+ return user.assigned_factories.join(", ");
53901
+ }
53902
+ const assignedFactoryIds = getFactoryIdsFromUser(user);
53903
+ if (assignedFactoryIds.length === 1) {
53904
+ return "1 factory assigned";
53905
+ }
53906
+ if (assignedFactoryIds.length > 1) {
53907
+ return `${assignedFactoryIds.length} factories assigned`;
53908
+ }
53909
+ return "No factories assigned";
53580
53910
  }
53581
53911
  if (user.role_level === "supervisor" && user.assigned_lines) {
53582
53912
  return user.assigned_lines.join(", ") || "No lines assigned";
@@ -53768,22 +54098,28 @@ var UserManagementTable = ({
53768
54098
  onUpdate: onLineAssignmentUpdate || (async () => {
53769
54099
  })
53770
54100
  }
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
- })
54101
+ ) : user.role_level === "plant_head" ? (() => {
54102
+ const canEditFactoryAssignments = permissions.canAssignFactories(user) && !!onFactoryAssignmentUpdate;
54103
+ if (!canEditFactoryAssignments) {
54104
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) });
53785
54105
  }
53786
- ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) }) }),
54106
+ return /* @__PURE__ */ jsxRuntime.jsx(
54107
+ FactoryAssignmentDropdown,
54108
+ {
54109
+ userId: user.user_id,
54110
+ currentFactoryIds: getFactoryIdsFromUser(user),
54111
+ availableFactories: (
54112
+ // Filter factories to only show those from the target user's company
54113
+ user.properties?.company_id ? availableFactories.filter(
54114
+ (factory) => factory.company_id === user.properties?.company_id
54115
+ ) : availableFactories
54116
+ ),
54117
+ canEdit: canEditFactoryAssignments,
54118
+ onUpdate: onFactoryAssignmentUpdate || (async () => {
54119
+ })
54120
+ }
54121
+ );
54122
+ })() : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-900", children: formatAssignments(user) }) }),
53787
54123
  showUsageStats && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-6 py-4", children: user.role_level === "plant_head" || user.role_level === "supervisor" ? /* @__PURE__ */ jsxRuntime.jsx(
53788
54124
  "button",
53789
54125
  {
@@ -54948,52 +55284,18 @@ function HomeView({
54948
55284
  });
54949
55285
  return merged;
54950
55286
  }, [lineNames, dbLines]);
54951
- const isOwner = user?.role_level === "owner";
54952
- const isPlantHead = user?.role_level === "plant_head";
55287
+ const enabledLineIdSet = React26.useMemo(
55288
+ () => new Set(dbLines.filter((line) => line.enable).map((line) => line.id)),
55289
+ [dbLines]
55290
+ );
54953
55291
  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
55292
  const visibleLineIds = React26.useMemo(() => {
54983
- if (isOwner) {
54984
- return allLineIds;
55293
+ const scoped = Array.from(new Set(allLineIds.filter(Boolean)));
55294
+ if (enabledLineIdSet.size === 0) {
55295
+ return scoped;
54985
55296
  }
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);
54991
- }
54992
- if (isSupervisor) {
54993
- return assignedLineIdsInConfig;
54994
- }
54995
- return allLineIds;
54996
- }, [isOwner, isPlantHead, isSupervisor, allLineIds, plantHeadLineIds, assignedLineIdsInConfig]);
55297
+ return scoped.filter((lineId) => enabledLineIdSet.has(lineId));
55298
+ }, [allLineIds, enabledLineIdSet]);
54997
55299
  const fallbackLineId = visibleLineIds[0] || defaultLineId;
54998
55300
  const defaultHomeLineId = fallbackLineId;
54999
55301
  const availableLineIds = React26.useMemo(() => {
@@ -55020,7 +55322,7 @@ function HomeView({
55020
55322
  return defaultHomeLineId;
55021
55323
  });
55022
55324
  React26.useEffect(() => {
55023
- if (!user || !isSupervisor && !isPlantHead || availableLineIds.length === 0) {
55325
+ if (!user || availableLineIds.length === 0) {
55024
55326
  return;
55025
55327
  }
55026
55328
  try {
@@ -55034,13 +55336,14 @@ function HomeView({
55034
55336
  } catch (error) {
55035
55337
  console.warn("Failed to read line filter from sessionStorage:", error);
55036
55338
  }
55339
+ if (availableLineIds.includes(selectedLineId)) {
55340
+ return;
55341
+ }
55037
55342
  if (defaultHomeLineId !== selectedLineId) {
55038
55343
  setSelectedLineId(defaultHomeLineId);
55039
55344
  }
55040
55345
  }, [
55041
55346
  user,
55042
- isSupervisor,
55043
- isPlantHead,
55044
55347
  availableLineIds,
55045
55348
  defaultHomeLineId,
55046
55349
  selectedLineId,
@@ -55059,14 +55362,14 @@ function HomeView({
55059
55362
  const [diagnoses, setDiagnoses] = React26.useState([]);
55060
55363
  const timezone = useAppTimezone();
55061
55364
  const { shiftConfigMap: lineShiftConfigs } = useMultiLineShiftConfigs(
55062
- allLineIds,
55365
+ visibleLineIds,
55063
55366
  dashboardConfig?.shiftConfig
55064
55367
  );
55065
55368
  React26.useEffect(() => {
55066
55369
  const initDisplayNames = async () => {
55067
55370
  try {
55068
55371
  if (selectedLineId === factoryViewId) {
55069
- for (const lineId of allLineIds) {
55372
+ for (const lineId of visibleLineIds) {
55070
55373
  await preInitializeWorkspaceDisplayNames(lineId);
55071
55374
  }
55072
55375
  } else {
@@ -55079,7 +55382,7 @@ function HomeView({
55079
55382
  }
55080
55383
  };
55081
55384
  initDisplayNames();
55082
- }, [selectedLineId, factoryViewId, allLineIds]);
55385
+ }, [selectedLineId, factoryViewId, visibleLineIds]);
55083
55386
  const displayNameLineId = selectedLineId === factoryViewId ? void 0 : selectedLineId;
55084
55387
  const {
55085
55388
  displayNames: workspaceDisplayNames,
@@ -58830,8 +59133,32 @@ var KPIsOverviewView = ({
58830
59133
  const dbTimezone = useAppTimezone();
58831
59134
  const configuredTimezone = dbTimezone || dateTimeConfig.defaultTimezone || "UTC";
58832
59135
  const { startDate: monthStartDate, endDate: monthEndDateKey, monthEndDate } = getMonthDateInfo(configuredTimezone);
58833
- const isSupervisor = user?.role_level === "supervisor";
58834
- const assignedLineIdsForLeaderboard = isSupervisor ? lineIds : void 0;
59136
+ const isSuperAdmin = user?.scope_mode === "SUPER_ADMIN" || !!user?.access_scope?.is_super_admin;
59137
+ const scopedLineIds = React26__namespace.default.useMemo(
59138
+ () => Array.isArray(user?.access_scope?.line_ids) ? user.access_scope.line_ids.filter((lineId) => typeof lineId === "string" && lineId.length > 0) : [],
59139
+ [user?.access_scope?.line_ids]
59140
+ );
59141
+ const hasCanonicalScope = !!user?.scope_mode || !!user?.access_scope;
59142
+ const scopeRole = (user?.role_level || user?.role || "").toLowerCase();
59143
+ const isStrictLineScopedRole = scopeRole === "supervisor" || scopeRole === "plant_head";
59144
+ const resolvedAssignedLineIds = React26__namespace.default.useMemo(() => {
59145
+ if (isSuperAdmin) return [];
59146
+ if (scopedLineIds.length > 0) return scopedLineIds;
59147
+ if (lineIds && lineIds.length > 0) return lineIds;
59148
+ if (isStrictLineScopedRole && hasCanonicalScope) return [];
59149
+ return [];
59150
+ }, [isSuperAdmin, scopedLineIds, lineIds, isStrictLineScopedRole, hasCanonicalScope]);
59151
+ const assignedLineIdSet = React26__namespace.default.useMemo(
59152
+ () => new Set(resolvedAssignedLineIds),
59153
+ [resolvedAssignedLineIds]
59154
+ );
59155
+ const metricsLineIds = React26__namespace.default.useMemo(() => {
59156
+ if (isSuperAdmin) {
59157
+ return lineIds ?? [];
59158
+ }
59159
+ return resolvedAssignedLineIds;
59160
+ }, [isSuperAdmin, lineIds, resolvedAssignedLineIds]);
59161
+ const assignedLineIdsForLeaderboard = isSuperAdmin ? void 0 : resolvedAssignedLineIds;
58835
59162
  const leaderboardLinesForView = React26__namespace.default.useMemo(() => {
58836
59163
  const targetMode = viewType === "machine" ? "uptime" : "output";
58837
59164
  return leaderboardLines.filter((line) => (line.monitoring_mode ?? "output") === targetMode);
@@ -58883,7 +59210,7 @@ var KPIsOverviewView = ({
58883
59210
  error: metricsError
58884
59211
  } = useDashboardMetrics({
58885
59212
  lineId: factoryViewId,
58886
- userAccessibleLineIds: lineIds
59213
+ userAccessibleLineIds: metricsLineIds
58887
59214
  });
58888
59215
  const defaultKPIs = React26__namespace.default.useMemo(() => createDefaultKPIs(), []);
58889
59216
  const kpisByLineId = React26__namespace.default.useMemo(() => {
@@ -58937,16 +59264,16 @@ var KPIsOverviewView = ({
58937
59264
  console.log("[KPIsOverviewView] Fetching lines with lineIds filter:", lineIds);
58938
59265
  const allLines = await dashboardService.getAllLines();
58939
59266
  let filteredLines = allLines;
58940
- if (lineIds && lineIds.length > 0) {
58941
- filteredLines = allLines.filter((line) => lineIds.includes(line.id));
58942
- console.log("[KPIsOverviewView] Filtered lines:", {
59267
+ if (!isSuperAdmin) {
59268
+ filteredLines = allLines.filter((line) => assignedLineIdSet.has(line.id));
59269
+ console.log("[KPIsOverviewView] Applied scoped line filter:", {
58943
59270
  total: allLines.length,
58944
59271
  filtered: filteredLines.length,
58945
- lineIds,
59272
+ allowedLineIds: resolvedAssignedLineIds,
58946
59273
  filteredLineIds: filteredLines.map((l) => l.id)
58947
59274
  });
58948
59275
  } else {
58949
- console.log("[KPIsOverviewView] No lineIds filter, showing all lines:", allLines.length);
59276
+ console.log("[KPIsOverviewView] Super admin view, showing all lines:", allLines.length);
58950
59277
  }
58951
59278
  setLines(filteredLines);
58952
59279
  } catch (err) {
@@ -58957,34 +59284,36 @@ var KPIsOverviewView = ({
58957
59284
  }
58958
59285
  };
58959
59286
  fetchLines();
58960
- }, [supabase, dashboardConfig, lineIds]);
59287
+ }, [supabase, dashboardConfig, lineIds, isSuperAdmin, assignedLineIdSet, resolvedAssignedLineIds]);
58961
59288
  React26.useEffect(() => {
58962
59289
  let isMounted = true;
58963
59290
  const fetchLeaderboardLines = async () => {
58964
59291
  if (!supabase || !resolvedCompanyId) {
58965
- setLeaderboardLines(lines);
59292
+ setLeaderboardLines(lines.filter((line) => line.enable !== false));
58966
59293
  return;
58967
59294
  }
58968
59295
  setLeaderboardLinesLoading(true);
58969
59296
  try {
58970
- const linesService2 = new LinesService(supabase);
58971
- const companyLines = await linesService2.getLinesByCompanyId(resolvedCompanyId);
59297
+ const data = await fetchBackendJson(
59298
+ supabase,
59299
+ `/api/dashboard/leaderboard-lines?company_id=${encodeURIComponent(resolvedCompanyId)}`
59300
+ );
58972
59301
  if (!isMounted) return;
58973
- const transformed = companyLines.map((line) => ({
59302
+ const transformed = (data.lines || []).filter((line) => line.enable !== false).map((line) => ({
58974
59303
  id: line.id,
58975
- line_name: line.name,
58976
- factory_id: line.factoryId || "",
59304
+ line_name: line.line_name,
59305
+ factory_id: line.factory_id || "",
58977
59306
  factory_name: "N/A",
58978
- company_id: line.companyId,
59307
+ company_id: line.company_id,
58979
59308
  company_name: "",
58980
- enable: line.isActive ?? true,
58981
- monitoring_mode: line.monitoringMode ?? "output"
59309
+ enable: line.enable ?? true,
59310
+ monitoring_mode: line.monitoring_mode ?? "output"
58982
59311
  }));
58983
59312
  setLeaderboardLines(transformed);
58984
59313
  } catch (err) {
58985
59314
  console.error("[KPIsOverviewView] Failed to load leaderboard lines:", err);
58986
59315
  if (!isMounted) return;
58987
- setLeaderboardLines(lines);
59316
+ setLeaderboardLines(lines.filter((line) => line.enable !== false));
58988
59317
  } finally {
58989
59318
  if (isMounted) setLeaderboardLinesLoading(false);
58990
59319
  }
@@ -59065,11 +59394,10 @@ var KPIsOverviewView = ({
59065
59394
  lineMode: viewType === "machine" ? "uptime" : "output"
59066
59395
  });
59067
59396
  const nextMap = /* @__PURE__ */ new Map();
59397
+ targetLineIds.forEach((lineId) => nextMap.set(lineId, 0));
59068
59398
  entries.forEach((entry) => {
59069
59399
  const value = Number(entry.avg_efficiency);
59070
- if (Number.isFinite(value)) {
59071
- nextMap.set(entry.line_id, value);
59072
- }
59400
+ nextMap.set(entry.line_id, Number.isFinite(value) ? value : 0);
59073
59401
  });
59074
59402
  setTodayEfficiencyByLineId(nextMap);
59075
59403
  } catch (err) {
@@ -59152,6 +59480,9 @@ var KPIsOverviewView = ({
59152
59480
  };
59153
59481
  };
59154
59482
  const handleLineClick = (line, kpis) => {
59483
+ if (!isSuperAdmin && !assignedLineIdSet.has(line.id)) {
59484
+ return;
59485
+ }
59155
59486
  const trackProps = {
59156
59487
  line_id: line.id,
59157
59488
  line_name: line.line_name,
@@ -59664,6 +59995,7 @@ var MobileWorkspaceCard = React26.memo(({
59664
59995
  workspace,
59665
59996
  rank,
59666
59997
  cardClass,
59998
+ isClickable,
59667
59999
  onWorkspaceClick,
59668
60000
  getMedalIcon,
59669
60001
  efficiencyLabel
@@ -59676,8 +60008,8 @@ var MobileWorkspaceCard = React26.memo(({
59676
60008
  transition: {
59677
60009
  layout: { duration: 0.3, ease: "easeInOut" }
59678
60010
  },
59679
- onClick: () => onWorkspaceClick(workspace, rank),
59680
- className: `${cardClass} p-3 rounded-lg border shadow-sm active:scale-[0.98] cursor-pointer`,
60011
+ onClick: isClickable ? () => onWorkspaceClick(workspace, rank) : void 0,
60012
+ className: `${cardClass} p-3 rounded-lg border shadow-sm active:scale-[0.98] ${isClickable ? "cursor-pointer" : "cursor-not-allowed opacity-75"}`,
59681
60013
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
59682
60014
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
59683
60015
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
@@ -59699,13 +60031,14 @@ var MobileWorkspaceCard = React26.memo(({
59699
60031
  ] })
59700
60032
  }
59701
60033
  ), (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;
60034
+ 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
60035
  });
59704
60036
  MobileWorkspaceCard.displayName = "MobileWorkspaceCard";
59705
60037
  var DesktopWorkspaceRow = React26.memo(({
59706
60038
  workspace,
59707
60039
  index,
59708
60040
  rowClass,
60041
+ isClickable,
59709
60042
  onWorkspaceClick,
59710
60043
  getMedalIcon
59711
60044
  }) => /* @__PURE__ */ jsxRuntime.jsxs(
@@ -59715,8 +60048,8 @@ var DesktopWorkspaceRow = React26.memo(({
59715
60048
  layoutId: `row-${workspace.workspace_uuid}`,
59716
60049
  initial: false,
59717
60050
  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`,
60051
+ onClick: isClickable ? () => onWorkspaceClick(workspace, index + 1) : void 0,
60052
+ className: `${rowClass} transition-colors duration-150 ${isClickable ? "hover:bg-gray-50/90 cursor-pointer group" : "cursor-not-allowed opacity-75"}`,
59720
60053
  children: [
59721
60054
  /* @__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
60055
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: index + 1 }),
@@ -59728,7 +60061,7 @@ var DesktopWorkspaceRow = React26.memo(({
59728
60061
  ]
59729
60062
  }
59730
60063
  ), (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;
60064
+ 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
60065
  });
59733
60066
  DesktopWorkspaceRow.displayName = "DesktopWorkspaceRow";
59734
60067
  var LeaderboardDetailView = React26.memo(({
@@ -59888,6 +60221,16 @@ var LeaderboardDetailView = React26.memo(({
59888
60221
  }
59889
60222
  return allLineIds;
59890
60223
  }, [entityConfig, userAccessibleLineIds]);
60224
+ const accessibleLineIdSet = React26.useMemo(
60225
+ () => new Set((userAccessibleLineIds || []).filter(Boolean)),
60226
+ [userAccessibleLineIds]
60227
+ );
60228
+ const canOpenWorkspace = React26.useCallback((workspaceLineId) => {
60229
+ if (!workspaceLineId) return false;
60230
+ if (!userAccessibleLineIds) return true;
60231
+ if (accessibleLineIdSet.size === 0) return false;
60232
+ return accessibleLineIdSet.has(workspaceLineId);
60233
+ }, [accessibleLineIdSet, userAccessibleLineIds]);
59891
60234
  const { hasUptime: lineModeHasUptime, hasOutput: lineModeHasOutput } = React26.useMemo(() => {
59892
60235
  if (!lines || lines.length === 0) {
59893
60236
  return { hasUptime: false, hasOutput: false };
@@ -60285,6 +60628,9 @@ var LeaderboardDetailView = React26.memo(({
60285
60628
  return `${startDate.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${endDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}`;
60286
60629
  }, [normalizedRange]);
60287
60630
  const handleWorkspaceClick = React26.useCallback((workspace, rank) => {
60631
+ if (!canOpenWorkspace(workspace.line_id)) {
60632
+ return;
60633
+ }
60288
60634
  trackCoreEvent("Workspace from Leaderboard Clicked", {
60289
60635
  workspace_name: workspace.workspace_name,
60290
60636
  workspace_id: workspace.workspace_uuid,
@@ -60317,7 +60663,7 @@ var LeaderboardDetailView = React26.memo(({
60317
60663
  const combinedParams = navParams ? `${navParams}&${contextParamString}` : `?${contextParamString}`;
60318
60664
  navigation.navigate(`/workspace/${workspace.workspace_uuid}${combinedParams}`);
60319
60665
  }
60320
- }, [onWorkspaceClick, navigation, date, shiftId]);
60666
+ }, [canOpenWorkspace, onWorkspaceClick, navigation, date, shiftId]);
60321
60667
  React26.useEffect(() => {
60322
60668
  workspacesLengthRef.current = activeEntries.length || 0;
60323
60669
  }, [activeEntries.length]);
@@ -60608,6 +60954,7 @@ var LeaderboardDetailView = React26.memo(({
60608
60954
  workspace: ws,
60609
60955
  rank,
60610
60956
  cardClass,
60957
+ isClickable: canOpenWorkspace(ws.line_id),
60611
60958
  onWorkspaceClick: stableHandleWorkspaceClick,
60612
60959
  getMedalIcon: stableGetMedalIcon,
60613
60960
  efficiencyLabel
@@ -60632,6 +60979,7 @@ var LeaderboardDetailView = React26.memo(({
60632
60979
  workspace: ws,
60633
60980
  index,
60634
60981
  rowClass,
60982
+ isClickable: canOpenWorkspace(ws.line_id),
60635
60983
  onWorkspaceClick: stableHandleWorkspaceClick,
60636
60984
  getMedalIcon: stableGetMedalIcon
60637
60985
  },
@@ -60642,7 +60990,7 @@ var LeaderboardDetailView = React26.memo(({
60642
60990
  ) })
60643
60991
  ] });
60644
60992
  }, (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;
60993
+ 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
60994
  });
60647
60995
  LeaderboardDetailView.displayName = "LeaderboardDetailView";
60648
60996
  var LeaderboardDetailViewWithDisplayNames = withAllWorkspaceDisplayNames(LeaderboardDetailView);
@@ -66403,9 +66751,38 @@ var TeamManagementView = ({
66403
66751
  optifye: 0
66404
66752
  });
66405
66753
  const [isAddUserDialogOpen, setIsAddUserDialogOpen] = React26.useState(false);
66754
+ const normalizeIds = React26.useCallback((value) => {
66755
+ if (Array.isArray(value)) {
66756
+ return value.filter((id3) => typeof id3 === "string" && id3.length > 0);
66757
+ }
66758
+ if (typeof value === "string" && value.length > 0) {
66759
+ return [value];
66760
+ }
66761
+ return [];
66762
+ }, []);
66763
+ const plantHeadFactoryIds = React26.useMemo(() => {
66764
+ if (user?.role_level !== "plant_head") return [];
66765
+ const scopedFactoryIds = normalizeIds(user?.access_scope?.factory_ids);
66766
+ if (scopedFactoryIds.length > 0) {
66767
+ return Array.from(new Set(scopedFactoryIds));
66768
+ }
66769
+ const propertyFactoryIds = normalizeIds(
66770
+ user?.properties?.factory_ids ?? user?.properties?.factory_id
66771
+ );
66772
+ if (propertyFactoryIds.length > 0) {
66773
+ return Array.from(new Set(propertyFactoryIds));
66774
+ }
66775
+ return entityConfig?.factoryId ? [entityConfig.factoryId] : [];
66776
+ }, [user, entityConfig?.factoryId, normalizeIds]);
66777
+ const notifyScopeRefresh = React26.useCallback(() => {
66778
+ if (typeof window !== "undefined") {
66779
+ window.dispatchEvent(new Event("rbac:refresh-scope"));
66780
+ }
66781
+ }, []);
66406
66782
  const canAddUsers = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "plant_head" || user?.role_level === "optifye";
66407
66783
  const canViewUsageStats = user?.role_level === "owner" || user?.role_level === "it" || user?.role_level === "optifye";
66408
- const companyIdForUsage = entityConfig?.companyId || user?.properties?.company_id;
66784
+ const pageCompanyId = entityConfig?.companyId || user?.properties?.company_id;
66785
+ const companyIdForUsage = pageCompanyId;
66409
66786
  const usageDateRange = React26.useMemo(() => {
66410
66787
  const today = /* @__PURE__ */ new Date();
66411
66788
  const dayOfWeek = today.getDay();
@@ -66436,8 +66813,8 @@ var TeamManagementView = ({
66436
66813
  return acc;
66437
66814
  }, {});
66438
66815
  }, [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";
66816
+ const pageTitle = "Team Management";
66817
+ 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
66818
  const hasAccess = user ? user.role_level === void 0 || ["optifye", "owner", "it", "plant_head"].includes(user.role_level) : false;
66442
66819
  const loadData = React26.useCallback(async () => {
66443
66820
  if (!supabase) {
@@ -66445,20 +66822,16 @@ var TeamManagementView = ({
66445
66822
  setIsLoading(false);
66446
66823
  return;
66447
66824
  }
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
- }
66825
+ const companyId = pageCompanyId;
66826
+ if (!companyId) {
66827
+ setError("Company not found. Please contact your administrator.");
66828
+ setIsLoading(false);
66829
+ return;
66456
66830
  }
66457
- const companyId = entityConfig?.companyId || user?.properties?.company_id;
66458
66831
  console.log("[TeamManagementView] Loading data:", {
66459
66832
  role: user?.role_level,
66460
- isOptifye: isOptifyeUser,
66461
- companyId: isOptifyeUser ? "ALL" : companyId
66833
+ isOptifye: user?.role_level === "optifye",
66834
+ companyId
66462
66835
  });
66463
66836
  setIsLoading(true);
66464
66837
  setError(void 0);
@@ -66473,23 +66846,7 @@ var TeamManagementView = ({
66473
66846
  if (!backendUrl) {
66474
66847
  throw new Error("Backend URL is not configured. Please set NEXT_PUBLIC_BACKEND_URL in your environment.");
66475
66848
  }
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) {
66849
+ if ((user?.role_level === "optifye" || user?.role_level === "owner" || user?.role_level === "it") && companyId) {
66493
66850
  const { data: factories } = await supabase.from("factories").select("id, factory_name, company_id").eq("company_id", companyId).order("factory_name");
66494
66851
  const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
66495
66852
  headers: {
@@ -66504,9 +66861,8 @@ var TeamManagementView = ({
66504
66861
  const lines = linesData.lines || [];
66505
66862
  setAvailableFactories(factories || []);
66506
66863
  setAvailableLines(lines);
66507
- console.log("[TeamManagementView] Owner/IT - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
66864
+ console.log("[TeamManagementView] Company-scoped team view - Company:", companyId, "Loaded factories:", factories?.length, "lines:", lines?.length);
66508
66865
  } else if (user?.role_level === "plant_head") {
66509
- const plantHeadFactoryIds = user?.properties?.factory_ids || [];
66510
66866
  if (plantHeadFactoryIds.length > 0) {
66511
66867
  if (companyId) {
66512
66868
  const linesResponse = await fetch(`${backendUrl}/api/lines?company_id=${companyId}`, {
@@ -66559,35 +66915,46 @@ var TeamManagementView = ({
66559
66915
  setAvailableFactories([]);
66560
66916
  console.log("[TeamManagementView] Fallback - Company:", companyId, "Loaded lines:", lines?.length);
66561
66917
  }
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
- }
66918
+ const usersPromise = user?.role_level === "plant_head" ? (async () => {
66919
+ if (plantHeadFactoryIds.length === 0) {
66920
+ return [];
66921
+ }
66922
+ const results = await Promise.allSettled(
66923
+ plantHeadFactoryIds.map((factoryId) => userManagementService.getFactoryUsers(factoryId))
66924
+ );
66925
+ const successful = results.filter(
66926
+ (result) => result.status === "fulfilled"
66927
+ ).flatMap((result) => result.value || []);
66928
+ if (successful.length > 0) {
66929
+ const byUserId = /* @__PURE__ */ new Map();
66930
+ successful.forEach((u) => {
66931
+ if (u?.user_id) {
66932
+ byUserId.set(u.user_id, u);
66933
+ }
66934
+ });
66935
+ return Array.from(byUserId.values());
66936
+ }
66937
+ const firstRejected = results.find(
66938
+ (result) => result.status === "rejected"
66939
+ );
66940
+ if (firstRejected) {
66941
+ throw firstRejected.reason;
66942
+ }
66943
+ return [];
66944
+ })() : userManagementService.getCompanyUsers(companyId);
66945
+ const [usersData, userStats] = await Promise.all([
66946
+ usersPromise,
66947
+ userManagementService.getUserStats(companyId)
66948
+ ]);
66949
+ setUsers(usersData);
66950
+ setStats({
66951
+ totalUsers: userStats.total,
66952
+ owners: userStats.owners,
66953
+ it: userStats.it,
66954
+ plantHeads: userStats.plant_heads,
66955
+ supervisors: userStats.supervisors,
66956
+ optifye: 0
66957
+ });
66591
66958
  } catch (err) {
66592
66959
  console.error("Error loading team management data:", err);
66593
66960
  setError(err instanceof Error ? err.message : "Failed to load data");
@@ -66595,16 +66962,16 @@ var TeamManagementView = ({
66595
66962
  } finally {
66596
66963
  setIsLoading(false);
66597
66964
  }
66598
- }, [supabase, user, entityConfig]);
66965
+ }, [supabase, user, pageCompanyId, entityConfig?.factoryId, plantHeadFactoryIds]);
66599
66966
  React26.useEffect(() => {
66600
- const companyId = entityConfig?.companyId || user?.properties?.company_id;
66601
- const canLoad = hasAccess && user && (user.role_level === "optifye" || !!companyId);
66967
+ const companyId = pageCompanyId;
66968
+ const canLoad = hasAccess && user && !!companyId;
66602
66969
  if (canLoad) {
66603
66970
  loadData();
66604
66971
  } else if (!user) {
66605
66972
  setIsLoading(true);
66606
66973
  }
66607
- }, [hasAccess, loadData, user, entityConfig?.companyId]);
66974
+ }, [hasAccess, loadData, user, pageCompanyId]);
66608
66975
  const handleUserAdded = React26.useCallback(() => {
66609
66976
  loadData();
66610
66977
  }, [loadData]);
@@ -66618,12 +66985,13 @@ var TeamManagementView = ({
66618
66985
  updated_by: user.id
66619
66986
  });
66620
66987
  sonner.toast.success("User role updated successfully");
66988
+ notifyScopeRefresh();
66621
66989
  loadData();
66622
66990
  } catch (err) {
66623
66991
  console.error("Error updating user role:", err);
66624
66992
  sonner.toast.error("Failed to update user role");
66625
66993
  }
66626
- }, [supabase, user, loadData]);
66994
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66627
66995
  const handleRemoveUser = React26.useCallback(async (userId) => {
66628
66996
  if (!supabase || !user) return;
66629
66997
  try {
@@ -66646,12 +67014,13 @@ var TeamManagementView = ({
66646
67014
  assigned_by: user.id
66647
67015
  });
66648
67016
  sonner.toast.success("Line assignments updated successfully");
67017
+ notifyScopeRefresh();
66649
67018
  loadData();
66650
67019
  } catch (err) {
66651
67020
  console.error("Error updating line assignments:", err);
66652
67021
  sonner.toast.error("Failed to update line assignments");
66653
67022
  }
66654
- }, [supabase, user, loadData]);
67023
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66655
67024
  const handleFactoryAssignmentUpdate = React26.useCallback(async (userId, factoryIds) => {
66656
67025
  if (!supabase || !user) return;
66657
67026
  try {
@@ -66662,12 +67031,13 @@ var TeamManagementView = ({
66662
67031
  assigned_by: user.id
66663
67032
  });
66664
67033
  sonner.toast.success("Factory assignments updated successfully");
67034
+ notifyScopeRefresh();
66665
67035
  loadData();
66666
67036
  } catch (err) {
66667
67037
  console.error("Error updating factory assignments:", err);
66668
67038
  sonner.toast.error("Failed to update factory assignments");
66669
67039
  }
66670
- }, [supabase, user, loadData]);
67040
+ }, [supabase, user, loadData, notifyScopeRefresh]);
66671
67041
  const handleProfileUpdate = React26.useCallback(async (userId, firstName, lastName, profilePhotoUrl) => {
66672
67042
  if (!supabase || !user) return;
66673
67043
  try {