No description
Find a file
2026-05-05 12:53:41 -03:00
.claude Acert script on database 2026-05-05 12:53:41 -03:00
backend Acert script on database 2026-05-05 12:53:41 -03:00
toptran-app Acert script on database 2026-05-05 12:53:41 -03:00
.gitignore Adjusts in git config 2026-04-27 19:29:21 -03:00
proposta-toptran.html Acert script on database 2026-05-05 12:53:41 -03:00
proposta-toptran.pdf Acert script on database 2026-05-05 12:53:41 -03:00
README.md Acert script on database 2026-05-05 12:53:41 -03:00

Toptran

Aplicação full-stack composta por um backend REST em Node.js/Express com autenticação JWT e um aplicativo mobile em React Native/Expo, voltado para o gerenciamento de corridas de trabalhadores do transporte.


Estrutura do projeto

toptran/
├── backend/        → API REST (Node.js, Express, Prisma, PostgreSQL)
└── toptran-app/    → App mobile (React Native, Expo)

Backend

Tecnologias

Pacote Versão Função
Node.js LTS Runtime
Express 5 Framework HTTP
TypeScript 5 Tipagem
Prisma 7 ORM
PostgreSQL Banco de dados
bcryptjs Hash de senha
jsonwebtoken Access token (JWT)
zod Validação de input
@prisma/adapter-pg Driver nativo do Prisma 7

Arquitetura em camadas

src/
├── server.ts                        → Inicialização do Express e registro das rotas
├── lib/
│   └── prisma.ts                    → Singleton do PrismaClient com adapter PG
├── repositories/
│   ├── users.repository.ts          → Queries de usuários no Prisma
│   ├── tokens.repository.ts         → Queries de tokens no Prisma
│   ├── companies.repository.ts      → Queries de empresas no Prisma
│   └── rides.repository.ts          → Queries de corridas no Prisma
├── services/
│   ├── auth.service.ts              → Lógica de registro, login, logout e JWT
│   ├── users.service.ts             → Lógica de leitura, atualização e remoção de usuários
│   └── sync.service.ts              → Lógica de sincronização de empresas e corridas
├── controllers/
│   ├── auth.controller.ts           → Recebe requests e chama auth.service
│   ├── users.controller.ts          → Recebe requests e chama users.service
│   └── sync.controller.ts           → Recebe requests e chama sync.service
├── middlewares/
│   └── auth.middleware.ts           → Valida o Bearer token nas rotas protegidas
└── routes/
    ├── auth.routes.ts               → Rotas públicas de autenticação
    ├── users.routes.ts              → Rotas protegidas de usuário
    └── sync.routes.ts               → Rotas protegidas de sincronização

Fluxo de uma requisição:

Request → routes → middleware (se protegida) → controller → service → repository → Prisma → PostgreSQL

Modelos do banco de dados

model users {
  id           String   @id
  name         String
  email        String   @unique
  password     String
  profilePhoto String?  @default("")
  bio          String?  @default("")
  createdAt    DateTime @default(now())
  updatedAt    DateTime
  rides        rides[]
  tokens       tokens[]
}

model tokens {
  id        String    @id
  token     String    @unique
  type      TokenType
  userId    String
  expiresAt DateTime
  createdAt DateTime  @default(now())
  users     users     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model companies {
  id          String    @id
  name        String
  cost_per_km Decimal   @db.Decimal(10, 2)
  notes       String?   @default("")
  createdAt   DateTime? @default(now()) @db.Timestamptz(6)
  updatedAt   DateTime? @default(now()) @db.Timestamptz(6)
}

model rides {
  id             String    @id
  user_id        String
  company        String
  km             Decimal   @db.Decimal(10, 2)
  cost_per_km    Decimal   @db.Decimal(10, 2)
  total          Decimal   @db.Decimal(10, 2)
  ride_date      String
  departures     Int?      @default(0)
  failed_service Int?      @default(0)
  idle_hours     Decimal?  @default(0) @db.Decimal(10, 2)
  synced         Int?      @default(0) @db.SmallInt
  createdAt      DateTime? @default(now()) @db.Timestamptz(6)
  updatedAt      DateTime? @default(now()) @db.Timestamptz(6)
  users          users     @relation(fields: [user_id], references: [id], onDelete: Cascade)
}

enum TokenType {
  REFRESH
}

Variáveis de ambiente

O backend trabalha com dois bancos — um de homologação (toptrandev) e um de produção (toptranprod) — e dois arquivos .env separados:

Arquivo Banco Porta
backend/.env.development toptrandev 4000
backend/.env.production toptranprod 5000

Modelo do .env.development:

DATABASE_URL=postgresql://USER:PASS@HOST:5432/toptrandev
DB_USER=...
DB_PASSWORD=...
DB_HOST=...
DB_PORT=5432
DB_NAME=toptrandev

PORT=4000
NODE_ENV=development

JWT_SECRET=...
JWT_REFRESH_SECRET=...

Modelo do .env.production: idêntico, trocando DB_NAME=toptranprod, PORT=5000 e NODE_ENV=production.

JWT_SECRET é obrigatório em produção. O access token expira em 15 minutos e o refresh token em 7 dias.

Instalação e execução

cd backend

# Instalar dependências
npm install

# Gerar cliente Prisma
npx prisma generate

# Rodar a API apontando pro banco de homologação (toptrandev)
npm run dev

# Rodar a API apontando pro banco de produção (toptranprod)
npm run dev:prod

# Build + start em produção
npm run build
npm start            # usa .env.production por padrão
npm run start:dev    # caso queira o build rodando contra o banco de dev

Migrations e Prisma CLI por ambiente (via dotenv-cli):

# Homologação
npm run prisma:dev migrate dev
npm run prisma:dev studio

# Produção
npm run prisma:prod migrate deploy
npm run prisma:prod studio

API Reference

Autenticação — /auth

Rotas públicas, não requerem token.

POST /auth/register

Cria uma nova conta de usuário.

// Request body
{
  "name": "João Silva",
  "email": "joao@email.com",
  "password": "minhasenha"
}

// Response 201
{
  "id": "uuid",
  "name": "João Silva",
  "email": "joao@email.com"
}

POST /auth/login

Autentica o usuário e retorna os tokens.

// Request body
{
  "email": "joao@email.com",
  "password": "minhasenha"
}

// Response 200
{
  "accessToken": "eyJ...",
  "refreshToken": "uuid"
}

POST /auth/logout

Revoga o refresh token.

// Request body
{
  "refreshToken": "uuid"
}

// Response 204 (sem body)

Usuários — /users

Rotas protegidas. Enviar o header Authorization: Bearer <accessToken>.

GET /users/me

Retorna os dados básicos do usuário autenticado.

// Response 200
{
  "id": "uuid",
  "name": "João Silva",
  "email": "joao@email.com",
  "createdAt": "2026-04-27T00:00:00.000Z"
}

GET /users/profile

Retorna o perfil completo do usuário, incluindo foto e bio.

// Response 200
{
  "id": "uuid",
  "name": "João Silva",
  "email": "joao@email.com",
  "profilePhoto": "base64_ou_url",
  "bio": "Motorista há 5 anos.",
  "createdAt": "2026-04-27T00:00:00.000Z"
}

PUT /users/me

Atualiza nome, e-mail e/ou senha. Todos os campos são opcionais.

// Request body
{
  "name": "João Atualizado",
  "email": "novo@email.com",
  "password": "novasenha"
}

// Response 200
{
  "id": "uuid",
  "name": "João Atualizado",
  "email": "novo@email.com"
}

PATCH /users/profile

Atualiza foto de perfil e/ou bio. Todos os campos são opcionais.

// Request body
{
  "profilePhoto": "base64_ou_url",
  "bio": "Nova bio aqui."
}

// Response 200
{
  "id": "uuid",
  "name": "João Silva",
  "profilePhoto": "base64_ou_url",
  "bio": "Nova bio aqui."
}

DELETE /users/me

Remove a conta do usuário autenticado.

// Response 204 (sem body)

Sincronização — /sync

Rotas protegidas. Enviar o header Authorization: Bearer <accessToken>.

GET /sync/companies

Retorna todas as empresas cadastradas no servidor.

// Response 200
{
  "success": true,
  "data": [
    { "id": "uuid", "name": "Empresa X", "cost_per_km": "2.50", "notes": "" }
  ]
}

POST /sync/companies

Sincroniza (upsert) uma lista de empresas no servidor.

// Request body
{
  "companies": [
    { "id": "uuid", "name": "Empresa X", "cost_per_km": 2.50, "notes": "" }
  ]
}

// Response 200
{
  "success": true,
  "message": "1 company/companies synced",
  "data": [...]
}

GET /sync/rides

Retorna todas as corridas cadastradas no servidor.

// Response 200
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "company": "Empresa X",
      "km": "12.5",
      "cost_per_km": "2.50",
      "total": "31.25",
      "ride_date": "2026-05-01",
      "departures": 3,
      "failed_service": 1,
      "idle_hours": "1.50",
      "synced": 1
    }
  ]
}

