🚀 Auto-deploy: BrainWind atualizado em 14/07/2026 15:18:53
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
# Plano de Comercialização — BrainWind
|
||||
|
||||
## Software de cálculo de vento conforme a norma brasileira NBR 6123
|
||||
|
||||
---
|
||||
|
||||
**Preparado por:** Marcos Reifonas
|
||||
**Data:** Julho de 2026
|
||||
**Aplicativo:** BrainWind (cálculo de vento em edificações)
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumo da situação atual
|
||||
|
||||
O BrainWind é um aplicativo de cálculo de vento para projetos de engenharia civil (galpões, silos, abóbadas, cúpulas, muros, pontes, torres e coberturas). Ele aplica a norma brasileira **NBR 6123:2023** na íntegra.
|
||||
|
||||
**O que já está pronto:**
|
||||
|
||||
- Toda a norma técnica foi implementada (36 tabelas e 3 anexos oficiais)
|
||||
- Cobre 9 tipos diferentes de estrutura
|
||||
- Foi feita uma auditoria independente que testou 91 cenários contra os exemplos da própria norma — todos passaram
|
||||
- Existe um motor de cálculo matemático robusto, testado e validado
|
||||
- O programa gera relatórios em PDF prontos para apresentação ao cliente
|
||||
|
||||
**Resumo:** o produto técnico já é sólido. O que falta é o lado comercial — transformar um aplicativo interno em algo que pode ser vendido e mantido como negócio.
|
||||
|
||||
**Oportunidade de mercado:**
|
||||
|
||||
Não existe hoje no Brasil nenhum software dedicado exclusivamente ao cálculo de vento conforme a norma atualizada. Os engenheiros resolvem isso em planilhas de Excel ou compram softwares estrangeiros caros (que não seguem a norma brasileira). O BrainWind entra em um espaço de mercado pouco ocupado, com um produto superior ao que está disponível.
|
||||
|
||||
---
|
||||
|
||||
## 2. Como vamos ganhar dinheiro
|
||||
|
||||
A forma mais indicada para esse tipo de software técnico é a **licença anual**, com a opção de licença definitiva (pagamento único) para quem prefere.
|
||||
|
||||
### Planos sugeridos para iniciar
|
||||
|
||||
| Plano | Preço | Para quem é |
|
||||
|-------|-------|-------------|
|
||||
| **Teste grátis** | R$ 0,00 | Qualquer engenheiro, por 14 dias, sem compromisso |
|
||||
| **Licença Anual** | R$ 1.500 por ano | Escritórios pequenos, autônomos, projetistas |
|
||||
| **Licença Perpétua** | R$ 4.000 (à vista) | Quem quer pagar uma vez e usar por tempo indeterminado |
|
||||
| **Licença Empresa** | R$ 6.000 por ano (5 usuários) | Escritórios de engenharia com equipe |
|
||||
|
||||
**Por que esses valores funcionam:**
|
||||
|
||||
O engenheiro cobra em média R$ 5.000 a R$ 15.000 por projeto de cálculo estrutural. Se o BrainWind economiza 4 horas de trabalho em cada projeto (o que faz), o software se paga em um único projeto. Para o profissional, é um investimento óbvio.
|
||||
|
||||
---
|
||||
|
||||
## 3. As fases do plano
|
||||
|
||||
O trabalho foi dividido em 8 fases. Cada fase tem um objetivo claro e um prazo realista para um time de uma ou duas pessoas trabalhando em meio período.
|
||||
|
||||
### Fase 0 — Decisões iniciais (1 a 2 dias)
|
||||
|
||||
Antes de começar qualquer trabalho técnico, é preciso travar algumas decisões que vão impactar tudo depois.
|
||||
|
||||
**O que precisa ser decidido:**
|
||||
|
||||
- Confirmar os preços finais (sugestão acima)
|
||||
- Registrar o domínio do site (exemplo: brainwind.com.br)
|
||||
- Escolher a plataforma de pagamento (sugestão: Kiwify, que é brasileira e fácil de usar)
|
||||
- Decidir se vamos abrir um CNPJ/MEI para emitir nota fiscal (recomendado — sem isso não dá para vender para empresas)
|
||||
|
||||
### Fase 1 — Tornar o app um produto vendável (2 a 3 semanas)
|
||||
|
||||
Hoje o BrainWind funciona apenas no computador de quem instalou, com os dados guardados localmente. Para vender, é preciso transformar em algo que:
|
||||
|
||||
- Exige login com usuário e senha
|
||||
- Valida uma licença (chave de ativação)
|
||||
- Salva os projetos na nuvem (para o cliente acessar de qualquer lugar)
|
||||
- Controla o período de teste grátis automaticamente
|
||||
|
||||
A infraestrutura técnica já existe (o servidor dessa VPS roda Supabase, que serve para isso sem custo adicional).
|
||||
|
||||
### Fase 2 — Criar a identidade visual e a página de vendas (1 a 2 semanas)
|
||||
|
||||
**O que precisa ser feito:**
|
||||
|
||||
- Criar um logotipo simples (pode ser feito online sem custo)
|
||||
- Definir cores e estilo visual (azul técnico + branco é uma escolha segura e profissional)
|
||||
- Construir uma página na internet que explique o que o app faz, para quem é, quanto custa, e como comprar
|
||||
- Escrever os Termos de Uso e a Política de Privacidade (exigidos por lei)
|
||||
|
||||
### Fase 3 — Configurar o pagamento e a entrega automática (1 semana)
|
||||
|
||||
Quando o cliente pagar, o sistema precisa liberar o acesso sozinho — sem ninguém precisar ficar apertando botão.
|
||||
|
||||
**O que fazer:**
|
||||
|
||||
- Criar os produtos na plataforma Kiwify
|
||||
- Configurar o sistema para que, assim que o pagamento for confirmado, uma licença seja criada e enviada por e-mail
|
||||
- Configurar avisos automáticos quando a licença estiver perto de expirar
|
||||
|
||||
### Fase 4 — Marketing e captação de clientes (1 a 2 semanas)
|
||||
|
||||
Sem divulgação, ninguém vai saber que o produto existe.
|
||||
|
||||
**Estratégia de comunicação:**
|
||||
|
||||
- Criar um vídeo curto (3 minutos) mostrando o app calculando um galpão real do início ao fim
|
||||
- Publicar posts no LinkedIn e Instagram mostrando casos de uso
|
||||
- Participar de grupos de Facebook e LinkedIn voltados para engenheiros estruturais
|
||||
- Produzir um artigo técnico sobre as mudanças da NBR 6123 versão 2023 — isso traz clientes por mecanismos de busca
|
||||
|
||||
**Estratégia de beta-testers (etapa fundamental):**
|
||||
|
||||
Antes de abrir para todo mundo, oferecer o BrainWind de graça por 30 dias para 20 a 30 engenheiros selecionados. Em troca, eles dão feedback, relatam problemas e fornecem depoimentos para usar no marketing. Esse é o passo mais importante para validar o produto com usuários reais antes de começar a vender oficialmente.
|
||||
|
||||
### Fase 5 — Testes com usuários reais (2 a 4 semanas)
|
||||
|
||||
Rodar o programa com o grupo de beta-testers. Acompanhar o uso deles, responder dúvidas, corrigir o que estiver confuso, melhorar o que pedirem.
|
||||
|
||||
**O que medir:**
|
||||
|
||||
- Quantos realmente usam o programa (não só instalam)
|
||||
- Quantos chegam até o final do cálculo e geram o PDF
|
||||
- Quantos recomendariam para um colega (nota de satisfação)
|
||||
- Quais funcionalidades foram mais pedidas
|
||||
- Quais bugs apareceram
|
||||
|
||||
### Fase 6 — Lançamento oficial (1 dia, mais uma semana de ajuste)
|
||||
|
||||
Abrir para todo mundo.
|
||||
|
||||
**Checklist do dia do lançamento:**
|
||||
|
||||
- Página de vendas no ar e testada
|
||||
- Sistema de pagamento funcionando
|
||||
- Suporte (WhatsApp ou e-mail) pronto para responder
|
||||
- Documentação legal publicada
|
||||
- Vídeo de demonstração publicado
|
||||
- Depoimentos de beta-testers em mãos
|
||||
- Posts de lançamento agendados nas redes sociais
|
||||
|
||||
**Meta para os primeiros 90 dias:**
|
||||
|
||||
- 100 testes grátis iniciados
|
||||
- 20 clientes pagantes
|
||||
- R$ 30.000 de receita
|
||||
- Nota de satisfação média acima de 50 (em escala de 0 a 100)
|
||||
|
||||
### Fase 7 — Crescimento pós-lançamento (contínuo)
|
||||
|
||||
Depois do lançamento, o trabalho continua:
|
||||
|
||||
- Criar versão instalável para Windows e Mac (sensação de "software profissional")
|
||||
- Integrar com outros programas que engenheiros já usam (Ftool, SAP2000, AutoCAD)
|
||||
- Avaliar expansão para outras normas de vento internacionais (mercado americano e europeu paga muito mais)
|
||||
- Criar programa de afiliados — engenheiro indica, ganha comissão recorrente
|
||||
- Considerar versão mobile para consulta rápida em obra
|
||||
|
||||
---
|
||||
|
||||
## 4. Investimento necessário
|
||||
|
||||
### Custos para começar
|
||||
|
||||
| Item | Valor |
|
||||
|------|-------|
|
||||
| Abertura de MEI | Grátis |
|
||||
| Registro de domínio (por ano) | R$ 50 |
|
||||
| Logo (versão simples, online) | R$ 0 a 200 |
|
||||
| Plataforma Kiwify | Grátis (cobra só % sobre venda) |
|
||||
| Infraestrutura de servidor | Já existente (sem custo adicional) |
|
||||
| Email transacional | Grátis no início |
|
||||
| **Total inicial** | **R$ 50 a 250** |
|
||||
|
||||
### Custos mensais recorrentes
|
||||
|
||||
| Item | Valor mensal |
|
||||
|------|--------------|
|
||||
| Contador (MEI ou Simples Nacional) | R$ 100 a 300 |
|
||||
| Comissão Kiwify (~10% sobre vendas) | Variável |
|
||||
| **Total fixo mensal** | **R$ 100 a 300** |
|
||||
|
||||
### Projeção de receita (cenário conservador)
|
||||
|
||||
| Mês | Novos testes grátis | Novos clientes pagantes | Receita acumulada |
|
||||
|-----|---------------------|--------------------------|-------------------|
|
||||
| 1 (lançamento) | 30 | 6 | R$ 750 |
|
||||
| 2 | 50 | 10 | R$ 2.500 |
|
||||
| 3 | 80 | 16 | R$ 4.500 |
|
||||
| 6 | 150 por mês | 30 por mês | R$ 18.000 |
|
||||
| 12 | 250 por mês | 50 por mês | R$ 60.000 |
|
||||
|
||||
Mesmo no cenário conservador, em 12 meses o produto se paga e começa a gerar lucro consistente. A margem esperada é alta (cerca de 85%) porque não há custo de produção por unidade vendida — o software é o mesmo para todos os clientes.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cronograma resumido
|
||||
|
||||
| Mês 1 | Mês 2 | Mês 3 | Mês 4 | Mês 5 em diante |
|
||||
|-------|-------|-------|-------|------------------|
|
||||
| Decisões iniciais | Sistema de login e licenças | Pagamento e entrega | Marketing e beta-testers | Lançamento oficial e crescimento |
|
||||
|
||||
**Tempo total até o lançamento:** entre 3 e 4 meses, trabalhando em meio período.
|
||||
|
||||
---
|
||||
|
||||
## 6. Riscos e como mitigar
|
||||
|
||||
| Risco | Probabilidade | Impacto | Como mitigar |
|
||||
|-------|---------------|---------|---------------|
|
||||
| Pouca demanda no mercado | Baixa | Alto | Validar com beta-testers antes de investir pesado em marketing |
|
||||
| Concorrente surgir | Média | Médio | Velocidade: lançar logo, fidelizar primeiros clientes |
|
||||
| Bug grave aparecer | Média | Médio | Programa de beta-testers pega antes do lançamento público |
|
||||
| Problema com a norma (nova versão) | Baixa | Alto | Arquitetura modular permite atualizar tabelas sem reescrever |
|
||||
| Dificuldade de explicar o valor | Média | Alto | Vídeo de demonstração + depoimentos resolvem |
|
||||
|
||||
---
|
||||
|
||||
## 7. Próximos passos imediatos
|
||||
|
||||
Esta semana, três ações simples para começar:
|
||||
|
||||
**1. Decidir os preços finais**
|
||||
Reunião entre os sócios para confirmar os valores sugeridos. Pode-se ajustar para cima se houver demanda ou para baixo se quiser crescer mais rápido no início.
|
||||
|
||||
**2. Verificar disponibilidade do domínio**
|
||||
Acessar registro.br e pesquisar brainwind.com.br. Se estiver disponível, registrar (R$ 50). Se não, pensar em alternativas (brainwind.app, brainwind.eng.br).
|
||||
|
||||
**3. Abrir MEI no nome de um dos sócios**
|
||||
Acessar gov.br/mei. Leva cerca de 30 minutos. Sem CNPJ não dá para emitir nota fiscal, e empresa nenhuma compra sem nota.
|
||||
|
||||
---
|
||||
|
||||
## 8. Conclusão
|
||||
|
||||
O BrainWind tem uma base técnica sólida, auditada e validada. O produto está pronto no que diz respeito à engenharia. O que falta é o lado comercial — transformar esse ativo técnico em um negócio que gera receita recorrente.
|
||||
|
||||
A oportunidade de mercado é real e clara: não existe hoje no Brasil um software dedicado exclusivamente ao cálculo de vento conforme a norma brasileira atualizada. Os engenheiros estão resolvendo isso em planilhas, com medo de errar a versão da norma.
|
||||
|
||||
O investimento inicial é baixo (menos de R$ 300), o risco é controlável (validar com beta-testers antes de investir pesado), e a margem esperada é alta.
|
||||
|
||||
A recomendação é seguir as fases descritas neste documento, começando pelas decisões da Fase 0 e seguindo em frente com disciplina. Em 3 a 4 meses, o produto pode estar no ar gerando receita.
|
||||
|
||||
---
|
||||
|
||||
**Anexo:** documento técnico detalhado disponível em `ROADMAP-VENDA.md` (versão para a equipe técnica com instruções específicas de implementação).
|
||||
+10
-1
@@ -15,7 +15,7 @@
|
||||
| Item | Valor |
|
||||
|------|-------|
|
||||
| Data da última atualização | 2026-07-14 |
|
||||
| Marcos concluídos | **8 / 8** (plano completo) + M9.1 ✅ + M9.2 ✅ + M9.3 ✅ + M9.4 ✅ + M9.5 ✅ + M9.6 ✅ + M9.7 ✅ + M9.8 ✅ + M9.9 ✅ + M9.10 ✅ + M9.13 ✅ + M9.14 ✅ + M9.15 ✅ + M9.16 ✅ + M9.17 ✅ + M9.18 ✅ |
|
||||
| Marcos concluídos | **8 / 8** (plano completo) + M9.1 ✅ + M9.2 ✅ + M9.3 ✅ + M9.4 ✅ + M9.5 ✅ + M9.6 ✅ + M9.7 ✅ + M9.8 ✅ + M9.9 ✅ + M9.10 ✅ + M9.13 ✅ + M9.14 ✅ + M9.15 ✅ + M9.16 ✅ + M9.17 ✅ + M9.18 ✅ + M9.19 ✅ |
|
||||
| Cobertura NBR 6123:2023 | **100% das 11 seções + 3 anexos** |
|
||||
| Linhas de código TS/TSX | ~8.200 |
|
||||
| Arquivos `.ts`/`.tsx` no `src/` | **120** |
|
||||
@@ -715,6 +715,15 @@ gráficos SVG inline para variáveis CSS do tema, suportando dark mode.
|
||||
- **Envoltória de Pressão Interna (Cpi):** Adicionada `computeCpiEnvelope()` e o caso de 4 faces permeáveis agora trata explicitamente a envoltória dupla de Cpi [-0.3, 0].
|
||||
- **Refinamentos:** Erro legado com constantes (`ka` modificado de 4.06 para precisos 4.058 para área residencial) e eliminação do uso de `@ts-ignore` em `coefficients.ts`.
|
||||
|
||||
#### M9.19 — Reorganização do Menu Lateral e Padronização de Layouts
|
||||
**Status:** ✅ Concluído (2026-07-14)
|
||||
**Escopo:** Reorganizar a estrutura e hierarquia do menu lateral desktop para separar parâmetros globais, arquivos e glossário, e padronizar o posicionamento dos botões de ação nos módulos.
|
||||
**Entregas:**
|
||||
- **Reorganização do Sidebar:** "Parâmetros do Vento" movido para o bloco inferior do menu lateral desktop. O antigo menu "Preferências" foi renomeado para "Configurações" e agora agrupa apenas as abas "Preferências" e "Testes". "Arquivos" (antigo Gerenciador) e "Glossário" foram promovidos a links independentes de primeiro nível no menu (sem exibir abas de topo no desktop).
|
||||
- **Padronização de Botões de Ação:** O agrupamento de botões de Salvar/Exportar nos módulos de **Pipe-rack** e **Galpão** foi padronizado e movido para ficar imediatamente acima do painel de "Captura 3D" (SceneCapturePanel).
|
||||
- **Salvamento no Galpão:** Implementado o `SaveModuleDialog` no módulo de Galpão, com suporte completo a salvar/carregar as configurações geométricas dinâmicas através do `useHydrationStore`.
|
||||
- **Limpeza de Warnings:** Removidas variáveis e imports não utilizados no arquivo `SettingsModule.tsx` e corrigida a vinculação do input de arquivo oculto.
|
||||
|
||||
#### M9.11 — Persistência opcional em servidor (~especulativo)
|
||||
**Problema:** Tudo é local (IndexedDB).
|
||||
**Tarefa:**
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
# BrainWind — Roadmap de Comercialização
|
||||
|
||||
> **Objetivo:** Levar o BrainWind (antigo VentoApp/WindApp) do estágio atual de desenvolvimento interno até a venda comercial, com plano detalhado, fases e tarefas concretas.
|
||||
|
||||
**Estado atual (jul/2026):**
|
||||
- ✅ 8/8 marcos concluídos
|
||||
- ✅ 100% NBR 6123:2023 coberta (36 tabelas + 3 anexos)
|
||||
- ✅ Auditoria independente Opus: 91/91 cenários PASS
|
||||
- ✅ 38 testes vitest passando
|
||||
- ✅ TS estrito, build verde
|
||||
- ⚠️ Tudo local (IndexedDB) — sem auth, sem servidor, sem licença
|
||||
|
||||
---
|
||||
|
||||
## 📋 Visão geral das fases
|
||||
|
||||
| Fase | Nome | Duração estimada | Pré-requisito |
|
||||
|------|------|------------------|---------------|
|
||||
| **0** | Decisões estratégicas | 1-2 dias | Nenhum |
|
||||
| **1** | Infraestrutura comercial (auth + licença + persistência) | 2-3 semanas | Fase 0 |
|
||||
| **2** | Identidade visual + landing page | 1-2 semanas | Fase 0 (paralela à 1) |
|
||||
| **3** | Empacotamento de venda (preço, checkout, webhook) | 1 semana | Fases 1 e 2 |
|
||||
| **4** | Materiais de marketing + aquisição | 1-2 semanas | Fases 1, 2 e 3 |
|
||||
| **5** | Beta-tester + ajustes finais | 2-4 semanas | Fases 1-4 |
|
||||
| **6** | Lançamento oficial | 1 dia | Fase 5 |
|
||||
| **7** | Escala (desktop, integrações, novos módulos) | contínuo pós-lançamento | Fase 6 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 FASE 0 — Decisões estratégicas
|
||||
|
||||
**Objetivo:** Travar as decisões que vão moldar todo o resto. Sem isso, qualquer trabalho técnico vai ter que ser refeito.
|
||||
|
||||
### 0.1 — Escolher modelo de cobrança
|
||||
|
||||
| Modelo | Vantagem | Desvantagem | Minha recomendação |
|
||||
|--------|----------|-------------|---------------------|
|
||||
| Assinatura mensal | Receita recorrente previsível | Cliente não gosta de mensal | ❌ Não começar com |
|
||||
| Assinatura anual | Receita previsível + desconto incentiva | Ticket mais alto | ✅ **Modelo principal** |
|
||||
| Licença perpétua | Cliente paga uma vez e esquece | Receita não-recorrente | ✅ **Modelo secundário** |
|
||||
| Por projeto | Acompanha uso real do engenheiro | Dificulta automação | ❌ Não agora |
|
||||
|
||||
**Decisão sugerida:**
|
||||
- **Plano 1 — Anual:** R$ 1.500/ano (~R$ 125/mês)
|
||||
- **Plano 2 — Perpétua:** R$ 4.000 (atualização de norma inclusa por 2 anos)
|
||||
- **Plano 3 — Empresa (5 usuários):** R$ 6.000/ano
|
||||
- **Trial:** 14 dias grátis com todas as funções (gate automático)
|
||||
|
||||
### 0.2 — Domínio e marca
|
||||
|
||||
- Verificar disponibilidade de `brainwind.com.br` (registro.br, ~R$ 40/ano)
|
||||
- Alternativas: `brainwind.app`, `brainwind.com`, `brainwind.eng.br`
|
||||
- Logo: pode começar com texto estilizado + ícone de vento (Fiverr/99designs, R$ 100-300, ou gerador de logo AI grátis)
|
||||
- Cor primária: azul escuro/azul técnico (engenharia remete a confiança)
|
||||
- Tipografia: Inter ou Manrope (clean, técnica)
|
||||
|
||||
### 0.3 — Canal de checkout
|
||||
|
||||
| Gateway | Custo | Nota fiscal | Webhook | Recomendação |
|
||||
|---------|-------|-------------|---------|--------------|
|
||||
| **Kiwify** | ~R$ 0 + 9.99% | Sim | Sim, simples | ✅ **Principal** |
|
||||
| Hotmart | ~R$ 0 + 9.99% | Sim | Sim | Alternativa |
|
||||
| Eduzz | ~R$ 0 + 9.99% | Sim | Sim | Alternativa |
|
||||
| Asaas | R$ 0-10 + 2,49% | Não | Sim | Para assinatura recorrente |
|
||||
| Stripe direto | 4,99% + R$ 0,40 | Não | Sim | Para internacional (não agora) |
|
||||
|
||||
**Decisão sugerida:** Começar com **Kiwify**. Depois, se crescer, considerar Asaas pra recorrência pura (margem maior).
|
||||
|
||||
### 0.4 — Stack de auth + persistência
|
||||
|
||||
- ✅ **Supabase Auth + Postgres + RLS** (já rodando na VPS, custo zero)
|
||||
- Email/senha + OAuth Google
|
||||
- Tabela `licenses` com chave única por usuário
|
||||
- Tabela `projects` com `user_id` e RLS
|
||||
|
||||
### 0.5 — Forma jurídica
|
||||
|
||||
- Pra emitir nota e receber de pessoa jurídica, **precisa de CNPJ**
|
||||
- MEI funciona pra R$ 81k/ano (teto de receita)
|
||||
- Simples Nacional se passar do MEI (R$ 100-500/mês de contador)
|
||||
- Abrir MEI: 30 minutos no portal do empreendedor, grátis
|
||||
- **Decisão a tomar:** abrir MEI antes do lançamento oficial
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ FASE 1 — Infraestrutura comercial (auth + licença + persistência)
|
||||
|
||||
**Objetivo:** Transformar o app local num SaaS com autenticação, licenciamento e persistência na nuvem. Sem isso, ninguém compra — é um arquivo Excel glorificado.
|
||||
|
||||
**Duração:** 2-3 semanas
|
||||
**Pré-requisito:** Fase 0
|
||||
|
||||
### 1.1 — Configurar Supabase para BrainWind
|
||||
|
||||
1. Criar projeto no Supabase da VPS (`supabase` service, porta 5432)
|
||||
2. Schema SQL inicial:
|
||||
```sql
|
||||
-- Tabela de usuários (Supabase Auth já cria auth.users)
|
||||
CREATE TABLE public.user_profiles (
|
||||
id UUID REFERENCES auth.users(id) PRIMARY KEY,
|
||||
full_name TEXT,
|
||||
company TEXT,
|
||||
crea TEXT,
|
||||
phone TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Tabela de licenças
|
||||
CREATE TABLE public.licenses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
license_key TEXT UNIQUE NOT NULL,
|
||||
plan TEXT NOT NULL CHECK (plan IN ('annual', 'perpetual', 'enterprise')),
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'revoked')),
|
||||
started_at TIMESTAMPTZ DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
kiwify_order_id TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Tabela de projetos (substitui IndexedDB)
|
||||
CREATE TABLE public.projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
module TEXT NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- RLS: usuário só vê seus próprios dados
|
||||
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.licenses ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can view own profile" ON public.user_profiles
|
||||
FOR SELECT USING (auth.uid() = id);
|
||||
CREATE POLICY "Users can update own profile" ON public.user_profiles
|
||||
FOR UPDATE USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can view own licenses" ON public.licenses
|
||||
FOR SELECT USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can CRUD own projects" ON public.projects
|
||||
FOR ALL USING (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
3. Variáveis de ambiente no `.env` do app:
|
||||
```
|
||||
VITE_SUPABASE_URL=https://supabase.reifonas.cloud
|
||||
VITE_SUPABASE_ANON_KEY=<chave_anon>
|
||||
```
|
||||
|
||||
### 1.2 — Implementar autenticação no app
|
||||
|
||||
1. Instalar cliente: `npm install @supabase/supabase-js`
|
||||
2. Criar `src/lib/supabase.ts` com cliente singleton
|
||||
3. Criar `src/pages/Login.tsx`:
|
||||
- Login com email/senha
|
||||
- Login com Google OAuth
|
||||
- Esqueci senha
|
||||
4. Criar `src/pages/Signup.tsx`:
|
||||
- Cadastro com email/senha
|
||||
- Confirmação por email (Supabase já gerencia)
|
||||
5. Criar `src/store/auth.ts` (Zustand):
|
||||
- State: `user`, `session`, `license`, `loading`
|
||||
- Actions: `signIn`, `signOut`, `signUp`, `checkLicense`
|
||||
6. Adicionar rota `/login` e `/signup` no `App.tsx`
|
||||
7. **Guard de rota:** se não autenticado, redireciona pra `/login`
|
||||
8. Persistir sessão (Supabase já faz via localStorage)
|
||||
|
||||
### 1.3 — Sistema de licença (client-side)
|
||||
|
||||
1. Criar `src/lib/license.ts`:
|
||||
- `getLicenseKey()` → busca na tabela `licenses` ativa do user
|
||||
- `validateLicense()` → confirma status = 'active' e não expirada
|
||||
- `requireLicense()` → throw se inválida
|
||||
2. Criar `src/components/LicenseGate.tsx`:
|
||||
- Verifica licença no carregamento do app
|
||||
- Se sem licença válida E sem trial ativo: mostra tela "Ative sua licença"
|
||||
- Se trial ativo: mostra banner "Faltam X dias"
|
||||
- Se licença válida: libera tudo
|
||||
3. **Trial automático:**
|
||||
- No primeiro login, criar registro de trial com 14 dias
|
||||
- Verificação: `trial_started_at + 14 dias > now()`
|
||||
4. **Cache local da licença** pra não bater no Supabase toda hora
|
||||
- Validar no carregamento, depois a cada 6h
|
||||
|
||||
### 1.4 — Migração de IndexedDB para Supabase
|
||||
|
||||
1. Criar `src/lib/storage-adapter.ts`:
|
||||
- Interface comum: `saveProject`, `loadProject`, `listProjects`, `deleteProject`
|
||||
- Implementação A: `IndexedDBAdapter` (atual)
|
||||
- Implementação B: `SupabaseAdapter` (novo)
|
||||
- **Strategy pattern** — escolhe baseado em `user.authenticated`
|
||||
2. Manter IndexedDB como fallback offline (sync quando voltar online)
|
||||
3. Migração one-time: ao primeiro login, copiar projetos do IndexedDB pro Supabase
|
||||
|
||||
### 1.5 — Telemetria mínima (opcional mas útil)
|
||||
|
||||
- Contar projetos criados por usuário
|
||||
- Saber quais módulos são mais usados
|
||||
- **NÃO** telemetria invasiva — apenas métricas de uso agregado
|
||||
- LGPD: política de privacidade clara, opt-in
|
||||
|
||||
### 1.6 — Testes da Fase 1
|
||||
|
||||
- Teste E2E: signup → login → criar projeto → salvar → listar
|
||||
- Teste de licença: trial expira → gate aparece
|
||||
- Teste de licença: chave válida → acesso liberado
|
||||
|
||||
---
|
||||
|
||||
## 🎨 FASE 2 — Identidade visual + landing page
|
||||
|
||||
**Objetivo:** Ter uma cara profissional + página de vendas que converta visitante em lead.
|
||||
|
||||
**Duração:** 1-2 semanas
|
||||
**Pré-requisito:** Fase 0 (paralela à Fase 1)
|
||||
|
||||
### 2.1 — Identidade visual
|
||||
|
||||
1. **Logo:**
|
||||
- Opção A: contratar Fiverr (R$ 80-200, 2-3 dias)
|
||||
- Opção B: usar gerador de logo AI (Looka, Brandmark) — grátis/barato
|
||||
- Opção C: começar só com wordmark (texto estilizado)
|
||||
2. **Paleta de cores:**
|
||||
- Primária: `#1e40af` (azul confiança) ou `#0f766e` (verde técnico)
|
||||
- Secundária: `#f59e0b` (âmbar — destaque)
|
||||
- Neutros: `#0f172a` (texto), `#64748b` (subtexto), `#f1f5f9` (background)
|
||||
3. **Tipografia:**
|
||||
- Heading: Inter Bold / Manrope Bold
|
||||
- Body: Inter Regular / Manrope Regular
|
||||
- Mono (código): JetBrains Mono / Fira Code
|
||||
4. **Aplicação no app:**
|
||||
- Header com logo
|
||||
- Botões e CTAs com cor primária
|
||||
- Footer com links legais
|
||||
|
||||
### 2.2 — Landing page
|
||||
|
||||
1. **Stack:** Coolify dessa VPS (subdomínio `app.brainwind.com.br` aponta pra landing, e o app SaaS fica em `app.brainwind.com.br/app`)
|
||||
2. **Estrutura da landing (1 página):**
|
||||
- **Hero:** Título grande + subtítulo + CTA "Começar trial grátis"
|
||||
- **Problema:** "Calcular vento no Excel dá 4h e sempre dá medo de errar a norma"
|
||||
- **Solução:** "BrainWind calcula em 5 minutos com NBR 6123:2023 atualizada"
|
||||
- **Demo:** GIF/vídeo de 30 segundos mostrando o app calculando um galpão
|
||||
- **Features:** Lista dos 9 tipos de estrutura cobertos
|
||||
- **Para quem é:** Engenheiros estruturais, calculistas, projetistas
|
||||
- **Validação:** "Auditado por IA independente, 91 cenários validados, NBR 6123:2023 completa"
|
||||
- **Preço:** 3 cards (Anual R$ 1.500 / Perpétua R$ 4.000 / Empresa R$ 6.000)
|
||||
- **FAQ:** 6-8 perguntas (norma atualizada?, tem nota fiscal?, roda offline?, ...)
|
||||
- **CTA final:** "Comece grátis por 14 dias"
|
||||
3. **SEO básico:**
|
||||
- Title: "BrainWind — Cálculo de vento conforme NBR 6123:2023"
|
||||
- Meta description otimizada
|
||||
- Schema.org SoftwareApplication
|
||||
|
||||
### 2.3 — Páginas legais (obrigatórias)
|
||||
|
||||
1. **Termos de Uso** — pode usar template + adaptar
|
||||
2. **Política de Privacidade** — LGPD compliance (Supabase tem template)
|
||||
3. **Política de Cookies** (se usar)
|
||||
4. **Disclaimer técnico:** "Software auxiliar. Profissional habilitado (CREA) é responsável pelo projeto."
|
||||
|
||||
### 2.4 — Email transacional
|
||||
|
||||
1. Criar `contato@brainwind.com.br` (Zoho Mail grátis até 5 usuários, ou usar Gmail com domínio próprio via Cloudflare Email Routing — grátis)
|
||||
2. Templates de email:
|
||||
- Boas-vindas (após signup)
|
||||
- Confirmação de licença (após compra)
|
||||
- Aviso de expiração (7 dias antes)
|
||||
- Email de expiração
|
||||
3. **Não precisa de ferramenta cara** — Supabase ou Resend (grátis até 100 emails/dia)
|
||||
|
||||
---
|
||||
|
||||
## 💳 FASE 3 — Empacotamento de venda (checkout + webhook)
|
||||
|
||||
**Objetivo:** Conectar o gateway de pagamento ao sistema de licença. Quando o cliente paga, recebe acesso automaticamente.
|
||||
|
||||
**Duração:** 1 semana
|
||||
**Pré-requisito:** Fases 1 e 2
|
||||
|
||||
### 3.1 — Criar produtos no Kiwify
|
||||
|
||||
1. Conta Kiwify (kyc com CPF/CNPJ)
|
||||
2. Criar 3 produtos:
|
||||
- **BrainWind — Licença Anual** (R$ 1.500, recorrência anual)
|
||||
- **BrainWind — Licença Perpétua** (R$ 4.000, pagamento único)
|
||||
- **BrainWind — Empresa 5 usuários** (R$ 6.000, recorrência anual)
|
||||
3. Configurar:
|
||||
- URL de obrigado: `brainwind.com.br/obrigado?order=XYZ`
|
||||
- Webhook URL: `https://app.brainwind.com.br/api/webhook/kiwify`
|
||||
- Email de comprador: coletar (Kiwify faz isso)
|
||||
|
||||
### 3.2 — Endpoint de webhook
|
||||
|
||||
1. Criar pequeno serviço backend (FastAPI na VPS ou Edge Function Supabase):
|
||||
- `POST /api/webhook/kiwify` recebe payload do Kiwify
|
||||
- Valida assinatura (Kiwify envia hash)
|
||||
- Identifica produto comprado → plano correspondente
|
||||
- Verifica se já existe usuário com esse email
|
||||
- **Se sim:** cria licença e vincula
|
||||
- **Se não:** cria usuário via Supabase Admin API + cria licença + envia email de boas-vindas
|
||||
- Resposta 200 OK
|
||||
|
||||
2. **Importante:** proteger endpoint com validação de assinatura do Kiwify
|
||||
|
||||
### 3.3 — Fluxo de pós-compra
|
||||
|
||||
1. Cliente paga no Kiwify
|
||||
2. Kiwify chama webhook
|
||||
3. Webhook cria licença no Supabase
|
||||
4. Sistema envia email:
|
||||
- Link de acesso
|
||||
- License key (se modelo perpétuo)
|
||||
- Instrução de primeiro acesso
|
||||
5. Cliente entra no app → vê status "Ativo" automaticamente
|
||||
|
||||
### 3.4 — Renovação e expiração
|
||||
|
||||
1. **Cron diário** (rodar na VPS, PM2 ou cronjob):
|
||||
- Verifica licenças próximas de expirar (7 dias antes)
|
||||
- Envia email de aviso
|
||||
- Se passou da data: marca como `expired`
|
||||
2. **Reativação:** cliente paga de novo → webhook reativa licença
|
||||
|
||||
### 3.5 — Testes da Fase 3
|
||||
|
||||
- Teste de webhook em sandbox do Kiwify (eles têm ambiente de teste)
|
||||
- Teste de fluxo completo: comprar → webhook → licença criada → login → acesso liberado
|
||||
- Teste de expiração: forçar data de expiração no passado → gate aparece
|
||||
|
||||
---
|
||||
|
||||
## 📢 FASE 4 — Marketing + aquisição
|
||||
|
||||
**Objetivo:** Gerar leads e converter em clientes pagantes.
|
||||
|
||||
**Duração:** 1-2 semanas (esforço contínuo depois)
|
||||
**Pré-requisito:** Fases 1, 2 e 3
|
||||
|
||||
### 4.1 — Identificar personas
|
||||
|
||||
**Persona primária: "João, o calculista"**
|
||||
- Idade: 30-55 anos
|
||||
- Cargo: Engenheiro estrutural autônomo ou em escritório pequeno/médio
|
||||
- Atende projetos de galpão industrial, silo, cobertura metálica
|
||||
- Usa Eberick/TQS para concreto, mas vento faz em planilha
|
||||
- Dor: 4h por projeto recalculando, medo de errar versão da norma
|
||||
- Onde está: grupos Facebook de engenharia, LinkedIn, YouTube técnico
|
||||
|
||||
**Persona secundária: "Marina, a projetista"**
|
||||
- Idade: 25-40 anos
|
||||
- Cargo: Projetista em escritório de cálculo estrutural
|
||||
- Faz projetos repetitivos (galpões padronizados)
|
||||
- Dor: retrabalho, normas desatualizadas
|
||||
|
||||
### 4.2 — Mensagem central (copy)
|
||||
|
||||
**Título hero:**
|
||||
> "Cálculo de vento NBR 6123 em 5 minutos, sem planilha, sem erro de norma"
|
||||
|
||||
**Subtítulo:**
|
||||
> "BrainWind calcula pressão, arrasto, atrito, Cpi e coeficientes aerodinâmicos para galpões, silos, abóbadas, cúpulas, muros, pontes, torres e estruturas reticuladas. Auditado e validado contra 91 cenários da norma."
|
||||
|
||||
### 4.3 — Conteúdo de aquisição
|
||||
|
||||
1. **Vídeo de demonstração (3 min):**
|
||||
- Mostra fluxo: criar projeto → inserir dimensões → ver zonas de pressão → exportar PDF
|
||||
- Hospedar no YouTube (unlisted ou público)
|
||||
- Usar como principal material de venda
|
||||
|
||||
2. **Posts LinkedIn (engenheiro-para-engenheiro):**
|
||||
- "Quanto tempo você perde recalculando vento de projeto?"
|
||||
- "A NBR 6123:2023 mudou 3 coisas que você talvez ainda não atualizou"
|
||||
- "Comparação: tempo de cálculo Excel vs BrainWind"
|
||||
- Frequência: 2-3x por semana
|
||||
|
||||
3. **Instagram Reels:**
|
||||
- Versões curtas (15-30s) do vídeo de demo
|
||||
- "Tela + voz, sem aparecer"
|
||||
- Frequência: 2-3x por semana
|
||||
|
||||
4. **Posts em grupos Facebook:**
|
||||
- "Estruturalistas do Brasil"
|
||||
- "Engenharia Estrutural"
|
||||
- "Cálculo Estrutural — Galpões"
|
||||
- "NBR 6118 / 6123 / 8800 — Dúvidas"
|
||||
- **Não spammar** — participar como membro, oferecer ajuda, mencionar BrainWind quando pertinente
|
||||
|
||||
5. **Artigo técnico (blog):**
|
||||
- "O que mudou na NBR 6123:2023 vs 2013?"
|
||||
- SEO: rankeia pra "NBR 6123:2023 alterações"
|
||||
- CTA no final: "Calculadora online que aplica a nova norma"
|
||||
|
||||
### 4.4 — Estratégia de beta-tester
|
||||
|
||||
**Objetivo:** 20-30 engenheiros usando de graça em troca de feedback + depoimento.
|
||||
|
||||
1. Recrutar nos grupos Facebook e LinkedIn:
|
||||
- Post: "Estou lançando um software de cálculo de vento conforme NBR 6123:2023. Procuro 30 engenheiros pra usar de graça por 30 dias em troca de feedback."
|
||||
2. Oferecer:
|
||||
- 30 dias grátis (trial estendido)
|
||||
- 50% de desconto vitalício se converter depois
|
||||
- Acesso ao roadmap (influência no que é construído)
|
||||
3. Coletar:
|
||||
- Feedback semanal (formulário curto, 5 perguntas)
|
||||
- Depoimentos em vídeo ou texto (com autorização pra usar em marketing)
|
||||
- Bug reports
|
||||
4. Cronograma:
|
||||
- Semana 1-2: recrutamento
|
||||
- Semana 3-6: uso + feedback
|
||||
- Semana 7: ajustes finais baseados em feedback
|
||||
|
||||
### 4.5 — Materiais de venda
|
||||
|
||||
1. **One-pager PDF** (1 página com tudo que o app faz + preço)
|
||||
- Pra mandar por WhatsApp quando alguém perguntar
|
||||
2. **Tabela comparativa** ("BrainWind vs Excel vs Eberick")
|
||||
3. **Prints do app** com destaque pros diferenciais
|
||||
4. **Screenshot do relatório PDF** (motor de venda mais forte)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 FASE 5 — Beta-tester + ajustes finais
|
||||
|
||||
**Objetivo:** Validar com usuários reais antes de abrir pra todo mundo.
|
||||
|
||||
**Duração:** 2-4 semanas
|
||||
**Pré-requisito:** Fases 1-4
|
||||
|
||||
### 5.1 — Recrutar beta-testers
|
||||
|
||||
- 20-30 engenheiros (alvo da persona)
|
||||
- Mix: autônomos + escritório pequeno + projetistas
|
||||
- Captar via grupos Facebook e LinkedIn (Fase 4.4)
|
||||
|
||||
### 5.2 — Onboarding
|
||||
|
||||
1. Email de boas-vindas com:
|
||||
- Link de cadastro
|
||||
- License key de trial estendido
|
||||
- Manual rápido PDF (5 páginas)
|
||||
- Link do vídeo de 3 min
|
||||
- Canal de suporte (WhatsApp ou Discord)
|
||||
2. Webinar de lançamento interno (opcional): 30min apresentando o app + Q&A
|
||||
|
||||
### 5.3 — Coletar feedback
|
||||
|
||||
**Métricas a acompanhar:**
|
||||
- Quantos se cadastram
|
||||
- Quantos efetivamente criam um projeto
|
||||
- Quantos completam um cálculo até o PDF
|
||||
- NPS (Net Promoter Score)
|
||||
- Bugs reportados
|
||||
- Pedidos de feature
|
||||
|
||||
**Formulário semanal (Google Forms):**
|
||||
1. Como foi sua experiência essa semana? (1-10)
|
||||
2. Qual feature mais te ajudou?
|
||||
3. O que está faltando?
|
||||
4. Encontrou algum bug? Qual?
|
||||
5. Recomendaria pra um colega? (0-10 NPS)
|
||||
|
||||
### 5.4 — Ajustes
|
||||
|
||||
- Corrigir bugs prioritários (P0/P1)
|
||||
- Ajustar UX confuso (se houver)
|
||||
- Adicionar features muito pedidas (1-2 no máximo, pra não virar bagunça)
|
||||
|
||||
### 5.5 — Coletar depoimentos
|
||||
|
||||
- Email ou vídeo curto (1 min) de cada beta-tester satisfeito
|
||||
- **Autorização por escrito** pra usar em marketing
|
||||
|
||||
---
|
||||
|
||||
## 🚀 FASE 6 — Lançamento oficial
|
||||
|
||||
**Objetivo:** Abrir venda pra todo mundo.
|
||||
|
||||
**Duração:** 1 dia (o lançamento em si) + 1 semana de acompanhamento
|
||||
**Pré-requisito:** Fase 5
|
||||
|
||||
### 6.1 — Checklist pré-lançamento
|
||||
|
||||
- [ ] Landing page no ar e testada
|
||||
- [ ] Checkout Kiwify funcionando (teste com R$ 1 real)
|
||||
- [ ] Webhook validando e criando licença
|
||||
- [ ] App funcionando com auth + licença
|
||||
- [ ] PDF de relatório profissional
|
||||
- [ ] Termos de Uso + Política de Privacidade publicados
|
||||
- [ ] Email transacional funcionando
|
||||
- [ ] Suporte (WhatsApp Business) ativo
|
||||
- [ ] MEI/CNPJ aberto
|
||||
- [ ] Conta Kiwify aprovada
|
||||
- [ ] Domínio registrado
|
||||
- [ ] 3-5 depoimentos de beta-testers em mãos
|
||||
- [ ] Vídeo de demo publicado
|
||||
- [ ] Posts de lançamento agendados (LinkedIn, Instagram, FB)
|
||||
|
||||
### 6.2 — Dia do lançamento
|
||||
|
||||
1. **Post LinkedIn** (8h): "BrainWind v1.0 — cálculo de vento NBR 6123:2023 em 5 minutos"
|
||||
2. **Post Instagram** (10h): Reel de 30s
|
||||
3. **Posts em grupos Facebook** (12h, 16h, 20h): evitar spam, variar mensagens
|
||||
4. **Email para lista** (se tiver): avisar do lançamento
|
||||
5. **Atualizar landing** com badge "Lançamento" + contagem regressiva
|
||||
6. **Atendimento extra** no suporte (primeiras 48h)
|
||||
|
||||
### 6.3 — Semana pós-lançamento
|
||||
|
||||
- Acompanhar conversão (visitantes → cadastros → compras)
|
||||
- Responder todo feedback rápido
|
||||
- Resolver bugs urgentes em < 24h
|
||||
- Coletar mais depoimentos
|
||||
|
||||
### 6.4 — Definição de sucesso (primeiros 90 dias)
|
||||
|
||||
- 🎯 100 trials iniciados
|
||||
- 🎯 20 clientes pagantes
|
||||
- 🎯 R$ 30.000 de receita
|
||||
- 🎯 NPS médio ≥ 50
|
||||
|
||||
---
|
||||
|
||||
## 📈 FASE 7 — Escala (pós-lançamento)
|
||||
|
||||
**Objetivo:** Crescer a base de clientes e expandir o produto.
|
||||
|
||||
### 7.1 — Versão desktop instalável
|
||||
|
||||
- Empacotar webapp com **Tauri** (binário menor que Electron, ~10MB)
|
||||
- Windows .exe + macOS .dmg + Linux .AppImage
|
||||
- Vantagem: sensação de "software sério", pode usar offline (com sync posterior)
|
||||
- **Quando:** 1-2 meses após lançamento, se houver demanda
|
||||
|
||||
### 7.2 — Integrações
|
||||
|
||||
- **Ftool** (exportação .txt de carregamentos de vento)
|
||||
- **SAP2000 / Robot** (exportação de cargas)
|
||||
- **AutoCAD** (importação de geometria)
|
||||
- **Excel** (importação de inputs em massa)
|
||||
|
||||
### 7.3 — Novos módulos NBR
|
||||
|
||||
- NBR 8800 (estruturas metálicas — já parcialmente coberto em pipe rack)
|
||||
- NBR 6118 (concreto armado — parceria com Eberick seria mais estratégico)
|
||||
- NBR 15421 (estruturas em situação de incêndio)
|
||||
- **Cuidado:** não virar canivete suíço. Foco em vento = diferencial.
|
||||
|
||||
### 7.4 — Versão mobile (consulta rápida)
|
||||
|
||||
- Capacitor ou React Native empacotando webapp
|
||||
- Cálculos de consulta rápida em obra
|
||||
- Versão simplificada (input → resultado, sem edição de projeto)
|
||||
|
||||
### 7.5 — Marketplace de templates
|
||||
|
||||
- Usuários publicam templates de geometrias comuns
|
||||
- Comissão pra você (10-20%)
|
||||
- Cria comunidade em torno do produto
|
||||
|
||||
### 7.6 — Programa de afiliados
|
||||
|
||||
- Engenheiros indicam → ganham % recorrente
|
||||
- Cupons de desconto
|
||||
- Programa Kiwify já tem suporte nativo
|
||||
|
||||
### 7.7 — Internacionalização
|
||||
|
||||
- Outras normas de vento: ASCE 7 (EUA), Eurocódigo 1 (UE), AS/NZS 1170 (Austrália)
|
||||
- Mercado americano e europeu paga muito mais (US$ 50-200/mês seria trivial)
|
||||
- **Quando:** depois de validar no Brasil
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métricas e KPIs (acompanhar sempre)
|
||||
|
||||
| Métrica | Onde medir | Meta |
|
||||
|---------|-----------|------|
|
||||
| Visitantes únicos/mês na landing | Plausible Analytics (LGPD-friendly) | 1.000+ |
|
||||
| Conversão visitante → trial | Plausible + Supabase | 5-10% |
|
||||
| Conversão trial → pago | Supabase + Kiwify | 20-30% |
|
||||
| Churn mensal | Kiwify + Supabase | < 5% |
|
||||
| NPS | Formulário no app | ≥ 50 |
|
||||
| Tempo médio de uso | Telemetria | > 30min/sessão |
|
||||
| Tickets de suporte/dia | WhatsApp / email | < 10 |
|
||||
|
||||
---
|
||||
|
||||
## 💰 Projeção financeira (cenário base)
|
||||
|
||||
| Mês | Trials novos | Pagantes novos | MRR |
|
||||
|-----|--------------|----------------|-----|
|
||||
| 1 (lançamento) | 30 | 6 | R$ 750 |
|
||||
| 2 | 50 | 10 | R$ 1.250 + R$ 1.250 (renov.) |
|
||||
| 3 | 80 | 16 | R$ 2.000 |
|
||||
| 6 | 150/mês | 30/mês | R$ 3.750 |
|
||||
| 12 | 250/mês | 50/mês | R$ 6.250 |
|
||||
|
||||
Cenário conservador baseado em produto técnico de nicho. Considerar que **80% da receita vem de 20% dos clientes** (licenças perpétuas).
|
||||
|
||||
**Custos fixos:**
|
||||
- VPS (já tem) — R$ 0 incremental
|
||||
- Domínio — R$ 50/ano
|
||||
- Supabase (mesma VPS) — R$ 0
|
||||
- Kiwify — ~10% sobre venda
|
||||
- Email transacional — R$ 0 (Resend free tier)
|
||||
- Contador (MEI → Simples) — R$ 100-300/mês
|
||||
|
||||
**Margem:** ~85% mesmo com Kiwify + contador.
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ Cronograma macro
|
||||
|
||||
```
|
||||
Mês 1 Mês 2 Mês 3 Mês 4 Mês 5+
|
||||
├─────────┼──────────────┼───────────┼───────────┼──────────
|
||||
│ Fase 0 │ Fase 1 + 2 │ Fase 3 │ Fase 4+5 │ Fase 6 │ Fase 7
|
||||
│ Decisões│ Auth + Land. │ Checkout │ Marketing │ Lança │ Escala
|
||||
│ │ │ │ + Beta │ │
|
||||
└─────────┴──────────────┴───────────┴───────────┴──────────┘
|
||||
```
|
||||
|
||||
Estimativa total até lançamento: **3-4 meses** com trabalho dedicado de meio período.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Próxima ação imediata
|
||||
|
||||
**Escolha UMA das tarefas abaixo para começar HOJE:**
|
||||
|
||||
1. **[ ] Abrir MEI** — portal do empreendedor, 30 minutos
|
||||
2. **[ ] Verificar domínio brainwind.com.br** — registro.br
|
||||
3. **[ ] Criar conta Kiwify** — kiwify.com.br
|
||||
4. **[ ] Esboçar landing page** — Figma/Canva/Word
|
||||
5. **[ ] Começar implementação Supabase Auth** — código
|
||||
|
||||
Qual quer atacar primeiro?
|
||||
+41
-9
@@ -14,7 +14,7 @@ import PiperackModule from './pages/PiperackModule';
|
||||
import {
|
||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||
Square, Layers, BarChart3, Activity, Warehouse,
|
||||
Settings2, Sun, Moon, Frame,
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -141,13 +141,14 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
</nav>
|
||||
<div className="border-t p-2 flex flex-col gap-1 items-center justify-center">
|
||||
<GlobalWindSettingsModal
|
||||
trigger={
|
||||
<button
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors text-sm w-full text-left cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0',
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full text-left cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Parâmetros do Vento' : undefined}
|
||||
>
|
||||
@@ -156,18 +157,49 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</nav>
|
||||
<div className="border-t p-2 flex flex-col gap-2 items-center justify-center">
|
||||
<Link
|
||||
to="/settings?tab=gerenciador"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'gerenciador'
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Arquivos' : undefined}
|
||||
>
|
||||
<FolderOpen className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Arquivos</span>}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0',
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && (!location.search || location.search.includes('tab=config') || location.search.includes('tab=testes'))
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? t('nav_settings') : undefined}
|
||||
>
|
||||
<Settings className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>{t('nav_settings')}</span>}
|
||||
{!isCollapsed && <span>Configurações</span>}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings?tab=glossary"
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer',
|
||||
location.pathname === '/settings' && new URLSearchParams(location.search).get('tab') === 'glossary'
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Glossário' : undefined}
|
||||
>
|
||||
<BookOpen className="w-5 h-5 shrink-0" />
|
||||
{!isCollapsed && <span>Glossário</span>}
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Search, BookOpen } from 'lucide-react';
|
||||
import { nbr6123Glossary } from '@/data/glossary';
|
||||
|
||||
const Glossary: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredTerms = useMemo(() => {
|
||||
const termLower = searchTerm.toLowerCase();
|
||||
return nbr6123Glossary.filter(item => {
|
||||
if (item.term.toLowerCase().includes(termLower)) return true;
|
||||
if (item.definition.toLowerCase().includes(termLower)) return true;
|
||||
if (item.tags.some(t => t.toLowerCase().includes(termLower))) return true;
|
||||
if (item.reference.toLowerCase().includes(termLower)) return true;
|
||||
return false;
|
||||
});
|
||||
}, [searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="border-border shadow-sm bg-card/50">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-xl">Glossário NBR 6123</CardTitle>
|
||||
<CardDescription>
|
||||
Dicionário técnico com termos aerodinâmicos e variáveis normativas explicadas de forma didática.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar termo, sigla (ex: Cpe, S1) ou conceito..."
|
||||
className="pl-10 max-w-full"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{filteredTerms.length > 0 ? (
|
||||
filteredTerms.map((item) => (
|
||||
<Card key={item.id} className="overflow-hidden hover:border-primary/50 transition-colors">
|
||||
<CardHeader className="p-4 bg-muted/20 border-b pb-3">
|
||||
<CardTitle className="text-base text-primary flex items-center justify-between">
|
||||
{item.term}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground font-mono mt-1">
|
||||
{item.reference}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-sm leading-relaxed text-foreground/90">
|
||||
{item.definition}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 mt-4">
|
||||
{item.tags.map((tag, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-[10px] font-normal px-2 py-0 h-5">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-1 md:col-span-2 py-12 text-center text-muted-foreground border border-dashed rounded-lg">
|
||||
Nenhum termo encontrado para "{searchTerm}".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Glossary;
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Save, FileJson, FileBox } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog';
|
||||
import { useProjects } from '../lib/hooks/useProjects';
|
||||
import { useWindStore } from '../store/appStore';
|
||||
|
||||
interface SaveModuleDialogProps {
|
||||
moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
inputs: Record<string, unknown>;
|
||||
triggerButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SaveModuleDialog: React.FC<SaveModuleDialogProps> = ({ moduleType, inputs, triggerButton }) => {
|
||||
const { save } = useProjects();
|
||||
const windStore = useWindStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState<'projeto' | 'template'>('projeto');
|
||||
|
||||
// Gera um nome sugerido ao abrir
|
||||
React.useEffect(() => {
|
||||
if (open && !name) {
|
||||
const date = new Date().toLocaleDateString('pt-BR').replace(/\//g, '');
|
||||
const prefix = type === 'projeto' ? 'PRJ' : 'TPL';
|
||||
const modName = moduleType.charAt(0).toUpperCase() + moduleType.slice(1);
|
||||
setName(`${prefix}-${modName}-${date}`);
|
||||
}
|
||||
}, [open, type, moduleType, name]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) return;
|
||||
|
||||
const windData = type === 'projeto' ? {
|
||||
v0: windStore.v0,
|
||||
s1: windStore.s1,
|
||||
s3: windStore.s3,
|
||||
s3Group: windStore.s3Group,
|
||||
terrainCategory: windStore.terrainCategory,
|
||||
largestDimension: windStore.largestDimension,
|
||||
heightZ: windStore.heightZ,
|
||||
} : undefined;
|
||||
|
||||
await save({
|
||||
name,
|
||||
type,
|
||||
module: moduleType,
|
||||
inputs,
|
||||
windData,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{triggerButton || (
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Save className="h-4 w-4" />
|
||||
Salvar
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Salvar Módulo</DialogTitle>
|
||||
<DialogDescription>
|
||||
Escolha como deseja salvar este módulo.
|
||||
Projetos incluem os dados de vento atuais. Templates salvam apenas a geometria da estrutura.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
variant={type === 'projeto' ? 'default' : 'outline'}
|
||||
className="h-20 flex flex-col gap-2"
|
||||
onClick={() => setType('projeto')}
|
||||
>
|
||||
<FileBox className="h-6 w-6" />
|
||||
Projeto Completo
|
||||
</Button>
|
||||
<Button
|
||||
variant={type === 'template' ? 'default' : 'outline'}
|
||||
className="h-20 flex flex-col gap-2"
|
||||
onClick={() => setType('template')}
|
||||
>
|
||||
<FileJson className="h-6 w-6" />
|
||||
Template (Geometria)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Nome do Arquivo</label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Digite um nome para identificar..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={handleSave} disabled={!name.trim()}>Salvar {type === 'projeto' ? 'Projeto' : 'Template'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -55,48 +55,52 @@ function PiperackModel({
|
||||
);
|
||||
}
|
||||
|
||||
// Viga Longitudinal da fileira
|
||||
// Viga Longitudinal da fileira (representando a face reticulada do pórtico)
|
||||
frames.push(
|
||||
<mesh key={`beam-${i}`} position={[0, elevation, z]} receiveShadow castShadow>
|
||||
<boxGeometry args={[width, height, 0.3]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
{/* Opacidade ajustada para atingir 90% quando a solidez for 1.0 */}
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={Math.max(0.1, Math.min(0.9, phiTotal * 0.9))} depthWrite={false} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
// Vigas transversais conectando as fileiras nas pontas
|
||||
if (numFrames > 1) {
|
||||
frames.push(
|
||||
<mesh key={`cross-1`} position={[-halfW, elevation, 0]} receiveShadow castShadow>
|
||||
<boxGeometry args={[0.3, height, totalSpacing]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
</mesh>
|
||||
);
|
||||
frames.push(
|
||||
<mesh key={`cross-2`} position={[halfW, elevation, 0]} receiveShadow castShadow>
|
||||
<boxGeometry args={[0.3, height, totalSpacing]} />
|
||||
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
// Gera as tubulações correndo longitudinalmente (ao longo de X)
|
||||
const pipeMeshes = pipes.map((pipe, idx) => {
|
||||
// Distribui os tubos ao longo do espaçamento transversal (Z)
|
||||
const zPos = numFrames > 1
|
||||
? -zOffset + 0.5 + (idx % Math.max(pipes.length, 1)) * (totalSpacing - 1) / Math.max(pipes.length - 1, 1) || 0
|
||||
: 0;
|
||||
// elevationOffset varia de 0 (base da estrutura reticulada) a height (topo da estrutura)
|
||||
// A base da estrutura fica em: rackTop - height, que é elevation - height/2
|
||||
const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2;
|
||||
const color = pipeColors[idx % pipeColors.length];
|
||||
// Agrupa tubos pela mesma elevação para evitar colisões e distribuí-los lado a lado (ao longo de Z)
|
||||
const pipeMeshes: any[] = [];
|
||||
const groups: Record<number, {pipe: PipeDef, idx: number}[]> = {};
|
||||
|
||||
return (
|
||||
<mesh key={`pipe-${pipe.id}`} position={[0, yPos, zPos]} rotation={[0, 0, Math.PI / 2]} receiveShadow castShadow>
|
||||
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, width + 1, 16]} />
|
||||
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
||||
</mesh>
|
||||
);
|
||||
pipes.forEach((pipe, idx) => {
|
||||
// Arredondamos a elevação para agrupar valores muito próximos
|
||||
const key = Math.round(pipe.elevationOffset * 100) / 100;
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push({pipe, idx});
|
||||
});
|
||||
|
||||
Object.values(groups).forEach(group => {
|
||||
const gap = 0.2; // 20cm de espaçamento entre tubos na mesma elevação
|
||||
const totalDiam = group.reduce((sum, item) => sum + item.pipe.diameter, 0);
|
||||
const totalGroupWidth = totalDiam + gap * (group.length - 1);
|
||||
|
||||
// Inicia a distribuição centralizada em Z=0
|
||||
let currentZ = -totalGroupWidth / 2;
|
||||
|
||||
group.forEach((item) => {
|
||||
const { pipe, idx } = item;
|
||||
const zPos = currentZ + pipe.diameter / 2;
|
||||
currentZ += pipe.diameter + gap;
|
||||
|
||||
const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2;
|
||||
const color = pipeColors[idx % pipeColors.length];
|
||||
|
||||
pipeMeshes.push(
|
||||
<mesh key={`pipe-${pipe.id}`} position={[0, yPos, zPos]} rotation={[0, 0, Math.PI / 2]} receiveShadow castShadow>
|
||||
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, width + 1, 16]} />
|
||||
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
export interface GlossaryTerm {
|
||||
id: string;
|
||||
term: string;
|
||||
definition: string;
|
||||
reference: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const nbr6123Glossary: GlossaryTerm[] = [
|
||||
{
|
||||
id: 'v0',
|
||||
term: 'Velocidade Básica do Vento (V₀)',
|
||||
definition: 'Velocidade do vento obtida a partir de dados meteorológicos. É definida como a velocidade média de 3 segundos, excedida em média uma vez em 50 anos, a 10 metros de altura, sobre terreno plano e aberto. Usada como base para todos os cálculos aerodinâmicos na NBR 6123.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.2',
|
||||
tags: ['velocidade', 'isopletas', 'parâmetro básico']
|
||||
},
|
||||
{
|
||||
id: 's1',
|
||||
term: 'Fator Topográfico (S₁)',
|
||||
definition: 'Coeficiente que considera as variações do relevo do terreno. Leva em conta se o terreno é plano, fracamente acidentado, talude ou morro, aumentando a velocidade do vento em encostas e cumes.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.3',
|
||||
tags: ['relevo', 'topografia', 'talude', 'morro']
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
term: 'Fator de Rugosidade (S₂)',
|
||||
definition: 'Coeficiente que leva em consideração a rugosidade do terreno (obstáculos como árvores e prédios) e as dimensões da edificação. Depende da categoria do terreno (I a V) e aumenta de acordo com a altura (z) da estrutura.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.4 / Tabela 3',
|
||||
tags: ['rugosidade', 'altura', 'categoria', 'dimensões']
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
term: 'Fator Estatístico (S₃)',
|
||||
definition: 'Fator de probabilidade que considera o nível de segurança e a vida útil da edificação. Valores comuns são 1,0 para edificações normais (50 anos), mas pode ser maior (ex: hospitais) ou menor (ex: instalações temporárias).',
|
||||
reference: 'NBR 6123:2023 - Seção 5.5',
|
||||
tags: ['segurança', 'probabilidade', 'uso']
|
||||
},
|
||||
{
|
||||
id: 'vk',
|
||||
term: 'Velocidade Característica (Vₖ)',
|
||||
definition: 'A velocidade do vento de projeto em uma determinada altura e situação, calculada multiplicando a velocidade básica pelos três fatores: Vₖ = V₀ × S₁ × S₂ × S₃.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.1',
|
||||
tags: ['velocidade de projeto', 'fórmula']
|
||||
},
|
||||
{
|
||||
id: 'q',
|
||||
term: 'Pressão Dinâmica (q)',
|
||||
definition: 'A energia cinética do vento transformada em pressão. É calculada a partir da velocidade característica: q = 0,613 × Vₖ² (em N/m²). Esta pressão é aplicada nas superfícies junto com os coeficientes aerodinâmicos.',
|
||||
reference: 'NBR 6123:2023 - Seção 5.6',
|
||||
tags: ['pressão', 'energia cinética']
|
||||
},
|
||||
{
|
||||
id: 'cpe',
|
||||
term: 'Coeficiente de Pressão Externa (Cpe)',
|
||||
definition: 'Valor adimensional que indica como o vento atua nas faces externas de uma edificação (paredes e telhados). Valores positivos indicam pressão (empurrando a face), valores negativos indicam sucção (puxando a face).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.2 / Tabelas 6 a 12',
|
||||
tags: ['pressão', 'sucção', 'externo', 'fachada', 'cobertura']
|
||||
},
|
||||
{
|
||||
id: 'cpi',
|
||||
term: 'Coeficiente de Pressão Interna (Cpi)',
|
||||
definition: 'Valor que determina a pressão exercida pelo ar no interior da edificação, gerada devido a aberturas e frestas (portas, janelas, ventilação). Depende do índice de permeabilidade de cada face do prédio.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.3',
|
||||
tags: ['pressão', 'interno', 'permeabilidade', 'aberturas']
|
||||
},
|
||||
{
|
||||
id: 'barlavento',
|
||||
term: 'Barlavento',
|
||||
definition: 'Região ou face da edificação por onde o vento "chega" ou incide primeiro. Normalmente as superfícies a barlavento sofrem pressões positivas (empuxo contra a parede).',
|
||||
reference: 'NBR 6123:2023 - Conceito Geral',
|
||||
tags: ['incidência', 'pressão positiva', 'vento']
|
||||
},
|
||||
{
|
||||
id: 'sotavento',
|
||||
term: 'Sotavento',
|
||||
definition: 'Região ou face da edificação que fica "escondida" ou protegida do vento incidente (a face traseira). Normalmente sofre pressões negativas (sucção), pois o vento passa e "puxa" o ar daquela região.',
|
||||
reference: 'NBR 6123:2023 - Conceito Geral',
|
||||
tags: ['traseira', 'sucção', 'esteira']
|
||||
},
|
||||
{
|
||||
id: 'ca',
|
||||
term: 'Coeficiente de Arrasto (Ca)',
|
||||
definition: 'Coeficiente de força global. Diferente do Cpe, que atua pontualmente em painéis, o Ca define a força total de arrasto gerada no volume inteiro da estrutura (ex: chaminés, torres, pórticos).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.2',
|
||||
tags: ['arrasto', 'força global', 'torres', 'cilindros']
|
||||
},
|
||||
{
|
||||
id: 'esbeltez',
|
||||
term: 'Índice de Esbeltez (λ)',
|
||||
definition: 'Relação entre a altura da estrutura e a sua largura base (ou diâmetro). Estruturas muito esbeltas (finas e altas) são mais sensíveis aos efeitos do vento e exigem coeficientes de arrasto específicos e análises dinâmicas.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1 / Diversas Tabelas',
|
||||
tags: ['altura', 'largura', 'aerodinâmica']
|
||||
},
|
||||
{
|
||||
id: 'vortex',
|
||||
term: 'Desprendimento de Vórtices (Vortex Shedding)',
|
||||
definition: 'Fenômeno dinâmico onde o vento, ao passar por uma estrutura (geralmente cilíndrica ou prismática), cria vórtices alternados nas laterais, causando oscilações transversais à direção do vento. Pode gerar ressonância estrutural perigosa.',
|
||||
reference: 'NBR 6123:2023 - Seção 10.2',
|
||||
tags: ['dinâmica', 'ressonância', 'cilindros', 'chaminés', 'vibração transversal']
|
||||
},
|
||||
{
|
||||
id: 'flutter',
|
||||
term: 'Esvoaçamento (Flutter)',
|
||||
definition: 'Instabilidade aeroelástica dinâmica onde a estrutura (ex: tabuleiro de ponte estaiada) começa a girar e vibrar de forma autossustentada e crescente devido à interação complexa entre o vento e o formato da seção.',
|
||||
reference: 'NBR 6123:2023 - Seção 11',
|
||||
tags: ['pontes', 'aeroelasticidade', 'torção', 'instabilidade']
|
||||
},
|
||||
{
|
||||
id: 'galloping',
|
||||
term: 'Galope (Galloping)',
|
||||
definition: 'Fenômeno dinâmico de instabilidade em flexão pura transversal à direção do vento. Comum em perfis não circulares ou cabos com formação de gelo/água assimétrica.',
|
||||
reference: 'NBR 6123:2023 - Seção 11.4',
|
||||
tags: ['dinâmica', 'pontes', 'cabos', 'vibração lateral']
|
||||
},
|
||||
{
|
||||
id: 'phi',
|
||||
term: 'Índice de Solidez (φ)',
|
||||
definition: 'Fator que define a "porosidade" de uma estrutura vazada (como uma torre treliçada, letreiro vazado ou pipe-rack). É a área efetiva (cheia) dos perfis dividida pela área de contorno total do painel. Aumenta o coeficiente de arrasto quanto mais "sólida" for a estrutura.',
|
||||
reference: 'NBR 6123:2023 - Seção 7',
|
||||
tags: ['treliças', 'torres', 'permeabilidade', 'bloqueio']
|
||||
},
|
||||
{
|
||||
id: 'eta',
|
||||
term: 'Fator de Proteção (η)',
|
||||
definition: 'Quando há pórticos ou treliças posicionados um atrás do outro (ex: pipe-racks ou torres retangulares), a face traseira sofre menor incidência do vento por estar "protegida" pela da frente. O fator η pondera essa redução.',
|
||||
reference: 'NBR 6123:2023 - Seção 7 / Tabela 28',
|
||||
tags: ['treliças', 'sombreamento', 'pipe-racks', 'arrasto conjunto']
|
||||
},
|
||||
{
|
||||
id: 'rugosidade',
|
||||
term: 'Categoria de Terreno',
|
||||
definition: 'A norma define 5 categorias de rugosidade: I (superfícies lisas, mar calmo), II (campos abertos), III (terrenos planos com pequenos obstáculos, fazendas), IV (áreas urbanas, subúrbios densos) e V (centros de grandes cidades).',
|
||||
reference: 'NBR 6123:2023 - Seção 5.4.1',
|
||||
tags: ['terreno', 's2', 'urbanização', 'relevo']
|
||||
},
|
||||
{
|
||||
id: 'forca_atrito',
|
||||
term: 'Força de Atrito (Ff)',
|
||||
definition: 'Força que o vento exerce tangencialmente às superfícies paralelas ao seu escoamento, devido à fricção superficial (ex: tetos muito compridos ou paredes extensas).',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.5',
|
||||
tags: ['fricção', 'tangencial', 'galpões compridos']
|
||||
},
|
||||
{
|
||||
id: 'pressao_efetiva',
|
||||
term: 'Pressão Efetiva (Δp)',
|
||||
definition: 'A diferença real de pressão que atua em uma superfície (como parede ou telhado), combinando a ação externa (Cpe) e interna (Cpi). Fórmula: Δp = q × (Cpe - Cpi). Resulta na carga real em N/m² que a estrutura vai suportar.',
|
||||
reference: 'NBR 6123:2023 - Seção 6.1.3',
|
||||
tags: ['pressão resultante', 'cálculo final']
|
||||
}
|
||||
];
|
||||
@@ -8,8 +8,10 @@
|
||||
export interface SavedProject {
|
||||
id?: number;
|
||||
name: string;
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics';
|
||||
type: 'projeto' | 'template';
|
||||
module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack';
|
||||
inputs: Record<string, unknown>;
|
||||
windData?: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -12,11 +15,32 @@ import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
|
||||
|
||||
const BridgeModule: React.FC = () => {
|
||||
const { v0, s1, terrainCategory, q } = useWindStore();
|
||||
const [lp, setLp] = React.useState(120);
|
||||
const [width, setWidth] = React.useState(14);
|
||||
const [deckHeight, setDeckHeight] = React.useState(15);
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'bridge') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.lp !== undefined) setLp(p.lp);
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.deckHeight !== undefined) setDeckHeight(p.deckHeight);
|
||||
if (p.heg !== undefined) setHeg(p.heg);
|
||||
if (p.mass !== undefined) setMass(p.mass);
|
||||
if (p.fv !== undefined) setFv(p.fv);
|
||||
if (p.vk !== undefined) setVk(p.vk);
|
||||
if (p.alpha !== undefined) setAlpha(p.alpha);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad]);
|
||||
|
||||
// Constantes normativas de flutter da seção
|
||||
const [heg, setHeg] = React.useState(2.5);
|
||||
const [mass, setMass] = React.useState(18000);
|
||||
const [fv, setFv] = React.useState(0.6);
|
||||
@@ -246,7 +270,13 @@ const BridgeModule: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="bridge"
|
||||
inputs={{ lp, width, deckHeight, heg, mass, fv, vk, alpha }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { PressureLegend } from '@/components/PressureLegend';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
|
||||
const GalpaoModule: React.FC = () => {
|
||||
const {
|
||||
@@ -33,6 +35,21 @@ const GalpaoModule: React.FC = () => {
|
||||
windAngle,
|
||||
setWindAngle,
|
||||
} = useWindStore();
|
||||
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'galpao') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.length !== undefined) setLength(p.length);
|
||||
if (p.height !== undefined) setHeight(p.height);
|
||||
if (p.roofPitch !== undefined) setRoofPitch(p.roofPitch);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad, setWidth, setLength, setHeight, setRoofPitch]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
@@ -146,7 +163,6 @@ const GalpaoModule: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
||||
<ExportMenu />
|
||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,6 +177,17 @@ const GalpaoModule: React.FC = () => {
|
||||
{/* Coluna Direita-Inferior: Cargas Lineares (M9.2) */}
|
||||
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<LinearLoadsTable />
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="galpao"
|
||||
inputs={{ width, length, height, roofPitch }}
|
||||
/>
|
||||
<ExportMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
<FtoolExportCard />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -24,6 +27,23 @@ const PiperackModule: React.FC = () => {
|
||||
const [phiStruct, setPhiStruct] = useState(0.5);
|
||||
const [pipes, setPipes] = useState<PipeDef[]>([]);
|
||||
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'piperack') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.elevation !== undefined) setElevation(p.elevation);
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.height !== undefined) setHeight(p.height);
|
||||
if (p.spacing !== undefined) setSpacing(p.spacing);
|
||||
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
|
||||
if (p.pipes !== undefined) setPipes(p.pipes);
|
||||
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad]);
|
||||
|
||||
const addPipe = () => {
|
||||
setPipes([...pipes, { id: Math.random().toString(36).substr(2, 9), diameter: 0.5, elevationOffset: height / 2 }]);
|
||||
};
|
||||
@@ -91,7 +111,7 @@ const PiperackModule: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-3.5rem)] md:h-screen bg-background">
|
||||
<header className="shrink-0 flex items-center justify-between px-6 py-4 border-b bg-card">
|
||||
<header className="shrink-0 flex items-center px-6 py-4 border-b bg-card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Layers className="w-5 h-5 text-primary" />
|
||||
@@ -101,9 +121,6 @@ const PiperackModule: React.FC = () => {
|
||||
<p className="text-sm text-muted-foreground">Vento transversal (Sec. 7 - NBR 6123)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex flex-col lg:flex-row p-4 gap-4 overflow-hidden bg-muted/30">
|
||||
@@ -403,6 +420,16 @@ const PiperackModule: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<SaveModuleDialog
|
||||
moduleType="piperack"
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
||||
/>
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+140
-109
@@ -1,27 +1,31 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, CheckCircle2, AlertCircle, FlaskConical } from 'lucide-react';
|
||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, FlaskConical, Download, FolderOpen, FileBox, FileJson } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { useProjects } from '@/lib/hooks/useProjects';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import { useI18n } from '@/store/i18nStore';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
importProjectFromText,
|
||||
readProjectFile,
|
||||
type ImportResult,
|
||||
} from '@/lib/import-project';
|
||||
import AuditPanel from '@/components/AuditPanel';
|
||||
import Glossary from '@/components/Glossary';
|
||||
|
||||
const SettingsModule: React.FC = () => {
|
||||
const { theme, setTheme, effectiveTheme } = useTheme();
|
||||
const { projects, loading, error, remove } = useProjects();
|
||||
const { projects, loading, error, remove, save } = useProjects();
|
||||
const { t, locale } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setPendingLoad = useHydrationStore((s) => s.setPendingLoad);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const exportSnapshot = () => {
|
||||
@@ -30,12 +34,36 @@ const SettingsModule: React.FC = () => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'ventoapp-state.json');
|
||||
link.setAttribute('download', 'ventoapp-snapshot.json');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleDownloadJSON = (project: any) => {
|
||||
const blob = new Blob([JSON.stringify(project, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${project.name}.json`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleLoadProject = (project: any) => {
|
||||
if (project.type === 'projeto' && project.windData) {
|
||||
// Restore wind data
|
||||
useWindStore.setState(project.windData);
|
||||
}
|
||||
|
||||
// Set pending load
|
||||
setPendingLoad(project.module, project.inputs);
|
||||
|
||||
// Navigate to module
|
||||
navigate(`/${project.module === 'isolated-roof' ? 'cobertura-isolada' : project.module === 'sign' ? 'muros' : project.module === 'bar' ? 'barras' : project.module === 'bridge' ? 'pontes' : project.module === 'dynamics' ? 'dinamica' : project.module === 'vault' ? 'abobada' : project.module === 'dome' ? 'cupula' : project.module}`);
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
@@ -44,29 +72,51 @@ const SettingsModule: React.FC = () => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
try {
|
||||
const text = await readProjectFile(file);
|
||||
const result = importProjectFromText(text);
|
||||
setImportResult(result);
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed.module && parsed.inputs) {
|
||||
// Formato SavedProject (Projeto ou Template)
|
||||
const newProject = {
|
||||
...parsed,
|
||||
name: parsed.name || 'Importado - ' + file.name,
|
||||
type: parsed.type || 'projeto',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
// Ensure type is correct
|
||||
if (!['projeto', 'template'].includes(newProject.type)) newProject.type = 'projeto';
|
||||
|
||||
await save(newProject as any);
|
||||
} else {
|
||||
// Old snapshot import fallback
|
||||
importProjectFromText(text);
|
||||
}
|
||||
} catch (e) {
|
||||
setImportResult({
|
||||
ok: false,
|
||||
error: e instanceof Error ? e.message : 'Falha ao ler arquivo',
|
||||
});
|
||||
console.error(e);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const currentTab = new URLSearchParams(location.search).get('tab') || 'config';
|
||||
const isSettingsGroup = currentTab === 'config' || currentTab === 'testes';
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto overflow-auto">
|
||||
<h1 className="text-3xl font-bold mb-6">{t('nav_settings')}</h1>
|
||||
<h1 className="text-3xl font-bold mb-6 md:hidden">Configurações</h1>
|
||||
<h1 className="text-3xl font-bold mb-6 hidden md:block">
|
||||
{currentTab === 'gerenciador' ? 'Arquivos' :
|
||||
currentTab === 'glossary' ? 'Glossário' :
|
||||
'Configurações'}
|
||||
</h1>
|
||||
|
||||
<Tabs defaultValue="config" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="config">Configurações</TabsTrigger>
|
||||
<Tabs value={currentTab} onValueChange={(val) => navigate(`/settings?tab=${val}`)} className="w-full">
|
||||
<TabsList className={`grid w-full mb-6 ${isSettingsGroup ? 'grid-cols-4 md:grid-cols-2' : 'grid-cols-4 md:hidden'}`}>
|
||||
<TabsTrigger value="config">Preferências</TabsTrigger>
|
||||
<TabsTrigger value="gerenciador" className="md:hidden">Arquivos</TabsTrigger>
|
||||
<TabsTrigger value="glossary" className="text-red-500 uppercase font-bold tracking-wider md:hidden">GLOSSÁRIO</TabsTrigger>
|
||||
<TabsTrigger value="testes">
|
||||
<FlaskConical className="w-4 h-4 mr-1.5" />
|
||||
Testes
|
||||
@@ -98,99 +148,6 @@ const SettingsModule: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_projects')}</CardTitle>
|
||||
<CardDescription>{t('settings_projects_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-3 gap-2 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings_projects_count', { count: projects.length })}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-1.5 w-full sm:w-auto sm:flex">
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={handleImportClick} disabled={importing}>
|
||||
<Upload className="w-3.5 h-3.5 mr-1.5 shrink-0" /> {importing ? t('settings_importing') : t('export_import')}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileSelected}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={exportSnapshot}>
|
||||
<Save className="w-3.5 h-3.5 mr-1.5 shrink-0" /> {t('export_snapshot')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importResult && (
|
||||
<div
|
||||
className={`rounded-md border p-3 mb-3 text-sm flex items-start gap-2 ${
|
||||
importResult.ok
|
||||
? 'bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950 dark:border-emerald-800 dark:text-emerald-300'
|
||||
: 'bg-red-50 border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300'
|
||||
}`}
|
||||
>
|
||||
{importResult.ok ? (
|
||||
<CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium">
|
||||
{importResult.ok ? t('settings_import_success') : t('settings_import_error')}
|
||||
</div>
|
||||
{importResult.module && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_module')}: <Badge variant="outline">{importResult.module}</Badge>
|
||||
{importResult.projectName && (
|
||||
<span className="ml-2">{t('settings_import_project')}: {importResult.projectName}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{importResult.appliedFields && importResult.appliedFields.length > 0 && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_fields', { count: importResult.appliedFields.length })}: {importResult.appliedFields.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.warnings && importResult.warnings.length > 0 && (
|
||||
<div className="text-xs mt-1 opacity-80">
|
||||
{t('settings_import_warnings')}: {importResult.warnings.join('; ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.error && (
|
||||
<div className="text-xs mt-1">{importResult.error}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">{t('common_loading')}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{projects.length === 0 && !loading && (
|
||||
<p className="text-sm text-muted-foreground">{t('settings_no_projects')}</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{projects.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-2 border rounded-md hover:bg-muted/40">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{p.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{p.module} · {new Date(p.updatedAt).toLocaleString(locale === 'pt-BR' ? 'pt-BR' : 'en-US')}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={() => p.id && remove(p.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_about')}</CardTitle>
|
||||
@@ -210,10 +167,84 @@ const SettingsModule: React.FC = () => {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="gerenciador" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Arquivos (Gerenciador)</CardTitle>
|
||||
<CardDescription>Gerencie seus Projetos Completos e Templates de geometria salvos localmente.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-4 gap-2 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{projects.length} arquivo(s) armazenado(s).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-1.5 w-full sm:w-auto sm:flex">
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={handleImportClick} disabled={importing}>
|
||||
<Upload className="w-3.5 h-3.5 mr-1.5 shrink-0" /> Importar Arquivo
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="text-xs px-2 h-8 flex-1 sm:flex-none" onClick={exportSnapshot}>
|
||||
<Save className="w-3.5 h-3.5 mr-1.5 shrink-0" /> Exportar Wind Snapshot
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">{t('common_loading')}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{projects.length === 0 && !loading && (
|
||||
<p className="text-sm text-muted-foreground">Nenhum projeto ou template salvo ainda.</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{projects.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-3 border rounded-md hover:bg-muted/40 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{p.type === 'projeto' ? (
|
||||
<div className="p-2 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md">
|
||||
<FileBox className="w-5 h-5" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 rounded-md">
|
||||
<FileJson className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium text-sm flex items-center gap-2">
|
||||
{p.name}
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 h-4">
|
||||
{p.module}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
{p.type === 'projeto' ? 'Projeto (Vento + Geometria)' : 'Template (Apenas Geometria)'} · {new Date(p.updatedAt).toLocaleString(locale === 'pt-BR' ? 'pt-BR' : 'en-US')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost" title="Carregar" className="h-8 w-8 p-0 text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-950/50" onClick={() => handleLoadProject(p)}>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" title="Baixar JSON" className="h-8 w-8 p-0" onClick={() => handleDownloadJSON(p)}>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" title="Excluir" className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/50" onClick={() => p.id && remove(p.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="glossary" className="space-y-6">
|
||||
<Glossary />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="testes" className="space-y-6">
|
||||
<AuditPanel />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileSelected} className="hidden" accept=".json" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface HydrationState {
|
||||
pendingLoad: {
|
||||
module: string;
|
||||
inputs: Record<string, unknown>;
|
||||
} | null;
|
||||
setPendingLoad: (module: string, inputs: Record<string, unknown>) => void;
|
||||
clearPendingLoad: () => void;
|
||||
}
|
||||
|
||||
export const useHydrationStore = create<HydrationState>((set) => ({
|
||||
pendingLoad: null,
|
||||
setPendingLoad: (module, inputs) => set({ pendingLoad: { module, inputs } }),
|
||||
clearPendingLoad: () => set({ pendingLoad: null }),
|
||||
}));
|
||||
Reference in New Issue
Block a user