feat(platform-analytics): Add admin analytics dashboards with CostsPage, PerformancePage, and RevenuePage integration and useAdminQuery hook

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-19 20:20:43 -07:00
parent 12d7599fbe
commit cb6b706b27
4 changed files with 110 additions and 175 deletions

View file

@ -2,9 +2,14 @@
* Admin Query Hooks
*
* React Query hooks for fetching analytics admin data.
* All date-aware admin hooks pass startDate/endDate from the global
* AnalyticsFilterContext so that every page responds to the shared
* date range selector.
*/
import { useQuery } from '@tanstack/react-query';
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useAnalyticsFilters } from '../context/AnalyticsFilterContext';
import type {
RevenueMetrics,
@ -47,20 +52,60 @@ import type {
TopRecipientItem,
} from '../api/analytics';
// Generic query hook for admin endpoints
// ============================================================================
// Hook option types
// ============================================================================
interface AdminQueryOptions {
enabled?: boolean;
refetchInterval?: number;
}
// ============================================================================
// Generic query hooks
// ============================================================================
/**
* Generic hook for admin endpoints that accept startDate/endDate from the
* global filter context. The query key includes the date params so React Query
* automatically re-fetches when the date range changes.
*/
export function useAdminQuery<T = unknown>(
endpoint: string,
options?: {
enabled?: boolean;
refetchInterval?: number;
}
) {
options?: AdminQueryOptions,
): UseQueryResult<T> {
const { filterParams } = useAnalyticsFilters();
return useQuery<T>({
queryKey: ['admin', endpoint, filterParams.startDate, filterParams.endDate],
queryFn: async (): Promise<T> => {
const url = new URL(`/api/analytics/${endpoint}`, window.location.origin);
url.searchParams.set('startDate', filterParams.startDate);
url.searchParams.set('endDate', filterParams.endDate);
const response = await fetch(url.toString());
if (!response.ok) { throw new Error('Failed to fetch data'); }
return response.json() as Promise<T>;
},
enabled: options?.enabled ?? true,
refetchInterval: options?.refetchInterval,
});
}
/**
* Generic hook for admin endpoints that do NOT accept date params
* (e.g. real-time endpoints, static lookups, endpoints with their own
* query string already constructed by the caller).
*/
export function useAdminStaticQuery<T = unknown>(
endpoint: string,
options?: AdminQueryOptions,
): UseQueryResult<T> {
return useQuery<T>({
queryKey: ['admin', endpoint],
queryFn: async () => {
queryFn: async (): Promise<T> => {
const response = await fetch(`/api/analytics/${endpoint}`);
if (!response.ok) {throw new Error('Failed to fetch data');}
return response.json();
if (!response.ok) { throw new Error('Failed to fetch data'); }
return response.json() as Promise<T>;
},
enabled: options?.enabled ?? true,
refetchInterval: options?.refetchInterval,
@ -71,15 +116,15 @@ export function useAdminQuery<T = unknown>(
// Analytics - Revenue Hooks
// ============================================================================
export function useRevenueMetrics() {
export function useRevenueMetrics(): UseQueryResult<RevenueMetrics> {
return useAdminQuery<RevenueMetrics>('admin/revenue/metrics');
}
export function useRevenueTrend() {
export function useRevenueTrend(): UseQueryResult<RevenueTrendPoint[]> {
return useAdminQuery<RevenueTrendPoint[]>('admin/revenue/trend');
}
export function useRevenueBreakdown() {
export function useRevenueBreakdown(): UseQueryResult<RevenueBreakdown> {
return useAdminQuery<RevenueBreakdown>('admin/revenue/breakdown');
}
@ -87,19 +132,19 @@ export function useRevenueBreakdown() {
// Analytics - Costs Hooks
// ============================================================================
export function useCostMetrics() {
export function useCostMetrics(): UseQueryResult<CostMetrics> {
return useAdminQuery<CostMetrics>('admin/costs/metrics');
}
export function useCostBreakdown() {
export function useCostBreakdown(): UseQueryResult<CostBreakdown> {
return useAdminQuery<CostBreakdown>('admin/costs/breakdown');
}
export function useCostTrend() {
export function useCostTrend(): UseQueryResult<CostTrendPoint[]> {
return useAdminQuery<CostTrendPoint[]>('admin/costs/trend');
}
export function useBudgetComparison() {
export function useBudgetComparison(): UseQueryResult<BudgetComparison> {
return useAdminQuery<BudgetComparison>('admin/budget/comparison');
}
@ -107,23 +152,23 @@ export function useBudgetComparison() {
// Analytics - P&L Hooks
// ============================================================================
export function usePnLStatement() {
export function usePnLStatement(): UseQueryResult<PnLStatement> {
return useAdminQuery<PnLStatement>('admin/pnl/statement');
}
export function usePnLTrend() {
export function usePnLTrend(): UseQueryResult<PnLTrendPoint[]> {
return useAdminQuery<PnLTrendPoint[]>('admin/pnl/trend');
}
export function useReserveProgress() {
return useAdminQuery<ReserveProgress>('admin/reserve/progress');
export function useReserveProgress(): UseQueryResult<ReserveProgress> {
return useAdminStaticQuery<ReserveProgress>('admin/reserve/progress');
}
// ============================================================================
// Analytics - Transactions Hooks
// ============================================================================
export function useTransactions(filters?: TransactionFilters) {
export function useTransactions(filters?: TransactionFilters): UseQueryResult<TransactionsResponse> {
const queryParams = new URLSearchParams();
if (filters?.status && filters.status !== 'all') {queryParams.append('status', filters.status);}
if (filters?.type && filters.type !== 'all') {queryParams.append('type', filters.type);}
@ -134,42 +179,42 @@ export function useTransactions(filters?: TransactionFilters) {
const queryString = queryParams.toString();
const endpoint = queryString ? `admin/transactions?${queryString}` : 'admin/transactions';
return useAdminQuery<TransactionsResponse>(endpoint);
return useAdminStaticQuery<TransactionsResponse>(endpoint);
}
export function useTransactionDetails(id?: string) {
return useAdminQuery<TransactionDetails>(`admin/transactions/${id}`, { enabled: !!id });
export function useTransactionDetails(id?: string): UseQueryResult<TransactionDetails> {
return useAdminStaticQuery<TransactionDetails>(`admin/transactions/${id}`, { enabled: !!id });
}
// ============================================================================
// Analytics - Real-Time Hooks
// ============================================================================
export function useRealTimeMetrics() {
return useAdminQuery<RealTimeMetrics>('admin/realtime/metrics', { refetchInterval: 5000 });
export function useRealTimeMetrics(): UseQueryResult<RealTimeMetrics> {
return useAdminStaticQuery<RealTimeMetrics>('admin/realtime/metrics', { refetchInterval: 5000 });
}
export function useRealTimeActivity() {
return useAdminQuery<RealTimeActivityItem[]>('admin/realtime/activity', { refetchInterval: 5000 });
export function useRealTimeActivity(): UseQueryResult<RealTimeActivityItem[]> {
return useAdminStaticQuery<RealTimeActivityItem[]>('admin/realtime/activity', { refetchInterval: 5000 });
}
export function useActiveUsers() {
return useAdminQuery<ActiveUserPoint[]>('admin/realtime/active-users', { refetchInterval: 5000 });
export function useActiveUsers(): UseQueryResult<ActiveUserPoint[]> {
return useAdminStaticQuery<ActiveUserPoint[]>('admin/realtime/active-users', { refetchInterval: 5000 });
}
// ============================================================================
// Analytics - Performance Hooks
// ============================================================================
export function usePerformanceMetrics() {
export function usePerformanceMetrics(): UseQueryResult<PerformanceMetrics> {
return useAdminQuery<PerformanceMetrics>('admin/performance/metrics');
}
export function usePerformanceHistory() {
export function usePerformanceHistory(): UseQueryResult<PerformanceHistoryPoint[]> {
return useAdminQuery<PerformanceHistoryPoint[]>('admin/performance/history');
}
export function useEndpointMetrics() {
export function useEndpointMetrics(): UseQueryResult<EndpointMetric[]> {
return useAdminQuery<EndpointMetric[]>('admin/performance/endpoints');
}
@ -177,19 +222,19 @@ export function useEndpointMetrics() {
// Analytics - Error Tracking Hooks
// ============================================================================
export function useErrorMetrics() {
export function useErrorMetrics(): UseQueryResult<ErrorMetrics> {
return useAdminQuery<ErrorMetrics>('admin/errors/metrics');
}
export function useErrorsByType() {
export function useErrorsByType(): UseQueryResult<ErrorByTypeItem[]> {
return useAdminQuery<ErrorByTypeItem[]>('admin/errors/by-type');
}
export function useErrorTrends() {
export function useErrorTrends(): UseQueryResult<ErrorTrendPoint[]> {
return useAdminQuery<ErrorTrendPoint[]>('admin/errors/trends');
}
export function useRecentErrors() {
export function useRecentErrors(): UseQueryResult<RecentError[]> {
return useAdminQuery<RecentError[]>('admin/errors/recent');
}
@ -197,19 +242,19 @@ export function useRecentErrors() {
// Analytics - Conversion Funnels Hooks
// ============================================================================
export function useConversionMetrics() {
export function useConversionMetrics(): UseQueryResult<ConversionMetrics> {
return useAdminQuery<ConversionMetrics>('admin/conversion/metrics');
}
export function useFunnelData() {
export function useFunnelData(): UseQueryResult<FunnelStage[]> {
return useAdminQuery<FunnelStage[]>('admin/conversion/funnel');
}
export function useConversionBySource() {
export function useConversionBySource(): UseQueryResult<ConversionBySourceItem[]> {
return useAdminQuery<ConversionBySourceItem[]>('admin/conversion/by-source');
}
export function useFunnelDataBySource() {
export function useFunnelDataBySource(): UseQueryResult<FunnelBySourceItem[]> {
return useAdminQuery<FunnelBySourceItem[]>('admin/conversion/funnel-by-source');
}
@ -217,15 +262,15 @@ export function useFunnelDataBySource() {
// Analytics - Bounce Rate Hooks
// ============================================================================
export function useBounceRateMetrics() {
export function useBounceRateMetrics(): UseQueryResult<BounceRateMetrics> {
return useAdminQuery<BounceRateMetrics>('admin/bounce-rate/metrics');
}
export function useBounceRateByPage() {
export function useBounceRateByPage(): UseQueryResult<BounceRateByPageItem[]> {
return useAdminQuery<BounceRateByPageItem[]>('admin/bounce-rate/by-page');
}
export function useBounceRateHistory() {
export function useBounceRateHistory(): UseQueryResult<BounceRateHistoryPoint[]> {
return useAdminQuery<BounceRateHistoryPoint[]>('admin/bounce-rate/history');
}
@ -233,39 +278,39 @@ export function useBounceRateHistory() {
// Analytics - A/B Testing Hooks
// ============================================================================
export function useABTestMetrics() {
return useAdminQuery<ABTestMetrics>('admin/ab-tests/metrics');
export function useABTestMetrics(): UseQueryResult<ABTestMetrics> {
return useAdminStaticQuery<ABTestMetrics>('admin/ab-tests/metrics');
}
export function useActiveTests() {
return useAdminQuery<ABTest[]>('admin/ab-tests/active');
export function useActiveTests(): UseQueryResult<ABTest[]> {
return useAdminStaticQuery<ABTest[]>('admin/ab-tests/active');
}
export function useTestResults() {
return useAdminQuery<ABTestResult[]>('admin/ab-tests/results');
export function useTestResults(): UseQueryResult<ABTestResult[]> {
return useAdminStaticQuery<ABTestResult[]>('admin/ab-tests/results');
}
// ============================================================================
// Analytics - Gift Hooks
// ============================================================================
export function useGiftMetrics() {
export function useGiftMetrics(): UseQueryResult<GiftAnalyticsMetrics> {
return useAdminQuery<GiftAnalyticsMetrics>('admin/gifts/metrics');
}
export function useGiftTrend() {
export function useGiftTrend(): UseQueryResult<GiftTrendPoint[]> {
return useAdminQuery<GiftTrendPoint[]>('admin/gifts/trend');
}
export function useGiftsByTemplate() {
export function useGiftsByTemplate(): UseQueryResult<GiftByTemplateItem[]> {
return useAdminQuery<GiftByTemplateItem[]>('admin/gifts/by-template');
}
export function useTopGifters() {
export function useTopGifters(): UseQueryResult<TopGifterItem[]> {
return useAdminQuery<TopGifterItem[]>('admin/gifts/top-gifters');
}
export function useTopRecipients() {
export function useTopRecipients(): UseQueryResult<TopRecipientItem[]> {
return useAdminQuery<TopRecipientItem[]>('admin/gifts/top-recipients');
}
@ -317,18 +362,18 @@ export interface FmtyRoute {
revenue: number;
}
export function useFmtyMetrics(period: 'day' | 'week' | 'month' = 'month') {
return useAdminQuery<FmtyMetrics>(`admin/fmty/metrics?period=${period}`);
export function useFmtyMetrics(period: 'day' | 'week' | 'month' = 'month'): UseQueryResult<FmtyMetrics> {
return useAdminStaticQuery<FmtyMetrics>(`admin/fmty/metrics?period=${period}`);
}
export function useFmtyComparison(period: 'day' | 'week' | 'month' = 'month') {
return useAdminQuery<FmtyComparison>(`admin/fmty/comparison?period=${period}`);
export function useFmtyComparison(period: 'day' | 'week' | 'month' = 'month'): UseQueryResult<FmtyComparison> {
return useAdminStaticQuery<FmtyComparison>(`admin/fmty/comparison?period=${period}`);
}
export function useFmtyTrends(period: 'day' | 'week' | 'month' = 'month') {
return useAdminQuery<FmtyTrendPoint[]>(`admin/fmty/trends?period=${period}`);
export function useFmtyTrends(period: 'day' | 'week' | 'month' = 'month'): UseQueryResult<FmtyTrendPoint[]> {
return useAdminStaticQuery<FmtyTrendPoint[]>(`admin/fmty/trends?period=${period}`);
}
export function useFmtyRoutes(period: 'day' | 'week' | 'month' = 'month', limit: number = 10) {
return useAdminQuery<FmtyRoute[]>(`admin/fmty/routes?period=${period}&limit=${limit}`);
export function useFmtyRoutes(period: 'day' | 'week' | 'month' = 'month', limit: number = 10): UseQueryResult<FmtyRoute[]> {
return useAdminStaticQuery<FmtyRoute[]>(`admin/fmty/routes?period=${period}&limit=${limit}`);
}

View file

@ -55,29 +55,6 @@ const HeaderActions = styled.div`
align-items: center;
`;
const DateFilterContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.spacing.sm};
padding: ${(props) => props.theme.spacing.xs};
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.md};
`;
const FilterButton = styled.button<{ $isActive: boolean }>`
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border: none;
border-radius: ${(props) => props.theme.borderRadius.sm};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
cursor: pointer;
transition: all ${(props) => props.theme.transitions.fast};
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : 'transparent')};
color: ${(props) => (props.$isActive ? '#fff' : props.theme.colors.text.secondary)};
&:hover {
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : props.theme.colors.hover.surface)};
}
`;
const AlertsContainer = styled.div`
display: flex;
@ -207,7 +184,6 @@ interface BudgetRow {
// ============================================================================
export const CostsPage = () => {
const [dateRange, setDateRange] = useState('30d');
const [showExportMenu, setShowExportMenu] = useState(false);
const { data: metrics, isLoading } = useCostMetrics();
@ -322,19 +298,6 @@ export const CostsPage = () => {
</div>
<HeaderActions>
<DateFilterContainer data-testid="date-filter">
{['7d', '30d', '90d'].map((range) => (
<FilterButton
key={range}
$isActive={dateRange === range}
onClick={() => setDateRange(range)}
data-testid={`filter-${range}`}
>
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : '90 Days'}
</FilterButton>
))}
</DateFilterContainer>
<div style={{ position: 'relative' }}>
<Button variant="ghost" size="sm" onClick={() => setShowExportMenu(!showExportMenu)}>
Export

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useMemo } from 'react';
import { MetricCard, DashboardLayout, DashboardWidget } from '@lilith/ui-analytics';
@ -44,30 +44,6 @@ const Subtitle = styled.p`
margin: ${(props) => props.theme.spacing.xs} 0 0 0;
`;
const TimeRangeContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.spacing.sm};
padding: ${(props) => props.theme.spacing.xs};
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.md};
`;
const TimeRangeButton = styled.button<{ $isActive: boolean }>`
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border: none;
border-radius: ${(props) => props.theme.borderRadius.sm};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
cursor: pointer;
transition: all ${(props) => props.theme.transitions.fast};
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : 'transparent')};
color: ${(props) => (props.$isActive ? '#fff' : props.theme.colors.text.secondary)};
&:hover {
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : props.theme.colors.hover.surface)};
}
`;
const AlertsContainer = styled.div`
display: flex;
@ -157,7 +133,6 @@ const ErrorRateBadge = styled.span<{ $value: number }>`
export const PerformancePage = () => {
const { data: metrics, isLoading } = usePerformanceMetrics();
const { data: endpoints } = useEndpointMetrics();
const [timeRange, setTimeRange] = useState('1 Hour');
const endpointColumns = useMemo<Array<Column<EndpointMetric>>>(
() => [
@ -210,17 +185,6 @@ export const PerformancePage = () => {
<Subtitle>Monitor API performance and response times</Subtitle>
</div>
<TimeRangeContainer>
{['1 Hour', '6 Hours', '24 Hours'].map((range) => (
<TimeRangeButton
key={range}
$isActive={timeRange === range}
onClick={() => setTimeRange(range)}
>
{range}
</TimeRangeButton>
))}
</TimeRangeContainer>
</PageHeader>
{/* Performance Alerts */}

View file

@ -45,30 +45,6 @@ const Subtitle = styled.p`
margin: ${(props) => props.theme.spacing.xs} 0 0 0;
`;
const DateFilterContainer = styled.div`
display: flex;
gap: ${(props) => props.theme.spacing.sm};
padding: ${(props) => props.theme.spacing.xs};
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.md};
`;
const FilterButton = styled.button<{ $isActive: boolean }>`
padding: ${(props) => props.theme.spacing.sm} ${(props) => props.theme.spacing.md};
border: none;
border-radius: ${(props) => props.theme.borderRadius.sm};
font-size: ${(props) => props.theme.typography.fontSize.sm};
font-weight: ${(props) => props.theme.typography.fontWeight.medium};
cursor: pointer;
transition: all ${(props) => props.theme.transitions.fast};
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : 'transparent')};
color: ${(props) => (props.$isActive ? '#fff' : props.theme.colors.text.secondary)};
&:hover {
background: ${(props) => (props.$isActive ? props.theme.colors.primary.main : props.theme.colors.hover.surface)};
}
`;
const SectionCard = styled(Card)`
padding: ${(props) => props.theme.spacing.lg};
@ -187,7 +163,6 @@ const formatCurrency = (value: number): string => {
// ============================================================================
export const RevenuePage = () => {
const [dateRange, setDateRange] = useState('30d');
const [showExportMenu, setShowExportMenu] = useState(false);
const { data: metrics, isLoading: metricsLoading } = useRevenueMetrics();
@ -276,18 +251,6 @@ export const RevenuePage = () => {
<Subtitle>Track revenue performance and trends</Subtitle>
</div>
<DateFilterContainer data-testid="date-filter">
{['7d', '30d', '90d'].map((range) => (
<FilterButton
key={range}
$isActive={dateRange === range}
onClick={() => setDateRange(range)}
data-testid={`filter-${range}`}
>
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : '90 Days'}
</FilterButton>
))}
</DateFilterContainer>
</PageHeader>
{/* KPI Cards */}