feat: add useOrders hook with CRUD, status updates, and Realtime
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// src/admin/orders/__tests__/useOrders.test.js
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
const mockChannel = vi.hoisted(() => ({
|
||||
on: vi.fn().mockReturnThis(),
|
||||
subscribe: vi.fn().mockReturnThis(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/supabase', () => ({
|
||||
supabase: {
|
||||
from: vi.fn(),
|
||||
channel: vi.fn(() => mockChannel),
|
||||
removeChannel: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { useOrders } from '../useOrders';
|
||||
import { supabase } from '../../../lib/supabase';
|
||||
|
||||
function makeFromMock(resolvedValue) {
|
||||
return {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
order: vi.fn().mockResolvedValue(resolvedValue),
|
||||
update: vi.fn().mockReturnThis(),
|
||||
insert: vi.fn().mockResolvedValue({ error: null }),
|
||||
eq: vi.fn().mockResolvedValue({ data: null, error: null }),
|
||||
single: vi.fn().mockResolvedValue({ data: null, error: null }),
|
||||
};
|
||||
}
|
||||
|
||||
describe('useOrders', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockChannel.on.mockReturnThis();
|
||||
mockChannel.subscribe.mockReturnThis();
|
||||
});
|
||||
|
||||
it('starts with loading=true and empty orders', () => {
|
||||
supabase.from.mockReturnValue({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
order: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
});
|
||||
const { result } = renderHook(() => useOrders());
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.orders).toEqual([]);
|
||||
});
|
||||
|
||||
it('populates orders and sets loading=false after fetch', async () => {
|
||||
const mockOrders = [
|
||||
{ id: '1', name: 'Иванов', status: 'новая', created_at: '2026-01-01' },
|
||||
{ id: '2', name: 'Петров', status: 'замер', created_at: '2026-01-02' },
|
||||
];
|
||||
supabase.from.mockReturnValue(makeFromMock({ data: mockOrders, error: null }));
|
||||
const { result } = renderHook(() => useOrders());
|
||||
await act(async () => {});
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.orders).toEqual(mockOrders);
|
||||
});
|
||||
|
||||
it('groups orders by status in ordersByStatus', async () => {
|
||||
const mockOrders = [
|
||||
{ id: '1', name: 'А', status: 'новая', created_at: '2026-01-01' },
|
||||
{ id: '2', name: 'Б', status: 'замер', created_at: '2026-01-02' },
|
||||
{ id: '3', name: 'В', status: 'новая', created_at: '2026-01-03' },
|
||||
];
|
||||
supabase.from.mockReturnValue(makeFromMock({ data: mockOrders, error: null }));
|
||||
const { result } = renderHook(() => useOrders());
|
||||
await act(async () => {});
|
||||
expect(result.current.ordersByStatus['новая']).toHaveLength(2);
|
||||
expect(result.current.ordersByStatus['замер']).toHaveLength(1);
|
||||
expect(result.current.ordersByStatus['монтаж']).toHaveLength(0);
|
||||
expect(result.current.ordersByStatus['закрыт']).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('sets error when fetch fails', async () => {
|
||||
supabase.from.mockReturnValue(makeFromMock({ data: null, error: { message: 'DB error' } }));
|
||||
const { result } = renderHook(() => useOrders());
|
||||
await act(async () => {});
|
||||
expect(result.current.error).toBe('DB error');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт'];
|
||||
|
||||
export function useOrders() {
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchOrders = useCallback(async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('orders')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setOrders(data || []);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
|
||||
const channel = supabase
|
||||
.channel('orders-realtime')
|
||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'orders' }, fetchOrders)
|
||||
.subscribe();
|
||||
|
||||
return () => supabase.removeChannel(channel);
|
||||
}, [fetchOrders]);
|
||||
|
||||
const ordersByStatus = STATUSES.reduce((acc, status) => {
|
||||
acc[status] = orders.filter((o) => o.status === status);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const updateStatus = async (orderId, newStatus, userEmail) => {
|
||||
const order = orders.find((o) => o.id === orderId);
|
||||
const updates = { status: newStatus };
|
||||
if (newStatus === 'закрыт') updates.closed_at = new Date().toISOString();
|
||||
|
||||
const { error } = await supabase.from('orders').update(updates).eq('id', orderId);
|
||||
|
||||
if (!error) {
|
||||
await supabase.from('order_events').insert({
|
||||
order_id: orderId,
|
||||
created_by: userEmail,
|
||||
event_type: 'status_change',
|
||||
description: `Статус изменён: ${order?.status} → ${newStatus}`,
|
||||
});
|
||||
}
|
||||
return { error };
|
||||
};
|
||||
|
||||
const createOrder = async (orderData, userEmail) => {
|
||||
const { data, error } = await supabase
|
||||
.from('orders')
|
||||
.insert({ ...orderData, source: 'manual', status: 'новая' })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (!error && data) {
|
||||
await supabase.from('order_events').insert({
|
||||
order_id: data.id,
|
||||
created_by: userEmail,
|
||||
event_type: 'status_change',
|
||||
description: 'Заявка создана вручную',
|
||||
});
|
||||
}
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
const updateOrder = async (orderId, updates, userEmail) => {
|
||||
const { error } = await supabase.from('orders').update(updates).eq('id', orderId);
|
||||
if (!error) {
|
||||
await supabase.from('order_events').insert({
|
||||
order_id: orderId,
|
||||
created_by: userEmail,
|
||||
event_type: 'edit',
|
||||
description: 'Данные заказа обновлены',
|
||||
});
|
||||
}
|
||||
return { error };
|
||||
};
|
||||
|
||||
return { orders, ordersByStatus, loading, error, updateStatus, createOrder, updateOrder, refetch: fetchOrders };
|
||||
}
|
||||
Reference in New Issue
Block a user