feat: enhance Banco de Horas management with new reporting features, including adjustments and inconsistencies tracking, advanced filters, and Excel export functionality
This commit is contained in:
@@ -15,10 +15,16 @@
|
||||
Edit,
|
||||
Settings,
|
||||
Download,
|
||||
FileText
|
||||
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 {
|
||||
@@ -56,9 +62,35 @@
|
||||
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(() => {
|
||||
@@ -185,6 +217,24 @@
|
||||
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
|
||||
@@ -226,6 +276,130 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
@@ -250,6 +424,140 @@
|
||||
// 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">
|
||||
@@ -267,6 +575,16 @@
|
||||
<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"
|
||||
@@ -493,6 +811,226 @@
|
||||
</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">
|
||||
|
||||
163
apps/web/src/lib/components/ponto/TimePicker.svelte
Normal file
163
apps/web/src/lib/components/ponto/TimePicker.svelte
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { ChevronUp, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
onChange: (hours: number, minutes: number) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { hours, minutes, onChange, label, disabled = false }: Props = $props();
|
||||
|
||||
function incrementHours() {
|
||||
if (disabled) return;
|
||||
const newHours = hours + 1;
|
||||
onChange(newHours, minutes);
|
||||
}
|
||||
|
||||
function decrementHours() {
|
||||
if (disabled) return;
|
||||
const newHours = Math.max(0, hours - 1);
|
||||
onChange(newHours, minutes);
|
||||
}
|
||||
|
||||
function incrementMinutes() {
|
||||
if (disabled) return;
|
||||
const newMinutes = minutes + 15;
|
||||
if (newMinutes >= 60) {
|
||||
const extraHours = Math.floor(newMinutes / 60);
|
||||
const remainingMinutes = newMinutes % 60;
|
||||
onChange(hours + extraHours, remainingMinutes);
|
||||
} else {
|
||||
onChange(hours, newMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
function decrementMinutes() {
|
||||
if (disabled) return;
|
||||
const newMinutes = minutes - 15;
|
||||
if (newMinutes < 0) {
|
||||
if (hours > 0) {
|
||||
onChange(hours - 1, 60 + newMinutes);
|
||||
} else {
|
||||
onChange(0, 0);
|
||||
}
|
||||
} else {
|
||||
onChange(hours, newMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
function handleHoursInput(e: Event) {
|
||||
if (disabled) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = parseInt(target.value) || 0;
|
||||
onChange(Math.max(0, value), minutes);
|
||||
}
|
||||
|
||||
function handleMinutesInput(e: Event) {
|
||||
if (disabled) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = parseInt(target.value) || 0;
|
||||
const clampedValue = Math.max(0, Math.min(59, value));
|
||||
onChange(hours, clampedValue);
|
||||
}
|
||||
|
||||
const totalMinutes = $derived(hours * 60 + minutes);
|
||||
const displayText = $derived.by(() => {
|
||||
if (totalMinutes === 0) return '0h 0min';
|
||||
const h = Math.floor(totalMinutes / 60);
|
||||
const m = totalMinutes % 60;
|
||||
return `${h}h ${m}min`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="time-picker">
|
||||
{#if label}
|
||||
<div class="mb-2 block text-sm font-medium text-gray-700">{label}</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Horas -->
|
||||
<div class="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={incrementHours}
|
||||
disabled={disabled}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronUp class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={hours}
|
||||
oninput={handleHoursInput}
|
||||
disabled={disabled}
|
||||
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={decrementHours}
|
||||
disabled={disabled || hours === 0}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronDown class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<span class="mt-1 text-xs text-gray-500">horas</span>
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="text-2xl font-bold text-gray-400">:</div>
|
||||
|
||||
<!-- Minutos -->
|
||||
<div class="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={incrementMinutes}
|
||||
disabled={disabled}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronUp class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
value={minutes}
|
||||
oninput={handleMinutesInput}
|
||||
disabled={disabled}
|
||||
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={decrementMinutes}
|
||||
disabled={disabled || (hours === 0 && minutes === 0)}
|
||||
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<ChevronDown class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<span class="mt-1 text-xs text-gray-500">min</span>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="ml-4 flex flex-col items-center justify-center rounded-lg bg-primary/10 px-4 py-2">
|
||||
<span class="text-xs text-gray-600">Total</span>
|
||||
<span class="text-lg font-bold text-primary">{displayText}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.time-picker input[type='number']::-webkit-inner-spin-button,
|
||||
.time-picker input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time-picker input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user