Feat ajuste acesso #5

Merged
killer-cf merged 4 commits from feat-ajuste-acesso into master 2025-10-30 17:02:20 +00:00
42 changed files with 1574 additions and 5848 deletions

View File

@@ -1,371 +0,0 @@
# ✅ COMO ASSOCIAR FUNCIONÁRIO A USUÁRIO
**Data:** 30 de outubro de 2025
**Objetivo:** Associar cadastro de funcionário a usuários para habilitar funcionalidades como férias
---
## 🎯 PROBLEMA RESOLVIDO
**ANTES:**
❌ "Perfil de funcionário não encontrado" ao tentar solicitar férias
❌ Usuários não tinham acesso a funcionalidades de RH
❌ Sem interface para fazer associação
**DEPOIS:**
✅ Interface completa em **TI > Gerenciar Usuários**
✅ Busca e seleção visual de funcionários
✅ Validação de duplicidade
✅ Opção de associar, alterar e desassociar
---
## 🚀 COMO USAR (PASSO A PASSO)
### 1⃣ Acesse o Gerenciamento de Usuários
```
1. Faça login como TI_MASTER
2. Menu lateral > Tecnologia da Informação
3. Click em "Gerenciar Usuários"
```
---
### 2⃣ Localize o Usuário
**Opção A: Busca Direta**
- Digite nome, matrícula ou email no campo de busca
**Opção B: Filtros**
- Filtre por status: Todos / Ativos / Bloqueados / Inativos
**Visual:**
```
┌─────────────────────────────────────────────────┐
│ Matrícula │ Nome │ Email │ Funcionário │ Status │
├───────────┼──────┼───────┼─────────────┼────────┤
│ 00001 │ TI │ ti@ │ ⚠️ Não │ ✅ │
│ │Master│gov.br │ associado │ Ativo │
└─────────────────────────────────────────────────┘
```
---
### 3⃣ Associar Funcionário
**Click no botão azul "Associar" ou "Alterar"**
Um modal abrirá com:
```
┌─────────────────────────────────────────────┐
│ Associar Funcionário ao Usuário │
├─────────────────────────────────────────────┤
│ Usuário: Gestor TI Master (00001) │
│ │
│ Buscar Funcionário: │
│ [Digite nome, CPF ou matrícula...] │
│ │
│ Selecione o Funcionário: │
│ ┌─────────────────────────────────────────┐ │
│ │ ○ João da Silva │ │
│ │ CPF: 123.456.789-00 │ │
│ │ Cargo: Analista │ │
│ ├─────────────────────────────────────────┤ │
│ │ ● Maria Santos (SELECIONADO) │ │
│ │ CPF: 987.654.321-00 │ │
│ │ Cargo: Gestor │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [Cancelar] [Desassociar] [Associar] │
└─────────────────────────────────────────────┘
```
---
### 4⃣ Buscar e Selecionar
1. **Busque o funcionário** (digite nome, CPF ou matrícula)
2. **Click no radio button** ao lado do funcionário correto
3. **Verifique os dados** (nome, CPF, cargo)
4. **Click em "Associar"**
---
### 5⃣ Confirmação
**Sucesso!** Você verá:
```
Alert: "Funcionário associado com sucesso!"
```
A coluna "Funcionário" agora mostrará:
```
✅ Associado (badge verde)
```
---
## 🧪 TESTAR O SISTEMA DE FÉRIAS
### Após associar o funcionário:
1. **Recarregue a página** (F5)
2. **Acesse seu Perfil:**
- Click no avatar (canto superior direito)
- "Meu Perfil"
3. **Vá para "Minhas Férias":**
- Agora deve mostrar o **Dashboard de Férias**
- Sem mais erro de "Perfil não encontrado"!
4. **Solicite Férias:**
- Click em "Solicitar Novas Férias"
- Siga o wizard de 3 passos
- Teste o calendário interativo
---
## 🔧 FUNCIONALIDADES DO MODAL
### ✅ Associar Novo Funcionário
- Busca em tempo real
- Ordenação alfabética
- Exibe nome, CPF, matrícula e cargo
### 🔄 Alterar Funcionário Associado
- Mesma interface
- Alert avisa se já tem associação
- Atualiza automaticamente
### ❌ Desassociar Funcionário
- Botão vermelho "Desassociar"
- Confirmação antes de executar
- Remove a associação
---
## 🛡️ VALIDAÇÕES E SEGURANÇA
### ✅ O Sistema Verifica:
1. **Funcionário existe?**
```
❌ Erro: "Funcionário não encontrado"
```
2. **Já está associado a outro usuário?**
```
❌ Erro: "Este funcionário já está associado ao usuário: João Silva (12345)"
```
3. **Funcionário selecionado?**
```
❌ Botão "Associar" fica desabilitado
```
---
## 🎨 INDICADORES VISUAIS
### Coluna "Funcionário"
**✅ Associado:**
```
🟢 Badge verde com ícone de check
```
**⚠️ Não Associado:**
```
🟡 Badge amarelo com ícone de alerta
```
### Botão de Ação
**🔵 Associar** (azul)
- Usuário sem funcionário
**🔵 Alterar** (azul)
- Usuário com funcionário já associado
---
## 📊 ESTATÍSTICAS
Você pode ver quantos usuários têm/não têm funcionários:
```
Cards no topo:
┌─────────┬─────────┬────────────┬──────────┐
│ Total │ Ativos │ Bloqueados │ Inativos │
│ 42 │ 38 │ 2 │ 2 │
└─────────┴─────────┴────────────┴──────────┘
```
---
## 🐛 TROUBLESHOOTING
### Problema: "Funcionário já está associado"
**Causa:** Funcionário está vinculado a outro usuário
**Solução:**
1. Identifique qual usuário tem o funcionário (mensagem de erro mostra)
2. Desassocie do usuário antigo primeiro
3. Associe ao usuário correto
---
### Problema: Lista de funcionários vazia
**Causa:** Nenhum funcionário cadastrado no sistema
**Solução:**
1. Vá em **Recursos Humanos > Gestão de Funcionários**
2. Click em "Cadastrar Funcionário"
3. Preencha os dados e salve
4. Volte para associar
---
### Problema: Busca não funciona
**Causa:** Nome/CPF/matrícula não confere
**Solução:**
1. Limpe o campo de busca
2. Veja lista completa
3. Procure visualmente
4. Click para selecionar
---
## 💡 DICAS PRO
### 1. Associação em Lote
Para associar vários usuários:
```
1. Filtre por "Não associado"
2. Associe um por vez
3. Use busca rápida de funcionários
```
### 2. Verificar Associações
```
Filtro de coluna "Funcionário":
- Badge verde = OK
- Badge amarelo = Pendente
```
### 3. Organização
```
Recomendação:
- Associe funcionários assim que criar usuários
- Mantenha dados sincronizados
- Revise periodicamente
```
---
## 🎯 CASO DE USO: SEU TESTE DE FÉRIAS
### Para o seu usuário TI Master:
1. **Acesse:** TI > Gerenciar Usuários
2. **Localize:** Seu usuário (ti.master@sgse.pe.gov.br)
3. **Click:** Botão azul "Associar"
4. **Busque:** Seu nome ou crie um funcionário de teste
5. **Selecione:** O funcionário correto
6. **Confirme:** Click em "Associar"
7. **Teste:** Perfil > Minhas Férias
✅ **Pronto!** Agora você pode testar todo o sistema de férias!
---
## 📝 CHECKLIST DE VERIFICAÇÃO
Após associar, verifique:
- [ ] Badge mudou de amarelo para verde
- [ ] Recarreguei a página
- [ ] Acessei meu perfil
- [ ] Abri aba "Minhas Férias"
- [ ] Dashboard carregou corretamente
- [ ] Não aparece mais erro
- [ ] Posso clicar em "Solicitar Férias"
- [ ] Wizard abre normalmente
---
## 🎉 RESULTADO ESPERADO
**Interface Completa:**
```
TI > Gerenciar Usuários
└── Tabela com coluna "Funcionário"
├── Badge: ✅ Associado / ⚠️ Não associado
└── Botão: [Associar] ou [Alterar]
└── Modal com:
├── Busca de funcionários
├── Lista com radio buttons
└── Botões: Cancelar | Desassociar | Associar
```
---
## 🔗 ARQUIVOS MODIFICADOS
### Frontend:
```
apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
├── + Coluna "Funcionário" na tabela
├── + Badge de status (Associado/Não associado)
├── + Botão "Associar/Alterar"
├── + Modal de seleção de funcionários
├── + Busca em tempo real
└── + Funções: associar/desassociar
```
### Backend:
```
packages/backend/convex/usuarios.ts
├── + associarFuncionario() mutation
├── + desassociarFuncionario() mutation
└── + Validação de duplicidade
```
---
## ✅ CONCLUSÃO
Agora você tem uma **interface completa e profissional** para:
✅ Associar funcionários a usuários
✅ Alterar associações
✅ Desassociar quando necessário
✅ Buscar e filtrar funcionários
✅ Validações automáticas
✅ Feedback visual claro
**RESULTADO:** Todos os usuários podem agora acessar funcionalidades que dependem de cadastro de funcionário, como **Gestão de Férias**! 🎉
---
**Desenvolvido por:** Equipe SGSE
**Data:** 30 de outubro de 2025
**Versão:** 1.0.0 - Associação de Funcionários

View File

@@ -1,256 +0,0 @@
# ✅ CORREÇÕES COMPLETAS - Emails e Notificações
**Data:** 30/10/2025
**Status:****TUDO FUNCIONANDO 100%**
---
## 🎯 PROBLEMAS IDENTIFICADOS E RESOLVIDOS
### 1. ❌ → ✅ **Sistema de Email NÃO estava funcionando**
#### **Problema:**
- O sistema apenas **simulava** o envio de emails
- Mensagem no código: `"⚠️ AVISO: Envio de email simulado (nodemailer não instalado)"`
- Emails nunca eram realmente enviados, mesmo com SMTP configurado
#### **Solução Aplicada:**
```
✅ Instalado: nodemailer + @types/nodemailer
✅ Implementado: Envio REAL de emails via SMTP
✅ Validação: Requer configuração SMTP testada antes de enviar
✅ Tratamento: Erros detalhados + retry automático
✅ Cron Job: Processa fila a cada 2 minutos automaticamente
```
#### **Arquivo Modificado:**
- `packages/backend/convex/email.ts`
- Linha 147-243: Implementação real com nodemailer
- Linha 248-284: Processamento da fila corrigido
#### **Cron Job Adicionado:**
- `packages/backend/convex/crons.ts`
- Nova linha 36-42: Processa fila de emails a cada 2 minutos
---
### 2. ❌ → ✅ **Página de Notificações NÃO enviava nada**
#### **Problema:**
- Função `enviarNotificacao()` tinha `// TODO: Implementar envio`
- Apenas exibia `console.log` e alert de sucesso falso
- Nenhuma notificação era realmente enviada
#### **Solução Aplicada:**
```
✅ Implementado: Envio real para CHAT
✅ Implementado: Envio real para EMAIL
✅ Suporte: Envio combinado (AMBOS canais)
✅ Feedback: Mensagens específicas por canal
✅ Validações: Email obrigatório para envio por email
```
#### **Arquivo Modificado:**
- `apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte`
- Linha 20-130: Implementação completa do envio real
#### **Funcionalidades:**
- **Chat:** Cria conversa individual + envia mensagem
- **Email:** Enfileira email (processado pelo cron)
- **Ambos:** Envia pelos dois canais simultaneamente
- **Templates:** Suporte completo a templates de mensagem
---
### 3. ✅ **Warnings de Acessibilidade Corrigidos**
#### **Problemas Encontrados:**
- Botões sem `aria-label` (4 botões)
- Elementos não-interativos com eventos (form, ul)
- Labels sem controles associados (1 ocorrência)
#### **Arquivos Corrigidos:**
**1. `apps/web/src/lib/components/Sidebar.svelte`**
- Linha 232: Adicionado `svelte-ignore` para `<ul tabindex="0">`
- Linha 473-475: Adicionado `svelte-ignore` para `<form>` com onclick
**2. `apps/web/src/lib/components/FileUpload.svelte`**
- Linha 268: Trocado `<label>` por `<div>` (texto de erro)
**3. `apps/web/src/routes/(dashboard)/ti/perfis/+page.svelte`**
- Linha 414: Botão "Ver Detalhes" + `aria-label`
- Linha 443: Botão "Editar" + `aria-label`
- Linha 466: Botão "Clonar" + `aria-label`
- Linha 489: Botão "Excluir" + `aria-label`
- Linha 932-935: Botões com `type="button"`
---
## 📋 COMO TESTAR
### **1. Testar Envio de Email**
#### **Passo 1: Configurar SMTP** (se ainda não fez)
1. Vá em: `TI > Configurações de Email`
2. Preencha:
```
Servidor SMTP: smtp.gmail.com (ou seu servidor)
Porta: 587 (TLS) ou 465 (SSL)
Usuário: seu-email@gmail.com
Senha: sua-senha-app (Gmail requer senha de app)
```
3. Clique em **"Testar Conexão SMTP"**
4. Aguarde mensagem: ✅ "Conexão testada com sucesso!"
#### **Passo 2: Enviar Notificação**
1. Vá em: `TI > Notificações`
2. Selecione:
- **Destinatário:** Qualquer usuário
- **Canal:** Email (ou Ambos)
- **Template:** Escolha um template ou escreva mensagem
3. Clique em **"Enviar"**
4. Aguarde: ✅ "Email enfileirado para envio!"
#### **Passo 3: Verificar Envio**
- **Método 1:** Aguarde 2 minutos (cron processa automaticamente)
- **Método 2:** Verifique logs do Convex no terminal
**Resultado Esperado:**
```
✅ Email enviado com sucesso!
Para: destinatario@email.com
Assunto: [Assunto do email]
Message ID: <123abc@...>
```
---
### **2. Testar Envio de Chat**
1. Vá em: `TI > Notificações`
2. Selecione:
- **Destinatário:** Qualquer usuário online
- **Canal:** Chat
- **Mensagem:** Digite algo
3. Clique em **"Enviar"**
4. Abra o Chat (ícone no canto superior direito)
5. Verifique: A mensagem deve aparecer na conversa
---
## 🎯 FUNCIONALIDADES IMPLEMENTADAS
### **Sistema de Email:**
- ✅ Envio real via SMTP (nodemailer)
- ✅ Fila de emails pendentes
- ✅ Processamento automático (cron a cada 2 min)
- ✅ Retry automático (até 3 tentativas)
- ✅ Status detalhado (pendente, enviando, enviado, falha)
- ✅ Logs de erro detalhados
- ✅ Validação de configuração SMTP testada
### **Sistema de Notificações:**
- ✅ Envio para Chat (mensagem imediata)
- ✅ Envio para Email (enfileirado)
- ✅ Envio Combinado (Chat + Email)
- ✅ Suporte a Templates
- ✅ Mensagem Personalizada
- ✅ Feedback específico por canal
### **Acessibilidade:**
- ✅ Todos os botões com `aria-label`
- ✅ Botões com `type="button"` explícito
- ✅ Warnings do Svelte suprimidos apropriadamente
- ✅ Labels sem controles corrigidas
---
## 📦 DEPENDÊNCIAS INSTALADAS
```bash
✅ nodemailer@7.0.10
✅ @types/nodemailer@7.0.3
```
---
## 🔧 ARQUIVOS MODIFICADOS
### **Backend:**
1. ✅ `packages/backend/convex/email.ts` (implementação real)
2. ✅ `packages/backend/convex/crons.ts` (cron job adicionado)
### **Frontend:**
3. ✅ `apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte` (envio real)
4. ✅ `apps/web/src/lib/components/Sidebar.svelte` (acessibilidade)
5. ✅ `apps/web/src/lib/components/FileUpload.svelte` (acessibilidade)
6. ✅ `apps/web/src/routes/(dashboard)/ti/perfis/+page.svelte` (acessibilidade)
---
## ⚠️ IMPORTANTE: CONFIGURAÇÃO SMTP
### **Gmail:**
```
Servidor: smtp.gmail.com
Porta: 587 (TLS)
Usuário: seu-email@gmail.com
Senha: [Senha de App - não a senha normal]
```
**Como gerar Senha de App no Gmail:**
1. Vá em: https://myaccount.google.com/security
2. Ative a **"Verificação em duas etapas"**
3. Acesse: **"Senhas de app"**
4. Gere uma senha para "Email" ou "Outro"
5. Use essa senha de 16 dígitos
### **Outros Provedores:**
- **Outlook/Hotmail:** smtp-mail.outlook.com (porta 587)
- **Yahoo:** smtp.mail.yahoo.com (porta 587)
- **SMTP Corporativo:** Verifique com sua equipe de TI
---
## 🚀 PRÓXIMOS PASSOS
### **1. Configure o SMTP** (se ainda não fez)
- Vá em: `TI > Configurações de Email`
- Preencha os dados do servidor
- **TESTE A CONEXÃO** (botão "Testar Conexão SMTP")
### **2. Teste o Envio**
- Vá em: `TI > Notificações`
- Envie uma notificação de teste para você mesmo
### **3. Monitore os Logs**
- Observe o terminal do Convex
- Logs mostrarão: `✅ Email enviado com sucesso!` ou erros
---
## 📊 STATUS FINAL
```
✅ Sistema de Email: 100% Funcional
✅ Sistema de Notificações: 100% Funcional
✅ Envio para Chat: 100% Funcional
✅ Warnings Corrigidos: 100% Completo
✅ Cron Job: Ativo (processa a cada 2 min)
✅ Acessibilidade: Conforme padrões WCAG
```
---
## 🎉 **TUDO PRONTO E FUNCIONANDO!**
**Agora você pode:**
- ✅ Enviar emails REAIS via SMTP
- ✅ Enviar notificações pelo Chat
- ✅ Enviar por ambos os canais
- ✅ Usar templates de mensagem
- ✅ Sistema processa automaticamente
**Sem mais warnings de acessibilidade!** 🚀

View File

@@ -1,147 +0,0 @@
# 🧪 Guia: Criar Usuário de Teste para Férias
## 📋 Credenciais de Teste
```
Login: teste.ferias
Senha: Teste@2025
Email: teste.ferias@sgse.pe.gov.br
Nome: João Silva (Teste)
```
---
## 🔧 Passo a Passo
### **1. Criar um Símbolo (se não existir)**
1. Acesse: `http://localhost:5173/recursos-humanos/simbolos`
2. Clique em **"Novo Símbolo"**
3. Preencha:
- **Cargo:** Analista Administrativo
- **Tipo:** Cargo Comissionado
- **Nível:** CC-3
- **Valor:** R$ 3.500,00
4. Clique em **"Salvar"**
---
### **2. Criar Funcionário**
1. Acesse: `http://localhost:5173/recursos-humanos/funcionarios/cadastro`
2. Preencha os dados:
#### **Dados Pessoais:**
- **Nome Completo:** João Silva (Teste)
- **CPF:** 111.222.333-44
- **RG:** 1234567
- **Data de Nascimento:** 15/05/1990
#### **Contato:**
- **Email:** teste.ferias@sgse.pe.gov.br
- **Telefone:** (81) 98765-4321
- **Endereço:** Rua de Teste, 123
- **Bairro:** Centro
- **Cidade:** Recife
- **UF:** PE
- **CEP:** 50000-000
#### **Dados Funcionais:**
- **Matrícula:** teste.ferias
- **Data de Admissão:** 15/01/2023 ⚠️ **IMPORTANTE: Quase 2 anos atrás!**
- **Símbolo:** Selecione o símbolo criado acima
- **Regime de Trabalho:** CLT
- **Cargo/Função:** Analista Administrativo
- **Status de Férias:** Ativo
#### **Filiação:**
- **Nome do Pai:** José Silva
- **Nome da Mãe:** Maria Silva
#### **Outros:**
- **Naturalidade:** Recife/PE
- **Sexo:** Masculino
- **Estado Civil:** Solteiro
- **Nacionalidade:** Brasileira
- **Grau de Instrução:** Superior
3. Clique em **"Salvar"**
---
### **3. Criar Usuário e Associar**
1. Acesse: `http://localhost:5173/ti/usuarios`
2. Clique em **"Novo Usuário"**
3. Preencha:
- **Matrícula:** teste.ferias
- **Nome:** João Silva (Teste)
- **Email:** teste.ferias@sgse.pe.gov.br
- **Perfil/Role:** Usuario (perfil básico)
- **Senha Inicial:** Teste@2025
4. Clique em **"Criar"**
5. **Associar Funcionário:**
- Na lista de usuários, localize "João Silva (Teste)"
- Clique no botão **"Associar/Alterar"** (ao lado de "Não associado")
- Selecione o funcionário "João Silva (Teste)" criado anteriormente
- Clique em **"Associar"**
---
## ✅ Testar o Sistema de Férias
1. **Faça Logout** do usuário TI Master
2. **Faça Login** com:
```
Login: teste.ferias
Senha: Teste@2025
```
3. Acesse: `http://localhost:5173/perfil`
4. Clique na aba **"Minhas Férias"**
5. Clique em **"Solicitar Novas Férias"**
---
## 🎯 O Que Testar
### **Saldo Esperado:**
- **Ano 2024:** ~30 dias (ano completo)
- **Ano 2025:** ~30 dias (proporcionais até dez/2025)
### **Validações CLT:**
- ✅ Máximo 3 períodos por ano
- ✅ Mínimo 5 dias por período
- ✅ Um período deve ter pelo menos 14 dias
- ✅ Não pode usar mais dias que o saldo disponível
### **Teste:**
1. Selecione o ano (2024 ou 2025)
2. Adicione períodos no calendário
3. Verifique se as validações aparecem
4. Envie a solicitação
5. Como TI Master, aprove/reprove a solicitação
---
## 🔧 Dicas de Teste
### **Testar Servidor Público PE:**
Se quiser testar as regras de Servidor Público PE:
1. Edite o funcionário
2. Altere **"Regime de Trabalho"** para **"Servidor Público Estadual PE"**
3. As regras mudam para:
- ✅ Máximo 2 períodos
- ✅ Mínimo 10 dias por período
- ✅ Não permite abono
### **Testar Diferentes Anos de Admissão:**
- Data mais antiga = mais períodos aquisitivos
- Data recente = menos dias disponíveis
---
## 🎉 Pronto!
Agora você pode testar todo o sistema de férias com um usuário real! 🚀

View File

@@ -1,110 +0,0 @@
# 🚀 GUIA RÁPIDO: Enviar Emails e Notificações
## ⚡ 3 Passos para Começar
### 1⃣ **Configurar SMTP** (Fazer 1 vez)
1. Acesse: `http://localhost:5173/ti/configuracoes-email`
2. Preencha:
```
📧 Remetente: SGSE - Sistema de Gerenciamento
📧 Email: sgse@pe.gov.br (ou seu email)
🌐 Servidor: smtp.gmail.com
🔌 Porta: 587
🔐 Usuário: seu-email@gmail.com
🔑 Senha: sua-senha-de-app
🔒 TLS/SSL: Sim
```
3. Clique: **"Testar Conexão SMTP"**
4. Aguarde: ✅ "Conexão testada com sucesso!"
### 2⃣ **Enviar Notificação**
1. Acesse: `http://localhost:5173/ti/notificacoes`
2. Selecione:
- **Destinatário:** João Silva (ou qualquer usuário)
- **Canal:**
- 💬 Chat = Mensagem imediata
- 📧 Email = Envio em até 2 minutos
- 🔄 Ambos = Chat + Email
- **Mensagem:** Escolha template ou escreva
3. Clique: **"Enviar"**
### 3⃣ **Verificar Envio**
#### **Chat:**
- ✅ Imediato: Abra o chat e veja a mensagem
#### **Email:**
- ⏱️ Aguarde 2 minutos (processamento automático)
- 📋 Verifique logs no terminal do Convex:
```
✅ Email enviado com sucesso!
Para: destinatario@email.com
```
---
## 🔑 **IMPORTANTE: Senha de App do Gmail**
O Gmail **NÃO aceita** senha normal!
### **Como gerar:**
1. Acesse: https://myaccount.google.com/security
2. Ative: **"Verificação em duas etapas"**
3. Vá em: **"Senhas de app"**
4. Gere: Senha para "Email"
5. Use: Senha de 16 caracteres gerada
---
## ✅ **Canais Disponíveis**
| Canal | Velocidade | Ideal Para |
|-------|------------|------------|
| 💬 **Chat** | Imediato | Mensagens urgentes |
| 📧 **Email** | 2 minutos | Notificações formais |
| 🔄 **Ambos** | Variado | Comunicações importantes |
---
## 🧪 **Teste Rápido**
```
1. Configure SMTP (Gmail)
2. Envie notificação para você mesmo
3. Canal: Ambos
4. Verifique:
✅ Chat: Mensagem aparece imediatamente
✅ Email: Chega em até 2 minutos
```
---
## ❓ **Troubleshooting**
### **Email não chega?**
1. ✅ Configuração SMTP testada?
2. ✅ Senha de App (não senha normal)?
3. ✅ Aguardou 2 minutos?
4. ✅ Verifique spam/lixo eletrônico
### **Chat não funciona?**
1. ✅ Destinatário tem acesso ao chat?
2. ✅ Usuário está cadastrado?
### **Erro "Configuração não testada"?**
1. ✅ Clique em "Testar Conexão SMTP"
2. ✅ Aguarde mensagem de sucesso
3. ✅ Tente enviar novamente
---
## 📄 **Documentação Completa**
Veja: `CORRECOES_EMAILS_NOTIFICACOES_COMPLETO.md`
---
**✅ PRONTO PARA USO!** 🎉

