Alteração Para uso dos Usuarios
This commit is contained in:
50
README.md
50
README.md
@@ -149,7 +149,7 @@ docker-compose down # parar tudo
|
|||||||
| **4** | ✅ Concluída | Job Whenever (1h) + HistoricoEstimado + AuditoriaLog + API métricas |
|
| **4** | ✅ Concluída | Job Whenever (1h) + HistoricoEstimado + AuditoriaLog + API métricas |
|
||||||
| **5** | ✅ Concluída | Nova Consolidação + filtros + Wizard Passo 1 |
|
| **5** | ✅ Concluída | Nova Consolidação + filtros + Wizard Passo 1 |
|
||||||
| **6** | ✅ Concluída | Toggles coloridos + ações em massa + Wizard Passos 2 e 3 (Stimulus) |
|
| **6** | ✅ Concluída | Toggles coloridos + ações em massa + Wizard Passos 2 e 3 (Stimulus) |
|
||||||
| **7** | ✅ Concluída | PDFs Prawn: relatório + holerite com QR code + modal preview |
|
| **7** | ✅ Concluída | PDFs Prawn: relatório + extrato com QR code + modal preview |
|
||||||
| **8** | ✅ Concluída | Painel motorista + login PIN/QR + notificações WhatsApp/email |
|
| **8** | ✅ Concluída | Painel motorista + login PIN/QR + notificações WhatsApp/email |
|
||||||
|
|
||||||
**🎉 PROJETO: 8 de 8 fases implementadas.**
|
**🎉 PROJETO: 8 de 8 fases implementadas.**
|
||||||
@@ -197,7 +197,7 @@ As Fases 2/3 (commit `4724847`) foram desenvolvidas em paralelo às Fases 4-8 (c
|
|||||||
- Seeds dos 3 motoristas de teste (João/1234, Maria/5678, Carlos/9012) restaurados — tinham sido sobrescritos
|
- Seeds dos 3 motoristas de teste (João/1234, Maria/5678, Carlos/9012) restaurados — tinham sido sobrescritos
|
||||||
|
|
||||||
### ⚠️ Observação sobre login PIN duplicado
|
### ⚠️ Observação sobre login PIN duplicado
|
||||||
Existem **dois fluxos de login por PIN** funcionais: a aba PIN na tela Devise (`/auth/login`, Fase 2) e a tela dedicada (`/motorista/login`, Fase 8 — usada pelo QR Code do holerite). Ambos usam `pin_code` e funcionam; recomenda-se manter os dois (a tela dedicada é melhor para mobile) ou unificar futuramente.
|
Existem **dois fluxos de login por PIN** funcionais: a aba PIN na tela Devise (`/auth/login`, Fase 2) e a tela dedicada (`/motorista/login`, Fase 8 — usada pelo QR Code do extrato). Ambos usam `pin_code` e funcionam; recomenda-se manter os dois (a tela dedicada é melhor para mobile) ou unificar futuramente.
|
||||||
|
|
||||||
### 📂 Arquivos das Fases 4, 5 e 6
|
### 📂 Arquivos das Fases 4, 5 e 6
|
||||||
|
|
||||||
@@ -248,10 +248,10 @@ Gemfile # + rqrcode, chunky_png
|
|||||||
db/migrate/20260101000009_add_login_token_to_users.rb # token para QR — rodar db:migrate!
|
db/migrate/20260101000009_add_login_token_to_users.rb # token para QR — rodar db:migrate!
|
||||||
app/services/pdf/base_pdf.rb # layout base: cabeçalho preto/laranja + rodapé
|
app/services/pdf/base_pdf.rb # layout base: cabeçalho preto/laranja + rodapé
|
||||||
app/services/pdf/relatorio_motorista_pdf.rb # tabela detalhada de entregas + resumo + total
|
app/services/pdf/relatorio_motorista_pdf.rb # tabela detalhada de entregas + resumo + total
|
||||||
app/services/pdf/holerite_pdf.rb # estilo contracheque + QR Code de login rápido
|
app/services/pdf/extrato_pdf.rb # estilo contracheque + QR Code de login rápido
|
||||||
app/controllers/consolidacoes_controller.rb # ATUALIZADO: gerar_pdf_relatorio/gerar_pdf_holerite/preview_holerite
|
app/controllers/consolidacoes_controller.rb # ATUALIZADO: gerar_pdf_relatorio/gerar_pdf_extrato/preview_extrato
|
||||||
app/views/consolidacoes/show.html.erb # botões por motorista + modal de preview
|
app/views/consolidacoes/show.html.erb # botões por motorista + modal de preview
|
||||||
app/views/consolidacoes/preview_holerite.html.erb # preview HTML antes de confirmar o PDF
|
app/views/consolidacoes/preview_extrato.html.erb # preview HTML antes de confirmar o PDF
|
||||||
app/models/user.rb # ATUALIZADO: regenerar_login_token!
|
app/models/user.rb # ATUALIZADO: regenerar_login_token!
|
||||||
```
|
```
|
||||||
Soft delete já implementado: consolidações finalizadas vão para "arquivada", nunca são excluídas.
|
Soft delete já implementado: consolidações finalizadas vão para "arquivada", nunca são excluídas.
|
||||||
@@ -262,7 +262,7 @@ Gemfile # + twilio-ruby
|
|||||||
app/controllers/motorista/sessoes_controller.rb # login PIN 4 dígitos + acesso via QR (/motorista/acesso/:token)
|
app/controllers/motorista/sessoes_controller.rb # login PIN 4 dígitos + acesso via QR (/motorista/acesso/:token)
|
||||||
app/controllers/motorista/dashboard_controller.rb # 2 cards: estimado (mês) + consolidado (finalizadas)
|
app/controllers/motorista/dashboard_controller.rb # 2 cards: estimado (mês) + consolidado (finalizadas)
|
||||||
app/views/motorista/sessoes/new.html.erb # teclado numérico gigante mobile-first (envia no 4º dígito)
|
app/views/motorista/sessoes/new.html.erb # teclado numérico gigante mobile-first (envia no 4º dígito)
|
||||||
app/views/motorista/dashboard/index.html.erb # cards laranja/branco + lista com download do próprio holerite
|
app/views/motorista/dashboard/index.html.erb # cards laranja/branco + lista com download do próprio extrato
|
||||||
app/services/notificacao_service.rb # WhatsApp (Twilio) + email — falha nunca quebra a finalização
|
app/services/notificacao_service.rb # WhatsApp (Twilio) + email — falha nunca quebra a finalização
|
||||||
app/mailers/application_mailer.rb
|
app/mailers/application_mailer.rb
|
||||||
app/mailers/consolidacao_mailer.rb # pagamento_fechado
|
app/mailers/consolidacao_mailer.rb # pagamento_fechado
|
||||||
@@ -279,18 +279,18 @@ config/routes.rb # ATUALIZADO: /motorista/log
|
|||||||
|
|
||||||
**Fluxo do motorista (público de baixo nível técnico):**
|
**Fluxo do motorista (público de baixo nível técnico):**
|
||||||
- Acessa `/motorista/login` → digita PIN de 4 números num teclado gigante (envia sozinho no 4º dígito)
|
- Acessa `/motorista/login` → digita PIN de 4 números num teclado gigante (envia sozinho no 4º dígito)
|
||||||
- OU escaneia o QR Code impresso no holerite → entra direto sem digitar nada
|
- OU escaneia o QR Code impresso no extrato → entra direto sem digitar nada
|
||||||
- Vê 2 cards: 🟧 valor estimado do mês ("aproximado") e ⬜ valor fechado para pagamento
|
- Vê 2 cards: 🟧 valor estimado do mês ("aproximado") e ⬜ valor fechado para pagamento
|
||||||
- Baixa o próprio holerite das consolidações finalizadas
|
- Baixa o próprio extrato das consolidações finalizadas
|
||||||
|
|
||||||
### 🧪 Teste end-to-end sugerido
|
### 🧪 Teste end-to-end sugerido
|
||||||
1. Login admin → criar consolidação (nome + período + motoristas)
|
1. Login admin → criar consolidação (nome + período + motoristas)
|
||||||
2. Wizard Passo 1 → escolher motorista → Passo 2: classificar entregas com os toggles
|
2. Wizard Passo 1 → escolher motorista → Passo 2: classificar entregas com os toggles
|
||||||
3. Usar "Marcar todos como Normal" → Passo 3: revisar → próximo motorista
|
3. Usar "Marcar todos como Normal" → Passo 3: revisar → próximo motorista
|
||||||
4. Com tudo classificado → Finalizar (motoristas notificados)
|
4. Com tudo classificado → Finalizar (motoristas notificados)
|
||||||
5. Na tela da consolidação → "Ver antes de gerar" → Confirmar → baixar Holerite PDF
|
5. Na tela da consolidação → "Ver antes de gerar" → Confirmar → baixar Extrato PDF
|
||||||
6. Logout → `/motorista/login` → entrar com PIN do motorista
|
6. Logout → `/motorista/login` → entrar com PIN do motorista
|
||||||
7. Conferir cards de valores → baixar próprio holerite → sair
|
7. Conferir cards de valores → baixar próprio extrato → sair
|
||||||
|
|
||||||
### ▶️ Próximos passos (deploy)
|
### ▶️ Próximos passos (deploy)
|
||||||
|
|
||||||
@@ -423,13 +423,13 @@ docker-compose exec app bundle exec rake reem:diagnostico INICIO=2026-02-01 FIM=
|
|||||||
- **Dashboard travava ao voltar (só com F5):** era o cache de preview do Turbo servindo um snapshot congelado. Adicionado `turbo-cache-control: no-cache` na página e o gráfico Chart.js virou idempotente (`Chart.getChart(ctx)?.destroy()`).
|
- **Dashboard travava ao voltar (só com F5):** era o cache de preview do Turbo servindo um snapshot congelado. Adicionado `turbo-cache-control: no-cache` na página e o gráfico Chart.js virou idempotente (`Chart.getChart(ctx)?.destroy()`).
|
||||||
- **500 ao criar motorista:** `users.email` era `NOT NULL` com índice único e default `""` — o 2º motorista sem e-mail colidia em `""` (`RecordNotUnique`). Agora a coluna permite `NULL` e e-mail em branco vira `nil` (`User#normalizar_email_em_branco`); no Postgres vários `NULL` convivem no índice único.
|
- **500 ao criar motorista:** `users.email` era `NOT NULL` com índice único e default `""` — o 2º motorista sem e-mail colidia em `""` (`RecordNotUnique`). Agora a coluna permite `NULL` e e-mail em branco vira `nil` (`User#normalizar_email_em_branco`); no Postgres vários `NULL` convivem no índice único.
|
||||||
- **500 ao gerar relatório PDF** (`gerar_pdf_relatorio`): `@entregas.group(:tipo).count` com `.order(:created_at)` gerava `GROUP BY ... ORDER BY created_at` → `PG::GroupingError`. Corrigido com `reorder(nil)`. Também removidas as opções `color:` passadas ao `Prawn#text` (não suportadas — uso de `fill_color`).
|
- **500 ao gerar relatório PDF** (`gerar_pdf_relatorio`): `@entregas.group(:tipo).count` com `.order(:created_at)` gerava `GROUP BY ... ORDER BY created_at` → `PG::GroupingError`. Corrigido com `reorder(nil)`. Também removidas as opções `color:` passadas ao `Prawn#text` (não suportadas — uso de `fill_color`).
|
||||||
- **Motorista não baixava o próprio holerite** ("sem permissão"): `ConsolidacaoPolicy#show?` exige `pode_consolidar?` (falso para motorista). Novo `autorizar_holerite!` no `ConsolidacoesController` — staff baixa qualquer um; motorista baixa só o próprio (PDF já filtrado pelo nome dele).
|
- **Motorista não baixava o próprio extrato** ("sem permissão"): `ConsolidacaoPolicy#show?` exige `pode_consolidar?` (falso para motorista). Novo `autorizar_extrato!` no `ConsolidacoesController` — staff baixa qualquer um; motorista baixa só o próprio (PDF já filtrado pelo nome dele).
|
||||||
- **QR code do holerite não abria:** URL estava fixa em `https://`, mas o servidor roda em **http** (IP:porta). Novo `url_acesso` respeita o esquema do `APP_HOST` (sem esquema = http). Defina `APP_HOST` com o IP/host acessível pelo celular.
|
- **QR code do extrato não abria:** URL estava fixa em `https://`, mas o servidor roda em **http** (IP:porta). Novo `url_acesso` respeita o esquema do `APP_HOST` (sem esquema = http). Defina `APP_HOST` com o IP/host acessível pelo celular.
|
||||||
|
|
||||||
### 🆕 Funcionalidades
|
### 🆕 Funcionalidades
|
||||||
- **NF + Endereço no relatório PDF:** colunas separadas; a coluna **Tracking** foi substituída por **Veículo** (relatório e tela de revisão / passo 3).
|
- **NF + Endereço no relatório PDF:** colunas separadas; a coluna **Tracking** foi substituída por **Veículo** (relatório e tela de revisão / passo 3).
|
||||||
- **Endereço nas telas da consolidação:** linha de endereço nos cards do passo 2 (validação) e coluna Endereço no passo 3 (revisão, paginado 20/50/100).
|
- **Endereço nas telas da consolidação:** linha de endereço nos cards do passo 2 (validação) e coluna Endereço no passo 3 (revisão, paginado 20/50/100).
|
||||||
- **Campos de assinatura nos PDFs:** bloco com assinatura do **Motorista** e do **Administrador** (Reem Transporte) no relatório e no holerite (`BasePdf#assinaturas`).
|
- **Campos de assinatura nos PDFs:** bloco com assinatura do **Motorista** e do **Administrador** (Reem Transporte) no relatório e no extrato (`BasePdf#assinaturas`).
|
||||||
- **Selecionar todos** no passo 2: checkbox que marca/desmarca todas as entregas de uma vez.
|
- **Selecionar todos** no passo 2: checkbox que marca/desmarca todas as entregas de uma vez.
|
||||||
- **Desselecionar em massa:** toggle "Modo remover" — os botões Normal/Retirada/Bônus/Desconto (e "marcar todos") passam a **remover** o pilar das entregas selecionadas (`classificar_em_massa` aceita `acao: remover`).
|
- **Desselecionar em massa:** toggle "Modo remover" — os botões Normal/Retirada/Bônus/Desconto (e "marcar todos") passam a **remover** o pilar das entregas selecionadas (`classificar_em_massa` aceita `acao: remover`).
|
||||||
- **Botões de voltar** entre os passos do wizard (2→1, 3→2) viraram botões visíveis (antes eram texto cinza discreto).
|
- **Botões de voltar** entre os passos do wizard (2→1, 3→2) viraram botões visíveis (antes eram texto cinza discreto).
|
||||||
@@ -575,7 +575,7 @@ Conjunto de melhorias para deixar o sistema pronto para entrega final.
|
|||||||
scopes `pagamento_pago`/`pagamento_pendente`/`pagamento_parcial` (filtro da lista).
|
scopes `pagamento_pago`/`pagamento_pendente`/`pagamento_parcial` (filtro da lista).
|
||||||
- Tela **show**: marcar motorista individual **ou a operação inteira** (com `<select>` de forma);
|
- Tela **show**: marcar motorista individual **ou a operação inteira** (com `<select>` de forma);
|
||||||
estorno. Ações `registrar_pagamento`/`cancelar_pagamento` (admin/gerente).
|
estorno. Ações `registrar_pagamento`/`cancelar_pagamento` (admin/gerente).
|
||||||
- Helper `badge_pagamento`; filtro pago/pendente na **index**; selo no **holerite**.
|
- Helper `badge_pagamento`; filtro pago/pendente na **index**; selo no **extrato**.
|
||||||
- **Notificação de pagamento**: `NotificacaoService.notificar_pagamento` (WhatsApp + e-mail
|
- **Notificação de pagamento**: `NotificacaoService.notificar_pagamento` (WhatsApp + e-mail
|
||||||
`ConsolidacaoMailer#pagamento_efetuado`) — falhas só logam.
|
`ConsolidacaoMailer#pagamento_efetuado`) — falhas só logam.
|
||||||
- **Aviso in-app no painel do motorista**: faixa "Pagamento confirmado" (últimos 7 dias) + selo
|
- **Aviso in-app no painel do motorista**: faixa "Pagamento confirmado" (últimos 7 dias) + selo
|
||||||
@@ -604,11 +604,11 @@ Conjunto de melhorias para deixar o sistema pronto para entrega final.
|
|||||||
- Aplicado em `contar_pagas`, elegibilidade do wizard, gráfico principal (`DATE(checkout)`) e na
|
- Aplicado em `contar_pagas`, elegibilidade do wizard, gráfico principal (`DATE(checkout)`) e na
|
||||||
exibição da data das entregas.
|
exibição da data das entregas.
|
||||||
|
|
||||||
### 🧾 PDFs (Relatório e Holerite)
|
### 🧾 PDFs (Relatório e Extrato)
|
||||||
- **Relatório**: endereço completo com quebra de linha, larguras de coluna fixas, quebra de
|
- **Relatório**: endereço completo com quebra de linha, larguras de coluna fixas, quebra de
|
||||||
página antes do RESUMO. **Rodapé via `canvas`** (na margem inferior — não sobrepõe mais o
|
página antes do RESUMO. **Rodapé via `canvas`** (na margem inferior — não sobrepõe mais o
|
||||||
conteúdo); margem inferior 72.
|
conteúdo); margem inferior 72.
|
||||||
- **Holerite**: coluna **Unit.**, os 4 pilares sempre exibidos, **total de entregas + veículos**,
|
- **Extrato**: coluna **Unit.**, os 4 pilares sempre exibidos, **total de entregas + veículos**,
|
||||||
**selo de pagamento** (PAGO/PENDENTE) e **garantia de espaço para o QR Code**
|
**selo de pagamento** (PAGO/PENDENTE) e **garantia de espaço para o QR Code**
|
||||||
(`start_new_page` se faltar espaço — o Prawn não pagina imagem sozinho).
|
(`start_new_page` se faltar espaço — o Prawn não pagina imagem sozinho).
|
||||||
|
|
||||||
@@ -645,24 +645,24 @@ Aplicar com: `docker-compose exec app bundle exec rails db:migrate`
|
|||||||
---
|
---
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>🔧 Correções — Holerite PDF (18/06/2026)</strong></summary>
|
<summary><strong>🔧 Correções — Extrato PDF (18/06/2026)</strong></summary>
|
||||||
|
|
||||||
### 🔴 Bugs corrigidos
|
### 🔴 Bugs corrigidos
|
||||||
- **500 ao gerar holerite** (`gerar_pdf_holerite`): `Prawn::Errors::IncompatibleStringEncoding`
|
- **500 ao gerar extrato** (`gerar_pdf_extrato`): `Prawn::Errors::IncompatibleStringEncoding`
|
||||||
(`Encoding::UndefinedConversionError`). Os PDFs usavam a fonte **embutida** do Prawn
|
(`Encoding::UndefinedConversionError`). Os PDFs usavam a fonte **embutida** do Prawn
|
||||||
(Helvetica/AFM), que só aceita o charset **Windows-1252** — o caractere **`✓`** do selo
|
(Helvetica/AFM), que só aceita o charset **Windows-1252** — o caractere **`✓`** do selo
|
||||||
"✓ PAGO" não existe nesse charset e derrubava a geração.
|
"✓ PAGO" não existe nesse charset e derrubava a geração.
|
||||||
✅ **Corrigido:** `BasePdf` agora registra a família **DejaVu Sans (TTF)** e a define como
|
✅ **Corrigido:** `BasePdf` agora registra a família **DejaVu Sans (TTF)** e a define como
|
||||||
fonte padrão (`registrar_fonte_utf8`), com suporte total a **UTF-8**. Vale para todos os PDFs
|
fonte padrão (`registrar_fonte_utf8`), com suporte total a **UTF-8**. Vale para todos os PDFs
|
||||||
(holerite e relatório) — qualquer caractere Unicode passa a funcionar.
|
(extrato e relatório) — qualquer caractere Unicode passa a funcionar.
|
||||||
- Fontes versionadas em `app/assets/fonts/DejaVuSans.ttf` e `DejaVuSans-Bold.ttf`
|
- Fontes versionadas em `app/assets/fonts/DejaVuSans.ttf` e `DejaVuSans-Bold.ttf`
|
||||||
(a imagem Docker `ruby:3.2.2-slim` não traz fontes do sistema; o `COPY . .` do Dockerfile e
|
(a imagem Docker `ruby:3.2.2-slim` não traz fontes do sistema; o `COPY . .` do Dockerfile e
|
||||||
o volume `.:/app` garantem que estejam disponíveis em runtime).
|
o volume `.:/app` garantem que estejam disponíveis em runtime).
|
||||||
- **Assinaturas do holerite caíam na 2ª página:** após o QR Code o cursor ficava abaixo do
|
- **Assinaturas do extrato caíam na 2ª página:** após o QR Code o cursor ficava abaixo do
|
||||||
limite de quebra (`start_new_page if cursor < 120`), empurrando o bloco de assinaturas para
|
limite de quebra (`start_new_page if cursor < 120`), empurrando o bloco de assinaturas para
|
||||||
uma página nova. ✅ **Corrigido:** espaçamentos compactados com segurança (padding das
|
uma página nova. ✅ **Corrigido:** espaçamentos compactados com segurança (padding das
|
||||||
tabelas `[4,8]`, QR Code menor) + folga antes das assinaturas reduzida (`BasePdf#assinaturas`,
|
tabelas `[4,8]`, QR Code menor) + folga antes das assinaturas reduzida (`BasePdf#assinaturas`,
|
||||||
60 → 36, limite de quebra 120 → 100) — **o holerite agora cabe em uma única página**.
|
60 → 36, limite de quebra 120 → 100) — **o extrato agora cabe em uma única página**.
|
||||||
- **QR Code sobrepondo / sumindo:** o posicionamento por `move_up`/`move_down` era frágil e,
|
- **QR Code sobrepondo / sumindo:** o posicionamento por `move_up`/`move_down` era frágil e,
|
||||||
com a fonte nova, fazia o QR se sobrepor ao texto/assinaturas. ✅ **Corrigido:**
|
com a fonte nova, fazia o QR se sobrepor ao texto/assinaturas. ✅ **Corrigido:**
|
||||||
`desenhar_qrcode` agora desenha a imagem com **posição absoluta (`at:`)** — que não move o
|
`desenhar_qrcode` agora desenha a imagem com **posição absoluta (`at:`)** — que não move o
|
||||||
@@ -678,9 +678,9 @@ Aplicar com: `docker-compose exec app bundle exec rails db:migrate`
|
|||||||
(dado externo) e `users.nome` (digitado no admin). ✅ `ConsolidacoesController#buscar_user_motorista`
|
(dado externo) e `users.nome` (digitado no admin). ✅ `ConsolidacoesController#buscar_user_motorista`
|
||||||
agora tenta o match exato e, se falhar, compara nomes **normalizados** (sem espaços
|
agora tenta o match exato e, se falhar, compara nomes **normalizados** (sem espaços
|
||||||
extras/duplos, case-insensitive).
|
extras/duplos, case-insensitive).
|
||||||
- **Motorista sem conta:** ✅ em vez de omitir, o holerite agora desenha um **aviso**
|
- **Motorista sem conta:** ✅ em vez de omitir, o extrato agora desenha um **aviso**
|
||||||
("Cadastro de acesso ainda não disponível… solicite ao administrador") no lugar do QR
|
("Cadastro de acesso ainda não disponível… solicite ao administrador") no lugar do QR
|
||||||
(`HoleritePdf#desenhar_aviso_sem_acesso`). O QR só existe para motoristas **com cadastro**
|
(`ExtratoPdf#desenhar_aviso_sem_acesso`). O QR só existe para motoristas **com cadastro**
|
||||||
no site — comportamento esperado, já que sem conta não há painel para acessar.
|
no site — comportamento esperado, já que sem conta não há painel para acessar.
|
||||||
|
|
||||||
### 📌 Padrão (novo)
|
### 📌 Padrão (novo)
|
||||||
@@ -757,7 +757,7 @@ docker-compose exec app bundle exec rails db:migrate
|
|||||||
### 🆕 Funcionalidades
|
### 🆕 Funcionalidades
|
||||||
- **Pilar "Entrega Extraordinária"** — preço coringa para entregas fora do planejamento.
|
- **Pilar "Entrega Extraordinária"** — preço coringa para entregas fora do planejamento.
|
||||||
- Novo valor `extraordinaria` no `enum tipo` de `ConsolidacaoEntrega` (soma positiva, como Bônus). Sem migration de enum (coluna `integer`).
|
- Novo valor `extraordinaria` no `enum tipo` de `ConsolidacaoEntrega` (soma positiva, como Bônus). Sem migration de enum (coluna `integer`).
|
||||||
- Aparece em **todas** as telas dos pilares: wizard (passo 1), validação, revisão, nota avulsa, holerite, PDFs e composição do dashboard.
|
- Aparece em **todas** as telas dos pilares: wizard (passo 1), validação, revisão, nota avulsa, extrato, PDFs e composição do dashboard.
|
||||||
- **Preço padrão configurável** em *Configurações* (`preco_extraordinaria`) **e** **valor customizável por entrega**: ao marcar "Extra" no wizard abre um prompt já preenchido com o padrão (`valor_classificacao` no controller + `pedirValor()` no Stimulus).
|
- **Preço padrão configurável** em *Configurações* (`preco_extraordinaria`) **e** **valor customizável por entrega**: ao marcar "Extra" no wizard abre um prompt já preenchido com o padrão (`valor_classificacao` no controller + `pedirValor()` no Stimulus).
|
||||||
- **Card "Consolidado / Pago"** no topo do dashboard (total consolidado + total já pago do período; reusa `@fin_custo_total` / `@fin_pago`).
|
- **Card "Consolidado / Pago"** no topo do dashboard (total consolidado + total já pago do período; reusa `@fin_custo_total` / `@fin_pago`).
|
||||||
- **Entregas falhadas no dashboard** — o card "Total Entregas" agora separa `entregues · pendentes · falhadas`.
|
- **Entregas falhadas no dashboard** — o card "Total Entregas" agora separa `entregues · pendentes · falhadas`.
|
||||||
@@ -776,7 +776,7 @@ docker-compose exec app bundle exec rails db:migrate
|
|||||||
- **R$ "quebrado" no painel do motorista** (celular) — os valores grandes quebravam "R$" e o número em linhas separadas. Adicionado `whitespace-nowrap` + fonte responsiva (`text-4xl sm:text-5xl`) nos cards.
|
- **R$ "quebrado" no painel do motorista** (celular) — os valores grandes quebravam "R$" e o número em linhas separadas. Adicionado `whitespace-nowrap` + fonte responsiva (`text-4xl sm:text-5xl`) nos cards.
|
||||||
|
|
||||||
### 🔐 Segurança
|
### 🔐 Segurança
|
||||||
- **QR Code do holerite não loga mais direto.** Antes, ler o QR (`/motorista/acesso/:token`) autenticava o motorista sem PIN. Agora o QR apenas **identifica** o motorista e leva à tela de PIN; ele precisa digitar o **PIN de 4 dígitos** para entrar — e o PIN tem que ser **do mesmo motorista do QR** (impede usar o QR de outra pessoa). A tela saúda pelo nome quando vem do QR.
|
- **QR Code do extrato não loga mais direto.** Antes, ler o QR (`/motorista/acesso/:token`) autenticava o motorista sem PIN. Agora o QR apenas **identifica** o motorista e leva à tela de PIN; ele precisa digitar o **PIN de 4 dígitos** para entrar — e o PIN tem que ser **do mesmo motorista do QR** (impede usar o QR de outra pessoa). A tela saúda pelo nome quando vem do QR.
|
||||||
|
|
||||||
### 🆕 Migration adicionada (rodar `db:migrate`)
|
### 🆕 Migration adicionada (rodar `db:migrate`)
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -159,5 +159,5 @@ docker-compose up -d
|
|||||||
| **4** | Job Whenever (1h) + HistoricoEstimado + AuditoriaLog UI |
|
| **4** | Job Whenever (1h) + HistoricoEstimado + AuditoriaLog UI |
|
||||||
| **5** | Nova Consolidação + Wizard Passo 1 (selecionar motorista) |
|
| **5** | Nova Consolidação + Wizard Passo 1 (selecionar motorista) |
|
||||||
| **6** | Toggle switches + ações em massa + barra progresso (Stimulus) |
|
| **6** | Toggle switches + ações em massa + barra progresso (Stimulus) |
|
||||||
| **7** | PDFs com Prawn: relatório individual + holerite com QR code |
|
| **7** | PDFs com Prawn: relatório individual + extrato com QR code |
|
||||||
| **8** | Notificações WhatsApp/email + responsividade final |
|
| **8** | Notificações WhatsApp/email + responsividade final |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class ConsolidacoesController < ApplicationController
|
|||||||
before_action :set_consolidacao,
|
before_action :set_consolidacao,
|
||||||
only: %i[show edit update destroy finalizar arquivar reativar wizard
|
only: %i[show edit update destroy finalizar arquivar reativar wizard
|
||||||
registrar_pagamento cancelar_pagamento
|
registrar_pagamento cancelar_pagamento
|
||||||
preview_holerite gerar_pdf_relatorio gerar_pdf_holerite
|
preview_extrato gerar_pdf_relatorio gerar_pdf_extrato
|
||||||
gerar_pdf_financeiro]
|
gerar_pdf_financeiro]
|
||||||
|
|
||||||
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
|
# Formas de pagamento aceitas (o select da UI usa esta mesma lista).
|
||||||
@@ -234,13 +234,22 @@ class ConsolidacoesController < ApplicationController
|
|||||||
forma = params[:forma_pagamento].presence_in(FORMAS_PAGAMENTO) || 'pix'
|
forma = params[:forma_pagamento].presence_in(FORMAS_PAGAMENTO) || 'pix'
|
||||||
alvos = motoristas_alvo.select { |cm| !cm.pago? }
|
alvos = motoristas_alvo.select { |cm| !cm.pago? }
|
||||||
|
|
||||||
|
# NF obrigatória por motorista. Se faltar para qualquer alvo, aborta sem
|
||||||
|
# marcar nenhum (a NF vai pro painel do motorista e pro Extrato).
|
||||||
|
notas = nf_por_motorista(alvos)
|
||||||
|
if notas.nil?
|
||||||
|
return redirect_to @consolidacao,
|
||||||
|
alert: 'Informe o número da Nota Fiscal de cada motorista antes de marcar como pago.'
|
||||||
|
end
|
||||||
|
|
||||||
alvos.each do |cm|
|
alvos.each do |cm|
|
||||||
cm.marcar_pago!(current_user, forma: forma)
|
cm.marcar_pago!(current_user, forma: forma, nota_fiscal: notas[cm.id])
|
||||||
NotificacaoService.notificar_pagamento(@consolidacao, cm)
|
NotificacaoService.notificar_pagamento(@consolidacao, cm)
|
||||||
end
|
end
|
||||||
|
|
||||||
auditar!(:registrar_pagamento, @consolidacao,
|
auditar!(:registrar_pagamento, @consolidacao,
|
||||||
dados_novos: { motoristas: alvos.map(&:motorista_nome), forma: forma })
|
dados_novos: { motoristas: alvos.map(&:motorista_nome), forma: forma,
|
||||||
|
notas_fiscais: alvos.map { |cm| [cm.motorista_nome, notas[cm.id]] }.to_h })
|
||||||
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :pago)
|
redirect_to @consolidacao, notice: pagamento_notice(alvos.size, :pago)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -285,27 +294,28 @@ class ConsolidacoesController < ApplicationController
|
|||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /consolidacoes/:id/gerar_pdf_holerite?motorista=X
|
# GET /consolidacoes/:id/gerar_pdf_extrato?motorista=X
|
||||||
def gerar_pdf_holerite
|
def gerar_pdf_extrato
|
||||||
autorizar_holerite!(params[:motorista])
|
autorizar_extrato!(params[:motorista])
|
||||||
motorista = params[:motorista]
|
motorista = params[:motorista]
|
||||||
user_mot = buscar_user_motorista(motorista)
|
user_mot = buscar_user_motorista(motorista)
|
||||||
user_mot&.regenerar_login_token! if user_mot&.login_token.blank?
|
user_mot&.regenerar_login_token! if user_mot&.login_token.blank?
|
||||||
cm = @consolidacao.consolidacao_motoristas.find_by(motorista_nome: motorista)
|
cm = @consolidacao.consolidacao_motoristas.find_by(motorista_nome: motorista)
|
||||||
|
|
||||||
pdf = Pdf::HoleritePdf.new(consolidacao: @consolidacao, motorista: motorista,
|
pdf = Pdf::ExtratoPdf.new(consolidacao: @consolidacao, motorista: motorista,
|
||||||
user_motorista: user_mot, app_host: ENV['APP_HOST'],
|
user_motorista: user_mot, app_host: ENV['APP_HOST'],
|
||||||
pago_em: cm&.pago_em, forma_pagamento: cm&.forma_pagamento)
|
pago_em: cm&.pago_em, forma_pagamento: cm&.forma_pagamento,
|
||||||
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'holerite', motorista: motorista })
|
nota_fiscal: cm&.nota_fiscal)
|
||||||
|
auditar!(:editar, @consolidacao, dados_novos: { pdf: 'extrato', motorista: motorista })
|
||||||
|
|
||||||
send_data pdf.render,
|
send_data pdf.render,
|
||||||
filename: "holerite_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
|
filename: "extrato_#{motorista.parameterize}_#{@consolidacao.id}.pdf",
|
||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /consolidacoes/:id/preview_holerite?motorista=X — preview HTML antes do PDF
|
# GET /consolidacoes/:id/preview_extrato?motorista=X — preview HTML antes do PDF
|
||||||
def preview_holerite
|
def preview_extrato
|
||||||
authorize @consolidacao, :show?
|
authorize @consolidacao, :show?
|
||||||
@motorista = params[:motorista]
|
@motorista = params[:motorista]
|
||||||
@entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
|
@entregas = @consolidacao.consolidacao_entregas.where(motorista_nome: @motorista)
|
||||||
@@ -319,7 +329,7 @@ class ConsolidacoesController < ApplicationController
|
|||||||
# Acha o User motorista pelo nome para gerar o QR Code de acesso. O
|
# Acha o User motorista pelo nome para gerar o QR Code de acesso. O
|
||||||
# motorista_nome vem dos dados externos de entrega e o users.nome é digitado
|
# motorista_nome vem dos dados externos de entrega e o users.nome é digitado
|
||||||
# no admin — então toleramos diferenças de caixa e de espaços (extras/duplos).
|
# no admin — então toleramos diferenças de caixa e de espaços (extras/duplos).
|
||||||
# Sem isso, qualquer divergência deixava user_mot nil e o QR sumia do holerite.
|
# Sem isso, qualquer divergência deixava user_mot nil e o QR sumia do extrato.
|
||||||
def buscar_user_motorista(nome)
|
def buscar_user_motorista(nome)
|
||||||
exato = User.motorista.find_by('LOWER(nome) = ?', nome.to_s.downcase.strip)
|
exato = User.motorista.find_by('LOWER(nome) = ?', nome.to_s.downcase.strip)
|
||||||
return exato if exato
|
return exato if exato
|
||||||
@@ -332,9 +342,9 @@ class ConsolidacoesController < ApplicationController
|
|||||||
nome.to_s.strip.downcase.gsub(/\s+/, ' ')
|
nome.to_s.strip.downcase.gsub(/\s+/, ' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Staff (show?) baixa qualquer holerite. O motorista baixa só o PRÓPRIO — o PDF
|
# Staff (show?) baixa qualquer extrato. O motorista baixa só o PRÓPRIO — o PDF
|
||||||
# já é filtrado pelo nome dele, então não há vazamento de dados de terceiros.
|
# já é filtrado pelo nome dele, então não há vazamento de dados de terceiros.
|
||||||
def autorizar_holerite!(motorista_nome)
|
def autorizar_extrato!(motorista_nome)
|
||||||
if current_user.motorista?
|
if current_user.motorista?
|
||||||
raise Pundit::NotAuthorizedError unless motorista_nome.to_s.casecmp?(current_user.nome.to_s)
|
raise Pundit::NotAuthorizedError unless motorista_nome.to_s.casecmp?(current_user.nome.to_s)
|
||||||
skip_authorization
|
skip_authorization
|
||||||
@@ -356,6 +366,18 @@ class ConsolidacoesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Mapeia { cm.id => "numero da NF" } a partir de params[:notas_fiscais].
|
||||||
|
# Retorna nil se a NF de QUALQUER alvo estiver em branco (pagamento bloqueado).
|
||||||
|
def nf_por_motorista(alvos)
|
||||||
|
informadas = params[:notas_fiscais] || {}
|
||||||
|
alvos.each_with_object({}) do |cm, mapa|
|
||||||
|
nf = informadas[cm.id.to_s].to_s.strip
|
||||||
|
return nil if nf.blank?
|
||||||
|
|
||||||
|
mapa[cm.id] = nf
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def pagamento_indisponivel
|
def pagamento_indisponivel
|
||||||
redirect_to @consolidacao, alert: 'Só é possível registrar pagamento de uma consolidação finalizada.'
|
redirect_to @consolidacao, alert: 'Só é possível registrar pagamento de uma consolidação finalizada.'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -100,11 +100,10 @@ class DashboardController < ApplicationController
|
|||||||
# Valor estimado total
|
# Valor estimado total
|
||||||
@valor_estimado = (@entregas_pagas * config[:entrega]).round(2)
|
@valor_estimado = (@entregas_pagas * config[:entrega]).round(2)
|
||||||
|
|
||||||
# Por motorista (top 10)
|
# Por motorista (todos)
|
||||||
@motoristas = pagas.group(:driver)
|
@motoristas = pagas.group(:driver)
|
||||||
.count
|
.count
|
||||||
.sort_by { |_, v| -v }
|
.sort_by { |_, v| -v }
|
||||||
.first(10)
|
|
||||||
.map do |driver, qtd|
|
.map do |driver, qtd|
|
||||||
{
|
{
|
||||||
nome: driver,
|
nome: driver,
|
||||||
@@ -214,9 +213,9 @@ class DashboardController < ApplicationController
|
|||||||
{ label: cfg[:label], valor: valor }
|
{ label: cfg[:label], valor: valor }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custo por motorista (top 10)
|
# Custo por motorista (todos)
|
||||||
@fin_por_motorista = cms.group(:motorista_nome).sum(:valor_total)
|
@fin_por_motorista = cms.group(:motorista_nome).sum(:valor_total)
|
||||||
.sort_by { |_, v| -v }.first(10)
|
.sort_by { |_, v| -v }
|
||||||
end
|
end
|
||||||
|
|
||||||
# `pagas` já vem filtrado por checkout no período. Agrupa pela DATA REAL
|
# `pagas` já vem filtrado por checkout no período. Agrupa pela DATA REAL
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# app/controllers/motorista/sessoes_controller.rb
|
# app/controllers/motorista/sessoes_controller.rb
|
||||||
#
|
#
|
||||||
# Login do motorista por PIN de 4 dígitos ou por QR Code (token do holerite).
|
# Login do motorista por PIN de 4 dígitos ou por QR Code (token do extrato).
|
||||||
# Não usa Devise password — fluxo simplificado para público de baixo nível técnico.
|
# Não usa Devise password — fluxo simplificado para público de baixo nível técnico.
|
||||||
#
|
#
|
||||||
module Motorista
|
module Motorista
|
||||||
@@ -44,7 +44,7 @@ module Motorista
|
|||||||
redirect_to motorista_dashboard_path, notice: "Bem-vindo, #{user.nome.split.first}!"
|
redirect_to motorista_dashboard_path, notice: "Bem-vindo, #{user.nome.split.first}!"
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /motorista/acesso/:token — abre o login a partir do QR Code do holerite.
|
# GET /motorista/acesso/:token — abre o login a partir do QR Code do extrato.
|
||||||
# SEGURANÇA: o QR apenas identifica o motorista; ele ainda precisa digitar o
|
# SEGURANÇA: o QR apenas identifica o motorista; ele ainda precisa digitar o
|
||||||
# PIN de 4 dígitos para entrar (antes o QR logava direto, sem PIN).
|
# PIN de 4 dígitos para entrar (antes o QR logava direto, sem PIN).
|
||||||
def acesso_qr
|
def acesso_qr
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ class ConsolidacaoMotorista < ApplicationRecord
|
|||||||
pago_em.present?
|
pago_em.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def marcar_pago!(user, forma:)
|
def marcar_pago!(user, forma:, nota_fiscal:)
|
||||||
update!(pago_em: Time.current, pago_por: user&.id, forma_pagamento: forma)
|
update!(pago_em: Time.current, pago_por: user&.id, forma_pagamento: forma,
|
||||||
|
nota_fiscal: nota_fiscal)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cancelar_pagamento!(_user)
|
def cancelar_pagamento!(_user)
|
||||||
update!(pago_em: nil, pago_por: nil, forma_pagamento: nil)
|
update!(pago_em: nil, pago_por: nil, forma_pagamento: nil, nota_fiscal: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def recalcular_valor
|
def recalcular_valor
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class User < ApplicationRecord
|
|||||||
admin? || gerente? || operador?
|
admin? || gerente? || operador?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Token de login rápido via QR Code (holerite) — Fase 7/8
|
# Token de login rápido via QR Code (extrato) — Fase 7/8
|
||||||
def regenerar_login_token!
|
def regenerar_login_token!
|
||||||
update!(login_token: SecureRandom.urlsafe_base64(24))
|
update!(login_token: SecureRandom.urlsafe_base64(24))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class NotificacaoService
|
|||||||
"Seu pagamento de entregas foi fechado.\n" \
|
"Seu pagamento de entregas foi fechado.\n" \
|
||||||
"📋 #{@consolidacao.nome}\n" \
|
"📋 #{@consolidacao.nome}\n" \
|
||||||
"💰 Valor: #{valor}\n" \
|
"💰 Valor: #{valor}\n" \
|
||||||
"Acesse seu painel para baixar o holerite: " \
|
"Acesse seu painel para baixar o extrato: " \
|
||||||
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
"https://#{ENV.fetch('APP_HOST', 'localhost:3000')}/motorista"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# app/services/pdf/holerite_pdf.rb
|
# app/services/pdf/extrato_pdf.rb
|
||||||
#
|
#
|
||||||
# Holerite: resumo do valor a pagar ao motorista, estilo contracheque,
|
# Extrato: resumo do valor a pagar ao motorista, estilo contracheque,
|
||||||
# com QR Code para login rápido no painel do motorista.
|
# com QR Code para login rápido no painel do motorista.
|
||||||
#
|
#
|
||||||
require 'rqrcode'
|
require 'rqrcode'
|
||||||
|
|
||||||
module Pdf
|
module Pdf
|
||||||
class HoleritePdf < BasePdf
|
class ExtratoPdf < BasePdf
|
||||||
VERDE = '22C55E'
|
VERDE = '22C55E'
|
||||||
CINZA_BG = 'E5E7EB'
|
CINZA_BG = 'E5E7EB'
|
||||||
|
|
||||||
def initialize(consolidacao:, motorista:, user_motorista: nil, app_host: nil,
|
def initialize(consolidacao:, motorista:, user_motorista: nil, app_host: nil,
|
||||||
pago_em: nil, forma_pagamento: nil)
|
pago_em: nil, forma_pagamento: nil, nota_fiscal: nil)
|
||||||
super()
|
super()
|
||||||
@consolidacao = consolidacao
|
@consolidacao = consolidacao
|
||||||
@motorista = motorista
|
@motorista = motorista
|
||||||
@@ -19,13 +19,14 @@ module Pdf
|
|||||||
@app_host = app_host || ENV.fetch('APP_HOST', 'localhost:3000')
|
@app_host = app_host || ENV.fetch('APP_HOST', 'localhost:3000')
|
||||||
@pago_em = pago_em
|
@pago_em = pago_em
|
||||||
@forma_pagamento = forma_pagamento
|
@forma_pagamento = forma_pagamento
|
||||||
|
@nota_fiscal = nota_fiscal
|
||||||
@entregas = consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
|
@entregas = consolidacao.consolidacao_entregas.where(motorista_nome: motorista)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def corpo
|
def corpo
|
||||||
secao "HOLERITE DE PAGAMENTO — ENTREGAS"
|
secao "EXTRATO DE PAGAMENTO — ENTREGAS"
|
||||||
|
|
||||||
# Bloco de identificação (estilo contracheque)
|
# Bloco de identificação (estilo contracheque)
|
||||||
info = [
|
info = [
|
||||||
@@ -34,6 +35,7 @@ module Pdf
|
|||||||
['Período', "#{@consolidacao.data_inicio.strftime('%d/%m/%Y')} a #{@consolidacao.data_fim.strftime('%d/%m/%Y')}"],
|
['Período', "#{@consolidacao.data_inicio.strftime('%d/%m/%Y')} a #{@consolidacao.data_fim.strftime('%d/%m/%Y')}"],
|
||||||
['Emissão', Time.current.strftime('%d/%m/%Y')]
|
['Emissão', Time.current.strftime('%d/%m/%Y')]
|
||||||
]
|
]
|
||||||
|
info << ['Nota Fiscal', @nota_fiscal] if @nota_fiscal.present?
|
||||||
@pdf.table(info, width: @pdf.bounds.width, cell_style: { size: 10, padding: [4, 8] }) do |t|
|
@pdf.table(info, width: @pdf.bounds.width, cell_style: { size: 10, padding: [4, 8] }) do |t|
|
||||||
t.columns(0).font_style = :bold
|
t.columns(0).font_style = :bold
|
||||||
t.columns(0).background_color = 'F3F4F6'
|
t.columns(0).background_color = 'F3F4F6'
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
<p style="text-align: center; margin: 24px 0;">
|
<p style="text-align: center; margin: 24px 0;">
|
||||||
<a href="<%= @link_painel %>"
|
<a href="<%= @link_painel %>"
|
||||||
style="background: #0a0a0a; color: #f97316; text-decoration: none; padding: 14px 28px; border-radius: 8px; font-weight: bold; display: inline-block;">
|
style="background: #0a0a0a; color: #f97316; text-decoration: none; padding: 14px 28px; border-radius: 8px; font-weight: bold; display: inline-block;">
|
||||||
Ver meu painel e baixar holerite →
|
Ver meu painel e baixar extrato →
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="color: #9ca3af; font-size: 12px; text-align: center;">
|
<p style="color: #9ca3af; font-size: 12px; text-align: center;">
|
||||||
Entre com seu PIN de 4 números ou escaneie o QR Code do seu holerite.
|
Entre com seu PIN de 4 números ou escaneie o QR Code do seu extrato.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
<p style="text-align: center; margin: 24px 0;">
|
<p style="text-align: center; margin: 24px 0;">
|
||||||
<a href="<%= @link_painel %>"
|
<a href="<%= @link_painel %>"
|
||||||
style="background: #0a0a0a; color: #f97316; text-decoration: none; padding: 14px 28px; border-radius: 8px; font-weight: bold; display: inline-block;">
|
style="background: #0a0a0a; color: #f97316; text-decoration: none; padding: 14px 28px; border-radius: 8px; font-weight: bold; display: inline-block;">
|
||||||
Ver meu painel e baixar holerite →
|
Ver meu painel e baixar extrato →
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="color: #9ca3af; font-size: 12px; text-align: center;">
|
<p style="color: #9ca3af; font-size: 12px; text-align: center;">
|
||||||
Entre com seu PIN de 4 números ou escaneie o QR Code do seu holerite.
|
Entre com seu PIN de 4 números ou escaneie o QR Code do seu extrato.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%# app/views/consolidacoes/preview_holerite.html.erb — renderizado sem layout, dentro do modal %>
|
<%# app/views/consolidacoes/preview_extrato.html.erb — renderizado sem layout, dentro do modal %>
|
||||||
<div class="bg-white text-black rounded-xl p-6 font-sans">
|
<div class="bg-white text-black rounded-xl p-6 font-sans">
|
||||||
|
|
||||||
<%# Cabeçalho estilo PDF %>
|
<%# Cabeçalho estilo PDF %>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<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">REEM TRANSPORTE</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">Extrato de Pagamento — Entregas</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,12 +51,14 @@
|
|||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 mt-4 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2 mt-4 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-4">
|
||||||
<span class="text-gray-400 text-sm font-medium mr-1">💵 Operação inteira:</span>
|
<span class="text-gray-400 text-sm font-medium mr-1">💵 Operação inteira:</span>
|
||||||
<% if @consolidacao.status_pagamento != :pago %>
|
<% if @consolidacao.status_pagamento != :pago %>
|
||||||
|
<%# Form único (id) — os inputs de NF e os botões "Marcar pago" de cada
|
||||||
|
linha se associam a ele via atributo HTML5 form=, evitando <form> aninhado. %>
|
||||||
<%= form_with url: registrar_pagamento_consolidacao_path(@consolidacao), method: :post,
|
<%= form_with url: registrar_pagamento_consolidacao_path(@consolidacao), method: :post,
|
||||||
data: { turbo_confirm: 'Marcar TODOS os motoristas pendentes desta consolidação como PAGOS?' },
|
id: 'form-pagar-tudo', class: 'flex items-center gap-2' do |f| %>
|
||||||
class: 'flex items-center gap-2' do |f| %>
|
|
||||||
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
|
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
|
||||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 text-sm' %>
|
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-3 py-2.5 text-sm' %>
|
||||||
<%= f.submit '💰 Marcar tudo como pago',
|
<%= f.submit '💰 Marcar tudo como pago',
|
||||||
|
data: { turbo_confirm: 'Marcar TODOS os motoristas pendentes desta consolidação como PAGOS? Confirme que a Nota Fiscal de cada um foi preenchida.' },
|
||||||
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-4 py-2.5 rounded-lg text-sm cursor-pointer min-h-[44px]' %>
|
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-4 py-2.5 rounded-lg text-sm cursor-pointer min-h-[44px]' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -95,6 +97,9 @@
|
|||||||
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
||||||
<% if cm.pagador.present? %>por <%= cm.pagador.nome_display %><% end %>
|
<% if cm.pagador.present? %>por <%= cm.pagador.nome_display %><% end %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if cm.nota_fiscal.present? %>
|
||||||
|
<p class="text-gray-300 text-xs mt-0.5">🧾 NF <%= cm.nota_fiscal %></p>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if pode_pagar %>
|
<% if pode_pagar %>
|
||||||
@@ -104,21 +109,23 @@
|
|||||||
data: { turbo_confirm: "Estornar o pagamento de #{cm.motorista_nome}?" },
|
data: { turbo_confirm: "Estornar o pagamento de #{cm.motorista_nome}?" },
|
||||||
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
|
class: 'text-gray-400 hover:text-white border border-[#2a2a2a] px-3 py-2 rounded-lg text-xs min-h-[40px]' %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= form_with url: registrar_pagamento_consolidacao_path(@consolidacao, consolidacao_motorista_id: cm.id), method: :post,
|
<%# Inputs/botão associados ao form único #form-pagar-tudo via atributo form=. %>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<%= text_field_tag "notas_fiscais[#{cm.id}]", nil, form: 'form-pagar-tudo',
|
||||||
|
placeholder: 'Nº Nota Fiscal',
|
||||||
|
class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-2 py-2 text-xs w-36' %>
|
||||||
|
<%= button_tag '💰 Marcar pago', type: :submit, form: 'form-pagar-tudo',
|
||||||
|
name: 'consolidacao_motorista_id', value: cm.id,
|
||||||
data: { turbo_confirm: "Marcar #{cm.motorista_nome} como pago?" },
|
data: { turbo_confirm: "Marcar #{cm.motorista_nome} como pago?" },
|
||||||
class: 'flex items-center gap-2' do |f| %>
|
|
||||||
<%= f.select :forma_pagamento, options_for_select(formas_pagamento, 'pix'),
|
|
||||||
{}, class: 'bg-[#0a0a0a] border border-[#2a2a2a] text-white rounded-lg px-2 py-2 text-xs' %>
|
|
||||||
<%= f.submit '💰 Marcar pago',
|
|
||||||
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2 rounded-lg text-xs cursor-pointer min-h-[40px]' %>
|
class: 'bg-green-600 hover:bg-green-700 text-white font-bold px-3 py-2 rounded-lg text-xs cursor-pointer min-h-[40px]' %>
|
||||||
<% end %>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<%# Preview abre modal %>
|
<%# Preview abre modal %>
|
||||||
<button onclick="abrirPreview('<%= preview_holerite_consolidacao_path(@consolidacao, motorista: cm.motorista_nome) %>')"
|
<button onclick="abrirPreview('<%= preview_extrato_consolidacao_path(@consolidacao, motorista: cm.motorista_nome) %>')"
|
||||||
class="bg-[#0a0a0a] hover:bg-[#2a2a2a] text-white border border-orange-500 font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px]">
|
class="bg-[#0a0a0a] hover:bg-[#2a2a2a] text-white border border-orange-500 font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px]">
|
||||||
👁️ Ver antes de gerar
|
👁️ Ver antes de gerar
|
||||||
</button>
|
</button>
|
||||||
@@ -126,8 +133,8 @@
|
|||||||
gerar_pdf_relatorio_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
|
gerar_pdf_relatorio_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
|
||||||
data: { turbo: false },
|
data: { turbo: false },
|
||||||
class: 'bg-orange-800 hover:bg-orange-700 text-white font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
|
class: 'bg-orange-800 hover:bg-orange-700 text-white font-semibold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
|
||||||
<%= link_to '🧾 Gerar Holerite',
|
<%= link_to '🧾 Gerar Extrato',
|
||||||
gerar_pdf_holerite_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
|
gerar_pdf_extrato_consolidacao_path(@consolidacao, motorista: cm.motorista_nome),
|
||||||
data: { turbo: false },
|
data: { turbo: false },
|
||||||
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
|
class: 'bg-orange-500 hover:bg-orange-600 text-black font-bold px-4 py-3 rounded-lg text-sm min-h-[48px] flex items-center' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +195,7 @@
|
|||||||
<div id="modal-preview" class="fixed inset-0 bg-black/80 z-50 hidden items-center justify-center p-4">
|
<div id="modal-preview" class="fixed inset-0 bg-black/80 z-50 hidden items-center justify-center p-4">
|
||||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl max-w-2xl w-full max-h-[85vh] flex flex-col">
|
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl max-w-2xl w-full max-h-[85vh] flex flex-col">
|
||||||
<div class="flex items-center justify-between px-6 py-4 border-b border-[#2a2a2a]">
|
<div class="flex items-center justify-between px-6 py-4 border-b border-[#2a2a2a]">
|
||||||
<h3 class="text-white font-bold">👁️ Pré-visualização do Holerite</h3>
|
<h3 class="text-white font-bold">👁️ Pré-visualização do Extrato</h3>
|
||||||
<button onclick="fecharPreview()" class="text-gray-400 hover:text-white text-2xl min-w-[44px] min-h-[44px]">✕</button>
|
<button onclick="fecharPreview()" class="text-gray-400 hover:text-white text-2xl min-w-[44px] min-h-[44px]">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-preview-conteudo" class="overflow-y-auto p-6 flex-1">
|
<div id="modal-preview-conteudo" class="overflow-y-auto p-6 flex-1">
|
||||||
@@ -220,7 +227,7 @@
|
|||||||
|
|
||||||
// O link de confirmar aponta para o PDF do mesmo motorista
|
// O link de confirmar aponta para o PDF do mesmo motorista
|
||||||
document.getElementById('modal-confirmar').href =
|
document.getElementById('modal-confirmar').href =
|
||||||
url.replace('preview_holerite', 'gerar_pdf_holerite');
|
url.replace('preview_extrato', 'gerar_pdf_extrato');
|
||||||
}
|
}
|
||||||
function fecharPreview() {
|
function fecharPreview() {
|
||||||
const modal = document.getElementById('modal-preview');
|
const modal = document.getElementById('modal-preview');
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
|
|
||||||
<%# Top motoristas %>
|
<%# Top motoristas %>
|
||||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">🏆 Top Motoristas</h2>
|
<h2 class="text-lg font-semibold text-white mb-4">🏆 Motoristas</h2>
|
||||||
|
|
||||||
<% if @motoristas.any? %>
|
<% if @motoristas.any? %>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<span class="text-white text-sm font-medium truncate"><%= m[:nome].split.first %></span>
|
<span class="text-white text-sm font-medium truncate"><%= m[:nome] %></span>
|
||||||
<span class="text-[#f97316] text-sm font-semibold ml-2 flex-shrink-0">
|
<span class="text-[#f97316] text-sm font-semibold ml-2 flex-shrink-0">
|
||||||
<%= moeda(m[:valor]) %>
|
<%= moeda(m[:valor]) %>
|
||||||
</span>
|
</span>
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
<%# Custo por motorista (barra) %>
|
<%# Custo por motorista (barra) %>
|
||||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||||
<h2 class="text-lg font-semibold text-white mb-1">👤 Custo por motorista</h2>
|
<h2 class="text-lg font-semibold text-white mb-1">👤 Custo por motorista</h2>
|
||||||
<p class="text-gray-400 text-sm mb-4">Top 10 no período</p>
|
<p class="text-gray-400 text-sm mb-4">Por motorista no período</p>
|
||||||
<% if @fin_por_motorista.any? %>
|
<% if @fin_por_motorista.any? %>
|
||||||
<div class="relative h-64"><canvas id="grafico-custo-motorista"></canvas></div>
|
<div class="relative h-64"><canvas id="grafico-custo-motorista"></canvas></div>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<p class="text-gray-300 text-sm">
|
<p class="text-gray-300 text-sm">
|
||||||
💰 <%= l cm.pago_em.to_date, format: :short %>
|
💰 <%= l cm.pago_em.to_date, format: :short %>
|
||||||
<% if cm.forma_pagamento.present? %>· <%= cm.forma_pagamento.humanize %><% end %>
|
<% if cm.forma_pagamento.present? %>· <%= cm.forma_pagamento.humanize %><% end %>
|
||||||
|
<% if cm.nota_fiscal.present? %>· 🧾 NF <%= cm.nota_fiscal %><% end %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -78,11 +79,14 @@
|
|||||||
💰 Pago em <%= l cm.pago_em.to_date, format: :short %>
|
💰 Pago em <%= l cm.pago_em.to_date, format: :short %>
|
||||||
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
<% if cm.forma_pagamento.present? %>via <%= cm.forma_pagamento.humanize %><% end %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if cm.nota_fiscal.present? %>
|
||||||
|
<p class="text-gray-300 text-sm mt-0.5">🧾 Nota Fiscal: <%= cm.nota_fiscal %></p>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% if c.finalizada? %>
|
<% if c.finalizada? %>
|
||||||
<%= link_to '⬇️ Baixar meu holerite',
|
<%= link_to '⬇️ Baixar meu extrato',
|
||||||
gerar_pdf_holerite_consolidacao_path(c, motorista: current_user.nome),
|
gerar_pdf_extrato_consolidacao_path(c, motorista: current_user.nome),
|
||||||
data: { turbo: false },
|
data: { turbo: false },
|
||||||
class: 'w-full md:w-auto bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %>
|
class: 'w-full md:w-auto bg-orange-500 hover:bg-orange-600 text-black font-bold text-base px-6 py-4 rounded-xl min-h-[52px] flex items-center justify-center text-center' %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p class="text-gray-300 text-base text-center mt-6">
|
<p class="text-gray-300 text-base text-center mt-6">
|
||||||
💡 Você também pode escanear o QR Code do seu holerite para entrar direto.
|
💡 Você também pode escanear o QR Code do seu extrato para entrar direto.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-center mt-4">
|
<p class="text-center mt-4">
|
||||||
<%= link_to 'Sou administrador/gerente →', new_user_session_path, class: 'text-orange-500 hover:text-orange-400 text-sm' %>
|
<%= link_to 'Sou administrador/gerente →', new_user_session_path, class: 'text-orange-500 hover:text-orange-400 text-sm' %>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Rails.application.routes.draw do
|
|||||||
# Painel motorista
|
# Painel motorista
|
||||||
get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard
|
get '/motorista', to: 'motorista/dashboard#index', as: :motorista_dashboard
|
||||||
|
|
||||||
# Login motorista — PIN de 4 dígitos + acesso via QR Code do holerite
|
# Login motorista — PIN de 4 dígitos + acesso via QR Code do extrato
|
||||||
get '/motorista/login', to: 'motorista/sessoes#new', as: :motorista_login
|
get '/motorista/login', to: 'motorista/sessoes#new', as: :motorista_login
|
||||||
post '/motorista/login', to: 'motorista/sessoes#create'
|
post '/motorista/login', to: 'motorista/sessoes#create'
|
||||||
get '/motorista/acesso/:token', to: 'motorista/sessoes#acesso_qr', as: :motorista_acesso_qr
|
get '/motorista/acesso/:token', to: 'motorista/sessoes#acesso_qr', as: :motorista_acesso_qr
|
||||||
@@ -42,9 +42,9 @@ Rails.application.routes.draw do
|
|||||||
post :reativar # Restaura uma consolidação arquivada
|
post :reativar # Restaura uma consolidação arquivada
|
||||||
post :registrar_pagamento # Marca 1 motorista (ou todos) como pago
|
post :registrar_pagamento # Marca 1 motorista (ou todos) como pago
|
||||||
post :cancelar_pagamento # Estorna o pagamento de 1 motorista (ou todos)
|
post :cancelar_pagamento # Estorna o pagamento de 1 motorista (ou todos)
|
||||||
get :preview_holerite
|
get :preview_extrato
|
||||||
get :gerar_pdf_relatorio
|
get :gerar_pdf_relatorio
|
||||||
get :gerar_pdf_holerite
|
get :gerar_pdf_extrato
|
||||||
get :gerar_pdf_financeiro # relatório financeiro da consolidação
|
get :gerar_pdf_financeiro # relatório financeiro da consolidação
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Número da Nota Fiscal do motorista, obrigatório no momento de marcar como pago.
|
||||||
|
# Exibido no painel do motorista e no Extrato (PDF).
|
||||||
|
class AddNotaFiscalToConsolidacaoMotoristas < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :consolidacao_motoristas, :nota_fiscal, :string unless column_exists?(:consolidacao_motoristas, :nota_fiscal)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -154,7 +154,7 @@ Design: moderno, minimalista, <strong>cores padrão PRETO + LARANJA + BRANCO</st
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box improvement">
|
<div class="box improvement">
|
||||||
<h3>✨ MELHORIA 3 — Preview do Holerite Antes de Gerar PDF <span class="badge badge-new">NOVO</span></h3>
|
<h3>✨ MELHORIA 3 — Preview do Extrato Antes de Gerar PDF <span class="badge badge-new">NOVO</span></h3>
|
||||||
<p>Modal de preview HTML antes de gerar PDF — evita erros e re-trabalho.</p>
|
<p>Modal de preview HTML antes de gerar PDF — evita erros e re-trabalho.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ Design: moderno, minimalista, <strong>cores padrão PRETO + LARANJA + BRANCO</st
|
|||||||
|
|
||||||
<div class="box improvement">
|
<div class="box improvement">
|
||||||
<h3>🔁 MUDANÇA 4 — Login por PIN de 4 Dígitos + QR Code <span class="badge badge-change">MUDANÇA</span></h3>
|
<h3>🔁 MUDANÇA 4 — Login por PIN de 4 Dígitos + QR Code <span class="badge badge-change">MUDANÇA</span></h3>
|
||||||
<p>Motoristas usam PIN numérico ou escaneiam QR do holerite. Sem senha complexa.</p>
|
<p>Motoristas usam PIN numérico ou escaneiam QR do extrato. Sem senha complexa.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ==================== ARQUITETURA ==================== -->
|
<!-- ==================== ARQUITETURA ==================== -->
|
||||||
@@ -316,7 +316,7 @@ Thumbs.db
|
|||||||
<table>
|
<table>
|
||||||
<tr><th>Perfil</th><th>Acesso</th></tr>
|
<tr><th>Perfil</th><th>Acesso</th></tr>
|
||||||
<tr><td>Admin</td><td>Tudo: Dashboard, Consolidação, Configurações, Motoristas, Auditoria</td></tr>
|
<tr><td>Admin</td><td>Tudo: Dashboard, Consolidação, Configurações, Motoristas, Auditoria</td></tr>
|
||||||
<tr><td>Gerente</td><td>Dashboard, Consolidação, gerar PDFs/holerites, visualizar arquivadas</td></tr>
|
<tr><td>Gerente</td><td>Dashboard, Consolidação, gerar PDFs/extratos, visualizar arquivadas</td></tr>
|
||||||
<tr><td>Operador</td><td>Apenas Consolidação (criar, validar entregas, toggles), ver próprias consolidações</td></tr>
|
<tr><td>Operador</td><td>Apenas Consolidação (criar, validar entregas, toggles), ver próprias consolidações</td></tr>
|
||||||
<tr><td>Motorista</td><td>Dashboard pessoal: valores estimados + consolidados validados, baixar próprio PDF</td></tr>
|
<tr><td>Motorista</td><td>Dashboard pessoal: valores estimados + consolidados validados, baixar próprio PDF</td></tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -387,7 +387,7 @@ Thumbs.db
|
|||||||
</li>
|
</li>
|
||||||
<li><strong>Barra de progresso:</strong> "85% classificado — 340 de 400 entregas"</li>
|
<li><strong>Barra de progresso:</strong> "85% classificado — 340 de 400 entregas"</li>
|
||||||
<li><strong>Botões:</strong> "Salvar Rascunho" | "Finalizar Consolidação" (modal de confirmação)</li>
|
<li><strong>Botões:</strong> "Salvar Rascunho" | "Finalizar Consolidação" (modal de confirmação)</li>
|
||||||
<li>Após finalizar: <strong>Preview do Holerite</strong> em modal HTML → "Gerar PDF"</li>
|
<li>Após finalizar: <strong>Preview do Extrato</strong> em modal HTML → "Gerar PDF"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>3. CONFIGURAÇÕES</h3>
|
<h3>3. CONFIGURAÇÕES</h3>
|
||||||
@@ -587,7 +587,7 @@ end
|
|||||||
# O job serve apenas para manter histórico de gráficos.
|
# O job serve apenas para manter histórico de gráficos.
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<h2>📄 GERADOR DE PDF (Holerite)</h2>
|
<h2>📄 GERADOR DE PDF (Extrato)</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Cabeçalho:</strong> logo da empresa (preto + laranja), nome da consolidação, período, data de geração</li>
|
<li><strong>Cabeçalho:</strong> logo da empresa (preto + laranja), nome da consolidação, período, data de geração</li>
|
||||||
<li><strong>Dados do motorista:</strong> nome, período trabalhado, total de entregas</li>
|
<li><strong>Dados do motorista:</strong> nome, período trabalhado, total de entregas</li>
|
||||||
@@ -690,14 +690,14 @@ end
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box phase">
|
<div class="box phase">
|
||||||
<h3>🟠 FASE 7 — Geração de PDFs (Preview + Relatório + Holerite)</h3>
|
<h3>🟠 FASE 7 — Geração de PDFs (Preview + Relatório + Extrato)</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Instalar e configurar Prawn (mais leve que WickedPDF)</li>
|
<li>Instalar e configurar Prawn (mais leve que WickedPDF)</li>
|
||||||
<li>Layout de PDF: logo preto/laranja, nome consolidação, período, tabela de entregas, total</li>
|
<li>Layout de PDF: logo preto/laranja, nome consolidação, período, tabela de entregas, total</li>
|
||||||
<li>Modal de Preview HTML antes de gerar PDF (modal com botão Confirmar/Voltar)</li>
|
<li>Modal de Preview HTML antes de gerar PDF (modal com botão Confirmar/Voltar)</li>
|
||||||
<li>Botão "Gerar Relatório Individual" por motorista (PDF com detalhe das entregas)</li>
|
<li>Botão "Gerar Relatório Individual" por motorista (PDF com detalhe das entregas)</li>
|
||||||
<li>Botão "Gerar Holerite" (PDF resumido com valor total a pagar — estilo holerite)</li>
|
<li>Botão "Gerar Extrato" (PDF resumido com valor total a pagar — estilo extrato)</li>
|
||||||
<li>QR code no holerite para login rápido do motorista</li>
|
<li>QR code no extrato para login rápido do motorista</li>
|
||||||
<li>Soft delete: consolidações finalizadas vão para status "arquivada", nunca excluídas</li>
|
<li>Soft delete: consolidações finalizadas vão para status "arquivada", nunca excluídas</li>
|
||||||
<li>Commit e push para o Git privado</li>
|
<li>Commit e push para o Git privado</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -709,7 +709,7 @@ end
|
|||||||
<ol>
|
<ol>
|
||||||
<li>Tela do motorista após login: 2 cards grandes (Valor Estimado laranja | Valor Consolidado branco)</li>
|
<li>Tela do motorista após login: 2 cards grandes (Valor Estimado laranja | Valor Consolidado branco)</li>
|
||||||
<li>Lista de consolidações em que ele aparece com link para baixar próprio PDF</li>
|
<li>Lista de consolidações em que ele aparece com link para baixar próprio PDF</li>
|
||||||
<li>Login por PIN ou QR Code (escaneia holerite para acessar)</li>
|
<li>Login por PIN ou QR Code (escaneia extrato para acessar)</li>
|
||||||
<li>Notificações: integração Twilio (WhatsApp) ou ActionMailer (email) — ao finalizar consolidação, enviar mensagem automática para motoristas</li>
|
<li>Notificações: integração Twilio (WhatsApp) ou ActionMailer (email) — ao finalizar consolidação, enviar mensagem automática para motoristas</li>
|
||||||
<li>Ajustes finais de responsividade (mobile)</li>
|
<li>Ajustes finais de responsividade (mobile)</li>
|
||||||
<li>Revisar todas as permissões (Pundit)</li>
|
<li>Revisar todas as permissões (Pundit)</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user