Skip to content

Repository files navigation

Estética Automotiva

Micro-SaaS para estúdio de estética automotiva. O problema que ele resolve é o cliente que volta dizendo que o risco no para-lama não estava lá quando deixou o carro.

O operador recebe o veículo no pátio, registra cada dano tocando num mapa do carro na tela do celular e tira as fotos. O sistema gera um laudo com link público, que vai para o cliente por WhatsApp. O cliente abre, confere e aceita; nesse momento o laudo é congelado, com nome, IP e horário do aceite gravados. A ordem de serviço só entra na fila de trabalho depois disso.

O laudo que o cliente recebe

Cada pino é um dano registrado pelo operador no pátio. O mapa é o mesmo no celular dele e aqui, porque a posição é guardada relativa ao desenho, não em pixel.

Mapa de danos do laudo público Lista de danos e aceite do cliente

No aceite, à direita, o laudo grava nome, data, hora e IP, e o checklist passa a ser somente leitura. É o que dá valor ao documento depois.

O painel do dono

Dashboard

Faturamento do mês e do dia, O.S. ativas, giro de carros, insumos em alerta, receita por semana e os serviços mais vendidos.

Kanban de ordens de serviço

Kanban de O.S.

As sete fases do ciclo. O arraste entre colunas só aceita as transições válidas. Cada card mostra o tempo parado na fase, o operador responsável e se o laudo já foi aceito.

Estoque e caixa

Estoque de insumos Fluxo de caixa

Insumo abaixo do mínimo fica vermelho e alimenta o card de alerta do painel. No caixa, a receita de cada O.S. concluída entra sozinha; as despesas são lançadas à mão.


Dois usuários, dois dispositivos

O app do operador e o painel do dono não são a mesma interface em tamanhos diferentes. Os contextos de uso são bem distintos:

Operador (apps/mobile) Dono (apps/web)
onde no pátio, de pé, com o carro na frente no escritório, desktop
como toque, câmera, uma mão mouse, teclado, tela grande
faz check-in, mapa de danos, fotos, check-out Kanban, preço, estoque, caixa, configuração
rede Wi-Fi ruim de galpão, funciona offline conexão normal

O fluxo

  ┌── operador, no celular ──┐  ┌─ cliente ─┐  ┌──────── estúdio ────────┐

EM_CHECKIN ─▶ AGUARDANDO_APROVACAO ─▶ APROVADO ─▶ EM_ANDAMENTO ─▶ INSPECAO ─▶ CONCLUIDO ─▶ ENTREGUE
    │                   │                 ▲
    │                   └─ link público ──┘
    │                      do laudo          (o aceite congela o checklist)
    └── fotos + pins de dano no mapa do carro

O portão de aprovação no meio é o ponto do produto: o trabalho não começa antes do cliente dar ciência do estado de entrada.

Estúdio que não quer esse rigor liga o modo FLEXIVEL e o operador aprova direto — cliente recorrente, pátio corrido. É configuração por estúdio, não fork do código.


Detalhes de implementação

Fila de sincronização offline com idempotência

O operador não pode parar de trabalhar porque a rede caiu. Cada dano registrado vai para uma fila no AsyncStorage e a interface responde na hora, de forma otimista; quando a conexão volta, a fila é drenada.

A parte que importa é a classificação do erro, porque retentar tudo indefinidamente é como se perde dado:

/// Erro permanente = 4xx (cliente/validação), exceto 408/429 (transitórios).
/// Ex.: 409 = laudo já aprovado (checklist travado) → nunca vai dar certo.
function erroPermanente(e: unknown): boolean {
  const s = statusDoErro(e);
  return s !== null && s >= 400 && s < 500 && s !== 408 && s !== 429;
}
  • erro de rede ou 5xx: fica na fila e retenta, até 5 tentativas;
  • 4xx: descarta, porque não vai passar numa próxima tentativa e um item assim travaria a fila inteira atrás dele;
  • 408 e 429 são 4xx, mas são transitórios, então voltam para a fila.

Cada item leva um clientId gerado no celular, então o reenvio não duplica o dano no servidor. A foto vai embutida no mesmo payload de propósito: dano e foto sobem numa requisição só, e assim não existe o estado intermediário de dano salvo sem foto.

O mapa de danos guarda coordenada relativa

O pino é salvo como posX/posY entre 0 e 1, relativo ao SVG do carro (react-native-svg), e não em pixel. Assim o mesmo dano cai no lugar certo no celular do operador, no laudo que o cliente abre no navegador e num tablet, sem recalcular nada.

O dano também guarda registradoEm, que é a hora em que o operador tocou na tela, e não a hora em que o servidor recebeu. Num documento que serve como prova isso muda o conteúdo: o carro entrou às 8h04, não às 11h20 de quando o Wi-Fi voltou.

