feat: enhance call and point registration features with sensor data integration
- Updated the CallWindow component to include connection quality states and reconnection attempts, improving user experience during calls. - Enhanced the ChatWindow to allow starting audio and video calls in a new window, providing users with more flexibility. - Integrated accelerometer and gyroscope data collection in the RegistroPonto component, enabling validation of point registration authenticity. - Improved error handling and user feedback for sensor permissions and data validation, ensuring a smoother registration process. - Updated backend logic to validate sensor data and adjust confidence scores for point registration, enhancing security against spoofing.
This commit is contained in:
@@ -28,6 +28,8 @@
|
||||
let carregando = $state(false);
|
||||
let mostrarModalImpressao = $state(false);
|
||||
let funcionarioParaImprimir = $state<Id<'funcionarios'> | ''>('');
|
||||
let mostrarModalDetalhes = $state(false);
|
||||
let registroDetalhesId = $state<Id<'registrosPonto'> | ''>('');
|
||||
let chartCanvas: HTMLCanvasElement;
|
||||
let chartInstance: Chart | null = null;
|
||||
|
||||
@@ -190,8 +192,12 @@
|
||||
|
||||
// Aguardar um pouco para garantir que o canvas está renderizado
|
||||
const timeoutId = setTimeout(() => {
|
||||
criarGrafico();
|
||||
}, 100);
|
||||
try {
|
||||
criarGrafico();
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar gráfico no effect:', error);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -201,11 +207,20 @@
|
||||
|
||||
// Também tentar criar quando o canvas for montado
|
||||
onMount(() => {
|
||||
if (chartCanvas && estatisticas && chartData) {
|
||||
setTimeout(() => {
|
||||
criarGrafico();
|
||||
}, 200);
|
||||
}
|
||||
// Tentar criar o gráfico após um pequeno delay para garantir que tudo está renderizado
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (chartCanvas && estatisticas && chartData && !chartInstance) {
|
||||
try {
|
||||
criarGrafico();
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar gráfico no onMount:', error);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -683,6 +698,12 @@
|
||||
minuto: number;
|
||||
dentroDoPrazo: boolean;
|
||||
dentroRaioPermitido: boolean | null | undefined;
|
||||
acelerometroX?: number | undefined;
|
||||
acelerometroY?: number | undefined;
|
||||
acelerometroZ?: number | undefined;
|
||||
movimentoDetectado?: boolean | undefined;
|
||||
magnitudeMovimento?: number | undefined;
|
||||
sensorDisponivel?: boolean | undefined;
|
||||
}>
|
||||
> = {};
|
||||
|
||||
@@ -698,6 +719,12 @@
|
||||
minuto: r.minuto,
|
||||
dentroDoPrazo: r.dentroDoPrazo,
|
||||
dentroRaioPermitido: r.dentroRaioPermitido,
|
||||
acelerometroX: r.acelerometroX,
|
||||
acelerometroY: r.acelerometroY,
|
||||
acelerometroZ: r.acelerometroZ,
|
||||
movimentoDetectado: r.movimentoDetectado,
|
||||
magnitudeMovimento: r.magnitudeMovimento,
|
||||
sensorDisponivel: r.sensorDisponivel,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -744,6 +771,21 @@
|
||||
|
||||
linha.push(reg.dentroDoPrazo ? 'Sim' : 'Não');
|
||||
|
||||
// Adicionar dados de acelerômetro (se disponível)
|
||||
if (reg.acelerometroX !== undefined || reg.sensorDisponivel !== undefined) {
|
||||
if (reg.sensorDisponivel === false && !reg.acelerometroX) {
|
||||
linha.push('Sensor: Não disponível');
|
||||
} else if (reg.acelerometroX !== undefined) {
|
||||
const movimento = reg.movimentoDetectado ? 'Sim' : 'Não';
|
||||
const magnitude = reg.magnitudeMovimento !== undefined ? reg.magnitudeMovimento.toFixed(2) : 'N/A';
|
||||
linha.push(`Mov: ${movimento} | Mag: ${magnitude} m/s²`);
|
||||
} else {
|
||||
linha.push('Sensor: N/A');
|
||||
}
|
||||
} else {
|
||||
linha.push('-');
|
||||
}
|
||||
|
||||
tableData.push(linha);
|
||||
}
|
||||
}
|
||||
@@ -754,6 +796,7 @@
|
||||
}
|
||||
headers.push('Localização');
|
||||
headers.push('Dentro do Prazo');
|
||||
headers.push('Acelerômetro');
|
||||
|
||||
// Salvar a posição Y antes da tabela
|
||||
const yPosAntesTabela = yPosition;
|
||||
@@ -985,6 +1028,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function abrirModalDetalhes(registroId: Id<'registrosPonto'>) {
|
||||
if (!registroId) {
|
||||
console.error('Erro: registroId inválido');
|
||||
return;
|
||||
}
|
||||
registroDetalhesId = registroId;
|
||||
mostrarModalDetalhes = true;
|
||||
}
|
||||
|
||||
function fecharModalDetalhes() {
|
||||
mostrarModalDetalhes = false;
|
||||
registroDetalhesId = '';
|
||||
}
|
||||
|
||||
async function imprimirDetalhesRegistro(registroId: Id<'registrosPonto'>) {
|
||||
try {
|
||||
// Buscar dados completos do registro
|
||||
@@ -1909,12 +1966,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<canvas bind:this={chartCanvas} class="w-full h-full"></canvas>
|
||||
{#if !chartInstance && estatisticas && chartData}
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-base-200/30 rounded-xl">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2234,8 +2286,8 @@
|
||||
<td class="whitespace-nowrap">
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-primary gap-2 hover:btn-primary hover:shadow-md transition-all"
|
||||
onclick={() => imprimirDetalhesRegistro(registro._id)}
|
||||
title="Imprimir Detalhes"
|
||||
onclick={() => abrirModalDetalhes(registro._id)}
|
||||
title="Ver Detalhes"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
Detalhes
|
||||
@@ -2267,3 +2319,124 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Detalhes do Registro -->
|
||||
{#if mostrarModalDetalhes && registroDetalhesId}
|
||||
{@const registroDetalhesQuery = useQuery(api.pontos.obterRegistro, registroDetalhesId ? { registroId: registroDetalhesId } : 'skip')}
|
||||
{@const registroDetalhes = registroDetalhesQuery?.data}
|
||||
|
||||
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && fecharModalDetalhes()}>
|
||||
<div class="modal-box max-w-4xl max-h-[90vh] overflow-hidden flex flex-col" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="flex items-center justify-between mb-4 pb-4 border-b border-base-300 flex-shrink-0">
|
||||
<h3 class="font-bold text-xl">Detalhes do Registro de Ponto</h3>
|
||||
<button class="btn btn-sm btn-circle btn-ghost" onclick={fecharModalDetalhes}>✕</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto pr-2">
|
||||
{#if registroDetalhesQuery === undefined || registroDetalhesQuery?.isLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<span class="ml-4">Carregando detalhes...</span>
|
||||
</div>
|
||||
{:else if registroDetalhesQuery?.error}
|
||||
<div class="alert alert-error">
|
||||
<XCircle class="h-6 w-6" />
|
||||
<div>
|
||||
<h3 class="font-bold">Erro ao carregar detalhes</h3>
|
||||
<div class="text-sm mt-1">{registroDetalhesQuery.error?.message || String(registroDetalhesQuery.error) || 'Erro desconhecido'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !registroDetalhes}
|
||||
<div class="alert alert-warning">
|
||||
<FileText class="h-6 w-6" />
|
||||
<span>Registro não encontrado</span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Informações Básicas -->
|
||||
<div class="card bg-base-200 mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold mb-2">Informações do Registro</h4>
|
||||
{#if registroDetalhes.funcionario}
|
||||
<p><strong>Funcionário:</strong> {registroDetalhes.funcionario.nome}</p>
|
||||
{#if registroDetalhes.funcionario.matricula}
|
||||
<p><strong>Matrícula:</strong> {registroDetalhes.funcionario.matricula}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
<p><strong>Data:</strong> {formatarDataDDMMAAAA(registroDetalhes.data)}</p>
|
||||
<p><strong>Horário:</strong> {formatarHoraPonto(registroDetalhes.hora, registroDetalhes.minuto)}</p>
|
||||
<p><strong>Status:</strong> <span class="badge {registroDetalhes.dentroDoPrazo ? 'badge-success' : 'badge-error'}">{registroDetalhes.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Localização GPS -->
|
||||
{#if registroDetalhes.latitude !== undefined && registroDetalhes.longitude !== undefined}
|
||||
<div class="card bg-base-200 mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold mb-2">Localização GPS</h4>
|
||||
<p><strong>Latitude:</strong> {registroDetalhes.latitude.toFixed(6)}</p>
|
||||
<p><strong>Longitude:</strong> {registroDetalhes.longitude.toFixed(6)}</p>
|
||||
{#if registroDetalhes.precisao !== undefined}
|
||||
<p><strong>Precisão:</strong> {registroDetalhes.precisao.toFixed(2)}m</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.endereco || registroDetalhes.cidade}
|
||||
<p><strong>Endereço:</strong> {registroDetalhes.endereco || ''} {registroDetalhes.cidade ? `, ${registroDetalhes.cidade}` : ''} {registroDetalhes.estado ? ` - ${registroDetalhes.estado}` : ''}</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.confiabilidadeGPS !== undefined}
|
||||
<p><strong>Confiabilidade GPS:</strong> {(registroDetalhes.confiabilidadeGPS * 100).toFixed(1)}%</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.scoreConfiancaBackend !== undefined}
|
||||
<p><strong>Score de Confiança:</strong> {(registroDetalhes.scoreConfiancaBackend * 100).toFixed(1)}%</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Dados de Sensores (Acelerômetro) -->
|
||||
{#if registroDetalhes.acelerometroX !== undefined || registroDetalhes.sensorDisponivel !== undefined}
|
||||
<div class="card bg-base-200 mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="font-bold mb-2">Dados de Sensores</h4>
|
||||
{#if registroDetalhes.sensorDisponivel === false && registroDetalhes.isDesktop !== true}
|
||||
<p class="text-warning"><strong>Sensor:</strong> Não disponível neste dispositivo</p>
|
||||
{:else if registroDetalhes.permissaoSensorNegada === true}
|
||||
<p class="text-error"><strong>Sensor:</strong> Permissão negada</p>
|
||||
{:else if registroDetalhes.acelerometroX !== undefined}
|
||||
<p><strong>Sensor:</strong> Disponível</p>
|
||||
<p><strong>Acelerômetro X:</strong> {registroDetalhes.acelerometroX.toFixed(3)} m/s²</p>
|
||||
{#if registroDetalhes.acelerometroY !== undefined}
|
||||
<p><strong>Acelerômetro Y:</strong> {registroDetalhes.acelerometroY.toFixed(3)} m/s²</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.acelerometroZ !== undefined}
|
||||
<p><strong>Acelerômetro Z:</strong> {registroDetalhes.acelerometroZ.toFixed(3)} m/s²</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.magnitudeMovimento !== undefined}
|
||||
<p><strong>Magnitude:</strong> {registroDetalhes.magnitudeMovimento.toFixed(3)} m/s²</p>
|
||||
{/if}
|
||||
{#if registroDetalhes.movimentoDetectado !== undefined}
|
||||
<p><strong>Movimento Detectado:</strong> <span class="badge {registroDetalhes.movimentoDetectado ? 'badge-success' : 'badge-warning'}">{registroDetalhes.movimentoDetectado ? 'Sim' : 'Não'}</span></p>
|
||||
{/if}
|
||||
{#if registroDetalhes.variacaoAcelerometro !== undefined}
|
||||
<p><strong>Variação:</strong> {registroDetalhes.variacaoAcelerometro.toFixed(6)}</p>
|
||||
{/if}
|
||||
{:else if registroDetalhes.isDesktop === true}
|
||||
<p class="text-info"><strong>Sensor:</strong> Não disponível em desktop (normal)</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-4 mt-4 border-t border-base-300 flex-shrink-0">
|
||||
{#if registroDetalhes}
|
||||
<button class="btn btn-primary gap-2" onclick={() => imprimirDetalhesRegistro(registroDetalhesId)}>
|
||||
<Printer class="h-4 w-4" />
|
||||
Imprimir PDF
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-outline" onclick={fecharModalDetalhes}>Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModalDetalhes}></form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { browser } from '$app/environment';
|
||||
import type { ConstructorOfATypedSvelteComponent } from 'svelte/types/runtime/component';
|
||||
import CybersecurityWizcard from '$lib/components/ti/CybersecurityWizcard.svelte';
|
||||
// Usar tipo amplo para evitar conflitos de tipagem do import dinâmico no build
|
||||
let Comp: ConstructorOfATypedSvelteComponent<typeof CybersecurityWizcard> | null = null;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -27,11 +23,5 @@
|
||||
<a href={resolve('/ti')} class="btn btn-outline btn-primary">Voltar para TI</a>
|
||||
</header>
|
||||
|
||||
{#if browser && Comp}
|
||||
<svelte:component this={Comp} />
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<span>Carregando módulo de cibersegurança…</span>
|
||||
</div>
|
||||
{/if}
|
||||
<CybersecurityWizcard />
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user