1254 lines
39 KiB
Svelte
1254 lines
39 KiB
Svelte
<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 {
|
|
Clock,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Calendar,
|
|
AlertTriangle,
|
|
Info,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
History,
|
|
Edit,
|
|
Settings,
|
|
Download,
|
|
FileText,
|
|
CheckCircle2,
|
|
XCircle,
|
|
FileCheck,
|
|
FileX,
|
|
AlertCircle
|
|
} from 'lucide-svelte';
|
|
import LineChart from '$lib/components/ti/charts/LineChart.svelte';
|
|
import jsPDF from 'jspdf';
|
|
import * as ExcelJS from 'exceljs';
|
|
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
|
|
|
interface Props {
|
|
funcionarioId: Id<'funcionarios'>;
|
|
}
|
|
|
|
let { funcionarioId }: Props = $props();
|
|
|
|
// Estado para mês selecionado
|
|
const hoje = new Date();
|
|
let mesSelecionado = $state(
|
|
`${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`
|
|
);
|
|
|
|
// Query para banco de horas mensal
|
|
const bancoMensalQuery = useQuery(api.pontos.obterBancoHorasMensal, {
|
|
funcionarioId,
|
|
mes: mesSelecionado
|
|
});
|
|
|
|
// Query para histórico mensal (últimos 6 meses)
|
|
const historicoQuery = useQuery(api.pontos.listarHistoricoMensal, {
|
|
funcionarioId,
|
|
mesInicio: (() => {
|
|
const data = new Date(mesSelecionado + '-01');
|
|
data.setMonth(data.getMonth() - 5);
|
|
return `${data.getFullYear()}-${String(data.getMonth() + 1).padStart(2, '0')}`;
|
|
})(),
|
|
mesFim: mesSelecionado
|
|
});
|
|
|
|
// Query para histórico de alterações
|
|
const historicoAlteracoesQuery = useQuery(api.pontos.listarHistoricoAlteracoesBancoHoras, {
|
|
funcionarioId,
|
|
mes: mesSelecionado
|
|
});
|
|
|
|
// Query para ajustes do banco de horas
|
|
const ajustesQuery = useQuery(api.pontos.listarAjustesBancoHoras, {
|
|
funcionarioId,
|
|
dataInicio: `${mesSelecionado}-01`,
|
|
dataFim: (() => {
|
|
const data = new Date(mesSelecionado + '-01');
|
|
data.setMonth(data.getMonth() + 1);
|
|
data.setDate(0); // Último dia do mês
|
|
return data.toISOString().split('T')[0]!;
|
|
})()
|
|
});
|
|
|
|
// Query para inconsistências
|
|
const inconsistenciasQuery = useQuery(api.pontos.verificarInconsistencias, {
|
|
funcionarioId,
|
|
dataInicio: `${mesSelecionado}-01`,
|
|
dataFim: (() => {
|
|
const data = new Date(mesSelecionado + '-01');
|
|
data.setMonth(data.getMonth() + 1);
|
|
data.setDate(0); // Último dia do mês
|
|
return data.toISOString().split('T')[0]!;
|
|
})()
|
|
});
|
|
|
|
const bancoMensal = $derived(bancoMensalQuery?.data);
|
|
const historico = $derived(historicoQuery?.data || []);
|
|
const historicoAlteracoes = $derived(historicoAlteracoesQuery?.data || []);
|
|
const ajustes = $derived(ajustesQuery?.data || []);
|
|
const inconsistencias = $derived(inconsistenciasQuery?.data || []);
|
|
|
|
// Dados para o gráfico de evolução
|
|
const chartData = $derived(() => {
|
|
if (!historico || historico.length === 0) return null;
|
|
|
|
// Ordenar por mês (crescente)
|
|
const historicoOrdenado = [...historico].sort((a, b) => {
|
|
if (a.mes < b.mes) return -1;
|
|
if (a.mes > b.mes) return 1;
|
|
return 0;
|
|
});
|
|
|
|
return {
|
|
labels: historicoOrdenado.map((h) => {
|
|
const [ano, mesNum] = h.mes.split('-');
|
|
const data = new Date(parseInt(ano), parseInt(mesNum) - 1, 1);
|
|
return data.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' });
|
|
}),
|
|
datasets: [
|
|
{
|
|
label: 'Saldo Final (horas)',
|
|
data: historicoOrdenado.map((h) => h.saldoFinalMinutos / 60),
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}
|
|
]
|
|
};
|
|
});
|
|
|
|
// Função para formatar mês
|
|
function formatarMes(mes: string): string {
|
|
const [ano, mesNum] = mes.split('-');
|
|
const data = new Date(parseInt(ano), parseInt(mesNum) - 1, 1);
|
|
return data.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
|
|
}
|
|
|
|
// Função para navegar entre meses
|
|
function mudarMes(direcao: 'anterior' | 'proximo') {
|
|
const data = new Date(mesSelecionado + '-01');
|
|
if (direcao === 'anterior') {
|
|
data.setMonth(data.getMonth() - 1);
|
|
} else {
|
|
data.setMonth(data.getMonth() + 1);
|
|
}
|
|
mesSelecionado = `${data.getFullYear()}-${String(data.getMonth() + 1).padStart(2, '0')}`;
|
|
}
|
|
|
|
// Verificar se há saldo negativo acumulado
|
|
const saldoNegativo = $derived(
|
|
bancoMensal?.saldoFinalMinutos !== undefined && bancoMensal.saldoFinalMinutos < 0
|
|
);
|
|
|
|
// Função para exportar relatório em PDF
|
|
async function exportarPDF() {
|
|
if (!bancoMensal || !historico) return;
|
|
|
|
const doc = new jsPDF('portrait', 'mm', 'a4');
|
|
let yPosition = 20;
|
|
|
|
// Logo (se disponível)
|
|
try {
|
|
const logoImg = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
const img = new Image();
|
|
img.crossOrigin = 'anonymous';
|
|
img.onload = () => resolve(img);
|
|
img.onerror = reject;
|
|
img.src = logoGovPE;
|
|
});
|
|
|
|
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 BANCO DE HORAS', 105, yPosition, { align: 'center' });
|
|
|
|
yPosition += 10;
|
|
|
|
// Mês de referência
|
|
doc.setFontSize(11);
|
|
doc.setTextColor(0, 0, 0);
|
|
doc.text(`Mês de Referência: ${formatarMes(mesSelecionado)}`, 105, yPosition, {
|
|
align: 'center'
|
|
});
|
|
|
|
yPosition += 15;
|
|
|
|
// Resumo do Mês
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(41, 128, 185);
|
|
doc.text('Resumo do Mês', 15, yPosition);
|
|
|
|
yPosition += 8;
|
|
|
|
doc.setFontSize(10);
|
|
doc.setTextColor(0, 0, 0);
|
|
|
|
const formatarMinutos = (minutos: number) => {
|
|
const horas = Math.floor(Math.abs(minutos) / 60);
|
|
const mins = Math.abs(minutos) % 60;
|
|
const sinal = minutos >= 0 ? '+' : '-';
|
|
return `${sinal}${horas}h ${mins}min`;
|
|
};
|
|
|
|
doc.text(`Saldo Inicial: ${formatarMinutos(bancoMensal.saldoInicialMinutos)}`, 20, yPosition);
|
|
yPosition += 6;
|
|
doc.text(`Saldo do Mês: ${formatarMinutos(bancoMensal.saldoMesMinutos)}`, 20, yPosition);
|
|
yPosition += 6;
|
|
doc.text(`Saldo Final: ${formatarMinutos(bancoMensal.saldoFinalMinutos)}`, 20, yPosition);
|
|
yPosition += 6;
|
|
doc.text(`Dias Trabalhados: ${bancoMensal.diasTrabalhados} dias`, 20, yPosition);
|
|
yPosition += 6;
|
|
doc.text(`Horas Extras: ${formatarMinutos(bancoMensal.horasExtras)}`, 20, yPosition);
|
|
yPosition += 6;
|
|
doc.text(`Déficit: ${formatarMinutos(-bancoMensal.horasDeficit)}`, 20, yPosition);
|
|
|
|
// Adicionar informações de ajustes se disponíveis
|
|
if (bancoMensal.totalAjustes !== undefined && bancoMensal.totalAjustes > 0) {
|
|
yPosition += 6;
|
|
doc.text(`Total de Ajustes: ${formatarMinutos(bancoMensal.totalAjustes)}`, 20, yPosition);
|
|
}
|
|
if (bancoMensal.totalAbonos !== undefined && bancoMensal.totalAbonos > 0) {
|
|
yPosition += 6;
|
|
doc.text(`Total de Abonos: ${formatarMinutos(bancoMensal.totalAbonos)}`, 20, yPosition);
|
|
}
|
|
if (bancoMensal.totalDescontos !== undefined && bancoMensal.totalDescontos > 0) {
|
|
yPosition += 6;
|
|
doc.text(`Total de Descontos: ${formatarMinutos(-bancoMensal.totalDescontos)}`, 20, yPosition);
|
|
}
|
|
if (bancoMensal.inconsistenciasResolvidas !== undefined) {
|
|
yPosition += 6;
|
|
doc.text(`Inconsistências Resolvidas: ${bancoMensal.inconsistenciasResolvidas}`, 20, yPosition);
|
|
}
|
|
|
|
yPosition += 15;
|
|
|
|
// Histórico dos Últimos 6 Meses
|
|
if (historico.length > 0) {
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(41, 128, 185);
|
|
doc.text('Histórico dos Últimos 6 Meses', 15, yPosition);
|
|
|
|
yPosition += 8;
|
|
|
|
// Cabeçalho da tabela
|
|
doc.setFontSize(9);
|
|
doc.setTextColor(100, 100, 100);
|
|
doc.text('Mês', 20, yPosition);
|
|
doc.text('Saldo Inicial', 60, yPosition);
|
|
doc.text('Saldo Mês', 100, yPosition);
|
|
doc.text('Saldo Final', 140, yPosition);
|
|
doc.text('Dias', 180, yPosition);
|
|
|
|
yPosition += 6;
|
|
|
|
// Linhas da tabela
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(0, 0, 0);
|
|
|
|
for (const item of historico.slice(0, 6)) {
|
|
if (yPosition > 250) {
|
|
doc.addPage();
|
|
yPosition = 20;
|
|
}
|
|
|
|
doc.text(formatarMes(item.mes), 20, yPosition);
|
|
doc.text(formatarMinutos(item.saldoInicialMinutos), 60, yPosition);
|
|
doc.text(formatarMinutos(item.saldoMesMinutos), 100, yPosition);
|
|
doc.text(formatarMinutos(item.saldoFinalMinutos), 140, yPosition);
|
|
doc.text(item.diasTrabalhados.toString(), 180, yPosition);
|
|
|
|
yPosition += 6;
|
|
}
|
|
}
|
|
|
|
// Ajustes Aplicados
|
|
if (ajustes && ajustes.length > 0) {
|
|
yPosition += 10;
|
|
if (yPosition > 250) {
|
|
doc.addPage();
|
|
yPosition = 20;
|
|
}
|
|
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(41, 128, 185);
|
|
doc.text('Ajustes Aplicados', 15, yPosition);
|
|
|
|
yPosition += 8;
|
|
|
|
// Cabeçalho da tabela
|
|
doc.setFontSize(9);
|
|
doc.setTextColor(100, 100, 100);
|
|
doc.text('Data', 20, yPosition);
|
|
doc.text('Tipo', 50, yPosition);
|
|
doc.text('Motivo', 80, yPosition);
|
|
doc.text('Valor', 150, yPosition);
|
|
|
|
yPosition += 6;
|
|
|
|
// Linhas da tabela
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(0, 0, 0);
|
|
|
|
for (const ajuste of ajustes.slice(0, 10)) {
|
|
if (yPosition > 250) {
|
|
doc.addPage();
|
|
yPosition = 20;
|
|
}
|
|
|
|
doc.text(
|
|
new Date(ajuste.dataAplicacao).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit'
|
|
}),
|
|
20,
|
|
yPosition
|
|
);
|
|
doc.text(
|
|
ajuste.tipo === 'abonar' ? 'Abonar' : ajuste.tipo === 'descontar' ? 'Descontar' : 'Compensar',
|
|
50,
|
|
yPosition
|
|
);
|
|
doc.text(
|
|
(ajuste.motivoDescricao || ajuste.motivoTipo || 'N/A').substring(0, 30),
|
|
80,
|
|
yPosition
|
|
);
|
|
doc.text(formatarMinutos(ajuste.valorMinutos), 150, yPosition);
|
|
|
|
yPosition += 6;
|
|
}
|
|
}
|
|
|
|
// Inconsistências
|
|
if (inconsistencias && inconsistencias.length > 0) {
|
|
yPosition += 10;
|
|
if (yPosition > 250) {
|
|
doc.addPage();
|
|
yPosition = 20;
|
|
}
|
|
|
|
doc.setFontSize(14);
|
|
doc.setTextColor(255, 152, 0);
|
|
doc.text('Inconsistências Detectadas', 15, yPosition);
|
|
|
|
yPosition += 8;
|
|
|
|
// Cabeçalho da tabela
|
|
doc.setFontSize(9);
|
|
doc.setTextColor(100, 100, 100);
|
|
doc.text('Data', 20, yPosition);
|
|
doc.text('Tipo', 60, yPosition);
|
|
doc.text('Status', 120, yPosition);
|
|
|
|
yPosition += 6;
|
|
|
|
// Linhas da tabela
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(0, 0, 0);
|
|
|
|
for (const inconsistencia of inconsistencias.slice(0, 10)) {
|
|
if (yPosition > 250) {
|
|
doc.addPage();
|
|
yPosition = 20;
|
|
}
|
|
|
|
doc.text(
|
|
new Date(inconsistencia.dataDetectada).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit'
|
|
}),
|
|
20,
|
|
yPosition
|
|
);
|
|
doc.text(
|
|
inconsistencia.tipo === 'ponto_com_atestado'
|
|
? 'Ponto + Atestado'
|
|
: inconsistencia.tipo === 'ponto_com_licenca'
|
|
? 'Ponto + Licença'
|
|
: inconsistencia.tipo === 'ponto_com_ausencia'
|
|
? 'Ponto + Ausência'
|
|
: inconsistencia.tipo,
|
|
60,
|
|
yPosition
|
|
);
|
|
doc.text(
|
|
inconsistencia.status === 'resolvida'
|
|
? 'Resolvida'
|
|
: inconsistencia.status === 'ignorada'
|
|
? 'Ignorada'
|
|
: 'Pendente',
|
|
120,
|
|
yPosition
|
|
);
|
|
|
|
yPosition += 6;
|
|
}
|
|
}
|
|
|
|
// Rodapé
|
|
const pageCount = doc.getNumberOfPages();
|
|
for (let i = 1; i <= pageCount; i++) {
|
|
doc.setPage(i);
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(100, 100, 100);
|
|
doc.text(
|
|
`Gerado em: ${new Date().toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}`,
|
|
105,
|
|
285,
|
|
{ align: 'center' }
|
|
);
|
|
doc.text(`Página ${i} de ${pageCount}`, 105, 290, { align: 'center' });
|
|
}
|
|
|
|
// Salvar PDF
|
|
doc.save(`banco-horas-${mesSelecionado}.pdf`);
|
|
}
|
|
|
|
// Função para exportar relatório em Excel
|
|
async function exportarExcel() {
|
|
if (!bancoMensal || !historico) return;
|
|
|
|
const workbook = new ExcelJS.Workbook();
|
|
workbook.creator = 'SGSE - Sistema de Gerenciamento';
|
|
workbook.created = new Date();
|
|
|
|
// Planilha 1: Resumo Mensal
|
|
const resumoSheet = workbook.addWorksheet('Resumo Mensal');
|
|
resumoSheet.columns = [
|
|
{ header: 'Item', key: 'item', width: 30 },
|
|
{ header: 'Valor', key: 'valor', width: 20 }
|
|
];
|
|
|
|
resumoSheet.addRow({ item: 'Mês de Referência', valor: formatarMes(mesSelecionado) });
|
|
resumoSheet.addRow({ item: 'Saldo Inicial', valor: `${Math.floor(bancoMensal.saldoInicialMinutos / 60)}h ${Math.abs(bancoMensal.saldoInicialMinutos) % 60}min` });
|
|
resumoSheet.addRow({ item: 'Saldo do Mês', valor: `${Math.floor(bancoMensal.saldoMesMinutos / 60)}h ${Math.abs(bancoMensal.saldoMesMinutos) % 60}min` });
|
|
resumoSheet.addRow({ item: 'Saldo Final', valor: `${Math.floor(bancoMensal.saldoFinalMinutos / 60)}h ${Math.abs(bancoMensal.saldoFinalMinutos) % 60}min` });
|
|
resumoSheet.addRow({ item: 'Dias Trabalhados', valor: `${bancoMensal.diasTrabalhados} dias` });
|
|
resumoSheet.addRow({ item: 'Horas Extras', valor: `${Math.floor(bancoMensal.horasExtras / 60)}h ${bancoMensal.horasExtras % 60}min` });
|
|
resumoSheet.addRow({ item: 'Déficit', valor: `${Math.floor(bancoMensal.horasDeficit / 60)}h ${bancoMensal.horasDeficit % 60}min` });
|
|
|
|
if (bancoMensal.totalAjustes !== undefined) {
|
|
resumoSheet.addRow({ item: 'Total de Ajustes', valor: `${Math.floor(bancoMensal.totalAjustes / 60)}h ${bancoMensal.totalAjustes % 60}min` });
|
|
}
|
|
if (bancoMensal.totalAbonos !== undefined) {
|
|
resumoSheet.addRow({ item: 'Total de Abonos', valor: `${Math.floor(bancoMensal.totalAbonos / 60)}h ${bancoMensal.totalAbonos % 60}min` });
|
|
}
|
|
if (bancoMensal.totalDescontos !== undefined) {
|
|
resumoSheet.addRow({ item: 'Total de Descontos', valor: `${Math.floor(bancoMensal.totalDescontos / 60)}h ${bancoMensal.totalDescontos % 60}min` });
|
|
}
|
|
|
|
// Planilha 2: Histórico
|
|
if (historico.length > 0) {
|
|
const historicoSheet = workbook.addWorksheet('Histórico');
|
|
historicoSheet.columns = [
|
|
{ header: 'Mês', key: 'mes', width: 20 },
|
|
{ header: 'Saldo Inicial', key: 'saldoInicial', width: 15 },
|
|
{ header: 'Saldo do Mês', key: 'saldoMes', width: 15 },
|
|
{ header: 'Saldo Final', key: 'saldoFinal', width: 15 },
|
|
{ header: 'Dias Trabalhados', key: 'dias', width: 15 }
|
|
];
|
|
|
|
historico.forEach((item) => {
|
|
historicoSheet.addRow({
|
|
mes: formatarMes(item.mes),
|
|
saldoInicial: `${Math.floor(item.saldoInicialMinutos / 60)}h ${Math.abs(item.saldoInicialMinutos) % 60}min`,
|
|
saldoMes: `${Math.floor(item.saldoMesMinutos / 60)}h ${Math.abs(item.saldoMesMinutos) % 60}min`,
|
|
saldoFinal: `${Math.floor(item.saldoFinalMinutos / 60)}h ${Math.abs(item.saldoFinalMinutos) % 60}min`,
|
|
dias: item.diasTrabalhados
|
|
});
|
|
});
|
|
|
|
// Formatação condicional para saldo final
|
|
historicoSheet.getColumn('saldoFinal').eachCell((cell, rowNumber) => {
|
|
if (rowNumber > 1) {
|
|
const item = historico[rowNumber - 2];
|
|
if (item && item.saldoFinalMinutos < 0) {
|
|
cell.font = { color: { argb: 'FFFF0000' } };
|
|
} else if (item && item.saldoFinalMinutos > 0) {
|
|
cell.font = { color: { argb: 'FF00FF00' } };
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Planilha 3: Ajustes
|
|
if (ajustes && ajustes.length > 0) {
|
|
const ajustesSheet = workbook.addWorksheet('Ajustes');
|
|
ajustesSheet.columns = [
|
|
{ header: 'Data', key: 'data', width: 15 },
|
|
{ header: 'Tipo', key: 'tipo', width: 15 },
|
|
{ header: 'Motivo', key: 'motivo', width: 30 },
|
|
{ header: 'Valor', key: 'valor', width: 15 },
|
|
{ header: 'Gestor', key: 'gestor', width: 25 },
|
|
{ header: 'Status', key: 'status', width: 15 }
|
|
];
|
|
|
|
ajustes.forEach((ajuste) => {
|
|
ajustesSheet.addRow({
|
|
data: new Date(ajuste.dataAplicacao).toLocaleDateString('pt-BR'),
|
|
tipo: ajuste.tipo === 'abonar' ? 'Abonar' : ajuste.tipo === 'descontar' ? 'Descontar' : 'Compensar',
|
|
motivo: ajuste.motivoDescricao || ajuste.motivoTipo || 'N/A',
|
|
valor: `${Math.floor(Math.abs(ajuste.valorMinutos) / 60)}h ${Math.abs(ajuste.valorMinutos) % 60}min`,
|
|
gestor: ajuste.gestor?.nome || 'Sistema',
|
|
status: ajuste.aplicado ? 'Aplicado' : 'Pendente'
|
|
});
|
|
});
|
|
}
|
|
|
|
// Planilha 4: Inconsistências
|
|
if (inconsistencias && inconsistencias.length > 0) {
|
|
const inconsistenciasSheet = workbook.addWorksheet('Inconsistências');
|
|
inconsistenciasSheet.columns = [
|
|
{ header: 'Data Detectada', key: 'data', width: 15 },
|
|
{ header: 'Tipo', key: 'tipo', width: 25 },
|
|
{ header: 'Descrição', key: 'descricao', width: 40 },
|
|
{ header: 'Status', key: 'status', width: 15 }
|
|
];
|
|
|
|
inconsistencias.forEach((inconsistencia) => {
|
|
inconsistenciasSheet.addRow({
|
|
data: new Date(inconsistencia.dataDetectada).toLocaleDateString('pt-BR'),
|
|
tipo: inconsistencia.tipo === 'ponto_com_atestado'
|
|
? 'Ponto + Atestado'
|
|
: inconsistencia.tipo === 'ponto_com_licenca'
|
|
? 'Ponto + Licença'
|
|
: inconsistencia.tipo === 'ponto_com_ausencia'
|
|
? 'Ponto + Ausência'
|
|
: inconsistencia.tipo,
|
|
descricao: inconsistencia.descricao,
|
|
status: inconsistencia.status === 'resolvida'
|
|
? 'Resolvida'
|
|
: inconsistencia.status === 'ignorada'
|
|
? 'Ignorada'
|
|
: 'Pendente'
|
|
});
|
|
});
|
|
}
|
|
|
|
// Salvar arquivo
|
|
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 = `banco-horas-${mesSelecionado}.xlsx`;
|
|
link.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Header com navegação de mês -->
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-2xl font-bold">Banco de Horas Mensal</h2>
|
|
<div class="flex items-center gap-4">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-primary"
|
|
onclick={exportarPDF}
|
|
disabled={!bancoMensal}
|
|
title="Exportar relatório em PDF"
|
|
>
|
|
<Download class="h-4 w-4" strokeWidth={2} />
|
|
Exportar PDF
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-success"
|
|
onclick={exportarExcel}
|
|
disabled={!bancoMensal}
|
|
title="Exportar relatório em Excel"
|
|
>
|
|
<FileText class="h-4 w-4" strokeWidth={2} />
|
|
Exportar Excel
|
|
</button>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle"
|
|
onclick={() => mudarMes('anterior')}
|
|
aria-label="Mês anterior"
|
|
>
|
|
<ChevronLeft class="h-5 w-5" strokeWidth={2} />
|
|
</button>
|
|
<span class="min-w-[200px] text-center text-lg font-semibold">
|
|
{formatarMes(mesSelecionado)}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-circle"
|
|
onclick={() => mudarMes('proximo')}
|
|
aria-label="Próximo mês"
|
|
disabled={mesSelecionado >= `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}`}
|
|
>
|
|
<ChevronRight class="h-5 w-5" strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerta de saldo negativo -->
|
|
{#if saldoNegativo && bancoMensal}
|
|
<div class="alert alert-warning shadow-lg">
|
|
<AlertTriangle class="h-6 w-6 shrink-0" strokeWidth={2} />
|
|
<div>
|
|
<h3 class="font-bold">Atenção: Saldo Negativo Acumulado</h3>
|
|
<div class="text-sm">
|
|
Seu saldo acumulado está negativo em{' '}
|
|
<strong>
|
|
{Math.abs(bancoMensal.saldoFormatado.final.horas)}h{' '}
|
|
{Math.abs(bancoMensal.saldoFormatado.final.minutos)}min
|
|
</strong>
|
|
. Considere compensar horas ou entrar em contato com seu gestor.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if bancoMensalQuery?.isLoading}
|
|
<div class="flex items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
{:else if bancoMensal}
|
|
<!-- Cards de Resumo -->
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
<!-- Saldo Inicial -->
|
|
<div class="card bg-base-100 border-base-300 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Saldo Inicial</p>
|
|
<p
|
|
class={`text-2xl font-bold ${
|
|
bancoMensal.saldoFormatado.inicial.positivo
|
|
? 'text-success'
|
|
: 'text-error'
|
|
}`}
|
|
>
|
|
{bancoMensal.saldoFormatado.inicial.positivo ? '+' : '-'}
|
|
{bancoMensal.saldoFormatado.inicial.horas}h{' '}
|
|
{bancoMensal.saldoFormatado.inicial.minutos}min
|
|
</p>
|
|
</div>
|
|
<div
|
|
class={`rounded-full p-3 ${
|
|
bancoMensal.saldoFormatado.inicial.positivo
|
|
? 'bg-success/20'
|
|
: 'bg-error/20'
|
|
}`}
|
|
>
|
|
{#if bancoMensal.saldoFormatado.inicial.positivo}
|
|
<TrendingUp class="h-6 w-6 text-success" strokeWidth={2} />
|
|
{:else}
|
|
<TrendingDown class="h-6 w-6 text-error" strokeWidth={2} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Saldo do Mês -->
|
|
<div class="card bg-base-100 border-base-300 shadow-xl">
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Saldo do Mês</p>
|
|
<p
|
|
class={`text-2xl font-bold ${
|
|
bancoMensal.saldoFormatado.mes.positivo
|
|
? 'text-success'
|
|
: 'text-error'
|
|
}`}
|
|
>
|
|
{bancoMensal.saldoFormatado.mes.positivo ? '+' : '-'}
|
|
{bancoMensal.saldoFormatado.mes.horas}h{' '}
|
|
{bancoMensal.saldoFormatado.mes.minutos}min
|
|
</p>
|
|
</div>
|
|
<div
|
|
class={`rounded-full p-3 ${
|
|
bancoMensal.saldoFormatado.mes.positivo
|
|
? 'bg-success/20'
|
|
: 'bg-error/20'
|
|
}`}
|
|
>
|
|
{#if bancoMensal.saldoFormatado.mes.positivo}
|
|
<TrendingUp class="h-6 w-6 text-success" strokeWidth={2} />
|
|
{:else}
|
|
<TrendingDown class="h-6 w-6 text-error" strokeWidth={2} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Saldo Final -->
|
|
<div
|
|
class={`card border-base-300 shadow-xl ${
|
|
saldoNegativo ? 'border-error/50 bg-error/5' : 'bg-base-100'
|
|
}`}
|
|
>
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Saldo Final</p>
|
|
<p
|
|
class={`text-2xl font-bold ${
|
|
bancoMensal.saldoFormatado.final.positivo
|
|
? 'text-success'
|
|
: 'text-error'
|
|
}`}
|
|
>
|
|
{bancoMensal.saldoFormatado.final.positivo ? '+' : '-'}
|
|
{bancoMensal.saldoFormatado.final.horas}h{' '}
|
|
{bancoMensal.saldoFormatado.final.minutos}min
|
|
</p>
|
|
</div>
|
|
<div
|
|
class={`rounded-full p-3 ${
|
|
bancoMensal.saldoFormatado.final.positivo
|
|
? 'bg-success/20'
|
|
: 'bg-error/20'
|
|
}`}
|
|
>
|
|
{#if bancoMensal.saldoFormatado.final.positivo}
|
|
<TrendingUp class="h-6 w-6 text-success" strokeWidth={2} />
|
|
{:else}
|
|
<TrendingDown class="h-6 w-6 text-error" strokeWidth={2} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Estatísticas Detalhadas -->
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<!-- Horas Extras -->
|
|
<div class="card bg-success/10 border-success/30 shadow-lg">
|
|
<div class="card-body">
|
|
<div class="flex items-center gap-4">
|
|
<div class="rounded-full bg-success/20 p-4">
|
|
<TrendingUp class="h-8 w-8 text-success" strokeWidth={2} />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Horas Extras</p>
|
|
<p class="text-2xl font-bold text-success">
|
|
{Math.floor(bancoMensal.horasExtras / 60)}h{' '}
|
|
{bancoMensal.horasExtras % 60}min
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Déficit de Horas -->
|
|
<div class="card bg-error/10 border-error/30 shadow-lg">
|
|
<div class="card-body">
|
|
<div class="flex items-center gap-4">
|
|
<div class="rounded-full bg-error/20 p-4">
|
|
<TrendingDown class="h-8 w-8 text-error" strokeWidth={2} />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Déficit de Horas</p>
|
|
<p class="text-2xl font-bold text-error">
|
|
{Math.floor(bancoMensal.horasDeficit / 60)}h{' '}
|
|
{bancoMensal.horasDeficit % 60}min
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Informações Adicionais -->
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<Info class="h-5 w-5" strokeWidth={2} />
|
|
Informações do Mês
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Dias Trabalhados</p>
|
|
<p class="text-xl font-bold">{bancoMensal.diasTrabalhados} dias</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Última Atualização</p>
|
|
<p class="text-xl font-bold">
|
|
{new Date(bancoMensal.atualizadoEm).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ajustes Aplicados -->
|
|
{#if ajustes && ajustes.length > 0}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<FileCheck class="h-5 w-5" strokeWidth={2} />
|
|
Ajustes Aplicados - {formatarMes(mesSelecionado)}
|
|
</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Data</th>
|
|
<th>Tipo</th>
|
|
<th>Motivo</th>
|
|
<th class="text-right">Valor</th>
|
|
<th>Gestor</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each ajustes as ajuste}
|
|
<tr>
|
|
<td>
|
|
{new Date(ajuste.dataAplicacao).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})}
|
|
</td>
|
|
<td>
|
|
<span
|
|
class={`badge ${
|
|
ajuste.tipo === 'abonar'
|
|
? 'badge-success'
|
|
: ajuste.tipo === 'descontar'
|
|
? 'badge-error'
|
|
: 'badge-info'
|
|
}`}
|
|
>
|
|
{ajuste.tipo === 'abonar'
|
|
? 'Abonar'
|
|
: ajuste.tipo === 'descontar'
|
|
? 'Descontar'
|
|
: 'Compensar'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex flex-col">
|
|
<span class="font-medium">
|
|
{ajuste.motivoTipo === 'atestado'
|
|
? 'Atestado Médico'
|
|
: ajuste.motivoTipo === 'licenca'
|
|
? 'Licença'
|
|
: ajuste.motivoTipo === 'ausencia'
|
|
? 'Ausência'
|
|
: 'Manual'}
|
|
</span>
|
|
{#if ajuste.motivoDescricao}
|
|
<span class="text-xs text-base-content/60">
|
|
{ajuste.motivoDescricao}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</td>
|
|
<td class="text-right">
|
|
<span
|
|
class={ajuste.valorMinutos >= 0 ? 'text-success' : 'text-error'}
|
|
>
|
|
{ajuste.valorMinutos >= 0 ? '+' : ''}
|
|
{Math.floor(Math.abs(ajuste.valorMinutos) / 60)}h{' '}
|
|
{Math.abs(ajuste.valorMinutos) % 60}min
|
|
</span>
|
|
</td>
|
|
<td>{ajuste.gestor?.nome || 'Sistema'}</td>
|
|
<td>
|
|
{#if ajuste.aplicado}
|
|
<span class="badge badge-success badge-sm">
|
|
<CheckCircle2 class="h-3 w-3" />
|
|
Aplicado
|
|
</span>
|
|
{:else}
|
|
<span class="badge badge-warning badge-sm">
|
|
<AlertCircle class="h-3 w-3" />
|
|
Pendente
|
|
</span>
|
|
{/if}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Inconsistências Detectadas -->
|
|
{#if inconsistencias && inconsistencias.length > 0}
|
|
<div class="card bg-warning/10 border-warning/30 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold text-warning">
|
|
<AlertTriangle class="h-5 w-5" strokeWidth={2} />
|
|
Inconsistências Detectadas - {formatarMes(mesSelecionado)}
|
|
</h3>
|
|
<div class="space-y-3">
|
|
{#each inconsistencias as inconsistencia}
|
|
<div
|
|
class={`rounded-lg border p-4 ${
|
|
inconsistencia.status === 'resolvida'
|
|
? 'border-success/30 bg-success/5'
|
|
: inconsistencia.status === 'ignorada'
|
|
? 'border-base-300 bg-base-200'
|
|
: 'border-warning/50 bg-warning/10'
|
|
}`}
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="mb-2 flex items-center gap-2">
|
|
<span
|
|
class={`badge ${
|
|
inconsistencia.status === 'resolvida'
|
|
? 'badge-success'
|
|
: inconsistencia.status === 'ignorada'
|
|
? 'badge-base-300'
|
|
: 'badge-warning'
|
|
}`}
|
|
>
|
|
{inconsistencia.status === 'resolvida'
|
|
? 'Resolvida'
|
|
: inconsistencia.status === 'ignorada'
|
|
? 'Ignorada'
|
|
: 'Pendente'}
|
|
</span>
|
|
<span class="text-sm font-medium">
|
|
{inconsistencia.tipo === 'ponto_com_atestado'
|
|
? 'Registro de Ponto com Atestado'
|
|
: inconsistencia.tipo === 'ponto_com_licenca'
|
|
? 'Registro de Ponto com Licença'
|
|
: inconsistencia.tipo === 'ponto_com_ausencia'
|
|
? 'Registro de Ponto com Ausência'
|
|
: inconsistencia.tipo === 'registro_duplicado'
|
|
? 'Registro Duplicado'
|
|
: inconsistencia.tipo === 'sequencia_invalida'
|
|
? 'Sequência Inválida'
|
|
: 'Saldo Inconsistente'}
|
|
</span>
|
|
</div>
|
|
<p class="text-sm text-base-content/80">{inconsistencia.descricao}</p>
|
|
<p class="mt-2 text-xs text-base-content/60">
|
|
Detectada em:{' '}
|
|
{new Date(inconsistencia.dataDetectada).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})}
|
|
</p>
|
|
</div>
|
|
{#if inconsistencia.status === 'pendente'}
|
|
<XCircle class="h-5 w-5 text-warning" strokeWidth={2} />
|
|
{:else if inconsistencia.status === 'resolvida'}
|
|
<CheckCircle2 class="h-5 w-5 text-success" strokeWidth={2} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Resumo de Ajustes e Inconsistências -->
|
|
{#if bancoMensal && (bancoMensal.totalAjustes || bancoMensal.totalAbonos || bancoMensal.totalDescontos || bancoMensal.inconsistenciasResolvidas !== undefined)}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<Info class="h-5 w-5" strokeWidth={2} />
|
|
Resumo de Ajustes e Inconsistências
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
{#if bancoMensal.totalAjustes !== undefined}
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">Total de Ajustes</p>
|
|
<p class="text-xl font-bold">
|
|
{Math.floor(Math.abs(bancoMensal.totalAjustes) / 60)}h{' '}
|
|
{Math.abs(bancoMensal.totalAjustes) % 60}min
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
{#if bancoMensal.totalAbonos !== undefined}
|
|
<div>
|
|
<p class="text-sm font-medium text-success">Total de Abonos</p>
|
|
<p class="text-xl font-bold text-success">
|
|
+{Math.floor(bancoMensal.totalAbonos / 60)}h{' '}
|
|
{bancoMensal.totalAbonos % 60}min
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
{#if bancoMensal.totalDescontos !== undefined}
|
|
<div>
|
|
<p class="text-sm font-medium text-error">Total de Descontos</p>
|
|
<p class="text-xl font-bold text-error">
|
|
-{Math.floor(bancoMensal.totalDescontos / 60)}h{' '}
|
|
{bancoMensal.totalDescontos % 60}min
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
{#if bancoMensal.inconsistenciasResolvidas !== undefined}
|
|
<div>
|
|
<p class="text-sm font-medium text-base-content/60">
|
|
Inconsistências Resolvidas
|
|
</p>
|
|
<p class="text-xl font-bold">{bancoMensal.inconsistenciasResolvidas}</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body text-center">
|
|
<Calendar class="mx-auto mb-4 h-12 w-12 text-base-content/40" strokeWidth={2} />
|
|
<p class="text-lg font-semibold">Nenhum dado disponível para este mês</p>
|
|
<p class="text-sm text-base-content/60">
|
|
Não há registros de banco de horas para {formatarMes(mesSelecionado)}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Gráfico de Evolução -->
|
|
{#if chartData && historico && historico.length > 0}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<TrendingUp class="h-5 w-5" strokeWidth={2} />
|
|
Evolução do Banco de Horas
|
|
</h3>
|
|
<div class="h-64 w-full">
|
|
<LineChart data={chartData} title="Evolução do Banco de Horas" height={256} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Histórico dos Últimos 6 Meses -->
|
|
{#if historico && historico.length > 0}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<Clock class="h-5 w-5" strokeWidth={2} />
|
|
Histórico dos Últimos 6 Meses
|
|
</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Mês</th>
|
|
<th class="text-right">Saldo Inicial</th>
|
|
<th class="text-right">Saldo do Mês</th>
|
|
<th class="text-right">Saldo Final</th>
|
|
<th class="text-right">Dias</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each historico as item}
|
|
<tr
|
|
class={item.mes === mesSelecionado ? 'bg-primary/10 font-semibold' : ''}
|
|
>
|
|
<td>{formatarMes(item.mes)}</td>
|
|
<td class="text-right">
|
|
<span
|
|
class={item.saldoFormatado.inicial.positivo
|
|
? 'text-success'
|
|
: 'text-error'}
|
|
>
|
|
{item.saldoFormatado.inicial.positivo ? '+' : '-'}
|
|
{item.saldoFormatado.inicial.horas}h{' '}
|
|
{item.saldoFormatado.inicial.minutos}min
|
|
</span>
|
|
</td>
|
|
<td class="text-right">
|
|
<span
|
|
class={item.saldoFormatado.mes.positivo
|
|
? 'text-success'
|
|
: 'text-error'}
|
|
>
|
|
{item.saldoFormatado.mes.positivo ? '+' : '-'}
|
|
{item.saldoFormatado.mes.horas}h{' '}
|
|
{item.saldoFormatado.mes.minutos}min
|
|
</span>
|
|
</td>
|
|
<td class="text-right">
|
|
<span
|
|
class={item.saldoFormatado.final.positivo
|
|
? 'text-success'
|
|
: 'text-error'}
|
|
>
|
|
{item.saldoFormatado.final.positivo ? '+' : '-'}
|
|
{item.saldoFormatado.final.horas}h{' '}
|
|
{item.saldoFormatado.final.minutos}min
|
|
</span>
|
|
</td>
|
|
<td class="text-right">{item.diasTrabalhados}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Histórico de Alterações -->
|
|
{#if historicoAlteracoes && historicoAlteracoes.length > 0}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body">
|
|
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
<History class="h-5 w-5" strokeWidth={2} />
|
|
Histórico de Alterações - {formatarMes(mesSelecionado)}
|
|
</h3>
|
|
<div class="space-y-4">
|
|
{#each historicoAlteracoes as alteracao}
|
|
<div class="border-base-300 rounded-lg border p-4 shadow-sm">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="mb-2 flex items-center gap-2">
|
|
{#if alteracao.tipoAlteracao === 'edicao_registro'}
|
|
<Edit class="h-4 w-4 text-info" strokeWidth={2} />
|
|
<span class="badge badge-info badge-sm">Edição de Registro</span>
|
|
{:else if alteracao.tipoAlteracao === 'ajuste_banco'}
|
|
<Settings class="h-4 w-4 text-warning" strokeWidth={2} />
|
|
<span class="badge badge-warning badge-sm">
|
|
Ajuste de Banco de Horas
|
|
</span>
|
|
{:else}
|
|
<Info class="h-4 w-4 text-base-content/60" strokeWidth={2} />
|
|
<span class="badge badge-ghost badge-sm">Outro</span>
|
|
{/if}
|
|
<span class="text-xs text-base-content/60">
|
|
{alteracao.dataFormatada}
|
|
</span>
|
|
</div>
|
|
|
|
{#if alteracao.tipoAlteracao === 'edicao_registro' && alteracao.registro}
|
|
<div class="text-sm">
|
|
<p>
|
|
<strong>Registro:</strong> {alteracao.registro.tipo} em{' '}
|
|
{alteracao.registro.data}
|
|
</p>
|
|
<p>
|
|
<strong>Alteração:</strong>{' '}
|
|
<span class="text-error">
|
|
{alteracao.registro.horaAnterior}
|
|
</span>{' '}
|
|
→{' '}
|
|
<span class="text-success">
|
|
{alteracao.registro.horaNova}
|
|
</span>
|
|
</p>
|
|
{#if alteracao.diferencaMinutos !== undefined}
|
|
<p>
|
|
<strong>Diferença:</strong>{' '}
|
|
<span
|
|
class={alteracao.diferencaMinutos >= 0
|
|
? 'text-success'
|
|
: 'text-error'}
|
|
>
|
|
{alteracao.diferencaMinutos >= 0 ? '+' : ''}
|
|
{Math.floor(Math.abs(alteracao.diferencaMinutos) / 60)}h{' '}
|
|
{Math.abs(alteracao.diferencaMinutos) % 60}min
|
|
</span>
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{:else if alteracao.tipoAlteracao === 'ajuste_banco'}
|
|
<div class="text-sm">
|
|
<p>
|
|
<strong>Tipo:</strong> {alteracao.tipoAjuste === 'compensar'
|
|
? 'Compensar'
|
|
: alteracao.tipoAjuste === 'abonar'
|
|
? 'Abonar'
|
|
: 'Descontar'}
|
|
</p>
|
|
{#if alteracao.ajusteMinutos !== undefined}
|
|
<p>
|
|
<strong>Ajuste:</strong>{' '}
|
|
<span
|
|
class={alteracao.ajusteMinutos >= 0
|
|
? 'text-success'
|
|
: 'text-error'}
|
|
>
|
|
{alteracao.ajusteMinutos >= 0 ? '+' : ''}
|
|
{Math.floor(Math.abs(alteracao.ajusteMinutos) / 60)}h{' '}
|
|
{Math.abs(alteracao.ajusteMinutos) % 60}min
|
|
</span>
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if alteracao.motivoDescricao}
|
|
<p class="mt-2 text-sm">
|
|
<strong>Motivo:</strong> {alteracao.motivoDescricao}
|
|
</p>
|
|
{/if}
|
|
|
|
{#if alteracao.observacoes}
|
|
<p class="mt-1 text-sm text-base-content/70">
|
|
<strong>Observações:</strong> {alteracao.observacoes}
|
|
</p>
|
|
{/if}
|
|
|
|
{#if alteracao.gestor}
|
|
<p class="mt-1 text-xs text-base-content/60">
|
|
Alterado por: <strong>{alteracao.gestor.nome}</strong>
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{:else if historicoAlteracoesQuery?.data && historicoAlteracoesQuery.data.length === 0}
|
|
<div class="card bg-base-100 border-base-300 shadow-lg">
|
|
<div class="card-body text-center">
|
|
<History class="mx-auto mb-4 h-12 w-12 text-base-content/40" strokeWidth={2} />
|
|
<p class="text-lg font-semibold">Nenhuma alteração registrada</p>
|
|
<p class="text-sm text-base-content/60">
|
|
Não há histórico de alterações para {formatarMes(mesSelecionado)}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|