feat: add date formatting utility and enhance filtering in registro-pontos

- Introduced a new utility function `formatarDataDDMMAAAA` to format dates in DD/MM/AAAA format, supporting various input types.
- Updated the `registro-pontos` page to utilize the new date formatting function for displaying dates consistently.
- Implemented advanced filtering options for status and location, allowing users to filter records based on their criteria.
- Enhanced CSV export functionality to include formatted dates and additional filtering capabilities, improving data management for users.
This commit is contained in:
2025-11-22 19:32:05 -03:00
parent c056506ce5
commit fc4b5c5ba5
4 changed files with 322 additions and 49 deletions

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { MapPin, AlertCircle, HelpCircle } from 'lucide-svelte';
interface Props {
dentroRaioPermitido: boolean | null | undefined;
showTooltip?: boolean;
}
let { dentroRaioPermitido, showTooltip = true }: Props = $props();
</script>
{#if dentroRaioPermitido === true}
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Dentro do Raio' : ''}>
<MapPin class="h-5 w-5 text-success" strokeWidth={2.5} />
</div>
{:else if dentroRaioPermitido === false}
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Fora do Raio' : ''}>
<AlertCircle class="h-5 w-5 text-error" strokeWidth={2.5} />
</div>
{:else}
<div class="tooltip tooltip-top" data-tip={showTooltip ? 'Não Validado' : ''}>
<HelpCircle class="h-5 w-5 text-base-content/40" strokeWidth={2.5} />
</div>
{/if}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
interface Props {
saldo?: {
saldoMinutos: number;
horas: number;
minutos: number;
positivo: boolean;
} | null;
size?: 'sm' | 'md' | 'lg';
}
let { saldo, size = 'md' }: Props = $props();
function formatarSaldo(saldo: NonNullable<Props['saldo']>): string {
const sinal = saldo.positivo ? '+' : '-';
return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
}
const sizeClasses = {
sm: 'badge-sm',
md: 'badge-lg',
lg: 'badge-xl'
};
</script>
{#if saldo}
<span
class="badge font-semibold shadow-sm {sizeClasses[size]} {saldo.positivo
? 'badge-success'
: 'badge-error'}"
>
{formatarSaldo(saldo)}
</span>
{:else}
<span class="badge badge-ghost {sizeClasses[size]}">-</span>
{/if}

View File

@@ -122,3 +122,37 @@ export function getProximoTipoRegistro(
} }
} }
/**
* Formata data no formato DD/MM/AAAA
* Suporta strings ISO (YYYY-MM-DD), objetos Date, e timestamps
*/
export function formatarDataDDMMAAAA(data: string | Date | number): string {
if (!data) return '';
let dataObj: Date;
if (typeof data === 'string') {
// Se for string no formato ISO (YYYY-MM-DD), adicionar hora para evitar problemas de timezone
if (data.match(/^\d{4}-\d{2}-\d{2}$/)) {
dataObj = new Date(data + 'T12:00:00');
} else {
dataObj = new Date(data);
}
} else if (typeof data === 'number') {
dataObj = new Date(data);
} else {
dataObj = data;
}
// Verificar se a data é válida
if (isNaN(dataObj.getTime())) {
return '';
}
const dia = dataObj.getDate().toString().padStart(2, '0');
const mes = (dataObj.getMonth() + 1).toString().padStart(2, '0');
const ano = dataObj.getFullYear();
return `${dia}/${mes}/${ano}`;
}

View File

