feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -7,29 +7,29 @@ A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.number(),
|
||||
second: v.string(),
|
||||
},
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.number(),
|
||||
second: v.string()
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query('tablename').collect();
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
},
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -37,8 +37,8 @@ Using this query function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const data = useQuery(api.functions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
first: 10,
|
||||
second: 'hello'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -46,27 +46,27 @@ A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { mutation } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.string(),
|
||||
second: v.string(),
|
||||
},
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.string(),
|
||||
second: v.string()
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert('messages', message);
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
},
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -75,13 +75,11 @@ Using this mutation function in a React component looks like:
|
||||
```ts
|
||||
const mutation = useMutation(api.functions.myMutationFunction);
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: 'Hello!', second: 'me' });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: 'Hello!', second: 'me' }).then((result) => console.log(result));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
12
packages/backend/convex/_generated/api.d.ts
vendored
12
packages/backend/convex/_generated/api.d.ts
vendored
@@ -14,6 +14,7 @@ import type * as actions_linkPreview from "../actions/linkPreview.js";
|
||||
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
|
||||
import type * as actions_smtp from "../actions/smtp.js";
|
||||
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
||||
import type * as atas from "../atas.js";
|
||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||
import type * as ausencias from "../ausencias.js";
|
||||
import type * as autenticacao from "../autenticacao.js";
|
||||
@@ -45,11 +46,11 @@ import type * as logsAcesso from "../logsAcesso.js";
|
||||
import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as objetos from "../objetos.js";
|
||||
import type * as pedidos from "../pedidos.js";
|
||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||
import type * as pontos from "../pontos.js";
|
||||
import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
|
||||
import type * as produtos from "../produtos.js";
|
||||
import type * as pushNotifications from "../pushNotifications.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as saldoFerias from "../saldoFerias.js";
|
||||
@@ -57,6 +58,7 @@ import type * as security from "../security.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as setores from "../setores.js";
|
||||
import type * as simbolos from "../simbolos.js";
|
||||
import type * as tables_atas from "../tables/atas.js";
|
||||
import type * as tables_atestados from "../tables/atestados.js";
|
||||
import type * as tables_ausencias from "../tables/ausencias.js";
|
||||
import type * as tables_auth from "../tables/auth.js";
|
||||
@@ -69,9 +71,9 @@ import type * as tables_ferias from "../tables/ferias.js";
|
||||
import type * as tables_flows from "../tables/flows.js";
|
||||
import type * as tables_funcionarios from "../tables/funcionarios.js";
|
||||
import type * as tables_licencas from "../tables/licencas.js";
|
||||
import type * as tables_objetos from "../tables/objetos.js";
|
||||
import type * as tables_pedidos from "../tables/pedidos.js";
|
||||
import type * as tables_ponto from "../tables/ponto.js";
|
||||
import type * as tables_produtos from "../tables/produtos.js";
|
||||
import type * as tables_security from "../tables/security.js";
|
||||
import type * as tables_setores from "../tables/setores.js";
|
||||
import type * as tables_system from "../tables/system.js";
|
||||
@@ -99,6 +101,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"actions/pushNotifications": typeof actions_pushNotifications;
|
||||
"actions/smtp": typeof actions_smtp;
|
||||
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
||||
atas: typeof atas;
|
||||
atestadosLicencas: typeof atestadosLicencas;
|
||||
ausencias: typeof ausencias;
|
||||
autenticacao: typeof autenticacao;
|
||||
@@ -130,11 +133,11 @@ declare const fullApi: ApiFromModules<{
|
||||
logsAtividades: typeof logsAtividades;
|
||||
logsLogin: typeof logsLogin;
|
||||
monitoramento: typeof monitoramento;
|
||||
objetos: typeof objetos;
|
||||
pedidos: typeof pedidos;
|
||||
permissoesAcoes: typeof permissoesAcoes;
|
||||
pontos: typeof pontos;
|
||||
preferenciasNotificacao: typeof preferenciasNotificacao;
|
||||
produtos: typeof produtos;
|
||||
pushNotifications: typeof pushNotifications;
|
||||
roles: typeof roles;
|
||||
saldoFerias: typeof saldoFerias;
|
||||
@@ -142,6 +145,7 @@ declare const fullApi: ApiFromModules<{
|
||||
seed: typeof seed;
|
||||
setores: typeof setores;
|
||||
simbolos: typeof simbolos;
|
||||
"tables/atas": typeof tables_atas;
|
||||
"tables/atestados": typeof tables_atestados;
|
||||
"tables/ausencias": typeof tables_ausencias;
|
||||
"tables/auth": typeof tables_auth;
|
||||
@@ -154,9 +158,9 @@ declare const fullApi: ApiFromModules<{
|
||||
"tables/flows": typeof tables_flows;
|
||||
"tables/funcionarios": typeof tables_funcionarios;
|
||||
"tables/licencas": typeof tables_licencas;
|
||||
"tables/objetos": typeof tables_objetos;
|
||||
"tables/pedidos": typeof tables_pedidos;
|
||||
"tables/ponto": typeof tables_ponto;
|
||||
"tables/produtos": typeof tables_produtos;
|
||||
"tables/security": typeof tables_security;
|
||||
"tables/setores": typeof tables_setores;
|
||||
"tables/system": typeof tables_system;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
|
||||
@@ -1,233 +1,216 @@
|
||||
"use node";
|
||||
'use node';
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { internal } from "../_generated/api";
|
||||
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
|
||||
import nodemailer from "nodemailer";
|
||||
import { v } from 'convex/values';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { internal } from '../_generated/api';
|
||||
import { action } from '../_generated/server';
|
||||
import { decryptSMTPPasswordNode } from './utils/nodeCrypto';
|
||||
|
||||
export const enviar = action({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
"use node";
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
'use node';
|
||||
|
||||
let email;
|
||||
try {
|
||||
// Buscar email da fila
|
||||
email = await ctx.runQuery(internal.email.getEmailById, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
let email;
|
||||
try {
|
||||
// Buscar email da fila
|
||||
email = await ctx.runQuery(internal.email.getEmailById, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: "Email não encontrado" };
|
||||
}
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: 'Email não encontrado' };
|
||||
}
|
||||
|
||||
// Buscar configuração SMTP ativa
|
||||
const configRaw = await ctx.runQuery(
|
||||
internal.email.getActiveEmailConfig,
|
||||
{}
|
||||
);
|
||||
// Buscar configuração SMTP ativa
|
||||
const configRaw = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
|
||||
|
||||
if (!configRaw) {
|
||||
console.error(
|
||||
"❌ Configuração SMTP não encontrada ou inativa para email:",
|
||||
email.destinatario
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
|
||||
};
|
||||
}
|
||||
if (!configRaw) {
|
||||
console.error(
|
||||
'❌ Configuração SMTP não encontrada ou inativa para email:',
|
||||
email.destinatario
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.'
|
||||
};
|
||||
}
|
||||
|
||||
console.log("📧 Tentando enviar email:", {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
servidor: configRaw.servidor,
|
||||
porta: configRaw.porta,
|
||||
});
|
||||
console.log('📧 Tentando enviar email:', {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
servidor: configRaw.servidor,
|
||||
porta: configRaw.porta
|
||||
});
|
||||
|
||||
// Descriptografar senha usando função compatível com Node.js
|
||||
let senhaDescriptografada: string;
|
||||
try {
|
||||
senhaDescriptografada = await decryptSMTPPasswordNode(
|
||||
configRaw.senhaHash
|
||||
);
|
||||
} catch (decryptError) {
|
||||
const decryptErrorMessage =
|
||||
decryptError instanceof Error
|
||||
? decryptError.message
|
||||
: String(decryptError);
|
||||
console.error(
|
||||
"Erro ao descriptografar senha SMTP:",
|
||||
decryptErrorMessage
|
||||
);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`,
|
||||
};
|
||||
}
|
||||
// Descriptografar senha usando função compatível com Node.js
|
||||
let senhaDescriptografada: string;
|
||||
try {
|
||||
senhaDescriptografada = await decryptSMTPPasswordNode(configRaw.senhaHash);
|
||||
} catch (decryptError) {
|
||||
const decryptErrorMessage =
|
||||
decryptError instanceof Error ? decryptError.message : String(decryptError);
|
||||
console.error('Erro ao descriptografar senha SMTP:', decryptErrorMessage);
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
...configRaw,
|
||||
senha: senhaDescriptografada,
|
||||
};
|
||||
const config = {
|
||||
...configRaw,
|
||||
senha: senhaDescriptografada
|
||||
};
|
||||
|
||||
// Config já foi validado acima
|
||||
// Config já foi validado acima
|
||||
|
||||
// Avisar mas não bloquear se não foi testado
|
||||
if (!config.testadoEm) {
|
||||
console.warn(
|
||||
"⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..."
|
||||
);
|
||||
}
|
||||
// Avisar mas não bloquear se não foi testado
|
||||
if (!config.testadoEm) {
|
||||
console.warn('⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim...');
|
||||
}
|
||||
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
// Marcar como enviando
|
||||
await ctx.runMutation(internal.email.markEmailEnviando, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
// Criar transporter do nodemailer com configuração melhorada
|
||||
const transporterOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
requireTLS?: boolean;
|
||||
auth: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
tls?: {
|
||||
rejectUnauthorized: boolean;
|
||||
ciphers?: string;
|
||||
};
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
pool?: boolean;
|
||||
maxConnections?: number;
|
||||
maxMessages?: number;
|
||||
} = {
|
||||
host: config.servidor,
|
||||
port: config.porta,
|
||||
secure: config.usarSSL,
|
||||
auth: {
|
||||
user: config.usuario,
|
||||
pass: config.senha, // Senha já descriptografada
|
||||
},
|
||||
connectionTimeout: 15000, // 15 segundos
|
||||
greetingTimeout: 15000,
|
||||
socketTimeout: 15000,
|
||||
pool: true, // Usar pool de conexões
|
||||
maxConnections: 5,
|
||||
maxMessages: 100,
|
||||
};
|
||||
// Criar transporter do nodemailer com configuração melhorada
|
||||
const transporterOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
requireTLS?: boolean;
|
||||
auth: {
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
tls?: {
|
||||
rejectUnauthorized: boolean;
|
||||
ciphers?: string;
|
||||
};
|
||||
connectionTimeout: number;
|
||||
greetingTimeout: number;
|
||||
socketTimeout: number;
|
||||
pool?: boolean;
|
||||
maxConnections?: number;
|
||||
maxMessages?: number;
|
||||
} = {
|
||||
host: config.servidor,
|
||||
port: config.porta,
|
||||
secure: config.usarSSL,
|
||||
auth: {
|
||||
user: config.usuario,
|
||||
pass: config.senha // Senha já descriptografada
|
||||
},
|
||||
connectionTimeout: 15000, // 15 segundos
|
||||
greetingTimeout: 15000,
|
||||
socketTimeout: 15000,
|
||||
pool: true, // Usar pool de conexões
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
};
|
||||
|
||||
// Adicionar TLS apenas se necessário
|
||||
if (config.usarTLS) {
|
||||
transporterOptions.requireTLS = true;
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false, // Permitir certificados autoassinados
|
||||
};
|
||||
} else if (config.usarSSL) {
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
}
|
||||
// Adicionar TLS apenas se necessário
|
||||
if (config.usarTLS) {
|
||||
transporterOptions.requireTLS = true;
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false // Permitir certificados autoassinados
|
||||
};
|
||||
} else if (config.usarSSL) {
|
||||
transporterOptions.tls = {
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
const transporter = nodemailer.createTransport(transporterOptions);
|
||||
|
||||
// Verificar conexão antes de enviar
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log("✅ Conexão SMTP verificada com sucesso");
|
||||
} catch (verifyError) {
|
||||
const verifyErrorMessage =
|
||||
verifyError instanceof Error
|
||||
? verifyError.message
|
||||
: String(verifyError);
|
||||
console.warn(
|
||||
"⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:",
|
||||
verifyErrorMessage
|
||||
);
|
||||
// Não bloquear envio por falha na verificação, apenas avisar
|
||||
}
|
||||
// Verificar conexão antes de enviar
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log('✅ Conexão SMTP verificada com sucesso');
|
||||
} catch (verifyError) {
|
||||
const verifyErrorMessage =
|
||||
verifyError instanceof Error ? verifyError.message : String(verifyError);
|
||||
console.warn(
|
||||
'⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:',
|
||||
verifyErrorMessage
|
||||
);
|
||||
// Não bloquear envio por falha na verificação, apenas avisar
|
||||
}
|
||||
|
||||
// Validar email destinatário antes de enviar
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email.destinatario)) {
|
||||
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
|
||||
}
|
||||
// Validar email destinatário antes de enviar
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email.destinatario)) {
|
||||
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
|
||||
}
|
||||
|
||||
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
|
||||
const textoPlano = email.corpo
|
||||
.replace(/<[^>]*>/g, "") // Remover tags HTML
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.trim();
|
||||
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
|
||||
const textoPlano = email.corpo
|
||||
.replace(/<[^>]*>/g, '') // Remover tags HTML
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.trim();
|
||||
|
||||
// Enviar email
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
||||
to: email.destinatario,
|
||||
subject: email.assunto,
|
||||
html: email.corpo,
|
||||
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
|
||||
headers: {
|
||||
"X-Mailer": "SGSE-Sistema-de-Gerenciamento-de-Secretaria",
|
||||
"X-Priority": "3",
|
||||
},
|
||||
});
|
||||
// Enviar email
|
||||
const info = await transporter.sendMail({
|
||||
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
||||
to: email.destinatario,
|
||||
subject: email.assunto,
|
||||
html: email.corpo,
|
||||
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
|
||||
headers: {
|
||||
'X-Mailer': 'SGSE-Sistema-de-Gerenciamento-de-Secretaria',
|
||||
'X-Priority': '3'
|
||||
}
|
||||
});
|
||||
|
||||
interface MessageInfo {
|
||||
messageId?: string;
|
||||
response?: string;
|
||||
}
|
||||
interface MessageInfo {
|
||||
messageId?: string;
|
||||
response?: string;
|
||||
}
|
||||
|
||||
const messageInfo = info as MessageInfo;
|
||||
const messageInfo = info as MessageInfo;
|
||||
|
||||
console.log("✅ Email enviado com sucesso!", {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
messageId: messageInfo.messageId,
|
||||
response: messageInfo.response,
|
||||
});
|
||||
console.log('✅ Email enviado com sucesso!', {
|
||||
para: email.destinatario,
|
||||
assunto: email.assunto,
|
||||
messageId: messageInfo.messageId,
|
||||
response: messageInfo.response
|
||||
});
|
||||
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
emailId: args.emailId,
|
||||
});
|
||||
// Marcar como enviado
|
||||
await ctx.runMutation(internal.email.markEmailEnviado, {
|
||||
emailId: args.emailId
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
return { sucesso: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
console.error("❌ Erro ao enviar email:", {
|
||||
emailId: args.emailId,
|
||||
destinatario: email?.destinatario,
|
||||
erro: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
console.error('❌ Erro ao enviar email:', {
|
||||
emailId: args.emailId,
|
||||
destinatario: email?.destinatario,
|
||||
erro: errorMessage,
|
||||
stack: errorStack
|
||||
});
|
||||
|
||||
// Marcar como falha com detalhes completos
|
||||
const erroCompleto = errorStack
|
||||
? `${errorMessage}\n\nStack: ${errorStack}`
|
||||
: errorMessage;
|
||||
// Marcar como falha com detalhes completos
|
||||
const erroCompleto = errorStack ? `${errorMessage}\n\nStack: ${errorStack}` : errorMessage;
|
||||
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro
|
||||
});
|
||||
await ctx.runMutation(internal.email.markEmailFalha, {
|
||||
emailId: args.emailId,
|
||||
erro: erroCompleto.substring(0, 2000) // Limitar tamanho do erro
|
||||
});
|
||||
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
},
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
// // Configurar SSH
|
||||
// let sshPasswordDecrypted: string | undefined = undefined;
|
||||
|
||||
|
||||
// // Se senha foi fornecida, usar ela. Caso contrário, tentar descriptografar a armazenada
|
||||
// if (args.sshPassword) {
|
||||
// sshPasswordDecrypted = args.sshPassword;
|
||||
@@ -429,4 +429,3 @@
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
|
||||
@@ -1,138 +1,155 @@
|
||||
"use node";
|
||||
'use node';
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { internal } from "../_generated/api";
|
||||
import { v } from 'convex/values';
|
||||
import { internal } from '../_generated/api';
|
||||
import { action } from '../_generated/server';
|
||||
|
||||
/**
|
||||
* Extrair preview de link (metadados Open Graph) - função auxiliar
|
||||
*/
|
||||
async function extrairPreviewLinkHelper(url: string) {
|
||||
try {
|
||||
// Validar URL
|
||||
let urlObj: URL;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Validar URL
|
||||
let urlObj: URL;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Buscar HTML da página
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; SGSE-Bot/1.0)",
|
||||
},
|
||||
signal: AbortSignal.timeout(5000), // Timeout de 5 segundos
|
||||
});
|
||||
// Buscar HTML da página
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; SGSE-Bot/1.0)'
|
||||
},
|
||||
signal: AbortSignal.timeout(5000) // Timeout de 5 segundos
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const html = await response.text();
|
||||
|
||||
// Extrair metadados Open Graph e Twitter Cards
|
||||
const metadata: {
|
||||
titulo?: string;
|
||||
descricao?: string;
|
||||
imagem?: string;
|
||||
site?: string;
|
||||
} = {};
|
||||
// Extrair metadados Open Graph e Twitter Cards
|
||||
const metadata: {
|
||||
titulo?: string;
|
||||
descricao?: string;
|
||||
imagem?: string;
|
||||
site?: string;
|
||||
} = {};
|
||||
|
||||
// Título (og:title ou twitter:title ou <title>)
|
||||
const ogTitleMatch = html.match(/<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i);
|
||||
const twitterTitleMatch = html.match(/<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i);
|
||||
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
|
||||
|
||||
metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined;
|
||||
if (metadata.titulo) {
|
||||
metadata.titulo = metadata.titulo.trim().substring(0, 200);
|
||||
}
|
||||
// Título (og:title ou twitter:title ou <title>)
|
||||
const ogTitleMatch = html.match(
|
||||
/<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
const twitterTitleMatch = html.match(
|
||||
/<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
|
||||
|
||||
// Descrição (og:description ou twitter:description ou meta description)
|
||||
const ogDescMatch = html.match(/<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i);
|
||||
const twitterDescMatch = html.match(/<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i);
|
||||
const metaDescMatch = html.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i);
|
||||
|
||||
metadata.descricao = ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined;
|
||||
if (metadata.descricao) {
|
||||
metadata.descricao = metadata.descricao.trim().substring(0, 300);
|
||||
}
|
||||
metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined;
|
||||
if (metadata.titulo) {
|
||||
metadata.titulo = metadata.titulo.trim().substring(0, 200);
|
||||
}
|
||||
|
||||
// Imagem (og:image ou twitter:image)
|
||||
const ogImageMatch = html.match(/<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i);
|
||||
const twitterImageMatch = html.match(/<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i);
|
||||
|
||||
const imageUrl = ogImageMatch?.[1] || twitterImageMatch?.[1];
|
||||
if (imageUrl) {
|
||||
// Resolver URL relativa
|
||||
try {
|
||||
metadata.imagem = new URL(imageUrl, url).href;
|
||||
} catch {
|
||||
metadata.imagem = imageUrl;
|
||||
}
|
||||
}
|
||||
// Descrição (og:description ou twitter:description ou meta description)
|
||||
const ogDescMatch = html.match(
|
||||
/<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
const twitterDescMatch = html.match(
|
||||
/<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
const metaDescMatch = html.match(
|
||||
/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
|
||||
// Site (og:site_name ou domínio)
|
||||
const ogSiteMatch = html.match(/<meta\s+property=["']og:site_name["']\s+content=["']([^"']+)["']/i);
|
||||
metadata.site = ogSiteMatch?.[1] || urlObj.hostname.replace(/^www\./, "");
|
||||
metadata.descricao =
|
||||
ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined;
|
||||
if (metadata.descricao) {
|
||||
metadata.descricao = metadata.descricao.trim().substring(0, 300);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
titulo: metadata.titulo,
|
||||
descricao: metadata.descricao,
|
||||
imagem: metadata.imagem,
|
||||
site: metadata.site,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao extrair preview de link:", error);
|
||||
return null;
|
||||
}
|
||||
// Imagem (og:image ou twitter:image)
|
||||
const ogImageMatch = html.match(
|
||||
/<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
const twitterImageMatch = html.match(
|
||||
/<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
|
||||
const imageUrl = ogImageMatch?.[1] || twitterImageMatch?.[1];
|
||||
if (imageUrl) {
|
||||
// Resolver URL relativa
|
||||
try {
|
||||
metadata.imagem = new URL(imageUrl, url).href;
|
||||
} catch {
|
||||
metadata.imagem = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Site (og:site_name ou domínio)
|
||||
const ogSiteMatch = html.match(
|
||||
/<meta\s+property=["']og:site_name["']\s+content=["']([^"']+)["']/i
|
||||
);
|
||||
metadata.site = ogSiteMatch?.[1] || urlObj.hostname.replace(/^www\./, '');
|
||||
|
||||
return {
|
||||
url,
|
||||
titulo: metadata.titulo,
|
||||
descricao: metadata.descricao,
|
||||
imagem: metadata.imagem,
|
||||
site: metadata.site
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Erro ao extrair preview de link:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar preview de link e atualizar mensagem
|
||||
*/
|
||||
export const processarPreviewLink = action({
|
||||
args: {
|
||||
mensagemId: v.id("mensagens"),
|
||||
url: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Extrair preview
|
||||
const preview = await extrairPreviewLinkHelper(args.url);
|
||||
|
||||
if (preview) {
|
||||
// Atualizar mensagem com preview
|
||||
await ctx.runMutation(internal.chat.atualizarLinkPreview, {
|
||||
mensagemId: args.mensagemId,
|
||||
linkPreview: preview,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
mensagemId: v.id('mensagens'),
|
||||
url: v.string()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Extrair preview
|
||||
const preview = await extrairPreviewLinkHelper(args.url);
|
||||
|
||||
if (preview) {
|
||||
// Atualizar mensagem com preview
|
||||
await ctx.runMutation(internal.chat.atualizarLinkPreview, {
|
||||
mensagemId: args.mensagemId,
|
||||
linkPreview: preview
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extrair preview de link (metadados Open Graph) - versão pública
|
||||
*/
|
||||
export const extrairPreviewLink = action({
|
||||
args: {
|
||||
url: v.string(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
url: v.string(),
|
||||
titulo: v.optional(v.string()),
|
||||
descricao: v.optional(v.string()),
|
||||
imagem: v.optional(v.string()),
|
||||
site: v.optional(v.string()),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await extrairPreviewLinkHelper(args.url);
|
||||
},
|
||||
args: {
|
||||
url: v.string()
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
url: v.string(),
|
||||
titulo: v.optional(v.string()),
|
||||
descricao: v.optional(v.string()),
|
||||
imagem: v.optional(v.string()),
|
||||
site: v.optional(v.string())
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await extrairPreviewLinkHelper(args.url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,103 +1,108 @@
|
||||
"use node";
|
||||
'use node';
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { internal } from "../_generated/api";
|
||||
import { v } from 'convex/values';
|
||||
import { internal } from '../_generated/api';
|
||||
import { action } from '../_generated/server';
|
||||
|
||||
/**
|
||||
* Enviar push notification usando Web Push API
|
||||
*/
|
||||
export const enviarPush = action({
|
||||
args: {
|
||||
subscriptionId: v.id("pushSubscriptions"),
|
||||
titulo: v.string(),
|
||||
corpo: v.string(),
|
||||
data: v.optional(
|
||||
v.object({
|
||||
conversaId: v.optional(v.string()),
|
||||
mensagemId: v.optional(v.string()),
|
||||
tipo: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Buscar subscription
|
||||
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
|
||||
subscriptionId: args.subscriptionId,
|
||||
});
|
||||
args: {
|
||||
subscriptionId: v.id('pushSubscriptions'),
|
||||
titulo: v.string(),
|
||||
corpo: v.string(),
|
||||
data: v.optional(
|
||||
v.object({
|
||||
conversaId: v.optional(v.string()),
|
||||
mensagemId: v.optional(v.string()),
|
||||
tipo: v.optional(v.string())
|
||||
})
|
||||
)
|
||||
},
|
||||
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Buscar subscription
|
||||
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
|
||||
subscriptionId: args.subscriptionId
|
||||
});
|
||||
|
||||
if (!subscription || !subscription.ativo) {
|
||||
return { sucesso: false, erro: "Subscription não encontrada ou inativa" };
|
||||
}
|
||||
if (!subscription || !subscription.ativo) {
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Subscription não encontrada ou inativa'
|
||||
};
|
||||
}
|
||||
|
||||
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
|
||||
// Por enquanto, vamos usar uma implementação básica
|
||||
// Em produção, você precisará configurar VAPID keys
|
||||
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
|
||||
// Por enquanto, vamos usar uma implementação básica
|
||||
// Em produção, você precisará configurar VAPID keys
|
||||
|
||||
const webpushModule = await import("web-push");
|
||||
// web-push pode exportar como default ou named exports
|
||||
// Usar a declaração de tipo do módulo web-push
|
||||
interface WebPushType {
|
||||
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
|
||||
sendNotification: (
|
||||
subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
|
||||
payload: string | Buffer
|
||||
) => Promise<void>;
|
||||
}
|
||||
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
|
||||
const webpushModule = await import('web-push');
|
||||
// web-push pode exportar como default ou named exports
|
||||
// Usar a declaração de tipo do módulo web-push
|
||||
interface WebPushType {
|
||||
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
|
||||
sendNotification: (
|
||||
subscription: {
|
||||
endpoint: string;
|
||||
keys: { p256dh: string; auth: string };
|
||||
},
|
||||
payload: string | Buffer
|
||||
) => Promise<void>;
|
||||
}
|
||||
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
|
||||
|
||||
// VAPID keys devem vir de variáveis de ambiente
|
||||
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
|
||||
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
|
||||
// VAPID keys devem vir de variáveis de ambiente
|
||||
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
|
||||
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
|
||||
|
||||
if (!publicKey || !privateKey) {
|
||||
console.warn("⚠️ VAPID keys não configuradas. Push notifications não funcionarão.");
|
||||
// Em desenvolvimento, podemos retornar sucesso sem enviar
|
||||
return { sucesso: true };
|
||||
}
|
||||
if (!publicKey || !privateKey) {
|
||||
console.warn('⚠️ VAPID keys não configuradas. Push notifications não funcionarão.');
|
||||
// Em desenvolvimento, podemos retornar sucesso sem enviar
|
||||
return { sucesso: true };
|
||||
}
|
||||
|
||||
webpush.setVapidDetails("mailto:suporte@sgse.app", publicKey, privateKey);
|
||||
webpush.setVapidDetails('mailto:suporte@sgse.app', publicKey, privateKey);
|
||||
|
||||
// Preparar payload da notificação
|
||||
const payload = JSON.stringify({
|
||||
title: args.titulo,
|
||||
body: args.corpo,
|
||||
icon: "/favicon.png",
|
||||
badge: "/favicon.png",
|
||||
data: args.data || {},
|
||||
tag: args.data?.conversaId || "default",
|
||||
requireInteraction: args.data?.tipo === "mencao", // Menções requerem interação
|
||||
});
|
||||
// Preparar payload da notificação
|
||||
const payload = JSON.stringify({
|
||||
title: args.titulo,
|
||||
body: args.corpo,
|
||||
icon: '/favicon.png',
|
||||
badge: '/favicon.png',
|
||||
data: args.data || {},
|
||||
tag: args.data?.conversaId || 'default',
|
||||
requireInteraction: args.data?.tipo === 'mencao' // Menções requerem interação
|
||||
});
|
||||
|
||||
// Enviar push notification
|
||||
await webpush.sendNotification(
|
||||
{
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth,
|
||||
},
|
||||
},
|
||||
payload
|
||||
);
|
||||
// Enviar push notification
|
||||
await webpush.sendNotification(
|
||||
{
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.keys.p256dh,
|
||||
auth: subscription.keys.auth
|
||||
}
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
|
||||
return { sucesso: true };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("❌ Erro ao enviar push notification:", errorMessage);
|
||||
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
|
||||
return { sucesso: true };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('❌ Erro ao enviar push notification:', errorMessage);
|
||||
|
||||
// Se subscription inválida, marcar como inativa
|
||||
if (errorMessage.includes("410") || errorMessage.includes("expired")) {
|
||||
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
|
||||
subscriptionId: args.subscriptionId,
|
||||
});
|
||||
}
|
||||
// Se subscription inválida, marcar como inativa
|
||||
if (errorMessage.includes('410') || errorMessage.includes('expired')) {
|
||||
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
|
||||
subscriptionId: args.subscriptionId
|
||||
});
|
||||
}
|
||||
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
},
|
||||
return { sucesso: false, erro: errorMessage };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,64 +1,60 @@
|
||||
"use node";
|
||||
|
||||
import { action } from "../_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
'use node';
|
||||
import { v } from 'convex/values';
|
||||
// Importar nodemailer de forma estática para evitar problemas com caminhos no Windows
|
||||
import nodemailer from "nodemailer";
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import { action } from '../_generated/server';
|
||||
|
||||
export const testarConexao = action({
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean()
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim() === '') {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Servidor SMTP não pode estar vazio'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim() === "") {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Servidor SMTP não pode estar vazio",
|
||||
};
|
||||
}
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: 'Porta inválida' };
|
||||
}
|
||||
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: "Porta inválida" };
|
||||
}
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: args.servidor,
|
||||
port: args.porta,
|
||||
secure: args.usarSSL,
|
||||
auth: {
|
||||
user: args.usuario,
|
||||
pass: args.senha
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
},
|
||||
connectionTimeout: 10000, // 10 segundos
|
||||
greetingTimeout: 10000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: args.servidor,
|
||||
port: args.porta,
|
||||
secure: args.usarSSL,
|
||||
auth: {
|
||||
user: args.usuario,
|
||||
pass: args.senha,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
connectionTimeout: 10000, // 10 segundos
|
||||
greetingTimeout: 10000,
|
||||
socketTimeout: 10000,
|
||||
});
|
||||
// Verificar conexão
|
||||
await transporter.verify();
|
||||
|
||||
// Verificar conexão
|
||||
await transporter.verify();
|
||||
|
||||
return { sucesso: true as const };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { sucesso: false as const, erro: errorMessage };
|
||||
}
|
||||
},
|
||||
return { sucesso: true as const };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { sucesso: false as const, erro: errorMessage };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use node";
|
||||
'use node';
|
||||
|
||||
/**
|
||||
* Utilitários de criptografia compatíveis com Node.js
|
||||
@@ -10,65 +10,64 @@
|
||||
* Esta versão funciona em ambiente Node.js (actions)
|
||||
*/
|
||||
export async function decryptSMTPPasswordNode(encryptedPassword: string): Promise<string> {
|
||||
try {
|
||||
// Em Node.js, crypto.subtle está disponível globalmente
|
||||
const crypto = globalThis.crypto;
|
||||
|
||||
if (!crypto || !crypto.subtle) {
|
||||
throw new Error("Web Crypto API não disponível");
|
||||
}
|
||||
try {
|
||||
// Em Node.js, crypto.subtle está disponível globalmente
|
||||
const crypto = globalThis.crypto;
|
||||
|
||||
// Chave base - mesma usada em auth/utils.ts
|
||||
const keyMaterial = new TextEncoder().encode("SGSE-EMAIL-ENCRYPTION-KEY-2024");
|
||||
|
||||
// Importar chave material
|
||||
const keyMaterialKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyMaterial,
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits", "deriveKey"]
|
||||
);
|
||||
if (!crypto || !crypto.subtle) {
|
||||
throw new Error('Web Crypto API não disponível');
|
||||
}
|
||||
|
||||
// Derivar chave de 256 bits usando PBKDF2
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: new TextEncoder().encode("SGSE-SALT"),
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterialKey,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
|
||||
// Decodificar base64 manualmente (compatível com Node.js e browser)
|
||||
const binaryString = atob(encryptedPassword);
|
||||
const combined = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
||||
// Chave base - mesma usada em auth/utils.ts
|
||||
const keyMaterial = new TextEncoder().encode('SGSE-EMAIL-ENCRYPTION-KEY-2024');
|
||||
|
||||
// Extrair IV e dados criptografados
|
||||
const iv = combined.slice(0, 12);
|
||||
const encrypted = combined.slice(12);
|
||||
// Importar chave material
|
||||
const keyMaterialKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyMaterial,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
|
||||
// Descriptografar
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
encrypted
|
||||
);
|
||||
// Derivar chave de 256 bits usando PBKDF2
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: new TextEncoder().encode('SGSE-SALT'),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterialKey,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Converter para string
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error("Erro ao descriptografar senha SMTP (Node.js):", errorMessage);
|
||||
throw new Error(`Falha ao descriptografar senha SMTP: ${errorMessage}`);
|
||||
}
|
||||
// Decodificar base64 manualmente (compatível com Node.js e browser)
|
||||
const binaryString = atob(encryptedPassword);
|
||||
const combined = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
||||
|
||||
// Extrair IV e dados criptografados
|
||||
const iv = combined.slice(0, 12);
|
||||
const encrypted = combined.slice(12);
|
||||
|
||||
// Descriptografar
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
encrypted
|
||||
);
|
||||
|
||||
// Converter para string
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('Erro ao descriptografar senha SMTP (Node.js):', errorMessage);
|
||||
throw new Error(`Falha ao descriptografar senha SMTP: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
packages/backend/convex/atas.ts
Normal file
77
packages/backend/convex/atas.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('atas').collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const get = query({
|
||||
args: { id: v.id('atas') },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
numero: v.string(),
|
||||
dataInicio: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string()),
|
||||
empresaId: v.id('empresas'),
|
||||
pdf: v.optional(v.string()),
|
||||
numeroSei: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
return await ctx.db.insert('atas', {
|
||||
...args,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id('atas'),
|
||||
numero: v.string(),
|
||||
dataInicio: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string()),
|
||||
empresaId: v.id('empresas'),
|
||||
pdf: v.optional(v.string()),
|
||||
numeroSei: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
numero: args.numero,
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim,
|
||||
empresaId: args.empresaId,
|
||||
pdf: args.pdf,
|
||||
numeroSei: args.numeroSei,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: {
|
||||
id: v.id('atas')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
}
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
@@ -553,7 +553,6 @@ export const obterEventosCalendario = query({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erro ao processar atestado ${atestado._id}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -601,7 +600,6 @@ export const obterEventosCalendario = query({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erro ao processar licença ${licenca._id}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -647,7 +645,6 @@ export const obterEventosCalendario = query({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erro ao processar férias ${feriasRegistro._id}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { mutation } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { updatePassword } from './auth';
|
||||
import { authComponent } from './auth';
|
||||
import { mutation } from './_generated/server';
|
||||
import { authComponent, updatePassword } from './auth';
|
||||
|
||||
/**
|
||||
* Alterar senha do usuário autenticado
|
||||
@@ -47,7 +46,7 @@ export const alterarSenha = mutation({
|
||||
} catch (error: any) {
|
||||
// Capturar erros específicos do Better Auth
|
||||
let mensagemErro = 'Erro ao alterar senha';
|
||||
|
||||
|
||||
if (error?.message) {
|
||||
mensagemErro = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
@@ -55,10 +54,12 @@ export const alterarSenha = mutation({
|
||||
}
|
||||
|
||||
// Mensagens de erro mais amigáveis
|
||||
if (mensagemErro.toLowerCase().includes('password') ||
|
||||
mensagemErro.toLowerCase().includes('senha') ||
|
||||
mensagemErro.toLowerCase().includes('incorrect') ||
|
||||
mensagemErro.toLowerCase().includes('incorreta')) {
|
||||
if (
|
||||
mensagemErro.toLowerCase().includes('password') ||
|
||||
mensagemErro.toLowerCase().includes('senha') ||
|
||||
mensagemErro.toLowerCase().includes('incorrect') ||
|
||||
mensagemErro.toLowerCase().includes('incorreta')
|
||||
) {
|
||||
mensagemErro = 'Senha atual incorreta';
|
||||
}
|
||||
|
||||
@@ -69,4 +70,3 @@ export const alterarSenha = mutation({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: 'convex'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { MutationCtx, query, QueryCtx } from './_generated/server';
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { components } from './_generated/api';
|
||||
import type { DataModel } from './_generated/dataModel';
|
||||
import { type MutationCtx, type QueryCtx, query } from './_generated/server';
|
||||
|
||||
const siteUrl = process.env.SITE_URL!;
|
||||
|
||||
|
||||
@@ -7,127 +7,112 @@
|
||||
* Gera um hash seguro de senha usando PBKDF2
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
|
||||
// Gerar salt aleatório
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Importar a senha como chave
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
data,
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
// Derivar a chave usando PBKDF2
|
||||
const derivedBits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
// Combinar salt + hash
|
||||
const hashArray = new Uint8Array(derivedBits);
|
||||
const combined = new Uint8Array(salt.length + hashArray.length);
|
||||
combined.set(salt);
|
||||
combined.set(hashArray, salt.length);
|
||||
|
||||
// Converter para base64
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
|
||||
// Gerar salt aleatório
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Importar a senha como chave
|
||||
const keyMaterial = await crypto.subtle.importKey('raw', data, 'PBKDF2', false, ['deriveBits']);
|
||||
|
||||
// Derivar a chave usando PBKDF2
|
||||
const derivedBits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
// Combinar salt + hash
|
||||
const hashArray = new Uint8Array(derivedBits);
|
||||
const combined = new Uint8Array(salt.length + hashArray.length);
|
||||
combined.set(salt);
|
||||
combined.set(hashArray, salt.length);
|
||||
|
||||
// Converter para base64
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma senha corresponde ao hash
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Decodificar o hash de base64
|
||||
const combined = Uint8Array.from(atob(hash), (c) => c.charCodeAt(0));
|
||||
|
||||
// Extrair salt e hash
|
||||
const salt = combined.slice(0, 16);
|
||||
const storedHash = combined.slice(16);
|
||||
|
||||
// Gerar hash da senha fornecida
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
data,
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
|
||||
const derivedBits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
const newHash = new Uint8Array(derivedBits);
|
||||
|
||||
// Comparar os hashes
|
||||
if (newHash.length !== storedHash.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < newHash.length; i++) {
|
||||
if (newHash[i] !== storedHash[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Erro ao verificar senha:", error);
|
||||
return false;
|
||||
}
|
||||
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
try {
|
||||
// Decodificar o hash de base64
|
||||
const combined = Uint8Array.from(atob(hash), (c) => c.charCodeAt(0));
|
||||
|
||||
// Extrair salt e hash
|
||||
const salt = combined.slice(0, 16);
|
||||
const storedHash = combined.slice(16);
|
||||
|
||||
// Gerar hash da senha fornecida
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
|
||||
const keyMaterial = await crypto.subtle.importKey('raw', data, 'PBKDF2', false, ['deriveBits']);
|
||||
|
||||
const derivedBits = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
const newHash = new Uint8Array(derivedBits);
|
||||
|
||||
// Comparar os hashes
|
||||
if (newHash.length !== storedHash.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < newHash.length; i++) {
|
||||
if (newHash[i] !== storedHash[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar senha:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera um token aleatório seguro
|
||||
*/
|
||||
export function generateToken(): string {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode(...array))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode(...array))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida formato de matrícula (apenas números)
|
||||
*/
|
||||
export function validarMatricula(matricula: string): boolean {
|
||||
return /^\d+$/.test(matricula) && matricula.length >= 3;
|
||||
return /^\d+$/.test(matricula) && matricula.length >= 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida formato de senha (alfanuméricos e símbolos)
|
||||
*/
|
||||
export function validarSenha(senha: string): boolean {
|
||||
// Mínimo 8 caracteres, pelo menos uma letra, um número e um símbolo
|
||||
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
|
||||
return regex.test(senha);
|
||||
// Mínimo 8 caracteres, pelo menos uma letra, um número e um símbolo
|
||||
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
|
||||
return regex.test(senha);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,97 +124,93 @@ export function validarSenha(senha: string): boolean {
|
||||
// Chave de criptografia derivada (em produção, deve vir de variável de ambiente)
|
||||
// Para desenvolvimento, usando uma chave fixa. Em produção, deve ser configurada via env var.
|
||||
const getEncryptionKey = async (): Promise<CryptoKey> => {
|
||||
// Chave base - em produção, isso deve vir de process.env.ENCRYPTION_KEY
|
||||
// Por enquanto, usando uma chave derivada de um valor fixo
|
||||
const keyMaterial = new TextEncoder().encode("SGSE-EMAIL-ENCRYPTION-KEY-2024");
|
||||
|
||||
// Deriva uma chave de 256 bits usando PBKDF2
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyMaterial,
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits", "deriveKey"]
|
||||
);
|
||||
// Chave base - em produção, isso deve vir de process.env.ENCRYPTION_KEY
|
||||
// Por enquanto, usando uma chave derivada de um valor fixo
|
||||
const keyMaterial = new TextEncoder().encode('SGSE-EMAIL-ENCRYPTION-KEY-2024');
|
||||
|
||||
return await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: new TextEncoder().encode("SGSE-SALT"),
|
||||
iterations: 100000,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
key,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
// Deriva uma chave de 256 bits usando PBKDF2
|
||||
const key = await crypto.subtle.importKey('raw', keyMaterial, { name: 'PBKDF2' }, false, [
|
||||
'deriveBits',
|
||||
'deriveKey'
|
||||
]);
|
||||
|
||||
return await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: new TextEncoder().encode('SGSE-SALT'),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
key,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Criptografa uma senha SMTP usando AES-GCM
|
||||
*/
|
||||
export async function encryptSMTPPassword(password: string): Promise<string> {
|
||||
try {
|
||||
const key = await getEncryptionKey();
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
try {
|
||||
const key = await getEncryptionKey();
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(password);
|
||||
|
||||
// Gerar IV (Initialization Vector) aleatório
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
// Gerar IV (Initialization Vector) aleatório
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Criptografar
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
// Criptografar
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
data
|
||||
);
|
||||
|
||||
// Combinar IV + dados criptografados e converter para base64
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
// Combinar IV + dados criptografados e converter para base64
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
combined.set(iv);
|
||||
combined.set(new Uint8Array(encrypted), iv.length);
|
||||
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
} catch (error) {
|
||||
console.error("Erro ao criptografar senha SMTP:", error);
|
||||
throw new Error("Falha ao criptografar senha SMTP");
|
||||
}
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
} catch (error) {
|
||||
console.error('Erro ao criptografar senha SMTP:', error);
|
||||
throw new Error('Falha ao criptografar senha SMTP');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptografa uma senha SMTP usando AES-GCM
|
||||
*/
|
||||
export async function decryptSMTPPassword(encryptedPassword: string): Promise<string> {
|
||||
try {
|
||||
const key = await getEncryptionKey();
|
||||
|
||||
// Decodificar base64
|
||||
const combined = Uint8Array.from(atob(encryptedPassword), (c) => c.charCodeAt(0));
|
||||
try {
|
||||
const key = await getEncryptionKey();
|
||||
|
||||
// Extrair IV e dados criptografados
|
||||
const iv = combined.slice(0, 12);
|
||||
const encrypted = combined.slice(12);
|
||||
// Decodificar base64
|
||||
const combined = Uint8Array.from(atob(encryptedPassword), (c) => c.charCodeAt(0));
|
||||
|
||||
// Descriptografar
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
},
|
||||
key,
|
||||
encrypted
|
||||
);
|
||||
// Extrair IV e dados criptografados
|
||||
const iv = combined.slice(0, 12);
|
||||
const encrypted = combined.slice(12);
|
||||
|
||||
// Converter para string
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
} catch (error) {
|
||||
console.error("Erro ao descriptografar senha SMTP:", error);
|
||||
throw new Error("Falha ao descriptografar senha SMTP");
|
||||
}
|
||||
// Descriptografar
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
encrypted
|
||||
);
|
||||
|
||||
// Converter para string
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
} catch (error) {
|
||||
console.error('Erro ao descriptografar senha SMTP:', error);
|
||||
throw new Error('Falha ao descriptografar senha SMTP');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
@@ -35,8 +35,11 @@ async function gerarRoomName(
|
||||
const roomPrefix = configJitsi?.roomPrefix || 'sgse';
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 9);
|
||||
const conversaHash = conversaId.replace('conversas|', '').replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
|
||||
|
||||
const conversaHash = conversaId
|
||||
.replace('conversas|', '')
|
||||
.replace(/[^a-zA-Z0-9]/g, '')
|
||||
.substring(0, 10);
|
||||
|
||||
return `${roomPrefix}-${tipo}-${conversaHash}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
@@ -93,10 +96,7 @@ export const criarChamada = mutation({
|
||||
.query('chamadas')
|
||||
.withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId))
|
||||
.filter((q) =>
|
||||
q.or(
|
||||
q.eq(q.field('status'), 'aguardando'),
|
||||
q.eq(q.field('status'), 'em_andamento')
|
||||
)
|
||||
q.or(q.eq(q.field('status'), 'aguardando'), q.eq(q.field('status'), 'em_andamento'))
|
||||
)
|
||||
.collect();
|
||||
|
||||
@@ -123,11 +123,15 @@ export const criarChamada = mutation({
|
||||
gravando: false,
|
||||
configuracoes: {
|
||||
audioHabilitado: args.audioHabilitado ?? true,
|
||||
videoHabilitado: args.videoHabilitado ?? (args.tipo === 'video'),
|
||||
videoHabilitado: args.videoHabilitado ?? args.tipo === 'video',
|
||||
participantesConfig: conversa.participantes.map((participanteId) => ({
|
||||
usuarioId: participanteId,
|
||||
audioHabilitado: participanteId === usuarioAtual._id ? (args.audioHabilitado ?? true) : true,
|
||||
videoHabilitado: participanteId === usuarioAtual._id ? (args.videoHabilitado ?? (args.tipo === 'video')) : (args.tipo === 'video'),
|
||||
audioHabilitado:
|
||||
participanteId === usuarioAtual._id ? (args.audioHabilitado ?? true) : true,
|
||||
videoHabilitado:
|
||||
participanteId === usuarioAtual._id
|
||||
? (args.videoHabilitado ?? args.tipo === 'video')
|
||||
: args.tipo === 'video',
|
||||
forcadoPeloAnfitriao: false
|
||||
}))
|
||||
},
|
||||
@@ -280,15 +284,18 @@ export const adicionarParticipante = mutation({
|
||||
|
||||
// Atualizar participantes
|
||||
const novosParticipantes = [...chamada.participantes, args.usuarioId];
|
||||
|
||||
|
||||
// Atualizar configurações
|
||||
const configParticipantes = chamada.configuracoes?.participantesConfig || [];
|
||||
const novaConfig = [...configParticipantes, {
|
||||
usuarioId: args.usuarioId,
|
||||
audioHabilitado: chamada.configuracoes?.audioHabilitado ?? true,
|
||||
videoHabilitado: chamada.configuracoes?.videoHabilitado ?? (chamada.tipo === 'video'),
|
||||
forcadoPeloAnfitriao: false
|
||||
}];
|
||||
const novaConfig = [
|
||||
...configParticipantes,
|
||||
{
|
||||
usuarioId: args.usuarioId,
|
||||
audioHabilitado: chamada.configuracoes?.audioHabilitado ?? true,
|
||||
videoHabilitado: chamada.configuracoes?.videoHabilitado ?? chamada.tipo === 'video',
|
||||
forcadoPeloAnfitriao: false
|
||||
}
|
||||
];
|
||||
|
||||
await ctx.db.patch(args.chamadaId, {
|
||||
participantes: novosParticipantes,
|
||||
@@ -333,7 +340,7 @@ export const removerParticipante = mutation({
|
||||
|
||||
// Atualizar participantes
|
||||
const novosParticipantes = chamada.participantes.filter((id) => id !== args.usuarioId);
|
||||
|
||||
|
||||
// Atualizar configurações
|
||||
const configParticipantes = chamada.configuracoes?.participantesConfig || [];
|
||||
const novaConfig = configParticipantes.filter((config) => config.usuarioId !== args.usuarioId);
|
||||
@@ -388,17 +395,32 @@ export const toggleAudioVideoParticipante = mutation({
|
||||
// Adicionar configuração se não existir
|
||||
configParticipantes.push({
|
||||
usuarioId: args.participanteId,
|
||||
audioHabilitado: args.tipo === 'audio' ? args.habilitado : (chamada.configuracoes?.audioHabilitado ?? true),
|
||||
videoHabilitado: args.tipo === 'video' ? args.habilitado : (chamada.configuracoes?.videoHabilitado ?? (chamada.tipo === 'video')),
|
||||
audioHabilitado:
|
||||
args.tipo === 'audio'
|
||||
? args.habilitado
|
||||
: (chamada.configuracoes?.audioHabilitado ?? true),
|
||||
videoHabilitado:
|
||||
args.tipo === 'video'
|
||||
? args.habilitado
|
||||
: (chamada.configuracoes?.videoHabilitado ?? chamada.tipo === 'video'),
|
||||
forcadoPeloAnfitriao: !ehOProprioParticipante && args.habilitado === false
|
||||
});
|
||||
} else {
|
||||
// Atualizar configuração existente
|
||||
configParticipantes[participanteIndex] = {
|
||||
...configParticipantes[participanteIndex],
|
||||
audioHabilitado: args.tipo === 'audio' ? args.habilitado : configParticipantes[participanteIndex].audioHabilitado,
|
||||
videoHabilitado: args.tipo === 'video' ? args.habilitado : configParticipantes[participanteIndex].videoHabilitado,
|
||||
forcadoPeloAnfitriao: !ehOProprioParticipante && !args.habilitado ? true : configParticipantes[participanteIndex].forcadoPeloAnfitriao
|
||||
audioHabilitado:
|
||||
args.tipo === 'audio'
|
||||
? args.habilitado
|
||||
: configParticipantes[participanteIndex].audioHabilitado,
|
||||
videoHabilitado:
|
||||
args.tipo === 'video'
|
||||
? args.habilitado
|
||||
: configParticipantes[participanteIndex].videoHabilitado,
|
||||
forcadoPeloAnfitriao:
|
||||
!ehOProprioParticipante && !args.habilitado
|
||||
? true
|
||||
: configParticipantes[participanteIndex].forcadoPeloAnfitriao
|
||||
};
|
||||
}
|
||||
|
||||
@@ -509,10 +531,7 @@ export const obterChamadaAtiva = query({
|
||||
.query('chamadas')
|
||||
.withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId))
|
||||
.filter((q) =>
|
||||
q.or(
|
||||
q.eq(q.field('status'), 'aguardando'),
|
||||
q.eq(q.field('status'), 'em_andamento')
|
||||
)
|
||||
q.or(q.eq(q.field('status'), 'aguardando'), q.eq(q.field('status'), 'em_andamento'))
|
||||
)
|
||||
.collect();
|
||||
|
||||
@@ -589,5 +608,3 @@ export const obterChamada = query({
|
||||
return chamada;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query, internalMutation } from './_generated/server';
|
||||
import { Doc, Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import { internal, api } from './_generated/api';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
@@ -917,22 +917,34 @@ export const editarMensagem = mutation({
|
||||
|
||||
// Verificar se usuário é o remetente
|
||||
if (mensagem.remetenteId !== usuarioAtual._id) {
|
||||
return { sucesso: false, erro: 'Você só pode editar suas próprias mensagens' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Você só pode editar suas próprias mensagens'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se mensagem não foi deletada
|
||||
if (mensagem.deletada) {
|
||||
return { sucesso: false, erro: 'Não é possível editar uma mensagem deletada' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Não é possível editar uma mensagem deletada'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se não é mensagem agendada
|
||||
if (mensagem.agendadaPara) {
|
||||
return { sucesso: false, erro: 'Não é possível editar mensagens agendadas' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Não é possível editar mensagens agendadas'
|
||||
};
|
||||
}
|
||||
|
||||
// Validar novo conteúdo
|
||||
if (!args.novoConteudo || args.novoConteudo.trim().length === 0) {
|
||||
return { sucesso: false, erro: 'O conteúdo da mensagem não pode estar vazio' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'O conteúdo da mensagem não pode estar vazio'
|
||||
};
|
||||
}
|
||||
|
||||
// Normalizar conteúdo para busca
|
||||
@@ -1104,13 +1116,19 @@ export const adicionarParticipanteSala = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
|
||||
if (!isAdmin) {
|
||||
return { sucesso: false, erro: 'Apenas administradores podem adicionar participantes' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Apenas administradores podem adicionar participantes'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se participante já está na sala
|
||||
@@ -1168,13 +1186,19 @@ export const removerParticipanteSala = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
|
||||
if (!isAdmin) {
|
||||
return { sucesso: false, erro: 'Apenas administradores podem remover participantes' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Apenas administradores podem remover participantes'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se participante está na sala
|
||||
@@ -1189,7 +1213,10 @@ export const removerParticipanteSala = mutation({
|
||||
args.participanteId
|
||||
);
|
||||
if (isParticipanteAdmin) {
|
||||
return { sucesso: false, erro: 'Não é possível remover outros administradores' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Não é possível remover outros administradores'
|
||||
};
|
||||
}
|
||||
|
||||
// Remover participante
|
||||
@@ -1239,7 +1266,10 @@ export const promoverAdministrador = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
@@ -1314,7 +1344,10 @@ export const rebaixarAdministrador = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
@@ -1343,7 +1376,10 @@ export const rebaixarAdministrador = mutation({
|
||||
|
||||
// Não permitir rebaixar o criador da sala
|
||||
if (conversa.criadoPor === args.participanteId) {
|
||||
return { sucesso: false, erro: 'Não é possível rebaixar o criador da sala' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Não é possível rebaixar o criador da sala'
|
||||
};
|
||||
}
|
||||
|
||||
// Remover da lista de administradores
|
||||
@@ -1465,13 +1501,19 @@ export const encerrarReuniao = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
|
||||
if (!isAdmin) {
|
||||
return { sucesso: false, erro: 'Apenas administradores podem encerrar a reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Apenas administradores podem encerrar a reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Criar notificação para todos os participantes informando que a reunião foi encerrada
|
||||
@@ -1524,13 +1566,19 @@ export const enviarNotificacaoReuniao = mutation({
|
||||
|
||||
// Verificar se é sala de reunião
|
||||
if (conversa.tipo !== 'sala_reuniao') {
|
||||
return { sucesso: false, erro: 'Esta funcionalidade é apenas para salas de reunião' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Esta funcionalidade é apenas para salas de reunião'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se usuário é administrador
|
||||
const isAdmin = await verificarPermissaoAdmin(ctx, args.conversaId, usuarioAtual._id);
|
||||
if (!isAdmin) {
|
||||
return { sucesso: false, erro: 'Apenas administradores podem enviar notificações' };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Apenas administradores podem enviar notificações'
|
||||
};
|
||||
}
|
||||
|
||||
// Criar notificação para todos os participantes
|
||||
@@ -2033,7 +2081,7 @@ export const listarTodosUsuarios = query({
|
||||
usuarios
|
||||
.filter((u) => u._id !== usuarioAtual._id)
|
||||
.map(async (u) => {
|
||||
let matricula: string | undefined = undefined;
|
||||
let matricula: string | undefined;
|
||||
if (u.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(u.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const getComprasSetor = query({
|
||||
|
||||
@@ -1,229 +1,247 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, action, internalMutation } from "./_generated/server";
|
||||
import { encryptSMTPPassword } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import { action, internalMutation, mutation, query } from './_generated/server';
|
||||
import { encryptSMTPPassword } from './auth/utils';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
|
||||
/**
|
||||
* Obter configuração de email ativa (senha mascarada)
|
||||
*/
|
||||
export const obterConfigEmail = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retornar config com senha mascarada
|
||||
return {
|
||||
_id: config._id,
|
||||
servidor: config.servidor,
|
||||
porta: config.porta,
|
||||
usuario: config.usuario,
|
||||
senhaHash: "********", // Mascarar senha
|
||||
emailRemetente: config.emailRemetente,
|
||||
nomeRemetente: config.nomeRemetente,
|
||||
usarSSL: config.usarSSL,
|
||||
usarTLS: config.usarTLS,
|
||||
ativo: config.ativo,
|
||||
testadoEm: config.testadoEm,
|
||||
atualizadoEm: config.atualizadoEm,
|
||||
};
|
||||
},
|
||||
// Retornar config com senha mascarada
|
||||
return {
|
||||
_id: config._id,
|
||||
servidor: config.servidor,
|
||||
porta: config.porta,
|
||||
usuario: config.usuario,
|
||||
senhaHash: '********', // Mascarar senha
|
||||
emailRemetente: config.emailRemetente,
|
||||
nomeRemetente: config.nomeRemetente,
|
||||
usarSSL: config.usarSSL,
|
||||
usarTLS: config.usarTLS,
|
||||
ativo: config.ativo,
|
||||
testadoEm: config.testadoEm,
|
||||
atualizadoEm: config.atualizadoEm
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Salvar configuração de email (apenas TI_MASTER)
|
||||
*/
|
||||
export const salvarConfigEmail = mutation({
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
emailRemetente: v.string(),
|
||||
nomeRemetente: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
configuradoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), configId: v.id("configuracaoEmail") }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(args.emailRemetente)) {
|
||||
return { sucesso: false as const, erro: "Email remetente inválido" };
|
||||
}
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
emailRemetente: v.string(),
|
||||
nomeRemetente: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
configuradoPorId: v.id('usuarios')
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), configId: v.id('configuracaoEmail') }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(args.emailRemetente)) {
|
||||
return { sucesso: false as const, erro: 'Email remetente inválido' };
|
||||
}
|
||||
|
||||
// Validar porta
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: "Porta deve ser um número entre 1 e 65535" };
|
||||
}
|
||||
// Validar porta
|
||||
if (args.porta < 1 || args.porta > 65535) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Porta deve ser um número entre 1 e 65535'
|
||||
};
|
||||
}
|
||||
|
||||
// Buscar config ativa anterior para manter senha se não fornecida
|
||||
const configAtiva = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
// Buscar config ativa anterior para manter senha se não fornecida
|
||||
const configAtiva = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
// Determinar senhaHash: usar nova senha se fornecida, senão manter a atual
|
||||
let senhaHash: string;
|
||||
if (args.senha && args.senha.trim().length > 0) {
|
||||
// Nova senha fornecida, criptografar usando criptografia reversível (AES)
|
||||
senhaHash = await encryptSMTPPassword(args.senha);
|
||||
} else if (configAtiva) {
|
||||
// Senha não fornecida, manter a atual (já criptografada)
|
||||
senhaHash = configAtiva.senhaHash;
|
||||
} else {
|
||||
// Sem senha e sem config existente - erro
|
||||
return { sucesso: false as const, erro: "Senha é obrigatória para nova configuração" };
|
||||
}
|
||||
// Determinar senhaHash: usar nova senha se fornecida, senão manter a atual
|
||||
let senhaHash: string;
|
||||
if (args.senha && args.senha.trim().length > 0) {
|
||||
// Nova senha fornecida, criptografar usando criptografia reversível (AES)
|
||||
senhaHash = await encryptSMTPPassword(args.senha);
|
||||
} else if (configAtiva) {
|
||||
// Senha não fornecida, manter a atual (já criptografada)
|
||||
senhaHash = configAtiva.senhaHash;
|
||||
} else {
|
||||
// Sem senha e sem config existente - erro
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Senha é obrigatória para nova configuração'
|
||||
};
|
||||
}
|
||||
|
||||
// Desativar config anterior
|
||||
const configsAntigas = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.collect();
|
||||
// Desativar config anterior
|
||||
const configsAntigas = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
for (const config of configsAntigas) {
|
||||
await ctx.db.patch(config._id, { ativo: false });
|
||||
}
|
||||
for (const config of configsAntigas) {
|
||||
await ctx.db.patch(config._id, { ativo: false });
|
||||
}
|
||||
|
||||
// Criar nova config
|
||||
const configId = await ctx.db.insert("configuracaoEmail", {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senhaHash,
|
||||
emailRemetente: args.emailRemetente,
|
||||
nomeRemetente: args.nomeRemetente,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
ativo: true,
|
||||
configuradoPor: args.configuradoPorId,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
// Criar nova config
|
||||
const configId = await ctx.db.insert('configuracaoEmail', {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senhaHash,
|
||||
emailRemetente: args.emailRemetente,
|
||||
nomeRemetente: args.nomeRemetente,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
ativo: true,
|
||||
configuradoPor: args.configuradoPorId,
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.configuradoPorId,
|
||||
"configurar",
|
||||
"email",
|
||||
JSON.stringify({ servidor: args.servidor, porta: args.porta }),
|
||||
configId
|
||||
);
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.configuradoPorId,
|
||||
'configurar',
|
||||
'email',
|
||||
JSON.stringify({ servidor: args.servidor, porta: args.porta }),
|
||||
configId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, configId };
|
||||
},
|
||||
return { sucesso: true as const, configId };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para atualizar testadoEm
|
||||
*/
|
||||
export const atualizarTestadoEm = internalMutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoEmail"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
});
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
configId: v.id('configuracaoEmail')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Testar conexão SMTP (action que chama action real)
|
||||
*/
|
||||
export const testarConexaoSMTP = action({
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<{ sucesso: true } | { sucesso: false; erro: string }> => {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Servidor SMTP não pode estar vazio" };
|
||||
}
|
||||
args: {
|
||||
servidor: v.string(),
|
||||
porta: v.number(),
|
||||
usuario: v.string(),
|
||||
senha: v.string(),
|
||||
usarSSL: v.boolean(),
|
||||
usarTLS: v.boolean()
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<{ sucesso: true } | { sucesso: false; erro: string }> => {
|
||||
// Validações básicas
|
||||
if (!args.servidor || args.servidor.trim().length === 0) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Servidor SMTP não pode estar vazio'
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.porta || args.porta < 1 || args.porta > 65535) {
|
||||
return { sucesso: false as const, erro: "Porta inválida. Deve ser entre 1 e 65535" };
|
||||
}
|
||||
if (!args.porta || args.porta < 1 || args.porta > 65535) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Porta inválida. Deve ser entre 1 e 65535'
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.usuario || args.usuario.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Usuário não pode estar vazio" };
|
||||
}
|
||||
if (!args.usuario || args.usuario.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: 'Usuário não pode estar vazio' };
|
||||
}
|
||||
|
||||
if (!args.senha || args.senha.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Senha não pode estar vazia" };
|
||||
}
|
||||
if (!args.senha || args.senha.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: 'Senha não pode estar vazia' };
|
||||
}
|
||||
|
||||
// Validação de SSL/TLS mutuamente exclusivos
|
||||
if (args.usarSSL && args.usarTLS) {
|
||||
return { sucesso: false as const, erro: "SSL e TLS não podem estar habilitados simultaneamente" };
|
||||
}
|
||||
// Validação de SSL/TLS mutuamente exclusivos
|
||||
if (args.usarSSL && args.usarTLS) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'SSL e TLS não podem estar habilitados simultaneamente'
|
||||
};
|
||||
}
|
||||
|
||||
// Chamar action de teste real (que usa nodemailer)
|
||||
try {
|
||||
const resultado: { sucesso: true } | { sucesso: false; erro: string } = await ctx.runAction(api.actions.smtp.testarConexao, {
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senha: args.senha,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS,
|
||||
});
|
||||
// Chamar action de teste real (que usa nodemailer)
|
||||
try {
|
||||
const resultado: { sucesso: true } | { sucesso: false; erro: string } = await ctx.runAction(
|
||||
api.actions.smtp.testarConexao,
|
||||
{
|
||||
servidor: args.servidor,
|
||||
porta: args.porta,
|
||||
usuario: args.usuario,
|
||||
senha: args.senha,
|
||||
usarSSL: args.usarSSL,
|
||||
usarTLS: args.usarTLS
|
||||
}
|
||||
);
|
||||
|
||||
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
|
||||
if (resultado.sucesso) {
|
||||
const configAtiva = await ctx.runQuery(api.configuracaoEmail.obterConfigEmail, {});
|
||||
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoEmail.atualizarTestadoEm, {
|
||||
configId: configAtiva._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
|
||||
if (resultado.sucesso) {
|
||||
const configAtiva = await ctx.runQuery(api.configuracaoEmail.obterConfigEmail, {});
|
||||
|
||||
return resultado;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: errorMessage || "Erro ao conectar com o servidor SMTP"
|
||||
};
|
||||
}
|
||||
},
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoEmail.atualizarTestadoEm, {
|
||||
configId: configAtiva._id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resultado;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: errorMessage || 'Erro ao conectar com o servidor SMTP'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Marcar que a configuração foi testada com sucesso
|
||||
*/
|
||||
export const marcarConfigTestada = mutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
});
|
||||
},
|
||||
args: {
|
||||
configId: v.id('configuracaoEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, action, internalMutation } from "./_generated/server";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import { action, internalMutation, mutation, query } from './_generated/server';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
|
||||
/**
|
||||
* Obter configuração de Jitsi ativa
|
||||
@@ -10,8 +10,8 @@ export const obterConfigJitsi = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query("configuracaoJitsi")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.query('configuracaoJitsi')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
@@ -27,12 +27,11 @@ export const obterConfigJitsi = query({
|
||||
acceptSelfSignedCert: config.acceptSelfSignedCert ?? false, // Default para false se não existir
|
||||
ativo: config.ativo,
|
||||
testadoEm: config.testadoEm,
|
||||
atualizadoEm: config.atualizadoEm,
|
||||
atualizadoEm: config.atualizadoEm
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Salvar configuração de Jitsi (apenas TI_MASTER)
|
||||
*/
|
||||
@@ -43,26 +42,29 @@ export const salvarConfigJitsi = mutation({
|
||||
roomPrefix: v.string(),
|
||||
useHttps: v.boolean(),
|
||||
acceptSelfSignedCert: v.boolean(),
|
||||
configuradoPorId: v.id("usuarios"),
|
||||
configuradoPorId: v.id('usuarios')
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), configId: v.id("configuracaoJitsi") }),
|
||||
v.object({ sucesso: v.literal(true), configId: v.id('configuracaoJitsi') }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Validar domínio (deve ser não vazio)
|
||||
if (!args.domain || args.domain.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Domínio não pode estar vazio" };
|
||||
return { sucesso: false as const, erro: 'Domínio não pode estar vazio' };
|
||||
}
|
||||
|
||||
// Validar appId (deve ser não vazio)
|
||||
if (!args.appId || args.appId.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "App ID não pode estar vazio" };
|
||||
return { sucesso: false as const, erro: 'App ID não pode estar vazio' };
|
||||
}
|
||||
|
||||
// Validar roomPrefix (deve ser não vazio e alfanumérico)
|
||||
if (!args.roomPrefix || args.roomPrefix.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Prefixo de sala não pode estar vazio" };
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: 'Prefixo de sala não pode estar vazio'
|
||||
};
|
||||
}
|
||||
|
||||
// Validar formato do roomPrefix (apenas letras, números e hífens)
|
||||
@@ -70,14 +72,14 @@ export const salvarConfigJitsi = mutation({
|
||||
if (!roomPrefixRegex.test(args.roomPrefix.trim())) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Prefixo de sala deve conter apenas letras, números e hífens",
|
||||
erro: 'Prefixo de sala deve conter apenas letras, números e hífens'
|
||||
};
|
||||
}
|
||||
|
||||
// Desativar config anterior
|
||||
const configsAntigas = await ctx.db
|
||||
.query("configuracaoJitsi")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.query('configuracaoJitsi')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
for (const config of configsAntigas) {
|
||||
@@ -85,7 +87,7 @@ export const salvarConfigJitsi = mutation({
|
||||
}
|
||||
|
||||
// Criar nova config
|
||||
const configId = await ctx.db.insert("configuracaoJitsi", {
|
||||
const configId = await ctx.db.insert('configuracaoJitsi', {
|
||||
domain: args.domain.trim(),
|
||||
appId: args.appId.trim(),
|
||||
roomPrefix: args.roomPrefix.trim(),
|
||||
@@ -93,21 +95,21 @@ export const salvarConfigJitsi = mutation({
|
||||
acceptSelfSignedCert: args.acceptSelfSignedCert,
|
||||
ativo: true,
|
||||
configuradoPor: args.configuradoPorId,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.configuradoPorId,
|
||||
"configurar",
|
||||
"jitsi",
|
||||
'configurar',
|
||||
'jitsi',
|
||||
JSON.stringify({ domain: args.domain, appId: args.appId }),
|
||||
configId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, configId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -115,15 +117,15 @@ export const salvarConfigJitsi = mutation({
|
||||
*/
|
||||
export const atualizarTestadoEm = internalMutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoJitsi"),
|
||||
configId: v.id('configuracaoJitsi')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
return null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -133,22 +135,25 @@ export const testarConexaoJitsi = action({
|
||||
args: {
|
||||
domain: v.string(),
|
||||
useHttps: v.boolean(),
|
||||
acceptSelfSignedCert: v.optional(v.boolean()),
|
||||
acceptSelfSignedCert: v.optional(v.boolean())
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), aviso: v.optional(v.string()) }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args): Promise<{ sucesso: true; aviso?: string } | { sucesso: false; erro: string }> => {
|
||||
handler: async (
|
||||
ctx,
|
||||
args
|
||||
): Promise<{ sucesso: true; aviso?: string } | { sucesso: false; erro: string }> => {
|
||||
// Validações básicas
|
||||
if (!args.domain || args.domain.trim().length === 0) {
|
||||
return { sucesso: false as const, erro: "Domínio não pode estar vazio" };
|
||||
return { sucesso: false as const, erro: 'Domínio não pode estar vazio' };
|
||||
}
|
||||
|
||||
try {
|
||||
const protocol = args.useHttps ? "https" : "http";
|
||||
const protocol = args.useHttps ? 'https' : 'http';
|
||||
// Extrair host e porta do domain
|
||||
const [host, portStr] = args.domain.split(":");
|
||||
const [host, portStr] = args.domain.split(':');
|
||||
const port = portStr ? parseInt(portStr, 10) : args.useHttps ? 443 : 80;
|
||||
const url = `${protocol}://${host}:${port}/http-bind`;
|
||||
|
||||
@@ -159,11 +164,11 @@ export const testarConexaoJitsi = action({
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
@@ -176,7 +181,7 @@ export const testarConexaoJitsi = action({
|
||||
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
|
||||
configId: configAtiva._id,
|
||||
configId: configAtiva._id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,30 +189,29 @@ export const testarConexaoJitsi = action({
|
||||
} else {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Servidor retornou status ${response.status}`,
|
||||
erro: `Servidor retornou status ${response.status}`
|
||||
};
|
||||
}
|
||||
} catch (fetchError: unknown) {
|
||||
clearTimeout(timeoutId);
|
||||
const errorMessage =
|
||||
fetchError instanceof Error ? fetchError.message : String(fetchError);
|
||||
const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
||||
|
||||
// Se for erro de timeout
|
||||
if (errorMessage.includes("aborted") || errorMessage.includes("timeout")) {
|
||||
if (errorMessage.includes('aborted') || errorMessage.includes('timeout')) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: "Timeout: Servidor não respondeu em 5 segundos",
|
||||
erro: 'Timeout: Servidor não respondeu em 5 segundos'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se é erro de certificado SSL autoassinado
|
||||
const isSSLError =
|
||||
errorMessage.includes("CERTIFICATE_VERIFY_FAILED") ||
|
||||
errorMessage.includes("self signed certificate") ||
|
||||
errorMessage.includes("self-signed certificate") ||
|
||||
errorMessage.includes("certificate") ||
|
||||
errorMessage.includes("SSL") ||
|
||||
errorMessage.includes("certificate verify failed");
|
||||
const isSSLError =
|
||||
errorMessage.includes('CERTIFICATE_VERIFY_FAILED') ||
|
||||
errorMessage.includes('self signed certificate') ||
|
||||
errorMessage.includes('self-signed certificate') ||
|
||||
errorMessage.includes('certificate') ||
|
||||
errorMessage.includes('SSL') ||
|
||||
errorMessage.includes('certificate verify failed');
|
||||
|
||||
// Se for erro de certificado e aceitar autoassinado está configurado
|
||||
if (isSSLError && args.acceptSelfSignedCert) {
|
||||
@@ -218,26 +222,27 @@ export const testarConexaoJitsi = action({
|
||||
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
|
||||
configId: configAtiva._id,
|
||||
configId: configAtiva._id
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
sucesso: true as const,
|
||||
aviso: "Servidor acessível com certificado autoassinado. No navegador, você precisará aceitar o certificado manualmente na primeira conexão."
|
||||
aviso:
|
||||
'Servidor acessível com certificado autoassinado. No navegador, você precisará aceitar o certificado manualmente na primeira conexão.'
|
||||
};
|
||||
}
|
||||
|
||||
// Para servidores Jitsi, pode ser normal receber erro 405 (Method Not Allowed)
|
||||
// para GET em /http-bind, pois esse endpoint espera POST (BOSH)
|
||||
// Isso indica que o servidor está acessível, apenas não aceita GET
|
||||
if (errorMessage.includes("405") || errorMessage.includes("Method Not Allowed")) {
|
||||
if (errorMessage.includes('405') || errorMessage.includes('Method Not Allowed')) {
|
||||
// Se o teste foi bem-sucedido e há uma config ativa, atualizar testadoEm
|
||||
const configAtiva = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {});
|
||||
|
||||
if (configAtiva) {
|
||||
await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, {
|
||||
configId: configAtiva._id,
|
||||
configId: configAtiva._id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,23 +253,23 @@ export const testarConexaoJitsi = action({
|
||||
if (isSSLError) {
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro de certificado SSL: O servidor está usando um certificado não confiável (provavelmente autoassinado). Para desenvolvimento local, habilite "Aceitar Certificados Autoassinados" nas configurações de segurança. Em produção, use um certificado válido (ex: Let's Encrypt).`,
|
||||
erro: `Erro de certificado SSL: O servidor está usando um certificado não confiável (provavelmente autoassinado). Para desenvolvimento local, habilite "Aceitar Certificados Autoassinados" nas configurações de segurança. Em produção, use um certificado válido (ex: Let's Encrypt).`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: `Erro ao conectar: ${errorMessage}`,
|
||||
erro: `Erro ao conectar: ${errorMessage}`
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false as const,
|
||||
erro: errorMessage || "Erro ao conectar com o servidor Jitsi",
|
||||
erro: errorMessage || 'Erro ao conectar com o servidor Jitsi'
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -272,13 +277,13 @@ export const testarConexaoJitsi = action({
|
||||
*/
|
||||
export const marcarConfigTestada = mutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoJitsi"),
|
||||
configId: v.id('configuracaoJitsi')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
testadoEm: Date.now(),
|
||||
testadoEm: Date.now()
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -286,16 +291,15 @@ export const marcarConfigTestada = mutation({
|
||||
*/
|
||||
export const marcarConfiguradoNoServidor = internalMutation({
|
||||
args: {
|
||||
configId: v.id("configuracaoJitsi"),
|
||||
configId: v.id('configuracaoJitsi')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
configuradoNoServidor: true,
|
||||
configuradoNoServidorEm: Date.now(),
|
||||
configuradoEm: Date.now(),
|
||||
configuradoEm: Date.now()
|
||||
});
|
||||
return null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
|
||||
/**
|
||||
* Valida formato de horário HH:mm
|
||||
@@ -36,7 +36,7 @@ export const obterConfiguracao = query({
|
||||
nomeSaida: 'Saída 2',
|
||||
validarLocalizacao: true,
|
||||
toleranciaDistanciaMetros: 100,
|
||||
ativo: false,
|
||||
ativo: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ export const obterConfiguracao = query({
|
||||
nomeRetornoAlmoco: config.nomeRetornoAlmoco || 'Entrada 2',
|
||||
nomeSaida: config.nomeSaida || 'Saída 2',
|
||||
validarLocalizacao: config.validarLocalizacao ?? true,
|
||||
toleranciaDistanciaMetros: config.toleranciaDistanciaMetros ?? 100,
|
||||
toleranciaDistanciaMetros: config.toleranciaDistanciaMetros ?? 100
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -68,7 +68,7 @@ export const salvarConfiguracao = mutation({
|
||||
nomeRetornoAlmoco: v.optional(v.string()),
|
||||
nomeSaida: v.optional(v.string()),
|
||||
validarLocalizacao: v.optional(v.boolean()),
|
||||
toleranciaDistanciaMetros: v.optional(v.number()),
|
||||
toleranciaDistanciaMetros: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -101,7 +101,9 @@ export const salvarConfiguracao = mutation({
|
||||
// Validar sequência lógica de horários
|
||||
const [horaEntrada, minutoEntrada] = args.horarioEntrada.split(':').map(Number);
|
||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = args.horarioSaidaAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = args.horarioRetornoAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = args.horarioRetornoAlmoco
|
||||
.split(':')
|
||||
.map(Number);
|
||||
const [horaSaida, minutoSaida] = args.horarioSaida.split(':').map(Number);
|
||||
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
@@ -151,10 +153,9 @@ export const salvarConfiguracao = mutation({
|
||||
toleranciaDistanciaMetros: args.toleranciaDistanciaMetros ?? 100,
|
||||
ativo: true,
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { configId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { action, internalMutation, internalQuery, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { api, internal } from './_generated/api';
|
||||
|
||||
/**
|
||||
* Tipo de retorno da configuração do relógio
|
||||
@@ -24,14 +24,13 @@ export const obterConfiguracao = query({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<ConfiguracaoRelogioRetorno> => {
|
||||
// Buscar todas as configurações e pegar a mais recente (por atualizadoEm)
|
||||
const configs = await ctx.db
|
||||
.query('configuracaoRelogio')
|
||||
.collect();
|
||||
|
||||
const configs = await ctx.db.query('configuracaoRelogio').collect();
|
||||
|
||||
// Pegar a configuração mais recente (ordenar por atualizadoEm desc)
|
||||
const config = configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
const config =
|
||||
configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
|
||||
if (!config) {
|
||||
// Retornar configuração padrão (GMT-3 para Brasília)
|
||||
@@ -42,7 +41,7 @@ export const obterConfiguracao = query({
|
||||
fallbackParaPC: true,
|
||||
ultimaSincronizacao: null,
|
||||
offsetSegundos: null,
|
||||
gmtOffset: -3, // GMT-3 para Brasília
|
||||
gmtOffset: -3 // GMT-3 para Brasília
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,9 +49,9 @@ export const obterConfiguracao = query({
|
||||
...config,
|
||||
ultimaSincronizacao: config.ultimaSincronizacao ?? null, // Converter undefined para null
|
||||
offsetSegundos: config.offsetSegundos ?? null, // Converter undefined para null
|
||||
gmtOffset: config.gmtOffset ?? -3, // Padrão GMT-3 para Brasília se não configurado
|
||||
gmtOffset: config.gmtOffset ?? -3 // Padrão GMT-3 para Brasília se não configurado
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -62,14 +61,13 @@ export const obterConfiguracaoInternal = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<ConfiguracaoRelogioRetorno> => {
|
||||
// Buscar todas as configurações e pegar a mais recente (por atualizadoEm)
|
||||
const configs = await ctx.db
|
||||
.query('configuracaoRelogio')
|
||||
.collect();
|
||||
|
||||
const configs = await ctx.db.query('configuracaoRelogio').collect();
|
||||
|
||||
// Pegar a configuração mais recente (ordenar por atualizadoEm desc)
|
||||
const config = configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
const config =
|
||||
configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
|
||||
if (!config) {
|
||||
// Retornar configuração padrão (GMT-3 para Brasília)
|
||||
@@ -80,7 +78,7 @@ export const obterConfiguracaoInternal = internalQuery({
|
||||
fallbackParaPC: true,
|
||||
ultimaSincronizacao: null,
|
||||
offsetSegundos: null,
|
||||
gmtOffset: -3, // GMT-3 para Brasília
|
||||
gmtOffset: -3 // GMT-3 para Brasília
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,9 +86,9 @@ export const obterConfiguracaoInternal = internalQuery({
|
||||
...config,
|
||||
ultimaSincronizacao: config.ultimaSincronizacao ?? null, // Converter undefined para null
|
||||
offsetSegundos: config.offsetSegundos ?? null, // Converter undefined para null
|
||||
gmtOffset: config.gmtOffset ?? -3, // Padrão GMT-3 para Brasília se não configurado
|
||||
gmtOffset: config.gmtOffset ?? -3 // Padrão GMT-3 para Brasília se não configurado
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -102,7 +100,7 @@ export const salvarConfiguracao = mutation({
|
||||
portaNTP: v.optional(v.number()),
|
||||
usarServidorExterno: v.boolean(),
|
||||
fallbackParaPC: v.boolean(),
|
||||
gmtOffset: v.optional(v.number()),
|
||||
gmtOffset: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -123,13 +121,12 @@ export const salvarConfiguracao = mutation({
|
||||
}
|
||||
|
||||
// Buscar configuração existente (pegar a mais recente)
|
||||
const configs = await ctx.db
|
||||
.query('configuracaoRelogio')
|
||||
.collect();
|
||||
|
||||
const configExistente = configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
const configs = await ctx.db.query('configuracaoRelogio').collect();
|
||||
|
||||
const configExistente =
|
||||
configs.length > 0
|
||||
? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0]
|
||||
: null;
|
||||
|
||||
if (configExistente) {
|
||||
// Atualizar configuração existente
|
||||
@@ -140,7 +137,7 @@ export const salvarConfiguracao = mutation({
|
||||
fallbackParaPC: args.fallbackParaPC,
|
||||
gmtOffset: args.gmtOffset ?? -3, // Padrão GMT-3 para Brasília
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
return { configId: configExistente._id };
|
||||
} else {
|
||||
@@ -152,11 +149,11 @@ export const salvarConfiguracao = mutation({
|
||||
fallbackParaPC: args.fallbackParaPC,
|
||||
gmtOffset: args.gmtOffset ?? -3, // Padrão GMT-3 para Brasília
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
return { configId };
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -167,9 +164,9 @@ export const obterTempoServidor = query({
|
||||
handler: async () => {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
data: new Date().toISOString(),
|
||||
data: new Date().toISOString()
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -191,14 +188,17 @@ export const sincronizarTempo = action({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<SincronizacaoRetorno> => {
|
||||
// Buscar configuração usando query interna para evitar referência circular
|
||||
const config: ConfiguracaoRelogioRetorno = await ctx.runQuery(internal.configuracaoRelogio.obterConfiguracaoInternal, {});
|
||||
const config: ConfiguracaoRelogioRetorno = await ctx.runQuery(
|
||||
internal.configuracaoRelogio.obterConfiguracaoInternal,
|
||||
{}
|
||||
);
|
||||
|
||||
if (!config.usarServidorExterno) {
|
||||
return {
|
||||
sucesso: true,
|
||||
timestamp: Date.now(),
|
||||
usandoServidorExterno: false,
|
||||
offsetSegundos: 0,
|
||||
offsetSegundos: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ export const sincronizarTempo = action({
|
||||
try {
|
||||
const servidorNTP = config.servidorNTP || 'pool.ntp.org';
|
||||
let serverTime: number;
|
||||
|
||||
|
||||
// Se o servidor configurado for uma URL HTTP/HTTPS, tentar usar diretamente
|
||||
if (servidorNTP.startsWith('http://') || servidorNTP.startsWith('https://')) {
|
||||
try {
|
||||
@@ -216,7 +216,11 @@ export const sincronizarTempo = action({
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao obter tempo do servidor configurado');
|
||||
}
|
||||
const data = (await response.json()) as { unixtime?: number; unixTime?: number; unixtimestamp?: number };
|
||||
const data = (await response.json()) as {
|
||||
unixtime?: number;
|
||||
unixTime?: number;
|
||||
unixtimestamp?: number;
|
||||
};
|
||||
// Tentar diferentes formatos de resposta
|
||||
if (data.unixtime) {
|
||||
serverTime = data.unixtime * 1000; // Converter segundos para milissegundos
|
||||
@@ -261,7 +265,7 @@ export const sincronizarTempo = action({
|
||||
if (configExistente) {
|
||||
await ctx.runMutation(internal.configuracaoRelogio.atualizarSincronizacao, {
|
||||
configId: configExistente._id,
|
||||
offsetSegundos,
|
||||
offsetSegundos
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,7 +273,7 @@ export const sincronizarTempo = action({
|
||||
sucesso: true,
|
||||
timestamp: serverTime, // Retorna UTC (sem GMT offset aplicado)
|
||||
usandoServidorExterno: true,
|
||||
offsetSegundos,
|
||||
offsetSegundos
|
||||
};
|
||||
} catch (error) {
|
||||
// Sempre usar fallback como última opção, mesmo se desabilitado
|
||||
@@ -277,18 +281,18 @@ export const sincronizarTempo = action({
|
||||
const aviso: string = config.fallbackParaPC
|
||||
? 'Falha ao sincronizar com servidor externo, usando relógio do PC'
|
||||
: 'Falha ao sincronizar com servidor externo. Fallback desabilitado, mas usando relógio do PC como última opção.';
|
||||
|
||||
|
||||
console.warn('Erro ao sincronizar tempo com servidor externo:', error);
|
||||
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
timestamp: Date.now(),
|
||||
usandoServidorExterno: false,
|
||||
offsetSegundos: 0,
|
||||
aviso,
|
||||
aviso
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -298,7 +302,7 @@ export const listarConfiguracoes = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('configuracaoRelogio').collect();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -307,13 +311,12 @@ export const listarConfiguracoes = internalQuery({
|
||||
export const atualizarSincronizacao = internalMutation({
|
||||
args: {
|
||||
configId: v.id('configuracaoRelogio'),
|
||||
offsetSegundos: v.number(),
|
||||
offsetSegundos: v.number()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
ultimaSincronizacao: Date.now(),
|
||||
offsetSegundos: args.offsetSegundos,
|
||||
offsetSegundos: args.offsetSegundos
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { situacaoContrato } from './tables/contratos';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { internal } from './_generated/api';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { situacaoContrato } from './tables/contratos';
|
||||
|
||||
export const listar = query({
|
||||
args: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineApp } from "convex/server";
|
||||
import betterAuth from "@convex-dev/better-auth/convex.config";
|
||||
import rateLimiter from "@convex-dev/rate-limiter/convex.config";
|
||||
import betterAuth from '@convex-dev/better-auth/convex.config';
|
||||
import rateLimiter from '@convex-dev/rate-limiter/convex.config';
|
||||
import { defineApp } from 'convex/server';
|
||||
|
||||
const app = defineApp();
|
||||
app.use(betterAuth);
|
||||
|
||||
@@ -1,58 +1,53 @@
|
||||
import { cronJobs } from "convex/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { cronJobs } from 'convex/server';
|
||||
import { internal } from './_generated/api';
|
||||
|
||||
const crons = cronJobs();
|
||||
|
||||
// Enviar mensagens agendadas a cada minuto
|
||||
crons.interval(
|
||||
"enviar-mensagens-agendadas",
|
||||
{ minutes: 1 },
|
||||
internal.chat.enviarMensagensAgendadas
|
||||
'enviar-mensagens-agendadas',
|
||||
{ minutes: 1 },
|
||||
internal.chat.enviarMensagensAgendadas
|
||||
);
|
||||
|
||||
// Processar fila de emails (incluindo agendados) a cada minuto
|
||||
crons.interval(
|
||||
"processar-fila-emails",
|
||||
{ minutes: 1 },
|
||||
internal.email.processarFilaEmails
|
||||
);
|
||||
crons.interval('processar-fila-emails', { minutes: 1 }, internal.email.processarFilaEmails);
|
||||
|
||||
// Limpar indicadores de digitação antigos (>10s) a cada minuto
|
||||
crons.interval(
|
||||
"limpar-indicadores-digitacao",
|
||||
{ minutes: 1 },
|
||||
internal.chat.limparIndicadoresDigitacao
|
||||
'limpar-indicadores-digitacao',
|
||||
{ minutes: 1 },
|
||||
internal.chat.limparIndicadoresDigitacao
|
||||
);
|
||||
|
||||
// Atualizar status de férias dos funcionários diariamente
|
||||
crons.interval(
|
||||
"atualizar-status-ferias",
|
||||
{ hours: 24 },
|
||||
internal.ferias.atualizarStatusTodosFuncionarios,
|
||||
{}
|
||||
'atualizar-status-ferias',
|
||||
{ hours: 24 },
|
||||
internal.ferias.atualizarStatusTodosFuncionarios,
|
||||
{}
|
||||
);
|
||||
|
||||
crons.interval(
|
||||
"expirar-bloqueios-ip-automaticos",
|
||||
{ minutes: 5 },
|
||||
internal.security.expirarBloqueiosIpAutomaticos,
|
||||
{}
|
||||
'expirar-bloqueios-ip-automaticos',
|
||||
{ minutes: 5 },
|
||||
internal.security.expirarBloqueiosIpAutomaticos,
|
||||
{}
|
||||
);
|
||||
|
||||
crons.interval(
|
||||
"sincronizar-threat-intel",
|
||||
{ hours: 2 },
|
||||
internal.security.atualizarThreatIntelFeedsInternal,
|
||||
{}
|
||||
'sincronizar-threat-intel',
|
||||
{ hours: 2 },
|
||||
internal.security.atualizarThreatIntelFeedsInternal,
|
||||
{}
|
||||
);
|
||||
|
||||
// Monitorar logs de login e detectar brute force a cada 5 minutos
|
||||
crons.interval(
|
||||
"monitorar-logs-login-brute-force",
|
||||
{ minutes: 5 },
|
||||
internal.security.monitorarLogsLogin,
|
||||
{}
|
||||
'monitorar-logs-login-brute-force',
|
||||
{ minutes: 5 },
|
||||
internal.security.monitorarLogsLogin,
|
||||
{}
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
|
||||
@@ -1,67 +1,64 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
|
||||
export const listarPorFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("cursos"),
|
||||
_creationTime: v.number(),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("cursos")
|
||||
.withIndex("by_funcionario", (q) =>
|
||||
q.eq("funcionarioId", args.funcionarioId)
|
||||
)
|
||||
.collect();
|
||||
},
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('cursos'),
|
||||
_creationTime: v.number(),
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id('_storage'))
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query('cursos')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
},
|
||||
returns: v.id("cursos"),
|
||||
handler: async (ctx, args) => {
|
||||
const cursoId = await ctx.db.insert("cursos", args);
|
||||
return cursoId;
|
||||
},
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id('_storage'))
|
||||
},
|
||||
returns: v.id('cursos'),
|
||||
handler: async (ctx, args) => {
|
||||
const cursoId = await ctx.db.insert('cursos', args);
|
||||
return cursoId;
|
||||
}
|
||||
});
|
||||
|
||||
export const atualizar = mutation({
|
||||
args: {
|
||||
id: v.id("cursos"),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id("_storage")),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...updates } = args;
|
||||
await ctx.db.patch(id, updates);
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
id: v.id('cursos'),
|
||||
descricao: v.string(),
|
||||
data: v.string(),
|
||||
certificadoId: v.optional(v.id('_storage'))
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...updates } = args;
|
||||
await ctx.db.patch(id, updates);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const excluir = mutation({
|
||||
args: {
|
||||
id: v.id("cursos"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
id: v.id('cursos')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.id);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,152 +1,144 @@
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { v } from 'convex/values';
|
||||
import { query } from './_generated/server';
|
||||
|
||||
// Obter estatísticas gerais do sistema
|
||||
export const getStats = query({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
totalFuncionarios: v.number(),
|
||||
totalSimbolos: v.number(),
|
||||
funcionariosAtivos: v.number(),
|
||||
funcionariosDesligados: v.number(),
|
||||
cargoComissionado: v.number(),
|
||||
funcaoGratificada: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
// Contar funcionários
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
const totalFuncionarios = funcionarios.length;
|
||||
|
||||
// Funcionários ativos (sem data de desligamento)
|
||||
const funcionariosAtivos = funcionarios.filter(
|
||||
(f) => !f.desligamentoData
|
||||
).length;
|
||||
|
||||
// Funcionários desligados
|
||||
const funcionariosDesligados = funcionarios.filter(
|
||||
(f) => f.desligamentoData
|
||||
).length;
|
||||
args: {},
|
||||
returns: v.object({
|
||||
totalFuncionarios: v.number(),
|
||||
totalSimbolos: v.number(),
|
||||
funcionariosAtivos: v.number(),
|
||||
funcionariosDesligados: v.number(),
|
||||
cargoComissionado: v.number(),
|
||||
funcaoGratificada: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
// Contar funcionários
|
||||
const funcionarios = await ctx.db.query('funcionarios').collect();
|
||||
const totalFuncionarios = funcionarios.length;
|
||||
|
||||
// Contar por tipo de símbolo
|
||||
const cargoComissionado = funcionarios.filter(
|
||||
(f) => f.simboloTipo === "cargo_comissionado"
|
||||
).length;
|
||||
|
||||
const funcaoGratificada = funcionarios.filter(
|
||||
(f) => f.simboloTipo === "funcao_gratificada"
|
||||
).length;
|
||||
// Funcionários ativos (sem data de desligamento)
|
||||
const funcionariosAtivos = funcionarios.filter((f) => !f.desligamentoData).length;
|
||||
|
||||
// Contar símbolos
|
||||
const simbolos = await ctx.db.query("simbolos").collect();
|
||||
const totalSimbolos = simbolos.length;
|
||||
// Funcionários desligados
|
||||
const funcionariosDesligados = funcionarios.filter((f) => f.desligamentoData).length;
|
||||
|
||||
return {
|
||||
totalFuncionarios,
|
||||
totalSimbolos,
|
||||
funcionariosAtivos,
|
||||
funcionariosDesligados,
|
||||
cargoComissionado,
|
||||
funcaoGratificada,
|
||||
};
|
||||
},
|
||||
// Contar por tipo de símbolo
|
||||
const cargoComissionado = funcionarios.filter(
|
||||
(f) => f.simboloTipo === 'cargo_comissionado'
|
||||
).length;
|
||||
|
||||
const funcaoGratificada = funcionarios.filter(
|
||||
(f) => f.simboloTipo === 'funcao_gratificada'
|
||||
).length;
|
||||
|
||||
// Contar símbolos
|
||||
const simbolos = await ctx.db.query('simbolos').collect();
|
||||
const totalSimbolos = simbolos.length;
|
||||
|
||||
return {
|
||||
totalFuncionarios,
|
||||
totalSimbolos,
|
||||
funcionariosAtivos,
|
||||
funcionariosDesligados,
|
||||
cargoComissionado,
|
||||
funcaoGratificada
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Obter atividades recentes (últimas 24 horas)
|
||||
export const getRecentActivity = query({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
funcionariosCadastrados24h: v.number(),
|
||||
simbolosCadastrados24h: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const now = Date.now();
|
||||
const last24h = now - 24 * 60 * 60 * 1000;
|
||||
args: {},
|
||||
returns: v.object({
|
||||
funcionariosCadastrados24h: v.number(),
|
||||
simbolosCadastrados24h: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const now = Date.now();
|
||||
const last24h = now - 24 * 60 * 60 * 1000;
|
||||
|
||||
// Funcionários cadastrados nas últimas 24h
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
const funcionariosCadastrados24h = funcionarios.filter(
|
||||
(f) => f._creationTime >= last24h
|
||||
).length;
|
||||
// Funcionários cadastrados nas últimas 24h
|
||||
const funcionarios = await ctx.db.query('funcionarios').collect();
|
||||
const funcionariosCadastrados24h = funcionarios.filter(
|
||||
(f) => f._creationTime >= last24h
|
||||
).length;
|
||||
|
||||
// Símbolos cadastrados nas últimas 24h
|
||||
const simbolos = await ctx.db.query('simbolos').collect();
|
||||
const simbolosCadastrados24h = simbolos.filter((s) => s._creationTime >= last24h).length;
|
||||
|
||||
// Símbolos cadastrados nas últimas 24h
|
||||
const simbolos = await ctx.db.query("simbolos").collect();
|
||||
const simbolosCadastrados24h = simbolos.filter(
|
||||
(s) => s._creationTime >= last24h
|
||||
).length;
|
||||
|
||||
return {
|
||||
funcionariosCadastrados24h,
|
||||
simbolosCadastrados24h,
|
||||
};
|
||||
},
|
||||
return {
|
||||
funcionariosCadastrados24h,
|
||||
simbolosCadastrados24h
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Obter distribuição de funcionários por cidade
|
||||
export const getFuncionariosPorCidade = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
cidade: v.string(),
|
||||
quantidade: v.number(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
|
||||
const cidadesMap: Record<string, number> = {};
|
||||
|
||||
for (const func of funcionarios) {
|
||||
if (!func.desligamentoData) {
|
||||
cidadesMap[func.cidade] = (cidadesMap[func.cidade] || 0) + 1;
|
||||
}
|
||||
}
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
cidade: v.string(),
|
||||
quantidade: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query('funcionarios').collect();
|
||||
|
||||
const result = Object.entries(cidadesMap)
|
||||
.map(([cidade, quantidade]) => ({ cidade, quantidade }))
|
||||
.sort((a, b) => b.quantidade - a.quantidade)
|
||||
.slice(0, 5); // Top 5 cidades
|
||||
const cidadesMap: Record<string, number> = {};
|
||||
|
||||
return result;
|
||||
},
|
||||
for (const func of funcionarios) {
|
||||
if (!func.desligamentoData) {
|
||||
cidadesMap[func.cidade] = (cidadesMap[func.cidade] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result = Object.entries(cidadesMap)
|
||||
.map(([cidade, quantidade]) => ({ cidade, quantidade }))
|
||||
.sort((a, b) => b.quantidade - a.quantidade)
|
||||
.slice(0, 5); // Top 5 cidades
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Obter evolução de cadastros por mês
|
||||
export const getEvolucaoCadastros = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
mes: v.string(),
|
||||
funcionarios: v.number(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
mes: v.string(),
|
||||
funcionarios: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const funcionarios = await ctx.db.query('funcionarios').collect();
|
||||
|
||||
const now = new Date();
|
||||
const meses: Array<{ mes: string; funcionarios: number }> = [];
|
||||
const now = new Date();
|
||||
const meses: Array<{ mes: string; funcionarios: number }> = [];
|
||||
|
||||
// Últimos 6 meses
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
|
||||
|
||||
const mesNome = date.toLocaleDateString("pt-BR", {
|
||||
month: "short",
|
||||
year: "2-digit",
|
||||
});
|
||||
// Últimos 6 meses
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
|
||||
|
||||
const funcCount = funcionarios.filter(
|
||||
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
|
||||
).length;
|
||||
const mesNome = date.toLocaleDateString('pt-BR', {
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
});
|
||||
|
||||
meses.push({
|
||||
mes: mesNome,
|
||||
funcionarios: funcCount,
|
||||
});
|
||||
}
|
||||
const funcCount = funcionarios.filter(
|
||||
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
|
||||
).length;
|
||||
|
||||
return meses;
|
||||
},
|
||||
meses.push({
|
||||
mes: mesNome,
|
||||
funcionarios: funcCount
|
||||
});
|
||||
}
|
||||
|
||||
return meses;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,202 +1,201 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { mutation, query } from './_generated/server';
|
||||
|
||||
// Mutation para fazer upload de arquivo e obter o storage ID
|
||||
export const generateUploadUrl = mutation({
|
||||
args: {},
|
||||
returns: v.string(),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
},
|
||||
args: {},
|
||||
returns: v.string(),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation para atualizar um campo de documento do funcionário
|
||||
export const updateDocumento = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
campo: v.string(),
|
||||
storageId: v.union(v.id("_storage"), v.null()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
campo: v.string(),
|
||||
storageId: v.union(v.id('_storage'), v.null())
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error('Funcionário não encontrado');
|
||||
}
|
||||
|
||||
// Atualizar o campo específico do documento
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
[args.campo]: args.storageId,
|
||||
});
|
||||
// Atualizar o campo específico do documento
|
||||
await ctx.db.patch(args.funcionarioId, {
|
||||
[args.campo]: args.storageId
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Query para obter URLs de todos os documentos de um funcionário
|
||||
export const getDocumentosUrls = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
returns: v.object({
|
||||
certidaoAntecedentesPF: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesJFPE: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesSDS: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesTJPE: v.union(v.string(), v.null()),
|
||||
certidaoImprobidade: v.union(v.string(), v.null()),
|
||||
rgFrente: v.union(v.string(), v.null()),
|
||||
rgVerso: v.union(v.string(), v.null()),
|
||||
cpfFrente: v.union(v.string(), v.null()),
|
||||
cpfVerso: v.union(v.string(), v.null()),
|
||||
situacaoCadastralCPF: v.union(v.string(), v.null()),
|
||||
tituloEleitorFrente: v.union(v.string(), v.null()),
|
||||
tituloEleitorVerso: v.union(v.string(), v.null()),
|
||||
comprovanteVotacao: v.union(v.string(), v.null()),
|
||||
carteiraProfissionalFrente: v.union(v.string(), v.null()),
|
||||
carteiraProfissionalVerso: v.union(v.string(), v.null()),
|
||||
comprovantePIS: v.union(v.string(), v.null()),
|
||||
certidaoRegistroCivil: v.union(v.string(), v.null()),
|
||||
certidaoNascimentoDependentes: v.union(v.string(), v.null()),
|
||||
cpfDependentes: v.union(v.string(), v.null()),
|
||||
reservistaDoc: v.union(v.string(), v.null()),
|
||||
comprovanteEscolaridade: v.union(v.string(), v.null()),
|
||||
comprovanteResidencia: v.union(v.string(), v.null()),
|
||||
comprovanteContaBradesco: v.union(v.string(), v.null()),
|
||||
declaracaoAcumulacaoCargo: v.union(v.string(), v.null()),
|
||||
declaracaoDependentesIR: v.union(v.string(), v.null()),
|
||||
declaracaoIdoneidade: v.union(v.string(), v.null()),
|
||||
termoNepotismo: v.union(v.string(), v.null()),
|
||||
termoOpcaoRemuneracao: v.union(v.string(), v.null()),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error("Funcionário não encontrado");
|
||||
}
|
||||
args: { funcionarioId: v.id('funcionarios') },
|
||||
returns: v.object({
|
||||
certidaoAntecedentesPF: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesJFPE: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesSDS: v.union(v.string(), v.null()),
|
||||
certidaoAntecedentesTJPE: v.union(v.string(), v.null()),
|
||||
certidaoImprobidade: v.union(v.string(), v.null()),
|
||||
rgFrente: v.union(v.string(), v.null()),
|
||||
rgVerso: v.union(v.string(), v.null()),
|
||||
cpfFrente: v.union(v.string(), v.null()),
|
||||
cpfVerso: v.union(v.string(), v.null()),
|
||||
situacaoCadastralCPF: v.union(v.string(), v.null()),
|
||||
tituloEleitorFrente: v.union(v.string(), v.null()),
|
||||
tituloEleitorVerso: v.union(v.string(), v.null()),
|
||||
comprovanteVotacao: v.union(v.string(), v.null()),
|
||||
carteiraProfissionalFrente: v.union(v.string(), v.null()),
|
||||
carteiraProfissionalVerso: v.union(v.string(), v.null()),
|
||||
comprovantePIS: v.union(v.string(), v.null()),
|
||||
certidaoRegistroCivil: v.union(v.string(), v.null()),
|
||||
certidaoNascimentoDependentes: v.union(v.string(), v.null()),
|
||||
cpfDependentes: v.union(v.string(), v.null()),
|
||||
reservistaDoc: v.union(v.string(), v.null()),
|
||||
comprovanteEscolaridade: v.union(v.string(), v.null()),
|
||||
comprovanteResidencia: v.union(v.string(), v.null()),
|
||||
comprovanteContaBradesco: v.union(v.string(), v.null()),
|
||||
declaracaoAcumulacaoCargo: v.union(v.string(), v.null()),
|
||||
declaracaoDependentesIR: v.union(v.string(), v.null()),
|
||||
declaracaoIdoneidade: v.union(v.string(), v.null()),
|
||||
termoNepotismo: v.union(v.string(), v.null()),
|
||||
termoOpcaoRemuneracao: v.union(v.string(), v.null())
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error('Funcionário não encontrado');
|
||||
}
|
||||
|
||||
// Tipo exato do retorno para alinhar com o validator
|
||||
type DocumentUrls = {
|
||||
certidaoAntecedentesPF: string | null;
|
||||
certidaoAntecedentesJFPE: string | null;
|
||||
certidaoAntecedentesSDS: string | null;
|
||||
certidaoAntecedentesTJPE: string | null;
|
||||
certidaoImprobidade: string | null;
|
||||
rgFrente: string | null;
|
||||
rgVerso: string | null;
|
||||
cpfFrente: string | null;
|
||||
cpfVerso: string | null;
|
||||
situacaoCadastralCPF: string | null;
|
||||
tituloEleitorFrente: string | null;
|
||||
tituloEleitorVerso: string | null;
|
||||
comprovanteVotacao: string | null;
|
||||
carteiraProfissionalFrente: string | null;
|
||||
carteiraProfissionalVerso: string | null;
|
||||
comprovantePIS: string | null;
|
||||
certidaoRegistroCivil: string | null;
|
||||
certidaoNascimentoDependentes: string | null;
|
||||
cpfDependentes: string | null;
|
||||
reservistaDoc: string | null;
|
||||
comprovanteEscolaridade: string | null;
|
||||
comprovanteResidencia: string | null;
|
||||
comprovanteContaBradesco: string | null;
|
||||
declaracaoAcumulacaoCargo: string | null;
|
||||
declaracaoDependentesIR: string | null;
|
||||
declaracaoIdoneidade: string | null;
|
||||
termoNepotismo: string | null;
|
||||
termoOpcaoRemuneracao: string | null;
|
||||
};
|
||||
// Tipo exato do retorno para alinhar com o validator
|
||||
type DocumentUrls = {
|
||||
certidaoAntecedentesPF: string | null;
|
||||
certidaoAntecedentesJFPE: string | null;
|
||||
certidaoAntecedentesSDS: string | null;
|
||||
certidaoAntecedentesTJPE: string | null;
|
||||
certidaoImprobidade: string | null;
|
||||
rgFrente: string | null;
|
||||
rgVerso: string | null;
|
||||
cpfFrente: string | null;
|
||||
cpfVerso: string | null;
|
||||
situacaoCadastralCPF: string | null;
|
||||
tituloEleitorFrente: string | null;
|
||||
tituloEleitorVerso: string | null;
|
||||
comprovanteVotacao: string | null;
|
||||
carteiraProfissionalFrente: string | null;
|
||||
carteiraProfissionalVerso: string | null;
|
||||
comprovantePIS: string | null;
|
||||
certidaoRegistroCivil: string | null;
|
||||
certidaoNascimentoDependentes: string | null;
|
||||
cpfDependentes: string | null;
|
||||
reservistaDoc: string | null;
|
||||
comprovanteEscolaridade: string | null;
|
||||
comprovanteResidencia: string | null;
|
||||
comprovanteContaBradesco: string | null;
|
||||
declaracaoAcumulacaoCargo: string | null;
|
||||
declaracaoDependentesIR: string | null;
|
||||
declaracaoIdoneidade: string | null;
|
||||
termoNepotismo: string | null;
|
||||
termoOpcaoRemuneracao: string | null;
|
||||
};
|
||||
|
||||
const urls: DocumentUrls = {
|
||||
certidaoAntecedentesPF: null,
|
||||
certidaoAntecedentesJFPE: null,
|
||||
certidaoAntecedentesSDS: null,
|
||||
certidaoAntecedentesTJPE: null,
|
||||
certidaoImprobidade: null,
|
||||
rgFrente: null,
|
||||
rgVerso: null,
|
||||
cpfFrente: null,
|
||||
cpfVerso: null,
|
||||
situacaoCadastralCPF: null,
|
||||
tituloEleitorFrente: null,
|
||||
tituloEleitorVerso: null,
|
||||
comprovanteVotacao: null,
|
||||
carteiraProfissionalFrente: null,
|
||||
carteiraProfissionalVerso: null,
|
||||
comprovantePIS: null,
|
||||
certidaoRegistroCivil: null,
|
||||
certidaoNascimentoDependentes: null,
|
||||
cpfDependentes: null,
|
||||
reservistaDoc: null,
|
||||
comprovanteEscolaridade: null,
|
||||
comprovanteResidencia: null,
|
||||
comprovanteContaBradesco: null,
|
||||
declaracaoAcumulacaoCargo: null,
|
||||
declaracaoDependentesIR: null,
|
||||
declaracaoIdoneidade: null,
|
||||
termoNepotismo: null,
|
||||
termoOpcaoRemuneracao: null,
|
||||
};
|
||||
const urls: DocumentUrls = {
|
||||
certidaoAntecedentesPF: null,
|
||||
certidaoAntecedentesJFPE: null,
|
||||
certidaoAntecedentesSDS: null,
|
||||
certidaoAntecedentesTJPE: null,
|
||||
certidaoImprobidade: null,
|
||||
rgFrente: null,
|
||||
rgVerso: null,
|
||||
cpfFrente: null,
|
||||
cpfVerso: null,
|
||||
situacaoCadastralCPF: null,
|
||||
tituloEleitorFrente: null,
|
||||
tituloEleitorVerso: null,
|
||||
comprovanteVotacao: null,
|
||||
carteiraProfissionalFrente: null,
|
||||
carteiraProfissionalVerso: null,
|
||||
comprovantePIS: null,
|
||||
certidaoRegistroCivil: null,
|
||||
certidaoNascimentoDependentes: null,
|
||||
cpfDependentes: null,
|
||||
reservistaDoc: null,
|
||||
comprovanteEscolaridade: null,
|
||||
comprovanteResidencia: null,
|
||||
comprovanteContaBradesco: null,
|
||||
declaracaoAcumulacaoCargo: null,
|
||||
declaracaoDependentesIR: null,
|
||||
declaracaoIdoneidade: null,
|
||||
termoNepotismo: null,
|
||||
termoOpcaoRemuneracao: null
|
||||
};
|
||||
|
||||
const campos: Array<keyof DocumentUrls> = [
|
||||
"certidaoAntecedentesPF",
|
||||
"certidaoAntecedentesJFPE",
|
||||
"certidaoAntecedentesSDS",
|
||||
"certidaoAntecedentesTJPE",
|
||||
"certidaoImprobidade",
|
||||
"rgFrente",
|
||||
"rgVerso",
|
||||
"cpfFrente",
|
||||
"cpfVerso",
|
||||
"situacaoCadastralCPF",
|
||||
"tituloEleitorFrente",
|
||||
"tituloEleitorVerso",
|
||||
"comprovanteVotacao",
|
||||
"carteiraProfissionalFrente",
|
||||
"carteiraProfissionalVerso",
|
||||
"comprovantePIS",
|
||||
"certidaoRegistroCivil",
|
||||
"certidaoNascimentoDependentes",
|
||||
"cpfDependentes",
|
||||
"reservistaDoc",
|
||||
"comprovanteEscolaridade",
|
||||
"comprovanteResidencia",
|
||||
"comprovanteContaBradesco",
|
||||
"declaracaoAcumulacaoCargo",
|
||||
"declaracaoDependentesIR",
|
||||
"declaracaoIdoneidade",
|
||||
"termoNepotismo",
|
||||
"termoOpcaoRemuneracao",
|
||||
];
|
||||
const campos: Array<keyof DocumentUrls> = [
|
||||
'certidaoAntecedentesPF',
|
||||
'certidaoAntecedentesJFPE',
|
||||
'certidaoAntecedentesSDS',
|
||||
'certidaoAntecedentesTJPE',
|
||||
'certidaoImprobidade',
|
||||
'rgFrente',
|
||||
'rgVerso',
|
||||
'cpfFrente',
|
||||
'cpfVerso',
|
||||
'situacaoCadastralCPF',
|
||||
'tituloEleitorFrente',
|
||||
'tituloEleitorVerso',
|
||||
'comprovanteVotacao',
|
||||
'carteiraProfissionalFrente',
|
||||
'carteiraProfissionalVerso',
|
||||
'comprovantePIS',
|
||||
'certidaoRegistroCivil',
|
||||
'certidaoNascimentoDependentes',
|
||||
'cpfDependentes',
|
||||
'reservistaDoc',
|
||||
'comprovanteEscolaridade',
|
||||
'comprovanteResidencia',
|
||||
'comprovanteContaBradesco',
|
||||
'declaracaoAcumulacaoCargo',
|
||||
'declaracaoDependentesIR',
|
||||
'declaracaoIdoneidade',
|
||||
'termoNepotismo',
|
||||
'termoOpcaoRemuneracao'
|
||||
];
|
||||
|
||||
for (const campo of campos) {
|
||||
const storageId = (funcionario as Record<string, unknown>)[campo as string] as
|
||||
| Id<"_storage">
|
||||
| undefined;
|
||||
if (storageId) {
|
||||
urls[campo] = await ctx.storage.getUrl(storageId);
|
||||
} else {
|
||||
urls[campo] = null;
|
||||
}
|
||||
}
|
||||
for (const campo of campos) {
|
||||
const storageId = (funcionario as Record<string, unknown>)[campo as string] as
|
||||
| Id<'_storage'>
|
||||
| undefined;
|
||||
if (storageId) {
|
||||
urls[campo] = await ctx.storage.getUrl(storageId);
|
||||
} else {
|
||||
urls[campo] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
},
|
||||
return urls;
|
||||
}
|
||||
});
|
||||
|
||||
// Query para obter metadados de um documento
|
||||
export const getDocumentoMetadata = query({
|
||||
args: { storageId: v.id("_storage") },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("_storage"),
|
||||
_creationTime: v.number(),
|
||||
contentType: v.optional(v.string()),
|
||||
sha256: v.string(),
|
||||
size: v.number(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.system.get(args.storageId);
|
||||
},
|
||||
args: { storageId: v.id('_storage') },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('_storage'),
|
||||
_creationTime: v.number(),
|
||||
contentType: v.optional(v.string()),
|
||||
sha256: v.string(),
|
||||
size: v.number()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.system.get(args.storageId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
|
||||
import { internal, api } from "./_generated/api";
|
||||
import {
|
||||
renderizarTemplateEmailFromDoc,
|
||||
type VariaveisTemplate,
|
||||
} from "./templatesMensagens";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import { action, internalMutation, internalQuery, mutation, query } from './_generated/server';
|
||||
import { renderizarTemplateEmailFromDoc, type VariaveisTemplate } from './templatesMensagens';
|
||||
|
||||
// ========== INTERNAL QUERIES ==========
|
||||
|
||||
@@ -13,45 +10,45 @@ import type { Doc, Id } from "./_generated/dataModel";
|
||||
* Obter email por ID (internal query)
|
||||
*/
|
||||
export const getEmailById = internalQuery({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.emailId);
|
||||
},
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.emailId);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter configuração SMTP ativa (internal query)
|
||||
*/
|
||||
export const getActiveEmailConfig = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query("configuracaoEmail")
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.first();
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query('configuracaoEmail')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
return config;
|
||||
},
|
||||
return config;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar emails pendentes (internal query)
|
||||
*/
|
||||
export const listarEmailsPendentes = internalQuery({
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const emails = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.withIndex("by_status", (q) => q.eq("status", "pendente"))
|
||||
.order("asc") // Mais antigos primeiro
|
||||
.take(args.limite || 10);
|
||||
args: {
|
||||
limite: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const emails = await ctx.db
|
||||
.query('notificacoesEmail')
|
||||
.withIndex('by_status', (q) => q.eq('status', 'pendente'))
|
||||
.order('asc') // Mais antigos primeiro
|
||||
.take(args.limite || 10);
|
||||
|
||||
return emails;
|
||||
},
|
||||
return emails;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== INTERNAL MUTATIONS ==========
|
||||
@@ -60,51 +57,51 @@ export const listarEmailsPendentes = internalQuery({
|
||||
* Marcar email como enviando (internal mutation)
|
||||
*/
|
||||
export const markEmailEnviando = internalMutation({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
if (!email) return;
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
if (!email) return;
|
||||
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviando",
|
||||
ultimaTentativa: Date.now(),
|
||||
tentativas: email.tentativas + 1,
|
||||
});
|
||||
},
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: 'enviando',
|
||||
ultimaTentativa: Date.now(),
|
||||
tentativas: email.tentativas + 1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Marcar email como enviado (internal mutation)
|
||||
*/
|
||||
export const markEmailEnviado = internalMutation({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "enviado",
|
||||
enviadoEm: Date.now(),
|
||||
});
|
||||
},
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: 'enviado',
|
||||
enviadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Marcar email como falha (internal mutation)
|
||||
*/
|
||||
export const markEmailFalha = internalMutation({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
erro: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: "falha",
|
||||
erroDetalhes: args.erro,
|
||||
ultimaTentativa: Date.now(),
|
||||
});
|
||||
},
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail'),
|
||||
erro: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.emailId, {
|
||||
status: 'falha',
|
||||
erroDetalhes: args.erro,
|
||||
ultimaTentativa: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== PUBLIC MUTATIONS ==========
|
||||
@@ -113,136 +110,137 @@ export const markEmailFalha = internalMutation({
|
||||
* Enfileirar email para envio assíncrono
|
||||
*/
|
||||
export const enfileirarEmail = mutation({
|
||||
args: {
|
||||
destinatario: v.string(),
|
||||
destinatarioId: v.optional(v.id("usuarios")),
|
||||
assunto: v.string(),
|
||||
corpo: v.string(),
|
||||
templateId: v.optional(v.id("templatesMensagens")),
|
||||
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Validar agendamento se fornecido
|
||||
if (args.agendadaPara !== undefined && args.agendadaPara <= Date.now()) {
|
||||
throw new Error("Data de agendamento deve ser futura");
|
||||
}
|
||||
args: {
|
||||
destinatario: v.string(),
|
||||
destinatarioId: v.optional(v.id('usuarios')),
|
||||
assunto: v.string(),
|
||||
corpo: v.string(),
|
||||
templateId: v.optional(v.id('templatesMensagens')),
|
||||
enviadoPor: v.id('usuarios'), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()) // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Validar agendamento se fornecido
|
||||
if (args.agendadaPara !== undefined && args.agendadaPara <= Date.now()) {
|
||||
throw new Error('Data de agendamento deve ser futura');
|
||||
}
|
||||
|
||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: args.assunto,
|
||||
corpo: args.corpo,
|
||||
templateId: args.templateId,
|
||||
status: "pendente",
|
||||
tentativas: 0,
|
||||
criadoEm: Date.now(),
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
const emailId = await ctx.db.insert('notificacoesEmail', {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: args.assunto,
|
||||
corpo: args.corpo,
|
||||
templateId: args.templateId,
|
||||
status: 'pendente',
|
||||
tentativas: 0,
|
||||
criadoEm: Date.now(),
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara
|
||||
});
|
||||
|
||||
// Processar imediatamente se não houver agendamento ou se o agendamento já passou
|
||||
const agora = Date.now();
|
||||
const deveProcessarAgora =
|
||||
args.agendadaPara === undefined ||
|
||||
args.agendadaPara <= agora;
|
||||
// Processar imediatamente se não houver agendamento ou se o agendamento já passou
|
||||
const agora = Date.now();
|
||||
const deveProcessarAgora = args.agendadaPara === undefined || args.agendadaPara <= agora;
|
||||
|
||||
if (deveProcessarAgora) {
|
||||
// Agendar envio imediato via action (não bloqueia a mutation)
|
||||
ctx.scheduler
|
||||
.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: emailId,
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio imediato de email ${emailId}:`, errorMessage);
|
||||
// Não falha a mutation se houver erro ao agendar - o cron pode processar depois
|
||||
});
|
||||
}
|
||||
// Emails agendados para o futuro serão processados pelo cron quando a hora chegar
|
||||
if (deveProcessarAgora) {
|
||||
// Agendar envio imediato via action (não bloqueia a mutation)
|
||||
ctx.scheduler
|
||||
.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: emailId
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio imediato de email ${emailId}:`, errorMessage);
|
||||
// Não falha a mutation se houver erro ao agendar - o cron pode processar depois
|
||||
});
|
||||
}
|
||||
// Emails agendados para o futuro serão processados pelo cron quando a hora chegar
|
||||
|
||||
return emailId;
|
||||
},
|
||||
return emailId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Cancelar agendamento de email
|
||||
*/
|
||||
export const cancelarAgendamentoEmail = mutation({
|
||||
args: {
|
||||
emailId: v.id("notificacoesEmail"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: "Email não encontrado" };
|
||||
}
|
||||
args: {
|
||||
emailId: v.id('notificacoesEmail')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const email = await ctx.db.get(args.emailId);
|
||||
if (!email) {
|
||||
return { sucesso: false, erro: 'Email não encontrado' };
|
||||
}
|
||||
|
||||
if (email.status !== "pendente") {
|
||||
return { sucesso: false, erro: "Apenas emails pendentes podem ser cancelados" };
|
||||
}
|
||||
if (email.status !== 'pendente') {
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: 'Apenas emails pendentes podem ser cancelados'
|
||||
};
|
||||
}
|
||||
|
||||
// Remove o email da fila
|
||||
await ctx.db.delete(args.emailId);
|
||||
return { sucesso: true };
|
||||
},
|
||||
// Remove o email da fila
|
||||
await ctx.db.delete(args.emailId);
|
||||
return { sucesso: true };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Enviar email usando template
|
||||
*/
|
||||
export const enviarEmailComTemplate = action({
|
||||
args: {
|
||||
destinatario: v.string(),
|
||||
destinatarioId: v.optional(v.id("usuarios")),
|
||||
templateCodigo: v.string(),
|
||||
variaveis: v.optional(v.record(v.string(), v.string())),
|
||||
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args): Promise<Id<"notificacoesEmail">> => {
|
||||
// Buscar template
|
||||
const template: Doc<"templatesMensagens"> | null = await ctx.runQuery(
|
||||
api.templatesMensagens.obterTemplatePorCodigo,
|
||||
{
|
||||
codigo: args.templateCodigo,
|
||||
}
|
||||
);
|
||||
args: {
|
||||
destinatario: v.string(),
|
||||
destinatarioId: v.optional(v.id('usuarios')),
|
||||
templateCodigo: v.string(),
|
||||
variaveis: v.optional(v.record(v.string(), v.string())),
|
||||
enviadoPor: v.id('usuarios'), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()) // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args): Promise<Id<'notificacoesEmail'>> => {
|
||||
// Buscar template
|
||||
const template: Doc<'templatesMensagens'> | null = await ctx.runQuery(
|
||||
api.templatesMensagens.obterTemplatePorCodigo,
|
||||
{
|
||||
codigo: args.templateCodigo
|
||||
}
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Template não encontrado: ${args.templateCodigo}`);
|
||||
}
|
||||
if (!template) {
|
||||
throw new Error(`Template não encontrado: ${args.templateCodigo}`);
|
||||
}
|
||||
|
||||
// Renderizar template com variáveis
|
||||
const variaveisTemplate: VariaveisTemplate = args.variaveis ?? {};
|
||||
// Renderizar template com variáveis
|
||||
const variaveisTemplate: VariaveisTemplate = args.variaveis ?? {};
|
||||
|
||||
// Garantir que urlSistema sempre tenha protocolo se presente
|
||||
if (
|
||||
typeof variaveisTemplate.urlSistema === "string" &&
|
||||
!variaveisTemplate.urlSistema.match(/^https?:\/\//i)
|
||||
) {
|
||||
variaveisTemplate.urlSistema = `http://${variaveisTemplate.urlSistema}`;
|
||||
}
|
||||
// Garantir que urlSistema sempre tenha protocolo se presente
|
||||
if (
|
||||
typeof variaveisTemplate.urlSistema === 'string' &&
|
||||
!variaveisTemplate.urlSistema.match(/^https?:\/\//i)
|
||||
) {
|
||||
variaveisTemplate.urlSistema = `http://${variaveisTemplate.urlSistema}`;
|
||||
}
|
||||
|
||||
const emailRenderizado = renderizarTemplateEmailFromDoc(template, variaveisTemplate);
|
||||
const emailRenderizado = renderizarTemplateEmailFromDoc(template, variaveisTemplate);
|
||||
|
||||
// Enfileirar email via mutation
|
||||
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: emailRenderizado.titulo,
|
||||
corpo: emailRenderizado.html, // HTML completo com wrapper
|
||||
templateId: template._id, // template._id sempre existe se template não é null
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
// Enfileirar email via mutation
|
||||
const emailId: Id<'notificacoesEmail'> = await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: emailRenderizado.titulo,
|
||||
corpo: emailRenderizado.html, // HTML completo com wrapper
|
||||
templateId: template._id, // template._id sempre existe se template não é null
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara
|
||||
});
|
||||
|
||||
if (!emailId) {
|
||||
throw new Error("Erro ao enfileirar email: ID não retornado");
|
||||
}
|
||||
if (!emailId) {
|
||||
throw new Error('Erro ao enfileirar email: ID não retornado');
|
||||
}
|
||||
|
||||
return emailId;
|
||||
},
|
||||
return emailId;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== INTERNAL MUTATION (CRON) ==========
|
||||
@@ -251,47 +249,46 @@ export const enviarEmailComTemplate = action({
|
||||
* Processar fila de emails pendentes (chamado pelo cron)
|
||||
*/
|
||||
export const processarFilaEmails = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const agora = Date.now();
|
||||
|
||||
// Buscar emails pendentes que devem ser processados agora
|
||||
// (sem agendamento OU com agendamento que já passou)
|
||||
const emailsParaProcessar = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.filter((q) => {
|
||||
const statusPendente = q.eq(q.field("status"), "pendente");
|
||||
const semAgendamento = q.eq(q.field("agendadaPara"), undefined);
|
||||
const agendamentoJaPassou = q.and(
|
||||
q.neq(q.field("agendadaPara"), undefined),
|
||||
q.lte(q.field("agendadaPara"), agora)
|
||||
);
|
||||
|
||||
return q.and(
|
||||
statusPendente,
|
||||
q.or(semAgendamento, agendamentoJaPassou)
|
||||
);
|
||||
})
|
||||
.order("asc") // Mais antigos primeiro
|
||||
.take(10);
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const agora = Date.now();
|
||||
|
||||
if (emailsParaProcessar.length === 0) {
|
||||
return { processados: 0 };
|
||||
}
|
||||
// Buscar emails pendentes que devem ser processados agora
|
||||
// (sem agendamento OU com agendamento que já passou)
|
||||
const emailsParaProcessar = await ctx.db
|
||||
.query('notificacoesEmail')
|
||||
.filter((q) => {
|
||||
const statusPendente = q.eq(q.field('status'), 'pendente');
|
||||
const semAgendamento = q.eq(q.field('agendadaPara'), undefined);
|
||||
const agendamentoJaPassou = q.and(
|
||||
q.neq(q.field('agendadaPara'), undefined),
|
||||
q.lte(q.field('agendadaPara'), agora)
|
||||
);
|
||||
|
||||
// Agendar envio de cada email via action
|
||||
for (const email of emailsParaProcessar) {
|
||||
// Agendar envio assíncrono (não bloqueia o cron)
|
||||
ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: email._id,
|
||||
}).catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
return q.and(statusPendente, q.or(semAgendamento, agendamentoJaPassou));
|
||||
})
|
||||
.order('asc') // Mais antigos primeiro
|
||||
.take(10);
|
||||
|
||||
return { processados: emailsParaProcessar.length };
|
||||
}
|
||||
if (emailsParaProcessar.length === 0) {
|
||||
return { processados: 0 };
|
||||
}
|
||||
|
||||
// Agendar envio de cada email via action
|
||||
for (const email of emailsParaProcessar) {
|
||||
// Agendar envio assíncrono (não bloqueia o cron)
|
||||
ctx.scheduler
|
||||
.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: email._id
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
return { processados: emailsParaProcessar.length };
|
||||
}
|
||||
});
|
||||
|
||||
// ========== QUERIES ==========
|
||||
@@ -300,138 +297,138 @@ export const processarFilaEmails = internalMutation({
|
||||
* Listar emails da fila (para monitoramento)
|
||||
*/
|
||||
export const listarFilaEmails = query({
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
status: v.optional(v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("enviando"),
|
||||
v.literal("enviado"),
|
||||
v.literal("falha")
|
||||
)),
|
||||
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let emails;
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
status: v.optional(
|
||||
v.union(
|
||||
v.literal('pendente'),
|
||||
v.literal('enviando'),
|
||||
v.literal('enviado'),
|
||||
v.literal('falha')
|
||||
)
|
||||
),
|
||||
_refresh: v.optional(v.number()) // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let emails;
|
||||
|
||||
// Filtrar por status se fornecido
|
||||
if (args.status) {
|
||||
emails = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.withIndex("by_status", (q) => q.eq("status", args.status!))
|
||||
.order("desc")
|
||||
.take(args.limite || 50);
|
||||
} else {
|
||||
// Sem filtro, buscar todos e ordenar por data de criação
|
||||
const todosEmails = await ctx.db.query("notificacoesEmail").collect();
|
||||
todosEmails.sort((a, b) => b.criadoEm - a.criadoEm);
|
||||
emails = todosEmails.slice(0, args.limite || 50);
|
||||
}
|
||||
// Filtrar por status se fornecido
|
||||
if (args.status) {
|
||||
emails = await ctx.db
|
||||
.query('notificacoesEmail')
|
||||
.withIndex('by_status', (q) => q.eq('status', args.status!))
|
||||
.order('desc')
|
||||
.take(args.limite || 50);
|
||||
} else {
|
||||
// Sem filtro, buscar todos e ordenar por data de criação
|
||||
const todosEmails = await ctx.db.query('notificacoesEmail').collect();
|
||||
todosEmails.sort((a, b) => b.criadoEm - a.criadoEm);
|
||||
emails = todosEmails.slice(0, args.limite || 50);
|
||||
}
|
||||
|
||||
return emails;
|
||||
},
|
||||
return emails;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter estatísticas da fila de emails (para debug e monitoramento)
|
||||
*/
|
||||
export const obterEstatisticasFilaEmails = query({
|
||||
args: {
|
||||
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
returns: v.object({
|
||||
pendentes: v.number(),
|
||||
enviando: v.number(),
|
||||
enviados: v.number(),
|
||||
falhas: v.number(),
|
||||
total: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const todosEmails = await ctx.db.query("notificacoesEmail").collect();
|
||||
|
||||
const estatisticas = {
|
||||
pendentes: 0,
|
||||
enviando: 0,
|
||||
enviados: 0,
|
||||
falhas: 0,
|
||||
total: todosEmails.length,
|
||||
};
|
||||
args: {
|
||||
_refresh: v.optional(v.number()) // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
returns: v.object({
|
||||
pendentes: v.number(),
|
||||
enviando: v.number(),
|
||||
enviados: v.number(),
|
||||
falhas: v.number(),
|
||||
total: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const todosEmails = await ctx.db.query('notificacoesEmail').collect();
|
||||
|
||||
for (const email of todosEmails) {
|
||||
switch (email.status) {
|
||||
case "pendente":
|
||||
estatisticas.pendentes++;
|
||||
break;
|
||||
case "enviando":
|
||||
estatisticas.enviando++;
|
||||
break;
|
||||
case "enviado":
|
||||
estatisticas.enviados++;
|
||||
break;
|
||||
case "falha":
|
||||
estatisticas.falhas++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const estatisticas = {
|
||||
pendentes: 0,
|
||||
enviando: 0,
|
||||
enviados: 0,
|
||||
falhas: 0,
|
||||
total: todosEmails.length
|
||||
};
|
||||
|
||||
return estatisticas;
|
||||
},
|
||||
for (const email of todosEmails) {
|
||||
switch (email.status) {
|
||||
case 'pendente':
|
||||
estatisticas.pendentes++;
|
||||
break;
|
||||
case 'enviando':
|
||||
estatisticas.enviando++;
|
||||
break;
|
||||
case 'enviado':
|
||||
estatisticas.enviados++;
|
||||
break;
|
||||
case 'falha':
|
||||
estatisticas.falhas++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return estatisticas;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Buscar emails por IDs (para monitoramento de status)
|
||||
*/
|
||||
export const buscarEmailsPorIds = query({
|
||||
args: {
|
||||
emailIds: v.array(v.id("notificacoesEmail")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const emails = [];
|
||||
for (const emailId of args.emailIds) {
|
||||
const email = await ctx.db.get(emailId);
|
||||
if (email) {
|
||||
emails.push(email);
|
||||
}
|
||||
}
|
||||
return emails;
|
||||
},
|
||||
args: {
|
||||
emailIds: v.array(v.id('notificacoesEmail'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const emails = [];
|
||||
for (const emailId of args.emailIds) {
|
||||
const email = await ctx.db.get(emailId);
|
||||
if (email) {
|
||||
emails.push(email);
|
||||
}
|
||||
}
|
||||
return emails;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listar agendamentos de email (emails com agendadaPara definido)
|
||||
*/
|
||||
export const listarAgendamentosEmail = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar todos os emails agendados (pendentes, enviando ou já enviados que tinham agendamento)
|
||||
const emailsAgendados = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.filter((q) => {
|
||||
// Apenas emails que têm agendadaPara definido
|
||||
return q.neq(q.field("agendadaPara"), undefined);
|
||||
})
|
||||
.collect();
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar todos os emails agendados (pendentes, enviando ou já enviados que tinham agendamento)
|
||||
const emailsAgendados = await ctx.db
|
||||
.query('notificacoesEmail')
|
||||
.filter((q) => {
|
||||
// Apenas emails que têm agendadaPara definido
|
||||
return q.neq(q.field('agendadaPara'), undefined);
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Enriquecer com informações de destinatário e template
|
||||
const emailsEnriquecidos = await Promise.all(
|
||||
emailsAgendados.map(async (email) => {
|
||||
const destinatarioInfo = email.destinatarioId
|
||||
? await ctx.db.get(email.destinatarioId)
|
||||
: null;
|
||||
|
||||
const templateInfo = email.templateId
|
||||
? await ctx.db.get(email.templateId)
|
||||
: null;
|
||||
// Enriquecer com informações de destinatário e template
|
||||
const emailsEnriquecidos = await Promise.all(
|
||||
emailsAgendados.map(async (email) => {
|
||||
const destinatarioInfo = email.destinatarioId
|
||||
? await ctx.db.get(email.destinatarioId)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...email,
|
||||
destinatarioInfo,
|
||||
templateInfo,
|
||||
};
|
||||
})
|
||||
);
|
||||
const templateInfo = email.templateId ? await ctx.db.get(email.templateId) : null;
|
||||
|
||||
return emailsEnriquecidos;
|
||||
},
|
||||
return {
|
||||
...email,
|
||||
destinatarioInfo,
|
||||
templateInfo
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return emailsEnriquecidos;
|
||||
}
|
||||
});
|
||||
|
||||
// ========== PUBLIC MUTATIONS (MANUAL) ==========
|
||||
@@ -440,53 +437,53 @@ export const listarAgendamentosEmail = query({
|
||||
* Processar fila de emails manualmente (para uso em interface)
|
||||
*/
|
||||
export const processarFilaEmailsManual = action({
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
processados: v.number(),
|
||||
falhas: v.number(),
|
||||
erro: v.optional(v.string()),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Buscar emails pendentes
|
||||
const emailsPendentes = await ctx.runQuery(internal.email.listarEmailsPendentes, {
|
||||
limite: args.limite || 10,
|
||||
});
|
||||
args: {
|
||||
limite: v.optional(v.number())
|
||||
},
|
||||
returns: v.object({
|
||||
sucesso: v.boolean(),
|
||||
processados: v.number(),
|
||||
falhas: v.number(),
|
||||
erro: v.optional(v.string())
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
// Buscar emails pendentes
|
||||
const emailsPendentes = await ctx.runQuery(internal.email.listarEmailsPendentes, {
|
||||
limite: args.limite || 10
|
||||
});
|
||||
|
||||
if (emailsPendentes.length === 0) {
|
||||
return { sucesso: true, processados: 0, falhas: 0 };
|
||||
}
|
||||
if (emailsPendentes.length === 0) {
|
||||
return { sucesso: true, processados: 0, falhas: 0 };
|
||||
}
|
||||
|
||||
let processados = 0;
|
||||
let falhas = 0;
|
||||
let processados = 0;
|
||||
let falhas = 0;
|
||||
|
||||
// Processar cada email
|
||||
for (const email of emailsPendentes) {
|
||||
try {
|
||||
// Agendar envio via action
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: email._id,
|
||||
});
|
||||
processados++;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
falhas++;
|
||||
}
|
||||
}
|
||||
// Processar cada email
|
||||
for (const email of emailsPendentes) {
|
||||
try {
|
||||
// Agendar envio via action
|
||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: email._id
|
||||
});
|
||||
processados++;
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
falhas++;
|
||||
}
|
||||
}
|
||||
|
||||
return { sucesso: true, processados, falhas };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false,
|
||||
processados: 0,
|
||||
falhas: 0,
|
||||
erro: errorMessage,
|
||||
};
|
||||
}
|
||||
},
|
||||
return { sucesso: true, processados, falhas };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false,
|
||||
processados: 0,
|
||||
falhas: 0,
|
||||
erro: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,292 +1,290 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import { getCurrentUserFunction } from "./auth";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { v } from 'convex/values';
|
||||
import { internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "empresas",
|
||||
acao: "listar",
|
||||
});
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'empresas',
|
||||
acao: 'listar'
|
||||
});
|
||||
|
||||
const empresas = await ctx.db.query("empresas").collect();
|
||||
return empresas;
|
||||
},
|
||||
const empresas = await ctx.db.query('empresas').collect();
|
||||
return empresas;
|
||||
}
|
||||
});
|
||||
|
||||
export const getById = query({
|
||||
args: { id: v.id("empresas") },
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "empresas",
|
||||
acao: "ver",
|
||||
});
|
||||
args: { id: v.id('empresas') },
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'empresas',
|
||||
acao: 'ver'
|
||||
});
|
||||
|
||||
const empresa = await ctx.db.get(args.id);
|
||||
if (!empresa) {
|
||||
return null;
|
||||
}
|
||||
const empresa = await ctx.db.get(args.id);
|
||||
if (!empresa) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contatos = await ctx.db
|
||||
.query("contatosEmpresa")
|
||||
.withIndex("by_empresa", (q) => q.eq("empresaId", args.id))
|
||||
.collect();
|
||||
const endereco = empresa.enderecoId
|
||||
? await ctx.db.get(empresa.enderecoId as Id<"enderecos">)
|
||||
: null;
|
||||
const contatos = await ctx.db
|
||||
.query('contatosEmpresa')
|
||||
.withIndex('by_empresa', (q) => q.eq('empresaId', args.id))
|
||||
.collect();
|
||||
const endereco = empresa.enderecoId
|
||||
? await ctx.db.get(empresa.enderecoId as Id<'enderecos'>)
|
||||
: null;
|
||||
|
||||
return { ...empresa, endereco, contatos };
|
||||
},
|
||||
return { ...empresa, endereco, contatos };
|
||||
}
|
||||
});
|
||||
|
||||
const contatoInput = v.object({
|
||||
_id: v.optional(v.id("contatosEmpresa")),
|
||||
empresaId: v.optional(v.id("empresas")),
|
||||
nome: v.string(),
|
||||
funcao: v.string(),
|
||||
email: v.string(),
|
||||
telefone: v.string(),
|
||||
adicionadoPor: v.optional(v.id("usuarios")),
|
||||
descricao: v.optional(v.string()),
|
||||
_deleted: v.optional(v.boolean()),
|
||||
_id: v.optional(v.id('contatosEmpresa')),
|
||||
empresaId: v.optional(v.id('empresas')),
|
||||
nome: v.string(),
|
||||
funcao: v.string(),
|
||||
email: v.string(),
|
||||
telefone: v.string(),
|
||||
adicionadoPor: v.optional(v.id('usuarios')),
|
||||
descricao: v.optional(v.string()),
|
||||
_deleted: v.optional(v.boolean())
|
||||
});
|
||||
|
||||
const enderecoInput = v.object({
|
||||
cep: v.string(),
|
||||
logradouro: v.string(),
|
||||
numero: v.string(),
|
||||
complemento: v.optional(v.string()),
|
||||
bairro: v.string(),
|
||||
cidade: v.string(),
|
||||
uf: v.string(),
|
||||
cep: v.string(),
|
||||
logradouro: v.string(),
|
||||
numero: v.string(),
|
||||
complemento: v.optional(v.string()),
|
||||
bairro: v.string(),
|
||||
cidade: v.string(),
|
||||
uf: v.string()
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
razao_social: v.string(),
|
||||
nome_fantasia: v.optional(v.string()),
|
||||
cnpj: v.string(),
|
||||
telefone: v.string(),
|
||||
email: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
endereco: v.optional(enderecoInput),
|
||||
contatos: v.optional(v.array(contatoInput)),
|
||||
},
|
||||
returns: v.id("empresas"),
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
args: {
|
||||
razao_social: v.string(),
|
||||
nome_fantasia: v.optional(v.string()),
|
||||
cnpj: v.string(),
|
||||
telefone: v.string(),
|
||||
email: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
endereco: v.optional(enderecoInput),
|
||||
contatos: v.optional(v.array(contatoInput))
|
||||
},
|
||||
returns: v.id('empresas'),
|
||||
handler: async (ctx, args) => {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
|
||||
if (!usuarioAtual) {
|
||||
throw new Error("Usuário não autenticado.");
|
||||
}
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Usuário não autenticado.');
|
||||
}
|
||||
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "empresas",
|
||||
acao: "criar",
|
||||
});
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'empresas',
|
||||
acao: 'criar'
|
||||
});
|
||||
|
||||
const cnpjExistente = await ctx.db
|
||||
.query("empresas")
|
||||
.withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj))
|
||||
.unique();
|
||||
const cnpjExistente = await ctx.db
|
||||
.query('empresas')
|
||||
.withIndex('by_cnpj', (q) => q.eq('cnpj', args.cnpj))
|
||||
.unique();
|
||||
|
||||
if (cnpjExistente) {
|
||||
throw new Error("Já existe uma empresa cadastrada com este CNPJ.");
|
||||
}
|
||||
let enderecoId: Id<"enderecos"> | undefined;
|
||||
if (args.endereco) {
|
||||
enderecoId = await ctx.db.insert("enderecos", {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
criadoPor: usuarioAtual._id,
|
||||
atualizadoPor: usuarioAtual._id,
|
||||
});
|
||||
}
|
||||
if (cnpjExistente) {
|
||||
throw new Error('Já existe uma empresa cadastrada com este CNPJ.');
|
||||
}
|
||||
let enderecoId: Id<'enderecos'> | undefined;
|
||||
if (args.endereco) {
|
||||
enderecoId = await ctx.db.insert('enderecos', {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
criadoPor: usuarioAtual._id,
|
||||
atualizadoPor: usuarioAtual._id
|
||||
});
|
||||
}
|
||||
|
||||
const empresaDoc: {
|
||||
razao_social: string;
|
||||
nome_fantasia?: string;
|
||||
cnpj: string;
|
||||
telefone: string;
|
||||
email: string;
|
||||
descricao?: string;
|
||||
enderecoId?: Id<"enderecos">;
|
||||
criadoPor: Id<"usuarios">;
|
||||
} = {
|
||||
razao_social: args.razao_social,
|
||||
cnpj: args.cnpj,
|
||||
telefone: args.telefone,
|
||||
email: args.email,
|
||||
criadoPor: usuarioAtual._id,
|
||||
};
|
||||
const empresaDoc: {
|
||||
razao_social: string;
|
||||
nome_fantasia?: string;
|
||||
cnpj: string;
|
||||
telefone: string;
|
||||
email: string;
|
||||
descricao?: string;
|
||||
enderecoId?: Id<'enderecos'>;
|
||||
criadoPor: Id<'usuarios'>;
|
||||
} = {
|
||||
razao_social: args.razao_social,
|
||||
cnpj: args.cnpj,
|
||||
telefone: args.telefone,
|
||||
email: args.email,
|
||||
criadoPor: usuarioAtual._id
|
||||
};
|
||||
|
||||
if (args.nome_fantasia !== undefined) {
|
||||
empresaDoc.nome_fantasia = args.nome_fantasia;
|
||||
}
|
||||
if (args.descricao !== undefined) {
|
||||
empresaDoc.descricao = args.descricao;
|
||||
}
|
||||
if (enderecoId) {
|
||||
empresaDoc.enderecoId = enderecoId;
|
||||
}
|
||||
if (args.nome_fantasia !== undefined) {
|
||||
empresaDoc.nome_fantasia = args.nome_fantasia;
|
||||
}
|
||||
if (args.descricao !== undefined) {
|
||||
empresaDoc.descricao = args.descricao;
|
||||
}
|
||||
if (enderecoId) {
|
||||
empresaDoc.enderecoId = enderecoId;
|
||||
}
|
||||
|
||||
const empresaId = await ctx.db.insert("empresas", empresaDoc);
|
||||
const empresaId = await ctx.db.insert('empresas', empresaDoc);
|
||||
|
||||
if (args.contatos && args.contatos.length > 0) {
|
||||
for (const contato of args.contatos) {
|
||||
await ctx.db.insert("contatosEmpresa", {
|
||||
empresaId,
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
adicionadoPor: usuarioAtual._id,
|
||||
descricao: contato.descricao,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (args.contatos && args.contatos.length > 0) {
|
||||
for (const contato of args.contatos) {
|
||||
await ctx.db.insert('contatosEmpresa', {
|
||||
empresaId,
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
adicionadoPor: usuarioAtual._id,
|
||||
descricao: contato.descricao
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return empresaId;
|
||||
},
|
||||
return empresaId;
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id("empresas"),
|
||||
razao_social: v.string(),
|
||||
nome_fantasia: v.optional(v.string()),
|
||||
cnpj: v.string(),
|
||||
telefone: v.string(),
|
||||
email: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
endereco: v.optional(enderecoInput),
|
||||
contatos: v.optional(v.array(contatoInput)),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: "empresas",
|
||||
acao: "editar",
|
||||
});
|
||||
args: {
|
||||
id: v.id('empresas'),
|
||||
razao_social: v.string(),
|
||||
nome_fantasia: v.optional(v.string()),
|
||||
cnpj: v.string(),
|
||||
telefone: v.string(),
|
||||
email: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
endereco: v.optional(enderecoInput),
|
||||
contatos: v.optional(v.array(contatoInput))
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'empresas',
|
||||
acao: 'editar'
|
||||
});
|
||||
|
||||
const cnpjExistente = await ctx.db
|
||||
.query("empresas")
|
||||
.withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj))
|
||||
.unique();
|
||||
const cnpjExistente = await ctx.db
|
||||
.query('empresas')
|
||||
.withIndex('by_cnpj', (q) => q.eq('cnpj', args.cnpj))
|
||||
.unique();
|
||||
|
||||
if (cnpjExistente && cnpjExistente._id !== args.id) {
|
||||
throw new Error("Já existe uma empresa cadastrada com este CNPJ.");
|
||||
}
|
||||
const empresa = await ctx.db.get(args.id);
|
||||
if (!empresa) {
|
||||
throw new Error("Empresa não encontrada.");
|
||||
}
|
||||
if (cnpjExistente && cnpjExistente._id !== args.id) {
|
||||
throw new Error('Já existe uma empresa cadastrada com este CNPJ.');
|
||||
}
|
||||
const empresa = await ctx.db.get(args.id);
|
||||
if (!empresa) {
|
||||
throw new Error('Empresa não encontrada.');
|
||||
}
|
||||
|
||||
if (args.endereco) {
|
||||
if (empresa.enderecoId) {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
await ctx.db.patch(empresa.enderecoId as Id<"enderecos">, {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
atualizadoPor: usuarioAtual?._id,
|
||||
});
|
||||
} else {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
if (args.endereco) {
|
||||
if (empresa.enderecoId) {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
await ctx.db.patch(empresa.enderecoId as Id<'enderecos'>, {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
atualizadoPor: usuarioAtual?._id
|
||||
});
|
||||
} else {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
|
||||
if (!usuarioAtual) {
|
||||
throw new Error("Usuário não autenticado.");
|
||||
}
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Usuário não autenticado.');
|
||||
}
|
||||
|
||||
const novoEnderecoId: Id<"enderecos"> = await ctx.db.insert("enderecos", {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
criadoPor: usuarioAtual._id,
|
||||
atualizadoPor: usuarioAtual._id,
|
||||
});
|
||||
const novoEnderecoId: Id<'enderecos'> = await ctx.db.insert('enderecos', {
|
||||
cep: args.endereco.cep,
|
||||
logradouro: args.endereco.logradouro,
|
||||
numero: args.endereco.numero,
|
||||
complemento: args.endereco.complemento,
|
||||
bairro: args.endereco.bairro,
|
||||
cidade: args.endereco.cidade,
|
||||
uf: args.endereco.uf,
|
||||
criadoPor: usuarioAtual._id,
|
||||
atualizadoPor: usuarioAtual._id
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.id, {
|
||||
enderecoId: novoEnderecoId,
|
||||
});
|
||||
}
|
||||
}
|
||||
await ctx.db.patch(args.id, {
|
||||
enderecoId: novoEnderecoId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const patchDoc: {
|
||||
razao_social: string;
|
||||
nome_fantasia?: string;
|
||||
cnpj: string;
|
||||
telefone: string;
|
||||
email: string;
|
||||
descricao?: string;
|
||||
} = {
|
||||
razao_social: args.razao_social,
|
||||
cnpj: args.cnpj,
|
||||
telefone: args.telefone,
|
||||
email: args.email,
|
||||
};
|
||||
const patchDoc: {
|
||||
razao_social: string;
|
||||
nome_fantasia?: string;
|
||||
cnpj: string;
|
||||
telefone: string;
|
||||
email: string;
|
||||
descricao?: string;
|
||||
} = {
|
||||
razao_social: args.razao_social,
|
||||
cnpj: args.cnpj,
|
||||
telefone: args.telefone,
|
||||
email: args.email
|
||||
};
|
||||
|
||||
if (args.nome_fantasia !== undefined) {
|
||||
patchDoc.nome_fantasia = args.nome_fantasia;
|
||||
}
|
||||
if (args.descricao !== undefined) {
|
||||
patchDoc.descricao = args.descricao;
|
||||
}
|
||||
if (args.nome_fantasia !== undefined) {
|
||||
patchDoc.nome_fantasia = args.nome_fantasia;
|
||||
}
|
||||
if (args.descricao !== undefined) {
|
||||
patchDoc.descricao = args.descricao;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, patchDoc);
|
||||
await ctx.db.patch(args.id, patchDoc);
|
||||
|
||||
if (!args.contatos) {
|
||||
return null;
|
||||
}
|
||||
if (!args.contatos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const contato of args.contatos) {
|
||||
if (contato._id && contato._deleted) {
|
||||
await ctx.db.delete(contato._id);
|
||||
} else if (contato._id) {
|
||||
await ctx.db.patch(contato._id, {
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
descricao: contato.descricao,
|
||||
});
|
||||
} else if (!contato._deleted) {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
for (const contato of args.contatos) {
|
||||
if (contato._id && contato._deleted) {
|
||||
await ctx.db.delete(contato._id);
|
||||
} else if (contato._id) {
|
||||
await ctx.db.patch(contato._id, {
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
descricao: contato.descricao
|
||||
});
|
||||
} else if (!contato._deleted) {
|
||||
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||
|
||||
if (!usuarioAtual) {
|
||||
throw new Error("Usuário não autenticado.");
|
||||
}
|
||||
if (!usuarioAtual) {
|
||||
throw new Error('Usuário não autenticado.');
|
||||
}
|
||||
|
||||
await ctx.db.insert("contatosEmpresa", {
|
||||
empresaId: args.id,
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
adicionadoPor: usuarioAtual._id,
|
||||
descricao: contato.descricao,
|
||||
});
|
||||
}
|
||||
}
|
||||
await ctx.db.insert('contatosEmpresa', {
|
||||
empresaId: args.id,
|
||||
nome: contato.nome,
|
||||
funcao: contato.funcao,
|
||||
email: contato.email,
|
||||
telefone: contato.telefone,
|
||||
adicionadoPor: usuarioAtual._id,
|
||||
descricao: contato.descricao
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
* Calcula distância entre duas coordenadas (fórmula de Haversine)
|
||||
* Retorna distância em metros
|
||||
*/
|
||||
function calcularDistancia(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const R = 6371000; // Raio da Terra em metros
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||
@@ -32,7 +27,7 @@ function calcularDistancia(
|
||||
*/
|
||||
export const listarEnderecos = query({
|
||||
args: {
|
||||
incluirInativos: v.optional(v.boolean()),
|
||||
incluirInativos: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -52,7 +47,7 @@ export const listarEnderecos = query({
|
||||
|
||||
// Ordenar por nome
|
||||
return enderecos.sort((a, b) => a.nome.localeCompare(b.nome));
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -60,7 +55,7 @@ export const listarEnderecos = query({
|
||||
*/
|
||||
export const obterEndereco = query({
|
||||
args: {
|
||||
enderecoId: v.id('enderecosMarcacao'),
|
||||
enderecoId: v.id('enderecosMarcacao')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -74,7 +69,7 @@ export const obterEndereco = query({
|
||||
}
|
||||
|
||||
return endereco;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -98,7 +93,7 @@ export const criarEndereco = mutation({
|
||||
v.literal('home_office'),
|
||||
v.literal('deslocamento'),
|
||||
v.literal('cliente')
|
||||
),
|
||||
)
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -155,11 +150,11 @@ export const criarEndereco = mutation({
|
||||
tipo: args.tipo,
|
||||
ativo: true,
|
||||
criadoPor: usuario._id as Id<'usuarios'>,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { enderecoId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -187,7 +182,7 @@ export const atualizarEndereco = mutation({
|
||||
v.literal('deslocamento'),
|
||||
v.literal('cliente')
|
||||
)
|
||||
),
|
||||
)
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -235,14 +230,7 @@ export const atualizarEndereco = mutation({
|
||||
const lat = args.latitude ?? endereco.latitude;
|
||||
const lon = args.longitude ?? endereco.longitude;
|
||||
|
||||
if (
|
||||
isNaN(lat) ||
|
||||
lat < -90 ||
|
||||
lat > 90 ||
|
||||
isNaN(lon) ||
|
||||
lon < -180 ||
|
||||
lon > 180
|
||||
) {
|
||||
if (isNaN(lat) || lat < -90 || lat > 90 || isNaN(lon) || lon < -180 || lon > 180) {
|
||||
throw new Error('Coordenadas inválidas');
|
||||
}
|
||||
|
||||
@@ -304,7 +292,7 @@ export const atualizarEndereco = mutation({
|
||||
await ctx.db.patch(args.enderecoId, atualizacoes);
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -312,7 +300,7 @@ export const atualizarEndereco = mutation({
|
||||
*/
|
||||
export const desativarEndereco = mutation({
|
||||
args: {
|
||||
enderecoId: v.id('enderecosMarcacao'),
|
||||
enderecoId: v.id('enderecosMarcacao')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -330,11 +318,11 @@ export const desativarEndereco = mutation({
|
||||
await ctx.db.patch(args.enderecoId, {
|
||||
ativo: false,
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -344,7 +332,7 @@ export const desativarEndereco = mutation({
|
||||
export const obterEnderecosFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
dataAtual: v.optional(v.string()), // YYYY-MM-DD para validar períodos
|
||||
dataAtual: v.optional(v.string()) // YYYY-MM-DD para validar períodos
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -387,13 +375,12 @@ export const obterEnderecosFuncionario = query({
|
||||
if (!endereco || !endereco.ativo) continue;
|
||||
|
||||
// Usar raio personalizado se disponível, senão usar o raio do endereço
|
||||
const raioMetros =
|
||||
associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
|
||||
const raioMetros = associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
|
||||
|
||||
enderecosPermitidos.push({
|
||||
enderecoId: endereco._id,
|
||||
raioMetros,
|
||||
periodoValido: true,
|
||||
periodoValido: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -409,7 +396,7 @@ export const obterEnderecosFuncionario = query({
|
||||
enderecosPermitidos.push({
|
||||
enderecoId: endereco._id,
|
||||
raioMetros: endereco.raioMetros,
|
||||
periodoValido: true,
|
||||
periodoValido: true
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -423,7 +410,7 @@ export const obterEnderecosFuncionario = query({
|
||||
return {
|
||||
...endereco,
|
||||
raioMetros: item.raioMetros,
|
||||
periodoValido: item.periodoValido,
|
||||
periodoValido: item.periodoValido
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -431,7 +418,7 @@ export const obterEnderecosFuncionario = query({
|
||||
return enderecosCompletos.filter(
|
||||
(endereco): endereco is NonNullable<typeof endereco> => endereco !== null
|
||||
);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -465,7 +452,7 @@ async function validarLocalizacaoGeofencingInternal(
|
||||
) {
|
||||
return {
|
||||
dentroRaio: false,
|
||||
avisos: ['Coordenadas inválidas'],
|
||||
avisos: ['Coordenadas inválidas']
|
||||
};
|
||||
}
|
||||
|
||||
@@ -500,12 +487,11 @@ async function validarLocalizacaoGeofencingInternal(
|
||||
const endereco = await ctx.db.get(associacao.enderecoMarcacaoId);
|
||||
if (!endereco || !endereco.ativo) continue;
|
||||
|
||||
const raioMetros =
|
||||
associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
|
||||
const raioMetros = associacao.raioMetrosPersonalizado ?? endereco.raioMetros;
|
||||
|
||||
enderecosParaValidar.push({
|
||||
enderecoId: endereco._id,
|
||||
raioMetros,
|
||||
raioMetros
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,39 +506,32 @@ async function validarLocalizacaoGeofencingInternal(
|
||||
for (const endereco of enderecosSede) {
|
||||
enderecosParaValidar.push({
|
||||
enderecoId: endereco._id,
|
||||
raioMetros: endereco.raioMetros,
|
||||
raioMetros: endereco.raioMetros
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Se ainda não houver endereços, usar raio padrão
|
||||
if (enderecosParaValidar.length === 0) {
|
||||
avisos.push(
|
||||
'Nenhum endereço de marcação configurado. Usando validação padrão.'
|
||||
);
|
||||
avisos.push('Nenhum endereço de marcação configurado. Usando validação padrão.');
|
||||
return {
|
||||
dentroRaio: true, // Não bloquear se não houver configuração
|
||||
raioUsado: raioPadrao,
|
||||
avisos,
|
||||
avisos
|
||||
};
|
||||
}
|
||||
|
||||
// Calcular distância para cada endereço e encontrar o mais próximo
|
||||
let enderecoMaisProximo: Id<'enderecosMarcacao'> | undefined = undefined;
|
||||
let distanciaMinima: number | undefined = undefined;
|
||||
let raioUsado: number | undefined = undefined;
|
||||
let enderecoEncontrado: string | undefined = undefined;
|
||||
let enderecoMaisProximo: Id<'enderecosMarcacao'> | undefined;
|
||||
let distanciaMinima: number | undefined;
|
||||
let raioUsado: number | undefined;
|
||||
let enderecoEncontrado: string | undefined;
|
||||
|
||||
for (const item of enderecosParaValidar) {
|
||||
const endereco = await ctx.db.get(item.enderecoId);
|
||||
if (!endereco) continue;
|
||||
|
||||
const distancia = calcularDistancia(
|
||||
latitude,
|
||||
longitude,
|
||||
endereco.latitude,
|
||||
endereco.longitude
|
||||
);
|
||||
const distancia = calcularDistancia(latitude, longitude, endereco.latitude, endereco.longitude);
|
||||
|
||||
if (distanciaMinima === undefined || distancia < distanciaMinima) {
|
||||
distanciaMinima = distancia;
|
||||
@@ -565,7 +544,7 @@ async function validarLocalizacaoGeofencingInternal(
|
||||
if (enderecoMaisProximo === undefined || distanciaMinima === undefined) {
|
||||
return {
|
||||
dentroRaio: false,
|
||||
avisos: ['Não foi possível validar localização'],
|
||||
avisos: ['Não foi possível validar localização']
|
||||
};
|
||||
}
|
||||
|
||||
@@ -585,7 +564,7 @@ async function validarLocalizacaoGeofencingInternal(
|
||||
distanciaMetros: distanciaMinima,
|
||||
raioUsado: raioUsado ?? raioPadrao,
|
||||
enderecoEncontrado,
|
||||
avisos,
|
||||
avisos
|
||||
};
|
||||
}
|
||||
|
||||
@@ -598,7 +577,7 @@ export const validarLocalizacaoGeofencing = mutation({
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
latitude: v.number(),
|
||||
longitude: v.number(),
|
||||
raioPadrao: v.optional(v.number()), // metros - usado se não houver endereços configurados
|
||||
raioPadrao: v.optional(v.number()) // metros - usado se não houver endereços configurados
|
||||
},
|
||||
returns: v.object({
|
||||
dentroRaio: v.boolean(),
|
||||
@@ -606,7 +585,7 @@ export const validarLocalizacaoGeofencing = mutation({
|
||||
distanciaMetros: v.optional(v.number()),
|
||||
raioUsado: v.optional(v.number()),
|
||||
enderecoEncontrado: v.optional(v.string()),
|
||||
avisos: v.array(v.string()),
|
||||
avisos: v.array(v.string())
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
return await validarLocalizacaoGeofencingInternal(
|
||||
@@ -616,12 +595,10 @@ export const validarLocalizacaoGeofencing = mutation({
|
||||
args.longitude,
|
||||
args.raioPadrao || 100
|
||||
);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Exporta a função auxiliar para uso interno em outras mutations
|
||||
*/
|
||||
export { validarLocalizacaoGeofencingInternal };
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
import { query, mutation } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id, Doc } from './_generated/dataModel';
|
||||
import { internal } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { flowInstanceStatus, flowInstanceStepStatus, flowTemplateStatus } from './tables/flows';
|
||||
|
||||
// ============================================
|
||||
@@ -66,7 +66,10 @@ async function obterEtapaAnterior(
|
||||
.collect();
|
||||
|
||||
// Obter posições de cada passo
|
||||
const stepsWithPosition: Array<{ step: Doc<'flowInstanceSteps'>; position: number }> = [];
|
||||
const stepsWithPosition: Array<{
|
||||
step: Doc<'flowInstanceSteps'>;
|
||||
position: number;
|
||||
}> = [];
|
||||
for (const s of allSteps) {
|
||||
const flowStep = await ctx.db.get(s.flowStepId);
|
||||
if (flowStep) {
|
||||
@@ -1154,7 +1157,10 @@ export const completeStep = mutation({
|
||||
.collect();
|
||||
|
||||
// Encontrar o próximo passo pendente
|
||||
const flowSteps: Array<{ stepId: Id<'flowInstanceSteps'>; position: number }> = [];
|
||||
const flowSteps: Array<{
|
||||
stepId: Id<'flowInstanceSteps'>;
|
||||
position: number;
|
||||
}> = [];
|
||||
for (const s of allSteps) {
|
||||
const flowStep = await ctx.db.get(s.flowStepId);
|
||||
if (flowStep) {
|
||||
@@ -1192,7 +1198,9 @@ export const completeStep = mutation({
|
||||
|
||||
if (nextStep) {
|
||||
// Atualizar currentStepId para o próximo passo
|
||||
await ctx.db.patch(step.flowInstanceId, { currentStepId: nextStep.stepId });
|
||||
await ctx.db.patch(step.flowInstanceId, {
|
||||
currentStepId: nextStep.stepId
|
||||
});
|
||||
|
||||
// Criar notificações para o setor do próximo passo
|
||||
const nextStepData = await ctx.db.get(nextStep.stepId);
|
||||
@@ -1424,7 +1432,9 @@ export const reassignStep = mutation({
|
||||
throw new Error('O funcionário atribuído não pertence ao setor deste passo');
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.instanceStepId, { assignedToId: args.assignedToId });
|
||||
await ctx.db.patch(args.instanceStepId, {
|
||||
assignedToId: args.assignedToId
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
|
||||
/**
|
||||
* Lista todas as associações de endereços para um funcionário
|
||||
@@ -9,7 +9,7 @@ import type { Id } from './_generated/dataModel';
|
||||
export const listarAssociacoesFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
incluirInativos: v.optional(v.boolean()),
|
||||
incluirInativos: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -57,16 +57,14 @@ export const listarAssociacoesFuncionario = query({
|
||||
pais: endereco.pais,
|
||||
raioMetros: endereco.raioMetros, // Raio padrão do endereço
|
||||
tipo: endereco.tipo,
|
||||
ativo: endereco.ativo,
|
||||
},
|
||||
ativo: endereco.ativo
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return associacoesCompletas.filter(
|
||||
(item): item is NonNullable<typeof item> => item !== null
|
||||
);
|
||||
},
|
||||
return associacoesCompletas.filter((item): item is NonNullable<typeof item> => item !== null);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -78,7 +76,7 @@ export const associarEnderecoFuncionario = mutation({
|
||||
enderecoMarcacaoId: v.id('enderecosMarcacao'),
|
||||
raioMetrosPersonalizado: v.optional(v.number()),
|
||||
dataInicio: v.optional(v.string()), // YYYY-MM-DD
|
||||
dataFim: v.optional(v.string()), // YYYY-MM-DD
|
||||
dataFim: v.optional(v.string()) // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -132,7 +130,7 @@ export const associarEnderecoFuncionario = mutation({
|
||||
raioMetrosPersonalizado: args.raioMetrosPersonalizado,
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim,
|
||||
ativo: true,
|
||||
ativo: true
|
||||
});
|
||||
|
||||
return { associacaoId: associacaoExistente._id, atualizado: true };
|
||||
@@ -147,11 +145,11 @@ export const associarEnderecoFuncionario = mutation({
|
||||
dataFim: args.dataFim,
|
||||
ativo: true,
|
||||
criadoPor: usuario._id as Id<'usuarios'>,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { associacaoId, atualizado: false };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -163,7 +161,7 @@ export const atualizarAssociacao = mutation({
|
||||
raioMetrosPersonalizado: v.optional(v.number()),
|
||||
dataInicio: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string()),
|
||||
ativo: v.optional(v.boolean()),
|
||||
ativo: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -213,7 +211,7 @@ export const atualizarAssociacao = mutation({
|
||||
await ctx.db.patch(args.associacaoId, atualizacoes);
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -221,7 +219,7 @@ export const atualizarAssociacao = mutation({
|
||||
*/
|
||||
export const removerAssociacao = mutation({
|
||||
args: {
|
||||
associacaoId: v.id('funcionarioEnderecosMarcacao'),
|
||||
associacaoId: v.id('funcionarioEnderecosMarcacao')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -238,9 +236,9 @@ export const removerAssociacao = mutation({
|
||||
|
||||
// Desativar ao invés de deletar
|
||||
await ctx.db.patch(args.associacaoId, {
|
||||
ativo: false,
|
||||
ativo: false
|
||||
});
|
||||
|
||||
return { sucesso: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Infer, v } from 'convex/values';
|
||||
import { query, mutation } from './_generated/server';
|
||||
import { type Infer, v } from 'convex/values';
|
||||
import { internal } from './_generated/api';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { simboloTipo } from './tables/funcionarios';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { query } from "./_generated/server";
|
||||
import { query } from './_generated/server';
|
||||
|
||||
export const get = query({
|
||||
handler: async () => {
|
||||
return "OK";
|
||||
},
|
||||
return 'OK';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import { httpRouter } from "convex/server";
|
||||
import { authComponent, createAuth } from "./auth";
|
||||
import { httpAction } from "./_generated/server";
|
||||
import { api } from "./_generated/api";
|
||||
import { getClientIP } from "./utils/getClientIP";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
// Action HTTP para análise de segurança de requisições
|
||||
// Pode ser chamada do frontend ou de outros sistemas
|
||||
http.route({
|
||||
path: "/security/analyze",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method;
|
||||
|
||||
// Extrair IP do cliente
|
||||
const ipOrigem = getClientIP(request);
|
||||
|
||||
// Extrair headers
|
||||
const headers: Record<string, string> = {};
|
||||
request.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
// Extrair query params
|
||||
const queryParams: Record<string, string> = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
queryParams[key] = value;
|
||||
});
|
||||
|
||||
// Extrair body se disponível
|
||||
let body: string | undefined;
|
||||
try {
|
||||
body = await request.text();
|
||||
} catch {
|
||||
// Ignorar erros ao ler body
|
||||
}
|
||||
|
||||
// Analisar requisição para detectar ataques
|
||||
const resultado = await ctx.runMutation(api.security.analisarRequisicaoHTTP, {
|
||||
url: url.pathname + url.search,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
queryParams,
|
||||
ipOrigem,
|
||||
userAgent: request.headers.get('user-agent') ?? undefined
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(resultado), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Seed de rate limit para ambiente de desenvolvimento
|
||||
http.route({
|
||||
path: "/security/rate-limit/seed-dev",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx) => {
|
||||
const resultado = await ctx.runMutation(api.security.seedRateLimitDev, {});
|
||||
return new Response(JSON.stringify(resultado), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
authComponent.registerRoutes(http, createAuth);
|
||||
|
||||
export default http;
|
||||
import { httpRouter } from 'convex/server';
|
||||
import { api } from './_generated/api';
|
||||
import { httpAction } from './_generated/server';
|
||||
import { authComponent, createAuth } from './auth';
|
||||
import { getClientIP } from './utils/getClientIP';
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
// Action HTTP para análise de segurança de requisições
|
||||
// Pode ser chamada do frontend ou de outros sistemas
|
||||
http.route({
|
||||
path: '/security/analyze',
|
||||
method: 'POST',
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method;
|
||||
|
||||
// Extrair IP do cliente
|
||||
const ipOrigem = getClientIP(request);
|
||||
|
||||
// Extrair headers
|
||||
const headers: Record<string, string> = {};
|
||||
request.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
// Extrair query params
|
||||
const queryParams: Record<string, string> = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
queryParams[key] = value;
|
||||
});
|
||||
|
||||
// Extrair body se disponível
|
||||
let body: string | undefined;
|
||||
try {
|
||||
body = await request.text();
|
||||
} catch {
|
||||
// Ignorar erros ao ler body
|
||||
}
|
||||
|
||||
// Analisar requisição para detectar ataques
|
||||
const resultado = await ctx.runMutation(api.security.analisarRequisicaoHTTP, {
|
||||
url: url.pathname + url.search,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
queryParams,
|
||||
ipOrigem,
|
||||
userAgent: request.headers.get('user-agent') ?? undefined
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(resultado), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Seed de rate limit para ambiente de desenvolvimento
|
||||
http.route({
|
||||
path: '/security/rate-limit/seed-dev',
|
||||
method: 'POST',
|
||||
handler: httpAction(async (ctx) => {
|
||||
const resultado = await ctx.runMutation(api.security.seedRateLimitDev, {});
|
||||
return new Response(JSON.stringify(resultado), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
authComponent.registerRoutes(http, createAuth);
|
||||
|
||||
export default http;
|
||||
|
||||
@@ -1,232 +1,219 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
|
||||
/**
|
||||
* Listar logs de acesso com filtros
|
||||
*/
|
||||
export const listar = query({
|
||||
args: {
|
||||
usuarioId: v.optional(v.id("usuarios")),
|
||||
tipo: v.optional(
|
||||
v.union(
|
||||
v.literal("login"),
|
||||
v.literal("logout"),
|
||||
v.literal("acesso_negado"),
|
||||
v.literal("senha_alterada"),
|
||||
v.literal("sessao_expirada")
|
||||
)
|
||||
),
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number()),
|
||||
limite: v.optional(v.number()),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("logsAcesso"),
|
||||
tipo: v.union(
|
||||
v.literal("login"),
|
||||
v.literal("logout"),
|
||||
v.literal("acesso_negado"),
|
||||
v.literal("senha_alterada"),
|
||||
v.literal("sessao_expirada")
|
||||
),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
detalhes: v.optional(v.string()),
|
||||
timestamp: v.number(),
|
||||
usuario: v.optional(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
let logs;
|
||||
args: {
|
||||
usuarioId: v.optional(v.id('usuarios')),
|
||||
tipo: v.optional(
|
||||
v.union(
|
||||
v.literal('login'),
|
||||
v.literal('logout'),
|
||||
v.literal('acesso_negado'),
|
||||
v.literal('senha_alterada'),
|
||||
v.literal('sessao_expirada')
|
||||
)
|
||||
),
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number()),
|
||||
limite: v.optional(v.number())
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('logsAcesso'),
|
||||
tipo: v.union(
|
||||
v.literal('login'),
|
||||
v.literal('logout'),
|
||||
v.literal('acesso_negado'),
|
||||
v.literal('senha_alterada'),
|
||||
v.literal('sessao_expirada')
|
||||
),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
detalhes: v.optional(v.string()),
|
||||
timestamp: v.number(),
|
||||
usuario: v.optional(
|
||||
v.object({
|
||||
_id: v.id('usuarios'),
|
||||
matricula: v.string(),
|
||||
nome: v.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
let logs;
|
||||
|
||||
// Filtrar por usuário
|
||||
if (args.usuarioId !== undefined) {
|
||||
const usuarioId = args.usuarioId; // TypeScript agora sabe que não é undefined
|
||||
logs = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", usuarioId))
|
||||
.collect();
|
||||
} else {
|
||||
logs = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.withIndex("by_timestamp")
|
||||
.collect();
|
||||
}
|
||||
// Filtrar por usuário
|
||||
if (args.usuarioId !== undefined) {
|
||||
const usuarioId = args.usuarioId; // TypeScript agora sabe que não é undefined
|
||||
logs = await ctx.db
|
||||
.query('logsAcesso')
|
||||
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuarioId))
|
||||
.collect();
|
||||
} else {
|
||||
logs = await ctx.db.query('logsAcesso').withIndex('by_timestamp').collect();
|
||||
}
|
||||
|
||||
// Filtrar por tipo
|
||||
if (args.tipo) {
|
||||
logs = logs.filter((log) => log.tipo === args.tipo);
|
||||
}
|
||||
// Filtrar por tipo
|
||||
if (args.tipo) {
|
||||
logs = logs.filter((log) => log.tipo === args.tipo);
|
||||
}
|
||||
|
||||
// Filtrar por data
|
||||
if (args.dataInicio) {
|
||||
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
|
||||
}
|
||||
if (args.dataFim) {
|
||||
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
|
||||
}
|
||||
// Filtrar por data
|
||||
if (args.dataInicio) {
|
||||
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
|
||||
}
|
||||
if (args.dataFim) {
|
||||
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Ordenar por timestamp decrescente
|
||||
logs.sort((a, b) => b.timestamp - a.timestamp);
|
||||
// Ordenar por timestamp decrescente
|
||||
logs.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
// Limitar resultados
|
||||
if (args.limite) {
|
||||
logs = logs.slice(0, args.limite);
|
||||
}
|
||||
// Limitar resultados
|
||||
if (args.limite) {
|
||||
logs = logs.slice(0, args.limite);
|
||||
}
|
||||
|
||||
// Buscar informações dos usuários
|
||||
const resultado = [];
|
||||
for (const log of logs) {
|
||||
let usuario = undefined;
|
||||
if (log.usuarioId) {
|
||||
const user = await ctx.db.get(log.usuarioId);
|
||||
if (user) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (user.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(user.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
usuario = {
|
||||
_id: user._id,
|
||||
matricula: matricula || "",
|
||||
nome: user.nome,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Buscar informações dos usuários
|
||||
const resultado = [];
|
||||
for (const log of logs) {
|
||||
let usuario;
|
||||
if (log.usuarioId) {
|
||||
const user = await ctx.db.get(log.usuarioId);
|
||||
if (user) {
|
||||
let matricula: string | undefined;
|
||||
if (user.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(user.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
usuario = {
|
||||
_id: user._id,
|
||||
matricula: matricula || '',
|
||||
nome: user.nome
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
resultado.push({
|
||||
_id: log._id,
|
||||
tipo: log.tipo,
|
||||
ipAddress: log.ipAddress,
|
||||
userAgent: log.userAgent,
|
||||
detalhes: log.detalhes,
|
||||
timestamp: log.timestamp,
|
||||
usuario,
|
||||
});
|
||||
}
|
||||
resultado.push({
|
||||
_id: log._id,
|
||||
tipo: log.tipo,
|
||||
ipAddress: log.ipAddress,
|
||||
userAgent: log.userAgent,
|
||||
detalhes: log.detalhes,
|
||||
timestamp: log.timestamp,
|
||||
usuario
|
||||
});
|
||||
}
|
||||
|
||||
return resultado;
|
||||
},
|
||||
return resultado;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Obter estatísticas de acessos
|
||||
*/
|
||||
export const estatisticas = query({
|
||||
args: {
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number()),
|
||||
},
|
||||
returns: v.object({
|
||||
totalLogins: v.number(),
|
||||
totalLogouts: v.number(),
|
||||
totalAcessosNegados: v.number(),
|
||||
totalSenhasAlteradas: v.number(),
|
||||
totalSessoesExpiradas: v.number(),
|
||||
loginsPorDia: v.array(
|
||||
v.object({
|
||||
data: v.string(),
|
||||
quantidade: v.number(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
let logs = await ctx.db.query("logsAcesso").collect();
|
||||
args: {
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number())
|
||||
},
|
||||
returns: v.object({
|
||||
totalLogins: v.number(),
|
||||
totalLogouts: v.number(),
|
||||
totalAcessosNegados: v.number(),
|
||||
totalSenhasAlteradas: v.number(),
|
||||
totalSessoesExpiradas: v.number(),
|
||||
loginsPorDia: v.array(
|
||||
v.object({
|
||||
data: v.string(),
|
||||
quantidade: v.number()
|
||||
})
|
||||
)
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
let logs = await ctx.db.query('logsAcesso').collect();
|
||||
|
||||
// Filtrar por data
|
||||
if (args.dataInicio) {
|
||||
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
|
||||
}
|
||||
if (args.dataFim) {
|
||||
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
|
||||
}
|
||||
// Filtrar por data
|
||||
if (args.dataInicio) {
|
||||
logs = logs.filter((log) => log.timestamp >= args.dataInicio!);
|
||||
}
|
||||
if (args.dataFim) {
|
||||
logs = logs.filter((log) => log.timestamp <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Contar por tipo
|
||||
const totalLogins = logs.filter((log) => log.tipo === "login").length;
|
||||
const totalLogouts = logs.filter((log) => log.tipo === "logout").length;
|
||||
const totalAcessosNegados = logs.filter(
|
||||
(log) => log.tipo === "acesso_negado"
|
||||
).length;
|
||||
const totalSenhasAlteradas = logs.filter(
|
||||
(log) => log.tipo === "senha_alterada"
|
||||
).length;
|
||||
const totalSessoesExpiradas = logs.filter(
|
||||
(log) => log.tipo === "sessao_expirada"
|
||||
).length;
|
||||
// Contar por tipo
|
||||
const totalLogins = logs.filter((log) => log.tipo === 'login').length;
|
||||
const totalLogouts = logs.filter((log) => log.tipo === 'logout').length;
|
||||
const totalAcessosNegados = logs.filter((log) => log.tipo === 'acesso_negado').length;
|
||||
const totalSenhasAlteradas = logs.filter((log) => log.tipo === 'senha_alterada').length;
|
||||
const totalSessoesExpiradas = logs.filter((log) => log.tipo === 'sessao_expirada').length;
|
||||
|
||||
// Agrupar logins por dia
|
||||
const loginsPorDiaMap = new Map<string, number>();
|
||||
const loginsOnly = logs.filter((log) => log.tipo === "login");
|
||||
// Agrupar logins por dia
|
||||
const loginsPorDiaMap = new Map<string, number>();
|
||||
const loginsOnly = logs.filter((log) => log.tipo === 'login');
|
||||
|
||||
for (const log of loginsOnly) {
|
||||
const data = new Date(log.timestamp).toISOString().split("T")[0];
|
||||
loginsPorDiaMap.set(data, (loginsPorDiaMap.get(data) || 0) + 1);
|
||||
}
|
||||
for (const log of loginsOnly) {
|
||||
const data = new Date(log.timestamp).toISOString().split('T')[0];
|
||||
loginsPorDiaMap.set(data, (loginsPorDiaMap.get(data) || 0) + 1);
|
||||
}
|
||||
|
||||
const loginsPorDia = Array.from(loginsPorDiaMap.entries())
|
||||
.map(([data, quantidade]) => ({ data, quantidade }))
|
||||
.sort((a, b) => a.data.localeCompare(b.data));
|
||||
const loginsPorDia = Array.from(loginsPorDiaMap.entries())
|
||||
.map(([data, quantidade]) => ({ data, quantidade }))
|
||||
.sort((a, b) => a.data.localeCompare(b.data));
|
||||
|
||||
return {
|
||||
totalLogins,
|
||||
totalLogouts,
|
||||
totalAcessosNegados,
|
||||
totalSenhasAlteradas,
|
||||
totalSessoesExpiradas,
|
||||
loginsPorDia,
|
||||
};
|
||||
},
|
||||
return {
|
||||
totalLogins,
|
||||
totalLogouts,
|
||||
totalAcessosNegados,
|
||||
totalSenhasAlteradas,
|
||||
totalSessoesExpiradas,
|
||||
loginsPorDia
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Limpar logs antigos (apenas TI)
|
||||
*/
|
||||
export const limpar = mutation({
|
||||
args: {
|
||||
dataLimite: v.number(), // Excluir logs anteriores a esta data
|
||||
},
|
||||
returns: v.object({
|
||||
excluidos: v.number(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const logs = await ctx.db
|
||||
.query("logsAcesso")
|
||||
.withIndex("by_timestamp")
|
||||
.collect();
|
||||
args: {
|
||||
dataLimite: v.number() // Excluir logs anteriores a esta data
|
||||
},
|
||||
returns: v.object({
|
||||
excluidos: v.number()
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const logs = await ctx.db.query('logsAcesso').withIndex('by_timestamp').collect();
|
||||
|
||||
const logsAntigos = logs.filter((log) => log.timestamp < args.dataLimite);
|
||||
const logsAntigos = logs.filter((log) => log.timestamp < args.dataLimite);
|
||||
|
||||
for (const log of logsAntigos) {
|
||||
await ctx.db.delete(log._id);
|
||||
}
|
||||
for (const log of logsAntigos) {
|
||||
await ctx.db.delete(log._id);
|
||||
}
|
||||
|
||||
return { excluidos: logsAntigos.length };
|
||||
},
|
||||
return { excluidos: logsAntigos.length };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Limpar todos os logs (apenas TI)
|
||||
*/
|
||||
export const limparTodos = mutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
excluidos: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const logs = await ctx.db.query("logsAcesso").collect();
|
||||
args: {},
|
||||
returns: v.object({
|
||||
excluidos: v.number()
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const logs = await ctx.db.query('logsAcesso').collect();
|
||||
|
||||
for (const log of logs) {
|
||||
await ctx.db.delete(log._id);
|
||||
}
|
||||
for (const log of logs) {
|
||||
await ctx.db.delete(log._id);
|
||||
}
|
||||
|
||||
return { excluidos: logs.length };
|
||||
},
|
||||
return { excluidos: logs.length };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v } from 'convex/values';
|
||||
import { MutationCtx, query } from './_generated/server';
|
||||
import { Id } from './_generated/dataModel';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { type MutationCtx, query } from './_generated/server';
|
||||
|
||||
/**
|
||||
* Helper function para registrar atividades no sistema
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query, internalMutation } from './_generated/server';
|
||||
import { internal, api } from './_generated/api';
|
||||
import { Id } from './_generated/dataModel';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
|
||||
/**
|
||||
* Helper para obter usuário autenticado
|
||||
@@ -625,20 +625,15 @@ export const getStatusSistema = query({
|
||||
}
|
||||
|
||||
// Total de registros (estimativa baseada em tabelas principais)
|
||||
const [usuarios, funcionarios, simbolos, alertas, metricas] =
|
||||
await Promise.all([
|
||||
ctx.db.query('usuarios').collect(),
|
||||
ctx.db.query('funcionarios').collect(),
|
||||
ctx.db.query('simbolos').collect(),
|
||||
ctx.db.query('alertConfigurations').collect(),
|
||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
||||
]);
|
||||
const [usuarios, funcionarios, simbolos, alertas, metricas] = await Promise.all([
|
||||
ctx.db.query('usuarios').collect(),
|
||||
ctx.db.query('funcionarios').collect(),
|
||||
ctx.db.query('simbolos').collect(),
|
||||
ctx.db.query('alertConfigurations').collect(),
|
||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
||||
]);
|
||||
const totalRegistros =
|
||||
usuarios.length +
|
||||
funcionarios.length +
|
||||
simbolos.length +
|
||||
alertas.length +
|
||||
metricas.length;
|
||||
usuarios.length + funcionarios.length + simbolos.length + alertas.length + metricas.length;
|
||||
|
||||
// Métricas de performance com fallbacks seguros
|
||||
const tempoMedioResposta = ultimaMetrica?.tempoRespostaMedio ?? 0;
|
||||
@@ -703,18 +698,18 @@ export const getAtividadeBancoDados = query({
|
||||
for (let i = 0; i < numBuckets; i++) {
|
||||
const inicio = haUmMinuto + i * bucketSizeMs;
|
||||
const fim = inicio + bucketSizeMs;
|
||||
|
||||
|
||||
// Contar atividades de criação/inserção (entradas)
|
||||
const atividadesBucket = atividadesRecentes.filter(
|
||||
(a) => a.timestamp >= inicio && a.timestamp < fim
|
||||
);
|
||||
const entradasAtividades = atividadesBucket.filter(
|
||||
a => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
|
||||
(a) => a.acao === 'criar' || a.acao === 'inserir' || a.acao === 'cadastrar'
|
||||
).length;
|
||||
|
||||
|
||||
// Contar atividades de exclusão/remoção (saídas)
|
||||
const saidasAtividades = atividadesBucket.filter(
|
||||
a => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
|
||||
(a) => a.acao === 'excluir' || a.acao === 'remover' || a.acao === 'deletar'
|
||||
).length;
|
||||
|
||||
// Usar mensagensPorMinuto como adicional se disponível
|
||||
@@ -748,7 +743,7 @@ export const getDistribuicaoRequisicoes = query({
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const umaHoraAtras = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
|
||||
// Buscar atividades reais do sistema
|
||||
const atividades = await ctx.db
|
||||
.query('logsAtividades')
|
||||
@@ -764,14 +759,24 @@ export const getDistribuicaoRequisicoes = query({
|
||||
|
||||
// Contar operações de leitura (consultas, visualizações)
|
||||
const leituras = atividades.filter(
|
||||
a => a.acao === 'consultar' || a.acao === 'visualizar' || a.acao === 'listar' || a.acao === 'buscar'
|
||||
(a) =>
|
||||
a.acao === 'consultar' ||
|
||||
a.acao === 'visualizar' ||
|
||||
a.acao === 'listar' ||
|
||||
a.acao === 'buscar'
|
||||
).length;
|
||||
|
||||
// Contar operações de escrita (criar, editar, excluir)
|
||||
const escritas = atividades.filter(
|
||||
a => a.acao === 'criar' || a.acao === 'editar' || a.acao === 'excluir' ||
|
||||
a.acao === 'inserir' || a.acao === 'atualizar' || a.acao === 'deletar' ||
|
||||
a.acao === 'cadastrar' || a.acao === 'remover'
|
||||
(a) =>
|
||||
a.acao === 'criar' ||
|
||||
a.acao === 'editar' ||
|
||||
a.acao === 'excluir' ||
|
||||
a.acao === 'inserir' ||
|
||||
a.acao === 'atualizar' ||
|
||||
a.acao === 'deletar' ||
|
||||
a.acao === 'cadastrar' ||
|
||||
a.acao === 'remover'
|
||||
).length;
|
||||
|
||||
// Adicionar estimativa baseada em mensagens se disponível
|
||||
@@ -782,7 +787,7 @@ export const getDistribuicaoRequisicoes = query({
|
||||
|
||||
// Queries são leituras + parte das mensagens (como consultas de chat)
|
||||
const queries = leituras + Math.round(totalMensagens * 0.5);
|
||||
|
||||
|
||||
// Mutations são escritas + parte das mensagens (como envio de mensagens)
|
||||
const mutations = escritas + Math.round(totalMensagens * 0.3);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('produtos').collect();
|
||||
return await ctx.db.query('objetos').collect();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const search = query({
|
||||
args: { query: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query('produtos')
|
||||
.query('objetos')
|
||||
.withSearchIndex('search_nome', (q) => q.search('nome', args.query))
|
||||
.take(10);
|
||||
}
|
||||
@@ -23,13 +23,17 @@ export const create = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
valorEstimado: v.string(),
|
||||
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
|
||||
tipo: v.union(v.literal('material'), v.literal('servico')),
|
||||
codigoEfisco: v.string(),
|
||||
codigoCatmat: v.optional(v.string()),
|
||||
codigoCatserv: v.optional(v.string()),
|
||||
unidade: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
if (!user) throw new Error('Unauthorized');
|
||||
|
||||
return await ctx.db.insert('produtos', {
|
||||
return await ctx.db.insert('objetos', {
|
||||
...args,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now()
|
||||
@@ -39,10 +43,14 @@ export const create = mutation({
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
id: v.id('produtos'),
|
||||
id: v.id('objetos'),
|
||||
nome: v.string(),
|
||||
valorEstimado: v.string(),
|
||||
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo'))
|
||||
tipo: v.union(v.literal('material'), v.literal('servico')),
|
||||
codigoEfisco: v.string(),
|
||||
codigoCatmat: v.optional(v.string()),
|
||||
codigoCatserv: v.optional(v.string()),
|
||||
unidade: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
@@ -51,14 +59,18 @@ export const update = mutation({
|
||||
await ctx.db.patch(args.id, {
|
||||
nome: args.nome,
|
||||
valorEstimado: args.valorEstimado,
|
||||
tipo: args.tipo
|
||||
tipo: args.tipo,
|
||||
codigoEfisco: args.codigoEfisco,
|
||||
codigoCatmat: args.codigoCatmat,
|
||||
codigoCatserv: args.codigoCatserv,
|
||||
unidade: args.unidade
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: {
|
||||
id: v.id('produtos')
|
||||
id: v.id('objetos')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await getCurrentUserFunction(ctx);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mutation, query, internalMutation } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { QueryCtx, MutationCtx } from './_generated/server';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
@@ -30,7 +30,7 @@ export const list = query({
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
// acaoId removed from return
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
@@ -72,10 +72,17 @@ export const getItems = query({
|
||||
args: { pedidoId: v.id('pedidos') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pedidoItems'),
|
||||
_id: v.id('objetoItems'),
|
||||
_creationTime: v.number(),
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
modalidade: v.union(
|
||||
v.literal('dispensa'),
|
||||
v.literal('inexgibilidade'),
|
||||
v.literal('adesao'),
|
||||
v.literal('consumo')
|
||||
),
|
||||
valorEstimado: v.string(),
|
||||
valorReal: v.optional(v.string()),
|
||||
quantidade: v.number(),
|
||||
@@ -86,7 +93,7 @@ export const getItems = query({
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
@@ -137,9 +144,9 @@ export const getHistory = query({
|
||||
|
||||
export const checkExisting = query({
|
||||
args: {
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
acaoId: v.optional(v.id('acoes')), // Used to filter items
|
||||
numeroSei: v.optional(v.string()),
|
||||
produtoIds: v.optional(v.array(v.id('produtos')))
|
||||
objetoIds: v.optional(v.array(v.id('objetos')))
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
@@ -154,14 +161,14 @@ export const checkExisting = query({
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
// acaoId removed
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
matchingItems: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
quantidade: v.number()
|
||||
})
|
||||
)
|
||||
@@ -186,42 +193,47 @@ export const checkExisting = query({
|
||||
pedidosAbertos = pedidosAbertos.concat(partial);
|
||||
}
|
||||
|
||||
// 2) Filtros opcionais: acaoId e numeroSei
|
||||
// 2) Filtros opcionais: numeroSei
|
||||
pedidosAbertos = pedidosAbertos.filter((p) => {
|
||||
if (args.acaoId && p.acaoId !== args.acaoId) return false;
|
||||
if (args.numeroSei && p.numeroSei !== args.numeroSei) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 3) Filtro por produtos (se informado) e coleta de matchingItems
|
||||
// 3) Filtro por acaoId (via items)
|
||||
if (args.acaoId) {
|
||||
// This is expensive, but for now we iterate. Better would be to query items by acaoId first.
|
||||
// Optimization: Query items by acaoId and get unique pedidoIds.
|
||||
const itemsComAcao = await ctx.db
|
||||
.query('objetoItems')
|
||||
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
|
||||
.collect();
|
||||
|
||||
const pedidoIdsComAcao = new Set(itemsComAcao.map((i) => i.pedidoId));
|
||||
pedidosAbertos = pedidosAbertos.filter((p) => pedidoIdsComAcao.has(p._id));
|
||||
}
|
||||
|
||||
// 4) Filtro por objetos (se informado) e coleta de matchingItems
|
||||
const resultados = [];
|
||||
|
||||
for (const pedido of pedidosAbertos) {
|
||||
let include = true;
|
||||
let matchingItems: { produtoId: Id<'produtos'>; quantidade: number }[] = [];
|
||||
let matchingItems: { objetoId: Id<'objetos'>; quantidade: number }[] = [];
|
||||
|
||||
// Se houver filtro de produtos, verificamos se o pedido tem ALGUM dos produtos
|
||||
if (args.produtoIds && args.produtoIds.length > 0) {
|
||||
// Se houver filtro de objetos, verificamos se o pedido tem ALGUM dos objetos
|
||||
if (args.objetoIds && args.objetoIds.length > 0) {
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
|
||||
.collect();
|
||||
|
||||
// const pedidoProdutoIds = new Set(items.map((i) => i.produtoId)); // Unused
|
||||
const matching = items.filter((i) => args.produtoIds?.includes(i.produtoId));
|
||||
const matching = items.filter((i) => args.objetoIds?.includes(i.objetoId));
|
||||
|
||||
if (matching.length > 0) {
|
||||
matchingItems = matching.map((i) => ({
|
||||
produtoId: i.produtoId,
|
||||
objetoId: i.objetoId,
|
||||
quantidade: i.quantidade
|
||||
}));
|
||||
} else {
|
||||
// Se foi pedido filtro por produtos e não tem nenhum match, ignoramos este pedido
|
||||
// A MENOS que tenha dado match por numeroSei ou acaoId?
|
||||
// A regra original era: "Filtro por produtos (se informado)"
|
||||
// Se o usuário informou produtos, ele quer ver pedidos que tenham esses produtos.
|
||||
// Mas se ele TAMBÉM informou numeroSei, talvez ele queira ver aquele pedido específico mesmo sem o produto?
|
||||
// Vamos manter a lógica de "E": se informou produtos, tem que ter o produto.
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +244,6 @@ export const checkExisting = query({
|
||||
_creationTime: pedido._creationTime,
|
||||
numeroSei: pedido.numeroSei,
|
||||
status: pedido.status,
|
||||
acaoId: pedido.acaoId,
|
||||
criadoPor: pedido.criadoPor,
|
||||
criadoEm: pedido.criadoEm,
|
||||
atualizadoEm: pedido.atualizadoEm,
|
||||
@@ -249,8 +260,8 @@ export const checkExisting = query({
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
numeroSei: v.optional(v.string()),
|
||||
acaoId: v.optional(v.id('acoes'))
|
||||
numeroSei: v.optional(v.string())
|
||||
// acaoId removed
|
||||
},
|
||||
returns: v.id('pedidos'),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -262,31 +273,12 @@ export const create = mutation({
|
||||
throw new Error('Setor de Compras não configurado. Contate o administrador.');
|
||||
}
|
||||
|
||||
// 2. Check Existing (Double check)
|
||||
if (args.acaoId) {
|
||||
const existing = await ctx.db
|
||||
.query('pedidos')
|
||||
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
|
||||
.filter((q) =>
|
||||
q.or(
|
||||
q.eq(q.field('status'), 'em_rascunho'),
|
||||
q.eq(q.field('status'), 'aguardando_aceite'),
|
||||
q.eq(q.field('status'), 'em_analise'),
|
||||
q.eq(q.field('status'), 'precisa_ajustes')
|
||||
)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
throw new Error('Já existe um pedido em andamento para esta ação.');
|
||||
}
|
||||
}
|
||||
// 2. Check Existing (Double check) - Removed acaoId check here as it's now per item
|
||||
|
||||
// 3. Create Order
|
||||
const pedidoId = await ctx.db.insert('pedidos', {
|
||||
numeroSei: args.numeroSei,
|
||||
status: 'em_rascunho',
|
||||
acaoId: args.acaoId,
|
||||
criadoPor: user._id,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now()
|
||||
@@ -297,7 +289,7 @@ export const create = mutation({
|
||||
pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'criacao',
|
||||
detalhes: JSON.stringify({ numeroSei: args.numeroSei, acaoId: args.acaoId }),
|
||||
detalhes: JSON.stringify({ numeroSei: args.numeroSei }),
|
||||
data: Date.now()
|
||||
});
|
||||
|
||||
@@ -348,7 +340,14 @@ export const updateSeiNumber = mutation({
|
||||
export const addItem = mutation({
|
||||
args: {
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
modalidade: v.union(
|
||||
v.literal('dispensa'),
|
||||
v.literal('inexgibilidade'),
|
||||
v.literal('adesao'),
|
||||
v.literal('consumo')
|
||||
),
|
||||
valorEstimado: v.string(),
|
||||
quantidade: v.number()
|
||||
},
|
||||
@@ -361,34 +360,71 @@ export const addItem = mutation({
|
||||
throw new Error('Usuário não vinculado a um funcionário.');
|
||||
}
|
||||
|
||||
await ctx.db.insert('pedidoItems', {
|
||||
pedidoId: args.pedidoId,
|
||||
produtoId: args.produtoId,
|
||||
valorEstimado: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
adicionadoPor: user.funcionarioId,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
// Check if item already exists with same parameters (user, object, action, modalidade)
|
||||
const existingItem = await ctx.db
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.filter((q) =>
|
||||
q.and(
|
||||
q.eq(q.field('objetoId'), args.objetoId),
|
||||
q.eq(q.field('adicionadoPor'), user.funcionarioId),
|
||||
q.eq(q.field('acaoId'), args.acaoId),
|
||||
q.eq(q.field('modalidade'), args.modalidade)
|
||||
)
|
||||
)
|
||||
.first();
|
||||
|
||||
if (existingItem) {
|
||||
// Increment quantity
|
||||
const novaQuantidade = existingItem.quantidade + args.quantidade;
|
||||
await ctx.db.patch(existingItem._id, { quantidade: novaQuantidade });
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item_incremento',
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: args.objetoId,
|
||||
quantidadeAdicionada: args.quantidade,
|
||||
novaQuantidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
} else {
|
||||
// Insert new item
|
||||
await ctx.db.insert('objetoItems', {
|
||||
pedidoId: args.pedidoId,
|
||||
objetoId: args.objetoId,
|
||||
acaoId: args.acaoId,
|
||||
modalidade: args.modalidade,
|
||||
valorEstimado: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
adicionadoPor: user.funcionarioId,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item',
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: args.objetoId,
|
||||
valor: args.valorEstimado,
|
||||
quantidade: args.quantidade,
|
||||
acaoId: args.acaoId,
|
||||
modalidade: args.modalidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
||||
|
||||
await ctx.db.insert('historicoPedidos', {
|
||||
pedidoId: args.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'adicao_item',
|
||||
detalhes: JSON.stringify({
|
||||
produtoId: args.produtoId,
|
||||
valor: args.valorEstimado,
|
||||
quantidade: args.quantidade
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const updateItemQuantity = mutation({
|
||||
args: {
|
||||
itemId: v.id('pedidoItems'),
|
||||
itemId: v.id('objetoItems'),
|
||||
novaQuantidade: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
@@ -404,14 +440,11 @@ export const updateItemQuantity = mutation({
|
||||
|
||||
const quantidadeAnterior = item.quantidade;
|
||||
|
||||
// Check permission: only item owner can decrease quantity
|
||||
// Check permission: only item owner can change quantity
|
||||
const isOwner = item.adicionadoPor === user.funcionarioId;
|
||||
const isDecreasing = args.novaQuantidade < quantidadeAnterior;
|
||||
|
||||
if (isDecreasing && !isOwner) {
|
||||
throw new Error(
|
||||
'Apenas quem adicionou este item pode diminuir a quantidade. Você pode apenas aumentar.'
|
||||
);
|
||||
if (!isOwner) {
|
||||
throw new Error('Apenas quem adicionou este item pode alterar a quantidade.');
|
||||
}
|
||||
|
||||
// Update quantity
|
||||
@@ -424,7 +457,7 @@ export const updateItemQuantity = mutation({
|
||||
usuarioId: user._id,
|
||||
acao: 'alteracao_quantidade',
|
||||
detalhes: JSON.stringify({
|
||||
produtoId: item.produtoId,
|
||||
objetoId: item.objetoId,
|
||||
quantidadeAnterior,
|
||||
novaQuantidade: args.novaQuantidade
|
||||
}),
|
||||
@@ -435,7 +468,7 @@ export const updateItemQuantity = mutation({
|
||||
|
||||
export const removeItem = mutation({
|
||||
args: {
|
||||
itemId: v.id('pedidoItems')
|
||||
itemId: v.id('objetoItems')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
@@ -451,7 +484,10 @@ export const removeItem = mutation({
|
||||
pedidoId: item.pedidoId,
|
||||
usuarioId: user._id,
|
||||
acao: 'remocao_item',
|
||||
detalhes: JSON.stringify({ produtoId: item.produtoId, valor: item.valorEstimado }),
|
||||
detalhes: JSON.stringify({
|
||||
objetoId: item.objetoId,
|
||||
valor: item.valorEstimado
|
||||
}),
|
||||
data: Date.now()
|
||||
});
|
||||
}
|
||||
@@ -550,7 +586,7 @@ export const notifyStatusChange = internalMutation({
|
||||
|
||||
// Notify item adders
|
||||
const items = await ctx.db
|
||||
.query('pedidoItems')
|
||||
.query('objetoItems')
|
||||
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { query, mutation, internalQuery } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import type { Doc } from './_generated/dataModel';
|
||||
import { internalQuery, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// Catálogo de permissões base para seed controlado via mutation
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import { validarLocalizacaoGeofencingInternal } from './enderecosMarcacao';
|
||||
|
||||
/**
|
||||
* Calcula distância entre duas coordenadas (fórmula de Haversine)
|
||||
* Retorna distância em metros
|
||||
*/
|
||||
function calcularDistancia(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const R = 6371000; // Raio da Terra em metros
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||
@@ -97,10 +92,10 @@ async function validarLocalizacao(
|
||||
const avisos: string[] = [];
|
||||
let scoreConfianca = confiabilidadeGPS || 0.5;
|
||||
let valida = true;
|
||||
let distanciaIPvsGPS: number | undefined = undefined;
|
||||
let velocidadeUltimoRegistro: number | undefined = undefined;
|
||||
let distanciaUltimoRegistro: number | undefined = undefined;
|
||||
let tempoDecorridoHoras: number | undefined = undefined;
|
||||
let distanciaIPvsGPS: number | undefined;
|
||||
let velocidadeUltimoRegistro: number | undefined;
|
||||
let distanciaUltimoRegistro: number | undefined;
|
||||
let tempoDecorridoHoras: number | undefined;
|
||||
|
||||
// 1. Validar coordenadas básicas
|
||||
if (
|
||||
@@ -127,12 +122,7 @@ async function validarLocalizacao(
|
||||
if (ipAddress) {
|
||||
const ipGeo = await obterGeoPorIP(ipAddress);
|
||||
if (ipGeo) {
|
||||
distanciaIPvsGPS = calcularDistancia(
|
||||
latitude,
|
||||
longitude,
|
||||
ipGeo.latitude,
|
||||
ipGeo.longitude
|
||||
);
|
||||
distanciaIPvsGPS = calcularDistancia(latitude, longitude, ipGeo.latitude, ipGeo.longitude);
|
||||
|
||||
// Se diferença > 50km, muito suspeito
|
||||
if (distanciaIPvsGPS > 50000) {
|
||||
@@ -176,7 +166,7 @@ async function validarLocalizacao(
|
||||
|
||||
// Calcular velocidade (km/h) se tempo decorrido > 0
|
||||
if (tempoDecorridoHoras > 0 && tempoDecorridoHoras < 24) {
|
||||
velocidadeUltimoRegistro = (distanciaUltimoRegistro / 1000) / tempoDecorridoHoras; // km/h
|
||||
velocidadeUltimoRegistro = distanciaUltimoRegistro / 1000 / tempoDecorridoHoras; // km/h
|
||||
|
||||
// Se velocidade > 1000 km/h, impossível (mais rápido que avião)
|
||||
if (velocidadeUltimoRegistro > 1000) {
|
||||
@@ -280,19 +270,34 @@ function validarAcelerometro(
|
||||
// Se há dados de acelerômetro, validar
|
||||
if (acelerometroX !== undefined && acelerometroY !== undefined && acelerometroZ !== undefined) {
|
||||
// Verificar se valores são realistas (aceleração geralmente entre -20 e +20 m/s² em uso normal)
|
||||
const magnitude = magnitudeMovimento || Math.sqrt(acelerometroX * acelerometroX + acelerometroY * acelerometroY + acelerometroZ * acelerometroZ);
|
||||
|
||||
const magnitude =
|
||||
magnitudeMovimento ||
|
||||
Math.sqrt(
|
||||
acelerometroX * acelerometroX +
|
||||
acelerometroY * acelerometroY +
|
||||
acelerometroZ * acelerometroZ
|
||||
);
|
||||
|
||||
if (magnitude > 50) {
|
||||
// Aceleração muito alta pode indicar leitura errada ou emulador
|
||||
scoreConfianca *= 0.6;
|
||||
avisos.push(`Magnitude de movimento muito alta (${magnitude.toFixed(2)} m/s²). Pode indicar leitura incorreta.`);
|
||||
avisos.push(
|
||||
`Magnitude de movimento muito alta (${magnitude.toFixed(2)} m/s²). Pode indicar leitura incorreta.`
|
||||
);
|
||||
}
|
||||
|
||||
// Se não há movimento detectado quando deveria haver (em móvel), pode ser suspeito
|
||||
if (isDesktop !== true && movimentoDetectado === false && variacaoAcelerometro !== undefined && variacaoAcelerometro < 0.001) {
|
||||
if (
|
||||
isDesktop !== true &&
|
||||
movimentoDetectado === false &&
|
||||
variacaoAcelerometro !== undefined &&
|
||||
variacaoAcelerometro < 0.001
|
||||
) {
|
||||
// Variância muito baixa pode indicar que o dispositivo está parado ou emulador
|
||||
scoreConfianca *= 0.9;
|
||||
avisos.push('Nenhum movimento detectado durante o registro. Pode ser normal se o dispositivo estava parado.');
|
||||
avisos.push(
|
||||
'Nenhum movimento detectado durante o registro. Pode ser normal se o dispositivo estava parado.'
|
||||
);
|
||||
}
|
||||
|
||||
// Se há movimento, aumenta confiança
|
||||
@@ -320,7 +325,7 @@ export const generateUploadUrl = mutation({
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -434,21 +439,21 @@ export const registrarPonto = mutation({
|
||||
movimentoDetectado: v.boolean(),
|
||||
magnitude: v.number(),
|
||||
variacao: v.number(),
|
||||
timestamp: v.number(),
|
||||
timestamp: v.number()
|
||||
})
|
||||
),
|
||||
giroscopio: v.optional(
|
||||
v.object({
|
||||
alpha: v.number(),
|
||||
beta: v.number(),
|
||||
gamma: v.number(),
|
||||
gamma: v.number()
|
||||
})
|
||||
),
|
||||
)
|
||||
})
|
||||
),
|
||||
timestamp: v.number(),
|
||||
sincronizadoComServidor: v.boolean(),
|
||||
justificativa: v.optional(v.string()),
|
||||
justificativa: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -487,7 +492,7 @@ export const registrarPonto = mutation({
|
||||
const hora = dataObj.getUTCHours();
|
||||
const minuto = dataObj.getUTCMinutes();
|
||||
const segundo = dataObj.getUTCSeconds();
|
||||
|
||||
|
||||
// Obter data no formato YYYY-MM-DD usando UTC
|
||||
const ano = dataObj.getUTCFullYear();
|
||||
const mes = String(dataObj.getUTCMonth() + 1).padStart(2, '0');
|
||||
@@ -498,12 +503,12 @@ export const registrarPonto = mutation({
|
||||
const funcionarioId = usuario.funcionarioId; // Já verificado acima, não é undefined
|
||||
const registrosMinuto = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', funcionarioId).eq('data', data)
|
||||
)
|
||||
.collect();
|
||||
|
||||
const registroDuplicado = registrosMinuto.find(
|
||||
(r) => r.hora === hora && r.minuto === minuto
|
||||
);
|
||||
const registroDuplicado = registrosMinuto.find((r) => r.hora === hora && r.minuto === minuto);
|
||||
|
||||
if (registroDuplicado) {
|
||||
throw new Error('Já existe um registro neste minuto');
|
||||
@@ -553,7 +558,7 @@ export const registrarPonto = mutation({
|
||||
if (agora.getTime() > dataFimTimestamp && !dispensa.isento) {
|
||||
// Desativar dispensa expirada (mutation pode fazer isso)
|
||||
await ctx.db.patch(dispensa._id, {
|
||||
ativo: false,
|
||||
ativo: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -578,7 +583,12 @@ export const registrarPonto = mutation({
|
||||
break;
|
||||
}
|
||||
|
||||
const dentroDoPrazo = calcularStatusPonto(hora, minuto, horarioConfigurado, config.toleranciaMinutos);
|
||||
const dentroDoPrazo = calcularStatusPonto(
|
||||
hora,
|
||||
minuto,
|
||||
horarioConfigurado,
|
||||
config.toleranciaMinutos
|
||||
);
|
||||
|
||||
// Validar localização se fornecida e salvar informações detalhadas
|
||||
let validacaoLocalizacao: {
|
||||
@@ -592,10 +602,7 @@ export const registrarPonto = mutation({
|
||||
tempoDecorridoHoras?: number;
|
||||
} | null = null;
|
||||
|
||||
if (
|
||||
args.informacoesDispositivo?.latitude &&
|
||||
args.informacoesDispositivo?.longitude
|
||||
) {
|
||||
if (args.informacoesDispositivo?.latitude && args.informacoesDispositivo?.longitude) {
|
||||
validacaoLocalizacao = await validarLocalizacao(
|
||||
ctx,
|
||||
usuario.funcionarioId,
|
||||
@@ -612,16 +619,19 @@ export const registrarPonto = mutation({
|
||||
const baixaConfianca = validacaoLocalizacao.scoreConfianca < 0.5;
|
||||
|
||||
if (suspeitaFrontend || suspeitaBackend || baixaConfianca) {
|
||||
console.warn('⚠️ LOCALIZAÇÃO COM BAIXA CONFIABILIDADE DETECTADA (registrando normalmente):', {
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
latitude: args.informacoesDispositivo.latitude,
|
||||
longitude: args.informacoesDispositivo.longitude,
|
||||
confiabilidadeGPSFrontend: args.informacoesDispositivo.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: validacaoLocalizacao.scoreConfianca,
|
||||
suspeitaFrontend: suspeitaFrontend ? args.informacoesDispositivo.motivoSuspeita : null,
|
||||
suspeitaBackend: suspeitaBackend ? validacaoLocalizacao.motivo : null,
|
||||
avisos: validacaoLocalizacao.avisos
|
||||
});
|
||||
console.warn(
|
||||
'⚠️ LOCALIZAÇÃO COM BAIXA CONFIABILIDADE DETECTADA (registrando normalmente):',
|
||||
{
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
latitude: args.informacoesDispositivo.latitude,
|
||||
longitude: args.informacoesDispositivo.longitude,
|
||||
confiabilidadeGPSFrontend: args.informacoesDispositivo.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: validacaoLocalizacao.scoreConfianca,
|
||||
suspeitaFrontend: suspeitaFrontend ? args.informacoesDispositivo.motivoSuspeita : null,
|
||||
suspeitaBackend: suspeitaBackend ? validacaoLocalizacao.motivo : null,
|
||||
avisos: validacaoLocalizacao.avisos
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,17 +666,14 @@ export const registrarPonto = mutation({
|
||||
validacaoLocalizacao = {
|
||||
valida: true,
|
||||
scoreConfianca: 1,
|
||||
avisos: [],
|
||||
avisos: []
|
||||
};
|
||||
}
|
||||
validacaoLocalizacao.avisos.push(...geofencing.avisos);
|
||||
|
||||
|
||||
// Reduzir score de confiança se estiver fora do raio
|
||||
if (!geofencing.dentroRaio) {
|
||||
validacaoLocalizacao.scoreConfianca = Math.min(
|
||||
validacaoLocalizacao.scoreConfianca,
|
||||
0.7
|
||||
);
|
||||
validacaoLocalizacao.scoreConfianca = Math.min(validacaoLocalizacao.scoreConfianca, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,7 +704,8 @@ export const registrarPonto = mutation({
|
||||
let scoreFinalConfianca = 1.0;
|
||||
if (validacaoLocalizacao && validacaoAcelerometro) {
|
||||
// GPS tem peso 0.7, acelerômetro tem peso 0.3
|
||||
scoreFinalConfianca = (validacaoLocalizacao.scoreConfianca * 0.7) + (validacaoAcelerometro.scoreConfianca * 0.3);
|
||||
scoreFinalConfianca =
|
||||
validacaoLocalizacao.scoreConfianca * 0.7 + validacaoAcelerometro.scoreConfianca * 0.3;
|
||||
} else if (validacaoLocalizacao) {
|
||||
scoreFinalConfianca = validacaoLocalizacao.scoreConfianca;
|
||||
} else if (validacaoAcelerometro) {
|
||||
@@ -738,8 +746,19 @@ export const registrarPonto = mutation({
|
||||
speed: args.informacoesDispositivo?.speed,
|
||||
confiabilidadeGPS: args.informacoesDispositivo?.confiabilidadeGPS,
|
||||
scoreConfiancaBackend: scoreFinalConfianca,
|
||||
suspeitaSpoofing: args.informacoesDispositivo?.suspeitaSpoofing || (validacaoLocalizacao ? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida : undefined) || (validacaoAcelerometro ? validacaoAcelerometro.scoreConfianca < 0.5 || !validacaoAcelerometro.valida : undefined),
|
||||
motivoSuspeita: args.informacoesDispositivo?.motivoSuspeita || validacaoLocalizacao?.motivo || validacaoAcelerometro?.motivo || (todosAvisos.length > 0 ? todosAvisos.join('; ') : undefined),
|
||||
suspeitaSpoofing:
|
||||
args.informacoesDispositivo?.suspeitaSpoofing ||
|
||||
(validacaoLocalizacao
|
||||
? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida
|
||||
: undefined) ||
|
||||
(validacaoAcelerometro
|
||||
? validacaoAcelerometro.scoreConfianca < 0.5 || !validacaoAcelerometro.valida
|
||||
: undefined),
|
||||
motivoSuspeita:
|
||||
args.informacoesDispositivo?.motivoSuspeita ||
|
||||
validacaoLocalizacao?.motivo ||
|
||||
validacaoAcelerometro?.motivo ||
|
||||
(todosAvisos.length > 0 ? todosAvisos.join('; ') : undefined),
|
||||
// Informações detalhadas de validação (sempre salvar quando houver validação)
|
||||
avisosValidacao: todosAvisos.length > 0 ? todosAvisos : undefined,
|
||||
// Informações de Geofencing
|
||||
@@ -775,14 +794,14 @@ export const registrarPonto = mutation({
|
||||
giroscopioGamma: args.informacoesDispositivo?.giroscopio?.gamma,
|
||||
sensorDisponivel: args.informacoesDispositivo?.sensorDisponivel,
|
||||
permissaoSensorNegada: args.informacoesDispositivo?.permissaoNegada,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar banco de horas após registrar
|
||||
await atualizarBancoHoras(ctx, usuario.funcionarioId, data, config);
|
||||
|
||||
return { registroId, tipo, dentroDoPrazo };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -791,7 +810,7 @@ export const registrarPonto = mutation({
|
||||
export const listarRegistrosDia = query({
|
||||
args: {
|
||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
_refresh: v.optional(v.number()) // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -802,24 +821,33 @@ export const listarRegistrosDia = query({
|
||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||
|
||||
console.log('[listarRegistrosDia] Buscando registros:', { funcionarioId, data });
|
||||
console.log('[listarRegistrosDia] Buscando registros:', {
|
||||
funcionarioId,
|
||||
data
|
||||
});
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', funcionarioId).eq('data', data)
|
||||
)
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
console.log('[listarRegistrosDia] Registros encontrados:', registros.length, registros.map(r => ({
|
||||
_id: r._id,
|
||||
tipo: r.tipo,
|
||||
data: r.data,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto
|
||||
})));
|
||||
console.log(
|
||||
'[listarRegistrosDia] Registros encontrados:',
|
||||
registros.length,
|
||||
registros.map((r) => ({
|
||||
_id: r._id,
|
||||
tipo: r.tipo,
|
||||
data: r.data,
|
||||
hora: r.hora,
|
||||
minuto: r.minuto
|
||||
}))
|
||||
);
|
||||
|
||||
return registros;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -828,7 +856,7 @@ export const listarRegistrosDia = query({
|
||||
export const obterSaldoDiario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
data: v.string() // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar banco de horas do dia
|
||||
@@ -844,7 +872,7 @@ export const obterSaldoDiario = query({
|
||||
saldoMinutos: 0,
|
||||
horas: 0,
|
||||
minutos: 0,
|
||||
positivo: true,
|
||||
positivo: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -856,9 +884,9 @@ export const obterSaldoDiario = query({
|
||||
saldoMinutos: bancoHoras.saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
positivo
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -868,7 +896,7 @@ export const listarRegistrosPeriodo = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string() // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -890,9 +918,9 @@ export const listarRegistrosPeriodo = query({
|
||||
// Validar formato YYYY-MM-DD
|
||||
const dataInicioRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dataInicioRegex.test(args.dataInicio) || !dataInicioRegex.test(args.dataFim)) {
|
||||
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim
|
||||
console.warn('[listarRegistrosPeriodo] Formato de data inválido', {
|
||||
dataInicio: args.dataInicio,
|
||||
dataFim: args.dataFim
|
||||
});
|
||||
return [];
|
||||
}
|
||||
@@ -905,18 +933,18 @@ export const listarRegistrosPeriodo = query({
|
||||
});
|
||||
|
||||
let registrosFiltrados;
|
||||
|
||||
|
||||
// Se funcionário foi especificado, usar índice por funcionário e data (mais eficiente)
|
||||
if (args.funcionarioId) {
|
||||
// Garantir que funcionarioId não é undefined para TypeScript
|
||||
const funcionarioId = args.funcionarioId;
|
||||
|
||||
|
||||
// Buscar todos os registros do funcionário
|
||||
const todosRegistrosFuncionario = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId))
|
||||
.collect();
|
||||
|
||||
|
||||
// Filtrar por período de data usando comparação de strings (formato YYYY-MM-DD)
|
||||
registrosFiltrados = todosRegistrosFuncionario.filter((r) => {
|
||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||
@@ -928,47 +956,47 @@ export const listarRegistrosPeriodo = query({
|
||||
// Tentar usar índice por data primeiro
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_data', (q) =>
|
||||
q.gte('data', args.dataInicio).lte('data', args.dataFim)
|
||||
)
|
||||
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
||||
.collect();
|
||||
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros do índice by_data:', registros.length);
|
||||
|
||||
|
||||
// Garantir que as datas estão no formato correto e filtrar novamente para garantir
|
||||
registrosFiltrados = registros.filter((r) => {
|
||||
// Comparação de strings funciona para formato YYYY-MM-DD
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros após filtro:', registrosFiltrados.length);
|
||||
} catch (error) {
|
||||
console.error('[listarRegistrosPeriodo] Erro ao buscar registros:', error);
|
||||
// Fallback: buscar todos e filtrar manualmente
|
||||
const todosRegistros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.collect();
|
||||
|
||||
const todosRegistros = await ctx.db.query('registrosPonto').collect();
|
||||
|
||||
registrosFiltrados = todosRegistros.filter((r) => {
|
||||
return r.data >= args.dataInicio && r.data <= args.dataFim;
|
||||
});
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Fallback - registros encontrados:', registrosFiltrados.length);
|
||||
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Fallback - registros encontrados:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:', registrosFiltrados.length);
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Registros encontrados antes de buscar funcionários:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
|
||||
// Buscar informações dos funcionários
|
||||
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
||||
const funcionarios = await Promise.all(
|
||||
Array.from(funcionariosIds).map((id) => ctx.db.get(id))
|
||||
);
|
||||
const funcionarios = await Promise.all(Array.from(funcionariosIds).map((id) => ctx.db.get(id)));
|
||||
|
||||
// Buscar saldos diários para cada data/funcionário
|
||||
const saldosPorDataFuncionario: Record<string, number> = {};
|
||||
const datasUnicas = new Set(registrosFiltrados.map((r) => `${r.funcionarioId}-${r.data}`));
|
||||
|
||||
|
||||
for (const chave of datasUnicas) {
|
||||
const [funcId, data] = chave.split('-');
|
||||
const bancoHoras = await ctx.db
|
||||
@@ -977,13 +1005,16 @@ export const listarRegistrosPeriodo = query({
|
||||
q.eq('funcionarioId', funcId as Id<'funcionarios'>).eq('data', data)
|
||||
)
|
||||
.first();
|
||||
|
||||
|
||||
if (bancoHoras) {
|
||||
saldosPorDataFuncionario[chave] = bancoHoras.saldoMinutos;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[listarRegistrosPeriodo] Total de registros a retornar:', registrosFiltrados.length);
|
||||
console.log(
|
||||
'[listarRegistrosPeriodo] Total de registros a retornar:',
|
||||
registrosFiltrados.length
|
||||
);
|
||||
|
||||
return registrosFiltrados.map((registro) => {
|
||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||
@@ -999,18 +1030,18 @@ export const listarRegistrosPeriodo = query({
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
descricaoCargo: funcionario.descricaoCargo,
|
||||
descricaoCargo: funcionario.descricaoCargo
|
||||
}
|
||||
: null,
|
||||
saldoDiario: {
|
||||
saldoMinutos,
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
positivo
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1020,7 +1051,7 @@ export const obterEstatisticas = query({
|
||||
args: {
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioId: v.optional(v.id('funcionarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1032,7 +1063,7 @@ export const obterEstatisticas = query({
|
||||
foraDoPrazo: 0,
|
||||
totalFuncionarios: 0,
|
||||
funcionariosDentroPrazo: 0,
|
||||
funcionariosForaPrazo: 0,
|
||||
funcionariosForaPrazo: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1070,9 +1101,9 @@ export const obterEstatisticas = query({
|
||||
foraDoPrazo,
|
||||
totalFuncionarios,
|
||||
funcionariosDentroPrazo,
|
||||
funcionariosForaPrazo,
|
||||
funcionariosForaPrazo
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1080,7 +1111,7 @@ export const obterEstatisticas = query({
|
||||
*/
|
||||
export const obterRegistro = query({
|
||||
args: {
|
||||
registroId: v.id('registrosPonto'),
|
||||
registroId: v.id('registrosPonto')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1122,13 +1153,13 @@ export const obterRegistro = query({
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
tipo: simbolo.tipo,
|
||||
tipo: simbolo.tipo
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1142,7 +1173,9 @@ function calcularCargaHorariaDiaria(config: {
|
||||
}): number {
|
||||
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco
|
||||
.split(':')
|
||||
.map(Number);
|
||||
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
@@ -1160,11 +1193,13 @@ function calcularCargaHorariaDiaria(config: {
|
||||
/**
|
||||
* Calcula horas trabalhadas do dia baseado nos registros
|
||||
*/
|
||||
function calcularHorasTrabalhadas(registros: Array<{
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
}>): number {
|
||||
function calcularHorasTrabalhadas(
|
||||
registros: Array<{
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
}>
|
||||
): number {
|
||||
// Ordenar registros por timestamp
|
||||
const registrosOrdenados = [...registros].sort((a, b) => {
|
||||
const minutosA = a.hora * 60 + a.minuto;
|
||||
@@ -1247,7 +1282,7 @@ async function atualizarBancoHoras(
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
} else {
|
||||
// Criar novo
|
||||
@@ -1258,7 +1293,7 @@ async function atualizarBancoHoras(
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1270,7 +1305,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
_refresh: v.optional(v.number()), // Parâmetro usado pelo frontend para forçar refresh
|
||||
_refresh: v.optional(v.number()) // Parâmetro usado pelo frontend para forçar refresh
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1281,7 +1316,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
saldoMinutos: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1298,7 +1333,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
)
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
|
||||
console.log('[obterHistoricoESaldoDia] Registros encontrados:', registros.length, {
|
||||
funcionarioId: args.funcionarioId,
|
||||
data: args.data
|
||||
@@ -1315,7 +1350,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
saldoMinutos: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1336,10 +1371,10 @@ export const obterHistoricoESaldoDia = query({
|
||||
saldoFormatado: {
|
||||
horas,
|
||||
minutos,
|
||||
positivo,
|
||||
},
|
||||
positivo
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1347,7 +1382,7 @@ export const obterHistoricoESaldoDia = query({
|
||||
*/
|
||||
export const obterBancoHorasFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1373,9 +1408,9 @@ export const obterBancoHorasFuncionario = query({
|
||||
return {
|
||||
bancosHoras,
|
||||
saldoAcumuladoMinutos,
|
||||
totalDias: bancosHoras.length,
|
||||
totalDias: bancosHoras.length
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1411,7 +1446,7 @@ export const editarRegistroPonto = mutation({
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1439,7 +1474,7 @@ export const editarRegistroPonto = mutation({
|
||||
await ctx.db.patch(args.registroId, {
|
||||
hora: args.horaNova,
|
||||
minuto: args.minutoNova,
|
||||
editadoPorGestor: true,
|
||||
editadoPorGestor: true
|
||||
});
|
||||
|
||||
// Criar registro de homologação
|
||||
@@ -1455,12 +1490,12 @@ export const editarRegistroPonto = mutation({
|
||||
motivoTipo: args.motivoTipo,
|
||||
motivoDescricao: args.motivoDescricao,
|
||||
observacoes: args.observacoes,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar registro com ID da homologação
|
||||
await ctx.db.patch(args.registroId, {
|
||||
homologacaoId,
|
||||
homologacaoId
|
||||
});
|
||||
|
||||
// Recalcular banco de horas do dia
|
||||
@@ -1474,7 +1509,7 @@ export const editarRegistroPonto = mutation({
|
||||
}
|
||||
|
||||
return { success: true, homologacaoId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1490,7 +1525,7 @@ export const ajustarBancoHoras = mutation({
|
||||
motivoId: v.optional(v.string()),
|
||||
motivoTipo: v.optional(v.string()),
|
||||
motivoDescricao: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string()),
|
||||
observacoes: v.optional(v.string())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1505,8 +1540,7 @@ export const ajustarBancoHoras = mutation({
|
||||
}
|
||||
|
||||
// Calcular ajuste em minutos
|
||||
const ajusteMinutos =
|
||||
args.periodoDias * 24 * 60 + args.periodoHoras * 60 + args.periodoMinutos;
|
||||
const ajusteMinutos = args.periodoDias * 24 * 60 + args.periodoHoras * 60 + args.periodoMinutos;
|
||||
|
||||
// Aplicar sinal baseado no tipo de ajuste
|
||||
let ajusteFinal = ajusteMinutos;
|
||||
@@ -1526,7 +1560,7 @@ export const ajustarBancoHoras = mutation({
|
||||
if (bancoHorasAtual) {
|
||||
// Atualizar saldo do dia atual
|
||||
await ctx.db.patch(bancoHorasAtual._id, {
|
||||
saldoMinutos: bancoHorasAtual.saldoMinutos + ajusteFinal,
|
||||
saldoMinutos: bancoHorasAtual.saldoMinutos + ajusteFinal
|
||||
});
|
||||
} else {
|
||||
// Criar novo registro de banco de horas para o ajuste
|
||||
@@ -1548,7 +1582,7 @@ export const ajustarBancoHoras = mutation({
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: ajusteFinal,
|
||||
registrosPontoIds: [],
|
||||
calculadoEm: Date.now(),
|
||||
calculadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1565,11 +1599,11 @@ export const ajustarBancoHoras = mutation({
|
||||
periodoHoras: args.periodoHoras,
|
||||
periodoMinutos: args.periodoMinutos,
|
||||
ajusteMinutos: ajusteFinal,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, homologacaoId, ajusteMinutos: ajusteFinal };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1577,7 +1611,7 @@ export const ajustarBancoHoras = mutation({
|
||||
*/
|
||||
export const listarHomologacoes = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
funcionarioId: v.optional(v.id('funcionarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1623,26 +1657,26 @@ export const listarHomologacoes = query({
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
matricula: funcionario.matricula
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
nome: gestor.nome
|
||||
}
|
||||
: null,
|
||||
registro: registro
|
||||
? {
|
||||
data: registro.data,
|
||||
tipo: registro.tipo,
|
||||
tipo: registro.tipo
|
||||
}
|
||||
: null,
|
||||
: null
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return homologacoesComDetalhes;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1650,7 +1684,7 @@ export const listarHomologacoes = query({
|
||||
*/
|
||||
export const excluirHomologacao = mutation({
|
||||
args: {
|
||||
homologacaoId: v.id('homologacoesPonto'),
|
||||
homologacaoId: v.id('homologacoesPonto')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1664,7 +1698,11 @@ export const excluirHomologacao = mutation({
|
||||
}
|
||||
|
||||
// Verificar se é gestor do funcionário
|
||||
const isGestor = await verificarGestorDoFuncionario(ctx, usuario._id, homologacao.funcionarioId);
|
||||
const isGestor = await verificarGestorDoFuncionario(
|
||||
ctx,
|
||||
usuario._id,
|
||||
homologacao.funcionarioId
|
||||
);
|
||||
if (!isGestor && homologacao.gestorId !== usuario._id) {
|
||||
throw new Error('Você não tem permissão para excluir esta homologação');
|
||||
}
|
||||
@@ -1675,7 +1713,7 @@ export const excluirHomologacao = mutation({
|
||||
if (registro && registro.homologacaoId === args.homologacaoId) {
|
||||
await ctx.db.patch(homologacao.registroId, {
|
||||
homologacaoId: undefined,
|
||||
editadoPorGestor: false,
|
||||
editadoPorGestor: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1684,7 +1722,7 @@ export const excluirHomologacao = mutation({
|
||||
await ctx.db.delete(args.homologacaoId);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1710,10 +1748,10 @@ export const obterMotivosAtestados = query({
|
||||
'Ajuste Administrativo',
|
||||
'Compensação de Horas',
|
||||
'Abono',
|
||||
'Desconto em Folha',
|
||||
],
|
||||
'Desconto em Folha'
|
||||
]
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1729,7 +1767,7 @@ export const criarDispensaRegistro = mutation({
|
||||
horaFim: v.number(),
|
||||
minutoFim: v.number(),
|
||||
motivo: v.string(),
|
||||
isento: v.boolean(),
|
||||
isento: v.boolean()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1764,11 +1802,11 @@ export const criarDispensaRegistro = mutation({
|
||||
motivo: args.motivo,
|
||||
isento: args.isento,
|
||||
ativo: true,
|
||||
criadoEm: Date.now(),
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return { success: true, dispensaId };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1776,7 +1814,7 @@ export const criarDispensaRegistro = mutation({
|
||||
*/
|
||||
export const removerDispensaRegistro = mutation({
|
||||
args: {
|
||||
dispensaId: v.id('dispensasRegistro'),
|
||||
dispensaId: v.id('dispensasRegistro')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1797,11 +1835,11 @@ export const removerDispensaRegistro = mutation({
|
||||
|
||||
// Desativar dispensa
|
||||
await ctx.db.patch(args.dispensaId, {
|
||||
ativo: false,
|
||||
ativo: false
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1810,7 +1848,7 @@ export const removerDispensaRegistro = mutation({
|
||||
export const listarDispensas = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
apenasAtivas: v.optional(v.boolean()),
|
||||
apenasAtivas: v.optional(v.boolean())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
@@ -1877,21 +1915,21 @@ export const listarDispensas = query({
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
matricula: funcionario.matricula
|
||||
}
|
||||
: null,
|
||||
gestor: gestor
|
||||
? {
|
||||
nome: gestor.nome,
|
||||
nome: gestor.nome
|
||||
}
|
||||
: null,
|
||||
expirada,
|
||||
expirada
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return dispensasComDetalhes;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1902,7 +1940,7 @@ export const verificarDispensaAtiva = query({
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
hora: v.optional(v.number()),
|
||||
minuto: v.optional(v.number()),
|
||||
minuto: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const dispensas = await ctx.db
|
||||
@@ -1919,7 +1957,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: 'Isento de registro (caso excepcional)',
|
||||
motivo: 'Isento de registro (caso excepcional)'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1945,7 +1983,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
motivo: dispensa.motivo
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@@ -1953,7 +1991,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: true,
|
||||
dispensa,
|
||||
motivo: dispensa.motivo,
|
||||
motivo: dispensa.motivo
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1962,8 +2000,7 @@ export const verificarDispensaAtiva = query({
|
||||
return {
|
||||
dispensado: false,
|
||||
dispensa: null,
|
||||
motivo: null,
|
||||
motivo: null
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, internalMutation, internalQuery } from './_generated/server';
|
||||
import { internal, api } from './_generated/api';
|
||||
import { api, internal } from './_generated/api';
|
||||
import { internalMutation, internalQuery, mutation } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v } from 'convex/values';
|
||||
import { internalMutation, query, mutation } from './_generated/server';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { v } from "convex/values";
|
||||
import { query } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import type { QueryCtx } from "./_generated/server";
|
||||
import { v } from 'convex/values';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { QueryCtx } from './_generated/server';
|
||||
import { query } from './_generated/server';
|
||||
|
||||
/**
|
||||
* SISTEMA DE CÁLCULO DE SALDO DE FÉRIAS
|
||||
* Suporte a múltiplos regimes de trabalho
|
||||
*
|
||||
*
|
||||
* ============================================
|
||||
* REGRAS CLT (Consolidação das Leis do Trabalho):
|
||||
* ============================================
|
||||
@@ -17,7 +17,7 @@ import type { QueryCtx } from "./_generated/server";
|
||||
* - Um período deve ter no mínimo 14 dias
|
||||
* - Demais períodos: mínimo 5 dias cada
|
||||
* - Abono pecuniário: vender 1/3 das férias (10 dias) - OPCIONAL
|
||||
*
|
||||
*
|
||||
* ============================================
|
||||
* REGRAS SERVIDOR PÚBLICO ESTADUAL DE PERNAMBUCO
|
||||
* Lei nº 6.123/1968 - Estatuto dos Funcionários Públicos Civis do Estado de PE
|
||||
@@ -37,448 +37,444 @@ import type { QueryCtx } from "./_generated/server";
|
||||
* - Seguem as mesmas diretrizes do regime estadual acima
|
||||
*/
|
||||
|
||||
type RegimeTrabalho = "clt" | "estatutario_pe" | "estatutario_federal" | "estatutario_municipal";
|
||||
type RegimeTrabalho = 'clt' | 'estatutario_pe' | 'estatutario_federal' | 'estatutario_municipal';
|
||||
|
||||
// Configurações por regime
|
||||
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: 15, // Mínimo 15 dias por período
|
||||
minDiasPeriodoPrincipal: null, // Não há essa regra
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
periodosPermitidos: [15, 30], // Apenas 15 ou 30 dias por período
|
||||
},
|
||||
estatutario_federal: {
|
||||
nome: "Servidor Público Federal",
|
||||
maxPeriodos: 3,
|
||||
minDiasPeriodo: 5,
|
||||
minDiasPeriodoPrincipal: 14,
|
||||
abonoPermitido: true,
|
||||
maxDiasAbono: 10,
|
||||
},
|
||||
estatutario_municipal: {
|
||||
nome: "Servidor Público Municipal",
|
||||
maxPeriodos: 2,
|
||||
minDiasPeriodo: 15, // Mínimo 15 dias por período
|
||||
minDiasPeriodoPrincipal: null,
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
periodosPermitidos: [15, 30], // Apenas 15 ou 30 dias por período
|
||||
},
|
||||
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: 15, // Mínimo 15 dias por período
|
||||
minDiasPeriodoPrincipal: null, // Não há essa regra
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
periodosPermitidos: [15, 30] // Apenas 15 ou 30 dias por período
|
||||
},
|
||||
estatutario_federal: {
|
||||
nome: 'Servidor Público Federal',
|
||||
maxPeriodos: 3,
|
||||
minDiasPeriodo: 5,
|
||||
minDiasPeriodoPrincipal: 14,
|
||||
abonoPermitido: true,
|
||||
maxDiasAbono: 10
|
||||
},
|
||||
estatutario_municipal: {
|
||||
nome: 'Servidor Público Municipal',
|
||||
maxPeriodos: 2,
|
||||
minDiasPeriodo: 15, // Mínimo 15 dias por período
|
||||
minDiasPeriodoPrincipal: null,
|
||||
abonoPermitido: false,
|
||||
maxDiasAbono: 0,
|
||||
periodosPermitidos: [15, 30] // Apenas 15 ou 30 dias por período
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Calcular dias entre duas datas
|
||||
function calcularDiasEntreDatas(dataInicio: string, dataFim: string): number {
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 para incluir ambos os dias
|
||||
return diffDays;
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 para incluir ambos os dias
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
// Helper: Calcular data de fim do período aquisitivo
|
||||
function calcularDataFimPeriodo(dataAdmissao: string, anosPassados: number): string {
|
||||
const dataInicio = new Date(dataAdmissao);
|
||||
dataInicio.setFullYear(dataInicio.getFullYear() + anosPassados);
|
||||
return dataInicio.toISOString().split('T')[0];
|
||||
const dataInicio = new Date(dataAdmissao);
|
||||
dataInicio.setFullYear(dataInicio.getFullYear() + anosPassados);
|
||||
return dataInicio.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// Helper: Obter regime de trabalho do funcionário
|
||||
async function obterRegimeTrabalho(ctx: QueryCtx, funcionarioId: Id<"funcionarios">): Promise<RegimeTrabalho> {
|
||||
const funcionario = await ctx.db.get(funcionarioId);
|
||||
return funcionario?.regimeTrabalho || "clt"; // Default CLT
|
||||
async function obterRegimeTrabalho(
|
||||
ctx: QueryCtx,
|
||||
funcionarioId: Id<'funcionarios'>
|
||||
): Promise<RegimeTrabalho> {
|
||||
const funcionario = await ctx.db.get(funcionarioId);
|
||||
return funcionario?.regimeTrabalho || 'clt'; // Default CLT
|
||||
}
|
||||
|
||||
// Helper: Calcular saldo dinamicamente baseado na tabela ferias
|
||||
async function calcularSaldo(
|
||||
ctx: QueryCtx,
|
||||
funcionarioId: Id<"funcionarios">,
|
||||
anoReferencia: number,
|
||||
feriasIdExcluir?: Id<"ferias"> // ID do período a excluir do cálculo (para ajustes)
|
||||
ctx: QueryCtx,
|
||||
funcionarioId: Id<'funcionarios'>,
|
||||
anoReferencia: number,
|
||||
feriasIdExcluir?: Id<'ferias'> // ID do período a excluir do cálculo (para ajustes)
|
||||
): Promise<{
|
||||
diasDireito: number;
|
||||
diasUsados: number;
|
||||
diasPendentes: number;
|
||||
diasDisponiveis: number;
|
||||
diasAbono: number;
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
status: "ativo" | "vencido" | "concluido";
|
||||
diasDireito: number;
|
||||
diasUsados: number;
|
||||
diasPendentes: number;
|
||||
diasDisponiveis: number;
|
||||
diasAbono: number;
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
status: 'ativo' | 'vencido' | 'concluido';
|
||||
} | null> {
|
||||
const funcionario = await ctx.db.get(funcionarioId);
|
||||
if (!funcionario || !funcionario.admissaoData) return null;
|
||||
const funcionario = await ctx.db.get(funcionarioId);
|
||||
if (!funcionario || !funcionario.admissaoData) return null;
|
||||
|
||||
const regime = funcionario.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
const regime = funcionario.regimeTrabalho || 'clt';
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
// Calcular anos desde admissão
|
||||
const dataAdmissao = new Date(funcionario.admissaoData);
|
||||
const anosDesdeAdmissao = anoReferencia - dataAdmissao.getFullYear();
|
||||
// Calcular anos desde admissão
|
||||
const dataAdmissao = new Date(funcionario.admissaoData);
|
||||
const anosDesdeAdmissao = anoReferencia - dataAdmissao.getFullYear();
|
||||
|
||||
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
|
||||
if (anosDesdeAdmissao < 1) return null; // Ainda não tem direito
|
||||
|
||||
const dataInicio = calcularDataFimPeriodo(
|
||||
funcionario.admissaoData,
|
||||
anosDesdeAdmissao - 1
|
||||
);
|
||||
const dataFim = calcularDataFimPeriodo(
|
||||
funcionario.admissaoData,
|
||||
anosDesdeAdmissao
|
||||
);
|
||||
const dataInicio = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao - 1);
|
||||
const dataFim = calcularDataFimPeriodo(funcionario.admissaoData, anosDesdeAdmissao);
|
||||
|
||||
// Buscar todos os registros de férias para este funcionário e ano
|
||||
const todasFerias = await ctx.db
|
||||
.query("ferias")
|
||||
.withIndex("by_funcionario_and_ano", (q) =>
|
||||
q.eq("funcionarioId", funcionarioId).eq("anoReferencia", anoReferencia)
|
||||
)
|
||||
.collect();
|
||||
// Buscar todos os registros de férias para este funcionário e ano
|
||||
const todasFerias = await ctx.db
|
||||
.query('ferias')
|
||||
.withIndex('by_funcionario_and_ano', (q) =>
|
||||
q.eq('funcionarioId', funcionarioId).eq('anoReferencia', anoReferencia)
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Filtrar períodos a excluir (para ajustes)
|
||||
const feriasFiltradas = feriasIdExcluir
|
||||
? todasFerias.filter((f) => f._id !== feriasIdExcluir)
|
||||
: todasFerias;
|
||||
// Filtrar períodos a excluir (para ajustes)
|
||||
const feriasFiltradas = feriasIdExcluir
|
||||
? todasFerias.filter((f) => f._id !== feriasIdExcluir)
|
||||
: todasFerias;
|
||||
|
||||
// Calcular dias usados (aprovado, data_ajustada_aprovada, EmFérias)
|
||||
const diasUsados = feriasFiltradas
|
||||
.filter(
|
||||
(f) =>
|
||||
f.status === "aprovado" ||
|
||||
f.status === "data_ajustada_aprovada" ||
|
||||
f.status === "EmFérias"
|
||||
)
|
||||
.reduce((acc, f) => acc + f.diasFerias, 0);
|
||||
// Calcular dias usados (aprovado, data_ajustada_aprovada, EmFérias)
|
||||
const diasUsados = feriasFiltradas
|
||||
.filter(
|
||||
(f) =>
|
||||
f.status === 'aprovado' || f.status === 'data_ajustada_aprovada' || f.status === 'EmFérias'
|
||||
)
|
||||
.reduce((acc, f) => acc + f.diasFerias, 0);
|
||||
|
||||
// Calcular dias pendentes (aguardando_aprovacao)
|
||||
const diasPendentes = feriasFiltradas
|
||||
.filter((f) => f.status === "aguardando_aprovacao")
|
||||
.reduce((acc, f) => acc + f.diasFerias, 0);
|
||||
// Calcular dias pendentes (aguardando_aprovacao)
|
||||
const diasPendentes = feriasFiltradas
|
||||
.filter((f) => f.status === 'aguardando_aprovacao')
|
||||
.reduce((acc, f) => acc + f.diasFerias, 0);
|
||||
|
||||
// Calcular dias de abono
|
||||
const diasAbono = feriasFiltradas.reduce((acc, f) => acc + f.diasAbono, 0);
|
||||
// Calcular dias de abono
|
||||
const diasAbono = feriasFiltradas.reduce((acc, f) => acc + f.diasAbono, 0);
|
||||
|
||||
// Calcular dias disponíveis
|
||||
const diasDireito = 30;
|
||||
const diasDisponiveis = diasDireito - diasUsados - diasPendentes - diasAbono;
|
||||
// Calcular dias disponíveis
|
||||
const diasDireito = 30;
|
||||
const diasDisponiveis = diasDireito - diasUsados - diasPendentes - diasAbono;
|
||||
|
||||
// Determinar status do período
|
||||
const hoje = new Date();
|
||||
const dataFimPeriodo = new Date(dataFim);
|
||||
let status: "ativo" | "vencido" | "concluido";
|
||||
|
||||
if (diasDireito - diasUsados - diasAbono <= 0) {
|
||||
status = "concluido";
|
||||
} else if (hoje > dataFimPeriodo) {
|
||||
status = "vencido";
|
||||
} else {
|
||||
status = "ativo";
|
||||
}
|
||||
// Determinar status do período
|
||||
const hoje = new Date();
|
||||
const dataFimPeriodo = new Date(dataFim);
|
||||
let status: 'ativo' | 'vencido' | 'concluido';
|
||||
|
||||
return {
|
||||
diasDireito,
|
||||
diasUsados,
|
||||
diasPendentes,
|
||||
diasDisponiveis,
|
||||
diasAbono,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
status,
|
||||
};
|
||||
if (diasDireito - diasUsados - diasAbono <= 0) {
|
||||
status = 'concluido';
|
||||
} else if (hoje > dataFimPeriodo) {
|
||||
status = 'vencido';
|
||||
} else {
|
||||
status = 'ativo';
|
||||
}
|
||||
|
||||
return {
|
||||
diasDireito,
|
||||
diasUsados,
|
||||
diasPendentes,
|
||||
diasDisponiveis,
|
||||
diasAbono,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Query: Obter saldo de férias de um funcionário para um ano específico
|
||||
*/
|
||||
export const obterSaldo = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal("ativo"), v.literal("vencido"), v.literal("concluido")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
regimeTrabalho: v.string(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia);
|
||||
if (!saldo) return null;
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
anoReferencia: v.number()
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal('ativo'), v.literal('vencido'), v.literal('concluido')),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
regimeTrabalho: v.string()
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia);
|
||||
if (!saldo) return null;
|
||||
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
const regime = funcionario?.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
const regime = funcionario?.regimeTrabalho || 'clt';
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
return {
|
||||
anoReferencia: args.anoReferencia,
|
||||
...saldo,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
regimeTrabalho: config.nome,
|
||||
};
|
||||
},
|
||||
return {
|
||||
anoReferencia: args.anoReferencia,
|
||||
...saldo,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
regimeTrabalho: config.nome
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query: Listar todos os saldos de um funcionário
|
||||
*/
|
||||
export const listarSaldos = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal("ativo"), v.literal("vencido"), v.literal("concluido")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario || !funcionario.admissaoData) return [];
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
anoReferencia: v.number(),
|
||||
diasDireito: v.number(),
|
||||
diasUsados: v.number(),
|
||||
diasPendentes: v.number(),
|
||||
diasDisponiveis: v.number(),
|
||||
diasAbono: v.number(),
|
||||
abonoPermitido: v.boolean(),
|
||||
status: v.union(v.literal('ativo'), v.literal('vencido'), v.literal('concluido')),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||
if (!funcionario || !funcionario.admissaoData) return [];
|
||||
|
||||
const regime = funcionario.regimeTrabalho || "clt";
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
const regime = funcionario.regimeTrabalho || 'clt';
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
const dataAdmissao = new Date(funcionario.admissaoData);
|
||||
const anoAtual = new Date().getFullYear();
|
||||
const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
|
||||
const dataAdmissao = new Date(funcionario.admissaoData);
|
||||
const anoAtual = new Date().getFullYear();
|
||||
const anosDesdeAdmissao = anoAtual - dataAdmissao.getFullYear();
|
||||
|
||||
const saldos = [];
|
||||
const saldos = [];
|
||||
|
||||
// Calcular saldos para os últimos 3 anos (atual, anterior e anterior ao anterior)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ano = anoAtual - i;
|
||||
const anosPeriodo = ano - dataAdmissao.getFullYear();
|
||||
// Calcular saldos para os últimos 3 anos (atual, anterior e anterior ao anterior)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ano = anoAtual - i;
|
||||
const anosPeriodo = ano - dataAdmissao.getFullYear();
|
||||
|
||||
if (anosPeriodo < 1) continue;
|
||||
if (anosPeriodo < 1) continue;
|
||||
|
||||
const saldo = await calcularSaldo(ctx, args.funcionarioId, ano);
|
||||
if (saldo) {
|
||||
saldos.push({
|
||||
anoReferencia: ano,
|
||||
...saldo,
|
||||
abonoPermitido: config.abonoPermitido,
|
||||
});
|
||||
}
|
||||
}
|
||||
const saldo = await calcularSaldo(ctx, args.funcionarioId, ano);
|
||||
if (saldo) {
|
||||
saldos.push({
|
||||
anoReferencia: ano,
|
||||
...saldo,
|
||||
abonoPermitido: config.abonoPermitido
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return saldos;
|
||||
},
|
||||
return saldos;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query: Validar solicitação de férias (regras CLT ou Servidor Público PE)
|
||||
*/
|
||||
export const validarSolicitacao = query({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
anoReferencia: v.number(),
|
||||
periodos: v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
})
|
||||
),
|
||||
feriasIdExcluir: v.optional(v.id("ferias")), // ID do período a excluir do cálculo de saldo (para ajustes)
|
||||
},
|
||||
returns: v.object({
|
||||
valido: v.boolean(),
|
||||
erros: v.array(v.string()),
|
||||
avisos: v.array(v.string()),
|
||||
totalDias: v.number(),
|
||||
regimeTrabalho: v.string(),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const erros: string[] = [];
|
||||
const avisos: string[] = [];
|
||||
let totalDias = 0;
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
anoReferencia: v.number(),
|
||||
periodos: v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string()
|
||||
})
|
||||
),
|
||||
feriasIdExcluir: v.optional(v.id('ferias')) // ID do período a excluir do cálculo de saldo (para ajustes)
|
||||
},
|
||||
returns: v.object({
|
||||
valido: v.boolean(),
|
||||
erros: v.array(v.string()),
|
||||
avisos: v.array(v.string()),
|
||||
totalDias: v.number(),
|
||||
regimeTrabalho: v.string()
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const erros: string[] = [];
|
||||
const avisos: string[] = [];
|
||||
let totalDias = 0;
|
||||
|
||||
// Obter regime de trabalho
|
||||
const regime = await obterRegimeTrabalho(ctx, args.funcionarioId);
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
// Obter regime de trabalho
|
||||
const regime = await obterRegimeTrabalho(ctx, args.funcionarioId);
|
||||
const config = REGIMES_CONFIG[regime];
|
||||
|
||||
// Validação 1: Número de períodos
|
||||
if (args.periodos.length === 0) {
|
||||
erros.push("É necessário adicionar pelo menos 1 período de férias");
|
||||
}
|
||||
// Validação 1: Número de períodos
|
||||
if (args.periodos.length === 0) {
|
||||
erros.push('É necessário adicionar pelo menos 1 período de férias');
|
||||
}
|
||||
|
||||
if (args.periodos.length > config.maxPeriodos) {
|
||||
erros.push(
|
||||
`Máximo de ${config.maxPeriodos} períodos permitidos para ${config.nome}`
|
||||
);
|
||||
}
|
||||
if (args.periodos.length > config.maxPeriodos) {
|
||||
erros.push(`Máximo de ${config.maxPeriodos} períodos permitidos para ${config.nome}`);
|
||||
}
|
||||
|
||||
// Calcular dias de cada período e validar
|
||||
const diasPorPeriodo: number[] = [];
|
||||
for (const periodo of args.periodos) {
|
||||
const dias = calcularDiasEntreDatas(periodo.dataInicio, periodo.dataFim);
|
||||
diasPorPeriodo.push(dias);
|
||||
totalDias += dias;
|
||||
// Calcular dias de cada período e validar
|
||||
const diasPorPeriodo: number[] = [];
|
||||
for (const periodo of args.periodos) {
|
||||
const dias = calcularDiasEntreDatas(periodo.dataInicio, periodo.dataFim);
|
||||
diasPorPeriodo.push(dias);
|
||||
totalDias += dias;
|
||||
|
||||
// Validação 2: Mínimo de dias por período
|
||||
if (dias < config.minDiasPeriodo) {
|
||||
erros.push(
|
||||
`Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})`
|
||||
);
|
||||
}
|
||||
// Validação 2: Mínimo de dias por período
|
||||
if (dias < config.minDiasPeriodo) {
|
||||
erros.push(
|
||||
`Período de ${dias} dias é inválido. Mínimo: ${config.minDiasPeriodo} dias corridos (${config.nome})`
|
||||
);
|
||||
}
|
||||
|
||||
// Validação específica para regime estatutário PE e Municipal
|
||||
if ((regime === "estatutario_pe" || regime === "estatutario_municipal") && 'periodosPermitidos' in config) {
|
||||
if (!config.periodosPermitidos.includes(dias)) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, os períodos devem ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validação específica para regime estatutário PE e Municipal
|
||||
if (
|
||||
(regime === 'estatutario_pe' || regime === 'estatutario_municipal') &&
|
||||
'periodosPermitidos' in config
|
||||
) {
|
||||
if (!config.periodosPermitidos.includes(dias)) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, os períodos devem ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validação específica para regime estatutário PE e Municipal
|
||||
// Permite períodos fracionados: cada período deve ser 15 ou 30 dias
|
||||
// Total não pode exceder 30 dias, mas pode ser menos (períodos fracionados)
|
||||
if ((regime === "estatutario_pe" || regime === "estatutario_municipal")) {
|
||||
// Verificar se cada período individual é válido (15 ou 30 dias)
|
||||
for (const dias of diasPorPeriodo) {
|
||||
if (dias !== 15 && dias !== 30) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, cada período deve ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Validação específica para regime estatutário PE e Municipal
|
||||
// Permite períodos fracionados: cada período deve ser 15 ou 30 dias
|
||||
// Total não pode exceder 30 dias, mas pode ser menos (períodos fracionados)
|
||||
if (regime === 'estatutario_pe' || regime === 'estatutario_municipal') {
|
||||
// Verificar se cada período individual é válido (15 ou 30 dias)
|
||||
for (const dias of diasPorPeriodo) {
|
||||
if (dias !== 15 && dias !== 30) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, cada período deve ter exatamente 15 ou 30 dias. Período de ${dias} dias não é permitido.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Total não pode exceder 30 dias
|
||||
if (totalDias > 30) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, o total de dias não pode exceder 30 dias. Total solicitado: ${totalDias} dias.`
|
||||
);
|
||||
}
|
||||
// Total não pode exceder 30 dias
|
||||
if (totalDias > 30) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, o total de dias não pode exceder 30 dias. Total solicitado: ${totalDias} dias.`
|
||||
);
|
||||
}
|
||||
|
||||
// Máximo de 2 períodos
|
||||
if (args.periodos.length > 2) {
|
||||
erros.push(
|
||||
`Para ${config.nome}, o máximo de períodos permitidos é 2.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Máximo de 2 períodos
|
||||
if (args.periodos.length > 2) {
|
||||
erros.push(`Para ${config.nome}, o máximo de períodos permitidos é 2.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 3: CLT requer um período com 14+ dias se dividir
|
||||
if (regime === "clt" && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) {
|
||||
const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal!);
|
||||
if (!temPeriodo14Dias) {
|
||||
erros.push(
|
||||
`Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Validação 3: CLT requer um período com 14+ dias se dividir
|
||||
if (regime === 'clt' && args.periodos.length > 1 && config.minDiasPeriodoPrincipal) {
|
||||
const temPeriodo14Dias = diasPorPeriodo.some((d) => d >= config.minDiasPeriodoPrincipal!);
|
||||
if (!temPeriodo14Dias) {
|
||||
erros.push(
|
||||
`Ao dividir férias em CLT, um período deve ter no mínimo ${config.minDiasPeriodoPrincipal} dias corridos`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 4: Verificar saldo disponível (calculado dinamicamente)
|
||||
// Se for um ajuste (feriasIdExcluir fornecido), excluir esse período do cálculo
|
||||
const saldo = await calcularSaldo(ctx, args.funcionarioId, args.anoReferencia, args.feriasIdExcluir);
|
||||
// Validação 4: Verificar saldo disponível (calculado dinamicamente)
|
||||
// Se for um ajuste (feriasIdExcluir fornecido), excluir esse período do cálculo
|
||||
const saldo = await calcularSaldo(
|
||||
ctx,
|
||||
args.funcionarioId,
|
||||
args.anoReferencia,
|
||||
args.feriasIdExcluir
|
||||
);
|
||||
|
||||
if (!saldo) {
|
||||
erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`);
|
||||
} else {
|
||||
// Verificar saldo disponível (já excluindo o período original se for ajuste)
|
||||
if (totalDias > saldo.diasDisponiveis) {
|
||||
erros.push(
|
||||
`Total solicitado (${totalDias} dias) excede saldo disponível (${saldo.diasDisponiveis} dias)`
|
||||
);
|
||||
}
|
||||
if (!saldo) {
|
||||
erros.push(`Você ainda não tem direito a férias referentes ao ano ${args.anoReferencia}`);
|
||||
} else {
|
||||
// Verificar saldo disponível (já excluindo o período original se for ajuste)
|
||||
if (totalDias > saldo.diasDisponiveis) {
|
||||
erros.push(
|
||||
`Total solicitado (${totalDias} dias) excede saldo disponível (${saldo.diasDisponiveis} dias)`
|
||||
);
|
||||
}
|
||||
|
||||
// Aviso: Saldo baixo
|
||||
if (saldo.diasDisponiveis < 15 && saldo.diasDisponiveis > totalDias) {
|
||||
avisos.push(
|
||||
`Após essa solicitação, restará ${saldo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}`
|
||||
);
|
||||
}
|
||||
// Aviso: Saldo baixo
|
||||
if (saldo.diasDisponiveis < 15 && saldo.diasDisponiveis > totalDias) {
|
||||
avisos.push(
|
||||
`Após essa solicitação, restará ${saldo.diasDisponiveis - totalDias} dias de ${args.anoReferencia}`
|
||||
);
|
||||
}
|
||||
|
||||
// Aviso: Férias vencendo
|
||||
const hoje = new Date();
|
||||
const dataFim = new Date(saldo.dataFim);
|
||||
const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diasAteVencer < 90 && diasAteVencer > 0) {
|
||||
avisos.push(
|
||||
`⚠️ Atenção: Seu período aquisitivo ${args.anoReferencia} vence em ${diasAteVencer} dias!`
|
||||
);
|
||||
}
|
||||
// Aviso: Férias vencendo
|
||||
const hoje = new Date();
|
||||
const dataFim = new Date(saldo.dataFim);
|
||||
const diasAteVencer = Math.ceil((dataFim.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (diasAteVencer < 90 && diasAteVencer > 0) {
|
||||
avisos.push(
|
||||
`⚠️ Atenção: Seu período aquisitivo ${args.anoReferencia} vence em ${diasAteVencer} dias!`
|
||||
);
|
||||
}
|
||||
|
||||
if (diasAteVencer < 0) {
|
||||
avisos.push(
|
||||
`⚠️ URGENTE: Seu período aquisitivo ${args.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (diasAteVencer < 0) {
|
||||
avisos.push(
|
||||
`⚠️ URGENTE: Seu período aquisitivo ${args.anoReferencia} está VENCIDO há ${Math.abs(diasAteVencer)} dias!`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 5: Verificar conflitos de datas (sobreposição)
|
||||
for (let i = 0; i < args.periodos.length; i++) {
|
||||
for (let j = i + 1; j < args.periodos.length; j++) {
|
||||
const inicio1 = new Date(args.periodos[i].dataInicio);
|
||||
const fim1 = new Date(args.periodos[i].dataFim);
|
||||
const inicio2 = new Date(args.periodos[j].dataInicio);
|
||||
const fim2 = new Date(args.periodos[j].dataFim);
|
||||
// Validação 5: Verificar conflitos de datas (sobreposição)
|
||||
for (let i = 0; i < args.periodos.length; i++) {
|
||||
for (let j = i + 1; j < args.periodos.length; j++) {
|
||||
const inicio1 = new Date(args.periodos[i].dataInicio);
|
||||
const fim1 = new Date(args.periodos[i].dataFim);
|
||||
const inicio2 = new Date(args.periodos[j].dataInicio);
|
||||
const fim2 = new Date(args.periodos[j].dataFim);
|
||||
|
||||
if (
|
||||
(inicio1 <= fim2 && fim1 >= inicio2) ||
|
||||
(inicio2 <= fim1 && fim2 >= inicio1)
|
||||
) {
|
||||
erros.push("Os períodos não podem se sobrepor");
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((inicio1 <= fim2 && fim1 >= inicio2) || (inicio2 <= fim1 && fim2 >= inicio1)) {
|
||||
erros.push('Os períodos não podem se sobrepor');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 6: Datas no futuro (aviso)
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
for (const periodo of args.periodos) {
|
||||
const inicio = new Date(periodo.dataInicio);
|
||||
if (inicio < hoje) {
|
||||
avisos.push("⚠️ Período(s) com data de início no passado ou hoje");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Validação 6: Datas no futuro (aviso)
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
for (const periodo of args.periodos) {
|
||||
const inicio = new Date(periodo.dataInicio);
|
||||
if (inicio < hoje) {
|
||||
avisos.push('⚠️ Período(s) com data de início no passado ou hoje');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 7: Servidor PE - aviso sobre período preferencial para docentes
|
||||
if (regime === "estatutario_pe" || regime === "estatutario_municipal") {
|
||||
for (const periodo of args.periodos) {
|
||||
const mes = new Date(periodo.dataInicio).getMonth() + 1;
|
||||
if (mes === 12 || mes === 1) {
|
||||
avisos.push("📅 Período preferencial para docentes (20/12 a 10/01)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validação 7: Servidor PE - aviso sobre período preferencial para docentes
|
||||
if (regime === 'estatutario_pe' || regime === 'estatutario_municipal') {
|
||||
for (const periodo of args.periodos) {
|
||||
const mes = new Date(periodo.dataInicio).getMonth() + 1;
|
||||
if (mes === 12 || mes === 1) {
|
||||
avisos.push('📅 Período preferencial para docentes (20/12 a 10/01)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valido: erros.length === 0,
|
||||
erros,
|
||||
avisos,
|
||||
totalDias,
|
||||
regimeTrabalho: config.nome,
|
||||
};
|
||||
},
|
||||
return {
|
||||
valido: erros.length === 0,
|
||||
erros,
|
||||
avisos,
|
||||
totalDias,
|
||||
regimeTrabalho: config.nome
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { defineSchema } from 'convex/server';
|
||||
import { setoresTables } from './tables/setores';
|
||||
import { contratosTables } from './tables/contratos';
|
||||
import { enderecosTables } from './tables/enderecos';
|
||||
import { empresasTable } from './tables/empresas';
|
||||
import { funcionariosTables } from './tables/funcionarios';
|
||||
import { flowsTables } from './tables/flows';
|
||||
import { atasTables } from './tables/atas';
|
||||
import { atestadosTables } from './tables/atestados';
|
||||
import { licencasTables } from './tables/licencas';
|
||||
import { feriasTables } from './tables/ferias';
|
||||
import { ausenciasTables } from './tables/ausencias';
|
||||
import { timesTables } from './tables/times';
|
||||
import { cursosTables } from './tables/cursos';
|
||||
import { authTables } from './tables/auth';
|
||||
import { systemTables } from './tables/system';
|
||||
import { chatTables } from './tables/chat';
|
||||
import { ticketsTables } from './tables/tickets';
|
||||
import { securityTables } from './tables/security';
|
||||
import { pontoTables } from './tables/ponto';
|
||||
import { contratosTables } from './tables/contratos';
|
||||
import { cursosTables } from './tables/cursos';
|
||||
import { empresasTable } from './tables/empresas';
|
||||
import { enderecosTables } from './tables/enderecos';
|
||||
import { feriasTables } from './tables/ferias';
|
||||
import { flowsTables } from './tables/flows';
|
||||
import { funcionariosTables } from './tables/funcionarios';
|
||||
import { licencasTables } from './tables/licencas';
|
||||
import { objetosTables } from './tables/objetos';
|
||||
import { pedidosTables } from './tables/pedidos';
|
||||
import { produtosTables } from './tables/produtos';
|
||||
import { pontoTables } from './tables/ponto';
|
||||
import { securityTables } from './tables/security';
|
||||
import { setoresTables } from './tables/setores';
|
||||
import { systemTables } from './tables/system';
|
||||
import { ticketsTables } from './tables/tickets';
|
||||
import { timesTables } from './tables/times';
|
||||
|
||||
export default defineSchema({
|
||||
...setoresTables,
|
||||
@@ -40,5 +41,6 @@ export default defineSchema({
|
||||
...securityTables,
|
||||
...pontoTables,
|
||||
...pedidosTables,
|
||||
...produtosTables
|
||||
...objetosTables,
|
||||
...atasTables
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
|
||||
import { v } from 'convex/values';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { components, internal } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import type {
|
||||
AtaqueCiberneticoTipo,
|
||||
SeveridadeSeguranca,
|
||||
StatusEventoSeguranca
|
||||
} from './tables/security';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
|
||||
import { components } from './_generated/api';
|
||||
|
||||
type Indicador = {
|
||||
tipo: string;
|
||||
@@ -1675,7 +1674,12 @@ async function aplicarRateLimit(
|
||||
}
|
||||
} as Record<
|
||||
string,
|
||||
{ kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }
|
||||
{
|
||||
kind: 'token bucket' | 'fixed window';
|
||||
rate: number;
|
||||
period: number;
|
||||
capacity?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig);
|
||||
@@ -2142,7 +2146,10 @@ export const criarEventosTeste = mutation({
|
||||
const agora = Date.now();
|
||||
|
||||
// Tipos de ataque para teste
|
||||
const tiposAtaque: Array<{ tipo: AtaqueCiberneticoTipo; severidade: SeveridadeSeguranca }> = [
|
||||
const tiposAtaque: Array<{
|
||||
tipo: AtaqueCiberneticoTipo;
|
||||
severidade: SeveridadeSeguranca;
|
||||
}> = [
|
||||
{ tipo: 'sql_injection', severidade: 'alto' },
|
||||
{ tipo: 'xss', severidade: 'moderado' },
|
||||
{ tipo: 'brute_force', severidade: 'alto' },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { internalAction, internalMutation, mutation, query } from './_generated/server';
|
||||
import { internal, api } from './_generated/api';
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
// import { hashPassword } from './auth/utils';
|
||||
import { Id } from './_generated/dataModel';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { internalAction, internalMutation, mutation, query } from './_generated/server';
|
||||
import { createAuthUser } from './auth';
|
||||
|
||||
// Dados exportados do Convex Cloud
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { query, mutation } from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
@@ -303,9 +303,7 @@ export const remove = mutation({
|
||||
}
|
||||
|
||||
// Verificar se há passos de fluxo vinculados
|
||||
const passosVinculados = await ctx.db
|
||||
.query('flowSteps')
|
||||
.collect();
|
||||
const passosVinculados = await ctx.db.query('flowSteps').collect();
|
||||
const temPassosVinculados = passosVinculados.some((p) => p.setorId === args.id);
|
||||
if (temPassosVinculados) {
|
||||
throw new Error('Não é possível excluir um setor vinculado a passos de fluxo');
|
||||
@@ -315,4 +313,3 @@ export const remove = mutation({
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { v } from 'convex/values';
|
||||
import { query, mutation } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { simboloTipo } from './tables/funcionarios';
|
||||
|
||||
export const getAll = query({
|
||||
|
||||
19
packages/backend/convex/tables/atas.ts
Normal file
19
packages/backend/convex/tables/atas.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
export const atasTables = {
|
||||
atas: defineTable({
|
||||
numero: v.string(),
|
||||
dataInicio: v.optional(v.string()),
|
||||
dataFim: v.optional(v.string()),
|
||||
empresaId: v.id('empresas'),
|
||||
pdf: v.optional(v.string()), // storage ID
|
||||
numeroSei: v.string(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
.index('by_numero', ['numero'])
|
||||
.index('by_empresaId', ['empresaId'])
|
||||
.index('by_numeroSei', ['numeroSei'])
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { Infer, v } from 'convex/values';
|
||||
import { type Infer, v } from 'convex/values';
|
||||
|
||||
// Status de templates de fluxo
|
||||
export const flowTemplateStatus = v.union(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { Infer, v } from 'convex/values';
|
||||
import { type Infer, v } from 'convex/values';
|
||||
|
||||
export const simboloTipo = v.union(
|
||||
v.literal('cargo_comissionado'),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { v } from 'convex/values';
|
||||
|
||||
export const produtosTables = {
|
||||
produtos: defineTable({
|
||||
export const objetosTables = {
|
||||
objetos: defineTable({
|
||||
nome: v.string(),
|
||||
valorEstimado: v.string(),
|
||||
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')),
|
||||
tipo: v.union(v.literal('material'), v.literal('servico')),
|
||||
codigoEfisco: v.string(),
|
||||
codigoCatmat: v.optional(v.string()),
|
||||
codigoCatserv: v.optional(v.string()),
|
||||
unidade: v.string(),
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number()
|
||||
})
|
||||
@@ -12,19 +12,25 @@ export const pedidosTables = {
|
||||
v.literal('cancelado'),
|
||||
v.literal('concluido')
|
||||
),
|
||||
acaoId: v.optional(v.id('acoes')),
|
||||
// acaoId removed
|
||||
criadoPor: v.id('usuarios'),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
.index('by_numeroSei', ['numeroSei'])
|
||||
.index('by_status', ['status'])
|
||||
.index('by_criadoPor', ['criadoPor'])
|
||||
.index('by_acaoId', ['acaoId']),
|
||||
.index('by_criadoPor', ['criadoPor']),
|
||||
|
||||
pedidoItems: defineTable({
|
||||
objetoItems: defineTable({
|
||||
pedidoId: v.id('pedidos'),
|
||||
produtoId: v.id('produtos'),
|
||||
objetoId: v.id('objetos'), // was produtoId
|
||||
acaoId: v.optional(v.id('acoes')), // Moved from pedidos
|
||||
modalidade: v.union(
|
||||
v.literal('dispensa'),
|
||||
v.literal('inexgibilidade'),
|
||||
v.literal('adesao'),
|
||||
v.literal('consumo')
|
||||
),
|
||||
valorEstimado: v.string(),
|
||||
valorReal: v.optional(v.string()),
|
||||
quantidade: v.number(),
|
||||
@@ -32,8 +38,9 @@ export const pedidosTables = {
|
||||
criadoEm: v.number()
|
||||
})
|
||||
.index('by_pedidoId', ['pedidoId'])
|
||||
.index('by_produtoId', ['produtoId'])
|
||||
.index('by_adicionadoPor', ['adicionadoPor']),
|
||||
.index('by_objetoId', ['objetoId'])
|
||||
.index('by_adicionadoPor', ['adicionadoPor'])
|
||||
.index('by_acaoId', ['acaoId']),
|
||||
|
||||
historicoPedidos: defineTable({
|
||||
pedidoId: v.id('pedidos'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineTable } from 'convex/server';
|
||||
import { Infer, v } from 'convex/values';
|
||||
import { type Infer, v } from 'convex/values';
|
||||
|
||||
export const ataqueCiberneticoTipo = v.union(
|
||||
v.literal('phishing'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,352 +1,351 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { getCurrentUserFunction } from "./auth";
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
// Query: Listar todos os times
|
||||
// Tipo inferido automaticamente pelo Convex
|
||||
export const listar = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const times = await ctx.db.query("times").collect();
|
||||
|
||||
// Buscar gestor e contar membros de cada time
|
||||
const timesComDetalhes = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosAtivos = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
totalMembros: membrosAtivos.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComDetalhes;
|
||||
},
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const times = await ctx.db.query('times').collect();
|
||||
|
||||
// Buscar gestor e contar membros de cada time
|
||||
const timesComDetalhes = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosAtivos = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', time._id).eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
totalMembros: membrosAtivos.length
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComDetalhes;
|
||||
}
|
||||
});
|
||||
|
||||
// Query: Obter time por ID com membros
|
||||
// Tipo inferido automaticamente pelo Convex
|
||||
export const obterPorId = query({
|
||||
args: { id: v.id("times") },
|
||||
handler: async (ctx, args) => {
|
||||
const time = await ctx.db.get(args.id);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", args.id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
// Buscar dados completos dos membros
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
membros,
|
||||
};
|
||||
},
|
||||
args: { id: v.id('times') },
|
||||
handler: async (ctx, args) => {
|
||||
const time = await ctx.db.get(args.id);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', args.id).eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
// Buscar dados completos dos membros
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
membros
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Query: Obter time do funcionário
|
||||
// Tipo inferido automaticamente pelo Convex
|
||||
export const obterTimeFuncionario = query({
|
||||
args: { funcionarioId: v.id("funcionarios") },
|
||||
handler: async (ctx, args) => {
|
||||
const relacao = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (!relacao) return null;
|
||||
|
||||
const time = await ctx.db.get(relacao.timeId);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor,
|
||||
};
|
||||
},
|
||||
args: { funcionarioId: v.id('funcionarios') },
|
||||
handler: async (ctx, args) => {
|
||||
const relacao = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.first();
|
||||
|
||||
if (!relacao) return null;
|
||||
|
||||
const time = await ctx.db.get(relacao.timeId);
|
||||
if (!time) return null;
|
||||
|
||||
const gestor = await ctx.db.get(time.gestorId);
|
||||
|
||||
return {
|
||||
...time,
|
||||
gestor
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Query: Obter times do gestor
|
||||
// Tipo inferido automaticamente pelo Convex
|
||||
export const listarPorGestor = query({
|
||||
args: { gestorId: v.id("usuarios") },
|
||||
handler: async (ctx, args) => {
|
||||
const times = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor", (q) => q.eq("gestorId", args.gestorId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
const timesComMembros = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
membros,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComMembros;
|
||||
},
|
||||
args: { gestorId: v.id('usuarios') },
|
||||
handler: async (ctx, args) => {
|
||||
const times = await ctx.db
|
||||
.query('times')
|
||||
.withIndex('by_gestor', (q) => q.eq('gestorId', args.gestorId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.collect();
|
||||
|
||||
const timesComMembros = await Promise.all(
|
||||
times.map(async (time) => {
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', time._id).eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
const membros = await Promise.all(
|
||||
membrosRelacoes.map(async (rel) => {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
return {
|
||||
...rel,
|
||||
funcionario
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...time,
|
||||
membros
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return timesComMembros;
|
||||
}
|
||||
});
|
||||
|
||||
export const listarSubordinadosDoGestorAtual = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
return [];
|
||||
}
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const timesGestor = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor", (q) => q.eq("gestorId", usuario._id))
|
||||
.collect();
|
||||
const timesGestor = await ctx.db
|
||||
.query('times')
|
||||
.withIndex('by_gestor', (q) => q.eq('gestorId', usuario._id))
|
||||
.collect();
|
||||
|
||||
const timesComoSuperior = await ctx.db
|
||||
.query("times")
|
||||
.withIndex("by_gestor_superior", (q) => q.eq("gestorSuperiorId", usuario._id))
|
||||
.collect();
|
||||
const timesComoSuperior = await ctx.db
|
||||
.query('times')
|
||||
.withIndex('by_gestor_superior', (q) => q.eq('gestorSuperiorId', usuario._id))
|
||||
.collect();
|
||||
|
||||
const timesMap = new Map(
|
||||
[...timesGestor, ...timesComoSuperior]
|
||||
.filter((time) => time.ativo)
|
||||
.map((time) => [time._id, time])
|
||||
);
|
||||
const timesMap = new Map(
|
||||
[...timesGestor, ...timesComoSuperior]
|
||||
.filter((time) => time.ativo)
|
||||
.map((time) => [time._id, time])
|
||||
);
|
||||
|
||||
const resultado = [];
|
||||
const resultado = [];
|
||||
|
||||
for (const time of timesMap.values()) {
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true))
|
||||
.collect();
|
||||
for (const time of timesMap.values()) {
|
||||
const membrosRelacoes = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', time._id).eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
const membros = [];
|
||||
for (const rel of membrosRelacoes) {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
if (funcionario) {
|
||||
membros.push({
|
||||
relacaoId: rel._id,
|
||||
funcionario,
|
||||
dataEntrada: rel.dataEntrada,
|
||||
});
|
||||
}
|
||||
}
|
||||
const membros = [];
|
||||
for (const rel of membrosRelacoes) {
|
||||
const funcionario = await ctx.db.get(rel.funcionarioId);
|
||||
if (funcionario) {
|
||||
membros.push({
|
||||
relacaoId: rel._id,
|
||||
funcionario,
|
||||
dataEntrada: rel.dataEntrada
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resultado.push({
|
||||
...time,
|
||||
membros,
|
||||
});
|
||||
}
|
||||
resultado.push({
|
||||
...time,
|
||||
membros
|
||||
});
|
||||
}
|
||||
|
||||
return resultado;
|
||||
},
|
||||
return resultado;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Criar time
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id("usuarios"),
|
||||
gestorSuperiorId: v.optional(v.id("usuarios")),
|
||||
cor: v.optional(v.string()),
|
||||
},
|
||||
returns: v.id("times"),
|
||||
handler: async (ctx, args) => {
|
||||
const timeId = await ctx.db.insert("times", {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
gestorId: args.gestorId,
|
||||
gestorSuperiorId: args.gestorSuperiorId ?? args.gestorId,
|
||||
ativo: true,
|
||||
cor: args.cor || "#3B82F6",
|
||||
});
|
||||
|
||||
return timeId;
|
||||
},
|
||||
args: {
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id('usuarios'),
|
||||
gestorSuperiorId: v.optional(v.id('usuarios')),
|
||||
cor: v.optional(v.string())
|
||||
},
|
||||
returns: v.id('times'),
|
||||
handler: async (ctx, args) => {
|
||||
const timeId = await ctx.db.insert('times', {
|
||||
nome: args.nome,
|
||||
descricao: args.descricao,
|
||||
gestorId: args.gestorId,
|
||||
gestorSuperiorId: args.gestorSuperiorId ?? args.gestorId,
|
||||
ativo: true,
|
||||
cor: args.cor || '#3B82F6'
|
||||
});
|
||||
|
||||
return timeId;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Atualizar time
|
||||
export const atualizar = mutation({
|
||||
args: {
|
||||
id: v.id("times"),
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id("usuarios"),
|
||||
gestorSuperiorId: v.optional(v.id("usuarios")),
|
||||
cor: v.optional(v.string()),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...dados } = args;
|
||||
await ctx.db.patch(id, {
|
||||
...dados,
|
||||
gestorSuperiorId: dados.gestorSuperiorId ?? dados.gestorId,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
args: {
|
||||
id: v.id('times'),
|
||||
nome: v.string(),
|
||||
descricao: v.optional(v.string()),
|
||||
gestorId: v.id('usuarios'),
|
||||
gestorSuperiorId: v.optional(v.id('usuarios')),
|
||||
cor: v.optional(v.string())
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const { id, ...dados } = args;
|
||||
await ctx.db.patch(id, {
|
||||
...dados,
|
||||
gestorSuperiorId: dados.gestorSuperiorId ?? dados.gestorId
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Desativar time
|
||||
export const desativar = mutation({
|
||||
args: { id: v.id("times") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar o time
|
||||
await ctx.db.patch(args.id, { ativo: false });
|
||||
|
||||
// Desativar todos os membros
|
||||
const membros = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", args.id).eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
for (const membro of membros) {
|
||||
await ctx.db.patch(membro._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
args: { id: v.id('times') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar o time
|
||||
await ctx.db.patch(args.id, { ativo: false });
|
||||
|
||||
await ctx.db.patch(membro.funcionarioId, { gestorId: undefined });
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
// Desativar todos os membros
|
||||
const membros = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_time_and_ativo', (q) => q.eq('timeId', args.id).eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
for (const membro of membros) {
|
||||
await ctx.db.patch(membro._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.patch(membro.funcionarioId, { gestorId: undefined });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Adicionar membro ao time
|
||||
export const adicionarMembro = mutation({
|
||||
args: {
|
||||
timeId: v.id("times"),
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
},
|
||||
returns: v.id("timesMembros"),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se já não está em outro time ativo
|
||||
const membroExistente = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (membroExistente) {
|
||||
throw new Error("Funcionário já está em um time ativo");
|
||||
}
|
||||
|
||||
const time = await ctx.db.get(args.timeId);
|
||||
if (!time || !time.ativo) {
|
||||
throw new Error("Time inválido ou inativo");
|
||||
}
|
||||
|
||||
const membroId = await ctx.db.insert("timesMembros", {
|
||||
timeId: args.timeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
args: {
|
||||
timeId: v.id('times'),
|
||||
funcionarioId: v.id('funcionarios')
|
||||
},
|
||||
returns: v.id('timesMembros'),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se já não está em outro time ativo
|
||||
const membroExistente = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.first();
|
||||
|
||||
await ctx.db.patch(args.funcionarioId, { gestorId: time.gestorId });
|
||||
|
||||
return membroId;
|
||||
},
|
||||
if (membroExistente) {
|
||||
throw new Error('Funcionário já está em um time ativo');
|
||||
}
|
||||
|
||||
const time = await ctx.db.get(args.timeId);
|
||||
if (!time || !time.ativo) {
|
||||
throw new Error('Time inválido ou inativo');
|
||||
}
|
||||
|
||||
const membroId = await ctx.db.insert('timesMembros', {
|
||||
timeId: args.timeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.funcionarioId, { gestorId: time.gestorId });
|
||||
|
||||
return membroId;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Remover membro do time
|
||||
export const removerMembro = mutation({
|
||||
args: { membroId: v.id("timesMembros") },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const membro = await ctx.db.get(args.membroId);
|
||||
if (!membro) {
|
||||
throw new Error("Membro não encontrado");
|
||||
}
|
||||
await ctx.db.patch(args.membroId, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
args: { membroId: v.id('timesMembros') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const membro = await ctx.db.get(args.membroId);
|
||||
if (!membro) {
|
||||
throw new Error('Membro não encontrado');
|
||||
}
|
||||
await ctx.db.patch(args.membroId, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now()
|
||||
});
|
||||
|
||||
await ctx.db.patch(membro.funcionarioId, { gestorId: undefined });
|
||||
return null;
|
||||
},
|
||||
await ctx.db.patch(membro.funcionarioId, { gestorId: undefined });
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation: Transferir membro para outro time
|
||||
export const transferirMembro = mutation({
|
||||
args: {
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
novoTimeId: v.id("times"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar do time atual
|
||||
const relacaoAtual = await ctx.db
|
||||
.query("timesMembros")
|
||||
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.first();
|
||||
|
||||
if (relacaoAtual) {
|
||||
await ctx.db.patch(relacaoAtual._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Adicionar ao novo time
|
||||
const novoTime = await ctx.db.get(args.novoTimeId);
|
||||
if (!novoTime || !novoTime.ativo) {
|
||||
throw new Error("Novo time inválido ou inativo");
|
||||
}
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
novoTimeId: v.id('times')
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Desativar do time atual
|
||||
const relacaoAtual = await ctx.db
|
||||
.query('timesMembros')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.filter((q) => q.eq(q.field('ativo'), true))
|
||||
.first();
|
||||
|
||||
await ctx.db.insert("timesMembros", {
|
||||
timeId: args.novoTimeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
if (relacaoAtual) {
|
||||
await ctx.db.patch(relacaoAtual._id, {
|
||||
ativo: false,
|
||||
dataSaida: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.funcionarioId, { gestorId: novoTime.gestorId });
|
||||
|
||||
return null;
|
||||
},
|
||||
// Adicionar ao novo time
|
||||
const novoTime = await ctx.db.get(args.novoTimeId);
|
||||
if (!novoTime || !novoTime.ativo) {
|
||||
throw new Error('Novo time inválido ou inativo');
|
||||
}
|
||||
|
||||
await ctx.db.insert('timesMembros', {
|
||||
timeId: args.novoTimeId,
|
||||
funcionarioId: args.funcionarioId,
|
||||
dataEntrada: Date.now(),
|
||||
ativo: true
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.funcionarioId, { gestorId: novoTime.gestorId });
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,19 +14,12 @@
|
||||
"types": [],
|
||||
/* These compiler options are required by Convex */
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"ES2021",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["ES2021", "dom"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"./_generated"
|
||||
]
|
||||
}
|
||||
"include": ["./**/*"],
|
||||
"exclude": ["./_generated"]
|
||||
}
|
||||
|
||||
73
packages/backend/convex/types/web-push.d.ts
vendored
73
packages/backend/convex/types/web-push.d.ts
vendored
@@ -1,46 +1,41 @@
|
||||
declare module "web-push" {
|
||||
export interface PushSubscription {
|
||||
endpoint: string;
|
||||
keys: {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
};
|
||||
}
|
||||
declare module 'web-push' {
|
||||
export interface PushSubscription {
|
||||
endpoint: string;
|
||||
keys: {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SendOptions {
|
||||
TTL?: number;
|
||||
headers?: Record<string, string>;
|
||||
vapidDetails?: {
|
||||
subject: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
}
|
||||
export interface SendOptions {
|
||||
TTL?: number;
|
||||
headers?: Record<string, string>;
|
||||
vapidDetails?: {
|
||||
subject: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function setVapidDetails(
|
||||
subject: string,
|
||||
publicKey: string,
|
||||
privateKey: string
|
||||
): void;
|
||||
export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void;
|
||||
|
||||
export function sendNotification(
|
||||
subscription: PushSubscription,
|
||||
payload: string | Buffer,
|
||||
options?: SendOptions
|
||||
): Promise<void>;
|
||||
export function sendNotification(
|
||||
subscription: PushSubscription,
|
||||
payload: string | Buffer,
|
||||
options?: SendOptions
|
||||
): Promise<void>;
|
||||
|
||||
export function generateVAPIDKeys(): {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
export function generateVAPIDKeys(): {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
interface WebPush {
|
||||
setVapidDetails: typeof setVapidDetails;
|
||||
sendNotification: typeof sendNotification;
|
||||
generateVAPIDKeys: typeof generateVAPIDKeys;
|
||||
}
|
||||
interface WebPush {
|
||||
setVapidDetails: typeof setVapidDetails;
|
||||
sendNotification: typeof sendNotification;
|
||||
generateVAPIDKeys: typeof generateVAPIDKeys;
|
||||
}
|
||||
|
||||
const webpush: WebPush;
|
||||
export default webpush;
|
||||
const webpush: WebPush;
|
||||
export default webpush;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
import { Id, Doc } from './_generated/dataModel';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { QueryCtx } from './_generated/server';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { createAuthUser, getCurrentUserFunction } from './auth';
|
||||
import { registrarAtividade } from './logsAtividades';
|
||||
|
||||
/**
|
||||
* Helper para obter a matrícula do usuário (do funcionário se houver)
|
||||
@@ -184,7 +184,7 @@ export const listar = query({
|
||||
}
|
||||
|
||||
// Incluir usuário com role de erro
|
||||
let funcionario = undefined;
|
||||
let funcionario;
|
||||
if (usuario.funcionarioId) {
|
||||
try {
|
||||
const func = await ctx.db.get(usuario.funcionarioId);
|
||||
@@ -243,7 +243,7 @@ export const listar = query({
|
||||
}
|
||||
|
||||
// Buscar funcionário associado
|
||||
let funcionario = undefined;
|
||||
let funcionario;
|
||||
if (usuario.funcionarioId) {
|
||||
try {
|
||||
const func = await ctx.db.get(usuario.funcionarioId);
|
||||
@@ -559,7 +559,7 @@ export const atualizarTema = mutation({
|
||||
*/
|
||||
export const obterPerfil = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('usuarios'),
|
||||
nome: v.string(),
|
||||
@@ -624,7 +624,7 @@ export const obterPerfil = query({
|
||||
*/
|
||||
export const listarParaChat = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('usuarios'),
|
||||
nome: v.string(),
|
||||
|
||||
@@ -9,17 +9,17 @@
|
||||
* @returns Mensagem formatada
|
||||
*/
|
||||
export function wrapChatMessage(conteudo: string, tipo?: string): string {
|
||||
// Se já tiver formatação especial, retornar como está
|
||||
if (conteudo.includes('[SGSE]') || conteudo.includes('[Sistema]')) {
|
||||
return conteudo;
|
||||
}
|
||||
// Se já tiver formatação especial, retornar como está
|
||||
if (conteudo.includes('[SGSE]') || conteudo.includes('[Sistema]')) {
|
||||
return conteudo;
|
||||
}
|
||||
|
||||
// Para mensagens do sistema, adicionar prefixo
|
||||
if (tipo === 'sistema' || tipo === 'notificacao') {
|
||||
return `[SGSE] ${conteudo}`;
|
||||
}
|
||||
// Para mensagens do sistema, adicionar prefixo
|
||||
if (tipo === 'sistema' || tipo === 'notificacao') {
|
||||
return `[SGSE] ${conteudo}`;
|
||||
}
|
||||
|
||||
return conteudo;
|
||||
return conteudo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,18 +29,12 @@ export function wrapChatMessage(conteudo: string, tipo?: string): string {
|
||||
* @param acao - Ação sugerida (opcional)
|
||||
* @returns Mensagem formatada
|
||||
*/
|
||||
export function formatChatNotification(
|
||||
titulo: string,
|
||||
conteudo: string,
|
||||
acao?: string
|
||||
): string {
|
||||
let mensagem = `🔔 ${titulo}\n\n${conteudo}`;
|
||||
|
||||
if (acao) {
|
||||
mensagem += `\n\n💡 ${acao}`;
|
||||
}
|
||||
|
||||
return mensagem;
|
||||
export function formatChatNotification(titulo: string, conteudo: string, acao?: string): string {
|
||||
let mensagem = `🔔 ${titulo}\n\n${conteudo}`;
|
||||
|
||||
if (acao) {
|
||||
mensagem += `\n\n💡 ${acao}`;
|
||||
}
|
||||
|
||||
return mensagem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
* Obtém a URL base do sistema para uso em links de email
|
||||
*/
|
||||
function getBaseUrl(): string {
|
||||
// Em produção, usar variável de ambiente
|
||||
const url = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
// Garantir que tenha protocolo
|
||||
if (!url.match(/^https?:\/\//i)) {
|
||||
return `http://${url}`;
|
||||
}
|
||||
return url;
|
||||
// Em produção, usar variável de ambiente
|
||||
const url = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
// Garantir que tenha protocolo
|
||||
if (!url.match(/^https?:\/\//i)) {
|
||||
return `http://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera o HTML do header com logo do Governo de PE
|
||||
*/
|
||||
function generateHeader(): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
return `
|
||||
const baseUrl = getBaseUrl();
|
||||
return `
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #1a3a52; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
@@ -42,10 +42,10 @@ function generateHeader(): string {
|
||||
* Gera o HTML do footer com assinatura SGSE
|
||||
*/
|
||||
function generateFooter(): string {
|
||||
const baseUrl = getBaseUrl();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `
|
||||
const baseUrl = getBaseUrl();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f5f5f5; border-top: 3px solid #1a3a52; margin-top: 30px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
@@ -85,24 +85,24 @@ function generateFooter(): string {
|
||||
* @returns HTML completo do email pronto para envio
|
||||
*/
|
||||
export function wrapEmailHTML(conteudoHTML: string, titulo?: string): string {
|
||||
// Se o conteúdo já estiver dentro de um wrapper completo, retornar como está
|
||||
if (conteudoHTML.includes('<!DOCTYPE html>') || conteudoHTML.includes('<html')) {
|
||||
return conteudoHTML;
|
||||
}
|
||||
// Se o conteúdo já estiver dentro de um wrapper completo, retornar como está
|
||||
if (conteudoHTML.includes('<!DOCTYPE html>') || conteudoHTML.includes('<html')) {
|
||||
return conteudoHTML;
|
||||
}
|
||||
|
||||
// Garantir que o conteúdo tenha estrutura básica
|
||||
let conteudoProcessado = conteudoHTML.trim();
|
||||
|
||||
// Se não tiver tags HTML básicas, envolver em parágrafo
|
||||
if (!conteudoProcessado.match(/^<[a-z]/i)) {
|
||||
conteudoProcessado = `<p style="margin: 0 0 15px 0;">${conteudoProcessado}</p>`;
|
||||
}
|
||||
// Garantir que o conteúdo tenha estrutura básica
|
||||
let conteudoProcessado = conteudoHTML.trim();
|
||||
|
||||
const header = generateHeader();
|
||||
const footer = generateFooter();
|
||||
const emailTitle = titulo || "Notificação do SGSE";
|
||||
// Se não tiver tags HTML básicas, envolver em parágrafo
|
||||
if (!conteudoProcessado.match(/^<[a-z]/i)) {
|
||||
conteudoProcessado = `<p style="margin: 0 0 15px 0;">${conteudoProcessado}</p>`;
|
||||
}
|
||||
|
||||
return `
|
||||
const header = generateHeader();
|
||||
const footer = generateFooter();
|
||||
const emailTitle = titulo || 'Notificação do SGSE';
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
@@ -169,17 +169,18 @@ export function wrapEmailHTML(conteudoHTML: string, titulo?: string): string {
|
||||
* @returns HTML formatado
|
||||
*/
|
||||
export function textToHTML(texto: string): string {
|
||||
return texto
|
||||
.split('\n')
|
||||
.map(linha => {
|
||||
const linhaTrim = linha.trim();
|
||||
if (!linhaTrim) return '<br />';
|
||||
// Detectar links
|
||||
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const linhaComLinks = linhaTrim.replace(linkRegex, '<a href="$1" style="color: #1a3a52; text-decoration: underline;">$1</a>');
|
||||
return `<p style="margin: 0 0 15px 0;">${linhaComLinks}</p>`;
|
||||
})
|
||||
.join('');
|
||||
return texto
|
||||
.split('\n')
|
||||
.map((linha) => {
|
||||
const linhaTrim = linha.trim();
|
||||
if (!linhaTrim) return '<br />';
|
||||
// Detectar links
|
||||
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const linhaComLinks = linhaTrim.replace(
|
||||
linkRegex,
|
||||
'<a href="$1" style="color: #1a3a52; text-decoration: underline;">$1</a>'
|
||||
);
|
||||
return `<p style="margin: 0 0 15px 0;">${linhaComLinks}</p>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,144 +8,143 @@
|
||||
* Considera headers como X-Forwarded-For, X-Real-IP, etc.
|
||||
*/
|
||||
export function getClientIP(request: Request): string | undefined {
|
||||
// Headers que podem conter o IP do cliente (case-insensitive)
|
||||
const getHeader = (name: string): string | null => {
|
||||
// Tentar diferentes variações de case
|
||||
const variations = [
|
||||
name,
|
||||
name.toLowerCase(),
|
||||
name.toUpperCase(),
|
||||
name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(),
|
||||
];
|
||||
|
||||
for (const variation of variations) {
|
||||
const value = request.headers.get(variation);
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
// As variações de case já cobrem a maioria dos casos
|
||||
// Se não encontrou, retorna null
|
||||
return null;
|
||||
};
|
||||
|
||||
const forwardedFor = getHeader("x-forwarded-for");
|
||||
const realIP = getHeader("x-real-ip");
|
||||
const cfConnectingIP = getHeader("cf-connecting-ip"); // Cloudflare
|
||||
const trueClientIP = getHeader("true-client-ip"); // Cloudflare Enterprise
|
||||
const xClientIP = getHeader("x-client-ip");
|
||||
const forwarded = getHeader("forwarded");
|
||||
const remoteAddr = getHeader("remote-addr");
|
||||
|
||||
// Log para debug
|
||||
console.log("Procurando IP nos headers:", {
|
||||
"x-forwarded-for": forwardedFor,
|
||||
"x-real-ip": realIP,
|
||||
"cf-connecting-ip": cfConnectingIP,
|
||||
"true-client-ip": trueClientIP,
|
||||
"x-client-ip": xClientIP,
|
||||
"forwarded": forwarded,
|
||||
"remote-addr": remoteAddr,
|
||||
});
|
||||
|
||||
// Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain)
|
||||
// O primeiro IP é geralmente o IP original do cliente
|
||||
if (forwardedFor) {
|
||||
const ips = forwardedFor.split(",").map((ip) => ip.trim());
|
||||
// Pegar o primeiro IP válido
|
||||
for (const ip of ips) {
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em X-Forwarded-For:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarded header (RFC 7239)
|
||||
if (forwarded) {
|
||||
// Formato: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
const forMatch = forwarded.match(/for=([^;,\s]+)/i);
|
||||
if (forMatch && forMatch[1]) {
|
||||
const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6
|
||||
if (isValidIP(ip)) {
|
||||
console.log("IP encontrado em Forwarded:", ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outros headers com IP único
|
||||
if (realIP && isValidIP(realIP)) {
|
||||
console.log("IP encontrado em X-Real-IP:", realIP);
|
||||
return realIP;
|
||||
}
|
||||
|
||||
if (cfConnectingIP && isValidIP(cfConnectingIP)) {
|
||||
console.log("IP encontrado em CF-Connecting-IP:", cfConnectingIP);
|
||||
return cfConnectingIP;
|
||||
}
|
||||
|
||||
if (trueClientIP && isValidIP(trueClientIP)) {
|
||||
console.log("IP encontrado em True-Client-IP:", trueClientIP);
|
||||
return trueClientIP;
|
||||
}
|
||||
|
||||
if (xClientIP && isValidIP(xClientIP)) {
|
||||
console.log("IP encontrado em X-Client-IP:", xClientIP);
|
||||
return xClientIP;
|
||||
}
|
||||
|
||||
if (remoteAddr && isValidIP(remoteAddr)) {
|
||||
console.log("IP encontrado em Remote-Addr:", remoteAddr);
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
// Tentar extrair do URL (último recurso)
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Se o servidor estiver configurado para passar IP via query param
|
||||
const ipFromQuery = url.searchParams.get("ip");
|
||||
if (ipFromQuery && isValidIP(ipFromQuery)) {
|
||||
console.log("IP encontrado em query param:", ipFromQuery);
|
||||
return ipFromQuery;
|
||||
}
|
||||
} catch {
|
||||
// Ignorar erro de parsing do URL
|
||||
}
|
||||
|
||||
console.log("Nenhum IP válido encontrado nos headers");
|
||||
return undefined;
|
||||
// Headers que podem conter o IP do cliente (case-insensitive)
|
||||
const getHeader = (name: string): string | null => {
|
||||
// Tentar diferentes variações de case
|
||||
const variations = [
|
||||
name,
|
||||
name.toLowerCase(),
|
||||
name.toUpperCase(),
|
||||
name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
||||
];
|
||||
|
||||
for (const variation of variations) {
|
||||
const value = request.headers.get(variation);
|
||||
if (value) return value;
|
||||
}
|
||||
|
||||
// As variações de case já cobrem a maioria dos casos
|
||||
// Se não encontrou, retorna null
|
||||
return null;
|
||||
};
|
||||
|
||||
const forwardedFor = getHeader('x-forwarded-for');
|
||||
const realIP = getHeader('x-real-ip');
|
||||
const cfConnectingIP = getHeader('cf-connecting-ip'); // Cloudflare
|
||||
const trueClientIP = getHeader('true-client-ip'); // Cloudflare Enterprise
|
||||
const xClientIP = getHeader('x-client-ip');
|
||||
const forwarded = getHeader('forwarded');
|
||||
const remoteAddr = getHeader('remote-addr');
|
||||
|
||||
// Log para debug
|
||||
console.log('Procurando IP nos headers:', {
|
||||
'x-forwarded-for': forwardedFor,
|
||||
'x-real-ip': realIP,
|
||||
'cf-connecting-ip': cfConnectingIP,
|
||||
'true-client-ip': trueClientIP,
|
||||
'x-client-ip': xClientIP,
|
||||
forwarded: forwarded,
|
||||
'remote-addr': remoteAddr
|
||||
});
|
||||
|
||||
// Prioridade: X-Forwarded-For pode conter múltiplos IPs (proxy chain)
|
||||
// O primeiro IP é geralmente o IP original do cliente
|
||||
if (forwardedFor) {
|
||||
const ips = forwardedFor.split(',').map((ip) => ip.trim());
|
||||
// Pegar o primeiro IP válido
|
||||
for (const ip of ips) {
|
||||
if (isValidIP(ip)) {
|
||||
console.log('IP encontrado em X-Forwarded-For:', ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forwarded header (RFC 7239)
|
||||
if (forwarded) {
|
||||
// Formato: for=192.0.2.60;proto=http;by=203.0.113.43
|
||||
const forMatch = forwarded.match(/for=([^;,\s]+)/i);
|
||||
if (forMatch && forMatch[1]) {
|
||||
const ip = forMatch[1].replace(/^\[|\]$/g, ''); // Remove brackets de IPv6
|
||||
if (isValidIP(ip)) {
|
||||
console.log('IP encontrado em Forwarded:', ip);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outros headers com IP único
|
||||
if (realIP && isValidIP(realIP)) {
|
||||
console.log('IP encontrado em X-Real-IP:', realIP);
|
||||
return realIP;
|
||||
}
|
||||
|
||||
if (cfConnectingIP && isValidIP(cfConnectingIP)) {
|
||||
console.log('IP encontrado em CF-Connecting-IP:', cfConnectingIP);
|
||||
return cfConnectingIP;
|
||||
}
|
||||
|
||||
if (trueClientIP && isValidIP(trueClientIP)) {
|
||||
console.log('IP encontrado em True-Client-IP:', trueClientIP);
|
||||
return trueClientIP;
|
||||
}
|
||||
|
||||
if (xClientIP && isValidIP(xClientIP)) {
|
||||
console.log('IP encontrado em X-Client-IP:', xClientIP);
|
||||
return xClientIP;
|
||||
}
|
||||
|
||||
if (remoteAddr && isValidIP(remoteAddr)) {
|
||||
console.log('IP encontrado em Remote-Addr:', remoteAddr);
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
// Tentar extrair do URL (último recurso)
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
// Se o servidor estiver configurado para passar IP via query param
|
||||
const ipFromQuery = url.searchParams.get('ip');
|
||||
if (ipFromQuery && isValidIP(ipFromQuery)) {
|
||||
console.log('IP encontrado em query param:', ipFromQuery);
|
||||
return ipFromQuery;
|
||||
}
|
||||
} catch {
|
||||
// Ignorar erro de parsing do URL
|
||||
}
|
||||
|
||||
console.log('Nenhum IP válido encontrado nos headers');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se uma string é um endereço IP válido (IPv4 ou IPv6)
|
||||
*/
|
||||
function isValidIP(ip: string): boolean {
|
||||
if (!ip || ip.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split(".");
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// Validar IPv6 (formato simplificado)
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar IPv6 comprimido (com ::)
|
||||
const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6CompressedRegex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!ip || ip.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validar IPv4
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(ip)) {
|
||||
const parts = ip.split('.');
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// Validar IPv6 (formato simplificado)
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6Regex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar IPv6 comprimido (com ::)
|
||||
const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*::([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/;
|
||||
if (ipv6CompressedRegex.test(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
* Identifica todos os locais onde emails são enviados para gerar templates
|
||||
*/
|
||||
|
||||
import { Doc } from "../_generated/dataModel";
|
||||
import { Doc } from '../_generated/dataModel';
|
||||
|
||||
export interface EmailSendLocation {
|
||||
arquivo: string;
|
||||
funcao: string;
|
||||
tipo: "enfileirarEmail" | "enviarEmailComTemplate" | "enviarMensagem" | "html_inline";
|
||||
linha?: number;
|
||||
contexto?: string;
|
||||
assunto?: string;
|
||||
corpo?: string;
|
||||
templateCodigo?: string;
|
||||
variaveis?: string[];
|
||||
arquivo: string;
|
||||
funcao: string;
|
||||
tipo: 'enfileirarEmail' | 'enviarEmailComTemplate' | 'enviarMensagem' | 'html_inline';
|
||||
linha?: number;
|
||||
contexto?: string;
|
||||
assunto?: string;
|
||||
corpo?: string;
|
||||
templateCodigo?: string;
|
||||
variaveis?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,168 +22,187 @@ export interface EmailSendLocation {
|
||||
* Este é um mapeamento manual baseado na análise do código
|
||||
*/
|
||||
export const LOCAIS_ENVIO_EMAIL: EmailSendLocation[] = [
|
||||
// Chamados
|
||||
{
|
||||
arquivo: "packages/backend/convex/chamados.ts",
|
||||
funcao: "registrarNotificacoes",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao solicitante quando chamado é criado/atualizado",
|
||||
assunto: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/chamados.ts",
|
||||
funcao: "registrarNotificacoes",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao responsável quando chamado é atualizado",
|
||||
assunto: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
},
|
||||
|
||||
// Ausências
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "solicitar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao gestor quando funcionário solicita ausência",
|
||||
assunto: "Nova Solicitação de Ausência - {{funcionarioNome}}",
|
||||
corpo: "Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.",
|
||||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "aprovar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao funcionário quando ausência é aprovada",
|
||||
assunto: "Solicitação de Ausência Aprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/ausencias.ts",
|
||||
funcao: "reprovar",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Notificação ao funcionário quando ausência é reprovada",
|
||||
assunto: "Solicitação de Ausência Reprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao"],
|
||||
},
|
||||
|
||||
// Chat
|
||||
{
|
||||
arquivo: "packages/backend/convex/chat.ts",
|
||||
funcao: "enviarMensagem",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Email quando usuário recebe nova mensagem no chat (usuário offline)",
|
||||
templateCodigo: "chat_mensagem",
|
||||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||||
},
|
||||
{
|
||||
arquivo: "packages/backend/convex/chat.ts",
|
||||
funcao: "enviarMensagem",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Email quando usuário é mencionado no chat (usuário offline)",
|
||||
templateCodigo: "chat_mencao",
|
||||
variaveis: ["remetente", "mensagem", "conversaId", "urlSistema"],
|
||||
},
|
||||
|
||||
// Painel de Notificações
|
||||
{
|
||||
arquivo: "apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte",
|
||||
funcao: "enviarNotificacao",
|
||||
tipo: "enfileirarEmail",
|
||||
contexto: "Envio manual de notificação via painel de TI",
|
||||
assunto: "Notificação do Sistema",
|
||||
corpo: "{{mensagemPersonalizada}}",
|
||||
variaveis: ["mensagemPersonalizada"],
|
||||
},
|
||||
{
|
||||
arquivo: "apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte",
|
||||
funcao: "enviarNotificacao",
|
||||
tipo: "enviarEmailComTemplate",
|
||||
contexto: "Envio manual de notificação usando template via painel de TI",
|
||||
templateCodigo: "{{templateCodigo}}",
|
||||
variaveis: ["nome", "matricula"],
|
||||
},
|
||||
// Chamados
|
||||
{
|
||||
arquivo: 'packages/backend/convex/chamados.ts',
|
||||
funcao: 'registrarNotificacoes',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Notificação ao solicitante quando chamado é criado/atualizado',
|
||||
assunto: 'Chamado {{numeroTicket}} - {{titulo}}',
|
||||
corpo: '{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria',
|
||||
variaveis: ['numeroTicket', 'titulo', 'mensagem']
|
||||
},
|
||||
{
|
||||
arquivo: 'packages/backend/convex/chamados.ts',
|
||||
funcao: 'registrarNotificacoes',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Notificação ao responsável quando chamado é atualizado',
|
||||
assunto: 'Chamado {{numeroTicket}} - {{titulo}}',
|
||||
corpo: '{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria',
|
||||
variaveis: ['numeroTicket', 'titulo', 'mensagem']
|
||||
},
|
||||
|
||||
// Ausências
|
||||
{
|
||||
arquivo: 'packages/backend/convex/ausencias.ts',
|
||||
funcao: 'solicitar',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Notificação ao gestor quando funcionário solicita ausência',
|
||||
assunto: 'Nova Solicitação de Ausência - {{funcionarioNome}}',
|
||||
corpo:
|
||||
'Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.',
|
||||
variaveis: ['gestorNome', 'funcionarioNome', 'dataInicio', 'dataFim', 'motivo']
|
||||
},
|
||||
{
|
||||
arquivo: 'packages/backend/convex/ausencias.ts',
|
||||
funcao: 'aprovar',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Notificação ao funcionário quando ausência é aprovada',
|
||||
assunto: 'Solicitação de Ausência Aprovada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>',
|
||||
variaveis: ['funcionarioNome', 'gestorNome', 'dataInicio', 'dataFim', 'motivo']
|
||||
},
|
||||
{
|
||||
arquivo: 'packages/backend/convex/ausencias.ts',
|
||||
funcao: 'reprovar',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Notificação ao funcionário quando ausência é reprovada',
|
||||
assunto: 'Solicitação de Ausência Reprovada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>',
|
||||
variaveis: [
|
||||
'funcionarioNome',
|
||||
'gestorNome',
|
||||
'dataInicio',
|
||||
'dataFim',
|
||||
'motivo',
|
||||
'motivoReprovacao'
|
||||
]
|
||||
},
|
||||
|
||||
// Chat
|
||||
{
|
||||
arquivo: 'packages/backend/convex/chat.ts',
|
||||
funcao: 'enviarMensagem',
|
||||
tipo: 'enviarEmailComTemplate',
|
||||
contexto: 'Email quando usuário recebe nova mensagem no chat (usuário offline)',
|
||||
templateCodigo: 'chat_mensagem',
|
||||
variaveis: ['remetente', 'mensagem', 'conversaId', 'urlSistema']
|
||||
},
|
||||
{
|
||||
arquivo: 'packages/backend/convex/chat.ts',
|
||||
funcao: 'enviarMensagem',
|
||||
tipo: 'enviarEmailComTemplate',
|
||||
contexto: 'Email quando usuário é mencionado no chat (usuário offline)',
|
||||
templateCodigo: 'chat_mencao',
|
||||
variaveis: ['remetente', 'mensagem', 'conversaId', 'urlSistema']
|
||||
},
|
||||
|
||||
// Painel de Notificações
|
||||
{
|
||||
arquivo: 'apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte',
|
||||
funcao: 'enviarNotificacao',
|
||||
tipo: 'enfileirarEmail',
|
||||
contexto: 'Envio manual de notificação via painel de TI',
|
||||
assunto: 'Notificação do Sistema',
|
||||
corpo: '{{mensagemPersonalizada}}',
|
||||
variaveis: ['mensagemPersonalizada']
|
||||
},
|
||||
{
|
||||
arquivo: 'apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte',
|
||||
funcao: 'enviarNotificacao',
|
||||
tipo: 'enviarEmailComTemplate',
|
||||
contexto: 'Envio manual de notificação usando template via painel de TI',
|
||||
templateCodigo: '{{templateCodigo}}',
|
||||
variaveis: ['nome', 'matricula']
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Sugestões de templates baseadas nos locais de envio encontrados
|
||||
*/
|
||||
export interface TemplateSuggestion {
|
||||
codigo: string;
|
||||
nome: string;
|
||||
titulo: string;
|
||||
corpo: string;
|
||||
categoria: "email" | "chat" | "ambos";
|
||||
variaveis: string[];
|
||||
tags: string[];
|
||||
origem: string;
|
||||
codigo: string;
|
||||
nome: string;
|
||||
titulo: string;
|
||||
corpo: string;
|
||||
categoria: 'email' | 'chat' | 'ambos';
|
||||
variaveis: string[];
|
||||
tags: string[];
|
||||
origem: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar sugestões de templates baseadas nos locais de envio
|
||||
*/
|
||||
export function gerarSugestoesTemplates(): TemplateSuggestion[] {
|
||||
const sugestoes: TemplateSuggestion[] = [];
|
||||
const sugestoes: TemplateSuggestion[] = [];
|
||||
|
||||
// Template para ausência solicitada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_solicitada",
|
||||
nome: "Ausência Solicitada",
|
||||
titulo: "Nova Solicitação de Ausência - {{funcionarioNome}}",
|
||||
corpo: "Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.",
|
||||
categoria: "email",
|
||||
variaveis: ["gestorNome", "funcionarioNome", "dataInicio", "dataFim", "motivo"],
|
||||
tags: ["ausencia", "solicitacao", "gestao"],
|
||||
origem: "ausencias.ts - solicitar",
|
||||
});
|
||||
// Template para ausência solicitada
|
||||
sugestoes.push({
|
||||
codigo: 'ausencia_solicitada',
|
||||
nome: 'Ausência Solicitada',
|
||||
titulo: 'Nova Solicitação de Ausência - {{funcionarioNome}}',
|
||||
corpo:
|
||||
'Olá {{gestorNome}},\n\nO funcionário <strong>{{funcionarioNome}}</strong> solicitou uma ausência:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>\n\nPor favor, acesse o sistema para aprovar ou reprovar esta solicitação.',
|
||||
categoria: 'email',
|
||||
variaveis: ['gestorNome', 'funcionarioNome', 'dataInicio', 'dataFim', 'motivo'],
|
||||
tags: ['ausencia', 'solicitacao', 'gestao'],
|
||||
origem: 'ausencias.ts - solicitar'
|
||||
});
|
||||
|
||||
// Template para ausência aprovada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_aprovada",
|
||||
nome: "Ausência Aprovada",
|
||||
titulo: "Solicitação de Ausência Aprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>",
|
||||
categoria: "email",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo"],
|
||||
tags: ["ausencia", "aprovacao", "gestao"],
|
||||
origem: "ausencias.ts - aprovar",
|
||||
});
|
||||
// Template para ausência aprovada
|
||||
sugestoes.push({
|
||||
codigo: 'ausencia_aprovada',
|
||||
nome: 'Ausência Aprovada',
|
||||
titulo: 'Solicitação de Ausência Aprovada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>aprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li></ul>',
|
||||
categoria: 'email',
|
||||
variaveis: ['funcionarioNome', 'gestorNome', 'dataInicio', 'dataFim', 'motivo'],
|
||||
tags: ['ausencia', 'aprovacao', 'gestao'],
|
||||
origem: 'ausencias.ts - aprovar'
|
||||
});
|
||||
|
||||
// Template para ausência reprovada
|
||||
sugestoes.push({
|
||||
codigo: "ausencia_reprovada",
|
||||
nome: "Ausência Reprovada",
|
||||
titulo: "Solicitação de Ausência Reprovada",
|
||||
corpo: "Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>",
|
||||
categoria: "email",
|
||||
variaveis: ["funcionarioNome", "gestorNome", "dataInicio", "dataFim", "motivo", "motivoReprovacao"],
|
||||
tags: ["ausencia", "reprovacao", "gestao"],
|
||||
origem: "ausencias.ts - reprovar",
|
||||
});
|
||||
// Template para ausência reprovada
|
||||
sugestoes.push({
|
||||
codigo: 'ausencia_reprovada',
|
||||
nome: 'Ausência Reprovada',
|
||||
titulo: 'Solicitação de Ausência Reprovada',
|
||||
corpo:
|
||||
'Olá {{funcionarioNome}},\n\nSua solicitação de ausência foi <strong>reprovada</strong> pelo gestor {{gestorNome}}:\n\n<ul><li><strong>Período:</strong> {{dataInicio}} até {{dataFim}}</li><li><strong>Motivo:</strong> {{motivo}}</li><li><strong>Motivo da Reprovação:</strong> {{motivoReprovacao}}</li></ul>',
|
||||
categoria: 'email',
|
||||
variaveis: [
|
||||
'funcionarioNome',
|
||||
'gestorNome',
|
||||
'dataInicio',
|
||||
'dataFim',
|
||||
'motivo',
|
||||
'motivoReprovacao'
|
||||
],
|
||||
tags: ['ausencia', 'reprovacao', 'gestao'],
|
||||
origem: 'ausencias.ts - reprovar'
|
||||
});
|
||||
|
||||
// Template genérico para notificações de chamados
|
||||
sugestoes.push({
|
||||
codigo: "chamado_notificacao",
|
||||
nome: "Notificação de Chamado",
|
||||
titulo: "Chamado {{numeroTicket}} - {{titulo}}",
|
||||
corpo: "{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria",
|
||||
categoria: "email",
|
||||
variaveis: ["numeroTicket", "titulo", "mensagem"],
|
||||
tags: ["chamado", "notificacao", "suporte"],
|
||||
origem: "chamados.ts - registrarNotificacoes",
|
||||
});
|
||||
// Template genérico para notificações de chamados
|
||||
sugestoes.push({
|
||||
codigo: 'chamado_notificacao',
|
||||
nome: 'Notificação de Chamado',
|
||||
titulo: 'Chamado {{numeroTicket}} - {{titulo}}',
|
||||
corpo: '{{mensagem}}\n\n---\nCentral de Chamados SGSE - Sistema de Gerenciamento de Secretaria',
|
||||
categoria: 'email',
|
||||
variaveis: ['numeroTicket', 'titulo', 'mensagem'],
|
||||
tags: ['chamado', 'notificacao', 'suporte'],
|
||||
origem: 'chamados.ts - registrarNotificacoes'
|
||||
});
|
||||
|
||||
return sugestoes;
|
||||
return sugestoes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter todos os locais de envio de email
|
||||
*/
|
||||
export function obterLocaisEnvio(): EmailSendLocation[] {
|
||||
return LOCAIS_ENVIO_EMAIL;
|
||||
return LOCAIS_ENVIO_EMAIL;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +1,123 @@
|
||||
import { internalMutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
import { v } from 'convex/values';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import { internalMutation, query } from './_generated/server';
|
||||
|
||||
/**
|
||||
* Verificar duplicatas de matrícula (agora busca do funcionário associado)
|
||||
*/
|
||||
export const verificarDuplicatas = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
matricula: v.string(),
|
||||
count: v.number(),
|
||||
usuarios: v.array(
|
||||
v.object({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrar apenas duplicatas
|
||||
const duplicatas = Object.entries(gruposPorMatricula)
|
||||
.filter(([_, usuarios]) => usuarios.length > 1)
|
||||
.map(([matricula, usuarios]) => ({
|
||||
matricula,
|
||||
count: usuarios.length,
|
||||
usuarios,
|
||||
}));
|
||||
|
||||
return duplicatas;
|
||||
},
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
matricula: v.string(),
|
||||
count: v.number(),
|
||||
usuarios: v.array(
|
||||
v.object({
|
||||
_id: v.id('usuarios'),
|
||||
nome: v.string(),
|
||||
email: v.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<
|
||||
string,
|
||||
Array<{ _id: Id<'usuarios'>; nome: string; email: string }>
|
||||
> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrar apenas duplicatas
|
||||
const duplicatas = Object.entries(gruposPorMatricula)
|
||||
.filter(([_, usuarios]) => usuarios.length > 1)
|
||||
.map(([matricula, usuarios]) => ({
|
||||
matricula,
|
||||
count: usuarios.length,
|
||||
usuarios
|
||||
}));
|
||||
|
||||
return duplicatas;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Remover duplicatas mantendo apenas o mais recente (agora busca do funcionário associado)
|
||||
*/
|
||||
export const removerDuplicatas = internalMutation({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
removidos: v.number(),
|
||||
matriculas: v.array(v.string()),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<string, Doc<"usuarios">[]> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push(usuario);
|
||||
}
|
||||
}
|
||||
|
||||
let removidos = 0;
|
||||
const matriculasDuplicadas: string[] = [];
|
||||
|
||||
// Para cada grupo com duplicatas
|
||||
for (const [matricula, usuariosGrupo] of Object.entries(gruposPorMatricula)) {
|
||||
if (usuariosGrupo.length > 1) {
|
||||
matriculasDuplicadas.push(matricula);
|
||||
|
||||
// Ordenar por _creationTime (mais recente primeiro)
|
||||
usuariosGrupo.sort((a, b) => b._creationTime - a._creationTime);
|
||||
|
||||
// Manter o primeiro (mais recente) e remover os outros
|
||||
for (let i = 1; i < usuariosGrupo.length; i++) {
|
||||
await ctx.db.delete(usuariosGrupo[i]._id);
|
||||
removidos++;
|
||||
console.log(`🗑️ Removido usuário duplicado: ${usuariosGrupo[i].nome} (matrícula: ${matricula})`);
|
||||
}
|
||||
|
||||
console.log(`✅ Mantido usuário: ${usuariosGrupo[0].nome} (matrícula: ${matricula})`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
removidos,
|
||||
matriculas: matriculasDuplicadas,
|
||||
};
|
||||
},
|
||||
args: {},
|
||||
returns: v.object({
|
||||
removidos: v.number(),
|
||||
matriculas: v.array(v.string())
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
|
||||
// Agrupar por matrícula do funcionário associado
|
||||
const gruposPorMatricula: Record<string, Doc<'usuarios'>[]> = {};
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
let matricula: string | undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push(usuario);
|
||||
}
|
||||
}
|
||||
|
||||
let removidos = 0;
|
||||
const matriculasDuplicadas: string[] = [];
|
||||
|
||||
// Para cada grupo com duplicatas
|
||||
for (const [matricula, usuariosGrupo] of Object.entries(gruposPorMatricula)) {
|
||||
if (usuariosGrupo.length > 1) {
|
||||
matriculasDuplicadas.push(matricula);
|
||||
|
||||
// Ordenar por _creationTime (mais recente primeiro)
|
||||
usuariosGrupo.sort((a, b) => b._creationTime - a._creationTime);
|
||||
|
||||
// Manter o primeiro (mais recente) e remover os outros
|
||||
for (let i = 1; i < usuariosGrupo.length; i++) {
|
||||
await ctx.db.delete(usuariosGrupo[i]._id);
|
||||
removidos++;
|
||||
console.log(
|
||||
`🗑️ Removido usuário duplicado: ${usuariosGrupo[i].nome} (matrícula: ${matricula})`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`✅ Mantido usuário: ${usuariosGrupo[0].nome} (matrícula: ${matricula})`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
removidos,
|
||||
matriculas: matriculasDuplicadas
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { config } from '@sgse-app/eslint-config/convex';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default defineConfig([
|
||||
...config,
|
||||
]);
|
||||
export default defineConfig([...config]);
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
{
|
||||
"name": "@sgse-app/backend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "convex dev",
|
||||
"dev:setup": "convex dev --configure --until-success"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@sgse-app/eslint-config": "*",
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/pako": "^2.0.4",
|
||||
"@types/raf": "^3.4.3",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"typescript": "catalog:",
|
||||
"eslint": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/better-auth": "^0.9.7",
|
||||
"@convex-dev/rate-limiter": "^0.3.0",
|
||||
"@dicebear/avataaars": "^9.2.4",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"better-auth": "catalog:",
|
||||
"convex": "catalog:",
|
||||
"nodemailer": "^7.0.10"
|
||||
}
|
||||
}
|
||||
"name": "@sgse-app/backend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "convex dev",
|
||||
"dev:setup": "convex dev --configure --until-success",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@sgse-app/eslint-config": "*",
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/pako": "^2.0.4",
|
||||
"@types/raf": "^3.4.3",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"typescript": "catalog:",
|
||||
"eslint": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/better-auth": "^0.9.7",
|
||||
"@convex-dev/rate-limiter": "^0.3.0",
|
||||
"@dicebear/avataaars": "^9.2.4",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
"better-auth": "catalog:",
|
||||
"convex": "catalog:",
|
||||
"nodemailer": "^7.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user