fix: useAnalytics sort revenue by month, add error handling and test coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:32:01 +03:00
parent c34c99d86a
commit a6038abc1f
2 changed files with 64 additions and 7 deletions
@@ -84,4 +84,46 @@ describe('useAnalytics', () => {
const industrial = result.current.byObjectType.find((x) => x.type === 'industrial');
expect(industrial?.count).toBe(2);
});
it('sets error when orders fetch fails', async () => {
supabase.from.mockImplementation((table) => {
if (table === 'orders') {
return {
select: vi.fn().mockReturnThis(),
gte: vi.fn().mockReturnThis(),
lte: vi.fn().mockResolvedValue({ data: null, error: { message: 'DB error' } }),
};
}
return {
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
gte: vi.fn().mockReturnThis(),
in: vi.fn().mockReturnThis(),
order: vi.fn().mockResolvedValue({ data: [], error: null }),
};
});
const { result } = renderHook(() => useAnalytics(FROM, TO));
await act(async () => {});
expect(result.current.error).toBe('DB error');
expect(result.current.loading).toBe(false);
});
it('returns funnelData with all 5 statuses', async () => {
const orders = [
{ id: '1', status: 'новая', object_type: 'industrial', created_at: '2026-03-01', final_cost: null, closed_at: null },
{ id: '2', status: 'замер', object_type: 'industrial', created_at: '2026-03-02', final_cost: null, closed_at: null },
];
supabase.from.mockImplementation((table) => {
if (table === 'orders') {
return { select: vi.fn().mockReturnThis(), gte: vi.fn().mockReturnThis(), lte: vi.fn().mockResolvedValue({ data: orders, error: null }) };
}
return { select: vi.fn().mockReturnThis(), eq: vi.fn().mockReturnThis(), gte: vi.fn().mockReturnThis(), in: vi.fn().mockReturnThis(), order: vi.fn().mockResolvedValue({ data: [], error: null }) };
});
const { result } = renderHook(() => useAnalytics(FROM, TO));
await act(async () => {});
expect(result.current.funnelData).toHaveLength(5);
expect(result.current.funnelData.find((d) => d.status === 'новая')?.count).toBe(1);
expect(result.current.funnelData.find((d) => d.status === 'замер')?.count).toBe(1);
expect(result.current.funnelData.find((d) => d.status === 'монтаж')?.count).toBe(0);
});
});
+22 -7
View File
@@ -11,16 +11,24 @@ export function useAnalytics(dateFrom, dateTo) {
revenueByMonth: [], byObjectType: [], avgTimeData: [],
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchAnalytics = useCallback(async () => {
setLoading(true);
setError(null);
const { data: orders } = await supabase
const { data: orders, error: ordersError } = await supabase
.from('orders')
.select('*')
.gte('created_at', dateFrom)
.lte('created_at', dateTo);
if (ordersError) {
setError(ordersError.message);
setLoading(false);
return;
}
const safeOrders = orders || [];
const orderIds = safeOrders.map((o) => o.id);
@@ -43,13 +51,20 @@ export function useAnalytics(dateFrom, dateTo) {
count: safeOrders.filter((o) => o.status === status).length,
}));
const revMap = {};
const revEntries = [];
closed.forEach((o) => {
const key = new Date(o.closed_at || o.created_at)
.toLocaleString('ru-RU', { month: 'short', year: '2-digit' });
revMap[key] = (revMap[key] || 0) + (Number(o.final_cost) || 0);
const date = new Date(o.closed_at || o.created_at);
const key = date.toLocaleString('ru-RU', { month: 'short', year: '2-digit' });
const monthStart = new Date(date.getFullYear(), date.getMonth(), 1).getTime();
const existing = revEntries.find((e) => e.month === key);
if (existing) {
existing.revenue += Number(o.final_cost) || 0;
} else {
revEntries.push({ month: key, revenue: Number(o.final_cost) || 0, _sort: monthStart });
}
});
const revenueByMonth = Object.entries(revMap).map(([month, revenue]) => ({ month, revenue }));
revEntries.sort((a, b) => a._sort - b._sort);
const revenueByMonth = revEntries.map(({ month, revenue }) => ({ month, revenue }));
const typeMap = {};
safeOrders.forEach((o) => {
@@ -91,5 +106,5 @@ export function useAnalytics(dateFrom, dateTo) {
useEffect(() => { fetchAnalytics(); }, [fetchAnalytics]);
return { ...analytics, loading };
return { ...analytics, loading, error };
}