feat: add svelte-sonner dependency and enhance NotificationBell component

- Added `svelte-sonner` to dependencies for improved notification handling.
- Refactored the `NotificationBell.svelte` component for better readability and maintainability, including code formatting and structure improvements.
- Updated `package.json` and `bun.lock` to reflect the new dependency.
This commit is contained in:
2025-10-30 14:55:51 -03:00
parent 1b751efc5e
commit 5469c50d90
8 changed files with 1393 additions and 559 deletions

View File

@@ -5,18 +5,27 @@
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { authStore } from "$lib/stores/auth.svelte";
// Queries e Client // Queries e Client
const client = useConvexClient(); const client = useConvexClient();
const notificacoesQuery = useQuery(api.chat.obterNotificacoes, { apenasPendentes: true }); const notificacoesQuery = useQuery(api.chat.obterNotificacoes, {
apenasPendentes: true,
});
const countQuery = useQuery(api.chat.contarNotificacoesNaoLidas, {}); const countQuery = useQuery(api.chat.contarNotificacoesNaoLidas, {});
let dropdownOpen = $state(false); let dropdownOpen = $state(false);
let notificacoesFerias = $state<any[]>([]); let notificacoesFerias = $state<any[]>([]);
// Helpers para obter valores das queries // Helpers para obter valores das queries
const count = $derived((typeof countQuery === 'number' ? countQuery : countQuery?.data) ?? 0); const count = $derived(
const notificacoes = $derived((Array.isArray(notificacoesQuery) ? notificacoesQuery : notificacoesQuery?.data) ?? []); (typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0
);
const notificacoes = $derived(
(Array.isArray(notificacoesQuery)
? notificacoesQuery
: notificacoesQuery?.data) ?? []
);
// Atualizar contador no store // Atualizar contador no store
$effect(() => { $effect(() => {
@@ -27,11 +36,15 @@
// Buscar notificações de férias // Buscar notificações de férias
async function buscarNotificacoesFerias() { async function buscarNotificacoesFerias() {
try { try {
const usuarioStore = await import("$lib/stores/auth.svelte").then(m => m.authStore); const usuarioStore = authStore;
if (usuarioStore.usuario?._id) { if (usuarioStore.usuario?._id) {
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, { const notifsFerias = await client.query(
usuarioId: usuarioStore.usuario._id as any, api.ferias.obterNotificacoesNaoLidas,
}); {
usuarioId: usuarioStore.usuario._id as any,
}
);
notificacoesFerias = notifsFerias || []; notificacoesFerias = notifsFerias || [];
} }
} catch (e) { } catch (e) {
@@ -61,19 +74,25 @@
await client.mutation(api.chat.marcarTodasNotificacoesLidas, {}); await client.mutation(api.chat.marcarTodasNotificacoesLidas, {});
// Marcar todas as notificações de férias como lidas // Marcar todas as notificações de férias como lidas
for (const notif of notificacoesFerias) { for (const notif of notificacoesFerias) {
await client.mutation(api.ferias.marcarComoLida, { notificacaoId: notif._id }); await client.mutation(api.ferias.marcarComoLida, {
notificacaoId: notif._id,
});
} }
dropdownOpen = false; dropdownOpen = false;
await buscarNotificacoesFerias(); await buscarNotificacoesFerias();
} }
async function handleClickNotificacao(notificacaoId: string) { async function handleClickNotificacao(notificacaoId: string) {
await client.mutation(api.chat.marcarNotificacaoLida, { notificacaoId: notificacaoId as any }); await client.mutation(api.chat.marcarNotificacaoLida, {
notificacaoId: notificacaoId as any,
});
dropdownOpen = false; dropdownOpen = false;
} }
async function handleClickNotificacaoFerias(notificacaoId: string) { async function handleClickNotificacaoFerias(notificacaoId: string) {
await client.mutation(api.ferias.marcarComoLida, { notificacaoId: notificacaoId as any }); await client.mutation(api.ferias.marcarComoLida, {
notificacaoId: notificacaoId as any,
});
await buscarNotificacoesFerias(); await buscarNotificacoesFerias();
dropdownOpen = false; dropdownOpen = false;
// Redirecionar para a página de férias // Redirecionar para a página de férias
@@ -98,43 +117,6 @@
}); });
</script> </script>
<style>
@keyframes badge-bounce {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes pulse-ring-subtle {
0%, 100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.05);
}
}
@keyframes bell-ring {
0%, 100% {
transform: rotate(0deg);
}
10%, 30% {
transform: rotate(-10deg);
}
20%, 40% {
transform: rotate(10deg);
}
50% {
transform: rotate(0deg);
}
}
</style>
<div class="dropdown dropdown-end notification-bell"> <div class="dropdown dropdown-end notification-bell">
<!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) --> <!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) -->
<button <button
@@ -146,25 +128,39 @@
aria-label="Notificações" aria-label="Notificações"
> >
<!-- Efeito de brilho no hover --> <!-- Efeito de brilho no hover -->
<div class="absolute inset-0 bg-gradient-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> <div
class="absolute inset-0 bg-gradient-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
></div>
<!-- Anel de pulso sutil --> <!-- Anel de pulso sutil -->
<div class="absolute inset-0 rounded-2xl" style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"></div> <div
class="absolute inset-0 rounded-2xl"
style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
></div>
<!-- Glow effect quando tem notificações --> <!-- Glow effect quando tem notificações -->
{#if count && count > 0} {#if count && count > 0}
<div class="absolute inset-0 rounded-2xl bg-error/30 blur-lg animate-pulse"></div> <div
class="absolute inset-0 rounded-2xl bg-error/30 blur-lg animate-pulse"
></div>
{/if} {/if}
<!-- Ícone do sino PREENCHIDO moderno --> <!-- Ícone do sino PREENCHIDO moderno -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-7 h-7 text-white relative z-10 transition-all duration-300 group-hover:scale-110" class="w-7 h-7 text-white relative z-10 transition-all duration-300 group-hover:scale-110"
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3)); animation: {count && count > 0 ? 'bell-ring 2s ease-in-out infinite' : 'none'};" style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3)); animation: {count &&
count > 0
? 'bell-ring 2s ease-in-out infinite'
: 'none'};"
> >
<path fill-rule="evenodd" d="M5.25 9a6.75 6.75 0 0113.5 0v.75c0 2.123.8 4.057 2.118 5.52a.75.75 0 01-.297 1.206c-1.544.57-3.16.99-4.831 1.243a3.75 3.75 0 11-7.48 0 24.585 24.585 0 01-4.831-1.244.75.75 0 01-.298-1.205A8.217 8.217 0 005.25 9.75V9zm4.502 8.9a2.25 2.25 0 104.496 0 25.057 25.057 0 01-4.496 0z" clip-rule="evenodd" /> <path
fill-rule="evenodd"
d="M5.25 9a6.75 6.75 0 0113.5 0v.75c0 2.123.8 4.057 2.118 5.52a.75.75 0 01-.297 1.206c-1.544.57-3.16.99-4.831 1.243a3.75 3.75 0 11-7.48 0 24.585 24.585 0 01-4.831-1.244.75.75 0 01-.298-1.205A8.217 8.217 0 005.25 9.75V9zm4.502 8.9a2.25 2.25 0 104.496 0 25.057 25.057 0 01-4.496 0z"
clip-rule="evenodd"
/>
</svg> </svg>
<!-- Badge premium MODERNO com gradiente --> <!-- Badge premium MODERNO com gradiente -->
@@ -186,7 +182,9 @@
class="dropdown-content z-50 mt-3 w-80 max-h-96 overflow-auto rounded-box bg-base-100 p-2 shadow-2xl border border-base-300" class="dropdown-content z-50 mt-3 w-80 max-h-96 overflow-auto rounded-box bg-base-100 p-2 shadow-2xl border border-base-300"
> >
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between px-4 py-2 border-b border-base-300"> <div
class="flex items-center justify-between px-4 py-2 border-b border-base-300"
>
<h3 class="text-lg font-semibold">Notificações</h3> <h3 class="text-lg font-semibold">Notificações</h3>
{#if count > 0} {#if count > 0}
<button <button
@@ -297,11 +295,22 @@
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<!-- Ícone --> <!-- Ícone -->
<div class="flex-shrink-0 mt-1"> <div class="flex-shrink-0 mt-1">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg> </svg>
</div> </div>
<!-- Conteúdo --> <!-- Conteúdo -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm font-medium text-base-content"> <p class="text-sm font-medium text-base-content">
@@ -311,7 +320,7 @@
{formatarTempo(notificacao._creationTime)} {formatarTempo(notificacao._creationTime)}
</p> </p>
</div> </div>
<!-- Badge --> <!-- Badge -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="badge badge-primary badge-xs"></div> <div class="badge badge-primary badge-xs"></div>
@@ -346,3 +355,44 @@
{/if} {/if}
</div> </div>
<style>
@keyframes badge-bounce {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
@keyframes pulse-ring-subtle {
0%,
100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.05);
}
}
@keyframes bell-ring {
0%,
100% {
transform: rotate(0deg);
}
10%,
30% {
transform: rotate(-10deg);
}
20%,
40% {
transform: rotate(10deg);
}
50% {
transform: rotate(0deg);
}
}
</style>