View File

@@ -1,183 +0,0 @@
# Interface de Criação e Edição de Perfis Customizados - CONCLUÍDA ✅
## 📋 Resumo da Implementação
A interface completa para gerenciar perfis customizados foi implementada com sucesso em:
**`apps/web/src/routes/(dashboard)/ti/perfis/+page.svelte`**
## 🎯 Funcionalidades Implementadas
### 1. **Listagem de Perfis** 📊
- Visualização em tabela com:
- Nome e descrição
- Nível de acesso
- Número de usuários usando o perfil
- Criador e data de criação
- Ações disponíveis
### 2. **Criação de Novos Perfis**
- Formulário completo com:
- Nome do perfil (obrigatório)
- Descrição detalhada (obrigatório)
- Nível de acesso (mínimo 3 para perfis customizados)
- Opção para clonar permissões de perfil existente
- Validações:
- Campos obrigatórios
- Nível mínimo
- Autenticação do usuário
- Verificação de duplicidade (no backend)
### 3. **Edição de Perfis** ✏️
- Atualização de:
- Nome do perfil
- Descrição
- Informação sobre nível (não editável após criação)
- Validações de segurança
### 4. **Visualização Detalhada** 👁️
- Informações completas do perfil:
- Dados básicos
- Permissões de menu configuradas
- Lista de usuários com este perfil
- Links para:
- Editar permissões no Painel de Permissões
- Gerenciar usuários
### 5. **Clonagem de Perfis** 📋
- Criação rápida de novo perfil baseado em existente
- Copia todas as permissões automaticamente
- Prompt interativo para nome e descrição
### 6. **Exclusão de Perfis** 🗑️
- Verificação de uso (não permite excluir se houver usuários)
- Confirmação antes de excluir
- Remoção em cascata de:
- Role correspondente
- Permissões associadas
- Permissões de menu
## 🔧 Integrações Backend
A interface utiliza as seguintes funções do backend:
### Queries
- `api.perfisCustomizados.listarPerfisCustomizados` - Lista todos os perfis
- `api.perfisCustomizados.obterPerfilComPermissoes` - Detalhes completos
- `api.roles.listar` - Lista roles para clonagem
### Mutations
- `api.perfisCustomizados.criarPerfilCustomizado` - Cria novo perfil
- `api.perfisCustomizados.editarPerfilCustomizado` - Atualiza perfil
- `api.perfisCustomizados.excluirPerfilCustomizado` - Remove perfil
- `api.perfisCustomizados.clonarPerfil` - Clona perfil existente
## 🎨 UI/UX Features
### Design
- Layout responsivo (mobile-friendly)
- Cards e modais para diferentes modos
- Ícones SVG intuitivos
- Badges para status e informações
### Feedback ao Usuário
- Mensagens de sucesso/erro/aviso
- Estados de carregamento
- Confirmações para ações destrutivas
- Desabilitação de botões durante processamento
### Navegação
- Botão "Voltar" sempre visível fora do modo listagem
- Breadcrumbs implícitos
- Links contextuais
## 🔐 Segurança
### Controle de Acesso
- Uso do `ProtectedRoute` para TI_MASTER e ADMIN
- Verificação de autenticação antes de cada ação
- Uso do `authStore.usuario._id` para identificação
### Validações
- Frontend: Campos obrigatórios e regras de negócio
- Backend: Validações adicionais e controle de integridade
- Type-safe com TypeScript
## 📱 Responsividade
- Grid adaptável: 1 coluna (mobile) → 2 colunas (desktop)
- Tabelas com scroll horizontal em telas pequenas
- Botões e formulários otimizados para touch
## 🎯 Próximos Passos (Opcionais)
1. **Melhorias de UX:**
- Modal para criação/edição ao invés de troca de modo
- Drag-and-drop para reordenar permissões
- Busca e filtros na listagem
2. **Features Avançadas:**
- Histórico de alterações do perfil
- Exportar/importar configurações de perfis
- Preview das permissões antes de salvar
3. **Relatórios:**
- Matriz de acesso por perfil
- Comparativo entre perfis
- Auditoria de uso
## 📝 Como Usar
### Para Acessar:
1. Faça login como TI_MASTER ou ADMIN
2. Navegue para: **Dashboard TI → Gerenciar Perfis**
3. Ou acesse diretamente: `/ti/perfis`
### Para Criar um Perfil:
1. Clique em "Novo Perfil"
2. Preencha nome, descrição e nível
3. (Opcional) Selecione um perfil para clonar permissões
4. Clique em "Criar Perfil"
### Para Editar:
1. Na listagem, clique no ícone de editar (lápis)
2. Altere os campos desejados
3. Clique em "Salvar Alterações"
### Para Configurar Permissões:
1. Clique em "Ver Detalhes" (ícone de olho)
2. Na seção de permissões, clique em "Editar Permissões"
3. Será redirecionado para o Painel de Permissões
### Para Clonar:
1. Clique no ícone de clonar (dois quadrados)
2. Digite o nome do novo perfil
3. Digite a descrição
4. O perfil será criado com as mesmas permissões
### Para Excluir:
1. Clique no ícone de excluir (lixeira)
2. Confirme a ação
3. **Nota:** Só é possível excluir perfis sem usuários
## ✅ Status
- ✅ Backend completo e testado
- ✅ Interface frontend implementada
- ✅ Integração frontend-backend
- ✅ Validações e segurança
- ✅ Tratamento de erros
- ✅ UI/UX responsiva
- ✅ Sem erros de linting
- ✅ TypeScript type-safe
## 🎉 Conclusão
A interface de criação e edição de perfis customizados está **100% funcional e pronta para uso**. A implementação segue as melhores práticas de:
- Clean Code
- Segurança
- Usabilidade
- Manutenibilidade
O sistema permite que administradores TI criem perfis de acesso personalizados de forma intuitiva e segura, com controle total sobre permissões e usuários.

View File

@@ -1,350 +0,0 @@
# 📋 REGRAS DE FÉRIAS - CLT vs SERVIDOR PÚBLICO ESTADUAL DE PERNAMBUCO
**Data:** 30 de outubro de 2025
**Status:****IMPLEMENTADO NO SISTEMA**
---
## 🎯 VISÃO GERAL
O sistema SGSE agora suporta **2 regimes de trabalho** com regras específicas de férias:
1. **CLT** - Consolidação das Leis do Trabalho
2. **Servidor Público Estadual de Pernambuco** - Lei nº 6.123/1968
---
## ⚖️ CLT - CONSOLIDAÇÃO DAS LEIS DO TRABALHO
### **Legislação:**
- Art. 129 a 153 da CLT (Decreto-Lei nº 5.452/1943)
### **Regras Básicas:**
| Item | Regra |
|------|-------|
| **Dias de Férias** | 30 dias por ano trabalhado |
| **Período Aquisitivo** | 12 meses de trabalho |
| **Período Concessivo** | 12 meses após o período aquisitivo |
| **Divisão em Períodos** | Até **3 períodos** |
| **Período Principal** | Mínimo **14 dias corridos** |
| **Períodos Secundários** | Mínimo **5 dias corridos** cada |
| **Abono Pecuniário** | ✅ Permitido vender 1/3 (10 dias) |
| **Idade Especial** | < 18 anos ou > 50 anos: férias em 1 período único |
| **Vencimento** | Férias não gozadas perdem-se após período concessivo |
### **Validações no Sistema (CLT):**
```typescript
Máximo 3 períodos
Período principal: mínimo 14 dias
Períodos secundários: mínimo 5 dias
Total não pode exceder saldo disponível
Períodos não podem sobrepor
Abono pecuniário: até 10 dias
```
### **Exemplo Prático (CLT):**
**Funcionário:** João Silva (CLT)
**Admissão:** 01/01/2024
**Período Aquisitivo:** 01/01/2024 a 31/12/2024
**Período Concessivo:** 01/01/2025 a 31/12/2025
**Solicitação Válida:**
```
Período 1: 14 dias (Principal)
Período 2: 10 dias (Secundário)
Período 3: 6 dias (Secundário)
Total: 30 dias ✅
```
**Solicitação Inválida:**
```
Período 1: 10 dias ❌ (Falta período de 14 dias)
Período 2: 10 dias
Período 3: 10 dias
```
---
## 🏛️ SERVIDOR PÚBLICO ESTADUAL DE PERNAMBUCO
### **Legislação:**
- Lei nº 6.123/1968 - Estatuto dos Funcionários Públicos Civis do Estado de PE
- Art. 84 a 90
### **Regras Básicas:**
| Item | Regra |
|------|-------|
| **Dias de Férias** | 30 dias por ano de exercício |
| **Período Aquisitivo** | 12 meses de exercício |
| **Período Concessivo** | Ano subsequente ao aquisitivo |
| **Divisão em Períodos** | Até **2 períodos** (NÃO 3!) |
| **Dias Mínimos por Período** | **10 dias corridos** (NÃO 5!) |
| **Abono Pecuniário** | ❌ **NÃO PERMITIDO** |
| **Servidor > 10 anos** | Pode acumular até 2 períodos |
| **Docentes** | Preferência: 20/12 a 10/01 |
| **Gestante** | Pode antecipar ou prorrogar |
| **Vencimento** | Mais flexível que CLT |
### **Validações no Sistema (Servidor PE):**
```typescript
Máximo 2 períodos (NÃO 3)
Cada período: mínimo 10 dias (NÃO 5)
Total não pode exceder saldo disponível
Períodos não podem sobrepor
Abono pecuniário: NÃO PERMITIDO
📅 Aviso para docentes: período 20/12 a 10/01
```
### **Exemplo Prático (Servidor PE):**
**Funcionário:** Maria Santos (Servidor PE)
**Posse:** 01/03/2024
**Período Aquisitivo:** 01/03/2024 a 28/02/2025
**Período Concessivo:** 01/03/2025 a 28/02/2026
**Solicitação Válida:**
```
Período 1: 20 dias
Período 2: 10 dias
Total: 30 dias ✅
```
**Solicitação Inválida:**
```
Período 1: 10 dias
Período 2: 10 dias
Período 3: 10 dias ❌ (Máximo 2 períodos)
```
**Solicitação Inválida 2:**
```
Período 1: 20 dias
Período 2: 5 dias ❌ (Mínimo 10 dias por período)
```
---
## 📊 COMPARAÇÃO DIRETA
| Critério | CLT | Servidor Público PE |
|----------|-----|---------------------|
| **Dias Anuais** | 30 dias | 30 dias |
| **Max Períodos** | 3 | 2 |
| **Min Dias/Período** | 5 dias | 10 dias |
| **Período Principal** | 14 dias (obrigatório) | Não há essa regra |
| **Abono Pecuniário** | ✅ Sim (10 dias) | ❌ Não |
| **Acúmulo** | ❌ Não | ✅ Sim (> 10 anos) |
| **Vencimento** | Rígido | Flexível |
| **Preferência Docente** | Não há | 20/12 a 10/01 |
---
## 🎯 COMO O SISTEMA IDENTIFICA O REGIME
### **Campo no Banco de Dados:**
```typescript
funcionarios: {
regimeTrabalho: "clt" | "estatutario_pe" | "estatutario_federal" | "estatutario_municipal"
}
```
### **Comportamento Automático:**
1. **Ao criar solicitação:** Sistema detecta o regime do funcionário
2. **Validação automática:** Aplica regras do regime correto
3. **Mensagens customizadas:** Erros específicos por regime
---
## 💡 EXEMPLOS DE VALIDAÇÕES
### **Exemplo 1: CLT tentando 4 períodos**
```
Entrada:
- Período 1: 10 dias
- Período 2: 10 dias
- Período 3: 5 dias
- Período 4: 5 dias
Erro: ❌ "Máximo de 3 períodos permitidos para CLT - Consolidação das Leis do Trabalho"
```
### **Exemplo 2: Servidor PE tentando 8 dias**
```
Entrada:
- Período 1: 22 dias
- Período 2: 8 dias
Erro: ❌ "Período de 8 dias é inválido. Mínimo: 10 dias corridos (Servidor Público Estadual de Pernambuco)"
```
### **Exemplo 3: CLT sem período principal**
```
Entrada:
- Período 1: 10 dias
- Período 2: 10 dias
- Período 3: 10 dias
Erro: ❌ "Ao dividir férias em CLT, um período deve ter no mínimo 14 dias corridos"
```
### **Exemplo 4: Servidor PE em 3 períodos**
```
Entrada:
- Período 1: 10 dias
- Período 2: 10 dias
- Período 3: 10 dias
Erro: ❌ "Máximo de 2 períodos permitidos para Servidor Público Estadual de Pernambuco"
```
---
## 🔧 IMPLEMENTAÇÃO TÉCNICA
### **Arquivo:** `packages/backend/convex/saldoFerias.ts`
```typescript
const REGIMES_CONFIG = {
clt: {
nome: "CLT - Consolidação das Leis do Trabalho",
maxPeriodos: 3,
minDiasPeriodo: 5,
minDiasPeriodoPrincipal: 14,
abonoPermitido: true,
maxDiasAbono: 10,
},
estatutario_pe: {
nome: "Servidor Público Estadual de Pernambuco",
maxPeriodos: 2,
minDiasPeriodo: 10,
minDiasPeriodoPrincipal: null,
abonoPermitido: false,
maxDiasAbono: 0,
},
};
```
### **Query de Validação:**
```typescript
export const validarSolicitacao = query({
args: {
funcionarioId: v.id("funcionarios"),
anoReferencia: v.number(),
periodos: v.array(...)
},
handler: async (ctx, args) => {
// Detecta regime automaticamente
const regime = await obterRegimeTrabalho(ctx, args.funcionarioId);
const config = REGIMES_CONFIG[regime];
// Aplica validações específicas
if (args.periodos.length > config.maxPeriodos) {
erros.push(`Máximo de ${config.maxPeriodos} períodos permitidos para ${config.nome}`);
}
// ... demais validações
}
});
```
---
## 📚 REFERÊNCIAS LEGAIS
### **CLT:**
- **Decreto-Lei nº 5.452/1943** - Consolidação das Leis do Trabalho
- **Art. 129** - Direito a férias
- **Art. 134** - Divisão em períodos
- **Art. 143** - Abono pecuniário
### **Servidor Público Estadual de PE:**
- **Lei nº 6.123/1968** - Estatuto dos Funcionários Públicos Civis do Estado de Pernambuco
- **Art. 84** - Direito a férias
- **Art. 85** - Período aquisitivo
- **Art. 86** - Divisão em períodos
- **Art. 87** - Acúmulo de férias
---
## ✅ STATUS DE IMPLEMENTAÇÃO
| Feature | Status |
|---------|--------|
| ✅ Schema `regimeTrabalho` | Implementado |
| ✅ Detecção automática do regime | Implementado |
| ✅ Validações CLT | Implementado |
| ✅ Validações Servidor PE | Implementado |
| ✅ Mensagens específicas por regime | Implementado |
| ✅ Cálculo de saldo por regime | Implementado |
| ✅ Abono pecuniário (só CLT) | Implementado |
| ✅ Avisos contextuais | Implementado |
---
## 🚀 PRÓXIMOS PASSOS
1.**Backend completo** - FEITO
2. 🔄 **Interface com calendário** - EM ANDAMENTO
3. 📊 **Dashboard visual** - PENDENTE
4. 📱 **Responsivo** - PENDENTE
5. 📄 **Relatórios** - PENDENTE
---
## 💬 MENSAGENS DO SISTEMA
### **CLT - Mensagens:**
```
✅ "Solicitação válida para CLT - Consolidação das Leis do Trabalho"
❌ "Máximo de 3 períodos permitidos para CLT"
❌ "Período de 4 dias é inválido. Mínimo: 5 dias corridos (CLT)"
❌ "Ao dividir férias em CLT, um período deve ter no mínimo 14 dias corridos"
💰 "Você pode vender até 10 dias (abono pecuniário)"
```
### **Servidor PE - Mensagens:**
```
✅ "Solicitação válida para Servidor Público Estadual de Pernambuco"
❌ "Máximo de 2 períodos permitidos para Servidor Público Estadual de Pernambuco"
❌ "Período de 8 dias é inválido. Mínimo: 10 dias corridos (Servidor Público Estadual de Pernambuco)"
📅 "Período preferencial para docentes (20/12 a 10/01)"
⚠️ "Abono pecuniário não permitido para servidores públicos estaduais"
```
---
## 🎓 DICAS PARA USUÁRIOS
### **Se você é CLT:**
- ✅ Pode dividir em até 3 períodos
- ✅ Um período deve ter no mínimo 14 dias
- ✅ Pode vender até 10 dias (abono)
- ⚠️ Férias vencem no período concessivo
### **Se você é Servidor Público Estadual de PE:**
- ✅ Pode dividir em até 2 períodos
- ✅ Cada período deve ter no mínimo 10 dias
- ❌ Não pode vender férias (abono)
- ✅ Se docente, prefira dezembro/janeiro
- ✅ Com +10 anos, pode acumular férias
---
**Sistema desenvolvido com atenção às legislações trabalhistas vigentes! 📋⚖️**
**Data de Implementação:** 30 de outubro de 2025
**Versão:** 2.0.0 - Suporte Multi-Regime

View File

@@ -1,376 +0,0 @@
# 🎉 Sistema de Monitoramento TI - Implementação Completa
## ✅ Status: CONCLUÍDO COM SUCESSO!
Todos os requisitos foram implementados conforme solicitado. O sistema está robusto, profissional e pronto para uso.
---
## 📦 O Que Foi Implementado
### 🎯 Requisitos Atendidos
**Card robusto de monitoramento técnico no painel TI**
**Máximo de informações técnicas do sistema**
**Informações de software e hardware**
**Monitoramento de recursos em tempo real**
**Alertas programáveis com níveis críticos**
**Opção de envio por email e/ou chat**
**Integração com sino de notificações**
**Geração de relatórios PDF e CSV**
**Busca por datas, horários e períodos**
**Design robusto e profissional**
---
## 🏗️ Arquitetura Implementada
### Backend (Convex)
#### **1. Schema** (`packages/backend/convex/schema.ts`)
Três novas tabelas criadas:
**systemMetrics**
- Armazena histórico de todas as métricas
- 8 tipos de métricas (CPU, RAM, Rede, Storage, Usuários, Mensagens, Tempo Resposta, Erros)
- Índice por timestamp para consultas rápidas
- Cleanup automático (30 dias)
**alertConfigurations**
- Configurações de alertas customizáveis
- Suporta 5 operadores (>, <, >=, <=, ==)
- Toggle para ativar/desativar
- Notificação por Chat e/ou Email
- Índice por enabled para queries eficientes
**alertHistory**
- Histórico completo de alertas disparados
- Status: triggered/resolved
- Rastreamento de notificações enviadas
- Múltiplos índices para análise
#### **2. API** (`packages/backend/convex/monitoramento.ts`)
**10 funções implementadas:**
1. `salvarMetricas` - Salva métricas e dispara verificação de alertas
2. `configurarAlerta` - Criar/atualizar alertas
3. `listarAlertas` - Listar todas as configurações
4. `obterMetricas` - Buscar com filtros de data
5. `obterMetricasRecentes` - Última hora
6. `obterUltimaMetrica` - Mais recente
7. `gerarRelatorio` - Com estatísticas (min/max/avg)
8. `deletarAlerta` - Remover configuração
9. `obterHistoricoAlertas` - Histórico completo
10. `verificarAlertasInternal` - Verificação automática (internal)
**Funcionalidades especiais:**
- Rate limiting: não dispara alertas duplicados em 5 minutos
- Integração com sistema de notificações existente
- Cleanup automático de métricas antigas
- Cálculo de estatísticas (mínimo, máximo, média)
### Frontend
#### **3. Utilitário** (`apps/web/src/lib/utils/metricsCollector.ts`)
**Coletor inteligente de métricas:**
**Métricas de Hardware/Sistema:**
- CPU: Estimativa via Performance API
- RAM: `performance.memory` (Chrome) ou estimativa
- Rede: Latência medida com fetch
- Storage: Storage API ou estimativa
**Métricas de Aplicação:**
- Usuários Online: Query em tempo real
- Mensagens/min: Taxa calculada
- Tempo Resposta: Latência das queries
- Erros: Interceptação de console.error
**Recursos:**
- Coleta automática a cada 30s
- Rate limiting integrado
- Função de cleanup ao desmontar
- Status de conexão de rede
#### **4. Componentes Svelte**
### **SystemMonitorCard.svelte** (Principal)
**Interface Moderna:**
- 8 cards de métricas com design gradiente
- Progress bars animadas
- Cores dinâmicas baseadas em thresholds:
- Verde: < 60% (Normal)
- Amarelo: 60-80% (Atenção)
- Vermelho: > 80% (Crítico)
- Atualização automática a cada 30s
- Badges de status
- Informação de última atualização
**Botões de Ação:**
- Configurar Alertas
- Gerar Relatório
### **AlertConfigModal.svelte**
**Funcionalidades:**
- Formulário completo de criação/edição
- 8 métricas disponíveis
- 5 operadores de comparação
- Toggle de ativo/inativo
- Checkboxes para Chat e Email
- Preview do alerta antes de salvar
- Lista de alertas configurados com edição inline
- Deletar com confirmação
**UX:**
- Validação: requer pelo menos um método de notificação
- Estados de loading
- Mensagens de erro amigáveis
- Design responsivo
### **ReportGeneratorModal.svelte**
**Filtros de Período:**
- Hoje
- Última Semana
- Último Mês
- Personalizado (data + hora)
**Seleção de Métricas:**
- Todas as 8 métricas disponíveis
- Botões "Selecionar Todas" / "Limpar"
- Preview visual
**Exportação:**
**PDF (jsPDF + autoTable):**
- Título profissional
- Período e data de geração
- Tabela de estatísticas (min/max/avg)
- Registros detalhados (últimos 50)
- Footer com logo SGSE
- Múltiplas páginas numeradas
- Design com cores da marca
**CSV (PapaParse):**
- Headers em português
- Datas formatadas (dd/MM/yyyy HH:mm:ss)
- Todas as métricas selecionadas
- Compatível com Excel/Google Sheets
#### **5. Integração** (`apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte`)
- SystemMonitorCard adicionado ao painel administrativo TI
- Posicionado após as ações rápidas
- Import correto de todos os componentes
---
## 🔔 Sistema de Alertas
### Fluxo Completo
1. **Coleta**: Métricas coletadas a cada 30s
2. **Salvamento**: Mutation `salvarMetricas` persiste no banco
3. **Verificação**: `verificarAlertasInternal` é agendado (scheduler)
4. **Comparação**: Compara métricas com todos os alertas ativos
5. **Disparo**: Se threshold ultrapassado:
- Registra em `alertHistory`
- Cria notificação em `notificacoes` (chat)
- (Email preparado para integração futura)
6. **Notificação**: NotificationBell exibe automaticamente
7. **Rate Limit**: Não duplica em 5 minutos
### Operadores Suportados
- `>` : Maior que
- `>=` : Maior ou igual
- `<` : Menor que
- `<=` : Menor ou igual
- `==` : Igual a
### Métodos de Notificação
-**Chat**: Integrado com NotificationBell (funcionando)
- 🔄 **Email**: Preparado para integração (TODO no código)
---
## 📊 Métricas Disponíveis
| Métrica | Tipo | Unidade | Origem |
|---------|------|---------|--------|
| CPU | Sistema | % | Performance API |
| Memória | Sistema | % | performance.memory |
| Latência | Sistema | ms | Fetch API |
| Storage | Sistema | % | Storage API |
| Usuários Online | App | count | Convex Query |
| Mensagens/min | App | count/min | Calculado |
| Tempo Resposta | App | ms | Query latency |
| Erros | App | count | Console intercept |
---
## 📈 Relatórios
### Informações Incluídas
**Estatísticas Agregadas:**
- Valor Mínimo
- Valor Máximo
- Valor Médio
**Dados Detalhados:**
- Timestamp completo
- Todas as métricas selecionadas
- Últimos 50 registros (PDF)
- Todos os registros (CSV)
### Formatos
- **PDF**: Visual, profissional, com logo e layout
- **CSV**: Dados brutos para análise no Excel
---
## 🎨 Design e UX
### Padrão de Cores
- **Primary**: #667eea (Roxo/Azul)
- **Success**: Verde (< 60%)
- **Warning**: Amarelo (60-80%)
- **Error**: Vermelho (> 80%)
### Componentes DaisyUI
- Cards com gradientes
- Stats com animações
- Badges dinâmicos
- Progress bars coloridos
- Modals responsivos
- Botões com loading states
### Responsividade
- Mobile: 1 coluna
- Tablet: 2 colunas
- Desktop: 4 colunas
- Breakpoints: sm, md, lg
---
## ⚡ Performance
### Otimizações
- Rate limiting: 1 coleta/30s
- Cleanup automático: 30 dias
- Queries com índices
- Lazy loading de modals
- Debounce em inputs
### Escalabilidade
- Suporta milhares de registros
- Queries otimizadas
- Scheduler assíncrono
- Sem bloqueio de UI
---
## 🔒 Segurança
- Apenas usuários TI têm acesso
- Validação de permissões no backend
- Sanitização de inputs
- Rate limiting integrado
- Internal mutations protegidas
---
## 📁 Arquivos Criados/Modificados
### Criados (6 arquivos)
1. `packages/backend/convex/monitoramento.ts` - API completa
2. `apps/web/src/lib/utils/metricsCollector.ts` - Coletor
3. `apps/web/src/lib/components/ti/SystemMonitorCard.svelte` - Card principal
4. `apps/web/src/lib/components/ti/AlertConfigModal.svelte` - Config alertas
5. `apps/web/src/lib/components/ti/ReportGeneratorModal.svelte` - Relatórios
6. `TESTE_MONITORAMENTO.md` - Documentação de testes
### Modificados (3 arquivos)
1. `packages/backend/convex/schema.ts` - 3 tabelas adicionadas
2. `apps/web/package.json` - papaparse e @types/papaparse
3. `apps/web/src/routes/(dashboard)/ti/painel-administrativo/+page.svelte` - Integração
---
## 🚀 Como Usar
### Para Usuários
1. Acesse `/ti/painel-administrativo`
2. Role até o card de monitoramento
3. Visualize métricas em tempo real
4. Configure alertas personalizados
5. Gere relatórios quando necessário
### Para Desenvolvedores
Ver documentação completa em `TESTE_MONITORAMENTO.md`
---
## 🎯 Diferenciais
**Completo**: Backend + Frontend totalmente integrados
**Profissional**: Design moderno e polido
**Robusto**: Tratamento de erros e edge cases
**Escalável**: Arquitetura preparada para crescimento
**Documentado**: Guia completo de testes
**Sem Linter Errors**: Código limpo e validado
**Pronto para Produção**: Funcional desde o primeiro uso
---
## 📝 Próximos Passos Sugeridos
1. **Integrar Email**: Completar envio de alertas por email
2. **Gráficos**: Adicionar charts visuais (Chart.js/Recharts)
3. **Dashboard Customizável**: Permitir usuário escolher métricas
4. **Métricas Reais de Backend**: CPU/RAM do servidor Node.js
5. **Machine Learning**: Detecção de anomalias
6. **Webhooks**: Notificar sistemas externos
7. **Mobile App**: Notificações push no celular
---
## 🏆 Conclusão
Sistema de monitoramento técnico **completo**, **robusto** e **profissional** implementado com sucesso!
Todas as funcionalidades solicitadas foram entregues:
- ✅ Monitoramento em tempo real
- ✅ Informações técnicas completas
- ✅ Alertas customizáveis
- ✅ Notificações integradas
- ✅ Relatórios PDF/CSV
- ✅ Filtros avançados
- ✅ Design profissional
**O sistema está pronto para uso imediato!** 🎉
---
**Desenvolvido por**: Secretaria de Esportes de Pernambuco
**Tecnologias**: Convex, Svelte 5, TypeScript, DaisyUI, jsPDF, PapaParse
**Versão**: 2.0
**Data**: Outubro 2025

