Files
sgse-app/apps/web/src/routes/(dashboard)/gestao-pessoas/gestao-ausencias/+page.svelte

896 lines
27 KiB
Svelte

<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import { parseLocalDate } from '$lib/utils/datas';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import ExcelJS from 'exceljs';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { FileDown, FileSpreadsheet } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
// Buscar TODAS as solicitações de ausências
const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {});
// Buscar funcionários para filtro
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
let filtroStatus = $state<string>('todos');
let filtroFuncionario = $state<string>('');
let filtroPeriodoInicio = $state<string>('');
let filtroPeriodoFim = $state<string>('');
let gerandoRelatorio = $state(false);
let solicitacaoSelecionada = $state<Id<'solicitacoesAusencias'> | null>(null);
const ausencias = $derived(todasAusenciasQuery?.data || []);
const funcionarios = $derived(
Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
);
// Filtrar solicitações
let ausenciasFiltradas = $derived(
ausencias.filter((a) => {
// Filtro de status
if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false;
// Filtro por funcionário
if (filtroFuncionario) {
if (a.funcionario?._id !== filtroFuncionario) return false;
}
// Filtro por período
if (filtroPeriodoInicio) {
const inicioFiltro = new Date(filtroPeriodoInicio);
const inicioAusencia = parseLocalDate(a.dataInicio);
if (inicioAusencia < inicioFiltro) return false;
}
if (filtroPeriodoFim) {
const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false;
}
return true;
})
);
// Estatísticas gerais
let stats = $derived({
total: ausencias.length,
aguardando: ausencias.filter((a) => a.status === 'aguardando_aprovacao').length,
aprovadas: ausencias.filter((a) => a.status === 'aprovado').length,
reprovadas: ausencias.filter((a) => a.status === 'reprovado').length
});
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
aguardando_aprovacao: 'badge-warning',
aprovado: 'badge-success',
reprovado: 'badge-error'
};
return badges[status] || 'badge-neutral';
}
function getStatusTexto(status: string) {
const textos: Record<string, string> = {
aguardando_aprovacao: 'Aguardando',
aprovado: 'Aprovado',
reprovado: 'Reprovado'
};
return textos[status] || status;
}
function calcularDias(dataInicio: string, dataFim: string): number {
const inicio = parseLocalDate(dataInicio);
const fim = parseLocalDate(dataFim);
const diff = fim.getTime() - inicio.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
}
async function selecionarSolicitacao(solicitacaoId: Id<'solicitacoesAusencias'>) {
solicitacaoSelecionada = solicitacaoId;
}
async function recarregar() {
solicitacaoSelecionada = null;
}
// Função para gerar PDF
async function gerarPDF() {
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('RELATÓRIO DE AUSÊNCIAS', 105, yPosition, { align: 'center' });
yPosition += 10;
// Filtros aplicados
doc.setFontSize(10);
doc.setTextColor(0, 0, 0);
let filtrosTexto = 'Filtros aplicados: ';
if (filtroStatus !== 'todos') filtrosTexto += `Status: ${getStatusTexto(filtroStatus)}; `;
if (filtroFuncionario) {
const func = funcionarios.find((f) => f._id === filtroFuncionario);
filtrosTexto += `Funcionário: ${func?.nome || ''}; `;
}
if (filtroPeriodoInicio && filtroPeriodoFim) {
filtrosTexto += `Período: ${format(new Date(filtroPeriodoInicio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(filtroPeriodoFim), 'dd/MM/yyyy', { locale: ptBR })}; `;
}
if (filtrosTexto === 'Filtros aplicados: ') {
filtrosTexto = 'Todos os registros';
}
doc.text(filtrosTexto, 105, yPosition, { align: 'center', maxWidth: 180 });
yPosition += 8;
// Data de geração
doc.setFontSize(9);
doc.setTextColor(100, 100, 100);
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[][] = ausenciasFiltradas.map((a) => [
a.funcionario?.nome || '-',
a.funcionario?.matricula || '-',
a.time?.nome || '-',
parseLocalDate(a.dataInicio).toLocaleDateString('pt-BR'),
parseLocalDate(a.dataFim).toLocaleDateString('pt-BR'),
calcularDias(a.dataInicio, a.dataFim).toString(),
a.motivo.substring(0, 40) + (a.motivo.length > 40 ? '...' : ''),
getStatusTexto(a.status)
]);
// Tabela
if (dadosTabela.length > 0) {
autoTable(doc, {
startY: yPosition,
head: [
[
'Funcionário',
'Matrícula',
'Time',
'Data Início',
'Data Fim',
'Dias',
'Motivo',
'Status'
]
],
body: dadosTabela,
theme: 'striped',
headStyles: { fillColor: [41, 128, 185], textColor: 255, fontSize: 7, fontStyle: 'bold' },
bodyStyles: { fontSize: 7 },
alternateRowStyles: { fillColor: [245, 247, 250] },
styles: { cellPadding: 1.5, overflow: 'linebreak' },
margin: { top: yPosition, left: 10, right: 10 },
tableWidth: 'wrap',
columnStyles: {
0: { cellWidth: 38, fontSize: 7 }, // Funcionário
1: { cellWidth: 18, fontSize: 7 }, // Matrícula
2: { cellWidth: 22, fontSize: 7 }, // Time
3: { cellWidth: 20, fontSize: 7 }, // Data Início
4: { cellWidth: 20, fontSize: 7 }, // Data Fim
5: { cellWidth: 12, fontSize: 7 }, // Dias
6: { cellWidth: 35, fontSize: 7, overflow: 'linebreak' }, // Motivo
7: { cellWidth: 18, fontSize: 7 } // Status
}
});
} else {
doc.setFontSize(12);
doc.setTextColor(100, 100, 100);
doc.text('Nenhum registro encontrado com os filtros aplicados.', 105, yPosition + 20, {
align: 'center'
});
}
// Footer em todas as páginas
const pageCount = doc.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(7);
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() - 8,
{ align: 'center' }
);
}
// Salvar
const nomeArquivo = `relatorio-ausencias-${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 gerarExcel() {
if (ausenciasFiltradas.length === 0) {
toast.error('Não há ausências para exportar com os filtros aplicados.');
return;
}
gerandoRelatorio = true;
try {
// Preparar dados
const dados: Array<Record<string, string | number>> = ausenciasFiltradas.map((a) => ({
Funcionário: a.funcionario?.nome || '-',
Matrícula: a.funcionario?.matricula || '-',
Time: a.time?.nome || '-',
'Data Início': parseLocalDate(a.dataInicio).toLocaleDateString('pt-BR'),
'Data Fim': parseLocalDate(a.dataFim).toLocaleDateString('pt-BR'),
Dias: calcularDias(a.dataInicio, a.dataFim),
Motivo: a.motivo,
Status: getStatusTexto(a.status),
'Solicitado em': new Date(a.criadoEm).toLocaleDateString('pt-BR')
}));
// Criar workbook com ExcelJS
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Relatório de Ausências');
// 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 = 'RELATÓRIO DE AUSÊNCIAS';
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 === 'Solicitado em'
) {
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') {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFD4EDDA' }
};
cell.font = { size: 10, color: { argb: 'FF155724' } };
} else if (statusValue === 'Reprovado') {
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 }, // Data Início
{ width: 12 }, // Data Fim
{ width: 8 }, // Dias
{ width: 40 }, // Motivo
{ width: 15 }, // Status
{ width: 15 } // Solicitado em
];
// Congelar linha 2 (cabeçalho da tabela)
worksheet.views = [
{
state: 'frozen',
ySplit: 2,
topLeftCell: 'A3',
activeCell: 'A3'
}
];
// Gerar arquivo
const nomeArquivo = `relatorio-ausencias-${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;
}
}
</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('/gestao-pessoas')} class="text-primary hover:underline"
>Secretaria de Gestão de Pessoas</a
>
</li>
<li>Gestão de Ausências</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-orange-500/20 p-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-orange-600"
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>
<h1 class="text-primary text-3xl font-bold">Gestão de Ausências</h1>
<p class="text-base-content/70">Visão geral de todas as solicitações de ausências</p>
</div>
</div>
<button class="btn gap-2" onclick={() => goto(resolve('/gestao-pessoas'))}>
<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>
<!-- Estatísticas -->
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="stat bg-base-100 rounded-box border-base-300 border shadow-lg">
<div class="stat-figure text-orange-500">
<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-orange-500">{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">Pendentes</div>
<div class="stat-value text-warning">{stats.aguardando}</div>
<div class="stat-desc">Aguardando</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>
</div>
<!-- Filtros -->
<div class="card bg-base-100 mb-6 shadow-lg">
<div class="card-body">
<div class="mb-4 flex items-center justify-between">
<h2 class="card-title text-lg">Filtros</h2>
<div class="flex gap-2">
<button
type="button"
class="btn btn-primary btn-sm gap-2"
onclick={gerarPDF}
disabled={gerandoRelatorio || ausenciasFiltradas.length === 0}
>
{#if gerandoRelatorio}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<FileDown class="h-4 w-4" strokeWidth={2} />
{/if}
Gerar PDF
</button>
<button
type="button"
class="btn btn-success btn-sm gap-2"
onclick={gerarExcel}
disabled={gerandoRelatorio || ausenciasFiltradas.length === 0}
>
{#if gerandoRelatorio}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<FileSpreadsheet class="h-4 w-4" strokeWidth={2} />
{/if}
Gerar Excel
</button>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="form-control">
<label class="label" for="filtro-status">
<span class="label-text">Status</span>
</label>
<select id="filtro-status" class="select select-bordered" bind:value={filtroStatus}>
<option value="todos">Todos</option>
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
<option value="aprovado">Aprovado</option>
<option value="reprovado">Reprovado</option>
</select>
</div>
<div class="form-control">
<label class="label" for="filtro-funcionario">
<span class="label-text">Funcionário</span>
</label>
<select
id="filtro-funcionario"
class="select select-bordered"
bind:value={filtroFuncionario}
>
<option value="">Todos</option>
{#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
</select>
</div>
<div class="form-control">
<label class="label" for="filtro-periodo-inicio">
<span class="label-text">Período - Início</span>
</label>
<input
id="filtro-periodo-inicio"
type="date"
class="input input-bordered"
bind:value={filtroPeriodoInicio}
/>
</div>
<div class="form-control">
<label class="label" for="filtro-periodo-fim">
<span class="label-text">Período - Fim</span>
</label>
<input
id="filtro-periodo-fim"
type="date"
class="input input-bordered"
bind:value={filtroPeriodoFim}
/>
</div>
</div>
</div>
</div>
<!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title mb-4 text-lg">
Todas as Solicitações ({ausenciasFiltradas.length})
</h2>
{#if ausenciasFiltradas.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>
<tr>
<th>Funcionário</th>
<th>Time</th>
<th>Período</th>
<th>Dias</th>
<th>Motivo</th>
<th>Status</th>
<th>Solicitado em</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr>
<td class="font-semibold">
{ausencia.funcionario?.nome || 'N/A'}
</td>
<td>
{#if ausencia.time}
<div
class="badge badge-sm max-w-full overflow-hidden font-semibold text-ellipsis whitespace-nowrap"
style="background-color: {ausencia.time.cor}20; border-color: {ausencia.time
.cor}; color: {ausencia.time.cor}"
title={ausencia.time.nome}
>
{ausencia.time.nome}
</div>
{:else}
<span class="text-base-content/50">Sem time</span>
{/if}
</td>
<td>
{parseLocalDate(ausencia.dataInicio).toLocaleDateString('pt-BR')} até{' '}
{parseLocalDate(ausencia.dataFim).toLocaleDateString('pt-BR')}
</td>
<td class="font-bold">
{calcularDias(ausencia.dataInicio, ausencia.dataFim)} dias
</td>
<td class="max-w-xs truncate" title={ausencia.motivo}>
{ausencia.motivo}
</td>
<td>
<div class={`badge ${getStatusBadge(ausencia.status)}`}>
{getStatusTexto(ausencia.status)}
</div>
</td>
<td class="text-xs">
{new Date(ausencia.criadoEm).toLocaleDateString('pt-BR')}
</td>
<td>
{#if ausencia.status === 'aguardando_aprovacao'}
<button
type="button"
class="btn btn-primary btn-sm gap-2"
onclick={() => selecionarSolicitacao(ausencia._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>
Ver Detalhes
</button>
{:else}
<button
type="button"
class="btn btn-sm gap-2"
onclick={() => selecionarSolicitacao(ausencia._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>
Ver Detalhes
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
</main>
<!-- Modal de Aprovação -->
{#if solicitacaoSelecionada && currentUser.data}
{#await client.query( api.ausencias.obterDetalhes, { solicitacaoId: solicitacaoSelecionada } ) then detalhes}
{#if detalhes}
<dialog class="modal modal-open">
<div class="modal-box max-h-[95vh] w-full max-w-2xl overflow-y-auto p-4 md:p-6">
<AprovarAusencias
solicitacao={detalhes}
gestorId={currentUser.data._id}
onSucesso={recarregar}
onCancelar={() => (solicitacaoSelecionada = null)}
/>
</div>
<form method="dialog" class="modal-backdrop">
<button
type="button"
onclick={() => (solicitacaoSelecionada = null)}
aria-label="Fechar modal">Fechar</button
>
</form>
</dialog>
{/if}
{/await}
{/if}