From a4c8b04fed48b87890943dcfc46b2c4ab0530009 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Tue, 14 Apr 2026 00:00:21 -0300 Subject: [PATCH] feat: ReservationFlow orquestrando form + checkout + sucesso --- src/App.tsx | 71 ++-------- src/__tests__/App.test.tsx | 53 +++++--- src/__tests__/setup.ts | 37 +----- .../reservation/ReservationFlow.tsx | 121 ++++++++++++++++++ 4 files changed, 169 insertions(+), 113 deletions(-) create mode 100644 src/components/reservation/ReservationFlow.tsx diff --git a/src/App.tsx b/src/App.tsx index 5be6992..30dad37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,77 +1,24 @@ -import { useEffect, useState } from 'react' -import { supabase } from '@/lib/supabase' -import type { Database } from '@/types/database' - -type Marca = Database['reserva_hotel']['Tables']['marcas']['Row'] +import { ReservationFlow } from '@/components/reservation/ReservationFlow' export default function App() { - const [marcas, setMarcas] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - async function loadMarcas() { - const { data, error } = await supabase - .from('marcas') - .select('*') - .eq('ativa', true) - .order('nome', { ascending: true }) - - if (error) { - setError(error.message) - } else { - setMarcas(data ?? []) - } - setLoading(false) - } - loadMarcas() - }, []) - return ( -
-
+
+

Experiência exclusiva

-

+

Reserva Rede 1001

-

Escolha uma das nossas marcas para começar

+

+ Escolha, confirme e receba seu PIX na hora. +

- {loading &&

Carregando marcas...

} - - {error && ( -
- Erro ao carregar: {error} -
- )} - - {!loading && !error && marcas.length === 0 && ( -

Nenhuma marca cadastrada ainda.

- )} - - {!loading && !error && marcas.length > 0 && ( -
    - {marcas.map((marca) => ( -
  • -

    {marca.nome}

    - {marca.descricao &&

    {marca.descricao}

    } - {marca.categorias && marca.categorias.length > 0 && ( -

    - {marca.categorias.join(' · ')} -

    - )} -
  • - ))} -
- )} +
- © 2026 Reserva Rede 1001 · Fase 1 · Fundação + © 2026 Reserva Rede 1001 · Experiência Exclusiva
) diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 8cbe6c7..2653424 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,25 +1,48 @@ -import { render, screen, waitFor } from '@testing-library/react' -import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' + +vi.mock('@/hooks/useReservationForm', () => ({ + useReservationForm: () => ({ + form: { + marcaId: '', + unidadeId: '', + permanencia: '', + categoria: '', + checkinAt: '', + nome: '', + telefone: '', + cpf: '', + email: '', + observacao: '', + }, + update: vi.fn(), + marcas: [], + unidades: [], + marcaSelecionada: null, + unidadeSelecionada: null, + preco: null, + precoTotalCents: 0, + depositCents: 0, + fotos: [], + loading: false, + setLoading: vi.fn(), + error: null, + setError: vi.fn(), + canSubmit: false, + reset: vi.fn(), + }), +})) + import App from '@/App' describe('App', () => { - it('renderiza título premium', () => { + it('renderiza o titulo premium', () => { render() expect(screen.getByRole('heading', { name: /reserva rede 1001/i })).toBeInTheDocument() }) - it('exibe marcas retornadas do supabase após carregar', async () => { + it('renderiza o botao de confirmar', () => { render() - await waitFor(() => { - expect(screen.getByText('Hotel 1001 Noites')).toBeInTheDocument() - expect(screen.getByText('Dolce Amore')).toBeInTheDocument() - }) - }) - - it('não mostra estado de loading após fetch completar', async () => { - render() - await waitFor(() => { - expect(screen.queryByText(/carregando marcas/i)).not.toBeInTheDocument() - }) + expect(screen.getByRole('button', { name: /confirmar e pagar reserva/i })).toBeInTheDocument() }) }) diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 6ef4a0e..e87ca3f 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -1,42 +1,7 @@ import '@testing-library/jest-dom/vitest' import { cleanup } from '@testing-library/react' -import { afterEach, vi } from 'vitest' +import { afterEach } from 'vitest' afterEach(() => { cleanup() }) - -// Mock do Supabase client — imita from('marcas').select('*').eq('ativa', true).order('nome') -vi.mock('@/lib/supabase', () => { - const mockMarcas = [ - { - id: '3fac5ed4-100f-4c0a-82ce-06110758b9c9', - nome: 'Hotel 1001 Noites', - categorias: ['Standard', 'Superior', 'Luxo'], - permanencias: ['3hrs', '6hrs', 'Pernoite'], - descricao: null, - ativa: true, - created_at: '2026-04-13T00:00:00Z', - updated_at: '2026-04-13T00:00:00Z', - }, - { - id: '11111111-1111-1111-1111-111111111111', - nome: 'Dolce Amore', - categorias: ['Suite Master', 'Apartamento'], - permanencias: ['3hrs', '4hrs'], - descricao: null, - ativa: true, - created_at: '2026-04-13T00:00:00Z', - updated_at: '2026-04-13T00:00:00Z', - }, - ] - - const orderFn = vi.fn(() => Promise.resolve({ data: mockMarcas, error: null })) - const eqFn = vi.fn(() => ({ order: orderFn })) - const selectFn = vi.fn(() => ({ eq: eqFn, order: orderFn })) - const fromFn = vi.fn(() => ({ select: selectFn })) - - return { - supabase: { from: fromFn }, - } -}) diff --git a/src/components/reservation/ReservationFlow.tsx b/src/components/reservation/ReservationFlow.tsx new file mode 100644 index 0000000..1d743cd --- /dev/null +++ b/src/components/reservation/ReservationFlow.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import { useReservationForm } from '@/hooks/useReservationForm' +import { chatwootApi, type CreateReservationResponse } from '@/lib/chatwootApi' +import { onlyDigits } from '@/lib/formatters' +import { StayDetailsStep } from './StayDetailsStep' +import { ImageGallery } from './ImageGallery' +import { PriceSummary } from './PriceSummary' +import { CustomerForm } from './CustomerForm' +import { PixCheckout } from '@/components/checkout/PixCheckout' +import { SuccessScreen } from '@/components/checkout/SuccessScreen' +import { Button } from '@/components/ui/button' + +type Phase = 'form' | 'checkout' | 'success' + +export function ReservationFlow() { + const { + form, + update, + marcas, + unidades, + unidadeSelecionada, + precoTotalCents, + depositCents, + fotos, + canSubmit, + reset, + } = useReservationForm() + + const [phase, setPhase] = useState('form') + const [submitting, setSubmitting] = useState(false) + const [submitError, setSubmitError] = useState(null) + const [reservation, setReservation] = useState(null) + + async function handleSubmit() { + setSubmitting(true) + setSubmitError(null) + try { + if (unidadeSelecionada?.chatwoot_unit_id == null) { + throw new Error('Unidade não está vinculada ao Chatwoot') + } + + const res = await chatwootApi.createReservation({ + chatwoot_unit_id: Number(unidadeSelecionada.chatwoot_unit_id), + category: form.categoria, + stay_type: form.permanencia, + checkin_at: new Date(form.checkinAt).toISOString(), + customer: { + name: form.nome, + phone: onlyDigits(form.telefone), + cpf: onlyDigits(form.cpf), + email: form.email || undefined, + }, + total_cents: precoTotalCents, + deposit_cents: depositCents, + notes: form.observacao || undefined, + }) + + setReservation(res) + setPhase('checkout') + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Erro desconhecido') + } finally { + setSubmitting(false) + } + } + + function restart() { + reset() + setReservation(null) + setSubmitError(null) + setPhase('form') + } + + if (phase === 'success') { + return + } + + if (phase === 'checkout' && reservation) { + return ( + setPhase('success')} + onCancel={restart} + /> + ) + } + + return ( +
+ + + + + + + + + {submitError && ( +
+ {submitError} +
+ )} + + +
+ ) +}