@optifye/dashboard-core 6.10.36 → 6.10.37

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.d.mts CHANGED
@@ -4070,11 +4070,22 @@ type UseOperationalShiftKeyResult = {
4070
4070
  };
4071
4071
  declare const useOperationalShiftKey: ({ enabled, timezone, shiftConfig, pollIntervalMs, }: UseOperationalShiftKeyParams) => UseOperationalShiftKeyResult;
4072
4072
 
4073
- interface UseKpiTrendsOptions {
4073
+ interface KpiTrendGroup {
4074
4074
  lineIds: string[];
4075
4075
  date: string;
4076
4076
  shiftId: number;
4077
+ }
4078
+ interface UseKpiTrendsOptions {
4079
+ groups: KpiTrendGroup[];
4077
4080
  companyId?: string;
4081
+ /**
4082
+ * Optional refresh token to force re-fetch (e.g., on realtime updates).
4083
+ */
4084
+ refreshKey?: number;
4085
+ /**
4086
+ * Optional flag to bypass backend cache.
4087
+ */
4088
+ forceRefresh?: boolean;
4078
4089
  }
4079
4090
  interface UseKpiTrendsResult {
4080
4091
  trend: KpiTrend | null;
package/dist/index.d.ts CHANGED
@@ -4070,11 +4070,22 @@ type UseOperationalShiftKeyResult = {
4070
4070
  };
4071
4071
  declare const useOperationalShiftKey: ({ enabled, timezone, shiftConfig, pollIntervalMs, }: UseOperationalShiftKeyParams) => UseOperationalShiftKeyResult;
4072
4072
 
4073
- interface UseKpiTrendsOptions {
4073
+ interface KpiTrendGroup {
4074
4074
  lineIds: string[];
4075
4075
  date: string;
4076
4076
  shiftId: number;
4077
+ }
4078
+ interface UseKpiTrendsOptions {
4079
+ groups: KpiTrendGroup[];
4077
4080
  companyId?: string;
4081
+ /**
4082
+ * Optional refresh token to force re-fetch (e.g., on realtime updates).
4083
+ */
4084
+ refreshKey?: number;
4085
+ /**
4086
+ * Optional flag to bypass backend cache.
4087
+ */
4088
+ forceRefresh?: boolean;
4078
4089
  }
4079
4090
  interface UseKpiTrendsResult {
4080
4091
  trend: KpiTrend | null;
package/dist/index.js CHANGED
@@ -15007,53 +15007,110 @@ var useKpiTrends = (options) => {
15007
15007
  const [trend, setTrend] = React26.useState(null);
15008
15008
  const [isLoading, setIsLoading] = React26.useState(false);
15009
15009
  const [error, setError] = React26.useState(null);
15010
+ const activeRequestIdRef = React26.useRef(0);
15011
+ const lastParamsKeyRef = React26.useRef(null);
15010
15012
  const params = React26.useMemo(() => {
15011
15013
  const resolvedCompanyId = options?.companyId || entityConfig?.companyId;
15012
15014
  if (!options || !resolvedCompanyId) return null;
15013
- const line_ids = options.lineIds.filter(Boolean);
15014
- if (!line_ids.length) return null;
15015
+ const normalizedGroups = (options.groups || []).map((group) => ({
15016
+ lineIds: (group.lineIds || []).filter(Boolean).slice().sort(),
15017
+ date: group.date,
15018
+ shiftId: group.shiftId
15019
+ })).filter((group) => group.lineIds.length > 0 && Boolean(group.date) && group.shiftId !== void 0 && group.shiftId !== null);
15020
+ if (!normalizedGroups.length) return null;
15021
+ const groupsKey = normalizedGroups.map((group) => `${group.date}|${group.shiftId}|${group.lineIds.join(",")}`).sort().join("||");
15015
15022
  return {
15016
- lineIds: line_ids,
15017
15023
  companyId: resolvedCompanyId,
15018
- date: options.date,
15019
- shiftId: options.shiftId
15024
+ groups: normalizedGroups,
15025
+ key: groupsKey
15020
15026
  };
15021
15027
  }, [options, entityConfig?.companyId]);
15022
15028
  React26.useEffect(() => {
15023
15029
  let isMounted = true;
15024
- if (!params || !supabase) {
15030
+ const refreshKey = options?.refreshKey ?? 0;
15031
+ const paramsKey = params ? `${params.companyId}|${params.key}|${refreshKey}` : null;
15032
+ if (!params || !supabase || !paramsKey) {
15025
15033
  setTrend(null);
15034
+ lastParamsKeyRef.current = null;
15035
+ return;
15036
+ }
15037
+ if (lastParamsKeyRef.current === paramsKey) {
15026
15038
  return;
15027
15039
  }
15040
+ lastParamsKeyRef.current = paramsKey;
15041
+ const requestId = ++activeRequestIdRef.current;
15042
+ const averageNumber = (values) => {
15043
+ const nums = values.filter((value) => typeof value === "number" && Number.isFinite(value));
15044
+ if (!nums.length) return null;
15045
+ return nums.reduce((sum, value) => sum + value, 0) / nums.length;
15046
+ };
15047
+ const aggregateTrends = (trends) => ({
15048
+ efficiency: {
15049
+ delta_pp: averageNumber(trends.map((item) => item.efficiency?.delta_pp)),
15050
+ current: averageNumber(trends.map((item) => item.efficiency?.current)),
15051
+ previous: averageNumber(trends.map((item) => item.efficiency?.previous))
15052
+ },
15053
+ outputProgress: {
15054
+ delta_pp: averageNumber(trends.map((item) => item.outputProgress?.delta_pp)),
15055
+ current: averageNumber(trends.map((item) => item.outputProgress?.current)),
15056
+ previous: averageNumber(trends.map((item) => item.outputProgress?.previous))
15057
+ },
15058
+ underperformingWorkers: {
15059
+ delta_count: averageNumber(trends.map((item) => item.underperformingWorkers?.delta_count)),
15060
+ current: averageNumber(trends.map((item) => item.underperformingWorkers?.current)),
15061
+ previous: averageNumber(trends.map((item) => item.underperformingWorkers?.previous))
15062
+ },
15063
+ avgCycleTime: {
15064
+ delta_seconds: averageNumber(trends.map((item) => item.avgCycleTime?.delta_seconds)),
15065
+ current: averageNumber(trends.map((item) => item.avgCycleTime?.current)),
15066
+ previous: averageNumber(trends.map((item) => item.avgCycleTime?.previous))
15067
+ }
15068
+ });
15028
15069
  const fetchTrend = async () => {
15029
15070
  setIsLoading(true);
15030
15071
  setError(null);
15031
15072
  try {
15032
- const searchParams = new URLSearchParams();
15033
- if (params.lineIds.length === 1) {
15034
- searchParams.append("line_id", params.lineIds[0]);
15073
+ const forceRefresh = options?.forceRefresh;
15074
+ const groupResults = await Promise.all(
15075
+ params.groups.map(async (group) => {
15076
+ const searchParams = new URLSearchParams();
15077
+ if (group.lineIds.length === 1) {
15078
+ searchParams.append("line_id", group.lineIds[0]);
15079
+ } else {
15080
+ searchParams.append("line_ids", group.lineIds.join(","));
15081
+ }
15082
+ searchParams.append("date", group.date);
15083
+ searchParams.append("shift_id", group.shiftId.toString());
15084
+ searchParams.append("company_id", params.companyId);
15085
+ if (forceRefresh) {
15086
+ searchParams.append("force_refresh", "true");
15087
+ }
15088
+ const response = await fetchBackendJson(supabase, `/api/dashboard/line-kpis?${searchParams.toString()}`);
15089
+ return response?.kpis?.trend || null;
15090
+ })
15091
+ );
15092
+ if (!isMounted || requestId !== activeRequestIdRef.current) return;
15093
+ const trends = groupResults.filter((item) => Boolean(item));
15094
+ if (!trends.length) {
15095
+ setTrend(null);
15096
+ } else if (trends.length === 1) {
15097
+ setTrend(trends[0]);
15035
15098
  } else {
15036
- searchParams.append("line_ids", params.lineIds.join(","));
15099
+ setTrend(aggregateTrends(trends));
15037
15100
  }
15038
- searchParams.append("date", params.date);
15039
- searchParams.append("shift_id", params.shiftId.toString());
15040
- searchParams.append("company_id", params.companyId);
15041
- const response = await fetchBackendJson(supabase, `/api/dashboard/line-kpis?${searchParams.toString()}`);
15042
- if (!isMounted) return;
15043
- setTrend(response?.kpis?.trend || null);
15044
15101
  } catch (err) {
15045
- if (!isMounted) return;
15102
+ if (!isMounted || requestId !== activeRequestIdRef.current) return;
15046
15103
  setError(err);
15047
15104
  setTrend(null);
15048
15105
  } finally {
15049
- if (isMounted) setIsLoading(false);
15106
+ if (isMounted && requestId === activeRequestIdRef.current) setIsLoading(false);
15050
15107
  }
15051
15108
  };
15052
15109
  fetchTrend();
15053
15110
  return () => {
15054
15111
  isMounted = false;
15055
15112
  };
15056
- }, [params, supabase]);
15113
+ }, [params, supabase, options?.refreshKey, options?.forceRefresh]);
15057
15114
  return { trend, isLoading, error };
15058
15115
  };
15059
15116
  var useMonthlyTrend = (params) => {
@@ -17020,12 +17077,16 @@ var aggregateKPIsFromLineMetricsRows = (rows) => {
17020
17077
  (sum, row) => sum + (toNumber(row.ideal_output) || toNumber(row.line_threshold)),
17021
17078
  0
17022
17079
  );
17023
- const totalWorkspacesAll = rows.reduce((sum, row) => sum + toNumber(row.total_workspaces), 0);
17024
- const weightedEfficiencySum = rows.reduce(
17025
- (sum, row) => sum + toNumber(row.avg_efficiency) * toNumber(row.total_workspaces),
17026
- 0
17027
- );
17028
- const avgEfficiency = totalWorkspacesAll > 0 ? weightedEfficiencySum / totalWorkspacesAll : 0;
17080
+ const efficiencyValues = rows.map((row) => {
17081
+ const value = row?.avg_efficiency;
17082
+ if (typeof value === "number" && Number.isFinite(value)) return value;
17083
+ if (typeof value === "string" && value.trim() !== "") {
17084
+ const parsed = Number(value);
17085
+ return Number.isFinite(parsed) ? parsed : null;
17086
+ }
17087
+ return null;
17088
+ }).filter((value) => value !== null);
17089
+ const avgEfficiency = efficiencyValues.length > 0 ? efficiencyValues.reduce((sum, value) => sum + value, 0) / efficiencyValues.length : 0;
17029
17090
  const numLines = rows.length;
17030
17091
  const avgCycleTime = numLines > 0 ? rows.reduce((sum, row) => sum + toNumber(row.avg_cycle_time), 0) / numLines : 0;
17031
17092
  const totalUnderperforming = rows.reduce((sum, row) => sum + toNumber(row.underperforming_workspaces), 0);
@@ -45786,7 +45847,7 @@ var KPICard = ({
45786
45847
  style.compact ? "text-base" : "text-lg"
45787
45848
  ), children: suffix })
45788
45849
  ] }),
45789
- !isLoading && trendInfo.shouldShowTrend && trendInfo.trendValue !== "neutral" && title !== "Output" && (trendMode === "pill" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx(
45850
+ !isLoading && trendInfo.shouldShowTrend && title !== "Output" && ((trendInfo.trendValue !== "neutral" || change === 0 && showZeroChange) && (trendMode === "pill" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx(
45790
45851
  "flex items-center gap-1 px-2 py-0.5 rounded-full ml-2",
45791
45852
  trendInfo.bgClass,
45792
45853
  trendInfo.colorClass
@@ -45823,7 +45884,7 @@ var KPICard = ({
45823
45884
  formattedChange,
45824
45885
  "%"
45825
45886
  ] })
45826
- ] })),
45887
+ ] }))),
45827
45888
  status?.indicator && !isLoading && /* @__PURE__ */ jsxRuntime.jsx(
45828
45889
  "div",
45829
45890
  {
@@ -45981,7 +46042,7 @@ var KPISection = React26.memo(({
45981
46042
  const outputIsOnTarget = outputDifference >= 0;
45982
46043
  if (useSrcLayout) {
45983
46044
  const effChange = showSkeleton ? 0 : kpis.efficiency.change ?? 0;
45984
- const effTrend = effChange >= 0 ? "up" : "down";
46045
+ const effTrend = effChange > 0 ? "up" : effChange < 0 ? "down" : "neutral";
45985
46046
  return /* @__PURE__ */ jsxRuntime.jsxs(
45986
46047
  "div",
45987
46048
  {
@@ -46079,8 +46140,11 @@ var KPISection = React26.memo(({
46079
46140
  if (!prevKpis || !nextKpis) return false;
46080
46141
  if (prevKpis === nextKpis) return true;
46081
46142
  if (Math.abs(prevKpis.efficiency.value - nextKpis.efficiency.value) >= 0.5) return false;
46143
+ if (prevKpis.efficiency.change !== nextKpis.efficiency.change) return false;
46082
46144
  if (prevKpis.underperformingWorkers.current !== nextKpis.underperformingWorkers.current || prevKpis.underperformingWorkers.total !== nextKpis.underperformingWorkers.total) return false;
46145
+ if (prevKpis.underperformingWorkers.change !== nextKpis.underperformingWorkers.change) return false;
46083
46146
  if (prevKpis.outputProgress.current !== nextKpis.outputProgress.current || prevKpis.outputProgress.target !== nextKpis.outputProgress.target) return false;
46147
+ if (prevKpis.outputProgress.change !== nextKpis.outputProgress.change) return false;
46084
46148
  if (prevProps.layout !== nextProps.layout || prevProps.cardVariant !== nextProps.cardVariant || prevProps.compactCards !== nextProps.compactCards || prevProps.useSrcLayout !== nextProps.useSrcLayout || prevProps.className !== nextProps.className) return false;
46085
46149
  return true;
46086
46150
  });
@@ -53666,6 +53730,8 @@ function HomeView({
53666
53730
  const [hasInitialDataLoaded, setHasInitialDataLoaded] = React26.useState(false);
53667
53731
  const [showDataLoading, setShowDataLoading] = React26.useState(false);
53668
53732
  const loadingStartRef = React26.useRef(null);
53733
+ const [trendRefreshKey, setTrendRefreshKey] = React26.useState(0);
53734
+ const trendRefreshTimerRef = React26.useRef(null);
53669
53735
  const dashboardConfig = useDashboardConfig();
53670
53736
  const entityConfig = useEntityConfig();
53671
53737
  const supabaseClient = useSupabaseClient();
@@ -53816,6 +53882,20 @@ function HomeView({
53816
53882
  loading: displayNamesLoading,
53817
53883
  error: displayNamesError
53818
53884
  } = useWorkspaceDisplayNames(displayNameLineId, void 0);
53885
+ const handleLineMetricsUpdate = React26.useCallback(() => {
53886
+ if (trendRefreshTimerRef.current) {
53887
+ window.clearTimeout(trendRefreshTimerRef.current);
53888
+ }
53889
+ trendRefreshTimerRef.current = window.setTimeout(() => {
53890
+ setTrendRefreshKey((prev) => prev + 1);
53891
+ trendRefreshTimerRef.current = null;
53892
+ }, 2e3);
53893
+ }, []);
53894
+ React26.useEffect(() => () => {
53895
+ if (trendRefreshTimerRef.current) {
53896
+ window.clearTimeout(trendRefreshTimerRef.current);
53897
+ }
53898
+ }, []);
53819
53899
  const {
53820
53900
  workspaceMetrics,
53821
53901
  lineMetrics,
@@ -53826,32 +53906,59 @@ function HomeView({
53826
53906
  refetch: refetchMetrics
53827
53907
  } = useDashboardMetrics({
53828
53908
  lineId: selectedLineId,
53909
+ onLineMetricsUpdate: handleLineMetricsUpdate,
53829
53910
  userAccessibleLineIds: visibleLineIds
53830
53911
  // Pass user's accessible lines for supervisor filtering
53831
53912
  });
53832
- const appTimezone = timezone;
53833
- const defaultTimezone = appTimezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
53834
- const { shiftId: fallbackShiftId, date: fallbackDate } = React26.useMemo(
53835
- () => getCurrentShift(defaultTimezone, dashboardConfig?.shiftConfig),
53836
- [defaultTimezone, dashboardConfig?.shiftConfig]
53837
- );
53838
- const trendQuery = React26.useMemo(() => {
53913
+ const trendGroups = React26.useMemo(() => {
53839
53914
  const lineMetricsRows = lineMetrics || [];
53840
- const referenceRow = selectedLineId === factoryViewId ? lineMetricsRows[0] : lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
53841
- const lineIdsForTrend = selectedLineId === factoryViewId ? visibleLineIds.filter((id3) => id3 !== factoryViewId) : [selectedLineId];
53842
- const dateForTrend = referenceRow?.date || fallbackDate;
53843
- const shiftIdForTrend = referenceRow?.shift_id ?? fallbackShiftId;
53844
- if (!lineIdsForTrend.length || !dateForTrend || shiftIdForTrend === void 0 || shiftIdForTrend === null) {
53915
+ if (!lineMetricsRows.length) return null;
53916
+ if (selectedLineId === factoryViewId) {
53917
+ const candidateLineIds = visibleLineIds.filter((id3) => id3 && id3 !== factoryViewId);
53918
+ if (!candidateLineIds.length) return null;
53919
+ const rowsForLines = lineMetricsRows.filter((row2) => candidateLineIds.includes(row2?.line_id));
53920
+ if (!rowsForLines.length) return null;
53921
+ const groupsMap = /* @__PURE__ */ new Map();
53922
+ rowsForLines.forEach((row2) => {
53923
+ const lineId = row2?.line_id;
53924
+ const date = row2?.date;
53925
+ const shiftId = row2?.shift_id;
53926
+ if (!lineId || !date || shiftId === void 0 || shiftId === null) return;
53927
+ const key = `${date}|${shiftId}`;
53928
+ let group = groupsMap.get(key);
53929
+ if (!group) {
53930
+ group = { date, shiftId, lineIds: /* @__PURE__ */ new Set() };
53931
+ groupsMap.set(key, group);
53932
+ }
53933
+ group.lineIds.add(lineId);
53934
+ });
53935
+ if (!groupsMap.size) return null;
53936
+ return Array.from(groupsMap.values()).map((group) => ({
53937
+ lineIds: Array.from(group.lineIds),
53938
+ date: group.date,
53939
+ shiftId: group.shiftId
53940
+ }));
53941
+ }
53942
+ const row = lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
53943
+ if (!row?.date || row?.shift_id === void 0 || row?.shift_id === null) {
53845
53944
  return null;
53846
53945
  }
53946
+ return [{
53947
+ lineIds: [selectedLineId],
53948
+ date: row.date,
53949
+ shiftId: row.shift_id
53950
+ }];
53951
+ }, [selectedLineId, factoryViewId, visibleLineIds, lineMetrics]);
53952
+ const trendOptions = React26.useMemo(() => {
53953
+ if (!trendGroups || !userCompanyId) return null;
53847
53954
  return {
53848
- lineIds: lineIdsForTrend,
53849
- date: dateForTrend,
53850
- shiftId: shiftIdForTrend,
53851
- companyId: userCompanyId
53955
+ groups: trendGroups,
53956
+ companyId: userCompanyId,
53957
+ refreshKey: trendRefreshKey,
53958
+ forceRefresh: trendRefreshKey > 0
53852
53959
  };
53853
- }, [selectedLineId, factoryViewId, visibleLineIds, lineMetrics, fallbackDate, fallbackShiftId, userCompanyId]);
53854
- const { trend: kpiTrend } = useKpiTrends(trendQuery);
53960
+ }, [trendGroups, userCompanyId, trendRefreshKey]);
53961
+ const { trend: kpiTrend } = useKpiTrends(trendOptions);
53855
53962
  const hasFlowBuffers = Boolean(metricsMetadata?.hasFlowBuffers);
53856
53963
  React26.useEffect(() => {
53857
53964
  const sample = workspaceMetrics.slice(0, 3).map((workspace) => ({
package/dist/index.mjs CHANGED
@@ -14978,53 +14978,110 @@ var useKpiTrends = (options) => {
14978
14978
  const [trend, setTrend] = useState(null);
14979
14979
  const [isLoading, setIsLoading] = useState(false);
14980
14980
  const [error, setError] = useState(null);
14981
+ const activeRequestIdRef = useRef(0);
14982
+ const lastParamsKeyRef = useRef(null);
14981
14983
  const params = useMemo(() => {
14982
14984
  const resolvedCompanyId = options?.companyId || entityConfig?.companyId;
14983
14985
  if (!options || !resolvedCompanyId) return null;
14984
- const line_ids = options.lineIds.filter(Boolean);
14985
- if (!line_ids.length) return null;
14986
+ const normalizedGroups = (options.groups || []).map((group) => ({
14987
+ lineIds: (group.lineIds || []).filter(Boolean).slice().sort(),
14988
+ date: group.date,
14989
+ shiftId: group.shiftId
14990
+ })).filter((group) => group.lineIds.length > 0 && Boolean(group.date) && group.shiftId !== void 0 && group.shiftId !== null);
14991
+ if (!normalizedGroups.length) return null;
14992
+ const groupsKey = normalizedGroups.map((group) => `${group.date}|${group.shiftId}|${group.lineIds.join(",")}`).sort().join("||");
14986
14993
  return {
14987
- lineIds: line_ids,
14988
14994
  companyId: resolvedCompanyId,
14989
- date: options.date,
14990
- shiftId: options.shiftId
14995
+ groups: normalizedGroups,
14996
+ key: groupsKey
14991
14997
  };
14992
14998
  }, [options, entityConfig?.companyId]);
14993
14999
  useEffect(() => {
14994
15000
  let isMounted = true;
14995
- if (!params || !supabase) {
15001
+ const refreshKey = options?.refreshKey ?? 0;
15002
+ const paramsKey = params ? `${params.companyId}|${params.key}|${refreshKey}` : null;
15003
+ if (!params || !supabase || !paramsKey) {
14996
15004
  setTrend(null);
15005
+ lastParamsKeyRef.current = null;
15006
+ return;
15007
+ }
15008
+ if (lastParamsKeyRef.current === paramsKey) {
14997
15009
  return;
14998
15010
  }
15011
+ lastParamsKeyRef.current = paramsKey;
15012
+ const requestId = ++activeRequestIdRef.current;
15013
+ const averageNumber = (values) => {
15014
+ const nums = values.filter((value) => typeof value === "number" && Number.isFinite(value));
15015
+ if (!nums.length) return null;
15016
+ return nums.reduce((sum, value) => sum + value, 0) / nums.length;
15017
+ };
15018
+ const aggregateTrends = (trends) => ({
15019
+ efficiency: {
15020
+ delta_pp: averageNumber(trends.map((item) => item.efficiency?.delta_pp)),
15021
+ current: averageNumber(trends.map((item) => item.efficiency?.current)),
15022
+ previous: averageNumber(trends.map((item) => item.efficiency?.previous))
15023
+ },
15024
+ outputProgress: {
15025
+ delta_pp: averageNumber(trends.map((item) => item.outputProgress?.delta_pp)),
15026
+ current: averageNumber(trends.map((item) => item.outputProgress?.current)),
15027
+ previous: averageNumber(trends.map((item) => item.outputProgress?.previous))
15028
+ },
15029
+ underperformingWorkers: {
15030
+ delta_count: averageNumber(trends.map((item) => item.underperformingWorkers?.delta_count)),
15031
+ current: averageNumber(trends.map((item) => item.underperformingWorkers?.current)),
15032
+ previous: averageNumber(trends.map((item) => item.underperformingWorkers?.previous))
15033
+ },
15034
+ avgCycleTime: {
15035
+ delta_seconds: averageNumber(trends.map((item) => item.avgCycleTime?.delta_seconds)),
15036
+ current: averageNumber(trends.map((item) => item.avgCycleTime?.current)),
15037
+ previous: averageNumber(trends.map((item) => item.avgCycleTime?.previous))
15038
+ }
15039
+ });
14999
15040
  const fetchTrend = async () => {
15000
15041
  setIsLoading(true);
15001
15042
  setError(null);
15002
15043
  try {
15003
- const searchParams = new URLSearchParams();
15004
- if (params.lineIds.length === 1) {
15005
- searchParams.append("line_id", params.lineIds[0]);
15044
+ const forceRefresh = options?.forceRefresh;
15045
+ const groupResults = await Promise.all(
15046
+ params.groups.map(async (group) => {
15047
+ const searchParams = new URLSearchParams();
15048
+ if (group.lineIds.length === 1) {
15049
+ searchParams.append("line_id", group.lineIds[0]);
15050
+ } else {
15051
+ searchParams.append("line_ids", group.lineIds.join(","));
15052
+ }
15053
+ searchParams.append("date", group.date);
15054
+ searchParams.append("shift_id", group.shiftId.toString());
15055
+ searchParams.append("company_id", params.companyId);
15056
+ if (forceRefresh) {
15057
+ searchParams.append("force_refresh", "true");
15058
+ }
15059
+ const response = await fetchBackendJson(supabase, `/api/dashboard/line-kpis?${searchParams.toString()}`);
15060
+ return response?.kpis?.trend || null;
15061
+ })
15062
+ );
15063
+ if (!isMounted || requestId !== activeRequestIdRef.current) return;
15064
+ const trends = groupResults.filter((item) => Boolean(item));
15065
+ if (!trends.length) {
15066
+ setTrend(null);
15067
+ } else if (trends.length === 1) {
15068
+ setTrend(trends[0]);
15006
15069
  } else {
15007
- searchParams.append("line_ids", params.lineIds.join(","));
15070
+ setTrend(aggregateTrends(trends));
15008
15071
  }
15009
- searchParams.append("date", params.date);
15010
- searchParams.append("shift_id", params.shiftId.toString());
15011
- searchParams.append("company_id", params.companyId);
15012
- const response = await fetchBackendJson(supabase, `/api/dashboard/line-kpis?${searchParams.toString()}`);
15013
- if (!isMounted) return;
15014
- setTrend(response?.kpis?.trend || null);
15015
15072
  } catch (err) {
15016
- if (!isMounted) return;
15073
+ if (!isMounted || requestId !== activeRequestIdRef.current) return;
15017
15074
  setError(err);
15018
15075
  setTrend(null);
15019
15076
  } finally {
15020
- if (isMounted) setIsLoading(false);
15077
+ if (isMounted && requestId === activeRequestIdRef.current) setIsLoading(false);
15021
15078
  }
15022
15079
  };
15023
15080
  fetchTrend();
15024
15081
  return () => {
15025
15082
  isMounted = false;
15026
15083
  };
15027
- }, [params, supabase]);
15084
+ }, [params, supabase, options?.refreshKey, options?.forceRefresh]);
15028
15085
  return { trend, isLoading, error };
15029
15086
  };
15030
15087
  var useMonthlyTrend = (params) => {
@@ -16991,12 +17048,16 @@ var aggregateKPIsFromLineMetricsRows = (rows) => {
16991
17048
  (sum, row) => sum + (toNumber(row.ideal_output) || toNumber(row.line_threshold)),
16992
17049
  0
16993
17050
  );
16994
- const totalWorkspacesAll = rows.reduce((sum, row) => sum + toNumber(row.total_workspaces), 0);
16995
- const weightedEfficiencySum = rows.reduce(
16996
- (sum, row) => sum + toNumber(row.avg_efficiency) * toNumber(row.total_workspaces),
16997
- 0
16998
- );
16999
- const avgEfficiency = totalWorkspacesAll > 0 ? weightedEfficiencySum / totalWorkspacesAll : 0;
17051
+ const efficiencyValues = rows.map((row) => {
17052
+ const value = row?.avg_efficiency;
17053
+ if (typeof value === "number" && Number.isFinite(value)) return value;
17054
+ if (typeof value === "string" && value.trim() !== "") {
17055
+ const parsed = Number(value);
17056
+ return Number.isFinite(parsed) ? parsed : null;
17057
+ }
17058
+ return null;
17059
+ }).filter((value) => value !== null);
17060
+ const avgEfficiency = efficiencyValues.length > 0 ? efficiencyValues.reduce((sum, value) => sum + value, 0) / efficiencyValues.length : 0;
17000
17061
  const numLines = rows.length;
17001
17062
  const avgCycleTime = numLines > 0 ? rows.reduce((sum, row) => sum + toNumber(row.avg_cycle_time), 0) / numLines : 0;
17002
17063
  const totalUnderperforming = rows.reduce((sum, row) => sum + toNumber(row.underperforming_workspaces), 0);
@@ -45757,7 +45818,7 @@ var KPICard = ({
45757
45818
  style.compact ? "text-base" : "text-lg"
45758
45819
  ), children: suffix })
45759
45820
  ] }),
45760
- !isLoading && trendInfo.shouldShowTrend && trendInfo.trendValue !== "neutral" && title !== "Output" && (trendMode === "pill" ? /* @__PURE__ */ jsxs("span", { className: clsx(
45821
+ !isLoading && trendInfo.shouldShowTrend && title !== "Output" && ((trendInfo.trendValue !== "neutral" || change === 0 && showZeroChange) && (trendMode === "pill" ? /* @__PURE__ */ jsxs("span", { className: clsx(
45761
45822
  "flex items-center gap-1 px-2 py-0.5 rounded-full ml-2",
45762
45823
  trendInfo.bgClass,
45763
45824
  trendInfo.colorClass
@@ -45794,7 +45855,7 @@ var KPICard = ({
45794
45855
  formattedChange,
45795
45856
  "%"
45796
45857
  ] })
45797
- ] })),
45858
+ ] }))),
45798
45859
  status?.indicator && !isLoading && /* @__PURE__ */ jsx(
45799
45860
  "div",
45800
45861
  {
@@ -45952,7 +46013,7 @@ var KPISection = memo$1(({
45952
46013
  const outputIsOnTarget = outputDifference >= 0;
45953
46014
  if (useSrcLayout) {
45954
46015
  const effChange = showSkeleton ? 0 : kpis.efficiency.change ?? 0;
45955
- const effTrend = effChange >= 0 ? "up" : "down";
46016
+ const effTrend = effChange > 0 ? "up" : effChange < 0 ? "down" : "neutral";
45956
46017
  return /* @__PURE__ */ jsxs(
45957
46018
  "div",
45958
46019
  {
@@ -46050,8 +46111,11 @@ var KPISection = memo$1(({
46050
46111
  if (!prevKpis || !nextKpis) return false;
46051
46112
  if (prevKpis === nextKpis) return true;
46052
46113
  if (Math.abs(prevKpis.efficiency.value - nextKpis.efficiency.value) >= 0.5) return false;
46114
+ if (prevKpis.efficiency.change !== nextKpis.efficiency.change) return false;
46053
46115
  if (prevKpis.underperformingWorkers.current !== nextKpis.underperformingWorkers.current || prevKpis.underperformingWorkers.total !== nextKpis.underperformingWorkers.total) return false;
46116
+ if (prevKpis.underperformingWorkers.change !== nextKpis.underperformingWorkers.change) return false;
46054
46117
  if (prevKpis.outputProgress.current !== nextKpis.outputProgress.current || prevKpis.outputProgress.target !== nextKpis.outputProgress.target) return false;
46118
+ if (prevKpis.outputProgress.change !== nextKpis.outputProgress.change) return false;
46055
46119
  if (prevProps.layout !== nextProps.layout || prevProps.cardVariant !== nextProps.cardVariant || prevProps.compactCards !== nextProps.compactCards || prevProps.useSrcLayout !== nextProps.useSrcLayout || prevProps.className !== nextProps.className) return false;
46056
46120
  return true;
46057
46121
  });
@@ -53637,6 +53701,8 @@ function HomeView({
53637
53701
  const [hasInitialDataLoaded, setHasInitialDataLoaded] = useState(false);
53638
53702
  const [showDataLoading, setShowDataLoading] = useState(false);
53639
53703
  const loadingStartRef = useRef(null);
53704
+ const [trendRefreshKey, setTrendRefreshKey] = useState(0);
53705
+ const trendRefreshTimerRef = useRef(null);
53640
53706
  const dashboardConfig = useDashboardConfig();
53641
53707
  const entityConfig = useEntityConfig();
53642
53708
  const supabaseClient = useSupabaseClient();
@@ -53787,6 +53853,20 @@ function HomeView({
53787
53853
  loading: displayNamesLoading,
53788
53854
  error: displayNamesError
53789
53855
  } = useWorkspaceDisplayNames(displayNameLineId, void 0);
53856
+ const handleLineMetricsUpdate = useCallback(() => {
53857
+ if (trendRefreshTimerRef.current) {
53858
+ window.clearTimeout(trendRefreshTimerRef.current);
53859
+ }
53860
+ trendRefreshTimerRef.current = window.setTimeout(() => {
53861
+ setTrendRefreshKey((prev) => prev + 1);
53862
+ trendRefreshTimerRef.current = null;
53863
+ }, 2e3);
53864
+ }, []);
53865
+ useEffect(() => () => {
53866
+ if (trendRefreshTimerRef.current) {
53867
+ window.clearTimeout(trendRefreshTimerRef.current);
53868
+ }
53869
+ }, []);
53790
53870
  const {
53791
53871
  workspaceMetrics,
53792
53872
  lineMetrics,
@@ -53797,32 +53877,59 @@ function HomeView({
53797
53877
  refetch: refetchMetrics
53798
53878
  } = useDashboardMetrics({
53799
53879
  lineId: selectedLineId,
53880
+ onLineMetricsUpdate: handleLineMetricsUpdate,
53800
53881
  userAccessibleLineIds: visibleLineIds
53801
53882
  // Pass user's accessible lines for supervisor filtering
53802
53883
  });
53803
- const appTimezone = timezone;
53804
- const defaultTimezone = appTimezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
53805
- const { shiftId: fallbackShiftId, date: fallbackDate } = useMemo(
53806
- () => getCurrentShift(defaultTimezone, dashboardConfig?.shiftConfig),
53807
- [defaultTimezone, dashboardConfig?.shiftConfig]
53808
- );
53809
- const trendQuery = useMemo(() => {
53884
+ const trendGroups = useMemo(() => {
53810
53885
  const lineMetricsRows = lineMetrics || [];
53811
- const referenceRow = selectedLineId === factoryViewId ? lineMetricsRows[0] : lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
53812
- const lineIdsForTrend = selectedLineId === factoryViewId ? visibleLineIds.filter((id3) => id3 !== factoryViewId) : [selectedLineId];
53813
- const dateForTrend = referenceRow?.date || fallbackDate;
53814
- const shiftIdForTrend = referenceRow?.shift_id ?? fallbackShiftId;
53815
- if (!lineIdsForTrend.length || !dateForTrend || shiftIdForTrend === void 0 || shiftIdForTrend === null) {
53886
+ if (!lineMetricsRows.length) return null;
53887
+ if (selectedLineId === factoryViewId) {
53888
+ const candidateLineIds = visibleLineIds.filter((id3) => id3 && id3 !== factoryViewId);
53889
+ if (!candidateLineIds.length) return null;
53890
+ const rowsForLines = lineMetricsRows.filter((row2) => candidateLineIds.includes(row2?.line_id));
53891
+ if (!rowsForLines.length) return null;
53892
+ const groupsMap = /* @__PURE__ */ new Map();
53893
+ rowsForLines.forEach((row2) => {
53894
+ const lineId = row2?.line_id;
53895
+ const date = row2?.date;
53896
+ const shiftId = row2?.shift_id;
53897
+ if (!lineId || !date || shiftId === void 0 || shiftId === null) return;
53898
+ const key = `${date}|${shiftId}`;
53899
+ let group = groupsMap.get(key);
53900
+ if (!group) {
53901
+ group = { date, shiftId, lineIds: /* @__PURE__ */ new Set() };
53902
+ groupsMap.set(key, group);
53903
+ }
53904
+ group.lineIds.add(lineId);
53905
+ });
53906
+ if (!groupsMap.size) return null;
53907
+ return Array.from(groupsMap.values()).map((group) => ({
53908
+ lineIds: Array.from(group.lineIds),
53909
+ date: group.date,
53910
+ shiftId: group.shiftId
53911
+ }));
53912
+ }
53913
+ const row = lineMetricsRows.find((r2) => r2?.line_id === selectedLineId);
53914
+ if (!row?.date || row?.shift_id === void 0 || row?.shift_id === null) {
53816
53915
  return null;
53817
53916
  }
53917
+ return [{
53918
+ lineIds: [selectedLineId],
53919
+ date: row.date,
53920
+ shiftId: row.shift_id
53921
+ }];
53922
+ }, [selectedLineId, factoryViewId, visibleLineIds, lineMetrics]);
53923
+ const trendOptions = useMemo(() => {
53924
+ if (!trendGroups || !userCompanyId) return null;
53818
53925
  return {
53819
- lineIds: lineIdsForTrend,
53820
- date: dateForTrend,
53821
- shiftId: shiftIdForTrend,
53822
- companyId: userCompanyId
53926
+ groups: trendGroups,
53927
+ companyId: userCompanyId,
53928
+ refreshKey: trendRefreshKey,
53929
+ forceRefresh: trendRefreshKey > 0
53823
53930
  };
53824
- }, [selectedLineId, factoryViewId, visibleLineIds, lineMetrics, fallbackDate, fallbackShiftId, userCompanyId]);
53825
- const { trend: kpiTrend } = useKpiTrends(trendQuery);
53931
+ }, [trendGroups, userCompanyId, trendRefreshKey]);
53932
+ const { trend: kpiTrend } = useKpiTrends(trendOptions);
53826
53933
  const hasFlowBuffers = Boolean(metricsMetadata?.hasFlowBuffers);
53827
53934
  useEffect(() => {
53828
53935
  const sample = workspaceMetrics.slice(0, 3).map((workspace) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optifye/dashboard-core",
3
- "version": "6.10.36",
3
+ "version": "6.10.37",
4
4
  "description": "Reusable UI & logic for Optifye dashboard",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",