feat: enhance point registration and management features
- Added functionality to capture and display images during point registration, improving user experience. - Implemented error handling for image uploads and webcam access, ensuring smoother operation. - Introduced a justification field for point registration, allowing users to provide context for their entries. - Enhanced the backend to support new features, including image handling and justification storage. - Updated UI components for better layout and responsiveness, improving overall usability.
This commit is contained in:
@@ -5,17 +5,31 @@
|
||||
import RelogioSincronizado from './RelogioSincronizado.svelte';
|
||||
import WebcamCapture from './WebcamCapture.svelte';
|
||||
import ComprovantePonto from './ComprovantePonto.svelte';
|
||||
import ErrorModal from '../ErrorModal.svelte';
|
||||
import { obterTempoServidor } from '$lib/utils/sincronizacaoTempo';
|
||||
import { obterInformacoesDispositivo } from '$lib/utils/deviceInfo';
|
||||
import { formatarHoraPonto, getTipoRegistroLabel, getProximoTipoRegistro } from '$lib/utils/ponto';
|
||||
import { LogIn, LogOut, Clock, CheckCircle2, XCircle, Camera, MapPin } from 'lucide-svelte';
|
||||
import {
|
||||
formatarHoraPonto,
|
||||
getTipoRegistroLabel,
|
||||
getProximoTipoRegistro
|
||||
} from '$lib/utils/ponto';
|
||||
import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown } from 'lucide-svelte';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Queries
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||
const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, {});
|
||||
|
||||
// Query para histórico e saldo do dia
|
||||
const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null);
|
||||
const dataHoje = $derived(new Date().toISOString().split('T')[0]!);
|
||||
const historicoSaldoQuery = useQuery(
|
||||
api.pontos.obterHistoricoESaldoDia,
|
||||
funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip'
|
||||
);
|
||||
|
||||
// Estados
|
||||
let mostrandoWebcam = $state(false);
|
||||
@@ -26,6 +40,11 @@
|
||||
let mostrandoComprovante = $state(false);
|
||||
let imagemCapturada = $state<Blob | null>(null);
|
||||
let coletandoInfo = $state(false);
|
||||
let capturandoAutomaticamente = $state(false);
|
||||
let mostrarModalErro = $state(false);
|
||||
let mensagemErroModal = $state('');
|
||||
let detalhesErroModal = $state('');
|
||||
let justificativa = $state('');
|
||||
|
||||
const registrosHoje = $derived(registrosHojeQuery?.data || []);
|
||||
const config = $derived(configQuery?.data);
|
||||
@@ -54,7 +73,7 @@
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': file.type },
|
||||
body: file,
|
||||
body: file
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -87,10 +106,16 @@
|
||||
const timestamp = await obterTempoServidor(client);
|
||||
const sincronizadoComServidor = true; // Sempre true quando usamos obterTempoServidor
|
||||
|
||||
// Upload da imagem se houver
|
||||
// Upload da imagem se houver (não bloquear se falhar)
|
||||
let imagemId: Id<'_storage'> | undefined = undefined;
|
||||
if (imagemCapturada) {
|
||||
imagemId = await uploadImagem(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;
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar ponto
|
||||
@@ -99,11 +124,13 @@
|
||||
informacoesDispositivo,
|
||||
timestamp,
|
||||
sincronizadoComServidor,
|
||||
justificativa: justificativa.trim() || undefined
|
||||
});
|
||||
|
||||
registroId = resultado.registroId;
|
||||
sucesso = `Ponto registrado com sucesso! Tipo: ${getTipoRegistroLabel(resultado.tipo)}`;
|
||||
imagemCapturada = null;
|
||||
justificativa = ''; // Limpar justificativa após registro
|
||||
|
||||
// Mostrar comprovante após 1 segundo
|
||||
setTimeout(() => {
|
||||
@@ -111,23 +138,71 @@
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Erro ao registrar ponto:', error);
|
||||
erro = error instanceof Error ? error.message : 'Erro ao registrar ponto';
|
||||
const mensagemErro = error instanceof Error ? error.message : 'Erro ao registrar ponto';
|
||||
|
||||
// Verificar se é erro de registro duplicado
|
||||
if (
|
||||
mensagemErro.includes('Já existe um registro neste minuto') ||
|
||||
mensagemErro.includes('já existe um registro')
|
||||
) {
|
||||
mensagemErroModal = 'Registro de ponto duplicado';
|
||||
detalhesErroModal = `Não é possível registrar o ponto no mesmo minuto.\n\nVocê já possui um registro de ${getTipoRegistroLabel(proximoTipo)} para este minuto.\n\nPor favor, aguarde pelo menos 1 minuto antes de tentar registrar novamente.`;
|
||||
mostrarModalErro = true;
|
||||
} else {
|
||||
// Outros erros também mostram no modal
|
||||
mensagemErroModal = 'Erro ao registrar ponto';
|
||||
detalhesErroModal = mensagemErro;
|
||||
mostrarModalErro = true;
|
||||
}
|
||||
|
||||
erro = mensagemErro;
|
||||
} finally {
|
||||
registrando = false;
|
||||
coletandoInfo = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleWebcamCapture(blob: Blob) {
|
||||
imagemCapturada = blob;
|
||||
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) {
|
||||
capturandoAutomaticamente = false;
|
||||
// Pequeno delay para garantir que a imagem foi processada (se houver)
|
||||
setTimeout(() => {
|
||||
registrarPonto();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWebcamCancel() {
|
||||
const estavaCapturando = capturandoAutomaticamente;
|
||||
mostrandoWebcam = false;
|
||||
capturandoAutomaticamente = false;
|
||||
imagemCapturada = null;
|
||||
// Se estava capturando automaticamente e cancelou, registrar sem foto
|
||||
if (estavaCapturando) {
|
||||
registrarPonto();
|
||||
}
|
||||
}
|
||||
|
||||
function abrirWebcam() {
|
||||
function handleWebcamError() {
|
||||
// Em caso de erro na captura, registrar sem foto
|
||||
mostrandoWebcam = false;
|
||||
capturandoAutomaticamente = false;
|
||||
imagemCapturada = null;
|
||||
// Registrar ponto sem foto
|
||||
registrarPonto();
|
||||
}
|
||||
|
||||
async function iniciarRegistroComFoto() {
|
||||
if (registrando || coletandoInfo) return;
|
||||
|
||||
// Abrir webcam automaticamente
|
||||
capturandoAutomaticamente = true;
|
||||
mostrandoWebcam = true;
|
||||
}
|
||||
|
||||
@@ -136,10 +211,75 @@
|
||||
registroId = null;
|
||||
}
|
||||
|
||||
function fecharModalErro() {
|
||||
mostrarModalErro = false;
|
||||
mensagemErroModal = '';
|
||||
detalhesErroModal = '';
|
||||
erro = null;
|
||||
}
|
||||
|
||||
const podeRegistrar = $derived.by(() => {
|
||||
return !registrando && !coletandoInfo && config !== undefined;
|
||||
});
|
||||
|
||||
// Referência para o modal
|
||||
let modalRef: HTMLDivElement | null = $state(null);
|
||||
|
||||
// Efeito para garantir que o modal fique visível quando abrir
|
||||
$effect(() => {
|
||||
if (mostrandoWebcam && modalRef) {
|
||||
// Aguardar um frame para garantir que o DOM foi atualizado
|
||||
setTimeout(() => {
|
||||
if (modalRef) {
|
||||
modalRef.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Também garantir que o modal-box esteja visível
|
||||
const modalBox = modalRef.querySelector('.modal-box');
|
||||
if (modalBox) {
|
||||
(modalBox as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Solicitar permissões automaticamente ao montar o componente
|
||||
onMount(async () => {
|
||||
// Solicitar permissão de webcam
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
try {
|
||||
// Solicitar apenas permissão, sem iniciar o stream ainda
|
||||
await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
} catch (error) {
|
||||
// Ignorar erro silenciosamente - a permissão será solicitada quando necessário
|
||||
console.log('Permissão de webcam não concedida ainda');
|
||||
}
|
||||
}
|
||||
|
||||
// Solicitar permissão de geolocalização
|
||||
if (navigator.geolocation) {
|
||||
try {
|
||||
// Solicitar permissão de geolocalização (timeout curto para não bloquear)
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeoutId = setTimeout(() => resolve(), 2000); // Timeout de 2 segundos
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
() => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve();
|
||||
},
|
||||
() => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(); // Resolver mesmo se negado
|
||||
},
|
||||
{ timeout: 2000, maximumAge: 0, enableHighAccuracy: false } // enableHighAccuracy false para ser mais rápido
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignorar erro silenciosamente
|
||||
console.log('Permissão de geolocalização não concedida ainda');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mapaHorarios = $derived.by(() => {
|
||||
if (!config) return [];
|
||||
|
||||
@@ -147,7 +287,7 @@
|
||||
{ tipo: 'entrada', horario: config.horarioEntrada, label: 'Entrada' },
|
||||
{ tipo: 'saida_almoco', horario: config.horarioSaidaAlmoco, label: 'Saída para Almoço' },
|
||||
{ tipo: 'retorno_almoco', horario: config.horarioRetornoAlmoco, label: 'Retorno do Almoço' },
|
||||
{ tipo: 'saida', horario: config.horarioSaida, label: 'Saída' },
|
||||
{ tipo: 'saida', horario: config.horarioSaida, label: 'Saída' }
|
||||
];
|
||||
|
||||
return horarios.map((h) => {
|
||||
@@ -156,10 +296,33 @@
|
||||
...h,
|
||||
registrado: !!registro,
|
||||
horarioRegistrado: registro ? formatarHoraPonto(registro.hora, registro.minuto) : null,
|
||||
dentroDoPrazo: registro?.dentroDoPrazo ?? null,
|
||||
dentroDoPrazo: registro?.dentroDoPrazo ?? null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Dados do histórico e saldo
|
||||
const historicoSaldo = $derived(historicoSaldoQuery?.data);
|
||||
const registrosOrdenados = $derived.by(() => {
|
||||
if (!historicoSaldo?.registros) return [];
|
||||
return [...historicoSaldo.registros].sort((a, b) => {
|
||||
const minutosA = a.hora * 60 + a.minuto;
|
||||
const minutosB = b.hora * 60 + b.minuto;
|
||||
return minutosA - minutosB;
|
||||
});
|
||||
});
|
||||
|
||||
// Formatação do saldo
|
||||
const saldoFormatado = $derived.by(() => {
|
||||
if (!historicoSaldo) return null;
|
||||
const minutos = historicoSaldo.saldoMinutos;
|
||||
const horas = Math.floor(Math.abs(minutos) / 60);
|
||||
const mins = Math.abs(minutos) % 60;
|
||||
const sinal = minutos >= 0 ? '+' : '-';
|
||||
return `${sinal}${horas}h ${mins}min`;
|
||||
});
|
||||
|
||||
const saldoPositivo = $derived(historicoSaldo ? historicoSaldo.saldoMinutos >= 0 : false);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -177,25 +340,27 @@
|
||||
<Clock class="h-5 w-5" />
|
||||
Horários do Dia
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
||||
{#each mapaHorarios as horario}
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{#each mapaHorarios as horario (horario.tipo)}
|
||||
<div
|
||||
class="card {horario.registrado ? 'bg-success/10 border-success' : 'bg-base-200'} border-2"
|
||||
class="card {horario.registrado
|
||||
? 'bg-success/10 border-success'
|
||||
: 'bg-base-200'} border-2"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="font-semibold">{horario.label}</span>
|
||||
{#if horario.registrado}
|
||||
{#if horario.dentroDoPrazo}
|
||||
<CheckCircle2 class="h-5 w-5 text-success" />
|
||||
<CheckCircle2 class="text-success h-5 w-5" />
|
||||
{:else}
|
||||
<XCircle class="h-5 w-5 text-error" />
|
||||
<XCircle class="text-error h-5 w-5" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">{horario.horario}</div>
|
||||
{#if horario.registrado}
|
||||
<div class="text-sm text-base-content/70">
|
||||
<div class="text-base-content/70 text-sm">
|
||||
Registrado: {horario.horarioRegistrado}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -210,14 +375,7 @@
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body items-center">
|
||||
<h2 class="card-title mb-4">Registrar Ponto</h2>
|
||||
<div class="flex flex-col items-center gap-4 w-full">
|
||||
{#if erro}
|
||||
<div class="alert alert-error w-full">
|
||||
<XCircle class="h-5 w-5" />
|
||||
<span>{erro}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full flex-col items-center gap-4">
|
||||
{#if sucesso}
|
||||
<div class="alert alert-success w-full">
|
||||
<CheckCircle2 class="h-5 w-5" />
|
||||
@@ -225,62 +383,166 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-lg font-semibold">Próximo registro: {tipoLabel}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
{#if !imagemCapturada}
|
||||
<button class="btn btn-outline btn-primary" onclick={abrirWebcam} disabled={!podeRegistrar}>
|
||||
<Camera class="h-5 w-5" />
|
||||
Capturar Foto
|
||||
</button>
|
||||
{:else}
|
||||
<div class="badge badge-primary badge-lg gap-2">
|
||||
<Camera class="h-4 w-4" />
|
||||
Foto capturada
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-lg"
|
||||
onclick={registrarPonto}
|
||||
disabled={!podeRegistrar}
|
||||
>
|
||||
{#if registrando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{#if coletandoInfo}
|
||||
Coletando informações...
|
||||
{:else}
|
||||
Registrando...
|
||||
{/if}
|
||||
{:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
|
||||
<LogIn class="h-5 w-5" />
|
||||
Registrar Entrada
|
||||
{:else}
|
||||
<LogOut class="h-5 w-5" />
|
||||
Registrar Saída
|
||||
{/if}
|
||||
</button>
|
||||
<!-- Campo de Justificativa (Opcional) -->
|
||||
<div class="w-full">
|
||||
<label for="justificativa" class="label">
|
||||
<span class="label-text">Justificativa (Opcional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="justificativa"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Digite uma justificativa para este registro de ponto (opcional)"
|
||||
bind:value={justificativa}
|
||||
disabled={registrando}
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-lg"
|
||||
onclick={iniciarRegistroComFoto}
|
||||
disabled={!podeRegistrar}
|
||||
>
|
||||
{#if registrando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{#if coletandoInfo}
|
||||
Coletando informações...
|
||||
{:else}
|
||||
Registrando...
|
||||
{/if}
|
||||
{:else if proximoTipo === 'entrada' || proximoTipo === 'retorno_almoco'}
|
||||
<LogIn class="h-5 w-5" />
|
||||
Registrar Entrada
|
||||
{:else}
|
||||
<LogOut class="h-5 w-5" />
|
||||
Registrar Saída
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Histórico e Saldo do Dia -->
|
||||
{#if historicoSaldo && registrosOrdenados.length > 0}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<Clock class="h-5 w-5" />
|
||||
Histórico do Dia
|
||||
</h2>
|
||||
|
||||
<!-- Saldo de Horas -->
|
||||
<div class="my-4 rounded-lg border-2 p-4 {saldoPositivo ? 'border-success bg-success/10' : 'border-error bg-error/10'}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold opacity-70">Saldo de Horas</p>
|
||||
<p class="text-2xl font-bold">
|
||||
{saldoFormatado}
|
||||
</p>
|
||||
</div>
|
||||
{#if saldoPositivo}
|
||||
<TrendingUp class="h-8 w-8 text-success" />
|
||||
{:else}
|
||||
<TrendingDown class="h-8 w-8 text-error" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2 text-sm opacity-70">
|
||||
<p>Carga Horária Diária: {Math.floor(historicoSaldo.cargaHorariaDiaria / 60)}h {historicoSaldo.cargaHorariaDiaria % 60}min</p>
|
||||
<p>Horas Trabalhadas: {Math.floor(historicoSaldo.horasTrabalhadas / 60)}h {historicoSaldo.horasTrabalhadas % 60}min</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Registros -->
|
||||
<div class="divider"></div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-semibold">Registros Realizados</h3>
|
||||
<div class="space-y-3">
|
||||
{#each registrosOrdenados as registro (registro._id)}
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<span class="font-semibold">{getTipoRegistroLabel(registro.tipo)}</span>
|
||||
{#if registro.dentroDoPrazo}
|
||||
<CheckCircle2 class="h-4 w-4 text-success" />
|
||||
{:else}
|
||||
<XCircle class="h-4 w-4 text-error" />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-lg font-bold">
|
||||
{formatarHoraPonto(registro.hora, registro.minuto)}
|
||||
</p>
|
||||
{#if registro.justificativa}
|
||||
<div class="mt-2 rounded bg-base-300 p-2">
|
||||
<p class="text-xs font-semibold opacity-70">Justificativa:</p>
|
||||
<p class="text-sm">{registro.justificativa}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Webcam -->
|
||||
{#if mostrandoWebcam}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="font-bold text-lg mb-4">Capturar Foto</h3>
|
||||
<WebcamCapture onCapture={handleWebcamCapture} onCancel={handleWebcamCancel} />
|
||||
<div
|
||||
bind:this={modalRef}
|
||||
class="modal modal-open"
|
||||
style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999;"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="min-h-[200px] flex items-center justify-center py-4">
|
||||
<WebcamCapture
|
||||
onCapture={handleWebcamCapture}
|
||||
onCancel={handleWebcamCancel}
|
||||
onError={handleWebcamError}
|
||||
autoCapture={capturandoAutomaticamente}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={handleWebcamCancel}></div>
|
||||
<form
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleWebcamCancel();
|
||||
}}
|
||||
>
|
||||
<button type="submit" aria-label="Fechar modal">fechar</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Comprovante -->
|
||||
{#if mostrandoComprovante && registroId}
|
||||
<ComprovantePonto registroId={registroId} onClose={fecharComprovante} />
|
||||
<ComprovantePonto {registroId} onClose={fecharComprovante} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal de Erro -->
|
||||
<ErrorModal
|
||||
open={mostrarModalErro}
|
||||
title={mensagemErroModal || 'Erro ao registrar ponto'}
|
||||
message={detalhesErroModal || mensagemErroModal || 'Ocorreu um erro ao registrar o ponto. Tente novamente.'}
|
||||
onClose={fecharModalErro}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user