@optifye/dashboard-core 6.11.24 → 6.11.25

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.mjs CHANGED
@@ -13255,7 +13255,7 @@ var parseEfficiencyLegend = (legend) => {
13255
13255
  critical_threshold: coerce(legend.critical_threshold, DEFAULT_EFFICIENCY_LEGEND.critical_threshold)
13256
13256
  };
13257
13257
  };
13258
- var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds }) => {
13258
+ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, lineIds, userAccessibleLineIds }) => {
13259
13259
  const { supabaseUrl, supabaseKey } = useDashboardConfig();
13260
13260
  const entityConfig = useEntityConfig();
13261
13261
  const databaseConfig = useDatabaseConfig();
@@ -13272,9 +13272,9 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13272
13272
  return getConfiguredLineIds(entityConfig);
13273
13273
  }, [entityConfig]);
13274
13274
  const targetFactoryLineIds = useMemo(() => {
13275
- const sourceLineIds = userAccessibleLineIds !== void 0 ? userAccessibleLineIds : configuredLineIds;
13275
+ const sourceLineIds = lineIds !== void 0 ? lineIds : userAccessibleLineIds !== void 0 ? userAccessibleLineIds : configuredLineIds;
13276
13276
  return Array.from(new Set((sourceLineIds || []).filter(Boolean)));
13277
- }, [userAccessibleLineIds, configuredLineIds]);
13277
+ }, [lineIds, userAccessibleLineIds, configuredLineIds]);
13278
13278
  const { shiftConfig: staticShiftConfig } = useDashboardConfig();