@@ -4,13 +4,16 @@
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown, FileText } from 'lucide-svelte'; import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown, FileText } from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto'; import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
import logoGovPE from '$lib/assets/logo_governo_PE.png'; import logoGovPE from '$lib/assets/logo_governo_PE.png';
import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte'; import PrintPontoModal from '$lib/components/ponto/PrintPontoModal.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import Papa from 'papaparse';
Chart.register(...registerables); Chart.register(...registerables);
@@ -20,6 +23,8 @@
let dataInicio = $state(new Date().toISOString().split('T')[0]!); let dataInicio = $state(new Date().toISOString().split('T')[0]!);
let dataFim = $state(new Date().toISOString().split('T')[0]!); let dataFim = $state(new Date().toISOString().split('T')[0]!);
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>(''); let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
let statusFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
let localizacaoFiltro = $state<'todos' | 'dentro' | 'fora'>('todos');
let carregando = $state(false); let carregando = $state(false);
let mostrarModalImpressao = $state(false); let mostrarModalImpressao = $state(false);
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>(''); let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
@@ -209,6 +214,33 @@
} }
}); });
// Filtrar registros com base nos filtros avançados
const registrosFiltrados = $derived.by(() => {
if (!registros || registros.length === 0) return [];
let resultado = [...registros];
// Filtro de status (Dentro/Fora do Prazo)
if (statusFiltro !== 'todos') {
resultado = resultado.filter(r => {
if (statusFiltro === 'dentro') return r.dentroDoPrazo === true;
if (statusFiltro === 'fora') return r.dentroDoPrazo === false;
return true;
});
}
// Filtro de localização (Dentro/Fora do Raio)
if (localizacaoFiltro !== 'todos') {
resultado = resultado.filter(r => {
if (localizacaoFiltro === 'dentro') return r.dentroRaioPermitido === true;
if (localizacaoFiltro === 'fora') return r.dentroRaioPermitido === false;
return true;
});
}
return resultado;
});
// Agrupar registros por funcionário e data // Agrupar registros por funcionário e data
const registrosAgrupados = $derived.by(() => { const registrosAgrupados = $derived.by(() => {
const agrupados: Record< const agrupados: Record<
@@ -230,12 +262,15 @@
// Usar Set para evitar registros duplicados // Usar Set para evitar registros duplicados
const registrosProcessados = new Set<string>(); const registrosProcessados = new Set<string>();
// Usar registros filtrados ao invés de registros originais
const registrosParaAgrupar = registrosFiltrados;
// Verificar se registros é um array válido // Verificar se registros é um array válido
if (!Array.isArray(registros) || registros.length === 0) { if (!Array.isArray(registrosParaAgrupar) || registrosParaAgrupar.length === 0) {
return []; return [];
} }
for (const registro of registros) { for (const registro of registrosParaAgrupar) {
// Verificar se o registro tem os campos necessários // Verificar se o registro tem os campos necessários
if (!registro || !registro._id || !registro.funcionarioId || !registro.data) { if (!registro || !registro._id || !registro.funcionarioId || !registro.data) {
console.warn('⚠️ [DEBUG] Registro inválido ignorado:', registro); console.warn('⚠️ [DEBUG] Registro inválido ignorado:', registro);
@@ -345,16 +380,7 @@
return `${sinal}${saldo.horas}h ${saldo.minutos}min`; return `${sinal}${saldo.horas}h ${saldo.minutos}min`;
} }
// Função para formatar data em português // Usar função centralizada formatarDataDDMMAAAA da lib/utils/ponto.ts
function formatarData(data: string): string {
if (!data) return '';
const dataObj = new Date(data + 'T00:00:00');
return dataObj.toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
// Obter nome do funcionário selecionado // Obter nome do funcionário selecionado
const funcionarioSelecionadoNome = $derived.by(() => { const funcionarioSelecionadoNome = $derived.by(() => {
@@ -407,6 +433,93 @@
mostrarModalImpressao = true; mostrarModalImpressao = true;
} }
// Função para limpar todos os filtros
function limparFiltros() {
dataInicio = new Date().toISOString().split('T')[0]!;
dataFim = new Date().toISOString().split('T')[0]!;
funcionarioIdFiltro = '';
statusFiltro = 'todos';
localizacaoFiltro = 'todos';
toast.success('Filtros limpos com sucesso!');
}
// Função para exportar registros para CSV
async function exportarCSV() {
try {
const registrosParaExportar = registrosFiltrados;
if (!registrosParaExportar || registrosParaExportar.length === 0) {
toast.error('Nenhum registro para exportar');
return;
}
// Preparar dados para CSV
const csvData = registrosParaExportar.map((registro) => {
const funcionarioNome = registro.funcionario?.nome || 'N/A';
const funcionarioMatricula = registro.funcionario?.matricula || 'N/A';
const tipo = config
? getTipoRegistroLabel(registro.tipo, {
nomeEntrada: config.nomeEntrada,
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
nomeSaida: config.nomeSaida,
})
: getTipoRegistroLabel(registro.tipo);
const horario = formatarHoraPonto(registro.hora, registro.minuto);
const status = registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo';
const localizacao = registro.dentroRaioPermitido === true
? 'Dentro do Raio'
: registro.dentroRaioPermitido === false
? 'Fora do Raio'
: 'Não Validado';
return {
'Data': formatarDataDDMMAAAA(registro.data),
'Funcionário': funcionarioNome,
'Matrícula': funcionarioMatricula,
'Tipo': tipo,
'Horário': horario,
'Status': status,
'Localização': localizacao,
'IP': registro.ipAddress || 'N/A',
'Dispositivo': registro.deviceType || 'N/A',
};
});
// Gerar CSV usando Papa Parse
const csv = Papa.unparse(csvData, {
header: true,
delimiter: ';',
encoding: 'UTF-8',
});
// Adicionar BOM para Excel reconhecer UTF-8 corretamente
const BOM = '\uFEFF';
const csvComBOM = BOM + csv;
// Criar blob e download
const blob = new Blob([csvComBOM], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute(
'download',
`registros-ponto-${formatarDataDDMMAAAA(dataInicio)}-${formatarDataDDMMAAAA(dataFim)}.csv`
);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success('CSV exportado com sucesso!');
} catch (error) {
console.error('Erro ao exportar CSV:', error);
toast.error('Erro ao exportar CSV. Tente novamente.');
}
}
async function gerarPDFComSelecao(sections: { async function gerarPDFComSelecao(sections: {
dadosFuncionario: boolean; dadosFuncionario: boolean;
registrosPonto: boolean; registrosPonto: boolean;
@@ -497,10 +610,8 @@
} }
yPosition += 5; yPosition += 5;
// Formatar período para exibição // Formatar período para exibição usando função centralizada
const dataInicioParts = dataInicio.split('-'); const periodoFormatado = `${formatarDataDDMMAAAA(dataInicio)} a ${formatarDataDDMMAAAA(dataFim)}`;
const dataFimParts = dataFim.split('-');
const periodoFormatado = `${dataInicioParts[2]}/${dataInicioParts[1]}/${dataInicioParts[0]} a ${dataFimParts[2]}/${dataFimParts[1]}/${dataFimParts[0]}`;
doc.text(`Período: ${periodoFormatado}`, 15, yPosition); doc.text(`Período: ${periodoFormatado}`, 15, yPosition);
yPosition += 10; yPosition += 10;
} }
@@ -571,6 +682,7 @@
hora: number; hora: number;
minuto: number; minuto: number;
dentroDoPrazo: boolean; dentroDoPrazo: boolean;
dentroRaioPermitido: boolean | null | undefined;
}> }>
> = {}; > = {};
@@ -585,14 +697,14 @@
hora: r.hora, hora: r.hora,
minuto: r.minuto, minuto: r.minuto,
dentroDoPrazo: r.dentroDoPrazo, dentroDoPrazo: r.dentroDoPrazo,
dentroRaioPermitido: r.dentroRaioPermitido,
}); });
} }
// Criar dados da tabela com saldo diário // Criar dados da tabela com saldo diário
for (const [data, regs] of Object.entries(registrosPorData)) { for (const [data, regs] of Object.entries(registrosPorData)) {
// Formatar data para exibição (DD/MM/YYYY) // Formatar data para exibição usando função centralizada (DD/MM/AAAA)
const dataParts = data.split('-'); const dataFormatada = formatarDataDDMMAAAA(data);
const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`;
// Calcular saldo diário como diferença entre saída e entrada // Calcular saldo diário como diferença entre saída e entrada
const saldoDiarioDia = calcularSaldoDiario(regs); const saldoDiarioDia = calcularSaldoDiario(regs);
@@ -621,6 +733,15 @@
} }
} }
// Adicionar localização (geofencing)
if (reg.dentroRaioPermitido === true) {
linha.push('✅ Dentro do Raio');
} else if (reg.dentroRaioPermitido === false) {
linha.push('⚠️ Fora do Raio');
} else {
linha.push('❓ Não Validado');
}
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não'); linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
tableData.push(linha); tableData.push(linha);
@@ -631,6 +752,7 @@
if (sections.saldoDiario) { if (sections.saldoDiario) {
headers.push('Saldo Diário'); headers.push('Saldo Diário');
} }
headers.push('Localização');
headers.push('Dentro do Prazo'); headers.push('Dentro do Prazo');
// Salvar a posição Y antes da tabela // Salvar a posição Y antes da tabela
@@ -740,9 +862,8 @@
yPosition += 10; yPosition += 10;
const homologacoesData = homologacoes.map((h) => { const homologacoesData = homologacoes.map((h) => {
// Formatar data de criação // Formatar data de criação usando função centralizada (DD/MM/AAAA)
const dataCriacao = new Date(h.criadoEm); const dataFormatada = formatarDataDDMMAAAA(h.criadoEm);
const dataFormatada = `${dataCriacao.getDate().toString().padStart(2, '0')}/${(dataCriacao.getMonth() + 1).toString().padStart(2, '0')}/${dataCriacao.getFullYear()}`;
if (h.registroId && h.horaAnterior !== undefined) { if (h.registroId && h.horaAnterior !== undefined) {
return [ return [
@@ -804,13 +925,9 @@
yPosition += 10; yPosition += 10;
const dispensasData = dispensas.map((d) => { const dispensasData = dispensas.map((d) => {
// Formatar data de início // Formatar data de início e fim usando função centralizada (DD/MM/AAAA)
const dataInicioParts = d.dataInicio.split('-'); const dataInicioFormatada = formatarDataDDMMAAAA(d.dataInicio);
const dataInicioFormatada = `${dataInicioParts[2]}/${dataInicioParts[1]}/${dataInicioParts[0]}`; const dataFimFormatada = formatarDataDDMMAAAA(d.dataFim);
// Formatar data de fim
const dataFimParts = d.dataFim.split('-');
const dataFimFormatada = `${dataFimParts[2]}/${dataFimParts[1]}/${dataFimParts[0]}`;
return [ return [
`${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`, `${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`,
@@ -1805,13 +1922,34 @@
<!-- Filtros --> <!-- Filtros -->
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 shadow-xl mb-8"> <div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 shadow-xl mb-8">
<div class="card-body"> <div class="card-body">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="p-2 bg-secondary/10 rounded-lg"> <div class="p-2 bg-secondary/10 rounded-lg">
<Filter class="h-5 w-5 text-secondary" strokeWidth={2.5} /> <Filter class="h-5 w-5 text-secondary" strokeWidth={2.5} />
</div> </div>
<h2 class="card-title text-2xl mb-0">Filtros de Busca</h2> <h2 class="card-title text-2xl mb-0">Filtros de Busca</h2>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="flex items-center gap-3">
<button
type="button"
class="btn btn-outline btn-secondary gap-2 shadow-md hover:shadow-lg transition-all"
onclick={limparFiltros}
>
<Filter class="h-4 w-4" />
Limpar Filtros
</button>
<button
type="button"
class="btn btn-primary gap-2 shadow-md hover:shadow-lg transition-all"
onclick={exportarCSV}
disabled={registrosFiltrados.length === 0}
>
<Download class="h-4 w-4" />
Exportar CSV
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div class="form-control"> <div class="form-control">
<label class="label" for="data-inicio"> <label class="label" for="data-inicio">
<span class="label-text font-semibold">Data Início</span> <span class="label-text font-semibold">Data Início</span>
@@ -1851,7 +1989,51 @@
{/each} {/each}
</select> </select>
</div> </div>
<div class="form-control">
<label class="label" for="status">
<span class="label-text font-semibold">Status</span>
</label>
<select
id="status"
bind:value={statusFiltro}
class="select select-bordered select-primary focus:select-primary focus:ring-2 focus:ring-primary/20"
>
<option value="todos">Todos</option>
<option value="dentro">Dentro do Prazo</option>
<option value="fora">Fora do Prazo</option>
</select>
</div> </div>
<div class="form-control">
<label class="label" for="localizacao">
<span class="label-text font-semibold">Localização</span>
</label>
<select
id="localizacao"
bind:value={localizacaoFiltro}
class="select select-bordered select-primary focus:select-primary focus:ring-2 focus:ring-primary/20"
>
<option value="todos">Todas</option>
<option value="dentro">Dentro do Raio</option>
<option value="fora">Fora do Raio</option>
</select>
</div>
</div>
<!-- Indicador de registros filtrados -->
{#if statusFiltro !== 'todos' || localizacaoFiltro !== 'todos'}
<div class="mt-4 p-3 bg-info/10 border border-info/20 rounded-lg">
<p class="text-sm text-base-content/80">
<strong>{registrosFiltrados.length}</strong> registro(s) encontrado(s) com os filtros aplicados
{#if registros.length !== registrosFiltrados.length}
<span class="text-xs text-base-content/60">
(de {registros.length} total)
</span>
{/if}
</p>
</div>
{/if}
</div> </div>
</div> </div>
@@ -1878,13 +2060,13 @@
{#if dataInicio} {#if dataInicio}
<div class="badge badge-info badge-lg gap-2 px-4 py-3"> <div class="badge badge-info badge-lg gap-2 px-4 py-3">
<Clock class="h-4 w-4" /> <Clock class="h-4 w-4" />
De: {formatarData(dataInicio)} De: {formatarDataDDMMAAAA(dataInicio)}
</div> </div>
{/if} {/if}
{#if dataFim} {#if dataFim}
<div class="badge badge-info badge-lg gap-2 px-4 py-3"> <div class="badge badge-info badge-lg gap-2 px-4 py-3">
<Clock class="h-4 w-4" /> <Clock class="h-4 w-4" />
Até: {formatarData(dataFim)} Até: {formatarDataDDMMAAAA(dataFim)}
</div> </div>
{/if} {/if}
</div> </div>
@@ -1916,7 +2098,7 @@
<div class="flex-1"> <div class="flex-1">
<h3 class="font-bold text-base-content">Nenhum registro encontrado</h3> <h3 class="font-bold text-base-content">Nenhum registro encontrado</h3>
<div class="text-sm mt-2 opacity-80"> <div class="text-sm mt-2 opacity-80">
<p>Período: <span class="font-semibold">{formatarData(dataInicio)} até {formatarData(dataFim)}</span></p> <p>Período: <span class="font-semibold">{formatarDataDDMMAAAA(dataInicio)} até {formatarDataDDMMAAAA(dataFim)}</span></p>
{#if funcionarioIdFiltro && funcionarioSelecionadoNome} {#if funcionarioIdFiltro && funcionarioSelecionadoNome}
<p class="mt-1">Funcionário: <span class="font-semibold">{funcionarioSelecionadoNome}</span></p> <p class="mt-1">Funcionário: <span class="font-semibold">{funcionarioSelecionadoNome}</span></p>
{/if} {/if}
@@ -2011,6 +2193,7 @@
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th> <th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th> <th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th> <th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Localização</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th> <th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th> <th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th>
</tr> </tr>
@@ -2018,8 +2201,7 @@
<tbody> <tbody>
{#each Object.values(grupo.registrosPorData) as grupoData} {#each Object.values(grupo.registrosPorData) as grupoData}
{@const totalRegistros = grupoData.registros.length} {@const totalRegistros = grupoData.registros.length}
{@const dataParts = grupoData.data.split('-')} {@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
{@const dataFormatada = `${dataParts[2]}/${dataParts[1]}/${dataParts[0]}`}
{#each grupoData.registros as registro, index} {#each grupoData.registros as registro, index}
<tr> <tr>
<td class="whitespace-nowrap">{dataFormatada}</td> <td class="whitespace-nowrap">{dataFormatada}</td>
@@ -2036,17 +2218,12 @@
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td> <td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
{#if index === 0} {#if index === 0}
<td class="whitespace-nowrap" rowspan={totalRegistros}> <td class="whitespace-nowrap" rowspan={totalRegistros}>
{#if grupoData.saldoDiario} <SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
<span
class="badge badge-lg font-semibold {grupoData.saldoDiario.positivo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
>
{formatarSaldoDiario(grupoData.saldoDiario)}
</span>
{:else}
<span class="badge badge-ghost badge-lg">-</span>
{/if}
</td> </td>
{/if} {/if}
<td class="whitespace-nowrap">
<LocalizacaoIcon dentroRaioPermitido={registro.dentroRaioPermitido} />
</td>
<td class="whitespace-nowrap"> <td class="whitespace-nowrap">
<span <span
class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}" class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"