feat: update point registration process with mandatory photo capture and location details

- Removed location details from the point receipt, now displayed only in detailed reports.
- Implemented mandatory photo capture during point registration, enhancing accountability.
- Added confirmation modal for users to verify details before finalizing point registration.
- Improved error handling for webcam access and photo capture, ensuring a smoother user experience.
- Enhanced UI components for better feedback and interaction during the registration process.
This commit is contained in:
2025-11-19 04:54:03 -03:00
parent 67d6b3ec72
commit d16f76daeb
4 changed files with 882 additions and 192 deletions

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown } 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 { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
import jsPDF from 'jspdf';
@@ -184,6 +184,410 @@
alert('Erro ao gerar ficha de ponto. Tente novamente.');
}
}
async function imprimirDetalhesRegistro(registroId: Id<'registrosPonto'>) {
try {
// Buscar dados completos do registro
const registro = await client.query(api.pontos.obterRegistro, { registroId });
if (!registro) {
alert('Registro não encontrado');
return;
}
const doc = new jsPDF();
// Logo
let yPosition = 20;
try {
const logoImg = new Image();
logoImg.src = logoGovPE;
await new Promise<void>((resolve, reject) => {
logoImg.onload = () => resolve();
logoImg.onerror = () => reject();
setTimeout(() => reject(), 3000);
});
const logoWidth = 25;
const aspectRatio = logoImg.height / logoImg.width;
const logoHeight = logoWidth * aspectRatio;
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
yPosition = Math.max(20, 10 + logoHeight / 2);
} catch (err) {
console.warn('Não foi possível carregar a logo:', err);
}
// Cabeçalho
doc.setFontSize(16);
doc.setTextColor(41, 128, 185);
doc.text('DETALHES DO REGISTRO DE PONTO', 105, yPosition, { align: 'center' });
yPosition += 15;
// Informações do Funcionário
doc.setFontSize(12);
doc.setTextColor(0, 0, 0);
doc.setFont('helvetica', 'bold');
doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 8;
doc.setFontSize(10);
if (registro.funcionario) {
if (registro.funcionario.matricula) {
doc.text(`Matrícula: ${registro.funcionario.matricula}`, 15, yPosition);
yPosition += 6;
}
doc.text(`Nome: ${registro.funcionario.nome}`, 15, yPosition);
yPosition += 6;
if (registro.funcionario.descricaoCargo) {
doc.text(`Cargo/Função: ${registro.funcionario.descricaoCargo}`, 15, yPosition);
yPosition += 6;
}
}
yPosition += 5;
// Informações do Registro
doc.setFont('helvetica', 'bold');
doc.text('DADOS DO REGISTRO', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 8;
doc.setFontSize(10);
doc.text(`Tipo: ${getTipoRegistroLabel(registro.tipo)}`, 15, yPosition);
yPosition += 6;
const dataHora = `${registro.data} ${registro.hora.toString().padStart(2, '0')}:${registro.minuto.toString().padStart(2, '0')}:${registro.segundo.toString().padStart(2, '0')}`;
doc.text(`Data e Hora: ${dataHora}`, 15, yPosition);
yPosition += 6;
doc.text(`Status: ${registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}`, 15, yPosition);
yPosition += 6;
doc.text(`Tolerância: ${registro.toleranciaMinutos} minutos`, 15, yPosition);
yPosition += 6;
doc.text(
`Sincronizado: ${registro.sincronizadoComServidor ? 'Sim (Servidor)' : 'Não (PC Local)'}`,
15,
yPosition
);
yPosition += 6;
if (registro.justificativa) {
doc.text(`Justificativa: ${registro.justificativa}`, 15, yPosition);
yPosition += 6;
}
yPosition += 5;
// Localização
if (registro.latitude && registro.longitude) {
// Verificar se precisa de nova página
if (yPosition > 200) {
doc.addPage();
yPosition = 20;
}
doc.setFont('helvetica', 'bold');
doc.text('LOCALIZAÇÃO', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 8;
doc.setFontSize(10);
doc.text(`Latitude: ${registro.latitude.toFixed(6)}`, 15, yPosition);
yPosition += 6;
doc.text(`Longitude: ${registro.longitude.toFixed(6)}`, 15, yPosition);
yPosition += 6;
if (registro.precisao) {
doc.text(`Precisão: ${registro.precisao.toFixed(2)} metros`, 15, yPosition);
yPosition += 6;
}
if (registro.endereco) {
doc.text(`Endereço: ${registro.endereco}`, 15, yPosition);
yPosition += 6;
}
if (registro.cidade) {
doc.text(`Cidade: ${registro.cidade}`, 15, yPosition);
yPosition += 6;
}
if (registro.estado) {
doc.text(`Estado: ${registro.estado}`, 15, yPosition);
yPosition += 6;
}
if (registro.pais) {
doc.text(`País: ${registro.pais}`, 15, yPosition);
yPosition += 6;
}
if (registro.timezone) {
doc.text(`Fuso Horário: ${registro.timezone}`, 15, yPosition);
yPosition += 6;
}
yPosition += 5;
}
// Dados Técnicos
// Verificar se precisa de nova página
if (yPosition > 200) {
doc.addPage();
yPosition = 20;
}
doc.setFont('helvetica', 'bold');
doc.text('DADOS TÉCNICOS', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 8;
doc.setFontSize(10);
// Informações de Rede
if (registro.ipAddress || registro.ipPublico || registro.ipLocal) {
doc.setFont('helvetica', 'bold');
doc.text('Rede:', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 6;
if (registro.ipAddress) {
doc.text(` IP: ${registro.ipAddress}`, 20, yPosition);
yPosition += 6;
}
if (registro.ipPublico) {
doc.text(` IP Público: ${registro.ipPublico}`, 20, yPosition);
yPosition += 6;
}
if (registro.ipLocal) {
doc.text(` IP Local: ${registro.ipLocal}`, 20, yPosition);
yPosition += 6;
}
yPosition += 3;
}
// Informações do Navegador
if (registro.browser || registro.userAgent) {
doc.setFont('helvetica', 'bold');
doc.text('Navegador:', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 6;
if (registro.browser) {
doc.text(` Navegador: ${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}`, 20, yPosition);
yPosition += 6;
}
if (registro.engine) {
doc.text(` Engine: ${registro.engine}`, 20, yPosition);
yPosition += 6;
}
if (registro.userAgent) {
// Quebrar user agent em múltiplas linhas se necessário
const userAgentLines = doc.splitTextToSize(` User Agent: ${registro.userAgent}`, 170);
doc.text(userAgentLines, 20, yPosition);
yPosition += userAgentLines.length * 6;
}
yPosition += 3;
}
// Informações do Sistema
if (registro.sistemaOperacional || registro.arquitetura) {
doc.setFont('helvetica', 'bold');
doc.text('Sistema:', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 6;
if (registro.sistemaOperacional) {
doc.text(` SO: ${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}`, 20, yPosition);
yPosition += 6;
}
if (registro.arquitetura) {
doc.text(` Arquitetura: ${registro.arquitetura}`, 20, yPosition);
yPosition += 6;
}
if (registro.plataforma) {
doc.text(` Plataforma: ${registro.plataforma}`, 20, yPosition);
yPosition += 6;
}
yPosition += 3;
}
// Informações do Dispositivo
if (registro.deviceType || registro.screenResolution) {
doc.setFont('helvetica', 'bold');
doc.text('Dispositivo:', 15, yPosition);
doc.setFont('helvetica', 'normal');
yPosition += 6;
if (registro.deviceType) {
doc.text(` Tipo: ${registro.deviceType}`, 20, yPosition);
yPosition += 6;
}
if (registro.deviceModel) {
doc.text(` Modelo: ${registro.deviceModel}`, 20, yPosition);
yPosition += 6;
}
if (registro.screenResolution) {
doc.text(` Resolução: ${registro.screenResolution}`, 20, yPosition);
yPosition += 6;
}
if (registro.coresTela) {
doc.text(` Cores: ${registro.coresTela}`, 20, yPosition);
yPosition += 6;
}
if (registro.isMobile || registro.isTablet || registro.isDesktop) {
const tipoDispositivo = registro.isMobile ? 'Mobile' : registro.isTablet ? 'Tablet' : 'Desktop';
doc.text(` Categoria: ${tipoDispositivo}`, 20, yPosition);
yPosition += 6;
}
if (registro.idioma) {
doc.text(` Idioma: ${registro.idioma}`, 20, yPosition);
yPosition += 6;
}
if (registro.connectionType) {
doc.text(` Conexão: ${registro.connectionType}`, 20, yPosition);
yPosition += 6;
}
if (registro.memoryInfo) {
doc.text(` Memória: ${registro.memoryInfo}`, 20, yPosition);
yPosition += 6;
}
yPosition += 3;
}
// Imagem capturada (se disponível)
if (registro.imagemUrl) {
yPosition += 10;
// Verificar se precisa de nova página
if (yPosition > 200) {
doc.addPage();
yPosition = 20;
}
doc.setFont('helvetica', 'bold');
doc.text('FOTO CAPTURADA', 105, yPosition, { align: 'center' });
doc.setFont('helvetica', 'normal');
yPosition += 10;
try {
// Carregar imagem usando fetch para evitar problemas de CORS
const response = await fetch(registro.imagemUrl);
if (!response.ok) {
throw new Error('Erro ao carregar imagem');
}
const blob = await response.blob();
const reader = new FileReader();
// Converter blob para base64
const base64 = await new Promise<string>((resolve, reject) => {
reader.onloadend = () => {
if (typeof reader.result === 'string') {
resolve(reader.result);
} else {
reject(new Error('Erro ao converter imagem'));
}
};
reader.onerror = () => reject(new Error('Erro ao ler imagem'));
reader.readAsDataURL(blob);
});
// Criar elemento de imagem para obter dimensões
const img = new Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error('Erro ao processar imagem'));
img.src = base64;
setTimeout(() => reject(new Error('Timeout ao processar imagem')), 10000);
});
// Calcular dimensões para caber na página (largura máxima 80mm, manter proporção)
const maxWidth = 80;
const maxHeight = 60;
let imgWidth = img.width;
let imgHeight = img.height;
const aspectRatio = imgWidth / imgHeight;
if (imgWidth > maxWidth || imgHeight > maxHeight) {
if (aspectRatio > 1) {
// Imagem horizontal
imgWidth = maxWidth;
imgHeight = maxWidth / aspectRatio;
} else {
// Imagem vertical
imgHeight = maxHeight;
imgWidth = maxHeight * aspectRatio;
}
}
// Centralizar imagem
const xPosition = (doc.internal.pageSize.getWidth() - imgWidth) / 2;
// Verificar se cabe na página atual
if (yPosition + imgHeight > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPosition = 20;
}
// Adicionar imagem ao PDF usando base64
doc.addImage(base64, 'JPEG', xPosition, yPosition, imgWidth, imgHeight);
yPosition += imgHeight + 10;
} catch (error) {
console.warn('Erro ao adicionar imagem ao PDF:', error);
doc.setFontSize(10);
doc.text('Foto não disponível para impressão', 105, yPosition, { align: 'center' });
yPosition += 6;
}
}
// Rodapé
const pageCount = doc.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(128, 128, 128);
doc.text(
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
doc.internal.pageSize.getWidth() / 2,
doc.internal.pageSize.getHeight() - 10,
{ align: 'center' }
);
}
// Salvar
const nomeArquivo = `detalhes-ponto-${registro.data}-${registro.hora.toString().padStart(2, '0')}${registro.minuto.toString().padStart(2, '0')}.pdf`;
doc.save(nomeArquivo);
} catch (error) {
console.error('Erro ao gerar PDF detalhado:', error);
alert('Erro ao gerar relatório detalhado. Tente novamente.');
}
}
</script>
<div class="container mx-auto px-4 py-6">
@@ -373,6 +777,7 @@
<th>Tipo</th>
<th>Horário</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
@@ -388,6 +793,16 @@
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
</span>
</td>
<td>
<button
class="btn btn-sm btn-outline btn-primary gap-2"
onclick={() => imprimirDetalhesRegistro(registro._id)}
title="Imprimir Detalhes"
>
<FileText class="h-4 w-4" />
Imprimir Detalhes
</button>
</td>
</tr>
{/each}
</tbody>