View File

@@ -1,12 +1,13 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { Id } from "@sgse-app/backend/convex/betterAuth/_generated/dataModel";
interface Usuario { interface Usuario {
_id: string; _id: string;
matricula: string; matricula: string;
nome: string; nome: string;
email: string; email: string;
funcionarioId?: string; funcionarioId?: Id<"funcionarios">;
role: { role: {
_id: string; _id: string;
nome: string; nome: string;
@@ -100,19 +101,25 @@ class AuthStore {
// Importação dinâmica do convex para evitar problemas de SSR // Importação dinâmica do convex para evitar problemas de SSR
const { ConvexHttpClient } = await import("convex/browser"); const { ConvexHttpClient } = await import("convex/browser");
const { api } = await import("@sgse-app/backend/convex/_generated/api"); const { api } = await import("@sgse-app/backend/convex/_generated/api");
const client = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL); const client = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL);
client.setAuth(this.state.token); client.setAuth(this.state.token);
const usuarioAtualizado = await client.query(api.usuarios.obterPerfil, {}); const usuarioAtualizado = await client.query(
api.usuarios.obterPerfil,
{}
);
if (usuarioAtualizado && this.state.usuario) { if (usuarioAtualizado && this.state.usuario) {
this.state.usuario = { this.state.usuario = {
...this.state.usuario, ...this.state.usuario,
...usuarioAtualizado, ...usuarioAtualizado,
}; };
localStorage.setItem("auth_usuario", JSON.stringify(this.state.usuario)); localStorage.setItem(
"auth_usuario",
JSON.stringify(this.state.usuario)
);
} }
} catch (error) { } catch (error) {
console.error("Erro ao atualizar perfil:", error); console.error("Erro ao atualizar perfil:", error);
@@ -139,4 +146,3 @@ class AuthStore {
} }
export const authStore = new AuthStore(); export const authStore = new AuthStore();

File diff suppressed because it is too large Load Diff

View File

@@ -9,65 +9,74 @@
const client = useConvexClient(); const client = useConvexClient();
const usuarios = useQuery(api.usuarios.listar, {}); const usuarios = useQuery(api.usuarios.listar, {});
let filtroNome = $state(""); let filtroNome = $state("");
let filtroStatus = $state<"todos" | "ativo" | "bloqueado" | "inativo">("todos"); let filtroStatus = $state<"todos" | "ativo" | "bloqueado" | "inativo">(
"todos"
);
let usuarioSelecionado = $state<any>(null); let usuarioSelecionado = $state<any>(null);
let modalAberto = $state(false); let modalAberto = $state(false);
let modalAcao = $state<"bloquear" | "desbloquear" | "reset" | "associar">("bloquear"); let modalAcao = $state<"bloquear" | "desbloquear" | "reset" | "associar">(
"bloquear"
);
let motivo = $state(""); let motivo = $state("");
let processando = $state(false); let processando = $state(false);
// Modal de associar funcionário // Modal de associar funcionário
let modalAssociarAberto = $state(false); let modalAssociarAberto = $state(false);
let usuarioParaAssociar = $state<any>(null); let usuarioParaAssociar = $state<any>(null);
let funcionarioSelecionadoId = $state<string>(""); let funcionarioSelecionadoId = $state<string>("");
let buscaFuncionario = $state(""); let buscaFuncionario = $state("");
// Query de funcionários // Query de funcionários
const funcionarios = useQuery(api.funcionarios.list, {}); const funcionarios = useQuery(api.funcionarios.getAll, {});
// Usuários filtrados // Usuários filtrados
const usuariosFiltrados = $derived.by(() => { const usuariosFiltrados = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return []; if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
return usuarios.data.filter(u => { return usuarios.data.filter((u) => {
const matchNome = !filtroNome || const matchNome =
!filtroNome ||
u.nome.toLowerCase().includes(filtroNome.toLowerCase()) || u.nome.toLowerCase().includes(filtroNome.toLowerCase()) ||
u.matricula.includes(filtroNome) || u.matricula.includes(filtroNome) ||
u.email?.toLowerCase().includes(filtroNome.toLowerCase()); u.email?.toLowerCase().includes(filtroNome.toLowerCase());
const matchStatus = filtroStatus === "todos" || const matchStatus =
filtroStatus === "todos" ||
(filtroStatus === "ativo" && u.ativo && !u.bloqueado) || (filtroStatus === "ativo" && u.ativo && !u.bloqueado) ||
(filtroStatus === "bloqueado" && u.bloqueado) || (filtroStatus === "bloqueado" && u.bloqueado) ||
(filtroStatus === "inativo" && !u.ativo); (filtroStatus === "inativo" && !u.ativo);
return matchNome && matchStatus; return matchNome && matchStatus;
}); });
}); });
// Funcionários filtrados (sem associação ou disponíveis) // Funcionários filtrados (sem associação ou disponíveis)
const funcionariosFiltrados = $derived.by(() => { const funcionariosFiltrados = $derived.by(() => {
if (!funcionarios?.data || !Array.isArray(funcionarios.data)) return []; if (!funcionarios?.data || !Array.isArray(funcionarios.data)) return [];
return funcionarios.data.filter(f => { return funcionarios.data
// Filtro por busca .filter((f) => {
const matchBusca = !buscaFuncionario || // Filtro por busca
f.nome.toLowerCase().includes(buscaFuncionario.toLowerCase()) || const matchBusca =
f.cpf?.includes(buscaFuncionario) || !buscaFuncionario ||
f.matricula?.includes(buscaFuncionario); f.nome.toLowerCase().includes(buscaFuncionario.toLowerCase()) ||
f.cpf?.includes(buscaFuncionario) ||
return matchBusca; f.matricula?.includes(buscaFuncionario);
}).sort((a, b) => a.nome.localeCompare(b.nome));
return matchBusca;
})
.sort((a, b) => a.nome.localeCompare(b.nome));
}); });
const stats = $derived.by(() => { const stats = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return null; if (!usuarios?.data || !Array.isArray(usuarios.data)) return null;
return { return {
total: usuarios.data.length, total: usuarios.data.length,
ativos: usuarios.data.filter(u => u.ativo && !u.bloqueado).length, ativos: usuarios.data.filter((u) => u.ativo && !u.bloqueado).length,
bloqueados: usuarios.data.filter(u => u.bloqueado).length, bloqueados: usuarios.data.filter((u) => u.bloqueado).length,
inativos: usuarios.data.filter(u => !u.ativo).length inativos: usuarios.data.filter((u) => !u.ativo).length,
}; };
}); });
@@ -83,31 +92,31 @@
usuarioSelecionado = null; usuarioSelecionado = null;
motivo = ""; motivo = "";
} }
function abrirModalAssociar(usuario: any) { function abrirModalAssociar(usuario: any) {
usuarioParaAssociar = usuario; usuarioParaAssociar = usuario;
funcionarioSelecionadoId = usuario.funcionarioId || ""; funcionarioSelecionadoId = usuario.funcionarioId || "";
buscaFuncionario = ""; buscaFuncionario = "";
modalAssociarAberto = true; modalAssociarAberto = true;
} }
function fecharModalAssociar() { function fecharModalAssociar() {
modalAssociarAberto = false; modalAssociarAberto = false;
usuarioParaAssociar = null; usuarioParaAssociar = null;
funcionarioSelecionadoId = ""; funcionarioSelecionadoId = "";
buscaFuncionario = ""; buscaFuncionario = "";
} }
async function associarFuncionario() { async function associarFuncionario() {
if (!usuarioParaAssociar || !funcionarioSelecionadoId) return; if (!usuarioParaAssociar || !funcionarioSelecionadoId) return;
processando = true; processando = true;
try { try {
await client.mutation(api.usuarios.associarFuncionario, { await client.mutation(api.usuarios.associarFuncionario, {
usuarioId: usuarioParaAssociar._id as Id<"usuarios">, usuarioId: usuarioParaAssociar._id as Id<"usuarios">,
funcionarioId: funcionarioSelecionadoId as Id<"funcionarios"> funcionarioId: funcionarioSelecionadoId as Id<"funcionarios">,
}); });
alert("Funcionário associado com sucesso!"); alert("Funcionário associado com sucesso!");
fecharModalAssociar(); fecharModalAssociar();
} catch (error: any) { } catch (error: any) {
@@ -116,18 +125,19 @@
processando = false; processando = false;
} }
} }
async function desassociarFuncionario() { async function desassociarFuncionario() {
if (!usuarioParaAssociar) return; if (!usuarioParaAssociar) return;
if (!confirm("Deseja realmente desassociar o funcionário deste usuário?")) return; if (!confirm("Deseja realmente desassociar o funcionário deste usuário?"))
return;
processando = true; processando = true;
try { try {
await client.mutation(api.usuarios.desassociarFuncionario, { await client.mutation(api.usuarios.desassociarFuncionario, {
usuarioId: usuarioParaAssociar._id as Id<"usuarios"> usuarioId: usuarioParaAssociar._id as Id<"usuarios">,
}); });
alert("Funcionário desassociado com sucesso!"); alert("Funcionário desassociado com sucesso!");
fecharModalAssociar(); fecharModalAssociar();
} catch (error: any) { } catch (error: any) {
@@ -139,32 +149,32 @@
async function executarAcao() { async function executarAcao() {
if (!usuarioSelecionado) return; if (!usuarioSelecionado) return;
if (!authStore.usuario) { if (!authStore.usuario) {
alert("Usuário não autenticado"); alert("Usuário não autenticado");
return; return;
} }
processando = true; processando = true;
try { try {
if (modalAcao === "bloquear") { if (modalAcao === "bloquear") {
await client.mutation(api.usuarios.bloquearUsuario, { await client.mutation(api.usuarios.bloquearUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">, usuarioId: usuarioSelecionado._id as Id<"usuarios">,
motivo, motivo,
bloqueadoPorId: authStore.usuario._id as Id<"usuarios"> bloqueadoPorId: authStore.usuario._id as Id<"usuarios">,
}); });
} else if (modalAcao === "desbloquear") { } else if (modalAcao === "desbloquear") {
await client.mutation(api.usuarios.desbloquearUsuario, { await client.mutation(api.usuarios.desbloquearUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">, usuarioId: usuarioSelecionado._id as Id<"usuarios">,
desbloqueadoPorId: authStore.usuario._id as Id<"usuarios"> desbloqueadoPorId: authStore.usuario._id as Id<"usuarios">,
}); });
} else if (modalAcao === "reset") { } else if (modalAcao === "reset") {
await client.mutation(api.usuarios.resetarSenhaUsuario, { await client.mutation(api.usuarios.resetarSenhaUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">, usuarioId: usuarioSelecionado._id as Id<"usuarios">,
resetadoPorId: authStore.usuario._id as Id<"usuarios"> resetadoPorId: authStore.usuario._id as Id<"usuarios">,
}); });
} }
fecharModal(); fecharModal();
} catch (error) { } catch (error) {
console.error("Erro ao executar ação:", error); console.error("Erro ao executar ação:", error);
@@ -183,8 +193,19 @@
<p class="text-base-content/60 mt-1">Gerenciar usuários do sistema</p> <p class="text-base-content/60 mt-1">Gerenciar usuários do sistema</p>
</div> </div>
<a href="/ti/usuarios/criar" class="btn btn-primary"> <a href="/ti/usuarios/criar" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg> </svg>
Criar Usuário Criar Usuário
</a> </a>
@@ -220,20 +241,24 @@
<label class="label" for="buscar-usuario-input"> <label class="label" for="buscar-usuario-input">
<span class="label-text">Buscar por nome, matrícula ou email</span> <span class="label-text">Buscar por nome, matrícula ou email</span>
</label> </label>
<input <input
id="buscar-usuario-input" id="buscar-usuario-input"
type="text" type="text"
bind:value={filtroNome} bind:value={filtroNome}
placeholder="Digite para buscar..." placeholder="Digite para buscar..."
class="input input-bordered" class="input input-bordered"
/> />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label" for="filtro-status-select"> <label class="label" for="filtro-status-select">
<span class="label-text">Filtrar por status</span> <span class="label-text">Filtrar por status</span>
</label> </label>
<select id="filtro-status-select" bind:value={filtroStatus} class="select select-bordered"> <select
id="filtro-status-select"
bind:value={filtroStatus}
class="select select-bordered"
>
<option value="todos">Todos</option> <option value="todos">Todos</option>
<option value="ativo">Ativos</option> <option value="ativo">Ativos</option>
<option value="bloqueado">Bloqueados</option> <option value="bloqueado">Bloqueados</option>
@@ -250,7 +275,7 @@
<h2 class="card-title mb-4"> <h2 class="card-title mb-4">
Usuários ({usuariosFiltrados.length}) Usuários ({usuariosFiltrados.length})
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra"> <table class="table table-zebra">
<thead> <thead>
@@ -270,67 +295,138 @@
<td>{usuario.nome}</td> <td>{usuario.nome}</td>
<td>{usuario.email || "-"}</td> <td>{usuario.email || "-"}</td>
<td> <td>
{#if usuario.funcionarioId} {#if usuario.funcionario?._id}
<div class="badge badge-success gap-2"> <div class="badge badge-success gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg> </svg>
Associado Associado
</div> </div>
{:else} {:else}
<div class="badge badge-warning gap-2"> <div class="badge badge-warning gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg> </svg>
Não associado Não associado
</div> </div>
{/if} {/if}
</td> </td>
<td> <td>
<UserStatusBadge ativo={usuario.ativo} bloqueado={usuario.bloqueado} /> <UserStatusBadge
ativo={usuario.ativo}
bloqueado={usuario.bloqueado}
/>
</td> </td>
<td> <td>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
<!-- Botão Associar Funcionário --> <!-- Botão Associar Funcionário -->
<button <button
class="btn btn-sm btn-info" class="btn btn-sm btn-info"
onclick={() => abrirModalAssociar(usuario)} onclick={() => abrirModalAssociar(usuario)}
title={usuario.funcionarioId ? "Alterar funcionário associado" : "Associar funcionário"} title={usuario.funcionario?._id
? "Alterar funcionário associado"
: "Associar funcionário"}
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg> </svg>
{usuario.funcionarioId ? "Alterar" : "Associar"} {usuario.funcionario?._id ? "Alterar" : "Associar"}
</button> </button>
{#if usuario.bloqueado} {#if usuario.bloqueado}
<button <button
class="btn btn-sm btn-success" class="btn btn-sm btn-success"
onclick={() => abrirModal(usuario, "desbloquear")} onclick={() => abrirModal(usuario, "desbloquear")}
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" /> xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
/>
</svg> </svg>
Desbloquear Desbloquear
</button> </button>
{:else} {:else}
<button <button
class="btn btn-sm btn-error" class="btn btn-sm btn-error"
onclick={() => abrirModal(usuario, "bloquear")} onclick={() => abrirModal(usuario, "bloquear")}
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg> </svg>
Bloquear Bloquear
</button> </button>
{/if} {/if}
<button <button
class="btn btn-sm btn-warning" class="btn btn-sm btn-warning"
onclick={() => abrirModal(usuario, "reset")} onclick={() => abrirModal(usuario, "reset")}
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /> xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg> </svg>
Reset Senha Reset Senha
</button> </button>
@@ -356,14 +452,17 @@
<div class="modal modal-open"> <div class="modal modal-open">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg mb-4"> <h3 class="font-bold text-lg mb-4">
{modalAcao === "bloquear" ? "Bloquear Usuário" : {modalAcao === "bloquear"
modalAcao === "desbloquear" ? "Desbloquear Usuário" : ? "Bloquear Usuário"
"Resetar Senha"} : modalAcao === "desbloquear"
? "Desbloquear Usuário"
: "Resetar Senha"}
</h3> </h3>
<div class="mb-4"> <div class="mb-4">
<p class="text-base-content/80"> <p class="text-base-content/80">
<strong>Usuário:</strong> {usuarioSelecionado?.nome} ({usuarioSelecionado?.matricula}) <strong>Usuário:</strong>
{usuarioSelecionado?.nome} ({usuarioSelecionado?.matricula})
</p> </p>
</div> </div>
@@ -372,10 +471,10 @@
<label class="label" for="motivo-bloqueio-textarea"> <label class="label" for="motivo-bloqueio-textarea">
<span class="label-text">Motivo do bloqueio *</span> <span class="label-text">Motivo do bloqueio *</span>
</label> </label>
<textarea <textarea
id="motivo-bloqueio-textarea" id="motivo-bloqueio-textarea"
bind:value={motivo} bind:value={motivo}
class="textarea textarea-bordered" class="textarea textarea-bordered"
placeholder="Digite o motivo..." placeholder="Digite o motivo..."
rows="3" rows="3"
></textarea> ></textarea>
@@ -384,22 +483,32 @@
{#if modalAcao === "reset"} {#if modalAcao === "reset"}
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg> </svg>
<span>Uma senha temporária será gerada automaticamente.</span> <span>Uma senha temporária será gerada automaticamente.</span>
</div> </div>
{/if} {/if}
<div class="modal-action"> <div class="modal-action">
<button <button
class="btn btn-ghost" class="btn btn-ghost"
onclick={fecharModal} onclick={fecharModal}
disabled={processando} disabled={processando}
> >
Cancelar Cancelar
</button> </button>
<button <button
class="btn btn-primary" class="btn btn-primary"
onclick={executarAcao} onclick={executarAcao}
disabled={processando || (modalAcao === "bloquear" && !motivo.trim())} disabled={processando || (modalAcao === "bloquear" && !motivo.trim())}
@@ -421,21 +530,33 @@
{#if modalAssociarAberto && usuarioParaAssociar} {#if modalAssociarAberto && usuarioParaAssociar}
<div class="modal modal-open"> <div class="modal modal-open">
<div class="modal-box max-w-2xl"> <div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4"> <h3 class="font-bold text-lg mb-4">Associar Funcionário ao Usuário</h3>
Associar Funcionário ao Usuário
</h3>
<div class="mb-6"> <div class="mb-6">
<p class="text-base-content/80 mb-2"> <p class="text-base-content/80 mb-2">
<strong>Usuário:</strong> {usuarioParaAssociar.nome} ({usuarioParaAssociar.matricula}) <strong>Usuário:</strong>
{usuarioParaAssociar.nome} ({usuarioParaAssociar.matricula})
</p> </p>
{#if usuarioParaAssociar.funcionarioId} {#if usuarioParaAssociar.funcionario?._id}
<div class="alert alert-info"> <div class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg> </svg>
<span>Este usuário já possui um funcionário associado. Você pode alterá-lo ou desassociá-lo.</span> <span
>Este usuário já possui um funcionário associado. Você pode
alterá-lo ou desassociá-lo.</span
>
</div> </div>
{/if} {/if}
</div> </div>
@@ -445,11 +566,11 @@
<label for="busca-funcionario" class="label"> <label for="busca-funcionario" class="label">
<span class="label-text">Buscar Funcionário</span> <span class="label-text">Buscar Funcionário</span>
</label> </label>
<input <input
id="busca-funcionario" id="busca-funcionario"
type="text" type="text"
bind:value={buscaFuncionario} bind:value={buscaFuncionario}
placeholder="Digite nome, CPF ou matrícula..." placeholder="Digite nome, CPF ou matrícula..."
class="input input-bordered" class="input input-bordered"
/> />
</div> </div>
@@ -459,21 +580,25 @@
<div class="label"> <div class="label">
<span class="label-text">Selecione o Funcionário *</span> <span class="label-text">Selecione o Funcionário *</span>
</div> </div>
<div class="border rounded-lg max-h-96 overflow-y-auto"> <div class="border rounded-lg max-h-96 overflow-y-auto">
{#if funcionariosFiltrados.length === 0} {#if funcionariosFiltrados.length === 0}
<div class="p-4 text-center text-base-content/60"> <div class="p-4 text-center text-base-content/60">
{buscaFuncionario ? "Nenhum funcionário encontrado com esse critério" : "Carregando funcionários..."} {buscaFuncionario
? "Nenhum funcionário encontrado com esse critério"
: "Carregando funcionários..."}
</div> </div>
{:else} {:else}
{#each funcionariosFiltrados as func} {#each funcionariosFiltrados as func}
<label class="flex items-center gap-3 p-3 hover:bg-base-200 cursor-pointer border-b last:border-b-0"> <label
<input class="flex items-center gap-3 p-3 hover:bg-base-200 cursor-pointer border-b last:border-b-0"
type="radio" >
name="funcionario" <input
type="radio"
name="funcionario"
value={func._id} value={func._id}
bind:group={funcionarioSelecionadoId} bind:group={funcionarioSelecionadoId}
class="radio radio-primary" class="radio radio-primary"
/> />
<div class="flex-1"> <div class="flex-1">
<div class="font-semibold">{func.nome}</div> <div class="font-semibold">{func.nome}</div>
@@ -484,7 +609,9 @@
{/if} {/if}
</div> </div>
{#if func.descricaoCargo} {#if func.descricaoCargo}
<div class="text-xs text-base-content/60">{func.descricaoCargo}</div> <div class="text-xs text-base-content/60">
{func.descricaoCargo}
</div>
{/if} {/if}
</div> </div>
</label> </label>
@@ -495,16 +622,16 @@
<!-- Ações --> <!-- Ações -->
<div class="modal-action"> <div class="modal-action">
<button <button
class="btn btn-ghost" class="btn btn-ghost"
onclick={fecharModalAssociar} onclick={fecharModalAssociar}
disabled={processando} disabled={processando}
> >
Cancelar Cancelar
</button> </button>
{#if usuarioParaAssociar.funcionarioId} {#if usuarioParaAssociar.funcionarioId}
<button <button
class="btn btn-error" class="btn btn-error"
onclick={desassociarFuncionario} onclick={desassociarFuncionario}
disabled={processando} disabled={processando}
@@ -515,8 +642,8 @@
Desassociar Desassociar
</button> </button>
{/if} {/if}
<button <button
class="btn btn-primary" class="btn btn-primary"
onclick={associarFuncionario} onclick={associarFuncionario}
disabled={processando || !funcionarioSelecionadoId} disabled={processando || !funcionarioSelecionadoId}
@@ -533,4 +660,3 @@
<div class="modal-backdrop" onclick={fecharModalAssociar}></div> <div class="modal-backdrop" onclick={fecharModalAssociar}></div>
</div> </div>
{/if} {/if}

View File

@@ -8,6 +8,7 @@
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"lucide-svelte": "^0.548.0", "lucide-svelte": "^0.548.0",
"svelte-chartjs": "^3.1.5", "svelte-chartjs": "^3.1.5",
"svelte-sonner": "^1.0.5",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.2", "@biomejs/biome": "^2.3.2",

View File

@@ -24,7 +24,8 @@
"@tanstack/svelte-form": "^1.23.8", "@tanstack/svelte-form": "^1.23.8",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"lucide-svelte": "^0.548.0", "lucide-svelte": "^0.548.0",
"svelte-chartjs": "^3.1.5" "svelte-chartjs": "^3.1.5",
"svelte-sonner": "^1.0.5"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-win32-x64-msvc": "^4.52.5" "@rollup/rollup-win32-x64-msvc": "^4.52.5"

View File

@@ -16,23 +16,25 @@ export const listarTodas = query({
returns: v.array(v.any()), returns: v.array(v.any()),
handler: async (ctx) => { handler: async (ctx) => {
const solicitacoes = await ctx.db.query("solicitacoesFerias").collect(); const solicitacoes = await ctx.db.query("solicitacoesFerias").collect();
const solicitacoesComDetalhes = await Promise.all( const solicitacoesComDetalhes = await Promise.all(
solicitacoes.map(async (s) => { solicitacoes.map(async (s) => {
const funcionario = await ctx.db.get(s.funcionarioId); const funcionario = await ctx.db.get(s.funcionarioId);
// Buscar time do funcionário // Buscar time do funcionário
const membroTime = await ctx.db const membroTime = await ctx.db
.query("timesMembros") .query("timesMembros")
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", s.funcionarioId)) .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", s.funcionarioId)
)
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field("ativo"), true))
.first(); .first();
let time = null; let time = null;
if (membroTime) { if (membroTime) {
time = await ctx.db.get(membroTime.timeId); time = await ctx.db.get(membroTime.timeId);
} }
return { return {
...s, ...s,
funcionario, funcionario,
@@ -40,8 +42,10 @@ export const listarTodas = query({
}; };
}) })
); );
return solicitacoesComDetalhes.sort((a, b) => b._creationTime - a._creationTime); return solicitacoesComDetalhes.sort(
(a, b) => b._creationTime - a._creationTime
);
}, },
}); });
@@ -52,7 +56,9 @@ export const listarMinhasSolicitacoes = query({
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db return await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", args.funcionarioId)) .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", args.funcionarioId)
)
.order("desc") .order("desc")
.collect(); .collect();
}, },
@@ -69,23 +75,27 @@ export const listarSolicitacoesSubordinados = query({
.withIndex("by_gestor", (q) => q.eq("gestorId", args.gestorId)) .withIndex("by_gestor", (q) => q.eq("gestorId", args.gestorId))
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field("ativo"), true))
.collect(); .collect();
const solicitacoes: Array<any> = []; const solicitacoes: Array<any> = [];
for (const time of timesGestor) { for (const time of timesGestor) {
// Buscar membros do time // Buscar membros do time
const membros = await ctx.db const membros = await ctx.db
.query("timesMembros") .query("timesMembros")
.withIndex("by_time_and_ativo", (q) => q.eq("timeId", time._id).eq("ativo", true)) .withIndex("by_time_and_ativo", (q) =>
q.eq("timeId", time._id).eq("ativo", true)
)
.collect(); .collect();
// Buscar solicitações de cada membro // Buscar solicitações de cada membro
for (const membro of membros) { for (const membro of membros) {
const solic = await ctx.db const solic = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario", (q) => q.eq("funcionarioId", membro.funcionarioId)) .withIndex("by_funcionario", (q) =>
q.eq("funcionarioId", membro.funcionarioId)
)
.collect(); .collect();
// Adicionar info do funcionário // Adicionar info do funcionário
for (const s of solic) { for (const s of solic) {
const funcionario = await ctx.db.get(s.funcionarioId); const funcionario = await ctx.db.get(s.funcionarioId);
@@ -97,7 +107,7 @@ export const listarSolicitacoesSubordinados = query({
} }
} }
} }
return solicitacoes.sort((a, b) => b._creationTime - a._creationTime); return solicitacoes.sort((a, b) => b._creationTime - a._creationTime);
}, },
}); });
@@ -109,13 +119,13 @@ export const obterDetalhes = query({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) return null; if (!solicitacao) return null;
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(solicitacao.funcionarioId);
let gestor = null; let gestor = null;
if (solicitacao.gestorId) { if (solicitacao.gestorId) {
gestor = await ctx.db.get(solicitacao.gestorId); gestor = await ctx.db.get(solicitacao.gestorId);
} }
return { return {
...solicitacao, ...solicitacao,
funcionario, funcionario,
@@ -137,42 +147,46 @@ export const criarSolicitacao = mutation({
if (args.periodos.length === 0) { if (args.periodos.length === 0) {
throw new Error("É necessário adicionar pelo menos 1 período"); throw new Error("É necessário adicionar pelo menos 1 período");
} }
const funcionario = await ctx.db.get(args.funcionarioId); const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) throw new Error("Funcionário não encontrado"); if (!funcionario) throw new Error("Funcionário não encontrado");
// Calcular total de dias // Calcular total de dias
let totalDias = 0; let totalDias = 0;
for (const p of args.periodos) { for (const p of args.periodos) {
totalDias += p.diasCorridos; totalDias += p.diasCorridos;
} }
// Reservar dias no saldo (impede uso duplo) // Reservar dias no saldo (impede uso duplo)
await ctx.runMutation(internal.saldoFerias.reservarDias, { await ctx.runMutation(internal.saldoFerias.reservarDias, {
funcionarioId: args.funcionarioId, funcionarioId: args.funcionarioId,
anoReferencia: args.anoReferencia, anoReferencia: args.anoReferencia,
totalDias, totalDias,
}); });
// Buscar usuário que está criando (pode não ser o próprio funcionário) // Buscar usuário que está criando (pode não ser o próprio funcionário)
const usuario = await ctx.db const usuario = await ctx.db
.query("usuarios") .query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", args.funcionarioId)) .withIndex("by_funcionarioId", (q) =>
q.eq("funcionarioId", args.funcionarioId)
)
.first(); .first();
const solicitacaoId = await ctx.db.insert("solicitacoesFerias", { const solicitacaoId = await ctx.db.insert("solicitacoesFerias", {
funcionarioId: args.funcionarioId, funcionarioId: args.funcionarioId,
anoReferencia: args.anoReferencia, anoReferencia: args.anoReferencia,
status: "aguardando_aprovacao", status: "aguardando_aprovacao",
periodos: args.periodos, periodos: args.periodos,
observacao: args.observacao, observacao: args.observacao,
historicoAlteracoes: [{ historicoAlteracoes: [
data: Date.now(), {
usuarioId: usuario?._id || funcionario.gestorId!, data: Date.now(),
acao: "Solicitação criada", usuarioId: usuario?._id || funcionario.gestorId!,
}], acao: "Solicitação criada",
},
],
}); });
// Notificar gestor // Notificar gestor
if (funcionario.gestorId) { if (funcionario.gestorId) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
@@ -183,7 +197,7 @@ export const criarSolicitacao = mutation({
mensagem: `${funcionario.nome} solicitou férias`, mensagem: `${funcionario.nome} solicitou férias`,
}); });
} }
return solicitacaoId; return solicitacaoId;
}, },
}); });
@@ -198,13 +212,13 @@ export const aprovar = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) throw new Error("Solicitação não encontrada"); if (!solicitacao) throw new Error("Solicitação não encontrada");
if (solicitacao.status !== "aguardando_aprovacao") { if (solicitacao.status !== "aguardando_aprovacao") {
throw new Error("Esta solicitação já foi processada"); throw new Error("Esta solicitação já foi processada");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(solicitacao.funcionarioId);
await ctx.db.patch(args.solicitacaoId, { await ctx.db.patch(args.solicitacaoId, {
status: "aprovado", status: "aprovado",
gestorId: args.gestorId, gestorId: args.gestorId,
@@ -218,19 +232,21 @@ export const aprovar = mutation({
}, },
], ],
}); });
// Atualizar saldo (de pendente para usado) // Atualizar saldo (de pendente para usado)
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, { await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
solicitacaoId: args.solicitacaoId, solicitacaoId: args.solicitacaoId,
}); });
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
.query("usuarios") .query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id)) .withIndex("by_funcionarioId", (q) =>
q.eq("funcionarioId", funcionario._id)
)
.first(); .first();
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
@@ -241,7 +257,7 @@ export const aprovar = mutation({
}); });
} }
} }
return null; return null;
}, },
}); });
@@ -257,13 +273,13 @@ export const reprovar = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) throw new Error("Solicitação não encontrada"); if (!solicitacao) throw new Error("Solicitação não encontrada");
if (solicitacao.status !== "aguardando_aprovacao") { if (solicitacao.status !== "aguardando_aprovacao") {
throw new Error("Esta solicitação já foi processada"); throw new Error("Esta solicitação já foi processada");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(solicitacao.funcionarioId);
await ctx.db.patch(args.solicitacaoId, { await ctx.db.patch(args.solicitacaoId, {
status: "reprovado", status: "reprovado",
gestorId: args.gestorId, gestorId: args.gestorId,
@@ -278,19 +294,21 @@ export const reprovar = mutation({
}, },
], ],
}); });
// Liberar dias reservados de volta ao saldo // Liberar dias reservados de volta ao saldo
await ctx.runMutation(internal.saldoFerias.liberarDias, { await ctx.runMutation(internal.saldoFerias.liberarDias, {
solicitacaoId: args.solicitacaoId, solicitacaoId: args.solicitacaoId,
}); });
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
.query("usuarios") .query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id)) .withIndex("by_funcionarioId", (q) =>
q.eq("funcionarioId", funcionario._id)
)
.first(); .first();
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
@@ -301,7 +319,7 @@ export const reprovar = mutation({
}); });
} }
} }
return null; return null;
}, },
}); });
@@ -317,34 +335,34 @@ export const ajustarEAprovar = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const solicitacao = await ctx.db.get(args.solicitacaoId); const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) throw new Error("Solicitação não encontrada"); if (!solicitacao) throw new Error("Solicitação não encontrada");
if (solicitacao.status !== "aguardando_aprovacao") { if (solicitacao.status !== "aguardando_aprovacao") {
throw new Error("Esta solicitação já foi processada"); throw new Error("Esta solicitação já foi processada");
} }
if (args.novosPeriodos.length === 0) { if (args.novosPeriodos.length === 0) {
throw new Error("É necessário adicionar pelo menos 1 período"); throw new Error("É necessário adicionar pelo menos 1 período");
} }
const funcionario = await ctx.db.get(solicitacao.funcionarioId); const funcionario = await ctx.db.get(solicitacao.funcionarioId);
// Liberar dias antigos // Liberar dias antigos
await ctx.runMutation(internal.saldoFerias.liberarDias, { await ctx.runMutation(internal.saldoFerias.liberarDias, {
solicitacaoId: args.solicitacaoId, solicitacaoId: args.solicitacaoId,
}); });
// Calcular novos dias e reservar // Calcular novos dias e reservar
let totalNovosDias = 0; let totalNovosDias = 0;
for (const p of args.novosPeriodos) { for (const p of args.novosPeriodos) {
totalNovosDias += p.diasCorridos; totalNovosDias += p.diasCorridos;
} }
await ctx.runMutation(internal.saldoFerias.reservarDias, { await ctx.runMutation(internal.saldoFerias.reservarDias, {
funcionarioId: solicitacao.funcionarioId, funcionarioId: solicitacao.funcionarioId,
anoReferencia: solicitacao.anoReferencia, anoReferencia: solicitacao.anoReferencia,
totalDias: totalNovosDias, totalDias: totalNovosDias,
}); });
await ctx.db.patch(args.solicitacaoId, { await ctx.db.patch(args.solicitacaoId, {
status: "data_ajustada_aprovada", status: "data_ajustada_aprovada",
periodos: args.novosPeriodos, periodos: args.novosPeriodos,
@@ -360,19 +378,21 @@ export const ajustarEAprovar = mutation({
}, },
], ],
}); });
// Atualizar saldo (marcar como usado) // Atualizar saldo (marcar como usado)
await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, { await ctx.runMutation(internal.saldoFerias.atualizarSaldoAposAprovacao, {
solicitacaoId: args.solicitacaoId, solicitacaoId: args.solicitacaoId,
}); });
// Notificar funcionário // Notificar funcionário
if (funcionario) { if (funcionario) {
const usuario = await ctx.db const usuario = await ctx.db
.query("usuarios") .query("usuarios")
.withIndex("by_funcionarioId", (q) => q.eq("funcionarioId", funcionario._id)) .withIndex("by_funcionarioId", (q) =>
q.eq("funcionarioId", funcionario._id)
)
.first(); .first();
if (usuario) { if (usuario) {
await ctx.db.insert("notificacoesFerias", { await ctx.db.insert("notificacoesFerias", {
destinatarioId: usuario._id, destinatarioId: usuario._id,
@@ -383,7 +403,7 @@ export const ajustarEAprovar = mutation({
}); });
} }
} }
return null; return null;
}, },
}); });
@@ -395,38 +415,41 @@ export const verificarStatusFerias = query({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const hoje = new Date(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
const solicitacoesAprovadas = await ctx.db const solicitacoesAprovadas = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", args.funcionarioId) q.eq("funcionarioId", args.funcionarioId).eq("status", "aprovado")
.eq("status", "aprovado")
) )
.collect(); .collect();
const solicitacoesAjustadas = await ctx.db const solicitacoesAjustadas = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", args.funcionarioId) q
.eq("funcionarioId", args.funcionarioId)
.eq("status", "data_ajustada_aprovada") .eq("status", "data_ajustada_aprovada")
) )
.collect(); .collect();
const todasSolicitacoes = [...solicitacoesAprovadas, ...solicitacoesAjustadas]; const todasSolicitacoes = [
...solicitacoesAprovadas,
...solicitacoesAjustadas,
];
for (const solicitacao of todasSolicitacoes) { for (const solicitacao of todasSolicitacoes) {
for (const periodo of solicitacao.periodos) { for (const periodo of solicitacao.periodos) {
const inicio = new Date(periodo.dataInicio); const inicio = new Date(periodo.dataInicio);
const fim = new Date(periodo.dataFim); const fim = new Date(periodo.dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(23, 59, 59, 999); fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) { if (hoje >= inicio && hoje <= fim) {
return "em_ferias"; return "em_ferias";
} }
} }
} }
return "ativo"; return "ativo";
}, },
}); });
@@ -434,7 +457,6 @@ export const verificarStatusFerias = query({
// Query: Obter notificações não lidas // Query: Obter notificações não lidas
export const obterNotificacoesNaoLidas = query({ export const obterNotificacoesNaoLidas = query({
args: { usuarioId: v.id("usuarios") }, args: { usuarioId: v.id("usuarios") },
returns: v.array(v.any()),
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db return await ctx.db
.query("notificacoesFerias") .query("notificacoesFerias")
@@ -461,29 +483,30 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
returns: v.null(), returns: v.null(),
handler: async (ctx) => { handler: async (ctx) => {
const funcionarios = await ctx.db.query("funcionarios").collect(); const funcionarios = await ctx.db.query("funcionarios").collect();
for (const func of funcionarios) { for (const func of funcionarios) {
const hoje = new Date(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); hoje.setHours(0, 0, 0, 0);
const solicitacoesAprovadas = await ctx.db const solicitacoesAprovadas = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", func._id) q.eq("funcionarioId", func._id).eq("status", "aprovado")
.eq("status", "aprovado")
) )
.collect(); .collect();
const solicitacoesAjustadas = await ctx.db const solicitacoesAjustadas = await ctx.db
.query("solicitacoesFerias") .query("solicitacoesFerias")
.withIndex("by_funcionario_and_status", (q) => .withIndex("by_funcionario_and_status", (q) =>
q.eq("funcionarioId", func._id) q.eq("funcionarioId", func._id).eq("status", "data_ajustada_aprovada")
.eq("status", "data_ajustada_aprovada")
) )
.collect(); .collect();
const todasSolicitacoes = [...solicitacoesAprovadas, ...solicitacoesAjustadas]; const todasSolicitacoes = [
...solicitacoesAprovadas,
...solicitacoesAjustadas,
];
let emFerias = false; let emFerias = false;
for (const solicitacao of todasSolicitacoes) { for (const solicitacao of todasSolicitacoes) {
for (const periodo of solicitacao.periodos) { for (const periodo of solicitacao.periodos) {
@@ -491,7 +514,7 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
const fim = new Date(periodo.dataFim); const fim = new Date(periodo.dataFim);
inicio.setHours(0, 0, 0, 0); inicio.setHours(0, 0, 0, 0);
fim.setHours(23, 59, 59, 999); fim.setHours(23, 59, 59, 999);
if (hoje >= inicio && hoje <= fim) { if (hoje >= inicio && hoje <= fim) {
emFerias = true; emFerias = true;
break; break;
@@ -499,15 +522,14 @@ export const atualizarStatusTodosFuncionarios = internalMutation({
} }
if (emFerias) break; if (emFerias) break;
} }
const novoStatus = emFerias ? "em_ferias" : "ativo"; const novoStatus = emFerias ? "em_ferias" : "ativo";
if (func.statusFerias !== novoStatus) { if (func.statusFerias !== novoStatus) {
await ctx.db.patch(func._id, { statusFerias: novoStatus }); await ctx.db.patch(func._id, { statusFerias: novoStatus });
} }
} }
return null; return null;
}, },
}); });

View File

@@ -38,7 +38,6 @@ const aposentadoValidator = v.optional(
export const getAll = query({ export const getAll = query({
args: {}, args: {},
returns: v.array(v.any()),
handler: async (ctx) => { handler: async (ctx) => {
// Autorização: listar funcionários // Autorização: listar funcionários
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
@@ -47,7 +46,7 @@ export const getAll = query({
}); });
const funcionarios = await ctx.db.query("funcionarios").collect(); const funcionarios = await ctx.db.query("funcionarios").collect();
// Retornar apenas os campos necessários para listagem // Retornar apenas os campos necessários para listagem
return funcionarios.map((f: any) => ({ return funcionarios.map((f) => ({
_id: f._id, _id: f._id,
_creationTime: f._creationTime, _creationTime: f._creationTime,
nome: f.nome, nome: f.nome,
@@ -65,6 +64,7 @@ export const getAll = query({
simboloTipo: f.simboloTipo, simboloTipo: f.simboloTipo,
admissaoData: f.admissaoData, admissaoData: f.admissaoData,
desligamentoData: f.desligamentoData, desligamentoData: f.desligamentoData,
descricaoCargo: f.descricaoCargo,
})); }));
}, },
}); });
@@ -199,7 +199,9 @@ export const create = mutation({
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.unique(); .unique();
if (matriculaExists) { if (matriculaExists) {
throw new Error("Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco."); throw new Error(
"Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco."
);
} }
} }
@@ -326,7 +328,9 @@ export const update = mutation({
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula)) .withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.unique(); .unique();
if (matriculaExists && matriculaExists._id !== args.id) { if (matriculaExists && matriculaExists._id !== args.id) {
throw new Error("Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco."); throw new Error(
"Já existe um funcionário com esta matrícula. Por favor, use outra ou deixe em branco."
);
} }
} }