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:
2025-11-22 20:49:52 -03:00
parent fc4b5c5ba5
commit f818756efc
15 changed files with 2100 additions and 275 deletions

View File

@@ -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}

View File

@@ -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>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import CallWindow from '$lib/components/call/CallWindow.svelte';
import { obterDadosChamadaDaUrl, notificarJanelaPai } from '$lib/utils/callWindowManager';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
let dadosChamada = $state<{
chamadaId: Id<'chamadas'>;
conversaId: Id<'conversas'>;
tipo: 'audio' | 'video';
roomName: string;
ehAnfitriao: boolean;
} | null>(null);
let erro = $state<string | null>(null);
onMount(() => {
if (!browser) return;
// Obter dados da URL
const dados = obterDadosChamadaDaUrl();
if (!dados) {
erro = 'Dados da chamada não encontrados na URL.';
return;
}
dadosChamada = {
chamadaId: dados.chamadaId as Id<'chamadas'>,
conversaId: dados.conversaId as Id<'conversas'>,
tipo: dados.tipo,
roomName: dados.roomName,
ehAnfitriao: dados.ehAnfitriao
};
// Notificar janela pai que está pronto
notificarJanelaPai('ready');
// Detectar fechamento da janela
window.addEventListener('beforeunload', () => {
notificarJanelaPai('closed');
});
});
function handleClose(): void {
notificarJanelaPai('closed');
window.close();
}
</script>
<div class="h-screen w-screen bg-base-100">
{#if erro}
<div class="flex h-full w-full items-center justify-center">
<div class="text-center">
<div class="alert alert-error max-w-md">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{erro}</span>
</div>
<button class="btn btn-primary mt-4" onclick={handleClose}>Fechar</button>
</div>
</div>
{:else if dadosChamada}
<CallWindow
chamadaId={dadosChamada.chamadaId}
conversaId={dadosChamada.conversaId}
tipo={dadosChamada.tipo}
roomName={dadosChamada.roomName}
ehAnfitriao={dadosChamada.ehAnfitriao}
onClose={handleClose}
/>
{:else}
<div class="flex h-full w-full items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{/if}
</div>