Fix usuarios page #6
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@
|
||||
});
|
||||
|
||||
// Estados do formulário
|
||||
let matricula = $state("");
|
||||
let nome = $state("");
|
||||
let email = $state("");
|
||||
let roleId = $state("");
|
||||
@@ -30,7 +29,9 @@
|
||||
let senhaInicial = $state("");
|
||||
let confirmarSenha = $state("");
|
||||
let processando = $state(false);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
|
||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
||||
mensagem = { tipo, texto };
|
||||
@@ -43,8 +44,7 @@
|
||||
e.preventDefault();
|
||||
|
||||
// Validações
|
||||
const matriculaStr = String(matricula).trim();
|
||||
if (!matriculaStr || !nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||
if (!nome.trim() || !email.trim() || !roleId || !senhaInicial) {
|
||||
mostrarMensagem("error", "Preencha todos os campos obrigatórios");
|
||||
return;
|
||||
}
|
||||
@@ -63,11 +63,12 @@
|
||||
|
||||
try {
|
||||
const resultado = await client.mutation(api.usuarios.criar, {
|
||||
matricula: matriculaStr,
|
||||
nome: nome.trim(),
|
||||
email: email.trim(),
|
||||
roleId: roleId as Id<"roles">,
|
||||
funcionarioId: funcionarioId ? (funcionarioId as Id<"funcionarios">) : undefined,
|
||||
funcionarioId: funcionarioId
|
||||
? (funcionarioId as Id<"funcionarios">)
|
||||
: undefined,
|
||||
senhaInicial: senhaInicial,
|
||||
});
|
||||
|
||||
@@ -75,7 +76,7 @@
|
||||
if (senhaGerada) {
|
||||
mostrarMensagem(
|
||||
"success",
|
||||
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`
|
||||
`Usuário criado! SENHA TEMPORÁRIA: ${senhaGerada} - Anote esta senha, ela não será exibida novamente!`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
goto("/ti/usuarios");
|
||||
@@ -102,17 +103,19 @@
|
||||
// Auto-completar ao selecionar funcionário
|
||||
$effect(() => {
|
||||
if (funcionarioId && funcionarios?.data) {
|
||||
const funcSelecionado = funcionarios.data.find((f: any) => f._id === funcionarioId);
|
||||
const funcSelecionado = funcionarios.data.find(
|
||||
(f: any) => f._id === funcionarioId,
|
||||
);
|
||||
if (funcSelecionado) {
|
||||
email = funcSelecionado.email || email;
|
||||
nome = funcSelecionado.nome || nome;
|
||||
matricula = funcSelecionado.matricula || matricula;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function gerarSenhaAleatoria() {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$!";
|
||||
let senha = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
@@ -154,8 +157,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Criar Novo Usuário</h1>
|
||||
<p class="text-base-content/60 mt-1">Cadastre um novo usuário no sistema</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Criar Novo Usuário
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Cadastre um novo usuário no sistema
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/ti/usuarios" class="btn btn-outline btn-primary gap-2">
|
||||
@@ -248,7 +255,9 @@
|
||||
<!-- Funcionário (primeiro) -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="funcionario">
|
||||
<span class="label-text font-semibold">Vincular Funcionário (Opcional)</span>
|
||||
<span class="label-text font-semibold"
|
||||
>Vincular Funcionário (Opcional)</span
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
id="funcionario"
|
||||
@@ -256,34 +265,24 @@
|
||||
bind:value={funcionarioId}
|
||||
disabled={processando || !funcionarios?.data}
|
||||
>
|
||||
<option value="">Selecione um funcionário para auto-completar dados</option>
|
||||
<option value=""
|
||||
>Selecione um funcionário para auto-completar dados</option
|
||||
>
|
||||
{#if funcionarios?.data}
|
||||
{#each funcionarios.data as func}
|
||||
<option value={func._id}>{func.nome} - Mat: {func.matricula}</option>
|
||||
<option value={func._id}
|
||||
>{func.nome} - Mat: {func.matricula}</option
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Ao selecionar, os campos serão preenchidos automaticamente</span>
|
||||
<span class="label-text-alt"
|
||||
>Ao selecionar, os campos serão preenchidos automaticamente</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matrícula -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="matricula">
|
||||
<span class="label-text font-semibold">Matrícula *</span>
|
||||
</label>
|
||||
<input
|
||||
id="matricula"
|
||||
type="number"
|
||||
placeholder="Ex: 12345"
|
||||
class="input input-bordered"
|
||||
bind:value={matricula}
|
||||
required
|
||||
disabled={processando}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
@@ -301,7 +300,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<div class="form-control">
|
||||
<label class="label" for="email">
|
||||
<span class="label-text font-semibold">E-mail *</span>
|
||||
</label>
|
||||
@@ -341,7 +340,9 @@
|
||||
</select>
|
||||
{#if !roles?.data || !Array.isArray(roles.data)}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">Carregando perfis disponíveis...</span>
|
||||
<span class="label-text-alt text-warning"
|
||||
>Carregando perfis disponíveis...</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -446,7 +447,9 @@
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold">Senha Gerada:</h3>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<code class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all">
|
||||
<code
|
||||
class="bg-base-300 px-3 py-2 rounded text-lg font-mono select-all"
|
||||
>
|
||||
{senhaGerada}
|
||||
</code>
|
||||
<button
|
||||
@@ -473,8 +476,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm mt-2">
|
||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará repassá-la
|
||||
manualmente ao usuário até que o SMTP seja configurado.
|
||||
⚠️ <strong>IMPORTANTE:</strong> Anote esta senha! Você precisará
|
||||
repassá-la manualmente ao usuário até que o SMTP seja configurado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -500,18 +503,27 @@
|
||||
<h3 class="font-bold">Informações Importantes</h3>
|
||||
<ul class="text-sm list-disc list-inside mt-2 space-y-1">
|
||||
<li>O usuário deverá alterar a senha no primeiro acesso</li>
|
||||
<li>As credenciais devem ser repassadas manualmente (por enquanto)</li>
|
||||
<li>
|
||||
Configure o SMTP em <a href="/ti/configuracoes-email" class="link"
|
||||
>Configurações de Email</a
|
||||
As credenciais devem ser repassadas manualmente (por enquanto)
|
||||
</li>
|
||||
<li>
|
||||
Configure o SMTP em <a
|
||||
href="/ti/configuracoes-email"
|
||||
class="link">Configurações de Email</a
|
||||
> para envio automático
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-8 pt-6 border-t border-base-300">
|
||||
<a href="/ti/usuarios" class="btn btn-ghost gap-2" class:btn-disabled={processando}>
|
||||
<div
|
||||
class="card-actions justify-end mt-8 pt-6 border-t border-base-300"
|
||||
>
|
||||
<a
|
||||
href="/ti/usuarios"
|
||||
class="btn btn-ghost gap-2"
|
||||
class:btn-disabled={processando}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
@@ -528,7 +540,11 @@
|
||||
</svg>
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2" disabled={processando}>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary gap-2"
|
||||
disabled={processando}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Criando Usuário...
|
||||
@@ -556,4 +572,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -294,12 +294,19 @@ export const login = mutation({
|
||||
timestamp: agora,
|
||||
});
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: true as const,
|
||||
token,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
@@ -568,12 +575,19 @@ export const loginComIP = internalMutation({
|
||||
timestamp: agora,
|
||||
});
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: true as const,
|
||||
token,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
@@ -688,11 +702,18 @@ export const verificarSessao = query({
|
||||
return { valido: false as const, motivo: "Role não encontrada" };
|
||||
}
|
||||
|
||||
// Obter matrícula do funcionário se houver
|
||||
let matricula: string | undefined = undefined;
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true as const,
|
||||
usuario: {
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
|
||||
@@ -94,7 +94,9 @@ export const criarConversa = mutation({
|
||||
conversaId,
|
||||
remetenteId: usuarioAtual._id,
|
||||
titulo: "Adicionado a grupo",
|
||||
descricao: `Você foi adicionado ao grupo "${args.nome || "Sem nome"}" por ${usuarioAtual.nome}`,
|
||||
descricao: `Você foi adicionado ao grupo "${
|
||||
args.nome || "Sem nome"
|
||||
}" por ${usuarioAtual.nome}`,
|
||||
lida: false,
|
||||
criadaEm: Date.now(),
|
||||
});
|
||||
@@ -226,8 +228,9 @@ export const enviarMensagem = mutation({
|
||||
for (const participanteId of conversa.participantes) {
|
||||
// ✅ MODIFICADO: Permite notificação para si mesmo se flag estiver ativa
|
||||
const ehOMesmoUsuario = participanteId === usuarioAtual._id;
|
||||
const deveCriarNotificacao = !ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
|
||||
const deveCriarNotificacao =
|
||||
!ehOMesmoUsuario || args.permitirNotificacaoParaSiMesmo;
|
||||
|
||||
if (deveCriarNotificacao) {
|
||||
const tipoNotificacao = args.mencoes?.includes(participanteId)
|
||||
? "mencao"
|
||||
@@ -318,7 +321,10 @@ export const cancelarMensagemAgendada = mutation({
|
||||
}
|
||||
|
||||
if (mensagem.remetenteId !== usuarioAtual._id) {
|
||||
return { sucesso: false, erro: "Você só pode cancelar suas próprias mensagens" };
|
||||
return {
|
||||
sucesso: false,
|
||||
erro: "Você só pode cancelar suas próprias mensagens",
|
||||
};
|
||||
}
|
||||
|
||||
if (!mensagem.agendadaPara) {
|
||||
@@ -611,16 +617,20 @@ export const listarConversas = query({
|
||||
// Para conversas individuais, pegar o outro usuário
|
||||
let outroUsuario = null;
|
||||
if (conversa.tipo === "individual") {
|
||||
const outroUsuarioRaw = participantes.find((p) => p?._id !== usuarioAtual._id);
|
||||
const outroUsuarioRaw = participantes.find(
|
||||
(p) => p?._id !== usuarioAtual._id
|
||||
);
|
||||
if (outroUsuarioRaw) {
|
||||
// 🔄 BUSCAR DADOS ATUALIZADOS DO USUÁRIO (não usar snapshot)
|
||||
const usuarioAtualizado = await ctx.db.get(outroUsuarioRaw._id);
|
||||
|
||||
|
||||
if (usuarioAtualizado) {
|
||||
// Adicionar URL da foto de perfil
|
||||
let fotoPerfilUrl = null;
|
||||
if (usuarioAtualizado.fotoPerfil) {
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtualizado.fotoPerfil);
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(
|
||||
usuarioAtualizado.fotoPerfil
|
||||
);
|
||||
}
|
||||
outroUsuario = {
|
||||
...usuarioAtualizado,
|
||||
@@ -643,7 +653,7 @@ export const listarConversas = query({
|
||||
.query("mensagens")
|
||||
.withIndex("by_conversa", (q) => q.eq("conversaId", conversa._id))
|
||||
.collect();
|
||||
|
||||
|
||||
const mensagens = todasMensagens.filter((m) => !m.agendadaPara);
|
||||
|
||||
let naoLidas = 0;
|
||||
@@ -756,7 +766,16 @@ export const obterMensagensAgendadas = query({
|
||||
*/
|
||||
export const listarAgendamentosChat = query({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<Array<Doc<"mensagens"> & { conversaInfo: Doc<"conversas"> | null; destinatarioInfo: Doc<"usuarios"> | null }>> => {
|
||||
handler: async (
|
||||
ctx
|
||||
): Promise<
|
||||
Array<
|
||||
Doc<"mensagens"> & {
|
||||
conversaInfo: Doc<"conversas"> | null;
|
||||
destinatarioInfo: Doc<"usuarios"> | null;
|
||||
}
|
||||
>
|
||||
> => {
|
||||
const usuarioAtual = await getUsuarioAutenticado(ctx);
|
||||
if (!usuarioAtual) {
|
||||
return [];
|
||||
@@ -912,20 +931,31 @@ export const listarTodosUsuarios = query({
|
||||
.withIndex("by_ativo", (q) => q.eq("ativo", true))
|
||||
.collect();
|
||||
|
||||
// Excluir o usuário atual
|
||||
return usuarios
|
||||
.filter((u) => u._id !== usuarioAtual._id)
|
||||
.map((u) => ({
|
||||
_id: u._id,
|
||||
nome: u.nome,
|
||||
email: u.email,
|
||||
matricula: u.matricula,
|
||||
avatar: u.avatar,
|
||||
fotoPerfil: u.fotoPerfil,
|
||||
statusPresenca: u.statusPresenca,
|
||||
statusMensagem: u.statusMensagem,
|
||||
setor: u.setor,
|
||||
}));
|
||||
// Excluir o usuário atual e buscar matrículas
|
||||
const usuariosComMatricula = await Promise.all(
|
||||
usuarios
|
||||
.filter((u) => u._id !== usuarioAtual._id)
|
||||
.map(async (u) => {
|
||||
let matricula: string | undefined = undefined;
|
||||
if (u.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(u.funcionarioId);
|
||||
matricula = funcionario?.matricula;
|
||||
}
|
||||
return {
|
||||
_id: u._id,
|
||||
nome: u.nome,
|
||||
email: u.email,
|
||||
matricula,
|
||||
avatar: u.avatar,
|
||||
fotoPerfil: u.fotoPerfil,
|
||||
statusPresenca: u.statusPresenca,
|
||||
statusMensagem: u.statusMensagem,
|
||||
setor: u.setor,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return usuariosComMatricula;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -88,9 +88,14 @@ export const listar = query({
|
||||
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: user.matricula,
|
||||
matricula: matricula || "",
|
||||
nome: user.nome,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,10 +78,15 @@ export const listarAtividades = query({
|
||||
const atividadesComUsuarios = await Promise.all(
|
||||
atividades.map(async (atividade) => {
|
||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
||||
let matricula = "N/A";
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: usuario?.matricula || "N/A",
|
||||
usuarioMatricula: matricula,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -157,10 +162,15 @@ export const obterHistoricoRecurso = query({
|
||||
const atividadesComUsuarios = await Promise.all(
|
||||
atividades.map(async (atividade) => {
|
||||
const usuario = await ctx.db.get(atividade.usuarioId);
|
||||
let matricula = "N/A";
|
||||
if (usuario?.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
matricula = funcionario?.matricula || "N/A";
|
||||
}
|
||||
return {
|
||||
...atividade,
|
||||
usuarioNome: usuario?.nome || "Usuário Desconhecido",
|
||||
usuarioMatricula: usuario?.matricula || "N/A",
|
||||
usuarioMatricula: matricula,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -30,18 +30,19 @@ export default defineSchema({
|
||||
simboloId: v.id("simbolos"),
|
||||
simboloTipo: simboloTipo,
|
||||
gestorId: v.optional(v.id("usuarios")),
|
||||
statusFerias: v.optional(v.union(
|
||||
v.literal("ativo"),
|
||||
v.literal("em_ferias")
|
||||
)),
|
||||
|
||||
statusFerias: v.optional(
|
||||
v.union(v.literal("ativo"), v.literal("em_ferias"))
|
||||
),
|
||||
|
||||
// Regime de trabalho (para cálculo correto de férias)
|
||||
regimeTrabalho: v.optional(v.union(
|
||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||
)),
|
||||
regimeTrabalho: v.optional(
|
||||
v.union(
|
||||
v.literal("clt"), // CLT - Consolidação das Leis do Trabalho
|
||||
v.literal("estatutario_pe"), // Servidor Público Estadual de Pernambuco
|
||||
v.literal("estatutario_federal"), // Servidor Público Federal
|
||||
v.literal("estatutario_municipal") // Servidor Público Municipal
|
||||
)
|
||||
),
|
||||
|
||||
// Dados Pessoais Adicionais (opcionais)
|
||||
nomePai: v.optional(v.string()),
|
||||
@@ -191,10 +192,7 @@ export default defineSchema({
|
||||
|
||||
licencas: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
tipo: v.union(
|
||||
v.literal("maternidade"),
|
||||
v.literal("paternidade")
|
||||
),
|
||||
tipo: v.union(v.literal("maternidade"), v.literal("paternidade")),
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
documentoId: v.optional(v.id("_storage")),
|
||||
@@ -237,11 +235,15 @@ export default defineSchema({
|
||||
data: v.number(),
|
||||
usuarioId: v.id("usuarios"),
|
||||
acao: v.string(),
|
||||
periodosAnteriores: v.optional(v.array(v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
}))),
|
||||
periodosAnteriores: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
dataInicio: v.string(),
|
||||
dataFim: v.string(),
|
||||
diasCorridos: v.number(),
|
||||
})
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
),
|
||||
@@ -343,7 +345,6 @@ export default defineSchema({
|
||||
|
||||
// Sistema de Autenticação e Controle de Acesso
|
||||
usuarios: defineTable({
|
||||
matricula: v.string(),
|
||||
senhaHash: v.string(), // Senha criptografada com bcrypt
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
@@ -380,7 +381,6 @@ export default defineSchema({
|
||||
notificacoesAtivadas: v.optional(v.boolean()),
|
||||
somNotificacao: v.optional(v.boolean()),
|
||||
})
|
||||
.index("by_matricula", ["matricula"])
|
||||
.index("by_email", ["email"])
|
||||
.index("by_role", ["roleId"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
@@ -500,7 +500,7 @@ export default defineSchema({
|
||||
.index("by_data_inicio", ["dataInicio"]),
|
||||
|
||||
// Perfis Customizados
|
||||
|
||||
|
||||
// Templates de Mensagens
|
||||
templatesMensagens: defineTable({
|
||||
codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc.
|
||||
@@ -663,8 +663,7 @@ export default defineSchema({
|
||||
mensagensPorMinuto: v.optional(v.number()),
|
||||
tempoRespostaMedio: v.optional(v.number()),
|
||||
errosCount: v.optional(v.number()),
|
||||
})
|
||||
.index("by_timestamp", ["timestamp"]),
|
||||
}).index("by_timestamp", ["timestamp"]),
|
||||
|
||||
alertConfigurations: defineTable({
|
||||
metricName: v.string(),
|
||||
@@ -681,8 +680,7 @@ export default defineSchema({
|
||||
notifyByChat: v.boolean(),
|
||||
createdBy: v.id("usuarios"),
|
||||
lastModified: v.number(),
|
||||
})
|
||||
.index("by_enabled", ["enabled"]),
|
||||
}).index("by_enabled", ["enabled"]),
|
||||
|
||||
alertHistory: defineTable({
|
||||
configId: v.id("alertConfigurations"),
|
||||
|
||||
@@ -316,7 +316,6 @@ export const seedDatabase = internalMutation({
|
||||
|
||||
const senhaInicial = await hashPassword("Mudar@123");
|
||||
await ctx.db.insert("usuarios", {
|
||||
matricula: funcionario.matricula,
|
||||
senhaHash: senhaInicial,
|
||||
nome: funcionario.nome,
|
||||
email: funcionario.email,
|
||||
|
||||
@@ -4,6 +4,21 @@ import { hashPassword, generateToken } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
import { api } from "./_generated/api";
|
||||
import type { QueryCtx } from "./_generated/server";
|
||||
|
||||
/**
|
||||
* Helper para obter a matrícula do usuário (do funcionário se houver)
|
||||
*/
|
||||
async function obterMatriculaUsuario(
|
||||
ctx: QueryCtx,
|
||||
usuario: Doc<"usuarios">
|
||||
): Promise<string | undefined> {
|
||||
if (usuario.funcionarioId) {
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
return funcionario?.matricula;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associar funcionário a um usuário
|
||||
@@ -30,8 +45,11 @@ export const associarFuncionario = mutation({
|
||||
.first();
|
||||
|
||||
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuarioExistente);
|
||||
throw new Error(
|
||||
`Este funcionário já está associado ao usuário: ${usuarioExistente.nome} (${usuarioExistente.matricula})`
|
||||
`Este funcionário já está associado ao usuário: ${
|
||||
usuarioExistente.nome
|
||||
}${matricula ? ` (${matricula})` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +84,6 @@ export const desassociarFuncionario = mutation({
|
||||
*/
|
||||
export const criar = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
roleId: v.id("roles"),
|
||||
@@ -78,16 +95,6 @@ export const criar = mutation({
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se matrícula já existe
|
||||
const existente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -103,7 +110,6 @@ export const criar = mutation({
|
||||
|
||||
// Criar usuário
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -194,9 +200,17 @@ export const listar = query({
|
||||
handler: async (ctx, args) => {
|
||||
let usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Filtrar por matrícula
|
||||
// Filtrar por matrícula (buscar no funcionário)
|
||||
if (args.matricula) {
|
||||
usuarios = usuarios.filter((u) => u.matricula.includes(args.matricula!));
|
||||
const usuariosComMatricula = await Promise.all(
|
||||
usuarios.map(async (u) => {
|
||||
const matricula = await obterMatriculaUsuario(ctx, u);
|
||||
return { usuario: u, matricula };
|
||||
})
|
||||
);
|
||||
usuarios = usuariosComMatricula
|
||||
.filter(({ matricula }) => matricula?.includes(args.matricula!))
|
||||
.map(({ usuario }) => usuario);
|
||||
}
|
||||
|
||||
// Filtrar por ativo
|
||||
@@ -206,20 +220,25 @@ export const listar = query({
|
||||
|
||||
// Buscar roles e funcionários
|
||||
const resultado = [];
|
||||
const usuariosSemRole: Array<{ nome: string; matricula: string; roleId: Id<"roles"> }> = [];
|
||||
const usuariosSemRole: Array<{
|
||||
nome: string;
|
||||
matricula: string;
|
||||
roleId: Id<"roles">;
|
||||
}> = [];
|
||||
|
||||
for (const usuario of usuarios) {
|
||||
try {
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
|
||||
|
||||
// Se a role não existe, criar uma role de erro mas ainda incluir o usuário
|
||||
if (!role) {
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||
usuariosSemRole.push({
|
||||
nome: usuario.nome,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matricula || "N/A",
|
||||
roleId: usuario.roleId,
|
||||
});
|
||||
|
||||
|
||||
// Filtrar por setor - se filtro está ativo e role não existe, pular
|
||||
if (args.setor) {
|
||||
continue;
|
||||
@@ -240,14 +259,19 @@ export const listar = query({
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
|
||||
console.error(
|
||||
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
// Criar role de erro (sem _creationTime pois a role não existe)
|
||||
resultado.push({
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matriculaUsuario,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
ativo: usuario.ativo,
|
||||
@@ -294,7 +318,10 @@ export const listar = query({
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`, error);
|
||||
console.error(
|
||||
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +332,18 @@ export const listar = query({
|
||||
nome: role.nome,
|
||||
nivel: role.nivel,
|
||||
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
|
||||
...(role.customizado !== undefined && { customizado: role.customizado }),
|
||||
...(role.customizado !== undefined && {
|
||||
customizado: role.customizado,
|
||||
}),
|
||||
...(role.editavel !== undefined && { editavel: role.editavel }),
|
||||
...(role.setor !== undefined && { setor: role.setor }),
|
||||
};
|
||||
|
||||
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
resultado.push({
|
||||
_id: usuario._id,
|
||||
matricula: usuario.matricula,
|
||||
matricula: matriculaUsuario,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
ativo: usuario.ativo,
|
||||
@@ -334,7 +365,12 @@ export const listar = query({
|
||||
if (usuariosSemRole.length > 0) {
|
||||
console.warn(
|
||||
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
|
||||
usuariosSemRole.map((u) => `${u.nome} (${u.matricula}) - RoleID: ${u.roleId}`)
|
||||
usuariosSemRole.map(
|
||||
(u) =>
|
||||
`${u.nome}${
|
||||
u.matricula !== "N/A" ? ` (${u.matricula})` : ""
|
||||
} - RoleID: ${u.roleId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,7 +595,9 @@ export const atualizarPerfil = mutation({
|
||||
}
|
||||
|
||||
// Atualizar apenas os campos fornecidos
|
||||
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = { atualizadoEm: Date.now() };
|
||||
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = {
|
||||
atualizadoEm: Date.now(),
|
||||
};
|
||||
|
||||
if (args.avatar !== undefined) updates.avatar = args.avatar;
|
||||
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
|
||||
@@ -591,7 +629,7 @@ export const obterPerfil = query({
|
||||
_id: v.id("usuarios"),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
matricula: v.string(),
|
||||
matricula: v.optional(v.string()),
|
||||
funcionarioId: v.optional(v.id("funcionarios")),
|
||||
avatar: v.optional(v.string()),
|
||||
fotoPerfil: v.optional(v.id("_storage")),
|
||||
@@ -675,11 +713,13 @@ export const obterPerfil = query({
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
|
||||
}
|
||||
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuarioAtual);
|
||||
|
||||
return {
|
||||
_id: usuarioAtual._id,
|
||||
nome: usuarioAtual.nome,
|
||||
email: usuarioAtual.email,
|
||||
matricula: usuarioAtual.matricula,
|
||||
matricula: matricula || undefined,
|
||||
funcionarioId: usuarioAtual.funcionarioId,
|
||||
avatar: usuarioAtual.avatar,
|
||||
fotoPerfil: usuarioAtual.fotoPerfil,
|
||||
@@ -735,11 +775,13 @@ export const listarParaChat = query({
|
||||
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
|
||||
}
|
||||
|
||||
const matricula = await obterMatriculaUsuario(ctx, usuario);
|
||||
|
||||
return {
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
matricula: usuario.matricula || undefined,
|
||||
matricula: matricula || undefined,
|
||||
avatar: usuario.avatar,
|
||||
fotoPerfil: usuario.fotoPerfil,
|
||||
fotoPerfilUrl,
|
||||
@@ -1035,7 +1077,6 @@ export const editarUsuario = mutation({
|
||||
*/
|
||||
export const criarAdminMaster = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
senha: v.optional(v.string()),
|
||||
@@ -1074,32 +1115,9 @@ export const criarAdminMaster = mutation({
|
||||
};
|
||||
}
|
||||
|
||||
// Se já existir usuário por matrícula, promove/atualiza
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
|
||||
const senhaHash = await hashPassword(senhaTemporaria);
|
||||
|
||||
if (existentePorMatricula) {
|
||||
await ctx.db.patch(existentePorMatricula._id, {
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return {
|
||||
sucesso: true as const,
|
||||
usuarioId: existentePorMatricula._id,
|
||||
senhaTemporaria,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -1108,7 +1126,6 @@ export const criarAdminMaster = mutation({
|
||||
if (existentePorEmail) {
|
||||
// Promove usuário existente por email
|
||||
await ctx.db.patch(existentePorEmail._id, {
|
||||
matricula: args.matricula,
|
||||
nome: args.nome,
|
||||
senhaHash,
|
||||
roleId: roleTIMaster._id,
|
||||
@@ -1125,7 +1142,6 @@ export const criarAdminMaster = mutation({
|
||||
|
||||
// Criar novo usuário TI Master
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -1194,7 +1210,6 @@ export const excluirUsuarioLogico = mutation({
|
||||
*/
|
||||
export const criarUsuarioCompleto = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
roleId: v.id("roles"),
|
||||
@@ -1212,16 +1227,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se matrícula já existe
|
||||
const existente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
@@ -1238,7 +1243,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
|
||||
// Criar usuário
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
@@ -1256,7 +1260,7 @@ export const criarUsuarioCompleto = mutation({
|
||||
args.criadoPorId,
|
||||
"criar",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }),
|
||||
JSON.stringify({ usuarioId, nome: args.nome }),
|
||||
usuarioId
|
||||
);
|
||||
|
||||
@@ -1272,7 +1276,6 @@ export const criarUsuarioCompleto = mutation({
|
||||
*/
|
||||
export const criarAdminPadrao = mutation({
|
||||
args: {
|
||||
matricula: v.optional(v.string()),
|
||||
nome: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
senha: v.optional(v.string()),
|
||||
@@ -1282,7 +1285,6 @@ export const criarAdminPadrao = mutation({
|
||||
usuarioId: v.optional(v.id("usuarios")),
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const matricula = args.matricula ?? "0000";
|
||||
const nome = args.nome ?? "Administrador Geral";
|
||||
const email = args.email ?? "admin@sgse.pe.gov.br";
|
||||
const senha = args.senha ?? "Admin@123";
|
||||
@@ -1306,12 +1308,7 @@ export const criarAdminPadrao = mutation({
|
||||
|
||||
if (!roleAdmin) return { sucesso: false };
|
||||
|
||||
// Verificar se já existe por matrícula ou email
|
||||
const existentePorMatricula = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", matricula))
|
||||
.first();
|
||||
|
||||
// Verificar se já existe por email
|
||||
const existentePorEmail = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", email))
|
||||
@@ -1319,10 +1316,8 @@ export const criarAdminPadrao = mutation({
|
||||
|
||||
const senhaHash = await hashPassword(senha);
|
||||
|
||||
if (existentePorMatricula || existentePorEmail) {
|
||||
const alvo = existentePorMatricula ?? existentePorEmail!;
|
||||
await ctx.db.patch(alvo._id, {
|
||||
matricula,
|
||||
if (existentePorEmail) {
|
||||
await ctx.db.patch(existentePorEmail._id, {
|
||||
nome,
|
||||
email,
|
||||
senhaHash,
|
||||
@@ -1331,11 +1326,10 @@ export const criarAdminPadrao = mutation({
|
||||
primeiroAcesso: false,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return { sucesso: true, usuarioId: alvo._id };
|
||||
return { sucesso: true, usuarioId: existentePorEmail._id };
|
||||
}
|
||||
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula,
|
||||
senhaHash,
|
||||
nome,
|
||||
email,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { v } from "convex/values";
|
||||
import { Id, Doc } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Verificar duplicatas de matrícula
|
||||
* Verificar duplicatas de matrícula (agora busca do funcionário associado)
|
||||
*/
|
||||
export const verificarDuplicatas = query({
|
||||
args: {},
|
||||
@@ -23,18 +23,27 @@ export const verificarDuplicatas = query({
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula
|
||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
||||
if (!acc[usuario.matricula]) {
|
||||
acc[usuario.matricula] = [];
|
||||
// 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;
|
||||
}
|
||||
acc[usuario.matricula].push({
|
||||
_id: usuario._id,
|
||||
nome: usuario.nome,
|
||||
email: usuario.email || "",
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, Array<{ _id: Id<"usuarios">; nome: string; email: string }>>);
|
||||
|
||||
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)
|
||||
@@ -50,7 +59,7 @@ export const verificarDuplicatas = query({
|
||||
});
|
||||
|
||||
/**
|
||||
* Remover duplicatas mantendo apenas o mais recente
|
||||
* Remover duplicatas mantendo apenas o mais recente (agora busca do funcionário associado)
|
||||
*/
|
||||
export const removerDuplicatas = internalMutation({
|
||||
args: {},
|
||||
@@ -61,14 +70,23 @@ export const removerDuplicatas = internalMutation({
|
||||
handler: async (ctx) => {
|
||||
const usuarios = await ctx.db.query("usuarios").collect();
|
||||
|
||||
// Agrupar por matrícula
|
||||
const gruposPorMatricula = usuarios.reduce((acc, usuario) => {
|
||||
if (!acc[usuario.matricula]) {
|
||||
acc[usuario.matricula] = [];
|
||||
// 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;
|
||||
}
|
||||
acc[usuario.matricula].push(usuario);
|
||||
return acc;
|
||||
}, {} as Record<string, Doc<"usuarios">[]>);
|
||||
|
||||
if (matricula) {
|
||||
if (!gruposPorMatricula[matricula]) {
|
||||
gruposPorMatricula[matricula] = [];
|
||||
}
|
||||
gruposPorMatricula[matricula].push(usuario);
|
||||
}
|
||||
}
|
||||
|
||||
let removidos = 0;
|
||||
const matriculasDuplicadas: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user