diff --git a/apps/web/CONFIGURACAO_ENV.md b/apps/web/CONFIGURACAO_ENV.md new file mode 100644 index 0000000..91f3551 --- /dev/null +++ b/apps/web/CONFIGURACAO_ENV.md @@ -0,0 +1,29 @@ +# ⚙️ Configuração de Variáveis de Ambiente + +## 📁 Arquivo .env + +Crie um arquivo `.env` na pasta `apps/web/` com as seguintes variáveis: + +```env +# Google Maps API Key (opcional) +# Obtenha sua chave em: https://console.cloud.google.com/ +# Ative a "Geocoding API" para buscar coordenadas por endereço +# Deixe vazio para usar OpenStreetMap (gratuito, sem necessidade de chave) +VITE_GOOGLE_MAPS_API_KEY= + +# VAPID Public Key para Push Notifications (opcional) +VITE_VAPID_PUBLIC_KEY= +``` + +## 📖 Documentação Completa + +Para instruções detalhadas sobre como obter e configurar a Google Maps API Key, consulte: + +📄 **[GOOGLE_MAPS_SETUP.md](./GOOGLE_MAPS_SETUP.md)** + +## ⚠️ Importante + +- O arquivo `.env` não deve ser commitado no Git (já está no .gitignore) +- Variáveis de ambiente começam com `VITE_` para serem acessíveis no frontend +- Reinicie o servidor de desenvolvimento após alterar o arquivo `.env` + diff --git a/apps/web/GOOGLE_MAPS_SETUP.md b/apps/web/GOOGLE_MAPS_SETUP.md new file mode 100644 index 0000000..7ccfb87 --- /dev/null +++ b/apps/web/GOOGLE_MAPS_SETUP.md @@ -0,0 +1,174 @@ +# 📍 Configuração do Google Maps API para Busca de Coordenadas + +Este guia explica como configurar a API do Google Maps para obter coordenadas GPS de forma automática e precisa no sistema de Endereços de Marcação. + +## 🎯 Por que usar Google Maps? + +- ✅ **Maior Precisão**: Resultados mais exatos para endereços brasileiros +- ✅ **Melhor Cobertura**: Banco de dados mais completo e atualizado +- ✅ **Geocoding Avançado**: Entende melhor endereços incompletos ou parciais + +> **Nota**: O sistema funciona perfeitamente sem a API key do Google Maps, usando OpenStreetMap (gratuito). A configuração do Google Maps é opcional. + +--- + +## 📋 Passo a Passo + +### 1. Criar Projeto no Google Cloud Platform + +1. Acesse [Google Cloud Console](https://console.cloud.google.com/) +2. Clique em **"Criar Projeto"** ou selecione um projeto existente +3. Preencha o nome do projeto (ex: "SGSE-App") +4. Clique em **"Criar"** + +### 2. Ativar a Geocoding API + +1. No menu lateral, vá em **"APIs e Serviços"** > **"Biblioteca"** +2. Procure por **"Geocoding API"** +3. Clique no resultado e depois em **"Ativar"** +4. Aguarde alguns segundos para a ativação + +### 3. Criar Chave de API + +1. Ainda em **"APIs e Serviços"**, vá em **"Credenciais"** +2. Clique em **"Criar Credenciais"** > **"Chave de API"** +3. Copie a chave gerada (você precisará dela depois) + +### 4. Configurar Restrições de Segurança (Recomendado) + +Para proteger sua chave de API: + +1. Clique na chave criada para editá-la +2. Em **"Restrições de API"**: + - Selecione **"Restringir chave"** + - Escolha **"Geocoding API"** +3. Em **"Restrições de aplicativo"**: + - Para desenvolvimento local: escolha **"Referenciadores de sites HTTP"** + - Adicione: `http://localhost:*` e `http://127.0.0.1:*` + - Para produção: adicione o domínio do seu site +4. Clique em **"Salvar"** + +### 5. Configurar no Projeto + +1. No diretório `apps/web/`, copie o arquivo de exemplo: + ```bash + cp .env.example .env + ``` + +2. Abra o arquivo `.env` e adicione sua chave: + ```env + VITE_GOOGLE_MAPS_API_KEY=sua_chave_aqui + ``` + +3. Reinicie o servidor de desenvolvimento: + ```bash + npm run dev + ``` + +### 6. Verificar se está funcionando + +1. Acesse a página de **Endereços de Marcação** (`/ti/configuracoes-ponto/enderecos`) +2. Clique em **"Novo Endereço"** +3. Preencha um endereço e clique em **"Buscar GPS"** +4. Se configurado corretamente, verá a mensagem: *"Coordenadas encontradas via Google Maps!"* + +--- + +## 💰 Custos + +### Google Maps Geocoding API + +- **$5.00 por 1.000 requisições** (primeiros 40.000 são gratuitos por mês) +- **$0.005 por requisição** após os 40.000 gratuitos + +> 💡 Para a maioria dos casos de uso, os 40.000 gratuitos são suficientes! + +### OpenStreetMap (Fallback) + +- **100% Gratuito** e ilimitado +- Sem necessidade de configuração +- Precisão levemente menor, mas ainda muito boa + +--- + +## 🔄 Como funciona o sistema + +O sistema foi projetado para usar uma estratégia de **fallback inteligente**: + +1. **Primeiro**: Tenta buscar via Google Maps (se API key configurada) +2. **Se falhar ou não tiver API key**: Usa automaticamente OpenStreetMap +3. **Feedback**: Informa qual serviço foi usado na mensagem de sucesso + +Isso garante que o sistema sempre funcione, mesmo sem a API key do Google Maps. + +--- + +## 🔒 Segurança + +### ⚠️ Importante + +- **Nunca** commite o arquivo `.env` no Git (já está no .gitignore) +- **Nunca** compartilhe sua chave de API publicamente +- Configure **restrições de API** no Google Cloud Console +- Para produção, use variáveis de ambiente seguras no seu provedor de hospedagem + +### Configuração em Produção + +Para ambientes de produção (Vercel, Netlify, etc.): + +1. Acesse as configurações do projeto no seu provedor +2. Vá em **"Environment Variables"** ou **"Variáveis de Ambiente"** +3. Adicione: `VITE_GOOGLE_MAPS_API_KEY` com o valor da sua chave +4. Faça o deploy novamente + +--- + +## ❓ Solução de Problemas + +### A busca não está usando Google Maps + +- Verifique se a variável `VITE_GOOGLE_MAPS_API_KEY` está no arquivo `.env` +- Reinicie o servidor de desenvolvimento +- Verifique no console do navegador se há erros + +### Erro: "This API project is not authorized to use this API" + +- Verifique se a **Geocoding API** está ativada no projeto +- Aguarde alguns minutos após a ativação (pode levar até 5 minutos) + +### Erro: "API key not valid" + +- Verifique se copiou a chave corretamente +- Verifique se as restrições de API permitem o uso da Geocoding API +- Verifique se as restrições de aplicativo permitem seu domínio/endereço + +### Mensagem: "Coordenadas encontradas via OpenStreetMap" + +- Isso é normal se: + - Não há API key configurada + - A API key não é válida + - O Google Maps falhou na busca +- O sistema continua funcionando normalmente com OpenStreetMap + +--- + +## 📚 Recursos Úteis + +- [Google Cloud Console](https://console.cloud.google.com/) +- [Documentação Geocoding API](https://developers.google.com/maps/documentation/geocoding) +- [Preços Google Maps](https://developers.google.com/maps/billing-and-pricing/pricing) +- [OpenStreetMap Nominatim](https://nominatim.org/) + +--- + +## ✅ Resumo + +1. ✅ Crie projeto no Google Cloud +2. ✅ Ative Geocoding API +3. ✅ Crie chave de API +4. ✅ Configure restrições (recomendado) +5. ✅ Adicione `VITE_GOOGLE_MAPS_API_KEY` no `.env` +6. ✅ Reinicie o servidor + +**Pronto!** O sistema agora usará Google Maps para busca de coordenadas com maior precisão. + diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index 801507b..ed3bfb4 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -34,6 +34,12 @@ funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' ); + // Query para verificar dispensa ativa + const dispensaQuery = useQuery( + api.pontos.verificarDispensaAtiva, + funcionarioId && dataHoje ? { funcionarioId, data: dataHoje } : 'skip' + ); + // Estados let mostrandoWebcam = $state(false); let registrando = $state(false); @@ -150,6 +156,22 @@ async function registrarPonto() { if (registrando) return; + // 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.'; + mostrarModalErro = true; + return; + } + + // Verificar se está dispensado antes de registrar + if (estaDispensado) { + mensagemErroModal = 'Registro dispensado pelo gestor'; + detalhesErroModal = motivoDispensa || 'Você está dispensado de registrar ponto no momento.'; + mostrarModalErro = true; + return; + } + // Verificar permissões antes de registrar const permissoes = await verificarPermissoes(); if (!permissoes.localizacao || !permissoes.webcam) { @@ -296,6 +318,22 @@ async function iniciarRegistroComFoto() { if (registrando || coletandoInfo) return; + // 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.'; + mostrarModalErro = true; + return; + } + + // Verificar se está dispensado antes de abrir webcam + if (estaDispensado) { + mensagemErroModal = 'Registro dispensado pelo gestor'; + detalhesErroModal = motivoDispensa || 'Você está dispensado de registrar ponto no momento.'; + mostrarModalErro = true; + return; + } + // Verificar permissões antes de abrir webcam const permissoes = await verificarPermissoes(); if (!permissoes.localizacao || !permissoes.webcam) { @@ -542,8 +580,13 @@ } } + const dispensaAtiva = $derived(dispensaQuery?.data); + const estaDispensado = $derived(dispensaAtiva?.dispensado ?? false); + const motivoDispensa = $derived(dispensaAtiva?.motivo ?? null); + const temFuncionarioAssociado = $derived(funcionarioId !== null); + const podeRegistrar = $derived.by(() => { - return !registrando && !coletandoInfo && config !== undefined; + return !registrando && !coletandoInfo && config !== undefined && !estaDispensado && temFuncionarioAssociado; }); // Referência para o modal @@ -650,55 +693,67 @@
- {formatarHoraPonto(registro.hora, registro.minuto)} -
- {#if registro.justificativa} -Justificativa:
-{registro.justificativa}
++ {formatarHoraPonto(registro.hora, registro.minuto)} +
+ + + {#if config} + {@const horarioEsperado = registro.tipo === 'entrada' ? config.horarioEntrada : config.horarioRetornoAlmoco} + {@const [horaEsperada, minutoEsperado] = horarioEsperado.split(':').map(Number)} + {@const minutosEsperados = horaEsperada * 60 + minutoEsperado} + {@const minutosRegistrados = registro.hora * 60 + registro.minuto} + {@const diferenca = minutosRegistrados - minutosEsperados} + {@const diferencaAbs = Math.abs(diferenca)} + {@const diferencaTexto = diferencaAbs >= 60 + ? `${Math.floor(diferencaAbs / 60)}h ${diferencaAbs % 60}min` + : `${diferencaAbs}min`} + +Justificativa:
+{registro.justificativa}
+{horarioEsperado.label} (não registrado)
+{horarioEsperado.horario}
++ {formatarHoraPonto(registro.hora, registro.minuto)} +
+ + + {#if config} + {@const horarioEsperado = registro.tipo === 'saida_almoco' ? config.horarioSaidaAlmoco : config.horarioSaida} + {@const [horaEsperada, minutoEsperado] = horarioEsperado.split(':').map(Number)} + {@const minutosEsperados = horaEsperada * 60 + minutoEsperado} + {@const minutosRegistrados = registro.hora * 60 + registro.minuto} + {@const diferenca = minutosRegistrados - minutosEsperados} + {@const diferencaAbs = Math.abs(diferenca)} + {@const diferencaTexto = diferencaAbs >= 60 + ? `${Math.floor(diferencaAbs / 60)}h ${diferencaAbs % 60}min` + : `${diferencaAbs}min`} + +Justificativa:
+{registro.justificativa}
+{horarioEsperado.label} (não registrado)
+{horarioEsperado.horario}
+- Se marcado, o funcionário ficará permanentemente dispensado de registrar ponto -
+