feat: enhance absence management with new filters and reporting options, including PDF and Excel generation capabilities
This commit is contained in:
@@ -6,23 +6,61 @@
|
||||
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
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';
|
||||
|
||||
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
|
||||
const 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 Date(filtroPeriodoFim);
|
||||
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
|
||||
const fimAusencia = parseLocalDate(a.dataFim);
|
||||
if (fimAusencia > fimFiltro) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
@@ -67,6 +105,383 @@
|
||||
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">
|
||||
@@ -221,8 +636,38 @@
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 mb-6 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4 text-lg">Filtros</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<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>
|
||||
@@ -234,6 +679,43 @@
|
||||
<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}
|
||||
<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>
|
||||
@@ -286,9 +768,10 @@
|
||||
<td>
|
||||
{#if ausencia.time}
|
||||
<div
|
||||
class="badge badge-sm font-semibold"
|
||||
class="badge badge-sm font-semibold max-w-full overflow-hidden 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>
|
||||
@@ -388,7 +871,7 @@
|
||||
{#await client.query( api.ausencias.obterDetalhes, { solicitacaoId: solicitacaoSelecionada } ) then detalhes}
|
||||
{#if detalhes}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl">
|
||||
<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}
|
||||
|
||||
Reference in New Issue
Block a user