From 02b8d72f599752c8a18d666eda79b09a7a82f995 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 29 Nov 2025 11:58:55 -0300 Subject: [PATCH] feat: enhance point registration with improved timestamp synchronization and direct photo processing, and add Convex Svelte best practices documentation. --- .agent/rules/convex-svelte-best-practices.md | 127 +++ .../lib/components/ponto/RegistroPonto.svelte | 809 ++++++++++-------- 2 files changed, 586 insertions(+), 350 deletions(-) create mode 100644 .agent/rules/convex-svelte-best-practices.md diff --git a/.agent/rules/convex-svelte-best-practices.md b/.agent/rules/convex-svelte-best-practices.md new file mode 100644 index 0000000..720db6d --- /dev/null +++ b/.agent/rules/convex-svelte-best-practices.md @@ -0,0 +1,127 @@ +--- +trigger: glob +globs: **/*.svelte.ts,**/*.svelte +--- + +# Convex + Svelte Best Practices + +This document outlines the mandatory rules and best practices for integrating Convex with Svelte in this project. + +## 1. Imports + +Always use the following import paths. Do NOT use `$lib/convex` or relative paths for generated files unless specifically required by a local override. + +### Correct Imports: + +```typescript +import { useQuery, useConvexClient } from 'convex-svelte'; +import { api } from '@sgse-app/backend/convex/_generated/api'; +import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel'; +``` + +### Incorrect Imports (Avoid): + +```typescript +import { convex } from '$lib/convex'; // Avoid direct client usage for queries +import { api } from '$lib/convex/_generated/api'; // Incorrect path +import { api } from '../convex/_generated/api'; // Relative path +``` + +## 2. Data Fetching + +### Use `useQuery` for Reactivity + +Instead of manually fetching data inside `onMount`, use the `useQuery` hook. This ensures your data is reactive and automatically updates when the backend data changes. + +**Preferred Pattern:** + +```svelte + +``` + +**Avoid Pattern:** + +```svelte + +``` + +### Mutations + +Use `useConvexClient` to access the client for mutations. + +```svelte + +``` + +## 3. Type Safety + +### No `any` + +Strictly avoid using `any`. The Convex generated data model provides precise types for all your tables. + +### Use Generated Types + +Use `Doc<"tableName">` for full document objects and `Id<"tableName">` for IDs. + +**Correct:** + +```typescript +import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel'; + +let selectedTask: Doc<'tasks'> | null = $state(null); +let taskId: Id<'tasks'>; +``` + +**Incorrect:** + +```typescript +let selectedTask: any = $state(null); +let taskId: string; +``` + +### Union Types for Enums + +When dealing with status fields or other enums, define the specific union type instead of casting to `any`. + +**Correct:** + +```typescript +async function updateStatus(newStatus: 'pending' | 'completed' | 'archived') { + // ... +} +``` + +**Incorrect:** + +```typescript +async function updateStatus(newStatus: string) { + // ... + status: newStatus as any; // Avoid this +} +``` \ No newline at end of file diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index 40170bc..7c47383 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -13,7 +13,17 @@ getTipoRegistroLabel, getProximoTipoRegistro } from '$lib/utils/ponto'; - import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown, Printer, Camera } from 'lucide-svelte'; + import { + LogIn, + LogOut, + Clock, + CheckCircle2, + XCircle, + TrendingUp, + TrendingDown, + Printer, + Camera + } from 'lucide-svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import jsPDF from 'jspdf'; import logoGovPE from '$lib/assets/logo_governo_PE.png'; @@ -27,17 +37,17 @@ // Queries const currentUser = useQuery(api.auth.getCurrentUser, {}); const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {}); - + // Query para histórico e saldo do dia const funcionarioId = $derived(currentUser?.data?.funcionarioId ?? null); const dataHoje = $derived(new Date().toISOString().split('T')[0]!); - + // Usar refreshKey para forçar atualização após registro - const registrosHojeQuery = useQuery( - api.pontos.listarRegistrosDia, - { data: dataHoje, _refresh: refreshKey } - ); - + const registrosHojeQuery = useQuery(api.pontos.listarRegistrosDia, { + data: dataHoje, + _refresh: refreshKey + }); + const historicoSaldoQuery = useQuery( api.pontos.obterHistoricoESaldoDia, funcionarioId && dataHoje ? { funcionarioId, data: dataHoje, _refresh: refreshKey } : 'skip' @@ -63,9 +73,12 @@ let detalhesErroModal = $state(''); let justificativa = $state(''); let mostrandoModalConfirmacao = $state(false); + let mostrandoTransicao = $state(false); // Novo estado para transição let dataHoraAtual = $state<{ data: string; hora: string } | null>(null); let aguardandoProcessamento = $state(false); - let etapaProcessamento = $state<'coletando' | 'sincronizando' | 'upload' | 'registrando' | null>(null); + let etapaProcessamento = $state<'coletando' | 'sincronizando' | 'upload' | 'registrando' | null>( + null + ); const registrosHoje = $derived(registrosHojeQuery?.data || []); const config = $derived(configQuery?.data); @@ -84,7 +97,7 @@ nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, + nomeSaida: config.nomeSaida }); } return getTipoRegistroLabel(proximoTipo); @@ -119,8 +132,8 @@ } // Verificar permissões de localização e webcam - async function verificarPermissoes(): Promise<{ - localizacao: boolean; + async function verificarPermissoes(): Promise<{ + localizacao: boolean; webcam: boolean; permissoesNecessarias: string[]; }> { @@ -163,7 +176,7 @@ 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()); + stream.getTracks().forEach((track) => track.stop()); } catch (error) { console.warn('Permissão de webcam não concedida:', error); permissoesNecessarias.push('câmera'); @@ -179,7 +192,8 @@ // Verificar se tem funcionário associado if (!temFuncionarioAssociado) { mensagemErroModal = 'Usuário não possui funcionário associado'; - detalhesErroModal = 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; + detalhesErroModal = + 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; mostrarModalErro = true; return; } @@ -213,7 +227,7 @@ etapaProcessamento = 'coletando'; const informacoesDispositivo = await obterInformacoesDispositivo(); // Nota: A permissão de sensor não é impeditiva - apenas câmera e localização são obrigatórias - + coletandoInfo = false; // Obter tempo sincronizado e aplicar GMT offset (igual ao relógio) @@ -221,9 +235,9 @@ const configRelogio = await client.query(api.configuracaoRelogio.obterConfiguracao, {}); // Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido const gmtOffset = configRelogio.gmtOffset ?? 0; - + let timestampBase: number; - + if (configRelogio.usarServidorExterno) { try { const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {}); @@ -244,24 +258,25 @@ // Usar relógio do PC (sem sincronização com servidor) timestampBase = Date.now(); } - + // Aplicar GMT offset ao timestamp // Quando GMT é 0, compensar o timezone local do navegador para que o timestamp // represente o horário local (não UTC), evitando que apareça 3 horas a mais let timestamp: number; if (gmtOffset !== 0) { // Aplicar offset configurado - timestamp = timestampBase + (gmtOffset * 60 * 60 * 1000); + timestamp = timestampBase + gmtOffset * 60 * 60 * 1000; } else { // Quando GMT = 0, ajustar para horário local do navegador // getTimezoneOffset() retorna minutos POSITIVOS para fusos ATRÁS de UTC // Exemplo: Brasil (UTC-3) retorna 180 minutos // Subtrair esses minutos para que o timestamp represente o horário local const timezoneOffset = new Date().getTimezoneOffset(); // Offset em minutos - timestamp = timestampBase - (timezoneOffset * 60 * 1000); // Subtrair minutos em milissegundos + timestamp = timestampBase - timezoneOffset * 60 * 1000; // Subtrair minutos em milissegundos } // Sincronizado apenas se usar servidor externo e sincronização foi bem-sucedida - const sincronizadoComServidor = configRelogio.usarServidorExterno && timestampBase !== Date.now(); + const sincronizadoComServidor = + configRelogio.usarServidorExterno && timestampBase !== Date.now(); // Upload da imagem (obrigatória agora) let imagemId: Id<'_storage'> | undefined = undefined; @@ -293,7 +308,7 @@ nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, + nomeSaida: config.nomeSaida }) : getTipoRegistroLabel(resultado.tipo); sucesso = `Ponto registrado com sucesso! Tipo: ${tipoLabelSucesso}`; @@ -303,14 +318,14 @@ // Forçar atualização das queries para mostrar o novo registro refreshKey++; - + if (import.meta.env.DEV) { console.log('[RegistroPonto] Registro bem-sucedido, refreshKey incrementado:', refreshKey); } - + // Aguardar um pouco para garantir que o backend processou o registro - await new Promise(resolve => setTimeout(resolve, 500)); - + await new Promise((resolve) => setTimeout(resolve, 500)); + // Forçar mais uma atualização após o delay para garantir sincronização refreshKey++; @@ -324,10 +339,10 @@ etapaProcessamento = null; let mensagemErro = 'Erro desconhecido ao registrar ponto'; let detalhesErro = 'Tente novamente em alguns instantes.'; - + if (error instanceof Error) { const erroMessage = error.message || ''; - + // Erro de registro duplicado if ( erroMessage.includes('Já existe um registro neste minuto') || @@ -339,7 +354,7 @@ nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, + nomeSaida: config.nomeSaida }) : getTipoRegistroLabel(proximoTipo); detalhesErro = `Não é possível registrar o ponto no mesmo minuto.\n\nVocê já possui um registro de ${tipoLabelErro} para este minuto.\n\nPor favor, aguarde pelo menos 1 minuto antes de tentar registrar novamente.`; @@ -351,7 +366,8 @@ erroMessage.includes('validation') ) { mensagemErro = 'Erro na validação dos dados'; - detalhesErro = 'Ocorreu um erro ao validar as informações do dispositivo.\n\nPor favor, tente novamente ou recarregue a página.'; + detalhesErro = + 'Ocorreu um erro ao validar as informações do dispositivo.\n\nPor favor, tente novamente ou recarregue a página.'; } // Erro de autenticação else if ( @@ -369,12 +385,14 @@ erroMessage.includes('location') ) { mensagemErro = 'Erro na validação de localização'; - detalhesErro = 'Não foi possível validar sua localização.\n\nPor favor, verifique se você autorizou o compartilhamento de localização e tente novamente.'; + detalhesErro = + 'Não foi possível validar sua localização.\n\nPor favor, verifique se você autorizou o compartilhamento de localização e tente novamente.'; } // Erro genérico do servidor else if (erroMessage.includes('Server Error') || erroMessage.includes('Server')) { mensagemErro = 'Erro no servidor'; - detalhesErro = 'Ocorreu um erro no servidor ao processar seu registro.\n\nPor favor, tente novamente em alguns instantes.'; + detalhesErro = + 'Ocorreu um erro no servidor ao processar seu registro.\n\nPor favor, tente novamente em alguns instantes.'; } // Outros erros - mostrar mensagem simplificada else { @@ -386,13 +404,14 @@ erroMessage.includes('ReferenceError') || erroMessage.length > 200 ) { - detalhesErro = 'Ocorreu um erro ao processar o registro.\n\nPor favor, tente novamente ou recarregue a página.'; + detalhesErro = + 'Ocorreu um erro ao processar o registro.\n\nPor favor, tente novamente ou recarregue a página.'; } else { detalhesErro = erroMessage; } } } - + mensagemErroModal = mensagemErro; detalhesErroModal = detalhesErro; mostrarModalErro = true; @@ -410,59 +429,17 @@ } mostrandoWebcam = false; - // Se capturou a foto, mostrar modal de confirmação - if (blob && capturandoAutomaticamente) { + // Se capturou a foto, iniciar o processo automaticamente + if (blob) { capturandoAutomaticamente = false; - // Obter data e hora sincronizada do servidor com GMT offset (igual ao relógio) - try { - const configRelogio = await client.query(api.configuracaoRelogio.obterConfiguracao, {}); - // Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido - const gmtOffset = configRelogio.gmtOffset ?? 0; - - let timestampBase: number; - - if (configRelogio.usarServidorExterno) { - try { - const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {}); - if (resultado.sucesso && resultado.timestamp) { - timestampBase = resultado.timestamp; - } else { - timestampBase = await obterTempoServidor(client); - } - } catch (error) { - console.warn('Erro ao sincronizar com servidor externo:', error); - if (configRelogio.fallbackParaPC) { - timestampBase = Date.now(); - } else { - timestampBase = await obterTempoServidor(client); - } - } - } else { - // Usar relógio do PC (sem sincronização com servidor) - timestampBase = Date.now(); - } - - // Aplicar GMT offset ao timestamp - // Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática - // Quando GMT ≠ 0, aplicar offset configurado ao timestamp - let timestamp: number; - if (gmtOffset !== 0) { - // Aplicar offset configurado - timestamp = timestampBase + (gmtOffset * 60 * 60 * 1000); - } else { - // Quando GMT = 0, manter timestamp UTC puro - // O toLocaleTimeString() converterá automaticamente para o timezone local do navegador - timestamp = timestampBase; - } - 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; + // Mostrar tela de transição e iniciar o processo automaticamente + mostrandoTransicao = true; + + // Aguardar 1.5s mostrando a mensagem de "Registro em Andamento" e então confirmar + setTimeout(() => { + mostrandoTransicao = false; + confirmarRegistro(); + }, 1500); } } @@ -487,7 +464,11 @@ 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' }); + const hora = agora.toLocaleTimeString('pt-BR', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); dataHoraAtual = { data, hora }; } @@ -497,7 +478,8 @@ // Verificar se tem funcionário associado if (!temFuncionarioAssociado) { mensagemErroModal = 'Usuário não possui funcionário associado'; - detalhesErroModal = 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; + detalhesErroModal = + 'Você não possui um funcionário associado à sua conta. Entre em contato com o administrador do sistema.'; mostrarModalErro = true; return; } @@ -514,7 +496,8 @@ 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.'; + detalhesErroModal = + 'Para registrar o ponto, é necessário autorizar o compartilhamento de localização e a captura de foto.'; mostrarModalErro = true; return; } @@ -526,6 +509,7 @@ function confirmarRegistro() { mostrandoModalConfirmacao = false; + mostrandoTransicao = false; aguardandoProcessamento = true; etapaProcessamento = 'coletando'; // Usar setTimeout para garantir que o modal de processamento apareça antes de iniciar o registro @@ -554,7 +538,7 @@ try { // Buscar dados completos do registro const registro = await client.query(api.pontos.obterRegistro, { registroId }); - + if (!registro) { alert('Registro não encontrado'); return; @@ -631,17 +615,26 @@ nomeEntrada: configComprovante.nomeEntrada, nomeSaidaAlmoco: configComprovante.nomeSaidaAlmoco, nomeRetornoAlmoco: configComprovante.nomeRetornoAlmoco, - nomeSaida: configComprovante.nomeSaida, + nomeSaida: configComprovante.nomeSaida }) : getTipoRegistroLabel(registro.tipo); doc.text(`Tipo: ${tipoLabelComprovante}`, 15, yPosition); yPosition += 6; - const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo); + const dataHora = formatarDataHoraCompleta( + registro.data, + registro.hora, + registro.minuto, + registro.segundo + ); doc.text(`Data e Hora: ${dataHora}`, 15, yPosition); yPosition += 6; - doc.text(`Status: ${registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}`, 15, yPosition); + doc.text( + `Status: ${registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}`, + 15, + yPosition + ); yPosition += 6; doc.text(`Tolerância: ${registro.toleranciaMinutos} minutos`, 15, yPosition); @@ -673,10 +666,10 @@ 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((resolve, reject) => { reader.onloadend = () => { @@ -720,7 +713,7 @@ // 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(); @@ -767,7 +760,13 @@ const temFuncionarioAssociado = $derived(funcionarioId !== null); const podeRegistrar = $derived.by(() => { - return !registrando && !coletandoInfo && config !== undefined && !estaDispensado && temFuncionarioAssociado; + return ( + !registrando && + !coletandoInfo && + config !== undefined && + !estaDispensado && + temFuncionarioAssociado + ); }); // Os modais agora são centralizados automaticamente via CSS (fixed inset-0 flex items-center justify-center) @@ -815,8 +814,16 @@ const horarios = [ { tipo: 'entrada', horario: config.horarioEntrada, label: config.nomeEntrada || 'Entrada 1' }, - { tipo: 'saida_almoco', horario: config.horarioSaidaAlmoco, label: config.nomeSaidaAlmoco || 'Saída 1' }, - { tipo: 'retorno_almoco', horario: config.horarioRetornoAlmoco, label: config.nomeRetornoAlmoco || 'Entrada 2' }, + { + tipo: 'saida_almoco', + horario: config.horarioSaidaAlmoco, + label: config.nomeSaidaAlmoco || 'Saída 1' + }, + { + tipo: 'retorno_almoco', + horario: config.horarioRetornoAlmoco, + label: config.nomeRetornoAlmoco || 'Entrada 2' + }, { tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' } ]; @@ -834,8 +841,8 @@ if (import.meta.env.DEV) { console.log('[RegistroPonto] mapaHorarios atualizado:', { totalRegistrosHoje: registrosHoje.length, - horariosComRegistro: resultado.filter(h => h.registrado).length, - registrosHoje: registrosHoje.map(r => ({ tipo: r.tipo, hora: `${r.hora}:${r.minuto}` })) + horariosComRegistro: resultado.filter((h) => h.registrado).length, + registrosHoje: registrosHoje.map((r) => ({ tipo: r.tipo, hora: `${r.hora}:${r.minuto}` })) }); } @@ -865,6 +872,7 @@ const saldoPositivo = $derived(historicoSaldo ? historicoSaldo.saldoMinutos >= 0 : false); + // Posicionamento dos modais // Posicionamento dos modais let modalPosition = $state<{ top: number; left: number } | null>(null); @@ -872,31 +880,34 @@ function calcularPosicaoModal() { // Procurar pelo elemento do card de registro de ponto const cardRef = document.getElementById('card-registro-ponto-ref'); - + if (cardRef) { const rect = cardRef.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // Posicionar o modal na mesma posição do card de registro - // Centralizado horizontalmente no card - const left = rect.left + (rect.width / 2); - // Posicionar abaixo do card com um pequeno espaçamento - const top = rect.bottom + 20; - + + // Posicionar o modal centralizado horizontalmente na tela + const left = window.innerWidth / 2; + // Posicionar no topo do card + const top = rect.top; + return { top: top, left: left }; } - + // Se não encontrar, usar posição padrão (centro da tela) return null; } // Atualizar posição quando os modais forem abertos ou quando a página rolar $effect(() => { - if (mostrandoWebcam || mostrandoModalConfirmacao || aguardandoProcessamento || mostrarModalErro) { + if ( + mostrandoWebcam || + mostrandoModalConfirmacao || + aguardandoProcessamento || + mostrarModalErro || + mostrandoTransicao + ) { // Usar requestAnimationFrame para garantir que o DOM está completamente renderizado const updatePosition = () => { requestAnimationFrame(() => { @@ -906,18 +917,18 @@ } }); }; - + // Aguardar um pouco mais para garantir que o DOM está atualizado setTimeout(updatePosition, 50); - + // Adicionar listener de scroll para atualizar posição const handleScroll = () => { updatePosition(); }; - + window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleScroll); - + return () => { window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('resize', handleScroll); @@ -928,40 +939,13 @@ } }); - // Função para obter estilo do modal baseado na posição calculada + // Função para obter estilo do modal function getModalStyle() { if (modalPosition) { - // Garantir que o modal não saia da viewport - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - const modalWidth = 800; // Aproximadamente max-w-2xl ou max-w-3xl - const modalHeight = Math.min(viewportHeight * 0.9, 600); - - let left = modalPosition.left; - let top = modalPosition.top; - - // Ajustar se o modal sair da viewport à direita - if (left + (modalWidth / 2) > viewportWidth - 20) { - left = viewportWidth - (modalWidth / 2) - 20; - } - // Ajustar se o modal sair da viewport à esquerda - if (left - (modalWidth / 2) < 20) { - left = (modalWidth / 2) + 20; - } - // Ajustar se o modal sair da viewport abaixo - if (top + modalHeight > viewportHeight - 20) { - top = viewportHeight - modalHeight - 20; - } - // Ajustar se o modal sair da viewport acima - if (top < 20) { - top = 20; - } - - // Usar transform para centralizar horizontalmente baseado no left calculado - return `position: fixed; top: ${top}px; left: ${left}px; transform: translateX(-50%); max-width: ${Math.min(modalWidth, viewportWidth - 40)}px;`; + const top = Math.max(20, modalPosition.top); // Garantir margem mínima + return `position: fixed; top: ${top}px; left: ${modalPosition.left}px; transform: translateX(-50%); width: 100%; max-width: 800px;`; } - // Se não houver posição calculada, centralizar na tela - return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);'; + return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 800px;'; } @@ -1014,39 +998,45 @@
Você está dispensado de registrar ponto no momento.
- Motivo: {motivoDispensa} + Motivo: + {motivoDispensa}
{/if} -
+
-
-
- +
+
+
-

Registrar Ponto

+

Registrar Ponto

-
+