diff --git a/src/admin/analytics/__tests__/useAnalytics.test.js b/src/admin/analytics/__tests__/useAnalytics.test.js index fe5bf06..177f9f9 100644 --- a/src/admin/analytics/__tests__/useAnalytics.test.js +++ b/src/admin/analytics/__tests__/useAnalytics.test.js @@ -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); + }); }); diff --git a/src/admin/analytics/useAnalytics.js b/src/admin/analytics/useAnalytics.js index 0205643..8f7282d 100644 --- a/src/admin/analytics/useAnalytics.js +++ b/src/admin/analytics/useAnalytics.js @@ -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 }; }