13279
13279
  const {
13280
13280
  shiftConfigMap: multiLineShiftConfigMap,
@@ -13327,6 +13327,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13327
13327
  const operationalShiftKeyRef = useRef(operationalShiftKey);
13328
13328
  const configuredLineIdsRef = useRef(configuredLineIds);
13329
13329
  const userAccessibleLineIdsRef = useRef(userAccessibleLineIds);
13330
+ const explicitLineIdsRef = useRef(lineIds);
13330
13331
  useEffect(() => {
13331
13332
  onLineMetricsUpdateRef.current = onLineMetricsUpdate;
13332
13333
  }, [onLineMetricsUpdate]);
@@ -13342,6 +13343,9 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13342
13343
  useEffect(() => {
13343
13344
  userAccessibleLineIdsRef.current = userAccessibleLineIds;
13344
13345
  }, [userAccessibleLineIds]);
13346
+ useEffect(() => {
13347
+ explicitLineIdsRef.current = lineIds;
13348
+ }, [lineIds]);
13345
13349
  const companySpecificMetricsTable = useMemo(
13346
13350
  () => getCompanyMetricsTableName(entityConfig.companyId, "performance_metrics"),
13347
13351
  [entityConfig.companyId]
@@ -13692,6 +13696,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13692
13696
  isFactoryView,
13693
13697
  multiLineShiftConfigMap,
13694
13698
  staticShiftConfig,
13699
+ lineIds,
13695
13700
  userAccessibleLineIds,
13696
13701
  effectiveWorkspaceConfig,
13697
13702
  shiftLoading,
@@ -13809,7 +13814,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13809
13814
  logDebug("[useDashboardMetrics] Setting up group subscriptions:", {
13810
13815
  groupCount: currentShiftGroups.length
13811
13816
  });
13812
- const targetFactoryLineIdsForSubscriptions = currentUserAccessibleLineIds !== void 0 ? currentUserAccessibleLineIds : currentConfiguredLineIds;
13817
+ const targetFactoryLineIdsForSubscriptions = explicitLineIdsRef.current !== void 0 ? explicitLineIdsRef.current : currentUserAccessibleLineIds !== void 0 ? currentUserAccessibleLineIds : currentConfiguredLineIds;
13813
13818
  const targetFactoryLineIdSet = new Set(
13814
13819
  (targetFactoryLineIdsForSubscriptions || []).filter(Boolean)
13815
13820
  );
@@ -13980,7 +13985,7 @@ var useDashboardMetrics = ({ onLineMetricsUpdate, lineId, userAccessibleLineIds
13980
13985
  });
13981
13986
  const currentShiftDetails = shiftConfig ? getCurrentShift(defaultTimezone, shiftConfig) : getCurrentShift(defaultTimezone, staticShiftConfig);
13982
13987
  const operationalDateForSubscription = currentShiftDetails.date;
13983
- const targetLineIds = isFactory ? currentUserAccessibleLineIds || currentConfiguredLineIds : [currentLineIdToUse];
13988
+ const targetLineIds = isFactory ? explicitLineIdsRef.current || currentUserAccessibleLineIds || currentConfiguredLineIds : [currentLineIdToUse];
13984
13989
  const filteredLineIds = targetLineIds.filter((id3) => id3 && id3 !== factoryViewIdentifier);
13985
13990
  if (filteredLineIds.length === 0) {
13986
13991
  logDebug("[useDashboardMetrics] Realtime setup skipped: no line IDs after filtering", {
@@ -20394,13 +20399,15 @@ var buildKPIsFromLineMetricsRow = (row) => {
20394
20399
  };
20395
20400
  var aggregateKPIsFromLineMetricsRows = (rows) => {
20396
20401
  if (!rows || rows.length === 0) return createDefaultKPIs();
20397
- const currentOutputSum = rows.reduce((sum, row) => sum + toNumber(row.current_output), 0);
20398
- const lineThresholdSum = rows.reduce((sum, row) => sum + toNumber(row.line_threshold), 0);
20399
- const idealOutputSum = rows.reduce(
20402
+ const eligibleRows = rows.filter((row) => toNumber(row?.avg_efficiency) >= 5);
20403
+ if (eligibleRows.length === 0) return createDefaultKPIs();
20404
+ const currentOutputSum = eligibleRows.reduce((sum, row) => sum + toNumber(row.current_output), 0);
20405
+ const lineThresholdSum = eligibleRows.reduce((sum, row) => sum + toNumber(row.line_threshold), 0);
20406
+ const idealOutputSum = eligibleRows.reduce(
20400
20407
  (sum, row) => sum + (toNumber(row.ideal_output) || toNumber(row.line_threshold)),
20401
20408
  0
20402
20409
  );
20403
- const efficiencyValues = rows.map((row) => {
20410
+ const efficiencyValues = eligibleRows.map((row) => {
20404
20411
  const value = row?.avg_efficiency;
20405
20412
  if (typeof value === "number" && Number.isFinite(value)) return value;
20406
20413
  if (typeof value === "string" && value.trim() !== "") {
@@ -20410,10 +20417,10 @@ var aggregateKPIsFromLineMetricsRows = (rows) => {
20410
20417
  return null;
20411
20418
  }).filter((value) => value !== null);
20412
20419
  const avgEfficiency = efficiencyValues.length > 0 ? efficiencyValues.reduce((sum, value) => sum + value, 0) / efficiencyValues.length : 0;
20413
- const numLines = rows.length;
20414
- const avgCycleTime = numLines > 0 ? rows.reduce((sum, row) => sum + toNumber(row.avg_cycle_time), 0) / numLines : 0;
20415
- const totalUnderperforming = rows.reduce((sum, row) => sum + toNumber(row.underperforming_workspaces), 0);
20416
- const totalWorkspaces = rows.reduce((sum, row) => sum + toNumber(row.total_workspaces), 0);
20420
+ const numLines = eligibleRows.length;
20421
+ const avgCycleTime = numLines > 0 ? eligibleRows.reduce((sum, row) => sum + toNumber(row.avg_cycle_time), 0) / numLines : 0;
20422
+ const totalUnderperforming = eligibleRows.reduce((sum, row) => sum + toNumber(row.underperforming_workspaces), 0);
20423
+ const totalWorkspaces = eligibleRows.reduce((sum, row) => sum + toNumber(row.total_workspaces), 0);
20417
20424
  return {
20418
20425
  underperformingWorkers: {
20419
20426
  current: totalUnderperforming,
@@ -34569,6 +34576,8 @@ var logDebug2 = (...args) => {
34569
34576
  var VideoGridView = React142__default.memo(({
34570
34577
  workspaces,
34571
34578
  selectedLine,
34579
+ lineNames = {},
34580
+ lineOrder = [],
34572
34581
  className = "",
34573
34582
  legend,
34574
34583
  videoSources = {},
@@ -34671,6 +34680,44 @@ var VideoGridView = React142__default.memo(({
34671
34680
  }
34672
34681
  }) : workspaces;
34673
34682
  }, [workspaces, selectedLine]);
34683
+ const sortedWorkspaces = useMemo(() => {
34684
+ return [...filteredWorkspaces].sort((a, b) => {
34685
+ if (a.line_id !== b.line_id) {
34686
+ return (a.line_id || "").localeCompare(b.line_id || "");
34687
+ }
34688
+ const aMatch = a.workspace_name.match(/WS(\d+)/);
34689
+ const bMatch = b.workspace_name.match(/WS(\d+)/);
34690
+ if (aMatch && bMatch) {
34691
+ const aNum = parseInt(aMatch[1], 10);
34692
+ const bNum = parseInt(bMatch[1], 10);
34693
+ return aNum - bNum;
34694
+ }
34695
+ return a.workspace_name.localeCompare(b.workspace_name);
34696
+ });
34697
+ }, [filteredWorkspaces]);
34698
+ const lineGroups = useMemo(() => {
34699
+ const grouped = /* @__PURE__ */ new Map();
34700
+ sortedWorkspaces.forEach((workspace) => {
34701
+ const lineId = workspace.line_id || "unknown";
34702
+ const existing = grouped.get(lineId);
34703
+ if (existing) {
34704
+ existing.push(workspace);
34705
+ return;
34706
+ }
34707
+ grouped.set(lineId, [workspace]);
34708
+ });
34709
+ const sortedRemainingLineIds = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
34710
+ const orderedLineIds = [
34711
+ ...lineOrder.filter((lineId) => grouped.has(lineId)),
34712
+ ...sortedRemainingLineIds.filter((lineId) => !lineOrder.includes(lineId))
34713
+ ];
34714
+ return orderedLineIds.map((lineId) => ({
34715
+ lineId,
34716
+ lineName: lineNames[lineId] || `Line ${lineId.substring(0, 4)}`,
34717
+ workspaces: grouped.get(lineId) || []
34718
+ }));
34719
+ }, [sortedWorkspaces, lineOrder, lineNames]);
34720
+ lineGroups.length > 1;
34674
34721
  const streamsReady = !videoStreamsLoading;
34675
34722
  const calculateOptimalGrid = useCallback(() => {
34676
34723
  if (!containerRef.current) return;
@@ -34834,108 +34881,130 @@ var VideoGridView = React142__default.memo(({
34834
34881
  stream_source: isR2Stream ? "r2" : "media_config"
34835
34882
  });
34836
34883
  }, []);
34884
+ const workspaceCards = useMemo(() => {
34885
+ return sortedWorkspaces.map((workspace) => {
34886
+ const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
34887
+ const workspaceKey = `${workspace.line_id || "unknown"}-${workspaceId}`;
34888
+ const isVisible = visibleWorkspaces.has(workspaceId);
34889
+ const isVeryLowEfficiency = workspace.show_exclamation ?? (workspace.efficiency < 50 && workspace.efficiency >= 10);
34890
+ const workspaceCropping = getWorkspaceCropping(workspaceId, workspace.workspace_name);
34891
+ const workspaceStream = videoStreamsByWorkspaceId?.[workspaceId];
34892
+ const lastSeenLabel = workspace.workspace_uuid ? lastSeenByWorkspaceId[workspace.workspace_uuid]?.timeSinceLastUpdate : void 0;
34893
+ const r2Url = workspaceStream?.hls_url;
34894
+ const fallbackUrl = getWorkspaceHlsUrl(workspace.workspace_name, workspace.line_id);
34895
+ const hasR2Stream = Boolean(r2Url);
34896
+ const useFallback = r2FallbackWorkspaces.has(workspaceId) || streamsReady && !hasR2Stream;
34897
+ const hlsUrl = useFallback ? fallbackUrl : r2Url ?? "";
34898
+ const isR2Stream = !useFallback && hasR2Stream;
34899
+ const canAttemptR2 = hasR2Stream && !r2FallbackWorkspaces.has(workspaceId);
34900
+ const shouldPlay = isVisible && Boolean(hlsUrl) && (!failedStreams.has(workspaceId) || canAttemptR2);
34901
+ return {
34902
+ workspace,
34903
+ workspaceId,
34904
+ workspaceKey,
34905
+ isVisible,
34906
+ isVeryLowEfficiency,
34907
+ workspaceCropping,
34908
+ fallbackUrl,
34909
+ hlsUrl,
34910
+ isR2Stream,
34911
+ shouldPlay,
34912
+ lastSeenLabel
34913
+ };
34914
+ });
34915
+ }, [
34916
+ sortedWorkspaces,
34917
+ visibleWorkspaces,
34918
+ getWorkspaceCropping,
34919
+ videoStreamsByWorkspaceId,
34920
+ lastSeenByWorkspaceId,
34921
+ getWorkspaceHlsUrl,
34922
+ r2FallbackWorkspaces,
34923
+ streamsReady,
34924
+ failedStreams
34925
+ ]);
34926
+ useMemo(() => {
34927
+ const map = /* @__PURE__ */ new Map();
34928
+ workspaceCards.forEach((card) => {
34929
+ map.set(card.workspaceKey, card);
34930
+ });
34931
+ return map;
34932
+ }, [workspaceCards]);
34933
+ const croppedActiveCount = useMemo(() => {
34934
+ return workspaceCards.reduce((count, card) => {
34935
+ if (card.shouldPlay && card.workspaceCropping) {
34936
+ return count + 1;
34937
+ }
34938
+ return count;
34939
+ }, 0);
34940
+ }, [workspaceCards]);
34941
+ const throttleCropping = croppedActiveCount > 10;
34942
+ const effectiveCanvasFps = throttleCropping ? 10 : canvasConfig?.fps;
34943
+ const effectiveUseRAF = throttleCropping ? false : canvasConfig?.useRAF;
34837
34944
  const displayMinuteBucket = Math.floor(Date.now() / 6e4);
34838
- return /* @__PURE__ */ jsx("div", { className: `relative overflow-hidden h-full w-full ${className}`, children: /* @__PURE__ */ jsx("div", { ref: containerRef, className: "h-full w-full p-3 sm:p-2", children: /* @__PURE__ */ jsx(
34945
+ const renderWorkspaceCard = useCallback((card, className2) => /* @__PURE__ */ jsx(
34839
34946
  "div",
34840
34947
  {
34841
- className: "grid h-full w-full gap-3 sm:gap-2",
34842
- style: {
34843
- gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
34844
- gridTemplateRows: `repeat(${gridRows}, 1fr)`,
34845
- gridAutoFlow: "row"
34846
- },
34847
- children: (() => {
34848
- const sortedWorkspaces = [...filteredWorkspaces].sort((a, b) => {
34849
- if (a.line_id !== b.line_id) {
34850
- return (a.line_id || "").localeCompare(b.line_id || "");
34851
- }
34852
- const aMatch = a.workspace_name.match(/WS(\d+)/);
34853
- const bMatch = b.workspace_name.match(/WS(\d+)/);
34854
- if (aMatch && bMatch) {
34855
- const aNum = parseInt(aMatch[1]);
34856
- const bNum = parseInt(bMatch[1]);
34857
- return aNum - bNum;
34858
- }
34859
- return a.workspace_name.localeCompare(b.workspace_name);
34860
- });
34861
- const workspaceCards = sortedWorkspaces.map((workspace) => {
34862
- const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
34863
- `${workspace.line_id || "unknown"}-${workspaceId}`;
34864
- const isVisible = visibleWorkspaces.has(workspaceId);
34865
- const isVeryLowEfficiency = workspace.show_exclamation ?? (workspace.efficiency < 50 && workspace.efficiency >= 10);
34866
- const workspaceCropping = getWorkspaceCropping(workspaceId, workspace.workspace_name);
34867
- const workspaceStream = videoStreamsByWorkspaceId?.[workspaceId];
34868
- const lastSeenLabel = workspace.workspace_uuid ? lastSeenByWorkspaceId[workspace.workspace_uuid]?.timeSinceLastUpdate : void 0;
34869
- const r2Url = workspaceStream?.hls_url;
34870
- const fallbackUrl = getWorkspaceHlsUrl(workspace.workspace_name, workspace.line_id);
34871
- const hasR2Stream = Boolean(r2Url);
34872
- const useFallback = r2FallbackWorkspaces.has(workspaceId) || streamsReady && !hasR2Stream;
34873
- const hlsUrl = useFallback ? fallbackUrl : r2Url ?? "";
34874
- const isR2Stream = !useFallback && hasR2Stream;
34875
- const canAttemptR2 = hasR2Stream && !r2FallbackWorkspaces.has(workspaceId);
34876
- const shouldPlay = isVisible && Boolean(hlsUrl) && (!failedStreams.has(workspaceId) || canAttemptR2);
34877
- return {
34878
- workspace,
34879
- workspaceId,
34880
- isVisible,
34881
- isVeryLowEfficiency,
34882
- workspaceCropping,
34883
- fallbackUrl,
34884
- hlsUrl,
34885
- isR2Stream,
34886
- shouldPlay,
34887
- lastSeenLabel
34888
- };
34889
- });
34890
- const croppedActiveCount = workspaceCards.reduce((count, card) => {
34891
- if (card.shouldPlay && card.workspaceCropping) {
34892
- return count + 1;
34893
- }
34894
- return count;
34895
- }, 0);
34896
- const throttleCropping = croppedActiveCount > 10;
34897
- const effectiveCanvasFps = throttleCropping ? 10 : canvasConfig?.fps;
34898
- const effectiveUseRAF = throttleCropping ? false : canvasConfig?.useRAF;
34899
- return workspaceCards.map((card) => /* @__PURE__ */ jsx(
34900
- "div",
34901
- {
34902
- "data-workspace-id": card.workspaceId,
34903
- className: "workspace-card relative w-full h-full",
34904
- children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsx(
34905
- VideoCard,
34906
- {
34907
- workspace: card.workspace,
34908
- hlsUrl: card.hlsUrl,
34909
- shouldPlay: card.shouldPlay,
34910
- onClick: () => handleWorkspaceClick(card.workspace),
34911
- onFatalError: () => handleStreamError(card.workspaceId, {
34912
- isR2Stream: card.isR2Stream,
34913
- fallbackUrl: card.fallbackUrl
34914
- }),
34915
- isVeryLowEfficiency: card.isVeryLowEfficiency,
34916
- legend: effectiveLegend,
34917
- cropping: card.workspaceCropping,
34918
- canvasFps: effectiveCanvasFps,
34919
- displayName: (
34920
- // Create line-aware lookup key: lineId_workspaceName
34921
- // This ensures correct mapping when multiple lines have same workspace names
34922
- displayNames[`${card.workspace.line_id}_${card.workspace.workspace_name}`] || // Always pass line_id to fallback to ensure correct mapping per line
34923
- getWorkspaceDisplayName(card.workspace.workspace_name, card.workspace.line_id)
34924
- ),
34925
- lastSeenLabel: card.lastSeenLabel,
34926
- useRAF: effectiveUseRAF,
34927
- displayMinuteBucket,
34928
- compact: !selectedLine,
34929
- onMouseEnter: onWorkspaceHover ? () => onWorkspaceHover(card.workspaceId) : void 0,
34930
- onMouseLeave: onWorkspaceHoverEnd ? () => onWorkspaceHoverEnd(card.workspaceId) : void 0
34931
- }
34932
- ) })
34948
+ "data-workspace-id": card.workspaceId,
34949
+ className: className2,
34950
+ children: /* @__PURE__ */ jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsx(
34951
+ VideoCard,
34952
+ {
34953
+ workspace: card.workspace,
34954
+ hlsUrl: card.hlsUrl,
34955
+ shouldPlay: card.shouldPlay,
34956
+ onClick: () => handleWorkspaceClick(card.workspace),
34957
+ onFatalError: () => handleStreamError(card.workspaceId, {
34958
+ isR2Stream: card.isR2Stream,
34959
+ fallbackUrl: card.fallbackUrl
34960
+ }),
34961
+ isVeryLowEfficiency: card.isVeryLowEfficiency,
34962
+ legend: effectiveLegend,
34963
+ cropping: card.workspaceCropping,
34964
+ canvasFps: effectiveCanvasFps,
34965
+ displayName: displayNames[`${card.workspace.line_id}_${card.workspace.workspace_name}`] || getWorkspaceDisplayName(card.workspace.workspace_name, card.workspace.line_id),
34966
+ lastSeenLabel: card.lastSeenLabel,
34967
+ useRAF: effectiveUseRAF,
34968
+ displayMinuteBucket,
34969
+ compact: !selectedLine,
34970
+ onMouseEnter: onWorkspaceHover ? () => onWorkspaceHover(card.workspaceId) : void 0,
34971
+ onMouseLeave: onWorkspaceHoverEnd ? () => onWorkspaceHoverEnd(card.workspaceId) : void 0
34972
+ }
34973
+ ) })
34974
+ },
34975
+ card.workspaceKey
34976
+ ), [
34977
+ displayNames,
34978
+ displayMinuteBucket,
34979
+ effectiveCanvasFps,
34980
+ effectiveLegend,
34981
+ effectiveUseRAF,
34982
+ getWorkspaceDisplayName,
34983
+ handleStreamError,
34984
+ handleWorkspaceClick,
34985
+ onWorkspaceHover,
34986
+ onWorkspaceHoverEnd,
34987
+ selectedLine
34988
+ ]);
34989
+ return /* @__PURE__ */ jsx("div", { className: `relative overflow-hidden h-full w-full bg-slate-50/30 ${className}`, children: /* @__PURE__ */ jsx(
34990
+ "div",
34991
+ {
34992
+ ref: containerRef,
34993
+ className: "h-full w-full px-1 sm:px-2 py-1 sm:py-2",
34994
+ children: /* @__PURE__ */ jsx(
34995
+ "div",
34996
+ {
34997
+ className: "grid h-full w-full gap-1.5 sm:gap-2",
34998
+ style: {
34999
+ gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
35000
+ gridTemplateRows: `repeat(${gridRows}, 1fr)`,
35001
+ gridAutoFlow: "row"
34933
35002
  },
34934
- card.workspaceId
34935
- ));
34936
- })()
35003
+ children: workspaceCards.map((card) => renderWorkspaceCard(card, "workspace-card relative w-full h-full"))
35004
+ }
35005
+ )
34937
35006
  }
34938
- ) }) });
35007
+ ) });
34939
35008
  });
34940
35009
  VideoGridView.displayName = "VideoGridView";
34941
35010
  var MapGridView = React142__default.memo(({
@@ -50358,29 +50427,29 @@ var Legend5 = ({
50358
50427
  }) => {
50359
50428
  const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
50360
50429
  const exclamationLabel = useBottleneckLabel ? "Bottleneck" : "<50% efficiency";
50361
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5 sm:gap-3 bg-white/95 rounded-lg shadow-sm px-2 sm:px-4 py-1 sm:py-1.5 border border-gray-200/60 backdrop-blur-sm text-[10px] sm:text-sm", children: [
50430
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 sm:gap-4 text-xs font-medium text-slate-600", children: [
50362
50431
  /* @__PURE__ */ jsxs("div", { className: "font-medium text-gray-700 hidden sm:block", children: [
50363
50432
  metricLabel,
50364
50433
  ":"
50365
50434
  ] }),
50366
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 sm:gap-4", children: [
50367
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
50368
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 sm:w-2.5 sm:h-2.5 rounded-[12px] bg-[#00AB45]/90 ring-1 ring-[#00AB45]/20" }),
50369
- /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: formatPercentRange(effectiveLegend.green_min, effectiveLegend.green_max) })
50435
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 sm:gap-4", children: [
50436
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
50437
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 sm:w-2.5 sm:h-2.5 rounded-full bg-[#00AB45]" }),
50438
+ /* @__PURE__ */ jsx("span", { children: formatPercentRange(effectiveLegend.green_min, effectiveLegend.green_max) })
50370
50439
  ] }),
50371
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
50372
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 sm:w-2.5 sm:h-2.5 rounded-[12px] bg-[#FFB020]/90 ring-1 ring-[#FFB020]/20" }),
50373
- /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: formatPercentRange(effectiveLegend.yellow_min, effectiveLegend.yellow_max) })
50440
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
50441
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 sm:w-2.5 sm:h-2.5 rounded-full bg-[#FFB020]" }),
50442
+ /* @__PURE__ */ jsx("span", { children: formatPercentRange(effectiveLegend.yellow_min, effectiveLegend.yellow_max) })
50374
50443
  ] }),
50375
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
50376
- /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 sm:w-2.5 sm:h-2.5 rounded-[12px] bg-[#E34329]/90 ring-1 ring-[#E34329]/20" }),
50377
- /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: formatPercentRange(effectiveLegend.red_min, effectiveLegend.red_max) })
50444
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
50445
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 sm:w-2.5 sm:h-2.5 rounded-full bg-[#E34329]" }),
50446
+ /* @__PURE__ */ jsx("span", { children: formatPercentRange(effectiveLegend.red_min, effectiveLegend.red_max) })
50378
50447
  ] })
50379
50448
  ] }),
50380
- /* @__PURE__ */ jsx("div", { className: "hidden sm:block w-px h-6 bg-gray-200 mx-1" }),
50381
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
50382
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-3 h-3 sm:w-5 sm:h-5 bg-[#E34329]/90 rounded-full ring-1 ring-[#E34329]/20 text-white font-bold text-[8px] sm:text-xs", children: "!" }),
50383
- /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-600", children: exclamationLabel })
50449
+ /* @__PURE__ */ jsx("div", { className: "hidden sm:block w-px h-4 bg-slate-200 mx-1" }),
50450
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
50451
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-3 h-3 sm:w-4 sm:h-4 bg-[#E34329] rounded-full text-white font-bold text-[8px] sm:text-[10px]", children: "!" }),
50452
+ /* @__PURE__ */ jsx("span", { children: exclamationLabel })
50384
50453
  ] })
50385
50454
  ] });
