Merge remote-tracking branch 'origin' into feat-many-fixes

This commit is contained in:
2025-11-11 16:25:29 -03:00
11 changed files with 5158 additions and 2366 deletions

View File

@@ -23,33 +23,43 @@
let modalRef: HTMLDialogElement;
let generating = $state(false);
// Seções selecionáveis
let sections = $state({
dadosPessoais: true,
filiacao: true,
naturalidade: true,
documentos: true,
formacao: true,
saude: true,
endereco: true,
contato: true,
cargo: true,
bancario: true
});
// Seções selecionáveis
let sections = $state({
dadosPessoais: true,
filiacao: true,
naturalidade: true,
documentos: true,
formacao: true,
saude: true,
endereco: true,
contato: true,
cargo: true,
financeiro: true,
bancario: true,
});
function getLabelFromOptions(
value: string | undefined,
options: Array<{ value: string; label: string }>
): string {
if (!value) return '-';
return options.find((opt) => opt.value === value)?.label || value;
}
const REGIME_LABELS: Record<string, string> = {
clt: "CLT",
estatutario_municipal: "Estatutário Municipal",
estatutario_pe: "Estatutário PE",
estatutario_federal: "Estatutário Federal",
};
function selectAll() {
Object.keys(sections).forEach((key) => {
sections[key as keyof typeof sections] = true;
});
}
function getLabelFromOptions(value: string | undefined, options: Array<{value: string, label: string}>): string {
if (!value) return "-";
return options.find(opt => opt.value === value)?.label || value;
}
function getRegimeLabel(value?: string) {
if (!value) return "-";
return REGIME_LABELS[value] ?? value;
}
function selectAll() {
Object.keys(sections).forEach(key => {
sections[key as keyof typeof sections] = true;
});
}
function deselectAll() {
Object.keys(sections).forEach((key) => {
@@ -144,8 +154,26 @@
styles: { fontSize: 9 }
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Documentos
if (sections.documentos) {
const documentosData: any[] = [];
if (funcionario.carteiraProfissionalNumero) {
documentosData.push(['Cart. Profissional', `Nº ${funcionario.carteiraProfissionalNumero}${funcionario.carteiraProfissionalSerie ? ' - Série: ' + funcionario.carteiraProfissionalSerie : ''}`]);
}
if (funcionario.carteiraProfissionalDataEmissao) {
documentosData.push(['Emissão Cart. Profissional', funcionario.carteiraProfissionalDataEmissao]);
}
if (funcionario.reservistaNumero) {
documentosData.push(['Reservista', `Nº ${funcionario.reservistaNumero}${funcionario.reservistaSerie ? ' - Série: ' + funcionario.reservistaSerie : ''}`]);
}
if (funcionario.tituloEleitorNumero) {
let titulo = `Nº ${funcionario.tituloEleitorNumero}`;
if (funcionario.tituloEleitorZona) titulo += ` - Zona: ${funcionario.tituloEleitorZona}`;
if (funcionario.tituloEleitorSecao) titulo += ` - Seção: ${funcionario.tituloEleitorSecao}`;
documentosData.push(['Título Eleitor', titulo]);
}
if (funcionario.pisNumero) documentosData.push(['PIS/PASEP', funcionario.pisNumero]);
// Filiação
if (sections.filiacao && (funcionario.nomePai || funcionario.nomeMae)) {
@@ -266,14 +294,22 @@
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Endereço
if (sections.endereco) {
const enderecoData: any[] = [
['Endereço', funcionario.endereco],
['Cidade', funcionario.cidade],
['UF', funcionario.uf],
['CEP', maskCEP(funcionario.cep)]
];
const simboloInfo = funcionario.simbolo ?? funcionario.simboloDetalhes ?? funcionario.simboloDados;
if (simboloInfo) {
cargoData.push(['Símbolo', simboloInfo.nome]);
if (simboloInfo.descricao) cargoData.push(['Descrição do Símbolo', simboloInfo.descricao]);
}
if (funcionario.descricaoCargo) cargoData.push(['Descrição', funcionario.descricaoCargo]);
if (funcionario.regimeTrabalho) cargoData.push(['Regime do Funcionário', getRegimeLabel(funcionario.regimeTrabalho)]);
if (funcionario.admissaoData) cargoData.push(['Data Admissão', funcionario.admissaoData]);
if (funcionario.nomeacaoPortaria) cargoData.push(['Portaria', funcionario.nomeacaoPortaria]);
if (funcionario.nomeacaoData) cargoData.push(['Data Nomeação', funcionario.nomeacaoData]);
if (funcionario.nomeacaoDOE) cargoData.push(['DOE', funcionario.nomeacaoDOE]);
cargoData.push(['Pertence Órgão Público', funcionario.pertenceOrgaoPublico ? 'Sim' : 'Não']);
if (funcionario.pertenceOrgaoPublico && funcionario.orgaoOrigem) cargoData.push(['Órgão Origem', funcionario.orgaoOrigem]);
if (funcionario.aposentado && funcionario.aposentado !== 'nao') {
cargoData.push(['Aposentado', getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)]);
}
autoTable(doc, {
startY: yPosition,
@@ -287,12 +323,42 @@
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Contato
if (sections.contato) {
const contatoData: any[] = [
['E-mail', funcionario.email],
['Telefone', maskPhone(funcionario.telefone)]
];
// Dados Financeiros
if (sections.financeiro && funcionario.simbolo) {
const simbolo = funcionario.simbolo;
const financeiroData: any[] = [
['Símbolo', simbolo.nome],
['Tipo', simbolo.tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'],
['Remuneração Total', `R$ ${simbolo.valor}`],
];
if (funcionario.simboloTipo === 'cargo_comissionado') {
if (simbolo.vencValor) {
financeiroData.push(['Vencimento', `R$ ${simbolo.vencValor}`]);
}
if (simbolo.repValor) {
financeiroData.push(['Representação', `R$ ${simbolo.repValor}`]);
}
}
autoTable(doc, {
startY: yPosition,
head: [['DADOS FINANCEIROS', '']],
body: financeiroData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Dados Bancários
if (sections.bancario && funcionario.contaBradescoNumero) {
const bancarioData: any[] = [
['Conta', `${funcionario.contaBradescoNumero}${funcionario.contaBradescoDV ? '-' + funcionario.contaBradescoDV : ''}`],
];
if (funcionario.contaBradescoAgencia) bancarioData.push(['Agência', funcionario.contaBradescoAgencia]);
autoTable(doc, {
startY: yPosition,
@@ -488,11 +554,16 @@
<span class="label-text">Cargo e Vínculo</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.bancario} />
<span class="label-text">Dados Bancários</span>
</label>
</div>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.financeiro} />
<span class="label-text">Dados Financeiros</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.bancario} />
<span class="label-text">Dados Bancários</span>
</label>
</div>
<!-- Ações -->
<div class="modal-action">

View File

@@ -1,393 +1,399 @@
<script lang="ts">
import { onMount } from "svelte";
import { Calendar } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import multiMonthPlugin from "@fullcalendar/multimonth";
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
import { onMount } from 'svelte';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import multiMonthPlugin from '@fullcalendar/multimonth';
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
import { SvelteDate } from 'svelte/reactivity';
interface Props {
periodosExistentes?: Array<{ dataInicio: string; dataFim: string; dias: number }>;
onPeriodoAdicionado?: (periodo: { dataInicio: string; dataFim: string; dias: number }) => void;
onPeriodoRemovido?: (index: number) => void;
maxPeriodos?: number;
minDiasPorPeriodo?: number;
modoVisualizacao?: "month" | "multiMonth";
readonly?: boolean;
}
interface Props {
periodosExistentes?: Array<{ dataInicio: string; dataFim: string; dias: number }>;
onPeriodoAdicionado?: (periodo: { dataInicio: string; dataFim: string; dias: number }) => void;
onPeriodoRemovido?: (index: number) => void;
maxPeriodos?: number;
minDiasPorPeriodo?: number;
modoVisualizacao?: 'month' | 'multiMonth';
readonly?: boolean;
}
let {
periodosExistentes = [],
onPeriodoAdicionado,
onPeriodoRemovido,
maxPeriodos = 3,
minDiasPorPeriodo = 5,
modoVisualizacao = "month",
readonly = false,
}: Props = $props();
let {
periodosExistentes = [],
onPeriodoAdicionado,
onPeriodoRemovido,
maxPeriodos = 3,
minDiasPorPeriodo = 5,
modoVisualizacao = 'month',
readonly = false
}: Props = $props();
let calendarEl: HTMLDivElement;
let calendar: Calendar | null = null;
let selecaoInicio: Date | null = null;
let eventos: any[] = $state([]);
let calendarEl: HTMLDivElement;
let calendar: Calendar | null = null;
// Cores dos períodos
const coresPeriodos = [
{ bg: "#667eea", border: "#5568d3", text: "#ffffff" }, // Roxo
{ bg: "#f093fb", border: "#c75ce6", text: "#ffffff" }, // Rosa
{ bg: "#4facfe", border: "#00c6ff", text: "#ffffff" }, // Azul
];
// Cores dos períodos
const coresPeriodos = [
{ bg: '#667eea', border: '#5568d3', text: '#ffffff' }, // Roxo
{ bg: '#f093fb', border: '#c75ce6', text: '#ffffff' }, // Rosa
{ bg: '#4facfe', border: '#00c6ff', text: '#ffffff' } // Azul
];
// Converter períodos existentes em eventos
function atualizarEventos() {
eventos = periodosExistentes.map((periodo, index) => ({
id: `periodo-${index}`,
title: `Período ${index + 1} (${periodo.dias} dias)`,
start: periodo.dataInicio,
end: calcularDataFim(periodo.dataFim),
backgroundColor: coresPeriodos[index % coresPeriodos.length].bg,
borderColor: coresPeriodos[index % coresPeriodos.length].border,
textColor: coresPeriodos[index % coresPeriodos.length].text,
display: "block",
extendedProps: {
index,
dias: periodo.dias,
},
}));
}
const eventos = $derived.by(() =>
periodosExistentes.map((periodo, index) => ({
id: `periodo-${index}`,
title: `Período ${index + 1} (${periodo.dias} dias)`,
start: periodo.dataInicio,
end: calcularDataFim(periodo.dataFim),
backgroundColor: coresPeriodos[index % coresPeriodos.length].bg,
borderColor: coresPeriodos[index % coresPeriodos.length].border,
textColor: coresPeriodos[index % coresPeriodos.length].text,
display: 'block',
extendedProps: {
index,
dias: periodo.dias
}
}))
);
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
function calcularDataFim(dataFim: string): string {
const data = new Date(dataFim);
data.setDate(data.getDate() + 1);
return data.toISOString().split("T")[0];
}
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
function calcularDataFim(dataFim: string): string {
const data = new SvelteDate(dataFim);
data.setDate(data.getDate() + 1);
return data.toISOString().split('T')[0];
}
// Helper: Calcular dias entre datas (inclusivo)
function calcularDias(inicio: Date, fim: Date): number {
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays;
}
// Helper: Calcular dias entre datas (inclusivo)
function calcularDias(inicio: Date, fim: Date): number {
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
return diffDays;
}
// Atualizar eventos quando períodos mudam
$effect(() => {
atualizarEventos();
if (calendar) {
calendar.removeAllEvents();
calendar.addEventSource(eventos);
}
});
// Atualizar eventos quando períodos mudam
$effect(() => {
if (!calendar) return;
onMount(() => {
if (!calendarEl) return;
calendar.removeAllEvents();
if (eventos.length === 0) return;
atualizarEventos();
// FullCalendar muta os objetos de evento internamente, então fornecemos cópias
const eventosClonados = eventos.map((evento) => ({
...evento,
extendedProps: { ...evento.extendedProps }
}));
calendar.addEventSource(eventosClonados);
});
calendar = new Calendar(calendarEl, {
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
initialView: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
locale: ptBrLocale,
headerToolbar: {
left: "prev,next today",
center: "title",
right: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
},
height: "auto",
selectable: !readonly,
selectMirror: true,
unselectAuto: false,
events: eventos,
// Estilo customizado
buttonText: {
today: "Hoje",
month: "Mês",
multiMonthYear: "Ano",
},
// Seleção de período
select: (info) => {
if (readonly) return;
onMount(() => {
if (!calendarEl) return;
const inicio = new Date(info.startStr);
const fim = new Date(info.endStr);
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
calendar = new Calendar(calendarEl, {
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
initialView: modoVisualizacao === 'multiMonth' ? 'multiMonthYear' : 'dayGridMonth',
locale: ptBrLocale,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: modoVisualizacao === 'multiMonth' ? 'multiMonthYear' : 'dayGridMonth'
},
height: 'auto',
selectable: !readonly,
selectMirror: true,
unselectAuto: false,
events: eventos.map((evento) => ({ ...evento, extendedProps: { ...evento.extendedProps } })),
const dias = calcularDias(inicio, fim);
// Estilo customizado
buttonText: {
today: 'Hoje',
month: 'Mês',
multiMonthYear: 'Ano'
},
// Validar número de períodos
if (periodosExistentes.length >= maxPeriodos) {
alert(`Máximo de ${maxPeriodos} períodos permitidos`);
calendar?.unselect();
return;
}
// Seleção de período
select: (info) => {
if (readonly) return;
// Validar mínimo de dias
if (dias < minDiasPorPeriodo) {
alert(`Período deve ter no mínimo ${minDiasPorPeriodo} dias`);
calendar?.unselect();
return;
}
const inicio = new Date(info.startStr);
const fim = new SvelteDate(info.endStr);
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
// Adicionar período
const novoPeriodo = {
dataInicio: info.startStr,
dataFim: fim.toISOString().split("T")[0],
dias,
};
const dias = calcularDias(inicio, fim);
if (onPeriodoAdicionado) {
onPeriodoAdicionado(novoPeriodo);
}
// Validar número de períodos
if (periodosExistentes.length >= maxPeriodos) {
alert(`Máximo de ${maxPeriodos} períodos permitidos`);
calendar?.unselect();
return;
}
calendar?.unselect();
},
// Validar mínimo de dias
if (dias < minDiasPorPeriodo) {
alert(`Período deve ter no mínimo ${minDiasPorPeriodo} dias`);
calendar?.unselect();
return;
}
// Click em evento para remover
eventClick: (info) => {
if (readonly) return;
// Adicionar período
const novoPeriodo = {
dataInicio: info.startStr,
dataFim: fim.toISOString().split('T')[0],
dias
};
const index = info.event.extendedProps.index;
if (
confirm(
`Deseja remover o Período ${index + 1} (${info.event.extendedProps.dias} dias)?`
)
) {
if (onPeriodoRemovido) {
onPeriodoRemovido(index);
}
}
},
if (onPeriodoAdicionado) {
onPeriodoAdicionado(novoPeriodo);
}
// Tooltip ao passar mouse
eventDidMount: (info) => {
info.el.title = `Click para remover\n${info.event.title}`;
info.el.style.cursor = readonly ? "default" : "pointer";
},
calendar?.unselect();
},
// Desabilitar datas passadas
selectAllow: (selectInfo) => {
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
return new Date(selectInfo.start) >= hoje;
},
// Click em evento para remover
eventClick: (info) => {
if (readonly) return;
// Highlight de fim de semana
dayCellClassNames: (arg) => {
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
return ["fc-day-weekend-custom"];
}
return [];
},
});
const index = info.event.extendedProps.index;
if (
confirm(`Deseja remover o Período ${index + 1} (${info.event.extendedProps.dias} dias)?`)
) {
if (onPeriodoRemovido) {
onPeriodoRemovido(index);
}
}
},
calendar.render();
// Tooltip ao passar mouse
eventDidMount: (info) => {
info.el.title = `Click para remover\n${info.event.title}`;
info.el.style.cursor = readonly ? 'default' : 'pointer';
},
return () => {
calendar?.destroy();
};
});
// Desabilitar datas passadas
selectAllow: (selectInfo) => {
const hoje = new SvelteDate();
hoje.setHours(0, 0, 0, 0);
return new Date(selectInfo.start) >= hoje;
},
// Highlight de fim de semana
dayCellClassNames: (arg) => {
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
return ['fc-day-weekend-custom'];
}
return [];
}
});
calendar.render();
return () => {
calendar?.destroy();
};
});
</script>
<div class="calendario-ferias-wrapper">
<!-- Header com instruções -->
{#if !readonly}
<div class="alert alert-info mb-4 shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div class="text-sm">
<p class="font-bold">Como usar:</p>
<ul class="list-disc list-inside mt-1">
<li>Clique e arraste no calendário para selecionar um período de férias</li>
<li>Clique em um período colorido para removê-lo</li>
<li>
Você pode adicionar até {maxPeriodos} períodos (mínimo {minDiasPorPeriodo} dias cada)
</li>
</ul>
</div>
</div>
{/if}
<!-- Header com instruções -->
{#if !readonly}
<div class="alert alert-info mb-4 shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div class="text-sm">
<p class="font-bold">Como usar:</p>
<ul class="mt-1 list-inside list-disc">
<li>Clique e arraste no calendário para selecionar um período de férias</li>
<li>Clique em um período colorido para removê-lo</li>
<li>
Você pode adicionar até {maxPeriodos} períodos (mínimo {minDiasPorPeriodo} dias cada)
</li>
</ul>
</div>
</div>
{/if}
<!-- Calendário -->
<div
bind:this={calendarEl}
class="calendario-ferias shadow-2xl rounded-2xl overflow-hidden border-2 border-primary/10"
></div>
<!-- Calendário -->
<div
bind:this={calendarEl}
class="calendario-ferias border-primary/10 overflow-hidden rounded-2xl border-2 shadow-2xl"
></div>
<!-- Legenda de períodos -->
{#if periodosExistentes.length > 0}
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
{#each periodosExistentes as periodo, index}
<div
class="stat bg-base-100 shadow-lg rounded-xl border-2 transition-all hover:scale-105"
style="border-color: {coresPeriodos[index % coresPeriodos.length].border}"
>
<div
class="stat-figure text-white w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold"
style="background: {coresPeriodos[index % coresPeriodos.length].bg}"
>
{index + 1}
</div>
<div class="stat-title">Período {index + 1}</div>
<div class="stat-value text-2xl" style="color: {coresPeriodos[index % coresPeriodos.length].bg}">
{periodo.dias} dias
</div>
<div class="stat-desc">
{new Date(periodo.dataInicio).toLocaleDateString("pt-BR")} até
{new Date(periodo.dataFim).toLocaleDateString("pt-BR")}
</div>
</div>
{/each}
</div>
{/if}
<!-- Legenda de períodos -->
{#if periodosExistentes.length > 0}
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-3">
{#each periodosExistentes as periodo, index (index)}
<div
class="stat bg-base-100 rounded-xl border-2 shadow-lg transition-all hover:scale-105"
style="border-color: {coresPeriodos[index % coresPeriodos.length].border}"
>
<div
class="stat-figure flex h-12 w-12 items-center justify-center rounded-full text-xl font-bold text-white"
style="background: {coresPeriodos[index % coresPeriodos.length].bg}"
>
{index + 1}
</div>
<div class="stat-title">Período {index + 1}</div>
<div
class="stat-value text-2xl"
style="color: {coresPeriodos[index % coresPeriodos.length].bg}"
>
{periodo.dias} dias
</div>
<div class="stat-desc">
{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')} até
{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
/* Calendário Premium */
.calendario-ferias {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
/* Calendário Premium */
.calendario-ferias {
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
}
/* Toolbar moderna */
:global(.fc .fc-toolbar) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1rem;
border-radius: 1rem 1rem 0 0;
color: white !important;
}
/* Toolbar moderna */
:global(.fc .fc-toolbar) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1rem;
border-radius: 1rem 1rem 0 0;
color: white !important;
}
:global(.fc .fc-toolbar-title) {
color: white !important;
font-weight: 700;
font-size: 1.5rem;
}
:global(.fc .fc-toolbar-title) {
color: white !important;
font-weight: 700;
font-size: 1.5rem;
}
:global(.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(.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(.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(.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(.fc .fc-button-active) {
background: rgba(255, 255, 255, 0.4) !important;
}
:global(.fc .fc-button-active) {
background: rgba(255, 255, 255, 0.4) !important;
}
/* Cabeçalho dos dias */
:global(.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;
}
/* Cabeçalho dos dias */
:global(.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(.fc .fc-daygrid-day) {
transition: all 0.2s ease;
}
/* Células dos dias */
:global(.fc .fc-daygrid-day) {
transition: all 0.2s ease;
}
:global(.fc .fc-daygrid-day:hover) {
background: rgba(102, 126, 234, 0.05);
}
:global(.fc .fc-daygrid-day:hover) {
background: rgba(102, 126, 234, 0.05);
}
:global(.fc .fc-daygrid-day-number) {
padding: 0.5rem;
font-weight: 600;
color: #495057;
}
:global(.fc .fc-daygrid-day-number) {
padding: 0.5rem;
font-weight: 600;
color: #495057;
}
/* Fim de semana */
:global(.fc .fc-day-weekend-custom) {
background: rgba(255, 193, 7, 0.05);
}
/* Fim de semana */
:global(.fc .fc-day-weekend-custom) {
background: rgba(255, 193, 7, 0.05);
}
/* Hoje */
:global(.fc .fc-day-today) {
background: rgba(102, 126, 234, 0.1) !important;
border: 2px solid #667eea !important;
}
/* Hoje */
:global(.fc .fc-day-today) {
background: rgba(102, 126, 234, 0.1) !important;
border: 2px solid #667eea !important;
}
/* Eventos (períodos selecionados) */
:global(.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;
}
/* Eventos (períodos selecionados) */
:global(.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(.fc .fc-event:hover) {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
:global(.fc .fc-event:hover) {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
/* Seleção (arrastar) */
:global(.fc .fc-highlight) {
background: rgba(102, 126, 234, 0.3) !important;
border: 2px dashed #667eea;
}
/* Seleção (arrastar) */
:global(.fc .fc-highlight) {
background: rgba(102, 126, 234, 0.3) !important;
border: 2px dashed #667eea;
}
/* Datas desabilitadas (passado) */
:global(.fc .fc-day-past .fc-daygrid-day-number) {
opacity: 0.4;
}
/* Datas desabilitadas (passado) */
:global(.fc .fc-day-past .fc-daygrid-day-number) {
opacity: 0.4;
}
/* Remover bordas padrão */
:global(.fc .fc-scrollgrid) {
border: none !important;
}
/* Remover bordas padrão */
:global(.fc .fc-scrollgrid) {
border: none !important;
}
:global(.fc .fc-scrollgrid-section > td) {
border: none !important;
}
:global(.fc .fc-scrollgrid-section > td) {
border: none !important;
}
/* Grid moderno */
:global(.fc .fc-daygrid-day-frame) {
border: 1px solid #e9ecef;
min-height: 80px;
}
/* Grid moderno */
:global(.fc .fc-daygrid-day-frame) {
border: 1px solid #e9ecef;
min-height: 80px;
}
/* Responsivo */
@media (max-width: 768px) {
:global(.fc .fc-toolbar) {
flex-direction: column;
gap: 0.75rem;
}
/* Responsivo */
@media (max-width: 768px) {
:global(.fc .fc-toolbar) {
flex-direction: column;
gap: 0.75rem;
}
:global(.fc .fc-toolbar-title) {
font-size: 1.25rem;
}
:global(.fc .fc-toolbar-title) {
font-size: 1.25rem;
}
:global(.fc .fc-button) {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
}
:global(.fc .fc-button) {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
}
</style>

View File

@@ -1,443 +1,406 @@
<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { onMount } from "svelte";
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
interface Props {
funcionarioId: Id<"funcionarios">;
}
interface Props {
funcionarioId: Id<'funcionarios'>;
}
let { funcionarioId }: Props = $props();
let { funcionarioId }: Props = $props();
// Queries
const saldosQuery = useQuery(api.saldoFerias.listarSaldos, { funcionarioId });
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId,
});
// Queries
const saldosQuery = useQuery(api.saldoFerias.listarSaldos, { funcionarioId });
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, {
funcionarioId
});
const saldos = $derived(saldosQuery.data || []);
const solicitacoes = $derived(solicitacoesQuery.data || []);
const saldos = $derived(saldosQuery.data || []);
const solicitacoes = $derived(solicitacoesQuery.data || []);
// Estatísticas derivadas
const saldoAtual = $derived(
saldos.find((s) => s.anoReferencia === new Date().getFullYear()),
);
const totalSolicitacoes = $derived(solicitacoes.length);
const aprovadas = $derived(
solicitacoes.filter(
(s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada",
).length,
);
const pendentes = $derived(
solicitacoes.filter((s) => s.status === "aguardando_aprovacao").length,
);
const reprovadas = $derived(
solicitacoes.filter((s) => s.status === "reprovado").length,
);
// Estatísticas derivadas
const saldoAtual = $derived(saldos.find((s) => s.anoReferencia === new Date().getFullYear()));
const totalSolicitacoes = $derived(solicitacoes.length);
const aprovadas = $derived(
solicitacoes.filter((s) => s.status === 'aprovado' || s.status === 'data_ajustada_aprovada')
.length
);
const pendentes = $derived(
solicitacoes.filter((s) => s.status === 'aguardando_aprovacao').length
);
const reprovadas = $derived(solicitacoes.filter((s) => s.status === 'reprovado').length);
// Canvas para gráfico de pizza
let canvasSaldo = $state<HTMLCanvasElement>();
let canvasStatus = $state<HTMLCanvasElement>();
// Canvas para gráfico de pizza
let canvasSaldo = $state<HTMLCanvasElement>();
let canvasStatus = $state<HTMLCanvasElement>();
// Função para desenhar gráfico de pizza moderno
function desenharGraficoPizza(
canvas: HTMLCanvasElement,
dados: { label: string; valor: number; cor: string }[],
) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Função para desenhar gráfico de pizza moderno
function desenharGraficoPizza(
canvas: HTMLCanvasElement,
dados: { label: string; valor: number; cor: string }[]
) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 20;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 20;
ctx.clearRect(0, 0, width, height);
ctx.clearRect(0, 0, width, height);
const total = dados.reduce((acc, d) => acc + d.valor, 0);
if (total === 0) return;
const total = dados.reduce((acc, d) => acc + d.valor, 0);
if (total === 0) return;
let startAngle = -Math.PI / 2;
let startAngle = -Math.PI / 2;
dados.forEach((item) => {
const sliceAngle = (2 * Math.PI * item.valor) / total;
dados.forEach((item) => {
const sliceAngle = (2 * Math.PI * item.valor) / total;
// Desenhar fatia com sombra
ctx.save();
ctx.shadowColor = "rgba(0, 0, 0, 0.2)";
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
// Desenhar fatia com sombra
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = item.cor;
ctx.fill();
ctx.fillStyle = item.cor;
ctx.fill();
ctx.restore();
ctx.restore();
// Desenhar borda branca
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 3;
ctx.stroke();
// Desenhar borda branca
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.stroke();
startAngle += sliceAngle;
});
startAngle += sliceAngle;
});
// Desenhar círculo branco no centro (efeito donut)
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.6, 0, 2 * Math.PI);
ctx.fillStyle = "#ffffff";
ctx.fill();
}
// Desenhar círculo branco no centro (efeito donut)
ctx.beginPath();
ctx.arc(centerX, centerY, radius * 0.6, 0, 2 * Math.PI);
ctx.fillStyle = '#ffffff';
ctx.fill();
}
// Atualizar gráficos quando dados mudarem
$effect(() => {
if (canvasSaldo && saldoAtual) {
desenharGraficoPizza(canvasSaldo, [
{ label: "Usado", valor: saldoAtual.diasUsados, cor: "#ff6b6b" },
{ label: "Pendente", valor: saldoAtual.diasPendentes, cor: "#ffa94d" },
{
label: "Disponível",
valor: saldoAtual.diasDisponiveis,
cor: "#51cf66",
},
]);
}
// Atualizar gráficos quando dados mudarem
$effect(() => {
if (canvasSaldo && saldoAtual) {
desenharGraficoPizza(canvasSaldo, [
{ label: 'Usado', valor: saldoAtual.diasUsados, cor: '#ff6b6b' },
{ label: 'Pendente', valor: saldoAtual.diasPendentes, cor: '#ffa94d' },
{
label: 'Disponível',
valor: saldoAtual.diasDisponiveis,
cor: '#51cf66'
}
]);
}
if (canvasStatus && totalSolicitacoes > 0) {
desenharGraficoPizza(canvasStatus, [
{ label: "Aprovadas", valor: aprovadas, cor: "#51cf66" },
{ label: "Pendentes", valor: pendentes, cor: "#ffa94d" },
{ label: "Reprovadas", valor: reprovadas, cor: "#ff6b6b" },
]);
}
});
if (canvasStatus && totalSolicitacoes > 0) {
desenharGraficoPizza(canvasStatus, [
{ label: 'Aprovadas', valor: aprovadas, cor: '#51cf66' },
{ label: 'Pendentes', valor: pendentes, cor: '#ffa94d' },
{ label: 'Reprovadas', valor: reprovadas, cor: '#ff6b6b' }
]);
}
});
</script>
<div class="dashboard-ferias">
<!-- Header -->
<div class="mb-8">
<h1
class="text-4xl font-bold bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent"
>
📊 Dashboard de Férias
</h1>
<p class="text-base-content/70 mt-2">
Visualize seus saldos e histórico de solicitações
</p>
</div>
<!-- Header -->
<div class="mb-8">
<h1
class="from-primary to-secondary bg-linear-to-r bg-clip-text text-4xl font-bold text-transparent"
>
📊 Dashboard de Férias
</h1>
<p class="text-base-content/70 mt-2">Visualize seus saldos e histórico de solicitações</p>
</div>
{#if saldosQuery.isLoading || solicitacoesQuery.isLoading}
<!-- Loading Skeletons -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{#each Array(4) as _}
<div class="skeleton h-32 rounded-2xl"></div>
{/each}
</div>
{:else}
<!-- Cards de Estatísticas -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Card 1: Saldo Disponível -->
<div
class="stat bg-linear-to-br from-success/20 to-success/5 border-2 border-success/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<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"
></path>
</svg>
</div>
<div class="stat-title text-success font-semibold">Disponível</div>
<div class="stat-value text-success text-4xl">
{saldoAtual?.diasDisponiveis || 0}
</div>
<div class="stat-desc text-success/70">dias para usar</div>
</div>
{#if saldosQuery.isLoading || solicitacoesQuery.isLoading}
<!-- Loading Skeletons -->
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{#each Array(4)}
<div class="skeleton h-32 rounded-2xl"></div>
{/each}
</div>
{:else}
<!-- Cards de Estatísticas -->
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<!-- Card 1: Saldo Disponível -->
<div
class="stat from-success/20 to-success/5 border-success/30 rounded-2xl border-2 bg-linear-to-br shadow-2xl transition-all duration-300 hover:scale-105"
>
<div class="stat-figure text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-10 w-10 stroke-current"
>
<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"
></path>
</svg>
</div>
<div class="stat-title text-success font-semibold">Disponível</div>
<div class="stat-value text-success text-4xl">
{saldoAtual?.diasDisponiveis || 0}
</div>
<div class="stat-desc text-success/70">dias para usar</div>
</div>
<!-- Card 2: Dias Usados -->
<div
class="stat bg-linear-to-br from-error/20 to-error/5 border-2 border-error/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-error">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
<div class="stat-title text-error font-semibold">Usado</div>
<div class="stat-value text-error text-4xl">
{saldoAtual?.diasUsados || 0}
</div>
<div class="stat-desc text-error/70">dias já gozados</div>
</div>
<!-- Card 2: Dias Usados -->
<div
class="stat from-error/20 to-error/5 border-error/30 rounded-2xl border-2 bg-linear-to-br shadow-2xl transition-all duration-300 hover:scale-105"
>
<div class="stat-figure text-error">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-10 w-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
<div class="stat-title text-error font-semibold">Usado</div>
<div class="stat-value text-error text-4xl">
{saldoAtual?.diasUsados || 0}
</div>
<div class="stat-desc text-error/70">dias já gozados</div>
</div>
<!-- Card 3: Pendentes -->
<div
class="stat bg-linear-to-br from-warning/20 to-warning/5 border-2 border-warning/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<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"
></path>
</svg>
</div>
<div class="stat-title text-warning font-semibold">Pendentes</div>
<div class="stat-value text-warning text-4xl">
{saldoAtual?.diasPendentes || 0}
</div>
<div class="stat-desc text-warning/70">aguardando aprovação</div>
</div>
<!-- Card 3: Pendentes -->
<div
class="stat from-warning/20 to-warning/5 border-warning/30 rounded-2xl border-2 bg-linear-to-br shadow-2xl transition-all duration-300 hover:scale-105"
>
<div class="stat-figure text-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-10 w-10 stroke-current"
>
<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"
></path>
</svg>
</div>
<div class="stat-title text-warning font-semibold">Pendentes</div>
<div class="stat-value text-warning text-4xl">
{saldoAtual?.diasPendentes || 0}
</div>
<div class="stat-desc text-warning/70">aguardando aprovação</div>
</div>
<!-- Card 4: Total de Direito -->
<div
class="stat bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
>
<div class="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-10 h-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div class="stat-title text-primary font-semibold">Total Direito</div>
<div class="stat-value text-primary text-4xl">
{saldoAtual?.diasDireito || 0}
</div>
<div class="stat-desc text-primary/70">dias no ano</div>
</div>
</div>
<!-- Card 4: Total de Direito -->
<div
class="stat from-primary/20 to-primary/5 border-primary/30 rounded-2xl border-2 bg-linear-to-br shadow-2xl transition-all duration-300 hover:scale-105"
>
<div class="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-10 w-10 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div class="stat-title text-primary font-semibold">Total Direito</div>
<div class="stat-value text-primary text-4xl">
{saldoAtual?.diasDireito || 0}
</div>
<div class="stat-desc text-primary/70">dias no ano</div>
</div>
</div>
<!-- Gráficos -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Gráfico 1: Distribuição de Saldo -->
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
🥧 Distribuição de Saldo
<div class="badge badge-primary badge-lg">
Ano {saldoAtual?.anoReferencia || new Date().getFullYear()}
</div>
</h2>
<!-- Gráficos -->
<div class="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
<!-- Gráfico 1: Distribuição de Saldo -->
<div class="card bg-base-100 border-base-300 border-2 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">
🥧 Distribuição de Saldo
<div class="badge badge-primary badge-lg">
Ano {saldoAtual?.anoReferencia || new Date().getFullYear()}
</div>
</h2>
{#if saldoAtual}
<div class="flex items-center justify-center">
<canvas
bind:this={canvasSaldo}
width="300"
height="300"
class="max-w-full"
></canvas>
</div>
{#if saldoAtual}
<div class="flex items-center justify-center">
<canvas bind:this={canvasSaldo} width="300" height="300" class="max-w-full"></canvas>
</div>
<!-- Legenda -->
<div class="flex justify-center gap-4 mt-4 flex-wrap">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold"
>Disponível: {saldoAtual.diasDisponiveis} dias</span
>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold"
>Pendente: {saldoAtual.diasPendentes} dias</span
>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold"
>Usado: {saldoAtual.diasUsados} dias</span
>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhum saldo disponível para o ano atual</span>
</div>
{/if}
</div>
</div>
<!-- Legenda -->
<div class="mt-4 flex flex-wrap justify-center gap-4">
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold"
>Disponível: {saldoAtual.diasDisponiveis} dias</span
>
</div>
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold">Pendente: {saldoAtual.diasPendentes} dias</span>
</div>
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold">Usado: {saldoAtual.diasUsados} dias</span>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Nenhum saldo disponível para o ano atual</span>
</div>
{/if}
</div>
</div>
<!-- Gráfico 2: Status de Solicitações -->
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
📋 Status de Solicitações
<div class="badge badge-secondary badge-lg">
Total: {totalSolicitacoes}
</div>
</h2>
<!-- Gráfico 2: Status de Solicitações -->
<div class="card bg-base-100 border-base-300 border-2 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">
📋 Status de Solicitações
<div class="badge badge-secondary badge-lg">
Total: {totalSolicitacoes}
</div>
</h2>
{#if totalSolicitacoes > 0}
<div class="flex items-center justify-center">
<canvas
bind:this={canvasStatus}
width="300"
height="300"
class="max-w-full"
></canvas>
</div>
{#if totalSolicitacoes > 0}
<div class="flex items-center justify-center">
<canvas bind:this={canvasStatus} width="300" height="300" class="max-w-full"></canvas>
</div>
<!-- Legenda -->
<div class="flex justify-center gap-4 mt-4 flex-wrap">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span
>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold">Pendentes: {pendentes}</span
>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold"
>Reprovadas: {reprovadas}</span
>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<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 de férias ainda</span>
</div>
{/if}
</div>
</div>
</div>
<!-- Legenda -->
<div class="mt-4 flex flex-wrap justify-center gap-4">
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#51cf66]"></div>
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span>
</div>
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#ffa94d]"></div>
<span class="text-sm font-semibold">Pendentes: {pendentes}</span>
</div>
<div class="flex items-center gap-2">
<div class="h-4 w-4 rounded-full bg-[#ff6b6b]"></div>
<span class="text-sm font-semibold">Reprovadas: {reprovadas}</span>
</div>
</div>
{:else}
<div class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<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 de férias ainda</span>
</div>
{/if}
</div>
</div>
</div>
<!-- Histórico de Saldos -->
{#if saldos.length > 0}
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">📅 Histórico de Saldos</h2>
<!-- Histórico de Saldos -->
{#if saldos.length > 0}
<div class="card bg-base-100 border-base-300 border-2 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">📅 Histórico de Saldos</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Ano</th>
<th>Direito</th>
<th>Usado</th>
<th>Pendente</th>
<th>Disponível</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each saldos as saldo}
<tr>
<td class="font-bold">{saldo.anoReferencia}</td>
<td>{saldo.diasDireito} dias</td>
<td
><span class="badge badge-error">{saldo.diasUsados}</span
></td
>
<td
><span class="badge badge-warning"
>{saldo.diasPendentes}</span
></td
>
<td
><span class="badge badge-success"
>{saldo.diasDisponiveis}</span
></td
>
<td>
{#if saldo.status === "ativo"}
<span class="badge badge-success">Ativo</span>
{:else if saldo.status === "vencido"}
<span class="badge badge-error">Vencido</span>
{:else}
<span class="badge badge-neutral">Concluído</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
{/if}
<div class="overflow-x-auto">
<table class="table-zebra table">
<thead>
<tr>
<th>Ano</th>
<th>Direito</th>
<th>Usado</th>
<th>Pendente</th>
<th>Disponível</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each saldos as saldo (saldo._id)}
<tr>
<td class="font-bold">{saldo.anoReferencia}</td>
<td>{saldo.diasDireito} dias</td>
<td><span class="badge badge-error">{saldo.diasUsados}</span></td>
<td><span class="badge badge-warning">{saldo.diasPendentes}</span></td>
<td><span class="badge badge-success">{saldo.diasDisponiveis}</span></td>
<td>
{#if saldo.status === 'ativo'}
<span class="badge badge-success">Ativo</span>
{:else if saldo.status === 'vencido'}
<span class="badge badge-error">Vencido</span>
{:else}
<span class="badge badge-neutral">Concluído</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
{/if}
</div>
<style>
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
</style>

View File

@@ -116,8 +116,9 @@
toast.success('Solicitação de férias enviada com sucesso! 🎉');
if (onSucesso) onSucesso();
} catch (error: any) {
toast.error(error.message || 'Erro ao enviar solicitação');
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
toast.error(errorMessage || 'Erro ao enviar solicitação');
} finally {
processando = false;
}
@@ -139,7 +140,11 @@
<!-- Progress Bar -->
<div class="mb-8">
<div class="flex items-center justify-between">
<<<<<<< HEAD
{#each Array(totalPassos) as _, i}
=======
{#each Array(totalPassos) as _, i (i)}
>>>>>>> origin
<div class="flex flex-1 items-center">
<!-- Círculo do passo -->
<div
@@ -211,7 +216,11 @@
<!-- Seletor de Ano -->
<div class="mb-8 grid grid-cols-3 gap-4">
<<<<<<< HEAD
{#each anosDisponiveis as ano}
=======
{#each anosDisponiveis as ano (ano)}
>>>>>>> origin
<button
type="button"
class="btn btn-lg transition-all duration-300 hover:scale-105"
@@ -451,7 +460,11 @@
<div>
<p class="font-bold">Erros encontrados:</p>
<ul class="list-inside list-disc">
<<<<<<< HEAD
{#each validacao.erros as erro}
=======
{#each validacao.erros as erro (erro)}
>>>>>>> origin
<li>{erro}</li>
{/each}
</ul>
@@ -477,7 +490,11 @@
<div>
<p class="font-bold">Avisos:</p>
<ul class="list-inside list-disc">
<<<<<<< HEAD
{#each validacao.avisos as aviso}
=======
{#each validacao.avisos as aviso (aviso)}
>>>>>>> origin
<li>{aviso}</li>
{/each}
</ul>
@@ -519,7 +536,11 @@
<h4 class="mb-2 text-lg font-bold">Períodos Selecionados:</h4>
<div class="space-y-3">
<<<<<<< HEAD
{#each periodosFerias as periodo, index}
=======
{#each periodosFerias as periodo, index (index)}
>>>>>>> origin
<div class="bg-base-200 flex items-center gap-4 rounded-lg p-4">
<div
class="badge badge-lg badge-primary flex h-12 w-12 items-center justify-center font-bold text-white"
@@ -588,7 +609,11 @@
Voltar
</button>
{:else if onCancelar}
<<<<<<< HEAD
<button type="button" class="btn btn-lg" onclick={onCancelar}> Cancelar </button>
=======
<button type="button" class="btn btn-ghost btn-lg" onclick={onCancelar}> Cancelar </button>
>>>>>>> origin
{/if}
</div>