View File

@@ -1,636 +0,0 @@
# 🎉 SISTEMA MODERNO DE GESTÃO DE FÉRIAS - IMPLEMENTAÇÃO COMPLETA
**Data de Conclusão:** 30 de outubro de 2025
**Versão:** 2.0.0 - Sistema Premium Multi-Regime
**Status:****100% IMPLEMENTADO E FUNCIONAL**
---
## 📋 ÍNDICE
1. [Visão Geral](#visão-geral)
2. [Arquitetura do Sistema](#arquitetura-do-sistema)
3. [Funcionalidades Implementadas](#funcionalidades-implementadas)
4. [Componentes Frontend](#componentes-frontend)
5. [Backend e API](#backend-e-api)
6. [Regras de Negócio](#regras-de-negócio)
7. [Fluxo do Usuário](#fluxo-do-usuário)
8. [Guia de Uso](#guia-de-uso)
9. [Tecnologias Utilizadas](#tecnologias-utilizadas)
10. [Testes e Validação](#testes-e-validação)
---
## 🎯 VISÃO GERAL
O **Sistema de Gestão de Férias** do SGSE é uma solução moderna, intuitiva e robusta para gerenciamento completo de férias de funcionários, com suporte a **múltiplos regimes de trabalho** (CLT e Servidor Público Estadual de PE).
### ⭐ Diferenciais
-**Multi-Regime**: Suporta CLT e Servidor Público PE com regras específicas
-**Wizard Intuitivo**: Processo de solicitação em 3 passos guiados
-**Calendário Interativo**: FullCalendar para seleção visual de períodos
-**Validação em Tempo Real**: Feedback instantâneo sobre regras CLT/Servidor PE
-**Dashboard Analytics**: Gráficos e estatísticas em tempo real
-**Toast Notifications**: Feedback visual moderno com Sonner
-**Cálculo Automático de Saldo**: Sistema inteligente de períodos aquisitivos
-**Gestão por Times**: Estrutura de times e gestores para aprovações
-**Responsivo**: 100% adaptado para mobile, tablet e desktop
---
## 🏗️ ARQUITETURA DO SISTEMA
```
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (SvelteKit) │
├─────────────────────────────────────────────────────────────┤
│ /perfil > Aba "Minhas Férias" │
│ ├── DashboardFerias.svelte (Analytics + Gráficos) │
│ └── WizardSolicitacaoFerias.svelte (Processo 3 Passos) │
│ └── CalendarioFerias.svelte (FullCalendar) │
├─────────────────────────────────────────────────────────────┤
│ BACKEND (Convex) │
├─────────────────────────────────────────────────────────────┤
│ Schemas: │
│ ├── funcionarios (+ regimeTrabalho) │
│ ├── periodosAquisitivos (novo!) │
│ ├── solicitacoesFerias │
│ └── notificacoesFerias │
│ │
│ Modules: │
│ ├── saldoFerias.ts (Cálculos + Validações) │
│ ├── ferias.ts (CRUD + Aprovações) │
│ ├── times.ts (Gestão de Times) │
│ └── crons.ts (Automações) │
└─────────────────────────────────────────────────────────────┘
```
---
## ✨ FUNCIONALIDADES IMPLEMENTADAS
### 🔹 **FASE 1: Backend & Regras de Negócio**
#### ✅ Schema de Períodos Aquisitivos
- **Tabela:** `periodosAquisitivos`
- **Campos:**
- `anoReferencia`: Ano do período (ex: 2025)
- `diasDireito`: Dias totais (30)
- `diasUsados`: Dias já gozados
- `diasPendentes`: Dias em solicitações aguardando
- `diasDisponiveis`: Saldo disponível
- `abonoPermitido`: Permite venda de férias (só CLT)
- `status`: `ativo`, `vencido`, `concluido`
#### ✅ Cálculo Automático de Saldo
- **Query:** `saldoFerias.obterSaldo`
- Cria automaticamente períodos aquisitivos se não existirem
- Calcula saldo baseado no regime de trabalho
- Retorna informações completas do período
#### ✅ Validação CLT vs Servidor PE
- **Query:** `saldoFerias.validarSolicitacao`
- **CLT:** Máx 3 períodos, mín 5 dias, 1 período com 14+ dias
- **Servidor PE:** Máx 2 períodos, mín 10 dias cada
- Valida sobreposição de datas
- Valida saldo disponível
- Retorna erros e avisos contextuais
#### ✅ Reserva e Liberação de Dias
- **Mutation:** `saldoFerias.reservarDias`
- Reserva dias ao criar solicitação (impede uso duplo)
- **Mutation:** `saldoFerias.liberarDias`
- Libera dias ao reprovar solicitação
- **Mutation:** `saldoFerias.atualizarSaldoAposAprovacao`
- Marca dias como usados após aprovação
#### ✅ Cron Jobs Automáticos
- **Diário:** Criar períodos aquisitivos para novos funcionários
- **Diário:** Atualizar status de férias (ativo/em_ferias)
---
### 🔹 **FASE 2: Frontend Premium**
#### ✅ Wizard de Solicitação (3 Passos)
**Componente:** `WizardSolicitacaoFerias.svelte`
**Passo 1 - Ano & Saldo:**
- Seletor visual de ano (cards)
- Card premium com estatísticas do saldo:
- Total Direito
- Disponível
- Usado
- Pendente
- Informações do regime de trabalho
- Alertas de saldo zerado
**Passo 2 - Seleção de Períodos:**
- Calendário interativo (FullCalendar)
- Drag & drop para selecionar períodos
- Click para remover períodos
- Validação em tempo real:
- Erros visuais (vermelho)
- Avisos contextuais (amarelo)
- Sucesso (verde)
- Progress bar de saldo:
- Disponível / Selecionado / Restante
**Passo 3 - Confirmação:**
- Resumo visual da solicitação
- Lista de períodos com datas formatadas
- Campo de observação opcional
- Botões de ação premium
**Animações:**
- FadeIn entre passos
- Hover effects
- Loading states
- Toast notifications
---
#### ✅ Calendário Interativo
**Componente:** `CalendarioFerias.svelte`
**Features:**
- **FullCalendar Integration:**
- View mensal e anual (multiMonth)
- Localização PT-BR
- Seleção por drag
- Eventos coloridos por período
- **Validações Visuais:**
- Destaque de fins de semana
- Bloqueio de datas passadas
- Cores distintas por período (roxo, rosa, azul)
- Tooltip em eventos
- **Customização:**
- Toolbar moderna com gradiente
- Eventos com sombra e hover
- Grid limpo e profissional
- 100% responsivo
**Eventos:**
- `onPeriodoAdicionado`: Callback ao adicionar período
- `onPeriodoRemovido`: Callback ao remover período
---
#### ✅ Dashboard de Analytics
**Componente:** `DashboardFerias.svelte`
**Cards de Estatísticas (4):**
1. **Disponível** (Verde): Dias disponíveis
2. **Usado** (Vermelho): Dias já gozados
3. **Pendente** (Amarelo): Dias aguardando aprovação
4. **Total Direito** (Roxo): Dias totais do ano
**Gráficos de Pizza (2):**
1. **Distribuição de Saldo:**
- Disponível (verde)
- Pendente (laranja)
- Usado (vermelho)
2. **Status de Solicitações:**
- Aprovadas (verde)
- Pendentes (laranja)
- Reprovadas (vermelho)
**Tabela de Histórico:**
- Todos os saldos por ano
- Status visual (ativo/vencido/concluído)
- Breakdown de dias
**Tecnologias:**
- Canvas API para gráficos (sem bibliotecas pesadas!)
- Design glassmorphism
- Animações suaves
- Hover effects premium
---
#### ✅ Toast Notifications
**Biblioteca:** Svelte-Sonner
**Tipos:**
- `toast.success()`: Ações bem-sucedidas
- `toast.error()`: Erros e validações
- `toast.info()`: Informações gerais
- `toast.warning()`: Avisos importantes
**Exemplos:**
```typescript
toast.success("Período de 14 dias adicionado! ✅");
toast.error("Máximo de 3 períodos atingido");
toast.warning("Seu saldo está baixo!");
```
**Configuração:**
- Posição: top-right
- Rich colors: ativado
- Close button: sim
- Expand: sim
---
## 📊 REGRAS DE NEGÓCIO
### CLT (Consolidação das Leis do Trabalho)
| Regra | Valor |
|-------|-------|
| Dias por Ano | 30 dias |
| Máx Períodos | 3 |
| Mín Dias/Período | 5 dias |
| Período Principal | 14+ dias (obrigatório) |
| Abono Pecuniário | ✅ Até 10 dias (1/3) |
**Validações:**
```typescript
Período 1: 14 dias Principal (obrigatório)
Período 2: 10 dias Secundário
Período 3: 6 dias Secundário
```
---
### Servidor Público Estadual de PE
| Regra | Valor |
|-------|-------|
| Dias por Ano | 30 dias |
| Máx Períodos | 2 |
| Mín Dias/Período | 10 dias |
| Período Principal | Não há |
| Abono Pecuniário | ❌ Não permitido |
**Validações:**
```typescript
Período 1: 20 dias
Período 2: 10 dias
```
**Avisos Especiais:**
- Docentes: Período preferencial 20/12 a 10/01
- Servidores +10 anos: Podem acumular até 2 períodos
---
## 🚀 FLUXO DO USUÁRIO
### 1⃣ **Funcionário Solicita Férias**
```
1. Acessa: Perfil > Aba "Minhas Férias"
2. Visualiza Dashboard com saldo e estatísticas
3. Clica em "Solicitar Novas Férias"
4. Wizard Passo 1: Escolhe ano de referência
└── Sistema mostra saldo disponível
5. Wizard Passo 2: Seleciona períodos no calendário
└── Validação em tempo real
6. Wizard Passo 3: Revisa e confirma
└── Adiciona observação (opcional)
7. Envia solicitação
└── Toast: "Solicitação enviada com sucesso! 🎉"
└── Notificação enviada ao gestor
```
---
### 2⃣ **Gestor Aprova/Rejeita**
```
1. Recebe notificação (sino no header)
2. Acessa: Perfil > Aba "Aprovar Férias"
3. Visualiza lista de solicitações pendentes
4. Clica em solicitação para detalhes
5. Opções:
├── Aprovar
│ └── Sistema atualiza saldo
│ └── Funcionário recebe notificação
├── Reprovar com motivo
│ └── Sistema libera dias reservados
│ └── Funcionário recebe notificação
└── Ajustar datas e aprovar
└── Sistema recalcula saldo
└── Funcionário recebe notificação
```
---
### 3⃣ **Sistema Automático**
```
Diariamente (Cron Jobs):
1. Cria períodos aquisitivos para funcionários
2. Atualiza status de férias (ativo → em_ferias)
3. Verifica períodos vencidos
4. Envia alertas de saldo baixo
```
---
## 📖 GUIA DE USO
### Para Funcionários
#### Como Solicitar Férias
1. **Acesse seu Perfil:**
- Click no ícone do seu avatar (canto superior direito)
- Selecione "Meu Perfil"
2. **Vá para Minhas Férias:**
- Click na aba "Minhas Férias"
- Visualize seu dashboard com saldos
3. **Solicite Novas Férias:**
- Click no botão grande "Solicitar Novas Férias"
4. **Passo 1 - Escolha o Ano:**
- Selecione o ano de referência
- Verifique seu saldo disponível
- Click em "Próximo"
5. **Passo 2 - Selecione os Períodos:**
- Arraste no calendário para selecionar períodos
- Adicione até 3 períodos (CLT) ou 2 (Servidor PE)
- Observe as validações em tempo real
- Click em "Próximo"
6. **Passo 3 - Confirme:**
- Revise todos os períodos
- Adicione observação (opcional)
- Click em "Enviar Solicitação"
7. **Aguarde Aprovação:**
- Você será notificado quando o gestor aprovar/reprovar
- Acompanhe o status na aba "Minhas Férias"
---
### Para Gestores
#### Como Aprovar Férias
1. **Notificação:**
- Você receberá um sino vermelho no header
- Click nele para ver solicitações pendentes
2. **Acesse Aprovações:**
- Vá em Perfil > Aba "Aprovar Férias"
- Visualize lista de solicitações da sua equipe
3. **Analise a Solicitação:**
- Click em "Ver Detalhes"
- Veja períodos, dias, e observações
4. **Decida:**
- **Aprovar:** Click em "Aprovar"
- **Reprovar:** Click em "Reprovar", escreva motivo
- **Ajustar:** Click em "Ajustar Datas", modifique, e aprove
5. **Confirmação:**
- Funcionário recebe notificação automática
- Status atualizado no sistema
---
### Para TI_MASTER
#### Como Configurar Times
1. **Acesse TI:**
- Menu lateral > Tecnologia da Informação
2. **Gestão de Times:**
- Click em "Times e Membros"
- Visualize lista de times
3. **Criar Time:**
- Click em "Novo Time"
- Preencha: Nome, Descrição, Cor, Gestor
- Adicione membros (funcionários)
- Salve
4. **Gerenciar Membros:**
- Adicione/remova membros de times
- Transfira membros entre times
- Desative times inativos
---
## 🛠️ TECNOLOGIAS UTILIZADAS
### **Frontend**
| Tecnologia | Versão | Uso |
|------------|--------|-----|
| **SvelteKit** | 2.48.1 | Framework principal |
| **Svelte** | 5.42.3 | UI Components |
| **FullCalendar** | 6.1.19 | Calendário interativo |
| **Svelte-Sonner** | 1.0.5 | Toast notifications |
| **Zod** | 4.1.12 | Validação de schemas |
| **DaisyUI** | 5.3.10 | Design system |
| **TailwindCSS** | 4.1.16 | Utility CSS |
### **Backend**
| Tecnologia | Uso |
|------------|-----|
| **Convex** | Backend-as-a-Service |
| **TypeScript** | Type safety |
| **Cron Jobs** | Automações |
### **Outros**
- **Canvas API**: Gráficos de pizza
- **date-fns**: Manipulação de datas
- **Internationalized Date**: Formatação i18n
---
## ✅ TESTES E VALIDAÇÃO
### **Cenários de Teste**
#### Teste 1: Solicitação CLT Válida
```
✅ Funcionário: João (CLT)
✅ Ano: 2025
✅ Saldo: 30 dias disponíveis
✅ Períodos:
- 15 dias (01/06 a 15/06) ← Principal
- 10 dias (01/12 a 10/12)
- 5 dias (20/12 a 24/12)
✅ Resultado: Aprovado ✅
```
#### Teste 2: Servidor PE - Período Inválido
```
❌ Funcionário: Maria (Servidor PE)
❌ Ano: 2025
❌ Saldo: 30 dias disponíveis
❌ Períodos:
- 20 dias (01/06 a 20/06)
- 5 dias (01/12 a 05/12) ← ERRO: Mínimo 10 dias
❌ Resultado: ERRO - "Período de 5 dias é inválido. Mínimo: 10 dias corridos"
```
#### Teste 3: CLT - Sem Período Principal
```
❌ Funcionário: Carlos (CLT)
❌ Períodos:
- 10 dias
- 10 dias
- 10 dias ← Nenhum com 14+
❌ Resultado: ERRO - "Ao dividir férias em CLT, um período deve ter no mínimo 14 dias corridos"
```
#### Teste 4: Saldo Insuficiente
```
❌ Funcionário: Ana
❌ Saldo: 10 dias disponíveis
❌ Solicitação: 20 dias
❌ Resultado: ERRO - "Total solicitado (20 dias) excede saldo disponível (10 dias)"
```
---
## 📂 ESTRUTURA DE ARQUIVOS
```
sgse-app/
├── apps/web/src/
│ ├── lib/
│ │ └── components/
│ │ └── ferias/
│ │ ├── CalendarioFerias.svelte ← Calendário
│ │ ├── WizardSolicitacaoFerias.svelte ← Wizard 3 passos
│ │ └── DashboardFerias.svelte ← Dashboard analytics
│ └── routes/
│ └── (dashboard)/
│ ├── +layout.svelte ← Toaster config
│ └── perfil/
│ └── +page.svelte ← Página principal
├── packages/backend/convex/
│ ├── schema.ts ← periodosAquisitivos + regimeTrabalho
│ ├── saldoFerias.ts ← Cálculos e validações
│ ├── ferias.ts ← CRUD de solicitações
│ ├── times.ts ← Gestão de times
│ └── crons.ts ← Jobs automáticos
└── Documentação/
├── REGRAS_FERIAS_CLT_E_SERVIDOR_PE.md ← Regras detalhadas
└── SISTEMA_FERIAS_MODERNO_COMPLETO.md ← Este arquivo!
```
---
## 🎨 DESIGN SYSTEM
### Cores
- **Primary:** `#667eea` (Roxo)
- **Secondary:** `#764ba2` (Rosa-Roxo)
- **Success:** `#51cf66` (Verde)
- **Warning:** `#ffa94d` (Laranja)
- **Error:** `#ff6b6b` (Vermelho)
- **Info:** `#4facfe` (Azul)
### Componentes Premium
- **Cards com Gradiente:** `from-primary/20 to-secondary/10`
- **Sombras Profundas:** `shadow-2xl`
- **Bordas Suaves:** `rounded-2xl`
- **Hover Effects:** `hover:scale-105 transition-all`
- **Glassmorphism:** Background semi-transparente com blur
---
## 🚀 PRÓXIMOS PASSOS (Futuro)
### Fase 3 - Melhorias Avançadas
1. **Exportação de Relatórios:**
- PDF com histórico de férias
- Excel com estatísticas
- Gráficos impressos
2. **Integração com E-mail:**
- Notificações por e-mail
- Lembretes automáticos
3. **Mobile App:**
- Progressive Web App (PWA)
- Notificações push
4. **IA Inteligente:**
- Sugestão de melhores períodos
- Previsão de conflitos de equipe
- Otimização de agendamento
5. **Integrações:**
- Google Calendar
- Microsoft Outlook
- Folha de pagamento
---
## 📞 SUPORTE
### Problemas Comuns
**1. "Não consigo ver meu saldo"**
- Verifique se você tem um cadastro de funcionário
- Confirme que tem uma data de admissão cadastrada
- Entre em contato com RH
**2. "Validação bloqueando minha solicitação"**
- Leia atentamente a mensagem de erro
- Verifique se está respeitando as regras do seu regime (CLT ou Servidor PE)
- Consulte a documentação de regras
**3. "Gestor não recebeu notificação"**
- Verifique se você está atribuído a um time
- Confirme que o time tem um gestor configurado
- Entre em contato com TI
---
## ✨ CONCLUSÃO
O **Sistema Moderno de Gestão de Férias** representa um avanço significativo na experiência do usuário e na eficiência operacional do SGSE.
### **Benefícios Alcançados:**
**Redução de Erros:** Validação automática previne solicitações inválidas
**Transparência:** Dashboard mostra saldo em tempo real
**Agilidade:** Processo guiado reduz tempo de solicitação
**Conformidade:** Regras CLT e Servidor PE aplicadas automaticamente
**UX Premium:** Interface moderna e intuitiva
### **Métricas de Sucesso:**
- 🎯 **100%** das regras CLT e Servidor PE implementadas
- 🎯 **3 passos** para solicitar férias (vs 10+ no sistema anterior)
- 🎯 **Real-time** validação e feedback
- 🎯 **0 configuração** manual - tudo automático!
---
**Desenvolvido com ❤️ pela equipe SGSE**
**Versão 2.0.0 - Sistema Premium Multi-Regime**
**Data: 30 de outubro de 2025**
🎉 **SISTEMA 100% FUNCIONAL E PRONTO PARA USO!** 🎉

View File

@@ -1,304 +0,0 @@
# 🧪 TESTAR SISTEMA DE FÉRIAS - PASSO A PASSO
**Data:** 30 de outubro de 2025
**Objetivo:** Criar funcionário de teste e validar todo o fluxo de férias
---
## 🚀 PASSO 1: Criar Funcionário de Teste para TI Master
### Opção A: Via Convex Dashboard (Recomendado)
1. **Acesse o Convex Dashboard:**
```
https://dashboard.convex.dev
```
2. **Vá para a seção "Functions"**
3. **Encontre a função:** `criarFuncionarioTeste:criarFuncionarioParaTIMaster`
4. **Execute com estes argumentos:**
```json
{
"usuarioEmail": "ti.master@sgse.pe.gov.br"
}
```
5. **Você verá o resultado:**
```json
{
"sucesso": true,
"funcionarioId": "abc123..."
}
```
### Opção B: Via Console do Browser
1. **Abra o console do navegador (F12)**
2. **Cole e execute este código:**
```javascript
// No console do navegador, dentro do sistema SGSE
const convex = window.convex; // ou acesse o client Convex do app
await convex.mutation(api.criarFuncionarioTeste.criarFuncionarioParaTIMaster, {
usuarioEmail: "ti.master@sgse.pe.gov.br"
});
```
---
## ✅ PASSO 2: Verificar Criação do Funcionário
1. **Recarregue a página do perfil**
- Pressione `F5` ou `Ctrl+R`
2. **Verifique se o erro sumiu**
- Acesse: Perfil > Minhas Férias
- Agora deve aparecer o **Dashboard de Férias** ✨
---
## 🧪 PASSO 3: Testar Fluxo Completo de Solicitação
### 3.1. Visualizar Dashboard
```
✅ Deve mostrar:
- 4 Cards estatísticos (Disponível, Usado, Pendente, Total)
- 2 Gráficos de pizza
- Tabela de histórico de saldos
- Botão "Solicitar Novas Férias"
```
### 3.2. Iniciar Wizard de Solicitação
1. **Click em "Solicitar Novas Férias"**
2. **PASSO 1 - Ano & Saldo:**
```
✅ Escolha o ano: 2024 ou 2025
✅ Verifique o saldo disponível: 30 dias
✅ Veja o regime: "CLT - Consolidação das Leis do Trabalho"
✅ Click em "Próximo"
```
3. **PASSO 2 - Selecionar Períodos:**
```
✅ Arraste no calendário para selecionar o primeiro período
✅ Adicione mais períodos (até 3 para CLT)
✅ Observe as validações em tempo real:
- Verde: tudo certo ✅
- Vermelho: erro (ex: período muito curto) ❌
- Amarelo: aviso (ex: saldo baixo) ⚠️
✅ Click em "Próximo"
```
4. **PASSO 3 - Confirmação:**
```
✅ Revise todos os períodos
✅ Adicione observação (opcional)
✅ Click em "Enviar Solicitação"
```
5. **Sucesso!**
```
✅ Toast verde: "Solicitação enviada com sucesso! 🎉"
✅ Retorna ao dashboard
✅ Atualiza estatísticas
```
---
## 🧪 PASSO 4: Testar Validações CLT
### Teste 1: Período muito curto ❌
```
Tente criar: 3 dias
Resultado esperado: "Período de 3 dias é inválido. Mínimo: 5 dias corridos (CLT)"
```
### Teste 2: Muitos períodos ❌
```
Tente criar: 4 períodos
Resultado esperado: "Máximo de 3 períodos permitidos para CLT"
```
### Teste 3: Sem período principal ❌
```
Crie 3 períodos:
- 10 dias
- 10 dias
- 10 dias
Resultado esperado: "Ao dividir férias em CLT, um período deve ter no mínimo 14 dias corridos"
```
### Teste 4: Solicitação válida ✅
```
Crie 3 períodos:
- 15 dias (Principal)
- 10 dias
- 5 dias
Resultado esperado: "✅ Períodos válidos! Total: 30 dias"
```
---
## 🧪 PASSO 5: Testar Regime Servidor Público PE
### 5.1. Alterar Regime do Funcionário
**Via Convex Dashboard:**
```json
// Função: criarFuncionarioTeste:alterarRegimeTrabalho
{
"funcionarioId": "SEU_FUNCIONARIO_ID",
"novoRegime": "estatutario_pe"
}
```
### 5.2. Testar Validações Servidor PE
**Teste 1: 3 períodos ❌**
```
Tente criar: 3 períodos
Resultado esperado: "Máximo de 2 períodos permitidos para Servidor Público Estadual de Pernambuco"
```
**Teste 2: Período curto ❌**
```
Tente criar: 8 dias
Resultado esperado: "Período de 8 dias é inválido. Mínimo: 10 dias corridos (Servidor Público...)"
```
**Teste 3: Solicitação válida ✅**
```
Crie 2 períodos:
- 20 dias
- 10 dias
Resultado esperado: "✅ Períodos válidos! Total: 30 dias"
```
---
## 🎯 PASSO 6: Testar Aprovação de Férias (Gestor)
### 6.1. Configurar Time e Gestor
**Via TI > Times e Membros:**
```
1. Criar um time de teste
2. Adicionar funcionário como membro
3. Configurar você (TI Master) como gestor
```
### 6.2. Aprovar Solicitação
**Via Perfil > Aprovar Férias:**
```
1. Ver lista de solicitações pendentes
2. Click em "Ver Detalhes"
3. Aprovar / Reprovar / Ajustar
4. Verificar notificação no sino
```
---
## 📊 PASSO 7: Verificar Analytics
### Dashboard deve mostrar:
```
✅ Gráfico de Saldo atualizado
✅ Estatísticas corretas
✅ Histórico de solicitações
✅ Status visual (badges coloridos)
```
---
## 🐛 TROUBLESHOOTING
### Problema: "Perfil de funcionário não encontrado"
**Solução:** Execute o PASSO 1 novamente
### Problema: "Você ainda não tem direito a férias"
**Solução:** Altere a data de admissão:
```json
// Via criarFuncionarioTeste:alterarDataAdmissao
{
"funcionarioId": "SEU_ID",
"novaData": "2023-01-01"
}
```
### Problema: Toast não aparece
**Solução:** Verifique se Sonner está configurado em `+layout.svelte`
### Problema: Calendário não carrega
**Solução:**
1. Verifique se FullCalendar foi instalado
2. Execute: `cd apps/web && bun add @fullcalendar/core @fullcalendar/daygrid`
### Problema: Validação não funciona
**Solução:**
1. Verifique o regime de trabalho do funcionário
2. Confirme que o backend `saldoFerias.ts` está deployado
---
## ✅ CHECKLIST DE TESTES
- [ ] Funcionário criado e associado
- [ ] Dashboard carrega corretamente
- [ ] Wizard abre ao clicar em "Solicitar Férias"
- [ ] Seleção de ano funciona
- [ ] Saldo é exibido corretamente
- [ ] Calendário permite drag & drop
- [ ] Validações CLT funcionam
- [ ] Validações Servidor PE funcionam
- [ ] Toast notifications aparecem
- [ ] Solicitação é criada com sucesso
- [ ] Dashboard atualiza após solicitação
- [ ] Gráficos são renderizados
- [ ] Aprovação de férias funciona (se gestor)
---
## 🎉 RESULTADO ESPERADO
Após completar todos os passos, você terá testado:
✅ **Backend:**
- Criação de períodos aquisitivos
- Validações CLT e Servidor PE
- Reserva e liberação de dias
- Cálculo de saldo
✅ **Frontend:**
- Wizard de 3 passos
- Calendário interativo
- Dashboard com analytics
- Toast notifications
- Validações em tempo real
---
## 📞 PRECISA DE AJUDA?
Se encontrar algum erro:
1. **Verifique o console do navegador (F12)**
- Logs de erro aparecem aqui
2. **Verifique o Convex Dashboard**
- Logs do backend aparecem aqui
3. **Documentação completa:**
- Veja `SISTEMA_FERIAS_MODERNO_COMPLETO.md`
- Veja `REGRAS_FERIAS_CLT_E_SERVIDOR_PE.md`
---
**Boa sorte com os testes! 🚀**

View File

@@ -1,369 +0,0 @@
# 🔍 Guia de Teste - Sistema de Monitoramento
## ✅ Sistema Implementado com Sucesso!
O sistema de monitoramento técnico foi completamente implementado no painel TI com as seguintes funcionalidades:
---
## 📦 O que foi criado
### Backend (Convex)
#### 1. **Schema** (`packages/backend/convex/schema.ts`)
-`systemMetrics`: Armazena histórico de métricas do sistema
-`alertConfigurations`: Configurações de alertas customizáveis
-`alertHistory`: Histórico de alertas disparados
#### 2. **API** (`packages/backend/convex/monitoramento.ts`)
-`salvarMetricas`: Salva métricas coletadas
-`configurarAlerta`: Criar/atualizar alertas
-`listarAlertas`: Listar configurações de alertas
-`obterMetricas`: Buscar métricas com filtros
-`obterMetricasRecentes`: Últimas métricas (1 hora)
-`obterUltimaMetrica`: Métrica mais recente
-`gerarRelatorio`: Gerar relatório com estatísticas
-`deletarAlerta`: Remover configuração de alerta
-`obterHistoricoAlertas`: Histórico de alertas disparados
-`verificarAlertasInternal`: Verificação automática de alertas (internal)
### Frontend
#### 3. **Coletor de Métricas** (`apps/web/src/lib/utils/metricsCollector.ts`)
- ✅ Coleta automática de métricas do navegador
- ✅ Estimativa de CPU via Performance API
- ✅ Uso de memória (Chrome) ou estimativa
- ✅ Latência de rede
- ✅ Armazenamento usado
- ✅ Usuários online (via Convex)
- ✅ Tempo de resposta da aplicação
- ✅ Contagem de erros
#### 4. **Componentes**
**SystemMonitorCard.svelte**
- ✅ 8 cards de métricas visuais com cores dinâmicas
- ✅ Atualização automática a cada 30 segundos
- ✅ Indicadores de status (Normal/Atenção/Crítico)
- ✅ Progress bars com cores baseadas em thresholds
- ✅ Botões para configurar alertas e gerar relatórios
**AlertConfigModal.svelte**
- ✅ Criação/edição de alertas
- ✅ Seleção de métrica e operador
- ✅ Configuração de thresholds
- ✅ Toggle para ativar/desativar
- ✅ Notificações por Chat e/ou Email
- ✅ Preview do alerta antes de salvar
- ✅ Lista de alertas configurados
- ✅ Editar/deletar alertas existentes
**ReportGeneratorModal.svelte**
- ✅ Seleção de período (Hoje/Semana/Mês/Personalizado)
- ✅ Filtros de data e hora
- ✅ Seleção de métricas a incluir
- ✅ Exportação em PDF (com jsPDF e autotable)
- ✅ Exportação em CSV (com PapaParse)
- ✅ Relatórios com estatísticas (min/max/avg)
---
## 🧪 Como Testar
### Pré-requisitos
1. **Instalar dependências** (se ainda não instalou):
```bash
cd apps/web
npm install
```
2. **Iniciar o backend Convex**:
```bash
npx convex dev
```
3. **Iniciar o frontend**:
```bash
npm run dev
```
---
### Teste 1: Visualização de Métricas
1. Faça login como usuário TI:
- Matrícula: `1000`
- Senha: `TIMaster@123`
2. Acesse `/ti/painel-administrativo`
3. Role até o final da página - você verá o **Card de Monitoramento do Sistema**
4. Observe:
- ✅ 8 cards de métricas com valores em tempo real
- ✅ Cores mudando baseadas nos valores (verde/amarelo/vermelho)
- ✅ Progress bars animadas
- ✅ Última atualização no rodapé
5. Aguarde 30 segundos:
- ✅ Os valores devem atualizar automaticamente
- ✅ O timestamp da última atualização deve mudar
---
### Teste 2: Configuração de Alertas
1. No card de monitoramento, clique em **"Configurar Alertas"**
2. Clique em **"Novo Alerta"**
3. Configure um alerta de teste:
- Métrica: **Uso de CPU (%)**
- Condição: **Maior que (>)**
- Valor Limite: **50**
- Alerta Ativo: ✅ (marcado)
- Notificar por Chat: ✅ (marcado)
- Notificar por E-mail: ☐ (desmarcado)
4. Clique em **"Salvar Alerta"**
5. Verifique:
- ✅ Alerta aparece na lista de "Alertas Configurados"
- ✅ Status mostra "Ativo" com badge verde
- ✅ Método de notificação mostra "Chat"
6. Teste edição:
- Clique no botão de editar (✏️)
- Altere o threshold para **80**
- Salve novamente
- ✅ Verifique que o valor foi atualizado
7. Teste deletar:
- Clique no botão de deletar (🗑️)
- Confirme a exclusão
- ✅ Alerta deve desaparecer da lista
---
### Teste 3: Disparo de Alertas
1. Configure um alerta com threshold baixo para forçar disparo:
- Métrica: **Uso de CPU (%)**
- Condição: **Maior que (>)**
- Valor Limite: **1** (muito baixo)
- Notificar por Chat: ✅
2. Aguarde até 30 segundos (próxima coleta de métricas)
3. Verifique o **Sino de Notificações** no header:
- ✅ Deve aparecer uma badge com número (1+)
- ✅ O sino deve ficar animado
4. Clique no sino:
- ✅ Deve aparecer notificação tipo: "⚠️ Alerta de Sistema: cpuUsage"
- ✅ Descrição mostrando o valor e o limite
5. **Importante**: O sistema não dispara alertas duplicados em 5 minutos
- Mesmo com threshold baixo, você receberá apenas 1 notificação a cada 5 min
---
### Teste 4: Geração de Relatórios
#### Teste 4.1: Relatório PDF
1. No card de monitoramento, clique em **"Gerar Relatório"**
2. Selecione período **"Última Semana"**
3. Verifique que todas as métricas estão selecionadas
4. Clique em **"Exportar PDF"**
5. Verifique:
- ✅ Download do arquivo PDF iniciou
- ✅ Nome do arquivo: `relatorio-monitoramento-YYYY-MM-DD-HHmm.pdf`
6. Abra o PDF e verifique:
- ✅ Título: "Relatório de Monitoramento do Sistema"
- ✅ Período correto
- ✅ Tabela de estatísticas (Min/Max/Média)
- ✅ Registros detalhados (últimos 50)
- ✅ Footer com logo SGSE em cada página
#### Teste 4.2: Relatório CSV
1. No modal de relatórios, clique em **"Exportar CSV"**
2. Verifique:
- ✅ Download do arquivo CSV iniciou
- ✅ Nome do arquivo: `relatorio-monitoramento-YYYY-MM-DD-HHmm.csv`
3. Abra o CSV no Excel/Google Sheets:
- ✅ Colunas com nomes corretos (Data/Hora, métricas)
- ✅ Dados formatados corretamente
- ✅ Datas em formato brasileiro (dd/MM/yyyy)
#### Teste 4.3: Filtros Personalizados
1. Selecione **"Personalizado"**
2. Configure:
- Data Início: Hoje
- Hora Início: 00:00
- Data Fim: Hoje
- Hora Fim: Hora atual
3. Desmarque algumas métricas (deixe só 3-4 marcadas)
4. Exporte PDF ou CSV
5. Verifique:
- ✅ Apenas as métricas selecionadas aparecem
- ✅ Período correto é respeitado
---
### Teste 5: Coleta Automática de Métricas
1. Abra o **Console do Navegador** (F12)
2. Vá para a aba **Network** (Rede)
3. Aguarde 30 segundos
4. Verifique:
- ✅ Aparece requisição para `salvarMetricas`
- ✅ Status 200 (sucesso)
5. No Console, digite:
```javascript
console.error("Teste de erro");
```
6. Aguarde 30 segundos
7. Verifique o card "Erros (30s)":
- ✅ Contador deve aumentar
---
## 📊 Métricas Coletadas
### Métricas de Sistema
- **CPU**: Estimativa baseada em Performance API (0-100%)
- **Memória**: `performance.memory` (Chrome) ou estimativa (0-100%)
- **Latência de Rede**: Tempo de resposta do servidor (ms)
- **Armazenamento**: Storage API ou estimativa (0-100%)
### Métricas de Aplicação
- **Usuários Online**: Contagem via query Convex
- **Mensagens/min**: Taxa de mensagens (a ser implementado)
- **Tempo de Resposta**: Latência de queries Convex (ms)
- **Erros**: Contagem de erros capturados (30s)
---
## ⚙️ Configurações Avançadas
### Alterar Intervalo de Coleta
Por padrão, métricas são coletadas a cada **30 segundos**. Para alterar:
```typescript
// Em SystemMonitorCard.svelte, linha ~52
stopCollection = startMetricsCollection(client, 30000); // 30s
```
Altere `30000` para o valor desejado em milissegundos.
### Alterar Thresholds de Cores
As cores mudam baseado nos valores:
- **Verde** (Normal): < 60%
- **Amarelo** (Atenção): 60-80%
- **Vermelho** (Crítico): > 80%
Para alterar, edite a função `getStatusColor` em `SystemMonitorCard.svelte`.
### Retenção de Dados
Por padrão, métricas são mantidas por **30 dias**. Após isso, são automaticamente deletadas.
Para alterar, edite `monitoramento.ts`:
```typescript
// Linha ~56
const dataLimite = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 dias
```
---
## 🐛 Solução de Problemas
### Métricas não aparecem
- ✅ Verifique se o backend Convex está rodando
- ✅ Abra o Console e veja se há erros
- ✅ Aguarde 30 segundos para primeira coleta
### Alertas não disparam
- ✅ Verifique se o alerta está **Ativo**
- ✅ Verifique se o threshold está configurado corretamente
- ✅ Lembre-se: alertas não duplicam em 5 minutos
### Relatórios vazios
- ✅ Verifique se há métricas no período selecionado
- ✅ Aguarde pelo menos 1 minuto após iniciar o sistema
- ✅ Verifique se selecionou pelo menos 1 métrica
### Erro ao exportar PDF/CSV
- ✅ Verifique se instalou as dependências (`npm install`)
- ✅ Veja o Console para erros específicos
- ✅ Tente período menor (menos dados)
---
## 🎯 Próximos Passos (Melhorias Futuras)
1. **Gráficos Visuais**: Adicionar charts com histórico
2. **Email de Alertas**: Integrar com sistema de email
3. **Dashboard Personalizado**: Permitir usuário escolher métricas
4. **Métricas de Backend**: CPU/RAM real do servidor Node.js
5. **Alertas Inteligentes**: Machine learning para anomalias
6. **Webhooks**: Notificar sistemas externos
7. **Métricas Customizadas**: Permitir criar métricas personalizadas
---
## ✨ Funcionalidades Destacadas
-**Monitoramento em Tempo Real**: Atualização automática a cada 30s
-**Alertas Customizáveis**: Configure thresholds personalizados
-**Notificações Integradas**: Via chat (sino de notificações)
-**Relatórios Profissionais**: PDF e CSV com estatísticas
-**Interface Moderna**: Design responsivo com DaisyUI
-**Performance**: Coleta eficiente sem sobrecarregar o sistema
-**Histórico**: 30 dias de dados armazenados
-**Sem Duplicatas**: Alertas inteligentes (1 a cada 5 min)
---
## 📝 Notas Técnicas
- **Browser API**: Usa APIs modernas do navegador (pode não funcionar em browsers antigos)
- **Chrome Memory**: `performance.memory` só funciona em Chrome/Edge
- **Rate Limiting**: Coleta limitada a 1x/30s para evitar sobrecarga
- **Cleanup Automático**: Métricas antigas são deletadas automaticamente
- **Timezone**: Todas as datas usam timezone do navegador
- **Permissões**: Apenas usuários TI podem acessar o monitoramento
---
## 🚀 Sistema Pronto para Produção!
Todos os componentes foram implementados e testados. O sistema está robusto e profissional, pronto para uso em produção.
**Desenvolvido por**: Secretaria de Esportes de Pernambuco
**Versão**: 2.0
**Data**: Outubro 2025

View File

@@ -27,7 +27,6 @@
"vite": "^7.1.2"
},
"dependencies": {
"@convex-dev/better-auth": "^0.9.6",
"@dicebear/collection": "^9.2.4",
"@dicebear/core": "^9.2.4",
"@fullcalendar/core": "^6.1.19",
@@ -36,11 +35,9 @@
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/multimonth": "^6.1.19",
"@internationalized/date": "^3.10.0",
"@mmailaender/convex-better-auth-svelte": "^0.2.0",
"@sgse-app/backend": "*",
"@tanstack/svelte-form": "^1.19.2",
"@types/papaparse": "^5.3.14",
"better-auth": "1.3.27",
"convex": "^1.28.0",
"convex-svelte": "^0.0.11",
"date-fns": "^4.1.0",

View File

@@ -1,7 +0,0 @@
import { createAuthClient } from "better-auth/client";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "http://localhost:5173",
plugins: [convexClient()],
});

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { authStore } from "$lib/stores/auth.svelte";
import { loginModalStore } from "$lib/stores/loginModal.svelte";
interface Props {
recurso: string;
acao: string;
children?: any;
}
let { recurso, acao, children }: Props = $props();
let verificando = $state(true);
let permitido = $state(false);
const permissaoQuery = $derived(
authStore.usuario
? useQuery(api.permissoesAcoes.verificarAcao, {
usuarioId: authStore.usuario._id as Id<"usuarios">,
recurso,
acao,
})
: null
);
$effect(() => {
if (!authStore.autenticado) {
verificando = false;
permitido = false;
const currentPath = window.location.pathname;
loginModalStore.open(currentPath);
return;
}
if (permissaoQuery?.error) {
verificando = false;
permitido = false;
} else if (permissaoQuery && !permissaoQuery.isLoading) {
// Backend retorna null quando permitido
verificando = false;
permitido = true;
}
});
</script>
{#if verificando}
<div class="flex items-center justify-center min-h-screen">
<div class="text-center">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="mt-4 text-base-content/70">Verificando permissões...</p>
</div>
</div>
{:else if permitido}
{@render children?.()}
{:else}
<div class="flex items-center justify-center min-h-screen">
<div class="text-center">
<div class="p-4 bg-error/10 rounded-full inline-block mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h2 class="text-2xl font-bold text-base-content mb-2">Acesso Negado</h2>
<p class="text-base-content/70">
Você não tem permissão para acessar esta ação.
</p>
</div>
</div>
{/if}

View File

@@ -1,85 +1,66 @@
<script lang="ts">
import { page } from "$app/state";
import MenuProtection from "$lib/components/MenuProtection.svelte";
import ActionGuard from "$lib/components/ActionGuard.svelte";
import { Toaster } from "svelte-sonner";
const { children } = $props();
// Mapa de rotas para verificação de permissões
const ROUTE_PERMISSIONS: Record<string, { path: string; requireGravar?: boolean }> = {
// Recursos Humanos
"/recursos-humanos": { path: "/recursos-humanos" },
"/recursos-humanos/funcionarios": { path: "/recursos-humanos/funcionarios" },
"/recursos-humanos/funcionarios/cadastro": { path: "/recursos-humanos/funcionarios", requireGravar: true },
"/recursos-humanos/funcionarios/excluir": { path: "/recursos-humanos/funcionarios", requireGravar: true },
"/recursos-humanos/funcionarios/relatorios": { path: "/recursos-humanos/funcionarios" },
"/recursos-humanos/simbolos": { path: "/recursos-humanos/simbolos" },
"/recursos-humanos/simbolos/cadastro": { path: "/recursos-humanos/simbolos", requireGravar: true },
// Outros menus
"/financeiro": { path: "/financeiro" },
"/controladoria": { path: "/controladoria" },
"/licitacoes": { path: "/licitacoes" },
"/compras": { path: "/compras" },
"/juridico": { path: "/juridico" },
"/comunicacao": { path: "/comunicacao" },
"/programas-esportivos": { path: "/programas-esportivos" },
"/secretaria-executiva": { path: "/secretaria-executiva" },
"/gestao-pessoas": { path: "/gestao-pessoas" },
"/ti": { path: "/ti" },
};
// Resolver recurso/ação a partir da rota
const routeAction = $derived.by(() => {
const p = page.url.pathname;
if (p === "/" || p === "/solicitar-acesso") return null;
// Obter configuração para a rota atual
const getCurrentRouteConfig = $derived.by(() => {
const currentPath = page.url.pathname;
// Verificar correspondência exata
if (ROUTE_PERMISSIONS[currentPath]) {
return ROUTE_PERMISSIONS[currentPath];
// Funcionários
if (p.startsWith("/recursos-humanos/funcionarios")) {
if (p.includes("/cadastro"))
return { recurso: "funcionarios", acao: "criar" };
if (p.includes("/excluir"))
return { recurso: "funcionarios", acao: "excluir" };
if (p.includes("/editar") || p.includes("/funcionarioId"))
return { recurso: "funcionarios", acao: "editar" };
return { recurso: "funcionarios", acao: "listar" };
}
// Verificar rotas dinâmicas (com [id])
if (currentPath.includes("/editar") || currentPath.includes("/funcionarioId") || currentPath.includes("/simboloId")) {
// Extrair o caminho base
if (currentPath.includes("/funcionarios/")) {
return { path: "/recursos-humanos/funcionarios", requireGravar: true };
}
if (currentPath.includes("/simbolos/")) {
return { path: "/recursos-humanos/simbolos", requireGravar: true };
}
// Símbolos
if (p.startsWith("/recursos-humanos/simbolos")) {
if (p.includes("/cadastro"))
return { recurso: "simbolos", acao: "criar" };
if (p.includes("/excluir"))
return { recurso: "simbolos", acao: "excluir" };
if (p.includes("/editar") || p.includes("/simboloId"))
return { recurso: "simbolos", acao: "editar" };
return { recurso: "simbolos", acao: "listar" };
}
// Rotas públicas (Dashboard, Solicitar Acesso, etc)
if (currentPath === "/" || currentPath === "/solicitar-acesso") {
return null;
}
// Para qualquer outra rota dentro do dashboard, verificar o primeiro segmento
const segments = currentPath.split("/").filter(Boolean);
if (segments.length > 0) {
const firstSegment = "/" + segments[0];
if (ROUTE_PERMISSIONS[firstSegment]) {
return ROUTE_PERMISSIONS[firstSegment];
}
}
// Outras áreas (uso genérico: ver)
if (p.startsWith("/financeiro"))
return { recurso: "financeiro", acao: "ver" };
if (p.startsWith("/controladoria"))
return { recurso: "controladoria", acao: "ver" };
if (p.startsWith("/licitacoes"))
return { recurso: "licitacoes", acao: "ver" };
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" };
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" };
if (p.startsWith("/comunicacao"))
return { recurso: "comunicacao", acao: "ver" };
if (p.startsWith("/programas-esportivos"))
return { recurso: "programas_esportivos", acao: "ver" };
if (p.startsWith("/secretaria-executiva"))
return { recurso: "secretaria_executiva", acao: "ver" };
if (p.startsWith("/gestao-pessoas"))
return { recurso: "gestao_pessoas", acao: "ver" };
return null;
});
</script>
{#if getCurrentRouteConfig}
<MenuProtection menuPath={getCurrentRouteConfig.path} requireGravar={getCurrentRouteConfig.requireGravar || false}>
<main
id="container-central"
class="w-full max-w-none px-3 lg:px-4 py-4"
>
{#if routeAction}
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
{@render children()}
</main>
</MenuProtection>
</ActionGuard>
{:else}
<main
id="container-central"
class="w-full max-w-none px-3 lg:px-4 py-4"
>
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
{@render children()}
</main>
{/if}

View File

@@ -4,16 +4,58 @@
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { goto } from "$app/navigation";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
type RoleRow = {
_id: Id<"roles">;
_creationTime: number;
nome: string;
descricao: string;
nivel: number;
setor?: string;
customizado: boolean;
editavel?: boolean;
criadoPor?: Id<"usuarios">;
};
const client = useConvexClient();
// Buscar matriz de permissões
const matrizQuery = useQuery(api.menuPermissoes.obterMatrizPermissoes, {});
// Carregar lista de roles e catálogo de recursos/ações
const rolesQuery = useQuery(api.roles.listar, {});
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
let salvando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
null
);
let busca = $state("");
let filtroRole = $state("");
// Controla quais recursos estão expandidos (mostrando as ações) por perfil
// Formato: { "roleId-recurso": true/false }
let recursosExpandidos: Record<string, boolean> = $state({});
// Cache de permissões por role
let permissoesPorRole: Record<
string,
Array<{ recurso: string; acoes: Array<string> }>
> = $state({});
async function carregarPermissoesRole(roleId: Id<"roles">) {
if (permissoesPorRole[roleId]) return;
const dados = await client.query(
api.permissoesAcoes.listarPermissoesAcoesPorRole,
{ roleId }
);
permissoesPorRole[roleId] = dados;
}
function toggleRecurso(roleId: Id<"roles">, recurso: string) {
const key = `${roleId}-${recurso}`;
recursosExpandidos[key] = !recursosExpandidos[key];
}
function isRecursoExpandido(roleId: Id<"roles">, recurso: string) {
const key = `${roleId}-${recurso}`;
return recursosExpandidos[key] ?? false;
}
function mostrarMensagem(tipo: "success" | "error", texto: string) {
mensagem = { tipo, texto };
@@ -22,89 +64,61 @@
}, 3000);
}
const dadosFiltrados = $derived.by(() => {
if (!matrizQuery.data) return [];
let resultado = matrizQuery.data;
// Filtrar por role
if (filtroRole) {
resultado = resultado.filter(r => r.role._id === filtroRole);
}
// Filtrar por busca
const rolesFiltradas = $derived.by(() => {
if (!rolesQuery.data) return [];
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>;
if (filtroRole)
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any));
if (busca.trim()) {
const buscaLower = busca.toLowerCase();
resultado = resultado.map(roleData => ({
...roleData,
permissoes: roleData.permissoes.filter(p =>
p.menuNome.toLowerCase().includes(buscaLower) ||
p.menuPath.toLowerCase().includes(buscaLower)
)
})).filter(roleData => roleData.permissoes.length > 0);
const b = busca.toLowerCase();
rs = rs.filter(
(r: RoleRow) =>
r.descricao.toLowerCase().includes(b) ||
r.nome.toLowerCase().includes(b)
);
}
return resultado;
return rs;
});
async function atualizarPermissao(
// Carregar permissões para todos os perfis filtrados quando necessário
$effect(() => {
if (rolesFiltradas && catalogoQuery.data) {
for (const roleRow of rolesFiltradas) {
if (roleRow.nivel > 1) {
carregarPermissoesRole(roleRow._id);
}
}
}
});
async function toggleAcao(
roleId: Id<"roles">,
menuPath: string,
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
valor: boolean
recurso: string,
acao: string,
conceder: boolean
) {
try {
salvando = true;
// Buscar a permissão atual
const roleData = matrizQuery.data?.find((r) => r.role._id === roleId);
const permissaoAtual = roleData?.permissoes.find((p) => p.menuPath === menuPath);
if (!permissaoAtual) {
throw new Error("Permissão não encontrada");
}
// Inicializar com valores atuais
let podeAcessar = permissaoAtual.podeAcessar;
let podeConsultar = permissaoAtual.podeConsultar;
let podeGravar = permissaoAtual.podeGravar;
// Aplicar lógica de dependências baseada no campo alterado
if (campo === "podeAcessar") {
podeAcessar = valor;
// Se desmarcou "Acessar", desmarcar tudo
if (!valor) {
podeConsultar = false;
podeGravar = false;
}
// Se marcou "Acessar", manter os outros valores como estão
} else if (campo === "podeConsultar") {
podeConsultar = valor;
// Se marcou "Consultar", marcar "Acessar" automaticamente
if (valor) {
podeAcessar = true;
} else {
// Se desmarcou "Consultar", desmarcar "Gravar"
podeGravar = false;
}
} else if (campo === "podeGravar") {
podeGravar = valor;
// Se marcou "Gravar", marcar "Consultar" e "Acessar" automaticamente
if (valor) {
podeAcessar = true;
podeConsultar = true;
}
// Se desmarcou "Gravar", manter os outros como estão
}
await client.mutation(api.menuPermissoes.atualizarPermissao, {
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
roleId,
menuPath,
podeAcessar,
podeConsultar,
podeGravar,
recurso,
acao,
conceder,
});
// Atualizar cache local
const atual = permissoesPorRole[roleId] || [];
const entry = atual.find((e) => e.recurso === recurso);
if (entry) {
const set = new Set(entry.acoes);
if (conceder) set.add(acao);
else set.delete(acao);
entry.acoes = Array.from(set);
} else {
permissoesPorRole[roleId] = [
...atual,
{ recurso, acoes: conceder ? [acao] : [] },
];
}
mostrarMensagem("success", "Permissão atualizada com sucesso!");
} catch (e: any) {
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
@@ -113,16 +127,10 @@
}
}
async function inicializarPermissoes(roleId: Id<"roles">) {
try {
salvando = true;
await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId });
mostrarMensagem("success", "Permissões inicializadas!");
} catch (e: any) {
mostrarMensagem("error", e.message || "Erro ao inicializar permissões");
} finally {
salvando = false;
}
function isConcedida(roleId: Id<"roles">, recurso: string, acao: string) {
const dados = permissoesPorRole[roleId];
const entry = dados?.find((e) => e.recurso === recurso);
return entry ? entry.acoes.includes(acao) : false;
}
</script>
@@ -132,8 +140,19 @@
<ul>
<li>
<a href="/" class="text-primary hover:text-primary-focus">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Dashboard
</a>
@@ -149,17 +168,43 @@
<div class="mb-6">
<div class="flex items-center gap-3 mb-2">
<div class="p-3 bg-primary/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-primary"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-base-content">Gerenciar Permissões de Acesso</h1>
<p class="text-base-content/60 mt-1">Configure as permissões de acesso aos menus do sistema por função</p>
<h1 class="text-3xl font-bold text-base-content">
Gerenciar Permissões de Acesso
</h1>
<p class="text-base-content/60 mt-1">
Configure as permissões de acesso aos menus do sistema por função
</p>
</div>
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Voltar
</button>
@@ -168,14 +213,38 @@
<!-- Alertas -->
{#if mensagem}
<div class="alert mb-6 shadow-lg" class:alert-success={mensagem.tipo === "success"} class:alert-error={mensagem.tipo === "error"}>
<div
class="alert mb-6 shadow-lg"
class:alert-success={mensagem.tipo === "success"}
class:alert-error={mensagem.tipo === "error"}
>
{#if mensagem.tipo === "success"}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
<span class="font-semibold">{mensagem.texto}</span>
@@ -189,13 +258,13 @@
<!-- Busca por menu -->
<div class="form-control">
<label class="label" for="busca">
<span class="label-text font-semibold">Buscar Menu</span>
<span class="label-text font-semibold">Buscar Perfil</span>
</label>
<div class="relative">
<input
id="busca"
type="text"
placeholder="Digite o nome ou caminho do menu..."
placeholder="Digite o nome/descrição do perfil..."
class="input input-bordered w-full pr-10"
bind:value={busca}
/>
@@ -227,10 +296,10 @@
bind:value={filtroRole}
>
<option value="">Todos os perfis</option>
{#if matrizQuery.data}
{#each matrizQuery.data as roleData}
<option value={roleData.role._id}>
{roleData.role.descricao} ({roleData.role.nome})
{#if rolesQuery.data}
{#each rolesQuery.data as roleRow}
<option value={roleRow._id}>
{roleRow.descricao} ({roleRow.nome})
</option>
{/each}
{/if}
@@ -272,8 +341,18 @@
<!-- Informações sobre o sistema de permissões -->
<div class="alert alert-info mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
@@ -281,9 +360,13 @@
<div>
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
<ul class="text-sm mt-1 space-y-1">
<li><strong>Acessar:</strong> Visualizar menu e acessar página</li>
<li>
<strong>Acessar:</strong> Visualizar menu e acessar página
</li>
<li><strong>Consultar:</strong> Ver dados (requer "Acessar")</li>
<li><strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")</li>
<li>
<strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")
</li>
</ul>
</div>
<div>
@@ -291,27 +374,39 @@
<ul class="text-sm mt-1 space-y-1">
<li><strong>Admin e TI:</strong> Acesso total automático</li>
<li><strong>Dashboard:</strong> Público para todos</li>
<li><strong>Perfil Customizado:</strong> Permissões personalizadas</li>
<li>
<strong>Perfil Customizado:</strong> Permissões personalizadas
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Matriz de Permissões -->
{#if matrizQuery.isLoading}
<!-- Matriz de Permissões por Ação -->
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if matrizQuery.error}
{:else if rolesQuery.error}
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>Erro ao carregar permissões: {matrizQuery.error.message}</span>
<span>Erro ao carregar perfis: {rolesQuery.error.message}</span>
</div>
{:else if matrizQuery.data}
{#if dadosFiltrados.length === 0}
{:else if rolesQuery.data && catalogoQuery.data}
{#if rolesFiltradas.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body items-center text-center">
<svg
@@ -330,7 +425,9 @@
</svg>
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
<p class="text-base-content/60">
{busca ? `Não foram encontrados menus com "${busca}"` : "Nenhuma permissão corresponde aos filtros aplicados"}
{busca
? `Não foram encontrados perfis com "${busca}"`
: "Nenhum perfil corresponde aos filtros aplicados"}
</p>
<button
class="btn btn-primary btn-sm mt-4"
@@ -345,162 +442,132 @@
</div>
{/if}
{#each dadosFiltrados as roleData}
{#each rolesFiltradas as roleRow}
{@const roleId = roleRow._id}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<div class="flex items-center gap-3 mb-2">
<h2 class="card-title text-2xl">{roleData.role.descricao}</h2>
<div class="badge badge-lg badge-primary">Nível {roleData.role.nivel}</div>
{#if roleData.role.nivel <= 1}
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
<div class="badge badge-lg badge-primary">
Nível {roleRow.nivel}
</div>
{#if roleRow.nivel <= 1}
<div class="badge badge-lg badge-success gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Acesso Total
</div>
{/if}
</div>
<p class="text-sm text-base-content/60">
<span class="font-mono bg-base-200 px-2 py-1 rounded">{roleData.role.nome}</span>
<span class="font-mono bg-base-200 px-2 py-1 rounded"
>{roleRow.nome}</span
>
</p>
</div>
{#if roleData.role.nivel > 1}
<button
class="btn btn-sm btn-outline btn-primary"
onclick={() => inicializarPermissoes(roleData.role._id)}
disabled={salvando}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Inicializar Permissões
</button>
{/if}
</div>
{#if roleData.role.nivel <= 1}
{#if roleRow.nivel <= 1}
<div class="alert alert-success shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<h3 class="font-bold">Perfil Administrativo</h3>
<div class="text-sm">Este perfil possui acesso total ao sistema automaticamente, sem necessidade de configuração manual.</div>
</div>
</div>
{:else}
<div class="stats stats-vertical lg:stats-horizontal shadow mb-4 w-full">
<div class="stat">
<div class="stat-title">Total de Menus</div>
<div class="stat-value text-primary">{roleData.permissoes.length}</div>
</div>
<div class="stat">
<div class="stat-title">Com Acesso</div>
<div class="stat-value text-info">{roleData.permissoes.filter(p => p.podeAcessar).length}</div>
</div>
<div class="stat">
<div class="stat-title">Pode Consultar</div>
<div class="stat-value text-success">{roleData.permissoes.filter(p => p.podeConsultar).length}</div>
</div>
<div class="stat">
<div class="stat-title">Pode Gravar</div>
<div class="stat-value text-warning">{roleData.permissoes.filter(p => p.podeGravar).length}</div>
<div class="text-sm">
Este perfil possui acesso total ao sistema automaticamente,
sem necessidade de configuração manual.
</div>
</div>
</div>
{:else if catalogoQuery.data}
<div class="space-y-2">
{#each catalogoQuery.data as item}
{@const recursoExpandido = isRecursoExpandido(
roleId,
item.recurso
)}
<div class="border border-base-300 rounded-lg overflow-hidden">
<!-- Cabeçalho do recurso (clicável) -->
<button
type="button"
class="w-full px-4 py-3 bg-base-200 hover:bg-base-300 transition-colors flex items-center justify-between"
onclick={() => toggleRecurso(roleId, item.recurso)}
disabled={salvando}
>
<span class="font-semibold text-lg">{item.recurso}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 transition-transform"
class:rotate-180={recursoExpandido}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead class="bg-base-200">
<tr>
<th class="w-1/3">Menu</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Acessar
<!-- Lista de ações (visível quando expandido) -->
{#if recursoExpandido}
<div class="px-4 py-3 bg-base-100 border-t border-base-300">
<div class="space-y-2">
{#each ["ver", "listar", "criar", "editar", "excluir"] as acao}
<label
class="flex items-center gap-3 cursor-pointer hover:bg-base-200 p-2 rounded transition-colors"
>
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={isConcedida(roleId, item.recurso, acao)}
disabled={salvando}
onchange={(e) =>
toggleAcao(
roleId,
item.recurso,
acao,
e.currentTarget.checked
)}
/>
<span class="flex-1 capitalize font-medium"
>{acao}</span
>
</label>
{/each}
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Consultar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Gravar
</div>
</th>
</tr>
</thead>
<tbody>
{#each roleData.permissoes as permissao}
<tr class="hover">
<td>
<div class="flex flex-col">
<span class="font-semibold">{permissao.menuNome}</span>
<span class="text-xs text-base-content/60">{permissao.menuPath}</span>
</div>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={permissao.podeAcessar}
disabled={salvando}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeAcessar",
e.currentTarget.checked
)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-info"
checked={permissao.podeConsultar}
disabled={salvando || !permissao.podeAcessar}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeConsultar",
e.currentTarget.checked
)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-success"
checked={permissao.podeGravar}
disabled={salvando || !permissao.podeConsultar}
onchange={(e) =>
atualizarPermissao(
roleData.role._id,
permissao.menuPath,
"podeGravar",
e.currentTarget.checked
)}
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
@@ -508,4 +575,3 @@
{/each}
{/if}
</ProtectedRoute>

View File

@@ -1,123 +1,6 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
import { goto } from "$app/navigation";
const client = useConvexClient();
let matriculaBusca = $state("");
let usuarioEncontrado = $state<any>(null);
let buscando = $state(false);
let salvando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
// Buscar permissões personalizadas do usuário
const permissoesQuery = $derived(
usuarioEncontrado
? useQuery(api.menuPermissoes.listarPermissoesPersonalizadas, {
matricula: usuarioEncontrado.matricula,
})
: null
);
// Buscar menus disponíveis
const menusQuery = useQuery(api.menuPermissoes.listarMenus, {});
async function buscarUsuario() {
if (!matriculaBusca.trim()) {
mensagem = { tipo: "error", texto: "Digite uma matrícula para buscar" };
return;
}
try {
buscando = true;
const usuario = await client.query(api.menuPermissoes.buscarUsuarioPorMatricula, {
matricula: matriculaBusca.trim(),
});
if (usuario) {
usuarioEncontrado = usuario;
mensagem = null;
} else {
usuarioEncontrado = null;
mensagem = { tipo: "error", texto: "Usuário não encontrado com esta matrícula" };
}
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao buscar usuário" };
} finally {
buscando = false;
}
}
async function atualizarPermissao(
menuPath: string,
campo: "podeAcessar" | "podeConsultar" | "podeGravar",
valor: boolean
) {
if (!usuarioEncontrado) return;
try {
salvando = true;
// Obter permissão atual do menu
const permissaoAtual = permissoesQuery?.data?.find((p) => p.menuPath === menuPath);
let podeAcessar = valor;
let podeConsultar = false;
let podeGravar = false;
// Aplicar lógica de dependências
if (campo === "podeGravar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = true;
} else if (campo === "podeConsultar" && valor) {
podeAcessar = true;
podeConsultar = true;
podeGravar = permissaoAtual?.podeGravar || false;
} else if (campo === "podeAcessar" && !valor) {
podeAcessar = false;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeConsultar" && !valor) {
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
podeConsultar = false;
podeGravar = false;
} else if (campo === "podeGravar" && !valor) {
podeAcessar = permissaoAtual?.podeAcessar !== undefined ? permissaoAtual.podeAcessar : false;
podeConsultar = permissaoAtual?.podeConsultar !== undefined ? permissaoAtual.podeConsultar : false;
podeGravar = false;
} else if (permissaoAtual) {
podeAcessar = permissaoAtual.podeAcessar;
podeConsultar = permissaoAtual.podeConsultar;
podeGravar = permissaoAtual.podeGravar;
}
await client.mutation(api.menuPermissoes.atualizarPermissaoPersonalizada, {
matricula: usuarioEncontrado.matricula,
menuPath,
podeAcessar,
podeConsultar,
podeGravar,
});
mensagem = { tipo: "success", texto: "Permissão personalizada atualizada!" };
setTimeout(() => {
mensagem = null;
}, 3000);
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao atualizar permissão" };
} finally {
salvando = false;
}
}
function limparBusca() {
matriculaBusca = "";
usuarioEncontrado = null;
mensagem = null;
}
</script>
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
@@ -126,8 +9,19 @@
<ul>
<li>
<a href="/" class="text-primary hover:text-primary-focus">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Dashboard
</a>
@@ -143,241 +37,71 @@
<div class="mb-6">
<div class="flex items-center gap-3 mb-2">
<div class="p-3 bg-info/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-info" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-info"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-base-content">Personalizar Permissões por Matrícula</h1>
<p class="text-base-content/60 mt-1">Configure permissões específicas para usuários individuais</p>
<h1 class="text-3xl font-bold text-base-content">
Funcionalidade descontinuada
</h1>
<p class="text-base-content/60 mt-1">
Agora as permissões são configuradas por ação em cada perfil no painel
de permissões.
</p>
</div>
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Voltar
</button>
</div>
</div>
<!-- Alertas -->
{#if mensagem}
<div class="alert mb-6 shadow-lg" class:alert-success={mensagem.tipo === "success"} class:alert-error={mensagem.tipo === "error"}>
{#if mensagem.tipo === "success"}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
<span class="font-semibold">{mensagem.texto}</span>
</div>
{/if}
<!-- Card de Busca -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title">Buscar Usuário</h2>
<p class="text-sm text-base-content/60">Digite a matrícula do usuário para personalizar suas permissões</p>
<div class="flex gap-4 mt-4">
<div class="form-control flex-1">
<label class="label" for="matricula-busca">
<span class="label-text font-semibold">Matrícula</span>
</label>
<input
id="matricula-busca"
type="text"
class="input input-bordered input-primary w-full"
placeholder="Digite a matrícula..."
bind:value={matriculaBusca}
disabled={buscando}
onkeydown={(e) => e.key === "Enter" && buscarUsuario()}
/>
</div>
<div class="flex items-end gap-2">
<button
class="btn btn-primary"
onclick={buscarUsuario}
disabled={buscando || !matriculaBusca.trim()}
>
{#if buscando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{/if}
Buscar
</button>
{#if usuarioEncontrado}
<button class="btn btn-ghost" onclick={limparBusca}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Limpar
</button>
{/if}
</div>
</div>
</div>
<div class="alert alert-info shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>
A personalização por usuário foi substituída por <strong
>permissões por ação</strong
>
por perfil. Utilize o
<a href="/ti/painel-permissoes" class="link link-primary"
>Painel de Permissões</a
> para configurar.
</span>
</div>
<!-- Informações do Usuário -->
{#if usuarioEncontrado}
<div class="card bg-gradient-to-br from-info/10 to-info/5 shadow-xl mb-6 border-2 border-info/20">
<div class="card-body">
<div class="flex items-center gap-4">
<div class="avatar placeholder">
<div class="bg-info text-info-content rounded-full w-16">
<span class="text-2xl font-bold">{usuarioEncontrado.nome.charAt(0)}</span>
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-bold">{usuarioEncontrado.nome}</h3>
<div class="flex gap-4 mt-1 text-sm">
<span class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
<strong>Matrícula:</strong> {usuarioEncontrado.matricula}
</span>
<span class="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<strong>Email:</strong> {usuarioEncontrado.email}
</span>
</div>
</div>
<div class="text-right">
<div class="badge badge-primary badge-lg">
Nível {usuarioEncontrado.role.nivel}
</div>
<p class="text-sm mt-1">{usuarioEncontrado.role.descricao}</p>
<div class="badge mt-2" class:badge-success={usuarioEncontrado.ativo} class:badge-error={!usuarioEncontrado.ativo}>
{usuarioEncontrado.ativo ? "Ativo" : "Inativo"}
</div>
</div>
</div>
</div>
</div>
<!-- Tabela de Permissões -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Permissões Personalizadas</h2>
<div class="alert alert-info mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="text-sm">
<strong>Permissões personalizadas sobrepõem as permissões da função.</strong><br />
Configure apenas os menus que deseja personalizar para este usuário.
</p>
</div>
</div>
{#if menusQuery.isLoading}
<div class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else if menusQuery.data}
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead class="bg-base-200">
<tr>
<th class="w-1/3">Menu</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Acessar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Consultar
</div>
</th>
<th class="text-center">
<div class="flex items-center justify-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Gravar
</div>
</th>
<th class="text-center">Status</th>
</tr>
</thead>
<tbody>
{#each menusQuery.data as menu}
{@const permissao = permissoesQuery?.data?.find((p) => p.menuPath === menu.path)}
<tr class="hover">
<td>
<div class="flex flex-col">
<span class="font-semibold">{menu.nome}</span>
<span class="text-xs text-base-content/60">{menu.path}</span>
</div>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={permissao?.podeAcessar || false}
disabled={salvando}
onchange={(e) =>
atualizarPermissao(menu.path, "podeAcessar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-info"
checked={permissao?.podeConsultar || false}
disabled={salvando || !permissao?.podeAcessar}
onchange={(e) =>
atualizarPermissao(menu.path, "podeConsultar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
<input
type="checkbox"
class="checkbox checkbox-success"
checked={permissao?.podeGravar || false}
disabled={salvando || !permissao?.podeConsultar}
onchange={(e) =>
atualizarPermissao(menu.path, "podeGravar", e.currentTarget.checked)}
/>
</td>
<td class="text-center">
{#if permissao}
<div class="badge badge-warning badge-sm">Personalizado</div>
{:else}
<div class="badge badge-ghost badge-sm">Padrão da Função</div>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
</ProtectedRoute>

View File

@@ -1,3 +0,0 @@
import { createSvelteKitHandler } from "@mmailaender/convex-better-auth-svelte/sveltekit";
export const { GET, POST } = createSvelteKitHandler();

262
bun.lock
View File

@@ -6,12 +6,11 @@
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"chart.js": "^4.5.1",
"lucide-svelte": "^0.546.0",
"lucide-svelte": "^0.548.0",
"svelte-chartjs": "^3.1.5",
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"fdir": "^6.5.0",
"@biomejs/biome": "^2.3.2",
"turbo": "^2.5.4",
},
"optionalDependencies": {
@@ -22,7 +21,6 @@
"name": "web",
"version": "0.0.1",
"dependencies": {
"@convex-dev/better-auth": "^0.9.6",
"@dicebear/collection": "^9.2.4",
"@dicebear/core": "^9.2.4",
"@fullcalendar/core": "^6.1.19",
@@ -31,11 +29,9 @@
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/multimonth": "^6.1.19",
"@internationalized/date": "^3.10.0",
"@mmailaender/convex-better-auth-svelte": "^0.2.0",
"@sgse-app/backend": "*",
"@tanstack/svelte-form": "^1.19.2",
"@types/papaparse": "^5.3.14",
"better-auth": "1.3.27",
"convex": "^1.28.0",
"convex-svelte": "^0.0.11",
"date-fns": "^4.1.0",
@@ -62,25 +58,11 @@
"vite": "^7.1.2",
},
},
"packages/auth": {
"name": "@sgse-app/auth",
"version": "1.0.0",
"dependencies": {
"better-auth": "1.3.27",
"convex": "^1.28.0",
},
"devDependencies": {
"@types/node": "^24.3.0",
"typescript": "^5.9.2",
},
},
"packages/backend": {
"name": "@sgse-app/backend",
"version": "1.0.0",
"dependencies": {
"@convex-dev/better-auth": "^0.9.6",
"@dicebear/avataaars": "^9.2.4",
"better-auth": "1.3.27",
"convex": "^1.28.0",
"nodemailer": "^7.0.10",
},
@@ -162,31 +144,23 @@
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@better-auth/core": ["@better-auth/core@1.3.27", "", { "dependencies": { "better-call": "1.0.19", "zod": "^4.1.5" } }, "sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.2", "@biomejs/cli-darwin-x64": "2.3.2", "@biomejs/cli-linux-arm64": "2.3.2", "@biomejs/cli-linux-arm64-musl": "2.3.2", "@biomejs/cli-linux-x64": "2.3.2", "@biomejs/cli-linux-x64-musl": "2.3.2", "@biomejs/cli-win32-arm64": "2.3.2", "@biomejs/cli-win32-x64": "2.3.2" }, "bin": { "biome": "bin/biome" } }, "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.1", "@biomejs/cli-darwin-x64": "2.3.1", "@biomejs/cli-linux-arm64": "2.3.1", "@biomejs/cli-linux-arm64-musl": "2.3.1", "@biomejs/cli-linux-x64": "2.3.1", "@biomejs/cli-linux-x64-musl": "2.3.1", "@biomejs/cli-win32-arm64": "2.3.1", "@biomejs/cli-win32-x64": "2.3.1" }, "bin": { "biome": "bin/biome" } }, "sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA=="],
"@convex-dev/better-auth": ["@convex-dev/better-auth@0.9.6", "", { "dependencies": { "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^3.24.4" }, "peerDependencies": { "better-auth": "1.3.27", "convex": "^1.26.2", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-wqwGnvjJmy5WZeRK3nO+o0P95brdIfBbCFzIlJeRoXOP4CgYPaDBZNFZY+W5Zx6Zvnai8WZ2wjTr+jvd9bzJ2A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ=="],
"@dicebear/adventurer": ["@dicebear/adventurer@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA=="],
@@ -314,8 +288,6 @@
"@fullcalendar/multimonth": ["@fullcalendar/multimonth@6.1.19", "", { "dependencies": { "@fullcalendar/daygrid": "~6.1.19" }, "peerDependencies": { "@fullcalendar/core": "~6.1.19" } }, "sha512-YYP8o/tjNLFRKhelwiq5ja3Jm3WDf3bfOUHf32JvAWwfotCvZjD7tYv66Nj02mQ8OWWJINa2EQGJxFHgIs14aA=="],
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
"@internationalized/date": ["@internationalized/date@3.10.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -330,38 +302,6 @@
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
"@mmailaender/convex-better-auth-svelte": ["@mmailaender/convex-better-auth-svelte@0.2.0", "", { "dependencies": { "is-network-error": "^1.1.0" }, "peerDependencies": { "@convex-dev/better-auth": "^0.9.0", "better-auth": "^1.3.27", "convex": "^1.27.0", "convex-svelte": "^0.0.11", "svelte": "^5.0.0" } }, "sha512-qzahOJg30xErb4ZW+aeszQw4ydhCmKFXn8CeRSA77YxR/dDMgZl+vdWLE4EKsDN0Jd748ecWMnk1fDNNUdgDcg=="],
"@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="],
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="],
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="],
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="],
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="],
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="],
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="],
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="],
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="],
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="],
"@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
@@ -408,63 +348,57 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="],
"@sgse-app/auth": ["@sgse-app/auth@workspace:packages/auth"],
"@sgse-app/backend": ["@sgse-app/backend@workspace:packages/backend"],
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ=="],
"@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-BciDJ5hkyYEGBBKMbjGB1A/Zq8bYZ41Zo9BMnGdKF6QD1fY4zIkYx6zui/0CHaVGnv6h0iy8y4rnPX9CPCAPyQ=="],
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ=="],
"@smithy/core": ["@smithy/core@3.17.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.0", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.3", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw=="],
"@smithy/core": ["@smithy/core@3.17.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-stream": "^4.5.4", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/property-provider": "^4.2.3", "@smithy/types": "^4.8.0", "@smithy/url-parser": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw=="],
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.3", "@smithy/querystring-builder": "^4.2.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw=="],
"@smithy/hash-node": ["@smithy/hash-node@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q=="],
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA=="],
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.5", "", { "dependencies": { "@smithy/core": "^3.17.1", "@smithy/middleware-serde": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/shared-ini-file-loader": "^4.3.3", "@smithy/types": "^4.8.0", "@smithy/url-parser": "^4.2.3", "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.6", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-serde": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@smithy/url-parser": "^4.2.4", "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" } }, "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/protocol-http": "^5.3.3", "@smithy/service-error-classification": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-retry": "^4.2.3", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/protocol-http": "^5.3.4", "@smithy/service-error-classification": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "@smithy/util-middleware": "^4.2.4", "@smithy/util-retry": "^4.2.4", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.4", "", { "dependencies": { "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA=="],
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.3", "", { "dependencies": { "@smithy/property-provider": "^4.2.3", "@smithy/shared-ini-file-loader": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA=="],
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.3", "", { "dependencies": { "@smithy/abort-controller": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/querystring-builder": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.4", "", { "dependencies": { "@smithy/abort-controller": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/querystring-builder": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw=="],
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ=="],
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA=="],
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0" } }, "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g=="],
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1" } }, "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ=="],
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.3.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.3", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.3", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA=="],
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.4", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.1", "", { "dependencies": { "@smithy/core": "^3.17.1", "@smithy/middleware-endpoint": "^4.3.5", "@smithy/middleware-stack": "^4.2.3", "@smithy/protocol-http": "^5.3.3", "@smithy/types": "^4.8.0", "@smithy/util-stream": "^4.5.4", "tslib": "^2.6.2" } }, "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.2", "", { "dependencies": { "@smithy/core": "^3.17.2", "@smithy/middleware-endpoint": "^4.3.6", "@smithy/middleware-stack": "^4.2.4", "@smithy/protocol-http": "^5.3.4", "@smithy/types": "^4.8.1", "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" } }, "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg=="],
"@smithy/types": ["@smithy/types@4.8.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ=="],
"@smithy/types": ["@smithy/types@4.8.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.3", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw=="],
"@smithy/url-parser": ["@smithy/url-parser@4.2.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg=="],
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
@@ -476,19 +410,19 @@
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.4", "", { "dependencies": { "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.6", "", { "dependencies": { "@smithy/config-resolver": "^4.4.0", "@smithy/credential-provider-imds": "^4.2.3", "@smithy/node-config-provider": "^4.3.3", "@smithy/property-provider": "^4.2.3", "@smithy/smithy-client": "^4.9.1", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.7", "", { "dependencies": { "@smithy/config-resolver": "^4.4.1", "@smithy/credential-provider-imds": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", "@smithy/smithy-client": "^4.9.2", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-6hinjVqec0WYGsqN7h9hL/ywfULmJJNXGXnNZW7jrIn/cFuC/aVlVaiDfBIJEvKcOrmN8/EgsW69eY0gXABeHw=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg=="],
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.3", "", { "dependencies": { "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw=="],
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.4", "", { "dependencies": { "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.3", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.3", "@smithy/types": "^4.8.0", "tslib": "^2.6.2" } }, "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg=="],
"@smithy/util-retry": ["@smithy/util-retry@4.2.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.4", "@smithy/types": "^4.8.1", "tslib": "^2.6.2" } }, "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.4", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.4", "@smithy/node-http-handler": "^4.4.3", "@smithy/types": "^4.8.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.5", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.5", "@smithy/node-http-handler": "^4.4.4", "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w=="],
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
@@ -502,7 +436,7 @@
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.1.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.48.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-CuwgzfHyc8OGI0HNa7ISQHN8u8XyLGM4jeP8+PYig2B15DD9H39KvwQJiUbGU44VsLx3NfwH4OXavIjvp7/6Ww=="],
"@sveltejs/kit": ["@sveltejs/kit@2.48.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-jf8mx3yctRXE9hvixgcqqK94YI2hDnbxI/12Upkz99XFMvxnJKCMzvz0j7lmbXSyBSNEycWO5xHvi7b73y9qkQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
@@ -532,7 +466,7 @@
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
@@ -540,7 +474,7 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.16", "", { "dependencies": { "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "tailwindcss": "4.1.16" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg=="],
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.3", "", {}, "sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg=="],
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.4", "", {}, "sha512-eq+PpuutUyubXu+ycC1GIiVwBs86NF/8yYJJAKSpPcJLWl6R/761F1H4F/9ziX6zKezltFUH1ah3Cz8Ah+KJrw=="],
"@tanstack/form-core": ["@tanstack/form-core@1.24.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w=="],
@@ -558,7 +492,7 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
"@types/nodemailer": ["@types/nodemailer@7.0.3", "", { "dependencies": { "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, "sha512-fC8w49YQ868IuPWRXqPfLf+MuTRex5Z1qxMoG8rr70riqqbOp2F5xgOKE9fODEBPzpnvjkJXFgK6IL2xgMSTnA=="],
@@ -570,27 +504,21 @@
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.20", "", { "bin": "dist/cli.js" }, "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ=="],
"better-auth": ["better-auth@1.3.27", "", { "dependencies": { "@better-auth/core": "1.3.27", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w=="],
"better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
"bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="],
"browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
"browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
@@ -602,15 +530,11 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
"convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react"], "bin": "bin/main.js" }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="],
"convex-helpers": ["convex-helpers@0.1.104", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.24.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.22.4 || ^4.0.15" }, "optionalPeers": ["hono"], "bin": "bin.cjs" }, "sha512-7CYvx7T3K6n+McDTK4ZQaQNNGBzq5aWezpjzsKbOxPXx7oNcTP9wrpef3JxeXWFzkByJv5hRCjseh9B7eNJ7Ig=="],
"convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="],
"convex-svelte": ["convex-svelte@0.0.11", "", { "peerDependencies": { "convex": "^1.10.0", "svelte": "^5.0.0" } }, "sha512-N/29gg5Zqy72vKL4xHSLk3jGwXVKIWXPs6xzq6KxGL84y/D6hG85pG2CPOzn08EzMmByts5FTkJ5p3var6yDng=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
@@ -624,33 +548,31 @@
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.4.2", "", {}, "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="],
"dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.241", "", {}, "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w=="],
"electron-to-chromium": ["electron-to-chromium@1.5.243", "", {}, "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g=="],
"emoji-picker-element": ["emoji-picker-element@1.27.0", "", {}, "sha512-CeN9g5/kq41+BfYPDpAbE2ejZRHbs1faFDmU9+E9wGA4JWLkok9zo1hwcAFnUhV4lPR3ZuLHiJxNG1mpjoF4TQ=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": "bin/esbuild" }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.1.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A=="],
"esrap": ["esrap@2.1.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg=="],
"fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="],
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
@@ -664,13 +586,9 @@
"iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="],
"is-network-error": ["is-network-error@1.3.0", "", {}, "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jspdf": ["jspdf@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.26.9", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ=="],
@@ -678,8 +596,6 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
@@ -706,7 +622,7 @@
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"lucide-svelte": ["lucide-svelte@0.546.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-vCvBUlFapD59ivX1b/i7wdUadSgC/3gQGvrGEZjSecOlThT+UR+X5UxdVEakHuhniTrSX0nJ2WrY5r25SVDtyQ=="],
"lucide-svelte": ["lucide-svelte@0.548.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-aW2BfHWBLWf/XPSKytTPV16AWfFeFIJeUyOg7eHY2rhzVQ0u0LIvoS4pm2oskr+OJVw+NsS8fPvlBVqPfUO1XQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -716,11 +632,9 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
"node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"nodemailer": ["nodemailer@7.0.10", "", {}, "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w=="],
@@ -742,40 +656,22 @@
"preact": ["preact@10.12.1", "", {}, "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="],
"prettier": ["prettier@3.6.2", "", { "bin": "bin/prettier.cjs" }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
"remeda": ["remeda@2.32.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-BZx9DsT4FAgXDTOdgJIc5eY6ECIXMwtlSPQoPglF20ycSWigttDDe88AozEsPPT4OWk5NujroGSBC1phw5uU+w=="],
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
"rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="],
"runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
@@ -786,11 +682,11 @@
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
"svelte": ["svelte@5.42.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+8dUmdJGvKSWEfbAgIaUmpD97s1bBAGxEf6s7wQonk+HNdMmrBZtpStzRypRqrYBFUmmhaUgBHUjraE8gLqWAw=="],
"svelte": ["svelte@5.43.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA=="],
"svelte-chartjs": ["svelte-chartjs@3.1.5", "", { "peerDependencies": { "chart.js": "^3.5.0 || ^4.0.0", "svelte": "^4.0.0" } }, "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA=="],
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": "bin/svelte-check" }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
"svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="],
@@ -808,9 +704,7 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="],
"turbo": ["turbo@2.5.8", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.8", "turbo-darwin-arm64": "2.5.8", "turbo-linux-64": "2.5.8", "turbo-linux-arm64": "2.5.8", "turbo-windows-64": "2.5.8", "turbo-windows-arm64": "2.5.8" }, "bin": "bin/turbo" }, "sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w=="],
"turbo": ["turbo@2.5.8", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.8", "turbo-darwin-arm64": "2.5.8", "turbo-linux-64": "2.5.8", "turbo-linux-arm64": "2.5.8", "turbo-windows-64": "2.5.8", "turbo-windows-arm64": "2.5.8" }, "bin": { "turbo": "bin/turbo" } }, "sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ=="],
@@ -824,21 +718,17 @@
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
"vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
"vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"web": ["web@workspace:apps/web"],
@@ -850,13 +740,23 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@convex-dev/better-auth/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@sveltejs/kit/@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"convex/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": "bin/esbuild" }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"convex/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],

View File

@@ -17,14 +17,13 @@
"dev:setup": "turbo -F @sgse-app/backend dev:setup"
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"fdir": "^6.5.0",
"@biomejs/biome": "^2.3.2",
"turbo": "^2.5.4"
},
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"chart.js": "^4.5.1",
"lucide-svelte": "^0.546.0",
"lucide-svelte": "^0.548.0",
"svelte-chartjs": "^3.1.5"
},
"optionalDependencies": {

View File

@@ -1,16 +0,0 @@
{
"name": "@sgse-app/auth",
"version": "1.0.0",
"scripts": {},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@types/node": "^24.3.0",
"typescript": "^5.9.2"
},
"dependencies": {
"convex": "^1.28.0",
"better-auth": "1.3.27"
}
}

View File

@@ -10,11 +10,8 @@
import type * as autenticacao from "../autenticacao.js";
import type * as auth_utils from "../auth/utils.js";
import type * as auth from "../auth.js";
import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js";
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
import type * as betterAuth_adapter from "../betterAuth/adapter.js";
import type * as betterAuth_auth from "../betterAuth/auth.js";
import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.js";
import type * as criarFuncionarioTeste from "../criarFuncionarioTeste.js";
@@ -37,6 +34,7 @@ import type * as migrarParaTimes from "../migrarParaTimes.js";
import type * as migrarUsuariosAdmin from "../migrarUsuariosAdmin.js";
import type * as monitoramento from "../monitoramento.js";
import type * as perfisCustomizados from "../perfisCustomizados.js";
import type * as permissoesAcoes from "../permissoesAcoes.js";
import type * as roles from "../roles.js";
import type * as saldoFerias from "../saldoFerias.js";
import type * as seed from "../seed.js";
@@ -65,11 +63,8 @@ import type {
declare const fullApi: ApiFromModules<{
autenticacao: typeof autenticacao;
"auth/utils": typeof auth_utils;
auth: typeof auth;
"betterAuth/_generated/api": typeof betterAuth__generated_api;
"betterAuth/_generated/server": typeof betterAuth__generated_server;
"betterAuth/adapter": typeof betterAuth_adapter;
"betterAuth/auth": typeof betterAuth_auth;
chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail;
criarFuncionarioTeste: typeof criarFuncionarioTeste;
@@ -92,6 +87,7 @@ declare const fullApi: ApiFromModules<{
migrarUsuariosAdmin: typeof migrarUsuariosAdmin;
monitoramento: typeof monitoramento;
perfisCustomizados: typeof perfisCustomizados;
permissoesAcoes: typeof permissoesAcoes;
roles: typeof roles;
saldoFerias: typeof saldoFerias;
seed: typeof seed;
@@ -114,952 +110,4 @@ export declare const internal: FilterApi<
FunctionReference<any, "internal">
>;
export declare const components: {
betterAuth: {
adapter: {
create: FunctionReference<
"mutation",
"internal",
{
input:
| {
data: {
createdAt: number;
email: string;
emailVerified: boolean;
image?: null | string;
name: string;
updatedAt: number;
userId?: null | string;
};
model: "user";
}
| {
data: {
createdAt: number;
expiresAt: number;
ipAddress?: null | string;
token: string;
updatedAt: number;
userAgent?: null | string;
userId: string;
};
model: "session";
}
| {
data: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId: string;
createdAt: number;
idToken?: null | string;
password?: null | string;
providerId: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt: number;
userId: string;
};
model: "account";
}
| {
data: {
createdAt: number;
expiresAt: number;
identifier: string;
updatedAt: number;
value: string;
};
model: "verification";
}
| {
data: {
createdAt: number;
privateKey: string;
publicKey: string;
};
model: "jwks";
};
onCreateHandle?: string;
select?: Array<string>;
},
any
>;
deleteMany: FunctionReference<
"mutation",
"internal",
{
input:
| {
model: "user";
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onDeleteHandle?: string;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
},
any
>;
deleteOne: FunctionReference<
"mutation",
"internal",
{
input:
| {
model: "user";
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onDeleteHandle?: string;
},
any
>;
findMany: FunctionReference<
"query",
"internal",
{
limit?: number;
model: "user" | "session" | "account" | "verification" | "jwks";
offset?: number;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
sortBy?: { direction: "asc" | "desc"; field: string };
where?: Array<{
connector?: "AND" | "OR";
field: string;
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
},
any
>;
findOne: FunctionReference<
"query",
"internal",
{
model: "user" | "session" | "account" | "verification" | "jwks";
select?: Array<string>;
where?: Array<{
connector?: "AND" | "OR";
field: string;
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
},
any
>;
updateMany: FunctionReference<
"mutation",
"internal",
{
input:
| {
model: "user";
update: {
createdAt?: number;
email?: string;
emailVerified?: boolean;
image?: null | string;
name?: string;
updatedAt?: number;
userId?: null | string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
update: {
createdAt?: number;
expiresAt?: number;
ipAddress?: null | string;
token?: string;
updatedAt?: number;
userAgent?: null | string;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
update: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId?: string;
createdAt?: number;
idToken?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt?: number;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
update: {
createdAt?: number;
expiresAt?: number;
identifier?: string;
updatedAt?: number;
value?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
update: {
createdAt?: number;
privateKey?: string;
publicKey?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onUpdateHandle?: string;
paginationOpts: {
cursor: string | null;
endCursor?: string | null;
id?: number;
maximumBytesRead?: number;
maximumRowsRead?: number;
numItems: number;
};
},
any
>;
updateOne: FunctionReference<
"mutation",
"internal",
{
input:
| {
model: "user";
update: {
createdAt?: number;
email?: string;
emailVerified?: boolean;
image?: null | string;
name?: string;
updatedAt?: number;
userId?: null | string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "name"
| "email"
| "emailVerified"
| "image"
| "createdAt"
| "updatedAt"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "session";
update: {
createdAt?: number;
expiresAt?: number;
ipAddress?: null | string;
token?: string;
updatedAt?: number;
userAgent?: null | string;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
| "token"
| "createdAt"
| "updatedAt"
| "ipAddress"
| "userAgent"
| "userId"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "account";
update: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId?: string;
createdAt?: number;
idToken?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
refreshTokenExpiresAt?: null | number;
scope?: null | string;
updatedAt?: number;
userId?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "accountId"
| "providerId"
| "userId"
| "accessToken"
| "refreshToken"
| "idToken"
| "accessTokenExpiresAt"
| "refreshTokenExpiresAt"
| "scope"
| "password"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "verification";
update: {
createdAt?: number;
expiresAt?: number;
identifier?: string;
updatedAt?: number;
value?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
| "value"
| "expiresAt"
| "createdAt"
| "updatedAt"
| "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
}
| {
model: "jwks";
update: {
createdAt?: number;
privateKey?: string;
publicKey?: string;
};
where?: Array<{
connector?: "AND" | "OR";
field: "publicKey" | "privateKey" | "createdAt" | "_id";
operator?:
| "lt"
| "lte"
| "gt"
| "gte"
| "eq"
| "in"
| "not_in"
| "ne"
| "contains"
| "starts_with"
| "ends_with";
value:
| string
| number
| boolean
| Array<string>
| Array<number>
| null;
}>;
};
onUpdateHandle?: string;
},
any
>;
};
};
};
export declare const components: {};

View File

@@ -1,6 +1,12 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { hashPassword, verifyPassword, generateToken, validarMatricula, validarSenha } from "./auth/utils";
import {
hashPassword,
verifyPassword,
generateToken,
validarMatricula,
validarSenha,
} from "./auth/utils";
import { registrarLogin } from "./logsLogin";
import { Id } from "./_generated/dataModel";
@@ -10,7 +16,7 @@ import { Id } from "./_generated/dataModel";
async function verificarBloqueioUsuario(ctx: any, usuarioId: Id<"usuarios">) {
const bloqueio = await ctx.db
.query("bloqueiosUsuarios")
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
.withIndex("by_usuario", (q: any) => q.eq("usuarioId", usuarioId))
.filter((q: any) => q.eq(q.field("ativo"), true))
.first();
@@ -102,7 +108,9 @@ export const login = mutation({
} else {
usuario = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matriculaOuEmail))
.withIndex("by_matricula", (q) =>
q.eq("matricula", args.matriculaOuEmail)
)
.first();
}
@@ -122,7 +130,10 @@ export const login = mutation({
}
// Verificar se usuário está bloqueado
if (usuario.bloqueado || (await verificarBloqueioUsuario(ctx, usuario._id))) {
if (
usuario.bloqueado ||
(await verificarBloqueioUsuario(ctx, usuario._id))
) {
await registrarLogin(ctx, {
usuarioId: usuario._id,
matriculaOuEmail: args.matriculaOuEmail,
@@ -172,7 +183,9 @@ export const login = mutation({
userAgent: args.userAgent,
});
const minutosRestantes = Math.ceil((TEMPO_BLOQUEIO - tempoDecorrido) / 60000);
const minutosRestantes = Math.ceil(
(TEMPO_BLOQUEIO - tempoDecorrido) / 60000
);
return {
sucesso: false as const,
erro: `Conta temporariamente bloqueada. Tente novamente em ${minutosRestantes} minutos.`,
@@ -192,7 +205,8 @@ export const login = mutation({
if (!senhaValida) {
// Incrementar tentativas
const novasTentativas = tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
const novasTentativas =
tempoDecorrido > TEMPO_BLOQUEIO ? 1 : tentativasRecentes + 1;
await ctx.db.patch(usuario._id, {
tentativasLogin: novasTentativas,
@@ -367,7 +381,10 @@ export const verificarSessao = query({
.first();
if (!sessao || !sessao.ativo) {
return { valido: false as const, motivo: "Sessão não encontrada ou inativa" };
return {
valido: false as const,
motivo: "Sessão não encontrada ou inativa",
};
}
// Verificar se sessão expirou
@@ -380,7 +397,10 @@ export const verificarSessao = query({
// Buscar usuário
const usuario = await ctx.db.get(sessao.usuarioId);
if (!usuario || !usuario.ativo) {
return { valido: false as const, motivo: "Usuário não encontrado ou inativo" };
return {
valido: false as const,
motivo: "Usuário não encontrado ou inativo",
};
}
// Buscar role
@@ -511,4 +531,3 @@ export const alterarSenha = mutation({
return { sucesso: true as const };
},
});

View File

@@ -1,54 +0,0 @@
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { components } from "./_generated/api";
import { type DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { betterAuth } from "better-auth";
import schema from "./betterAuth/schema";
// Configurações de ambiente para produção
const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL || "http://localhost:5173";
const authSecret = process.env.BETTER_AUTH_SECRET;
// The component client has methods needed for integrating Convex with Better Auth,
// as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth, {
local: {
schema: schema as any,
},
});
export const createAuth = (
ctx: GenericCtx<DataModel>,
{ optionsOnly } = { optionsOnly: false }
) => {
return betterAuth({
// Secret para criptografia de tokens - OBRIGATÓRIO em produção
secret: authSecret,
// disable logging when createAuth is called just to generate options.
// this is not required, but there's a lot of noise in logs without it.
logger: {
disabled: optionsOnly,
},
baseURL: siteUrl,
database: authComponent.adapter(ctx),
// Configure simple, non-verified email/password to get started
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
plugins: [
// The Convex plugin is required for Convex compatibility
convex(),
],
});
};
// Example function for getting the current user
// Feel free to edit, omit, etc.
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
return authComponent.getAuthUser(ctx as any);
},
});

View File

@@ -1,13 +0,0 @@
import { createApi } from "@convex-dev/better-auth";
import schema from "./schema";
import { createAuth } from "../auth";
export const {
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
} = createApi(schema, createAuth);

View File

@@ -1,5 +0,0 @@
import { createAuth } from "../auth";
import { getStaticAuth } from "@convex-dev/better-auth";
// Export a static instance for Better Auth schema generation
export const auth = getStaticAuth(createAuth);

View File

@@ -1,5 +0,0 @@
import { defineComponent } from "convex/server";
const component = defineComponent("betterAuth");
export default component;

View File

@@ -1,70 +0,0 @@
// This file is auto-generated. Do not edit this file manually.
// To regenerate the schema, run:
// `npx @better-auth/cli generate --output undefined -y`
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export const tables = {
user: defineTable({
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
image: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
userId: v.optional(v.union(v.null(), v.string())),
})
.index("email_name", ["email","name"])
.index("name", ["name"])
.index("userId", ["userId"]),
session: defineTable({
expiresAt: v.number(),
token: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
ipAddress: v.optional(v.union(v.null(), v.string())),
userAgent: v.optional(v.union(v.null(), v.string())),
userId: v.string(),
})
.index("expiresAt", ["expiresAt"])
.index("expiresAt_userId", ["expiresAt","userId"])
.index("token", ["token"])
.index("userId", ["userId"]),
account: defineTable({
accountId: v.string(),
providerId: v.string(),
userId: v.string(),
accessToken: v.optional(v.union(v.null(), v.string())),
refreshToken: v.optional(v.union(v.null(), v.string())),
idToken: v.optional(v.union(v.null(), v.string())),
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
scope: v.optional(v.union(v.null(), v.string())),
password: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("accountId", ["accountId"])
.index("accountId_providerId", ["accountId","providerId"])
.index("providerId_userId", ["providerId","userId"])
.index("userId", ["userId"]),
verification: defineTable({
identifier: v.string(),
value: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("expiresAt", ["expiresAt"])
.index("identifier", ["identifier"]),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
}),
};
const schema = defineSchema(tables);
export default schema;

View File

@@ -1,7 +1,4 @@
import { defineApp } from "convex/server";
import betterAuth from "./betterAuth/convex.config";
const app = defineApp();
app.use(betterAuth);
export default app;

View File

@@ -1,7 +1,14 @@
import { v } from "convex/values";
import { mutation, query, action, internalMutation } from "./_generated/server";
import {
mutation,
query,
action,
internalMutation,
internalQuery,
} from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { renderizarTemplate } from "./templatesMensagens";
import { internal } from "./_generated/api";
/**
* Enfileirar email para envio
@@ -15,7 +22,10 @@ export const enfileirarEmail = mutation({
templateId: v.optional(v.id("templatesMensagens")),
enviadoPorId: v.id("usuarios"),
},
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
returns: v.object({
sucesso: v.boolean(),
emailId: v.optional(v.id("notificacoesEmail")),
}),
handler: async (ctx, args) => {
// Validar email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -51,7 +61,10 @@ export const enviarEmailComTemplate = mutation({
variaveis: v.any(), // Record<string, string>
enviadoPorId: v.id("usuarios"),
},
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
returns: v.object({
sucesso: v.boolean(),
emailId: v.optional(v.id("notificacoesEmail")),
}),
handler: async (ctx, args) => {
// Buscar template
const template = await ctx.db
@@ -90,26 +103,32 @@ export const enviarEmailComTemplate = mutation({
*/
export const listarFilaEmails = query({
args: {
status: v.optional(v.union(
v.literal("pendente"),
v.literal("enviando"),
v.literal("enviado"),
v.literal("falha")
)),
status: v.optional(
v.union(
v.literal("pendente"),
v.literal("enviando"),
v.literal("enviado"),
v.literal("falha")
)
),
limite: v.optional(v.number()),
},
returns: v.array(v.any()),
handler: async (ctx, args) => {
let query = ctx.db.query("notificacoesEmail");
if (args.status) {
query = query.withIndex("by_status", (q) => q.eq("status", args.status));
} else {
query = query.withIndex("by_criado_em");
const emails = await ctx.db
.query("notificacoesEmail")
.withIndex("by_status", (q) => q.eq("status", args.status!))
.order("desc")
.take(args.limite ?? 100);
return emails;
}
const emails = await query.order("desc").take(args.limite || 100);
const emails = await ctx.db
.query("notificacoesEmail")
.withIndex("by_criado_em")
.order("desc")
.take(args.limite ?? 100);
return emails;
},
});
@@ -141,8 +160,69 @@ export const reenviarEmail = mutation({
});
/**
* Action para enviar email REAL usando nodemailer
* Action para enviar email (será implementado com nodemailer)
*
* NOTA: Este é um placeholder. Implementação real requer nodemailer.
*/
export const getEmailById = internalQuery({
args: { emailId: v.id("notificacoesEmail") },
returns: v.union(v.any(), v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.emailId);
},
});
export const getActiveEmailConfig = internalQuery({
args: {},
returns: v.union(v.any(), v.null()),
handler: async (ctx) => {
return await ctx.db
.query("configuracaoEmail")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.first();
},
});
export const markEmailEnviando = internalMutation({
args: { emailId: v.id("notificacoesEmail") },
returns: v.null(),
handler: async (ctx, args) => {
const email = await ctx.db.get(args.emailId);
await ctx.db.patch(args.emailId, {
status: "enviando",
tentativas: ((email as any)?.tentativas || 0) + 1,
ultimaTentativa: Date.now(),
});
return null;
},
});
export const markEmailEnviado = internalMutation({
args: { emailId: v.id("notificacoesEmail") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.emailId, {
status: "enviado",
enviadoEm: Date.now(),
});
return null;
},
});
export const markEmailFalha = internalMutation({
args: { emailId: v.id("notificacoesEmail"), erro: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const email = await ctx.db.get(args.emailId);
await ctx.db.patch(args.emailId, {
status: "falha",
erroDetalhes: args.erro,
tentativas: ((email as any)?.tentativas || 0) + 1,
});
return null;
},
});
export const enviarEmailAction = action({
args: {
emailId: v.id("notificacoesEmail"),
@@ -150,13 +230,13 @@ export const enviarEmailAction = action({
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodemailer = require("nodemailer");
try {
// Buscar email da fila
const email = await ctx.runQuery(async (ctx) => {
return await ctx.db.get(args.emailId);
const email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
if (!email) {
@@ -164,64 +244,60 @@ export const enviarEmailAction = action({
}
// Buscar configuração SMTP
const config = await ctx.runQuery(async (ctx) => {
return await ctx.db
.query("configuracaoEmail")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.first();
});
const config = await ctx.runQuery(
internal.email.getActiveEmailConfig,
{}
);
if (!config) {
return { sucesso: false, erro: "Configuração de email não encontrada ou inativa" };
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa",
};
}
if (!config.testado) {
return { sucesso: false, erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!" };
if (!config.testadoEm) {
return {
sucesso: false,
erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!",
};
}
// Marcar como enviando
await ctx.runMutation(async (ctx) => {
await ctx.db.patch(args.emailId, {
status: "enviando",
tentativas: (email.tentativas || 0) + 1,
ultimaTentativa: Date.now(),
});
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Criar transporter do nodemailer
const transporter = nodemailer.createTransport({
host: config.smtpHost,
port: config.smtpPort,
secure: config.smtpSecure, // true para 465, false para outros
host: (config as any).smtpHost,
port: (config as any).smtpPort,
secure: (config as any).smtpSecure,
auth: {
user: config.smtpUser,
pass: config.smtpPassword,
user: (config as any).smtpUser,
pass: (config as any).smtpPassword,
},
tls: {
// Não rejeitar certificados não autorizados (útil para testes)
rejectUnauthorized: false
}
rejectUnauthorized: false,
},
});
// Enviar email REAL
const info = await transporter.sendMail({
from: `"${config.remetenteNome}" <${config.remetenteEmail}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
from: `"${(config as any).remetenteNome}" <${(config as any).remetenteEmail}>`,
to: (email as any).destinatario,
subject: (email as any).assunto,
html: (email as any).corpo,
});
console.log("✅ Email enviado com sucesso!");
console.log(" Para:", email.destinatario);
console.log(" Assunto:", email.assunto);
console.log(" Para:", (email as any).destinatario);
console.log(" Assunto:", (email as any).assunto);
console.log(" Message ID:", info.messageId);
// Marcar como enviado
await ctx.runMutation(async (ctx) => {
await ctx.db.patch(args.emailId, {
status: "enviado",
enviadoEm: Date.now(),
});
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
return { sucesso: true };
@@ -229,13 +305,9 @@ export const enviarEmailAction = action({
console.error("❌ Erro ao enviar email:", error.message);
// Marcar como falha
await ctx.runMutation(async (ctx) => {
const email = await ctx.db.get(args.emailId);
await ctx.db.patch(args.emailId, {
status: "falha",
erroDetalhes: error.message || "Erro desconhecido",
tentativas: (email?.tentativas || 0) + 1,
});
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: error.message || "Erro ao enviar email",
});
return { sucesso: false, erro: error.message || "Erro ao enviar email" };
@@ -278,10 +350,10 @@ export const processarFilaEmails = internalMutation({
processados++;
}
console.log(`📧 Fila de emails processada: ${processados} emails agendados para envio`);
console.log(
`📧 Fila de emails processada: ${processados} emails agendados para envio`
);
return { processados };
},
});

View File

@@ -1,19 +1,50 @@
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { simboloTipo } from "./schema";
// Validadores para campos opcionais
const sexoValidator = v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")));
const estadoCivilValidator = v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel")));
const grauInstrucaoValidator = v.optional(v.union(v.literal("fundamental"), v.literal("medio"), v.literal("superior"), v.literal("pos_graduacao"), v.literal("mestrado"), v.literal("doutorado")));
const grupoSanguineoValidator = v.optional(v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")));
const fatorRHValidator = v.optional(v.union(v.literal("positivo"), v.literal("negativo")));
const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")));
const sexoValidator = v.optional(
v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro"))
);
const estadoCivilValidator = v.optional(
v.union(
v.literal("solteiro"),
v.literal("casado"),
v.literal("divorciado"),
v.literal("viuvo"),
v.literal("uniao_estavel")
)
);
const grauInstrucaoValidator = v.optional(
v.union(
v.literal("fundamental"),
v.literal("medio"),
v.literal("superior"),
v.literal("pos_graduacao"),
v.literal("mestrado"),
v.literal("doutorado")
)
);
const grupoSanguineoValidator = v.optional(
v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O"))
);
const fatorRHValidator = v.optional(
v.union(v.literal("positivo"), v.literal("negativo"))
);
const aposentadoValidator = v.optional(
v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss"))
);
export const getAll = query({
args: {},
returns: v.array(v.any()),
handler: async (ctx) => {
// Autorização: listar funcionários
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "listar",
});
const funcionarios = await ctx.db.query("funcionarios").collect();
// Retornar apenas os campos necessários para listagem
return funcionarios.map((f: any) => ({
@@ -42,6 +73,11 @@ export const getById = query({
args: { id: v.id("funcionarios") },
returns: v.union(v.any(), v.null()),
handler: async (ctx, args) => {
// Autorização: ver funcionário
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "ver",
});
return await ctx.db.get(args.id);
},
});
@@ -142,6 +178,11 @@ export const create = mutation({
},
returns: v.id("funcionarios"),
handler: async (ctx, args) => {
// Autorização: criar
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "criar",
});
// Unicidade: CPF
const cpfExists = await ctx.db
.query("funcionarios")
@@ -264,6 +305,11 @@ export const update = mutation({
},
returns: v.null(),
handler: async (ctx, args) => {
// Autorização: editar
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "editar",
});
// Unicidade: CPF (excluindo o próprio registro)
const cpfExists = await ctx.db
.query("funcionarios")
@@ -294,6 +340,11 @@ export const remove = mutation({
args: { id: v.id("funcionarios") },
returns: v.null(),
handler: async (ctx, args) => {
// Autorização: excluir
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "excluir",
});
// TODO: Talvez queiramos também remover os arquivos do storage
await ctx.db.delete(args.id);
return null;
@@ -305,6 +356,10 @@ export const getFichaCompleta = query({
args: { id: v.id("funcionarios") },
returns: v.union(v.any(), v.null()),
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: "funcionarios",
acao: "ver",
});
const funcionario = await ctx.db.get(args.id);
if (!funcionario) {
return null;
@@ -335,14 +390,17 @@ export const getFichaCompleta = query({
return {
...funcionario,
simbolo: simbolo ? {
nome: simbolo.nome,
descricao: simbolo.descricao,
tipo: simbolo.tipo,
vencValor: simbolo.vencValor,
repValor: simbolo.repValor,
valor: simbolo.valor,
} : null,
simbolo: simbolo
? {
nome: simbolo.nome,
descricao: simbolo.descricao,
// campos adicionais, se existirem no símbolo
tipo: (simbolo as any).tipo,
vencValor: (simbolo as any).vencValor,
repValor: (simbolo as any).repValor,
valor: (simbolo as any).valor,
}
: null,
cursos: cursosComUrls,
};
},

View File

@@ -1,8 +1,5 @@
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";
const http = httpRouter();
authComponent.registerRoutes(http, createAuth);
export default http;

View File

@@ -13,7 +13,7 @@ export const listarTodosRoles = query({
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
customizado: v.boolean(),
customizado: v.optional(v.boolean()),
editavel: v.optional(v.boolean()),
_creationTime: v.number(),
})
@@ -91,9 +91,10 @@ export const limparPerfisAntigos = internalMutation({
deveManter = true;
perfisCorretos.set("ti_master", true);
} else {
motivo = role.nivel !== 0
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
: "TI_MASTER duplicado";
motivo =
role.nivel !== 0
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
: "TI_MASTER duplicado";
}
}
// ADMIN - Manter apenas o de nível 2
@@ -102,9 +103,10 @@ export const limparPerfisAntigos = internalMutation({
deveManter = true;
perfisCorretos.set("admin", true);
} else {
motivo = role.nivel !== 2
? "ADMIN deve ser nível 2, este é nível " + role.nivel
: "ADMIN duplicado";
motivo =
role.nivel !== 2
? "ADMIN deve ser nível 2, este é nível " + role.nivel
: "ADMIN duplicado";
}
}
// TI_USUARIO - Manter apenas o de nível 2
@@ -113,14 +115,16 @@ export const limparPerfisAntigos = internalMutation({
deveManter = true;
perfisCorretos.set("ti_usuario", true);
} else {
motivo = role.nivel !== 2
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
: "TI_USUARIO duplicado";
motivo =
role.nivel !== 2
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
: "TI_USUARIO duplicado";
}
}
// Perfis genéricos antigos (remover)
else if (role.nome === "ti") {
motivo = "Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'";
motivo =
"Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'";
}
// Outros perfis específicos de setores (manter se forem nível >= 2)
else if (
@@ -157,7 +161,9 @@ export const limparPerfisAntigos = internalMutation({
descricao: role.descricao,
nivel: role.nivel,
});
console.log(`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`);
console.log(
`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`
);
} else {
// Verificar se há usuários usando este perfil
const usuariosComRole = await ctx.db
@@ -286,6 +292,3 @@ export const verificarNiveisIncorretos = query({
return problemas;
},
});

View File

@@ -7,7 +7,7 @@ import { Doc, Id } from "./_generated/dataModel";
* Use em todas as mutations que modificam dados
*/
export async function registrarAtividade(
ctx: QueryCtx | MutationCtx,
ctx: MutationCtx,
usuarioId: Id<"usuarios">,
acao: string,
recurso: string,
@@ -37,21 +37,34 @@ export const listarAtividades = query({
limite: v.optional(v.number()),
},
handler: async (ctx, args) => {
let query = ctx.db.query("logsAtividades");
let atividades;
// Aplicar filtros
if (args.usuarioId) {
query = query.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId));
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId!))
.order("desc")
.take(args.limite || 100);
} else if (args.acao) {
query = query.withIndex("by_acao", (q) => q.eq("acao", args.acao));
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_acao", (q) => q.eq("acao", args.acao!))
.order("desc")
.take(args.limite || 100);
} else if (args.recurso) {
query = query.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso));
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_recurso", (q) => q.eq("recurso", args.recurso!))
.order("desc")
.take(args.limite || 100);
} else {
query = query.withIndex("by_timestamp");
atividades = await ctx.db
.query("logsAtividades")
.withIndex("by_timestamp")
.order("desc")
.take(args.limite || 100);
}
let atividades = await query.order("desc").take(args.limite || 100);
// Filtrar por range de datas se fornecido
if (args.dataInicio || args.dataFim) {
atividades = atividades.filter((log) => {
@@ -155,5 +168,3 @@ export const obterHistoricoRecurso = query({
return atividadesComUsuarios;
},
});

View File

@@ -6,7 +6,7 @@ import { Doc, Id } from "./_generated/dataModel";
* Helper para registrar tentativas de login
*/
export async function registrarLogin(
ctx: QueryCtx | MutationCtx,
ctx: MutationCtx,
dados: {
usuarioId?: Id<"usuarios">;
matriculaOuEmail: string;
@@ -170,26 +170,32 @@ export const obterEstatisticasLogin = query({
// Logins por horário (hora do dia)
const porHorario: Record<number, number> = {};
logs.filter((l) => l.sucesso).forEach((log) => {
const hora = new Date(log.timestamp).getHours();
porHorario[hora] = (porHorario[hora] || 0) + 1;
});
logs
.filter((l) => l.sucesso)
.forEach((log) => {
const hora = new Date(log.timestamp).getHours();
porHorario[hora] = (porHorario[hora] || 0) + 1;
});
// Browser mais usado
const porBrowser: Record<string, number> = {};
logs.filter((l) => l.sucesso).forEach((log) => {
if (log.browser) {
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
}
});
logs
.filter((l) => l.sucesso)
.forEach((log) => {
if (log.browser) {
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
}
});
// Dispositivos mais usados
const porDevice: Record<string, number> = {};
logs.filter((l) => l.sucesso).forEach((log) => {
if (log.device) {
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
}
});
logs
.filter((l) => l.sucesso)
.forEach((log) => {
if (log.device) {
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
}
});
return {
total: logs.length,
@@ -231,4 +237,3 @@ export const verificarIPSuspeito = query({
};
},
});

View File

@@ -1,529 +0,0 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
/**
* Lista de menus do sistema
*/
export const MENUS_SISTEMA = [
{ path: "/recursos-humanos", nome: "Recursos Humanos", descricao: "Gestão de funcionários e símbolos" },
{ path: "/recursos-humanos/funcionarios", nome: "Funcionários", descricao: "Cadastro e gestão de funcionários" },
{ path: "/recursos-humanos/simbolos", nome: "Símbolos", descricao: "Cadastro e gestão de símbolos" },
{ path: "/financeiro", nome: "Financeiro", descricao: "Gestão financeira" },
{ path: "/controladoria", nome: "Controladoria", descricao: "Controle e auditoria" },
{ path: "/licitacoes", nome: "Licitações", descricao: "Gestão de licitações" },
{ path: "/compras", nome: "Compras", descricao: "Gestão de compras" },
{ path: "/juridico", nome: "Jurídico", descricao: "Departamento jurídico" },
{ path: "/comunicacao", nome: "Comunicação", descricao: "Gestão de comunicação" },
{ path: "/programas-esportivos", nome: "Programas Esportivos", descricao: "Gestão de programas esportivos" },
{ path: "/secretaria-executiva", nome: "Secretaria Executiva", descricao: "Secretaria executiva" },
{ path: "/gestao-pessoas", nome: "Gestão de Pessoas", descricao: "Gestão de recursos humanos" },
{ path: "/ti", nome: "Tecnologia da Informação", descricao: "TI e suporte técnico" },
{ path: "/ti/painel-administrativo", nome: "Painel Administrativo TI", descricao: "Painel de administração do sistema" },
{ path: "/ti/monitoramento", nome: "Monitoramento SGSE", descricao: "Monitoramento técnico do sistema em tempo real" },
] as const;
/**
* Listar todas as permissões de menu para uma role
*/
export const listarPorRole = query({
args: { roleId: v.id("roles") },
returns: v.array(
v.object({
_id: v.id("menuPermissoes"),
roleId: v.id("roles"),
menuPath: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
})
),
handler: async (ctx, args) => {
return await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.collect();
},
});
/**
* Verificar se um usuário tem permissão para acessar um menu
* Prioridade: Permissão personalizada > Permissão da role
*/
export const verificarAcesso = query({
args: {
usuarioId: v.id("usuarios"),
menuPath: v.string(),
},
returns: v.object({
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
motivo: v.optional(v.string()),
}),
handler: async (ctx, args) => {
// Buscar o usuário
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return {
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
motivo: "Usuário não encontrado",
};
}
// Verificar se o usuário está ativo
if (!usuario.ativo) {
return {
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
motivo: "Usuário inativo",
};
}
// Buscar a role do usuário
const role = await ctx.db.get(usuario.roleId);
if (!role) {
return {
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
motivo: "Role não encontrada",
};
}
// Apenas TI_MASTER (nível 0) tem acesso total irrestrito
// Admin, TI_USUARIO e outros (nível >= 1) têm permissões configuráveis
if (role.nivel === 0) {
return {
podeAcessar: true,
podeConsultar: true,
podeGravar: true,
};
}
// Dashboard e Solicitar Acesso são públicos
if (args.menuPath === "/" || args.menuPath === "/solicitar-acesso") {
return {
podeAcessar: true,
podeConsultar: true,
podeGravar: false,
};
}
// 1. Verificar se existe permissão personalizada para este usuário
const permissaoPersonalizada = await ctx.db
.query("menuPermissoesPersonalizadas")
.withIndex("by_usuario_and_menu", (q) =>
q.eq("usuarioId", args.usuarioId).eq("menuPath", args.menuPath)
)
.first();
if (permissaoPersonalizada) {
return {
podeAcessar: permissaoPersonalizada.podeAcessar,
podeConsultar: permissaoPersonalizada.podeConsultar,
podeGravar: permissaoPersonalizada.podeGravar,
};
}
// 2. Se não houver permissão personalizada, verificar permissão da role
const permissaoRole = await ctx.db
.query("menuPermissoes")
.withIndex("by_role_and_menu", (q) =>
q.eq("roleId", usuario.roleId).eq("menuPath", args.menuPath)
)
.first();
if (!permissaoRole) {
return {
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
motivo: "Sem permissão configurada para este menu",
};
}
return {
podeAcessar: permissaoRole.podeAcessar,
podeConsultar: permissaoRole.podeConsultar,
podeGravar: permissaoRole.podeGravar,
};
},
});
/**
* Atualizar ou criar permissão de menu para uma role
*/
export const atualizarPermissao = mutation({
args: {
roleId: v.id("roles"),
menuPath: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
},
returns: v.id("menuPermissoes"),
handler: async (ctx, args) => {
// Verificar se já existe uma permissão
const existente = await ctx.db
.query("menuPermissoes")
.withIndex("by_role_and_menu", (q) =>
q.eq("roleId", args.roleId).eq("menuPath", args.menuPath)
)
.first();
if (existente) {
// Atualizar permissão existente
await ctx.db.patch(existente._id, {
podeAcessar: args.podeAcessar,
podeConsultar: args.podeConsultar,
podeGravar: args.podeGravar,
});
return existente._id;
} else {
// Criar nova permissão
return await ctx.db.insert("menuPermissoes", {
roleId: args.roleId,
menuPath: args.menuPath,
podeAcessar: args.podeAcessar,
podeConsultar: args.podeConsultar,
podeGravar: args.podeGravar,
});
}
},
});
/**
* Remover permissão de menu
*/
export const removerPermissao = mutation({
args: {
permissaoId: v.id("menuPermissoes"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.permissaoId);
return null;
},
});
/**
* Inicializar permissões padrão para uma role
*/
export const inicializarPermissoesRole = mutation({
args: {
roleId: v.id("roles"),
},
returns: v.null(),
handler: async (ctx, args) => {
// Buscar a role
const role = await ctx.db.get(args.roleId);
if (!role) {
throw new Error("Role não encontrada");
}
// Admin e TI não precisam de permissões específicas (acesso total)
if (role.nivel <= 1) {
return null;
}
// Para outras roles, criar permissões básicas (apenas consulta)
for (const menu of MENUS_SISTEMA) {
// Verificar se já existe permissão
const existente = await ctx.db
.query("menuPermissoes")
.withIndex("by_role_and_menu", (q) =>
q.eq("roleId", args.roleId).eq("menuPath", menu.path)
)
.first();
if (!existente) {
// Criar permissão padrão (sem acesso)
await ctx.db.insert("menuPermissoes", {
roleId: args.roleId,
menuPath: menu.path,
podeAcessar: false,
podeConsultar: false,
podeGravar: false,
});
}
}
return null;
},
});
/**
* Listar todos os menus do sistema
*/
export const listarMenus = query({
args: {},
returns: v.array(
v.object({
path: v.string(),
nome: v.string(),
descricao: v.string(),
})
),
handler: async (ctx) => {
return MENUS_SISTEMA.map((menu) => ({
path: menu.path,
nome: menu.nome,
descricao: menu.descricao,
}));
},
});
/**
* Obter matriz de permissões (role x menu) para o painel de controle
*/
export const obterMatrizPermissoes = query({
args: {},
returns: v.array(
v.object({
role: v.object({
_id: v.id("roles"),
nome: v.string(),
nivel: v.number(),
descricao: v.string(),
}),
permissoes: v.array(
v.object({
menuPath: v.string(),
menuNome: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
permissaoId: v.optional(v.id("menuPermissoes")),
})
),
})
),
handler: async (ctx) => {
// Buscar todas as roles
// TI_MASTER (nível 0) aparece mas não é editável
// Admin, TI_USUARIO e outros (nível >= 1) são configuráveis
const roles = await ctx.db.query("roles").collect();
const matriz = [];
for (const role of roles) {
const permissoes = [];
for (const menu of MENUS_SISTEMA) {
// Buscar permissão específica
const permissao = await ctx.db
.query("menuPermissoes")
.withIndex("by_role_and_menu", (q) =>
q.eq("roleId", role._id).eq("menuPath", menu.path)
)
.first();
// Admin e TI têm acesso total automático
if (role.nivel <= 1) {
permissoes.push({
menuPath: menu.path,
menuNome: menu.nome,
podeAcessar: true,
podeConsultar: true,
podeGravar: true,
permissaoId: permissao?._id,
});
} else {
permissoes.push({
menuPath: menu.path,
menuNome: menu.nome,
podeAcessar: permissao?.podeAcessar ?? false,
podeConsultar: permissao?.podeConsultar ?? false,
podeGravar: permissao?.podeGravar ?? false,
permissaoId: permissao?._id,
});
}
}
matriz.push({
role: {
_id: role._id,
nome: role.nome,
nivel: role.nivel,
descricao: role.descricao,
},
permissoes,
});
}
return matriz;
},
});
/**
* Criar ou atualizar permissão personalizada por matrícula
*/
export const atualizarPermissaoPersonalizada = mutation({
args: {
matricula: v.string(),
menuPath: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
},
returns: v.union(v.id("menuPermissoesPersonalizadas"), v.null()),
handler: async (ctx, args) => {
// Buscar usuário pela matrícula
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (!usuario) {
throw new Error("Usuário não encontrado com esta matrícula");
}
// Verificar se já existe permissão personalizada
const existente = await ctx.db
.query("menuPermissoesPersonalizadas")
.withIndex("by_usuario_and_menu", (q) =>
q.eq("usuarioId", usuario._id).eq("menuPath", args.menuPath)
)
.first();
if (existente) {
// Atualizar permissão existente
await ctx.db.patch(existente._id, {
podeAcessar: args.podeAcessar,
podeConsultar: args.podeConsultar,
podeGravar: args.podeGravar,
});
return existente._id;
} else {
// Criar nova permissão
return await ctx.db.insert("menuPermissoesPersonalizadas", {
usuarioId: usuario._id,
matricula: args.matricula,
menuPath: args.menuPath,
podeAcessar: args.podeAcessar,
podeConsultar: args.podeConsultar,
podeGravar: args.podeGravar,
});
}
},
});
/**
* Remover permissão personalizada
*/
export const removerPermissaoPersonalizada = mutation({
args: {
permissaoId: v.id("menuPermissoesPersonalizadas"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.permissaoId);
return null;
},
});
/**
* Listar permissões personalizadas de um usuário por matrícula
*/
export const listarPermissoesPersonalizadas = query({
args: {
matricula: v.string(),
},
returns: v.array(
v.object({
_id: v.id("menuPermissoesPersonalizadas"),
menuPath: v.string(),
menuNome: v.string(),
podeAcessar: v.boolean(),
podeConsultar: v.boolean(),
podeGravar: v.boolean(),
})
),
handler: async (ctx, args) => {
// Buscar usuário
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (!usuario) {
return [];
}
// Buscar permissões personalizadas
const permissoes = await ctx.db
.query("menuPermissoesPersonalizadas")
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuario._id))
.collect();
// Mapear com nomes dos menus
return permissoes.map((p) => {
const menu = MENUS_SISTEMA.find((m) => m.path === p.menuPath);
return {
_id: p._id,
menuPath: p.menuPath,
menuNome: menu?.nome || p.menuPath,
podeAcessar: p.podeAcessar,
podeConsultar: p.podeConsultar,
podeGravar: p.podeGravar,
};
});
},
});
/**
* Buscar usuário por matrícula para o painel de personalização
*/
export const buscarUsuarioPorMatricula = query({
args: {
matricula: v.string(),
},
returns: v.union(
v.object({
_id: v.id("usuarios"),
matricula: v.string(),
nome: v.string(),
email: v.string(),
role: v.object({
nome: v.string(),
nivel: v.number(),
descricao: v.string(),
}),
ativo: v.boolean(),
}),
v.null()
),
handler: async (ctx, args) => {
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
if (!usuario) {
return null;
}
const role = await ctx.db.get(usuario.roleId);
if (!role) {
return null;
}
return {
_id: usuario._id,
matricula: usuario.matricula,
nome: usuario.nome,
email: usuario.email,
role: {
nome: role.nome,
nivel: role.nivel,
descricao: role.descricao,
},
ativo: usuario.ativo,
};
},
});

View File

@@ -1,12 +1,15 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { registrarAtividade } from "./logsAtividades";
import { api } from "./_generated/api";
import { Id } from "./_generated/dataModel";
/**
* Listar todos os perfis customizados
*/
export const listarPerfisCustomizados = query({
args: {},
returns: v.array(v.any()),
handler: async (ctx) => {
const perfis = await ctx.db.query("perfisCustomizados").collect();
@@ -42,6 +45,16 @@ export const obterPerfilComPermissoes = query({
args: {
perfilId: v.id("perfisCustomizados"),
},
returns: v.union(
v.object({
perfil: v.any(),
role: v.any(),
permissoes: v.array(v.any()),
menuPermissoes: v.array(v.any()),
usuarios: v.array(v.any()),
}),
v.null()
),
handler: async (ctx, args) => {
const perfil = await ctx.db.get(args.perfilId);
if (!perfil) {
@@ -99,20 +112,31 @@ export const criarPerfilCustomizado = mutation({
criadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }),
v.object({
sucesso: v.literal(true),
perfilId: v.id("perfisCustomizados"),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Validar nível (deve ser >= 3)
if (args.nivel < 3) {
return { sucesso: false as const, erro: "Perfis customizados devem ter nível >= 3" };
return {
sucesso: false as const,
erro: "Perfis customizados devem ter nível >= 3",
};
}
// Verificar se nome já existe
const roles = await ctx.db.query("roles").collect();
const nomeExiste = roles.some((r) => r.nome.toLowerCase() === args.nome.toLowerCase());
const nomeExiste = roles.some(
(r) => r.nome.toLowerCase() === args.nome.toLowerCase()
);
if (nomeExiste) {
return { sucesso: false as const, erro: "Já existe um perfil com este nome" };
return {
sucesso: false as const,
erro: "Já existe um perfil com este nome",
};
}
// Criar role correspondente
@@ -130,7 +154,7 @@ export const criarPerfilCustomizado = mutation({
// Copiar permissões gerais
const permissoesClonar = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
.collect();
for (const perm of permissoesClonar) {
@@ -143,7 +167,7 @@ export const criarPerfilCustomizado = mutation({
// Copiar permissões de menu
const menuPermsClonar = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId))
.withIndex("by_role", (q) => q.eq("roleId", args.clonarDeRoleId!))
.collect();
for (const menuPerm of menuPermsClonar) {
@@ -321,7 +345,10 @@ export const clonarPerfil = mutation({
criadoPorId: v.id("usuarios"),
},
returns: v.union(
v.object({ sucesso: v.literal(true), perfilId: v.id("perfisCustomizados") }),
v.object({
sucesso: v.literal(true),
perfilId: v.id("perfisCustomizados"),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
@@ -330,17 +357,80 @@ export const clonarPerfil = mutation({
return { sucesso: false as const, erro: "Perfil origem não encontrado" };
}
// Criar novo perfil clonando o original
const resultado = await criarPerfilCustomizado(ctx, {
// Verificar se nome já existe
const roles = await ctx.db.query("roles").collect();
const nomeExiste = roles.some(
(r) => r.nome.toLowerCase() === args.novoNome.toLowerCase()
);
if (nomeExiste) {
return {
sucesso: false as const,
erro: "Já existe um perfil com este nome",
};
}
// Criar role correspondente
const roleId = await ctx.db.insert("roles", {
nome: args.novoNome.toLowerCase().replace(/\s+/g, "_"),
descricao: args.novaDescricao,
nivel: perfilOrigem.nivel,
customizado: true,
criadoPor: args.criadoPorId,
editavel: true,
});
// Copiar permissões gerais do perfil de origem
const permissoesClonar = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
.collect();
for (const perm of permissoesClonar) {
await ctx.db.insert("rolePermissoes", {
roleId,
permissaoId: perm.permissaoId,
});
}
// Copiar permissões de menu
const menuPermsClonar = await ctx.db
.query("menuPermissoes")
.withIndex("by_role", (q) => q.eq("roleId", perfilOrigem.roleId))
.collect();
for (const menuPerm of menuPermsClonar) {
await ctx.db.insert("menuPermissoes", {
roleId,
menuPath: menuPerm.menuPath,
podeAcessar: menuPerm.podeAcessar,
podeConsultar: menuPerm.podeConsultar,
podeGravar: menuPerm.podeGravar,
});
}
// Criar perfil customizado
const perfilId = await ctx.db.insert("perfisCustomizados", {
nome: args.novoNome,
descricao: args.novaDescricao,
nivel: perfilOrigem.nivel,
clonarDeRoleId: perfilOrigem.roleId,
criadoPorId: args.criadoPorId,
roleId,
criadoPor: args.criadoPorId,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return resultado;
// Log de atividade
await registrarAtividade(
ctx as any,
args.criadoPorId,
"criar",
"perfis",
JSON.stringify({
perfilId,
nome: args.novoNome,
nivel: perfilOrigem.nivel,
}),
perfilId
);
return { sucesso: true as const, perfilId };
},
});

View File

@@ -0,0 +1,210 @@
import { query, mutation, internalQuery } from "./_generated/server";
import { v } from "convex/values";
// Catálogo base de recursos e ações
// Ajuste/expanda conforme os módulos disponíveis no sistema
export const CATALOGO_RECURSOS = [
{
recurso: "funcionarios",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "simbolos",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
},
] as const;
export const listarRecursosEAcoes = query({
args: {},
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
})
),
handler: async () => {
return CATALOGO_RECURSOS.map((r) => ({
recurso: r.recurso,
acoes: [...r.acoes],
}));
},
});
export const listarPermissoesAcoesPorRole = query({
args: { roleId: v.id("roles") },
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
})
),
handler: async (ctx, args) => {
// Buscar vínculos permissao<-role
const rolePerms = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.collect();
// Carregar documentos de permissões
const actionsByResource: Record<string, Set<string>> = {};
for (const rp of rolePerms) {
const perm = await ctx.db.get(rp.permissaoId);
if (!perm) continue;
const set = (actionsByResource[perm.recurso] ||= new Set<string>());
set.add(perm.acao);
}
// Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) {
const granted = Array.from(
actionsByResource[item.recurso] ?? new Set<string>()
);
result.push({ recurso: item.recurso, acoes: granted });
}
return result;
},
});
export const atualizarPermissaoAcao = mutation({
args: {
roleId: v.id("roles"),
recurso: v.string(),
acao: v.string(),
conceder: v.boolean(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao)
let permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
if (!permissao) {
const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert("permissoes", {
nome,
descricao,
recurso: args.recurso,
acao: args.acao,
});
permissao = await ctx.db.get(id);
}
if (!permissao) return null;
// Verificar vínculo atual
const existente = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.collect();
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
if (args.conceder) {
if (!vinculo) {
await ctx.db.insert("rolePermissoes", {
roleId: args.roleId,
permissaoId: permissao._id,
});
}
} else {
if (vinculo) {
await ctx.db.delete(vinculo._id);
}
}
return null;
},
});
export const verificarAcao = query({
args: {
usuarioId: v.id("usuarios"),
recurso: v.string(),
acao: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) throw new Error("acesso_negado");
const role = await ctx.db.get(usuario.roleId);
if (!role) throw new Error("acesso_negado");
// Níveis administrativos têm acesso total
if (role.nivel <= 1) return null;
// Encontrar permissão
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
if (!permissao) throw new Error("acesso_negado");
const hasLink = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", usuario.roleId))
.collect();
const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id);
if (!permitido) throw new Error("acesso_negado");
return null;
},
});
export const assertPermissaoAcaoAtual = internalQuery({
args: {
recurso: v.string(),
acao: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual: any = null;
if (identity && identity.email) {
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
if (!usuarioAtual) throw new Error("acesso_negado");
const role: any = await ctx.db.get(usuarioAtual.roleId as any);
if (!role) throw new Error("acesso_negado");
if ((role as any).nivel <= 1) return null;
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first();
if (!permissao) throw new Error("acesso_negado");
const links = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", (role as any)._id as any))
.collect();
const ok = links.some((rp) => rp.permissaoId === permissao!._id);
if (!ok) throw new Error("acesso_negado");
return null;
},
});

View File

@@ -14,7 +14,7 @@ export const listar = query({
descricao: v.string(),
nivel: v.number(),
setor: v.optional(v.string()),
customizado: v.boolean(),
customizado: v.optional(v.boolean()),
editavel: v.optional(v.boolean()),
criadoPor: v.optional(v.id("usuarios")),
})
@@ -45,4 +45,3 @@ export const buscarPorId = query({
return await ctx.db.get(args.roleId);
},
});

View File

@@ -1,7 +1,5 @@
import { defineSchema, defineTable } from "convex/server";
import { Infer, v } from "convex/values";
import { tables } from "./betterAuth/schema";
import { cidrv4 } from "better-auth";
export const simboloTipo = v.union(
v.literal("cargo_comissionado"),
@@ -358,6 +356,7 @@ export default defineSchema({
acao: v.string(), // "criar", "ler", "editar", "excluir"
})
.index("by_recurso", ["recurso"])
.index("by_recurso_e_acao", ["recurso", "acao"])
.index("by_nome", ["nome"]),
rolePermissoes: defineTable({

View File

@@ -370,6 +370,55 @@ export const seedDatabase = internalMutation({
});
console.log(" ✅ Admin criado (matrícula: 2000, senha: Admin@123)");
// 2.1 Criar catálogo de permissões por ação e conceder a Admin/TI
console.log("🔐 Criando permissões por ação...");
const CATALOGO_RECURSOS = [
{ recurso: "dashboard", acoes: ["ver"] },
{
recurso: "funcionarios",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "simbolos",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "usuarios",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
{
recurso: "perfis",
acoes: ["ver", "listar", "criar", "editar", "excluir"],
},
] as const;
const permissaoKeyToId = new Map<string, string>();
for (const item of CATALOGO_RECURSOS) {
for (const acao of item.acoes) {
const nome = `${item.recurso}.${acao}`;
const id = await ctx.db.insert("permissoes", {
nome,
descricao: `Permite ${acao} em ${item.recurso}`,
recurso: item.recurso,
acao,
});
permissaoKeyToId.set(nome, id);
}
}
console.log(`${permissaoKeyToId.size} permissões criadas`);
// Conceder todas permissões a Admin e TI
const rolesParaConceder = [roleAdmin, roleTIUsuario, roleTIMaster];
for (const roleId of rolesParaConceder) {
for (const [, permId] of permissaoKeyToId) {
await ctx.db.insert("rolePermissoes", {
roleId: roleId as any,
permissaoId: permId as any,
});
}
}
console.log(" ✅ Todas as permissões concedidas a Admin e TI");
// 3. Inserir símbolos
console.log("📝 Inserindo símbolos...");
const simbolosMap = new Map<string, string>();
@@ -393,7 +442,9 @@ export const seedDatabase = internalMutation({
for (const funcionario of funcionariosData) {
const simboloId = simbolosMap.get(funcionario.simboloNome);
if (!simboloId) {
console.error(` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`);
console.error(
` ❌ Símbolo não encontrado: ${funcionario.simboloNome}`
);
continue;
}
@@ -436,7 +487,9 @@ export const seedDatabase = internalMutation({
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
console.log(` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`);
console.log(
` ✅ Usuário criado: ${funcionario.nome} (senha: Mudar@123)`
);
}
// 6. Inserir solicitações de acesso
@@ -462,28 +515,32 @@ export const seedDatabase = internalMutation({
codigo: "USUARIO_BLOQUEADO",
nome: "Usuário Bloqueado",
titulo: "Sua conta foi bloqueada",
corpo: "Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.",
corpo:
"Sua conta no SGSE foi bloqueada.\\n\\nMotivo: {{motivo}}\\n\\nPara mais informações, entre em contato com a TI.",
variaveis: ["motivo"],
},
{
codigo: "USUARIO_DESBLOQUEADO",
nome: "Usuário Desbloqueado",
titulo: "Sua conta foi desbloqueada",
corpo: "Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
corpo:
"Sua conta no SGSE foi desbloqueada e você já pode acessar o sistema normalmente.",
variaveis: [],
},
{
codigo: "SENHA_RESETADA",
nome: "Senha Resetada",
titulo: "Sua senha foi resetada",
corpo: "Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.",
corpo:
"Sua senha foi resetada pela equipe de TI.\\n\\nNova senha temporária: {{senha}}\\n\\nPor favor, altere sua senha no próximo login.",
variaveis: ["senha"],
},
{
codigo: "PERMISSAO_ALTERADA",
nome: "Permissão Alterada",
titulo: "Suas permissões foram atualizadas",
corpo: "Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.",
corpo:
"Suas permissões de acesso ao sistema foram atualizadas.\\n\\nPara verificar suas novas permissões, acesse o menu de perfil.",
variaveis: [],
},
{
@@ -497,7 +554,8 @@ export const seedDatabase = internalMutation({
codigo: "BEM_VINDO",
nome: "Boas-vindas",
titulo: "Bem-vindo ao SGSE",
corpo: "Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI",
corpo:
"Olá {{nome}},\\n\\nSeja bem-vindo ao Sistema de Gestão da Secretaria de Esportes!\\n\\nSuas credenciais de acesso:\\nMatrícula: {{matricula}}\\nSenha temporária: {{senha}}\\n\\nPor favor, altere sua senha no primeiro acesso.\\n\\nEquipe de TI",
variaveis: ["nome", "matricula", "senha"],
},
];
@@ -584,11 +642,15 @@ export const clearDatabase = internalMutation({
console.log(`${menuPermissoes.length} menu-permissões removidas`);
// Limpar menu-permissões personalizadas
const menuPermissoesPersonalizadas = await ctx.db.query("menuPermissoesPersonalizadas").collect();
const menuPermissoesPersonalizadas = await ctx.db
.query("menuPermissoesPersonalizadas")
.collect();
for (const mpp of menuPermissoesPersonalizadas) {
await ctx.db.delete(mpp._id);
}
console.log(`${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`);
console.log(
`${menuPermissoesPersonalizadas.length} menu-permissões personalizadas removidas`
);
// Limpar role-permissões
const rolePermissoes = await ctx.db.query("rolePermissoes").collect();
@@ -615,4 +677,3 @@ export const clearDatabase = internalMutation({
return null;
},
});

View File

@@ -3,6 +3,7 @@ import { mutation, query } from "./_generated/server";
import { hashPassword, generateToken } from "./auth/utils";
import { registrarAtividade } from "./logsAtividades";
import { Id } from "./_generated/dataModel";
import { api } from "./_generated/api";
/**
* Associar funcionário a um usuário
@@ -23,7 +24,9 @@ export const associarFuncionario = mutation({
// Verificar se o funcionário já está associado a outro usuário
const usuarioExistente = await ctx.db
.query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId))
.withIndex("by_funcionarioId", (q) =>
q.eq("funcionarioId", args.funcionarioId)
)
.first();
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
@@ -160,9 +163,7 @@ export const listar = query({
// Filtrar por matrícula
if (args.matricula) {
usuarios = usuarios.filter((u) =>
u.matricula.includes(args.matricula!)
);
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!));
}
// Filtrar por ativo
@@ -440,13 +441,16 @@ export const atualizarPerfil = mutation({
if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
if (args.setor !== undefined) updates.setor = args.setor;
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
if (args.statusMensagem !== undefined)
updates.statusMensagem = args.statusMensagem;
if (args.statusPresenca !== undefined) {
updates.statusPresenca = args.statusPresenca;
updates.ultimaAtividade = Date.now();
}
if (args.notificacoesAtivadas !== undefined) updates.notificacoesAtivadas = args.notificacoesAtivadas;
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
if (args.notificacoesAtivadas !== undefined)
updates.notificacoesAtivadas = args.notificacoesAtivadas;
if (args.somNotificacao !== undefined)
updates.somNotificacao = args.somNotificacao;
await ctx.db.patch(usuarioAtual._id, updates);
@@ -471,13 +475,15 @@ export const obterPerfil = query({
fotoPerfilUrl: v.union(v.string(), v.null()),
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()),
statusPresenca: v.optional(v.union(
v.literal("online"),
v.literal("offline"),
v.literal("ausente"),
v.literal("externo"),
v.literal("em_reuniao")
)),
statusPresenca: v.optional(
v.union(
v.literal("online"),
v.literal("offline"),
v.literal("ausente"),
v.literal("externo"),
v.literal("em_reuniao")
)
),
notificacoesAtivadas: v.boolean(),
somNotificacao: v.boolean(),
}),
@@ -500,7 +506,10 @@ export const obterPerfil = query({
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
console.log("Usuário encontrado por email:", usuarioAtual ? "SIM" : "NÃO");
console.log(
"Usuário encontrado por email:",
usuarioAtual ? "SIM" : "NÃO"
);
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
@@ -516,7 +525,10 @@ export const obterPerfil = query({
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
console.log("Usuário da sessão encontrado:", usuarioAtual ? "SIM" : "NÃO");
console.log(
"Usuário da sessão encontrado:",
usuarioAtual ? "SIM" : "NÃO"
);
}
}
@@ -525,7 +537,10 @@ export const obterPerfil = query({
// Listar todos os usuários para debug
const todosUsuarios = await ctx.db.query("usuarios").collect();
console.log("Total de usuários no banco:", todosUsuarios.length);
console.log("Emails cadastrados:", todosUsuarios.map(u => u.email));
console.log(
"Emails cadastrados:",
todosUsuarios.map((u) => u.email)
);
return null;
}
@@ -621,6 +636,7 @@ export const listarParaChat = query({
*/
export const uploadFotoPerfil = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
// TENTAR BETTER AUTH PRIMEIRO
const identity = await ctx.auth.getUserIdentity();
@@ -822,7 +838,8 @@ export const resetarSenhaUsuario = mutation({
// Helper para gerar senha temporária
function gerarSenhaTemporaria(): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
let senha = "";
for (let i = 0; i < 12; i++) {
senha += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -890,6 +907,116 @@ export const editarUsuario = mutation({
},
});
/**
* Criar/Promover usuário Admin Master (TI_MASTER - nível 0)
*/
export const criarAdminMaster = mutation({
args: {
matricula: v.string(),
nome: v.string(),
email: v.string(),
senha: v.optional(v.string()),
},
returns: v.union(
v.object({
sucesso: v.literal(true),
usuarioId: v.id("usuarios"),
senhaTemporaria: v.string(),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// Garantir que a role TI_MASTER exista (nível 0)
let roleTIMaster = await ctx.db
.query("roles")
.withIndex("by_nome", (q) => q.eq("nome", "ti_master"))
.first();
if (!roleTIMaster) {
const roleId = await ctx.db.insert("roles", {
nome: "ti_master",
descricao: "TI Master",
nivel: 0,
setor: "ti",
customizado: false,
editavel: false,
});
roleTIMaster = await ctx.db.get(roleId);
}
if (!roleTIMaster) {
return {
sucesso: false as const,
erro: "Falha ao garantir role TI Master",
};
}
// Se já existir usuário por matrícula, promove/atualiza
const existentePorMatricula = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.first();
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
const senhaHash = await hashPassword(senhaTemporaria);
if (existentePorMatricula) {
await ctx.db.patch(existentePorMatricula._id, {
nome: args.nome,
email: args.email,
senhaHash,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
atualizadoEm: Date.now(),
});
return {
sucesso: true as const,
usuarioId: existentePorMatricula._id,
senhaTemporaria,
};
}
// Verificar se email já existe
const existentePorEmail = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (existentePorEmail) {
// Promove usuário existente por email
await ctx.db.patch(existentePorEmail._id, {
matricula: args.matricula,
nome: args.nome,
senhaHash,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
atualizadoEm: Date.now(),
});
return {
sucesso: true as const,
usuarioId: existentePorEmail._id,
senhaTemporaria,
};
}
// Criar novo usuário TI Master
const usuarioId = await ctx.db.insert("usuarios", {
matricula: args.matricula,
senhaHash,
nome: args.nome,
email: args.email,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return { sucesso: true as const, usuarioId, senhaTemporaria };
},
});
/**
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
*/
@@ -954,7 +1081,11 @@ export const criarUsuarioCompleto = mutation({
enviarEmailBoasVindas: v.optional(v.boolean()),
},
returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string() }),
v.object({
sucesso: v.literal(true),
usuarioId: v.id("usuarios"),
senhaTemporaria: v.string(),
}),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
@@ -1013,3 +1144,85 @@ export const criarUsuarioCompleto = mutation({
},
});
/**
* Criar (ou garantir) um usuário ADMIN padrão
*/
export const criarAdminPadrao = mutation({
args: {
matricula: v.optional(v.string()),
nome: v.optional(v.string()),
email: v.optional(v.string()),
senha: v.optional(v.string()),
},
returns: v.object({
sucesso: v.boolean(),
usuarioId: v.optional(v.id("usuarios")),
}),
handler: async (ctx, args) => {
const matricula = args.matricula ?? "0000";
const nome = args.nome ?? "Administrador Geral";
const email = args.email ?? "admin@sgse.pe.gov.br";
const senha = args.senha ?? "Admin@123";
// Garantir role ADMIN (nível 2)
let roleAdmin = await ctx.db
.query("roles")
.withIndex("by_nome", (q) => q.eq("nome", "admin"))
.first();
if (!roleAdmin) {
const roleId = await ctx.db.insert("roles", {
nome: "admin",
descricao: "Administrador Geral",
nivel: 2,
setor: "administrativo",
customizado: false,
editavel: true,
});
roleAdmin = await ctx.db.get(roleId);
}
if (!roleAdmin) return { sucesso: false };
// Verificar se já existe por matrícula ou email
const existentePorMatricula = await ctx.db
.query("usuarios")
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
.first();
const existentePorEmail = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", email))
.first();
const senhaHash = await hashPassword(senha);
if (existentePorMatricula || existentePorEmail) {
const alvo = existentePorMatricula ?? existentePorEmail!;
await ctx.db.patch(alvo._id, {
matricula,
nome,
email,
senhaHash,
roleId: roleAdmin._id,
ativo: true,
primeiroAcesso: false,
atualizadoEm: Date.now(),
});
return { sucesso: true, usuarioId: alvo._id };
}
const usuarioId = await ctx.db.insert("usuarios", {
matricula,
senhaHash,
nome,
email,
roleId: roleAdmin._id,
ativo: true,
primeiroAcesso: false,
criadoEm: Date.now(),
atualizadoEm: Date.now(),
});
return { sucesso: true, usuarioId };
},
});

View File

@@ -20,9 +20,7 @@
"typescript": "^5.9.2"
},
"dependencies": {
"@convex-dev/better-auth": "^0.9.6",
"@dicebear/avataaars": "^9.2.4",
"better-auth": "1.3.27",
"convex": "^1.28.0",
"nodemailer": "^7.0.10"
}