50386
50455
  };
@@ -50490,6 +50559,7 @@ var WorkspaceGrid = React142__default.memo(({
50490
50559
  isPdfMode = false,
50491
50560
  customWorkspacePositions,
50492
50561
  lineNames = {},
50562
+ lineOrder = [],
50493
50563
  factoryView = "factory",
50494
50564
  line2Uuid = "line-2",
50495
50565
  className = "",
@@ -50500,7 +50570,8 @@ var WorkspaceGrid = React142__default.memo(({
50500
50570
  videoStreamsLoading = false,
50501
50571
  displayNames = {},
50502
50572
  onWorkspaceHover,
50503
- onWorkspaceHoverEnd
50573
+ onWorkspaceHoverEnd,
50574
+ toolbarRightContent
50504
50575
  }) => {
50505
50576
  const dashboardConfig = useDashboardConfig();
50506
50577
  const mapViewEnabled = dashboardConfig?.mapViewConfig?.enabled ?? false;
@@ -50534,29 +50605,30 @@ var WorkspaceGrid = React142__default.memo(({
50534
50605
  () => viewMode === "video" ? getVideoGridLegendLabel(workspaces) : MAP_GRID_LEGEND_LABEL,
50535
50606
  [viewMode, workspaces]
50536
50607
  );
50537
- return /* @__PURE__ */ jsxs("div", { className: `relative w-full h-full overflow-hidden ${className}`, children: [
50538
- /* @__PURE__ */ jsxs("div", { className: "absolute top-0 left-2 sm:left-4 right-2 sm:right-8 z-20", children: [
50539
- /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center justify-between py-1 sm:py-1.5 gap-2", children: [
50540
- /* @__PURE__ */ jsx("div", { className: "hidden sm:block", children: /* @__PURE__ */ jsx(Legend5, { legend, useBottleneckLabel: hasFlowBuffers, metricLabel: legendMetricLabel }) }),
50608
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col w-full h-full overflow-hidden bg-slate-50/50 ${className}`, children: [
50609
+ /* @__PURE__ */ jsxs("div", { className: "flex-none px-4 py-3 z-20 flex flex-row items-center justify-between gap-4", children: [
50610
+ /* @__PURE__ */ jsx("div", { className: "hidden sm:block", children: /* @__PURE__ */ jsx("div", { className: "inline-flex bg-white/95 rounded-lg shadow-sm px-4 py-2 border border-slate-200/60 backdrop-blur-sm", children: /* @__PURE__ */ jsx(Legend5, { legend, useBottleneckLabel: hasFlowBuffers, metricLabel: legendMetricLabel }) }) }),
50611
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3 shrink-0", children: [
50612
+ toolbarRightContent,
50541
50613
  mapViewEnabled && /* @__PURE__ */ jsx(
50542
50614
  "button",
50543
50615
  {
50544
50616
  onClick: handleViewModeToggle,
50545
- className: "flex items-center gap-2 px-3 py-1.5 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition-colors duration-200",
50617
+ className: "flex items-center gap-2 px-3 py-1.5 bg-white border border-slate-200 rounded-md shadow-sm hover:bg-slate-50 transition-colors duration-200 text-slate-700",
50546
50618
  title: viewMode === "video" ? "Switch to Map View" : "Switch to Video View",
50547
50619
  children: viewMode === "video" ? /* @__PURE__ */ jsxs(Fragment, { children: [
50548
- /* @__PURE__ */ jsx(Map$1, { className: "w-4 h-4 text-gray-600" }),
50549
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline text-sm text-gray-700", children: "Map View" })
50620
+ /* @__PURE__ */ jsx(Map$1, { className: "w-4 h-4 text-slate-500" }),
50621
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline text-sm font-medium", children: "Map View" })
50550
50622
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
50551
- /* @__PURE__ */ jsx(Video, { className: "w-4 h-4 text-gray-600" }),
50552
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline text-sm text-gray-700", children: "Video View" })
50623
+ /* @__PURE__ */ jsx(Video, { className: "w-4 h-4 text-slate-500" }),
50624
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline text-sm font-medium", children: "Video View" })
50553
50625
  ] })
50554
50626
  }
50555
50627
  )
50556
- ] }),
50557
- /* @__PURE__ */ jsx("div", { className: "sm:hidden mt-1 mr-32", children: /* @__PURE__ */ jsx(Legend5, { legend, useBottleneckLabel: hasFlowBuffers, metricLabel: legendMetricLabel }) })
50628
+ ] })
50558
50629
  ] }),
50559
- /* @__PURE__ */ jsx("div", { className: "absolute top-14 sm:top-16 left-0 right-0 bottom-0", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: viewMode === "video" ? /* @__PURE__ */ jsx(
50630
+ /* @__PURE__ */ jsx("div", { className: "sm:hidden px-3 py-2 bg-white border-b border-slate-200/60 z-10", children: /* @__PURE__ */ jsx(Legend5, { legend, useBottleneckLabel: hasFlowBuffers, metricLabel: legendMetricLabel }) }),
50631
+ /* @__PURE__ */ jsx("div", { className: "flex-1 relative overflow-hidden", children: /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: viewMode === "video" ? /* @__PURE__ */ jsx(
50560
50632
  motion.div,
50561
50633
  {
50562
50634
  initial: { opacity: 0 },
@@ -50568,6 +50640,8 @@ var WorkspaceGrid = React142__default.memo(({
50568
50640
  VideoGridViewComponent,
50569
50641
  {
50570
50642
  workspaces,
50643
+ lineNames,
50644
+ lineOrder,
50571
50645
  videoSources,
50572
50646
  videoStreamsByWorkspaceId,
50573
50647
  videoStreamsLoading,
@@ -59430,6 +59504,7 @@ function HomeView({
59430
59504
  [dbLines]
59431
59505
  );
59432
59506
  const isSupervisor = user?.role_level === "supervisor";
59507
+ const hasUser = Boolean(user);
59433
59508
  const visibleLineIds = useMemo(() => {
59434
59509
  const scoped = Array.from(new Set(allLineIds.filter(Boolean)));
59435
59510
  if (enabledLineIdSet.size === 0) {
@@ -59439,57 +59514,100 @@ function HomeView({
59439
59514
  }, [allLineIds, enabledLineIdSet]);
59440
59515
  const fallbackLineId = visibleLineIds[0] || defaultLineId;
59441
59516
  const defaultHomeLineId = fallbackLineId;
59442
- const availableLineIds = useMemo(() => {
59443
- if (visibleLineIds.length > 1) {
59444
- return [...visibleLineIds, factoryViewId];
59445
- }
59446
- return visibleLineIds;
59447
- }, [visibleLineIds, factoryViewId]);
59448
- const LINE_FILTER_STORAGE_KEY = "optifye_home_line_filter";
59449
- const [selectedLineId, setSelectedLineId] = useState(() => {
59517
+ const visibleLineIdsKey = visibleLineIds.join(",");
59518
+ const normalizeSelectedLineIds = (lineIdsToNormalize) => {
59519
+ const allowedLineIds = new Set(visibleLineIds);
59520
+ const requestedLineIds = new Set((lineIdsToNormalize || []).filter(Boolean));
59521
+ const normalized = visibleLineIds.filter((lineId) => requestedLineIds.has(lineId) && allowedLineIds.has(lineId));
59522
+ if (normalized.length > 0) {
59523
+ return normalized;
59524
+ }
59525
+ if (defaultHomeLineId && allowedLineIds.has(defaultHomeLineId)) {
59526
+ return [defaultHomeLineId];
59527
+ }
59528
+ return visibleLineIds.length > 0 ? [visibleLineIds[0]] : [];
59529
+ };
59530
+ const selectedLineIdsEqual = (left, right) => {
59531
+ if (left.length !== right.length) return false;
59532
+ return left.every((value, index) => value === right[index]);
59533
+ };
59534
+ const isAllLinesSelection = (lineIdsToCheck) => visibleLineIds.length > 0 && lineIdsToCheck.length === visibleLineIds.length;
59535
+ const readPersistedSelectedLineIds = () => {
59450
59536
  if (typeof window === "undefined") {
59451
- return defaultHomeLineId;
59537
+ return null;
59452
59538
  }
59453
59539
  try {
59454
- const savedLineId = sessionStorage.getItem(LINE_FILTER_STORAGE_KEY);
59455
- if (savedLineId) {
59456
- if (availableLineIds.includes(savedLineId)) {
59457
- return savedLineId;
59540
+ const savedLineIds = sessionStorage.getItem("optifye_home_line_filters_v2");
59541
+ if (savedLineIds) {
59542
+ const parsedLineIds = JSON.parse(savedLineIds);
59543
+ if (Array.isArray(parsedLineIds)) {
59544
+ return normalizeSelectedLineIds(parsedLineIds.map((lineId) => String(lineId)));
59458
59545
  }
59459
59546
  }
59547
+ const legacyLineId = sessionStorage.getItem("optifye_home_line_filter");
59548
+ if (legacyLineId) {
59549
+ if (legacyLineId === factoryViewId) {
59550
+ return normalizeSelectedLineIds(visibleLineIds);
59551
+ }
59552
+ return normalizeSelectedLineIds([legacyLineId]);
59553
+ }
59460
59554
  } catch (error) {
59461
59555
  console.warn("Failed to read line filter from sessionStorage:", error);
59462
59556
  }
59463
- return defaultHomeLineId;
59464
- });
59557
+ return null;
59558
+ };
59559
+ const [selectedLineIds, setSelectedLineIds] = useState(() => readPersistedSelectedLineIds() || normalizeSelectedLineIds([defaultHomeLineId]));
59560
+ const [isLineSelectorOpen, setIsLineSelectorOpen] = useState(false);
59561
+ const [pendingSelectedLineIds, setPendingSelectedLineIds] = useState([]);
59562
+ const lineSelectorRef = useRef(null);
59465
59563
  useEffect(() => {
59466
- if (!user || availableLineIds.length === 0) {
59564
+ if (isLineSelectorOpen) {
59565
+ setPendingSelectedLineIds(selectedLineIds);
59566
+ }
59567
+ }, [isLineSelectorOpen, selectedLineIds]);
59568
+ useEffect(() => {
59569
+ if (!hasUser || visibleLineIds.length === 0) {
59467
59570
  return;
59468
59571
  }
59469
- try {
59470
- const savedLineId = sessionStorage.getItem(LINE_FILTER_STORAGE_KEY);
59471
- if (savedLineId && availableLineIds.includes(savedLineId)) {
59472
- if (savedLineId !== selectedLineId) {
59473
- setSelectedLineId(savedLineId);
59474
- }
59475
- return;
59572
+ const restoredLineIds = readPersistedSelectedLineIds();
59573
+ setSelectedLineIds((previousSelectedLineIds) => {
59574
+ if (restoredLineIds) {
59575
+ return selectedLineIdsEqual(restoredLineIds, previousSelectedLineIds) ? previousSelectedLineIds : restoredLineIds;
59476
59576
  }
59577
+ const normalizedCurrentLineIds = normalizeSelectedLineIds(previousSelectedLineIds);
59578
+ return selectedLineIdsEqual(normalizedCurrentLineIds, previousSelectedLineIds) ? previousSelectedLineIds : normalizedCurrentLineIds;
59579
+ });
59580
+ }, [hasUser, visibleLineIdsKey, defaultHomeLineId, factoryViewId]);
59581
+ useEffect(() => {
59582
+ try {
59583
+ sessionStorage.setItem("optifye_home_line_filters_v2", JSON.stringify(selectedLineIds));
59584
+ sessionStorage.removeItem("optifye_home_line_filter");
59477
59585
  } catch (error) {
59478
- console.warn("Failed to read line filter from sessionStorage:", error);
59586
+ console.warn("Failed to save line filter to sessionStorage:", error);
59479
59587
  }
59480
- if (availableLineIds.includes(selectedLineId)) {
59588
+ }, [selectedLineIds]);
59589
+ useEffect(() => {
59590
+ if (!isLineSelectorOpen) {
59481
59591
  return;
59482
59592
  }
59483
- if (defaultHomeLineId !== selectedLineId) {
59484
- setSelectedLineId(defaultHomeLineId);
59485
- }
59486
- }, [
59487
- user,
59488
- availableLineIds,
59489
- defaultHomeLineId,
59490
- selectedLineId,
59491
- LINE_FILTER_STORAGE_KEY
59492
- ]);
59593
+ const handleClickOutside = (event) => {
59594
+ if (lineSelectorRef.current && !lineSelectorRef.current.contains(event.target)) {
59595
+ setIsLineSelectorOpen(false);
59596
+ }
59597
+ };
59598
+ document.addEventListener("mousedown", handleClickOutside);
59599
+ return () => {
59600
+ document.removeEventListener("mousedown", handleClickOutside);
59601
+ };
59602
+ }, [isLineSelectorOpen]);
59603
+ const primarySelectedLineId = selectedLineIds[0] || defaultHomeLineId;
59604
+ const isMultiLineSelection = selectedLineIds.length > 1;
59605
+ const selectedLineIdsKey = selectedLineIds.join(",");
59606
+ const selectedLineIdSet = useMemo(
59607
+ () => new Set(selectedLineIds),
59608
+ [selectedLineIds]
59609
+ );
59610
+ const metricsScopeLineId = isMultiLineSelection ? factoryViewId : primarySelectedLineId;
59493
59611
  const userCompanyId = useMemo(() => {
59494
59612
  return user?.properties?.company_id || user?.company_id || entityConfig.companyId;
59495
59613
  }, [user, entityConfig.companyId]);
@@ -59509,12 +59627,8 @@ function HomeView({
59509
59627
  useEffect(() => {
59510
59628
  const initDisplayNames = async () => {
59511
59629
  try {
59512
- if (selectedLineId === factoryViewId) {
59513
- for (const lineId of visibleLineIds) {
59514
- await preInitializeWorkspaceDisplayNames(lineId);
59515
- }
59516
- } else {
59517
- await preInitializeWorkspaceDisplayNames(selectedLineId);
59630
+ for (const lineId of selectedLineIds) {
59631
+ await preInitializeWorkspaceDisplayNames(lineId);
59518
59632
  }
59519
59633
  setDisplayNamesInitialized(true);
59520
59634
  } catch (error) {
@@ -59523,8 +59637,8 @@ function HomeView({
59523
59637
  }
59524
59638
  };
59525
59639
  initDisplayNames();
59526
- }, [selectedLineId, factoryViewId, visibleLineIds]);
59527
- const displayNameLineId = selectedLineId === factoryViewId ? void 0 : selectedLineId;
59640
+ }, [selectedLineIdsKey]);
59641
+ const displayNameLineId = isMultiLineSelection ? void 0 : primarySelectedLineId;
59528
59642
  const {
59529
59643
  displayNames: workspaceDisplayNames,
59530
59644
  loading: displayNamesLoading,
@@ -59553,7 +59667,8 @@ function HomeView({
59553
59667
  error: metricsError,
59554
59668
  refetch: refetchMetrics
59555
59669
  } = useDashboardMetrics({
59556
- lineId: selectedLineId,
59670
+ lineId: metricsScopeLineId,
59671
+ lineIds: selectedLineIds,
59557
59672
  onLineMetricsUpdate: handleLineMetricsUpdate,
59558
59673
  userAccessibleLineIds: visibleLineIds
59559
59674
  // Pass user's accessible lines for supervisor filtering
@@ -59561,10 +59676,8 @@ function HomeView({
59561
59676
  const trendGroups = useMemo(() => {
59562
59677
  const lineMetricsRows = lineMetrics || [];
59563
59678
  if (!lineMetricsRows.length) return null;
59564
- if (selectedLineId === factoryViewId) {
59565
- const candidateLineIds = visibleLineIds.filter((id3) => id3 && id3 !== factoryViewId);
59566
- if (!candidateLineIds.length) return null;
59567
- const rowsForLines = lineMetricsRows.filter((row2) => candidateLineIds.includes(row2?.line_id));
59679
+ if (selectedLineIds.length > 1) {
59680
+ const rowsForLines = lineMetricsRows.filter((row2) => selectedLineIdSet.has(row2?.line_id));
59568
59681
  if (!rowsForLines.length) return null;
59569
59682
  const groupsMap = /* @__PURE__ */ new Map();
59570
59683
  rowsForLines.forEach((row2) => {
@@ -59587,16 +59700,16 @@ function HomeView({
59587
59700
  shiftId: group.shiftId
59588
59701
  }));
59589
59702
  }
59590
- const row = lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
59703
+ const row = lineMetricsRows.find((r2) => r2?.line_id === primarySelectedLineId);
59591
59704
  if (!row?.date || row?.shift_id === void 0 || row?.shift_id === null) {
59592
59705
  return null;
59593
59706
  }
59594
59707
  return [{
59595
- lineIds: [selectedLineId],
59708
+ lineIds: [primarySelectedLineId],
59596
59709
  date: row.date,
59597
59710
  shiftId: row.shift_id
59598
59711
  }];
59599
- }, [selectedLineId, factoryViewId, visibleLineIds, lineMetrics]);
59712
+ }, [lineMetrics, primarySelectedLineId, selectedLineIdSet, selectedLineIds.length]);
59600
59713
  const trendOptions = useMemo(() => {
59601
59714
  if (!trendGroups || !userCompanyId) return null;
59602
59715
  return {
@@ -59624,17 +59737,18 @@ function HomeView({
59624
59737
  }, [workspaceMetrics, metricsLoading, metricsError]);
59625
59738
  const kpis = useMemo(() => {
59626
59739
  const lineMetricsRows = lineMetrics || [];
59627
- if (selectedLineId === factoryViewId) {
59628
- if (metricsLoading && lineMetricsRows.length === 0) return null;
59629
- return aggregateKPIsFromLineMetricsRows(lineMetricsRows);
59740
+ if (selectedLineIds.length > 1) {
59741
+ const rowsForSelectedLines = lineMetricsRows.filter((row2) => selectedLineIdSet.has(row2?.line_id));
59742
+ if (metricsLoading && rowsForSelectedLines.length === 0) return null;
59743
+ return aggregateKPIsFromLineMetricsRows(rowsForSelectedLines);
59630
59744
  }
59631
- const row = lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
59745
+ const row = lineMetricsRows.find((r2) => r2?.line_id === primarySelectedLineId);
59632
59746
  if (!row) {
59633
59747
  if (metricsLoading) return null;
59634
59748
  return buildKPIsFromLineMetricsRow(null);
59635
59749
  }
59636
59750
  return buildKPIsFromLineMetricsRow(row);
59637
- }, [selectedLineId, factoryViewId, lineMetrics, metricsLoading]);
59751
+ }, [lineMetrics, metricsLoading, primarySelectedLineId, selectedLineIdSet, selectedLineIds.length]);
59638
59752
  const kpisWithTrend = useMemo(() => {
59639
59753
  if (!kpis) return null;
59640
59754
  if (!kpiTrend) return kpis;
@@ -59659,30 +59773,30 @@ function HomeView({
59659
59773
  };
59660
59774
  }, [kpis, kpiTrend]);
59661
59775
  const selectedLineMeta = useMemo(
59662
- () => dbLines.find((line) => line.id === selectedLineId),
59663
- [dbLines, selectedLineId]
59776
+ () => selectedLineIds.length === 1 ? dbLines.find((line) => line.id === primarySelectedLineId) : void 0,
59777
+ [dbLines, primarySelectedLineId, selectedLineIds.length]
59664
59778
  );
59665
- const selectedMonitoringMode = selectedLineId === factoryViewId ? "output" : selectedLineMeta?.monitoring_mode ?? "output";
59779
+ const selectedMonitoringMode = selectedLineIds.length === 1 ? selectedLineMeta?.monitoring_mode ?? "output" : "output";
59666
59780
  const isUptimeMode = selectedMonitoringMode === "uptime";
59667
59781
  const averageIdleTimeSeconds = useMemo(() => {
59668
59782
  if (!isUptimeMode) return null;
59669
- const targetWorkspaces = selectedLineId === factoryViewId ? workspaceMetrics : workspaceMetrics.filter((ws) => ws.line_id === selectedLineId);
59783
+ const targetWorkspaces = workspaceMetrics.filter((ws) => ws.line_id === primarySelectedLineId);
59670
59784
  const idleValues = targetWorkspaces.map((ws) => ws.idle_time).filter((value) => Number.isFinite(value));
59671
59785
  if (idleValues.length === 0) return 0;
59672
59786
  const totalIdle = idleValues.reduce((sum, value) => sum + value, 0);
59673
59787
  return totalIdle / idleValues.length;
59674
- }, [isUptimeMode, selectedLineId, factoryViewId, workspaceMetrics]);
59788
+ }, [isUptimeMode, primarySelectedLineId, workspaceMetrics]);
59675
59789
  const {
59676
59790
  activeBreaks: allActiveBreaks,
59677
59791
  isLoading: breaksLoading,
59678
59792
  error: breaksError
59679
- } = useActiveBreaks(allLineIds);
59793
+ } = useActiveBreaks(visibleLineIds);
59680
59794
  const activeBreaks = useMemo(() => {
59681
- if (selectedLineId === factoryViewId) {
59795
+ if (isAllLinesSelection(selectedLineIds)) {
59682
59796
  return allActiveBreaks;
59683
59797
  }
59684
- return allActiveBreaks.filter((breakItem) => breakItem.lineId === selectedLineId);
59685
- }, [allActiveBreaks, selectedLineId, factoryViewId]);
59798
+ return allActiveBreaks.filter((breakItem) => selectedLineIdSet.has(breakItem.lineId));
59799
+ }, [allActiveBreaks, selectedLineIdSet, selectedLineIds]);
59686
59800
  const activeBreakLineIds = useMemo(
59687
59801
  () => new Set(activeBreaks.map((breakItem) => breakItem.lineId)),
59688
59802
  [activeBreaks]
@@ -59989,7 +60103,7 @@ function HomeView({
59989
60103
  // Round to 1 decimal
59990
60104
  kpisWithTrend?.avgCycleTime?.change,
59991
60105
  kpisWithTrend?.qualityCompliance?.value ? Math.round(kpisWithTrend.qualityCompliance.value) : null,
59992
- selectedLineId
60106
+ selectedLineIdsKey
59993
60107
  ]);
59994
60108
  useEffect(() => {
59995
60109
  setIsHydrated(true);
@@ -60006,27 +60120,62 @@ function HomeView({
60006
60120
  setErrorMessage(null);
60007
60121
  }
60008
60122
  }, [metricsError]);
60009
- const handleLineChange = useCallback((value) => {
60123
+ const getTrackedLineScope = useCallback((lineIdsForScope) => {
60124
+ if (isAllLinesSelection(lineIdsForScope)) {
60125
+ return factoryViewId;
60126
+ }
60127
+ if (lineIdsForScope.length === 1) {
60128
+ return lineIdsForScope[0];
60129
+ }
60130
+ return "custom_multi";
60131
+ }, [factoryViewId, visibleLineIds.length]);
60132
+ const getLineSelectionLabel = useCallback((lineIdsForScope) => {
60133
+ if (isAllLinesSelection(lineIdsForScope)) {
60134
+ return "All Lines";
60135
+ }
60136
+ if (lineIdsForScope.length === 1) {
60137
+ const lineId = lineIdsForScope[0];
60138
+ return mergedLineNames[lineId] || `Line ${lineId.substring(0, 4)}`;
60139
+ }
60140
+ return `${lineIdsForScope.length} Lines Selected`;
60141
+ }, [mergedLineNames, visibleLineIds.length]);
60142
+ const updateSelectedLineIds = useCallback((nextLineIds) => {
60143
+ const normalizedLineIds = normalizeSelectedLineIds(nextLineIds);
60144
+ if (selectedLineIdsEqual(normalizedLineIds, selectedLineIds)) {
60145
+ return;
60146
+ }
60010
60147
  setIsChangingFilter(true);
60011
- setSelectedLineId(value);
60148
+ setSelectedLineIds(normalizedLineIds);
60012
60149
  trackCoreEvent("monitor line filter changed", {
60013
- previous_line_id: selectedLineId,
60014
- new_line_id: value,
60015
- line_name: mergedLineNames[value] || (value === factoryViewId ? "All Lines" : `Line ${value.substring(0, 4)}`)
60150
+ previous_line_id: getTrackedLineScope(selectedLineIds),
60151
+ new_line_id: getTrackedLineScope(normalizedLineIds),
60152
+ previous_line_ids: selectedLineIds,
60153
+ new_line_ids: normalizedLineIds,
60154
+ selected_line_count: normalizedLineIds.length,
60155
+ selection_mode: isAllLinesSelection(normalizedLineIds) ? "all" : normalizedLineIds.length === 1 ? "single" : "custom",
60156
+ line_name: getLineSelectionLabel(normalizedLineIds)
60016
60157
  });
60017
- try {
60018
- sessionStorage.setItem(LINE_FILTER_STORAGE_KEY, value);
60019
- } catch (error) {
60020
- console.warn("Failed to save line filter to sessionStorage:", error);
60158
+ }, [factoryViewId, getLineSelectionLabel, getTrackedLineScope, selectedLineIds, selectedLineIdsKey, visibleLineIds]);
60159
+ useCallback(() => {
60160
+ updateSelectedLineIds(visibleLineIds);
60161
+ }, [updateSelectedLineIds, visibleLineIds]);
60162
+ useCallback((lineId) => {
60163
+ const currentSelection = new Set(selectedLineIds);
60164
+ if (currentSelection.has(lineId)) {
60165
+ if (currentSelection.size <= 1) {
60166
+ return;
60167
+ }
60168
+ currentSelection.delete(lineId);
60169
+ } else {
60170
+ currentSelection.add(lineId);
60021
60171
  }
60022
- }, [LINE_FILTER_STORAGE_KEY, selectedLineId, mergedLineNames, factoryViewId]);
60172
+ updateSelectedLineIds(Array.from(currentSelection));
60173
+ }, [selectedLineIds, updateSelectedLineIds]);
60023
60174
  useEffect(() => {
60024
60175
  if (!metricsLoading && isChangingFilter) {
60025
- if (workspaceMetrics.length > 0 || selectedLineId === factoryViewId) {
60026
- setIsChangingFilter(false);
60027
- }
60176
+ setIsChangingFilter(false);
60028
60177
  }
60029
- }, [metricsLoading, workspaceMetrics, isChangingFilter, selectedLineId, factoryViewId]);
60178
+ }, [metricsLoading, isChangingFilter]);
60030
60179
  useEffect(() => {
60031
60180
  if (!metricsLoading && !hasInitialDataLoaded) {
60032
60181
  setHasInitialDataLoaded(true);
@@ -60045,11 +60194,107 @@ function HomeView({
60045
60194
  if (visibleLineIds.length <= 1) {
60046
60195
  return null;
60047
60196
  }
60048
- return /* @__PURE__ */ jsxs(Select, { onValueChange: handleLineChange, value: selectedLineId, children: [
60049
- /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full sm:w-[200px] bg-white border border-gray-200 shadow-sm rounded-md h-9 sm:h-9 text-xs sm:text-sm px-2 sm:px-3", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select a line" }) }),
60050
- /* @__PURE__ */ jsx(SelectContent, { className: "z-50 bg-white shadow-lg border border-gray-200 rounded-md text-xs sm:text-sm", children: availableLineIds.map((id3) => /* @__PURE__ */ jsx(SelectItem, { value: id3, children: mergedLineNames[id3] || (id3 === factoryViewId ? "All Lines" : `Line ${id3.substring(0, 4)}`) }, id3)) })
60197
+ const allLinesSelected = isAllLinesSelection(pendingSelectedLineIds);
60198
+ return /* @__PURE__ */ jsxs("div", { ref: lineSelectorRef, className: "relative", children: [
60199
+ /* @__PURE__ */ jsxs(
60200
+ "button",
60201
+ {
60202
+ type: "button",
60203
+ onClick: () => setIsLineSelectorOpen((previous) => !previous),
60204
+ className: "flex min-w-[180px] items-center justify-between gap-2 rounded-md border border-slate-200 bg-white px-3 py-1.5 text-left text-sm font-medium shadow-sm transition-colors hover:bg-slate-50 text-slate-700",
60205
+ "aria-haspopup": "menu",
60206
+ "aria-expanded": isLineSelectorOpen,
60207
+ "aria-label": "Select lines",
60208
+ children: [
60209
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: getLineSelectionLabel(selectedLineIds) }),
60210
+ /* @__PURE__ */ jsx(ChevronDown, { className: `h-4 w-4 text-slate-400 transition-transform ${isLineSelectorOpen ? "rotate-180" : ""}` })
60211
+ ]
60212
+ }
60213
+ ),
60214
+ isLineSelectorOpen ? /* @__PURE__ */ jsxs("div", { className: "absolute right-0 top-full z-50 mt-2 w-[280px] rounded-lg border border-slate-200 bg-white p-3 shadow-xl flex flex-col gap-2", children: [
60215
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pb-2 border-b border-slate-100", children: [
60216
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-slate-800", children: "Select Lines" }),
60217
+ /* @__PURE__ */ jsx(
60218
+ "button",
60219
+ {
60220
+ type: "button",
60221
+ onClick: () => setPendingSelectedLineIds([]),
60222
+ className: "text-xs font-medium text-blue-600 hover:text-blue-700 transition-colors",
60223
+ children: "Clear All"
60224
+ }
60225
+ )
60226
+ ] }),
60227
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm font-medium text-slate-900 hover:bg-slate-50 transition-colors", children: [
60228
+ /* @__PURE__ */ jsx(
60229
+ "input",
60230
+ {
60231
+ type: "checkbox",
60232
+ className: "h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500",
60233
+ checked: allLinesSelected,
60234
+ onChange: () => setPendingSelectedLineIds(allLinesSelected ? [] : visibleLineIds)
60235
+ }
60236
+ ),
60237
+ /* @__PURE__ */ jsx("span", { children: "All Lines" })
60238
+ ] }),
60239
+ /* @__PURE__ */ jsx("div", { className: "max-h-56 space-y-0.5 overflow-y-auto pr-1", children: visibleLineIds.map((lineId) => {
60240
+ const isChecked = pendingSelectedLineIds.includes(lineId);
60241
+ return /* @__PURE__ */ jsxs(
60242
+ "label",
60243
+ {
60244
+ className: "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors text-slate-700 hover:bg-slate-50",
60245
+ children: [
60246
+ /* @__PURE__ */ jsx(
60247
+ "input",
60248
+ {
60249
+ type: "checkbox",
60250
+ className: "h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500",
60251
+ checked: isChecked,
60252
+ onChange: () => {
60253
+ setPendingSelectedLineIds((prev) => {
60254
+ const current = new Set(prev);
60255
+ if (current.has(lineId)) {
60256
+ current.delete(lineId);
60257
+ } else {
60258
+ current.add(lineId);
60259
+ }
60260
+ return Array.from(current);
60261
+ });
60262
+ }
60263
+ }
60264
+ ),
60265
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: mergedLineNames[lineId] || `Line ${lineId.substring(0, 4)}` })
60266
+ ]
60267
+ },
60268
+ lineId
60269
+ );
60270
+ }) }),
60271
+ /* @__PURE__ */ jsx("div", { className: "pt-3 pb-1 mt-1 border-t border-slate-100 flex justify-end", children: /* @__PURE__ */ jsx(
60272
+ "button",
60273
+ {
60274
+ type: "button",
60275
+ onClick: () => {
60276
+ if (pendingSelectedLineIds.length > 0) {
60277
+ updateSelectedLineIds(pendingSelectedLineIds);
60278
+ }
60279
+ setIsLineSelectorOpen(false);
60280
+ },
60281
+ disabled: pendingSelectedLineIds.length === 0,
60282
+ className: "bg-blue-600 text-white px-4 py-1.5 rounded-md text-sm font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors w-full",
60283
+ children: "Apply"
60284
+ }
60285
+ ) })
60286
+ ] }) : null
60051
60287
  ] });
60052
- }, [availableLineIds, handleLineChange, selectedLineId, mergedLineNames, factoryViewId, visibleLineIds.length]);
60288
+ }, [
60289
+ getLineSelectionLabel,
60290
+ isLineSelectorOpen,
60291
+ mergedLineNames,
60292
+ selectedLineIds,
60293
+ pendingSelectedLineIds,
60294
+ visibleLineIds,
60295
+ updateSelectedLineIds,
60296
+ isAllLinesSelection
60297
+ ]);
60053
60298
  const useSmoothLoading = (isLoading, minDuration = 400) => {
60054
60299
  const [showLoading, setShowLoading] = useState(isLoading);
60055
60300
  const loadingStartRef2 = useRef(null);
@@ -60095,11 +60340,11 @@ function HomeView({
60095
60340
  const isDataLoading = metricsLoading || displayNamesLoading && workspaceMetrics.length === 0;
60096
60341
  const hasKpiDataReady = useMemo(() => {
60097
60342
  const lineMetricsRows = lineMetrics || [];
60098
- if (selectedLineId === factoryViewId) {
60099
- return lineMetricsRows.length > 0;
60343
+ if (selectedLineIds.length > 1) {
60344
+ return lineMetricsRows.some((row) => selectedLineIdSet.has(row?.line_id));
60100
60345
  }
60101
- return lineMetricsRows.some((row) => row?.line_id === selectedLineId);
60102
- }, [lineMetrics, selectedLineId, factoryViewId]);
60346
+ return lineMetricsRows.some((row) => row?.line_id === primarySelectedLineId);
60347
+ }, [lineMetrics, primarySelectedLineId, selectedLineIdSet, selectedLineIds.length]);
60103
60348
  const isKpiLoading = !hasKpiDataReady;
60104
60349
  useEffect(() => {
60105
60350
  const minLoadingDurationMs = 250;
@@ -60161,76 +60406,77 @@ function HomeView({
60161
60406
  DashboardHeader,
60162
60407
  {
60163
60408
  lineTitle,
60164
- lineId: selectedLineId === factoryViewId ? allLineIds[0] : selectedLineId,
60409
+ lineId: primarySelectedLineId,
60165
60410
  className: "w-full",
60166
60411
  headerControls: kpiSectionControl
60167
60412
  }
60168
60413
  ) }) }),
60169
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto sm:overflow-hidden relative", children: [
60170
- /* @__PURE__ */ jsx("div", { className: "absolute right-2 top-1 sm:right-6 sm:top-3 z-30 flex items-center space-x-2", children: lineSelectorComponent && lineSelectorComponent }),
60171
- /* @__PURE__ */ jsx("div", { className: "h-full sm:h-full min-h-[calc(100vh-100px)] sm:min-h-0", children: workspaceMetricsWithBreakState.length > 0 ? /* @__PURE__ */ jsx(
60172
- motion.div,
60173
- {
60174
- initial: { opacity: 0, scale: 0.98 },
60175
- animate: { opacity: 1, scale: 1 },
60176
- transition: { duration: 0.3 },
60414
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto sm:overflow-hidden relative flex flex-col", children: /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-[calc(100vh-100px)] sm:min-h-0", children: workspaceMetricsWithBreakState.length > 0 ? /* @__PURE__ */ jsx(
60415
+ motion.div,
60416
+ {
60417
+ initial: { opacity: 0, scale: 0.98 },
60418
+ animate: { opacity: 1, scale: 1 },
60419
+ transition: { duration: 0.3 },
60420
+ className: "h-full",
60421
+ children: React142__default.createElement(WorkspaceGrid, {
60422
+ workspaces: workspaceMetricsWithBreakState,
60423
+ lineNames: mergedLineNames,
60424
+ lineOrder: selectedLineIds,
60425
+ factoryView: factoryViewId,
60426
+ legend: efficiencyLegend,
60427
+ videoSources,
60428
+ videoStreamsByWorkspaceId,
60429
+ videoStreamsLoading,
60430
+ displayNames: workspaceDisplayNames,
60431
+ hasFlowBuffers,
60177
60432
  className: "h-full",
60178
- children: React142__default.createElement(WorkspaceGrid, {
60179
- workspaces: workspaceMetricsWithBreakState,
60180
- lineNames,
60181
- factoryView: factoryViewId,
60182
- legend: efficiencyLegend,
60183
- videoSources,
60184
- videoStreamsByWorkspaceId,
60185
- videoStreamsLoading,
60186
- displayNames: workspaceDisplayNames,
60187
- hasFlowBuffers,
60188
- className: "h-full",
60189
- onWorkspaceHover: handleWorkspaceHover,
60190
- onWorkspaceHoverEnd: handleWorkspaceHoverEnd
60191
- })
60192
- },
60193
- selectedLineId
60194
- ) : !shouldShowDataLoading && hasInitialDataLoaded ? /* @__PURE__ */ jsx(
60195
- motion.div,
60196
- {
60197
- initial: { opacity: 0 },
60198
- animate: { opacity: 1 },
60199
- transition: { duration: 0.3 },
60200
- children: /* @__PURE__ */ jsx(NoWorkspaceData, { message: "No workspace data available. Select another line or check configurations." })
60201
- }
60202
- ) : /* @__PURE__ */ jsx(
60203
- motion.div,
60204
- {
60205
- initial: { opacity: 0, scale: 0.98 },
60206
- animate: { opacity: 1, scale: 1 },
60207
- transition: { duration: 0.3 },
60433
+ toolbarRightContent: lineSelectorComponent,
60434
+ onWorkspaceHover: handleWorkspaceHover,
60435
+ onWorkspaceHoverEnd: handleWorkspaceHoverEnd
60436
+ })
60437
+ },
60438
+ selectedLineIdsKey
60439
+ ) : !shouldShowDataLoading && hasInitialDataLoaded ? /* @__PURE__ */ jsx(
60440
+ motion.div,
60441
+ {
60442
+ initial: { opacity: 0 },
60443
+ animate: { opacity: 1 },
60444
+ transition: { duration: 0.3 },
60445
+ children: /* @__PURE__ */ jsx(NoWorkspaceData, { message: "No workspace data available. Adjust the selected lines or check configurations." })
60446
+ }
60447
+ ) : /* @__PURE__ */ jsx(
60448
+ motion.div,
60449
+ {
60450
+ initial: { opacity: 0, scale: 0.98 },
60451
+ animate: { opacity: 1, scale: 1 },
60452
+ transition: { duration: 0.3 },
60453
+ className: "h-full",
60454
+ children: React142__default.createElement(WorkspaceGrid, {
60455
+ workspaces: [],
60456
+ // Show empty grid while loading
60457
+ lineNames: mergedLineNames,
60458
+ lineOrder: selectedLineIds,
60459
+ factoryView: factoryViewId,
60460
+ legend: efficiencyLegend,
60461
+ videoSources,
60462
+ videoStreamsByWorkspaceId,
60463
+ videoStreamsLoading,
60464
+ displayNames: workspaceDisplayNames,
60465
+ hasFlowBuffers,
60208
60466
  className: "h-full",
60209
- children: React142__default.createElement(WorkspaceGrid, {
60210
- workspaces: [],
60211
- // Show empty grid while loading
60212
- lineNames,
60213
- factoryView: factoryViewId,
60214
- legend: efficiencyLegend,
60215
- videoSources,
60216
- videoStreamsByWorkspaceId,
60217
- videoStreamsLoading,
60218
- displayNames: workspaceDisplayNames,
60219
- hasFlowBuffers,
60220
- className: "h-full",
60221
- onWorkspaceHover: handleWorkspaceHover,
60222
- onWorkspaceHoverEnd: handleWorkspaceHoverEnd
60223
- })
60224
- },
60225
- selectedLineId
60226
- ) })
60227
- ] })
60467
+ toolbarRightContent: lineSelectorComponent,
60468
+ onWorkspaceHover: handleWorkspaceHover,
60469
+ onWorkspaceHoverEnd: handleWorkspaceHoverEnd
60470
+ })
60471
+ },
60472
+ selectedLineIdsKey
60473
+ ) }) })
60228
60474
  ] }),
60229
60475
  /* @__PURE__ */ jsx(
60230
60476
  BreakNotificationPopup,
60231
60477
  {
60232
60478
  activeBreaks,
60233
- lineNames,
60479
+ lineNames: mergedLineNames,
60234
60480
  isVisible: !breaksLoading && !breaksError && !breakNotificationsDismissed,
60235
60481
  onDismiss: () => setBreakNotificationsDismissed(true)
60236
60482
  }