Correções para funcionamento do login adm

This commit is contained in:
2026-06-15 21:17:14 -03:00
parent b59fedec5c
commit 146aa3e762
26 changed files with 262 additions and 36 deletions

View File

@@ -38,4 +38,4 @@ SMTP_DOMAIN=gade.com.br
# ── App ─────────────────────────────────────────────────────── # ── App ───────────────────────────────────────────────────────
APP_HOST=localhost:3000 APP_HOST=localhost:3000
APP_NAME=Gade Logística APP_NAME=Reem Logística

125
CLAUDE.md Normal file
View File

@@ -0,0 +1,125 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project overview
**Reem Logística** is a Rails 7 web application for controlling delivery costs for **Gade Hospitalar** (account_id: 95907). It manages consolidations of driver payments across delivery routes (UBS SUL, UBS LESTE, EMAD, STS, SAD, etc.).
## Commands
All commands run inside Docker. The PostgreSQL database lives externally (not in docker-compose).
```bash
# Start
docker-compose up -d
# Rails console
docker-compose exec app bundle exec rails console
# Run migrations
docker-compose exec app bundle exec rails db:migrate
# Seed (creates admin + test drivers)
docker-compose exec app bundle exec rails db:seed
# Logs
docker-compose logs -f app
# Tests (RSpec)
docker-compose exec app bundle exec rspec
docker-compose exec app bundle exec rspec spec/models/consolidacao_spec.rb # single file
# Rake task (runs manually what the cron job runs hourly)
docker-compose exec app bundle exec rake historico:atualizar
# Activate the hourly cron job on the server
docker-compose exec app bundle exec whenever --update-crontab
```
## Setup from scratch
```bash
cp .env.example .env
# Edit .env: set DB_HOST, DB_PASSWORD, and generate SECRET_KEY_BASE with:
# openssl rand -hex 64
docker-compose build --no-cache
docker-compose up -d
docker-compose exec app bundle exec rails db:create db:migrate db:seed
```
Default credentials (change in production): `admin@gade.com` / `Gade@2026!`
## Critical constraint: read-only external table
The table `public.db_reem_simplerout_2026` is owned by an external system (SimpleRoute) that updates it every hour. **NEVER** create migrations for it, and never run INSERT/UPDATE/DELETE/DROP/TRUNCATE on it.
`Entrega` (app/models/entrega.rb) maps to this table and enforces `readonly? = true`. All queries go through its scopes — the most important being `.pagas` (`status='completed' AND checkin IS NOT NULL`), which defines payment eligibility.
## Architecture
### Data flow
1. External system writes to `db_reem_simplerout_2026` hourly
2. `Atualizar HistoricoEstimadoJob` (triggered by Whenever cron every hour) counts paid deliveries per driver and writes to `historico_estimados`
3. Admins/managers create **Consolidações** (payment batches) covering a date range + list of drivers
4. A 3-step wizard classifies each delivery per driver (Normal/Retirada/Bônus/Desconto toggles)
5. Finalizing a consolidation sends WhatsApp (Twilio) and email notifications to drivers
6. Drivers view their own panel at `/motorista` using a 4-digit PIN or QR code from their printed payslip
### Key models
| Model | Table | Notes |
|---|---|---|
| `Entrega` | `db_reem_simplerout_2026` | Read-only. External data. |
| `Consolidacao` | `consolidacoes` | Payment batch. Enum status: `rascunho/finalizada/arquivada`. Soft-delete via `deleted_at`. |
| `ConsolidacaoMotorista` | `consolidacao_motoristas` | Join: which drivers are in a consolidation. |
| `ConsolidacaoEntrega` | `consolidacao_entregas` | Each classified delivery. Enum tipo: `normal/retirada/bonus/desconto`. |
| `HistoricoEstimado` | `historico_estimados` | Snapshot of hourly estimated values per driver. |
| `Configuracao` | `configuracoes` | Key-value store for prices and feature flags. Access via `Configuracao.valor('chave')`. |
| `AuditoriaLog` | `auditoria_logs` | Audit trail. Controllers include `Auditavel` concern and call `auditar!(:acao, registro)`. |
| `User` | `users` | Devise auth. Roles: `admin/gerente/operador/motorista`. Drivers may have no email — they log in via `pin_code` (4 digits, unique per driver) or `login_token` (QR). |
### Authorization
Pundit policies in `app/policies/`. `ApplicationController` includes `Pundit::Authorization` and calls `authenticate_user!` on every action. The motorista panel (`/motorista/**`) uses a separate session mechanism (`motorista/sessoes_controller.rb`) — drivers are NOT authenticated via Devise.
### Two login flows for drivers
- `/auth/login` (Devise, Fase 2) — PIN tab on the standard login page
- `/motorista/login` (Fase 8) — dedicated numeric keypad, used by QR codes printed on payslips. Prefer this for mobile. Both use `pin_code`.
### PDF generation
`app/services/pdf/` contains three classes:
- `BasePdf` — shared black/orange header and footer layout
- `RelatorioMotoristaPdf` — detailed delivery table per driver
- `HoleritePdf` — payslip style + embedded QR code (via `rqrcode`/`chunky_png`) linking to `/motorista/acesso/:token`
### The consolidation wizard
`/consolidacoes/:id/wizard``/consolidacao_entregas/validar``/consolidacao_entregas/revisar`
- **Passo 1** (wizard): pick a driver. Shows progress per driver.
- **Passo 2** (validar): color-coded toggle buttons per delivery (orange=Normal, dark orange=Retirada, white=Bônus, black=Desconto). Toggle individual or mass-classify. Stimulus controller (`validacao_controller.js`) handles real-time totals and progress.
- **Passo 3** (revisar): summary for the selected driver, then move to the next driver or save as draft.
Finalizing is blocked (server-side + UI) until all drivers have all eligible deliveries classified.
### Notifications (NotificacaoService)
Controlled by `Configuracao` flags `notificacao_whatsapp` and `notificacao_email`. Failures are rescued and logged — they never abort the finalization. WhatsApp requires Twilio ENV vars + driver `telefone` field in `+5511999998888` format.
## Design system
Dark theme (default). Colors: background `#0a0a0a`, cards `#1a1a1a`, accent/buttons `#f97316` (orange), text `#ffffff`, success `#22c55e`, error `#ef4444`. Minimum font 16px desktop / 18px mobile. Touch targets minimum 48px height.
## Environment variables
All config comes from `.env` (never hardcoded). Key variables:
- `DB_HOST` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` — PostgreSQL connection
- `DB_EXISTING_ACCOUNT_ID` — filters `db_reem_simplerout_2026` to Gade's account (default: 95907)
- `SECRET_KEY_BASE` — generate with `openssl rand -hex 64`
- `APP_HOST` — used in QR code URLs and notification links
- `TWILIO_*` — WhatsApp notifications (optional)
- `SMTP_*` — Email notifications (optional, only activates if `SMTP_USERNAME` is set)

View File

@@ -1,4 +1,4 @@
# 🚛 Gade Logística — Sistema de Controle de Custos # 🚛 Reem Logística — Sistema de Controle de Custos
Sistema web para controle de custos de entregas hospitalares da **Gade Hospitalar**. Sistema web para controle de custos de entregas hospitalares da **Gade Hospitalar**.

2
Rakefile Normal file
View File

@@ -0,0 +1,2 @@
require_relative "config/application"
Rails.application.load_tasks

View File

@@ -4,7 +4,7 @@ class Admin::UsuariosController < ApplicationController
def index def index
authorize User authorize User
@usuarios = policy_scope(User).order(:nome).page(params[:page]).per(20) @usuarios = policy_scope(User).order(:nome).limit(200)
@roles = User.roles.keys @roles = User.roles.keys
end end

View File

@@ -47,14 +47,15 @@ class DashboardController < ApplicationController
.count .count
.sort_by { |_, v| -v } .sort_by { |_, v| -v }
.first(6) .first(6)
.to_h
# Evolução diária do mês (para Chart.js) # Evolução diária do mês (para Chart.js)
@grafico_diario = build_grafico_diario(entregas_mes, config[:entrega]) @grafico_diario = build_grafico_diario(entregas_mes, config[:entrega])
# Consolidações do mês # Consolidações do mês
@consolidacoes_mes = Consolidacao.do_mes(@data_referencia) @consolidacoes_mes = Consolidacao.ativas.where(created_at: @mes_inicio.beginning_of_day..@mes_fim.end_of_day)
@consolidacoes_abertas = @consolidacoes_mes.where(status: :aberta).count @consolidacoes_abertas = @consolidacoes_mes.where(status: :rascunho).count
@consolidacoes_fechadas = @consolidacoes_mes.where(status: :fechada).count @consolidacoes_fechadas = @consolidacoes_mes.where(status: :finalizada).count
# Histórico estimado mais recente # Histórico estimado mais recente
@historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5) @historico_recente = HistoricoEstimado.order(created_at: :desc).limit(5)

View File

@@ -23,7 +23,7 @@ class Users::SessionsController < Devise::SessionsController
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
case resource.role case resource.role
when 'motorista' when 'motorista'
motorista_path motorista_dashboard_path
else else
dashboard_path dashboard_path
end end
@@ -40,13 +40,13 @@ class Users::SessionsController < Devise::SessionsController
private private
def handle_pin_login def handle_pin_login
pin = params[:user][:pin].to_s.strip pin = params[:pin].to_s.strip
user = User.find_by(pin_code: pin, role: 'motorista') user = User.find_by(pin_code: pin, role: 'motorista')
if user&.active? if user&.ativo?
sign_in(user) sign_in(user)
AuditoriaLog.registrar(user: user, acao: 'login_pin', entidade: 'User', dados_novos: { detalhes: "" }, request: request) AuditoriaLog.registrar(user: user, acao: 'login_pin', entidade: 'User', dados_novos: { detalhes: "" }, request: request)
redirect_to motorista_path, notice: "Bem-vindo, #{user.nome_display}!" redirect_to motorista_dashboard_path, notice: "Bem-vindo, #{user.nome_display}!"
else else
flash.now[:alert] = 'PIN inválido ou motorista inativo.' flash.now[:alert] = 'PIN inválido ou motorista inativo.'
@pin_login = true @pin_login = true

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end

View File

@@ -1,6 +1,6 @@
# app/models/entrega.rb # app/models/entrega.rb
# #
# Model de LEITURA para a tabela existente da Gade Hospitalar. # Model de LEITURA para a tabela existente da Reem Transporte.
# NUNCA criar migration para esta tabela. # NUNCA criar migration para esta tabela.
# NUNCA executar INSERT, UPDATE, DELETE ou DROP nesta tabela. # NUNCA executar INSERT, UPDATE, DELETE ou DROP nesta tabela.
# #

View File

@@ -1,7 +1,7 @@
# app/models/user.rb # app/models/user.rb
class User < ApplicationRecord class User < ApplicationRecord
devise :database_authenticatable, devise :database_authenticatable,
:registerable,
:recoverable, :recoverable,
:rememberable, :rememberable,
:validatable :validatable

View File

@@ -30,7 +30,7 @@ module Pdf
@pdf.fill_rectangle [-40 + 0, @pdf.cursor + 40], 8, 70 # faixa laranja lateral @pdf.fill_rectangle [-40 + 0, @pdf.cursor + 40], 8, 70 # faixa laranja lateral
@pdf.fill_color 'FFFFFF' @pdf.fill_color 'FFFFFF'
@pdf.text_box 'GADE HOSPITALAR', @pdf.text_box 'REEM TRANSPORTE',
at: [10, @pdf.cursor + 20], size: 20, style: :bold at: [10, @pdf.cursor + 20], size: 20, style: :bold
@pdf.fill_color LARANJA @pdf.fill_color LARANJA
@pdf.text_box 'Sistema de Controle de Custos Logísticos', @pdf.text_box 'Sistema de Controle de Custos Logísticos',
@@ -44,7 +44,7 @@ module Pdf
@pdf.repeat(:all) do @pdf.repeat(:all) do
@pdf.bounding_box [0, 20], width: @pdf.bounds.width do @pdf.bounding_box [0, 20], width: @pdf.bounds.width do
@pdf.fill_color CINZA @pdf.fill_color CINZA
@pdf.text "Gerado em #{Time.current.strftime('%d/%m/%Y %H:%M')} · Gade Hospitalar — documento interno", @pdf.text "Gerado em #{Time.current.strftime('%d/%m/%Y %H:%M')} · Reem Transporte — documento interno",
size: 8, align: :center size: 8, align: :center
@pdf.fill_color '000000' @pdf.fill_color '000000'
end end

View File

@@ -47,7 +47,7 @@
</div> </div>
<div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5"> <div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5">
<p class="text-gray-400 text-xs mb-1">Account ID</p> <p class="text-gray-400 text-xs mb-1">Account ID</p>
<p class="text-white font-mono text-sm">95907 (Gade Hospitalar)</p> <p class="text-white font-mono text-sm">95907 (Reem Transporte)</p>
</div> </div>
<div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5"> <div class="p-4 bg-[#0a0a0a] rounded-xl border border-white/5">
<p class="text-gray-400 text-xs mb-1">Atualização</p> <p class="text-gray-400 text-xs mb-1">Atualização</p>

View File

@@ -2,7 +2,7 @@
<div style="font-family: Arial, sans-serif; max-width: 520px; margin: 0 auto; background: #0a0a0a; border-radius: 12px; overflow: hidden;"> <div style="font-family: Arial, sans-serif; max-width: 520px; margin: 0 auto; background: #0a0a0a; border-radius: 12px; overflow: hidden;">
<div style="background: #0a0a0a; padding: 24px; border-left: 6px solid #f97316;"> <div style="background: #0a0a0a; padding: 24px; border-left: 6px solid #f97316;">
<h1 style="color: #ffffff; margin: 0; font-size: 20px;">GADE HOSPITALAR</h1> <h1 style="color: #ffffff; margin: 0; font-size: 20px;">REEM TRANSPORTE</h1>
<p style="color: #f97316; margin: 4px 0 0; font-size: 13px;">Sistema de Logística</p> <p style="color: #f97316; margin: 4px 0 0; font-size: 13px;">Sistema de Logística</p>
</div> </div>

View File

@@ -5,7 +5,7 @@
<div class="bg-[#0a0a0a] rounded-lg p-4 mb-5 flex items-center gap-3"> <div class="bg-[#0a0a0a] rounded-lg p-4 mb-5 flex items-center gap-3">
<div class="w-2 h-12 bg-orange-500 rounded"></div> <div class="w-2 h-12 bg-orange-500 rounded"></div>
<div> <div>
<p class="text-white font-black text-lg leading-tight">GADE HOSPITALAR</p> <p class="text-white font-black text-lg leading-tight">REEM TRANSPORTE</p>
<p class="text-orange-500 text-xs">Holerite de Pagamento — Entregas</p> <p class="text-orange-500 text-xs">Holerite de Pagamento — Entregas</p>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,44 @@
<%# app/views/devise/passwords/edit.html.erb — Redefinir senha (link do e-mail) %>
<% content_for :title, 'Nova senha' %>
<div class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-[#f97316] rounded-2xl flex items-center justify-center mx-auto mb-3">
<span class="text-3xl">🔒</span>
</div>
<h1 class="text-white font-bold text-2xl">Definir nova senha</h1>
</div>
<div class="bg-[#111111] border border-white/10 rounded-2xl p-8">
<%= form_with(url: user_password_path, scope: :user, method: :put, data: { turbo: false }) do |f| %>
<%= f.hidden_field :reset_password_token, value: params[:reset_password_token] %>
<div class="space-y-5">
<div>
<%= f.label :password, 'Nova senha', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
<%= f.password_field :password, autofocus: true, autocomplete: 'new-password',
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
focus:outline-none focus:border-[#f97316] focus:ring-1 focus:ring-[#f97316] text-base' %>
<p class="text-gray-600 text-xs mt-1">Mínimo de 6 caracteres</p>
</div>
<div>
<%= f.label :password_confirmation, 'Confirmar nova senha', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
<%= f.password_field :password_confirmation, autocomplete: 'new-password',
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
focus:outline-none focus:border-[#f97316] focus:ring-1 focus:ring-[#f97316] text-base' %>
</div>
<%= f.submit 'Salvar nova senha',
class: 'w-full py-3.5 bg-[#f97316] hover:bg-orange-500 text-white font-semibold
rounded-xl transition-colors cursor-pointer text-base min-h-[48px]' %>
</div>
<% end %>
<p class="text-center mt-6">
<%= link_to '← Voltar para o login', new_user_session_path,
class: 'text-sm text-[#f97316] hover:text-orange-400 transition-colors' %>
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,47 @@
<%# app/views/devise/passwords/new.html.erb — Esqueci a senha (estilizada) %>
<% content_for :title, 'Recuperar senha' %>
<div class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-[#f97316] rounded-2xl flex items-center justify-center mx-auto mb-3">
<span class="text-3xl">🔑</span>
</div>
<h1 class="text-white font-bold text-2xl">Recuperar senha</h1>
<p class="text-gray-400 text-sm mt-1">Informe seu e-mail para receber as instruções</p>
</div>
<div class="bg-[#111111] border border-white/10 rounded-2xl p-8">
<%= form_with(url: user_password_path, scope: :user, method: :post, data: { turbo: false }) do |f| %>
<div class="space-y-5">
<div>
<%= f.label :email, 'E-mail', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
<%= f.email_field :email,
autofocus: true, autocomplete: 'email',
class: 'w-full px-4 py-3 bg-[#0a0a0a] border border-white/10 rounded-xl text-white
placeholder-gray-600 focus:outline-none focus:border-[#f97316] focus:ring-1
focus:ring-[#f97316] transition-colors text-base',
placeholder: 'seu@email.com' %>
</div>
<%= f.submit 'Enviar instruções',
class: 'w-full py-3.5 bg-[#f97316] hover:bg-orange-500 text-white font-semibold
rounded-xl transition-colors cursor-pointer text-base min-h-[48px]' %>
</div>
<% end %>
<p class="text-center mt-6">
<%= link_to '← Voltar para o login', new_user_session_path,
class: 'text-sm text-[#f97316] hover:text-orange-400 transition-colors' %>
</p>
</div>
<p class="text-gray-600 text-xs text-center mt-6">
⚠️ O envio de e-mail depende da configuração SMTP no servidor.<br>
Sem SMTP, peça ao administrador para redefinir sua senha.
</p>
<p class="text-gray-500 text-xs text-center mt-4">Reem Transporte © <%= Date.today.year %></p>
</div>
</div>

View File

@@ -10,7 +10,7 @@
d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"/> d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"/>
</svg> </svg>
</div> </div>
<h1 class="text-3xl font-bold text-white">Gade Logística</h1> <h1 class="text-3xl font-bold text-white">Reem Logística</h1>
<p class="text-gray-400 mt-1 text-sm">Sistema de Controle de Custos</p> <p class="text-gray-400 mt-1 text-sm">Sistema de Controle de Custos</p>
</div> </div>
@@ -40,7 +40,7 @@
<%# Login por e-mail %> <%# Login por e-mail %>
<div id="form-email" class="p-8"> <div id="form-email" class="p-8">
<%= form_with(url: user_session_path, method: :post) do |f| %> <%= form_with(url: user_session_path, scope: :user, method: :post, data: { turbo: false }) do |f| %>
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<%= f.label :email, 'E-mail', class: 'block text-sm font-medium text-gray-300 mb-1.5' %> <%= f.label :email, 'E-mail', class: 'block text-sm font-medium text-gray-300 mb-1.5' %>
@@ -82,7 +82,7 @@
<%# Login por PIN %> <%# Login por PIN %>
<div id="form-pin" class="p-8 hidden"> <div id="form-pin" class="p-8 hidden">
<%= form_with(url: user_session_path, method: :post) do |f| %> <%= form_with(url: user_session_path, method: :post, data: { turbo: false }) do |f| %>
<%= f.hidden_field :pin_login, value: '1' %> <%= f.hidden_field :pin_login, value: '1' %>
<div class="space-y-6"> <div class="space-y-6">
@@ -148,7 +148,7 @@
</div> </div>
<p class="text-center text-gray-600 text-xs mt-6"> <p class="text-center text-gray-600 text-xs mt-6">
Gade Hospitalar © <%= Date.today.year %> Reem Transporte © <%= Date.today.year %>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@
<div class="flex items-center gap-3 px-6 py-5 border-b border-[#2a2a2a]"> <div class="flex items-center gap-3 px-6 py-5 border-b border-[#2a2a2a]">
<div class="w-9 h-9 bg-orange-500 rounded-lg flex items-center justify-center font-black text-black text-lg">G</div> <div class="w-9 h-9 bg-orange-500 rounded-lg flex items-center justify-center font-black text-black text-lg">G</div>
<div> <div>
<p class="font-bold text-white text-sm leading-tight">Gade Hospitalar</p> <p class="font-bold text-white text-sm leading-tight">Reem Transporte</p>
<p class="text-orange-500 text-xs">Logística</p> <p class="text-orange-500 text-xs">Logística</p>
</div> </div>
</div> </div>
@@ -75,7 +75,7 @@
</button> </button>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-7 h-7 bg-orange-500 rounded flex items-center justify-center font-black text-black text-sm">G</div> <div class="w-7 h-7 bg-orange-500 rounded flex items-center justify-center font-black text-black text-sm">G</div>
<span class="font-bold text-white text-sm">Gade Logística</span> <span class="font-bold text-white text-sm">Reem Logística</span>
</div> </div>
<div class="w-12"></div> <div class="w-12"></div>
</header> </header>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-param" content="<%= request_forgery_protection_token %>"> <meta name="csrf-param" content="<%= request_forgery_protection_token %>">
<meta name="csrf-token" content="<%= form_authenticity_token %>"> <meta name="csrf-token" content="<%= form_authenticity_token %>">
<title><%= content_for?(:title) ? "#{yield(:title)} | Gade Logística" : "Gade Logística" %></title> <title><%= content_for?(:title) ? "#{yield(:title)} | Reem Logística" : "Reem Logística" %></title>
<%# Tailwind via CDN em desenvolvimento — compilar em produção %> <%# Tailwind via CDN em desenvolvimento — compilar em produção %>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>

View File

@@ -7,7 +7,7 @@
<%# Logo %> <%# Logo %>
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="w-16 h-16 bg-orange-500 rounded-2xl flex items-center justify-center font-black text-black text-3xl mx-auto mb-3">G</div> <div class="w-16 h-16 bg-orange-500 rounded-2xl flex items-center justify-center font-black text-black text-3xl mx-auto mb-3">G</div>
<h1 class="text-white font-bold text-2xl">Gade Logística</h1> <h1 class="text-white font-bold text-2xl">Reem Logística</h1>
<p class="text-gray-400 text-base mt-1">Digite seu PIN de 4 números</p> <p class="text-gray-400 text-base mt-1">Digite seu PIN de 4 números</p>
</div> </div>

View File

@@ -4,7 +4,7 @@ require "rails/all"
Bundler.require(*Rails.groups) Bundler.require(*Rails.groups)
require "dotenv/load" if defined?(Dotenv) require "dotenv/load" if defined?(Dotenv)
module GadeLogistica module ReemLogistica
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 7.1 config.load_defaults 7.1

View File

@@ -11,6 +11,4 @@ Devise.setup do |config|
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
config.reset_password_within = 6.hours config.reset_password_within = 6.hours
config.sign_out_via = :delete config.sign_out_via = :delete
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
end end

View File

@@ -0,0 +1,6 @@
# Ensina ao Rails os plurais corretos em português.
# Sem isso: consolidacao → "consolidacaos" e configuracao → "configuracaos"
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.irregular 'consolidacao', 'consolidacoes'
inflect.irregular 'configuracao', 'configuracoes'
end

View File

@@ -1,7 +1,7 @@
class CreateConsolidacaoMotoristas < ActiveRecord::Migration[7.1] class CreateConsolidacaoMotoristas < ActiveRecord::Migration[7.1]
def change def change
create_table :consolidacao_motoristas do |t| create_table :consolidacao_motoristas do |t|
t.references :consolidacao, null: false, foreign_key: true t.references :consolidacao, null: false, foreign_key: { to_table: :consolidacoes }
t.string :motorista_nome, null: false # driver da tabela existente t.string :motorista_nome, null: false # driver da tabela existente
t.decimal :valor_total, precision: 10, scale: 2, default: 0 t.decimal :valor_total, precision: 10, scale: 2, default: 0

View File

@@ -1,7 +1,7 @@
class CreateConsolidacaoEntregas < ActiveRecord::Migration[7.1] class CreateConsolidacaoEntregas < ActiveRecord::Migration[7.1]
def change def change
create_table :consolidacao_entregas do |t| create_table :consolidacao_entregas do |t|
t.references :consolidacao, null: false, foreign_key: true t.references :consolidacao, null: false, foreign_key: { to_table: :consolidacoes }
t.string :tracking_id, null: false # FK lógico para db_reem_simplerout_2026 t.string :tracking_id, null: false # FK lógico para db_reem_simplerout_2026
t.string :motorista_nome, null: false t.string :motorista_nome, null: false
t.integer :tipo, null: false, default: 0 t.integer :tipo, null: false, default: 0

View File

@@ -1,5 +1,5 @@
# db/seeds.rb # db/seeds.rb
# Dados iniciais do sistema Gade Hospitalar # Dados iniciais do sistema Reem Transporte
# Execute com: bundle exec rails db:seed # Execute com: bundle exec rails db:seed
puts "🌱 Criando dados iniciais..." puts "🌱 Criando dados iniciais..."
@@ -30,7 +30,7 @@ configs = [
{ chave: 'preco_desconto', valor: '15.00', descricao: 'Valor descontado por problema (R$)' }, { chave: 'preco_desconto', valor: '15.00', descricao: 'Valor descontado por problema (R$)' },
{ chave: 'notificacao_whatsapp', valor: 'false', descricao: 'Notificações via WhatsApp (true/false)' }, { chave: 'notificacao_whatsapp', valor: 'false', descricao: 'Notificações via WhatsApp (true/false)' },
{ chave: 'notificacao_email', valor: 'false', descricao: 'Notificações via e-mail (true/false)' }, { chave: 'notificacao_email', valor: 'false', descricao: 'Notificações via e-mail (true/false)' },
{ chave: 'empresa_nome', valor: 'Gade Hospitalar', descricao: 'Nome da empresa exibido no sistema' }, { chave: 'empresa_nome', valor: 'Reem Transporte', descricao: 'Nome da empresa exibido no sistema' },
] ]
configs.each do |cfg| configs.each do |cfg|
@@ -52,9 +52,9 @@ puts " ⚠️ ALTERE A SENHA DO ADMIN NO PRIMEIRO ACESSO!"
{ nome: 'Maria Santos', pin: '5678' }, { nome: 'Maria Santos', pin: '5678' },
{ nome: 'Carlos Souza', pin: '9012' }, { nome: 'Carlos Souza', pin: '9012' },
].each do |m| ].each do |m|
User.find_or_create_by!(nome: m[:nome], role: :motorista) do |u| u = User.find_or_initialize_by(nome: m[:nome], role: :motorista)
u.pin_code = m[:pin] u.pin_code = m[:pin]
u.ativo = true u.ativo = true
end u.save!(validate: false)
puts " ✅ Motorista: #{m[:nome]} / PIN: #{m[:pin]}" puts " ✅ Motorista: #{m[:nome]} / PIN: #{m[:pin]}"
end end