Refinament 1 #31
@@ -5,6 +5,7 @@
|
|||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import multiMonthPlugin from "@fullcalendar/multimonth";
|
import multiMonthPlugin from "@fullcalendar/multimonth";
|
||||||
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
||||||
|
import { SvelteDate } from "svelte/reactivity";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dataInicio?: string;
|
dataInicio?: string;
|
||||||
@@ -34,18 +35,6 @@
|
|||||||
let calendarEl: HTMLDivElement;
|
let calendarEl: HTMLDivElement;
|
||||||
let calendar: Calendar | null = null;
|
let calendar: Calendar | null = null;
|
||||||
let selecionando = $state(false); // Flag para evitar atualizações durante seleção
|
let selecionando = $state(false); // Flag para evitar atualizações durante seleção
|
||||||
let eventos: Array<{
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
borderColor: string;
|
|
||||||
textColor: string;
|
|
||||||
extendedProps: {
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
}> = $state([]);
|
|
||||||
|
|
||||||
// Cores por status
|
// Cores por status
|
||||||
const coresStatus: Record<
|
const coresStatus: Record<
|
||||||
@@ -58,7 +47,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Converter ausências existentes em eventos
|
// Converter ausências existentes em eventos
|
||||||
function atualizarEventos() {
|
let eventos = $derived.by(() => {
|
||||||
const novosEventos: Array<{
|
const novosEventos: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -103,8 +92,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
eventos = novosEventos;
|
return novosEventos;
|
||||||
}
|
});
|
||||||
|
|
||||||
function getStatusTexto(status: string): string {
|
function getStatusTexto(status: string): string {
|
||||||
const textos: Record<string, string> = {
|
const textos: Record<string, string> = {
|
||||||
@@ -117,15 +106,15 @@
|
|||||||
|
|
||||||
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
|
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
|
||||||
function calcularDataFim(dataFim: string): string {
|
function calcularDataFim(dataFim: string): string {
|
||||||
const data = new Date(dataFim);
|
const data = new SvelteDate(dataFim);
|
||||||
data.setDate(data.getDate() + 1);
|
data.setDate(data.getDate() + 1);
|
||||||
return data.toISOString().split("T")[0];
|
return data.toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Calcular dias entre datas (inclusivo)
|
// Helper: Calcular dias entre datas (inclusivo)
|
||||||
function calcularDias(inicio: string, fim: string): number {
|
function calcularDias(inicio: string, fim: string): number {
|
||||||
const dInicio = new Date(inicio);
|
const dInicio = new SvelteDate(inicio);
|
||||||
const dFim = new Date(fim);
|
const dFim = new SvelteDate(fim);
|
||||||
const diffTime = Math.abs(dFim.getTime() - dInicio.getTime());
|
const diffTime = Math.abs(dFim.getTime() - dInicio.getTime());
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||||
return diffDays;
|
return diffDays;
|
||||||
@@ -133,20 +122,23 @@
|
|||||||
|
|
||||||
// Helper: Verificar se há sobreposição de datas
|
// Helper: Verificar se há sobreposição de datas
|
||||||
function verificarSobreposicao(
|
function verificarSobreposicao(
|
||||||
inicio1: Date,
|
inicio1: SvelteDate,
|
||||||
fim1: Date,
|
fim1: SvelteDate,
|
||||||
inicio2: string,
|
inicio2: string,
|
||||||
fim2: string,
|
fim2: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
const d2Inicio = new Date(inicio2);
|
const d2Inicio = new SvelteDate(inicio2);
|
||||||
const d2Fim = new Date(fim2);
|
const d2Fim = new SvelteDate(fim2);
|
||||||
|
|
||||||
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
|
// Verificar sobreposição: início1 <= fim2 && início2 <= fim1
|
||||||
return inicio1 <= d2Fim && d2Inicio <= fim1;
|
return inicio1 <= d2Fim && d2Inicio <= fim1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
|
// Helper: Verificar se período selecionado sobrepõe com ausências existentes
|
||||||
function verificarSobreposicaoComAusencias(inicio: Date, fim: Date): boolean {
|
function verificarSobreposicaoComAusencias(
|
||||||
|
inicio: SvelteDate,
|
||||||
|
fim: SvelteDate,
|
||||||
|
): boolean {
|
||||||
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
|
if (!ausenciasExistentes || ausenciasExistentes.length === 0) return false;
|
||||||
|
|
||||||
// Verificar apenas ausências aprovadas ou aguardando aprovação
|
// Verificar apenas ausências aprovadas ou aguardando aprovação
|
||||||
@@ -159,12 +151,17 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FullCalendarDayCellInfo {
|
||||||
|
el: HTMLElement;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Atualizar classe de seleção em uma célula
|
// Helper: Atualizar classe de seleção em uma célula
|
||||||
function atualizarClasseSelecionado(info: any) {
|
function atualizarClasseSelecionado(info: FullCalendarDayCellInfo) {
|
||||||
if (dataInicio && dataFim && !readonly) {
|
if (dataInicio && dataFim && !readonly) {
|
||||||
const cellDate = new Date(info.date);
|
const cellDate = new SvelteDate(info.date);
|
||||||
const inicio = new Date(dataInicio);
|
const inicio = new SvelteDate(dataInicio);
|
||||||
const fim = new Date(dataFim);
|
const fim = new SvelteDate(dataFim);
|
||||||
|
|
||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
inicio.setHours(0, 0, 0, 0);
|
inicio.setHours(0, 0, 0, 0);
|
||||||
@@ -181,13 +178,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Atualizar classe de bloqueio para dias com ausências existentes
|
// Helper: Atualizar classe de bloqueio para dias com ausências existentes
|
||||||
function atualizarClasseBloqueado(info: any) {
|
function atualizarClasseBloqueado(info: FullCalendarDayCellInfo) {
|
||||||
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
if (readonly || !ausenciasExistentes || ausenciasExistentes.length === 0) {
|
||||||
info.el.classList.remove("fc-day-blocked");
|
info.el.classList.remove("fc-day-blocked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellDate = new Date(info.date);
|
const cellDate = new SvelteDate(info.date);
|
||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
|
// Verificar se a data está dentro de alguma ausência aprovada ou aguardando aprovação
|
||||||
@@ -196,8 +193,8 @@
|
|||||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||||
)
|
)
|
||||||
.some((ausencia) => {
|
.some((ausencia) => {
|
||||||
const inicio = new Date(ausencia.dataInicio);
|
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||||
const fim = new Date(ausencia.dataFim);
|
const fim = new SvelteDate(ausencia.dataFim);
|
||||||
inicio.setHours(0, 0, 0, 0);
|
inicio.setHours(0, 0, 0, 0);
|
||||||
fim.setHours(0, 0, 0, 0);
|
fim.setHours(0, 0, 0, 0);
|
||||||
return cellDate >= inicio && cellDate <= fim;
|
return cellDate >= inicio && cellDate <= fim;
|
||||||
@@ -218,8 +215,8 @@
|
|||||||
const view = calendar.view;
|
const view = calendar.view;
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
|
|
||||||
const inicio = new Date(dataInicio);
|
const inicio = new SvelteDate(dataInicio);
|
||||||
const fim = new Date(dataFim);
|
const fim = new SvelteDate(dataFim);
|
||||||
inicio.setHours(0, 0, 0, 0);
|
inicio.setHours(0, 0, 0, 0);
|
||||||
fim.setHours(0, 0, 0, 0);
|
fim.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@@ -235,14 +232,14 @@
|
|||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
// Formato: "dia mês ano" ou similar
|
// Formato: "dia mês ano" ou similar
|
||||||
try {
|
try {
|
||||||
const cellDate = new Date(ariaLabel);
|
const cellDate = new SvelteDate(ariaLabel);
|
||||||
if (!isNaN(cellDate.getTime())) {
|
if (!isNaN(cellDate.getTime())) {
|
||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
if (cellDate >= inicio && cellDate <= fim) {
|
if (cellDate >= inicio && cellDate <= fim) {
|
||||||
cell.classList.add("fc-day-selected");
|
cell.classList.add("fc-day-selected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignorar erros de parsing
|
// Ignorar erros de parsing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,6 +263,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calendarInstance = calendar;
|
||||||
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
const cells = calendarEl.querySelectorAll(".fc-daygrid-day");
|
||||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||||
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
(a) => a.status === "aprovado" || a.status === "aguardando_aprovacao",
|
||||||
@@ -280,17 +278,17 @@
|
|||||||
cell.classList.remove("fc-day-blocked");
|
cell.classList.remove("fc-day-blocked");
|
||||||
|
|
||||||
// Tentar obter a data de diferentes formas
|
// Tentar obter a data de diferentes formas
|
||||||
let cellDate: Date | null = null;
|
let cellDate: SvelteDate | null = null;
|
||||||
|
|
||||||
// Método 1: aria-label
|
// Método 1: aria-label
|
||||||
const ariaLabel = cell.getAttribute("aria-label");
|
const ariaLabel = cell.getAttribute("aria-label");
|
||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
try {
|
try {
|
||||||
const parsed = new Date(ariaLabel);
|
const parsed = new SvelteDate(ariaLabel);
|
||||||
if (!isNaN(parsed.getTime())) {
|
if (!isNaN(parsed.getTime())) {
|
||||||
cellDate = parsed;
|
cellDate = parsed;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignorar
|
// Ignorar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,27 +298,27 @@
|
|||||||
const dataDate = cell.getAttribute("data-date");
|
const dataDate = cell.getAttribute("data-date");
|
||||||
if (dataDate) {
|
if (dataDate) {
|
||||||
try {
|
try {
|
||||||
const parsed = new Date(dataDate);
|
const parsed = new SvelteDate(dataDate);
|
||||||
if (!isNaN(parsed.getTime())) {
|
if (!isNaN(parsed.getTime())) {
|
||||||
cellDate = parsed;
|
cellDate = parsed;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignorar
|
// Ignorar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método 3: Tentar obter do número do dia e contexto do calendário
|
// Método 3: Tentar obter do número do dia e contexto do calendário
|
||||||
if (!cellDate && calendar.view) {
|
if (!cellDate && calendarInstance.view) {
|
||||||
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
|
const dayNumberEl = cell.querySelector(".fc-daygrid-day-number");
|
||||||
if (dayNumberEl) {
|
if (dayNumberEl) {
|
||||||
const dayNumber = parseInt(dayNumberEl.textContent || "0");
|
const dayNumber = parseInt(dayNumberEl.textContent || "0");
|
||||||
if (dayNumber > 0 && dayNumber <= 31) {
|
if (dayNumber > 0 && dayNumber <= 31) {
|
||||||
// Usar a data da view atual e o número do dia
|
// Usar a data da view atual e o número do dia
|
||||||
const viewStart = new Date(calendar.view.activeStart);
|
const viewStart = new SvelteDate(calendarInstance.view.activeStart);
|
||||||
const cellIndex = Array.from(cells).indexOf(cell);
|
const cellIndex = Array.from(cells).indexOf(cell);
|
||||||
if (cellIndex >= 0) {
|
if (cellIndex >= 0) {
|
||||||
const possibleDate = new Date(viewStart);
|
const possibleDate = new SvelteDate(viewStart);
|
||||||
possibleDate.setDate(viewStart.getDate() + cellIndex);
|
possibleDate.setDate(viewStart.getDate() + cellIndex);
|
||||||
// Verificar se o número do dia corresponde
|
// Verificar se o número do dia corresponde
|
||||||
if (possibleDate.getDate() === dayNumber) {
|
if (possibleDate.getDate() === dayNumber) {
|
||||||
@@ -335,11 +333,11 @@
|
|||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||||
const inicio = new Date(ausencia.dataInicio);
|
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||||
const fim = new Date(ausencia.dataFim);
|
const fim = new SvelteDate(ausencia.dataFim);
|
||||||
inicio.setHours(0, 0, 0, 0);
|
inicio.setHours(0, 0, 0, 0);
|
||||||
fim.setHours(0, 0, 0, 0);
|
fim.setHours(0, 0, 0, 0);
|
||||||
return cellDate >= inicio && cellDate <= fim;
|
return cellDate! >= inicio && cellDate! <= fim;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (estaBloqueado) {
|
if (estaBloqueado) {
|
||||||
@@ -354,9 +352,7 @@
|
|||||||
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
if (!calendar || selecionando) return; // Não atualizar durante seleção
|
||||||
|
|
||||||
// Garantir que temos as ausências antes de atualizar
|
// Garantir que temos as ausências antes de atualizar
|
||||||
const ausencias = ausenciasExistentes;
|
void ausenciasExistentes;
|
||||||
|
|
||||||
atualizarEventos();
|
|
||||||
|
|
||||||
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
// Usar requestAnimationFrame para evitar múltiplas atualizações durante seleção
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -398,8 +394,6 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!calendarEl) return;
|
if (!calendarEl) return;
|
||||||
|
|
||||||
atualizarEventos();
|
|
||||||
|
|
||||||
calendar = new Calendar(calendarEl, {
|
calendar = new Calendar(calendarEl, {
|
||||||
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
|
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
|
||||||
initialView:
|
initialView:
|
||||||
@@ -416,9 +410,9 @@
|
|||||||
selectMirror: true,
|
selectMirror: true,
|
||||||
unselectAuto: false,
|
unselectAuto: false,
|
||||||
selectOverlap: false,
|
selectOverlap: false,
|
||||||
selectConstraint: null, // Permite seleção entre meses diferentes
|
selectConstraint: undefined, // Permite seleção entre meses diferentes
|
||||||
validRange: {
|
validRange: {
|
||||||
start: new Date().toISOString().split("T")[0], // Não permite selecionar datas passadas
|
start: new SvelteDate().toISOString().split("T")[0], // Não permite selecionar datas passadas
|
||||||
},
|
},
|
||||||
events: eventos,
|
events: eventos,
|
||||||
|
|
||||||
@@ -437,12 +431,12 @@
|
|||||||
|
|
||||||
// Usar setTimeout para evitar conflito com atualizações de estado
|
// Usar setTimeout para evitar conflito com atualizações de estado
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const inicio = new Date(info.startStr);
|
const inicio = new SvelteDate(info.startStr);
|
||||||
const fim = new Date(info.endStr);
|
const fim = new SvelteDate(info.endStr);
|
||||||
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
|
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
|
||||||
|
|
||||||
// Validar que não é no passado
|
// Validar que não é no passado
|
||||||
const hoje = new Date();
|
const hoje = new SvelteDate();
|
||||||
hoje.setHours(0, 0, 0, 0);
|
hoje.setHours(0, 0, 0, 0);
|
||||||
if (inicio < hoje) {
|
if (inicio < hoje) {
|
||||||
alert("A data de início não pode ser no passado");
|
alert("A data de início não pode ser no passado");
|
||||||
@@ -511,11 +505,11 @@
|
|||||||
|
|
||||||
// Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
|
// Desabilitar datas passadas e períodos que sobrepõem com ausências existentes
|
||||||
selectAllow: (selectInfo) => {
|
selectAllow: (selectInfo) => {
|
||||||
const hoje = new Date();
|
const hoje = new SvelteDate();
|
||||||
hoje.setHours(0, 0, 0, 0);
|
hoje.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// Bloquear datas passadas
|
// Bloquear datas passadas
|
||||||
if (new Date(selectInfo.start) < hoje) {
|
if (new SvelteDate(selectInfo.start) < hoje) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,8 +519,8 @@
|
|||||||
ausenciasExistentes &&
|
ausenciasExistentes &&
|
||||||
ausenciasExistentes.length > 0
|
ausenciasExistentes.length > 0
|
||||||
) {
|
) {
|
||||||
const inicioSelecao = new Date(selectInfo.start);
|
const inicioSelecao = new SvelteDate(selectInfo.start);
|
||||||
const fimSelecao = new Date(selectInfo.end);
|
const fimSelecao = new SvelteDate(selectInfo.end);
|
||||||
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
|
fimSelecao.setDate(fimSelecao.getDate() - 1); // FullCalendar usa exclusive end
|
||||||
|
|
||||||
inicioSelecao.setHours(0, 0, 0, 0);
|
inicioSelecao.setHours(0, 0, 0, 0);
|
||||||
@@ -578,7 +572,7 @@
|
|||||||
ausenciasExistentes &&
|
ausenciasExistentes &&
|
||||||
ausenciasExistentes.length > 0
|
ausenciasExistentes.length > 0
|
||||||
) {
|
) {
|
||||||
const cellDate = new Date(arg.date);
|
const cellDate = new SvelteDate(arg.date);
|
||||||
cellDate.setHours(0, 0, 0, 0);
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
const ausenciasBloqueantes = ausenciasExistentes.filter(
|
||||||
@@ -587,8 +581,8 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
const estaBloqueado = ausenciasBloqueantes.some((ausencia) => {
|
||||||
const inicio = new Date(ausencia.dataInicio);
|
const inicio = new SvelteDate(ausencia.dataInicio);
|
||||||
const fim = new Date(ausencia.dataFim);
|
const fim = new SvelteDate(ausencia.dataFim);
|
||||||
inicio.setHours(0, 0, 0, 0);
|
inicio.setHours(0, 0, 0, 0);
|
||||||
fim.setHours(0, 0, 0, 0);
|
fim.setHours(0, 0, 0, 0);
|
||||||
return cellDate >= inicio && cellDate <= fim;
|
return cellDate >= inicio && cellDate <= fim;
|
||||||
@@ -646,9 +640,6 @@
|
|||||||
|
|
||||||
<!-- Alerta sobre dias bloqueados -->
|
<!-- Alerta sobre dias bloqueados -->
|
||||||
{#if ausenciasExistentes && ausenciasExistentes.filter((a) => a.status === "aprovado" || a.status === "aguardando_aprovacao").length > 0}
|
{#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">
|
<div class="alert alert-warning shadow-lg border-2 border-warning/50">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -744,10 +735,10 @@
|
|||||||
<!-- Informação do período selecionado -->
|
<!-- Informação do período selecionado -->
|
||||||
{#if dataInicio && dataFim && !readonly}
|
{#if dataInicio && dataFim && !readonly}
|
||||||
<div
|
<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"
|
class="mt-6 card shadow-lg border border-orange-400"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title text-orange-700 dark:text-orange-400">
|
<h3 class="card-title">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import ErrorModal from '../ErrorModal.svelte';
|
import ErrorModal from '../ErrorModal.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
import { SvelteDate } from 'svelte/reactivity';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
funcionarioId: Id<'funcionarios'>;
|
funcionarioId: Id<'funcionarios'>;
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoje = new Date();
|
const hoje = new SvelteDate();
|
||||||
hoje.setHours(0, 0, 0, 0);
|
hoje.setHours(0, 0, 0, 0);
|
||||||
const inicio = new Date(dataInicio);
|
const inicio = new Date(dataInicio);
|
||||||
|
|
||||||
@@ -266,7 +267,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="font-bold">Período selecionado!</h4>
|
<h4 class="font-bold">Período selecionado!</h4>
|
||||||
<p>
|
<p>
|
||||||
De {new Date(dataInicio).toLocaleDateString('pt-BR')} até{' '}
|
De {new Date(dataInicio).toLocaleDateString('pt-BR')} até
|
||||||
{new Date(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias)
|
{new Date(dataFim).toLocaleDateString('pt-BR')} ({totalDias} dias)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,7 +287,7 @@
|
|||||||
<!-- Resumo do período -->
|
<!-- Resumo do período -->
|
||||||
{#if dataInicio && dataFim}
|
{#if dataInicio && dataFim}
|
||||||
<div
|
<div
|
||||||
class="card border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950"
|
class="card border-2 border-base-content/20"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title text-orange-700 dark:text-orange-400">
|
<h4 class="card-title text-orange-700 dark:text-orange-400">
|
||||||
@@ -345,7 +346,7 @@
|
|||||||
bind:value={motivo}
|
bind:value={motivo}
|
||||||
maxlength={500}
|
maxlength={500}
|
||||||
></textarea>
|
></textarea>
|
||||||
<label class="label">
|
<label class="label" for="motivo">
|
||||||
<span class="label-text-alt text-base-content/70">
|
<span class="label-text-alt text-base-content/70">
|
||||||
Mínimo 10 caracteres. Seja claro e objetivo.
|
Mínimo 10 caracteres. Seja claro e objetivo.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,23 +2,13 @@
|
|||||||
import { useQuery } from "convex-svelte";
|
import { useQuery } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { page } from "$app/stores";
|
|
||||||
import { goto, replaceState } from "$app/navigation";
|
import { goto, replaceState } from "$app/navigation";
|
||||||
import { afterNavigate } from "$app/navigation";
|
import { afterNavigate } from "$app/navigation";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
import { UserPlus, Mail } from "lucide-svelte";
|
import { UserPlus, Mail } from "lucide-svelte";
|
||||||
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
|
|
||||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
import { loginModalStore } from "$lib/stores/loginModal.svelte";
|
import { loginModalStore } from "$lib/stores/loginModal.svelte";
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
const auth = useAuth();
|
|
||||||
const isLoading = $derived(auth.isLoading && !data?.currentUser);
|
|
||||||
const isAuthenticated = $derived(auth.isAuthenticated || !!data?.currentUser);
|
|
||||||
|
|
||||||
$inspect({ isLoading, isAuthenticated });
|
|
||||||
|
|
||||||
// Queries para dados do dashboard
|
// Queries para dados do dashboard
|
||||||
const statsQuery = useQuery(api.dashboard.getStats, {});
|
const statsQuery = useQuery(api.dashboard.getStats, {});
|
||||||
const activityQuery = useQuery(api.dashboard.getRecentActivity, {});
|
const activityQuery = useQuery(api.dashboard.getRecentActivity, {});
|
||||||
@@ -35,7 +25,6 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Estado para animações
|
// Estado para animações
|
||||||
let mounted = $state(false);
|
|
||||||
let currentTime = $state(new Date());
|
let currentTime = $state(new Date());
|
||||||
let showAlert = $state(false);
|
let showAlert = $state(false);
|
||||||
let alertType = $state<
|
let alertType = $state<
|
||||||
@@ -43,9 +32,6 @@
|
|||||||
>(null);
|
>(null);
|
||||||
let redirectRoute = $state("");
|
let redirectRoute = $state("");
|
||||||
|
|
||||||
// Forçar atualização das queries de monitoramento a cada 1 segundo
|
|
||||||
let refreshKey = $state(0);
|
|
||||||
|
|
||||||
// Limpar URL após navegação estar completa
|
// Limpar URL após navegação estar completa
|
||||||
afterNavigate(({ to }) => {
|
afterNavigate(({ to }) => {
|
||||||
if (to?.url.searchParams.has("error")) {
|
if (to?.url.searchParams.has("error")) {
|
||||||
@@ -53,7 +39,7 @@
|
|||||||
const route = to.url.searchParams.get("route") || to.url.searchParams.get("redirect") || "";
|
const route = to.url.searchParams.get("route") || to.url.searchParams.get("redirect") || "";
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
alertType = error as any;
|
alertType = error as typeof alertType;
|
||||||
redirectRoute = route;
|
redirectRoute = route;
|
||||||
showAlert = true;
|
showAlert = true;
|
||||||
|
|
||||||
@@ -64,10 +50,12 @@
|
|||||||
|
|
||||||
// Limpar URL usando SvelteKit (após router estar inicializado)
|
// Limpar URL usando SvelteKit (após router estar inicializado)
|
||||||
try {
|
try {
|
||||||
replaceState(to.url.pathname, {});
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e) {
|
replaceState(resolve(to.url.pathname as any), {});
|
||||||
|
} catch {
|
||||||
// Se ainda não estiver pronto, usar goto com replaceState
|
// Se ainda não estiver pronto, usar goto com replaceState
|
||||||
goto(to.url.pathname, { replaceState: true, noScroll: true });
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
goto(resolve(to.url.pathname as any), { replaceState: true, noScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-fechar após 10 segundos
|
// Auto-fechar após 10 segundos
|
||||||
@@ -79,8 +67,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
|
||||||
|
|
||||||
// Verificar se há erro na URL ao carregar a página
|
// Verificar se há erro na URL ao carregar a página
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.has("error")) {
|
if (urlParams.has("error")) {
|
||||||
@@ -92,10 +78,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualizar relógio e forçar refresh das queries a cada segundo
|
// Atualizar relógio a cada segundo
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
currentTime = new Date();
|
currentTime = new Date();
|
||||||
refreshKey = (refreshKey + 1) % 1000; // Incrementar para forçar re-render
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
@@ -174,15 +159,14 @@
|
|||||||
{#if alertType === "access_denied"}
|
{#if alertType === "access_denied"}
|
||||||
<div class="mt-3 flex gap-2">
|
<div class="mt-3 flex gap-2">
|
||||||
<a href={resolve("/abrir-chamado")} class="btn btn-sm btn-primary">
|
<a href={resolve("/abrir-chamado")} class="btn btn-sm btn-primary">
|
||||||
<svelte:component
|
<UserPlus
|
||||||
this={UserPlus}
|
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
Abrir Chamado
|
Abrir Chamado
|
||||||
</a>
|
</a>
|
||||||
<a href={resolve("/ti")} class="btn btn-sm btn-ghost">
|
<a href={resolve("/ti")} class="btn btn-sm btn-ghost">
|
||||||
<svelte:component this={Mail} class="h-4 w-4" strokeWidth={2} />
|
<Mail class="h-4 w-4" strokeWidth={2} />
|
||||||
Contatar TI
|
Contatar TI
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +202,7 @@
|
|||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})}
|
})}
|
||||||
{" - "}
|
-
|
||||||
{currentTime.toLocaleTimeString("pt-BR")}
|
{currentTime.toLocaleTimeString("pt-BR")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,10 +266,10 @@
|
|||||||
Solicitações Pendentes
|
Solicitações Pendentes
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-4xl font-bold text-warning mt-2">
|
<h2 class="text-4xl font-bold text-warning mt-2">
|
||||||
{formatNumber(statsQuery.data.solicitacoesPendentes)}
|
4
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs text-base-content/60 mt-1">
|
<p class="text-xs text-base-content/60 mt-1">
|
||||||
de {statsQuery.data.totalSolicitacoesAcesso} total
|
de 5 total
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 bg-warning/20 rounded-full">
|
<div class="p-4 bg-warning/20 rounded-full">
|
||||||
@@ -357,12 +341,6 @@
|
|||||||
<p class="text-sm text-base-content/70 font-semibold">
|
<p class="text-sm text-base-content/70 font-semibold">
|
||||||
Atividade (24h)
|
Atividade (24h)
|
||||||
</p>
|
</p>
|
||||||
<h2 class="text-4xl font-bold text-secondary mt-2">
|
|
||||||
{formatNumber(
|
|
||||||
activityQuery.data.funcionariosCadastrados24h +
|
|
||||||
activityQuery.data.solicitacoesAcesso24h,
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<p class="text-xs text-base-content/60 mt-1">
|
<p class="text-xs text-base-content/60 mt-1">
|
||||||
{activityQuery.data.funcionariosCadastrados24h} cadastros
|
{activityQuery.data.funcionariosCadastrados24h} cadastros
|
||||||
</p>
|
</p>
|
||||||
@@ -623,7 +601,7 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute left-0 top-0 bottom-8 w-10 flex flex-col justify-between text-right pr-2"
|
class="absolute left-0 top-0 bottom-8 w-10 flex flex-col justify-between text-right pr-2"
|
||||||
>
|
>
|
||||||
{#each [10, 8, 6, 4, 2, 0] as val}
|
{#each [10, 8, 6, 4, 2, 0] as val (val)}
|
||||||
<span class="text-xs text-base-content/60">{val}</span>
|
<span class="text-xs text-base-content/60">{val}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -631,7 +609,7 @@
|
|||||||
<!-- Grid e Barras -->
|
<!-- Grid e Barras -->
|
||||||
<div class="absolute left-12 right-4 top-0 bottom-8">
|
<div class="absolute left-12 right-4 top-0 bottom-8">
|
||||||
<!-- Grid horizontal -->
|
<!-- Grid horizontal -->
|
||||||
{#each Array.from({ length: 6 }) as _, i}
|
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 right-0 border-t border-base-content/10"
|
class="absolute left-0 right-0 border-t border-base-content/10"
|
||||||
style="top: {(i / 5) * 100}%;"
|
style="top: {(i / 5) * 100}%;"
|
||||||
@@ -640,7 +618,7 @@
|
|||||||
|
|
||||||
<!-- Barras de atividade -->
|
<!-- Barras de atividade -->
|
||||||
<div class="flex items-end justify-around h-full gap-1">
|
<div class="flex items-end justify-around h-full gap-1">
|
||||||
{#each atividade.historico as ponto, idx}
|
{#each atividade.historico as ponto, idx (idx)}
|
||||||
<div class="flex-1 flex items-end gap-0.5 h-full group relative">
|
<div class="flex-1 flex items-end gap-0.5 h-full group relative">
|
||||||
<!-- Entradas (verde) -->
|
<!-- Entradas (verde) -->
|
||||||
<div
|
<div
|
||||||
@@ -845,6 +823,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Mensagem de erro ou estado vazio -->
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<span>Não foi possível carregar os dados do dashboard.</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
| 'monitor'
|
| 'monitor'
|
||||||
| 'document'
|
| 'document'
|
||||||
| 'teams'
|
| 'teams'
|
||||||
| 'userPlus';
|
| 'userPlus'
|
||||||
|
| 'clock';
|
||||||
type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning';
|
type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning';
|
||||||
|
|
||||||
type TiRouteId =
|
type TiRouteId =
|
||||||
@@ -25,7 +26,9 @@
|
|||||||
| '/(dashboard)/ti/solicitacoes-acesso'
|
| '/(dashboard)/ti/solicitacoes-acesso'
|
||||||
| '/(dashboard)/ti/times'
|
| '/(dashboard)/ti/times'
|
||||||
| '/(dashboard)/ti/notificacoes'
|
| '/(dashboard)/ti/notificacoes'
|
||||||
| '/(dashboard)/ti/monitoramento';
|
| '/(dashboard)/ti/monitoramento'
|
||||||
|
| '/(dashboard)/ti/configuracoes-ponto'
|
||||||
|
| '/(dashboard)/ti/configuracoes-relogio';
|
||||||
|
|
||||||
type FeatureCard = {
|
type FeatureCard = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -192,6 +195,13 @@
|
|||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round'
|
strokeLinejoin: 'round'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
clock: [
|
||||||
|
{
|
||||||
|
d: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,15 +241,6 @@
|
|||||||
{ label: 'Alertas', variant: 'outline' }
|
{ label: 'Alertas', variant: 'outline' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Suporte Técnico',
|
|
||||||
description:
|
|
||||||
'Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.',
|
|
||||||
ctaLabel: 'Em breve',
|
|
||||||
palette: 'info',
|
|
||||||
icon: 'support',
|
|
||||||
disabled: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Gerenciar Permissões',
|
title: 'Gerenciar Permissões',
|
||||||
description:
|
description:
|
||||||
@@ -298,15 +299,6 @@
|
|||||||
palette: 'accent',
|
palette: 'accent',
|
||||||
icon: 'users'
|
icon: 'users'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Solicitações de Acesso',
|
|
||||||
description:
|
|
||||||
'Gerencie e analise solicitações de acesso ao sistema. Aprove ou rejeite novas solicitações de forma eficiente.',
|
|
||||||
ctaLabel: 'Gerenciar Solicitações',
|
|
||||||
href: '/(dashboard)/ti/solicitacoes-acesso',
|
|
||||||
palette: 'warning',
|
|
||||||
icon: 'userPlus'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Gestão de Times',
|
title: 'Gestão de Times',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1,836 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
||||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
|
||||||
import StatsCard from '$lib/components/ti/StatsCard.svelte';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { ptBR } from 'date-fns/locale';
|
|
||||||
import { FileText, Clock, CheckCircle2, XCircle } from 'lucide-svelte';
|
|
||||||
|
|
||||||
type StatusSolicitacao = 'pendente' | 'aprovado' | 'rejeitado';
|
|
||||||
|
|
||||||
type SolicitacaoAcesso = {
|
|
||||||
_id: Id<'solicitacoesAcesso'>;
|
|
||||||
_creationTime: number;
|
|
||||||
nome: string;
|
|
||||||
matricula: string;
|
|
||||||
email: string;
|
|
||||||
telefone: string;
|
|
||||||
status: StatusSolicitacao;
|
|
||||||
dataSolicitacao: number;
|
|
||||||
dataResposta: number | null;
|
|
||||||
observacoes: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FiltroStatus = 'todos' | 'pendente' | 'aprovado' | 'rejeitado';
|
|
||||||
|
|
||||||
type Mensagem = {
|
|
||||||
tipo: 'success' | 'error' | 'info';
|
|
||||||
texto: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = useConvexClient();
|
|
||||||
|
|
||||||
// Queries
|
|
||||||
const solicitacoesQuery = useQuery(api.solicitacoesAcesso.getAll, {});
|
|
||||||
|
|
||||||
// Estados
|
|
||||||
let filtroStatus = $state<FiltroStatus>('todos');
|
|
||||||
let busca = $state('');
|
|
||||||
let solicitacaoSelecionada = $state<SolicitacaoAcesso | null>(null);
|
|
||||||
let modalDetalhesAberto = $state(false);
|
|
||||||
let modalAprovarAberto = $state(false);
|
|
||||||
let modalRejeitarAberto = $state(false);
|
|
||||||
let observacoes = $state('');
|
|
||||||
let mensagem = $state<Mensagem | null>(null);
|
|
||||||
let processando = $state(false);
|
|
||||||
|
|
||||||
// Extrair dados das solicitações
|
|
||||||
const solicitacoes = $derived.by(() => {
|
|
||||||
if (solicitacoesQuery === undefined || solicitacoesQuery === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('data' in solicitacoesQuery && solicitacoesQuery.data !== undefined) {
|
|
||||||
return Array.isArray(solicitacoesQuery.data) ? solicitacoesQuery.data : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(solicitacoesQuery)) {
|
|
||||||
return solicitacoesQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const carregando = $derived.by(() => {
|
|
||||||
return solicitacoesQuery === undefined || solicitacoesQuery === null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Estatísticas
|
|
||||||
const stats = $derived.by(() => {
|
|
||||||
if (carregando) return null;
|
|
||||||
|
|
||||||
const total = solicitacoes.length;
|
|
||||||
const pendentes = solicitacoes.filter((s) => s.status === 'pendente').length;
|
|
||||||
const aprovadas = solicitacoes.filter((s) => s.status === 'aprovado').length;
|
|
||||||
const rejeitadas = solicitacoes.filter((s) => s.status === 'rejeitado').length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
pendentes,
|
|
||||||
aprovadas,
|
|
||||||
rejeitadas
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filtrar e buscar solicitações
|
|
||||||
const solicitacoesFiltradas = $derived.by(() => {
|
|
||||||
let resultado = solicitacoes;
|
|
||||||
|
|
||||||
// Filtrar por status
|
|
||||||
if (filtroStatus !== 'todos') {
|
|
||||||
resultado = resultado.filter((s) => s.status === filtroStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buscar por nome, matrícula ou email
|
|
||||||
if (busca.trim()) {
|
|
||||||
const termo = busca.toLowerCase().trim();
|
|
||||||
resultado = resultado.filter(
|
|
||||||
(s) =>
|
|
||||||
s.nome.toLowerCase().includes(termo) ||
|
|
||||||
s.matricula.toLowerCase().includes(termo) ||
|
|
||||||
s.email.toLowerCase().includes(termo)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordenar por data (mais recente primeiro)
|
|
||||||
return resultado.sort((a, b) => b.dataSolicitacao - a.dataSolicitacao);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Funções auxiliares
|
|
||||||
function formatarData(timestamp: number): string {
|
|
||||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatarDataRelativa(timestamp: number): string {
|
|
||||||
const agora = Date.now();
|
|
||||||
const diff = agora - timestamp;
|
|
||||||
const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
||||||
const horas = Math.floor(diff / (1000 * 60 * 60));
|
|
||||||
const minutos = Math.floor(diff / (1000 * 60));
|
|
||||||
|
|
||||||
if (dias > 0) return `${dias} dia${dias > 1 ? 's' : ''} atrás`;
|
|
||||||
if (horas > 0) return `${horas} hora${horas > 1 ? 's' : ''} atrás`;
|
|
||||||
if (minutos > 0) return `${minutos} minuto${minutos > 1 ? 's' : ''} atrás`;
|
|
||||||
return 'Agora';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusBadge(status: StatusSolicitacao): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'pendente':
|
|
||||||
return 'badge-warning';
|
|
||||||
case 'aprovado':
|
|
||||||
return 'badge-success';
|
|
||||||
case 'rejeitado':
|
|
||||||
return 'badge-error';
|
|
||||||
default:
|
|
||||||
return 'badge-neutral';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusTexto(status: StatusSolicitacao): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'pendente':
|
|
||||||
return 'Pendente';
|
|
||||||
case 'aprovado':
|
|
||||||
return 'Aprovado';
|
|
||||||
case 'rejeitado':
|
|
||||||
return 'Rejeitado';
|
|
||||||
default:
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Funções de modal
|
|
||||||
function abrirDetalhes(solicitacao: SolicitacaoAcesso) {
|
|
||||||
solicitacaoSelecionada = solicitacao;
|
|
||||||
modalDetalhesAberto = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fecharDetalhes() {
|
|
||||||
modalDetalhesAberto = false;
|
|
||||||
solicitacaoSelecionada = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abrirAprovar(solicitacao: SolicitacaoAcesso) {
|
|
||||||
solicitacaoSelecionada = solicitacao;
|
|
||||||
observacoes = '';
|
|
||||||
modalAprovarAberto = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fecharAprovar() {
|
|
||||||
modalAprovarAberto = false;
|
|
||||||
solicitacaoSelecionada = null;
|
|
||||||
observacoes = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function abrirRejeitar(solicitacao: SolicitacaoAcesso) {
|
|
||||||
solicitacaoSelecionada = solicitacao;
|
|
||||||
observacoes = '';
|
|
||||||
modalRejeitarAberto = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fecharRejeitar() {
|
|
||||||
modalRejeitarAberto = false;
|
|
||||||
solicitacaoSelecionada = null;
|
|
||||||
observacoes = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Funções de ação
|
|
||||||
async function aprovarSolicitacao() {
|
|
||||||
if (!solicitacaoSelecionada) return;
|
|
||||||
|
|
||||||
processando = true;
|
|
||||||
mensagem = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.mutation(api.solicitacoesAcesso.aprovar, {
|
|
||||||
solicitacaoId: solicitacaoSelecionada._id,
|
|
||||||
observacoes: observacoes.trim() || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
mensagem = {
|
|
||||||
tipo: 'success',
|
|
||||||
texto: 'Solicitação aprovada com sucesso!'
|
|
||||||
};
|
|
||||||
|
|
||||||
fecharAprovar();
|
|
||||||
|
|
||||||
// Limpar mensagem após 3 segundos
|
|
||||||
setTimeout(() => {
|
|
||||||
mensagem = null;
|
|
||||||
}, 3000);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Erro ao aprovar solicitação';
|
|
||||||
mensagem = {
|
|
||||||
tipo: 'error',
|
|
||||||
texto: errorMessage
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
processando = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rejeitarSolicitacao() {
|
|
||||||
if (!solicitacaoSelecionada) return;
|
|
||||||
|
|
||||||
processando = true;
|
|
||||||
mensagem = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.mutation(api.solicitacoesAcesso.rejeitar, {
|
|
||||||
solicitacaoId: solicitacaoSelecionada._id,
|
|
||||||
observacoes: observacoes.trim() || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
mensagem = {
|
|
||||||
tipo: 'success',
|
|
||||||
texto: 'Solicitação rejeitada com sucesso!'
|
|
||||||
};
|
|
||||||
|
|
||||||
fecharRejeitar();
|
|
||||||
|
|
||||||
// Limpar mensagem após 3 segundos
|
|
||||||
setTimeout(() => {
|
|
||||||
mensagem = null;
|
|
||||||
}, 3000);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Erro ao rejeitar solicitação';
|
|
||||||
mensagem = {
|
|
||||||
tipo: 'error',
|
|
||||||
texto: errorMessage
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
processando = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
|
||||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
|
||||||
<!-- Mensagem de Feedback -->
|
|
||||||
{#if mensagem}
|
|
||||||
<div class="alert alert-{mensagem.tipo} mb-6 shadow-lg">
|
|
||||||
{#if mensagem.tipo === 'success'}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6 shrink-0 stroke-current"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{:else if mensagem.tipo === 'error'}
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6 shrink-0 stroke-current"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
<span>{mensagem.texto}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="mb-8 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="bg-primary/10 rounded-xl p-3">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="text-primary h-8 w-8"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-base-content text-3xl font-bold">Solicitações de Acesso</h1>
|
|
||||||
<p class="text-base-content/60 mt-1">
|
|
||||||
Gerencie e analise solicitações de acesso ao sistema
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estatísticas -->
|
|
||||||
{#if stats}
|
|
||||||
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<StatsCard
|
|
||||||
title="Total de Solicitações"
|
|
||||||
value={stats.total}
|
|
||||||
Icon={FileText}
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatsCard
|
|
||||||
title="Pendentes"
|
|
||||||
value={stats.pendentes}
|
|
||||||
description={stats.total > 0
|
|
||||||
? ((stats.pendentes / stats.total) * 100).toFixed(1) + '% do total'
|
|
||||||
: '0% do total'}
|
|
||||||
Icon={Clock}
|
|
||||||
color="warning"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatsCard
|
|
||||||
title="Aprovadas"
|
|
||||||
value={stats.aprovadas}
|
|
||||||
description={stats.total > 0
|
|
||||||
? ((stats.aprovadas / stats.total) * 100).toFixed(1) + '% do total'
|
|
||||||
: '0% do total'}
|
|
||||||
Icon={CheckCircle2}
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatsCard
|
|
||||||
title="Rejeitadas"
|
|
||||||
value={stats.rejeitadas}
|
|
||||||
description={stats.total > 0
|
|
||||||
? ((stats.rejeitadas / stats.total) * 100).toFixed(1) + '% do total'
|
|
||||||
: '0% do total'}
|
|
||||||
Icon={XCircle}
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center justify-center py-20">
|
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Filtros e Busca -->
|
|
||||||
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Tabs de Status -->
|
|
||||||
<div class="tabs tabs-boxed bg-base-200 mb-4 p-2">
|
|
||||||
<button
|
|
||||||
class="tab {filtroStatus === 'todos' ? 'tab-active' : ''}"
|
|
||||||
onclick={() => (filtroStatus = 'todos')}
|
|
||||||
>
|
|
||||||
Todas
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab {filtroStatus === 'pendente' ? 'tab-active' : ''}"
|
|
||||||
onclick={() => (filtroStatus = 'pendente')}
|
|
||||||
>
|
|
||||||
Pendentes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab {filtroStatus === 'aprovado' ? 'tab-active' : ''}"
|
|
||||||
onclick={() => (filtroStatus = 'aprovado')}
|
|
||||||
>
|
|
||||||
Aprovadas
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab {filtroStatus === 'rejeitado' ? 'tab-active' : ''}"
|
|
||||||
onclick={() => (filtroStatus = 'rejeitado')}
|
|
||||||
>
|
|
||||||
Rejeitadas
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Campo de Busca -->
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Buscar por nome, matrícula ou e-mail</span>
|
|
||||||
</label>
|
|
||||||
<div class="relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Digite para buscar..."
|
|
||||||
class="input input-bordered w-full pl-10"
|
|
||||||
bind:value={busca}
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lista de Solicitações -->
|
|
||||||
{#if carregando}
|
|
||||||
<div class="flex items-center justify-center py-20">
|
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
||||||
</div>
|
|
||||||
{:else if solicitacoesFiltradas.length === 0}
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body py-20 text-center">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="text-base-content/30 mx-auto mb-4 h-16 w-16"
|
|
||||||
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>
|
|
||||||
<h3 class="text-base-content/70 mb-2 text-xl font-semibold">
|
|
||||||
Nenhuma solicitação encontrada
|
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/50">
|
|
||||||
{#if busca.trim() || filtroStatus !== 'todos'}
|
|
||||||
Tente ajustar os filtros ou a busca.
|
|
||||||
{:else}
|
|
||||||
Ainda não há solicitações de acesso cadastradas.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
|
||||||
{#each solicitacoesFiltradas as solicitacao}
|
|
||||||
<div class="card bg-base-100 shadow-xl transition-shadow hover:shadow-2xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="mb-2 flex items-center gap-3">
|
|
||||||
<h3 class="text-base-content text-xl font-bold">{solicitacao.nome}</h3>
|
|
||||||
<span class="badge {getStatusBadge(solicitacao.status)} badge-lg">
|
|
||||||
{getStatusTexto(solicitacao.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-base-content/70 grid grid-cols-1 gap-2 text-sm md:grid-cols-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<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="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="font-semibold">Matrícula:</span>
|
|
||||||
<span>{solicitacao.matricula}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="font-semibold">E-mail:</span>
|
|
||||||
<span>{solicitacao.email}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="font-semibold">Telefone:</span>
|
|
||||||
<span>{solicitacao.telefone}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-base-content/50 mt-3 text-xs">
|
|
||||||
<span class="font-semibold">Solicitado em:</span>
|
|
||||||
{formatarData(solicitacao.dataSolicitacao)} ({formatarDataRelativa(
|
|
||||||
solicitacao.dataSolicitacao
|
|
||||||
)})
|
|
||||||
{#if solicitacao.dataResposta}
|
|
||||||
<span class="ml-4 font-semibold">Processado em:</span>
|
|
||||||
{formatarData(solicitacao.dataResposta)}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline btn-primary"
|
|
||||||
onclick={() => abrirDetalhes(solicitacao)}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Ver Detalhes
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if solicitacao.status === 'pendente'}
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-success"
|
|
||||||
onclick={() => abrirAprovar(solicitacao)}
|
|
||||||
>
|
|
||||||
<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="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Aprovar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-error" onclick={() => abrirRejeitar(solicitacao)}>
|
|
||||||
<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="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Rejeitar
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Modal de Detalhes -->
|
|
||||||
{#if modalDetalhesAberto && solicitacaoSelecionada}
|
|
||||||
<dialog class="modal modal-open">
|
|
||||||
<div class="modal-box max-w-2xl">
|
|
||||||
<h3 class="mb-4 text-2xl font-bold">Detalhes da Solicitação</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="mb-4 flex items-center gap-3">
|
|
||||||
<span class="badge {getStatusBadge(solicitacaoSelecionada.status)} badge-lg">
|
|
||||||
{getStatusTexto(solicitacaoSelecionada.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Nome Completo</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.nome}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Matrícula</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.matricula}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">E-mail</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.email}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Telefone</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">{solicitacaoSelecionada.telefone}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Data da Solicitação</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">
|
|
||||||
{formatarData(solicitacaoSelecionada.dataSolicitacao)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if solicitacaoSelecionada.dataResposta}
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Data de Processamento</span>
|
|
||||||
</label>
|
|
||||||
<div class="input input-bordered">
|
|
||||||
{formatarData(solicitacaoSelecionada.dataResposta)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if solicitacaoSelecionada.observacoes}
|
|
||||||
<div>
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Observações</span>
|
|
||||||
</label>
|
|
||||||
<div class="textarea textarea-bordered min-h-24">
|
|
||||||
{solicitacaoSelecionada.observacoes}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button class="btn" onclick={fecharDetalhes}>Fechar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button onclick={fecharDetalhes}>fechar</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Modal de Aprovar -->
|
|
||||||
{#if modalAprovarAberto && solicitacaoSelecionada}
|
|
||||||
<dialog class="modal modal-open">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="mb-4 text-2xl font-bold">Aprovar Solicitação</h3>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<p class="text-base-content/70 mb-2">
|
|
||||||
Você está prestes a aprovar a solicitação de acesso de <strong
|
|
||||||
>{solicitacaoSelecionada.nome}</strong
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<p class="text-base-content/60 text-sm">
|
|
||||||
Após aprovar, o sistema permitirá que esta pessoa solicite acesso ao sistema.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control mb-4">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
class="textarea textarea-bordered"
|
|
||||||
placeholder="Adicione observações sobre a aprovação..."
|
|
||||||
bind:value={observacoes}
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button class="btn" onclick={fecharAprovar} disabled={processando}> Cancelar </button>
|
|
||||||
<button class="btn btn-success" onclick={aprovarSolicitacao} disabled={processando}>
|
|
||||||
{#if processando}
|
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
|
||||||
Processando...
|
|
||||||
{: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>
|
|
||||||
Confirmar Aprovação
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button onclick={fecharAprovar}>fechar</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Modal de Rejeitar -->
|
|
||||||
{#if modalRejeitarAberto && solicitacaoSelecionada}
|
|
||||||
<dialog class="modal modal-open">
|
|
||||||
<div class="modal-box">
|
|
||||||
<h3 class="mb-4 text-2xl font-bold">Rejeitar Solicitação</h3>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<p class="text-base-content/70 mb-2">
|
|
||||||
Você está prestes a rejeitar a solicitação de acesso de <strong
|
|
||||||
>{solicitacaoSelecionada.nome}</strong
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<p class="text-base-content/60 text-sm">
|
|
||||||
Esta ação não pode ser desfeita. Recomendamos adicionar um motivo para a rejeição.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-control mb-4">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text font-semibold">Motivo da Rejeição (recomendado)</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
class="textarea textarea-bordered"
|
|
||||||
placeholder="Descreva o motivo da rejeição..."
|
|
||||||
bind:value={observacoes}
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button class="btn" onclick={fecharRejeitar} disabled={processando}> Cancelar </button>
|
|
||||||
<button class="btn btn-error" onclick={rejeitarSolicitacao} disabled={processando}>
|
|
||||||
{#if processando}
|
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
|
||||||
Processando...
|
|
||||||
{: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="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Confirmar Rejeição
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button onclick={fecharRejeitar}>fechar</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</ProtectedRoute>
|
|
||||||
6
packages/backend/convex/_generated/api.d.ts
vendored
6
packages/backend/convex/_generated/api.d.ts
vendored
@@ -21,6 +21,8 @@ import type * as auth_utils from "../auth/utils.js";
|
|||||||
import type * as chamados from "../chamados.js";
|
import type * as chamados from "../chamados.js";
|
||||||
import type * as chat from "../chat.js";
|
import type * as chat from "../chat.js";
|
||||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||||
|
import type * as configuracaoPonto from "../configuracaoPonto.js";
|
||||||
|
import type * as configuracaoRelogio from "../configuracaoRelogio.js";
|
||||||
import type * as contratos from "../contratos.js";
|
import type * as contratos from "../contratos.js";
|
||||||
import type * as crons from "../crons.js";
|
import type * as crons from "../crons.js";
|
||||||
import type * as cursos from "../cursos.js";
|
import type * as cursos from "../cursos.js";
|
||||||
@@ -45,7 +47,6 @@ import type * as saldoFerias from "../saldoFerias.js";
|
|||||||
import type * as security from "../security.js";
|
import type * as security from "../security.js";
|
||||||
import type * as seed from "../seed.js";
|
import type * as seed from "../seed.js";
|
||||||
import type * as simbolos from "../simbolos.js";
|
import type * as simbolos from "../simbolos.js";
|
||||||
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
|
|
||||||
import type * as templatesMensagens from "../templatesMensagens.js";
|
import type * as templatesMensagens from "../templatesMensagens.js";
|
||||||
import type * as times from "../times.js";
|
import type * as times from "../times.js";
|
||||||
import type * as todos from "../todos.js";
|
import type * as todos from "../todos.js";
|
||||||
@@ -73,9 +74,9 @@ declare const fullApi: ApiFromModules<{
|
|||||||
chamados: typeof chamados;
|
chamados: typeof chamados;
|
||||||
chat: typeof chat;
|
chat: typeof chat;
|
||||||
configuracaoEmail: typeof configuracaoEmail;
|
configuracaoEmail: typeof configuracaoEmail;
|
||||||
contratos: typeof contratos;
|
|
||||||
configuracaoPonto: typeof configuracaoPonto;
|
configuracaoPonto: typeof configuracaoPonto;
|
||||||
configuracaoRelogio: typeof configuracaoRelogio;
|
configuracaoRelogio: typeof configuracaoRelogio;
|
||||||
|
contratos: typeof contratos;
|
||||||
crons: typeof crons;
|
crons: typeof crons;
|
||||||
cursos: typeof cursos;
|
cursos: typeof cursos;
|
||||||
dashboard: typeof dashboard;
|
dashboard: typeof dashboard;
|
||||||
@@ -99,7 +100,6 @@ declare const fullApi: ApiFromModules<{
|
|||||||
security: typeof security;
|
security: typeof security;
|
||||||
seed: typeof seed;
|
seed: typeof seed;
|
||||||
simbolos: typeof simbolos;
|
simbolos: typeof simbolos;
|
||||||
solicitacoesAcesso: typeof solicitacoesAcesso;
|
|
||||||
templatesMensagens: typeof templatesMensagens;
|
templatesMensagens: typeof templatesMensagens;
|
||||||
times: typeof times;
|
times: typeof times;
|
||||||
todos: typeof todos;
|
todos: typeof todos;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ export const getStats = query({
|
|||||||
returns: v.object({
|
returns: v.object({
|
||||||
totalFuncionarios: v.number(),
|
totalFuncionarios: v.number(),
|
||||||
totalSimbolos: v.number(),
|
totalSimbolos: v.number(),
|
||||||
totalSolicitacoesAcesso: v.number(),
|
|
||||||
solicitacoesPendentes: v.number(),
|
|
||||||
funcionariosAtivos: v.number(),
|
funcionariosAtivos: v.number(),
|
||||||
funcionariosDesligados: v.number(),
|
funcionariosDesligados: v.number(),
|
||||||
cargoComissionado: v.number(),
|
cargoComissionado: v.number(),
|
||||||
@@ -42,19 +40,9 @@ export const getStats = query({
|
|||||||
const simbolos = await ctx.db.query("simbolos").collect();
|
const simbolos = await ctx.db.query("simbolos").collect();
|
||||||
const totalSimbolos = simbolos.length;
|
const totalSimbolos = simbolos.length;
|
||||||
|
|
||||||
// Contar solicitações de acesso
|
|
||||||
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
|
|
||||||
const totalSolicitacoesAcesso = solicitacoes.length;
|
|
||||||
|
|
||||||
const solicitacoesPendentes = solicitacoes.filter(
|
|
||||||
(s) => s.status === "pendente"
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalFuncionarios,
|
totalFuncionarios,
|
||||||
totalSimbolos,
|
totalSimbolos,
|
||||||
totalSolicitacoesAcesso,
|
|
||||||
solicitacoesPendentes,
|
|
||||||
funcionariosAtivos,
|
funcionariosAtivos,
|
||||||
funcionariosDesligados,
|
funcionariosDesligados,
|
||||||
cargoComissionado,
|
cargoComissionado,
|
||||||
@@ -68,7 +56,6 @@ export const getRecentActivity = query({
|
|||||||
args: {},
|
args: {},
|
||||||
returns: v.object({
|
returns: v.object({
|
||||||
funcionariosCadastrados24h: v.number(),
|
funcionariosCadastrados24h: v.number(),
|
||||||
solicitacoesAcesso24h: v.number(),
|
|
||||||
simbolosCadastrados24h: v.number(),
|
simbolosCadastrados24h: v.number(),
|
||||||
}),
|
}),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
@@ -81,11 +68,6 @@ export const getRecentActivity = query({
|
|||||||
(f) => f._creationTime >= last24h
|
(f) => f._creationTime >= last24h
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Solicitações de acesso nas últimas 24h
|
|
||||||
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
|
|
||||||
const solicitacoesAcesso24h = solicitacoes.filter(
|
|
||||||
(s) => s.dataSolicitacao >= last24h
|
|
||||||
).length;
|
|
||||||
|
|
||||||
// Símbolos cadastrados nas últimas 24h
|
// Símbolos cadastrados nas últimas 24h
|
||||||
const simbolos = await ctx.db.query("simbolos").collect();
|
const simbolos = await ctx.db.query("simbolos").collect();
|
||||||
@@ -95,7 +77,6 @@ export const getRecentActivity = query({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
funcionariosCadastrados24h,
|
funcionariosCadastrados24h,
|
||||||
solicitacoesAcesso24h,
|
|
||||||
simbolosCadastrados24h,
|
simbolosCadastrados24h,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -137,15 +118,13 @@ export const getEvolucaoCadastros = query({
|
|||||||
v.object({
|
v.object({
|
||||||
mes: v.string(),
|
mes: v.string(),
|
||||||
funcionarios: v.number(),
|
funcionarios: v.number(),
|
||||||
solicitacoes: v.number(),
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const funcionarios = await ctx.db.query("funcionarios").collect();
|
const funcionarios = await ctx.db.query("funcionarios").collect();
|
||||||
const solicitacoes = await ctx.db.query("solicitacoesAcesso").collect();
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const meses: Array<{ mes: string; funcionarios: number; solicitacoes: number }> = [];
|
const meses: Array<{ mes: string; funcionarios: number }> = [];
|
||||||
|
|
||||||
// Últimos 6 meses
|
// Últimos 6 meses
|
||||||
for (let i = 5; i >= 0; i--) {
|
for (let i = 5; i >= 0; i--) {
|
||||||
@@ -161,14 +140,9 @@ export const getEvolucaoCadastros = query({
|
|||||||
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
|
(f) => f._creationTime >= date.getTime() && f._creationTime < nextDate.getTime()
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const solCount = solicitacoes.filter(
|
|
||||||
(s) => s.dataSolicitacao >= date.getTime() && s.dataSolicitacao < nextDate.getTime()
|
|
||||||
).length;
|
|
||||||
|
|
||||||
meses.push({
|
meses.push({
|
||||||
mes: mesNome,
|
mes: mesNome,
|
||||||
funcionarios: funcCount,
|
funcionarios: funcCount,
|
||||||
solicitacoes: solCount,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -594,12 +594,11 @@ export const getStatusSistema = query({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Total de registros (estimativa baseada em tabelas principais)
|
// Total de registros (estimativa baseada em tabelas principais)
|
||||||
const [usuarios, funcionarios, simbolos, solicitacoesAcesso, alertas, metricas] =
|
const [usuarios, funcionarios, simbolos, alertas, metricas] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ctx.db.query('usuarios').collect(),
|
ctx.db.query('usuarios').collect(),
|
||||||
ctx.db.query('funcionarios').collect(),
|
ctx.db.query('funcionarios').collect(),
|
||||||
ctx.db.query('simbolos').collect(),
|
ctx.db.query('simbolos').collect(),
|
||||||
ctx.db.query('solicitacoesAcesso').collect(),
|
|
||||||
ctx.db.query('alertConfigurations').collect(),
|
ctx.db.query('alertConfigurations').collect(),
|
||||||
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
ctx.db.query('systemMetrics').take(100) // não precisa contar tudo
|
||||||
]);
|
]);
|
||||||
@@ -607,7 +606,6 @@ export const getStatusSistema = query({
|
|||||||
usuarios.length +
|
usuarios.length +
|
||||||
funcionarios.length +
|
funcionarios.length +
|
||||||
simbolos.length +
|
simbolos.length +
|
||||||
solicitacoesAcesso.length +
|
|
||||||
alertas.length +
|
alertas.length +
|
||||||
metricas.length;
|
metricas.length;
|
||||||
|
|
||||||
|
|||||||
@@ -514,24 +514,6 @@ export default defineSchema({
|
|||||||
valor: v.string(),
|
valor: v.string(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
solicitacoesAcesso: defineTable({
|
|
||||||
nome: v.string(),
|
|
||||||
matricula: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
telefone: v.string(),
|
|
||||||
status: v.union(
|
|
||||||
v.literal("pendente"),
|
|
||||||
v.literal("aprovado"),
|
|
||||||
v.literal("rejeitado")
|
|
||||||
),
|
|
||||||
dataSolicitacao: v.number(),
|
|
||||||
dataResposta: v.optional(v.number()),
|
|
||||||
observacoes: v.optional(v.string()),
|
|
||||||
})
|
|
||||||
.index("by_status", ["status"])
|
|
||||||
.index("by_matricula", ["matricula"])
|
|
||||||
.index("by_email", ["email"]),
|
|
||||||
|
|
||||||
// Sistema de Autenticação e Controle de Acesso
|
// Sistema de Autenticação e Controle de Acesso
|
||||||
usuarios: defineTable({
|
usuarios: defineTable({
|
||||||
authId: v.string(),
|
authId: v.string(),
|
||||||
|
|||||||
@@ -164,27 +164,6 @@ const funcionariosData = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const solicitacoesAcessoData = [
|
|
||||||
{
|
|
||||||
dataResposta: 1761445098933,
|
|
||||||
dataSolicitacao: 1761445038329,
|
|
||||||
email: 'severino@gmail.com',
|
|
||||||
matricula: '3231',
|
|
||||||
nome: 'Severino Gates',
|
|
||||||
observacoes: 'Aprovação realizada por Deyvison',
|
|
||||||
status: 'aprovado' as const,
|
|
||||||
telefone: '(81) 9942-3551'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataSolicitacao: 1761445187258,
|
|
||||||
email: 'michaeljackson@gmail.com',
|
|
||||||
matricula: '123321',
|
|
||||||
nome: 'Michael Jackson',
|
|
||||||
status: 'pendente' as const,
|
|
||||||
telefone: '(81) 99423-5551'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed inicial do banco de dados com os dados exportados do Convex Cloud
|
* Seed inicial do banco de dados com os dados exportados do Convex Cloud
|
||||||
*/
|
*/
|
||||||
@@ -338,8 +317,6 @@ export const seedCreateUsuariosParaFuncionarios = internalMutation({
|
|||||||
});
|
});
|
||||||
delay += 50;
|
delay += 50;
|
||||||
}
|
}
|
||||||
// Agenda próxima etapa após as criações individuais
|
|
||||||
await ctx.scheduler.runAfter(delay + 300, internal.seed.seedInserirSolicitacoesAcesso, {});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -402,55 +379,6 @@ export const seedCreateUsuarioParaFuncionario = internalMutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const seedInserirSolicitacoesAcesso = internalMutation({
|
|
||||||
args: {},
|
|
||||||
returns: v.null(),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
console.log('📋 Inserindo solicitações de acesso...');
|
|
||||||
for (const solicitacao of solicitacoesAcessoData) {
|
|
||||||
// Evitar duplicidade por matrícula
|
|
||||||
const existente = await ctx.db
|
|
||||||
.query('solicitacoesAcesso')
|
|
||||||
.withIndex('by_matricula', (q) => q.eq('matricula', solicitacao.matricula))
|
|
||||||
.first();
|
|
||||||
if (existente) {
|
|
||||||
console.log(` ℹ️ Solicitação já existe p/ matrícula ${solicitacao.matricula}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const dadosSolicitacao: {
|
|
||||||
nome: string;
|
|
||||||
matricula: string;
|
|
||||||
email: string;
|
|
||||||
telefone: string;
|
|
||||||
status: 'pendente' | 'aprovado' | 'rejeitado';
|
|
||||||
dataSolicitacao: number;
|
|
||||||
dataResposta?: number;
|
|
||||||
observacoes?: string;
|
|
||||||
} = {
|
|
||||||
nome: solicitacao.nome,
|
|
||||||
matricula: solicitacao.matricula,
|
|
||||||
email: solicitacao.email,
|
|
||||||
telefone: solicitacao.telefone,
|
|
||||||
status: solicitacao.status,
|
|
||||||
dataSolicitacao: solicitacao.dataSolicitacao
|
|
||||||
};
|
|
||||||
|
|
||||||
if (solicitacao.dataResposta) {
|
|
||||||
dadosSolicitacao.dataResposta = solicitacao.dataResposta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solicitacao.observacoes) {
|
|
||||||
dadosSolicitacao.observacoes = solicitacao.observacoes;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.db.insert('solicitacoesAcesso', dadosSolicitacao);
|
|
||||||
console.log(` ✅ Solicitação criada: ${solicitacao.nome} (${solicitacao.status})`);
|
|
||||||
}
|
|
||||||
console.log('✨ Seed concluído!');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const seedDatabase = internalAction({
|
export const seedDatabase = internalAction({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
@@ -460,7 +388,6 @@ export const seedDatabase = internalAction({
|
|||||||
await ctx.runMutation(internal.seed.seedCreateSimbolos, {});
|
await ctx.runMutation(internal.seed.seedCreateSimbolos, {});
|
||||||
await ctx.runMutation(internal.seed.seedCreateFuncionarios, {});
|
await ctx.runMutation(internal.seed.seedCreateFuncionarios, {});
|
||||||
await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {});
|
await ctx.runMutation(internal.seed.seedCreateUsuariosParaFuncionarios, {});
|
||||||
await ctx.runMutation(internal.seed.seedInserirSolicitacoesAcesso, {});
|
|
||||||
console.log('✨ Seed do banco de dados concluído com sucesso pela action!');
|
console.log('✨ Seed do banco de dados concluído com sucesso pela action!');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -677,13 +604,6 @@ export const clearDatabase = internalMutation({
|
|||||||
}
|
}
|
||||||
console.log(` ✅ ${funcionarios.length} funcionários removidos`);
|
console.log(` ✅ ${funcionarios.length} funcionários removidos`);
|
||||||
|
|
||||||
// 20. Solicitações de acesso
|
|
||||||
const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect();
|
|
||||||
for (const solicitacao of solicitacoesAcesso) {
|
|
||||||
await ctx.db.delete(solicitacao._id);
|
|
||||||
}
|
|
||||||
console.log(` ✅ ${solicitacoesAcesso.length} solicitações de acesso removidas`);
|
|
||||||
|
|
||||||
// 21. Símbolos
|
// 21. Símbolos
|
||||||
const simbolos = await ctx.db.query('simbolos').collect();
|
const simbolos = await ctx.db.query('simbolos').collect();
|
||||||
for (const simbolo of simbolos) {
|
for (const simbolo of simbolos) {
|
||||||
@@ -907,13 +827,6 @@ export const limparBanco = mutation({
|
|||||||
}
|
}
|
||||||
console.log(` ✅ ${funcionarios.length} funcionários removidos`);
|
console.log(` ✅ ${funcionarios.length} funcionários removidos`);
|
||||||
|
|
||||||
// 20. Solicitações de acesso
|
|
||||||
const solicitacoesAcesso = await ctx.db.query('solicitacoesAcesso').collect();
|
|
||||||
for (const solicitacao of solicitacoesAcesso) {
|
|
||||||
await ctx.db.delete(solicitacao._id);
|
|
||||||
}
|
|
||||||
console.log(` ✅ ${solicitacoesAcesso.length} solicitações de acesso removidas`);
|
|
||||||
|
|
||||||
// 21. Símbolos
|
// 21. Símbolos
|
||||||
const simbolos = await ctx.db.query('simbolos').collect();
|
const simbolos = await ctx.db.query('simbolos').collect();
|
||||||
for (const simbolo of simbolos) {
|
for (const simbolo of simbolos) {
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
import { mutation, query } from "./_generated/server";
|
|
||||||
import { v } from "convex/values";
|
|
||||||
|
|
||||||
// Criar uma nova solicitação de acesso
|
|
||||||
export const create = mutation({
|
|
||||||
args: {
|
|
||||||
nome: v.string(),
|
|
||||||
matricula: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
telefone: v.string(),
|
|
||||||
},
|
|
||||||
returns: v.object({
|
|
||||||
solicitacaoId: v.id("solicitacoesAcesso"),
|
|
||||||
}),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
// Verificar se já existe uma solicitação pendente com a mesma matrícula
|
|
||||||
const existingByMatricula = await ctx.db
|
|
||||||
.query("solicitacoesAcesso")
|
|
||||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
|
||||||
.filter((q) => q.eq(q.field("status"), "pendente"))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (existingByMatricula) {
|
|
||||||
throw new Error("Já existe uma solicitação pendente para esta matrícula.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar se já existe uma solicitação pendente com o mesmo email
|
|
||||||
const existingByEmail = await ctx.db
|
|
||||||
.query("solicitacoesAcesso")
|
|
||||||
.withIndex("by_email", (q) => q.eq("email", args.email))
|
|
||||||
.filter((q) => q.eq(q.field("status"), "pendente"))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (existingByEmail) {
|
|
||||||
throw new Error("Já existe uma solicitação pendente para este e-mail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const solicitacaoId = await ctx.db.insert("solicitacoesAcesso", {
|
|
||||||
nome: args.nome,
|
|
||||||
matricula: args.matricula,
|
|
||||||
email: args.email,
|
|
||||||
telefone: args.telefone,
|
|
||||||
status: "pendente",
|
|
||||||
dataSolicitacao: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { solicitacaoId };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listar todas as solicitações (para o painel administrativo)
|
|
||||||
export const getAll = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id("solicitacoesAcesso"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
nome: v.string(),
|
|
||||||
matricula: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
telefone: v.string(),
|
|
||||||
status: v.union(
|
|
||||||
v.literal("pendente"),
|
|
||||||
v.literal("aprovado"),
|
|
||||||
v.literal("rejeitado")
|
|
||||||
),
|
|
||||||
dataSolicitacao: v.number(),
|
|
||||||
dataResposta: v.union(v.number(), v.null()),
|
|
||||||
observacoes: v.union(v.string(), v.null()),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const solicitacoes = await ctx.db
|
|
||||||
.query("solicitacoesAcesso")
|
|
||||||
.order("desc")
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
return solicitacoes.map((s) => ({
|
|
||||||
_id: s._id,
|
|
||||||
_creationTime: s._creationTime,
|
|
||||||
nome: s.nome,
|
|
||||||
matricula: s.matricula,
|
|
||||||
email: s.email,
|
|
||||||
telefone: s.telefone,
|
|
||||||
status: s.status,
|
|
||||||
dataSolicitacao: s.dataSolicitacao,
|
|
||||||
dataResposta: s.dataResposta ?? null,
|
|
||||||
observacoes: s.observacoes ?? null,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listar apenas solicitações pendentes
|
|
||||||
export const getPendentes = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id("solicitacoesAcesso"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
nome: v.string(),
|
|
||||||
matricula: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
telefone: v.string(),
|
|
||||||
status: v.union(
|
|
||||||
v.literal("pendente"),
|
|
||||||
v.literal("aprovado"),
|
|
||||||
v.literal("rejeitado")
|
|
||||||
),
|
|
||||||
dataSolicitacao: v.number(),
|
|
||||||
dataResposta: v.union(v.number(), v.null()),
|
|
||||||
observacoes: v.union(v.string(), v.null()),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const solicitacoes = await ctx.db
|
|
||||||
.query("solicitacoesAcesso")
|
|
||||||
.withIndex("by_status", (q) => q.eq("status", "pendente"))
|
|
||||||
.order("desc")
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
return solicitacoes.map((s) => ({
|
|
||||||
_id: s._id,
|
|
||||||
_creationTime: s._creationTime,
|
|
||||||
nome: s.nome,
|
|
||||||
matricula: s.matricula,
|
|
||||||
email: s.email,
|
|
||||||
telefone: s.telefone,
|
|
||||||
status: s.status,
|
|
||||||
dataSolicitacao: s.dataSolicitacao,
|
|
||||||
dataResposta: s.dataResposta ?? null,
|
|
||||||
observacoes: s.observacoes ?? null,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Aprovar uma solicitação
|
|
||||||
export const aprovar = mutation({
|
|
||||||
args: {
|
|
||||||
solicitacaoId: v.id("solicitacoesAcesso"),
|
|
||||||
observacoes: v.optional(v.string()),
|
|
||||||
},
|
|
||||||
returns: v.null(),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
|
||||||
if (!solicitacao) {
|
|
||||||
throw new Error("Solicitação não encontrada.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solicitacao.status !== "pendente") {
|
|
||||||
throw new Error("Esta solicitação já foi processada.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.db.patch(args.solicitacaoId, {
|
|
||||||
status: "aprovado",
|
|
||||||
dataResposta: Date.now(),
|
|
||||||
observacoes: args.observacoes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rejeitar uma solicitação
|
|
||||||
export const rejeitar = mutation({
|
|
||||||
args: {
|
|
||||||
solicitacaoId: v.id("solicitacoesAcesso"),
|
|
||||||
observacoes: v.optional(v.string()),
|
|
||||||
},
|
|
||||||
returns: v.null(),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
|
||||||
if (!solicitacao) {
|
|
||||||
throw new Error("Solicitação não encontrada.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (solicitacao.status !== "pendente") {
|
|
||||||
throw new Error("Esta solicitação já foi processada.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.db.patch(args.solicitacaoId, {
|
|
||||||
status: "rejeitado",
|
|
||||||
dataResposta: Date.now(),
|
|
||||||
observacoes: args.observacoes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obter uma solicitação por ID
|
|
||||||
export const getById = query({
|
|
||||||
args: {
|
|
||||||
solicitacaoId: v.id("solicitacoesAcesso"),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
_id: v.id("solicitacoesAcesso"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
nome: v.string(),
|
|
||||||
matricula: v.string(),
|
|
||||||
email: v.string(),
|
|
||||||
telefone: v.string(),
|
|
||||||
status: v.union(
|
|
||||||
v.literal("pendente"),
|
|
||||||
v.literal("aprovado"),
|
|
||||||
v.literal("rejeitado")
|
|
||||||
),
|
|
||||||
dataSolicitacao: v.number(),
|
|
||||||
dataResposta: v.union(v.number(), v.null()),
|
|
||||||
observacoes: v.union(v.string(), v.null()),
|
|
||||||
}),
|
|
||||||
v.null()
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const solicitacao = await ctx.db.get(args.solicitacaoId);
|
|
||||||
if (!solicitacao) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
_id: solicitacao._id,
|
|
||||||
_creationTime: solicitacao._creationTime,
|
|
||||||
nome: solicitacao.nome,
|
|
||||||
matricula: solicitacao.matricula,
|
|
||||||
email: solicitacao.email,
|
|
||||||
telefone: solicitacao.telefone,
|
|
||||||
status: solicitacao.status,
|
|
||||||
dataSolicitacao: solicitacao.dataSolicitacao,
|
|
||||||
dataResposta: solicitacao.dataResposta ?? null,
|
|
||||||
observacoes: solicitacao.observacoes ?? null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user