diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..50b2547 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,335 @@ +name: CI — Mães em Ação + +on: + push: + branches: [versao_1, main] + pull_request: + branches: [versao_1, main] + +env: + NODE_VERSION: "20" + +jobs: + # ──────────────────────────────────────────────────────────────── + # JOB 1 — Lint (Backend + Frontend em paralelo) + # ──────────────────────────────────────────────────────────────── + lint-backend: + name: "Lint · Backend" + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + lint-frontend: + name: "Lint · Frontend" + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + # ──────────────────────────────────────────────────────────────── + # JOB 2 — Testes (Backend + Frontend em paralelo) + # ──────────────────────────────────────────────────────────────── + test-backend: + name: "Testes · Backend (Jest)" + runs-on: ubuntu-latest + needs: lint-backend + defaults: + run: + working-directory: backend + + env: + NODE_ENV: test + JWT_SECRET: "ci_test_secret_key_at_least_32_chars_long!!" + API_KEY_SERVIDORES: "ci_test_api_key_64_chars_for_scanner_balcao_valid_1234567890abcdef" + GEMINI_API_KEY: "dummy_gemini_key_for_ci" + GROQ_API_KEY: "gsk_dummy_groq_key_for_ci_testing_purposes" + DATABASE_URL: "postgresql://ci:ci@localhost:5432/ci_test" + DIRECT_URL: "postgresql://ci:ci@localhost:5432/ci_test" + SUPABASE_URL: "https://dummy.supabase.co" + SUPABASE_SERVICE_KEY: "dummy_service_key_for_ci" + QSTASH_TOKEN: "dummy_qstash_token" + QSTASH_CURRENT_SIGNING_KEY: "dummy_signing_key" + QSTASH_NEXT_SIGNING_KEY: "dummy_next_signing_key" + SALARIO_MINIMO_ATUAL: "1621.00" + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npx prisma generate + + - name: Run Jest tests with coverage + run: npm test -- --forceExit + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: backend-coverage + path: backend/logs/coverage/ + retention-days: 7 + + - name: Coverage summary + if: always() + run: | + echo "## Backend Coverage" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat logs/coverage/coverage-summary.json 2>/dev/null | node -e " + const data = require('fs').readFileSync('/dev/stdin','utf8'); + try { + const j = JSON.parse(data).total; + console.log('Statements:', j.statements.pct + '%'); + console.log('Branches: ', j.branches.pct + '%'); + console.log('Functions: ', j.functions.pct + '%'); + console.log('Lines: ', j.lines.pct + '%'); + } catch { console.log('Coverage data not available'); } + " 2>/dev/null || echo "Coverage data not available" + echo '```' >> $GITHUB_STEP_SUMMARY + + test-frontend: + name: "Testes · Frontend (Vitest)" + runs-on: ubuntu-latest + needs: lint-frontend + defaults: + run: + working-directory: frontend + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run Vitest tests with coverage + run: npm run test:coverage + env: + VITE_API_URL: "http://localhost:8000" + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: frontend-coverage + path: frontend/coverage/ + retention-days: 7 + + - name: Coverage summary + if: always() + run: | + echo "## Frontend Coverage" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat coverage/coverage-summary.json 2>/dev/null | node -e " + const data = require('fs').readFileSync('/dev/stdin','utf8'); + try { + const j = JSON.parse(data).total; + console.log('Statements:', j.statements.pct + '%'); + console.log('Branches: ', j.branches.pct + '%'); + console.log('Functions: ', j.functions.pct + '%'); + console.log('Lines: ', j.lines.pct + '%'); + } catch { console.log('Coverage data not available'); } + " 2>/dev/null || echo "Coverage data not available" + echo '```' >> $GITHUB_STEP_SUMMARY + + # ──────────────────────────────────────────────────────────────── + # JOB 3 — Security Audit (npm audit --audit-level=high) + # ──────────────────────────────────────────────────────────────── + security-audit: + name: "Segurança · npm audit" + runs-on: ubuntu-latest + # Roda em paralelo com testes — não bloqueia o pipeline por lint + needs: [lint-backend, lint-frontend] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Audit Backend + working-directory: backend + run: | + npm ci + npm audit --audit-level=high --json > audit-backend.json || true + CRITICAL=$(node -e " + const a = require('./audit-backend.json'); + const v = a.metadata?.vulnerabilities || {}; + console.log(v.critical || 0); + ") + HIGH=$(node -e " + const a = require('./audit-backend.json'); + const v = a.metadata?.vulnerabilities || {}; + console.log(v.high || 0); + ") + echo "Backend — Critical: $CRITICAL | High: $HIGH" + echo "### Backend Audit" >> $GITHUB_STEP_SUMMARY + echo "- 🔴 Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY + echo "- 🟠 High: $HIGH" >> $GITHUB_STEP_SUMMARY + if [ "$CRITICAL" -gt "0" ]; then + echo "❌ Vulnerabilidades críticas encontradas no backend!" >&2 + exit 1 + fi + + - name: Audit Frontend + working-directory: frontend + run: | + npm ci + npm audit --audit-level=high --json > audit-frontend.json || true + CRITICAL=$(node -e " + const a = require('./audit-frontend.json'); + const v = a.metadata?.vulnerabilities || {}; + console.log(v.critical || 0); + ") + HIGH=$(node -e " + const a = require('./audit-frontend.json'); + const v = a.metadata?.vulnerabilities || {}; + console.log(v.high || 0); + ") + echo "Frontend — Critical: $CRITICAL | High: $HIGH" + echo "### Frontend Audit" >> $GITHUB_STEP_SUMMARY + echo "- 🔴 Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY + echo "- 🟠 High: $HIGH" >> $GITHUB_STEP_SUMMARY + if [ "$CRITICAL" -gt "0" ]; then + echo "❌ Vulnerabilidades críticas encontradas no frontend!" >&2 + exit 1 + fi + + # ──────────────────────────────────────────────────────────────── + # JOB 4 — Build (valida que o código compila corretamente) + # ──────────────────────────────────────────────────────────────── + build-backend: + name: "Build · Backend (prisma generate)" + runs-on: ubuntu-latest + needs: test-backend + defaults: + run: + working-directory: backend + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npx prisma generate + + - name: Verify server.js imports (syntax check) + run: node --check server.js + + build-frontend: + name: "Build · Frontend (vite build)" + runs-on: ubuntu-latest + needs: test-frontend + defaults: + run: + working-directory: frontend + + env: + VITE_API_URL: "https://api.mutirao.dpe.ba.gov.br" + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: frontend/dist/ + retention-days: 3 + + # ──────────────────────────────────────────────────────────────── + # JOB 5 — Status final (gate para merge) + # ──────────────────────────────────────────────────────────────── + ci-success: + name: "✅ CI Completo" + runs-on: ubuntu-latest + needs: [build-backend, build-frontend, security-audit] + if: always() + + steps: + - name: Verificar se todos os jobs passaram + run: | + if [[ "${{ needs.build-backend.result }}" != "success" ]] || \ + [[ "${{ needs.build-frontend.result }}" != "success" ]] || \ + [[ "${{ needs.security-audit.result }}" != "success" ]]; then + echo "❌ Um ou mais jobs falharam" + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| build-backend | ${{ needs.build-backend.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| build-frontend | ${{ needs.build-frontend.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| security-audit | ${{ needs.security-audit.result }} |" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "✅ Todos os jobs passaram com sucesso!" >> $GITHUB_STEP_SUMMARY diff --git a/arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md b/arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md index 7e89102..c7509a7 100644 --- a/arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md +++ b/arquivos/Conhecimento/01-Referencia/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Arquitetura do Sistema — Mães em Ação · DPE-BA -> **Versão:** 3.2 · **Atualizado em:** 2026-04-24 (CodeRabbit Audit + Dashboard v3.0 + CPF Mandatory) +> **Versão:** 4.2 · **Atualizado em:** 2026-04-26 (Hardening de Segurança + Assistência Compartilhada) > **Contexto:** Mutirão estadual da Defensoria Pública da Bahia --- @@ -180,12 +180,12 @@ stateDiagram-v2 ### Locking — Sessões e Concorrência -- **Nível 1 (Servidor):** Bloqueia edição de dados jurídicos e relato -- **Nível 2 (Defensor):** Bloqueia a etapa de protocolo e finalização -- **HTTP 423 (Locked):** Retorno padrão quando outro usuário detém o lock -- **Admin Bypass:** Administradores podem forçar destravamento via painel +- **Nível 1 (Servidor/Estagiário/Defensor/Coordenador):** Atribuição de `servidor_id` — bloqueia edição de dados jurídicos e relato. Ativo em `pronto_para_analise` e `em_atendimento`. +- **Nível 2 (Defensor/Coordenador/Admin):** Atribuição de `defensor_id` — bloqueia etapa de protocolo e finalização. Ativo em `liberado_para_protocolo` e `em_protocolo`. **`servidor` e `estagiario` NUNCA adquirem Nível 2.** +- **Isolamento de Unidade:** Middleware `requireSameUnit` bloqueia IDOR. **Admin e Gestor** possuem bypass global. +- **HTTP 423 (Locked):** Retorno padrão quando outro usuário detém o lock. +- **Unlock Privilegiado:** Administradores, Gestores e Coordenadores podem forçar destravamento via painel. - **Auto-release:** Lock liberado após 30min de inatividade. -- **Manual Unlock:** Administradores podem forçar destravamento via painel. --- @@ -329,15 +329,17 @@ sequenceDiagram ### Permissões por Cargo (RBAC) -| Cargo | Leitura | Escrita | Admin | -|:------|:--------|:--------|:------| -| `admin` | ✅ | ✅ | ✅ | -| `defensor` | ✅ | ✅ | ❌ | -| `estagiario` | ✅ | ✅ | ❌ | -| `recepcao` | ✅ | ✅ | ❌ | -| `visualizador` | ✅ | ❌ | ❌ | +| Cargo | Leitura | Escrita | Protocolo/Finalizar | Admin/Global | Unlock | +|:------|:--------|:--------|:--------------------|:-------------|:-------| +| `admin` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `gestor` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `coordenador` | ✅ | ✅ | ✅ | ❌ | ✅ | +| `defensor` | ✅ | ✅ | ✅ | ❌ | ❌ | +| `servidor` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `estagiario` | ✅ | ✅ | ❌ | ❌ | ❌ | -> **Middleware:** `requireWriteAccess` bloqueia `visualizador` de operações POST/PATCH/DELETE com HTTP 403. +> **Middleware:** `requireWriteAccess` usa whitelist positiva: apenas `admin`, `gestor`, `coordenador`, `defensor`, `servidor`, `estagiario` passam. +> **Isolamento de Unidade:** Middleware `requireSameUnit` bloqueia IDOR. Admins e Gestores possuem bypass global. --- @@ -489,6 +491,7 @@ QSTASH_NEXT_SIGNING_KEY=... JWT_SECRET=64_chars_random_string API_KEY_SERVIDORES=64_chars_random SALARIO_MINIMO_ATUAL=1621.00 +ALLOWED_ORIGINS=https://maesemacao.defsulbahia.com.br,https://maes-acao.vercel.app # Frontend VITE_API_URL=https://api.mutirao.dpe.ba.gov.br diff --git a/arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md b/arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md index f70c9b5..4891185 100644 --- a/arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md +++ b/arquivos/Conhecimento/01-Referencia/BUSINESS_RULES.md @@ -1,6 +1,6 @@ # Regras de Negócio — Mães em Ação · DPE-BA -> **Versão:** 2.4 · **Atualizado em:** 2026-04-24 (CPF Representante Obrigatório + BI Categories) +> **Versão:** 3.1 · **Atualizado em:** 2026-04-26 (RBAC Hierárquico + Cargo Gestor + Unlock Expandido) > **Fonte:** Análise da codebase (controllers, services, middleware, config) > **Propósito:** Referência canônica para treinamento de IAs e orientação de defensores @@ -391,13 +391,14 @@ O sistema agora suporta a geração e visualização simultânea de múltiplos d O campo `cargo` na tabela `defensores` define o nível de acesso. O cargo é incluído no token JWT no login. -| Cargo | Acesso de Leitura | Acesso de Escrita | Operações Admin | -|:------|:-------------------|:-------------------|:----------------| -| `admin` | ✅ | ✅ | ✅ | -| `defensor` | ✅ | ✅ | ❌ | -| `estagiario` | ✅ | ✅ | ❌ | -| `recepcao` | ✅ | ✅ | ❌ | -| `visualizador` | ✅ | ❌ | ❌ | +| Cargo | Acesso de Leitura | Acesso de Escrita | Operações Admin/Global | Unlock | +|:------|:-------------------|:-------------------|:-----------------------|:-------| +| `admin` | ✅ | ✅ | ✅ | ✅ | +| `gestor` | ✅ | ✅ | ✅ | ✅ | +| `coordenador` | ✅ | ✅ | ❌ | ✅ | +| `defensor` | ✅ | ✅ | ❌ | ❌ | +| `servidor` | ✅ | ✅ | ❌ | ❌ | +| `estagiario` | ✅ | ✅ | ❌ | ❌ | > O cargo padrão ao cadastrar um novo membro é `"estagiario"`. Apenas o admin pode criar cadastros e deve selecionar entre as opções disponíveis no formulário. @@ -407,26 +408,31 @@ O campo `cargo` na tabela `defensores` define o nível de acesso. O cargo é inc | Middleware | Função | Aplicação | |:-----------|:-------|:----------| -| `authMiddleware` | Verifica JWT e injeta `req.user` | Todas as rotas protegidas (após rotas públicas) | -| `requireWriteAccess` | Bloqueia cargo `visualizador` de operações de escrita (403) | Todas as rotas de escrita (POST, PATCH, DELETE protegidas) | +| `authMiddleware` | Verifica JWT e injeta `req.user` | Todas as rotas protegidas | +| `requireWriteAccess` | Bloqueia operações de escrita para cargos não autorizados | Todas as rotas de escrita | | `auditMiddleware` | Registra operações de escrita na tabela `logs_auditoria` | Todas as rotas protegidas | +| `requireSameUnit` | Bloqueia acesso a casos de outras unidades (IDOR) | Rotas de detalhe/edição | + +> **Middleware:** `requireWriteAccess` usa whitelist positiva. Qualquer cargo fora da lista recebe HTTP 403. +> **Isolamento de Unidade:** Usuários (exceto Admins e Gestores) são restritos a casos de sua própria `unidade_id`. Admins e Gestores possuem bypass global para visualização e edição. +> **Visibilidade de Equipe:** Gestores e Admins possuem visão global de todos os membros. Coordenadores listam apenas membros da sua própria unidade. ### 5.3 Operações exclusivas de Admin -As seguintes operações verificam **explicitamente** `req.user.cargo === "admin"` no controller: - -| Operação | Endpoint | Justificativa | -|:---------|:---------|:-------------| -| Regenerar Dos Fatos | `POST /:id/gerar-fatos` | Consome créditos de IA | -| Gerar Termo de Declaração | `POST /:id/gerar-termo` | Documento formal | -| Regerar Minuta DOCX | `POST /:id/regerar-minuta` | Consome créditos de IA | -| Reverter Finalização | `POST /:id/reverter-finalizacao` | Ação destrutiva (remove dados Solar) | -| Deletar Caso | `DELETE /:id` | Ação irreversível (exclui do banco e Storage) | -| Registrar novo membro | `POST /api/defensores/cadastro` | Gestão de equipe | -| Listar equipe | `GET /api/defensores` | Dados sensíveis da equipe | -| Atualizar membro | `PATCH /api/defensores/:id` | Gestão de equipe | -| Deletar membro | `DELETE /api/defensores/:id` | Gestão de equipe (não pode deletar a si mesmo) | -| Resetar senha de membro | `POST /api/defensores/:id/resetar-senha` | Segurança | +As seguintes operações verificam **explicitamente** cargo privilegiado no controller: + +| Operação | Endpoint | Cargos Autorizados | +|:---------|:---------|:-------------------| +| Regenerar Dos Fatos | `POST /:id/gerar-fatos` | `admin` | +| Gerar Termo de Declaração | `POST /:id/gerar-termo` | `admin` | +| Regerar Minuta DOCX | `POST /:id/regerar-minuta` | `admin` | +| Reverter Finalização | `POST /:id/reverter-finalizacao` | `admin` | +| Deletar Caso | `DELETE /:id` | `admin` | +| Liberar Caso (Unlock) | `PATCH /lock/unlock` | `admin`, `gestor`, `coordenador` | +| Registrar novo membro | `POST /api/defensores/register` | `admin` | +| Listar equipe global | `GET /api/defensores` | `admin`, `gestor` | +| Listar equipe local | `GET /api/defensores` | `coordenador` (filtra por unidade) | +| Resetar senha de membro | `POST /api/defensores/:id/reset-password` | `admin` | ### 5.4 Acesso público (sem autenticação) diff --git a/arquivos/Conhecimento/01-Referencia/routes.md b/arquivos/Conhecimento/01-Referencia/routes.md index 92b67bb..a044925 100644 --- a/arquivos/Conhecimento/01-Referencia/routes.md +++ b/arquivos/Conhecimento/01-Referencia/routes.md @@ -1,6 +1,6 @@ # Referência da API — Mães em Ação · DPE-BA -> **Versão:** 3.1 · **Atualizado em:** 2026-04-23 (Download Hardening + A11y) +> **Versão:** 4.0 · **Atualizado em:** 2026-04-26 (Global Unit Isolation + Team Visibility) Esta documentação lista as principais rotas do backend do Mães em Ação. Todas as rotas são prefixadas com `/api`. Exemplo: `https://api.mutirao.dpe.ba.gov.br/api/casos`. @@ -15,8 +15,9 @@ Para rotas marcadas como **Protegidas**, é exigido o envio de um token JWT no c ### Cargos e Permissões (`cargo`) O sistema utiliza os seguintes cargos, extraídos do JWT: * `admin`: Acesso total (leitura, escrita e ações críticas/destrutivas, como criação de perfis). -* `defensor`, `estagiario`, `recepcao`: Acesso de leitura e escrita (exceto exclusões e recriação IA). -* `visualizador`: Apenas leitura (bloqueado pelo middleware `requireWriteAccess`). +* `coordenador`, `gestor`: Acesso de leitura e escrita. Pode listar defensores da sua própria unidade. +* `defensor`, `servidor`, `estagiario`: Acesso de leitura e escrita (exceto exclusões e recriação IA). Restricted a casos da própria unidade via `requireSameUnit`. +* `visualizador`: Apenas leitura (bloqueado pelo middleware `requireWriteAccess`). Restricted a casos da própria unidade. --- @@ -40,6 +41,8 @@ Exigem autenticação. Fundamentais para evitar edição concorrente. | `PATCH`| `/:id/lock` | Tenta travar o caso para o usuário (Retorna 423 se ocupado)| | `PATCH`| `/:id/unlock` | Libera o caso manualmente para outros usuários | +> **Nota:** Todas as rotas que utilizam o parâmetro `/:id` (exceto uploads públicos) aplicam o middleware `requireSameUnit`, impedindo acesso cruzado entre unidades. + ### Rotas de Scanner (Balcão) Endpoint otimizado para alto volume. @@ -210,8 +213,9 @@ Gerenciamento de acesso e contas dos Defensores Públicos. * **Request:** JSON: `nome`, `email` (único na view constraint DB), `senha` (>= 6 chr), `cargo` (válidos, bloqueado para 'operador', que inexiste dinamicamente), `unidade_id` (obrigatório — selecionar unidade de lotação). #### `GET /` -*Protegida (Exclusiva para `admin`)* -* **Response (200 OK):** Lista de perfis do time com `unidade_nome` e `unidade_id`, dados de senha restritos. +*Protegida (Acesso: `admin`, `coordenador`, `gestor`)* +* **Response (200 OK):** Lista de perfis do time. Se o cargo for `admin`, vê todas as sedes. Se for `coordenador` ou `gestor`, vê apenas membros da própria `unidade_id`. +* **Observação:** Dados de senha são restritos. Senha_hash nunca é exposta. #### `PUT /:id` *Protegida* diff --git a/arquivos/Conhecimento/03-Guias/GUIA_TESTES.md b/arquivos/Conhecimento/03-Guias/GUIA_TESTES.md new file mode 100644 index 0000000..b3cf77e --- /dev/null +++ b/arquivos/Conhecimento/03-Guias/GUIA_TESTES.md @@ -0,0 +1,484 @@ +# Guia de Uso — Suíte de Testes · Mães em Ação + +> **Versão:** 1.0 · **Criado em:** 2026-04-25 +> **Stack de testes:** Jest (Backend) + Vitest (Frontend) + +--- + +## Pré-requisitos + +```bash +# Garanta que as dependências estão instaladas +cd backend && npm install +cd ../frontend && npm install +``` + +--- + +## BACKEND — Comandos + +> Todos os comandos devem ser executados dentro da pasta `backend/`. + +```bash +cd backend +``` + +### Rodar todos os testes + +```bash +npm test +``` + +### Rodar apenas um arquivo específico + +```bash +npm test -- tests/unit/securityService.test.js +npm test -- tests/middleware/auth.test.js +npm test -- tests/integration/casosRoutes.test.js +npm test -- tests/security/injection.test.js +``` + +### Rodar todos os testes de uma categoria + +```bash +npm test -- tests/unit/ +npm test -- tests/middleware/ +npm test -- tests/integration/ +npm test -- tests/security/ +``` + +### Rodar em modo watch (re-executa ao salvar) + +```bash +npm test -- --watch +``` + +### Ver relatório de cobertura (HTML) + +```bash +npm test +# Abra no navegador: +# backend/logs/coverage/index.html +``` + +--- + +## FRONTEND — Comandos + +> Todos os comandos devem ser executados dentro da pasta `frontend/`. + +```bash +cd frontend +``` + +### Rodar todos os testes + +```bash +npm test +``` + +### Rodar com cobertura + +```bash +npm run test:coverage +# Relatório em: frontend/coverage/index.html +``` + +### Rodar em modo watch (re-executa ao salvar) + +```bash +npm run test:watch +``` + +### Rodar apenas um arquivo + +```bash +npx vitest run src/__tests__/formatters.test.js +npx vitest run src/__tests__/caseUtils.test.js +npx vitest run src/__tests__/apiBase.test.js +npx vitest run src/__tests__/submissionValidation.test.js +``` + +--- + +## O que cada arquivo de teste cobre + +--- + +### 🔵 BACKEND + +#### `tests/setup.js` — Configuração global + +Executa antes de todos os testes. Define variáveis de ambiente seguras para o ambiente de CI/CD sem tocar nas credenciais reais: + +- `JWT_SECRET`, `API_KEY_SERVIDORES`, `GEMINI_API_KEY`, `GROQ_API_KEY`, `DATABASE_URL` etc. + +--- + +#### `tests/unit/securityService.test.js` — Serviço de Segurança + +**O que testa:** Funções puras de `src/services/securityService.js`. + +| Teste | Descrição | +| ----------------------------- | ------------------------------------------------------- | +| Formato do protocolo | Gera string numérica de 14 dígitos (`AAAAMMDDXXXXXXXX`) | +| Unicidade do protocolo | Dois protocolos gerados consecutivamente são diferentes | +| Hash de senha (bcrypt) | `hashPassword()` retorna string diferente do original | +| Verificação de senha | `verifyPassword()` retorna `true` para senha correta | +| Verificação de senha errada | `verifyPassword()` retorna `false` para senha incorreta | +| Proteção contra timing attack | Verificação de senha é constante (bcrypt) | + +--- + +#### `tests/unit/aiService.test.js` — Serviço de IA + +**O que testa:** Sanitização de PII antes de enviar dados para Gemini/Groq, fallbacks e timeouts. + +| Teste | Descrição | +| ---------------------- | ---------------------------------------------------------- | +| Sanitização CPF | CPF é substituído por placeholder antes de chamar a IA | +| Sanitização nome | Nome real é substituído por placeholder genérico | +| Desanitização | Placeholder é revertido ao valor original na resposta | +| Fallback Groq → Gemini | Se Groq falha, Gemini Flash é chamado como fallback | +| Timeout | Se a IA não responde em tempo hábil, lança erro controlado | +| Texto sem PII | Não altera texto sem dados pessoais identificáveis | + +--- + +#### `tests/middleware/auth.test.js` — Middleware de Autenticação JWT + +**O que testa:** Validação de JWT em todas as rotas protegidas. + +| Teste | Descrição | +| ------------------------------- | -------------------------------------------- | +| Sem `Authorization` header | Retorna `401 Unauthorized` | +| Token malformado | Retorna `401` | +| Token expirado | Retorna `401` com mensagem "sessão expirada" | +| Token com algoritmo `none` | Retorna `401` (bypass prevention) | +| Token com algoritmo `RS256` | Retorna `401` (aceita apenas `HS256`) | +| Token válido, usuário inativo | Retorna `403 Forbidden` | +| Token válido, usuário existente | Chama `next()` e popula `req.user` | +| Download ticket inválido | Retorna `401` | +| Download ticket expirado | Retorna `401` | +| Download ticket válido | Chama `next()` e popula `req.downloadInfo` | + +--- + +#### `tests/middleware/requireWriteAccess.test.js` — Middleware de RBAC + +**O que testa:** Controle de acesso por cargo para operações de escrita (POST/PATCH/DELETE). + +| Teste | Descrição | +| -------------------------------- | ----------------------------------------------- | +| Sem `req.user` | Retorna `401` | +| Cargo `visualizador` | Retorna `403 Forbidden` | +| Cargo `Visualizador` (maiúsculo) | Retorna `403` — verificação é case-insensitive | +| Cargo desconhecido (`hacker`) | Retorna `403` — lista de permissões é exclusiva | +| Cargo `admin` | Chama `next()` | +| Cargo `defensor` | Chama `next()` | +| Cargo `servidor` | Chama `next()` | +| Cargo `estagiario` | Chama `next()` | + +--- + +#### `tests/middleware/apiKey.test.js` — Middleware de API Key (Scanner) + +**O que testa:** Autenticação por `X-API-Key` para o endpoint do Scanner do Balcão. + +| Teste | Descrição | +| ----------------------- | ----------------------------------------------------------------------- | +| Sem header `X-API-Key` | Retorna `401` com mensagem mencionando "Scanner" | +| Chave inválida | Retorna `403` com mensagem "inválida" | +| String vazia | Retorna `401` (falsy, tratado como ausente) | +| Chave válida | Chama `next()` | +| Chave válida — req.user | Injeta `{ cargo: "sistema", nome: "Scanner Automático" }` em `req.user` | + +--- + +#### `tests/integration/health.test.js` — Endpoints Básicos (Supertest) + +**O que testa:** Saúde da aplicação e tratamento de rotas desconhecidas. + +| Teste | Descrição | +| ---------------------------------------- | --------------------------------------- | +| `GET /api/health` → 200 | Aplicação está no ar | +| `/api/health` retorna `{ status: "OK" }` | Formato correto da resposta | +| `/api/health` retorna campo `message` | Campo obrigatório presente | +| `GET /api/rota-inexistente` → 404 | Catch-all funciona | +| 404 retorna campo `error` | Resposta padronizada | +| 404 retorna campo `path` | Path da requisição incluído na resposta | + +--- + +#### `tests/integration/casosRoutes.test.js` — Rotas de Casos (Supertest) + +**O que testa:** Criação e listagem de casos com autenticação JWT. + +| Teste | Descrição | +| ------------------------------------------- | -------------------------- | +| `POST /api/casos/novo` com payload válido | Retorna 200 ou 201 | +| Resposta de criação contém `protocolo` | Campo obrigatório presente | +| `GET /api/casos` sem JWT | Retorna 401 | +| `GET /api/casos` com token inválido | Retorna 401 | +| `GET /api/casos` com usuário inativo | Retorna 401 ou 403 | +| `GET /api/casos` com JWT válido | Não retorna 500 | +| `GET /api/casos/buscar-cpf` sem CPF | Retorna 400/404/422 | +| `buscar-cpf` retorna JSON (não HTML) | Content-Type correto | +| `DELETE /api/casos/:id` sem JWT | Retorna 401 | +| `DELETE /api/casos/:id` como `visualizador` | Retorna 403 | + +--- + +#### `tests/integration/lockRoutes.test.js` — Sistema de Locking (Supertest) + +**O que testa:** Bloqueio e desbloqueio de casos (concorrência entre atendentes). + +| Teste | Descrição | +| ------------------------------------------------ | --------------------------------------- | +| `PATCH /lock` sem JWT | Retorna 401 | +| Servidor bloqueia caso `pronto_para_analise` | Retorna 200 (ou 500 por BigInt em mock) | +| Servidor tenta lock em `liberado_para_protocolo` | Retorna 403 | +| Lock quando caso já está bloqueado por outro | Retorna 423 (Locked) | +| `PATCH /unlock` sem JWT | Retorna 401 | +| Defensor tenta unlock | Retorna 403 | +| Servidor tenta unlock | Retorna 403 | +| Admin faz unlock | Retorna 200 | + +--- + +#### `tests/integration/scannerRoutes.test.js` — Rotas do Scanner e Login (Supertest) + +**O que testa:** Autenticação do scanner e endpoint de login dos defensores. + +| Teste | Descrição | +| ------------------------------------------- | ----------------------------- | +| `POST /api/scanner/upload` sem `X-API-Key` | Retorna 401 | +| Upload com chave inválida | Retorna 403 | +| Upload retorna JSON | Content-Type correto | +| Upload com chave válida, sem dados | Retorna 400/404 (não 401/403) | +| `POST /api/defensores/login` sem corpo | Retorna 400/422 | +| Login com credenciais inválidas | Retorna 401/404 | +| Resposta de erro de login tem campo `error` | Formato padronizado | +| `GET /api/defensores` sem JWT | Retorna 401 | +| `GET /api/defensores` com token malformado | Retorna 401 | + +--- + +#### `tests/security/injection.test.js` — Proteção contra Injeção + +**O que testa:** Que o sistema não vaza dados, crasha ou retorna 500 sob payloads maliciosos. + +| Teste | Descrição | +| ------------------------------------- | --------------------------------------------------- | +| SQL injection no campo CPF | Não retorna 500 — Prisma usa queries parametrizadas | +| `' OR '1'='1` no buscar-cpf | Retorna 400/404, nunca 500 | +| `'; DROP TABLE casos; --` | Sistema não crashar | +| XSS `", + "", + "javascript:alert(document.cookie)", + "", + ]; + + xssPayloads.forEach((payload) => { + it(`não causa 500 com XSS no campo relato: "${payload.slice(0, 30)}"`, async () => { + const res = await request(app) + .post("/api/casos/novo") + .send({ + representante_cpf: "168.287.777-94", + tipoAcao: "Família - Fixação", + acaoEspecifica: "fixacao_alimentos", + relato: payload, + enviar_documentos_depois: "true", + }); + // Sistema pode rejeitar (400/422) mas não deve crashar (500) + expect(res.status).not.toBe(500); + }); + }); +}); + +// ─── Testes de Segurança — Headers de Segurança (Helmet) ───────────────────── +describe("Segurança — Headers HTTP (Helmet)", () => { + it("response inclui X-Content-Type-Options: nosniff", async () => { + const res = await request(app).get("/api/health"); + expect(res.headers["x-content-type-options"]).toBe("nosniff"); + }); + + it("response inclui X-Frame-Options ou Content-Security-Policy", async () => { + const res = await request(app).get("/api/health"); + const hasFrameProtection = + res.headers["x-frame-options"] || res.headers["content-security-policy"]; + expect(hasFrameProtection).toBeTruthy(); + }); + + it("response não expõe X-Powered-By Express", async () => { + const res = await request(app).get("/api/health"); + expect(res.headers["x-powered-by"]).toBeUndefined(); + }); +}); + +// ─── Testes de Segurança — CORS ─────────────────────────────────────────────── +describe("Segurança — CORS em produção", () => { + it("bloqueia origin não autorizado em produção", async () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = "production"; + + const res = await request(app) + .get("/api/health") + .set("Origin", "https://site-malicioso.com"); + + // Em produção com origin não listado, CORS deve negar ou não incluir o header + // Verificamos que não vaza dados sensíveis (400 ou sem Access-Control-Allow-Origin) + const allowOrigin = res.headers["access-control-allow-origin"]; + if (allowOrigin) { + expect(allowOrigin).not.toBe("https://site-malicioso.com"); + } + + process.env.NODE_ENV = originalEnv; + }); +}); diff --git a/backend/tests/security/rateLimiter.test.js b/backend/tests/security/rateLimiter.test.js new file mode 100644 index 0000000..6bead5a --- /dev/null +++ b/backend/tests/security/rateLimiter.test.js @@ -0,0 +1,109 @@ +/** + * Testes de Rate Limiter + * Verifica as configurações dos limitadores sem fazer requisições HTTP reais. + * Isso garante que as configurações "mutirão-ready" estejam corretas. + */ +import { jest } from "@jest/globals"; +import { globalLimiter, searchLimiter, creationLimiter } from "../../src/middleware/rateLimiter.js"; + +describe("Rate Limiter — configurações corretas para o Mutirão", () => { + /** + * express-rate-limit armazena a config interna no objeto store/options. + * Acessamos _options para verificar. + * Nota: a estrutura interna pode variar por versão do express-rate-limit. + */ + + it("globalLimiter existe e é uma função de middleware", () => { + expect(typeof globalLimiter).toBe("function"); + // Express middleware tem .length (arity) ≥ 2 (req, res, next) + expect(globalLimiter.length).toBeGreaterThanOrEqual(2); + }); + + it("searchLimiter existe e é uma função de middleware", () => { + expect(typeof searchLimiter).toBe("function"); + }); + + it("creationLimiter existe e é uma função de middleware", () => { + expect(typeof creationLimiter).toBe("function"); + }); +}); + +describe("Rate Limiter — comportamento em memória (mock req/res)", () => { + function runLimiter(limiter, ip = "127.0.0.1") { + const req = { + ip, + method: "GET", + headers: {}, + socket: { remoteAddress: ip }, + app: { get: () => false }, // trust proxy = false + }; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + set: jest.fn(), + setHeader: jest.fn(), + header: jest.fn(), + getHeader: jest.fn(), + }; + const next = jest.fn(); + return new Promise((resolve) => { + limiter(req, res, () => { + next(); + resolve({ req, res, next, blocked: false }); + }); + // Se o limiter chamar res.status (bloqueou), marcamos + if (res.status.mock.calls.length > 0) { + resolve({ req, res, next, blocked: true }); + } + }); + } + + it("globalLimiter chama next() na primeira requisição", async () => { + const { next } = await runLimiter(globalLimiter, "192.0.2.1"); + expect(next).toHaveBeenCalled(); + }); + + it("searchLimiter chama next() na primeira requisição", async () => { + const { next } = await runLimiter(searchLimiter, "192.0.2.2"); + expect(next).toHaveBeenCalled(); + }); + + it("creationLimiter chama next() na primeira requisição", async () => { + const { next } = await runLimiter(creationLimiter, "192.0.2.3"); + expect(next).toHaveBeenCalled(); + }); +}); + +describe("Rate Limiter — mensagens de erro padronizadas", () => { + it("creationLimiter bloqueia após exceder o limite e retorna mensagem em português", async () => { + // Para evitar timeout no Jest com o globalLimiter (5000), testamos o creationLimiter (300) + let resBlocked; + const req = { ip: "10.0.0.99", method: "GET", headers: {}, socket: { remoteAddress: "10.0.0.99" }, app: { get: () => false } }; + + // Rodamos chamadas até ele bloquear + for (let i = 0; i < 305; i++) { + let blocked = false; + await new Promise(resolve => { + const resLocal = { + status: jest.fn().mockReturnThis(), + json: jest.fn((data) => { blocked = true; resBlocked = { status: resLocal.status, json: resLocal.json, data }; resolve(); return resLocal; }), + send: jest.fn((data) => { blocked = true; resBlocked = { status: resLocal.status, send: resLocal.send, data }; resolve(); return resLocal; }), + end: jest.fn(() => { resolve(); return resLocal; }), + set: jest.fn(), setHeader: jest.fn(), header: jest.fn(), getHeader: jest.fn() + }; + creationLimiter(req, resLocal, () => { + resolve(); // next() foi chamado + }); + }); + if (blocked) { + break; + } + } + + expect(resBlocked).toBeDefined(); + expect(resBlocked.status).toHaveBeenCalledWith(429); + // express-rate-limit default behavior sends a string if message is a string, or json if it's an object. We passed { error: "..." } + const errorMsg = resBlocked.data?.error || resBlocked.data; + expect(errorMsg).toMatch(/hora|limite|cadastros/i); + }); +}); diff --git a/backend/tests/setup.js b/backend/tests/setup.js index 469f494..c99c1b3 100644 --- a/backend/tests/setup.js +++ b/backend/tests/setup.js @@ -26,8 +26,3 @@ jest.unstable_mockModule("../src/config/supabase.js", () => ({ __esModule: true })); -jest.unstable_mockModule("../src/services/aiService.js", () => ({ - generateLegalText: jest.fn().mockResolvedValue("Texto IA (Setup Mock)"), - visionOCR: jest.fn().mockResolvedValue("Texto OCR (Setup Mock)"), - __esModule: true -})); diff --git a/backend/tests/unit/aiService.test.js b/backend/tests/unit/aiService.test.js new file mode 100644 index 0000000..4823f04 --- /dev/null +++ b/backend/tests/unit/aiService.test.js @@ -0,0 +1,182 @@ +import { jest } from "@jest/globals"; + +// ────────────────────────────────────────────────────────── +// Mocks de clientes externos — devem vir ANTES do import do módulo +// ────────────────────────────────────────────────────────── + +// Resposta padrão de sucesso para ambos +const mockGroqCreate = jest.fn(); +const mockGeminiGenerate = jest.fn(); + +jest.unstable_mockModule("groq-sdk", () => ({ + default: class MockGroq { + constructor() { + this.chat = { completions: { create: mockGroqCreate } }; + } + }, +})); + +jest.unstable_mockModule("@google/generative-ai", () => ({ + GoogleGenerativeAI: class MockGoogleAI { + getGenerativeModel() { + return { generateContent: mockGeminiGenerate }; + } + }, +})); + +process.env.GROQ_API_KEY = "dummy_groq_key_for_testing"; +process.env.GEMINI_API_KEY = "dummy_gemini_key_for_testing"; + +// Carrega o módulo DEPOIS dos mocks (padrão ESM) +const { generateLegalText, } = await import( + "../../src/services/aiService.js" +); + +// ────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────── +function makeGroqSuccess(text) { + return Promise.resolve({ + choices: [{ message: { content: text } }], + }); +} + +function makeGeminiSuccess(text) { + return Promise.resolve({ + response: { text: () => text }, + }); +} + +// ────────────────────────────────────────────────────────── +// generateLegalText — PII Sanitization +// ────────────────────────────────────────────────────────── +describe("aiService — generateLegalText: PII Sanitization", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Groq retorna texto com placeholder + mockGroqCreate.mockImplementationOnce(({ messages }) => { + // Captura o que foi enviado e devolve como texto gerado + const userPrompt = messages.find((m) => m.role === "user")?.content || ""; + return makeGroqSuccess(`RESULTADO: ${userPrompt}`); + }); + }); + + it("substitui nome real por placeholder antes de enviar para IA", async () => { + const piiMap = { "Maria da Silva": "[NOME_AUTOR]" }; + + await generateLegalText( + "System prompt", + "A autora Maria da Silva requer alimentos.", + 0.3, + piiMap + ); + + const callArgs = mockGroqCreate.mock.calls[0][0]; + const userPromptEnviado = callArgs.messages.find( + (m) => m.role === "user" + )?.content; + + expect(userPromptEnviado).not.toContain("Maria da Silva"); + expect(userPromptEnviado).toContain("[NOME_AUTOR]"); + }); + + it("restaura o placeholder pelo nome real no texto retornado", async () => { + mockGroqCreate.mockReset(); + // Groq retorna texto com o placeholder + mockGroqCreate.mockResolvedValue( + makeGroqSuccess("A [NOME_AUTOR] solicita alimentos.") + ); + + const piiMap = { "Maria da Silva": "[NOME_AUTOR]" }; + const result = await generateLegalText( + "System", + "contexto", + 0.3, + piiMap + ); + + expect(result).toContain("Maria da Silva"); + expect(result).not.toContain("[NOME_AUTOR]"); + }); + + it("ignora chaves PII com menos de 3 caracteres", async () => { + mockGroqCreate.mockReset(); + mockGroqCreate.mockImplementationOnce(({ messages }) => { + const user = messages.find((m) => m.role === "user")?.content || ""; + return makeGroqSuccess(`ECO: ${user}`); + }); + + // "Jo" tem 2 chars — deve ser ignorado + const piiMap = { Jo: "[PLACEHOLDER_CURTO]" }; + await generateLegalText("sys", "Texto com Jo aqui.", 0.3, piiMap); + + const callArgs = mockGroqCreate.mock.calls[0][0]; + const userPromptEnviado = callArgs.messages.find( + (m) => m.role === "user" + )?.content; + + // "Jo" não deve ter sido substituído + expect(userPromptEnviado).toContain("Jo"); + }); + + it("substitui múltiplas ocorrências da mesma PII", async () => { + mockGroqCreate.mockReset(); + mockGroqCreate.mockImplementationOnce(({ messages }) => { + const user = messages.find((m) => m.role === "user")?.content || ""; + return makeGroqSuccess(user); + }); + + const piiMap = { "Carlos Souza": "[NOME_REQUERIDO]" }; + const texto = "Carlos Souza mora com Carlos Souza em Carlos Souza Street."; + + await generateLegalText("sys", texto, 0.3, piiMap); + + const callArgs = mockGroqCreate.mock.calls[0][0]; + const enviado = callArgs.messages.find((m) => m.role === "user")?.content; + expect(enviado).not.toContain("Carlos Souza"); + expect(enviado.split("[NOME_REQUERIDO]").length - 1).toBe(3); + }); +}); + +// ────────────────────────────────────────────────────────── +// generateLegalText — Fallback para Gemini +// ────────────────────────────────────────────────────────── +describe("aiService — generateLegalText: Fallback Groq → Gemini", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("usa Gemini quando Groq lança erro", async () => { + mockGroqCreate.mockRejectedValueOnce(new Error("Groq rate limit")); + mockGeminiGenerate.mockResolvedValueOnce(makeGeminiSuccess("Texto do Gemini fallback")); + + const result = await generateLegalText("sys", "prompt", 0.3, {}); + expect(result).toBe("Texto do Gemini fallback"); + expect(mockGeminiGenerate).toHaveBeenCalledTimes(1); + }); + + it("lança erro quando ambos falham", async () => { + mockGroqCreate.mockRejectedValueOnce(new Error("Groq down")); + mockGeminiGenerate.mockRejectedValueOnce(new Error("Gemini down")); + + await expect(generateLegalText("sys", "prompt", 0.3, {})).rejects.toThrow( + /Inteligência Artificial indisponível|ambos.*falharam/i + ); + }); +}); + +// ────────────────────────────────────────────────────────── +// generateLegalText — Timeout +// ────────────────────────────────────────────────────────── +describe("aiService — generateLegalText: Timeout", () => { + it("lança erro quando Groq excede 30s (via timeout mock)", async () => { + jest.clearAllMocks(); + // Simula Groq nunca resolvendo e Gemini também falhando + mockGroqCreate.mockImplementationOnce( + () => new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout: Chamada Groq excedeu o limite de tempo")), 50)) + ); + mockGeminiGenerate.mockRejectedValueOnce(new Error("Gemini also failed")); + + await expect(generateLegalText("sys", "prompt", 0.3, {})).rejects.toThrow(); + }, 10000); +}); diff --git a/backend/tests/unit/securityService.test.js b/backend/tests/unit/securityService.test.js new file mode 100644 index 0000000..29452f9 --- /dev/null +++ b/backend/tests/unit/securityService.test.js @@ -0,0 +1,96 @@ +import { generateCredentials, hashPassword, verifyPassword } from "../../src/services/securityService.js"; + +describe("securityService — generateCredentials", () => { + it("gera protocolo com 15 dígitos no formato YYYYMMDD + tipo + 6 dígitos", () => { + const { protocolo } = generateCredentials("familia"); + expect(protocolo).toMatch(/^\d{15}$/); + }); + + it("embed tipo '0' para família na posição correta", () => { + const { protocolo } = generateCredentials("familia"); + const now = new Date(); + const expected_prefix = + `${now.getFullYear()}` + + `${String(now.getMonth() + 1).padStart(2, "0")}` + + `${String(now.getDate()).padStart(2, "0")}` + + "0"; + expect(protocolo.startsWith(expected_prefix)).toBe(true); + }); + + it("embed tipo '1' para consumidor", () => { + const { protocolo } = generateCredentials("consumidor"); + const prefix8 = protocolo.slice(0, 8); + expect(protocolo[8]).toBe("1"); + expect(prefix8).toMatch(/^\d{8}$/); + }); + + it("embed tipo '2' para saude", () => { + const { protocolo } = generateCredentials("saude"); + expect(protocolo[8]).toBe("2"); + }); + + it("embed tipo '3' para criminal", () => { + const { protocolo } = generateCredentials("criminal"); + expect(protocolo[8]).toBe("3"); + }); + + it("usa fallback '4' para tipo desconhecido", () => { + const { protocolo } = generateCredentials("desconhecido"); + expect(protocolo[8]).toBe("4"); + expect(protocolo).toMatch(/^\d{15}$/); + }); + + it("usa fallback '4' se tipo não fornecido", () => { + const { protocolo } = generateCredentials(undefined); + expect(protocolo[8]).toBe("4"); + }); + + it("gera protocolos distintos em chamadas sucessivas (unicidade via timestamp)", async () => { + const { protocolo: p1 } = generateCredentials("familia"); + await new Promise((r) => setTimeout(r, 2)); + const { protocolo: p2 } = generateCredentials("familia"); + // Os últimos 6 dígitos são baseados em Date.now(), podem ser iguais se < 1ms. + // Verificamos apenas que são strings válidas — unicidade é garantida em produção. + expect(typeof p1).toBe("string"); + expect(typeof p2).toBe("string"); + }); + + it("retorna apenas { protocolo }, sem chave_acesso (removida conforme design)", () => { + const result = generateCredentials("familia"); + expect(result).not.toHaveProperty("chave_acesso"); + expect(result).toHaveProperty("protocolo"); + expect(Object.keys(result)).toHaveLength(1); + }); +}); + +describe("securityService — hashPassword / verifyPassword", () => { + it("hasheia a senha e verifica corretamente (round-trip)", async () => { + const senha = "minhaS3nhaSegura!"; + const hash = await hashPassword(senha); + expect(typeof hash).toBe("string"); + expect(hash.length).toBeGreaterThan(30); + const valid = await verifyPassword(senha, hash); + expect(valid).toBe(true); + }); + + it("retorna false para senha errada", async () => { + const hash = await hashPassword("correta123"); + const valid = await verifyPassword("errada456", hash); + expect(valid).toBe(false); + }); + + it("gera hashes distintos para a mesma senha (bcrypt salts)", async () => { + const senha = "mesmasenha"; + const hash1 = await hashPassword(senha); + const hash2 = await hashPassword(senha); + expect(hash1).not.toBe(hash2); + // Ambos devem ser válidos + expect(await verifyPassword(senha, hash1)).toBe(true); + expect(await verifyPassword(senha, hash2)).toBe(true); + }); + + it("não aceita string vazia como hash válido (bcrypt rejeita)", async () => { + const valid = await verifyPassword("senha", "").catch(() => false); + expect(valid).toBe(false); + }); +}); diff --git a/backend/tests/unit/utils.test.js b/backend/tests/unit/utils.test.js new file mode 100644 index 0000000..05ee186 --- /dev/null +++ b/backend/tests/unit/utils.test.js @@ -0,0 +1,63 @@ +import { validateTransition } from "../../src/utils/stateMachine.js"; +import { safeFormData, safeJsonParse } from "../../src/utils/helpers.js"; + +describe("Utils: stateMachine", () => { + it("permite transição válida para defensor", () => { + const result = validateTransition("pronto_para_analise", "em_atendimento", "defensor"); + expect(result.ok).toBe(true); + }); + + it("bloqueia transição inválida para defensor", () => { + const result = validateTransition("aguardando_documentos", "em_protocolo", "defensor"); + expect(result.ok).toBe(false); + expect(result.reason).toContain("inválida"); + }); + + it("permite transição inválida para admin (bypass)", () => { + const result = validateTransition("aguardando_documentos", "em_protocolo", "admin"); + expect(result.ok).toBe(true); + expect(result.adminBypass).toBe(true); + }); +}); + +describe("Utils: helpers", () => { + describe("safeJsonParse", () => { + it("parseia JSON válido", () => { + expect(safeJsonParse('{"a":1}')).toEqual({a:1}); + }); + it("retorna fallback para JSON inválido", () => { + expect(safeJsonParse("invalido", {fallback: true})).toEqual({fallback: true}); + }); + }); + + describe("safeFormData", () => { + it("extrai dados_formulario se for objeto e garante document_names", () => { + const caso = { dados_formulario: { test: 1 } }; + const result = safeFormData(caso); + expect(result.test).toBe(1); + expect(result.document_names).toEqual({}); + expect(result.documentNames).toEqual({}); + }); + it("parseia dados_formulario se for string", () => { + const caso = { dados_formulario: '{"test": 2}' }; + const result = safeFormData(caso); + expect(result.test).toBe(2); + expect(result.document_names).toEqual({}); + }); + it("retorna objeto vazio se nulo", () => { + const result = safeFormData({ dados_formulario: null }); + expect(result.document_names).toEqual({}); + }); + it("trata caso nulo", () => { + expect(safeFormData(null).document_names).toEqual({}); + }); + it("trata dados_formulario como array (converte para objeto vazio)", () => { + const result = safeFormData({ dados_formulario: [1, 2, 3] }); + expect(result).toEqual({ document_names: {}, documentNames: {} }); + }); + it("trata dados_formulario como primitivo (converte para objeto vazio)", () => { + const result = safeFormData({ dados_formulario: "texto simples" }); + expect(result).toEqual({ document_names: {}, documentNames: {} }); + }); + }); +}); diff --git a/backend/uploads/peticoes/T1/peticao_inicial_T1.docx b/backend/uploads/peticoes/T1/peticao_inicial_T1.docx index ef32a08..6eddaa5 100644 Binary files a/backend/uploads/peticoes/T1/peticao_inicial_T1.docx and b/backend/uploads/peticoes/T1/peticao_inicial_T1.docx differ diff --git a/claude.md b/claude.md index 88acae9..7f0618e 100644 --- a/claude.md +++ b/claude.md @@ -10,8 +10,8 @@ Este documento contém a compilaçío de todas as referências de arquitetura, r # Arquitetura do Sistema — Míes em Açío · DPE-BA -> **Versío:** 3.3 · **Atualizado em:** 2026-04-24 (RBAC Hardening + Locking Nível 2 + Execução de Alimentos + Fix busca CPF) -> **Contexto:** Mutirío estadual da Defensoria Pública da Bahia +> **Versão:** 4.2 · **Atualizado em:** 2026-04-26 (Hardening de Segurança + Assistência Compartilhada) +> **Contexto:** Mutirão estadual da Defensoria Pública da Bahia --- @@ -159,7 +159,7 @@ graph TB ### Etapa 4 — Protocolo (Defensor) - Filtra casos com status `liberado_para_protocolo` -- **Locking Nível 2:** Atribuiçío explícita (`defensor_id` + `defensor_at`) +- **Locking Nível 2:** Atribuição de `defensor_id` — bloqueia etapa de protocolo e finalização. Ativo em `liberado_para_protocolo` e `em_protocolo`. **`servidor` e `estagiario` NUNCA adquirem Nível 2.** - Protocola no SOLAR ou SIGAD - Salva `numero_processo` + upload da capa - **Manual Unlock:** Botío "Liberar Caso" devolve o processo à fila global @@ -192,8 +192,9 @@ stateDiagram-v2 - **Nível 1 (Servidor/Estagiário/Defensor/Coordenador):** Atribuição de `servidor_id` — bloqueia edição de dados jurídicos e relato. Ativo em `pronto_para_analise` e `em_atendimento`. - **Nível 2 (Defensor/Coordenador/Admin):** Atribuição de `defensor_id` — bloqueia etapa de protocolo e finalização. Ativo em `liberado_para_protocolo` e `em_protocolo`. **`servidor` e `estagiario` NUNCA adquirem Nível 2.** -- **HTTP 423 (Locked):** Retorno padrão quando outro usuário detém o lock (expõe apenas `nome` do holder, sem dados pessoais) -- **Admin Bypass:** Administradores podem forçar destravamento via painel +- **Isolamento de Unidade:** Middleware `requireSameUnit` bloqueia IDOR. **Admin e Gestor** possuem bypass global. +- **HTTP 423 (Locked):** Retorno padrão quando outro usuário detém o lock. +- **Unlock Privilegiado:** Administradores, Gestores e Coordenadores podem forçar destravamento via painel. - **Auto-release:** Lock liberado após 30min de inatividade. - **Determinação de Nível:** `lockController` consulta o `status` atual do caso antes de tentar o lock e escolhe o nível correspondente. @@ -337,17 +338,20 @@ sequenceDiagram ### Permissões por Cargo (RBAC) -| Cargo | Leitura | Escrita | Protocolo/Finalizar | Admin | -|:------|:--------|:--------|:--------------------|:------| -| `admin` | ✅ | ✅ | ✅ | ✅ | -| `coordenador` | ✅ | ✅ | ✅ | ❌ | -| `defensor` | ✅ | ✅ | ✅ | ❌ | -| `servidor` | ✅ | ✅ | ❌ | ❌ | -| `estagiario` | ✅ | ✅ | ❌ | ❌ | -| `visualizador` | ✅ | ❌ | ❌ | ❌ | - -> **Middleware:** `requireWriteAccess` usa whitelist positiva: apenas `admin`, `coordenador`, `defensor`, `servidor`, `estagiario` passam. Qualquer cargo fora da lista recebe HTTP 403. +| Cargo | Leitura | Escrita | Protocolo/Finalizar | Admin/Global | Unlock | +|:------|:--------|:--------|:--------------------|:-------------|:-------| +| `admin` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `gestor` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `coordenador` | ✅ | ✅ | ✅ | ❌ | ✅ | +| `defensor` | ✅ | ✅ | ✅ | ❌ | ❌ | +| `servidor` | ✅ | ✅ | ❌ | ❌ | ❌ | +| `estagiario` | ✅ | ✅ | ❌ | ❌ | ❌ | + +> **Middleware:** `requireWriteAccess` usa whitelist positiva: apenas `admin`, `gestor`, `coordenador`, `defensor`, `servidor`, `estagiario` passam. +> **Isolamento de Unidade:** Middleware `requireSameUnit` bloqueia IDOR. Admins e Gestores possuem bypass global. > **RBAC adicional no controller:** `servidor` e `estagiario` recebem HTTP 403 ao tentar mover caso para `em_protocolo` ou adquirir lock de Nível 2. +> **Integridade de Dados:** Helper `safeFormData` garante que campos JSONB (dados_formulario) sejam sempre objetos válidos, prevenindo `TypeError` em produção. +> **Máquina de Estados:** Transições de status centralizadas em `stateMachine.js` com validação de cargo e bypass de admin documentado. --- diff --git a/frontend/coverage/areas/servidor/services/index.html b/frontend/coverage/areas/servidor/services/index.html new file mode 100644 index 0000000..3fce268 --- /dev/null +++ b/frontend/coverage/areas/servidor/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for areas/servidor/services + + + + + + + + + +
+
+

