Novo app publico de reserva (Vite + React + Supabase) separado do Chatwoot, que reusa toda a tubulacao de PIX (CobService, PixCharge, webhook Inter, ConfirmationService) via um endpoint novo no Chatwoot. Cobre: arquitetura, paleta premium, modelo de dados reformado (corrige bug de preco nos domingos), contrato da API nova, fluxo do cliente, plano de entrega em 6 fases e riscos. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
Reserva Rede 1001 — Design
Data: 2026-04-13 Autor: Rodrigo Borba Machado (com assistência do Claude) Status: Aprovado para planejamento
1. Contexto e objetivo
O Grupo Nova opera 4 marcas de hotéis/motéis em Brasília e Natal: Hotel 1001 Noites, 1001 Noites Prime, 1001 Noites Express e Dolce Amore. Hoje a reserva é feita pela atendente via WhatsApp, que gera um PIX usando a ferramenta Captain::Tools::GeneratePix dentro do Chatwoot fazer.ai. Essa ferramenta pode falhar (erro de API, mTLS, autenticação), e quando falha não há fallback automatizado.
Objetivo: criar uma página pública de reserva onde o próprio cliente:
- Escolhe marca → unidade → permanência → categoria de suíte → data/hora de check-in
- Vê fotos da categoria e o preço total
- Preenche dados pessoais (nome, telefone, CPF, e-mail)
- Paga 50% de entrada via PIX (QR code + copia-e-cola) direto na página
- Ao confirmar o pagamento, uma mensagem automática aparece na conversa do WhatsApp da atendente no Chatwoot
É fallback pro fluxo manual e também canal direto de auto-atendimento.
Escopo EXCLUÍDO:
- Disponibilidade em tempo real (é catálogo fixo — atendente resolve conflitos no check-in)
- Integração com PMS externo
- Login de cliente / histórico de reservas do cliente
- Reserva de suíte específica (sempre por categoria)
- Segunda metade do pagamento online (50% restantes ficam para o check-in)
2. Decisões-chave
| Decisão | Escolha | Motivo |
|---|---|---|
| App separado do Chatwoot | ✅ Sim | Stack diferente (React vs Rails/Vue 2), deploy diferente, Chatwoot é dashboard interno ruim para página de conversão |
| Stack | Vite + React 19 + TS | Mantém o POC existente, evita overkill do Next.js para app de checkout |
| UI | Tailwind v4 + shadcn/ui + Radix | Mantém base do POC, adiciona shadcn oficial |
| Animação | framer-motion (já tem) + anime.js (adicionar) | Pedido do usuário |
| Banco | Supabase Postgres + Auth + Storage | Usuário já usa Supabase |
| Deploy | Vercel | Grátis, deploy em 30s |
| Geração de PIX | Chatwoot API (novo endpoint) | Reusa Captain::Inter::CobService, PixCharge, webhook Inter e ConfirmationService — zero duplicação, auto-mensagem no WhatsApp de graça |
| Detecção de pagamento | Webhook Inter existente (/api/v1/captain/webhooks/inter_pix) |
Já funciona hoje; novo app só consulta status |
| Repositório | fazer-ai-kanban/reserva-1001/ (irmão do chatwoot/) |
Separado mas próximo |
3. Identidade visual — paleta premium
Inspiração: hotelaria de luxo boutique, tema árabe noturno de 1001 Noites, sensual sem ser brega.
| Token | Hex | Uso |
|---|---|---|
obsidian |
#0B0D12 |
Fundo principal |
midnight |
#0F1A2E |
Superfícies elevadas (cards, modais) |
champagne |
#C9A961 |
Ação primária, destaques de luxo, botão "Confirmar e Pagar" |
rose-gold |
#E8B4A0 |
Ação secundária, acentos sensuais, labels de carinho |
ivory |
#F5F1E8 |
Texto principal sobre fundos escuros |
slate |
#6B7280 |
Texto secundário, placeholders |
emerald |
#10B981 |
Estado de sucesso (pagamento confirmado) |
ruby |
#E11D48 |
Estado de erro, validações |
Tipografia: manter Inter (já no POC) para UI. Adicionar Fraunces (serif display) para hero e títulos — dá contraste editorial/luxo.
Gradientes: obsidian → midnight no fundo, champagne → rose-gold em CTAs.
4. Arquitetura de alto nível
┌────────────────────────┐ ┌────────────────────────┐
│ reserva-1001 │ │ Chatwoot (Rails) │
│ React + Vite │──API─▶│ │
│ Supabase │ │ Novo controller: │
│ Vercel │ │ PublicReservations │
│ │ │ │
│ • Página pública │ │ Reusa existente: │
│ • Admin │ │ • Captain::Inter:: │
│ • Checkout PIX │ │ CobService │
└─────────┬──────────────┘ │ • PixCharge │
│ │ • ConfirmationSvc │
▼ │ • Webhook Inter │
┌─────────────┐ │ • Auto-msg WhatsApp │
│ Supabase │ └────────────────────────┘
│ Postgres │ ▲
│ Auth │ │
│ Storage │ webhook
└─────────────┘ │
┌───────┴──────┐
│ Banco Inter │
└──────────────┘
Separação de responsabilidades:
| Sistema | É dono de |
|---|---|
| reserva-1001 | UX pública, UX admin, cadastro (marcas/unidades/categorias/preços/fotos/extras), formulário de reserva, tela de checkout, polling de status |
| Chatwoot | Contato, conversa, inbox WhatsApp, geração de PIX via Inter, PixCharge, detecção de pagamento, mensagem automática na conversa da atendente |
O novo app não duplica nada do Chatwoot — ele consome a API.
5. Modelo de dados (Supabase)
Reformulação sobre o schema do POC, corrigindo problemas e adicionando o que falta.
-- Marcas (4 fixas inicialmente)
brands (
id bigserial primary key,
name text not null,
slug text not null unique,
logo_url text,
primary_color text,
created_at timestamptz default now()
)
-- Unidades físicas de cada marca
hotel_units (
id bigserial primary key,
brand_id bigint references brands(id) on delete cascade,
name text not null,
slug text not null,
chatwoot_unit_id bigint not null, -- FK lógica pro Captain::Unit do Chatwoot
active boolean default true,
created_at timestamptz default now(),
unique (brand_id, slug)
)
-- Categorias de suíte (antes era JSON em hotel_units)
suite_categories (
id bigserial primary key,
unit_id bigint references hotel_units(id) on delete cascade,
name text not null,
description text,
sort_order int default 0,
active boolean default true,
created_at timestamptz default now()
)
-- Fotos por categoria (antes era JSON)
suite_images (
id bigserial primary key,
category_id bigint references suite_categories(id) on delete cascade,
storage_path text not null, -- path no Supabase Storage
alt text,
sort_order int default 0,
created_at timestamptz default now()
)
-- Preços por categoria + faixa de dia + tipo de permanência (em centavos)
pricing (
id bigserial primary key,
category_id bigint references suite_categories(id) on delete cascade,
day_range text not null, -- 'weekday' | 'weekend' (enum)
stay_type text not null, -- '2hrs' | '3hrs' | '4hrs' | 'pernoite' | 'diaria'
price_cents int not null check (price_cents >= 0),
created_at timestamptz default now(),
unique (category_id, day_range, stay_type)
)
-- Extras (antes era localStorage)
extras (
id bigserial primary key,
brand_id bigint references brands(id) on delete cascade,
title text not null,
description text,
price_cents int not null,
image_url text,
active boolean default true,
sort_order int default 0,
created_at timestamptz default now()
)
-- Reservas (NOVA — hoje só vai pro N8N, perdendo histórico)
reservations (
id bigserial primary key,
brand_id bigint references brands(id),
unit_id bigint references hotel_units(id),
category_id bigint references suite_categories(id),
stay_type text not null,
checkin_at timestamptz not null,
customer_name text not null,
customer_phone text not null,
customer_cpf text not null,
customer_email text,
notes text,
total_cents int not null,
deposit_cents int not null,
chatwoot_contact_id bigint,
chatwoot_conversation_id bigint,
chatwoot_pix_charge_id bigint,
pix_txid text,
status text not null default 'pending', -- pending|paid|expired|canceled
created_at timestamptz default now(),
paid_at timestamptz
)
-- Reserva ↔ extras escolhidos
reservation_extras (
reservation_id bigint references reservations(id) on delete cascade,
extra_id bigint references extras(id),
price_cents int not null, -- snapshot do preço no momento
primary key (reservation_id, extra_id)
)
RLS (Row Level Security):
- Leitura pública:
brands,hotel_units(active),suite_categories(active),suite_images,pricing,extras(active) - Escrita: só pro role
admin(Supabase Auth) reservations: leitura só da própria (via service_role no backend Vite)
Bug corrigido do POC:
day_rangehoje checadayOfWeek >= 1 && <= 3, mas domingo é 0, fazendo domingo cair em "quinta a domingo" errado.- Novo schema usa enum
'weekday' | 'weekend'e a função de mapeamento é:// weekday: segunda(1) a quinta(4) // weekend: sexta(5), sábado(6), domingo(0) const isWeekend = (d: Date) => [0, 5, 6].includes(d.getDay())
6. Integração Chatwoot — endpoint novo
Único código novo no Chatwoot: um controller público autenticado por token.
Arquivo novo
enterprise/app/controllers/public/api/v1/captain/public_reservations_controller.rb
Rotas
# config/routes.rb — dentro do enterprise namespace
namespace :public, defaults: { format: 'json' } do
namespace :api do
namespace :v1 do
namespace :captain do
resources :public_reservations, only: [:create, :show] do
member do
get :status
end
end
end
end
end
end
Autenticação
Token opaco gerado como variável de ambiente no Chatwoot (RESERVA_1001_API_TOKEN), validado por before_action. Não é login de agente.
POST /public/api/v1/captain/public_reservations
Request:
{
"chatwoot_unit_id": 42,
"category": "Spa-Hidromassagem",
"stay_type": "3hrs",
"checkin_at": "2026-04-14T23:57:00-03:00",
"customer": {
"name": "João da Silva",
"phone": "+5561999998888",
"cpf": "123.456.789-00",
"email": "joao@exemplo.com"
},
"total_cents": 17000,
"deposit_cents": 8500,
"notes": "Chegaremos com 15 min de atraso"
}
Fluxo interno do controller:
- Valida token
- Carrega
Captain::Unitporchatwoot_unit_id - Cria ou atualiza
Contactno account da unit (match por CPF ou telefone) - Cria
Conversationno inbox WhatsApp da unit com nota inicial da reserva - Cria
Captain::Reservation(status pending) - Chama
Captain::Inter::CobService#callcom CPF, valor do depósito, PIX key da unit - Cria
Captain::PixChargevinculada à reservation - Retorna payload de checkout
Response:
{
"reservation_id": 9931,
"conversation_id": 4421,
"pix": {
"txid": "a1b2c3d4e5...",
"copia_e_cola": "00020126...",
"qrcode_base64": "data:image/png;base64,iVBORw0...",
"expires_at": "2026-04-14T00:57:00-03:00"
}
}
GET /public/api/v1/captain/public_reservations/:id/status
Retorna { "status": "pending" | "paid" | "expired" }. Usado pelo front pra polling a cada 3s na tela de checkout.
Pagamento
- Cliente paga no app do banco dele
- Inter bate webhook no endpoint que já existe:
/api/v1/captain/webhooks/inter_pix Captain::Payments::ConfirmationServicejá marca a reservation comopaid, adiciona labelspagamento_confirmado+reserva_feita, e já dispara a mensagem automática "✅ Pagamento confirmado!" na conversa da atendente
Zero código novo pro pagamento. O controller novo só orquestra a criação.
7. Fluxo do cliente (UX)
1. Landing pública
↓
2. Hero section ("Reserve a experiência que você merece")
↓
3. Formulário em cascata:
Marca → Unidade → Permanência → Categoria → Data/Hora
↓
4. Preview: fotos da categoria (carrossel + lightbox)
+ preço total destacado
+ valor do PIX (50%) destacado
+ extras opcionais (se tiver)
↓
5. Dados pessoais: nome, telefone, CPF, email, observação
↓
6. [Confirmar e Pagar Reserva]
↓
7. Loading (skeleton enquanto chama API)
↓
8. Tela de checkout:
- QR code (com glow animado champagne)
- Copia-e-cola (botão de copy com feedback)
- Timer regressivo ("Válido por 59:48")
- Polling de status a cada 3s
↓
9. Cliente paga no banco
↓
10. Status muda pra "paid" → animação de sucesso:
- Confetti champagne + rose-gold
- Check desenhado com anime.js (stroke draw)
- Mensagem: "Reserva confirmada! Sua atendente já foi avisada."
↓
11. Botão "Voltar ao início" / link do WhatsApp da atendente
8. Página admin
/admin — Supabase Auth (email + senha).
Abas:
- Marcas — CRUD simples (nome, logo, cor primária)
- Unidades — CRUD vinculado a marca, com
chatwoot_unit_id(dropdown buscando via API do Chatwoot) - Categorias — CRUD vinculado a unidade, ordem, ativo/inativo
- Fotos — upload drag-and-drop pro Supabase Storage, preview, reorder
- Preços — tabela editável (categoria × day_range × stay_type)
- Extras — CRUD vinculado a marca
- Reservas — só leitura, lista com filtros (status, data, unidade), detalhe com link pra conversa no Chatwoot
9. Refatoração do POC — o que herdar, o que refazer
Herdar (copiar direto ou com ajustes mínimos)
components/FormField.tsx,components/SelectField.tsxcomponents/ui/*(shadcn base)- Estrutura de seleção em cascata
index.css(design tokens base)vite.config.ts,tsconfig.json
Refazer
App.tsx(730 linhas, monolito) → quebrar em:pages/ReservationPage.tsxcomponents/reservation/HeroSection.tsxcomponents/reservation/ReservationForm.tsxcomponents/reservation/ImageCarousel.tsxcomponents/reservation/PriceSummary.tsxcomponents/reservation/ExtrasGrid.tsxcomponents/reservation/CustomerForm.tsxcomponents/checkout/PixCheckout.tsxcomponents/checkout/SuccessScreen.tsxhooks/useReservationForm.ts(consolida os 14+ useState)
components/AdminPage.tsx(881 linhas) → uma aba por arquivo empages/admin/*, comAuthGateservices/apiService.ts→ novoservices/chatwootApi.tsque fala com o controller novoservices/*.ts(brand/unit/pricing/suite) → regenerar após novo schemasupabaseClient.ts→ mover chaves pra.env.local, usarimport.meta.env.VITE_SUPABASE_*index.html→ removerimportmapdoesm.sh, Vite bundla tudotypes.ts→ regenerar viasupabase gen types typescript
Bug pra corrigir
- Lógica de
day_range(domingo no ramo errado) — ver seção 5
10. Melhorias visuais (animações + polish)
- Hero section — fundo com parallax suave, gradiente
obsidian → midnight, título em Fraunces serif com reveal escalonado (framer-motionstaggerChildren) - Stagger entrance nas opções de categoria/unidade quando muda a marca
- Carrossel de fotos — swipe horizontal, indicadores, clique pra lightbox em tela cheia
- Pulse animation no preço total quando recalcula (anime.js
scale: [1, 1.08, 1]) - QR code com glow — borda animada pulsando champagne (anime.js
keyframes) - Skeleton screens durante fetch inicial e submissão
- Tela de sucesso — confetti + check SVG desenhado com anime.js
stroke-dashoffset(efeito de "desenhar" o check) - Flip cards nos extras (CSS
rotateYon hover) - Transições entre etapas —
AnimatePresencedo framer-motion, slide + fade - Tipografia editorial — Fraunces nos títulos, Inter no resto, kerning generoso
- Microinterações em botões — hover com
scale: 1.02+ glow champagne sutil - Dark glassmorphism nos cards de categoria (backdrop-blur sobre fundo escuro)
11. Plano de entrega em fases
| Fase | Escopo | Critério de pronto |
|---|---|---|
| 1. Fundação | Novo projeto Vite limpo em reserva-1001/, migração do POC, Supabase novo schema aplicado, Tailwind v4 com paleta premium, .env configurado, remover importmap do HTML |
pnpm dev roda, Tailwind tokens ativos, schema no Supabase |
| 2. Backend Chatwoot | Controller PublicReservationsController com 2 endpoints + token auth + testes RSpec de integração |
POST retorna PIX real, GET retorna status |
| 3. Fluxo público core | Refatoração do App em componentes, hook useReservationForm, integração com API nova, checkout com polling, tela de sucesso |
Cliente completa reserva end-to-end em ambiente de teste |
| 4. Admin | Abas separadas, Supabase Auth, upload de fotos pro Storage, CRUD de todas entidades | Admin cadastra marca→unidade→categoria→fotos→preço |
| 5. Polish visual | Hero, carrossel, animações (framer-motion + anime.js), skeletons, tipografia, microinterações | Revisão visual aprovada |
| 6. Deploy + QA | Vercel com subdomínio, teste ponta-a-ponta com unidade real do Chatwoot, ajustes finais | Reserva real paga, mensagem cai na conversa da atendente |
12. Riscos e mitigações
| Risco | Mitigação |
|---|---|
| mTLS Inter falhar no controller novo | Controller reusa Captain::Inter::CobService — se falhar pra atendente, falha aqui também. Mesmo comportamento, mesmo suporte |
| Webhook Inter bate antes do front terminar de criar a reservation no Supabase | Chatwoot é a fonte de verdade da reserva e do PIX. Supabase só cacheia o ID pra tela do front. Ordem: chama Chatwoot → salva IDs no Supabase |
| Cliente sai da tela de checkout antes de pagar | Webhook Inter ainda dispara, reserva é marcada como paga no Chatwoot. Front perde a tela de sucesso mas a atendente recebe o aviso e entra em contato |
| Admin sem autenticação hoje (POC) | Fase 4 adiciona Supabase Auth obrigatório |
| Chave anon do Supabase exposta no repo do POC | Rotacionar antes do deploy, mover pra .env |
| Domingo precificado errado | Corrigido no novo schema (seção 5) |
13. Itens fora de escopo (V2)
- Segunda metade do pagamento online
- Disponibilidade em tempo real (bater PMS)
- Login de cliente / área logada
- Remarcação / cancelamento online
- E-mail transacional (confirmação)
- SMS de confirmação
- Google Analytics / tracking de conversão
- Multi-idioma (PT-BR apenas)
- App mobile nativo
14. Glossário
- PIX Copia-e-cola: string que o cliente cola no app do banco para pagar
- txid: ID único de transação PIX na Inter
- PixCharge: modelo no Chatwoot que rastreia uma cobrança PIX
- Captain::Unit: modelo no Chatwoot que representa uma unidade do hotel, com credenciais Inter próprias
- Day range: se o dia do check-in é dia de semana ou fim de semana, afeta preço
- Stay type: tipo de permanência (2hrs, 3hrs, 4hrs, pernoite, diária)