refactor: clean up Svelte components and improve code readability
- Refactored multiple Svelte components to enhance code clarity and maintainability. - Standardized formatting and indentation across various files for consistency. - Improved error handling messages in the AprovarAusencias component for better user feedback. - Updated class names in the UI components to align with the new design system. - Removed unnecessary whitespace and comments to streamline the codebase.
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
const totalDias = $derived(
|
||||
calcularDias(solicitacao.dataInicio, solicitacao.dataFim)
|
||||
calcularDias(solicitacao.dataInicio, solicitacao.dataFim),
|
||||
);
|
||||
|
||||
async function aprovar() {
|
||||
@@ -52,10 +52,15 @@
|
||||
if (onSucesso) onSucesso();
|
||||
} catch (e) {
|
||||
const mensagemErro = e instanceof Error ? e.message : String(e);
|
||||
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (mensagemErro.includes("permissão") || mensagemErro.includes("permission") || mensagemErro.includes("Você não tem permissão")) {
|
||||
mensagemErroModal = "Você não tem permissão para aprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.";
|
||||
if (
|
||||
mensagemErro.includes("permissão") ||
|
||||
mensagemErro.includes("permission") ||
|
||||
mensagemErro.includes("Você não tem permissão")
|
||||
) {
|
||||
mensagemErroModal =
|
||||
"Você não tem permissão para aprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.";
|
||||
mostrarModalErro = true;
|
||||
} else {
|
||||
erro = mensagemErro;
|
||||
@@ -85,10 +90,15 @@
|
||||
if (onSucesso) onSucesso();
|
||||
} catch (e) {
|
||||
const mensagemErro = e instanceof Error ? e.message : String(e);
|
||||
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (mensagemErro.includes("permissão") || mensagemErro.includes("permission") || mensagemErro.includes("Você não tem permissão")) {
|
||||
mensagemErroModal = "Você não tem permissão para reprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.";
|
||||
if (
|
||||
mensagemErro.includes("permissão") ||
|
||||
mensagemErro.includes("permission") ||
|
||||
mensagemErro.includes("Você não tem permissão")
|
||||
) {
|
||||
mensagemErroModal =
|
||||
"Você não tem permissão para reprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.";
|
||||
mostrarModalErro = true;
|
||||
} else {
|
||||
erro = mensagemErro;
|
||||
@@ -125,7 +135,9 @@
|
||||
<div class="aprovar-ausencia">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-3xl font-bold text-primary mb-2">Aprovar/Reprovar Ausência</h2>
|
||||
<h2 class="text-3xl font-bold text-primary mb-2">
|
||||
Aprovar/Reprovar Ausência
|
||||
</h2>
|
||||
<p class="text-base-content/70">Analise a solicitação e tome uma decisão</p>
|
||||
</div>
|
||||
|
||||
@@ -154,14 +166,18 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Nome</p>
|
||||
<p class="font-bold text-lg">{solicitacao.funcionario?.nome || "N/A"}</p>
|
||||
<p class="font-bold text-lg">
|
||||
{solicitacao.funcionario?.nome || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
{#if solicitacao.time}
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Time</p>
|
||||
<div
|
||||
class="badge badge-lg font-semibold"
|
||||
style="background-color: {solicitacao.time.cor}20; border-color: {solicitacao.time.cor}; color: {solicitacao.time.cor}"
|
||||
style="background-color: {solicitacao.time
|
||||
.cor}20; border-color: {solicitacao.time
|
||||
.cor}; color: {solicitacao.time.cor}"
|
||||
>
|
||||
{solicitacao.time.nome}
|
||||
</div>
|
||||
@@ -192,21 +208,33 @@
|
||||
Período da Ausência
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="stat bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30">
|
||||
<div
|
||||
class="stat bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30"
|
||||
>
|
||||
<div class="stat-title">Data Início</div>
|
||||
<div class="stat-value text-orange-600 dark:text-orange-400 text-2xl">
|
||||
<div
|
||||
class="stat-value text-orange-600 dark:text-orange-400 text-2xl"
|
||||
>
|
||||
{new Date(solicitacao.dataInicio).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30">
|
||||
<div
|
||||
class="stat bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30"
|
||||
>
|
||||
<div class="stat-title">Data Fim</div>
|
||||
<div class="stat-value text-orange-600 dark:text-orange-400 text-2xl">
|
||||
<div
|
||||
class="stat-value text-orange-600 dark:text-orange-400 text-2xl"
|
||||
>
|
||||
{new Date(solicitacao.dataFim).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30">
|
||||
<div
|
||||
class="stat bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 rounded-xl border-2 border-orange-500/30"
|
||||
>
|
||||
<div class="stat-title">Total de Dias</div>
|
||||
<div class="stat-value text-orange-600 dark:text-orange-400 text-3xl">
|
||||
<div
|
||||
class="stat-value text-orange-600 dark:text-orange-400 text-3xl"
|
||||
>
|
||||
{totalDias}
|
||||
</div>
|
||||
<div class="stat-desc">dias corridos</div>
|
||||
@@ -385,7 +413,8 @@
|
||||
<ErrorModal
|
||||
open={mostrarModalErro}
|
||||
title="Erro de Permissão"
|
||||
message={mensagemErroModal || "Você não tem permissão para realizar esta ação."}
|
||||
message={mensagemErroModal ||
|
||||
"Você não tem permissão para realizar esta ação."}
|
||||
onClose={fecharModalErro}
|
||||
/>
|
||||
|
||||
@@ -395,4 +424,3 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -155,45 +155,60 @@
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<!-- Header com filtros -->
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6">
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6"
|
||||
>
|
||||
<h2 class="card-title text-2xl">Calendário de Afastamentos</h2>
|
||||
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content/70">Filtrar:</span>
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'todos' ? 'btn-active btn-primary' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo === 'todos'
|
||||
? 'btn-active btn-primary'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "todos")}
|
||||
>
|
||||
Todos
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'atestado_medico' ? 'btn-active btn-error' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo === 'atestado_medico'
|
||||
? 'btn-active btn-error'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "atestado_medico")}
|
||||
>
|
||||
Atestados
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'declaracao_comparecimento' ? 'btn-active btn-warning' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo ===
|
||||
'declaracao_comparecimento'
|
||||
? 'btn-active btn-warning'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "declaracao_comparecimento")}
|
||||
>
|
||||
Declarações
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'maternidade' ? 'btn-active btn-secondary' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo === 'maternidade'
|
||||
? 'btn-active btn-secondary'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "maternidade")}
|
||||
>
|
||||
Maternidade
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'paternidade' ? 'btn-active btn-info' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo === 'paternidade'
|
||||
? 'btn-active btn-info'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "paternidade")}
|
||||
>
|
||||
Paternidade
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm {filtroAtivo === 'ferias' ? 'btn-active btn-success' : 'btn-ghost'}"
|
||||
class="join-item btn btn-sm {filtroAtivo === 'ferias'
|
||||
? 'btn-active btn-success'
|
||||
: 'btn-ghost'}"
|
||||
onclick={() => (filtroAtivo = "ferias")}
|
||||
>
|
||||
Férias
|
||||
@@ -247,13 +262,19 @@
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header do Modal -->
|
||||
<div class="p-6 border-b border-base-300 bg-gradient-to-r from-primary/10 to-secondary/10">
|
||||
<div
|
||||
class="p-6 border-b border-base-300 bg-linear-to-r from-primary/10 to-secondary/10"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-bold text-base-content mb-2">
|
||||
{eventoSelecionado.funcionarioNome}
|
||||
</h3>
|
||||
<p class="text-sm {getTipoCor(eventoSelecionado.tipo)} font-medium">
|
||||
<p
|
||||
class="text-sm {getTipoCor(
|
||||
eventoSelecionado.tipo,
|
||||
)} font-medium"
|
||||
>
|
||||
{getTipoNome(eventoSelecionado.tipo)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -299,7 +320,9 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Data Início</p>
|
||||
<p class="font-semibold">{formatarData(eventoSelecionado.start)}</p>
|
||||
<p class="font-semibold">
|
||||
{formatarData(eventoSelecionado.start)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -320,7 +343,9 @@
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/60">Data Fim</p>
|
||||
<p class="font-semibold">{formatarData(eventoSelecionado.end)}</p>
|
||||
<p class="font-semibold">
|
||||
{formatarData(eventoSelecionado.end)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -346,7 +371,8 @@
|
||||
const inicio = new Date(eventoSelecionado.start);
|
||||
const fim = new Date(eventoSelecionado.end);
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
const diffDays =
|
||||
Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
return `${diffDays} ${diffDays === 1 ? "dia" : "dias"}`;
|
||||
})()}
|
||||
</p>
|
||||
@@ -356,10 +382,7 @@
|
||||
|
||||
<!-- Footer do Modal -->
|
||||
<div class="p-6 border-t border-base-300 flex justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={() => (showModal = false)}
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => (showModal = false)}>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
dataFim: string;
|
||||
status: "aguardando_aprovacao" | "aprovado" | "reprovado";
|
||||
}>;
|
||||
onPeriodoSelecionado?: (periodo: { dataInicio: string; dataFim: string }) => void;
|
||||
onPeriodoSelecionado?: (periodo: {
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
}) => void;
|
||||
modoVisualizacao?: "month" | "multiMonth";
|
||||
readonly?: boolean;
|
||||
}
|
||||
@@ -45,7 +48,10 @@
|
||||
}> = $state([]);
|
||||
|
||||
// Cores por status
|
||||
const coresStatus: Record<string, { bg: string; border: string; text: string }> = {
|
||||
const coresStatus: Record<
|
||||
string,
|
||||
{ bg: string; border: string; text: string }
|
||||
> = {
|
||||
aguardando_aprovacao: { bg: "#f59e0b", border: "#d97706", text: "#ffffff" }, // Laranja
|
||||
aprovado: { bg: "#10b981", border: "#059669", text: "#ffffff" }, // Verde
|
||||
reprovado: { bg: "#ef4444", border: "#dc2626", text: "#ffffff" }, // Vermelho
|
||||
@@ -65,7 +71,8 @@
|
||||
status: string;
|
||||
};
|
||||
}> = ausenciasExistentes.map((ausencia, index) => {
|
||||
const cor = coresStatus[ausencia.status] || coresStatus.aguardando_aprovacao;
|
||||
const cor =
|
||||
coresStatus[ausencia.status] || coresStatus.aguardando_aprovacao;
|
||||
return {
|
||||
id: `ausencia-${index}`,
|
||||
title: `${getStatusTexto(ausencia.status)} - ${calcularDias(ausencia.dataInicio, ausencia.dataFim)} dias`,
|
||||
@@ -95,7 +102,7 @@
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
eventos = novosEventos;
|
||||
}
|
||||
|
||||
@@ -129,11 +136,11 @@
|
||||
inicio1: Date,
|
||||
fim1: Date,
|
||||
inicio2: string,
|
||||
fim2: string
|
||||
fim2: string,
|
||||
): boolean {
|
||||
const d2Inicio = new Date(inicio2);
|
||||
const d2Fim = new Date(fim2);
|
||||
|
||||
|
||||
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
|
||||
return inicio1 <= d2Fim && d2Inicio <= fim1;
|
||||
}
|
||||
@@ -141,14 +148,14 @@
|
||||
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
|
||||
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean {
|
||||
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
|
||||
|
||||
|
||||
// Verificar apenas ausências aprovadas ou aguardando aprovação
|
||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
);
|
||||
|
||||
|
||||
return ausenciasBloqueantes.some((ausencia) =>
|
||||
verificarSobreposicao(inicio, fim, ausencia.dataInicio, ausencia.dataFim)
|
||||
verificarSobreposicao(inicio, fim, ausencia.dataInicio, ausencia.dataFim),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,11 +165,11 @@
|
||||
const cellDate = new Date(info.date);
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
|
||||
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
if (cellDate >= inicio && cellDate <= fim) {
|
||||
info.el.classList.add("fc-day-selected");
|
||||
} else {
|
||||
@@ -185,7 +192,9 @@
|
||||
|
||||
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
|
||||
const estaBloqueado = ausenciasExistentes
|
||||
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
|
||||
.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
)
|
||||
.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
@@ -204,23 +213,23 @@
|
||||
// Helper: Atualizar todos os dias selecionados no calendário
|
||||
function atualizarDiasSelecionados() {
|
||||
if (!calendar || !calendarEl || !dataInicio || !dataFim || readonly) return;
|
||||
|
||||
|
||||
// Usar a API do FullCalendar para iterar sobre todas as células visíveis
|
||||
const view = calendar.view;
|
||||
if (!view) return;
|
||||
|
||||
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
inicio.setHours(0, 0, 0, 0);
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
// O FullCalendar renderiza as células, então podemos usar dayCellDidMount
|
||||
// Mas também precisamos atualizar células existentes
|
||||
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||
cells.forEach((cell) => {
|
||||
// Remover classe primeiro
|
||||
cell.classList.remove("fc-day-selected");
|
||||
|
||||
|
||||
// Tentar obter a data do aria-label ou do elemento
|
||||
const ariaLabel = cell.getAttribute("aria-label");
|
||||
if (ariaLabel) {
|
||||
@@ -242,7 +251,13 @@
|
||||
|
||||
// Helper: Atualizar todos os dias bloqueados no calendário
|
||||
function atualizarDiasBloqueados() {
|
||||
if (!calendar || !calendarEl || readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
||||
if (
|
||||
!calendar ||
|
||||
!calendarEl ||
|
||||
readonly ||
|
||||
!ausenciasExistentes ||
|
||||
ausenciasExistentes.length === 0
|
||||
) {
|
||||
// Remover classes de bloqueio se não houver ausências
|
||||
if (calendarEl) {
|
||||
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||
@@ -250,23 +265,23 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
);
|
||||
|
||||
|
||||
if (ausenciasBloqueantes.length === 0) {
|
||||
cells.forEach((cell) => cell.classList.remove("fc-day-blocked"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
cells.forEach((cell) => {
|
||||
cell.classList.remove("fc-day-blocked");
|
||||
|
||||
|
||||
// Tentar obter a data de diferentes formas
|
||||
let cellDate: Date | null = null;
|
||||
|
||||
|
||||
// Método 1: aria-label
|
||||
const ariaLabel = cell.getAttribute("aria-label");
|
||||
if (ariaLabel) {
|
||||
@@ -279,7 +294,7 @@
|
||||
// Ignorar
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Método 2: data-date attribute
|
||||
if (!cellDate) {
|
||||
const dataDate = cell.getAttribute("data-date");
|
||||
@@ -294,7 +309,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Método 3: Tentar obter do número do dia e contexto do calendário
|
||||
if (!cellDate && calendar.view) {
|
||||
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
|
||||
@@ -315,10 +330,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (cellDate) {
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
@@ -326,7 +341,7 @@
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
return cellDate >= inicio && cellDate <= fim;
|
||||
});
|
||||
|
||||
|
||||
if (estaBloqueado) {
|
||||
cell.classList.add("fc-day-blocked");
|
||||
}
|
||||
@@ -337,18 +352,18 @@
|
||||
// Atualizar eventos quando mudanças ocorrem (evitar loop infinito)
|
||||
$effect(() => {
|
||||
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
||||
|
||||
|
||||
// Garantir que temos as ausências antes de atualizar
|
||||
const ausencias = ausenciasExistentes;
|
||||
|
||||
|
||||
atualizarEventos();
|
||||
|
||||
|
||||
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
||||
requestAnimationFrame(() => {
|
||||
if (calendar && !selecionando) {
|
||||
calendar.removeAllEvents();
|
||||
calendar.addEventSource(eventos);
|
||||
|
||||
|
||||
// Atualizar classes de seleção e bloqueio quando as datas mudarem
|
||||
setTimeout(() => {
|
||||
atualizarDiasSelecionados();
|
||||
@@ -357,16 +372,17 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Efeito separado para atualizar quando ausências mudarem
|
||||
$effect(() => {
|
||||
if (!calendar || readonly) return;
|
||||
|
||||
|
||||
const ausencias = ausenciasExistentes;
|
||||
const ausenciasBloqueantes = ausencias?.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||
) || [];
|
||||
|
||||
const ausenciasBloqueantes =
|
||||
ausencias?.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
) || [];
|
||||
|
||||
// Se houver ausências bloqueantes, forçar atualização
|
||||
if (ausenciasBloqueantes.length > 0) {
|
||||
setTimeout(() => {
|
||||
@@ -386,12 +402,14 @@
|
||||
|
||||
calendar = new Calendar(calendarEl, {
|
||||
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
|
||||
initialView: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
initialView:
|
||||
modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
locale: ptBrLocale,
|
||||
headerToolbar: {
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
right:
|
||||
modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
},
|
||||
height: "auto",
|
||||
selectable: !readonly,
|
||||
@@ -443,7 +461,9 @@
|
||||
|
||||
// Validar sobreposição com ausências existentes
|
||||
if (verificarSobreposicaoComAusencias(inicio, fim)) {
|
||||
alert("Este período sobrepõe com uma ausência já aprovada ou aguardando aprovação. Por favor, escolha outro período.");
|
||||
alert(
|
||||
"Este período sobrepõe com uma ausência já aprovada ou aguardando aprovação. Por favor, escolha outro período.",
|
||||
);
|
||||
calendar?.unselect();
|
||||
selecionando = false;
|
||||
return;
|
||||
@@ -459,7 +479,7 @@
|
||||
|
||||
// Não remover seleção imediatamente para manter visualização
|
||||
// calendar?.unselect();
|
||||
|
||||
|
||||
// Liberar flag após um pequeno delay para garantir que o estado foi atualizado
|
||||
setTimeout(() => {
|
||||
selecionando = false;
|
||||
@@ -472,7 +492,9 @@
|
||||
if (readonly) {
|
||||
const status = info.event.extendedProps.status;
|
||||
const texto = getStatusTexto(status);
|
||||
alert(`Ausência ${texto}\nPeríodo: ${new Date(info.event.startStr).toLocaleDateString("pt-BR")} até ${new Date(calcularDataFim(info.event.endStr)).toLocaleDateString("pt-BR")}`);
|
||||
alert(
|
||||
`Ausência ${texto}\nPeríodo: ${new Date(info.event.startStr).toLocaleDateString("pt-BR")} até ${new Date(calcularDataFim(info.event.endStr)).toLocaleDateString("pt-BR")}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -491,35 +513,39 @@
|
||||
selectAllow: (selectInfo) => {
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
// Bloquear datas passadas
|
||||
if (new Date(selectInfo.start) < hoje) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Verificar sobreposição com ausências existentes
|
||||
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
|
||||
if (
|
||||
!readonly &&
|
||||
ausenciasExistentes &&
|
||||
ausenciasExistentes.length > 0
|
||||
) {
|
||||
const inicioSelecao = new Date(selectInfo.start);
|
||||
const fimSelecao = new Date(selectInfo.end);
|
||||
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
|
||||
|
||||
|
||||
inicioSelecao.setHours(0, 0, 0, 0);
|
||||
fimSelecao.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
if (verificarSobreposicaoComAusencias(inicioSelecao, fimSelecao)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
// Adicionar classe CSS aos dias selecionados e bloqueados
|
||||
dayCellDidMount: (info) => {
|
||||
atualizarClasseSelecionado(info);
|
||||
atualizarClasseBloqueado(info);
|
||||
},
|
||||
|
||||
|
||||
// Atualizar quando as datas mudarem (navegação do calendário)
|
||||
datesSet: () => {
|
||||
setTimeout(() => {
|
||||
@@ -527,7 +553,7 @@
|
||||
atualizarDiasBloqueados();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
|
||||
// Garantir que as classes sejam aplicadas após renderização inicial
|
||||
viewDidMount: () => {
|
||||
setTimeout(() => {
|
||||
@@ -541,20 +567,25 @@
|
||||
// Highlight de fim de semana e aplicar classe de bloqueio
|
||||
dayCellClassNames: (arg) => {
|
||||
const classes: string[] = [];
|
||||
|
||||
|
||||
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
|
||||
classes.push("fc-day-weekend-custom");
|
||||
}
|
||||
|
||||
|
||||
// Verificar se o dia está bloqueado
|
||||
if (!readonly && ausenciasExistentes && ausenciasExistentes.length > 0) {
|
||||
if (
|
||||
!readonly &&
|
||||
ausenciasExistentes &&
|
||||
ausenciasExistentes.length > 0
|
||||
) {
|
||||
const cellDate = new Date(arg.date);
|
||||
cellDate.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao"
|
||||
(a) =>
|
||||
a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
);
|
||||
|
||||
|
||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||
const inicio = new Date(ausencia.dataInicio);
|
||||
const fim = new Date(ausencia.dataFim);
|
||||
@@ -562,12 +593,12 @@
|
||||
fim.setHours(0, 0, 0, 0);
|
||||
return cellDate >= inicio && cellDate <= fim;
|
||||
});
|
||||
|
||||
|
||||
if (estaBloqueado) {
|
||||
classes.push("fc-day-blocked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return classes;
|
||||
},
|
||||
});
|
||||
@@ -585,32 +616,39 @@
|
||||
{#if !readonly}
|
||||
<div class="space-y-4 mb-4">
|
||||
<div class="alert alert-info shadow-lg">
|
||||
<svg
|
||||
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>
|
||||
<div class="text-sm">
|
||||
<p class="font-bold">Como usar:</p>
|
||||
<ul class="list-disc list-inside mt-1">
|
||||
<li>Clique e arraste no calendário para selecionar o período de ausência</li>
|
||||
<li>Você pode visualizar suas ausências já solicitadas no calendário</li>
|
||||
<li>A data de início não pode ser no passado</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg
|
||||
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>
|
||||
<div class="text-sm">
|
||||
<p class="font-bold">Como usar:</p>
|
||||
<ul class="list-disc list-inside mt-1">
|
||||
<li>
|
||||
Clique e arraste no calendário para selecionar o período de
|
||||
ausência
|
||||
</li>
|
||||
<li>
|
||||
Você pode visualizar suas ausências já solicitadas no calendário
|
||||
</li>
|
||||
<li>A data de início não pode ser no passado</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerta sobre dias bloqueados -->
|
||||
{#if ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
{@const ausenciasBloqueantes = ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao")}
|
||||
{#if ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
{@const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
)}
|
||||
<div class="alert alert-warning shadow-lg border-2 border-warning/50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -628,8 +666,18 @@
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold">Atenção: Períodos Indisponíveis</h3>
|
||||
<div class="text-sm mt-1">
|
||||
<p>Os dias marcados em <span class="font-bold text-error">vermelho</span> estão bloqueados porque você já possui solicitações <strong>aprovadas</strong> ou <strong>aguardando aprovação</strong> para esses períodos.</p>
|
||||
<p class="mt-2">Você não pode criar novas solicitações que sobreponham esses períodos. Escolha um período diferente.</p>
|
||||
<p>
|
||||
Os dias marcados em <span class="font-bold text-error"
|
||||
>vermelho</span
|
||||
>
|
||||
estão bloqueados porque você já possui solicitações
|
||||
<strong>aprovadas</strong>
|
||||
ou <strong>aguardando aprovação</strong> para esses períodos.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Você não pode criar novas solicitações que sobreponham esses
|
||||
períodos. Escolha um período diferente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -647,30 +695,46 @@
|
||||
{#if ausenciasExistentes.length > 0 || readonly}
|
||||
<div class="mt-6 space-y-4">
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
<div class="badge badge-lg gap-2" style="background-color: #f59e0b; border-color: #d97706; color: white;">
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Aguardando Aprovação
|
||||
</div>
|
||||
<div class="badge badge-lg gap-2" style="background-color: #10b981; border-color: #059669; color: white;">
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Aprovado
|
||||
</div>
|
||||
<div class="badge badge-lg gap-2" style="background-color: #ef4444; border-color: #dc2626; color: white;">
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Reprovado
|
||||
</div>
|
||||
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
<div class="badge badge-lg gap-2" style="background-color: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #dc2626;">
|
||||
<div class="w-3 h-3 rounded-full" style="background-color: #ef4444;"></div>
|
||||
<div
|
||||
class="badge badge-lg gap-2"
|
||||
style="background-color: #f59e0b; border-color: #d97706; color: white;"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Aguardando Aprovação
|
||||
</div>
|
||||
<div
|
||||
class="badge badge-lg gap-2"
|
||||
style="background-color: #10b981; border-color: #059669; color: white;"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Aprovado
|
||||
</div>
|
||||
<div
|
||||
class="badge badge-lg gap-2"
|
||||
style="background-color: #ef4444; border-color: #dc2626; color: white;"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full bg-white"></div>
|
||||
Reprovado
|
||||
</div>
|
||||
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
<div
|
||||
class="badge badge-lg gap-2"
|
||||
style="background-color: rgba(239, 68, 68, 0.2); border-color: #ef4444; color: #dc2626;"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
style="background-color: #ef4444;"
|
||||
></div>
|
||||
Dias Bloqueados (Indisponíveis)
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter(a => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
|
||||
{#if !readonly && ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-base-content/70">
|
||||
<span class="font-semibold text-error">Dias bloqueados</span> não podem ser selecionados para novas solicitações
|
||||
<span class="font-semibold text-error">Dias bloqueados</span> não podem
|
||||
ser selecionados para novas solicitações
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -679,7 +743,9 @@
|
||||
|
||||
<!-- Informação do período selecionado -->
|
||||
{#if dataInicio && dataFim && !readonly}
|
||||
<div class="mt-6 card bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 shadow-lg border-2 border-orange-500/30">
|
||||
<div
|
||||
class="mt-6 card bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 shadow-lg border-2 border-orange-500/30"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-orange-700 dark:text-orange-400">
|
||||
<svg
|
||||
@@ -701,15 +767,21 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Data Início</p>
|
||||
<p class="font-bold text-lg">{new Date(dataInicio).toLocaleDateString("pt-BR")}</p>
|
||||
<p class="font-bold text-lg">
|
||||
{new Date(dataInicio).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Data Fim</p>
|
||||
<p class="font-bold text-lg">{new Date(dataFim).toLocaleDateString("pt-BR")}</p>
|
||||
<p class="font-bold text-lg">
|
||||
{new Date(dataFim).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Total de Dias</p>
|
||||
<p class="font-bold text-2xl text-orange-600 dark:text-orange-400">{calcularDias(dataInicio, dataFim)} dias</p>
|
||||
<p class="font-bold text-2xl text-orange-600 dark:text-orange-400">
|
||||
{calcularDias(dataInicio, dataFim)} dias
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -720,7 +792,12 @@
|
||||
<style>
|
||||
/* Calendário Premium */
|
||||
.calendario-ausencias {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-family:
|
||||
"Inter",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
/* Toolbar moderna com cores laranja/amarelo */
|
||||
@@ -847,12 +924,18 @@
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-frame) {
|
||||
:global(
|
||||
.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-frame
|
||||
) {
|
||||
background-color: rgba(239, 68, 68, 0.2) !important;
|
||||
border-color: rgba(239, 68, 68, 0.4) !important;
|
||||
}
|
||||
|
||||
:global(.calendario-ausencias .fc-daygrid-day.fc-day-blocked .fc-daygrid-day-number) {
|
||||
:global(
|
||||
.calendario-ausencias
|
||||
.fc-daygrid-day.fc-day-blocked
|
||||
.fc-daygrid-day-number
|
||||
) {
|
||||
color: #dc2626 !important;
|
||||
font-weight: 700 !important;
|
||||
text-decoration: line-through !important;
|
||||
@@ -917,4 +1000,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -26,26 +26,31 @@
|
||||
let dataFim = $state<string>("");
|
||||
let motivo = $state("");
|
||||
let processando = $state(false);
|
||||
|
||||
|
||||
// Estados para modal de erro
|
||||
let mostrarModalErro = $state(false);
|
||||
let mensagemErroModal = $state("");
|
||||
let detalhesErroModal = $state("");
|
||||
|
||||
// Buscar ausências existentes para exibir no calendário
|
||||
const ausenciasExistentesQuery = useQuery(api.ausencias.listarMinhasSolicitacoes, {
|
||||
funcionarioId,
|
||||
});
|
||||
const ausenciasExistentesQuery = useQuery(
|
||||
api.ausencias.listarMinhasSolicitacoes,
|
||||
{
|
||||
funcionarioId,
|
||||
},
|
||||
);
|
||||
|
||||
// Filtrar apenas ausências aprovadas ou aguardando aprovação (que bloqueiam novas solicitações)
|
||||
const ausenciasExistentes = $derived(
|
||||
(ausenciasExistentesQuery?.data || [])
|
||||
.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao")
|
||||
.filter(
|
||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||
)
|
||||
.map((a) => ({
|
||||
dataInicio: a.dataInicio,
|
||||
dataFim: a.dataFim,
|
||||
dataInicio: a.dataInicio,
|
||||
dataFim: a.dataFim,
|
||||
status: a.status as "aguardando_aprovacao" | "aprovado",
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
// Calcular dias selecionados
|
||||
@@ -117,14 +122,15 @@
|
||||
});
|
||||
|
||||
toast.success("Solicitação de ausência criada com sucesso!");
|
||||
|
||||
|
||||
if (onSucesso) {
|
||||
onSucesso();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar solicitação:", error);
|
||||
const mensagemErro = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const mensagemErro =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Verificar se é erro de sobreposição de período
|
||||
if (
|
||||
mensagemErro.includes("Já existe uma solicitação") ||
|
||||
@@ -149,7 +155,10 @@
|
||||
detalhesErroModal = "";
|
||||
}
|
||||
|
||||
function handlePeriodoSelecionado(periodo: { dataInicio: string; dataFim: string }) {
|
||||
function handlePeriodoSelecionado(periodo: {
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
}) {
|
||||
dataInicio = periodo.dataInicio;
|
||||
dataFim = periodo.dataFim;
|
||||
}
|
||||
@@ -158,7 +167,9 @@
|
||||
<div class="wizard-ausencia">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<p class="text-base-content/70">Solicite uma ausência para assuntos particulares</p>
|
||||
<p class="text-base-content/70">
|
||||
Solicite uma ausência para assuntos particulares
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Indicador de progresso -->
|
||||
@@ -230,22 +241,25 @@
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold mb-2">Selecione o Período</h3>
|
||||
<p class="text-base-content/70">
|
||||
Clique e arraste no calendário para selecionar o período de ausência
|
||||
Clique e arraste no calendário para selecionar o período de
|
||||
ausência
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if ausenciasExistentesQuery === undefined}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<span class="ml-4 text-base-content/70">Carregando ausências existentes...</span>
|
||||
<span class="ml-4 text-base-content/70"
|
||||
>Carregando ausências existentes...</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<CalendarioAusencias
|
||||
dataInicio={dataInicio}
|
||||
dataFim={dataFim}
|
||||
ausenciasExistentes={ausenciasExistentes}
|
||||
onPeriodoSelecionado={handlePeriodoSelecionado}
|
||||
/>
|
||||
<CalendarioAusencias
|
||||
{dataInicio}
|
||||
{dataFim}
|
||||
{ausenciasExistentes}
|
||||
onPeriodoSelecionado={handlePeriodoSelecionado}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if dataInicio && dataFim}
|
||||
@@ -279,13 +293,16 @@
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold mb-2">Informe o Motivo</h3>
|
||||
<p class="text-base-content/70">
|
||||
Descreva o motivo da sua solicitação de ausência (mínimo 10 caracteres)
|
||||
Descreva o motivo da sua solicitação de ausência (mínimo 10
|
||||
caracteres)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Resumo do período -->
|
||||
{#if dataInicio && dataFim}
|
||||
<div class="card bg-gradient-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 border-2 border-orange-500/30">
|
||||
<div
|
||||
class="card bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950 border-2 border-orange-500/30"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-orange-700 dark:text-orange-400">
|
||||
<svg
|
||||
@@ -307,15 +324,21 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Data Início</p>
|
||||
<p class="font-bold">{new Date(dataInicio).toLocaleDateString("pt-BR")}</p>
|
||||
<p class="font-bold">
|
||||
{new Date(dataInicio).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Data Fim</p>
|
||||
<p class="font-bold">{new Date(dataFim).toLocaleDateString("pt-BR")}</p>
|
||||
<p class="font-bold">
|
||||
{new Date(dataFim).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Total de Dias</p>
|
||||
<p class="font-bold text-xl text-orange-600 dark:text-orange-400">
|
||||
<p
|
||||
class="font-bold text-xl text-orange-600 dark:text-orange-400"
|
||||
>
|
||||
{totalDias} dias
|
||||
</p>
|
||||
</div>
|
||||
@@ -478,4 +501,3 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import NewConversationModal from "./NewConversationModal.svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
|
||||
// Buscar todos os usuários para o chat
|
||||
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
||||
|
||||
|
||||
// Buscar o perfil do usuário logado
|
||||
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
||||
|
||||
@@ -24,54 +24,77 @@
|
||||
|
||||
// Debug: monitorar carregamento de dados
|
||||
$effect(() => {
|
||||
console.log("📊 [ChatList] Usuários carregados:", usuarios?.data?.length || 0);
|
||||
console.log("👤 [ChatList] Meu perfil:", meuPerfil?.data?.nome || "Carregando...");
|
||||
console.log("🆔 [ChatList] Meu ID:", meuPerfil?.data?._id || "Não encontrado");
|
||||
console.log(
|
||||
"📊 [ChatList] Usuários carregados:",
|
||||
usuarios?.data?.length || 0,
|
||||
);
|
||||
console.log(
|
||||
"👤 [ChatList] Meu perfil:",
|
||||
meuPerfil?.data?.nome || "Carregando...",
|
||||
);
|
||||
console.log(
|
||||
"🆔 [ChatList] Meu ID:",
|
||||
meuPerfil?.data?._id || "Não encontrado",
|
||||
);
|
||||
if (usuarios?.data) {
|
||||
const meuId = meuPerfil?.data?._id;
|
||||
const meusDadosNaLista = usuarios.data.find((u: any) => u._id === meuId);
|
||||
if (meusDadosNaLista) {
|
||||
console.warn("⚠️ [ChatList] ATENÇÃO: Meu usuário está na lista do backend!", meusDadosNaLista.nome);
|
||||
console.warn(
|
||||
"⚠️ [ChatList] ATENÇÃO: Meu usuário está na lista do backend!",
|
||||
meusDadosNaLista.nome,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const usuariosFiltrados = $derived.by(() => {
|
||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
||||
|
||||
|
||||
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
|
||||
if (!meuPerfil?.data) {
|
||||
console.log("⏳ [ChatList] Aguardando perfil do usuário...");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const meuId = meuPerfil.data._id;
|
||||
|
||||
|
||||
// Filtrar o próprio usuário da lista (filtro de segurança no frontend)
|
||||
let listaFiltrada = usuarios.data.filter((u: any) => u._id !== meuId);
|
||||
|
||||
|
||||
// Log se ainda estiver na lista após filtro (não deveria acontecer)
|
||||
const aindaNaLista = listaFiltrada.find((u: any) => u._id === meuId);
|
||||
if (aindaNaLista) {
|
||||
console.error("❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!");
|
||||
console.error(
|
||||
"❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Aplicar busca por nome/email/matrícula
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
listaFiltrada = listaFiltrada.filter((u: any) =>
|
||||
u.nome?.toLowerCase().includes(query) ||
|
||||
u.email?.toLowerCase().includes(query) ||
|
||||
u.matricula?.toLowerCase().includes(query)
|
||||
listaFiltrada = listaFiltrada.filter(
|
||||
(u: any) =>
|
||||
u.nome?.toLowerCase().includes(query) ||
|
||||
u.email?.toLowerCase().includes(query) ||
|
||||
u.matricula?.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Ordenar: Online primeiro, depois por nome
|
||||
return listaFiltrada.sort((a: any, b: any) => {
|
||||
const statusOrder = { online: 0, ausente: 1, externo: 2, em_reuniao: 3, offline: 4 };
|
||||
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||
|
||||
const statusOrder = {
|
||||
online: 0,
|
||||
ausente: 1,
|
||||
externo: 2,
|
||||
em_reuniao: 3,
|
||||
offline: 4,
|
||||
};
|
||||
const statusA =
|
||||
statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||
const statusB =
|
||||
statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||
|
||||
if (statusA !== statusB) return statusA - statusB;
|
||||
return a.nome.localeCompare(b.nome);
|
||||
});
|
||||
@@ -101,19 +124,22 @@
|
||||
try {
|
||||
processando = true;
|
||||
console.log("🔄 Clicou no usuário:", usuario.nome, "ID:", usuario._id);
|
||||
|
||||
|
||||
// Criar ou buscar conversa individual com este usuário
|
||||
console.log("📞 Chamando mutation criarOuBuscarConversaIndividual...");
|
||||
const conversaId = await client.mutation(api.chat.criarOuBuscarConversaIndividual, {
|
||||
outroUsuarioId: usuario._id,
|
||||
});
|
||||
|
||||
const conversaId = await client.mutation(
|
||||
api.chat.criarOuBuscarConversaIndividual,
|
||||
{
|
||||
outroUsuarioId: usuario._id,
|
||||
},
|
||||
);
|
||||
|
||||
console.log("✅ Conversa criada/encontrada. ID:", conversaId);
|
||||
|
||||
|
||||
// Abrir a conversa
|
||||
console.log("📂 Abrindo conversa...");
|
||||
abrirConversa(conversaId as any);
|
||||
|
||||
|
||||
console.log("✅ Conversa aberta com sucesso!");
|
||||
} catch (error) {
|
||||
console.error("❌ Erro ao abrir conversa:", error);
|
||||
@@ -122,7 +148,9 @@
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
usuario: usuario,
|
||||
});
|
||||
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||
alert(
|
||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
@@ -142,19 +170,17 @@
|
||||
// Filtrar conversas por tipo e busca
|
||||
const conversasFiltradas = $derived(() => {
|
||||
if (!conversas?.data) return [];
|
||||
|
||||
let lista = conversas.data.filter((c: any) =>
|
||||
c.tipo === "grupo" || c.tipo === "sala_reuniao"
|
||||
|
||||
let lista = conversas.data.filter(
|
||||
(c: any) => c.tipo === "grupo" || c.tipo === "sala_reuniao",
|
||||
);
|
||||
|
||||
|
||||
// Aplicar busca
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
lista = lista.filter((c: any) =>
|
||||
c.nome?.toLowerCase().includes(query)
|
||||
);
|
||||
lista = lista.filter((c: any) => c.nome?.toLowerCase().includes(query));
|
||||
}
|
||||
|
||||
|
||||
return lista;
|
||||
});
|
||||
|
||||
@@ -165,7 +191,9 @@
|
||||
abrirConversa(conversa._id);
|
||||
} catch (error) {
|
||||
console.error("Erro ao abrir conversa:", error);
|
||||
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||
alert(
|
||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
@@ -218,7 +246,7 @@
|
||||
💬 Conversas ({conversasFiltradas().length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Botão Nova Conversa -->
|
||||
<div class="px-4 pb-2 flex justify-end">
|
||||
<button
|
||||
@@ -236,7 +264,11 @@
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4 mr-1"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15"
|
||||
/>
|
||||
</svg>
|
||||
Nova Conversa
|
||||
</button>
|
||||
@@ -247,17 +279,21 @@
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{#if activeTab === "usuarios"}
|
||||
<!-- Lista de usuários -->
|
||||
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
||||
{#each usuariosFiltrados as usuario (usuario._id)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando ? 'opacity-50 cursor-wait' : 'cursor-pointer'}"
|
||||
onclick={() => handleClickUsuario(usuario)}
|
||||
disabled={processando}
|
||||
>
|
||||
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
||||
{#each usuariosFiltrados as usuario (usuario._id)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
|
||||
? 'opacity-50 cursor-wait'
|
||||
: 'cursor-pointer'}"
|
||||
onclick={() => handleClickUsuario(usuario)}
|
||||
disabled={processando}
|
||||
>
|
||||
<!-- Ícone de mensagem -->
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
|
||||
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
|
||||
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -268,72 +304,83 @@
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5 text-primary"
|
||||
>
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
<path d="M9 10h.01M15 10h.01"/>
|
||||
<path
|
||||
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
|
||||
/>
|
||||
<path d="M9 10h.01M15 10h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<UserAvatar
|
||||
avatar={usuario.avatar}
|
||||
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
||||
nome={usuario.nome}
|
||||
size="md"
|
||||
/>
|
||||
<!-- Status badge -->
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<UserStatusBadge status={usuario.statusPresenca || "offline"} size="sm" />
|
||||
<!-- Avatar -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<UserAvatar
|
||||
avatar={usuario.avatar}
|
||||
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
||||
nome={usuario.nome}
|
||||
size="md"
|
||||
/>
|
||||
<!-- Status badge -->
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<UserStatusBadge
|
||||
status={usuario.statusPresenca || "offline"}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<p class="font-semibold text-base-content truncate">
|
||||
{usuario.nome}
|
||||
</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {
|
||||
usuario.statusPresenca === 'online' ? 'bg-success/20 text-success' :
|
||||
usuario.statusPresenca === 'ausente' ? 'bg-warning/20 text-warning' :
|
||||
usuario.statusPresenca === 'em_reuniao' ? 'bg-error/20 text-error' :
|
||||
'bg-base-300 text-base-content/50'
|
||||
}">
|
||||
{getStatusLabel(usuario.statusPresenca)}
|
||||
</span>
|
||||
<!-- Conteúdo -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<p class="font-semibold text-base-content truncate">
|
||||
{usuario.nome}
|
||||
</p>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full {usuario.statusPresenca ===
|
||||
'online'
|
||||
? 'bg-success/20 text-success'
|
||||
: usuario.statusPresenca === 'ausente'
|
||||
? 'bg-warning/20 text-warning'
|
||||
: usuario.statusPresenca === 'em_reuniao'
|
||||
? 'bg-error/20 text-error'
|
||||
: 'bg-base-300 text-base-content/50'}"
|
||||
>
|
||||
{getStatusLabel(usuario.statusPresenca)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm text-base-content/70 truncate">
|
||||
{usuario.statusMensagem || usuario.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm text-base-content/70 truncate">
|
||||
{usuario.statusMensagem || usuario.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{:else if !usuarios?.data}
|
||||
<!-- Loading -->
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Nenhum usuário encontrado -->
|
||||
<div class="flex flex-col items-center justify-center h-full text-center px-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-16 h-16 text-base-content/30 mb-4"
|
||||
</button>
|
||||
{/each}
|
||||
{:else if !usuarios?.data}
|
||||
<!-- Loading -->
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Nenhum usuário encontrado -->
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-base-content/70">Nenhum usuário encontrado</p>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-16 h-16 text-base-content/30 mb-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-base-content/70">Nenhum usuário encontrado</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Lista de conversas (grupos e salas) -->
|
||||
@@ -341,23 +388,48 @@
|
||||
{#each conversasFiltradas() as conversa (conversa._id)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando ? 'opacity-50 cursor-wait' : 'cursor-pointer'}"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
|
||||
? 'opacity-50 cursor-wait'
|
||||
: 'cursor-pointer'}"
|
||||
onclick={() => handleClickConversa(conversa)}
|
||||
disabled={processando}
|
||||
>
|
||||
<!-- Ícone de grupo/sala -->
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {
|
||||
conversa.tipo === 'sala_reuniao'
|
||||
? 'bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
|
||||
: 'bg-gradient-to-br from-primary/20 to-secondary/20 border border-primary/30'
|
||||
}">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {conversa.tipo ===
|
||||
'sala_reuniao'
|
||||
? 'bg-linear-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
|
||||
: 'bg-linear-to-br from-primary/20 to-secondary/20 border border-primary/30'}"
|
||||
>
|
||||
{#if conversa.tipo === "sala_reuniao"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-blue-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-blue-500"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-primary"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -366,21 +438,34 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<p class="font-semibold text-base-content truncate">
|
||||
{conversa.nome || (conversa.tipo === "sala_reuniao" ? "Sala sem nome" : "Grupo sem nome")}
|
||||
{conversa.nome ||
|
||||
(conversa.tipo === "sala_reuniao"
|
||||
? "Sala sem nome"
|
||||
: "Grupo sem nome")}
|
||||
</p>
|
||||
{#if conversa.naoLidas > 0}
|
||||
<span class="badge badge-primary badge-sm">{conversa.naoLidas}</span>
|
||||
<span class="badge badge-primary badge-sm"
|
||||
>{conversa.naoLidas}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {
|
||||
conversa.tipo === 'sala_reuniao' ? 'bg-blue-500/20 text-blue-500' : 'bg-primary/20 text-primary'
|
||||
}">
|
||||
{conversa.tipo === "sala_reuniao" ? "👑 Sala de Reunião" : "👥 Grupo"}
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full {conversa.tipo ===
|
||||
'sala_reuniao'
|
||||
? 'bg-blue-500/20 text-blue-500'
|
||||
: 'bg-primary/20 text-primary'}"
|
||||
>
|
||||
{conversa.tipo === "sala_reuniao"
|
||||
? "👑 Sala de Reunião"
|
||||
: "👥 Grupo"}
|
||||
</span>
|
||||
{#if conversa.participantesInfo}
|
||||
<span class="text-xs text-base-content/50">
|
||||
{conversa.participantesInfo.length} participante{conversa.participantesInfo.length !== 1 ? 's' : ''}
|
||||
{conversa.participantesInfo.length} participante{conversa
|
||||
.participantesInfo.length !== 1
|
||||
? "s"
|
||||
: ""}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -394,12 +479,29 @@
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Nenhuma conversa encontrada -->
|
||||
<div class="flex flex-col items-center justify-center h-full text-center px-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 text-base-content/30 mb-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-16 h-16 text-base-content/30 mb-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-base-content/70 font-medium mb-2">Nenhuma conversa encontrada</p>
|
||||
<p class="text-sm text-base-content/50">Crie um grupo ou sala de reunião para começar</p>
|
||||
<p class="text-base-content/70 font-medium mb-2">
|
||||
Nenhuma conversa encontrada
|
||||
</p>
|
||||
<p class="text-sm text-base-content/50">
|
||||
Crie um grupo ou sala de reunião para começar
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,17 @@
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { onMount } from "svelte";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import { Bell, Mail, AtSign, Users, Calendar, Clock, BellOff, Trash2, X } from "lucide-svelte";
|
||||
import {
|
||||
Bell,
|
||||
Mail,
|
||||
AtSign,
|
||||
Users,
|
||||
Calendar,
|
||||
Clock,
|
||||
BellOff,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-svelte";
|
||||
|
||||
// Queries e Client
|
||||
const client = useConvexClient();
|
||||
@@ -18,31 +28,46 @@
|
||||
});
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let notificacoesFerias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
|
||||
let notificacoesAusencias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
|
||||
let notificacoesFerias = $state<
|
||||
Array<{
|
||||
_id: string;
|
||||
mensagem: string;
|
||||
tipo: string;
|
||||
_creationTime: number;
|
||||
}>
|
||||
>([]);
|
||||
let notificacoesAusencias = $state<
|
||||
Array<{
|
||||
_id: string;
|
||||
mensagem: string;
|
||||
tipo: string;
|
||||
_creationTime: number;
|
||||
}>
|
||||
>([]);
|
||||
let limpandoNotificacoes = $state(false);
|
||||
|
||||
// Helpers para obter valores das queries
|
||||
const count = $derived(
|
||||
(typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0
|
||||
(typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0,
|
||||
);
|
||||
const todasNotificacoes = $derived(
|
||||
(Array.isArray(todasNotificacoesQuery)
|
||||
? todasNotificacoesQuery
|
||||
: todasNotificacoesQuery?.data) ?? []
|
||||
: todasNotificacoesQuery?.data) ?? [],
|
||||
);
|
||||
|
||||
|
||||
// Separar notificações lidas e não lidas
|
||||
const notificacoesNaoLidas = $derived(
|
||||
todasNotificacoes.filter((n) => !n.lida)
|
||||
);
|
||||
const notificacoesLidas = $derived(
|
||||
todasNotificacoes.filter((n) => n.lida)
|
||||
todasNotificacoes.filter((n) => !n.lida),
|
||||
);
|
||||
const notificacoesLidas = $derived(todasNotificacoes.filter((n) => n.lida));
|
||||
|
||||
// Atualizar contador no store
|
||||
$effect(() => {
|
||||
const totalNotificacoes = count + (notificacoesFerias?.length || 0) + (notificacoesAusencias?.length || 0);
|
||||
const totalNotificacoes =
|
||||
count +
|
||||
(notificacoesFerias?.length || 0) +
|
||||
(notificacoesAusencias?.length || 0);
|
||||
notificacoesCount.set(totalNotificacoes);
|
||||
});
|
||||
|
||||
@@ -56,7 +81,7 @@
|
||||
api.ferias.obterNotificacoesNaoLidas,
|
||||
{
|
||||
usuarioId: usuarioStore.usuario._id,
|
||||
}
|
||||
},
|
||||
);
|
||||
notificacoesFerias = notifsFerias || [];
|
||||
}
|
||||
@@ -76,14 +101,20 @@
|
||||
api.ausencias.obterNotificacoesNaoLidas,
|
||||
{
|
||||
usuarioId: usuarioStore.usuario._id,
|
||||
}
|
||||
},
|
||||
);
|
||||
notificacoesAusencias = notifsAusencias || [];
|
||||
} catch (queryError: unknown) {
|
||||
// Silenciar erro se a função não estiver disponível ainda (Convex não sincronizado)
|
||||
const errorMessage = queryError instanceof Error ? queryError.message : String(queryError);
|
||||
const errorMessage =
|
||||
queryError instanceof Error
|
||||
? queryError.message
|
||||
: String(queryError);
|
||||
if (!errorMessage.includes("Could not find public function")) {
|
||||
console.error("Erro ao buscar notificações de ausências:", queryError);
|
||||
console.error(
|
||||
"Erro ao buscar notificações de ausências:",
|
||||
queryError,
|
||||
);
|
||||
}
|
||||
notificacoesAusencias = [];
|
||||
}
|
||||
@@ -201,7 +232,10 @@
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest(".notification-popup") && !target.closest(".notification-bell")) {
|
||||
if (
|
||||
!target.closest(".notification-popup") &&
|
||||
!target.closest(".notification-bell")
|
||||
) {
|
||||
modalOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -233,7 +267,7 @@
|
||||
>
|
||||
<!-- 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"
|
||||
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
></div>
|
||||
|
||||
<!-- Anel de pulso sutil -->
|
||||
@@ -271,52 +305,59 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Popup Flutuante de Notificações -->
|
||||
{#if modalOpen}
|
||||
<div class="notification-popup fixed right-4 top-24 z-[100] w-[calc(100vw-2rem)] max-w-2xl max-h-[calc(100vh-7rem)] flex flex-col bg-base-100 rounded-2xl shadow-2xl border border-base-300 overflow-hidden backdrop-blur-sm" style="animation: slideDown 0.2s ease-out;">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 bg-gradient-to-r from-primary/5 to-primary/10">
|
||||
<h3 class="text-2xl font-bold text-primary">Notificações</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if notificacoesNaoLidas.length > 0}
|
||||
<!-- Popup Flutuante de Notificações -->
|
||||
{#if modalOpen}
|
||||
<div
|
||||
class="notification-popup fixed right-4 top-24 z-[100] w-[calc(100vw-2rem)] max-w-2xl max-h-[calc(100vh-7rem)] flex flex-col bg-base-100 rounded-2xl shadow-2xl border border-base-300 overflow-hidden backdrop-blur-sm"
|
||||
style="animation: slideDown 0.2s ease-out;"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300 bg-linear-to-r from-primary/5 to-primary/10"
|
||||
>
|
||||
<h3 class="text-2xl font-bold text-primary">Notificações</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if notificacoesNaoLidas.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={handleLimparNotificacoesNaoLidas}
|
||||
disabled={limpandoNotificacoes}
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Limpar não lidas
|
||||
</button>
|
||||
{/if}
|
||||
{#if todasNotificacoes.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error btn-outline"
|
||||
onclick={handleLimparTodasNotificacoes}
|
||||
disabled={limpandoNotificacoes}
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Limpar todas
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={handleLimparNotificacoesNaoLidas}
|
||||
disabled={limpandoNotificacoes}
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={closeModal}
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Limpar não lidas
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if todasNotificacoes.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error btn-outline"
|
||||
onclick={handleLimparTodasNotificacoes}
|
||||
disabled={limpandoNotificacoes}
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
Limpar todas
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={closeModal}
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de notificações -->
|
||||
<div class="flex-1 overflow-y-auto px-2 py-4">
|
||||
<!-- Lista de notificações -->
|
||||
<div class="flex-1 overflow-y-auto px-2 py-4">
|
||||
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
|
||||
<!-- Notificações não lidas -->
|
||||
{#if notificacoesNaoLidas.length > 0}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-primary mb-2 px-2">Não lidas</h4>
|
||||
<h4 class="text-sm font-semibold text-primary mb-2 px-2">
|
||||
Não lidas
|
||||
</h4>
|
||||
{#each notificacoesNaoLidas as notificacao (notificacao._id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -329,7 +370,10 @@
|
||||
{#if notificacao.tipo === "nova_mensagem"}
|
||||
<Mail class="w-5 h-5 text-primary" strokeWidth={1.5} />
|
||||
{:else if notificacao.tipo === "mencao"}
|
||||
<AtSign class="w-5 h-5 text-warning" strokeWidth={1.5} />
|
||||
<AtSign
|
||||
class="w-5 h-5 text-warning"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{:else}
|
||||
<Users class="w-5 h-5 text-info" strokeWidth={1.5} />
|
||||
{/if}
|
||||
@@ -341,21 +385,27 @@
|
||||
<p class="text-sm font-semibold text-primary">
|
||||
{notificacao.remetente.nome}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/70 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{:else if notificacao.tipo === "mencao" && notificacao.remetente}
|
||||
<p class="text-sm font-semibold text-warning">
|
||||
{notificacao.remetente.nome} mencionou você
|
||||
</p>
|
||||
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/70 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm font-semibold text-base-content">
|
||||
{notificacao.titulo}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/70 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{/if}
|
||||
@@ -377,7 +427,9 @@
|
||||
<!-- Notificações lidas -->
|
||||
{#if notificacoesLidas.length > 0}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-base-content/60 mb-2 px-2">Lidas</h4>
|
||||
<h4 class="text-sm font-semibold text-base-content/60 mb-2 px-2">
|
||||
Lidas
|
||||
</h4>
|
||||
{#each notificacoesLidas as notificacao (notificacao._id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -388,9 +440,15 @@
|
||||
<!-- Ícone -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
{#if notificacao.tipo === "nova_mensagem"}
|
||||
<Mail class="w-5 h-5 text-primary/60" strokeWidth={1.5} />
|
||||
<Mail
|
||||
class="w-5 h-5 text-primary/60"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{:else if notificacao.tipo === "mencao"}
|
||||
<AtSign class="w-5 h-5 text-warning/60" strokeWidth={1.5} />
|
||||
<AtSign
|
||||
class="w-5 h-5 text-warning/60"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{:else}
|
||||
<Users class="w-5 h-5 text-info/60" strokeWidth={1.5} />
|
||||
{/if}
|
||||
@@ -402,21 +460,27 @@
|
||||
<p class="text-sm font-medium text-primary/70">
|
||||
{notificacao.remetente.nome}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/60 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{:else if notificacao.tipo === "mencao" && notificacao.remetente}
|
||||
<p class="text-sm font-medium text-warning/70">
|
||||
{notificacao.remetente.nome} mencionou você
|
||||
</p>
|
||||
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/60 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm font-medium text-base-content/70">
|
||||
{notificacao.titulo}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
|
||||
<p
|
||||
class="text-xs text-base-content/60 mt-1 line-clamp-2"
|
||||
>
|
||||
{notificacao.descricao}
|
||||
</p>
|
||||
{/if}
|
||||
@@ -433,7 +497,9 @@
|
||||
<!-- Notificações de Férias -->
|
||||
{#if notificacoesFerias.length > 0}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-purple-600 mb-2 px-2">Férias</h4>
|
||||
<h4 class="text-sm font-semibold text-purple-600 mb-2 px-2">
|
||||
Férias
|
||||
</h4>
|
||||
{#each notificacoesFerias as notificacao (notificacao._id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -443,7 +509,10 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Ícone -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<Calendar class="w-5 h-5 text-purple-600" strokeWidth={2} />
|
||||
<Calendar
|
||||
class="w-5 h-5 text-purple-600"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo -->
|
||||
@@ -469,12 +538,15 @@
|
||||
<!-- Notificações de Ausências -->
|
||||
{#if notificacoesAusencias.length > 0}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-orange-600 mb-2 px-2">Ausências</h4>
|
||||
<h4 class="text-sm font-semibold text-orange-600 mb-2 px-2">
|
||||
Ausências
|
||||
</h4>
|
||||
{#each notificacoesAusencias as notificacao (notificacao._id)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 border-l-4 border-orange-600"
|
||||
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
|
||||
onclick={() =>
|
||||
handleClickNotificacaoAusencias(notificacao._id)}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Ícone -->
|
||||
@@ -504,30 +576,37 @@
|
||||
{:else}
|
||||
<!-- Sem notificações -->
|
||||
<div class="px-4 py-12 text-center text-base-content/50">
|
||||
<BellOff class="w-16 h-16 mx-auto mb-4 opacity-50" strokeWidth={1.5} />
|
||||
<BellOff
|
||||
class="w-16 h-16 mx-auto mb-4 opacity-50"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<p class="text-base font-medium">Nenhuma notificação</p>
|
||||
<p class="text-sm mt-1">Você está em dia!</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer com estatísticas -->
|
||||
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
|
||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
||||
<div class="flex items-center justify-between text-xs text-base-content/60">
|
||||
<span>
|
||||
Total: {todasNotificacoes.length + notificacoesFerias.length + notificacoesAusencias.length} notificações
|
||||
</span>
|
||||
{#if notificacoesNaoLidas.length > 0}
|
||||
<span class="text-primary font-semibold">
|
||||
{notificacoesNaoLidas.length} não lidas
|
||||
<!-- Footer com estatísticas -->
|
||||
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
|
||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
||||
<div
|
||||
class="flex items-center justify-between text-xs text-base-content/60"
|
||||
>
|
||||
<span>
|
||||
Total: {todasNotificacoes.length +
|
||||
notificacoesFerias.length +
|
||||
notificacoesAusencias.length} notificações
|
||||
</span>
|
||||
{/if}
|
||||
{#if notificacoesNaoLidas.length > 0}
|
||||
<span class="text-primary font-semibold">
|
||||
{notificacoesNaoLidas.length} não lidas
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -582,4 +661,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -12,17 +12,29 @@
|
||||
|
||||
// Queries
|
||||
const saldosQuery = useQuery(api.saldoFerias.listarSaldos, { funcionarioId });
|
||||
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, { funcionarioId });
|
||||
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, {
|
||||
funcionarioId,
|
||||
});
|
||||
|
||||
const saldos = $derived(saldosQuery.data || []);
|
||||
const solicitacoes = $derived(solicitacoesQuery.data || []);
|
||||
|
||||
// Estatísticas derivadas
|
||||
const saldoAtual = $derived(saldos.find((s) => s.anoReferencia === new Date().getFullYear()));
|
||||
const saldoAtual = $derived(
|
||||
saldos.find((s) => s.anoReferencia === new Date().getFullYear()),
|
||||
);
|
||||
const totalSolicitacoes = $derived(solicitacoes.length);
|
||||
const aprovadas = $derived(solicitacoes.filter((s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length);
|
||||
const pendentes = $derived(solicitacoes.filter((s) => s.status === "aguardando_aprovacao").length);
|
||||
const reprovadas = $derived(solicitacoes.filter((s) => s.status === "reprovado").length);
|
||||
const aprovadas = $derived(
|
||||
solicitacoes.filter(
|
||||
(s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada",
|
||||
).length,
|
||||
);
|
||||
const pendentes = $derived(
|
||||
solicitacoes.filter((s) => s.status === "aguardando_aprovacao").length,
|
||||
);
|
||||
const reprovadas = $derived(
|
||||
solicitacoes.filter((s) => s.status === "reprovado").length,
|
||||
);
|
||||
|
||||
// Canvas para gráfico de pizza
|
||||
let canvasSaldo = $state<HTMLCanvasElement>();
|
||||
@@ -31,7 +43,7 @@
|
||||
// Função para desenhar gráfico de pizza moderno
|
||||
function desenharGraficoPizza(
|
||||
canvas: HTMLCanvasElement,
|
||||
dados: { label: string; valor: number; cor: string }[]
|
||||
dados: { label: string; valor: number; cor: string }[],
|
||||
) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
@@ -90,7 +102,11 @@
|
||||
desenharGraficoPizza(canvasSaldo, [
|
||||
{ label: "Usado", valor: saldoAtual.diasUsados, cor: "#ff6b6b" },
|
||||
{ label: "Pendente", valor: saldoAtual.diasPendentes, cor: "#ffa94d" },
|
||||
{ label: "Disponível", valor: saldoAtual.diasDisponiveis, cor: "#51cf66" },
|
||||
{
|
||||
label: "Disponível",
|
||||
valor: saldoAtual.diasDisponiveis,
|
||||
cor: "#51cf66",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -107,10 +123,14 @@
|
||||
<div class="dashboard-ferias">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<h1
|
||||
class="text-4xl font-bold bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
📊 Dashboard de Férias
|
||||
</h1>
|
||||
<p class="text-base-content/70 mt-2">Visualize seus saldos e histórico de solicitações</p>
|
||||
<p class="text-base-content/70 mt-2">
|
||||
Visualize seus saldos e histórico de solicitações
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if saldosQuery.isLoading || solicitacoesQuery.isLoading}
|
||||
@@ -125,7 +145,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Card 1: Saldo Disponível -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-success/20 to-success/5 border-2 border-success/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
class="stat bg-linear-to-br from-success/20 to-success/5 border-2 border-success/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-success">
|
||||
<svg
|
||||
@@ -143,13 +163,15 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-success font-semibold">Disponível</div>
|
||||
<div class="stat-value text-success text-4xl">{saldoAtual?.diasDisponiveis || 0}</div>
|
||||
<div class="stat-value text-success text-4xl">
|
||||
{saldoAtual?.diasDisponiveis || 0}
|
||||
</div>
|
||||
<div class="stat-desc text-success/70">dias para usar</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Dias Usados -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-error/20 to-error/5 border-2 border-error/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
class="stat bg-linear-to-br from-error/20 to-error/5 border-2 border-error/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-error">
|
||||
<svg
|
||||
@@ -167,13 +189,15 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-error font-semibold">Usado</div>
|
||||
<div class="stat-value text-error text-4xl">{saldoAtual?.diasUsados || 0}</div>
|
||||
<div class="stat-value text-error text-4xl">
|
||||
{saldoAtual?.diasUsados || 0}
|
||||
</div>
|
||||
<div class="stat-desc text-error/70">dias já gozados</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Pendentes -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-warning/20 to-warning/5 border-2 border-warning/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
class="stat bg-linear-to-br from-warning/20 to-warning/5 border-2 border-warning/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-warning">
|
||||
<svg
|
||||
@@ -191,13 +215,15 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-warning font-semibold">Pendentes</div>
|
||||
<div class="stat-value text-warning text-4xl">{saldoAtual?.diasPendentes || 0}</div>
|
||||
<div class="stat-value text-warning text-4xl">
|
||||
{saldoAtual?.diasPendentes || 0}
|
||||
</div>
|
||||
<div class="stat-desc text-warning/70">aguardando aprovação</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Total de Direito -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
class="stat bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-primary">
|
||||
<svg
|
||||
@@ -215,7 +241,9 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-primary font-semibold">Total Direito</div>
|
||||
<div class="stat-value text-primary text-4xl">{saldoAtual?.diasDireito || 0}</div>
|
||||
<div class="stat-value text-primary text-4xl">
|
||||
{saldoAtual?.diasDireito || 0}
|
||||
</div>
|
||||
<div class="stat-desc text-primary/70">dias no ano</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,15 +274,21 @@
|
||||
<div class="flex justify-center gap-4 mt-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
|
||||
<span class="text-sm font-semibold">Disponível: {saldoAtual.diasDisponiveis} dias</span>
|
||||
<span class="text-sm font-semibold"
|
||||
>Disponível: {saldoAtual.diasDisponiveis} dias</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
|
||||
<span class="text-sm font-semibold">Pendente: {saldoAtual.diasPendentes} dias</span>
|
||||
<span class="text-sm font-semibold"
|
||||
>Pendente: {saldoAtual.diasPendentes} dias</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
|
||||
<span class="text-sm font-semibold">Usado: {saldoAtual.diasUsados} dias</span>
|
||||
<span class="text-sm font-semibold"
|
||||
>Usado: {saldoAtual.diasUsados} dias</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -283,7 +317,9 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">
|
||||
📋 Status de Solicitações
|
||||
<div class="badge badge-secondary badge-lg">Total: {totalSolicitacoes}</div>
|
||||
<div class="badge badge-secondary badge-lg">
|
||||
Total: {totalSolicitacoes}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{#if totalSolicitacoes > 0}
|
||||
@@ -300,15 +336,19 @@
|
||||
<div class="flex justify-center gap-4 mt-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
|
||||
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span>
|
||||
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
|
||||
<span class="text-sm font-semibold">Pendentes: {pendentes}</span>
|
||||
<span class="text-sm font-semibold">Pendentes: {pendentes}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
|
||||
<span class="text-sm font-semibold">Reprovadas: {reprovadas}</span>
|
||||
<span class="text-sm font-semibold"
|
||||
>Reprovadas: {reprovadas}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -356,9 +396,20 @@
|
||||
<tr>
|
||||
<td class="font-bold">{saldo.anoReferencia}</td>
|
||||
<td>{saldo.diasDireito} dias</td>
|
||||
<td><span class="badge badge-error">{saldo.diasUsados}</span></td>
|
||||
<td><span class="badge badge-warning">{saldo.diasPendentes}</span></td>
|
||||
<td><span class="badge badge-success">{saldo.diasDisponiveis}</span></td>
|
||||
<td
|
||||
><span class="badge badge-error">{saldo.diasUsados}</span
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="badge badge-warning"
|
||||
>{saldo.diasPendentes}</span
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="badge badge-success"
|
||||
>{saldo.diasDisponiveis}</span
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
{#if saldo.status === "ativo"}
|
||||
<span class="badge badge-success">Ativo</span>
|
||||
@@ -390,5 +441,3 @@
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
|
||||
// Dados da solicitação
|
||||
let anoSelecionado = $state(new Date().getFullYear());
|
||||
let periodosFerias: Array<{ dataInicio: string; dataFim: string; dias: number }> = $state([]);
|
||||
let periodosFerias: Array<{
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
dias: number;
|
||||
}> = $state([]);
|
||||
let observacao = $state("");
|
||||
let processando = $state(false);
|
||||
|
||||
@@ -31,7 +35,7 @@
|
||||
useQuery(api.saldoFerias.obterSaldo, {
|
||||
funcionarioId,
|
||||
anoReferencia: anoSelecionado,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const validacaoQuery = $derived(
|
||||
@@ -44,14 +48,14 @@
|
||||
dataFim: p.dataFim,
|
||||
})),
|
||||
})
|
||||
: { data: null }
|
||||
: { data: null },
|
||||
);
|
||||
|
||||
// Derivados
|
||||
const saldo = $derived(saldoQuery.data);
|
||||
const validacao = $derived(validacaoQuery.data);
|
||||
const totalDiasSelecionados = $derived(
|
||||
periodosFerias.reduce((acc, p) => acc + p.dias, 0)
|
||||
periodosFerias.reduce((acc, p) => acc + p.dias, 0),
|
||||
);
|
||||
|
||||
// Anos disponíveis (últimos 3 anos + próximo ano)
|
||||
@@ -61,9 +65,11 @@
|
||||
});
|
||||
|
||||
// Configurações do calendário (baseado no saldo/regime)
|
||||
const maxPeriodos = $derived(saldo?.regimeTrabalho?.includes("Servidor") ? 2 : 3);
|
||||
const maxPeriodos = $derived(
|
||||
saldo?.regimeTrabalho?.includes("Servidor") ? 2 : 3,
|
||||
);
|
||||
const minDiasPorPeriodo = $derived(
|
||||
saldo?.regimeTrabalho?.includes("Servidor") ? 10 : 5
|
||||
saldo?.regimeTrabalho?.includes("Servidor") ? 10 : 5,
|
||||
);
|
||||
|
||||
// Funções
|
||||
@@ -154,7 +160,9 @@
|
||||
class:border-primary={passoAtual === i + 1}
|
||||
class:bg-base-200={passoAtual < i + 1}
|
||||
class:text-base-content={passoAtual < i + 1}
|
||||
style:box-shadow={passoAtual === i + 1 ? "0 0 20px rgba(102, 126, 234, 0.5)" : "none"}
|
||||
style:box-shadow={passoAtual === i + 1
|
||||
? "0 0 20px rgba(102, 126, 234, 0.5)"
|
||||
: "none"}
|
||||
>
|
||||
{#if passoAtual > i + 1}
|
||||
<svg
|
||||
@@ -191,13 +199,19 @@
|
||||
<!-- Labels dos passos -->
|
||||
<div class="flex justify-between mt-4 px-1">
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 1}>Ano & Saldo</p>
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 1}>
|
||||
Ano & Saldo
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 2}>Períodos</p>
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 2}>
|
||||
Períodos
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 3}>Confirmação</p>
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 3}>
|
||||
Confirmação
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +221,9 @@
|
||||
<!-- PASSO 1: Ano & Saldo -->
|
||||
{#if passoAtual === 1}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<h2
|
||||
class="text-3xl font-bold mb-6 text-center bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
Escolha o Ano de Referência
|
||||
</h2>
|
||||
|
||||
@@ -231,14 +247,16 @@
|
||||
<div class="skeleton h-64 w-full rounded-2xl"></div>
|
||||
{:else if saldo}
|
||||
<div
|
||||
class="card bg-gradient-to-br from-primary/10 to-secondary/10 shadow-2xl border-2 border-primary/20"
|
||||
class="card bg-linear-to-br from-primary/10 to-secondary/10 shadow-2xl border-2 border-primary/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-2xl mb-4">
|
||||
📊 Saldo de Férias {anoSelecionado}
|
||||
</h3>
|
||||
|
||||
<div class="stats stats-vertical lg:stats-horizontal shadow-lg w-full">
|
||||
<div
|
||||
class="stats stats-vertical lg:stats-horizontal shadow-lg w-full"
|
||||
>
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg
|
||||
@@ -277,7 +295,9 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Disponível</div>
|
||||
<div class="stat-value text-success">{saldo.diasDisponiveis}</div>
|
||||
<div class="stat-value text-success">
|
||||
{saldo.diasDisponiveis}
|
||||
</div>
|
||||
<div class="stat-desc">para usar</div>
|
||||
</div>
|
||||
|
||||
@@ -321,7 +341,9 @@
|
||||
<div>
|
||||
<h4 class="font-bold">{saldo.regimeTrabalho}</h4>
|
||||
<p class="text-sm">
|
||||
Período aquisitivo: {new Date(saldo.dataInicio).toLocaleDateString("pt-BR")}
|
||||
Período aquisitivo: {new Date(
|
||||
saldo.dataInicio,
|
||||
).toLocaleDateString("pt-BR")}
|
||||
a {new Date(saldo.dataFim).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -371,7 +393,9 @@
|
||||
<!-- PASSO 2: Seleção de Períodos -->
|
||||
{#if passoAtual === 2}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<h2
|
||||
class="text-3xl font-bold mb-6 text-center bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
Selecione os Períodos de Férias
|
||||
</h2>
|
||||
|
||||
@@ -393,7 +417,8 @@
|
||||
<div>
|
||||
<p>
|
||||
<strong>Saldo disponível:</strong>
|
||||
{saldo?.diasDisponiveis || 0} dias | <strong>Selecionados:</strong>
|
||||
{saldo?.diasDisponiveis || 0} dias |
|
||||
<strong>Selecionados:</strong>
|
||||
{totalDiasSelecionados} dias | <strong>Restante:</strong>
|
||||
{(saldo?.diasDisponiveis || 0) - totalDiasSelecionados} dias
|
||||
</p>
|
||||
@@ -405,10 +430,10 @@
|
||||
periodosExistentes={periodosFerias}
|
||||
onPeriodoAdicionado={handlePeriodoAdicionado}
|
||||
onPeriodoRemovido={handlePeriodoRemovido}
|
||||
maxPeriodos={maxPeriodos}
|
||||
minDiasPorPeriodo={minDiasPorPeriodo}
|
||||
modoVisualizacao="month">
|
||||
</CalendarioFerias>
|
||||
{maxPeriodos}
|
||||
{minDiasPorPeriodo}
|
||||
modoVisualizacao="month"
|
||||
></CalendarioFerias>
|
||||
|
||||
<!-- Validações -->
|
||||
{#if validacao && periodosFerias.length > 0}
|
||||
@@ -428,7 +453,9 @@
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>✅ Períodos válidos! Total: {validacao.totalDias} dias</span>
|
||||
<span
|
||||
>✅ Períodos válidos! Total: {validacao.totalDias} dias</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-error">
|
||||
@@ -489,7 +516,9 @@
|
||||
<!-- PASSO 3: Confirmação -->
|
||||
{#if passoAtual === 3}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
<h2
|
||||
class="text-3xl font-bold mb-6 text-center bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||
>
|
||||
Confirme sua Solicitação
|
||||
</h2>
|
||||
|
||||
@@ -506,7 +535,9 @@
|
||||
|
||||
<div class="stat bg-base-200 rounded-lg">
|
||||
<div class="stat-title">Total de Dias</div>
|
||||
<div class="stat-value text-success">{totalDiasSelecionados}</div>
|
||||
<div class="stat-value text-success">
|
||||
{totalDiasSelecionados}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -521,11 +552,14 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">
|
||||
{new Date(periodo.dataInicio).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
{new Date(periodo.dataInicio).toLocaleDateString(
|
||||
"pt-BR",
|
||||
{
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
até
|
||||
{new Date(periodo.dataFim).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
@@ -533,7 +567,9 @@
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/70">{periodo.dias} dias corridos</p>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{periodo.dias} dias corridos
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -542,7 +578,9 @@
|
||||
<!-- Campo de Observação -->
|
||||
<div class="form-control mt-6">
|
||||
<label for="observacao" class="label">
|
||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
||||
<span class="label-text font-semibold"
|
||||
>Observações (opcional)</span
|
||||
>
|
||||
</label>
|
||||
<textarea
|
||||
id="observacao"
|
||||
@@ -561,7 +599,11 @@
|
||||
<div class="flex justify-between mt-8">
|
||||
<div>
|
||||
{#if passoAtual > 1}
|
||||
<button type="button" class="btn btn-outline btn-lg gap-2" onclick={passoAnterior}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-lg gap-2"
|
||||
onclick={passoAnterior}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
@@ -685,4 +727,3 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
notifyByEmail,
|
||||
notifyByChat,
|
||||
});
|
||||
|
||||
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar alerta:", error);
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
async function deleteAlert(alertId: Id<"alertConfigurations">) {
|
||||
if (!confirm("Tem certeza que deseja deletar este alerta?")) return;
|
||||
|
||||
|
||||
try {
|
||||
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
||||
} catch (error) {
|
||||
@@ -100,16 +100,18 @@
|
||||
}
|
||||
|
||||
function getMetricLabel(metricName: string): string {
|
||||
return metricOptions.find(m => m.value === metricName)?.label || metricName;
|
||||
return (
|
||||
metricOptions.find((m) => m.value === metricName)?.label || metricName
|
||||
);
|
||||
}
|
||||
|
||||
function getOperatorLabel(op: string): string {
|
||||
return operatorOptions.find(o => o.value === op)?.label || op;
|
||||
return operatorOptions.find((o) => o.value === op)?.label || op;
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl bg-gradient-to-br from-base-100 to-base-200">
|
||||
<div class="modal-box max-w-4xl bg-linear-to-br from-base-100 to-base-200">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
@@ -118,18 +120,33 @@
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h3 class="font-bold text-3xl text-primary mb-2">⚙️ Configuração de Alertas</h3>
|
||||
<p class="text-base-content/60 mb-6">Configure alertas personalizados para monitoramento do sistema</p>
|
||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
||||
⚙️ Configuração de Alertas
|
||||
</h3>
|
||||
<p class="text-base-content/60 mb-6">
|
||||
Configure alertas personalizados para monitoramento do sistema
|
||||
</p>
|
||||
|
||||
<!-- Botão Novo Alerta -->
|
||||
{#if !showForm}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary mb-6"
|
||||
onclick={() => showForm = true}
|
||||
onclick={() => (showForm = true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Novo Alerta
|
||||
</button>
|
||||
@@ -149,7 +166,7 @@
|
||||
<label class="label" for="metric">
|
||||
<span class="label-text font-semibold">Métrica</span>
|
||||
</label>
|
||||
<select
|
||||
<select
|
||||
id="metric"
|
||||
class="select select-bordered select-primary"
|
||||
bind:value={metricName}
|
||||
@@ -165,7 +182,7 @@
|
||||
<label class="label" for="operator">
|
||||
<span class="label-text font-semibold">Condição</span>
|
||||
</label>
|
||||
<select
|
||||
<select
|
||||
id="operator"
|
||||
class="select select-bordered select-primary"
|
||||
bind:value={operator}
|
||||
@@ -181,7 +198,7 @@
|
||||
<label class="label" for="threshold">
|
||||
<span class="label-text font-semibold">Valor Limite</span>
|
||||
</label>
|
||||
<input
|
||||
<input
|
||||
id="threshold"
|
||||
type="number"
|
||||
class="input input-bordered input-primary"
|
||||
@@ -195,7 +212,7 @@
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<span class="label-text font-semibold">Alerta Ativo</span>
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={enabled}
|
||||
@@ -208,28 +225,50 @@
|
||||
<div class="divider">Método de Notificação</div>
|
||||
<div class="flex gap-6">
|
||||
<label class="label cursor-pointer gap-3">
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
bind:checked={notifyByChat}
|
||||
/>
|
||||
<span class="label-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 inline mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
||||
/>
|
||||
</svg>
|
||||
Notificar por Chat
|
||||
</span>
|
||||
</label>
|
||||
|
||||
|
||||
<label class="label cursor-pointer gap-3">
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-secondary"
|
||||
bind:checked={notifyByEmail}
|
||||
/>
|
||||
<span class="label-text">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 inline mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Notificar por E-mail
|
||||
</span>
|
||||
@@ -238,21 +277,32 @@
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="alert alert-info mt-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">
|
||||
<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
|
||||
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>
|
||||
<div>
|
||||
<h4 class="font-bold">Preview do Alerta:</h4>
|
||||
<p class="text-sm">
|
||||
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
||||
<strong>{getOperatorLabel(operator)}</strong> a <strong>{threshold}</strong>
|
||||
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
||||
<strong>{getOperatorLabel(operator)}</strong> a
|
||||
<strong>{threshold}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botões -->
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={resetForm}
|
||||
@@ -260,7 +310,7 @@
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onclick={saveAlert}
|
||||
@@ -270,8 +320,19 @@
|
||||
<span class="loading loading-spinner"></span>
|
||||
Salvando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Salvar Alerta
|
||||
{/if}
|
||||
@@ -283,7 +344,7 @@
|
||||
|
||||
<!-- Lista de Alertas -->
|
||||
<div class="divider">Alertas Configurados</div>
|
||||
|
||||
|
||||
{#if alertas.length > 0}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
@@ -300,25 +361,50 @@
|
||||
{#each alertas as alerta}
|
||||
<tr class={!alerta.enabled ? "opacity-50" : ""}>
|
||||
<td>
|
||||
<div class="font-semibold">{getMetricLabel(alerta.metricName)}</div>
|
||||
<div class="font-semibold">
|
||||
{getMetricLabel(alerta.metricName)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge badge-outline">
|
||||
{getOperatorLabel(alerta.operator)} {alerta.threshold}
|
||||
{getOperatorLabel(alerta.operator)}
|
||||
{alerta.threshold}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{#if alerta.enabled}
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Ativo
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-ghost gap-2">
|
||||
<svg 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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Inativo
|
||||
</div>
|
||||
@@ -341,8 +427,19 @@
|
||||
class="btn btn-ghost btn-xs"
|
||||
onclick={() => editAlert(alerta)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@@ -350,8 +447,19 @@
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
onclick={() => deleteAlert(alerta._id)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -363,10 +471,22 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info 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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info 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>
|
||||
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
|
||||
<span
|
||||
>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -380,4 +500,3 @@
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
function setPeriod(type: string) {
|
||||
periodType = type;
|
||||
const now = new Date();
|
||||
|
||||
|
||||
switch (type) {
|
||||
case "today":
|
||||
dataInicio = format(now, "yyyy-MM-dd");
|
||||
@@ -63,14 +63,16 @@
|
||||
}
|
||||
|
||||
function getDateRange(): { inicio: number; fim: number } {
|
||||
const inicio = startOfDay(new Date(`${dataInicio}T${horaInicio}`)).getTime();
|
||||
const inicio = startOfDay(
|
||||
new Date(`${dataInicio}T${horaInicio}`),
|
||||
).getTime();
|
||||
const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
|
||||
return { inicio, fim };
|
||||
}
|
||||
|
||||
async function generatePDF() {
|
||||
generating = true;
|
||||
|
||||
|
||||
try {
|
||||
const { inicio, fim } = getDateRange();
|
||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||
@@ -79,33 +81,37 @@
|
||||
});
|
||||
|
||||
const doc = new jsPDF();
|
||||
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(102, 126, 234); // Primary color
|
||||
doc.text("Relatório de Monitoramento do Sistema", 14, 20);
|
||||
|
||||
|
||||
// Subtítulo com período
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.text(
|
||||
`Período: ${format(inicio, "dd/MM/yyyy HH:mm", { locale: ptBR })} até ${format(fim, "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
|
||||
14,
|
||||
30
|
||||
30,
|
||||
);
|
||||
|
||||
|
||||
// Informações gerais
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`, 14, 38);
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
|
||||
14,
|
||||
38,
|
||||
);
|
||||
doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44);
|
||||
|
||||
|
||||
// Estatísticas
|
||||
let yPos = 55;
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text("Estatísticas do Período", 14, yPos);
|
||||
yPos += 10;
|
||||
|
||||
|
||||
const statsData: any[] = [];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
if (selected && relatorio.estatisticas[metric]) {
|
||||
@@ -120,7 +126,7 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [["Métrica", "Mínimo", "Máximo", "Média"]],
|
||||
@@ -128,16 +134,16 @@
|
||||
theme: "striped",
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
});
|
||||
|
||||
|
||||
// Dados detalhados (últimos 50 registros)
|
||||
const finalY = (doc as any).lastAutoTable.finalY || yPos + 10;
|
||||
yPos = finalY + 15;
|
||||
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text("Registros Detalhados (Últimos 50)", 14, yPos);
|
||||
yPos += 10;
|
||||
|
||||
|
||||
const detailsData = relatorio.metricas.slice(0, 50).map((m) => {
|
||||
const row = [format(m.timestamp, "dd/MM HH:mm", { locale: ptBR })];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
@@ -147,14 +153,14 @@
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
|
||||
const headers = ["Data/Hora"];
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
if (selected) {
|
||||
headers.push(metricLabels[metric]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [headers],
|
||||
@@ -163,7 +169,7 @@
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
styles: { fontSize: 8 },
|
||||
});
|
||||
|
||||
|
||||
// Footer
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
@@ -174,12 +180,14 @@
|
||||
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
{ align: "center" }
|
||||
{ align: "center" },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Salvar
|
||||
doc.save(`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.pdf`);
|
||||
doc.save(
|
||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.pdf`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Erro ao gerar PDF:", error);
|
||||
alert("Erro ao gerar relatório PDF. Tente novamente.");
|
||||
@@ -190,7 +198,7 @@
|
||||
|
||||
async function generateCSV() {
|
||||
generating = true;
|
||||
|
||||
|
||||
try {
|
||||
const { inicio, fim } = getDateRange();
|
||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||
@@ -201,27 +209,32 @@
|
||||
// Preparar dados para CSV
|
||||
const csvData = relatorio.metricas.map((m) => {
|
||||
const row: any = {
|
||||
"Data/Hora": format(m.timestamp, "dd/MM/yyyy HH:mm:ss", { locale: ptBR }),
|
||||
"Data/Hora": format(m.timestamp, "dd/MM/yyyy HH:mm:ss", {
|
||||
locale: ptBR,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
||||
if (selected) {
|
||||
row[metricLabels[metric]] = m[metric] || 0;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
|
||||
// Gerar CSV
|
||||
const csv = Papa.unparse(csvData);
|
||||
|
||||
|
||||
// Download
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`);
|
||||
link.setAttribute(
|
||||
"download",
|
||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`,
|
||||
);
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@@ -242,7 +255,7 @@
|
||||
</script>
|
||||
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-3xl bg-gradient-to-br from-base-100 to-base-200">
|
||||
<div class="modal-box max-w-3xl bg-linear-to-br from-base-100 to-base-200">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
@@ -251,53 +264,65 @@
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h3 class="font-bold text-3xl text-primary mb-2">📊 Gerador de Relatórios</h3>
|
||||
<p class="text-base-content/60 mb-6">Exporte dados de monitoramento em PDF ou CSV</p>
|
||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
||||
📊 Gerador de Relatórios
|
||||
</h3>
|
||||
<p class="text-base-content/60 mb-6">
|
||||
Exporte dados de monitoramento em PDF ou CSV
|
||||
</p>
|
||||
|
||||
<!-- Seleção de Período -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-xl">Período</h4>
|
||||
|
||||
|
||||
<!-- Botões de Período Rápido -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'today' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('today')}
|
||||
class="btn btn-sm {periodType === 'today'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("today")}
|
||||
>
|
||||
Hoje
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'week' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('week')}
|
||||
class="btn btn-sm {periodType === 'week'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("week")}
|
||||
>
|
||||
Última Semana
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'month' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => setPeriod('month')}
|
||||
class="btn btn-sm {periodType === 'month'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => setPeriod("month")}
|
||||
>
|
||||
Último Mês
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm {periodType === 'custom' ? 'btn-primary' : 'btn-outline'}"
|
||||
onclick={() => periodType = 'custom'}
|
||||
class="btn btn-sm {periodType === 'custom'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'}"
|
||||
onclick={() => (periodType = "custom")}
|
||||
>
|
||||
Personalizado
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if periodType === 'custom'}
|
||||
{#if periodType === "custom"}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="dataInicio">
|
||||
<span class="label-text font-semibold">Data Início</span>
|
||||
</label>
|
||||
<input
|
||||
<input
|
||||
id="dataInicio"
|
||||
type="date"
|
||||
class="input input-bordered input-primary"
|
||||
@@ -309,7 +334,7 @@
|
||||
<label class="label" for="horaInicio">
|
||||
<span class="label-text font-semibold">Hora Início</span>
|
||||
</label>
|
||||
<input
|
||||
<input
|
||||
id="horaInicio"
|
||||
type="time"
|
||||
class="input input-bordered input-primary"
|
||||
@@ -321,7 +346,7 @@
|
||||
<label class="label" for="dataFim">
|
||||
<span class="label-text font-semibold">Data Fim</span>
|
||||
</label>
|
||||
<input
|
||||
<input
|
||||
id="dataFim"
|
||||
type="date"
|
||||
class="input input-bordered input-primary"
|
||||
@@ -333,7 +358,7 @@
|
||||
<label class="label" for="horaFim">
|
||||
<span class="label-text font-semibold">Hora Fim</span>
|
||||
</label>
|
||||
<input
|
||||
<input
|
||||
id="horaFim"
|
||||
type="time"
|
||||
class="input input-bordered input-primary"
|
||||
@@ -370,8 +395,10 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each Object.entries(metricLabels) as [metric, label]}
|
||||
<label class="label cursor-pointer justify-start gap-3 hover:bg-base-200 rounded-lg p-2">
|
||||
<input
|
||||
<label
|
||||
class="label cursor-pointer justify-start gap-3 hover:bg-base-200 rounded-lg p-2"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
bind:checked={selectedMetrics[metric]}
|
||||
@@ -385,7 +412,7 @@
|
||||
|
||||
<!-- Botões de Exportação -->
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline"
|
||||
onclick={onClose}
|
||||
@@ -393,44 +420,76 @@
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onclick={generateCSV}
|
||||
disabled={generating || !Object.values(selectedMetrics).some(v => v)}
|
||||
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
||||
>
|
||||
{#if generating}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
Exportar CSV
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onclick={generatePDF}
|
||||
disabled={generating || !Object.values(selectedMetrics).some(v => v)}
|
||||
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
||||
>
|
||||
{#if generating}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
Exportar PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !Object.values(selectedMetrics).some(v => v)}
|
||||
{#if !Object.values(selectedMetrics).some((v) => v)}
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="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>
|
||||
<span>Selecione pelo menos uma métrica para gerar o relatório.</span>
|
||||
</div>
|
||||
@@ -442,4 +501,3 @@
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
const client = useConvexClient();
|
||||
const ultimaMetrica = useQuery(api.monitoramento.obterUltimaMetrica, {});
|
||||
|
||||
|
||||
let showAlertModal = $state(false);
|
||||
let showReportModal = $state(false);
|
||||
let stopCollection: (() => void) | null = null;
|
||||
@@ -17,9 +17,12 @@
|
||||
const metrics = $derived(ultimaMetrica || null);
|
||||
|
||||
// Função para obter cor baseada no valor
|
||||
function getStatusColor(value: number | undefined, type: "normal" | "inverted" = "normal"): string {
|
||||
function getStatusColor(
|
||||
value: number | undefined,
|
||||
type: "normal" | "inverted" = "normal",
|
||||
): string {
|
||||
if (value === undefined) return "badge-ghost";
|
||||
|
||||
|
||||
if (type === "normal") {
|
||||
// Para CPU, RAM, Storage: maior é pior
|
||||
if (value < 60) return "badge-success";
|
||||
@@ -35,7 +38,7 @@
|
||||
|
||||
function getProgressColor(value: number | undefined): string {
|
||||
if (value === undefined) return "progress-ghost";
|
||||
|
||||
|
||||
if (value < 60) return "progress-success";
|
||||
if (value < 80) return "progress-warning";
|
||||
return "progress-error";
|
||||
@@ -53,16 +56,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
function formatValue(value: number | undefined, suffix: string = "%"): string {
|
||||
function formatValue(
|
||||
value: number | undefined,
|
||||
suffix: string = "%",
|
||||
): string {
|
||||
if (value === undefined) return "N/A";
|
||||
return `${value.toFixed(1)}${suffix}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-2xl border-2 border-primary/20">
|
||||
<div
|
||||
class="card bg-linear-to-br from-base-100 to-base-200 shadow-2xl border-2 border-primary/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="badge badge-success badge-lg gap-2 animate-pulse">
|
||||
<div class="w-2 h-2 bg-white rounded-full"></div>
|
||||
@@ -70,23 +80,45 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick={() => showAlertModal = true}
|
||||
onclick={() => (showAlertModal = true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
Configurar Alertas
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
onclick={() => showReportModal = true}
|
||||
onclick={() => (showReportModal = true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Gerar Relatório
|
||||
</button>
|
||||
@@ -96,133 +128,302 @@
|
||||
<!-- Métricas Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- CPU Usage -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-primary/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-primary/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">CPU</div>
|
||||
<div class="stat-value text-primary text-3xl">{formatValue(metrics?.cpuUsage)}</div>
|
||||
<div class="stat-value text-primary text-3xl">
|
||||
{formatValue(metrics?.cpuUsage)}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {getStatusColor(metrics?.cpuUsage)} badge-sm">
|
||||
{metrics?.cpuUsage !== undefined && metrics.cpuUsage < 60 ? "Normal" :
|
||||
metrics?.cpuUsage !== undefined && metrics.cpuUsage < 80 ? "Atenção" : "Crítico"}
|
||||
{metrics?.cpuUsage !== undefined && metrics.cpuUsage < 60
|
||||
? "Normal"
|
||||
: metrics?.cpuUsage !== undefined && metrics.cpuUsage < 80
|
||||
? "Atenção"
|
||||
: "Crítico"}
|
||||
</div>
|
||||
</div>
|
||||
<progress class="progress {getProgressColor(metrics?.cpuUsage)} w-full mt-2" value={metrics?.cpuUsage || 0} max="100"></progress>
|
||||
<progress
|
||||
class="progress {getProgressColor(metrics?.cpuUsage)} w-full mt-2"
|
||||
value={metrics?.cpuUsage || 0}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<!-- Memory Usage -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-success/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-success/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Memória RAM</div>
|
||||
<div class="stat-value text-success text-3xl">{formatValue(metrics?.memoryUsage)}</div>
|
||||
<div class="stat-value text-success text-3xl">
|
||||
{formatValue(metrics?.memoryUsage)}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {getStatusColor(metrics?.memoryUsage)} badge-sm">
|
||||
{metrics?.memoryUsage !== undefined && metrics.memoryUsage < 60 ? "Normal" :
|
||||
metrics?.memoryUsage !== undefined && metrics.memoryUsage < 80 ? "Atenção" : "Crítico"}
|
||||
{metrics?.memoryUsage !== undefined && metrics.memoryUsage < 60
|
||||
? "Normal"
|
||||
: metrics?.memoryUsage !== undefined && metrics.memoryUsage < 80
|
||||
? "Atenção"
|
||||
: "Crítico"}
|
||||
</div>
|
||||
</div>
|
||||
<progress class="progress {getProgressColor(metrics?.memoryUsage)} w-full mt-2" value={metrics?.memoryUsage || 0} max="100"></progress>
|
||||
<progress
|
||||
class="progress {getProgressColor(metrics?.memoryUsage)} w-full mt-2"
|
||||
value={metrics?.memoryUsage || 0}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<!-- Network Latency -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-warning/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-warning/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Latência de Rede</div>
|
||||
<div class="stat-value text-warning text-3xl">{formatValue(metrics?.networkLatency, "ms")}</div>
|
||||
<div class="stat-value text-warning text-3xl">
|
||||
{formatValue(metrics?.networkLatency, "ms")}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {getStatusColor(metrics?.networkLatency, 'inverted')} badge-sm">
|
||||
{metrics?.networkLatency !== undefined && metrics.networkLatency < 100 ? "Excelente" :
|
||||
metrics?.networkLatency !== undefined && metrics.networkLatency < 500 ? "Boa" : "Lenta"}
|
||||
<div
|
||||
class="badge {getStatusColor(
|
||||
metrics?.networkLatency,
|
||||
'inverted',
|
||||
)} badge-sm"
|
||||
>
|
||||
{metrics?.networkLatency !== undefined &&
|
||||
metrics.networkLatency < 100
|
||||
? "Excelente"
|
||||
: metrics?.networkLatency !== undefined &&
|
||||
metrics.networkLatency < 500
|
||||
? "Boa"
|
||||
: "Lenta"}
|
||||
</div>
|
||||
</div>
|
||||
<progress class="progress progress-warning w-full mt-2" value={Math.min((metrics?.networkLatency || 0) / 10, 100)} max="100"></progress>
|
||||
<progress
|
||||
class="progress progress-warning w-full mt-2"
|
||||
value={Math.min((metrics?.networkLatency || 0) / 10, 100)}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<!-- Storage Usage -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-info/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-info/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Armazenamento</div>
|
||||
<div class="stat-value text-info text-3xl">{formatValue(metrics?.storageUsed)}</div>
|
||||
<div class="stat-value text-info text-3xl">
|
||||
{formatValue(metrics?.storageUsed)}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {getStatusColor(metrics?.storageUsed)} badge-sm">
|
||||
{metrics?.storageUsed !== undefined && metrics.storageUsed < 60 ? "Normal" :
|
||||
metrics?.storageUsed !== undefined && metrics.storageUsed < 80 ? "Atenção" : "Crítico"}
|
||||
{metrics?.storageUsed !== undefined && metrics.storageUsed < 60
|
||||
? "Normal"
|
||||
: metrics?.storageUsed !== undefined && metrics.storageUsed < 80
|
||||
? "Atenção"
|
||||
: "Crítico"}
|
||||
</div>
|
||||
</div>
|
||||
<progress class="progress progress-info w-full mt-2" value={metrics?.storageUsed || 0} max="100"></progress>
|
||||
<progress
|
||||
class="progress progress-info w-full mt-2"
|
||||
value={metrics?.storageUsed || 0}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<!-- Usuários Online -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-accent/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-accent/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Usuários Online</div>
|
||||
<div class="stat-value text-accent text-3xl">{metrics?.usuariosOnline || 0}</div>
|
||||
<div class="stat-value text-accent text-3xl">
|
||||
{metrics?.usuariosOnline || 0}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge badge-accent badge-sm">Tempo Real</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensagens por Minuto -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-secondary/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-secondary/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Mensagens/min</div>
|
||||
<div class="stat-value text-secondary text-3xl">{metrics?.mensagensPorMinuto || 0}</div>
|
||||
<div class="stat-value text-secondary text-3xl">
|
||||
{metrics?.mensagensPorMinuto || 0}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge badge-secondary badge-sm">Atividade</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tempo de Resposta -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-primary/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-primary/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Tempo Resposta</div>
|
||||
<div class="stat-value text-primary text-3xl">{formatValue(metrics?.tempoRespostaMedio, "ms")}</div>
|
||||
<div class="stat-value text-primary text-3xl">
|
||||
{formatValue(metrics?.tempoRespostaMedio, "ms")}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {getStatusColor(metrics?.tempoRespostaMedio, 'inverted')} badge-sm">
|
||||
{metrics?.tempoRespostaMedio !== undefined && metrics.tempoRespostaMedio < 100 ? "Rápido" :
|
||||
metrics?.tempoRespostaMedio !== undefined && metrics.tempoRespostaMedio < 500 ? "Normal" : "Lento"}
|
||||
<div
|
||||
class="badge {getStatusColor(
|
||||
metrics?.tempoRespostaMedio,
|
||||
'inverted',
|
||||
)} badge-sm"
|
||||
>
|
||||
{metrics?.tempoRespostaMedio !== undefined &&
|
||||
metrics.tempoRespostaMedio < 100
|
||||
? "Rápido"
|
||||
: metrics?.tempoRespostaMedio !== undefined &&
|
||||
metrics.tempoRespostaMedio < 500
|
||||
? "Normal"
|
||||
: "Lento"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erros -->
|
||||
<div class="stat bg-base-100 rounded-2xl shadow-lg border border-error/10 hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<div
|
||||
class="stat bg-base-100 rounded-2xl shadow-lg border border-error/10 hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<div class="stat-figure text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title font-semibold">Erros (30s)</div>
|
||||
<div class="stat-value text-error text-3xl">{metrics?.errosCount || 0}</div>
|
||||
<div class="stat-value text-error text-3xl">
|
||||
{metrics?.errosCount || 0}
|
||||
</div>
|
||||
<div class="stat-desc mt-2">
|
||||
<div class="badge {(metrics?.errosCount || 0) === 0 ? 'badge-success' : 'badge-error'} badge-sm">
|
||||
<div
|
||||
class="badge {(metrics?.errosCount || 0) === 0
|
||||
? 'badge-success'
|
||||
: 'badge-error'} badge-sm"
|
||||
>
|
||||
{(metrics?.errosCount || 0) === 0 ? "Sem erros" : "Verificar logs"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,15 +432,27 @@
|
||||
|
||||
<!-- Info Footer -->
|
||||
<div class="alert alert-info mt-6 shadow-lg">
|
||||
<svg 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
|
||||
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>
|
||||
<div>
|
||||
<h3 class="font-bold">Monitoramento Ativo</h3>
|
||||
<div class="text-xs">
|
||||
Métricas coletadas automaticamente a cada 2 segundos.
|
||||
Métricas coletadas automaticamente a cada 2 segundos.
|
||||
{#if metrics?.timestamp}
|
||||
Última atualização: {new Date(metrics.timestamp).toLocaleString('pt-BR')}
|
||||
Última atualização: {new Date(metrics.timestamp).toLocaleString(
|
||||
"pt-BR",
|
||||
)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,10 +462,9 @@
|
||||
|
||||
<!-- Modals -->
|
||||
{#if showAlertModal}
|
||||
<AlertConfigModal onClose={() => showAlertModal = false} />
|
||||
<AlertConfigModal onClose={() => (showAlertModal = false)} />
|
||||
{/if}
|
||||
|
||||
{#if showReportModal}
|
||||
<ReportGeneratorModal onClose={() => showReportModal = false} />
|
||||
<ReportGeneratorModal onClose={() => (showReportModal = false)} />
|
||||
{/if}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user