Gestão completa de frota corporativa — reservas com aprovação, entrega e devolução com controle de KM, troca de responsável auditada e quiosque com registro fotográfico.
Uma plataforma completa para gerenciar a frota corporativa — do pedido do colaborador à devolução da chave, com rastreabilidade total de quem está com cada veículo.
O controle de chaves e veículos da empresa era feito na base da confiança: sem registro de quem retirou, quando devolveu ou quantos KM rodou. O sistema digitaliza todo o ciclo com auditoria completa.
O colaborador solicita um veículo informando finalidade e período. O admin aprova, confirma a entrega com KM inicial e, na devolução, registra o KM final — o sistema calcula a quilometragem rodada.
Durante o uso, a chave pode mudar de mãos. A troca de responsável registra atomicamente quem passou, quem recebeu e o motivo — e o histórico mostra a cadeia completa de custódia do veículo.
Um tablet no local de retirada, protegido por PIN, fotografa o motorista na retirada e na devolução. As fotos vão para storage privado com URLs assinadas — evidência visual de cada movimentação.
Seis módulos integrados cobrindo todo o ciclo de vida de uma reserva de veículo.
Ciclo completo: pendente → aprovada → em uso → finalizada. Verificação de conflito de horário com buffer de 30 min entre usos para inspeção do veículo.
A chave pode ser repassada durante o uso — para outro colaborador ou motorista externo sem login. Cada troca gera um evento auditável com origem, destino e motivo.
Para demandas imediatas, o admin entrega a chave na hora: a reserva nasce em uso, com KM inicial gravado, pulando a etapa de solicitação — mas mantendo todo o registro.
Tablet no ponto de retirada, sem login — protegido por PIN com comparação em tempo constante. Selfie na retirada e na devolução, armazenada em bucket privado.
Histórico filtrado por veículo, colaborador, status e período. Relatórios com agregados por status, finalidade, veículo e colaborador — separando reservas de liberações diretas.
Perfil TI gerencia usuários, reseta senhas e bloqueia acessos com efeito imediato. Cada tentativa de login — sucesso ou falha — é registrada com IP e horário.
O ciclo completo — da solicitação à devolução da chave — com todos os atores envolvidos.
Escolhe o veículo disponível, informa finalidade, destino e período de uso.
Aprova ou recusa a solicitação — podendo trocar o veículo e anexar observações.
Entrega a chave e grava o KM inicial. A reserva passa para "em uso".
Pode transferir a responsabilidade a um colega — cada troca vira evento auditável.
Admin registra o KM final, o sistema calcula o KM rodado e atualiza o veículo.
Stack moderna escolhida para performance, DX e facilidade de deploy independente.
SPA com roteamento via React Router v6 e rotas protegidas por perfil. HMR instantâneo e build otimizado.
Transições de página e listas com stagger — componentes PageTransition e StaggerList reutilizáveis.
Cliente HTTP com interceptor que injeta o token JWT em todas as requisições autenticadas.
Service Worker registrado no boot — o sistema instala como app no celular dos colaboradores.
Feedback visual imediato para ações de sucesso, erro e validação.
API REST com roteamento modular por entidade: auth, veículos, reservas, usuários, motoristas e checkout.
Autenticação stateless com expiração de 12h. Senhas com hash bcrypt (salt 10) e role guard por perfil.
Driver nativo com pool de conexões, prepared statements e transações atômicas para operações críticas.
Headers de segurança, CORS restritivo em produção e rate limit no login (anti brute-force) e no quiosque.
Upload de fotos em memória (limite 8MB) direto para bucket privado, com paths sanitizados.
Deploy automático via Git. SPA routing via vercel.json com rewrite para index.html.
Web service Node.js com auto-deploy via render.yaml. Health check em /api/health.
PostgreSQL gerenciado via connection pooling + Storage privado para as fotos do quiosque.
Três níveis de acesso com permissões distintas, controlados por middleware JWT no backend.
Solicita e usa veículos
Gestor da frota
Administrador do sistema
Arquitetura desacoplada com frontend SPA e backend REST totalmente separados e implantados de forma independente.
Trechos reais do projeto com contexto e explicação técnica detalhada.
// Responsavel atual, admin ou TI podem transferir
const podeTransferir =
req.user.role === 'admin' ||
req.user.role === 'ti' ||
ehResponsavelAtual(r, req.user.id);
if (!podeTransferir)
return res.status(403).json({ error: 'Sem permissao para transferir esta reserva' });
// Novo responsável: colaborador com login OU motorista externo (nome livre)
let novoRespId = null;
let novoExterno = null;
if (novo_responsavel_id) {
const novoUser = await db.prepare(
'SELECT id, nome, bloqueado FROM usuarios WHERE id = ?'
).get(novo_responsavel_id);
if (!novoUser) return res.status(404).json({ error: 'Novo responsavel nao encontrado' });
if (novoUser.bloqueado) return res.status(400).json({ error: 'Novo responsavel esta bloqueado' });
novoRespId = novo_responsavel_id;
} else {
novoExterno = externoNome;
}
// Nome de quem era antes (para a auditoria ficar legível)
let origemNome;
if (r.responsavel_atual_id) {
origemNome = (await db.prepare('SELECT nome FROM usuarios WHERE id = ?')
.get(r.responsavel_atual_id))?.nome;
} else if (r.responsavel_externo_nome) {
origemNome = r.responsavel_externo_nome;
} else {
origemNome = (await db.prepare('SELECT nome FROM usuarios WHERE id = ?')
.get(r.usuario_id))?.nome;
}
// TRANSAÇÃO: atualização + evento gravados atomicamente
await db.transaction(async (tx) => {
await tx.prepare(
'UPDATE reservas SET responsavel_atual_id = ?, responsavel_externo_nome = ? WHERE id = ?'
).run(novoRespId, novoExterno, id);
await tx.prepare(`
INSERT INTO reserva_eventos
(reserva_id, tipo, autor_id, alvo_usuario_id, alvo_externo_nome, origem_nome, texto)
VALUES (?, 'transferencia', ?, ?, ?, ?, ?)
`).run(id, req.user.id, novoRespId, novoExterno, origemNome || null, motivo || null);
});
res.json({ ok: true });
const autenticar = asyncH(async (req, res, next) => {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'Token nao informado' });
let payload;
try {
payload = jwt.verify(token, process.env.JWT_SECRET);
} catch {
return res.status(401).json({ error: 'Token invalido ou expirado' });
}
// Busca o usuário a cada request: bloqueio tem efeito IMEDIATO,
// mesmo com um JWT ainda dentro da validade de 12h
const user = await db.prepare(
'SELECT id, nome, email, role, bloqueado FROM usuarios WHERE id = ?'
).get(payload.id);
if (!user) return res.status(401).json({ error: 'Usuario nao encontrado' });
if (user.bloqueado) return res.status(403).json({ error: 'Usuario bloqueado' });
req.user = user;
next();
});
// Guard composável — aceita N perfis
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Nao autenticado' });
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Acesso negado para o seu perfil' });
}
next();
};
}
// Uso nas rotas (composição de middlewares Express):
// router.use(autenticar); → todas exigem login
// router.post('/', requireRole('admin'), ctrl.criar); → só admin
// router.get('/logs', requireRole('ti'), ctrl.logs); → só TI
// Comparação em tempo constante — sem vazamento por timing
function pinIgual(a, b) {
const ab = Buffer.from(String(a));
const bb = Buffer.from(String(b));
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
// ── No upload da foto (checkoutController.js) ──────────────
// Pasta segregada: motorista cadastrado vs digitado à mão
if (cadastrado && pessoaId) {
const m = await db.prepare('SELECT id, nome FROM motoristas WHERE id = ?').get(pessoaId);
if (!m) return res.status(404).json({ error: 'Motorista nao encontrado' });
motoristaId = m.id; nome = m.nome;
pastaBase = sanitizeSegment(nome); // "João"
} else {
if (nome.length < 2) return res.status(400).json({ error: 'Informe o nome do motorista' });
pastaBase = `Motorista sem cadastro/${sanitizeSegment(nome)}`;
}
// Timestamp do SERVIDOR (autoritário — o relógio do tablet não manda)
const { agora } = await db.prepare(`SELECT ${db.NOW_SQL} AS agora`).get();
const fileStamp = agora.replace(' ', '_').replace(/:/g, '-'); // 2026-06-08_14-30-00
const pathKey = `${pastaBase}/${fileStamp}_${tipo}.jpg`;
// Upload para bucket PRIVADO — leitura só com URL assinada (TI)
await storage.uploadFoto(pathKey, req.file.buffer, req.file.mimetype || 'image/jpeg');
await db.prepare(
'INSERT INTO checkout_fotos (motorista_id, usuario_id, nome, cadastrado, tipo, storage_path) VALUES (?,?,?,?,?,?)'
).run(motoristaId, usuarioId, nome, cadastrado ? 1 : 0, tipo, pathKey);
res.status(201).json({ ok: true, nome, tipo, path: pathKey });
async function relatorio(req, res) {
const { usuario_id, veiculo_id, status, inicio, fim, tipo } = req.query;
// WHERE dinâmico — sempre com placeholders, nunca concatenação
const where = [];
const params = [];
if (usuario_id) { where.push('r.usuario_id = ?'); params.push(usuario_id); }
if (veiculo_id) { where.push('r.veiculo_id = ?'); params.push(veiculo_id); }
if (status) { where.push('r.status = ?'); params.push(status); }
if (inicio) { where.push('r.data_retirada >= ?'); params.push(inicio); }
if (fim) { where.push('r.data_devolucao <= ?'); params.push(fim); }
if (tipo === 'reserva') where.push('r.liberacao_direta = 0');
if (tipo === 'liberacao') where.push('r.liberacao_direta = 1');
const whereSql = where.length ? 'WHERE ' + where.join(' AND ') : '';
const rows = await db.prepare(`
SELECT r.*, u.nome AS usuario_nome, u.email AS usuario_email,
v.placa, v.modelo, v.marca, v.km_atual AS veiculo_km_atual
FROM reservas r
JOIN usuarios u ON u.id = r.usuario_id
JOIN veiculos v ON v.id = r.veiculo_id
${whereSql}
ORDER BY r.data_retirada DESC
LIMIT 2000
`).all(...params);
// Agregados em UMA passada — mais rápido que N queries
const stats = {
total: rows.length,
reservas: rows.filter(r => !r.liberacao_direta).length,
liberacoes: rows.filter(r => r.liberacao_direta).length,
porStatus: {}, porFinalidade: {}, porVeiculo: {}, porColaborador: {},
};
rows.forEach(r => {
stats.porStatus[r.status] = (stats.porStatus[r.status] || 0) + 1;
stats.porFinalidade[r.finalidade] = (stats.porFinalidade[r.finalidade] || 0) + 1;
const veic = `${r.marca} ${r.modelo} (${r.placa})`;
stats.porVeiculo[veic] = (stats.porVeiculo[veic] || 0) + 1;
stats.porColaborador[r.usuario_nome] = (stats.porColaborador[r.usuario_nome] || 0) + 1;
});
res.json({ data: rows, stats });
}