Feat ajuste acesso #5
@@ -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
|
||||
|
||||
|
||||
@@ -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!** 🚀
|
||||
|
||||
@@ -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! 🚀
|
||||
|
||||
@@ -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!** 🎉
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!** 🎉
|
||||
|
||||
|
||||
@@ -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! 🚀**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
83
apps/web/src/lib/components/ActionGuard.svelte
Normal file
83
apps/web/src/lib/components/ActionGuard.svelte
Normal 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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 class="text-sm">
|
||||
Este perfil possui acesso total ao sistema automaticamente,
|
||||
sem necessidade de configuração manual.
|
||||
</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>
|
||||
</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
|
||||
</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">
|
||||
<!-- 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={permissao.podeAcessar}
|
||||
checked={isConcedida(roleId, item.recurso, acao)}
|
||||
disabled={salvando}
|
||||
onchange={(e) =>
|
||||
atualizarPermissao(
|
||||
roleData.role._id,
|
||||
permissao.menuPath,
|
||||
"podeAcessar",
|
||||
toggleAcao(
|
||||
roleId,
|
||||
item.recurso,
|
||||
acao,
|
||||
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>
|
||||
<span class="flex-1 capitalize font-medium"
|
||||
>{acao}</span
|
||||
>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -508,4 +575,3 @@
|
||||
{/each}
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -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()}
|
||||
<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"
|
||||
>
|
||||
{#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" />
|
||||
<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>
|
||||
{/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>
|
||||
|
||||
<!-- 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>
|
||||
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>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createSvelteKitHandler } from "@mmailaender/convex-better-auth-svelte/sveltekit";
|
||||
|
||||
export const { GET, POST } = createSvelteKitHandler();
|
||||
262
bun.lock
262
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
958
packages/backend/convex/_generated/api.d.ts
vendored
958
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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: {};
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
|
||||
const component = defineComponent("betterAuth");
|
||||
|
||||
export default component;
|
||||
@@ -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;
|
||||
@@ -1,7 +1,4 @@
|
||||
import { defineApp } from "convex/server";
|
||||
import betterAuth from "./betterAuth/convex.config";
|
||||
|
||||
const app = defineApp();
|
||||
app.use(betterAuth);
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -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(
|
||||
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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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 ? {
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
descricao: simbolo.descricao,
|
||||
tipo: simbolo.tipo,
|
||||
vencValor: simbolo.vencValor,
|
||||
repValor: simbolo.repValor,
|
||||
valor: simbolo.valor,
|
||||
} : null,
|
||||
// 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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { httpRouter } from "convex/server";
|
||||
import { authComponent, createAuth } from "./auth";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
authComponent.registerRoutes(http, createAuth);
|
||||
|
||||
export default http;
|
||||
|
||||
@@ -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,7 +91,8 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("ti_master", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 0
|
||||
motivo =
|
||||
role.nivel !== 0
|
||||
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
||||
: "TI_MASTER duplicado";
|
||||
}
|
||||
@@ -102,7 +103,8 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("admin", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 2
|
||||
motivo =
|
||||
role.nivel !== 2
|
||||
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
||||
: "ADMIN duplicado";
|
||||
}
|
||||
@@ -113,14 +115,16 @@ export const limparPerfisAntigos = internalMutation({
|
||||
deveManter = true;
|
||||
perfisCorretos.set("ti_usuario", true);
|
||||
} else {
|
||||
motivo = role.nivel !== 2
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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,14 +170,18 @@ export const obterEstatisticasLogin = query({
|
||||
|
||||
// Logins por horário (hora do dia)
|
||||
const porHorario: Record<number, number> = {};
|
||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
||||
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) => {
|
||||
logs
|
||||
.filter((l) => l.sucesso)
|
||||
.forEach((log) => {
|
||||
if (log.browser) {
|
||||
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
|
||||
}
|
||||
@@ -185,7 +189,9 @@ export const obterEstatisticasLogin = query({
|
||||
|
||||
// Dispositivos mais usados
|
||||
const porDevice: Record<string, number> = {};
|
||||
logs.filter((l) => l.sucesso).forEach((log) => {
|
||||
logs
|
||||
.filter((l) => l.sucesso)
|
||||
.forEach((log) => {
|
||||
if (log.device) {
|
||||
porDevice[log.device] = (porDevice[log.device] || 0) + 1;
|
||||
}
|
||||
@@ -231,4 +237,3 @@ export const verificarIPSuspeito = query({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
210
packages/backend/convex/permissoesAcoes.ts
Normal file
210
packages/backend/convex/permissoesAcoes.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
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 };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user