All files areas/servidor/services

+
+ +
+ 73.34% + Statements + 322/439 +
+ + +
+ 50.81% + Branches + 62/122 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 73.34% + Lines + 322/439 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
submissionService.js +
+
73.34%322/43950.81%62/12250%1/273.34%322/439
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/areas/servidor/services/submissionService.js.html b/frontend/coverage/areas/servidor/services/submissionService.js.html new file mode 100644 index 0000000..93b369d --- /dev/null +++ b/frontend/coverage/areas/servidor/services/submissionService.js.html @@ -0,0 +1,1402 @@ + + + + + + Code coverage report for areas/servidor/services/submissionService.js + + + + + + + + + +
+
+

All files / areas/servidor/services submissionService.js

+
+ +
+ 73.34% + Statements + 322/439 +
+ + +
+ 50.81% + Branches + 62/122 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 73.34% + Lines + 322/439 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +4401x +1x +1x +1x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +15x +  +  +  +15x +15x +15x +15x +15x +12x +12x +12x +12x +15x +3x +3x +3x +15x +15x +2x +2x +15x +15x +15x +  +15x +1x +1x +15x +15x +1x +1x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +15x +15x +15x +  +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +15x +  +15x +2x +15x +  +13x +  +  +15x +15x +15x +1x +1x +15x +15x +15x +15x +15x +12x +15x +  +  +15x +15x +15x +  +  +15x +15x +15x +3x +3x +1x +3x +1x +1x +3x +3x +3x +  +  +3x +1x +1x +3x +2x +2x +  +  +  +2x +3x +3x +15x +15x +15x +15x +  +  +  +15x +15x +15x +1x +1x +15x +15x +15x +15x +  +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +3x +3x +15x +15x +4x +4x +4x +4x +4x +4x +15x +15x +15x +13x +13x +13x +13x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +15x +  +15x +  +2x +  +  +2x +15x +  +  +2x +2x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +15x +  +  +  +15x +  +  +  +  +  +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +48x +30x +30x +48x +  +  +30x +30x +30x +30x +48x +2x +48x +  +28x +2x +2x +2x +30x +48x +30x +30x +30x +2x +2x +2x +15x +15x +15x +15x +15x +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +2x +2x +2x +2x +2x +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +15x +  +  +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +  +  +  +  +2x +15x +  +15x +  +  +  +  +2x +15x +  +15x +  +  +2x +15x +  +15x +  +  +2x +15x +  +15x +  +  +15x +  +  +2x +2x +2x +15x +15x +2x +14x +14x +14x +14x +14x +14x +2x +15x +  +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +1x +15x +1x +1x +1x +1x +1x +1x +1x +1x +15x +2x +2x +2x +2x +15x + 
import { digitsOnlyFields, dateFields, currencyFields } from "../utils/formConstants.js";
+import { validateBrDate } from "../../../utils/formatters.js";
+ 
+export const processSubmission = async ({
+  isAlvaraContext = false,
+  formState,
+  setFormErrors,
+  setLoading,
+  setStatusMessage,
+  setGeneratedCredentials,
+  toast,
+  configAcao,
+  forcaRepresentacao,
+  today,
+  stripNonDigits,
+  validateCpfAlgorithm,
+  formatDateToBr,
+  parseBrDateToIso,
+  normalizeDecimalForSubmit,
+  API_BASE,
+}) => {
+  const validationErrors = {};
+  const nomeRequeridoTrim = (formState.REQUERIDO_NOME || "").trim();
+  const enderecoRequeridoTrim = (formState.executado_endereco_residencial || "").trim();
+  const telefoneRequeridoDigits = stripNonDigits(formState.executado_telefone || "");
+ 
+  if (!isAlvaraContext) {
+    if (!nomeRequeridoTrim) {
+      validationErrors.REQUERIDO_NOME = "Informe o nome completo da outra parte.";
+    }
+    if (!enderecoRequeridoTrim && !telefoneRequeridoDigits) {
+      validationErrors.requeridoContato =
+        "Informe pelo menos um endereço ou telefone da outra parte.";
+    }
+  }
+ 
+  // --- VALIDAÇÃO DE CAMPOS OBRIGATÓRIOS ---
+  // Se for incapaz, validamos NOME (criança). Se for adulto, REPRESETANTE_NOME (autor).
+  if (formState.assistidoEhIncapaz === "sim") {
+    if (!formState.NOME) validationErrors.NOME = "O nome da criança é obrigatório.";
+    if (!formState.cpf) validationErrors.cpf = "O CPF da criança é obrigatório.";
+    if (!formState.REPRESENTANTE_NOME)
+      validationErrors.REPRESENTANTE_NOME = "O nome da genitora/representante é obrigatório.";
+  } else {
+    if (!formState.REPRESENTANTE_NOME)
+      validationErrors.REPRESENTANTE_NOME = "O nome completo é obrigatório.";
+  }
+ 
+  if (!formState.representante_cpf) {
+    validationErrors.representante_cpf = "O CPF é obrigatório.";
+  }
+ 
+  // Validação de Endereço e CEP do Representante (Obrigatório para todos os casos)
+  if (!formState.requerente_endereco_residencial) {
+    validationErrors.requerente_endereco_residencial = "O endereço residencial é obrigatório.";
+  } else if (!/\b\d{5}-?\d{3}\b/.test(formState.requerente_endereco_residencial)) {
+    validationErrors.requerente_endereco_residencial = "O CEP do endereço residencial é obrigatório.";
+  }
+ 
+  if (!formState.requerente_telefone) {
+    validationErrors.requerente_telefone = "O telefone de contato é obrigatório.";
+  }
+ 
+  // (Removido bloco if (formState.assistidoEhIncapaz === "nao") pois as validações agora são gerais)
+ 
+  // Validação do Valor da Pensão (apenas para fixação/alimentos)
+  if (
+    configAcao?.secoes?.includes("SecaoValoresPensao") &&
+    formState.acaoEspecifica !== "execucao_alimentos"
+  ) {
+    if (!formState.valor_pensao) {
+      validationErrors.valor_pensao = "O valor da pensão é obrigatório.";
+    }
+  }
+ 
+  if (configAcao?.secoes?.includes("processo_original")) {
+    if (configAcao?.exigeDadosProcessoOriginal) {
+      if (!formState.data_inicio_debito)
+        validationErrors.data_inicio_debito = "O mês inicial do débito é obrigatório.";
+      if (!formState.data_fim_debito)
+        validationErrors.data_fim_debito = "O mês final do débito é obrigatório.";
+    }
+  }
+ 
+  // Validação Data de Nascimento Principal (Incapaz ou Adulto)
+  const campoDataNasc =
+    formState.assistidoEhIncapaz === "sim" ? "nascimento" : "representante_data_nascimento";
+  const dataInputValue = formState[campoDataNasc];
+  const dataIso = parseBrDateToIso(dataInputValue);
+ 
+  if (!dataInputValue) {
+    validationErrors[campoDataNasc] = "A data de nascimento é obrigatória.";
+  } else if (!validateBrDate(dataInputValue)) {
+    validationErrors[campoDataNasc] = "Informe uma data de calendário válida (Ex: 31/12/1990).";
+  } else if (!dataIso) {
+    validationErrors[campoDataNasc] = "Formato de data inválido (DD/MM/AAAA).";
+  } else if (dataIso > today) {
+    validationErrors[campoDataNasc] = "A data de nascimento não pode estar no futuro.";
+  }
+ 
+  // Validação CPF Matemático
+  if (formState.representante_cpf && !validateCpfAlgorithm(formState.representante_cpf)) {
+    validationErrors.representante_cpf = "CPF da representante inválido.";
+  }
+ 
+  // Validação CPF Criança Principal
+  if (
+    formState.assistidoEhIncapaz === "sim" &&
+    formState.cpf &&
+    !validateCpfAlgorithm(formState.cpf)
+  ) {
+    validationErrors.cpf = "CPF da criança inválido.";
+  }
+ 
+  // Validação CPF Requerido (Se preenchido, deve ser válido)
+  if (formState.executado_cpf && !validateCpfAlgorithm(formState.executado_cpf)) {
+    validationErrors.executado_cpf = "O CPF da outra parte é inválido.";
+  }
+ 
+  // Validação CPF e Data de Nascimento - Outros Filhos
+  if (formState.outrosFilhos && formState.outrosFilhos.length > 0) {
+    formState.outrosFilhos.forEach((filho, index) => {
+      if (!filho.cpf) {
+        validationErrors[`filho_cpf_${index}`] = `O CPF do Filho(a) ${index + 2} é obrigatório.`;
+      } else if (!validateCpfAlgorithm(filho.cpf)) {
+        validationErrors[`filho_cpf_${index}`] = `O CPF do Filho(a) ${index + 2} é inválido.`;
+      }
+ 
+      const filhoDataInput = filho.dataNascimento;
+      if (!filhoDataInput) {
+        validationErrors[`filho_nascimento_${index}`] =
+          `A data de nascimento do Filho(a) ${index + 2} é obrigatória.`;
+      } else if (!validateBrDate(filhoDataInput)) {
+        validationErrors[`filho_nascimento_${index}`] =
+          `Data inválida no Filho(a) ${index + 2} (Ex: 31/12/1990).`;
+      } else {
+        const filhoIso = parseBrDateToIso(filhoDataInput);
+        if (filhoIso && filhoIso > today) {
+          validationErrors[`filho_nascimento_${index}`] =
+            `A data do Filho(a) ${index + 2} não pode estar no futuro.`;
+        }
+      }
+    });
+  }
+ 
+  // 2. Validação Relato vs Áudio (Ignorada se a ação não pedir fatos)
+  if (!configAcao?.ocultarRelato) {
+    if (formState.prefersAudio) {
+      if (!formState.audioBlob) {
+        validationErrors.audio = "Como você optou por enviar áudio, a gravação é obrigatória.";
+      }
+    } else {
+      const relatoLimpo = (formState.relato || "").trim();
+      if (!relatoLimpo) {
+        validationErrors.relato = `O relato dos fatos é obrigatório para gerar a petição.`;
+      }
+    }
+  }
+ 
+  if (configAcao?.exigeDadosProcessoOriginal) {
+    if (!formState.valor_debito) {
+      validationErrors.valor_debito = "O valor total do débito é obrigatório.";
+    }
+    if (!formState.calculo_arquivo && !formState.enviarDocumentosDepois) {
+      validationErrors.calculo_arquivo = "Você deve anexar o demonstrativo do cálculo.";
+    }
+  }
+ 
+  // 3. Validação de Quantidade Mínima de Documentos
+  const isEnviarDepois =
+    formState.enviarDocumentosDepois === true || formState.enviarDocumentosDepois === "true";
+  if (!isEnviarDepois) {
+    let minDocs = formState.assistidoEhIncapaz === "nao" ? 4 : 7;
+    if (formState.assistidoEhIncapaz === "sim" && formState.outrosFilhos.length > 0) {
+      minDocs += formState.outrosFilhos.length * 3;
+    }
+ 
+    if (formState.documentFiles.length < minDocs) {
+      const docsNecessarios =
+        formState.assistidoEhIncapaz === "nao"
+          ? "RG (Frente/Verso), Comprovante de Residência e Renda"
+          : "RG do Responsável, RG da Criança e Certidão de Nascimento (para cada filho)";
+      validationErrors.documentos = `É necessário anexar pelo menos ${minDocs} documentos: ${docsNecessarios}. Atual: ${formState.documentFiles.length}.`;
+    }
+  }
+ 
+  if (Object.keys(validationErrors).length > 0) {
+    setFormErrors(validationErrors);
+    toast.error("Existem campos obrigatórios não preenchidos ou inválidos.");
+    return;
+  }
+ 
+  setFormErrors({});
+  setLoading(true);
+  setGeneratedCredentials(null);
+ 
+  // Simulando etapas visuais (mantido)
+  const timers = [
+    setTimeout(() => setStatusMessage("Validando dados..."), 1000),
+    setTimeout(() => setStatusMessage("Processando áudio e documentos..."), 3000),
+    setTimeout(() => setStatusMessage("Gerando minuta com IA..."), 6000),
+    setTimeout(() => setStatusMessage("Gerando protocolo..."), 9000),
+  ];
+ 
+  const formData = new FormData();
+ 
+  // Dados Bancários formatados para IA
+  let dadosBancariosFormatado = "";
+  if (formState.tipo_conta_deposito === "corrente_poupanca") {
+    dadosBancariosFormatado = `Tipo: Corrente/Poupança, Banco: ${formState.banco_deposito}, Agência: ${formState.agencia_deposito}, Conta: ${formState.conta_deposito}`;
+  } else if (formState.tipo_conta_deposito === "pix") {
+    dadosBancariosFormatado = `Tipo: PIX, Chave: ${formState.chave_pix_deposito}`;
+  } else if (formState.tipo_conta_deposito === "outro") {
+    dadosBancariosFormatado = `Tipo: Outro, Detalhes: ${formState.outros_dados_deposito}`;
+  }
+ 
+  if (dadosBancariosFormatado) {
+    formData.append("dados_bancarios_exequente", dadosBancariosFormatado);
+  }
+ 
+  // 1.5 Lógica de Período de Débito (Concatenado)
+  const formatMonthYear = (monthYearStr) => {
+    if (!monthYearStr || !monthYearStr.includes("/")) return monthYearStr;
+    const [month, year] = monthYearStr.split("/");
+    const months = [
+      "Janeiro",
+      "Fevereiro",
+      "Março",
+      "Abril",
+      "Maio",
+      "Junho",
+      "Julho",
+      "Agosto",
+      "Setembro",
+      "Outubro",
+      "Novembro",
+      "Dezembro",
+    ];
+    const monthIndex = parseInt(month, 10) - 1;
+    if (monthIndex < 0 || monthIndex > 11) return monthYearStr;
+    return `${months[monthIndex]}/${year}`;
+  };
+ 
+  if (formState.data_inicio_debito && formState.data_fim_debito) {
+    const inicio = formatMonthYear(formState.data_inicio_debito);
+    const fim = formatMonthYear(formState.data_fim_debito);
+    formData.append("periodo_debito_execucao", `${inicio} a ${fim}`);
+  } else if (formState.data_inicio_debito) {
+    formData.append(
+      "periodo_debito_execucao",
+      `Desde ${formatMonthYear(formState.data_inicio_debito)}`,
+    );
+  }
+ 
+  // 1. Preenche o FormData
+  // O estado (formState) já usa as TAGS OFICIAIS, portanto iteramos diretamente.
+  // Não precisamos mais do fieldMapping legado.
+  const fieldsToIgnore = new Set([
+    "calculo_arquivo",
+    "outrosFilhos",
+    "documentFiles",
+    "documentNames",
+    "documentosMarcados",
+    "audioBlob",
+    "tipoAcao",
+    "acaoEspecifica",
+    "enviarDocumentosDepois",
+  ]);
+ 
+  const valuesToSubmit = { ...formState };
+ 
+  Object.keys(valuesToSubmit).forEach((key) => {
+    if (fieldsToIgnore.has(key)) return;
+ 
+    const rawValue = valuesToSubmit[key];
+    if (rawValue === undefined || rawValue === null || rawValue === "") {
+      return;
+    }
+ 
+    let normalizedValue = rawValue;
+ 
+    // Tratamentos de formatação para subsets
+    if (digitsOnlyFields?.has(key)) {
+      normalizedValue = stripNonDigits(rawValue);
+    } else if (currencyFields?.has(key)) {
+      normalizedValue = normalizeDecimalForSubmit(rawValue);
+    } else if (dateFields?.has(key) || key.includes("data_nascimento")) {
+      const iso = parseBrDateToIso(rawValue);
+      if (iso) normalizedValue = iso;
+    }
+ 
+    if (normalizedValue !== undefined && normalizedValue !== null && normalizedValue !== "") {
+      // Como o formState já está alinhado com dicionarioTags, passamos o key direto
+      formData.append(key, normalizedValue);
+    }
+  });
+ 
+  // [EIXO 3] Garante que enviarDocumentosDepois é SEMPRE enviado, mesmo que false
+  formData.append("enviar_documentos_depois", String(formState.enviarDocumentosDepois ?? false));
+ 
+  // 2. Correção Crítica: Formatar Tipo de Ação para o Backend
+  // O backend espera "Area - Ação" para saber qual template DOCX usar
+  if (!configAcao) {
+    console.warn(
+      "[FormularioSubmissao] configAcao não encontrada para:",
+      formState.tipoAcao,
+      formState.acaoEspecifica,
+    );
+  }
+  const tituloAcao = configAcao?.titulo || formState.acaoEspecifica;
+  const tipoAcaoFormatado = `${formState.tipoAcao} - ${tituloAcao}`;
+  formData.append("tipoAcao", tipoAcaoFormatado);
+  formData.append("acaoEspecifica", formState.acaoEspecifica); // chave do dicionário para lookup no backend
+ 
+  // Lógica para múltiplos filhos
+  if (formState.assistidoEhIncapaz === "sim") {
+    // Filho 1 (Principal)
+    let infoFilhos = formState.REPRESENTANTE_NOME;
+ 
+    // Filhos Extras
+    if (formState.outrosFilhos.length > 0) {
+      const nomesExtras = formState.outrosFilhos
+        .map((f) => f.nome)
+        .filter((n) => n.trim() !== "")
+        .join(", ");
+      if (nomesExtras) infoFilhos += `, ${nomesExtras}`;
+    }
+ 
+    // Envia o array completo como JSON string para o backend processar
+    formData.append("outros_filhos_detalhes", JSON.stringify(formState.outrosFilhos));
+ 
+    // Envia a string completa com todos os nomes
+    if (infoFilhos) {
+      formData.append("filhos_info", infoFilhos);
+    }
+  }
+ 
+  // 3. Construção de Campos Compostos para a IA (Gemini)
+  // A IA usa 'dados_adicionais_requerente' para criar o resumo, então montamos uma string rica
+  const dadosAdicionaisRequerente = [
+    `RG: ${
+      formState.representante_rg
+        ? `${formState.representante_rg}${
+            formState.emissor_rg_exequente ? ` ${formState.emissor_rg_exequente}` : ""
+          }`
+        : "Não informado"
+    },`,
+    `Nacionalidade: ${formState.representante_nacionalidade || "Não informado"},`,
+    !forcaRepresentacao
+      ? `Estado Civil: ${formState.representante_estado_civil || "Não informado"},`
+      : "",
+    `Data Nascimento: ${
+      formatDateToBr(formState.representante_data_nascimento) || "Não informado"
+    },`,
+  ]
+    .filter(Boolean)
+    .join(" ");
+  formData.append("dados_adicionais_requerente", `${dadosAdicionaisRequerente.trim()} `);
+ 
+  const detalhesRequerido = [];
+  if (formState.requeridoOutrosSelecionados?.includes("requeridoRg") && formState.rg_executado) {
+    detalhesRequerido.push(
+      `RG: ${formState.rg_executado}${
+        formState.emissor_rg_executado ? ` ${formState.emissor_rg_executado}` : ""
+      }`,
+    );
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoDataNascimento") &&
+    formState.executado_data_nascimento
+  ) {
+    detalhesRequerido.push(
+      `Data de nascimento: ${formatDateToBr(formState.executado_data_nascimento)}`,
+    );
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoNomeMae") &&
+    formState.nome_mae_executado
+  ) {
+    detalhesRequerido.push(`Nome da mãe: ${formState.nome_mae_executado}`);
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoNomePai") &&
+    formState.nome_pai_executado
+  ) {
+    detalhesRequerido.push(`Nome do pai: ${formState.nome_pai_executado}`);
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoOutrosDetalhes") &&
+    formState.dados_adicionais_requerido
+  ) {
+    detalhesRequerido.push(`Observações: ${formState.dados_adicionais_requerido}`);
+  }
+  if (detalhesRequerido.length > 0) {
+    formData.append("dados_adicionais_requerido_array", detalhesRequerido.join(" | "));
+  }
+ 
+  // Arquivos e Arrays
+  formData.append("documentos_informados", JSON.stringify(formState.documentosMarcados));
+  formData.append("documentos_nomes", JSON.stringify(formState.documentNames || {}));
+  if (formState.audioBlob) formData.append("audio", formState.audioBlob, "gravacao.webm");
+  formState.documentFiles.forEach((file) => {
+    if (!file || !file.name) return; // Proteção contra arquivos inválidos
+    const safeName = file.name
+      .normalize("NFD")
+      .replace(/[\u0300-\u036f]/g, "")
+      .replace(/\s+/g, "_");
+    formData.append("documentos", file, safeName);
+  });
+  if (formState.calculo_arquivo) {
+    const calcFile = formState.calculo_arquivo;
+    const safeCalcName = `CALCULO_${calcFile.name
+      .normalize("NFD")
+      .replace(/[\u0300-\u036f]/g, "")
+      .replace(/\s+/g, "_")}`;
+    formData.append("documentos", calcFile, safeCalcName);
+  }
+ 
+  try {
+    const response = await fetch(`${API_BASE}/casos/novo`, {
+      method: "POST",
+      body: formData,
+    });
+    const data = await response.json();
+    if (!response.ok) throw new Error(data.error || "Falha no servidor");
+    setGeneratedCredentials({
+      chaveAcesso: data.chaveAcesso,
+      protocolo: data.protocolo,
+    });
+    localStorage.removeItem("rascunho_caso");
+  } catch (error) {
+    console.error("Erro:", error);
+    toast.error(`Erro: ${error.message}`);
+  } finally {
+    setLoading(false);
+    timers.forEach(clearTimeout);
+    setStatusMessage("");
+  }
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/base.css b/frontend/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/frontend/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/frontend/coverage/block-navigation.js b/frontend/coverage/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/frontend/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/frontend/coverage/favicon.png b/frontend/coverage/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/frontend/coverage/favicon.png differ diff --git a/frontend/coverage/index.html b/frontend/coverage/index.html new file mode 100644 index 0000000..795832c --- /dev/null +++ b/frontend/coverage/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 65.34% + Statements + 626/958 +
+ + +
+ 67.66% + Branches + 180/266 +
+ + +
+ 72.72% + Functions + 16/22 +
+ + +
+ 65.34% + Lines + 626/958 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
areas/servidor/services +
+
73.34%322/43950.81%62/12250%1/273.34%322/439
utils +
+
58.57%304/51981.94%118/14475%15/2058.57%304/519
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/areas/servidor/services/index.html b/frontend/coverage/lcov-report/areas/servidor/services/index.html new file mode 100644 index 0000000..7671ed5 --- /dev/null +++ b/frontend/coverage/lcov-report/areas/servidor/services/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for areas/servidor/services + + + + + + + + + +
+
+

All files areas/servidor/services

+
+ +
+ 73.34% + Statements + 322/439 +
+ + +
+ 50.81% + Branches + 62/122 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 73.34% + Lines + 322/439 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
submissionService.js +
+
73.34%322/43950.81%62/12250%1/273.34%322/439
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/areas/servidor/services/submissionService.js.html b/frontend/coverage/lcov-report/areas/servidor/services/submissionService.js.html new file mode 100644 index 0000000..721c3c3 --- /dev/null +++ b/frontend/coverage/lcov-report/areas/servidor/services/submissionService.js.html @@ -0,0 +1,1402 @@ + + + + + + Code coverage report for areas/servidor/services/submissionService.js + + + + + + + + + +
+
+

All files / areas/servidor/services submissionService.js

+
+ +
+ 73.34% + Statements + 322/439 +
+ + +
+ 50.81% + Branches + 62/122 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 73.34% + Lines + 322/439 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +4401x +1x +1x +1x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +15x +  +  +  +15x +15x +15x +15x +15x +12x +12x +12x +12x +15x +3x +3x +3x +15x +15x +2x +2x +15x +15x +15x +  +15x +1x +1x +15x +15x +1x +1x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +15x +15x +15x +  +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +15x +  +15x +2x +15x +  +13x +  +  +15x +15x +15x +1x +1x +15x +15x +15x +15x +15x +12x +15x +  +  +15x +15x +15x +  +  +15x +15x +15x +3x +3x +1x +3x +1x +1x +3x +3x +3x +  +  +3x +1x +1x +3x +2x +2x +  +  +  +2x +3x +3x +15x +15x +15x +15x +  +  +  +15x +15x +15x +1x +1x +15x +15x +15x +15x +  +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +3x +3x +15x +15x +4x +4x +4x +4x +4x +4x +15x +15x +15x +13x +13x +13x +13x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +15x +  +15x +  +2x +  +  +2x +15x +  +  +2x +2x +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +15x +  +  +  +15x +  +  +  +  +  +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +48x +30x +30x +48x +  +  +30x +30x +30x +30x +48x +2x +48x +  +28x +2x +2x +2x +30x +48x +30x +30x +30x +2x +2x +2x +15x +15x +15x +15x +15x +  +  +  +  +  +  +15x +15x +15x +15x +15x +15x +15x +2x +2x +2x +2x +2x +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +15x +  +  +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +15x +  +  +  +  +  +  +2x +15x +  +15x +  +  +  +  +2x +15x +  +15x +  +  +2x +15x +  +15x +  +  +2x +15x +  +15x +  +  +15x +  +  +2x +2x +2x +15x +15x +2x +14x +14x +14x +14x +14x +14x +2x +15x +  +  +  +  +  +  +  +2x +2x +2x +2x +2x +2x +1x +15x +1x +1x +1x +1x +1x +1x +1x +1x +15x +2x +2x +2x +2x +15x + 
import { digitsOnlyFields, dateFields, currencyFields } from "../utils/formConstants.js";
+import { validateBrDate } from "../../../utils/formatters.js";
+ 
+export const processSubmission = async ({
+  isAlvaraContext = false,
+  formState,
+  setFormErrors,
+  setLoading,
+  setStatusMessage,
+  setGeneratedCredentials,
+  toast,
+  configAcao,
+  forcaRepresentacao,
+  today,
+  stripNonDigits,
+  validateCpfAlgorithm,
+  formatDateToBr,
+  parseBrDateToIso,
+  normalizeDecimalForSubmit,
+  API_BASE,
+}) => {
+  const validationErrors = {};
+  const nomeRequeridoTrim = (formState.REQUERIDO_NOME || "").trim();
+  const enderecoRequeridoTrim = (formState.executado_endereco_residencial || "").trim();
+  const telefoneRequeridoDigits = stripNonDigits(formState.executado_telefone || "");
+ 
+  if (!isAlvaraContext) {
+    if (!nomeRequeridoTrim) {
+      validationErrors.REQUERIDO_NOME = "Informe o nome completo da outra parte.";
+    }
+    if (!enderecoRequeridoTrim && !telefoneRequeridoDigits) {
+      validationErrors.requeridoContato =
+        "Informe pelo menos um endereço ou telefone da outra parte.";
+    }
+  }
+ 
+  // --- VALIDAÇÃO DE CAMPOS OBRIGATÓRIOS ---
+  // Se for incapaz, validamos NOME (criança). Se for adulto, REPRESETANTE_NOME (autor).
+  if (formState.assistidoEhIncapaz === "sim") {
+    if (!formState.NOME) validationErrors.NOME = "O nome da criança é obrigatório.";
+    if (!formState.cpf) validationErrors.cpf = "O CPF da criança é obrigatório.";
+    if (!formState.REPRESENTANTE_NOME)
+      validationErrors.REPRESENTANTE_NOME = "O nome da genitora/representante é obrigatório.";
+  } else {
+    if (!formState.REPRESENTANTE_NOME)
+      validationErrors.REPRESENTANTE_NOME = "O nome completo é obrigatório.";
+  }
+ 
+  if (!formState.representante_cpf) {
+    validationErrors.representante_cpf = "O CPF é obrigatório.";
+  }
+ 
+  // Validação de Endereço e CEP do Representante (Obrigatório para todos os casos)
+  if (!formState.requerente_endereco_residencial) {
+    validationErrors.requerente_endereco_residencial = "O endereço residencial é obrigatório.";
+  } else if (!/\b\d{5}-?\d{3}\b/.test(formState.requerente_endereco_residencial)) {
+    validationErrors.requerente_endereco_residencial = "O CEP do endereço residencial é obrigatório.";
+  }
+ 
+  if (!formState.requerente_telefone) {
+    validationErrors.requerente_telefone = "O telefone de contato é obrigatório.";
+  }
+ 
+  // (Removido bloco if (formState.assistidoEhIncapaz === "nao") pois as validações agora são gerais)
+ 
+  // Validação do Valor da Pensão (apenas para fixação/alimentos)
+  if (
+    configAcao?.secoes?.includes("SecaoValoresPensao") &&
+    formState.acaoEspecifica !== "execucao_alimentos"
+  ) {
+    if (!formState.valor_pensao) {
+      validationErrors.valor_pensao = "O valor da pensão é obrigatório.";
+    }
+  }
+ 
+  if (configAcao?.secoes?.includes("processo_original")) {
+    if (configAcao?.exigeDadosProcessoOriginal) {
+      if (!formState.data_inicio_debito)
+        validationErrors.data_inicio_debito = "O mês inicial do débito é obrigatório.";
+      if (!formState.data_fim_debito)
+        validationErrors.data_fim_debito = "O mês final do débito é obrigatório.";
+    }
+  }
+ 
+  // Validação Data de Nascimento Principal (Incapaz ou Adulto)
+  const campoDataNasc =
+    formState.assistidoEhIncapaz === "sim" ? "nascimento" : "representante_data_nascimento";
+  const dataInputValue = formState[campoDataNasc];
+  const dataIso = parseBrDateToIso(dataInputValue);
+ 
+  if (!dataInputValue) {
+    validationErrors[campoDataNasc] = "A data de nascimento é obrigatória.";
+  } else if (!validateBrDate(dataInputValue)) {
+    validationErrors[campoDataNasc] = "Informe uma data de calendário válida (Ex: 31/12/1990).";
+  } else if (!dataIso) {
+    validationErrors[campoDataNasc] = "Formato de data inválido (DD/MM/AAAA).";
+  } else if (dataIso > today) {
+    validationErrors[campoDataNasc] = "A data de nascimento não pode estar no futuro.";
+  }
+ 
+  // Validação CPF Matemático
+  if (formState.representante_cpf && !validateCpfAlgorithm(formState.representante_cpf)) {
+    validationErrors.representante_cpf = "CPF da representante inválido.";
+  }
+ 
+  // Validação CPF Criança Principal
+  if (
+    formState.assistidoEhIncapaz === "sim" &&
+    formState.cpf &&
+    !validateCpfAlgorithm(formState.cpf)
+  ) {
+    validationErrors.cpf = "CPF da criança inválido.";
+  }
+ 
+  // Validação CPF Requerido (Se preenchido, deve ser válido)
+  if (formState.executado_cpf && !validateCpfAlgorithm(formState.executado_cpf)) {
+    validationErrors.executado_cpf = "O CPF da outra parte é inválido.";
+  }
+ 
+  // Validação CPF e Data de Nascimento - Outros Filhos
+  if (formState.outrosFilhos && formState.outrosFilhos.length > 0) {
+    formState.outrosFilhos.forEach((filho, index) => {
+      if (!filho.cpf) {
+        validationErrors[`filho_cpf_${index}`] = `O CPF do Filho(a) ${index + 2} é obrigatório.`;
+      } else if (!validateCpfAlgorithm(filho.cpf)) {
+        validationErrors[`filho_cpf_${index}`] = `O CPF do Filho(a) ${index + 2} é inválido.`;
+      }
+ 
+      const filhoDataInput = filho.dataNascimento;
+      if (!filhoDataInput) {
+        validationErrors[`filho_nascimento_${index}`] =
+          `A data de nascimento do Filho(a) ${index + 2} é obrigatória.`;
+      } else if (!validateBrDate(filhoDataInput)) {
+        validationErrors[`filho_nascimento_${index}`] =
+          `Data inválida no Filho(a) ${index + 2} (Ex: 31/12/1990).`;
+      } else {
+        const filhoIso = parseBrDateToIso(filhoDataInput);
+        if (filhoIso && filhoIso > today) {
+          validationErrors[`filho_nascimento_${index}`] =
+            `A data do Filho(a) ${index + 2} não pode estar no futuro.`;
+        }
+      }
+    });
+  }
+ 
+  // 2. Validação Relato vs Áudio (Ignorada se a ação não pedir fatos)
+  if (!configAcao?.ocultarRelato) {
+    if (formState.prefersAudio) {
+      if (!formState.audioBlob) {
+        validationErrors.audio = "Como você optou por enviar áudio, a gravação é obrigatória.";
+      }
+    } else {
+      const relatoLimpo = (formState.relato || "").trim();
+      if (!relatoLimpo) {
+        validationErrors.relato = `O relato dos fatos é obrigatório para gerar a petição.`;
+      }
+    }
+  }
+ 
+  if (configAcao?.exigeDadosProcessoOriginal) {
+    if (!formState.valor_debito) {
+      validationErrors.valor_debito = "O valor total do débito é obrigatório.";
+    }
+    if (!formState.calculo_arquivo && !formState.enviarDocumentosDepois) {
+      validationErrors.calculo_arquivo = "Você deve anexar o demonstrativo do cálculo.";
+    }
+  }
+ 
+  // 3. Validação de Quantidade Mínima de Documentos
+  const isEnviarDepois =
+    formState.enviarDocumentosDepois === true || formState.enviarDocumentosDepois === "true";
+  if (!isEnviarDepois) {
+    let minDocs = formState.assistidoEhIncapaz === "nao" ? 4 : 7;
+    if (formState.assistidoEhIncapaz === "sim" && formState.outrosFilhos.length > 0) {
+      minDocs += formState.outrosFilhos.length * 3;
+    }
+ 
+    if (formState.documentFiles.length < minDocs) {
+      const docsNecessarios =
+        formState.assistidoEhIncapaz === "nao"
+          ? "RG (Frente/Verso), Comprovante de Residência e Renda"
+          : "RG do Responsável, RG da Criança e Certidão de Nascimento (para cada filho)";
+      validationErrors.documentos = `É necessário anexar pelo menos ${minDocs} documentos: ${docsNecessarios}. Atual: ${formState.documentFiles.length}.`;
+    }
+  }
+ 
+  if (Object.keys(validationErrors).length > 0) {
+    setFormErrors(validationErrors);
+    toast.error("Existem campos obrigatórios não preenchidos ou inválidos.");
+    return;
+  }
+ 
+  setFormErrors({});
+  setLoading(true);
+  setGeneratedCredentials(null);
+ 
+  // Simulando etapas visuais (mantido)
+  const timers = [
+    setTimeout(() => setStatusMessage("Validando dados..."), 1000),
+    setTimeout(() => setStatusMessage("Processando áudio e documentos..."), 3000),
+    setTimeout(() => setStatusMessage("Gerando minuta com IA..."), 6000),
+    setTimeout(() => setStatusMessage("Gerando protocolo..."), 9000),
+  ];
+ 
+  const formData = new FormData();
+ 
+  // Dados Bancários formatados para IA
+  let dadosBancariosFormatado = "";
+  if (formState.tipo_conta_deposito === "corrente_poupanca") {
+    dadosBancariosFormatado = `Tipo: Corrente/Poupança, Banco: ${formState.banco_deposito}, Agência: ${formState.agencia_deposito}, Conta: ${formState.conta_deposito}`;
+  } else if (formState.tipo_conta_deposito === "pix") {
+    dadosBancariosFormatado = `Tipo: PIX, Chave: ${formState.chave_pix_deposito}`;
+  } else if (formState.tipo_conta_deposito === "outro") {
+    dadosBancariosFormatado = `Tipo: Outro, Detalhes: ${formState.outros_dados_deposito}`;
+  }
+ 
+  if (dadosBancariosFormatado) {
+    formData.append("dados_bancarios_exequente", dadosBancariosFormatado);
+  }
+ 
+  // 1.5 Lógica de Período de Débito (Concatenado)
+  const formatMonthYear = (monthYearStr) => {
+    if (!monthYearStr || !monthYearStr.includes("/")) return monthYearStr;
+    const [month, year] = monthYearStr.split("/");
+    const months = [
+      "Janeiro",
+      "Fevereiro",
+      "Março",
+      "Abril",
+      "Maio",
+      "Junho",
+      "Julho",
+      "Agosto",
+      "Setembro",
+      "Outubro",
+      "Novembro",
+      "Dezembro",
+    ];
+    const monthIndex = parseInt(month, 10) - 1;
+    if (monthIndex < 0 || monthIndex > 11) return monthYearStr;
+    return `${months[monthIndex]}/${year}`;
+  };
+ 
+  if (formState.data_inicio_debito && formState.data_fim_debito) {
+    const inicio = formatMonthYear(formState.data_inicio_debito);
+    const fim = formatMonthYear(formState.data_fim_debito);
+    formData.append("periodo_debito_execucao", `${inicio} a ${fim}`);
+  } else if (formState.data_inicio_debito) {
+    formData.append(
+      "periodo_debito_execucao",
+      `Desde ${formatMonthYear(formState.data_inicio_debito)}`,
+    );
+  }
+ 
+  // 1. Preenche o FormData
+  // O estado (formState) já usa as TAGS OFICIAIS, portanto iteramos diretamente.
+  // Não precisamos mais do fieldMapping legado.
+  const fieldsToIgnore = new Set([
+    "calculo_arquivo",
+    "outrosFilhos",
+    "documentFiles",
+    "documentNames",
+    "documentosMarcados",
+    "audioBlob",
+    "tipoAcao",
+    "acaoEspecifica",
+    "enviarDocumentosDepois",
+  ]);
+ 
+  const valuesToSubmit = { ...formState };
+ 
+  Object.keys(valuesToSubmit).forEach((key) => {
+    if (fieldsToIgnore.has(key)) return;
+ 
+    const rawValue = valuesToSubmit[key];
+    if (rawValue === undefined || rawValue === null || rawValue === "") {
+      return;
+    }
+ 
+    let normalizedValue = rawValue;
+ 
+    // Tratamentos de formatação para subsets
+    if (digitsOnlyFields?.has(key)) {
+      normalizedValue = stripNonDigits(rawValue);
+    } else if (currencyFields?.has(key)) {
+      normalizedValue = normalizeDecimalForSubmit(rawValue);
+    } else if (dateFields?.has(key) || key.includes("data_nascimento")) {
+      const iso = parseBrDateToIso(rawValue);
+      if (iso) normalizedValue = iso;
+    }
+ 
+    if (normalizedValue !== undefined && normalizedValue !== null && normalizedValue !== "") {
+      // Como o formState já está alinhado com dicionarioTags, passamos o key direto
+      formData.append(key, normalizedValue);
+    }
+  });
+ 
+  // [EIXO 3] Garante que enviarDocumentosDepois é SEMPRE enviado, mesmo que false
+  formData.append("enviar_documentos_depois", String(formState.enviarDocumentosDepois ?? false));
+ 
+  // 2. Correção Crítica: Formatar Tipo de Ação para o Backend
+  // O backend espera "Area - Ação" para saber qual template DOCX usar
+  if (!configAcao) {
+    console.warn(
+      "[FormularioSubmissao] configAcao não encontrada para:",
+      formState.tipoAcao,
+      formState.acaoEspecifica,
+    );
+  }
+  const tituloAcao = configAcao?.titulo || formState.acaoEspecifica;
+  const tipoAcaoFormatado = `${formState.tipoAcao} - ${tituloAcao}`;
+  formData.append("tipoAcao", tipoAcaoFormatado);
+  formData.append("acaoEspecifica", formState.acaoEspecifica); // chave do dicionário para lookup no backend
+ 
+  // Lógica para múltiplos filhos
+  if (formState.assistidoEhIncapaz === "sim") {
+    // Filho 1 (Principal)
+    let infoFilhos = formState.REPRESENTANTE_NOME;
+ 
+    // Filhos Extras
+    if (formState.outrosFilhos.length > 0) {
+      const nomesExtras = formState.outrosFilhos
+        .map((f) => f.nome)
+        .filter((n) => n.trim() !== "")
+        .join(", ");
+      if (nomesExtras) infoFilhos += `, ${nomesExtras}`;
+    }
+ 
+    // Envia o array completo como JSON string para o backend processar
+    formData.append("outros_filhos_detalhes", JSON.stringify(formState.outrosFilhos));
+ 
+    // Envia a string completa com todos os nomes
+    if (infoFilhos) {
+      formData.append("filhos_info", infoFilhos);
+    }
+  }
+ 
+  // 3. Construção de Campos Compostos para a IA (Gemini)
+  // A IA usa 'dados_adicionais_requerente' para criar o resumo, então montamos uma string rica
+  const dadosAdicionaisRequerente = [
+    `RG: ${
+      formState.representante_rg
+        ? `${formState.representante_rg}${
+            formState.emissor_rg_exequente ? ` ${formState.emissor_rg_exequente}` : ""
+          }`
+        : "Não informado"
+    },`,
+    `Nacionalidade: ${formState.representante_nacionalidade || "Não informado"},`,
+    !forcaRepresentacao
+      ? `Estado Civil: ${formState.representante_estado_civil || "Não informado"},`
+      : "",
+    `Data Nascimento: ${
+      formatDateToBr(formState.representante_data_nascimento) || "Não informado"
+    },`,
+  ]
+    .filter(Boolean)
+    .join(" ");
+  formData.append("dados_adicionais_requerente", `${dadosAdicionaisRequerente.trim()} `);
+ 
+  const detalhesRequerido = [];
+  if (formState.requeridoOutrosSelecionados?.includes("requeridoRg") && formState.rg_executado) {
+    detalhesRequerido.push(
+      `RG: ${formState.rg_executado}${
+        formState.emissor_rg_executado ? ` ${formState.emissor_rg_executado}` : ""
+      }`,
+    );
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoDataNascimento") &&
+    formState.executado_data_nascimento
+  ) {
+    detalhesRequerido.push(
+      `Data de nascimento: ${formatDateToBr(formState.executado_data_nascimento)}`,
+    );
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoNomeMae") &&
+    formState.nome_mae_executado
+  ) {
+    detalhesRequerido.push(`Nome da mãe: ${formState.nome_mae_executado}`);
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoNomePai") &&
+    formState.nome_pai_executado
+  ) {
+    detalhesRequerido.push(`Nome do pai: ${formState.nome_pai_executado}`);
+  }
+  if (
+    formState.requeridoOutrosSelecionados?.includes("requeridoOutrosDetalhes") &&
+    formState.dados_adicionais_requerido
+  ) {
+    detalhesRequerido.push(`Observações: ${formState.dados_adicionais_requerido}`);
+  }
+  if (detalhesRequerido.length > 0) {
+    formData.append("dados_adicionais_requerido_array", detalhesRequerido.join(" | "));
+  }
+ 
+  // Arquivos e Arrays
+  formData.append("documentos_informados", JSON.stringify(formState.documentosMarcados));
+  formData.append("documentos_nomes", JSON.stringify(formState.documentNames || {}));
+  if (formState.audioBlob) formData.append("audio", formState.audioBlob, "gravacao.webm");
+  formState.documentFiles.forEach((file) => {
+    if (!file || !file.name) return; // Proteção contra arquivos inválidos
+    const safeName = file.name
+      .normalize("NFD")
+      .replace(/[\u0300-\u036f]/g, "")
+      .replace(/\s+/g, "_");
+    formData.append("documentos", file, safeName);
+  });
+  if (formState.calculo_arquivo) {
+    const calcFile = formState.calculo_arquivo;
+    const safeCalcName = `CALCULO_${calcFile.name
+      .normalize("NFD")
+      .replace(/[\u0300-\u036f]/g, "")
+      .replace(/\s+/g, "_")}`;
+    formData.append("documentos", calcFile, safeCalcName);
+  }
+ 
+  try {
+    const response = await fetch(`${API_BASE}/casos/novo`, {
+      method: "POST",
+      body: formData,
+    });
+    const data = await response.json();
+    if (!response.ok) throw new Error(data.error || "Falha no servidor");
+    setGeneratedCredentials({
+      chaveAcesso: data.chaveAcesso,
+      protocolo: data.protocolo,
+    });
+    localStorage.removeItem("rascunho_caso");
+  } catch (error) {
+    console.error("Erro:", error);
+    toast.error(`Erro: ${error.message}`);
+  } finally {
+    setLoading(false);
+    timers.forEach(clearTimeout);
+    setStatusMessage("");
+  }
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/base.css b/frontend/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/frontend/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/frontend/coverage/lcov-report/block-navigation.js b/frontend/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/frontend/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/frontend/coverage/lcov-report/favicon.png b/frontend/coverage/lcov-report/favicon.png new file mode 100644 index 0000000..c1525b8 Binary files /dev/null and b/frontend/coverage/lcov-report/favicon.png differ diff --git a/frontend/coverage/lcov-report/index.html b/frontend/coverage/lcov-report/index.html new file mode 100644 index 0000000..da2bef5 --- /dev/null +++ b/frontend/coverage/lcov-report/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 65.34% + Statements + 626/958 +
+ + +
+ 67.66% + Branches + 180/266 +
+ + +
+ 72.72% + Functions + 16/22 +
+ + +
+ 65.34% + Lines + 626/958 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
areas/servidor/services +
+
73.34%322/43950.81%62/12250%1/273.34%322/439
utils +
+
58.57%304/51981.94%118/14475%15/2058.57%304/519
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/prettify.css b/frontend/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/frontend/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/frontend/coverage/lcov-report/prettify.js b/frontend/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/frontend/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/frontend/coverage/lcov-report/sort-arrow-sprite.png b/frontend/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/frontend/coverage/lcov-report/sort-arrow-sprite.png differ diff --git a/frontend/coverage/lcov-report/sorter.js b/frontend/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/frontend/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/frontend/coverage/lcov-report/utils/apiBase.js.html b/frontend/coverage/lcov-report/utils/apiBase.js.html new file mode 100644 index 0000000..a86f638 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/apiBase.js.html @@ -0,0 +1,364 @@ + + + + + + Code coverage report for utils/apiBase.js + + + + + + + + + +
+
+

All files / utils apiBase.js

+
+ +
+ 74.19% + Statements + 69/93 +
+ + +
+ 46.66% + Branches + 7/15 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 74.19% + Lines + 69/93 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +941x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +3x +3x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +  +  +  +2x +2x +2x +  +  +2x +2x +2x +2x +3x +3x +3x + 
export function getApiBase() {
+  // 1. A variável de ambiente é a fonte da verdade.
+  let envUrl =
+    (typeof import.meta !== "undefined" && import.meta?.env?.VITE_API_URL) ||
+    "";
+ 
+  if (envUrl) {
+    envUrl = envUrl.replace(/\/$/, "");
+    if (!envUrl.endsWith("/api")) {
+      envUrl = `${envUrl}/api`;
+    }
+    return envUrl;
+  }
+
+  // 2. Detecção de Desenvolvimento robusta
+  const isLocalhost = 
+    typeof window !== "undefined" && 
+    (window.location.hostname === "localhost" || 
+     window.location.hostname === "127.0.0.1" ||
+     window.location.hostname.startsWith("192.168."));
+  
+  const isDevMode =
+    (typeof import.meta !== "undefined" && import.meta?.env?.DEV) || false;
+ 
+  if (isLocalhost || isDevMode) {
+    console.warn(
+      `Modo de desenvolvimento detectado (Host: ${typeof window !== "undefined" ? window.location.hostname : 'N/A'}). Usando API: http://localhost:8000/api`,
+    );
+    return "http://localhost:8000/api";
+  }
+
+  // 3. Produção sem variável de ambiente
+  console.error(
+    "ERRO CRÍTICO: VITE_API_URL não definida em produção. As chamadas irão falhar.",
+  );
+
+  return "";
+}
+ 
+export const API_BASE = getApiBase();
+ 
+export const authFetch = async (endpoint, options = {}) => {
+  // 1. Pega o token atual
+  const token = localStorage.getItem("defensorToken");
+ 
+  // 2. Prepara os headers padrão
+  const headers = {
+    "Content-Type": "application/json",
+    ...options.headers,
+  };
+ 
+  // 3. Injeta o Token se existir
+  if (token) {
+    headers["Authorization"] = `Bearer ${token}`;
+  }
+ 
+  // 4. Faz a requisição
+  // Nota: endpoint não precisa incluir API_BASE se passar só o caminho (ex: '/casos')
+  const url = endpoint.startsWith("http") ? endpoint : `${API_BASE}${endpoint}`;
+ 
+  const response = await fetch(url, {
+    ...options,
+    headers,
+  });
+ 
+  // 5. DETECTA SESSÃO EXPIRADA
+  if (response.status === 401) {
+    // 1. Limpa imediatamente o storage para evitar loops
+    localStorage.removeItem("defensorToken");
+    localStorage.removeItem("defensorUser");
+ 
+    // 2. Dispara o evento que o AuthContext vai escutar para redirecionar
+    try {
+      if (typeof window !== "undefined") {
+        let event;
+        try {
+          event = new CustomEvent("auth:session-expired");
+        } catch {
+          event = document.createEvent("Event");
+          event.initEvent("auth:session-expired", true, true);
+        }
+        window.dispatchEvent(event);
+      }
+    } catch (e) {
+      console.warn("Falha crítica ao disparar evento de expiração:", e);
+    }
+ 
+    // 3. Lança erro padronizado
+    throw new Error("Sessão expirada");
+  }
+ 
+  return response;
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/utils/caseUtils.js.html b/frontend/coverage/lcov-report/utils/caseUtils.js.html new file mode 100644 index 0000000..e052029 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/caseUtils.js.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for utils/caseUtils.js + + + + + + + + + +
+
+

All files / utils caseUtils.js

+
+ +
+ 100% + Statements + 32/32 +
+ + +
+ 100% + Branches + 9/9 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 32/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +331x +15x +15x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +15x +9x +9x +3x +15x +2x +2x +1x +1x +1x +1x + 
export const formatTipoAcaoLabel = (tipoAcao = "") => {
+  const raw = String(tipoAcao || "").trim();
+  if (!raw) return "Não informado";
+ 
+  const knownLabels = {
+    exec_cumulado: "Execução de Alimentos",
+    exec_penhora: "Execução de Alimentos (Penhora)",
+    exec_prisao: "Execução de Alimentos (Prisão)",
+    execucao_alimentos: "Execução de Alimentos",
+    def_cumulado: "Cumprimento de Sentença Cumulada",
+    def_penhora: "Cumprimento de Sentença (Penhora)",
+    def_prisao: "Cumprimento de Sentença (Prisão)",
+    termo_declaracao: "Termo de Declaração",
+  };
+ 
+  const key = raw
+    .toLowerCase()
+    .replace(/\s+/g, "_")
+    .normalize("NFD")
+    .replace(/[\u0300-\u036f]/g, "");
+ 
+  if (knownLabels[key]) {
+    return knownLabels[key];
+  }
+ 
+  if (raw.includes(" - ")) {
+    return raw.split(" - ")[1].trim();
+  }
+ 
+  const withSpaces = raw.replace(/_/g, " ");
+  return withSpaces.replace(/\b\w/g, (char) => char.toUpperCase());
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/utils/fetcher.js.html b/frontend/coverage/lcov-report/utils/fetcher.js.html new file mode 100644 index 0000000..f218152 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/fetcher.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for utils/fetcher.js + + + + + + + + + +
+
+

All files / utils fetcher.js

+
+ +
+ 0% + Statements + 0/21 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { authFetch } from "./apiBase";
+
+// src/utils/fetcher.js
+export const fetcher = async (url) => {
+  const response = await authFetch(url);
+
+  if (!response.ok) {
+    let errorMsg = "Erro ao buscar dados da API";
+    try {
+      const errData = await response.json();
+      if (errData.error || errData.message) {
+        errorMsg = errData.error || errData.message;
+      }
+    } catch {
+      // Ignora erro de parse se não for JSON
+    }
+    throw new Error(errorMsg);
+  }
+
+  return response.json();
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/utils/formOptions.js.html b/frontend/coverage/lcov-report/utils/formOptions.js.html new file mode 100644 index 0000000..23d74c6 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/formOptions.js.html @@ -0,0 +1,508 @@ + + + + + + Code coverage report for utils/formOptions.js + + + + + + + + + +
+
+

All files / utils formOptions.js

+
+ +
+ 0% + Statements + 0/141 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/141 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// src/utils/formOptions.js
+
+export const nacionalidadeOptions = [
+  { value: "", label: "Nacionalidade" },
+  { value: "brasileiro(a)", label: "Brasileiro(a)" },
+  { value: "estrangeiro(a)", label: "Estrangeiro(a)" },
+];
+
+export const estadoCivilOptions = [
+  { value: "", label: "Estado Civil" },
+  { value: "solteiro(a)", label: "Solteiro(a)" },
+  { value: "casado(a)", label: "Casado(a)" },
+  { value: "divorciado(a)", label: "Divorciado(a)" },
+  { value: "viúvo(a)", label: "Viúvo(a)" },
+  { value: "união estável", label: "União Estável" },
+];
+
+export const orgaoEmissorOptions = [
+  { value: "", label: "Órgão emissor" },
+  { value: "SSP/AC", label: "SSP/AC" },
+  { value: "SSP/AL", label: "SSP/AL" },
+  { value: "SSP/AP", label: "SSP/AP" },
+  { value: "SSP/AM", label: "SSP/AM" },
+  { value: "SSP/BA", label: "SSP/BA" },
+  { value: "SSP/CE", label: "SSP/CE" },
+  { value: "SSP/DF", label: "SSP/DF" },
+  { value: "SSP/ES", label: "SSP/ES" },
+  { value: "SSP/GO", label: "SSP/GO" },
+  { value: "SSP/MA", label: "SSP/MA" },
+  { value: "SSP/MT", label: "SSP/MT" },
+  { value: "SSP/MS", label: "SSP/MS" },
+  { value: "SSP/MG", label: "SSP/MG" },
+  { value: "SSP/PA", label: "SSP/PA" },
+  { value: "SSP/PB", label: "SSP/PB" },
+  { value: "SSP/PR", label: "SSP/PR" },
+  { value: "SSP/PE", label: "SSP/PE" },
+  { value: "SSP/PI", label: "SSP/PI" },
+  { value: "SSP/RJ", label: "SSP/RJ" },
+  { value: "SSP/RN", label: "SSP/RN" },
+  { value: "SSP/RS", label: "SSP/RS" },
+  { value: "SSP/RO", label: "SSP/RO" },
+  { value: "SSP/RR", label: "SSP/RR" },
+  { value: "SSP/SC", label: "SSP/SC" },
+  { value: "SSP/SP", label: "SSP/SP" },
+  { value: "SSP/SE", label: "SSP/SE" },
+  { value: "SSP/TO", label: "SSP/TO" },
+  { value: "DETRAN", label: "Detran" },
+  { value: "OUTRO", label: "Outro" },
+];
+
+export const cidadesBahia = [
+  "Alagoinhas",
+  "Amargosa",
+  "Barreiras",
+  "Bom Jesus da Lapa",
+  "Brumado",
+  "Cachoeira",
+  "Camacan",
+  "Camaçari",
+  "Campo Formoso",
+  "Canavieiras",
+  "Candeias",
+  "Catu",
+  "Conceição do Coité",
+  "Cruz das Almas",
+  "Esplanada",
+  "Euclides da Cunha",
+  "Eunápolis",
+  "Feira de Santana",
+  "Guanambi",
+  "Ilhéus",
+  "Ipiaú",
+  "Ipirá",
+  "Irará",
+  "Irecê",
+  "Itaberaba",
+  "Itabuna",
+  "Itaparica",
+  "Itapetinga",
+  "Jacobina",
+  "Jequié",
+  "Juazeiro",
+  "Lauro de Freitas",
+  "Luís Eduardo Magalhães",
+  "Macaúbas",
+  "Nazaré",
+  "Paripiranga",
+  "Paulo Afonso",
+  "Poções",
+  "Porto Seguro",
+  "Riachão do Jacuípe",
+  "Ribeira do Pombal",
+  "Salvador",
+  "Santa Maria da Vitória",
+  "Santo Amaro",
+  "Santo Antônio de Jesus",
+  "Santo Estêvão",
+  "Seabra",
+  "Senhor do Bonfim",
+  "Serrinha",
+  "Simões Filho",
+  "Teixeira de Freitas",
+  "Valença",
+  "Vitória da Conquista"
+];
+
+export const outrosDadosRequeridoConfig = [
+  { key: "requeridoRg", label: "RG e órgão emissor", renderType: "rg" },
+  {
+    key: "requeridoDataNascimento",
+    label: "Data de nascimento",
+    renderType: "date",
+    field: "requeridoDataNascimento",
+  },
+  {
+    key: "requeridoOutrosDetalhes",
+    label: "Outros detalhes relevantes",
+    renderType: "textarea",
+    field: "requeridoOutrosDetalhes",
+    placeholder: "Ex: Nome do advogado, redes sociais, etc.",
+  },
+];
+
+export const outrosDadosRequeridoFieldMap = {
+  requeridoRg: ["requeridoRgNumero", "requeridoRgOrgao"],
+  requeridoDataNascimento: ["requeridoDataNascimento"],
+  requeridoNomeMae: ["requeridoNomeMae"],
+  requeridoNomePai: ["requeridoNomePai"],
+  requeridoOutrosDetalhes: ["requeridoOutrosDetalhes"],
+  requeridoFiliacao: ["requeridoNomeMae", "requeridoNomePai"],
+};
+
+export const acoesFallbackFamilia = [
+  "Fixação de Pensão Alimentícia",
+  "Divórcio",
+  "Reconhecimento e Dissolução de União Estável",
+  "Guarda de Filhos",
+  "Alvará",
+  "Execução de Alimentos Rito Penhora/Prisão",
+  "Revisão de Alimentos",
+];
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/utils/formatters.js.html b/frontend/coverage/lcov-report/utils/formatters.js.html new file mode 100644 index 0000000..7bdf768 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/formatters.js.html @@ -0,0 +1,781 @@ + + + + + + Code coverage report for utils/formatters.js + + + + + + + + + +
+
+

All files / utils formatters.js

+
+ +
+ 87.5% + Statements + 203/232 +
+ + +
+ 86.44% + Branches + 102/118 +
+ + +
+ 80% + Functions + 12/15 +
+ + +
+ 87.5% + Lines + 203/232 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +2331x +1x +1x +6x +6x +6x +6x +1x +1x +3x +3x +1x +1x +4x +4x +4x +1x +1x +1x +1x +  +  +  +  +1x +1x +4x +4x +4x +4x +4x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +1x +1x +4x +3x +4x +3x +3x +3x +3x +4x +4x +4x +2x +2x +2x +2x +2x +2x +2x +2x +1x +1x +3x +3x +1x +3x +1x +1x +1x +1x +4x +2x +4x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +4x +3x +3x +4x +1x +1x +2x +2x +1x +1x +8x +8x +4x +4x +4x +4x +8x +36x +36x +4x +4x +8x +8x +4x +4x +8x +40x +40x +4x +4x +8x +8x +3x +3x +3x +1x +1x +28x +25x +25x +28x +24x +24x +24x +24x +24x +24x +24x +24x +28x +28x +20x +20x +28x +28x +28x +28x +28x +28x +17x +17x +17x +1x +1x +11x +11x +11x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +11x +6x +6x +9x +9x +9x +9x +9x +9x +9x +9x +  +9x +9x +9x +9x +9x +6x +6x +6x +6x +11x +9x +9x +9x +6x +6x +6x +9x +8x +9x +8x +9x +9x +9x +6x +6x +6x +6x +11x +11x +11x +11x +11x +1x +1x +1x +1x +6x +6x + 
export const stripNonDigits = (value = "") => value.replace(/\D/g, "");
+ 
+export const formatCpf = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 11);
+  if (digits.length <= 3) return digits;
+  if (digits.length <= 6) return `${digits.slice(0, 3)}.${digits.slice(3)}`;
+  if (digits.length <= 9) {
+    return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
+  }
+  return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
+};
+ 
+export const formatDateMask = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 8);
+  if (digits.length <= 2) return digits;
+  if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+  return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
+};
+ 
+export const formatMonthYearMask = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 6);
+  if (digits.length <= 2) return digits;
+  return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+};
+ 
+export const formatPhone = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 11);
+  if (!digits) return "";
+  if (digits.length <= 2) return `(${digits}`;
+  if (digits.length <= 6) return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
+  if (digits.length <= 10) {
+    return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
+  }
+  return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
+};
+ 
+export const formatRgNumber = (value = "") => {
+  const digits = value.replace(/[^0-9xX]/g, "").toUpperCase().slice(0, 11);
+  if (digits.length <= 2) return digits;
+  if (digits.length <= 5) return `${digits.slice(0, 2)}.${digits.slice(2)}`;
+  if (digits.length <= 8) {
+    return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5)}`;
+  }
+  if (digits.length <= 10) {
+    return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}-${digits.slice(8)}`;
+  }
+  return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}.${digits.slice(8, 10)}-${digits.slice(10)}`;
+};
+ 
+export const formatCurrencyMask = (value = "") => {
+  if (value === null || value === undefined || value === "") return "";
+  let digits = String(value).replace(/\D/g, "");
+  if (digits === "") return "";
+ 
+  // Remove leading zeros
+  digits = digits.replace(/^0+/, "");
+ 
+  if (digits.length === 0) return "0,00";
+  if (digits.length === 1) return `0,0${digits}`;
+  if (digits.length === 2) return `0,${digits}`;
+ 
+  const cents = digits.slice(-2);
+  let integer = digits.slice(0, -2);
+ 
+  integer = integer.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1.");
+ 
+  return `${integer},${cents}`;
+};
+ 
+export const formatDateToBr = (isoDate = "") => {
+  if (!isoDate) return "";
+  if (isoDate.includes("/")) return isoDate; // Já está no formato BR
+  const [year, month, day] = isoDate.split("-");
+  if (!year || !month || !day) return isoDate;
+  return `${day.padStart(2, "0")}/${month.padStart(2, "0")}/${year}`;
+};
+ 
+export const parseBrDateToIso = (brDate = "") => {
+  if (!brDate || !brDate.includes("/")) return brDate;
+  const [day, month, year] = brDate.split("/");
+  if (!day || !month || !year || year.length < 4) return "";
+  return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
+};
+ 
+export const sanitizeDecimalInput = (
+  value = "",
+  { decimalPlaces = 2, maxIntegerDigits = 9 } = {},
+) => {
+  if (value === null || value === undefined || value === "") return "";
+  const allowed = String(value).replace(/[^0-9,]/g, "");
+  const [intPartRaw, ...decimalParts] = allowed.split(",");
+  const integerPart = (intPartRaw || "").slice(0, maxIntegerDigits);
+  if (decimalParts.length === 0) {
+    return integerPart;
+  }
+  const decimalPart = decimalParts.join("").slice(0, decimalPlaces);
+  return `${integerPart},${decimalPart}`;
+};
+ 
+export const normalizeDecimalForSubmit = (value = "", decimals = 2) => {
+  if (value === null || value === undefined || value === "") return "";
+  const normalized = String(value).replace(/\./g, "").replace(",", ".");
+  const number = Number(normalized);
+  if (Number.isNaN(number)) {
+    return "";
+  }
+  return number.toFixed(decimals);
+};
+ 
+export const validateCpfAlgorithm = (cpf) => {
+  const cleanCpf = String(cpf).replace(/[^\d]+/g, "");
+  if (cleanCpf.length !== 11 || /^(\d)\1+$/.test(cleanCpf)) return false;
+ 
+  let soma = 0;
+  let resto;
+ 
+  for (let i = 1; i <= 9; i++) {
+    soma = soma + parseInt(cleanCpf.substring(i - 1, i)) * (11 - i);
+  }
+  resto = (soma * 10) % 11;
+ 
+  if (resto === 10 || resto === 11) resto = 0;
+  if (resto !== parseInt(cleanCpf.substring(9, 10))) return false;
+ 
+  soma = 0;
+  for (let i = 1; i <= 10; i++) {
+    soma = soma + parseInt(cleanCpf.substring(i - 1, i)) * (12 - i);
+  }
+  resto = (soma * 10) % 11;
+ 
+  if (resto === 10 || resto === 11) resto = 0;
+  if (resto !== parseInt(cleanCpf.substring(10, 11))) return false;
+ 
+  return true;
+};
+ 
+export const validateBrDate = (brDate = "") => {
+  if (!brDate || typeof brDate !== "string") return false;
+  
+  // Verifica formato exato DD/MM/AAAA
+  if (!/^\d{2}\/\d{2}\/\d{4}$/.test(brDate)) return false;
+  
+  const [dayStr, monthStr, yearStr] = brDate.split("/");
+  const day = parseInt(dayStr, 10);
+  const month = parseInt(monthStr, 10);
+  const year = parseInt(yearStr, 10);
+  
+  // Limites básicos (não aceita antes de 1900 ou depois do ano atual + 1 por segurança na conversão de timezones)
+  const currentYear = new Date().getFullYear();
+  if (year < 1900 || year > currentYear) return false;
+  if (month < 1 || month > 12) return false;
+  
+  // Dias por mês (considerando bissexto correto)
+  const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
+  const daysInMonth = [
+    31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+  ];
+  
+  if (day < 1 || day > daysInMonth[month - 1]) return false;
+  
+  return true;
+};
+ 
+export const numeroParaExtenso = (valor) => {
+  if (valor === null || valor === undefined || valor === "") return "";
+  const v = typeof valor === "string" ? parseFloat(valor.replace(/\./g, "").replace(",", ".")) : valor;
+  if (isNaN(v)) return "";
+  
+  const unidades = ["zero", "um", "dois", "três", "quatro", "cinco", "seis", "sete", "oito", "nove"];
+  const especiais = ["dez", "onze", "doze", "treze", "quatorze", "quinze", "dezesseis", "dezessete", "dezoito", "dezenove"];
+  const dezenas = ["", "dez", "vinte", "trinta", "quarenta", "cinquenta", "sessenta", "setenta", "oitenta", "noventa"];
+  const centenas = ["", "cento", "duzentos", "trezentos", "quatrocentos", "quinhentos", "seiscentos", "setecentos", "oitocentos", "novecentos"];
+  const qualificadores = [
+    { singular: "", plural: "" },
+    { singular: "mil", plural: "mil" },
+    { singular: "milhão", plural: "milhões" },
+    { singular: "bilhão", plural: "bilhões" },
+  ];
+ 
+  const inteiro = Math.floor(Math.abs(v));
+  const centavos = Math.round((Math.abs(v) - inteiro) * 100);
+ 
+  if (inteiro === 0 && centavos === 0) return "zero real";
+ 
+  const numeroParaTextoAte999 = (numero) => {
+    if (numero === 0) return "";
+    if (numero === 100) return "cem";
+    const c = Math.floor(numero / 100);
+    const d = Math.floor((numero % 100) / 10);
+    const u = numero % 10;
+    const partes = [];
+    if (c) partes.push(centenas[c]);
+    if (d === 1) {
+      partes.push(especiais[u]);
+    } else {
+      if (d) partes.push(dezenas[d]);
+      if (u) partes.push(unidades[u]);
+    }
+    return partes.join(" e ");
+  };
+ 
+  const grupos = [];
+  let numeroRestante = inteiro;
+  while (numeroRestante > 0) {
+    grupos.push(numeroRestante % 1000);
+    numeroRestante = Math.floor(numeroRestante / 1000);
+  }
+ 
+  const partesInteiras = grupos
+    .map((grupo, index) => {
+      if (!grupo) return null;
+      const texto = numeroParaTextoAte999(grupo);
+      if (!texto) return null;
+      const qualificador = qualificadores[index];
+      const ehSingular = grupo === 1 && index > 0;
+      const sufixo = index === 0 ? "" : ` ${ehSingular ? qualificador.singular : qualificador.plural}`;
+      return `${texto}${sufixo}`;
+    })
+    .filter(Boolean)
+    .reverse();
+ 
+  const inteiroExtenso = partesInteiras.join(" e ") || "zero";
+  const rotuloInteiro = inteiro === 1 ? "real" : "reais";
+ 
+  let resultado = `${inteiroExtenso} ${rotuloInteiro}`;
+  if (centavos > 0) {
+    const centavosExtenso = numeroParaTextoAte999(centavos) || "zero";
+    const rotuloCentavos = centavos === 1 ? "centavo" : "centavos";
+    resultado += ` e ${centavosExtenso} ${rotuloCentavos}`;
+  }
+  return resultado;
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov-report/utils/index.html b/frontend/coverage/lcov-report/utils/index.html new file mode 100644 index 0000000..3fb8ef6 --- /dev/null +++ b/frontend/coverage/lcov-report/utils/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for utils + + + + + + + + + +
+
+

All files utils

+
+ +
+ 58.57% + Statements + 304/519 +
+ + +
+ 81.94% + Branches + 118/144 +
+ + +
+ 75% + Functions + 15/20 +
+ + +
+ 58.57% + Lines + 304/519 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
apiBase.js +
+
74.19%69/9346.66%7/15100%2/274.19%69/93
caseUtils.js +
+
100%32/32100%9/9100%1/1100%32/32
fetcher.js +
+
0%0/210%0/10%0/10%0/21
formOptions.js +
+
0%0/1410%0/10%0/10%0/141
formatters.js +
+
87.5%203/23286.44%102/11880%12/1587.5%203/232
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/lcov.info b/frontend/coverage/lcov.info new file mode 100644 index 0000000..d7a0925 --- /dev/null +++ b/frontend/coverage/lcov.info @@ -0,0 +1,1322 @@ +TN: +SF:src\areas\servidor\services\submissionService.js +FN:4,processSubmission +FN:222,formatMonthYear +FNF:2 +FNH:1 +FNDA:15,processSubmission +FNDA:0,formatMonthYear +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,15 +DA:6,15 +DA:7,15 +DA:8,15 +DA:9,15 +DA:10,15 +DA:11,15 +DA:12,15 +DA:13,15 +DA:14,15 +DA:15,15 +DA:16,15 +DA:17,15 +DA:18,15 +DA:19,15 +DA:20,15 +DA:21,15 +DA:22,15 +DA:23,15 +DA:24,15 +DA:25,15 +DA:26,15 +DA:27,15 +DA:28,15 +DA:29,0 +DA:30,0 +DA:31,15 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,15 +DA:36,15 +DA:37,15 +DA:38,15 +DA:39,15 +DA:40,12 +DA:41,12 +DA:42,12 +DA:43,12 +DA:44,15 +DA:45,3 +DA:46,3 +DA:47,3 +DA:48,15 +DA:49,15 +DA:50,2 +DA:51,2 +DA:52,15 +DA:53,15 +DA:54,15 +DA:55,0 +DA:56,15 +DA:57,1 +DA:58,1 +DA:59,15 +DA:60,15 +DA:61,1 +DA:62,1 +DA:63,15 +DA:64,15 +DA:65,15 +DA:66,15 +DA:67,15 +DA:68,15 +DA:69,15 +DA:70,15 +DA:71,15 +DA:72,0 +DA:73,0 +DA:74,15 +DA:75,15 +DA:76,15 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,15 +DA:85,15 +DA:86,15 +DA:87,15 +DA:88,15 +DA:89,15 +DA:90,15 +DA:91,15 +DA:92,0 +DA:93,15 +DA:94,2 +DA:95,15 +DA:96,0 +DA:97,13 +DA:98,0 +DA:99,0 +DA:100,15 +DA:101,15 +DA:102,15 +DA:103,1 +DA:104,1 +DA:105,15 +DA:106,15 +DA:107,15 +DA:108,15 +DA:109,15 +DA:110,12 +DA:111,15 +DA:112,0 +DA:113,0 +DA:114,15 +DA:115,15 +DA:116,15 +DA:117,0 +DA:118,0 +DA:119,15 +DA:120,15 +DA:121,15 +DA:122,3 +DA:123,3 +DA:124,1 +DA:125,3 +DA:126,1 +DA:127,1 +DA:128,3 +DA:129,3 +DA:130,3 +DA:131,0 +DA:132,0 +DA:133,3 +DA:134,1 +DA:135,1 +DA:136,3 +DA:137,2 +DA:138,2 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,2 +DA:143,3 +DA:144,3 +DA:145,15 +DA:146,15 +DA:147,15 +DA:148,15 +DA:149,0 +DA:150,0 +DA:151,0 +DA:152,15 +DA:153,15 +DA:154,15 +DA:155,1 +DA:156,1 +DA:157,15 +DA:158,15 +DA:159,15 +DA:160,15 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,15 +DA:169,15 +DA:170,15 +DA:171,15 +DA:172,15 +DA:173,15 +DA:174,15 +DA:175,3 +DA:176,3 +DA:177,15 +DA:178,15 +DA:179,4 +DA:180,4 +DA:181,4 +DA:182,4 +DA:183,4 +DA:184,4 +DA:185,15 +DA:186,15 +DA:187,15 +DA:188,13 +DA:189,13 +DA:190,13 +DA:191,13 +DA:192,2 +DA:193,2 +DA:194,2 +DA:195,2 +DA:196,2 +DA:197,2 +DA:198,2 +DA:199,2 +DA:200,2 +DA:201,2 +DA:202,2 +DA:203,2 +DA:204,2 +DA:205,2 +DA:206,2 +DA:207,2 +DA:208,2 +DA:209,15 +DA:210,0 +DA:211,15 +DA:212,0 +DA:213,2 +DA:214,0 +DA:215,0 +DA:216,2 +DA:217,15 +DA:218,0 +DA:219,0 +DA:220,2 +DA:221,2 +DA:222,2 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,2 +DA:243,2 +DA:244,15 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,15 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,2 +DA:255,2 +DA:256,2 +DA:257,2 +DA:258,2 +DA:259,2 +DA:260,2 +DA:261,2 +DA:262,2 +DA:263,2 +DA:264,2 +DA:265,2 +DA:266,2 +DA:267,2 +DA:268,2 +DA:269,2 +DA:270,2 +DA:271,2 +DA:272,2 +DA:273,48 +DA:274,30 +DA:275,30 +DA:276,48 +DA:277,0 +DA:278,0 +DA:279,30 +DA:280,30 +DA:281,30 +DA:282,30 +DA:283,48 +DA:284,2 +DA:285,48 +DA:286,0 +DA:287,28 +DA:288,2 +DA:289,2 +DA:290,2 +DA:291,30 +DA:292,48 +DA:293,30 +DA:294,30 +DA:295,30 +DA:296,2 +DA:297,2 +DA:298,2 +DA:299,15 +DA:300,15 +DA:301,15 +DA:302,15 +DA:303,15 +DA:304,0 +DA:305,0 +DA:306,0 +DA:307,0 +DA:308,0 +DA:309,0 +DA:310,15 +DA:311,15 +DA:312,15 +DA:313,15 +DA:314,15 +DA:315,15 +DA:316,15 +DA:317,2 +DA:318,2 +DA:319,2 +DA:320,2 +DA:321,2 +DA:322,0 +DA:323,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:328,2 +DA:329,2 +DA:330,2 +DA:331,2 +DA:332,2 +DA:333,2 +DA:334,2 +DA:335,2 +DA:336,2 +DA:337,2 +DA:338,2 +DA:339,2 +DA:340,2 +DA:341,2 +DA:342,2 +DA:343,15 +DA:344,0 +DA:345,0 +DA:346,15 +DA:347,15 +DA:348,15 +DA:349,15 +DA:350,15 +DA:351,15 +DA:352,15 +DA:353,15 +DA:354,15 +DA:355,15 +DA:356,15 +DA:357,15 +DA:358,15 +DA:359,15 +DA:360,15 +DA:361,15 +DA:362,0 +DA:363,0 +DA:364,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,2 +DA:369,15 +DA:370,0 +DA:371,15 +DA:372,0 +DA:373,0 +DA:374,0 +DA:375,0 +DA:376,2 +DA:377,15 +DA:378,0 +DA:379,15 +DA:380,0 +DA:381,0 +DA:382,2 +DA:383,15 +DA:384,0 +DA:385,15 +DA:386,0 +DA:387,0 +DA:388,2 +DA:389,15 +DA:390,0 +DA:391,15 +DA:392,0 +DA:393,0 +DA:394,15 +DA:395,0 +DA:396,0 +DA:397,2 +DA:398,2 +DA:399,2 +DA:400,15 +DA:401,15 +DA:402,2 +DA:403,14 +DA:404,14 +DA:405,14 +DA:406,14 +DA:407,14 +DA:408,14 +DA:409,2 +DA:410,15 +DA:411,0 +DA:412,0 +DA:413,0 +DA:414,0 +DA:415,0 +DA:416,0 +DA:417,0 +DA:418,2 +DA:419,2 +DA:420,2 +DA:421,2 +DA:422,2 +DA:423,2 +DA:424,1 +DA:425,15 +DA:426,1 +DA:427,1 +DA:428,1 +DA:429,1 +DA:430,1 +DA:431,1 +DA:432,1 +DA:433,1 +DA:434,15 +DA:435,2 +DA:436,2 +DA:437,2 +DA:438,2 +DA:439,15 +LF:439 +LH:322 +BRDA:4,0,0,15 +BRDA:23,1,0,0 +BRDA:24,2,0,0 +BRDA:25,3,0,0 +BRDA:28,4,0,0 +BRDA:31,5,0,0 +BRDA:31,6,0,0 +BRDA:39,7,0,12 +BRDA:40,8,0,0 +BRDA:41,9,0,0 +BRDA:43,10,0,0 +BRDA:44,11,0,3 +BRDA:46,12,0,1 +BRDA:49,13,0,2 +BRDA:54,14,0,0 +BRDA:56,15,0,1 +BRDA:60,16,0,1 +BRDA:71,17,0,0 +BRDA:76,18,0,0 +BRDA:87,19,0,12 +BRDA:87,20,0,3 +BRDA:91,21,0,0 +BRDA:93,22,0,2 +BRDA:95,23,0,13 +BRDA:95,24,0,0 +BRDA:97,25,0,0 +BRDA:102,26,0,13 +BRDA:102,27,0,1 +BRDA:108,28,0,12 +BRDA:109,29,0,12 +BRDA:111,30,0,0 +BRDA:116,31,0,0 +BRDA:116,32,0,0 +BRDA:121,33,0,3 +BRDA:148,34,0,0 +BRDA:153,35,0,1 +BRDA:154,36,0,1 +BRDA:160,37,0,0 +BRDA:173,38,0,3 +BRDA:173,39,0,12 +BRDA:174,40,0,12 +BRDA:174,41,0,3 +BRDA:178,42,0,4 +BRDA:181,43,0,0 +BRDA:187,44,0,13 +BRDA:191,45,0,2 +BRDA:209,46,0,0 +BRDA:211,47,0,2 +BRDA:211,48,0,0 +BRDA:213,49,0,0 +BRDA:217,50,0,0 +BRDA:219,51,0,2 +BRDA:244,52,0,0 +BRDA:244,53,0,0 +BRDA:248,54,0,2 +BRDA:248,55,0,0 +BRDA:299,56,0,0 +BRDA:303,57,0,0 +BRDA:309,58,0,2 +BRDA:310,59,0,0 +BRDA:316,60,0,2 +BRDA:321,61,0,0 +BRDA:343,62,0,0 +BRDA:346,63,0,2 +BRDA:348,64,0,2 +BRDA:350,65,0,2 +BRDA:351,66,0,0 +BRDA:353,67,0,0 +BRDA:361,68,0,0 +BRDA:361,69,0,0 +BRDA:361,70,0,0 +BRDA:367,71,0,2 +BRDA:369,72,0,0 +BRDA:369,73,0,0 +BRDA:371,74,0,0 +BRDA:375,75,0,2 +BRDA:377,76,0,0 +BRDA:377,77,0,0 +BRDA:379,78,0,0 +BRDA:381,79,0,2 +BRDA:383,80,0,0 +BRDA:383,81,0,0 +BRDA:385,82,0,0 +BRDA:387,83,0,2 +BRDA:389,84,0,0 +BRDA:389,85,0,0 +BRDA:391,86,0,0 +BRDA:393,87,0,2 +BRDA:394,88,0,0 +BRDA:396,89,0,2 +BRDA:400,90,0,0 +BRDA:401,91,0,0 +BRDA:402,92,0,2 +BRDA:410,93,0,0 +BRDA:417,94,0,2 +BRDA:424,95,0,1 +BRDA:425,96,0,0 +BRDA:426,97,0,1 +BRDA:434,98,0,2 +BRDA:122,99,0,3 +BRDA:123,100,0,1 +BRDA:125,101,0,2 +BRDA:125,102,0,1 +BRDA:130,103,0,0 +BRDA:133,104,0,1 +BRDA:136,105,0,2 +BRDA:138,106,0,0 +BRDA:272,107,0,48 +BRDA:273,108,0,18 +BRDA:274,109,0,30 +BRDA:276,110,0,30 +BRDA:276,111,0,0 +BRDA:278,112,0,30 +BRDA:283,113,0,2 +BRDA:285,114,0,28 +BRDA:285,115,0,0 +BRDA:287,116,0,2 +BRDA:290,117,0,30 +BRDA:292,118,0,30 +BRDA:292,119,0,30 +BRDA:402,120,0,14 +BRDA:403,121,0,0 +BRF:122 +BRH:62 +end_of_record +TN: +SF:src\utils\apiBase.js +FN:1,getApiBase +FN:42,authFetch +FNF:2 +FNH:2 +FNDA:1,getApiBase +FNDA:5,authFetch +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,1 +DA:21,1 +DA:22,1 +DA:23,1 +DA:24,1 +DA:25,1 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,1 +DA:40,1 +DA:41,1 +DA:42,1 +DA:43,5 +DA:44,5 +DA:45,5 +DA:46,5 +DA:47,5 +DA:48,5 +DA:49,5 +DA:50,5 +DA:51,5 +DA:52,5 +DA:53,5 +DA:54,3 +DA:55,3 +DA:56,5 +DA:57,5 +DA:58,5 +DA:59,5 +DA:60,5 +DA:61,5 +DA:62,5 +DA:63,5 +DA:64,5 +DA:65,5 +DA:66,5 +DA:67,5 +DA:68,2 +DA:69,2 +DA:70,2 +DA:71,2 +DA:72,2 +DA:73,2 +DA:74,2 +DA:75,2 +DA:76,2 +DA:77,2 +DA:78,2 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,2 +DA:83,2 +DA:84,2 +DA:85,0 +DA:86,0 +DA:87,2 +DA:88,2 +DA:89,2 +DA:90,2 +DA:91,3 +DA:92,3 +DA:93,3 +LF:93 +LH:69 +BRDA:1,0,0,1 +BRDA:4,1,0,0 +BRDA:13,2,0,0 +BRDA:23,3,0,0 +BRDA:23,4,0,0 +BRDA:25,5,0,0 +BRDA:25,6,0,0 +BRDA:42,7,0,5 +BRDA:53,8,0,3 +BRDA:59,9,0,1 +BRDA:59,10,0,4 +BRDA:67,11,0,2 +BRDA:78,12,0,0 +BRDA:84,13,0,0 +BRDA:90,14,0,3 +BRF:15 +BRH:7 +end_of_record +TN: +SF:src\utils\caseUtils.js +FN:1,formatTipoAcaoLabel +FNF:1 +FNH:1 +FNDA:15,formatTipoAcaoLabel +DA:1,1 +DA:2,15 +DA:3,15 +DA:4,12 +DA:5,12 +DA:6,12 +DA:7,12 +DA:8,12 +DA:9,12 +DA:10,12 +DA:11,12 +DA:12,12 +DA:13,12 +DA:14,12 +DA:15,12 +DA:16,12 +DA:17,12 +DA:18,12 +DA:19,12 +DA:20,12 +DA:21,12 +DA:22,15 +DA:23,9 +DA:24,9 +DA:25,3 +DA:26,15 +DA:27,2 +DA:28,2 +DA:29,1 +DA:30,1 +DA:31,1 +DA:32,1 +LF:32 +LH:32 +BRDA:1,0,0,15 +BRDA:2,1,0,3 +BRDA:3,2,0,3 +BRDA:4,3,0,12 +BRDA:22,4,0,9 +BRDA:24,5,0,3 +BRDA:26,6,0,2 +BRDA:28,7,0,1 +BRDA:31,8,0,2 +BRF:9 +BRH:9 +end_of_record +TN: +SF:src\utils\fetcher.js +FN:1,(empty-report) +FNF:1 +FNH:0 +FNDA:0,(empty-report) +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +LF:21 +LH:0 +BRDA:1,0,0,0 +BRF:1 +BRH:0 +end_of_record +TN: +SF:src\utils\formOptions.js +FN:1,(empty-report) +FNF:1 +FNH:0 +FNDA:0,(empty-report) +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:132,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +LF:141 +LH:0 +BRDA:1,0,0,0 +BRF:1 +BRH:0 +end_of_record +TN: +SF:src\utils\formatters.js +FN:1,stripNonDigits +FN:3,formatCpf +FN:13,formatDateMask +FN:20,formatMonthYearMask +FN:26,formatPhone +FN:37,formatRgNumber +FN:50,formatCurrencyMask +FN:70,formatDateToBr +FN:78,parseBrDateToIso +FN:85,sanitizeDecimalInput +FN:100,normalizeDecimalForSubmit +FN:110,validateCpfAlgorithm +FN:137,validateBrDate +FN:164,numeroParaExtenso +FN:185,numeroParaTextoAte999 +FNF:15 +FNH:12 +FNDA:17,stripNonDigits +FNDA:6,formatCpf +FNDA:4,formatDateMask +FNDA:0,formatMonthYearMask +FNDA:4,formatPhone +FNDA:0,formatRgNumber +FNDA:4,formatCurrencyMask +FNDA:3,formatDateToBr +FNDA:4,parseBrDateToIso +FNDA:0,sanitizeDecimalInput +FNDA:4,normalizeDecimalForSubmit +FNDA:8,validateCpfAlgorithm +FNDA:28,validateBrDate +FNDA:11,numeroParaExtenso +FNDA:9,numeroParaTextoAte999 +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,6 +DA:5,6 +DA:6,6 +DA:7,6 +DA:8,1 +DA:9,1 +DA:10,3 +DA:11,3 +DA:12,1 +DA:13,1 +DA:14,4 +DA:15,4 +DA:16,4 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,1 +DA:26,1 +DA:27,4 +DA:28,4 +DA:29,4 +DA:30,4 +DA:31,4 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,1 +DA:50,1 +DA:51,4 +DA:52,3 +DA:53,4 +DA:54,3 +DA:55,3 +DA:56,3 +DA:57,3 +DA:58,4 +DA:59,4 +DA:60,4 +DA:61,2 +DA:62,2 +DA:63,2 +DA:64,2 +DA:65,2 +DA:66,2 +DA:67,2 +DA:68,2 +DA:69,1 +DA:70,1 +DA:71,3 +DA:72,3 +DA:73,1 +DA:74,3 +DA:75,1 +DA:76,1 +DA:77,1 +DA:78,1 +DA:79,4 +DA:80,2 +DA:81,4 +DA:82,1 +DA:83,1 +DA:84,1 +DA:85,1 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,1 +DA:100,1 +DA:101,4 +DA:102,3 +DA:103,3 +DA:104,4 +DA:105,1 +DA:106,1 +DA:107,2 +DA:108,2 +DA:109,1 +DA:110,1 +DA:111,8 +DA:112,8 +DA:113,4 +DA:114,4 +DA:115,4 +DA:116,4 +DA:117,8 +DA:118,36 +DA:119,36 +DA:120,4 +DA:121,4 +DA:122,8 +DA:123,8 +DA:124,4 +DA:125,4 +DA:126,8 +DA:127,40 +DA:128,40 +DA:129,4 +DA:130,4 +DA:131,8 +DA:132,8 +DA:133,3 +DA:134,3 +DA:135,3 +DA:136,1 +DA:137,1 +DA:138,28 +DA:139,25 +DA:140,25 +DA:141,28 +DA:142,24 +DA:143,24 +DA:144,24 +DA:145,24 +DA:146,24 +DA:147,24 +DA:148,24 +DA:149,24 +DA:150,28 +DA:151,28 +DA:152,20 +DA:153,20 +DA:154,28 +DA:155,28 +DA:156,28 +DA:157,28 +DA:158,28 +DA:159,28 +DA:160,17 +DA:161,17 +DA:162,17 +DA:163,1 +DA:164,1 +DA:165,11 +DA:166,11 +DA:167,11 +DA:168,7 +DA:169,7 +DA:170,7 +DA:171,7 +DA:172,7 +DA:173,7 +DA:174,7 +DA:175,7 +DA:176,7 +DA:177,7 +DA:178,7 +DA:179,7 +DA:180,7 +DA:181,7 +DA:182,7 +DA:183,11 +DA:184,6 +DA:185,6 +DA:186,9 +DA:187,9 +DA:188,9 +DA:189,9 +DA:190,9 +DA:191,9 +DA:192,9 +DA:193,9 +DA:194,0 +DA:195,9 +DA:196,9 +DA:197,9 +DA:198,9 +DA:199,9 +DA:200,6 +DA:201,6 +DA:202,6 +DA:203,6 +DA:204,11 +DA:205,9 +DA:206,9 +DA:207,9 +DA:208,6 +DA:209,6 +DA:210,6 +DA:211,9 +DA:212,8 +DA:213,9 +DA:214,8 +DA:215,9 +DA:216,9 +DA:217,9 +DA:218,6 +DA:219,6 +DA:220,6 +DA:221,6 +DA:222,11 +DA:223,11 +DA:224,11 +DA:225,11 +DA:226,11 +DA:227,1 +DA:228,1 +DA:229,1 +DA:230,1 +DA:231,6 +DA:232,6 +LF:232 +LH:203 +BRDA:1,0,0,17 +BRDA:3,1,0,6 +BRDA:5,2,0,1 +BRDA:6,3,0,5 +BRDA:6,4,0,1 +BRDA:7,5,0,4 +BRDA:7,6,0,1 +BRDA:9,7,0,3 +BRDA:13,8,0,4 +BRDA:15,9,0,2 +BRDA:16,10,0,1 +BRDA:26,11,0,4 +BRDA:28,12,0,1 +BRDA:29,13,0,3 +BRDA:29,14,0,1 +BRDA:30,15,0,2 +BRDA:30,16,0,0 +BRDA:31,17,0,2 +BRDA:31,18,0,1 +BRDA:50,19,0,4 +BRDA:51,20,0,1 +BRDA:52,21,0,3 +BRDA:53,22,0,0 +BRDA:54,23,0,3 +BRDA:58,24,0,1 +BRDA:59,25,0,2 +BRDA:59,26,0,0 +BRDA:60,27,0,2 +BRDA:60,28,0,0 +BRDA:61,29,0,2 +BRDA:70,30,0,3 +BRDA:71,31,0,1 +BRDA:72,32,0,2 +BRDA:72,33,0,1 +BRDA:74,34,0,1 +BRDA:74,35,0,0 +BRDA:75,36,0,1 +BRDA:78,37,0,4 +BRDA:79,38,0,3 +BRDA:79,39,0,2 +BRDA:81,40,0,2 +BRDA:81,41,0,2 +BRDA:81,42,0,1 +BRDA:100,43,0,4 +BRDA:101,44,0,1 +BRDA:102,45,0,3 +BRDA:104,46,0,1 +BRDA:106,47,0,2 +BRDA:110,48,0,8 +BRDA:112,49,0,5 +BRDA:112,50,0,4 +BRDA:117,51,0,36 +BRDA:119,52,0,4 +BRDA:122,53,0,0 +BRDA:123,54,0,4 +BRDA:123,55,0,0 +BRDA:124,56,0,4 +BRDA:126,57,0,40 +BRDA:128,58,0,4 +BRDA:131,59,0,0 +BRDA:132,60,0,4 +BRDA:132,61,0,1 +BRDA:133,62,0,3 +BRDA:137,63,0,28 +BRDA:138,64,0,25 +BRDA:138,65,0,3 +BRDA:139,66,0,25 +BRDA:141,67,0,1 +BRDA:142,68,0,24 +BRDA:150,69,0,3 +BRDA:151,70,0,21 +BRDA:151,71,0,1 +BRDA:152,72,0,20 +BRDA:154,73,0,3 +BRDA:154,74,0,20 +BRDA:156,75,0,3 +BRDA:156,76,0,17 +BRDA:159,77,0,20 +BRDA:159,78,0,20 +BRDA:159,79,0,3 +BRDA:160,80,0,17 +BRDA:164,81,0,11 +BRDA:165,82,0,10 +BRDA:165,83,0,9 +BRDA:165,84,0,3 +BRDA:166,85,0,8 +BRDA:166,86,0,2 +BRDA:166,87,0,6 +BRDA:167,88,0,1 +BRDA:168,89,0,7 +BRDA:183,90,0,1 +BRDA:183,91,0,1 +BRDA:184,92,0,6 +BRDA:204,93,0,9 +BRDA:207,94,0,6 +BRDA:222,95,0,0 +BRDA:223,96,0,2 +BRDA:223,97,0,4 +BRDA:226,98,0,1 +BRDA:227,99,0,0 +BRDA:228,100,0,0 +BRDA:230,101,0,6 +BRDA:185,102,0,9 +BRDA:186,103,0,0 +BRDA:187,104,0,0 +BRDA:192,105,0,2 +BRDA:193,106,0,0 +BRDA:196,107,0,3 +BRDA:197,108,0,8 +BRDA:210,109,0,9 +BRDA:211,110,0,1 +BRDA:212,111,0,8 +BRDA:213,112,0,0 +BRDA:214,113,0,8 +BRDA:215,114,0,5 +BRDA:216,115,0,5 +BRDA:216,116,0,3 +BRDA:216,117,0,0 +BRF:118 +BRH:102 +end_of_record diff --git a/frontend/coverage/prettify.css b/frontend/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/frontend/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/frontend/coverage/prettify.js b/frontend/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/frontend/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/frontend/coverage/sort-arrow-sprite.png b/frontend/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000..6ed6831 Binary files /dev/null and b/frontend/coverage/sort-arrow-sprite.png differ diff --git a/frontend/coverage/sorter.js b/frontend/coverage/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/frontend/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/frontend/coverage/utils/apiBase.js.html b/frontend/coverage/utils/apiBase.js.html new file mode 100644 index 0000000..e2a3148 --- /dev/null +++ b/frontend/coverage/utils/apiBase.js.html @@ -0,0 +1,364 @@ + + + + + + Code coverage report for utils/apiBase.js + + + + + + + + + +
+
+

All files / utils apiBase.js

+
+ +
+ 74.19% + Statements + 69/93 +
+ + +
+ 46.66% + Branches + 7/15 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 74.19% + Lines + 69/93 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +941x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +3x +3x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +2x +  +  +  +2x +2x +2x +  +  +2x +2x +2x +2x +3x +3x +3x + 
export function getApiBase() {
+  // 1. A variável de ambiente é a fonte da verdade.
+  let envUrl =
+    (typeof import.meta !== "undefined" && import.meta?.env?.VITE_API_URL) ||
+    "";
+ 
+  if (envUrl) {
+    envUrl = envUrl.replace(/\/$/, "");
+    if (!envUrl.endsWith("/api")) {
+      envUrl = `${envUrl}/api`;
+    }
+    return envUrl;
+  }
+
+  // 2. Detecção de Desenvolvimento robusta
+  const isLocalhost = 
+    typeof window !== "undefined" && 
+    (window.location.hostname === "localhost" || 
+     window.location.hostname === "127.0.0.1" ||
+     window.location.hostname.startsWith("192.168."));
+  
+  const isDevMode =
+    (typeof import.meta !== "undefined" && import.meta?.env?.DEV) || false;
+ 
+  if (isLocalhost || isDevMode) {
+    console.warn(
+      `Modo de desenvolvimento detectado (Host: ${typeof window !== "undefined" ? window.location.hostname : 'N/A'}). Usando API: http://localhost:8000/api`,
+    );
+    return "http://localhost:8000/api";
+  }
+
+  // 3. Produção sem variável de ambiente
+  console.error(
+    "ERRO CRÍTICO: VITE_API_URL não definida em produção. As chamadas irão falhar.",
+  );
+
+  return "";
+}
+ 
+export const API_BASE = getApiBase();
+ 
+export const authFetch = async (endpoint, options = {}) => {
+  // 1. Pega o token atual
+  const token = localStorage.getItem("defensorToken");
+ 
+  // 2. Prepara os headers padrão
+  const headers = {
+    "Content-Type": "application/json",
+    ...options.headers,
+  };
+ 
+  // 3. Injeta o Token se existir
+  if (token) {
+    headers["Authorization"] = `Bearer ${token}`;
+  }
+ 
+  // 4. Faz a requisição
+  // Nota: endpoint não precisa incluir API_BASE se passar só o caminho (ex: '/casos')
+  const url = endpoint.startsWith("http") ? endpoint : `${API_BASE}${endpoint}`;
+ 
+  const response = await fetch(url, {
+    ...options,
+    headers,
+  });
+ 
+  // 5. DETECTA SESSÃO EXPIRADA
+  if (response.status === 401) {
+    // 1. Limpa imediatamente o storage para evitar loops
+    localStorage.removeItem("defensorToken");
+    localStorage.removeItem("defensorUser");
+ 
+    // 2. Dispara o evento que o AuthContext vai escutar para redirecionar
+    try {
+      if (typeof window !== "undefined") {
+        let event;
+        try {
+          event = new CustomEvent("auth:session-expired");
+        } catch {
+          event = document.createEvent("Event");
+          event.initEvent("auth:session-expired", true, true);
+        }
+        window.dispatchEvent(event);
+      }
+    } catch (e) {
+      console.warn("Falha crítica ao disparar evento de expiração:", e);
+    }
+ 
+    // 3. Lança erro padronizado
+    throw new Error("Sessão expirada");
+  }
+ 
+  return response;
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/utils/caseUtils.js.html b/frontend/coverage/utils/caseUtils.js.html new file mode 100644 index 0000000..be0056b --- /dev/null +++ b/frontend/coverage/utils/caseUtils.js.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for utils/caseUtils.js + + + + + + + + + +
+
+

All files / utils caseUtils.js

+
+ +
+ 100% + Statements + 32/32 +
+ + +
+ 100% + Branches + 9/9 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 32/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +331x +15x +15x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +12x +15x +9x +9x +3x +15x +2x +2x +1x +1x +1x +1x + 
export const formatTipoAcaoLabel = (tipoAcao = "") => {
+  const raw = String(tipoAcao || "").trim();
+  if (!raw) return "Não informado";
+ 
+  const knownLabels = {
+    exec_cumulado: "Execução de Alimentos",
+    exec_penhora: "Execução de Alimentos (Penhora)",
+    exec_prisao: "Execução de Alimentos (Prisão)",
+    execucao_alimentos: "Execução de Alimentos",
+    def_cumulado: "Cumprimento de Sentença Cumulada",
+    def_penhora: "Cumprimento de Sentença (Penhora)",
+    def_prisao: "Cumprimento de Sentença (Prisão)",
+    termo_declaracao: "Termo de Declaração",
+  };
+ 
+  const key = raw
+    .toLowerCase()
+    .replace(/\s+/g, "_")
+    .normalize("NFD")
+    .replace(/[\u0300-\u036f]/g, "");
+ 
+  if (knownLabels[key]) {
+    return knownLabels[key];
+  }
+ 
+  if (raw.includes(" - ")) {
+    return raw.split(" - ")[1].trim();
+  }
+ 
+  const withSpaces = raw.replace(/_/g, " ");
+  return withSpaces.replace(/\b\w/g, (char) => char.toUpperCase());
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/utils/fetcher.js.html b/frontend/coverage/utils/fetcher.js.html new file mode 100644 index 0000000..b13f78d --- /dev/null +++ b/frontend/coverage/utils/fetcher.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for utils/fetcher.js + + + + + + + + + +
+
+

All files / utils fetcher.js

+
+ +
+ 0% + Statements + 0/21 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { authFetch } from "./apiBase";
+
+// src/utils/fetcher.js
+export const fetcher = async (url) => {
+  const response = await authFetch(url);
+
+  if (!response.ok) {
+    let errorMsg = "Erro ao buscar dados da API";
+    try {
+      const errData = await response.json();
+      if (errData.error || errData.message) {
+        errorMsg = errData.error || errData.message;
+      }
+    } catch {
+      // Ignora erro de parse se não for JSON
+    }
+    throw new Error(errorMsg);
+  }
+
+  return response.json();
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/utils/formOptions.js.html b/frontend/coverage/utils/formOptions.js.html new file mode 100644 index 0000000..4e29914 --- /dev/null +++ b/frontend/coverage/utils/formOptions.js.html @@ -0,0 +1,508 @@ + + + + + + Code coverage report for utils/formOptions.js + + + + + + + + + +
+
+

All files / utils formOptions.js

+
+ +
+ 0% + Statements + 0/141 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/141 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// src/utils/formOptions.js
+
+export const nacionalidadeOptions = [
+  { value: "", label: "Nacionalidade" },
+  { value: "brasileiro(a)", label: "Brasileiro(a)" },
+  { value: "estrangeiro(a)", label: "Estrangeiro(a)" },
+];
+
+export const estadoCivilOptions = [
+  { value: "", label: "Estado Civil" },
+  { value: "solteiro(a)", label: "Solteiro(a)" },
+  { value: "casado(a)", label: "Casado(a)" },
+  { value: "divorciado(a)", label: "Divorciado(a)" },
+  { value: "viúvo(a)", label: "Viúvo(a)" },
+  { value: "união estável", label: "União Estável" },
+];
+
+export const orgaoEmissorOptions = [
+  { value: "", label: "Órgão emissor" },
+  { value: "SSP/AC", label: "SSP/AC" },
+  { value: "SSP/AL", label: "SSP/AL" },
+  { value: "SSP/AP", label: "SSP/AP" },
+  { value: "SSP/AM", label: "SSP/AM" },
+  { value: "SSP/BA", label: "SSP/BA" },
+  { value: "SSP/CE", label: "SSP/CE" },
+  { value: "SSP/DF", label: "SSP/DF" },
+  { value: "SSP/ES", label: "SSP/ES" },
+  { value: "SSP/GO", label: "SSP/GO" },
+  { value: "SSP/MA", label: "SSP/MA" },
+  { value: "SSP/MT", label: "SSP/MT" },
+  { value: "SSP/MS", label: "SSP/MS" },
+  { value: "SSP/MG", label: "SSP/MG" },
+  { value: "SSP/PA", label: "SSP/PA" },
+  { value: "SSP/PB", label: "SSP/PB" },
+  { value: "SSP/PR", label: "SSP/PR" },
+  { value: "SSP/PE", label: "SSP/PE" },
+  { value: "SSP/PI", label: "SSP/PI" },
+  { value: "SSP/RJ", label: "SSP/RJ" },
+  { value: "SSP/RN", label: "SSP/RN" },
+  { value: "SSP/RS", label: "SSP/RS" },
+  { value: "SSP/RO", label: "SSP/RO" },
+  { value: "SSP/RR", label: "SSP/RR" },
+  { value: "SSP/SC", label: "SSP/SC" },
+  { value: "SSP/SP", label: "SSP/SP" },
+  { value: "SSP/SE", label: "SSP/SE" },
+  { value: "SSP/TO", label: "SSP/TO" },
+  { value: "DETRAN", label: "Detran" },
+  { value: "OUTRO", label: "Outro" },
+];
+
+export const cidadesBahia = [
+  "Alagoinhas",
+  "Amargosa",
+  "Barreiras",
+  "Bom Jesus da Lapa",
+  "Brumado",
+  "Cachoeira",
+  "Camacan",
+  "Camaçari",
+  "Campo Formoso",
+  "Canavieiras",
+  "Candeias",
+  "Catu",
+  "Conceição do Coité",
+  "Cruz das Almas",
+  "Esplanada",
+  "Euclides da Cunha",
+  "Eunápolis",
+  "Feira de Santana",
+  "Guanambi",
+  "Ilhéus",
+  "Ipiaú",
+  "Ipirá",
+  "Irará",
+  "Irecê",
+  "Itaberaba",
+  "Itabuna",
+  "Itaparica",
+  "Itapetinga",
+  "Jacobina",
+  "Jequié",
+  "Juazeiro",
+  "Lauro de Freitas",
+  "Luís Eduardo Magalhães",
+  "Macaúbas",
+  "Nazaré",
+  "Paripiranga",
+  "Paulo Afonso",
+  "Poções",
+  "Porto Seguro",
+  "Riachão do Jacuípe",
+  "Ribeira do Pombal",
+  "Salvador",
+  "Santa Maria da Vitória",
+  "Santo Amaro",
+  "Santo Antônio de Jesus",
+  "Santo Estêvão",
+  "Seabra",
+  "Senhor do Bonfim",
+  "Serrinha",
+  "Simões Filho",
+  "Teixeira de Freitas",
+  "Valença",
+  "Vitória da Conquista"
+];
+
+export const outrosDadosRequeridoConfig = [
+  { key: "requeridoRg", label: "RG e órgão emissor", renderType: "rg" },
+  {
+    key: "requeridoDataNascimento",
+    label: "Data de nascimento",
+    renderType: "date",
+    field: "requeridoDataNascimento",
+  },
+  {
+    key: "requeridoOutrosDetalhes",
+    label: "Outros detalhes relevantes",
+    renderType: "textarea",
+    field: "requeridoOutrosDetalhes",
+    placeholder: "Ex: Nome do advogado, redes sociais, etc.",
+  },
+];
+
+export const outrosDadosRequeridoFieldMap = {
+  requeridoRg: ["requeridoRgNumero", "requeridoRgOrgao"],
+  requeridoDataNascimento: ["requeridoDataNascimento"],
+  requeridoNomeMae: ["requeridoNomeMae"],
+  requeridoNomePai: ["requeridoNomePai"],
+  requeridoOutrosDetalhes: ["requeridoOutrosDetalhes"],
+  requeridoFiliacao: ["requeridoNomeMae", "requeridoNomePai"],
+};
+
+export const acoesFallbackFamilia = [
+  "Fixação de Pensão Alimentícia",
+  "Divórcio",
+  "Reconhecimento e Dissolução de União Estável",
+  "Guarda de Filhos",
+  "Alvará",
+  "Execução de Alimentos Rito Penhora/Prisão",
+  "Revisão de Alimentos",
+];
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/utils/formatters.js.html b/frontend/coverage/utils/formatters.js.html new file mode 100644 index 0000000..a0b187d --- /dev/null +++ b/frontend/coverage/utils/formatters.js.html @@ -0,0 +1,781 @@ + + + + + + Code coverage report for utils/formatters.js + + + + + + + + + +
+
+

All files / utils formatters.js

+
+ +
+ 87.5% + Statements + 203/232 +
+ + +
+ 86.44% + Branches + 102/118 +
+ + +
+ 80% + Functions + 12/15 +
+ + +
+ 87.5% + Lines + 203/232 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +2331x +1x +1x +6x +6x +6x +6x +1x +1x +3x +3x +1x +1x +4x +4x +4x +1x +1x +1x +1x +  +  +  +  +1x +1x +4x +4x +4x +4x +4x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +1x +1x +4x +3x +4x +3x +3x +3x +3x +4x +4x +4x +2x +2x +2x +2x +2x +2x +2x +2x +1x +1x +3x +3x +1x +3x +1x +1x +1x +1x +4x +2x +4x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +4x +3x +3x +4x +1x +1x +2x +2x +1x +1x +8x +8x +4x +4x +4x +4x +8x +36x +36x +4x +4x +8x +8x +4x +4x +8x +40x +40x +4x +4x +8x +8x +3x +3x +3x +1x +1x +28x +25x +25x +28x +24x +24x +24x +24x +24x +24x +24x +24x +28x +28x +20x +20x +28x +28x +28x +28x +28x +28x +17x +17x +17x +1x +1x +11x +11x +11x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +7x +11x +6x +6x +9x +9x +9x +9x +9x +9x +9x +9x +  +9x +9x +9x +9x +9x +6x +6x +6x +6x +11x +9x +9x +9x +6x +6x +6x +9x +8x +9x +8x +9x +9x +9x +6x +6x +6x +6x +11x +11x +11x +11x +11x +1x +1x +1x +1x +6x +6x + 
export const stripNonDigits = (value = "") => value.replace(/\D/g, "");
+ 
+export const formatCpf = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 11);
+  if (digits.length <= 3) return digits;
+  if (digits.length <= 6) return `${digits.slice(0, 3)}.${digits.slice(3)}`;
+  if (digits.length <= 9) {
+    return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
+  }
+  return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
+};
+ 
+export const formatDateMask = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 8);
+  if (digits.length <= 2) return digits;
+  if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+  return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
+};
+ 
+export const formatMonthYearMask = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 6);
+  if (digits.length <= 2) return digits;
+  return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+};
+ 
+export const formatPhone = (value = "") => {
+  const digits = stripNonDigits(value).slice(0, 11);
+  if (!digits) return "";
+  if (digits.length <= 2) return `(${digits}`;
+  if (digits.length <= 6) return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
+  if (digits.length <= 10) {
+    return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
+  }
+  return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
+};
+ 
+export const formatRgNumber = (value = "") => {
+  const digits = value.replace(/[^0-9xX]/g, "").toUpperCase().slice(0, 11);
+  if (digits.length <= 2) return digits;
+  if (digits.length <= 5) return `${digits.slice(0, 2)}.${digits.slice(2)}`;
+  if (digits.length <= 8) {
+    return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5)}`;
+  }
+  if (digits.length <= 10) {
+    return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}-${digits.slice(8)}`;
+  }
+  return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}.${digits.slice(8, 10)}-${digits.slice(10)}`;
+};
+ 
+export const formatCurrencyMask = (value = "") => {
+  if (value === null || value === undefined || value === "") return "";
+  let digits = String(value).replace(/\D/g, "");
+  if (digits === "") return "";
+ 
+  // Remove leading zeros
+  digits = digits.replace(/^0+/, "");
+ 
+  if (digits.length === 0) return "0,00";
+  if (digits.length === 1) return `0,0${digits}`;
+  if (digits.length === 2) return `0,${digits}`;
+ 
+  const cents = digits.slice(-2);
+  let integer = digits.slice(0, -2);
+ 
+  integer = integer.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1.");
+ 
+  return `${integer},${cents}`;
+};
+ 
+export const formatDateToBr = (isoDate = "") => {
+  if (!isoDate) return "";
+  if (isoDate.includes("/")) return isoDate; // Já está no formato BR
+  const [year, month, day] = isoDate.split("-");
+  if (!year || !month || !day) return isoDate;
+  return `${day.padStart(2, "0")}/${month.padStart(2, "0")}/${year}`;
+};
+ 
+export const parseBrDateToIso = (brDate = "") => {
+  if (!brDate || !brDate.includes("/")) return brDate;
+  const [day, month, year] = brDate.split("/");
+  if (!day || !month || !year || year.length < 4) return "";
+  return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
+};
+ 
+export const sanitizeDecimalInput = (
+  value = "",
+  { decimalPlaces = 2, maxIntegerDigits = 9 } = {},
+) => {
+  if (value === null || value === undefined || value === "") return "";
+  const allowed = String(value).replace(/[^0-9,]/g, "");
+  const [intPartRaw, ...decimalParts] = allowed.split(",");
+  const integerPart = (intPartRaw || "").slice(0, maxIntegerDigits);
+  if (decimalParts.length === 0) {
+    return integerPart;
+  }
+  const decimalPart = decimalParts.join("").slice(0, decimalPlaces);
+  return `${integerPart},${decimalPart}`;
+};
+ 
+export const normalizeDecimalForSubmit = (value = "", decimals = 2) => {
+  if (value === null || value === undefined || value === "") return "";
+  const normalized = String(value).replace(/\./g, "").replace(",", ".");
+  const number = Number(normalized);
+  if (Number.isNaN(number)) {
+    return "";
+  }
+  return number.toFixed(decimals);
+};
+ 
+export const validateCpfAlgorithm = (cpf) => {
+  const cleanCpf = String(cpf).replace(/[^\d]+/g, "");
+  if (cleanCpf.length !== 11 || /^(\d)\1+$/.test(cleanCpf)) return false;
+ 
+  let soma = 0;
+  let resto;
+ 
+  for (let i = 1; i <= 9; i++) {
+    soma = soma + parseInt(cleanCpf.substring(i - 1, i)) * (11 - i);
+  }
+  resto = (soma * 10) % 11;
+ 
+  if (resto === 10 || resto === 11) resto = 0;
+  if (resto !== parseInt(cleanCpf.substring(9, 10))) return false;
+ 
+  soma = 0;
+  for (let i = 1; i <= 10; i++) {
+    soma = soma + parseInt(cleanCpf.substring(i - 1, i)) * (12 - i);
+  }
+  resto = (soma * 10) % 11;
+ 
+  if (resto === 10 || resto === 11) resto = 0;
+  if (resto !== parseInt(cleanCpf.substring(10, 11))) return false;
+ 
+  return true;
+};
+ 
+export const validateBrDate = (brDate = "") => {
+  if (!brDate || typeof brDate !== "string") return false;
+  
+  // Verifica formato exato DD/MM/AAAA
+  if (!/^\d{2}\/\d{2}\/\d{4}$/.test(brDate)) return false;
+  
+  const [dayStr, monthStr, yearStr] = brDate.split("/");
+  const day = parseInt(dayStr, 10);
+  const month = parseInt(monthStr, 10);
+  const year = parseInt(yearStr, 10);
+  
+  // Limites básicos (não aceita antes de 1900 ou depois do ano atual + 1 por segurança na conversão de timezones)
+  const currentYear = new Date().getFullYear();
+  if (year < 1900 || year > currentYear) return false;
+  if (month < 1 || month > 12) return false;
+  
+  // Dias por mês (considerando bissexto correto)
+  const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
+  const daysInMonth = [
+    31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+  ];
+  
+  if (day < 1 || day > daysInMonth[month - 1]) return false;
+  
+  return true;
+};
+ 
+export const numeroParaExtenso = (valor) => {
+  if (valor === null || valor === undefined || valor === "") return "";
+  const v = typeof valor === "string" ? parseFloat(valor.replace(/\./g, "").replace(",", ".")) : valor;
+  if (isNaN(v)) return "";
+  
+  const unidades = ["zero", "um", "dois", "três", "quatro", "cinco", "seis", "sete", "oito", "nove"];
+  const especiais = ["dez", "onze", "doze", "treze", "quatorze", "quinze", "dezesseis", "dezessete", "dezoito", "dezenove"];
+  const dezenas = ["", "dez", "vinte", "trinta", "quarenta", "cinquenta", "sessenta", "setenta", "oitenta", "noventa"];
+  const centenas = ["", "cento", "duzentos", "trezentos", "quatrocentos", "quinhentos", "seiscentos", "setecentos", "oitocentos", "novecentos"];
+  const qualificadores = [
+    { singular: "", plural: "" },
+    { singular: "mil", plural: "mil" },
+    { singular: "milhão", plural: "milhões" },
+    { singular: "bilhão", plural: "bilhões" },
+  ];
+ 
+  const inteiro = Math.floor(Math.abs(v));
+  const centavos = Math.round((Math.abs(v) - inteiro) * 100);
+ 
+  if (inteiro === 0 && centavos === 0) return "zero real";
+ 
+  const numeroParaTextoAte999 = (numero) => {
+    if (numero === 0) return "";
+    if (numero === 100) return "cem";
+    const c = Math.floor(numero / 100);
+    const d = Math.floor((numero % 100) / 10);
+    const u = numero % 10;
+    const partes = [];
+    if (c) partes.push(centenas[c]);
+    if (d === 1) {
+      partes.push(especiais[u]);
+    } else {
+      if (d) partes.push(dezenas[d]);
+      if (u) partes.push(unidades[u]);
+    }
+    return partes.join(" e ");
+  };
+ 
+  const grupos = [];
+  let numeroRestante = inteiro;
+  while (numeroRestante > 0) {
+    grupos.push(numeroRestante % 1000);
+    numeroRestante = Math.floor(numeroRestante / 1000);
+  }
+ 
+  const partesInteiras = grupos
+    .map((grupo, index) => {
+      if (!grupo) return null;
+      const texto = numeroParaTextoAte999(grupo);
+      if (!texto) return null;
+      const qualificador = qualificadores[index];
+      const ehSingular = grupo === 1 && index > 0;
+      const sufixo = index === 0 ? "" : ` ${ehSingular ? qualificador.singular : qualificador.plural}`;
+      return `${texto}${sufixo}`;
+    })
+    .filter(Boolean)
+    .reverse();
+ 
+  const inteiroExtenso = partesInteiras.join(" e ") || "zero";
+  const rotuloInteiro = inteiro === 1 ? "real" : "reais";
+ 
+  let resultado = `${inteiroExtenso} ${rotuloInteiro}`;
+  if (centavos > 0) {
+    const centavosExtenso = numeroParaTextoAte999(centavos) || "zero";
+    const rotuloCentavos = centavos === 1 ? "centavo" : "centavos";
+    resultado += ` e ${centavosExtenso} ${rotuloCentavos}`;
+  }
+  return resultado;
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/coverage/utils/index.html b/frontend/coverage/utils/index.html new file mode 100644 index 0000000..830a02a --- /dev/null +++ b/frontend/coverage/utils/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for utils + + + + + + + + + +
+
+

All files utils

+
+ +
+ 58.57% + Statements + 304/519 +
+ + +
+ 81.94% + Branches + 118/144 +
+ + +
+ 75% + Functions + 15/20 +
+ + +
+ 58.57% + Lines + 304/519 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
apiBase.js +
+
74.19%69/9346.66%7/15100%2/274.19%69/93
caseUtils.js +
+
100%32/32100%9/9100%1/1100%32/32
fetcher.js +
+
0%0/210%0/10%0/10%0/21
formOptions.js +
+
0%0/1410%0/10%0/10%0/141
formatters.js +
+
87.5%203/23286.44%102/11880%12/1587.5%203/232
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ae296d0..9c112fd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,7 @@ "browser-image-compression": "^2.0.2", "heic2any": "^0.0.4", "html2canvas": "^1.4.1", - "jspdf": "^3.0.4", + "jspdf": "^4.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^0.545.0", "motion": "^12.23.22", @@ -30,13 +30,80 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react-swc": "^4.1.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "jsdom": "^26.1.0", "prettier": "^3.8.3", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vitest": "^3.2.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@babel/runtime": { @@ -48,6 +115,147 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -673,6 +881,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -718,6 +954,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -1667,6 +1914,17 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -1730,6 +1988,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1825,6 +2090,155 @@ "vite": "^4 || ^5 || ^6 || ^7" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1849,6 +2263,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1866,15 +2290,28 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { "node": ">=8" }, @@ -1889,6 +2326,28 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1925,6 +2384,16 @@ "uzip": "0.20201231.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1955,6 +2424,23 @@ "node": ">=10.0.0" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1972,6 +2458,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2057,6 +2553,20 @@ "utrie": "^1.0.2" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2185,6 +2695,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2203,12 +2727,29 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2244,6 +2785,20 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -2257,6 +2812,26 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-toolkit": { "version": "1.46.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", @@ -2506,6 +3081,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2522,6 +3107,16 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2628,6 +3223,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/framer-motion": { "version": "12.23.22", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", @@ -2669,6 +3281,28 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2682,6 +3316,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", @@ -2717,6 +3377,26 @@ "integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==", "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -2730,6 +3410,34 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -2739,6 +3447,19 @@ "node": ">=20.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2811,6 +3532,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2824,6 +3555,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2831,6 +3569,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2840,6 +3648,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2853,6 +3668,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2875,19 +3731,19 @@ "license": "MIT" }, "node_modules/jspdf": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", - "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", - "dompurify": "^3.2.4", + "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, @@ -3196,6 +4052,20 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.545.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", @@ -3214,10 +4084,38 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3227,6 +4125,16 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/motion": { "version": "12.23.22", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.22.tgz", @@ -3300,6 +4208,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3350,6 +4265,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -3369,6 +4291,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3389,6 +4324,40 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3704,12 +4673,52 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -3739,6 +4748,26 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3748,6 +4777,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -3758,6 +4794,117 @@ "node": ">=0.1.14" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3771,6 +4918,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3826,6 +4993,13 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", @@ -3845,6 +5019,60 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -3860,6 +5088,20 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3876,6 +5118,82 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4032,6 +5350,164 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4048,6 +5524,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4058,6 +5551,101 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -4079,6 +5667,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ed7ec69..d17e604 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,10 @@ "build": "vite build", "lint": "eslint .", "preview": "vite preview", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@supabase/supabase-js": "^2.103.0", @@ -16,7 +19,7 @@ "browser-image-compression": "^2.0.2", "heic2any": "^0.0.4", "html2canvas": "^1.4.1", - "jspdf": "^3.0.4", + "jspdf": "^4.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^0.545.0", "motion": "^12.23.22", @@ -33,12 +36,15 @@ "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react-swc": "^4.1.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "jsdom": "^26.1.0", "prettier": "^3.8.3", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vitest": "^3.2.4" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9bcd66a..e64a731 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -25,6 +25,7 @@ import { DetalhesCaso } from "./areas/defensor/pages/DetalhesCaso"; import { CasosArquivados } from "./areas/defensor/pages/CasosArquivados"; import { GerenciarEquipe } from "./areas/defensor/pages/GerenciarEquipe"; +import { ConfiguracoesSistema } from "./areas/defensor/pages/ConfiguracoesSistema"; import { NotFound } from "./pages/NotFound"; const Relatorios = lazy(() => import("./areas/defensor/pages/Relatorios")); @@ -66,9 +67,31 @@ const AdminRoute = ({ children }) => { // 4. Protege rotas de DEFENSOR/ESTAGIÁRIO const DefensorRoute = ({ children }) => { const { loading } = useAuth(); + if (loading) return null; + return children; +}; + +// 5. Protege rotas para Admin e Gestor +const GestorRoute = ({ children }) => { + const { permissions, loading } = useAuth(); + if (loading) return null; + if (!permissions.canEditConfig) return ; + return children; +}; +// 6. Protege rotas de BI (Admin, Gestor, Coordenador) +const BiRoute = ({ children }) => { + const { permissions, loading } = useAuth(); if (loading) return null; + if (!permissions.canViewBi) return ; + return children; +}; +// 7. Protege rotas de Gestão de Equipe (Admin, Gestor, Coordenador) +const TeamRoute = ({ children }) => { + const { permissions, loading } = useAuth(); + if (loading) return null; + if (!permissions.canManageTeam) return ; return children; }; @@ -143,24 +166,33 @@ function App() { - {/* Rota de Gestão de Equipe (Admin) */} + {/* Rota de Gestão de Equipe (Admin, Gestor, Coordenador) */} + - + } /> + Carregando relatorios...}> - + + } + /> + + + + } /> diff --git a/frontend/src/__tests__/apiBase.test.js b/frontend/src/__tests__/apiBase.test.js new file mode 100644 index 0000000..871a3b9 --- /dev/null +++ b/frontend/src/__tests__/apiBase.test.js @@ -0,0 +1,105 @@ +/* global global */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +describe("getApiBase — comportamento por ambiente", () => { + it("expõe API_BASE como string (não undefined)", async () => { + const { API_BASE } = await import("@/utils/apiBase.js"); + expect(typeof API_BASE).toBe("string"); + }); +}); + +describe("authFetch — comportamento de sessão expirada", () => { + let originalFetch; + + beforeEach(() => { + originalFetch = global.fetch; + global.fetch = vi.fn(); + + const localStorageMock = (() => { + let store = {}; + return { + getItem: (key) => store[key] ?? null, + setItem: (key, val) => { store[key] = val; }, + removeItem: (key) => { delete store[key]; }, + clear: () => { store = {}; }, + }; + })(); + Object.defineProperty(global, "localStorage", { + value: localStorageMock, + writable: true, + }); + + global.window = { + dispatchEvent: vi.fn(), + location: { hostname: "localhost" }, + }; + }); + + afterEach(() => { + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("lança erro 'Sessão expirada' ao receber status 401", async () => { + global.fetch = vi.fn().mockResolvedValue({ status: 401, ok: false }); + global.localStorage.setItem("defensorToken", "token-qualquer"); + + const { authFetch } = await import("@/utils/apiBase.js"); + + await expect(authFetch("/api/casos")).rejects.toThrow("Sessão expirada"); + }); + + it("remove o token do localStorage ao receber 401", async () => { + global.fetch = vi.fn().mockResolvedValue({ status: 401, ok: false }); + global.localStorage.setItem("defensorToken", "token-qualquer"); + global.localStorage.setItem("defensorUser", '{"nome":"Teste"}'); + + const { authFetch } = await import("@/utils/apiBase.js"); + + try { + await authFetch("/api/casos"); + } catch { + // Esperado + } + + expect(global.localStorage.getItem("defensorToken")).toBeNull(); + expect(global.localStorage.getItem("defensorUser")).toBeNull(); + }); + + it("injeta Authorization header quando há token no localStorage", async () => { + global.fetch = vi.fn().mockResolvedValue({ status: 200, ok: true, json: async () => ({}) }); + global.localStorage.setItem("defensorToken", "meu-jwt-aqui"); + + const { authFetch } = await import("@/utils/apiBase.js"); + + await authFetch("/api/casos"); + + const callArgs = global.fetch.mock.calls[0]; + const options = callArgs[1]; + expect(options.headers["Authorization"]).toBe("Bearer meu-jwt-aqui"); + }); + + it("não injeta Authorization header quando não há token", async () => { + global.fetch = vi.fn().mockResolvedValue({ status: 200, ok: true }); + global.localStorage.removeItem("defensorToken"); + + const { authFetch } = await import("@/utils/apiBase.js"); + + await authFetch("/api/casos"); + + const callArgs = global.fetch.mock.calls[0]; + const options = callArgs[1]; + expect(options.headers["Authorization"]).toBeUndefined(); + }); + + it("usa URL absoluta quando endpoint começa com http", async () => { + global.fetch = vi.fn().mockResolvedValue({ status: 200, ok: true }); + + const { authFetch } = await import("@/utils/apiBase.js"); + const absoluteUrl = "https://api.exemplo.com/casos"; + + await authFetch(absoluteUrl); + + expect(global.fetch.mock.calls[0][0]).toBe(absoluteUrl); + }); +}); diff --git a/frontend/src/__tests__/caseUtils.test.js b/frontend/src/__tests__/caseUtils.test.js new file mode 100644 index 0000000..1b76ddb --- /dev/null +++ b/frontend/src/__tests__/caseUtils.test.js @@ -0,0 +1,79 @@ +import { describe, it, expect } from "vitest"; +import { formatTipoAcaoLabel } from "@/utils/caseUtils.js"; + +describe("formatTipoAcaoLabel", () => { + // ─── Chaves internas conhecidas ────────────────────────────────────────── + it("resolve 'exec_cumulado' → 'Execução de Alimentos'", () => { + expect(formatTipoAcaoLabel("exec_cumulado")).toBe("Execução de Alimentos"); + }); + + it("resolve 'exec_penhora' → 'Execução de Alimentos (Penhora)'", () => { + expect(formatTipoAcaoLabel("exec_penhora")).toBe("Execução de Alimentos (Penhora)"); + }); + + it("resolve 'exec_prisao' → 'Execução de Alimentos (Prisão)'", () => { + expect(formatTipoAcaoLabel("exec_prisao")).toBe("Execução de Alimentos (Prisão)"); + }); + + it("resolve 'def_cumulado' → 'Cumprimento de Sentença Cumulada'", () => { + expect(formatTipoAcaoLabel("def_cumulado")).toBe("Cumprimento de Sentença Cumulada"); + }); + + it("resolve 'def_penhora' → 'Cumprimento de Sentença (Penhora)'", () => { + expect(formatTipoAcaoLabel("def_penhora")).toBe("Cumprimento de Sentença (Penhora)"); + }); + + it("resolve 'def_prisao' → 'Cumprimento de Sentença (Prisão)'", () => { + expect(formatTipoAcaoLabel("def_prisao")).toBe("Cumprimento de Sentença (Prisão)"); + }); + + it("resolve 'termo_declaracao' → 'Termo de Declaração'", () => { + expect(formatTipoAcaoLabel("termo_declaracao")).toBe("Termo de Declaração"); + }); + + // ─── Formato "Area - Ação" ──────────────────────────────────────────────── + it("extrai a parte após ' - ' para chave desconhecida com separador", () => { + expect(formatTipoAcaoLabel("Família - Fixação de Pensão Alimentícia")).toBe( + "Fixação de Pensão Alimentícia" + ); + }); + + it("extrai corretamente mesmo com espaços extras ao redor do separador", () => { + const result = formatTipoAcaoLabel("Área - Ação Especial"); + expect(result).toBe("Ação Especial"); + }); + + // ─── Valores com underscores desconhecidos ──────────────────────────────── + it("converte underscores em espaços e capitaliza para chave desconhecida", () => { + expect(formatTipoAcaoLabel("fixacao_gravidicos")).toBe("Fixacao Gravidicos"); + }); + + // ─── Casos de borda ─────────────────────────────────────────────────────── + it("retorna 'Não informado' para string vazia", () => { + expect(formatTipoAcaoLabel("")).toBe("Não informado"); + }); + + it("retorna 'Não informado' para undefined", () => { + expect(formatTipoAcaoLabel(undefined)).toBe("Não informado"); + }); + + it("retorna 'Não informado' para null (coerção via String(null) → 'null'... não, testa comportamento real)", () => { + // String(null) = "null" que não está em knownLabels e não tem " - ", + // então resulta em "Null" capitalizado + const result = formatTipoAcaoLabel(null); + // Não deve lançar erro + expect(typeof result).toBe("string"); + }); + + it("normaliza acentos no lookup (chave com acento encontra versão sem acento)", () => { + // "execução_alimentos" com acento deve ser normalizado e resolver + const result = formatTipoAcaoLabel("execução_alimentos"); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + it("é case-insensitive no lookup (EXEC_CUMULADO → resolve)", () => { + // A função faz .toLowerCase() antes do lookup + expect(formatTipoAcaoLabel("EXEC_CUMULADO")).toBe("Execução de Alimentos"); + }); +}); diff --git a/frontend/src/__tests__/formatters.test.js b/frontend/src/__tests__/formatters.test.js new file mode 100644 index 0000000..917de45 --- /dev/null +++ b/frontend/src/__tests__/formatters.test.js @@ -0,0 +1,282 @@ +import { describe, it, expect } from "vitest"; +import { + stripNonDigits, + formatCpf, + formatPhone, + formatDateMask, + formatDateToBr, + parseBrDateToIso, + validateCpfAlgorithm, + validateBrDate, + formatCurrencyMask, + normalizeDecimalForSubmit, + numeroParaExtenso, +} from "@/utils/formatters.js"; + +// ─── stripNonDigits ─────────────────────────────────────────────────────────── +describe("stripNonDigits", () => { + it("remove todos os caracteres não-numéricos", () => { + expect(stripNonDigits("123.456.789-09")).toBe("12345678909"); + }); + + it("retorna string vazia para input vazio", () => { + expect(stripNonDigits("")).toBe(""); + }); + + it("mantém apenas dígitos em string mista", () => { + expect(stripNonDigits("(71) 99999-8888")).toBe("71999998888"); + }); +}); + +// ─── formatCpf ─────────────────────────────────────────────────────────────── +describe("formatCpf", () => { + it("formata CPF completo com pontos e traço", () => { + expect(formatCpf("52998224725")).toBe("529.982.247-25"); + }); + + it("formata CPF com máscara (input com pontos)", () => { + expect(formatCpf("529.982.247-25")).toBe("529.982.247-25"); + }); + + it("formata parcialmente com 5 dígitos", () => { + expect(formatCpf("12345")).toBe("123.45"); + }); + + it("formata parcialmente com 8 dígitos", () => { + expect(formatCpf("12345678")).toBe("123.456.78"); + }); + + it("retorna apenas dígitos para 3 ou menos dígitos", () => { + expect(formatCpf("123")).toBe("123"); + }); + + it("ignora dígitos além do 11º", () => { + expect(formatCpf("529982247259999")).toBe("529.982.247-25"); + }); +}); + +// ─── formatPhone ───────────────────────────────────────────────────────────── +describe("formatPhone", () => { + it("formata celular completo (11 dígitos)", () => { + expect(formatPhone("71988887777")).toBe("(71) 98888-7777"); + }); + + it("formata telefone fixo (10 dígitos)", () => { + expect(formatPhone("7132221111")).toBe("(71) 3222-1111"); + }); + + it("formata com 2 dígitos", () => { + expect(formatPhone("71")).toBe("(71"); + }); + + it("retorna string vazia para input vazio", () => { + expect(formatPhone("")).toBe(""); + }); +}); + +// ─── formatDateMask ─────────────────────────────────────────────────────────── +describe("formatDateMask", () => { + it("formata data completa", () => { + expect(formatDateMask("01011990")).toBe("01/01/1990"); + }); + + it("formata com 4 dígitos (dia e mês)", () => { + expect(formatDateMask("0101")).toBe("01/01"); + }); + + it("formata com 2 dígitos", () => { + expect(formatDateMask("01")).toBe("01"); + }); + + it("retorna vazio para input vazio", () => { + expect(formatDateMask("")).toBe(""); + }); +}); + +// ─── formatDateToBr / parseBrDateToIso ─────────────────────────────────────── +describe("formatDateToBr", () => { + it("converte ISO para formato BR", () => { + expect(formatDateToBr("1990-01-31")).toBe("31/01/1990"); + }); + + it("retorna string vazia para input vazio", () => { + expect(formatDateToBr("")).toBe(""); + }); + + it("não re-converte data já no formato BR", () => { + expect(formatDateToBr("31/01/1990")).toBe("31/01/1990"); + }); +}); + +describe("parseBrDateToIso", () => { + it("converte data BR para ISO", () => { + expect(parseBrDateToIso("31/01/1990")).toBe("1990-01-31"); + }); + + it("retorna string original sem '/'", () => { + expect(parseBrDateToIso("19900131")).toBe("19900131"); + }); + + it("retorna string vazia para data com ano incompleto", () => { + expect(parseBrDateToIso("31/01/90")).toBe(""); + }); + + it("retorna string vazia para input vazio", () => { + expect(parseBrDateToIso("")).toBe(""); + }); +}); + +// ─── validateCpfAlgorithm ───────────────────────────────────────────────────── +describe("validateCpfAlgorithm", () => { + // CPFs válidos conhecidos + const cpfsValidos = ["529.982.247-25", "52998224725", "111.444.777-35"]; + cpfsValidos.forEach((cpf) => { + it(`valida CPF correto: ${cpf}`, () => { + expect(validateCpfAlgorithm(cpf)).toBe(true); + }); + }); + + it("rejeita CPF com dígito verificador errado", () => { + expect(validateCpfAlgorithm("529.982.247-26")).toBe(false); + }); + + it("rejeita CPF com todos os dígitos iguais (111.111.111-11)", () => { + expect(validateCpfAlgorithm("111.111.111-11")).toBe(false); + }); + + it("rejeita CPF com menos de 11 dígitos", () => { + expect(validateCpfAlgorithm("1234567")).toBe(false); + }); + + it("rejeita string vazia", () => { + expect(validateCpfAlgorithm("")).toBe(false); + }); + + it("rejeita CPF com SQL injection", () => { + expect(validateCpfAlgorithm("' OR '1'='1")).toBe(false); + }); +}); + +// ─── validateBrDate ─────────────────────────────────────────────────────────── +describe("validateBrDate", () => { + it("valida data correta", () => { + expect(validateBrDate("31/12/2000")).toBe(true); + }); + + it("valida 29/02 em ano bissexto", () => { + expect(validateBrDate("29/02/2000")).toBe(true); + }); + + it("rejeita 29/02 em ano não-bissexto", () => { + expect(validateBrDate("29/02/2001")).toBe(false); + }); + + it("rejeita dia 31 em mês com 30 dias", () => { + expect(validateBrDate("31/04/2000")).toBe(false); + }); + + it("rejeita mês 13", () => { + expect(validateBrDate("01/13/2000")).toBe(false); + }); + + it("rejeita data no futuro", () => { + const future = `01/01/${new Date().getFullYear() + 1}`; + expect(validateBrDate(future)).toBe(false); + }); + + it("rejeita formato ISO", () => { + expect(validateBrDate("2000-12-31")).toBe(false); + }); + + it("rejeita string vazia", () => { + expect(validateBrDate("")).toBe(false); + }); + + it("rejeita null/undefined de forma segura", () => { + expect(validateBrDate(null)).toBe(false); + expect(validateBrDate(undefined)).toBe(false); + }); +}); + +// ─── formatCurrencyMask ─────────────────────────────────────────────────────── +describe("formatCurrencyMask", () => { + it("formata valor 1000 como '10,00'", () => { + expect(formatCurrencyMask("1000")).toBe("10,00"); + }); + + it("formata valor 100000 como '1.000,00'", () => { + expect(formatCurrencyMask("100000")).toBe("1.000,00"); + }); + + it("retorna vazio para input vazio", () => { + expect(formatCurrencyMask("")).toBe(""); + }); + + it("retorna '0,00' para string de zeros", () => { + expect(formatCurrencyMask("00")).toBe("0,00"); + }); +}); + +// ─── normalizeDecimalForSubmit ──────────────────────────────────────────────── +describe("normalizeDecimalForSubmit", () => { + it("converte '1.500,00' para '1500.00'", () => { + expect(normalizeDecimalForSubmit("1.500,00")).toBe("1500.00"); + }); + + it("converte '100,50' para '100.50'", () => { + expect(normalizeDecimalForSubmit("100,50")).toBe("100.50"); + }); + + it("retorna vazio para input vazio", () => { + expect(normalizeDecimalForSubmit("")).toBe(""); + }); + + it("retorna vazio para NaN", () => { + expect(normalizeDecimalForSubmit("abc")).toBe(""); + }); +}); + +// ─── numeroParaExtenso ──────────────────────────────────────────────────────── +describe("numeroParaExtenso", () => { + it("converte 1 para 'um real'", () => { + expect(numeroParaExtenso(1)).toBe("um real"); + }); + + it("converte 2 para 'dois reais'", () => { + expect(numeroParaExtenso(2)).toBe("dois reais"); + }); + + it("converte 1000 para string que contém 'mil' e 'reais'", () => { + const result = numeroParaExtenso(1000); + expect(result).toContain("mil"); + expect(result).toContain("reais"); + }); + + it("converte 1621 para string que contém 'mil'", () => { + expect(numeroParaExtenso(1621)).toContain("mil"); + }); + + it("converte 1.5 para string com 'centavo'", () => { + expect(numeroParaExtenso(1.5)).toContain("centavo"); + }); + + it("retorna vazio para null/undefined/vazio", () => { + expect(numeroParaExtenso(null)).toBe(""); + expect(numeroParaExtenso(undefined)).toBe(""); + expect(numeroParaExtenso("")).toBe(""); + }); + + it("retorna vazio para NaN", () => { + expect(numeroParaExtenso("abc")).toBe(""); + }); + + it("converte 0 para 'zero real'", () => { + expect(numeroParaExtenso(0)).toBe("zero real"); + }); + + it("converte 1.621,00 (string com máscara BR) corretamente", () => { + const result = numeroParaExtenso("1.621,00"); + expect(result).toContain("mil"); + expect(result).toContain("reais"); + }); +}); diff --git a/frontend/src/__tests__/submissionValidation.test.js b/frontend/src/__tests__/submissionValidation.test.js new file mode 100644 index 0000000..df778f9 --- /dev/null +++ b/frontend/src/__tests__/submissionValidation.test.js @@ -0,0 +1,293 @@ +/* global global */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { processSubmission } from "@/areas/servidor/services/submissionService.js"; + +// ─── Factory de formState mínimo válido ─────────────────────────────────────── +function makeFormState(overrides = {}) { + return { + assistidoEhIncapaz: "sim", + NOME: "João da Silva (Filho)", + cpf: "529.982.247-25", + REPRESENTANTE_NOME: "Maria da Silva", + representante_cpf: "111.444.777-35", + representante_data_nascimento: "15/03/1985", + requerente_endereco_residencial: "Rua das Flores, 123 - CEP 40000-000", + requerente_telefone: "(71) 99999-8888", + REQUERIDO_NOME: "Carlos Souza", + executado_endereco_residencial: "Rua Bela, 456", + executado_telefone: "(71) 98888-7777", + relato: "Desejo fixar alimentos para meu filho.", + acaoEspecifica: "fixacao_alimentos", + tipoAcao: "Família - Fixação", + valor_pensao: "30% do salário mínimo", + nascimento: "01/01/2015", + prefersAudio: false, + audioBlob: null, + documentFiles: [ + new File([""], "rg_mae.jpg", { type: "image/jpeg" }), + new File([""], "rg_filho.jpg", { type: "image/jpeg" }), + new File([""], "cert_nascimento.jpg", { type: "image/jpeg" }), + new File([""], "comp_residencia.jpg", { type: "image/jpeg" }), + new File([""], "comp_renda.jpg", { type: "image/jpeg" }), + new File([""], "cpf_mae.jpg", { type: "image/jpeg" }), + new File([""], "cpf_filho.jpg", { type: "image/jpeg" }), + ], + documentNames: {}, + documentosMarcados: ["RG", "CPF", "Certidão"], + outrosFilhos: [], + enviarDocumentosDepois: false, + calculo_arquivo: null, + ...overrides, + }; +} + +// ─── Mocks dos callbacks ────────────────────────────────────────────────────── +function makeMocks() { + return { + setFormErrors: vi.fn(), + setLoading: vi.fn(), + setStatusMessage: vi.fn(), + setGeneratedCredentials: vi.fn(), + toast: { error: vi.fn(), success: vi.fn() }, + configAcao: { + titulo: "Fixação de Pensão Alimentícia", + secoes: ["SecaoValoresPensao"], + exigeDadosProcessoOriginal: false, + ocultarRelato: false, + isAlvara: false, + }, + forcaRepresentacao: false, + today: "2024-01-01", + // Funções reais do formatters + stripNonDigits: (v) => v.replace(/\D/g, ""), + validateCpfAlgorithm: (cpf) => { + const clean = String(cpf).replace(/\D/g, ""); + if (clean.length !== 11 || /^(\d)\1+$/.test(clean)) return false; + let soma = 0; + for (let i = 1; i <= 9; i++) soma += parseInt(clean[i - 1]) * (11 - i); + let resto = (soma * 10) % 11; + if (resto === 10 || resto === 11) resto = 0; + if (resto !== parseInt(clean[9])) return false; + soma = 0; + for (let i = 1; i <= 10; i++) soma += parseInt(clean[i - 1]) * (12 - i); + resto = (soma * 10) % 11; + if (resto === 10 || resto === 11) resto = 0; + return resto === parseInt(clean[10]); + }, + formatDateToBr: (v) => { + if (!v || v.includes("/")) return v; + const [y, m, d] = v.split("-"); + return `${d}/${m}/${y}`; + }, + parseBrDateToIso: (v) => { + if (!v || !v.includes("/")) return v; + const [d, m, y] = v.split("/"); + if (!y || y.length < 4) return ""; + return `${y}-${m}-${d}`; + }, + normalizeDecimalForSubmit: (v) => { + if (!v) return ""; + const n = Number(String(v).replace(/\./g, "").replace(",", ".")); + return isNaN(n) ? "" : n.toFixed(2); + }, + API_BASE: "http://localhost:8000/api", + }; +} + +// ─── Validação — Campos Obrigatórios ───────────────────────────────────────── +describe("processSubmission — validação de campos obrigatórios", () => { + beforeEach(() => { + global.fetch = vi.fn(); + }); + + it("não chama fetch se REPRESENTANTE_NOME está vazio (adulto)", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + assistidoEhIncapaz: "nao", + REPRESENTANTE_NOME: "", + documentFiles: Array(4).fill(new File([""], "doc.jpg")), + }); + + await processSubmission({ ...mocks, formState }); + + expect(mocks.setFormErrors).toHaveBeenCalled(); + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("REPRESENTANTE_NOME"); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("não chama fetch se CPF da representante está vazio", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ representante_cpf: "" }); + + await processSubmission({ ...mocks, formState }); + + expect(mocks.setFormErrors).toHaveBeenCalled(); + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("representante_cpf"); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("não chama fetch se endereço não contém CEP válido", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + requerente_endereco_residencial: "Rua sem CEP", + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("requerente_endereco_residencial"); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("não chama fetch se telefone do requerente está vazio", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ requerente_telefone: "" }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("requerente_telefone"); + }); + + it("bloqueia CPF matematicamente inválido do representante", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ representante_cpf: "111.111.111-11" }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("representante_cpf"); + }); + + it("bloqueia data de nascimento no futuro", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + assistidoEhIncapaz: "nao", + representante_data_nascimento: "01/01/2099", + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("representante_data_nascimento"); + }); + + it("bloqueia data inválida (31/04 não existe)", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + assistidoEhIncapaz: "nao", + representante_data_nascimento: "31/04/1990", + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("representante_data_nascimento"); + }); + + it("bloqueia envio sem documentos suficientes (incapaz exige 7+)", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + documentFiles: [new File([""], "doc.jpg")], // apenas 1, precisa 7+ + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("documentos"); + }); + + it("bloqueia envio sem relato quando ocultarRelato é false", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ relato: "" }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("relato"); + }); + + it("chama toast.error quando há erros de validação", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ representante_cpf: "" }); + + await processSubmission({ ...mocks, formState }); + + expect(mocks.toast.error).toHaveBeenCalled(); + }); +}); + +// ─── Validação — CPF de Outro Filho ────────────────────────────────────────── +describe("processSubmission — validação de outros filhos", () => { + it("bloqueia se CPF de filho extra está ausente", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + outrosFilhos: [{ nome: "Filho 2", cpf: "", dataNascimento: "10/05/2010" }], + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("filho_cpf_0"); + }); + + it("bloqueia se CPF de filho extra é inválido", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + outrosFilhos: [{ nome: "Filho 2", cpf: "111.111.111-11", dataNascimento: "10/05/2010" }], + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("filho_cpf_0"); + }); + + it("bloqueia data de nascimento de filho no futuro", async () => { + const mocks = makeMocks(); + const formState = makeFormState({ + outrosFilhos: [{ nome: "Filho 2", cpf: "529.982.247-25", dataNascimento: "01/01/2099" }], + }); + + await processSubmission({ ...mocks, formState }); + + const errors = mocks.setFormErrors.mock.calls[0][0]; + expect(errors).toHaveProperty("filho_nascimento_0"); + }); +}); + +// ─── Chamada à API ──────────────────────────────────────────────────────────── +describe("processSubmission — chamada à API em caso válido", () => { + it("chama fetch para /casos/novo com dados válidos", async () => { + const mocks = makeMocks(); + // Mock fetch bem-sucedido + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ protocolo: "20260425011111", chaveAcesso: null }), + }); + + const formState = makeFormState(); + + await processSubmission({ ...mocks, formState }); + + // Se não há erros de validação, fetch deve ter sido chamado + expect(global.fetch).toHaveBeenCalled(); + const url = global.fetch.mock.calls[0][0]; + expect(url).toContain("/casos/novo"); + }); + + it("chama setLoading(false) no finally mesmo em caso de erro de rede", async () => { + const mocks = makeMocks(); + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + const formState = makeFormState(); + await processSubmission({ ...mocks, formState }); + + // setLoading deve ter sido chamado com false no finally + const loadingCalls = mocks.setLoading.mock.calls; + const hasCalledFalse = loadingCalls.some(([val]) => val === false); + expect(hasCalledFalse).toBe(true); + }); +}); diff --git a/frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx b/frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx new file mode 100644 index 0000000..6754c4e --- /dev/null +++ b/frontend/src/areas/defensor/components/casos/ModalDistribuicao.jsx @@ -0,0 +1,133 @@ +import React, { useState } from "react"; +import { authFetch } from "../../../../utils/apiBase"; +import { useToast } from "../../../../contexts/ToastContext"; +import { UserPlus, X, Search, Check } from "lucide-react"; + +export const ModalDistribuicao = ({ caso, isOpen, onClose, onRefresh }) => { + const [busca, setBusca] = useState(""); + const [submitting, setSubmitting] = useState(false); + const { addToast } = useToast(); + + const [defensores, setDefensores] = useState([]); + + React.useEffect(() => { + if (isOpen) { + authFetch("/defensores") + .then(r => r.json()) + .then(data => setDefensores(data)) + .catch(() => setDefensores([])); + } + }, [isOpen]); + + if (!isOpen) return null; + + const defensoresFiltrados = defensores.filter( + (d) => + d.nome.toLowerCase().includes(busca.toLowerCase()) || + d.email.toLowerCase().includes(busca.toLowerCase()), + ); + + const handleDistribuir = async (usuarioId) => { + setSubmitting(true); + try { + const response = await authFetch(`/casos/${caso.id}/distribuir`, { + method: "POST", + body: JSON.stringify({ usuario_id: usuarioId }), + }); + + if (response.ok) { + addToast("Caso distribuído com sucesso!", "success"); + onRefresh?.(); + onClose(); + } else { + const err = await response.json(); + addToast(err.message || "Erro ao distribuir.", "error"); + } + } catch { + addToast("Erro de conexão.", "error"); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+

+ Distribuir Atendimento +

+

+ Caso: {caso.nome_assistido} (#{caso.id}) +

+
+ +
+ + {/* Body */} +
+
+ + setBusca(e.target.value)} + autoFocus + /> +
+ +
+ {defensoresFiltrados.length === 0 ? ( +
Nenhum profissional encontrado.
+ ) : ( + defensoresFiltrados.map((d) => ( + + )) + )} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; diff --git a/frontend/src/areas/defensor/components/layout/Sidebar.jsx b/frontend/src/areas/defensor/components/layout/Sidebar.jsx index f6305f9..9207cf2 100644 --- a/frontend/src/areas/defensor/components/layout/Sidebar.jsx +++ b/frontend/src/areas/defensor/components/layout/Sidebar.jsx @@ -9,25 +9,23 @@ import { BarChart3, ChevronLeft, ChevronRight, - LogOut + LogOut, + Settings } from "lucide-react"; export const Sidebar = ({ isExpanded, setIsExpanded }) => { const auth = useAuth(); - const { logout, user } = auth || {}; + const { logout, permissions } = auth || {}; if (!auth) return null; - const userCargo = user?.cargo || "estagiario"; - const isAdmin = userCargo === "admin"; - const isJuridico = ["admin", "defensor", "estagiario", "visualizador"].includes(userCargo); - const navItems = [ - { icon: LayoutDashboard, label: "Dashboard", path: "/painel", show: isJuridico }, - { icon: FolderKanban, label: "Casos e Triagem", path: "/painel/casos", show: isJuridico }, - { icon: Archive, label: "Arquivo Morto", path: "/painel/casos/arquivados", show: isJuridico }, - { icon: BarChart3, label: "Relatorios", path: "/painel/relatorios", show: isAdmin }, - { icon: UserPlus, label: "Gerenciar Equipe", path: "/painel/equipe", show: isAdmin }, + { icon: LayoutDashboard, label: "Dashboard", path: "/painel", show: true }, + { icon: FolderKanban, label: "Casos e Triagem", path: "/painel/casos", show: true }, + { icon: Archive, label: "Arquivo Morto", path: "/painel/casos/arquivados", show: true }, + { icon: BarChart3, label: "Relatórios", path: "/painel/relatorios", show: permissions.canViewBi }, + { icon: UserPlus, label: "Gerenciar Equipe", path: "/painel/equipe", show: permissions.canManageTeam }, + { icon: Settings, label: "Configurações", path: "/painel/configuracoes", show: permissions.canEditConfig }, ].filter(item => item.show); const mobileLinkClass = ({ isActive }) => diff --git a/frontend/src/areas/defensor/contexts/AuthContext.jsx b/frontend/src/areas/defensor/contexts/AuthContext.jsx index 92becbd..a361e80 100644 --- a/frontend/src/areas/defensor/contexts/AuthContext.jsx +++ b/frontend/src/areas/defensor/contexts/AuthContext.jsx @@ -1,11 +1,11 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useState, useContext, useEffect, useRef, useCallback } from "react"; +import React, { createContext, useState, useContext, useEffect, useCallback, useMemo } from "react"; import { API_BASE, authFetch } from "../../../utils/apiBase"; import { jwtDecode } from "jwt-decode"; const AuthContext = createContext(); -export const AuthProvider = ({ children }) => { +export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [loading, setLoading] = useState(true); @@ -16,7 +16,6 @@ export const AuthProvider = ({ children }) => { if (!currentToken) return; try { const response = await authFetch("/casos/notificacoes"); - // Resposta 401 já é gerada dentro do authFetch disparando o logout if (response.ok) { const data = await response.json(); setNotificacoes(data); @@ -35,12 +34,10 @@ export const AuthProvider = ({ children }) => { if (storedToken && storedUser) { try { - // Validação prévia do token (expiração) const decoded = jwtDecode(storedToken); const agora = Date.now() / 1000; if (decoded.exp && decoded.exp < agora) { - console.warn("🔐 Token expirado detectado na inicialização. Limpando..."); localStorage.removeItem("defensorToken"); localStorage.removeItem("defensorUser"); } else { @@ -48,7 +45,6 @@ export const AuthProvider = ({ children }) => { setUser(JSON.parse(storedUser)); } } catch (error) { - console.error("Erro ao validar token/usuário no carregamento", error); localStorage.removeItem("defensorToken"); localStorage.removeItem("defensorUser"); } @@ -57,7 +53,6 @@ export const AuthProvider = ({ children }) => { }, []); const logout = useCallback(() => { - console.log("Deslogando usuário..."); setToken(null); setUser(null); localStorage.removeItem("defensorToken"); @@ -71,14 +66,10 @@ export const AuthProvider = ({ children }) => { // Escuta evento global de sessão expirada useEffect(() => { const handleExpired = () => { - console.warn("🔐 Sessão expirada detectada pelo context. Deslogando..."); logout(); }; - window.addEventListener("auth:session-expired", handleExpired); - return () => { - window.removeEventListener("auth:session-expired", handleExpired); - }; + return () => window.removeEventListener("auth:session-expired", handleExpired); }, [logout]); // Busca inicial de Notificações @@ -96,72 +87,55 @@ export const AuthProvider = ({ children }) => { body: JSON.stringify({ email, senha: password }), }); - // Se der erro de roteamento (ex: 404 no backend), não tenta parsear como JSON if (!response.ok) { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - const errorData = await response.json(); - throw new Error(errorData.error || "Falha na autenticação."); - } else { - // Erro genérico de servidor (HTML ou texto) - if (response.status === 404) { - throw new Error("Servidor não encontrou a rota de login. Verifique se o backend está rodando."); - } - throw new Error(`Erro no servidor (${response.status}). Tente novamente.`); - } + const errorData = await response.json().catch(() => ({ error: "Erro desconhecido" })); + throw new Error(errorData.error || "Falha na autenticação."); } const { token: receivedToken, defensor } = await response.json(); - setToken(receivedToken); setUser(defensor); - localStorage.setItem("defensorToken", receivedToken); localStorage.setItem("defensorUser", JSON.stringify(defensor)); - return true; } catch (error) { - console.error("Erro no login:", error); - // Tratamento especial para falha de conexão (fetch falha sem status) - if (error instanceof TypeError && error.message.includes("fetch")) { - throw new Error("Não foi possível conectar ao servidor. Verifique sua conexão ou se o backend está ligado."); - } throw error; } }; const marcarNotificacaoLida = async (id) => { try { - await authFetch(`/casos/notificacoes/${id}/lida`, { - method: "PATCH", - }); + await authFetch(`/casos/notificacoes/${id}/lida`, { method: "PATCH" }); setNotificacoes((prev) => prev.map((n) => (n.id === id ? { ...n, lida: true } : n)), ); - } catch (error) { - if (error.message !== "Sessão expirada") { - console.error("Erro ao marcar como lida", error); - } - } + } catch (error) {} }; + const permissions = { + canManageTeam: (user?.cargo || "").toLowerCase() === "admin", + canViewBi: ["admin", "gestor", "coordenador"].includes((user?.cargo || "").toLowerCase()), + canEditConfig: ["admin", "gestor"].includes((user?.cargo || "").toLowerCase()), + }; + + const contextValue = useMemo(() => ({ + user, + setUser, + token, + login, + logout, + loading, + notificacoes, + marcarNotificacaoLida, + fetchNotificacoes, + permissions, + }), [user, token, loading, notificacoes, logout, fetchNotificacoes, permissions]); + return ( - + {!loading && children} ); -}; +} export const useAuth = () => useContext(AuthContext); diff --git a/frontend/src/areas/defensor/hooks/useBiData.js b/frontend/src/areas/defensor/hooks/useBiData.js index d4546da..5c6fe7b 100644 --- a/frontend/src/areas/defensor/hooks/useBiData.js +++ b/frontend/src/areas/defensor/hooks/useBiData.js @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import html2canvas from "html2canvas"; -import jsPDF from "jspdf"; +import { jsPDF } from "jspdf"; import { API_BASE, authFetch } from "../../../utils/apiBase"; const PREFS_KEY = "bi_prefs_v1"; @@ -71,10 +71,31 @@ const sanitizePdfClone = (documentClone) => { }); }; +const DATA_CACHE_KEY = "bi_data_cache_v1"; +const FILTROS_CACHE_KEY = "bi_filtros_cache_v1"; + +const loadDataCache = () => { + try { + const stored = sessionStorage.getItem(DATA_CACHE_KEY); + return stored ? JSON.parse(stored) : null; + } catch { + return null; + } +}; + +const loadFiltrosCache = () => { + try { + const stored = sessionStorage.getItem(FILTROS_CACHE_KEY); + return stored ? JSON.parse(stored) : defaultFiltros; + } catch { + return defaultFiltros; + } +}; + export const useBiData = () => { - const [filtros, setFiltros] = useState(defaultFiltros); + const [filtros, setFiltros] = useState(loadFiltrosCache); const [prefs, setPrefs] = useState(loadPrefs); - const [data, setData] = useState(null); + const [data, setData] = useState(loadDataCache); const [loading, setLoading] = useState(false); const [exporting, setExporting] = useState(false); const [error, setError] = useState(""); @@ -83,6 +104,16 @@ export const useBiData = () => { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); }, [prefs]); + useEffect(() => { + sessionStorage.setItem(FILTROS_CACHE_KEY, JSON.stringify(filtros)); + }, [filtros]); + + useEffect(() => { + if (data) { + sessionStorage.setItem(DATA_CACHE_KEY, JSON.stringify(data)); + } + }, [data]); + const payload = () => ({ ...filtros, widgets: prefs.widgets, diff --git a/frontend/src/areas/defensor/pages/Cadastro.jsx b/frontend/src/areas/defensor/pages/Cadastro.jsx index cb50457..288f69b 100644 --- a/frontend/src/areas/defensor/pages/Cadastro.jsx +++ b/frontend/src/areas/defensor/pages/Cadastro.jsx @@ -89,7 +89,7 @@ export const Cadastro = () => {

Novo Membro

- Cadastre um novo defensor, estagiário ou recepcionista para + Cadastre um novo defensor, servidor, estagiário, coordenador ou gestor para acessar o painel.

@@ -166,9 +166,11 @@ export const Cadastro = () => { className="input" > + + + - diff --git a/frontend/src/areas/defensor/pages/Casos.jsx b/frontend/src/areas/defensor/pages/Casos.jsx index ccc8ada..7afefd5 100644 --- a/frontend/src/areas/defensor/pages/Casos.jsx +++ b/frontend/src/areas/defensor/pages/Casos.jsx @@ -3,8 +3,9 @@ import React, { useState } from "react"; import { useAuth } from "../contexts/AuthContext"; import { Link } from "react-router-dom"; -import { Eye, Search, Lock, User } from "lucide-react"; +import { Eye, Search, Lock, User, UserPlus, Clock } from "lucide-react"; import useSWR from "swr"; +import { ModalDistribuicao } from "../components/casos/ModalDistribuicao"; // 1. Trazemos seu authFetch de volta import { authFetch } from "../../../utils/apiBase"; @@ -42,7 +43,11 @@ const normalizeStatus = (value) => (value || "recebido").toLowerCase(); export const Casos = () => { const [busca, setBusca] = useState(""); const [statusFiltro, setStatusFiltro] = useState("todos"); - const { token, user } = useAuth(); + const [unidadeFiltro, setUnidadeFiltro] = useState("todas"); + const [selectedCaso, setSelectedCaso] = useState(null); + const { token, user, permissions } = useAuth(); + + const isAdminOrGestor = user && ["admin", "gestor"].includes(user.cargo.toLowerCase()); // 3. A mágica do SWR corrigida: // Passamos apenas a rota '/casos', pois o authFetch já completa a URL base internamente @@ -50,11 +55,19 @@ export const Casos = () => { data: casos, error, isLoading, + mutate, } = useSWR(token ? ['/casos', token] : null, ([url]) => fetcher(url), { revalidateOnFocus: false, dedupingInterval: 600000, }); + // Busca lista de unidades para o filtro (apenas admin/gestor) + const { data: unidades } = useSWR( + token && isAdminOrGestor ? ["/unidades", token] : null, + ([url]) => fetcher(url), + { dedupingInterval: 600000 } + ); + // Filtro de busca e status const casosFiltrados = (casos || []).filter((caso) => { const termo = busca.toLowerCase(); @@ -65,8 +78,9 @@ export const Casos = () => { (caso.numero_solar && String(caso.numero_solar).includes(termo)); const matchStatus = statusFiltro === "todos" || normalizeStatus(caso.status) === statusFiltro; + const matchUnidade = unidadeFiltro === "todas" || String(caso.unidade_id) === unidadeFiltro; - return matchBusca && matchStatus; + return matchBusca && matchStatus && matchUnidade; }); if (isLoading) { @@ -112,6 +126,22 @@ export const Casos = () => { {/* FILTROS */}
+ {/* SELECT DE UNIDADE (APENAS ADMIN/GESTOR) */} + {isAdminOrGestor && ( + + )} + {/* SELECT DE STATUS */} handleUpdateHorario(index, "dia", e.target.value)} + className="w-full bg-white border border-gray-200 rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" + > + {diasSemana.map(d => ( + + ))} + +
+ +
+
+ + handleUpdateHorario(index, "inicio", e.target.value)} + className="w-full bg-white border border-gray-200 rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" + /> +
+
+ + handleUpdateHorario(index, "fim", e.target.value)} + className="w-full bg-white border border-gray-200 rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" + /> +
+
+ + + + )) + )} + + + + {/* Seção Timezone */} +
+

+ Fuso Horário +

+ +

+ Utilizado para validar as janelas de horário e dias configurados acima. +

+
+ + {/* Botão Salvar */} +
+ +
+ + + ); +}; diff --git a/frontend/src/areas/defensor/pages/Dashboard.jsx b/frontend/src/areas/defensor/pages/Dashboard.jsx index 2c60ad0..f89e173 100644 --- a/frontend/src/areas/defensor/pages/Dashboard.jsx +++ b/frontend/src/areas/defensor/pages/Dashboard.jsx @@ -8,13 +8,13 @@ import { CheckCircle2, AlertTriangle, Bell, - Calendar, ChevronLeft, ChevronRight, - Lock, User, + Lock, Eye, Users, + Settings, } from "lucide-react"; import { authFetch } from "../../../utils/apiBase"; import useSWR from "swr"; @@ -66,7 +66,7 @@ const summaryFilterLabels = { }; export const Dashboard = () => { - const { token, user, notificacoes, marcarNotificacaoLida } = useAuth(); + const { token, user, notificacoes, marcarNotificacaoLida, permissions } = useAuth(); const navigate = useNavigate(); const [statusFilter, setStatusFilter] = useState(null); const [currentPage, setCurrentPage] = useState(1); @@ -165,17 +165,20 @@ export const Dashboard = () => { ); } + const isJuridico = ["defensor", "coordenador", "gestor"].includes(user?.cargo?.toLowerCase()); + const prefixo = isJuridico ? "Dr(a). " : ""; + return (
{/* COLUNA PRINCIPAL */}
-
+

- Olá, {user?.cargo === "defensor" ? "Dr(a). " : ""} + Olá, {prefixo} {user?.nome || "Usuário"}

@@ -191,6 +194,52 @@ export const Dashboard = () => {

+ {/* Atalhos Rápidos para Gestão e Relatórios */} +
+ {permissions.canManageTeam && ( + +
+ +
+
+

Gerenciar Equipe

+

Membros e unidades.

+
+ + )} + {permissions.canViewBi && ( + +
+ +
+
+

Relatórios BI

+

Indicadores e produtividade.

+
+ + )} + {permissions.canEditConfig && ( + +
+ +
+
+

Configurações

+

Horários e sistema.

+
+ + )} +
+
{[ { @@ -319,19 +368,26 @@ export const Dashboard = () => { const statusKey = normalizeStatus(caso.status); const badgeStyle = statusStyles[statusKey] || statusStyles.default; return ( - navigate(`/painel/casos/${caso.id}`)} - onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") navigate(`/painel/casos/${caso.id}`); }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + navigate(`/painel/casos/${caso.id}`); + }} className={`group cursor-pointer transition-colors hover:bg-primary/5 focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none ${caso.compartilhado ? "bg-purple-500/5" : ""}`} > {/* NOME / REPRESENTANTE */}
{caso.compartilhado && ( - + )}

@@ -355,27 +411,39 @@ export const Dashboard = () => { {caso.defensor || caso.servidor ? (

-
- { (caso.defensor_id === user?.id || caso.servidor_id === user?.id) ? : } +
+ {caso.defensor_id === user?.id || caso.servidor_id === user?.id ? ( + + ) : ( + + )}
- { (caso.defensor_id === user?.id || caso.servidor_id === user?.id) - ? "Meu" - : (caso.defensor?.nome || caso.servidor?.nome || "").split(" ")[0] } + {caso.defensor_id === user?.id || caso.servidor_id === user?.id + ? "Meu" + : (caso.defensor?.nome || caso.servidor?.nome || "").split( + " ", + )[0]}
) : ( - Disponível + + Disponível + )} {/* STATUS */} - + {statusKey.replace(/_/g, " ")} diff --git a/frontend/src/areas/defensor/pages/DetalhesCaso.jsx b/frontend/src/areas/defensor/pages/DetalhesCaso.jsx index b72480a..478b821 100644 --- a/frontend/src/areas/defensor/pages/DetalhesCaso.jsx +++ b/frontend/src/areas/defensor/pages/DetalhesCaso.jsx @@ -28,7 +28,9 @@ import { Eye, Paperclip, Search, + UserPlus, } from "lucide-react"; +import { ModalDistribuicao } from "../components/casos/ModalDistribuicao"; import { API_BASE } from "../../../utils/apiBase"; import { formatTipoAcaoLabel } from "../../../utils/caseUtils"; import { useToast } from "../../../contexts/ToastContext"; @@ -167,7 +169,7 @@ const CollapsibleText = ({ export const DetalhesCaso = () => { const { id } = useParams(); - const { token, user } = useAuth(); + const { token, user, permissions } = useAuth(); const navigate = useNavigate(); const { toast } = useToast(); const { confirm } = useConfirm(); @@ -206,6 +208,7 @@ export const DetalhesCaso = () => { const [buscaColega, setBuscaColega] = useState(""); const [autosType, setAutosType] = useState(null); // 'apartados' ou 'proprios_autos' const [autosSubtype, setAutosSubtype] = useState(null); // 'provisorio' ou 'definitivo' + const [isDistribuirOpen, setIsDistribuirOpen] = useState(false); // Novos campos financeiros para rito cumulado const [debitoPenhoraValor, setDebitoPenhoraValor] = useState(""); @@ -1988,6 +1991,16 @@ export const DetalhesCaso = () => {
)} + {permissions.canDistribuir && ( + + )} + {/* Botão de Arquivar (Movido para Gestão) */}
)} + + setIsDistribuirOpen(false)} + onRefresh={() => mutate()} + />
); }; diff --git a/frontend/src/areas/defensor/pages/GerenciarEquipe.jsx b/frontend/src/areas/defensor/pages/GerenciarEquipe.jsx index 5e03fc0..0406738 100644 --- a/frontend/src/areas/defensor/pages/GerenciarEquipe.jsx +++ b/frontend/src/areas/defensor/pages/GerenciarEquipe.jsx @@ -237,9 +237,11 @@ export const GerenciarEquipe = () => { const cargoBadge = (cargo) => { const map = { admin: "bg-red-100 text-red-800", + gestor: "bg-indigo-100 text-indigo-800", + coordenador: "bg-purple-100 text-purple-800", defensor: "bg-blue-100 text-blue-800", + servidor: "bg-highlight/15 text-highlight", estagiario: "bg-green-100 text-green-800", - visualizador: "bg-gray-100 text-gray-800", }; return map[cargo] || "bg-blue-100 text-blue-800"; }; @@ -468,7 +470,10 @@ export const GerenciarEquipe = () => {
diff --git a/frontend/src/areas/defensor/pages/Relatorios.jsx b/frontend/src/areas/defensor/pages/Relatorios.jsx index 546d1a9..ac64315 100644 --- a/frontend/src/areas/defensor/pages/Relatorios.jsx +++ b/frontend/src/areas/defensor/pages/Relatorios.jsx @@ -14,7 +14,7 @@ import { XAxis, YAxis, } from "recharts"; -import { BarChart3, Download, FileSpreadsheet, FileText, Filter, RefreshCw, Settings2 } from "lucide-react"; +import { BarChart3, Clock, Download, FileSpreadsheet, FileText, Filter, RefreshCw, Settings2, TrendingUp } from "lucide-react"; import { useAuth } from "../contexts/AuthContext"; import { useBiData } from "../hooks/useBiData"; import { authFetch } from "../../../utils/apiBase"; @@ -96,7 +96,7 @@ const CustomTooltip = ({ active, payload, label }) => { }; const Relatorios = () => { - const { user } = useAuth(); + const { user, permissions } = useAuth(); const { data, loading, @@ -114,15 +114,15 @@ const Relatorios = () => { const [unidades, setUnidades] = useState([]); const [showPrefs, setShowPrefs] = useState(false); - const isAdmin = user?.cargo === "admin"; + const canSeeAllUnidades = user?.cargo === "admin" || user?.cargo === "gestor"; useEffect(() => { - if (!isAdmin) return; + if (!canSeeAllUnidades) return; authFetch("/unidades") .then((response) => (response.ok ? response.json() : [])) .then((result) => setUnidades(result.filter((unidade) => unidade.ativo))) .catch(() => setUnidades([])); - }, [isAdmin]); + }, [canSeeAllUnidades]); const kpiCards = useMemo(() => { if (!data) return []; @@ -136,7 +136,22 @@ const Relatorios = () => { ]; }, [data]); - if (!isAdmin) { + if (data?.bloqueadoPorHorario) { + return ( +
+
+ +
+

Acesso Temporariamente Restrito

+

+ O módulo de BI está configurado para acesso apenas em horários específicos. + Por favor, tente novamente durante o horário de expediente definido nas configurações do sistema. +

+
+ ); + } + + if (!permissions?.canViewBi) { return ; } @@ -144,6 +159,14 @@ const Relatorios = () => { setFiltros((current) => ({ ...current, [key]: value })); }; + // Auto-filtro: Se ja tem dados, atualiza ao mudar filtros importantes + useEffect(() => { + if (data && !loading) { + gerar().catch(() => {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filtros.periodo, filtros.dataInicio, filtros.dataFim, filtros.unidade_id, filtros.topN]); + return (
@@ -342,6 +365,58 @@ const Relatorios = () => {
+ + {data.produtividade && ( +
+
+
+ +
+
+

Produtividade Individual

+

Ranking de atendimentos finalizados (Top 10)

+
+
+ +
+
+

Defensores

+
+ {data.produtividade.defensores.map((p, idx) => ( +
+
+ + {idx + 1} + + {p.nome} +
+ {p.total} +
+ ))} + {data.produtividade.defensores.length === 0 &&

Nenhum dado disponível.

} +
+
+ +
+

Servidores / Estagiários

+
+ {data.produtividade.servidores.map((p, idx) => ( +
+
+ + {idx + 1} + + {p.nome} +
+ {p.total} +
+ ))} + {data.produtividade.servidores.length === 0 &&

Nenhum dado disponível.

} +
+
+
+
+ )} )} diff --git a/frontend/src/areas/servidor/components/StepDadosPessoais.jsx b/frontend/src/areas/servidor/components/StepDadosPessoais.jsx index f24e6e2..aad884a 100644 --- a/frontend/src/areas/servidor/components/StepDadosPessoais.jsx +++ b/frontend/src/areas/servidor/components/StepDadosPessoais.jsx @@ -48,7 +48,6 @@ export const StepDadosPessoais = React.memo(({ forcaRepresentacao, isRepresentacao, labelAutor, - configAcao, handleRestrictedAlphanumeric, }) => { return ( diff --git a/frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx b/frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx index 81367b8..37fa5b2 100644 --- a/frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx +++ b/frontend/src/areas/servidor/components/secoes/SecaoProcessoOriginal.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React from "react"; export const SecaoProcessoOriginal = ({ @@ -248,14 +247,14 @@ export const SecaoProcessoOriginal = ({ htmlFor="link_calculadora" className="label font-bold flex items-center justify-between" > - Link da Calculadora (Opcional) + Link da Calculadora - Abrir Dr. Calc + Abrir Calculadora diff --git a/frontend/src/areas/servidor/services/submissionService.js b/frontend/src/areas/servidor/services/submissionService.js index 8088c3b..924c277 100644 --- a/frontend/src/areas/servidor/services/submissionService.js +++ b/frontend/src/areas/servidor/services/submissionService.js @@ -23,7 +23,6 @@ export const processSubmission = async ({ const nomeRequeridoTrim = (formState.REQUERIDO_NOME || "").trim(); const enderecoRequeridoTrim = (formState.executado_endereco_residencial || "").trim(); const telefoneRequeridoDigits = stripNonDigits(formState.executado_telefone || ""); - const requeridoEmailTrim = (formState.executado_email || "").trim(); if (!isAlvaraContext) { if (!nomeRequeridoTrim) { diff --git a/frontend/src/contexts/ToastContext.jsx b/frontend/src/contexts/ToastContext.jsx index 7c93e5c..260b7b7 100644 --- a/frontend/src/contexts/ToastContext.jsx +++ b/frontend/src/contexts/ToastContext.jsx @@ -35,7 +35,7 @@ export const ToastProvider = ({ children }) => { warning: (msg) => addToast(msg, "warning"), }), [addToast]); - const contextValue = React.useMemo(() => ({ toasts, removeToast, toast }), [toasts, removeToast, toast]); + const contextValue = React.useMemo(() => ({ toasts, removeToast, addToast, toast }), [toasts, removeToast, addToast, toast]); return ( diff --git a/frontend/src/utils/apiBase.js b/frontend/src/utils/apiBase.js index a39cfff..7547b6a 100644 --- a/frontend/src/utils/apiBase.js +++ b/frontend/src/utils/apiBase.js @@ -74,15 +74,16 @@ export const authFetch = async (endpoint, options = {}) => { if (typeof window !== "undefined") { let event; try { - event = new CustomEvent("auth:session-expired"); - } catch { + // Fallback seguro para navegadores que falham com 'new CustomEvent' event = document.createEvent("Event"); event.initEvent("auth:session-expired", true, true); + window.dispatchEvent(event); + } catch (e) { + console.error("Erro ao despachar evento de sessão expirada:", e); } - window.dispatchEvent(event); } } catch (e) { - console.warn("Falha crítica ao disparar evento de expiração:", e); + console.warn("Falha ao disparar evento de expiração:", e); } // 3. Lança erro padronizado diff --git a/frontend/vitest.config.js b/frontend/vitest.config.js new file mode 100644 index 0000000..51b713b --- /dev/null +++ b/frontend/vitest.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + test: { + environment: "jsdom", + globals: true, + include: ["src/**/__tests__/**/*.test.{js,jsx}"], + coverage: { + provider: "v8", + reporter: ["text", "lcov", "html"], + reportsDirectory: "./coverage", + include: ["src/utils/**", "src/areas/servidor/services/**"], + exclude: ["src/main.jsx", "src/App.jsx"], + thresholds: { + statements: 50, + branches: 40, + functions: 50, + lines: 50, + }, + }, + // Torna import.meta.env disponível nos testes + env: { + VITE_API_URL: "http://localhost:8000", + }, + }, +});