Smooth analytics page reloads
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34s

This commit is contained in:
Cauê Faleiros
2026-06-23 14:45:21 -03:00
parent 335d231903
commit 7d60073757
7 changed files with 233 additions and 126 deletions

View File

@@ -155,6 +155,23 @@ const initDB = async () => {
await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`);
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome
ON orders ((NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')));
`).catch(err => {
console.error('Notice: Could not create normalized client name index:', err.message);
});
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome_phone_date
ON orders (
(NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')),
data_pedido_date DESC,
id DESC
)
WHERE NULLIF(cliente_fone, '') IS NOT NULL;
`).catch(err => {
console.error('Notice: Could not create normalized client phone lookup index:', err.message);
});
await pool.query(` await pool.query(`
CREATE INDEX IF NOT EXISTS idx_orders_customer_key_date CREATE INDEX IF NOT EXISTS idx_orders_customer_key_date
ON orders ( ON orders (

View File

@@ -17,31 +17,28 @@ const PRODUCT_NAME_SQL = `
const NORMALIZED_CUSTOMER_NAME_SQL = "NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')"; const NORMALIZED_CUSTOMER_NAME_SQL = "NULLIF(LOWER(TRIM(regexp_replace(COALESCE(cliente_nome, ''), '\\s+', ' ', 'g'))), '')";
// Phone-less historical rows must follow the later known phone for the same client name. // Phone-less historical rows must follow the later known phone for the same client name.
const CUSTOMER_IDENTITY_CTE = ` const CUSTOMER_IDENTITY_CTE = `
WITH order_identity AS ( WITH customer_phone_by_name AS (
SELECT SELECT
orders.*, ${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name
FROM orders
),
customer_phone_by_name AS (
SELECT
normalized_customer_name,
(ARRAY_AGG(NULLIF(cliente_fone, '') ORDER BY data_pedido_date DESC NULLS LAST, id DESC) (ARRAY_AGG(NULLIF(cliente_fone, '') ORDER BY data_pedido_date DESC NULLS LAST, id DESC)
FILTER (WHERE NULLIF(cliente_fone, '') IS NOT NULL))[1] as canonical_phone )[1] as canonical_phone
FROM order_identity FROM orders
WHERE normalized_customer_name IS NOT NULL WHERE NULLIF(cliente_fone, '') IS NOT NULL
AND ${NORMALIZED_CUSTOMER_NAME_SQL} IS NOT NULL
GROUP BY normalized_customer_name GROUP BY normalized_customer_name
), ),
identity_orders AS ( identity_orders AS (
SELECT SELECT
order_identity.*, orders.*,
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
COALESCE( COALESCE(
NULLIF(order_identity.cliente_fone, ''), NULLIF(orders.cliente_fone, ''),
customer_phone_by_name.canonical_phone, customer_phone_by_name.canonical_phone,
'name:' || COALESCE(NULLIF(order_identity.cliente_nome, ''), 'Cliente Desconhecido') 'name:' || COALESCE(NULLIF(orders.cliente_nome, ''), 'Cliente Desconhecido')
) as customer_key ) as customer_key
FROM order_identity FROM orders
LEFT JOIN customer_phone_by_name USING (normalized_customer_name) LEFT JOIN customer_phone_by_name
ON customer_phone_by_name.normalized_customer_name = ${NORMALIZED_CUSTOMER_NAME_SQL}
) )
`; `;
const CUSTOMER_KEY_SQL = 'customer_key'; const CUSTOMER_KEY_SQL = 'customer_key';

View File

@@ -601,10 +601,10 @@ test('getClientAnalytics groups phone-less history through the customer identity
assert.equal(clients.length, 1); assert.equal(clients.length, 1);
assert.equal(clients[0].customerKey, '(16) 99999-9999'); assert.equal(clients[0].customerKey, '(16) 99999-9999');
assert.equal(clients[0].phone, '(16) 99999-9999'); assert.equal(clients[0].phone, '(16) 99999-9999');
assert.match(calls[0].sql, /WITH order_identity AS/);
assert.match(calls[0].sql, /customer_phone_by_name AS/); assert.match(calls[0].sql, /customer_phone_by_name AS/);
assert.match(calls[0].sql, /ARRAY_AGG\(NULLIF\(cliente_fone, ''\) ORDER BY data_pedido_date DESC NULLS LAST, id DESC\)/); assert.match(calls[0].sql, /ARRAY_AGG\(NULLIF\(cliente_fone, ''\) ORDER BY data_pedido_date DESC NULLS LAST, id DESC\)/);
assert.match(calls[0].sql, /NULLIF\(order_identity\.cliente_fone, ''\),\s+customer_phone_by_name\.canonical_phone/s); assert.match(calls[0].sql, /WHERE NULLIF\(cliente_fone, ''\) IS NOT NULL/);
assert.match(calls[0].sql, /NULLIF\(orders\.cliente_fone, ''\),\s+customer_phone_by_name\.canonical_phone/s);
assert.match(calls[0].sql, /FROM identity_orders/); assert.match(calls[0].sql, /FROM identity_orders/);
assert.match(calls[0].sql, /GROUP BY customer_key/); assert.match(calls[0].sql, /GROUP BY customer_key/);
} finally { } finally {

View File

@@ -2,6 +2,57 @@ import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSu
import { formatDateParam } from './dateRanges'; import { formatDateParam } from './dateRanges';
const API_URL = import.meta.env.VITE_API_URL || '/api'; const API_URL = import.meta.env.VITE_API_URL || '/api';
const ANALYTICS_CACHE_TTL_MS = 5 * 60 * 1000;
const IN_FLIGHT_CACHE_TTL_MS = 15 * 1000;
type CacheOptions = {
force?: boolean;
};
type ApiCacheEntry<T> = {
expiresAt: number;
data?: T;
promise?: Promise<T>;
};
const analyticsCache = new Map<string, ApiCacheEntry<unknown>>();
const getCachedAnalyticsValue = <T>(key: string): T | undefined => {
const entry = analyticsCache.get(key) as ApiCacheEntry<T> | undefined;
if (!entry || entry.data === undefined || entry.expiresAt <= Date.now()) return undefined;
return entry.data;
};
const getCachedAnalytics = async <T>(key: string, loader: () => Promise<T>, options: CacheOptions = {}): Promise<T> => {
const now = Date.now();
const cached = analyticsCache.get(key) as ApiCacheEntry<T> | undefined;
if (!options.force) {
if (cached?.data !== undefined && cached.expiresAt > now) return cached.data;
if (cached?.promise && cached.expiresAt > now) return cached.promise;
}
const promise = loader();
analyticsCache.set(key, { expiresAt: now + IN_FLIGHT_CACHE_TTL_MS, promise });
try {
const data = await promise;
if (data === null) {
analyticsCache.delete(key);
} else {
analyticsCache.set(key, { data, expiresAt: Date.now() + ANALYTICS_CACHE_TTL_MS });
}
return data;
} catch (error) {
analyticsCache.delete(key);
throw error;
}
};
const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({
start: formatDateParam(dateRange.start),
end: formatDateParam(dateRange.end)
});
export const login = async (email: string, password: string): Promise<boolean> => { export const login = async (email: string, password: string): Promise<boolean> => {
try { try {
@@ -109,49 +160,46 @@ const authFetch = async (path: string, options: RequestInit = {}): Promise<Respo
return response; return response;
}; };
export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<DashboardAnalytics | null> => { export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise<DashboardAnalytics | null> => {
try { const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`;
const params = new URLSearchParams({ return getCachedAnalytics(path, async () => {
start: formatDateParam(dateRange.start), try {
end: formatDateParam(dateRange.end) const response = await authFetch(path);
}); if (!response.ok) return null;
const response = await authFetch(`/analytics/dashboard?${params.toString()}`); return await response.json();
if (!response.ok) return null; } catch (error) {
return await response.json(); console.error('Fetch dashboard analytics failed', error);
} catch (error) { return null;
console.error('Fetch dashboard analytics failed', error); }
return null; }, options);
}
}; };
export const fetchProductAnalytics = async (dateRange: DateRange): Promise<ProductAnalyticsItem[]> => { export const fetchProductAnalytics = async (dateRange: DateRange): Promise<ProductAnalyticsItem[]> => {
try { const path = `/analytics/products?${buildDateRangeParams(dateRange).toString()}`;
const params = new URLSearchParams({ return getCachedAnalytics(path, async () => {
start: formatDateParam(dateRange.start), try {
end: formatDateParam(dateRange.end) const response = await authFetch(path);
}); if (!response.ok) return [];
const response = await authFetch(`/analytics/products?${params.toString()}`); return await response.json();
if (!response.ok) return []; } catch (error) {
return await response.json(); console.error('Fetch product analytics failed', error);
} catch (error) { return [];
console.error('Fetch product analytics failed', error); }
return []; });
}
}; };
export const fetchProductDetailsAnalytics = async (productId: string, dateRange: DateRange): Promise<ProductDetailsAnalytics | null> => { export const fetchProductDetailsAnalytics = async (productId: string, dateRange: DateRange): Promise<ProductDetailsAnalytics | null> => {
try { const path = `/analytics/products/${encodeURIComponent(productId)}/details?${buildDateRangeParams(dateRange).toString()}`;
const params = new URLSearchParams({ return getCachedAnalytics(path, async () => {
start: formatDateParam(dateRange.start), try {
end: formatDateParam(dateRange.end) const response = await authFetch(path);
}); if (!response.ok) return null;
const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/details?${params.toString()}`); return await response.json();
if (!response.ok) return null; } catch (error) {
return await response.json(); console.error('Fetch product details analytics failed', error);
} catch (error) { return null;
console.error('Fetch product details analytics failed', error); }
return null; });
}
}; };
const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Partial<ClientMetadataFilters>) => { const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Partial<ClientMetadataFilters>) => {
@@ -162,65 +210,87 @@ const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Par
}; };
export const fetchClientFilterOptions = async (dateRange: DateRange): Promise<ClientFilterOptions> => { export const fetchClientFilterOptions = async (dateRange: DateRange): Promise<ClientFilterOptions> => {
try { const path = `/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`;
const params = new URLSearchParams({ return getCachedAnalytics(path, async () => {
start: formatDateParam(dateRange.start), try {
end: formatDateParam(dateRange.end) const response = await authFetch(path);
}); if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] };
const response = await authFetch(`/analytics/clients/filters?${params.toString()}`); return await response.json();
if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] }; } catch (error) {
return await response.json(); console.error('Fetch client filter options failed', error);
} catch (error) { return { marketplaces: [], salesChannels: [], sellers: [] };
console.error('Fetch client filter options failed', error); }
return { marketplaces: [], salesChannels: [], sellers: [] }; });
}
}; };
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<RfmAnalytics | null> => { const buildRfmAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
try { const params = buildDateRangeParams(dateRange);
const params = new URLSearchParams({ appendClientMetadataFilterParams(params, filters);
start: formatDateParam(dateRange.start), return `/analytics/rfm?${params.toString()}`;
end: formatDateParam(dateRange.end)
});
appendClientMetadataFilterParams(params, filters);
const response = await authFetch(`/analytics/rfm?${params.toString()}`);
if (!response.ok) return null;
return await response.json();
} catch (error) {
console.error('Fetch RFM analytics failed', error);
return null;
}
}; };
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<ClientAnalyticsItem[]> => { export const getCachedRfmAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): RfmAnalytics | undefined => {
try { return getCachedAnalyticsValue<RfmAnalytics>(buildRfmAnalyticsPath(dateRange, filters));
const params = new URLSearchParams({ };
start: formatDateParam(dateRange.start),
end: formatDateParam(dateRange.end) export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<RfmAnalytics | null> => {
}); const path = buildRfmAnalyticsPath(dateRange, filters);
appendClientMetadataFilterParams(params, filters); return getCachedAnalytics(path, async () => {
const response = await authFetch(`/analytics/clients?${params.toString()}`); try {
if (!response.ok) return []; const response = await authFetch(path);
return await response.json(); if (!response.ok) return null;
} catch (error) { return await response.json();
console.error('Fetch client analytics failed', error); } catch (error) {
return []; console.error('Fetch RFM analytics failed', error);
} return null;
}
}, options);
};
const buildClientAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
const params = buildDateRangeParams(dateRange);
appendClientMetadataFilterParams(params, filters);
return `/analytics/clients?${params.toString()}`;
};
export const getCachedClientAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): ClientAnalyticsItem[] | undefined => {
return getCachedAnalyticsValue<ClientAnalyticsItem[]>(buildClientAnalyticsPath(dateRange, filters));
};
export const getCachedClientFilterOptions = (dateRange: DateRange): ClientFilterOptions | undefined => {
return getCachedAnalyticsValue<ClientFilterOptions>(`/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`);
};
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<ClientAnalyticsItem[]> => {
const path = buildClientAnalyticsPath(dateRange, filters);
return getCachedAnalytics(path, async () => {
try {
const response = await authFetch(path);
if (!response.ok) return [];
return await response.json();
} catch (error) {
console.error('Fetch client analytics failed', error);
return [];
}
}, options);
}; };
export const fetchClientDetailsAnalytics = async (clientToken: string, dateRange: DateRange): Promise<ClientDetailsAnalytics | null> => { export const fetchClientDetailsAnalytics = async (clientToken: string, dateRange: DateRange): Promise<ClientDetailsAnalytics | null> => {
try { const path = `/analytics/clients/${encodeURIComponent(clientToken)}/details?${buildDateRangeParams(dateRange).toString()}`;
const params = new URLSearchParams({ return getCachedAnalytics(path, async () => {
start: formatDateParam(dateRange.start), try {
end: formatDateParam(dateRange.end) const response = await authFetch(path);
}); if (!response.ok) return null;
const response = await authFetch(`/analytics/clients/${encodeURIComponent(clientToken)}/details?${params.toString()}`); return await response.json();
if (!response.ok) return null; } catch (error) {
return await response.json(); console.error('Fetch client details analytics failed', error);
} catch (error) { return null;
console.error('Fetch client details analytics failed', error); }
return null; });
} };
export const clearAnalyticsCache = () => {
analyticsCache.clear();
}; };
export const fetchCampaigns = async (): Promise<CampaignQueueSummary | null> => { export const fetchCampaigns = async (): Promise<CampaignQueueSummary | null> => {

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom'; import { Link, useOutletContext } from 'react-router-dom';
import { Search, ChevronRight, Filter, ChevronLeft, X } from 'lucide-react'; import { Search, ChevronRight, Filter, ChevronLeft, X } from 'lucide-react';
import type { ClientAnalyticsItem, ClientFilterOptions, ClientMetadataFilters, DateRange, RfmAnalytics, RfmClient } from '../types'; import type { ClientAnalyticsItem, ClientFilterOptions, ClientMetadataFilters, DateRange, RfmAnalytics, RfmClient } from '../types';
import { fetchClientAnalytics, fetchClientFilterOptions, fetchRfmAnalytics } from '../dataService'; import { fetchClientAnalytics, fetchClientFilterOptions, fetchRfmAnalytics, getCachedClientAnalytics, getCachedClientFilterOptions, getCachedRfmAnalytics } from '../dataService';
import { endOfLocalDay, formatDateParam, parseLocalDateInput, rangeForDay, rangeForLastDays, rangeForPreviousDay, startOfLocalDay } from '../dateRanges'; import { endOfLocalDay, formatDateParam, parseLocalDateInput, rangeForDay, rangeForLastDays, rangeForPreviousDay, startOfLocalDay } from '../dateRanges';
import type { ClientSortOption, ClientSummary } from '../analytics/clients'; import type { ClientSortOption, ClientSummary } from '../analytics/clients';
@@ -114,11 +114,14 @@ const Clients = () => {
const [sortBy, setSortBy] = useState<ClientSortOption>('recent'); const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
const [clientTypeFilter, setClientTypeFilter] = useState('all'); const [clientTypeFilter, setClientTypeFilter] = useState('all');
const [metadataFilters, setMetadataFilters] = useState<ClientMetadataFilters>(emptyClientFilters); const [metadataFilters, setMetadataFilters] = useState<ClientMetadataFilters>(emptyClientFilters);
const [filterOptions, setFilterOptions] = useState<ClientFilterOptions>(emptyClientFilterOptions); const initialFilterOptions = getCachedClientFilterOptions(dateRange);
const initialClientAnalytics = getCachedClientAnalytics(dateRange, emptyClientFilters);
const initialRfmAnalytics = getCachedRfmAnalytics(dateRange, emptyClientFilters);
const [filterOptions, setFilterOptions] = useState<ClientFilterOptions>(initialFilterOptions || emptyClientFilterOptions);
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false); const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
const filterMenuRef = useRef<HTMLDivElement>(null); const filterMenuRef = useRef<HTMLDivElement>(null);
const [rfmAnalytics, setRfmAnalytics] = useState<RfmAnalytics | null>(null); const [rfmAnalytics, setRfmAnalytics] = useState<RfmAnalytics | null>(initialRfmAnalytics || null);
const [clientAnalytics, setClientAnalytics] = useState<ClientAnalyticsItem[]>([]); const [clientAnalytics, setClientAnalytics] = useState<ClientAnalyticsItem[]>(initialClientAnalytics || []);
// Pagination state // Pagination state
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@@ -128,15 +131,27 @@ const Clients = () => {
let isMounted = true; let isMounted = true;
const loadClients = async () => { const loadClients = async () => {
const options = await fetchClientFilterOptions(dateRange); const cachedOptions = getCachedClientFilterOptions(dateRange);
const clientsData = await fetchClientAnalytics(dateRange, metadataFilters); const cachedClients = getCachedClientAnalytics(dateRange, metadataFilters);
const cachedRfm = getCachedRfmAnalytics(dateRange, metadataFilters);
if (isMounted) {
if (cachedOptions) setFilterOptions(cachedOptions);
if (cachedClients) setClientAnalytics(cachedClients);
setRfmAnalytics(cachedRfm || null);
}
const filterOptionsPromise = fetchClientFilterOptions(dateRange);
const clientsPromise = fetchClientAnalytics(dateRange, metadataFilters);
const rfmPromise = fetchRfmAnalytics(dateRange, metadataFilters);
const [options, clientsData] = await Promise.all([filterOptionsPromise, clientsPromise]);
if (isMounted) { if (isMounted) {
setFilterOptions(options); setFilterOptions(options);
setClientAnalytics(clientsData); setClientAnalytics(clientsData);
setRfmAnalytics(null);
} }
const rfmData = await fetchRfmAnalytics(dateRange, metadataFilters); const rfmData = await rfmPromise;
if (isMounted) { if (isMounted) {
setRfmAnalytics(rfmData); setRfmAnalytics(rfmData);
} }

View File

@@ -55,9 +55,9 @@ const Dashboard = () => {
const [serverMetrics, setServerMetrics] = useState<DashboardAnalytics | null>(null); const [serverMetrics, setServerMetrics] = useState<DashboardAnalytics | null>(null);
const [isMetricsLoading, setIsMetricsLoading] = useState(true); const [isMetricsLoading, setIsMetricsLoading] = useState(true);
const loadDashboardMetrics = useCallback(async (range: DateRange) => { const loadDashboardMetrics = useCallback(async (range: DateRange, options?: { force?: boolean }) => {
setIsMetricsLoading(true); setIsMetricsLoading(true);
const metrics = await fetchDashboardAnalytics(range); const metrics = await fetchDashboardAnalytics(range, options);
setServerMetrics(metrics); setServerMetrics(metrics);
setIsMetricsLoading(false); setIsMetricsLoading(false);
}, []); }, []);
@@ -72,7 +72,7 @@ const Dashboard = () => {
if (refreshInterval === 0) return; if (refreshInterval === 0) return;
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
void loadDashboardMetrics(dateRange); void loadDashboardMetrics(dateRange, { force: true });
}, refreshInterval); }, refreshInterval);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
@@ -84,7 +84,7 @@ const Dashboard = () => {
}, [dateRange, ordersData, serverMetrics]); }, [dateRange, ordersData, serverMetrics]);
const handleManualRefresh = () => { const handleManualRefresh = () => {
void loadDashboardMetrics(dateRange); void loadDashboardMetrics(dateRange, { force: true });
}; };
return ( return (

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom'; import { Link, useOutletContext } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react'; import { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker'; import DateRangePicker from '../components/DateRangePicker';
import { exportToCSV, fetchRfmAnalytics } from '../dataService'; import { exportToCSV, fetchRfmAnalytics, getCachedRfmAnalytics } from '../dataService';
import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types'; import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types';
const emptyClients: RfmClient[] = []; const emptyClients: RfmClient[] = [];
@@ -127,17 +127,25 @@ const Rfm = () => {
setRefreshInterval: (interval: number) => void; setRefreshInterval: (interval: number) => void;
}>(); }>();
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(null); const initialCachedAnalytics = getCachedRfmAnalytics(dateRange);
const [isLoading, setIsLoading] = useState(true); const [analytics, setAnalytics] = useState<RfmAnalytics | null>(initialCachedAnalytics || null);
const [isLoading, setIsLoading] = useState(!initialCachedAnalytics);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [segmentFilter, setSegmentFilter] = useState('all'); const [segmentFilter, setSegmentFilter] = useState('all');
const [selectedSegmentKey, setSelectedSegmentKey] = useState(''); const [selectedSegmentKey, setSelectedSegmentKey] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(20); const [itemsPerPage, setItemsPerPage] = useState(20);
const loadRfm = useCallback(async (range: DateRange) => { const loadRfm = useCallback(async (range: DateRange, options?: { force?: boolean }) => {
const cachedAnalytics = options?.force ? undefined : getCachedRfmAnalytics(range);
if (cachedAnalytics) {
setAnalytics(cachedAnalytics);
setIsLoading(false);
return;
}
setIsLoading(true); setIsLoading(true);
const data = await fetchRfmAnalytics(range); const data = await fetchRfmAnalytics(range, undefined, options);
setAnalytics(data); setAnalytics(data);
setIsLoading(false); setIsLoading(false);
}, []); }, []);
@@ -152,7 +160,7 @@ const Rfm = () => {
if (refreshInterval === 0) return; if (refreshInterval === 0) return;
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
void loadRfm(dateRange); void loadRfm(dateRange, { force: true });
}, refreshInterval); }, refreshInterval);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
@@ -276,7 +284,7 @@ const Rfm = () => {
onChange={setDateRange} onChange={setDateRange}
refreshInterval={refreshInterval} refreshInterval={refreshInterval}
setRefreshInterval={setRefreshInterval} setRefreshInterval={setRefreshInterval}
onManualRefresh={() => void loadRfm(dateRange)} onManualRefresh={() => void loadRfm(dateRange, { force: true })}
/> />
</div> </div>
</div> </div>