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

@@ -45,6 +45,8 @@
let mensagemErroModal = $state('');
let detalhesErroModal = $state('');
let justificativa = $state('');
let mostrandoModalConfirmacao = $state(false);
let dataHoraAtual = $state<{ data: string; hora: string } | null>(null);
const registrosHoje = $derived(registrosHojeQuery?.data || []);
const config = $derived(configQuery?.data);
@@ -89,9 +91,64 @@
}
}
// Verificar permissões de localização e webcam
async function verificarPermissoes(): Promise<{ localizacao: boolean; webcam: boolean }> {
let localizacaoAutorizada = false;
let webcamAutorizada = false;
// Verificar permissão de geolocalização
if (navigator.geolocation) {
try {
await new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Timeout'));
}, 5000);
navigator.geolocation.getCurrentPosition(
() => {
clearTimeout(timeoutId);
localizacaoAutorizada = true;
resolve();
},
() => {
clearTimeout(timeoutId);
reject(new Error('Permissão de localização negada'));
},
{ timeout: 5000, maximumAge: 0, enableHighAccuracy: false }
);
});
} catch (error) {
console.warn('Permissão de localização não concedida:', error);
}
}
// Verificar permissão de webcam
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
webcamAutorizada = true;
// Parar o stream imediatamente, apenas verificamos a permissão
stream.getTracks().forEach(track => track.stop());
} catch (error) {
console.warn('Permissão de webcam não concedida:', error);
}
}
return { localizacao: localizacaoAutorizada, webcam: webcamAutorizada };
}
async function registrarPonto() {
if (registrando) return;
// Verificar permissões antes de registrar
const permissoes = await verificarPermissoes();
if (!permissoes.localizacao || !permissoes.webcam) {
mensagemErroModal = 'Permissões necessárias';
detalhesErroModal = 'Para registrar o ponto, é necessário autorizar o compartilhamento de localização e a captura de foto.';
mostrarModalErro = true;
return;
}
registrando = true;
erro = null;
sucesso = null;
@@ -106,16 +163,17 @@
const timestamp = await obterTempoServidor(client);
const sincronizadoComServidor = true; // Sempre true quando usamos obterTempoServidor
// Upload da imagem se houver (não bloquear se falhar)
// Upload da imagem (obrigatória agora)
let imagemId: Id<'_storage'> | undefined = undefined;
if (imagemCapturada) {
try {
imagemId = await uploadImagem(imagemCapturada);
} catch (error) {
console.warn('Erro ao fazer upload da imagem, continuando sem foto:', error);
// Continuar sem foto se o upload falhar
imagemId = undefined;
console.error('Erro ao fazer upload da imagem:', error);
throw new Error('Erro ao fazer upload da imagem. Tente novamente.');
}
} else {
throw new Error('É necessário capturar uma foto para registrar o ponto.');
}
// Registrar ponto
@@ -131,6 +189,7 @@
sucesso = `Ponto registrado com sucesso! Tipo: ${getTipoRegistroLabel(resultado.tipo)}`;
imagemCapturada = null;
justificativa = ''; // Limpar justificativa após registro
mostrandoModalConfirmacao = false;
// Mostrar comprovante após 1 segundo
setTimeout(() => {
@@ -162,50 +221,82 @@
}
}
function handleWebcamCapture(blob: Blob | null) {
async function handleWebcamCapture(blob: Blob | null) {
if (blob) {
imagemCapturada = blob;
}
mostrandoWebcam = false;
// Se estava capturando automaticamente, registrar o ponto após capturar (com ou sem foto)
if (capturandoAutomaticamente) {
// Se capturou a foto, mostrar modal de confirmação
if (blob && capturandoAutomaticamente) {
capturandoAutomaticamente = false;
// Pequeno delay para garantir que a imagem foi processada (se houver)
setTimeout(() => {
registrarPonto();
}, 100);
// Obter data e hora sincronizada do servidor
try {
const timestamp = await obterTempoServidor(client);
const dataObj = new Date(timestamp);
const data = dataObj.toLocaleDateString('pt-BR');
const hora = dataObj.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
dataHoraAtual = { data, hora };
} catch (error) {
console.warn('Erro ao obter tempo do servidor, usando tempo local:', error);
atualizarDataHoraAtual();
}
mostrandoModalConfirmacao = true;
}
}
function handleWebcamCancel() {
const estavaCapturando = capturandoAutomaticamente;
mostrandoWebcam = false;
capturandoAutomaticamente = false;
imagemCapturada = null;
// Se estava capturando automaticamente e cancelou, registrar sem foto
if (estavaCapturando) {
registrarPonto();
}
mostrandoModalConfirmacao = false;
}
function handleWebcamError() {
// Em caso de erro na captura, registrar sem foto
// Em caso de erro na captura, fechar tudo
mostrandoWebcam = false;
capturandoAutomaticamente = false;
imagemCapturada = null;
// Registrar ponto sem foto
registrarPonto();
mostrandoModalConfirmacao = false;
mensagemErroModal = 'Erro ao capturar foto';
detalhesErroModal = 'Não foi possível acessar a webcam. Verifique as permissões do navegador.';
mostrarModalErro = true;
}
function atualizarDataHoraAtual() {
const agora = new Date();
const data = agora.toLocaleDateString('pt-BR');
const hora = agora.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
dataHoraAtual = { data, hora };
}
async function iniciarRegistroComFoto() {
if (registrando || coletandoInfo) return;
// Abrir webcam automaticamente
// Verificar permissões antes de abrir webcam
const permissoes = await verificarPermissoes();
if (!permissoes.localizacao || !permissoes.webcam) {
mensagemErroModal = 'Permissões necessárias';
detalhesErroModal = 'Para registrar o ponto, é necessário autorizar o compartilhamento de localização e a captura de foto.';
mostrarModalErro = true;
return;
}
// Abrir webcam
capturandoAutomaticamente = true;
mostrandoWebcam = true;
}
function confirmarRegistro() {
mostrandoModalConfirmacao = false;
registrarPonto();
}
function cancelarRegistro() {
mostrandoModalConfirmacao = false;
imagemCapturada = null;
}
function fecharComprovante() {
mostrandoComprovante = false;
registroId = null;
@@ -503,20 +594,15 @@
>
<div class="modal-box max-w-2xl w-[95%] max-h-[90vh] overflow-y-auto relative" style="margin: auto; position: relative;">
<div class="sticky top-0 bg-base-100 z-10 pb-3 mb-4 border-b border-base-300 -mx-6 px-6">
<h3 class="text-lg font-bold">
{#if capturandoAutomaticamente}
Capturando foto automaticamente...
{:else}
Capturar Foto
{/if}
</h3>
<h3 class="text-lg font-bold">Capturar Foto</h3>
</div>
<div class="min-h-[200px] flex items-center justify-center py-4">
<WebcamCapture
onCapture={handleWebcamCapture}
onCancel={handleWebcamCancel}
onError={handleWebcamError}
autoCapture={capturandoAutomaticamente}
autoCapture={false}
fotoObrigatoria={true}
/>
</div>
</div>
@@ -533,6 +619,61 @@
</div>
{/if}
<!-- Modal de Confirmação -->
{#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual}
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg">Confirmar Registro de Ponto</h3>
<button class="btn btn-sm btn-circle btn-ghost" onclick={cancelarRegistro}>
<XCircle class="h-5 w-5" />
</button>
</div>
<div class="space-y-4">
<!-- Imagem capturada -->
<div class="flex justify-center">
<img
src={URL.createObjectURL(imagemCapturada)}
alt="Foto capturada"
class="max-w-full max-h-96 rounded-lg border-2 border-primary object-contain"
/>
</div>
<!-- Data e Hora -->
<div class="text-center space-y-2">
<div class="text-lg font-semibold">
<span class="text-base-content/70">Data: </span>
<span>{dataHoraAtual.data}</span>
</div>
<div class="text-lg font-semibold">
<span class="text-base-content/70">Hora: </span>
<span>{dataHoraAtual.hora}</span>
</div>
</div>
<!-- Botões -->
<div class="flex justify-end gap-2 pt-4">
<button class="btn btn-outline" onclick={cancelarRegistro} disabled={registrando}>
<XCircle class="h-5 w-5" />
Cancelar
</button>
<button class="btn btn-primary" onclick={confirmarRegistro} disabled={registrando}>
{#if registrando}
<span class="loading loading-spinner loading-sm"></span>
Registrando...
{:else}
<CheckCircle2 class="h-5 w-5" />
Confirmar Registro
{/if}
</button>
</div>
</div>
</div>
<div class="modal-backdrop" onclick={cancelarRegistro}></div>
</div>
{/if}
<!-- Modal Comprovante -->
{#if mostrandoComprovante && registroId}
<ComprovantePonto {registroId} onClose={fecharComprovante} />