3293 lines
100 KiB
Svelte
3293 lines
100 KiB
Svelte
<script lang="ts">
|
|
import { goto } from '$app/navigation';
|
|
import { resolve } from '$app/paths';
|
|
import { onMount, tick } from 'svelte';
|
|
import { SvelteDate, SvelteMap } from 'svelte/reactivity';
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { FunctionReturnType } from 'convex/server';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import BarChart3D from '$lib/components/ti/charts/BarChart3D.svelte';
|
|
import AlterarStatusFerias from '$lib/components/AlterarStatusFerias.svelte';
|
|
import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte';
|
|
import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte';
|
|
import jsPDF from 'jspdf';
|
|
import autoTable from 'jspdf-autotable';
|
|
import ExcelJS from 'exceljs';
|
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
|
import { format } from 'date-fns';
|
|
import { ptBR } from 'date-fns/locale';
|
|
import { toast } from 'svelte-sonner';
|
|
type CalendarConstructor = typeof import('@fullcalendar/core').Calendar;
|
|
type CalendarInstance = import('@fullcalendar/core').Calendar;
|
|
type EventInput = import('@fullcalendar/core').EventInput;
|
|
type TodasSolicitacoes = FunctionReturnType<typeof api.ferias.listarTodas>;
|
|
type Solicitacao = TodasSolicitacoes[number];
|
|
type PeriodoDetalhado = {
|
|
funcionarioId: Id<'funcionarios'>;
|
|
anoReferencia: number;
|
|
feriasId: Id<'ferias'>;
|
|
funcionarioNome: string;
|
|
matricula?: string | null;
|
|
timeNome?: string | null;
|
|
timeCor?: string | null;
|
|
gestorNome?: string | null;
|
|
status: Solicitacao['status'];
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
diasCorridos: number;
|
|
};
|
|
|
|
type SolicitacoesPorAnoResumo = {
|
|
ano: number;
|
|
solicitacoes: number;
|
|
diasTotais: number;
|
|
};
|
|
|
|
// Buscar TODAS as solicitações de férias (Dashboard RH)
|
|
const todasSolicitacoesQuery = useQuery(api.ferias.listarTodas, {});
|
|
const client = useConvexClient();
|
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
|
|
|
// Estado da aba ativa
|
|
let abaAtiva = $state<'dashboard' | 'solicitacoes' | 'relatorios'>('dashboard');
|
|
|
|
// Estado para controlar qual período está selecionado para mudança de status
|
|
let periodoSelecionado = $state<Id<'ferias'> | null>(null);
|
|
|
|
// Estados de loading e error
|
|
const isLoading = $derived(todasSolicitacoesQuery?.isLoading ?? true);
|
|
|
|
// Debug: Log dos dados carregados (apenas uma vez quando dados mudam)
|
|
let ultimoTotalDados = $state<number>(-1);
|
|
$effect(() => {
|
|
const total = todasSolicitacoesQuery?.data?.length ?? 0;
|
|
if (total !== ultimoTotalDados && total > 0) {
|
|
ultimoTotalDados = total;
|
|
console.log('📦 [Backend] Dados carregados:', { total });
|
|
}
|
|
});
|
|
|
|
// Verificar erro de forma segura
|
|
const queryResult = $derived(todasSolicitacoesQuery);
|
|
const hasError = $derived(
|
|
queryResult !== undefined &&
|
|
queryResult !== null &&
|
|
typeof queryResult === 'object' &&
|
|
'error' in queryResult &&
|
|
(queryResult as { error?: unknown }).error !== undefined
|
|
);
|
|
const errorMessage = $derived(() => {
|
|
if (!hasError) return 'Erro desconhecido ao carregar dados';
|
|
const queryWithError = queryResult as { error?: unknown } | undefined;
|
|
if (!queryWithError?.error) return 'Erro desconhecido ao carregar dados';
|
|
if (queryWithError.error instanceof Error) {
|
|
return queryWithError.error.message;
|
|
}
|
|
if (typeof queryWithError.error === 'string') {
|
|
return queryWithError.error;
|
|
}
|
|
return 'Erro desconhecido ao carregar dados';
|
|
});
|
|
|
|
// Manter último valor válido para evitar dados desaparecendo
|
|
let ultimasSolicitacoesValidas = $state<TodasSolicitacoes>([]);
|
|
let ultimoDataHash = $state<string>('');
|
|
|
|
// Atualizar apenas quando temos dados válidos e quando realmente mudou
|
|
$effect(() => {
|
|
const dataAtual = todasSolicitacoesQuery?.data;
|
|
if (dataAtual && !hasError) {
|
|
// Criar hash simples para comparar se os dados realmente mudaram
|
|
const dataHash = JSON.stringify(dataAtual.map(d => d._id));
|
|
if (dataHash !== ultimoDataHash) {
|
|
ultimoDataHash = dataHash;
|
|
ultimasSolicitacoesValidas = dataAtual;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Usar último valor válido ou array vazio
|
|
const solicitacoes = $derived<TodasSolicitacoes>(
|
|
todasSolicitacoesQuery?.data ?? ultimasSolicitacoesValidas
|
|
);
|
|
|
|
// Filtros Dashboard
|
|
let filtroStatusDashboard = $state<string>('todos');
|
|
let filtroNomeDashboard = $state<string>('');
|
|
let filtroMatriculaDashboard = $state<string>('');
|
|
let filtroEmailDashboard = $state<string>('');
|
|
let filtroMesDashboard = $state<string>('');
|
|
let filtroPeriodoInicioDashboard = $state<string>('');
|
|
let filtroPeriodoFimDashboard = $state<string>('');
|
|
|
|
// Filtros Solicitações
|
|
let filtroStatusSolicitacoes = $state<string>('todos');
|
|
let filtroNomeSolicitacoes = $state<string>('');
|
|
let filtroMatriculaSolicitacoes = $state<string>('');
|
|
let filtroEmailSolicitacoes = $state<string>('');
|
|
let filtroMesSolicitacoes = $state<string>('');
|
|
let filtroPeriodoInicioSolicitacoes = $state<string>('');
|
|
let filtroPeriodoFimSolicitacoes = $state<string>('');
|
|
|
|
// Filtros Relatórios
|
|
let dataInicioRelatorio = $state<string>('');
|
|
let dataFimRelatorio = $state<string>('');
|
|
let filtroFuncionarioRelatorio = $state<string>('');
|
|
let filtroMatriculaRelatorio = $state<string>('');
|
|
let filtroStatusRelatorio = $state<string>('todos');
|
|
let filtroMesRelatorio = $state<string>('');
|
|
let gerandoRelatorio = $state(false);
|
|
|
|
// Função auxiliar para filtrar solicitações
|
|
function filtrarSolicitacoes(
|
|
lista: TodasSolicitacoes,
|
|
filtros: {
|
|
status: string;
|
|
nome: string;
|
|
matricula: string;
|
|
email: string;
|
|
mes: string;
|
|
periodoInicio: string;
|
|
periodoFim: string;
|
|
}
|
|
): TodasSolicitacoes {
|
|
return lista.filter((periodo) => {
|
|
if (filtros.status !== 'todos' && periodo.status !== filtros.status) {
|
|
return false;
|
|
}
|
|
|
|
const nomeFiltro = normalizarTexto(filtros.nome.trim());
|
|
const matriculaFiltro = normalizarTexto(filtros.matricula.trim());
|
|
const emailFiltro = normalizarTexto(filtros.email.trim());
|
|
|
|
if (nomeFiltro || matriculaFiltro || emailFiltro) {
|
|
const funcionario = periodo.funcionario;
|
|
if (!funcionario) return false;
|
|
|
|
const contato = funcionario as {
|
|
emailInstitucional?: string | null;
|
|
email?: string | null;
|
|
};
|
|
|
|
if (nomeFiltro) {
|
|
const nomeNormalizado = normalizarTexto(funcionario.nome ?? '');
|
|
if (!nomeNormalizado.includes(nomeFiltro)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (matriculaFiltro) {
|
|
const matriculaNormalizada = normalizarTexto(funcionario.matricula ?? '');
|
|
if (!matriculaNormalizada.includes(matriculaFiltro)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (emailFiltro) {
|
|
const emailNormalizado = normalizarTexto(
|
|
contato.emailInstitucional ?? contato.email ?? ''
|
|
);
|
|
if (!emailNormalizado.includes(emailFiltro)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
const aplicaMes = filtros.mes !== '';
|
|
const aplicaPeriodo = filtros.periodoInicio !== '' || filtros.periodoFim !== '';
|
|
|
|
if (!aplicaMes && !aplicaPeriodo) {
|
|
return true;
|
|
}
|
|
|
|
const intervaloMes = aplicaMes ? criarIntervaloDoMes(filtros.mes) : null;
|
|
const inicioFiltro = filtros.periodoInicio
|
|
? criarDataHora(filtros.periodoInicio, 'inicio')
|
|
: null;
|
|
const fimFiltro = filtros.periodoFim ? criarDataHora(filtros.periodoFim, 'fim') : null;
|
|
const inicioComparacao = inicioFiltro ?? new SvelteDate(-8640000000000000);
|
|
const fimComparacao = fimFiltro ?? new SvelteDate(8640000000000000);
|
|
|
|
const inicioPeriodo = criarDataHora(periodo.dataInicio, 'inicio');
|
|
const fimPeriodo = criarDataHora(periodo.dataFim, 'fim');
|
|
if (!inicioPeriodo || !fimPeriodo) {
|
|
return false;
|
|
}
|
|
|
|
if (intervaloMes) {
|
|
if (fimPeriodo < intervaloMes.inicio || inicioPeriodo > intervaloMes.fim) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (inicioFiltro || fimFiltro) {
|
|
if (fimPeriodo < inicioComparacao || inicioPeriodo > fimComparacao) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Filtros para Dashboard
|
|
const solicitacoesFiltradasDashboard = $derived(
|
|
filtrarSolicitacoes(solicitacoes, {
|
|
status: filtroStatusDashboard,
|
|
nome: filtroNomeDashboard,
|
|
matricula: filtroMatriculaDashboard,
|
|
email: filtroEmailDashboard,
|
|
mes: filtroMesDashboard,
|
|
periodoInicio: filtroPeriodoInicioDashboard,
|
|
periodoFim: filtroPeriodoFimDashboard
|
|
})
|
|
);
|
|
|
|
// Filtros para Solicitações
|
|
const solicitacoesFiltradas = $derived(
|
|
filtrarSolicitacoes(solicitacoes, {
|
|
status: filtroStatusSolicitacoes,
|
|
nome: filtroNomeSolicitacoes,
|
|
matricula: filtroMatriculaSolicitacoes,
|
|
email: filtroEmailSolicitacoes,
|
|
mes: filtroMesSolicitacoes,
|
|
periodoInicio: filtroPeriodoInicioSolicitacoes,
|
|
periodoFim: filtroPeriodoFimSolicitacoes
|
|
})
|
|
);
|
|
|
|
// Estatísticas gerais
|
|
const stats = $derived({
|
|
total: solicitacoes.length,
|
|
aguardando: solicitacoes.filter((solicitacao) => solicitacao.status === 'aguardando_aprovacao')
|
|
.length,
|
|
aprovadas: solicitacoes.filter(
|
|
(solicitacao) =>
|
|
solicitacao.status === 'aprovado' || solicitacao.status === 'data_ajustada_aprovada'
|
|
).length,
|
|
reprovadas: solicitacoes.filter((solicitacao) => solicitacao.status === 'reprovado').length
|
|
});
|
|
|
|
const solicitacoesAprovadas = $derived(
|
|
solicitacoesFiltradas.filter(
|
|
(p) =>
|
|
p.status === 'aprovado' || p.status === 'data_ajustada_aprovada' || p.status === 'EmFérias'
|
|
)
|
|
);
|
|
|
|
const periodosDetalhados = $derived<Array<PeriodoDetalhado>>(
|
|
solicitacoesAprovadas
|
|
.map((periodo) => ({
|
|
feriasId: periodo._id,
|
|
funcionarioId: periodo.funcionarioId,
|
|
anoReferencia: periodo.anoReferencia,
|
|
funcionarioNome: periodo.funcionario?.nome ?? 'Funcionário não encontrado',
|
|
matricula: periodo.funcionario?.matricula ?? null,
|
|
gestorNome: periodo.gestor?.nome ?? null,
|
|
timeCor: periodo.time?.cor ?? null,
|
|
status: periodo.status,
|
|
dataInicio: periodo.dataInicio,
|
|
dataFim: periodo.dataFim,
|
|
diasCorridos: periodo.diasFerias
|
|
}))
|
|
.sort(
|
|
(a, b) => new SvelteDate(a.dataInicio).getTime() - new SvelteDate(b.dataInicio).getTime()
|
|
)
|
|
);
|
|
|
|
type PeriodoPorMes = { label: string; totalDias: number; quantidadePeriodos: number };
|
|
|
|
const periodosPorMes = $derived<Array<PeriodoPorMes>>(
|
|
(() => {
|
|
const agregados = new SvelteMap<string, PeriodoPorMes>();
|
|
|
|
for (const periodo of periodosDetalhados) {
|
|
const inicio = new SvelteDate(`${periodo.dataInicio}T00:00:00`);
|
|
const chave = `${inicio.getFullYear()}-${String(inicio.getMonth() + 1).padStart(2, '0')}`;
|
|
const label = inicio.toLocaleDateString('pt-BR', {
|
|
month: 'short',
|
|
year: 'numeric'
|
|
});
|
|
|
|
const existente = agregados.get(chave) ?? {
|
|
label,
|
|
totalDias: 0,
|
|
quantidadePeriodos: 0
|
|
};
|
|
|
|
existente.totalDias += periodo.diasCorridos;
|
|
existente.quantidadePeriodos += 1;
|
|
agregados.set(chave, existente);
|
|
}
|
|
|
|
return Array.from(agregados.entries())
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([, valor]) => valor);
|
|
})()
|
|
);
|
|
|
|
let rangeInicioIndice = $state(0);
|
|
let rangeFimIndice = $state(0);
|
|
let ultimoTamanhoPeriodos = $state(0);
|
|
let atualizandoRanges = $state(false);
|
|
|
|
$effect(() => {
|
|
// Prevenir loops infinitos
|
|
if (atualizandoRanges) {
|
|
return;
|
|
}
|
|
|
|
const tamanhoAtual = periodosPorMes.length;
|
|
|
|
// Só atualizar se o tamanho mudou
|
|
if (tamanhoAtual === 0) {
|
|
if (rangeInicioIndice !== 0 || rangeFimIndice !== 0) {
|
|
atualizandoRanges = true;
|
|
rangeInicioIndice = 0;
|
|
rangeFimIndice = 0;
|
|
ultimoTamanhoPeriodos = 0;
|
|
atualizandoRanges = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Se o tamanho mudou, resetar os ranges
|
|
if (tamanhoAtual !== ultimoTamanhoPeriodos) {
|
|
atualizandoRanges = true;
|
|
ultimoTamanhoPeriodos = tamanhoAtual;
|
|
const ultimoIndice = tamanhoAtual - 1;
|
|
|
|
if (rangeFimIndice === 0 && rangeInicioIndice === 0) {
|
|
rangeFimIndice = ultimoIndice;
|
|
atualizandoRanges = false;
|
|
return;
|
|
}
|
|
atualizandoRanges = false;
|
|
}
|
|
|
|
const ultimoIndice = tamanhoAtual - 1;
|
|
let precisaAtualizar = false;
|
|
let novoInicio = rangeInicioIndice;
|
|
let novoFim = rangeFimIndice;
|
|
|
|
if (novoInicio > ultimoIndice) {
|
|
novoInicio = ultimoIndice;
|
|
precisaAtualizar = true;
|
|
}
|
|
|
|
if (novoFim > ultimoIndice) {
|
|
novoFim = ultimoIndice;
|
|
precisaAtualizar = true;
|
|
}
|
|
|
|
if (novoInicio > novoFim) {
|
|
novoInicio = novoFim;
|
|
precisaAtualizar = true;
|
|
}
|
|
|
|
// Só atualizar se realmente precisar e se os valores mudaram
|
|
if (precisaAtualizar && (novoInicio !== rangeInicioIndice || novoFim !== rangeFimIndice)) {
|
|
atualizandoRanges = true;
|
|
rangeInicioIndice = novoInicio;
|
|
rangeFimIndice = novoFim;
|
|
atualizandoRanges = false;
|
|
}
|
|
});
|
|
|
|
const periodosPorMesAtivos = $derived<Array<PeriodoPorMes>>(
|
|
(() => {
|
|
if (periodosPorMes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const ultimoIndice = periodosPorMes.length - 1;
|
|
const inicio = Math.max(0, Math.min(rangeInicioIndice, ultimoIndice));
|
|
const fim = Math.max(inicio, Math.min(rangeFimIndice, ultimoIndice));
|
|
|
|
return periodosPorMes.slice(inicio, fim + 1);
|
|
})()
|
|
);
|
|
|
|
const solicitacoesPorAno = $derived<Array<SolicitacoesPorAnoResumo>>(
|
|
(() => {
|
|
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
|
|
|
|
for (const periodo of solicitacoesAprovadas) {
|
|
// solicitacoesAprovadas são períodos individuais, não agrupados
|
|
const existente = agregados.get(periodo.anoReferencia) ?? {
|
|
ano: periodo.anoReferencia,
|
|
solicitacoes: 0,
|
|
diasTotais: 0
|
|
};
|
|
existente.solicitacoes += 1;
|
|
existente.diasTotais += periodo.diasFerias;
|
|
agregados.set(periodo.anoReferencia, existente);
|
|
}
|
|
|
|
return Array.from(agregados.values()).sort((a, b) => a.ano - b.ano);
|
|
})()
|
|
);
|
|
|
|
// Dados para gráfico de barras 3D - Dias por Mês
|
|
const chartDataMes = $derived(() => {
|
|
if (periodosPorMesAtivos.length === 0) {
|
|
return {
|
|
labels: [],
|
|
datasets: []
|
|
};
|
|
}
|
|
|
|
const cores = [
|
|
'#3b82f6', // Azul
|
|
'#10b981', // Verde
|
|
'#f59e0b', // Amarelo
|
|
'#ef4444', // Vermelho
|
|
'#8b5cf6', // Roxo
|
|
'#06b6d4', // Ciano
|
|
'#f97316', // Laranja
|
|
'#ec4899' // Rosa
|
|
];
|
|
|
|
// Cor principal para a legenda (azul primary)
|
|
const corPrincipal = '#3b82f6';
|
|
|
|
return {
|
|
labels: periodosPorMesAtivos.map((p) => p.label),
|
|
datasets: [
|
|
{
|
|
label: 'Dias de Férias',
|
|
data: periodosPorMesAtivos.map((p) => p.totalDias),
|
|
backgroundColor: periodosPorMesAtivos.map((_, index) => cores[index % cores.length]),
|
|
borderColor: periodosPorMesAtivos.map((_, index) => cores[index % cores.length]),
|
|
// Cor representativa para a legenda (primeira cor da paleta)
|
|
legendColor: corPrincipal,
|
|
borderWidth: 2
|
|
}
|
|
]
|
|
};
|
|
});
|
|
|
|
// Dados para gráfico de barras 3D - Dias por Ano
|
|
const chartDataAno = $derived(() => {
|
|
if (solicitacoesPorAno.length === 0) {
|
|
return {
|
|
labels: [],
|
|
datasets: []
|
|
};
|
|
}
|
|
|
|
const cores = [
|
|
'#10b981', // Verde
|
|
'#3b82f6', // Azul
|
|
'#f59e0b', // Amarelo
|
|
'#ef4444', // Vermelho
|
|
'#8b5cf6', // Roxo
|
|
'#06b6d4', // Ciano
|
|
'#f97316', // Laranja
|
|
'#ec4899' // Rosa
|
|
];
|
|
|
|
// Cores representativas para a legenda
|
|
const corDias = '#10b981'; // Verde para dias
|
|
const corSolicitacoes = lightenColor('#10b981', 25); // Verde claro para solicitações
|
|
|
|
return {
|
|
labels: solicitacoesPorAno.map((s) => String(s.ano)),
|
|
datasets: [
|
|
{
|
|
label: 'Dias Totais Aprovados',
|
|
data: solicitacoesPorAno.map((s) => s.diasTotais),
|
|
backgroundColor: solicitacoesPorAno.map((_, index) => cores[index % cores.length]),
|
|
borderColor: solicitacoesPorAno.map((_, index) => cores[index % cores.length]),
|
|
// Cor representativa para a legenda (verde)
|
|
legendColor: corDias,
|
|
borderWidth: 2
|
|
},
|
|
{
|
|
label: 'Número de Solicitações',
|
|
data: solicitacoesPorAno.map((s) => s.solicitacoes),
|
|
backgroundColor: solicitacoesPorAno.map((_, index) =>
|
|
lightenColor(cores[index % cores.length], 20)
|
|
),
|
|
borderColor: solicitacoesPorAno.map((_, index) => cores[index % cores.length]),
|
|
// Cor representativa para a legenda (verde claro)
|
|
legendColor: corSolicitacoes,
|
|
borderWidth: 2
|
|
}
|
|
]
|
|
};
|
|
});
|
|
|
|
// Função auxiliar para clarear cores
|
|
function lightenColor(color: string, percent: number): string {
|
|
const num = parseInt(color.replace('#', ''), 16);
|
|
const amt = Math.round(2.55 * percent);
|
|
const R = Math.min(255, (num >> 16) + amt);
|
|
const G = Math.min(255, ((num >> 8) & 0x00ff) + amt);
|
|
const B = Math.min(255, (num & 0x0000ff) + amt);
|
|
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
|
|
}
|
|
|
|
const coresCalendario = [
|
|
'#2563eb',
|
|
'#16a34a',
|
|
'#f97316',
|
|
'#9333ea',
|
|
'#0ea5e9',
|
|
'#facc15',
|
|
'#f43f5e'
|
|
] as const;
|
|
|
|
const eventosFerias = $derived<Array<EventInput>>(
|
|
periodosDetalhados.map((periodo, indice) => {
|
|
const corBase = periodo.timeCor ?? coresCalendario[indice % coresCalendario.length];
|
|
return {
|
|
id: `${String(periodo.feriasId)}-${indice}`,
|
|
title: `${periodo.funcionarioNome} (${periodo.diasCorridos} dia${periodo.diasCorridos === 1 ? '' : 's'})`,
|
|
start: periodo.dataInicio,
|
|
end: adicionarDias(periodo.dataFim, 1),
|
|
allDay: true,
|
|
backgroundColor: corBase,
|
|
borderColor: corBase,
|
|
extendedProps: {
|
|
funcionarioId: periodo.funcionarioId,
|
|
timeNome: periodo.timeNome,
|
|
diasCorridos: periodo.diasCorridos,
|
|
anoReferencia: periodo.anoReferencia,
|
|
status: periodo.status
|
|
}
|
|
} satisfies EventInput;
|
|
})
|
|
);
|
|
|
|
// Debug: Log dos eventos gerados (apenas quando mudar significativamente)
|
|
let ultimoTotalEventos = $state<number>(-1);
|
|
$effect(() => {
|
|
const totalEventos = eventosFerias.length;
|
|
if (totalEventos !== ultimoTotalEventos && import.meta.env.DEV) {
|
|
ultimoTotalEventos = totalEventos;
|
|
console.log('📅 [Eventos] Total:', totalEventos);
|
|
}
|
|
});
|
|
|
|
let calendarioContainer: HTMLDivElement | null = null;
|
|
let calendarioInstance: CalendarInstance | null = null;
|
|
let CalendarClass: CalendarConstructor | null = null;
|
|
let calendarioInicializado = $state(false);
|
|
|
|
// Função para inicializar o calendário
|
|
async function inicializarCalendario() {
|
|
if (
|
|
!calendarioContainer ||
|
|
calendarioInstance ||
|
|
calendarioInicializado ||
|
|
isLoading ||
|
|
hasError ||
|
|
abaAtiva !== 'dashboard'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (import.meta.env.DEV) {
|
|
console.log('🔄 [Calendário] Iniciando inicialização...');
|
|
}
|
|
|
|
const [coreModule, dayGridModule, interactionModule, localeModule] = await Promise.all([
|
|
import('@fullcalendar/core'),
|
|
import('@fullcalendar/daygrid'),
|
|
import('@fullcalendar/interaction'),
|
|
import('@fullcalendar/core/locales/pt-br')
|
|
]);
|
|
|
|
// Verificar novamente após os imports
|
|
if (
|
|
!calendarioContainer ||
|
|
isLoading ||
|
|
hasError ||
|
|
calendarioInstance ||
|
|
calendarioInicializado ||
|
|
abaAtiva !== 'dashboard'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
CalendarClass = coreModule.Calendar;
|
|
const dayGridPlugin = dayGridModule.default;
|
|
const interactionPlugin = interactionModule.default;
|
|
const ptBrLocale = localeModule.default;
|
|
|
|
calendarioInstance = new CalendarClass(calendarioContainer, {
|
|
plugins: [dayGridPlugin, interactionPlugin],
|
|
initialView: 'dayGridMonth',
|
|
height: 'auto',
|
|
locale: 'pt-br',
|
|
locales: [ptBrLocale],
|
|
navLinks: true,
|
|
weekNumbers: false,
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,dayGridWeek'
|
|
},
|
|
buttonText: {
|
|
today: 'Hoje',
|
|
month: 'Mês',
|
|
week: 'Semana'
|
|
},
|
|
events: eventosFerias
|
|
});
|
|
|
|
calendarioInstance.render();
|
|
calendarioInicializado = true;
|
|
if (import.meta.env.DEV) {
|
|
console.log('✅ [Calendário] Inicializado com sucesso!');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ [Calendário] Erro ao inicializar:', error);
|
|
if (error instanceof Error) {
|
|
console.error('❌ [Calendário] Mensagem de erro:', error.message);
|
|
console.error('❌ [Calendário] Stack:', error.stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Monitorar mudanças de aba e destruir/reinicializar calendário
|
|
$effect(() => {
|
|
const abaAtual = abaAtiva;
|
|
|
|
// Se não estiver na aba dashboard, destruir o calendário se existir
|
|
if (abaAtual !== 'dashboard') {
|
|
if (calendarioInstance) {
|
|
try {
|
|
calendarioInstance.destroy();
|
|
calendarioInstance = null;
|
|
calendarioInicializado = false;
|
|
} catch (error) {
|
|
console.error('❌ [Calendário] Erro ao destruir:', error);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
// Monitorar quando o container está disponível e os dados estão prontos
|
|
$effect(() => {
|
|
// Verificar se a aba dashboard está ativa
|
|
if (abaAtiva !== 'dashboard') {
|
|
return;
|
|
}
|
|
|
|
// Verificar se não está mais carregando e não há erro
|
|
const loading = isLoading;
|
|
const error = hasError;
|
|
const container = calendarioContainer;
|
|
|
|
// Se estiver carregando ou houver erro, não fazer nada
|
|
if (loading || error) {
|
|
return;
|
|
}
|
|
|
|
// Se não tiver container, não fazer nada
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
// Se já estiver inicializado, não fazer nada
|
|
if (calendarioInicializado || calendarioInstance) {
|
|
return;
|
|
}
|
|
|
|
// Aguardar o DOM estar pronto e inicializar
|
|
(async () => {
|
|
await tick();
|
|
|
|
// Verificar novamente após o tick
|
|
if (
|
|
!calendarioContainer ||
|
|
isLoading ||
|
|
hasError ||
|
|
calendarioInstance ||
|
|
calendarioInicializado ||
|
|
abaAtiva !== 'dashboard'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Aguardar um pouco mais para garantir que o elemento está completamente renderizado
|
|
setTimeout(() => {
|
|
// Verificar novamente antes de inicializar
|
|
if (
|
|
!calendarioContainer ||
|
|
isLoading ||
|
|
hasError ||
|
|
calendarioInstance ||
|
|
calendarioInicializado ||
|
|
abaAtiva !== 'dashboard'
|
|
) {
|
|
return;
|
|
}
|
|
inicializarCalendario().catch((error) => {
|
|
console.error('❌ [Calendário] Erro ao inicializar:', error);
|
|
});
|
|
}, 300);
|
|
})();
|
|
});
|
|
|
|
// Cleanup ao desmontar o componente
|
|
onMount(() => {
|
|
return () => {
|
|
if (calendarioInstance) {
|
|
try {
|
|
calendarioInstance.destroy();
|
|
} catch (error) {
|
|
console.error('❌ [Calendário] Erro ao destruir no unmount:', error);
|
|
}
|
|
calendarioInstance = null;
|
|
calendarioInicializado = false;
|
|
}
|
|
};
|
|
});
|
|
|
|
// Sincronizar eventos do calendário quando mudarem (com debounce)
|
|
let ultimosEventosSerializados = $state<string>('');
|
|
let timeoutAtualizacaoCalendario = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
$effect(() => {
|
|
// Não atualizar se não estiver na aba dashboard
|
|
if (abaAtiva !== 'dashboard') {
|
|
return;
|
|
}
|
|
|
|
// Não atualizar se o calendário não estiver inicializado ou estiver carregando
|
|
if (!calendarioInstance || !calendarioInicializado || isLoading || hasError) {
|
|
return;
|
|
}
|
|
|
|
// Serializar eventos para comparar mudanças
|
|
const eventosSerializados = JSON.stringify(eventosFerias.map(e => ({
|
|
id: e.id,
|
|
start: e.start,
|
|
end: e.end,
|
|
title: e.title
|
|
})));
|
|
|
|
// Se os eventos não mudaram, não fazer nada
|
|
if (eventosSerializados === ultimosEventosSerializados) {
|
|
return;
|
|
}
|
|
|
|
// Limpar timeout anterior se existir
|
|
if (timeoutAtualizacaoCalendario) {
|
|
clearTimeout(timeoutAtualizacaoCalendario);
|
|
}
|
|
|
|
// Atualizar referência serializada imediatamente para evitar re-execuções
|
|
ultimosEventosSerializados = eventosSerializados;
|
|
|
|
// Debounce para evitar atualizações muito frequentes
|
|
timeoutAtualizacaoCalendario = setTimeout(() => {
|
|
try {
|
|
// Verificar novamente se o calendário ainda está válido e na aba correta
|
|
if (!calendarioInstance || !calendarioInicializado || abaAtiva !== 'dashboard') {
|
|
return;
|
|
}
|
|
|
|
// Remover todos os eventos existentes
|
|
calendarioInstance.removeAllEvents();
|
|
|
|
// Adicionar novos eventos
|
|
const eventosClonados = eventosFerias.map((evento) => ({
|
|
...evento,
|
|
extendedProps: { ...evento.extendedProps }
|
|
}));
|
|
|
|
// Adicionar eventos em lote
|
|
for (const evento of eventosClonados) {
|
|
calendarioInstance.addEvent(evento);
|
|
}
|
|
} catch (error) {
|
|
// Log do erro, mas não interromper o fluxo
|
|
if (error instanceof Error) {
|
|
console.error('❌ Erro ao atualizar eventos do calendário:', error.message);
|
|
} else {
|
|
console.error('❌ Erro ao atualizar eventos do calendário:', error);
|
|
}
|
|
}
|
|
}, 300); // Debounce de 300ms
|
|
|
|
// Cleanup
|
|
return () => {
|
|
if (timeoutAtualizacaoCalendario) {
|
|
clearTimeout(timeoutAtualizacaoCalendario);
|
|
}
|
|
};
|
|
});
|
|
|
|
// Atualizar variáveis no window apenas em desenvolvimento (desabilitado temporariamente para evitar loops)
|
|
// if (typeof window !== 'undefined' && import.meta.env.DEV) {
|
|
// // Código comentado para evitar loops infinitos
|
|
// }
|
|
function getStatusBadge(status: string) {
|
|
const badges: Record<string, string> = {
|
|
aguardando_aprovacao: 'badge-warning',
|
|
aprovado: 'badge-success',
|
|
reprovado: 'badge-error',
|
|
data_ajustada_aprovada: 'badge-info',
|
|
Cancelado_RH: 'badge-error'
|
|
};
|
|
return badges[status] || 'badge-neutral';
|
|
}
|
|
|
|
function getStatusTexto(status: string) {
|
|
const textos: Record<string, string> = {
|
|
aguardando_aprovacao: 'Aguardando',
|
|
aprovado: 'Aprovado',
|
|
reprovado: 'Reprovado',
|
|
data_ajustada_aprovada: 'Ajustado',
|
|
Cancelado_RH: 'Cancelado RH'
|
|
};
|
|
return textos[status] || status;
|
|
}
|
|
|
|
// Função para formatar data sem problemas de timezone
|
|
function formatarDataString(dataString: string): string {
|
|
if (!dataString) return '';
|
|
// Dividir a string da data (formato YYYY-MM-DD)
|
|
const partes = dataString.split('-');
|
|
if (partes.length !== 3) return dataString;
|
|
// Retornar no formato DD/MM/YYYY
|
|
return `${partes[2]}/${partes[1]}/${partes[0]}`;
|
|
}
|
|
|
|
function formatarData(data: string | number | Date) {
|
|
const instancia = data instanceof Date ? data : new SvelteDate(data);
|
|
return instancia.toLocaleDateString('pt-BR');
|
|
}
|
|
|
|
function criarDataHora(dataISO: string | undefined, modo: 'inicio' | 'fim'): Date | null {
|
|
if (!dataISO) return null;
|
|
const data = new SvelteDate(`${dataISO}T00:00:00`);
|
|
if (Number.isNaN(data.getTime())) {
|
|
return null;
|
|
}
|
|
if (modo === 'fim') {
|
|
data.setHours(23, 59, 59, 999);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function criarIntervaloDoMes(valor: string): { inicio: Date; fim: Date } | null {
|
|
if (!valor || valor === 'todos') return null;
|
|
const [anoStr, mesStr] = valor.split('-');
|
|
const ano = Number(anoStr);
|
|
const mes = Number(mesStr) - 1;
|
|
if (Number.isNaN(ano) || Number.isNaN(mes)) return null;
|
|
const inicio = new SvelteDate(ano, mes, 1);
|
|
const fim = new SvelteDate(ano, mes + 1, 0, 23, 59, 59, 999);
|
|
return { inicio, fim };
|
|
}
|
|
|
|
function adicionarDias(dataISO: string, dias: number): string {
|
|
const data = new SvelteDate(`${dataISO}T00:00:00`);
|
|
if (Number.isNaN(data.getTime())) {
|
|
return dataISO;
|
|
}
|
|
data.setDate(data.getDate() + dias);
|
|
const ano = data.getUTCFullYear();
|
|
const mes = String(data.getUTCMonth() + 1).padStart(2, '0');
|
|
const dia = String(data.getUTCDate()).padStart(2, '0');
|
|
return `${ano}-${mes}-${dia}`;
|
|
}
|
|
|
|
function normalizarTexto(texto: string): string {
|
|
return texto
|
|
.trim()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.toLowerCase();
|
|
}
|
|
|
|
function limparTodosFiltrosDashboard() {
|
|
filtroStatusDashboard = 'todos';
|
|
filtroNomeDashboard = '';
|
|
filtroMatriculaDashboard = '';
|
|
filtroEmailDashboard = '';
|
|
filtroMesDashboard = '';
|
|
filtroPeriodoInicioDashboard = '';
|
|
filtroPeriodoFimDashboard = '';
|
|
}
|
|
|
|
function limparTodosFiltrosSolicitacoes() {
|
|
filtroStatusSolicitacoes = 'todos';
|
|
filtroNomeSolicitacoes = '';
|
|
filtroMatriculaSolicitacoes = '';
|
|
filtroEmailSolicitacoes = '';
|
|
filtroMesSolicitacoes = '';
|
|
filtroPeriodoInicioSolicitacoes = '';
|
|
filtroPeriodoFimSolicitacoes = '';
|
|
}
|
|
|
|
function limparTodosFiltrosRelatorios() {
|
|
dataInicioRelatorio = '';
|
|
dataFimRelatorio = '';
|
|
filtroFuncionarioRelatorio = '';
|
|
filtroMatriculaRelatorio = '';
|
|
filtroStatusRelatorio = 'todos';
|
|
filtroMesRelatorio = '';
|
|
}
|
|
|
|
function handleRangeInicio(event: Event) {
|
|
const target = event.target as HTMLInputElement | null;
|
|
if (!target) return;
|
|
const valor = Number(target.value);
|
|
if (Number.isNaN(valor)) return;
|
|
rangeInicioIndice = Math.min(valor, rangeFimIndice);
|
|
}
|
|
|
|
function handleRangeFim(event: Event) {
|
|
const target = event.target as HTMLInputElement | null;
|
|
if (!target) return;
|
|
const valor = Number(target.value);
|
|
if (Number.isNaN(valor)) return;
|
|
rangeFimIndice = Math.max(valor, rangeInicioIndice);
|
|
}
|
|
|
|
function limparPeriodoPersonalizadoDashboard() {
|
|
filtroPeriodoInicioDashboard = '';
|
|
filtroPeriodoFimDashboard = '';
|
|
}
|
|
|
|
function limparPeriodoPersonalizadoSolicitacoes() {
|
|
filtroPeriodoInicioSolicitacoes = '';
|
|
filtroPeriodoFimSolicitacoes = '';
|
|
}
|
|
|
|
async function selecionarPeriodo(feriasId: Id<'ferias'>) {
|
|
periodoSelecionado = feriasId;
|
|
}
|
|
|
|
async function recarregar() {
|
|
periodoSelecionado = null;
|
|
}
|
|
|
|
let chartContainer: HTMLDivElement | null = null;
|
|
|
|
function converteParaData(dateStr: string): Date {
|
|
return new SvelteDate(`${dateStr}T00:00:00`);
|
|
}
|
|
|
|
function periodosNoIntervalo(inicio: Date, fim: Date): Array<PeriodoDetalhado> {
|
|
if (Number.isNaN(inicio.getTime()) || Number.isNaN(fim.getTime())) return [];
|
|
|
|
return periodosDetalhados
|
|
.filter((periodo) => {
|
|
const inicioPeriodo = new SvelteDate(periodo.dataInicio);
|
|
const fimPeriodo = new SvelteDate(periodo.dataFim);
|
|
return inicioPeriodo <= fim && fimPeriodo >= inicio;
|
|
})
|
|
.sort(
|
|
(a, b) =>
|
|
new SvelteDate(a.dataInicio).getTime() - new SvelteDate(b.dataInicio).getTime() ||
|
|
a.funcionarioNome.localeCompare(b.funcionarioNome, 'pt-BR')
|
|
);
|
|
}
|
|
|
|
// Função para obter dados do relatório aplicando todos os filtros
|
|
function obterDadosRelatorioFerias(): Array<PeriodoDetalhado> {
|
|
let inicio: Date;
|
|
let fim: Date;
|
|
let periodosSelecionados: Array<PeriodoDetalhado>;
|
|
|
|
// Se houver mês de referência selecionado, usar o intervalo do mês
|
|
if (filtroMesRelatorio !== '') {
|
|
const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio);
|
|
if (!intervaloMes) {
|
|
return [];
|
|
}
|
|
inicio = intervaloMes.inicio;
|
|
fim = intervaloMes.fim;
|
|
periodosSelecionados = periodosNoIntervalo(inicio, fim);
|
|
} else if (dataInicioRelatorio && dataFimRelatorio) {
|
|
// Se não houver mês, usar o período informado
|
|
inicio = converteParaData(dataInicioRelatorio);
|
|
fim = converteParaData(dataFimRelatorio);
|
|
|
|
if (fim < inicio) {
|
|
return [];
|
|
}
|
|
|
|
periodosSelecionados = periodosNoIntervalo(inicio, fim);
|
|
} else {
|
|
return [];
|
|
}
|
|
|
|
// Aplicar filtros adicionais
|
|
if (filtroFuncionarioRelatorio.trim() !== '') {
|
|
const nomeFiltro = normalizarTexto(filtroFuncionarioRelatorio.trim());
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
const nomeNormalizado = normalizarTexto(periodo.funcionarioNome);
|
|
return nomeNormalizado.includes(nomeFiltro);
|
|
});
|
|
}
|
|
|
|
if (filtroMatriculaRelatorio.trim() !== '') {
|
|
const matriculaFiltro = normalizarTexto(filtroMatriculaRelatorio.trim());
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
const matriculaNormalizada = normalizarTexto(periodo.matricula ?? '');
|
|
return matriculaNormalizada.includes(matriculaFiltro);
|
|
});
|
|
}
|
|
|
|
if (filtroStatusRelatorio !== 'todos') {
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
return periodo.status === filtroStatusRelatorio;
|
|
});
|
|
}
|
|
|
|
return periodosSelecionados;
|
|
}
|
|
|
|
// Função para gerar PDF
|
|
async function gerarPDFFerias() {
|
|
const periodosSelecionados = obterDadosRelatorioFerias();
|
|
|
|
if (periodosSelecionados.length === 0) {
|
|
toast.error('Não há férias registradas para os filtros selecionados.');
|
|
return;
|
|
}
|
|
|
|
gerandoRelatorio = true;
|
|
try {
|
|
const doc = new jsPDF();
|
|
|
|
// Logo
|
|
let yPosition = 20;
|
|
try {
|
|
const logoImg = new Image();
|
|
logoImg.src = logoGovPE;
|
|
await new Promise<void>((resolve, reject) => {
|
|
logoImg.onload = () => resolve();
|
|
logoImg.onerror = () => reject();
|
|
setTimeout(() => reject(), 3000);
|
|
});
|
|
|
|
const logoWidth = 30;
|
|
const aspectRatio = logoImg.height / logoImg.width;
|
|
const logoHeight = logoWidth * aspectRatio;
|
|
|
|
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
|
|
yPosition = Math.max(25, 10 + logoHeight / 2);
|
|
} catch (err) {
|
|
console.warn('Não foi possível carregar a logo:', err);
|
|
}
|
|
|
|
// Título
|
|
doc.setFontSize(18);
|
|
doc.setTextColor(41, 128, 185);
|
|
doc.text('PROGRAMAÇÃO DE FÉRIAS', 105, yPosition, { align: 'center' });
|
|
|
|
yPosition += 10;
|
|
|
|
// Período
|
|
let periodoTexto = '';
|
|
if (filtroMesRelatorio !== '') {
|
|
const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio);
|
|
if (intervaloMes) {
|
|
periodoTexto = `Período: ${format(intervaloMes.inicio, 'dd/MM/yyyy', { locale: ptBR })} até ${format(intervaloMes.fim, 'dd/MM/yyyy', { locale: ptBR })}`;
|
|
}
|
|
} else if (dataInicioRelatorio && dataFimRelatorio) {
|
|
periodoTexto = `Período: ${format(new Date(dataInicioRelatorio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(dataFimRelatorio), 'dd/MM/yyyy', { locale: ptBR })}`;
|
|
}
|
|
|
|
if (periodoTexto) {
|
|
doc.setFontSize(11);
|
|
doc.setTextColor(0, 0, 0);
|
|
doc.text(periodoTexto, 105, yPosition, { align: 'center' });
|
|
yPosition += 8;
|
|
}
|
|
|
|
// Filtros aplicados
|
|
doc.setFontSize(9);
|
|
doc.setTextColor(100, 100, 100);
|
|
let filtrosTexto = 'Filtros: ';
|
|
const filtros = [];
|
|
if (filtroFuncionarioRelatorio) filtros.push(`Nome: ${filtroFuncionarioRelatorio}`);
|
|
if (filtroMatriculaRelatorio) filtros.push(`Matrícula: ${filtroMatriculaRelatorio}`);
|
|
if (filtroStatusRelatorio !== 'todos') filtros.push(`Status: ${getStatusTexto(filtroStatusRelatorio)}`);
|
|
if (filtroMesRelatorio) {
|
|
const [ano, mes] = filtroMesRelatorio.split('-');
|
|
const mesNome = new Date(Number(ano), Number(mes) - 1).toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
|
|
filtros.push(`Mês: ${mesNome}`);
|
|
}
|
|
filtrosTexto += filtros.length > 0 ? filtros.join('; ') : 'Nenhum filtro adicional';
|
|
doc.text(filtrosTexto, 105, yPosition, { align: 'center', maxWidth: 180 });
|
|
yPosition += 10;
|
|
|
|
// Data de geração
|
|
doc.setFontSize(9);
|
|
doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 15, yPosition);
|
|
yPosition += 12;
|
|
|
|
// Preparar dados para tabela
|
|
const dadosTabela: string[][] = periodosSelecionados.map((periodo) => [
|
|
periodo.funcionarioNome,
|
|
periodo.matricula ?? 'S/N',
|
|
periodo.gestorNome ?? 'Sem gestor',
|
|
periodo.anoReferencia.toString(),
|
|
formatarData(periodo.dataInicio),
|
|
formatarData(periodo.dataFim),
|
|
periodo.diasCorridos.toString(),
|
|
getStatusTexto(periodo.status)
|
|
]);
|
|
|
|
// Tabela
|
|
if (dadosTabela.length > 0) {
|
|
autoTable(doc, {
|
|
startY: yPosition,
|
|
head: [['Funcionário', 'Matrícula', 'Gestor', 'Ano Ref.', 'Início', 'Fim', 'Dias', 'Status']],
|
|
body: dadosTabela,
|
|
theme: 'striped',
|
|
headStyles: {
|
|
fillColor: [41, 128, 185],
|
|
textColor: [255, 255, 255],
|
|
fontStyle: 'bold',
|
|
fontSize: 7
|
|
},
|
|
styles: { fontSize: 7 },
|
|
columnStyles: {
|
|
0: { cellWidth: 40, fontSize: 7 }, // Funcionário
|
|
1: { cellWidth: 20, fontSize: 7 }, // Matrícula
|
|
2: { cellWidth: 25, fontSize: 7 }, // Gestor
|
|
3: { cellWidth: 15, fontSize: 7 }, // Ano Ref.
|
|
4: { cellWidth: 22, fontSize: 7 }, // Início
|
|
5: { cellWidth: 22, fontSize: 7 }, // Fim
|
|
6: { cellWidth: 12, fontSize: 7 }, // Dias
|
|
7: { cellWidth: 18, fontSize: 7 } // Status
|
|
},
|
|
margin: { top: yPosition, left: 10, right: 10 },
|
|
tableWidth: 'wrap'
|
|
});
|
|
|
|
// Rodapé
|
|
const pageCount = doc.getNumberOfPages();
|
|
for (let i = 1; i <= pageCount; i++) {
|
|
doc.setPage(i);
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(128, 128, 128);
|
|
doc.text(
|
|
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
|
|
doc.internal.pageSize.getWidth() / 2,
|
|
doc.internal.pageSize.getHeight() - 10,
|
|
{ align: 'center' }
|
|
);
|
|
}
|
|
} else {
|
|
doc.setFontSize(12);
|
|
doc.setTextColor(150, 150, 150);
|
|
doc.text('Nenhum registro encontrado para os filtros selecionados', 105, yPosition + 20, {
|
|
align: 'center'
|
|
});
|
|
}
|
|
|
|
// Salvar
|
|
const nomeArquivo = `programacao-ferias-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`;
|
|
doc.save(nomeArquivo);
|
|
toast.success('Relatório PDF gerado com sucesso!');
|
|
} catch (error) {
|
|
console.error('Erro ao gerar PDF:', error);
|
|
toast.error('Erro ao gerar relatório PDF. Tente novamente.');
|
|
} finally {
|
|
gerandoRelatorio = false;
|
|
}
|
|
}
|
|
|
|
// Função para gerar Excel
|
|
async function gerarExcelFerias() {
|
|
const periodosSelecionados = obterDadosRelatorioFerias();
|
|
|
|
if (periodosSelecionados.length === 0) {
|
|
toast.error('Não há férias registradas para os filtros selecionados.');
|
|
return;
|
|
}
|
|
|
|
gerandoRelatorio = true;
|
|
try {
|
|
// Preparar dados
|
|
const dados: Array<Record<string, string | number>> = periodosSelecionados.map((periodo) => ({
|
|
'Funcionário': periodo.funcionarioNome,
|
|
'Matrícula': periodo.matricula ?? 'S/N',
|
|
'Gestor': periodo.gestorNome ?? 'Sem gestor',
|
|
'Ano Ref.': periodo.anoReferencia,
|
|
'Data Início': formatarData(periodo.dataInicio),
|
|
'Data Fim': formatarData(periodo.dataFim),
|
|
'Dias': periodo.diasCorridos,
|
|
'Status': getStatusTexto(periodo.status)
|
|
}));
|
|
|
|
// Criar workbook com ExcelJS
|
|
const workbook = new ExcelJS.Workbook();
|
|
const worksheet = workbook.addWorksheet('Programação de Férias');
|
|
|
|
// Obter cabeçalhos
|
|
const headers = Object.keys(dados[0] || {});
|
|
|
|
// Carregar logo
|
|
let logoBuffer: ArrayBuffer | null = null;
|
|
try {
|
|
const response = await fetch(logoGovPE);
|
|
if (response.ok) {
|
|
logoBuffer = await response.arrayBuffer();
|
|
} else {
|
|
const logoImg = new Image();
|
|
logoImg.crossOrigin = 'anonymous';
|
|
logoImg.src = logoGovPE;
|
|
await new Promise<void>((resolve, reject) => {
|
|
logoImg.onload = () => resolve();
|
|
logoImg.onerror = () => reject();
|
|
setTimeout(() => reject(), 3000);
|
|
});
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = logoImg.width;
|
|
canvas.height = logoImg.height;
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx) {
|
|
ctx.drawImage(logoImg, 0, 0);
|
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
canvas.toBlob((blob) => {
|
|
if (blob) resolve(blob);
|
|
else reject(new Error('Falha ao converter imagem'));
|
|
}, 'image/png');
|
|
});
|
|
logoBuffer = await blob.arrayBuffer();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Não foi possível carregar a logo:', err);
|
|
}
|
|
|
|
// Linha 1: Cabeçalho com logo e título
|
|
worksheet.mergeCells('A1:B1');
|
|
const logoCell = worksheet.getCell('A1');
|
|
logoCell.alignment = { vertical: 'middle', horizontal: 'left' };
|
|
logoCell.border = {
|
|
right: { style: 'thin', color: { argb: 'FFE0E0E0' } }
|
|
};
|
|
|
|
// Adicionar logo se disponível
|
|
if (logoBuffer) {
|
|
const logoId = workbook.addImage({
|
|
buffer: new Uint8Array(logoBuffer),
|
|
extension: 'png'
|
|
});
|
|
worksheet.addImage(logoId, {
|
|
tl: { col: 0, row: 0 },
|
|
ext: { width: 140, height: 55 }
|
|
});
|
|
}
|
|
|
|
// Mesclar C1 até última coluna para título
|
|
const lastCol = String.fromCharCode(65 + headers.length - 1);
|
|
worksheet.mergeCells(`C1:${lastCol}1`);
|
|
const titleCell = worksheet.getCell('C1');
|
|
titleCell.value = 'PROGRAMAÇÃO DE FÉRIAS';
|
|
titleCell.font = { bold: true, size: 18, color: { argb: 'FF2980B9' } };
|
|
titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
|
|
|
|
// Ajustar altura da linha 1 para acomodar a logo
|
|
worksheet.getRow(1).height = 60;
|
|
|
|
// Linha 2: Cabeçalhos da tabela
|
|
headers.forEach((header, index) => {
|
|
const cell = worksheet.getCell(2, index + 1);
|
|
cell.value = header;
|
|
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } };
|
|
cell.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FF2980B9' }
|
|
};
|
|
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
cell.border = {
|
|
top: { style: 'thin', color: { argb: 'FF000000' } },
|
|
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
|
left: { style: 'thin', color: { argb: 'FF000000' } },
|
|
right: { style: 'thin', color: { argb: 'FF000000' } }
|
|
};
|
|
});
|
|
|
|
// Ajustar altura da linha 2
|
|
worksheet.getRow(2).height = 25;
|
|
|
|
// Linhas 3+: Dados
|
|
dados.forEach((rowData, rowIndex) => {
|
|
const row = rowIndex + 3;
|
|
headers.forEach((header, colIndex) => {
|
|
const cell = worksheet.getCell(row, colIndex + 1);
|
|
cell.value = rowData[header];
|
|
|
|
// Cor de fundo alternada (zebra striping)
|
|
const isEvenRow = rowIndex % 2 === 1;
|
|
cell.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: isEvenRow ? 'FFF8F9FA' : 'FFFFFFFF' }
|
|
};
|
|
|
|
// Alinhamento
|
|
let alignment: 'left' | 'center' | 'right' = 'left';
|
|
if (header === 'Dias' || header === 'Data Início' || header === 'Data Fim' || header === 'Status' || header === 'Ano Ref.') {
|
|
alignment = 'center';
|
|
}
|
|
cell.alignment = { vertical: 'middle', horizontal: alignment, wrapText: true };
|
|
|
|
// Fonte
|
|
cell.font = { size: 10, color: { argb: 'FF000000' } };
|
|
|
|
// Bordas
|
|
cell.border = {
|
|
top: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
|
bottom: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
|
left: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
|
right: { style: 'thin', color: { argb: 'FFE0E0E0' } }
|
|
};
|
|
|
|
// Formatação especial para Status
|
|
if (header === 'Status') {
|
|
const statusValue = rowData[header];
|
|
if (statusValue === 'Aprovado' || statusValue === 'Ajustado') {
|
|
cell.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FFD4EDDA' }
|
|
};
|
|
cell.font = { size: 10, color: { argb: 'FF155724' } };
|
|
} else if (statusValue === 'Reprovado' || statusValue === 'Cancelado RH') {
|
|
cell.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FFF8D7DA' }
|
|
};
|
|
cell.font = { size: 10, color: { argb: 'FF721C24' } };
|
|
} else if (statusValue === 'Aguardando') {
|
|
cell.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FFFFF3CD' }
|
|
};
|
|
cell.font = { size: 10, color: { argb: 'FF856404' } };
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Ajustar largura das colunas
|
|
worksheet.columns = [
|
|
{ width: 30 }, // Funcionário
|
|
{ width: 15 }, // Matrícula
|
|
{ width: 20 }, // Time
|
|
{ width: 12 }, // Ano Ref.
|
|
{ width: 12 }, // Data Início
|
|
{ width: 12 }, // Data Fim
|
|
{ width: 8 }, // Dias
|
|
{ width: 15 } // Status
|
|
];
|
|
|
|
// Congelar linha 2 (cabeçalho da tabela)
|
|
worksheet.views = [
|
|
{
|
|
state: 'frozen',
|
|
ySplit: 2,
|
|
topLeftCell: 'A3',
|
|
activeCell: 'A3'
|
|
}
|
|
];
|
|
|
|
// Gerar arquivo
|
|
const nomeArquivo = `programacao-ferias-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
|
|
const buffer = await workbook.xlsx.writeBuffer();
|
|
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = nomeArquivo;
|
|
link.click();
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
toast.success('Relatório Excel gerado com sucesso!');
|
|
} catch (error) {
|
|
console.error('Erro ao gerar Excel:', error);
|
|
toast.error('Erro ao gerar relatório Excel. Tente novamente.');
|
|
} finally {
|
|
gerandoRelatorio = false;
|
|
}
|
|
}
|
|
|
|
function gerarRelatorioImpressao() {
|
|
let inicio: Date;
|
|
let fim: Date;
|
|
let periodosSelecionados: Array<PeriodoDetalhado>;
|
|
|
|
// Se houver mês de referência selecionado, usar o intervalo do mês
|
|
if (filtroMesRelatorio !== '') {
|
|
const intervaloMes = criarIntervaloDoMes(filtroMesRelatorio);
|
|
if (!intervaloMes) {
|
|
window.alert('Mês de referência inválido.');
|
|
return;
|
|
}
|
|
inicio = intervaloMes.inicio;
|
|
fim = intervaloMes.fim;
|
|
periodosSelecionados = periodosNoIntervalo(inicio, fim);
|
|
} else if (dataInicioRelatorio && dataFimRelatorio) {
|
|
// Se não houver mês, usar o período informado
|
|
inicio = converteParaData(dataInicioRelatorio);
|
|
fim = converteParaData(dataFimRelatorio);
|
|
|
|
if (fim < inicio) {
|
|
window.alert('A data final não pode ser anterior à data inicial.');
|
|
return;
|
|
}
|
|
|
|
periodosSelecionados = periodosNoIntervalo(inicio, fim);
|
|
} else {
|
|
window.alert('Informe o período ou selecione um mês de referência para gerar a programação de férias.');
|
|
return;
|
|
}
|
|
|
|
// Aplicar filtros adicionais
|
|
if (filtroFuncionarioRelatorio.trim() !== '') {
|
|
const nomeFiltro = normalizarTexto(filtroFuncionarioRelatorio.trim());
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
const nomeNormalizado = normalizarTexto(periodo.funcionarioNome);
|
|
return nomeNormalizado.includes(nomeFiltro);
|
|
});
|
|
}
|
|
|
|
if (filtroMatriculaRelatorio.trim() !== '') {
|
|
const matriculaFiltro = normalizarTexto(filtroMatriculaRelatorio.trim());
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
const matriculaNormalizada = normalizarTexto(periodo.matricula ?? '');
|
|
return matriculaNormalizada.includes(matriculaFiltro);
|
|
});
|
|
}
|
|
|
|
if (filtroStatusRelatorio !== 'todos') {
|
|
periodosSelecionados = periodosSelecionados.filter((periodo) => {
|
|
return periodo.status === filtroStatusRelatorio;
|
|
});
|
|
}
|
|
|
|
// Nota: O filtro de mês de referência já foi aplicado no início da função quando usado como período principal
|
|
// Se o mês foi usado como período principal, não precisa filtrar novamente
|
|
|
|
if (periodosSelecionados.length === 0) {
|
|
window.alert('Não há férias registradas dentro do período informado.');
|
|
return;
|
|
}
|
|
|
|
const periodoLegivel = `${inicio.toLocaleDateString('pt-BR')} a ${fim.toLocaleDateString('pt-BR')}`;
|
|
const linhasTabela = periodosSelecionados
|
|
.map(
|
|
(periodo, index) => `
|
|
<tr>
|
|
<td>${index + 1}</td>
|
|
<td>${periodo.funcionarioNome}</td>
|
|
<td>${periodo.matricula ?? 'S/N'}</td>
|
|
<td>${periodo.gestorNome ?? 'Sem gestor'}</td>
|
|
<td>${periodo.anoReferencia}</td>
|
|
<td>${formatarData(periodo.dataInicio)}</td>
|
|
<td>${formatarData(periodo.dataFim)}</td>
|
|
<td>${periodo.diasCorridos}</td>
|
|
<td>${getStatusTexto(periodo.status)}</td>
|
|
</tr>`
|
|
)
|
|
.join('');
|
|
|
|
const totalDias = periodosSelecionados.reduce((acc, periodo) => acc + periodo.diasCorridos, 0);
|
|
const relatorioHTML = `
|
|
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Programação de Férias</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
padding: 32px;
|
|
background: #f5f7fb;
|
|
color: #1f2933;
|
|
}
|
|
h1 {
|
|
margin-bottom: 4px;
|
|
}
|
|
h2 {
|
|
margin-top: 24px;
|
|
margin-bottom: 8px;
|
|
font-size: 1rem;
|
|
color: #52606d;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 16px;
|
|
background: #ffffff;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.08);
|
|
}
|
|
th, td {
|
|
padding: 12px 16px;
|
|
font-size: 0.95rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
text-align: left;
|
|
}
|
|
th {
|
|
background: #eef2ff;
|
|
color: #1d4ed8;
|
|
text-transform: uppercase;
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
tfoot td {
|
|
font-weight: 600;
|
|
background: #f9fafb;
|
|
}
|
|
.meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
.meta-item {
|
|
padding: 12px 18px;
|
|
background: #e0f2fe;
|
|
border-radius: 999px;
|
|
color: #0369a1;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
@media print {
|
|
body {
|
|
background: #ffffff;
|
|
}
|
|
table {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Programação de Férias</h1>
|
|
<p>${periodoLegivel}</p>
|
|
</header>
|
|
<section class="meta">
|
|
<div class="meta-item">Total de colaboradores: ${
|
|
new Set(periodosSelecionados.map((periodo) => periodo.funcionarioNome)).size
|
|
}</div>
|
|
<div class="meta-item">Total de períodos: ${periodosSelecionados.length}</div>
|
|
<div class="meta-item">Dias planejados: ${totalDias}</div>
|
|
</section>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Funcionário</th>
|
|
<th>Matrícula</th>
|
|
<th>Gestor</th>
|
|
<th>Ano Ref.</th>
|
|
<th>Início</th>
|
|
<th>Fim</th>
|
|
<th>Dias</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${linhasTabela}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="7">Total planejado no período</td>
|
|
<td colspan="2">${totalDias} dia(s)</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// Criar blob URL com o HTML para garantir que o conteúdo seja carregado corretamente
|
|
const blob = new Blob([relatorioHTML], { type: 'text/html;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Abrir nova janela com o conteúdo
|
|
const relatorioJanela = window.open(url, 'programacao-ferias', 'width=1200,height=800');
|
|
|
|
if (
|
|
!relatorioJanela ||
|
|
relatorioJanela.closed ||
|
|
typeof relatorioJanela.closed === 'undefined'
|
|
) {
|
|
URL.revokeObjectURL(url);
|
|
window.alert(
|
|
'Pop-ups estão bloqueados. Por favor, permita pop-ups para este site e tente novamente.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Função para limpar a URL do blob
|
|
const limparURL = () => {
|
|
try {
|
|
URL.revokeObjectURL(url);
|
|
} catch {
|
|
// Ignorar erros ao revogar URL
|
|
}
|
|
};
|
|
|
|
// Flag para garantir que a impressão aconteça apenas uma vez
|
|
let jaImprimiu = false;
|
|
|
|
// Função para imprimir (garantir que execute apenas uma vez)
|
|
const executarImpressao = () => {
|
|
if (jaImprimiu || relatorioJanela.closed) {
|
|
return;
|
|
}
|
|
|
|
jaImprimiu = true;
|
|
|
|
try {
|
|
relatorioJanela.focus();
|
|
relatorioJanela.print();
|
|
|
|
// Fechar janela após impressão
|
|
relatorioJanela.addEventListener(
|
|
'afterprint',
|
|
() => {
|
|
setTimeout(() => {
|
|
if (!relatorioJanela.closed) {
|
|
relatorioJanela.close();
|
|
}
|
|
limparURL();
|
|
}, 100);
|
|
},
|
|
{ once: true }
|
|
);
|
|
} catch (error) {
|
|
console.error('Erro ao imprimir:', error);
|
|
limparURL();
|
|
window.alert(
|
|
'Erro ao abrir diálogo de impressão. Verifique as configurações do navegador.'
|
|
);
|
|
}
|
|
};
|
|
|
|
// Aguardar o conteúdo carregar completamente antes de imprimir
|
|
// Usar evento load da janela quando disponível
|
|
const prepararImpressao = () => {
|
|
try {
|
|
if (relatorioJanela.closed) {
|
|
limparURL();
|
|
return;
|
|
}
|
|
|
|
// Aguardar um delay para garantir que o conteúdo foi renderizado
|
|
setTimeout(() => {
|
|
executarImpressao();
|
|
}, 600);
|
|
} catch (error) {
|
|
console.error('Erro ao preparar impressão:', error);
|
|
limparURL();
|
|
}
|
|
};
|
|
|
|
// Tentar usar evento load da janela
|
|
try {
|
|
relatorioJanela.addEventListener(
|
|
'load',
|
|
() => {
|
|
prepararImpressao();
|
|
},
|
|
{ once: true }
|
|
);
|
|
} catch {
|
|
// Se não conseguir adicionar listener, usar timeout direto
|
|
prepararImpressao();
|
|
}
|
|
|
|
// Fallback: se após 1.5 segundos ainda não imprimiu, tentar imprimir diretamente
|
|
setTimeout(() => {
|
|
if (!jaImprimiu && !relatorioJanela.closed) {
|
|
executarImpressao();
|
|
} else if (relatorioJanela.closed) {
|
|
limparURL();
|
|
}
|
|
}, 1500);
|
|
}
|
|
</script>
|
|
|
|
<main class="container mx-auto max-w-7xl px-4 py-6">
|
|
<!-- Breadcrumb -->
|
|
<div class="breadcrumbs mb-4 text-sm">
|
|
<ul>
|
|
<li>
|
|
<a href={resolve('/recursos-humanos')} class="text-primary hover:underline"
|
|
>Recursos Humanos</a
|
|
>
|
|
</li>
|
|
<li>Gestão de Férias</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="rounded-xl bg-purple-500/20 p-3">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8 text-purple-600"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-primary text-3xl font-bold">Dashboard de Férias</h1>
|
|
<p class="text-base-content/70">Visão geral de todas as solicitações e funcionários</p>
|
|
</div>
|
|
</div>
|
|
<button class="btn gap-2" onclick={() => goto(resolve('/recursos-humanos'))}>
|
|
<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="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
/>
|
|
</svg>
|
|
Voltar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs tabs-boxed bg-base-100 mb-6 p-2 shadow-lg">
|
|
<button
|
|
class="tab gap-2 {abaAtiva === 'dashboard' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'dashboard')}
|
|
>
|
|
<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 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
/>
|
|
</svg>
|
|
<span class="font-medium">Dashboard</span>
|
|
</button>
|
|
<button
|
|
class="tab gap-2 {abaAtiva === 'solicitacoes' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'solicitacoes')}
|
|
>
|
|
<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>
|
|
<span class="font-medium">Solicitações</span>
|
|
</button>
|
|
<button
|
|
class="tab gap-2 {abaAtiva === 'relatorios' ? 'tab-active' : ''}"
|
|
onclick={() => (abaAtiva = 'relatorios')}
|
|
>
|
|
<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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
|
|
/>
|
|
</svg>
|
|
<span class="font-medium">Imprimir Relatórios</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Mensagem de erro -->
|
|
{#if hasError}
|
|
<div class="alert alert-error mb-6 shadow-lg">
|
|
<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>
|
|
<div>
|
|
<h3 class="font-bold">Erro ao carregar dados</h3>
|
|
<div class="text-sm">{errorMessage}</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Conteúdo das Abas -->
|
|
{#if abaAtiva === 'dashboard'}
|
|
<!-- Dashboard -->
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Dashboard de Férias</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Visão geral de todas as solicitações e funcionários com gráficos e estatísticas
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Estatísticas -->
|
|
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
{#if isLoading && !hasError}
|
|
{#each Array.from({ length: 4 }, (_, i) => i) as index (index)}
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="skeleton mb-2 h-8 w-8 rounded-full"></div>
|
|
<div class="skeleton mb-2 h-4 w-20"></div>
|
|
<div class="skeleton mb-2 h-8 w-16"></div>
|
|
<div class="skeleton h-4 w-24"></div>
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
|
|
<div class="stat-figure text-primary">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
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>
|
|
</div>
|
|
<div class="stat-title">Total</div>
|
|
<div class="stat-value text-primary">{stats.total}</div>
|
|
<div class="stat-desc">Solicitações</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-warning/30 border shadow-lg">
|
|
<div class="stat-figure text-warning">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
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">Aguardando</div>
|
|
<div class="stat-value text-warning">{stats.aguardando}</div>
|
|
<div class="stat-desc">Pendentes</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-success/30 border shadow-lg">
|
|
<div class="stat-figure text-success">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
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>
|
|
</div>
|
|
<div class="stat-title">Aprovadas</div>
|
|
<div class="stat-value text-success">{stats.aprovadas}</div>
|
|
<div class="stat-desc">Deferidas</div>
|
|
</div>
|
|
|
|
<div class="stat bg-base-100 rounded-box border-error/30 border shadow-lg">
|
|
<div class="stat-figure text-error">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-8 w-8"
|
|
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>
|
|
</div>
|
|
<div class="stat-title">Reprovadas</div>
|
|
<div class="stat-value text-error">{stats.reprovadas}</div>
|
|
<div class="stat-desc">Indeferidas</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Filtros -->
|
|
{#if isLoading && !hasError}
|
|
<div class="card border-base-300/60 bg-base-100 mb-6 border shadow-xl">
|
|
<div class="card-body space-y-6">
|
|
<div class="skeleton h-6 w-32"></div>
|
|
<div class="grid grid-cols-12 gap-5">
|
|
{#each Array.from({ length: 6 }, (_, i) => i) as index (index)}
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div class="skeleton h-32 rounded-2xl"></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="card border-base-300/60 bg-base-100 mb-6 border shadow-xl">
|
|
<div class="card-body space-y-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M3 4a1 1 0 011-1h4a1 1 0 01.894.553L10.382 6H19a1 1 0 011 1v2M3 4v16a1 1 0 001 1h16a1 1 0 001-1V9m-9 4h4m-4 4h4m-8-4h.01M9 17h.01"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Filtros</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Filtre as solicitações de férias para visualizar no dashboard
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline"
|
|
onclick={limparTodosFiltrosDashboard}
|
|
disabled={filtroStatusDashboard === 'todos' &&
|
|
filtroNomeDashboard.trim() === '' &&
|
|
filtroMatriculaDashboard.trim() === '' &&
|
|
filtroEmailDashboard.trim() === '' &&
|
|
filtroMesDashboard === '' &&
|
|
filtroPeriodoInicioDashboard === '' &&
|
|
filtroPeriodoFimDashboard === ''}
|
|
>
|
|
Limpar tudo
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-5">
|
|
<!-- Status -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Status</span>
|
|
{#if filtroStatusDashboard !== 'todos'}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroStatusDashboard = 'todos')}
|
|
>
|
|
Limpar
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
<select id="status-dashboard" class="select select-bordered w-full" bind:value={filtroStatusDashboard}>
|
|
<option value="todos">Todos</option>
|
|
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
|
|
<option value="aprovado">Aprovado</option>
|
|
<option value="reprovado">Reprovado</option>
|
|
<option value="data_ajustada_aprovada">Data Ajustada</option>
|
|
<option value="Cancelado_RH">Cancelado RH</option>
|
|
</select>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Defina o status das solicitações que deseja visualizar.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nome -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Nome do funcionário</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroNomeDashboard = '')}
|
|
disabled={filtroNomeDashboard.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-nome-dashboard"
|
|
class="input input-bordered w-full"
|
|
type="text"
|
|
placeholder="Ex.: Maria Souza"
|
|
bind:value={filtroNomeDashboard}
|
|
autocomplete="off"
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Pesquise por nome completo ou parcial para localizar rapidamente um colaborador.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Matrícula -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Matrícula</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMatriculaDashboard = '')}
|
|
disabled={filtroMatriculaDashboard.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-matricula-dashboard"
|
|
class="input input-bordered w-full"
|
|
type="text"
|
|
placeholder="Informe a matrícula"
|
|
bind:value={filtroMatriculaDashboard}
|
|
autocomplete="off"
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Utilize a matrícula funcional para filtrar solicitações específicas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- E-mail -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>E-mail institucional</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroEmailDashboard = '')}
|
|
disabled={filtroEmailDashboard.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-email-dashboard"
|
|
class="input input-bordered w-full"
|
|
type="text"
|
|
placeholder="nome.sobrenome@pe.gov.br"
|
|
bind:value={filtroEmailDashboard}
|
|
autocomplete="off"
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Busque usando o correio institucional cadastrado na ficha do colaborador.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mês -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Mês de referência</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMesDashboard = '')}
|
|
disabled={filtroMesDashboard === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-mes-dashboard"
|
|
type="month"
|
|
class="input input-bordered w-full"
|
|
bind:value={filtroMesDashboard}
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtra as solicitações que possuem períodos ativos dentro do mês informado.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Período personalizado -->
|
|
<div class="col-span-12 xl:col-span-8">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Período personalizado</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={limparPeriodoPersonalizadoDashboard}
|
|
disabled={!filtroPeriodoInicioDashboard && !filtroPeriodoFimDashboard}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data inicial</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered"
|
|
bind:value={filtroPeriodoInicioDashboard}
|
|
max={filtroPeriodoFimDashboard || undefined}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data final</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered"
|
|
bind:value={filtroPeriodoFimDashboard}
|
|
min={filtroPeriodoInicioDashboard || undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Combine as datas para localizar períodos específicos de férias aprovadas ou em
|
|
andamento.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Calendário Geral de Férias -->
|
|
{#if isLoading && !hasError}
|
|
<div class="card bg-base-100 border-primary/20 mb-6 border shadow-lg">
|
|
<div class="card-body space-y-4 p-6">
|
|
<div class="skeleton mb-4 h-6 w-64"></div>
|
|
<div class="skeleton h-96 w-full"></div>
|
|
</div>
|
|
</div>
|
|
{:else if !hasError}
|
|
<div class="card bg-base-100 border-primary/20 mb-6 border shadow-lg">
|
|
<div class="card-body space-y-4 p-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-base-content text-lg font-semibold">Calendário Geral de Férias</h3>
|
|
<p class="text-base-content/60 text-sm">
|
|
Visualize os períodos aprovados diretamente no calendário interativo
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="bg-base-200/40 relative w-full overflow-hidden rounded-xl p-4">
|
|
<div
|
|
class="calendario-ferias overflow-hidden rounded-xl shadow-2xl"
|
|
bind:this={calendarioContainer}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Visualizações e Relatórios -->
|
|
{#if isLoading && !hasError}
|
|
<div class="mb-6 space-y-6" bind:this={chartContainer}>
|
|
{#each Array.from({ length: 3 }, (_, i) => i) as index (index)}
|
|
<div class="card bg-base-100 border-primary/20 border shadow-lg">
|
|
<div class="card-body space-y-4 p-6">
|
|
<div class="skeleton mb-4 h-6 w-64"></div>
|
|
<div class="skeleton h-64 w-full"></div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="mb-6 space-y-6" bind:this={chartContainer}>
|
|
</div>
|
|
{/if}
|
|
{:else if abaAtiva === 'solicitacoes'}
|
|
<!-- Aba Solicitações -->
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-6 w-6"
|
|
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>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Solicitações de Férias</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Gerencie e visualize todas as solicitações de férias dos funcionários
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtros Solicitações -->
|
|
{#if isLoading && !hasError}
|
|
<div class="card border-base-300/60 bg-base-100 mb-6 border shadow-xl">
|
|
<div class="card-body space-y-6">
|
|
<div class="skeleton h-6 w-32"></div>
|
|
<div class="grid grid-cols-12 gap-5">
|
|
{#each Array.from({ length: 6 }, (_, i) => i) as index (index)}
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div class="skeleton h-32 rounded-2xl"></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="card border-base-300/60 bg-base-100 mb-6 border shadow-xl">
|
|
<div class="card-body space-y-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M3 4a1 1 0 011-1h4a1 1 0 01.894.553L10.382 6H19a1 1 0 011 1v2M3 4v16a1 1 0 001 1h16a1 1 0 001-1V9m-9 4h4m-4 4h4m-8-4h.01M9 17h.01"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Filtros</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Filtre as solicitações para encontrar o que você precisa
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline"
|
|
onclick={limparTodosFiltrosSolicitacoes}
|
|
disabled={filtroStatusSolicitacoes === 'todos' &&
|
|
filtroNomeSolicitacoes.trim() === '' &&
|
|
filtroMatriculaSolicitacoes.trim() === '' &&
|
|
filtroEmailSolicitacoes.trim() === '' &&
|
|
filtroMesSolicitacoes === '' &&
|
|
filtroPeriodoInicioSolicitacoes === '' &&
|
|
filtroPeriodoFimSolicitacoes === ''}
|
|
>
|
|
Limpar tudo
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-5">
|
|
<!-- Status -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Status</span>
|
|
{#if filtroStatusSolicitacoes !== 'todos'}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroStatusSolicitacoes = 'todos')}
|
|
>
|
|
Limpar
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
<select id="status-solicitacoes" class="select select-bordered w-full" bind:value={filtroStatusSolicitacoes}>
|
|
<option value="todos">Todos</option>
|
|
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
|
|
<option value="aprovado">Aprovado</option>
|
|
<option value="reprovado">Reprovado</option>
|
|
<option value="data_ajustada_aprovada">Data Ajustada</option>
|
|
<option value="Cancelado_RH">Cancelado RH</option>
|
|
</select>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Defina o status das solicitações que deseja visualizar.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nome -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Nome do funcionário</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroNomeSolicitacoes = '')}
|
|
disabled={filtroNomeSolicitacoes.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<FuncionarioNomeAutocomplete bind:value={filtroNomeSolicitacoes} />
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Pesquise por nome completo ou parcial para localizar rapidamente um colaborador.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Matrícula -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Matrícula</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMatriculaSolicitacoes = '')}
|
|
disabled={filtroMatriculaSolicitacoes.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<FuncionarioMatriculaAutocomplete bind:value={filtroMatriculaSolicitacoes} />
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Utilize a matrícula funcional para filtrar solicitações específicas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- E-mail -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>E-mail institucional</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroEmailSolicitacoes = '')}
|
|
disabled={filtroEmailSolicitacoes.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-email-solicitacoes"
|
|
class="input input-bordered w-full"
|
|
type="text"
|
|
placeholder="nome.sobrenome@pe.gov.br"
|
|
bind:value={filtroEmailSolicitacoes}
|
|
autocomplete="off"
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Busque usando o correio institucional cadastrado na ficha do colaborador.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mês -->
|
|
<div class="col-span-12 md:col-span-6 xl:col-span-4">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Mês de referência</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMesSolicitacoes = '')}
|
|
disabled={filtroMesSolicitacoes === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-mes-solicitacoes"
|
|
type="month"
|
|
class="input input-bordered w-full"
|
|
bind:value={filtroMesSolicitacoes}
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtra as solicitações que possuem períodos ativos dentro do mês informado.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Período personalizado -->
|
|
<div class="col-span-12 xl:col-span-8">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Período personalizado</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={limparPeriodoPersonalizadoSolicitacoes}
|
|
disabled={!filtroPeriodoInicioSolicitacoes && !filtroPeriodoFimSolicitacoes}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data inicial</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered"
|
|
bind:value={filtroPeriodoInicioSolicitacoes}
|
|
max={filtroPeriodoFimSolicitacoes || undefined}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data final</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered"
|
|
bind:value={filtroPeriodoFimSolicitacoes}
|
|
min={filtroPeriodoInicioSolicitacoes || undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Combine as datas para localizar períodos específicos de férias aprovadas ou em
|
|
andamento.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Lista de Solicitações -->
|
|
{#if isLoading && !hasError}
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<div class="skeleton mb-4 h-6 w-48"></div>
|
|
<div class="space-y-4">
|
|
{#each Array.from({ length: 5 }, (_, i) => i) as index (index)}
|
|
<div class="skeleton h-16 w-full"></div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4 text-lg">
|
|
Solicitações ({solicitacoesFiltradas.length})
|
|
</h2>
|
|
|
|
{#if solicitacoesFiltradas.length === 0}
|
|
<div class="alert">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
class="stroke-info h-6 w-6 shrink-0"
|
|
>
|
|
<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>Nenhuma solicitação encontrada com os filtros aplicados.</span>
|
|
</div>
|
|
{:else}
|
|
<div class="overflow-x-auto">
|
|
<table class="table-zebra table">
|
|
<thead class="bg-base-200/60">
|
|
<tr class="text-base-content/70 text-sm tracking-wide uppercase">
|
|
<th>Funcionário</th>
|
|
<th>Time</th>
|
|
<th>Ano</th>
|
|
<th>Período</th>
|
|
<th>Dias</th>
|
|
<th>Status</th>
|
|
<th>Solicitado em</th>
|
|
<th>Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each solicitacoesFiltradas as periodo (periodo._id)}
|
|
<tr>
|
|
<td>
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="bg-primary text-primary-content w-10 h-10 rounded-full flex items-center justify-center overflow-hidden">
|
|
{#if periodo.funcionario && 'fotoPerfilUrl' in periodo.funcionario && periodo.funcionario.fotoPerfilUrl}
|
|
<img
|
|
src={periodo.funcionario.fotoPerfilUrl}
|
|
alt={`Foto de perfil de ${periodo.funcionario?.nome || 'Usuário'}`}
|
|
class="h-full w-full object-cover"
|
|
/>
|
|
{:else}
|
|
<span class="text-xs"
|
|
>{periodo.funcionario?.nome?.substring(0, 2).toUpperCase() || '??'}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold">{periodo.funcionario?.nome}</div>
|
|
<div class="text-xs opacity-50">
|
|
{periodo.funcionario?.matricula || 'S/N'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{#if periodo.time}
|
|
<div class="badge badge-outline" style="border-color: {periodo.time.cor}">
|
|
{periodo.time.nome}
|
|
</div>
|
|
{:else}
|
|
<span class="text-base-content/50 text-xs">Sem time</span>
|
|
{/if}
|
|
</td>
|
|
<td>{periodo.anoReferencia}</td>
|
|
<td>
|
|
{formatarDataString(periodo.dataInicio)} - {formatarDataString(
|
|
periodo.dataFim
|
|
)}
|
|
</td>
|
|
<td class="text-base-content font-bold">{periodo.diasFerias} dia(s)</td>
|
|
<td>
|
|
<div class={`badge ${getStatusBadge(periodo.status)}`}>
|
|
{getStatusTexto(periodo.status)}
|
|
</div>
|
|
</td>
|
|
<td class="text-xs">{formatarData(periodo._creationTime)}</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm gap-2"
|
|
onclick={() => selecionarPeriodo(periodo._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="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>
|
|
Alterar Status
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{:else if abaAtiva === 'relatorios'}
|
|
<!-- Aba Imprimir Relatórios -->
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-accent/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-accent h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Imprimir Relatórios</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Configure os filtros e gere relatórios de programação de férias em PDF ou Excel
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtros Relatórios -->
|
|
<div class="card border-base-300/60 bg-base-100 mb-6 border shadow-xl">
|
|
<div class="card-body space-y-6">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-accent/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-accent h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M3 4a1 1 0 011-1h4a1 1 0 01.894.553L10.382 6H19a1 1 0 011 1v2M3 4v16a1 1 0 001 1h16a1 1 0 001-1V9m-9 4h4m-4 4h4m-8-4h.01M9 17h.01"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-base-content text-2xl font-semibold">Filtros</h2>
|
|
<p class="text-base-content/60 text-sm">
|
|
Configure os filtros para gerar o relatório personalizado
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline"
|
|
onclick={limparTodosFiltrosRelatorios}
|
|
disabled={dataInicioRelatorio === '' &&
|
|
dataFimRelatorio === '' &&
|
|
filtroFuncionarioRelatorio.trim() === '' &&
|
|
filtroMatriculaRelatorio.trim() === '' &&
|
|
filtroStatusRelatorio === 'todos' &&
|
|
filtroMesRelatorio === ''}
|
|
>
|
|
Limpar tudo
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-5">
|
|
<!-- Período -->
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Período</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => {
|
|
dataInicioRelatorio = '';
|
|
dataFimRelatorio = '';
|
|
}}
|
|
disabled={!dataInicioRelatorio && !dataFimRelatorio}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data inicial</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered w-full"
|
|
bind:value={dataInicioRelatorio}
|
|
max={dataFimRelatorio || undefined}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<span class="text-base-content/60 text-xs font-medium tracking-wide uppercase"
|
|
>Data final</span
|
|
>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered w-full"
|
|
bind:value={dataFimRelatorio}
|
|
min={dataInicioRelatorio || undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Selecione o período para gerar o relatório de programação de férias.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Funcionário -->
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Funcionário</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroFuncionarioRelatorio = '')}
|
|
disabled={filtroFuncionarioRelatorio.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<FuncionarioNomeAutocomplete bind:value={filtroFuncionarioRelatorio} />
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtre por nome do funcionário para gerar relatório específico.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Matrícula -->
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Matrícula</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMatriculaRelatorio = '')}
|
|
disabled={filtroMatriculaRelatorio.trim() === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<FuncionarioMatriculaAutocomplete bind:value={filtroMatriculaRelatorio} />
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtre por matrícula do funcionário para gerar relatório específico.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Status</span>
|
|
{#if filtroStatusRelatorio !== 'todos'}
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroStatusRelatorio = 'todos')}
|
|
>
|
|
Limpar
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
<select id="status-relatorios" class="select select-bordered w-full" bind:value={filtroStatusRelatorio}>
|
|
<option value="todos">Todos</option>
|
|
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
|
|
<option value="aprovado">Aprovado</option>
|
|
<option value="reprovado">Reprovado</option>
|
|
<option value="data_ajustada_aprovada">Data Ajustada</option>
|
|
<option value="Cancelado_RH">Cancelado RH</option>
|
|
</select>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtre por status das solicitações de férias.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mês de referência -->
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div
|
|
class="border-base-300/60 bg-base-200/40 flex h-full flex-col gap-3 rounded-2xl border p-4 shadow-sm"
|
|
>
|
|
<div
|
|
class="text-base-content/80 flex items-center justify-between text-sm font-semibold"
|
|
>
|
|
<span>Mês de referência</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-xs btn-ghost text-primary"
|
|
onclick={() => (filtroMesRelatorio = '')}
|
|
disabled={filtroMesRelatorio === ''}
|
|
>
|
|
Limpar
|
|
</button>
|
|
</div>
|
|
<input
|
|
id="filtro-mes-relatorios"
|
|
type="month"
|
|
class="input input-bordered w-full"
|
|
bind:value={filtroMesRelatorio}
|
|
/>
|
|
<p class="text-base-content/60 text-xs leading-relaxed">
|
|
Filtra as solicitações que possuem períodos ativos dentro do mês informado.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider my-6"></div>
|
|
|
|
<!-- Seção: Imprimir programação de Férias -->
|
|
<div class="space-y-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-accent/10 rounded-lg p-2.5">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-accent h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 17h2a2 2 0 002-2v-5a2 2 0 00-2-2h-2V6a2 2 0 00-2-2H9a2 2 0 00-2 2v2H5a2 2 0 00-2 2v5a2 2 0 002 2h2m10 0v2a2 2 0 01-2 2H9a2 2 0 01-2-2v-2m10 0H7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-base-content text-2xl font-semibold">Imprimir programação de Férias</h3>
|
|
<p class="text-base-content/60 text-sm">
|
|
Escolha o formato desejado para gerar o relatório de programação de férias
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-3">
|
|
<button
|
|
class="btn btn-primary gap-2"
|
|
type="button"
|
|
onclick={gerarPDFFerias}
|
|
disabled={((!dataInicioRelatorio || !dataFimRelatorio) && !filtroMesRelatorio) || gerandoRelatorio}
|
|
>
|
|
{#if gerandoRelatorio}
|
|
<span class="loading loading-spinner loading-sm"></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>
|
|
{/if}
|
|
Imprimir PDF
|
|
</button>
|
|
<button
|
|
class="btn btn-success gap-2"
|
|
type="button"
|
|
onclick={gerarExcelFerias}
|
|
disabled={((!dataInicioRelatorio || !dataFimRelatorio) && !filtroMesRelatorio) || gerandoRelatorio}
|
|
>
|
|
{#if gerandoRelatorio}
|
|
<span class="loading loading-spinner loading-sm"></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>
|
|
{/if}
|
|
Exportar Excel
|
|
</button>
|
|
</div>
|
|
<p class="text-base-content/60 text-xs">
|
|
Os relatórios serão gerados com base nos filtros selecionados acima. O PDF será aberto para impressão e o Excel será baixado automaticamente.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Visualizações e Relatórios -->
|
|
{#if isLoading && !hasError}
|
|
<div class="mb-6 space-y-6" bind:this={chartContainer}>
|
|
{#each Array.from({ length: 3 }, (_, i) => i) as index (index)}
|
|
<div class="card bg-base-100 border-primary/20 border shadow-lg">
|
|
<div class="card-body space-y-4 p-6">
|
|
<div class="skeleton mb-4 h-6 w-64"></div>
|
|
<div class="skeleton h-64 w-full"></div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="mb-6 space-y-6" bind:this={chartContainer}>
|
|
</div>
|
|
{/if}
|
|
</main>
|
|
|
|
<!-- Modal de Mudança de Status -->
|
|
{#if periodoSelecionado && currentUser.data}
|
|
{#await client.query(api.ferias.obterDetalhes, { feriasId: periodoSelecionado }) then detalhes}
|
|
{#if detalhes}
|
|
<dialog class="modal modal-open">
|
|
<div class="modal-box max-w-4xl">
|
|
<AlterarStatusFerias
|
|
solicitacao={detalhes}
|
|
usuarioId={currentUser.data._id}
|
|
onSucesso={recarregar}
|
|
onCancelar={() => (periodoSelecionado = null)}
|
|
/>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button
|
|
type="button"
|
|
onclick={() => (periodoSelecionado = null)}
|
|
aria-label="Fechar modal"
|
|
>
|
|
Fechar
|
|
</button>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|
|
{/await}
|
|
{/if}
|
|
|
|
<style>
|
|
/* Calendário de Férias */
|
|
.calendario-ferias {
|
|
font-family:
|
|
'Inter',
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
'Segoe UI',
|
|
sans-serif;
|
|
}
|
|
|
|
/* Toolbar moderna com cores azuis/primary */
|
|
:global(.calendario-ferias .fc .fc-toolbar) {
|
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
padding: 1rem;
|
|
border-radius: 1rem 1rem 0 0;
|
|
color: white !important;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-toolbar-title) {
|
|
color: white !important;
|
|
font-weight: 700;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-button) {
|
|
background: rgba(255, 255, 255, 0.2) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
color: white !important;
|
|
font-weight: 600;
|
|
text-transform: capitalize;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-button:hover) {
|
|
background: rgba(255, 255, 255, 0.3) !important;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-button-active) {
|
|
background: rgba(255, 255, 255, 0.4) !important;
|
|
}
|
|
|
|
/* Cabeçalho dos dias */
|
|
:global(.calendario-ferias .fc .fc-col-header-cell) {
|
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.05em;
|
|
padding: 0.75rem 0.5rem;
|
|
color: #495057;
|
|
}
|
|
|
|
/* Células dos dias */
|
|
:global(.calendario-ferias .fc .fc-daygrid-day) {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-daygrid-day:hover) {
|
|
background: rgba(37, 99, 235, 0.05);
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-daygrid-day-number) {
|
|
padding: 0.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
/* Fim de semana */
|
|
:global(.calendario-ferias .fc .fc-day-weekend) {
|
|
background: rgba(59, 130, 246, 0.05);
|
|
}
|
|
|
|
/* Hoje */
|
|
:global(.calendario-ferias .fc .fc-day-today) {
|
|
border: 2px solid #2563eb !important;
|
|
}
|
|
|
|
/* Eventos (férias) */
|
|
:global(.calendario-ferias .fc .fc-event) {
|
|
border-radius: 0.5rem;
|
|
padding: 0.25rem 0.5rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-event:hover) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
/* Seleção (arrastar) */
|
|
:global(.calendario-ferias .fc .fc-highlight) {
|
|
background: rgba(37, 99, 235, 0.3) !important;
|
|
border: 2px dashed #2563eb;
|
|
}
|
|
|
|
/* Datas desabilitadas (passado) */
|
|
:global(.calendario-ferias .fc .fc-day-past .fc-daygrid-day-number) {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
/* Remover bordas padrão */
|
|
:global(.calendario-ferias .fc .fc-scrollgrid) {
|
|
border: none !important;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-scrollgrid-section > td) {
|
|
border: none !important;
|
|
}
|
|
|
|
/* Grid moderno */
|
|
:global(.calendario-ferias .fc .fc-daygrid-day-frame) {
|
|
border: 1px solid #e9ecef;
|
|
min-height: 80px;
|
|
}
|
|
|
|
/* Garantir que o calendário seja visível */
|
|
:global(.calendario-ferias .fc) {
|
|
min-height: 400px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Responsivo */
|
|
@media (max-width: 768px) {
|
|
:global(.calendario-ferias .fc .fc-toolbar) {
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-toolbar-title) {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
:global(.calendario-ferias .fc .fc-button) {
|
|
font-size: 0.75rem;
|
|
padding: 0.25rem 0.5rem;
|
|
}
|
|
}
|
|
</style>
|