Cliente HTTP robusto para consumir a API do Restaurantix com React Query para cache inteligente, sincronização automática e otimizações.
React Query (TanStack Query) é a biblioteca mais poderosa para gerenciar estado do servidor, cache e sincronização de dados.
npm install @tanstack/react-query @tanstack/react-query-devtools npm install axios # ou pode usar fetch nativo
import { QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ defaultOptions: { queries: { // Tempo que os dados ficam "fresh" (não refetch) staleTime: 1000 * 60 * 5, // 5 minutos // Tempo que os dados ficam no cache gcTime: 1000 * 60 * 30, // 30 minutos (era cacheTime) // Retry automático em caso de erro retry: (failureCount, error: any) => { // Não retry em erros 4xx (client errors) if (error?.status >= 400 && error?.status < 500) { return false } // Máximo 3 tentativas para outros erros return failureCount < 3 }, // Refetch quando a janela ganha foco refetchOnWindowFocus: false, // Refetch quando reconecta à internet refetchOnReconnect: true, }, mutations: { // Retry para mutations retry: 1, }, }, })
'use client' import { QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { queryClient } from '@/lib/query-client' interface QueryProviderProps { children: React.ReactNode } export function QueryProvider({ children }: QueryProviderProps) { return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} position="bottom-right" /> </QueryClientProvider> ) }
// ✅ CORRIGIDO: Usando fetch com cookies em vez de axios com tokens import { env } from './env' export interface ApiError { message: string code?: string status?: number details?: any } class ApiClient { private baseURL: string constructor() { this.baseURL = env.NEXT_PUBLIC_API_URL } private async request<T>( endpoint: string, options: RequestInit = {} ): Promise<T> { const url = `${this.baseURL}${endpoint}` // ✅ CORREÇÃO PRINCIPAL: credentials: 'include' para cookies const config: RequestInit = { ...options, credentials: 'include', // Envia cookies automaticamente headers: { 'Content-Type': 'application/json', ...options.headers, }, } // Log da requisição em desenvolvimento if (process.env.NODE_ENV === 'development') { console.log('🚀 API Request:', { method: config.method || 'GET', url, data: config.body, }) } try { const response = await fetch(url, config) // Log da resposta em desenvolvimento if (process.env.NODE_ENV === 'development') { console.log('✅ API Response:', { status: response.status, url, }) } // Se não há conteúdo, retorna undefined if (response.status === 204) { return undefined as T } const data = await response.json() if (!response.ok) { const apiError: ApiError = { message: data?.message || 'Erro do servidor', code: data?.code, status: response.status, details: data?.details, } // Não autenticado - redirecionar para login if (response.status === 401) { if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) { window.location.href = '/login' } } console.error('❌ API Error:', apiError) throw apiError } return data } catch (error) { if (error instanceof Error && error.name === 'TypeError') { // Erro de rede const apiError: ApiError = { message: 'Erro de conexão. Verifique sua internet.', status: 0, } console.error('❌ Network Error:', apiError) throw apiError } throw error } } // ✅ Métodos HTTP simplificados (sem gerenciamento de token) async get<T>(endpoint: string): Promise<T> { return this.request<T>(endpoint, { method: 'GET' }) } async post<T>(endpoint: string, data?: any): Promise<T> { return this.request<T>(endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }) } async put<T>(endpoint: string, data?: any): Promise<T> { return this.request<T>(endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }) } async patch<T>(endpoint: string, data?: any): Promise<T> { return this.request<T>(endpoint, { method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }) } async delete<T>(endpoint: string): Promise<T> { return this.request<T>(endpoint, { method: 'DELETE' }) } } export const apiClient = new ApiClient()
import { apiClient } from '@/lib/api-client' export interface User { id: string name: string email: string phone: string | null role: 'manager' | 'customer' createdAt: string updatedAt: string } export interface Restaurant { id: string name: string description: string | null managerId: string createdAt: string updatedAt: string } export interface CsrfTokenResponse { token: string } export const authService = { // ✅ CORRETO: GET para obter token CSRF async getCsrfToken(): Promise<CsrfTokenResponse> { return apiClient.get('/csrf-token') }, // ✅ CORRETO: POST para enviar magic link async sendMagicLink(email: string): Promise<{ message: string }> { return apiClient.post('/auth-links/authenticate', { email }) }, // ✅ CORRIGIDO: GET (não PATCH) para autenticar async authenticateFromLink(code: string, redirect: string): Promise<void> { const params = new URLSearchParams({ code, redirect }) return apiClient.get(`/auth-links/authenticate?${params}`) }, // ✅ NOVO: GET para obter perfil do usuário async getProfile(): Promise<User> { return apiClient.get('/me') }, // ✅ CORRIGIDO: GET (não POST) para logout async signOut(): Promise<{ message: string }> { return apiClient.get('/sign-out') }, }
import { apiClient } from '@/lib/api-client' export interface CreateRestaurantRequest { restaurantName: string managerName: string email: string phone: string } export interface CreateRestaurantResponse { restaurantId: string } export const restaurantService = { // ✅ NOVO: POST para criar restaurante async createRestaurant(data: CreateRestaurantRequest): Promise<CreateRestaurantResponse> { return apiClient.post('/restaurants', data) }, // ✅ NOVO: GET para obter restaurante gerenciado async getManagedRestaurant(): Promise<Restaurant> { return apiClient.get('/managed-restaurant') }, }
import { apiClient } from '@/lib/api-client' import type { Order, OrderStatus } from '@/types/orders' export interface GetOrdersParams { orderId?: string customerName?: string status?: OrderStatus page?: number perPage?: number } export interface GetOrdersResponse { orders: Order[] meta: { pageIndex: number perPage: number totalCount: number } } export interface OrderDetailsResponse { order: Order & { orderItems: Array<{ id: string priceInCents: number quantity: number product: { name: string } }> customer: { name: string email: string phone: string | null } } } export const ordersService = { async getOrders(params: GetOrdersParams = {}): Promise<GetOrdersResponse> { const searchParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, String(value)) } }) return apiClient.get(`/orders?${searchParams}`) }, async getOrderDetails(orderId: string): Promise<OrderDetailsResponse> { return apiClient.get(`/orders/${orderId}`) }, async approveOrder(orderId: string): Promise<void> { return apiClient.patch(`/orders/${orderId}/approve`) }, async dispatchOrder(orderId: string): Promise<void> { return apiClient.patch(`/orders/${orderId}/dispatch`) }, async deliverOrder(orderId: string): Promise<void> { return apiClient.patch(`/orders/${orderId}/deliver`) }, async cancelOrder(orderId: string): Promise<void> { return apiClient.patch(`/orders/${orderId}/cancel`) }, }
import { apiClient } from '@/lib/api-client' export interface MonthRevenueResponse { receipt: number diffFromLastMonth: number } export interface DayOrdersAmountResponse { amount: number diffFromYesterday: number } export interface MonthOrdersAmountResponse { amount: number diffFromLastMonth: number } export interface PopularProductsResponse { products: Array<{ product: string amount: number }> } export interface DailyReceiptInPeriodResponse { data: Array<{ date: string receipt: number }> } export const metricsService = { // ✅ CORRETO: Receita mensal async getMonthRevenue(): Promise<MonthRevenueResponse> { return apiClient.get('/metrics/month-revenue') }, // ✅ NOVO: Pedidos do dia (estava faltando) async getDayOrdersAmount(): Promise<DayOrdersAmountResponse> { return apiClient.get('/metrics/day-orders-amount') }, // ✅ CORRETO: Pedidos do mês async getMonthOrdersAmount(): Promise<MonthOrdersAmountResponse> { return apiClient.get('/metrics/month-orders-amount') }, // ✅ CORRETO: Produtos populares async getPopularProducts(): Promise<PopularProductsResponse> { return apiClient.get('/metrics/popular-products') }, // ✅ NOVO: Receitas diárias em período (estava faltando) async getDailyReceiptInPeriod(params: { from?: string to?: string } = {}): Promise<DailyReceiptInPeriodResponse> { const searchParams = new URLSearchParams() if (params.from) searchParams.append('from', params.from) if (params.to) searchParams.append('to', params.to) return apiClient.get(`/metrics/daily-receipt-in-period?${searchParams}`) }, }
// ✅ CORRIGIDO: Hooks atualizados para usar cookies e endpoints corretos import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { authService } from '@/services/auth-service' import { restaurantService } from '@/services/restaurant-service' import { toast } from 'sonner' // ✅ NOVO: Hook para obter token CSRF export function useCsrfToken() { return useQuery({ queryKey: ['auth', 'csrf-token'], queryFn: authService.getCsrfToken, staleTime: 1000 * 60 * 30, // 30 minutos }) } // ✅ CORRIGIDO: Hook para obter perfil do usuário export function useProfile() { return useQuery({ queryKey: ['auth', 'profile'], queryFn: authService.getProfile, retry: false, // Não retry em 401 staleTime: 1000 * 60 * 10, // 10 minutos }) } // ✅ NOVO: Hook para obter restaurante gerenciado export function useManagedRestaurant() { const { data: user } = useProfile() return useQuery({ queryKey: ['restaurant', 'managed'], queryFn: restaurantService.getManagedRestaurant, enabled: !!user && user.role === 'manager', retry: false, staleTime: 1000 * 60 * 15, // 15 minutos }) } // ✅ CORRIGIDO: Hook para enviar magic link export function useSendMagicLink() { return useMutation({ mutationFn: authService.sendMagicLink, onSuccess: () => { toast.success('Link de autenticação enviado para seu email!') }, onError: (error: any) => { toast.error(error.message || 'Erro ao enviar link') }, }) } // ✅ CORRIGIDO: Hook para criar restaurante export function useCreateRestaurant() { const queryClient = useQueryClient() return useMutation({ mutationFn: restaurantService.createRestaurant, onSuccess: () => { // Invalidar cache do restaurante gerenciado queryClient.invalidateQueries({ queryKey: ['restaurant', 'managed'] }) toast.success('Restaurante criado com sucesso!') }, onError: (error: any) => { toast.error(error.message || 'Erro ao criar restaurante') }, }) } // ✅ CORRIGIDO: Hook para logout export function useSignOut() { const queryClient = useQueryClient() return useMutation({ mutationFn: authService.signOut, onSuccess: () => { // Limpar todo o cache queryClient.clear() toast.success('Logout realizado com sucesso!') // Redirecionar para login window.location.href = '/login' }, onError: (error: any) => { toast.error(error.message || 'Erro ao fazer logout') }, }) } // ✅ NOVO: Hook combinado para autenticação export function useAuth() { const { data: user, isLoading: isLoadingUser } = useProfile() const { data: restaurant, isLoading: isLoadingRestaurant } = useManagedRestaurant() const { data: csrfToken } = useCsrfToken() const sendMagicLinkMutation = useSendMagicLink() const signOutMutation = useSignOut() return { user, restaurant, csrfToken, isLoading: isLoadingUser || isLoadingRestaurant, isAuthenticated: !!user, sendMagicLink: sendMagicLinkMutation.mutate, signOut: signOutMutation.mutate, isLoadingSendMagicLink: sendMagicLinkMutation.isPending, isLoadingSignOut: signOutMutation.isPending, } }
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { ordersService, type GetOrdersParams } from '@/services/orders-service' import { useToast } from '@/hooks/use-toast' export function useOrders(params: GetOrdersParams = {}) { return useQuery({ queryKey: ['orders', params], queryFn: () => ordersService.getOrders(params), staleTime: 1000 * 60 * 2, // 2 minutos }) } export function useOrderDetails(orderId: string) { return useQuery({ queryKey: ['order', orderId], queryFn: () => ordersService.getOrderDetails(orderId), enabled: !!orderId, staleTime: 1000 * 60 * 5, // 5 minutos }) } export function useApproveOrder() { const queryClient = useQueryClient() const { toast } = useToast() return useMutation({ mutationFn: ordersService.approveOrder, onSuccess: () => { // Invalidar cache de pedidos queryClient.invalidateQueries({ queryKey: ['orders'] }) toast.success('Pedido aprovado!', 'O pedido foi aprovado com sucesso.') }, onError: (error: any) => { toast.error('Erro ao aprovar pedido', error.message) }, }) } export function useDispatchOrder() { const queryClient = useQueryClient() const { toast } = useToast() return useMutation({ mutationFn: ordersService.dispatchOrder, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['orders'] }) toast.success('Pedido despachado!', 'O pedido está a caminho.') }, onError: (error: any) => { toast.error('Erro ao despachar pedido', error.message) }, }) } export function useDeliverOrder() { const queryClient = useQueryClient() const { toast } = useToast() return useMutation({ mutationFn: ordersService.deliverOrder, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['orders'] }) toast.success('Pedido entregue!', 'O pedido foi entregue com sucesso.') }, onError: (error: any) => { toast.error('Erro ao entregar pedido', error.message) }, }) } export function useCancelOrder() { const queryClient = useQueryClient() const { toast } = useToast() return useMutation({ mutationFn: ordersService.cancelOrder, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['orders'] }) toast.success('Pedido cancelado!', 'O pedido foi cancelado.') }, onError: (error: any) => { toast.error('Erro ao cancelar pedido', error.message) }, }) }
import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { QueryProvider } from '@/providers/query-provider' import { ToastContainer } from '@/components/atoms/Toast/Toast' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Restaurantix - Dashboard', description: 'Sistema de gestão para restaurantes', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="pt-BR"> <body className={inter.className}> <QueryProvider> <div className="min-h-screen bg-background"> {children} </div> <ToastContainer /> </QueryProvider> </body> </html> ) }
'use client' import { useState } from 'react' import { useOrders, useApproveOrder } from '@/hooks/use-orders-queries' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' export function OrdersList() { const [page, setPage] = useState(1) const { data, isLoading, error } = useOrders({ page, perPage: 10 }) const approveOrder = useApproveOrder() if (isLoading) { return <div>Carregando pedidos...</div> } if (error) { return <div>Erro ao carregar pedidos: {error.message}</div> } return ( <div className="space-y-4"> <h2 className="text-2xl font-bold">Pedidos</h2> {data?.orders.map((order) => ( <Card key={order.id}> <CardHeader> <div className="flex items-center justify-between"> <CardTitle>Pedido #{order.id.slice(-6)}</CardTitle> <Badge variant={order.status === 'pending' ? 'secondary' : 'default'}> {order.status} </Badge> </div> </CardHeader> <CardContent> <div className="flex items-center justify-between"> <div> <p>Cliente: {order.customerName}</p> <p>Total: R$ {(order.totalInCents / 100).toFixed(2)}</p> </div> {order.status === 'pending' && ( <Button onClick={() => approveOrder.mutate(order.id)} disabled={approveOrder.isPending} > {approveOrder.isPending ? 'Aprovando...' : 'Aprovar'} </Button> )} </div> </CardContent> </Card> ))} </div> ) }
Implemente o React Query com API Client:
Configure o React Query no projeto:
npm install @tanstack/react-query @tanstack/react-query-devtools axios
Crie os services para consumir a API:
Implemente hooks para cada operação: