# 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 ```prisma 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 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 Crie o arquivo `backend/.env` com as variáveis abaixo: ```env DB_USER=postgres DB_PASSWORD=postgres DB_HOST=localhost DB_PORT=5432 DB_NAME=toptran DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toptran" PORT=4000 JWT_SECRET=sua_chave_secreta_aqui ``` > `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 ```bash cd backend # Instalar dependências npm install # Gerar cliente Prisma npx prisma generate # Rodar migrations npx prisma migrate deploy # Iniciar em modo desenvolvimento npm run dev # Build de produção npm run build npm start ``` ### API Reference #### Autenticação — `/auth` > Rotas públicas, não requerem token. **POST `/auth/register`** Cria uma nova conta de usuário. ```json // 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. ```json // Request body { "email": "joao@email.com", "password": "minhasenha" } // Response 200 { "accessToken": "eyJ...", "refreshToken": "uuid" } ``` --- **POST `/auth/logout`** Revoga o refresh token. ```json // Request body { "refreshToken": "uuid" } // Response 204 (sem body) ``` --- #### Usuários — `/users` > Rotas protegidas. Enviar o header `Authorization: Bearer `. **GET `/users/me`** Retorna os dados básicos do usuário autenticado. ```json // 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. ```json // 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. ```json // 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. ```json // 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 `. **GET `/sync/companies`** Retorna todas as empresas cadastradas no servidor. ```json // 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. ```json // 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. ```json // Response 200 { "success": true, "data": [ { "id": "uuid", "user_id": "uuid", "company": "Empresa X", "km": "12.5", "total": "31.25", ... } ] } ``` --- **POST `/sync/rides`** Sincroniza (upsert) uma lista de corridas no servidor. ```json // 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" } ] } // Response 200 { "success": true, "message": "1 ride(s) synced", "data": [...] } ``` --- #### 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 ```bash 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` ### Execução ```bash cd toptran-app # Instalar dependências npm install # Iniciar o servidor Expo 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 ```bash adb tcpip 5555 ``` 3. No terminal local, crie o túnel SSH reverso: ```bash ssh -R 5555:localhost:5555 dev@175.15.15.93 ``` 4. Conecte o ADB ao dispositivo tunelado: ```bash adb connect localhost:5555 ```