Smooth analytics page reloads
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34s
This commit is contained in:
@@ -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_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(`
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_customer_key_date
|
||||
ON orders (
|
||||
|
||||
@@ -17,31 +17,28 @@ const PRODUCT_NAME_SQL = `
|
||||
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.
|
||||
const CUSTOMER_IDENTITY_CTE = `
|
||||
WITH order_identity AS (
|
||||
WITH customer_phone_by_name AS (
|
||||
SELECT
|
||||
orders.*,
|
||||
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name
|
||||
FROM orders
|
||||
),
|
||||
customer_phone_by_name AS (
|
||||
SELECT
|
||||
normalized_customer_name,
|
||||
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
|
||||
(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
|
||||
FROM order_identity
|
||||
WHERE normalized_customer_name IS NOT NULL
|
||||
)[1] as canonical_phone
|
||||
FROM orders
|
||||
WHERE NULLIF(cliente_fone, '') IS NOT NULL
|
||||
AND ${NORMALIZED_CUSTOMER_NAME_SQL} IS NOT NULL
|
||||
GROUP BY normalized_customer_name
|
||||
),
|
||||
identity_orders AS (
|
||||
SELECT
|
||||
order_identity.*,
|
||||
orders.*,
|
||||
${NORMALIZED_CUSTOMER_NAME_SQL} as normalized_customer_name,
|
||||
COALESCE(
|
||||
NULLIF(order_identity.cliente_fone, ''),
|
||||
NULLIF(orders.cliente_fone, ''),
|
||||
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
|
||||
FROM order_identity
|
||||
LEFT JOIN customer_phone_by_name USING (normalized_customer_name)
|
||||
FROM orders
|
||||
LEFT JOIN customer_phone_by_name
|
||||
ON customer_phone_by_name.normalized_customer_name = ${NORMALIZED_CUSTOMER_NAME_SQL}
|
||||
)
|
||||
`;
|
||||
const CUSTOMER_KEY_SQL = 'customer_key';
|
||||
|
||||
@@ -601,10 +601,10 @@ test('getClientAnalytics groups phone-less history through the customer identity
|
||||
assert.equal(clients.length, 1);
|
||||
assert.equal(clients[0].customerKey, '(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, /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, /GROUP BY customer_key/);
|
||||
} finally {
|
||||
|
||||
@@ -2,6 +2,57 @@ import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSu
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
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> => {
|
||||
try {
|
||||
@@ -109,49 +160,46 @@ const authFetch = async (path: string, options: RequestInit = {}): Promise<Respo
|
||||
return response;
|
||||
};
|
||||
|
||||
export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<DashboardAnalytics | null> => {
|
||||
export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise<DashboardAnalytics | null> => {
|
||||
const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/dashboard?${params.toString()}`);
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch dashboard analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const fetchProductAnalytics = async (dateRange: DateRange): Promise<ProductAnalyticsItem[]> => {
|
||||
const path = `/analytics/products?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/products?${params.toString()}`);
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchProductDetailsAnalytics = async (productId: string, dateRange: DateRange): Promise<ProductDetailsAnalytics | null> => {
|
||||
const path = `/analytics/products/${encodeURIComponent(productId)}/details?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/details?${params.toString()}`);
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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> => {
|
||||
const path = `/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/clients/filters?${params.toString()}`);
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client filter options failed', error);
|
||||
return { marketplaces: [], salesChannels: [], sellers: [] };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<RfmAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const buildRfmAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
|
||||
const params = buildDateRangeParams(dateRange);
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
const response = await authFetch(`/analytics/rfm?${params.toString()}`);
|
||||
return `/analytics/rfm?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const getCachedRfmAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): RfmAnalytics | undefined => {
|
||||
return getCachedAnalyticsValue<RfmAnalytics>(buildRfmAnalyticsPath(dateRange, filters));
|
||||
};
|
||||
|
||||
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<RfmAnalytics | null> => {
|
||||
const path = buildRfmAnalyticsPath(dateRange, filters);
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch RFM analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
}, options);
|
||||
};
|
||||
|
||||
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): Promise<ClientAnalyticsItem[]> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const buildClientAnalyticsPath = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>) => {
|
||||
const params = buildDateRangeParams(dateRange);
|
||||
appendClientMetadataFilterParams(params, filters);
|
||||
const response = await authFetch(`/analytics/clients?${params.toString()}`);
|
||||
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> => {
|
||||
const path = `/analytics/clients/${encodeURIComponent(clientToken)}/details?${buildDateRangeParams(dateRange).toString()}`;
|
||||
return getCachedAnalytics(path, async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/clients/${encodeURIComponent(clientToken)}/details?${params.toString()}`);
|
||||
const response = await authFetch(path);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch client details analytics failed', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const clearAnalyticsCache = () => {
|
||||
analyticsCache.clear();
|
||||
};
|
||||
|
||||
export const fetchCampaigns = async (): Promise<CampaignQueueSummary | null> => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { Search, ChevronRight, Filter, ChevronLeft, X } from 'lucide-react';
|
||||
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 type { ClientSortOption, ClientSummary } from '../analytics/clients';
|
||||
|
||||
@@ -114,11 +114,14 @@ const Clients = () => {
|
||||
const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
|
||||
const [clientTypeFilter, setClientTypeFilter] = useState('all');
|
||||
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 filterMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [rfmAnalytics, setRfmAnalytics] = useState<RfmAnalytics | null>(null);
|
||||
const [clientAnalytics, setClientAnalytics] = useState<ClientAnalyticsItem[]>([]);
|
||||
const [rfmAnalytics, setRfmAnalytics] = useState<RfmAnalytics | null>(initialRfmAnalytics || null);
|
||||
const [clientAnalytics, setClientAnalytics] = useState<ClientAnalyticsItem[]>(initialClientAnalytics || []);
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -128,15 +131,27 @@ const Clients = () => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadClients = async () => {
|
||||
const options = await fetchClientFilterOptions(dateRange);
|
||||
const clientsData = await fetchClientAnalytics(dateRange, metadataFilters);
|
||||
const cachedOptions = getCachedClientFilterOptions(dateRange);
|
||||
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) {
|
||||
setFilterOptions(options);
|
||||
setClientAnalytics(clientsData);
|
||||
setRfmAnalytics(null);
|
||||
}
|
||||
|
||||
const rfmData = await fetchRfmAnalytics(dateRange, metadataFilters);
|
||||
const rfmData = await rfmPromise;
|
||||
if (isMounted) {
|
||||
setRfmAnalytics(rfmData);
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ const Dashboard = () => {
|
||||
const [serverMetrics, setServerMetrics] = useState<DashboardAnalytics | null>(null);
|
||||
const [isMetricsLoading, setIsMetricsLoading] = useState(true);
|
||||
|
||||
const loadDashboardMetrics = useCallback(async (range: DateRange) => {
|
||||
const loadDashboardMetrics = useCallback(async (range: DateRange, options?: { force?: boolean }) => {
|
||||
setIsMetricsLoading(true);
|
||||
const metrics = await fetchDashboardAnalytics(range);
|
||||
const metrics = await fetchDashboardAnalytics(range, options);
|
||||
setServerMetrics(metrics);
|
||||
setIsMetricsLoading(false);
|
||||
}, []);
|
||||
@@ -72,7 +72,7 @@ const Dashboard = () => {
|
||||
if (refreshInterval === 0) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
void loadDashboardMetrics(dateRange);
|
||||
void loadDashboardMetrics(dateRange, { force: true });
|
||||
}, refreshInterval);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
@@ -84,7 +84,7 @@ const Dashboard = () => {
|
||||
}, [dateRange, ordersData, serverMetrics]);
|
||||
|
||||
const handleManualRefresh = () => {
|
||||
void loadDashboardMetrics(dateRange);
|
||||
void loadDashboardMetrics(dateRange, { force: true });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { ChevronLeft, ChevronRight, Download, Filter, Loader2, Search, Users } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import { exportToCSV, fetchRfmAnalytics } from '../dataService';
|
||||
import { exportToCSV, fetchRfmAnalytics, getCachedRfmAnalytics } from '../dataService';
|
||||
import type { DateRange, RfmAnalytics, RfmClient, RfmSegment } from '../types';
|
||||
|
||||
const emptyClients: RfmClient[] = [];
|
||||
@@ -127,17 +127,25 @@ const Rfm = () => {
|
||||
setRefreshInterval: (interval: number) => void;
|
||||
}>();
|
||||
|
||||
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const initialCachedAnalytics = getCachedRfmAnalytics(dateRange);
|
||||
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(initialCachedAnalytics || null);
|
||||
const [isLoading, setIsLoading] = useState(!initialCachedAnalytics);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [segmentFilter, setSegmentFilter] = useState('all');
|
||||
const [selectedSegmentKey, setSelectedSegmentKey] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
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);
|
||||
const data = await fetchRfmAnalytics(range);
|
||||
const data = await fetchRfmAnalytics(range, undefined, options);
|
||||
setAnalytics(data);
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
@@ -152,7 +160,7 @@ const Rfm = () => {
|
||||
if (refreshInterval === 0) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
void loadRfm(dateRange);
|
||||
void loadRfm(dateRange, { force: true });
|
||||
}, refreshInterval);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
@@ -276,7 +284,7 @@ const Rfm = () => {
|
||||
onChange={setDateRange}
|
||||
refreshInterval={refreshInterval}
|
||||
setRefreshInterval={setRefreshInterval}
|
||||
onManualRefresh={() => void loadRfm(dateRange)}
|
||||
onManualRefresh={() => void loadRfm(dateRange, { force: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user