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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user