use-home-overview.ts2.9 KBView on GitHub
'use client';

import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '@/providers/query-provider';

/**
 * The ONE query behind every metric tile in the home rail.
 *
 * `statistics.getOverview` already returns response time, follow-up time, the pipeline
 * breakdown, the activity timeline and the event totals in a single 60s-cached call — so five
 * metric tiles cost one request, not five. Each tile is a pure projection of this result and
 * fetches nothing of its own.
 *
 * The window is a FIXED rolling 30 days and is not a parameter. Making it one would give each
 * tile its own range, and each distinct range is another query — which is the whole reason the
 * shared call works. The rail is a glance surface; `/statistics` is where rescoping lives.
 */
const WINDOW_DAYS = 30;

export type HomeOverview = {
  responseTime?: { medianSeconds: number | null } | null;
  followUpTime?: { medianSeconds: number | null } | null;
  pipelineStats?: { status: string; count: number; totalDealValue: number }[];
  activityTimeline?: {
    date: string;
    emailInbound: number;
    emailOutbound: number;
    meetings: number;
    slackMessages: number;
    calls: number;
    notes: number;
    total: number;
  }[];
  totalEvents?: number;
  totalConversations?: number;
};

export interface HomeOverviewScope {
  aopId?: string | null;
  targetUserId?: string;
}

export function useHomeOverview(enabled: boolean, scope: HomeOverviewScope = {}) {
  const trpc = useTRPC();

  /**
   * Day-granular bounds so the key is stable across a session. Deriving them from `new Date()`
   * on every render would mint a new query key every millisecond and refetch forever.
   */
  const dateRange = useMemo(() => {
    const to = new Date();
    to.setHours(23, 59, 59, 999);
    const from = new Date(to);
    from.setDate(from.getDate() - WINDOW_DAYS);
    from.setHours(0, 0, 0, 0);
    return { dateFrom: from.toISOString(), dateTo: to.toISOString() };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [new Date().toDateString()]);

  const { data, isLoading } = useQuery({
    // The scope comes from the Pipeline widget's filters and applies to the WHOLE overview —
    // it is one call, so the statistics rows follow the same pipeline the table is showing.
    // `aopId` matters most: `computePipelineStats` groups over EVERY crm_conversation the user
    // owns, so unscoped it counts cold inbound, vendors, recruiting and spam as "deals".
    ...trpc.statistics.getOverview.queryOptions({
      dateRange,
      aopId: scope.aopId ?? undefined,
      targetUserId: scope.targetUserId,
    }),
    // Gated on the rail actually holding a metric tile: a default rail (meetings + agents)
    // must issue no statistics call at all.
    enabled,
    staleTime: 5 * 60 * 1000,
  });

  return { overview: data as HomeOverview | undefined, isLoading: enabled && isLoading };
}