POST /sync/rides

Sincroniza (upsert) uma lista de corridas no servidor. Os campos departures, failed_service e idle_hours são opcionais e assumem 0 por padrão.

// Request body
{
  "rides": [
    {
      "id": "uuid",
      "user_id": "uuid",
      "company": "Empresa X",
      "km": 12.5,
      "cost_per_km": 2.50,
      "total": 31.25,
      "ride_date": "2026-05-01",
      "departures": 3,
      "failed_service": 1,
      "idle_hours": 1.5
    }
  ]
}

// Response 200
{
  "success": true,
  "message": "1 ride(s) synced",
  "data": [...]
}

Campos da corrida:

Campo Tipo Descrição
id string Identificador único da corrida
user_id string ID do usuário dono da corrida
company string Nome da empresa
km number|string Quilômetros rodados
cost_per_km number|string Custo por km no momento da corrida
total number|string Valor total da corrida
ride_date string Data da corrida (formato YYYY-MM-DD)
departures number? Quantidade de partidas/saídas (default 0)
failed_service number? Quantidade de serviços não realizados (default 0)
idle_hours number|string? Horas ociosas, com 2 casas decimais (default 0)
synced number? Flag de sincronização (0 ou 1)

Respostas de erro

Status Situação
400 Input inválido (falha na validação Zod)
401 Token ausente, inválido ou expirado
404 Recurso não encontrado
409 E-mail já cadastrado
500 Erro interno no servidor

Deploy com Docker / Podman

cd backend

# Build da imagem
podman build -t top-tran-backend .

# Subir com compose
podman-compose up -d

O container expõe a porta 3000 e o serviço é reiniciado automaticamente em caso de falha.


Mobile

Tecnologias

Pacote Versão Função
React Native 0.83.6 Framework mobile
Expo 55 Plataforma e toolchain
Expo Router 55 Navegação baseada em arquivos
Expo SQLite 55 Banco de dados local (offline-first)
Expo Secure Store 55 Armazenamento seguro de tokens
Expo Image Picker 55 Seleção de foto de perfil
Expo Print 55 Geração de PDF
Expo Sharing 55 Compartilhamento de arquivos
Axios 1 Cliente HTTP
TypeScript 5 Tipagem

Estrutura

