Optimize products list analytics loading
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 54s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 54s
This commit is contained in:
@@ -7,7 +7,7 @@ import { rangeForLastDays } from '../dateRanges';
|
||||
|
||||
const Layout = () => {
|
||||
const location = useLocation();
|
||||
const needsRawData = location.pathname.startsWith('/products');
|
||||
const needsRawData = location.pathname.startsWith('/products/');
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||
return localStorage.getItem('graph_sidebar_collapsed') === 'true';
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, CreateUserResult, DashboardAnalytics, DateRange, ManagedUser, OrderData, RfmAnalytics, StockData } from './types';
|
||||
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, CreateUserResult, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, RfmAnalytics, StockData } from './types';
|
||||
import { formatDateParam } from './dateRanges';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
@@ -124,6 +124,21 @@ export const fetchDashboardAnalytics = async (dateRange: DateRange): Promise<Das
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProductAnalytics = async (dateRange: DateRange): Promise<ProductAnalyticsItem[]> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: formatDateParam(dateRange.start),
|
||||
end: formatDateParam(dateRange.end)
|
||||
});
|
||||
const response = await authFetch(`/analytics/products?${params.toString()}`);
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch product analytics failed', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRfmAnalytics = async (dateRange: DateRange): Promise<RfmAnalytics | null> => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
|
||||
@@ -1,30 +1,63 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { Search, Package, TrendingUp, ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||
import { Search, Package, TrendingUp, ChevronLeft, ChevronRight, Download, Loader2 } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import type { OrderData, DateRange, StockData } from '../types';
|
||||
import { exportToCSV } from '../dataService';
|
||||
import { buildProductsSummary } from '../analytics/products';
|
||||
import type { DateRange, ProductAnalyticsItem } from '../types';
|
||||
import { exportToCSV, fetchProductAnalytics } from '../dataService';
|
||||
import type { ProductSummary } from '../analytics/products';
|
||||
|
||||
const Products = () => {
|
||||
const { dateRange, setDateRange, ordersData, stockData } = useOutletContext<{
|
||||
const { dateRange, setDateRange } = useOutletContext<{
|
||||
dateRange: DateRange,
|
||||
setDateRange: (range: DateRange) => void,
|
||||
ordersData: OrderData[],
|
||||
stockData: StockData[],
|
||||
refreshInterval: number,
|
||||
setRefreshInterval: (interval: number) => void,
|
||||
loadData: (showLoading?: boolean) => void
|
||||
setDateRange: (range: DateRange) => void
|
||||
}>();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [productAnalytics, setProductAnalytics] = useState<ProductAnalyticsItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadProducts = async () => {
|
||||
setIsLoading(true);
|
||||
const products = await fetchProductAnalytics(dateRange);
|
||||
|
||||
if (isMounted) {
|
||||
setProductAnalytics(products);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadProducts();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [dateRange]);
|
||||
|
||||
const productsData = useMemo(() => {
|
||||
return buildProductsSummary(ordersData, stockData, dateRange, searchTerm);
|
||||
}, [dateRange, searchTerm, ordersData, stockData]);
|
||||
const normalizedSearch = searchTerm.trim().toLowerCase();
|
||||
const products: ProductSummary[] = productAnalytics.map(product => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
totalSold: product.quantitySold,
|
||||
revenue: product.revenue,
|
||||
lastPrice: product.lastPrice,
|
||||
stock: product.stock
|
||||
}));
|
||||
const filteredProducts = normalizedSearch
|
||||
? products.filter(product =>
|
||||
product.name.toLowerCase().includes(normalizedSearch) ||
|
||||
product.id.toLowerCase().includes(normalizedSearch)
|
||||
)
|
||||
: products;
|
||||
|
||||
return filteredProducts.sort((a, b) => b.totalSold - a.totalSold);
|
||||
}, [productAnalytics, searchTerm]);
|
||||
|
||||
// Pagination logic
|
||||
const totalPages = Math.ceil(productsData.length / itemsPerPage);
|
||||
@@ -86,6 +119,13 @@ const Products = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="inline-flex items-center gap-2 rounded-xl border border-dark-border bg-dark-card px-3 py-2 text-sm font-semibold text-dark-muted">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-brand-primary" />
|
||||
Atualizando produtos
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -69,6 +69,18 @@ export interface DashboardAnalytics {
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ProductAnalyticsItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantitySold: number;
|
||||
revenue: number;
|
||||
orderLineCount: number;
|
||||
lastPrice: number;
|
||||
stock: number;
|
||||
firstSaleDate: string | null;
|
||||
lastSaleDate: string | null;
|
||||
}
|
||||
|
||||
export interface ClientAnalyticsItem {
|
||||
customerKey: string;
|
||||
clientToken: string;
|
||||
|
||||
Reference in New Issue
Block a user