Files
campaign-command-center/src/lib/data-context.tsx
Chris Olson 3c8c8ee594 Build AgentMinder Campaign Command Center - Phase 1 complete
Full-featured sales campaign dashboard replacing Looker Studio with
responsive Next.js app. Includes Executive Overview, Account Explorer
with priority algorithm, Activities with detail overlay, Pipeline/Opps
with deal panels, Implementation tracking, and Data Admin with CSV
import and CRUD operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-31 13:55:01 -04:00

174 lines
5.5 KiB
TypeScript

'use client';
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';
import { DashboardData, DashboardConfig, PipelineRecord, AccountRecord, ActivityRecord, TargetRecord } from '@/types/data';
import { startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfQuarter, endOfQuarter, subDays, parseISO, isWithinInterval } from 'date-fns';
export type DatePreset = 'this_week' | 'last_7' | 'this_month' | 'this_quarter' | 'last_quarter' | 'custom';
interface FilterState {
districts: string[];
datePreset: DatePreset;
dateRange: { start: string; end: string };
crossFilter: { key: string; value: string } | null;
}
interface DataContextValue {
raw: DashboardData;
config: DashboardConfig;
filters: FilterState;
setDistricts: (d: string[]) => void;
setDatePreset: (p: DatePreset) => void;
setDateRange: (start: string, end: string) => void;
setCrossFilter: (key: string, value: string) => void;
clearCrossFilter: () => void;
clearAllFilters: () => void;
filtered: {
pipeline: PipelineRecord[];
accounts: AccountRecord[];
activities: ActivityRecord[];
targets: TargetRecord[];
};
isLoading: boolean;
lastRefreshed: string;
refresh: () => Promise<void>;
}
const DataContext = createContext<DataContextValue | null>(null);
function getDateRange(preset: DatePreset, custom?: { start: string; end: string }): { start: Date; end: Date } {
const now = new Date();
switch (preset) {
case 'this_week':
return { start: startOfWeek(now, { weekStartsOn: 1 }), end: endOfWeek(now, { weekStartsOn: 1 }) };
case 'last_7':
return { start: subDays(now, 7), end: now };
case 'this_month':
return { start: startOfMonth(now), end: endOfMonth(now) };
case 'this_quarter':
return { start: startOfQuarter(now), end: endOfQuarter(now) };
case 'last_quarter': {
const lastQ = new Date(now);
lastQ.setMonth(lastQ.getMonth() - 3);
return { start: startOfQuarter(lastQ), end: endOfQuarter(lastQ) };
}
case 'custom':
if (custom) return { start: parseISO(custom.start), end: parseISO(custom.end) };
return { start: subDays(now, 30), end: now };
}
}
export function DataProvider({ children, initialData, config }: { children: ReactNode; initialData: DashboardData; config: DashboardConfig }) {
const [data, setData] = useState<DashboardData>(initialData);
const [isLoading, setIsLoading] = useState(false);
const [filters, setFilters] = useState<FilterState>({
districts: [],
datePreset: 'this_quarter',
dateRange: { start: '', end: '' },
crossFilter: null,
});
const setDistricts = useCallback((d: string[]) => {
setFilters(f => ({ ...f, districts: d }));
}, []);
const setDatePreset = useCallback((p: DatePreset) => {
setFilters(f => ({ ...f, datePreset: p }));
}, []);
const setDateRange = useCallback((start: string, end: string) => {
setFilters(f => ({ ...f, datePreset: 'custom', dateRange: { start, end } }));
}, []);
const setCrossFilter = useCallback((key: string, value: string) => {
setFilters(f => ({ ...f, crossFilter: { key, value } }));
}, []);
const clearCrossFilter = useCallback(() => {
setFilters(f => ({ ...f, crossFilter: null }));
}, []);
const clearAllFilters = useCallback(() => {
setFilters({ districts: [], datePreset: 'this_quarter', dateRange: { start: '', end: '' }, crossFilter: null });
}, []);
const refresh = useCallback(async () => {
setIsLoading(true);
try {
const res = await fetch('/api/sheets?bust=' + Date.now());
if (res.ok) {
const newData = await res.json();
setData(newData);
}
} finally {
setIsLoading(false);
}
}, []);
const filtered = useMemo(() => {
const { districts: distFilter, datePreset, dateRange, crossFilter } = filters;
const range = getDateRange(datePreset, dateRange);
let pipeline = data.pipeline;
let accounts = data.accounts;
let activities = data.activities;
let targets = data.targets;
if (distFilter.length > 0) {
pipeline = pipeline.filter(p => distFilter.includes(p.District_Name));
accounts = accounts.filter(a => distFilter.includes(a.District_Name));
activities = activities.filter(a => distFilter.includes(a.District_Name));
}
activities = activities.filter(a => {
try {
return isWithinInterval(parseISO(a.Activity_Date), range);
} catch {
return true;
}
});
if (crossFilter) {
const { key, value } = crossFilter;
const match = (obj: object) => String((obj as never)[key as never]) === value;
pipeline = pipeline.filter(match);
accounts = accounts.filter(match);
activities = activities.filter(match);
targets = targets.filter(match);
}
if (distFilter.length > 0) {
const accountNames = new Set(accounts.map(a => a.Account_Name));
targets = targets.filter(t => accountNames.has(t.Account_Name));
}
return { pipeline, accounts, activities, targets };
}, [data, filters]);
return (
<DataContext.Provider value={{
raw: data,
config,
filters,
setDistricts,
setDatePreset,
setDateRange,
setCrossFilter,
clearCrossFilter,
clearAllFilters,
filtered,
isLoading,
lastRefreshed: data.lastRefreshed,
refresh,
}}>
{children}
</DataContext.Provider>
);
}
export function useData() {
const ctx = useContext(DataContext);
if (!ctx) throw new Error('useData must be used within DataProvider');
return ctx;
}