Multi-tenant desde o schema

Todo dado de negócio é particionado por empresaId, e a API filtra por empresa do usuário logado nas consultas, não no frontend. O modelo Empresa é a raiz do tenant e guarda os módulos ativos: um estúdio que só quer o laudo desliga estoque e financeiro (usaEstoque, usaFinanceiro). O plano (FREE/PRO/PREMIUM) também fica ali.

Autorização é @Auth(role) + RolesGuard sobre JWT (Passport): DONO vê o painel, OPERADOR vê o pátio.

Upload que não passa pela API

Foto de celular tem vários MB, e passar isso pelo servidor gasta banda e memória do container sem necessidade. O mobile comprime a imagem, pede uma URL pré-assinada ao endpoint de storage e sobe direto para o bucket (S3/Supabase). A API guarda apenas a URL.

O laudo público é imutável depois do aceite

RelatorioPublico é 1:1 com a O.S., acessível por hash sem login, e grava aceito, aceitoEm, aceitoNome e aceitoIp. Depois do aceite o checklist passa a rejeitar alteração, e é o 409 dessa trava que a fila offline descarta em vez de retentar. Um laudo que pode ser editado depois de aceito não serviria para nada.


Domínio

Empresa (tenant) ──< Usuario (DONO | OPERADOR)
   │
   ├──< Cliente ──< Veiculo ──< OrdemServico ──< Servico           (o que foi vendido)
   │                               │         ├──< ChecklistDano    (pins no mapa)
   │                               │         ├──< FotoOS           (5 ângulos, entrada e saída)
   │                               │         └──── RelatorioPublico (1:1, o laudo)
   │
   ├──< Insumo ──< MovimentoEstoque     (auditoria de cada entrada/saída)
   ├──< ServicoCatalogo                 (preço cadastrado uma vez, reutilizado)
   └──< LancamentoFinanceiro            (caixa; receita vinculada à O.S. de origem)

Estoque guarda o movimento com motivo, e não só o saldo, para dar como auditar uma divergência depois.

Dinheiro é Decimal(10,2) no Postgres, nunca float.


Rodando

Node ≥ 20, pnpm ≥ 9, PostgreSQL, e Expo Go (ou simulador) pro mobile.

pnpm install
cp .env.example .env          # ajuste DATABASE_URL, DIRECT_URL, JWT_SECRET, STORAGE_*

pnpm db:generate              # gera o Prisma Client
pnpm db:migrate               # cria as tabelas
pnpm db:seed                  # dados de demonstração

O seed popula um mês de operação de um estúdio: 28 O.S. distribuídas por todas as colunas do Kanban, danos marcados sobre o mapa do carro com coordenada e hora de pátio, laudo já aceito num caso e aguardando aceite noutro, 12 insumos com histórico de movimento e três abaixo do mínimo, e caixa com receita e despesa espalhadas pelas semanas. A semente do gerador é fixa, então rodar duas vezes dá o mesmo resultado.

⚠️ O seed apaga os dados de negócio antes de popular. É para banco de desenvolvimento.

E se você já estava logado: saia e entre de novo. O seed recria a empresa com um id novo, e como a API filtra tudo por empresaId do usuário do token, uma sessão anterior passa a apontar para um tenant que não existe mais — as telas ficam zeradas mesmo com o banco cheio. É o isolamento multi-tenant funcionando, não um bug.

pnpm dev                      # api + web + mobile via Turborepo

Ou separado:

pnpm --filter api dev         # NestJS  → localhost:3333  (Swagger em /docs)
pnpm --filter web dev         # Next.js → localhost:3000
pnpm --filter mobile dev      # Expo    → QR no Expo Go

Em celular físico, localhost não resolve: troque por IP da máquina em NEXT_PUBLIC_API_URL / EXPO_PUBLIC_API_URL.

Contas do seed

Papel Email Senha Onde
Dono dono@studio.com 123456 web
Operador operador@studio.com 123456 mobile

O seed grava o hash com bcryptjs, o mesmo algoritmo da API, então o login funciona direto.


Documentação

A regra de negócio fica só na API. Web e mobile são clientes dela e nunca falam direto com o banco; o contrato entre os três é o pacote @repo/types (enums, labels e schemas Zod), e o schema Prisma em packages/database é a única fonte de verdade do banco.


Stack

TypeScript · Turborepo · pnpm workspaces · NestJS (REST + OpenAPI) · Next.js App Router · React Native / Expo · Prisma · PostgreSQL · JWT + Passport · Zod · TailwindCSS · @dnd-kit · react-native-svg · S3/Supabase Storage com URL pré-assinada

About

Micro-SaaS para estúdio de estética automotiva: checklist visual de danos no celular com sincronização offline, laudo público com aceite do cliente e gestão de O.S.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages