2026-04-27 20:33:09 -03:00
# Toptran
2026-04-27 17:26:53 -03:00
2026-05-04 19:50:31 -03:00
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.
2026-04-27 17:26:53 -03:00
2026-04-27 20:33:09 -03:00
---
2026-04-27 17:26:53 -03:00
2026-04-27 20:33:09 -03:00
## Estrutura do projeto
2026-04-27 17:26:53 -03:00
2026-04-27 20:33:09 -03:00
```
toptran/
├── backend/ → API REST (Node.js, Express, Prisma, PostgreSQL)
2026-05-04 19:50:31 -03:00
└── toptran-app/ → App mobile (React Native, Expo)
2026-04-27 20:33:09 -03:00
```
---
## 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/
2026-05-04 19:50:31 -03:00
├── server.ts → Inicialização do Express e registro das rotas
2026-04-27 20:33:09 -03:00
├── lib/
2026-05-04 19:50:31 -03:00
│ └── prisma.ts → Singleton do PrismaClient com adapter PG
2026-04-27 20:33:09 -03:00
├── repositories/
2026-05-04 19:50:31 -03:00
│ ├── 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
2026-04-27 20:33:09 -03:00
├── services/
2026-05-04 19:50:31 -03:00
│ ├── 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
2026-04-27 20:33:09 -03:00
├── controllers/
2026-05-04 19:50:31 -03:00
│ ├── 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
2026-04-27 20:33:09 -03:00
├── middlewares/
2026-05-04 19:50:31 -03:00
│ └── auth.middleware.ts → Valida o Bearer token nas rotas protegidas
2026-04-27 20:33:09 -03:00
└── routes/
2026-05-04 19:50:31 -03:00
├── 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
2026-04-27 20:33:09 -03:00
```
**Fluxo de uma requisição:**
```
Request → routes → middleware (se protegida) → controller → service → repository → Prisma → PostgreSQL
```
### Modelos do banco de dados
```prisma
model users {
2026-05-04 19:50:31 -03:00
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[]
2026-04-27 20:33:09 -03:00
}
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)
}
2026-05-04 19:50:31 -03:00
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)
}
2026-04-27 20:33:09 -03:00
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 <accessToken>`.
**GET `/users/me` **
2026-05-04 19:50:31 -03:00
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.
2026-04-27 20:33:09 -03:00
```json
// Response 200
{
"id": "uuid",
"name": "João Silva",
"email": "joao@email .com",
2026-05-04 19:50:31 -03:00
"profilePhoto": "base64_ou_url",
"bio": "Motorista há 5 anos.",
2026-04-27 20:33:09 -03:00
"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"
}
```
---
2026-05-04 19:50:31 -03:00
**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."
}
```
---
2026-04-27 20:33:09 -03:00
**DELETE `/users/me` **
Remove a conta do usuário autenticado.
```
// Response 204 (sem body)
```
---
2026-05-04 19:50:31 -03:00
#### Sincronização — `/sync`
> Rotas protegidas. Enviar o header `Authorization: Bearer <accessToken>`.
**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": [...]
}
```
---
2026-04-27 20:33:09 -03:00
#### 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 |
2026-05-04 19:50:31 -03:00
| 500 | Erro interno no servidor |
2026-04-27 20:33:09 -03:00
---
### 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 |
|---|---|---|
2026-05-04 19:50:31 -03:00
| 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 |
2026-04-27 20:33:09 -03:00
| Axios | 1 | Cliente HTTP |
| TypeScript | 5 | Tipagem |
2026-05-04 19:50:31 -03:00
### 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
```
2026-04-27 20:33:09 -03:00
### Telas
| Arquivo | Rota | Descrição |
|---|---|---|
2026-05-04 19:50:31 -03:00
| `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`
2026-04-27 20:33:09 -03:00
### Execução
```bash
2026-05-04 19:50:31 -03:00
cd toptran-app
2026-04-27 20:33:09 -03:00
# Instalar dependências
npm install
# Iniciar o servidor Expo
npm start
2026-05-04 19:50:31 -03:00
# Build e abrir no Android
2026-04-27 20:33:09 -03:00
npm run android
2026-05-04 19:50:31 -03:00
# Build e abrir no iOS
2026-04-27 20:33:09 -03:00
npm run ios
```
### Conectar ao dispositivo Android via AVD na VM
1. Ligue o dispositivo Android (AVD)
2026-05-04 19:50:31 -03:00
2. Rodar o comando
```bash
adb tcpip 5555
```
3. No terminal local, crie o túnel SSH reverso:
2026-04-27 20:33:09 -03:00
```bash
ssh -R 5555:localhost:5555 dev@175 .15.15.93
```
2026-05-04 19:50:31 -03:00
4. Conecte o ADB ao dispositivo tunelado:
2026-04-27 20:33:09 -03:00
```bash
adb connect localhost:5555
```