src/
├── app/
│   ├── _layout.tsx          → Layout raiz: inicializa DB e provedor de auth
│   ├── index.tsx            → Tela de login
│   ├── signup.tsx           → Tela de cadastro
│   ├── home.tsx             → Tela inicial com resumo de corridas
│   ├── lancamento.tsx       → Lançamento de nova corrida
│   ├── historico.tsx        → Histórico de corridas
│   ├── empresas.tsx         → Gerenciamento de empresas (CRUD local)
│   ├── relatorio.tsx        → Relatório mensal com exportação em PDF
│   ├── perfil.tsx           → Edição de perfil (nome, foto, bio)
│   ├── sincronizar.tsx      → Sincronização com o servidor (timeline visual)
│   ├── cadastros.tsx        → Hub de cadastros
│   └── corrida.tsx          → Rota de corrida (redirect)
├── components/
│   ├── Button.tsx           → Botão reutilizável
│   ├── Header.tsx           → Cabeçalho de tela
│   ├── Input.tsx            → Campo de texto reutilizável
│   └── Select.tsx           → Seletor (Picker) reutilizável
├── constants/
│   └── theme.ts             → Cores, espaçamentos e border-radius globais
├── contexts/
│   └── AuthContext.tsx      → Estado de autenticação global (JWT + usuário)
├── server/
│   └── api.ts               → Instância do Axios configurada para o backend
├── services/
│   └── db.ts                → Serviço SQLite: tabelas, CRUD de corridas, empresas e configurações
└── utils/
    └── jwt.ts               → Utilitário para decode do JWT

Telas

Arquivo Rota Descrição
src/app/index.tsx / Login
src/app/signup.tsx /signup Cadastro de conta
src/app/home.tsx /home Dashboard com resumo do dia
src/app/lancamento.tsx /lancamento Lançar nova corrida
src/app/historico.tsx /historico Histórico completo de corridas
src/app/empresas.tsx /empresas CRUD de empresas locais
src/app/relatorio.tsx /relatorio Relatório mensal + exportar PDF
src/app/perfil.tsx /perfil Edição de nome, foto e bio
src/app/sincronizar.tsx /sincronizar Sincronização com o servidor
src/app/cadastros.tsx /cadastros Hub de cadastros

Banco de dados local (SQLite)

O app usa expo-sqlite para persistência offline. Ao iniciar, o _layout.tsx chama initDB() que cria as seguintes tabelas:

Tabela Descrição
users Dados do usuário logado e token de acesso
rides Corridas com flag synced (0 = pendente, 1 = sincronizado)
companies Empresas cadastradas localmente
settings Configurações chave-valor (ex: foto de perfil, bio)

Sincronização

A tela /sincronizar executa três etapas em sequência com feedback visual em timeline:

  1. Upload de empresas — envia empresas locais para o servidor via POST /sync/companies
  2. Download de empresas — baixa empresas do servidor se o cadastro local estiver vazio
  3. Upload de corridas — compara IDs locais com os do servidor e envia apenas as corridas ausentes via POST /sync/rides

Ambientes (dev / prod)

O app não conversa direto com o PostgreSQL — ele consome a API do backend. Alternar de banco no app significa apontar pra um backend diferente via EXPO_PUBLIC_API_URL.

Arquivo API Banco
toptran-app/.env.development http://175.15.15.93:4000/api toptrandev
toptran-app/.env.production https://toptran.olymp.com.br/api toptranprod

O Expo carrega o arquivo certo automaticamente conforme o modo:

Comando Arquivo carregado
npx expo start (dev) .env.development
npx expo export / EAS Build production .env.production

Variáveis EXPO_PUBLIC_* são inlined no bundle em build time. Reinicie sempre com npx expo start -c após trocar o .env.

Perfis EAS

Configurados em toptran-app/eas.json:

Perfil Formato API/Banco Comando Pra quê serve
development APK + dev client dev (:4000toptrandev) eas build --profile development Rodar com expo start plugado, hot reload
preview APK standalone prod (toptranprod) eas build --profile preview APK pra testar/distribuir fora da Play
production AAB prod (toptranprod) eas build --profile production Bundle pra Google Play (eas submit)

Execução

cd toptran-app

# Instalar dependências
npm install

# Iniciar o servidor Expo (carrega .env.development → backend dev)
npm start

# Build e abrir no Android
npm run android

# Build e abrir no iOS
npm run ios

Conectar ao dispositivo Android via AVD na VM

  1. Ligue o dispositivo Android (AVD)
  2. Rodar o comando
    adb tcpip 5555
    
  3. No terminal local, crie o túnel SSH reverso:
    ssh -R 5555:localhost:5555 dev@175.15.15.93
    
  4. Conecte o ADB ao dispositivo tunelado:
    adb connect localhost:5555