diff --git a/apps/web/src/app.html b/apps/web/src/app.html index bd3affa..b66466c 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -5,6 +5,49 @@ %sveltekit.head% + + +
%sveltekit.body%
diff --git a/apps/web/src/lib/components/call/CallWindow.svelte b/apps/web/src/lib/components/call/CallWindow.svelte index 6734301..8ebe538 100644 --- a/apps/web/src/lib/components/call/CallWindow.svelte +++ b/apps/web/src/lib/components/call/CallWindow.svelte @@ -172,82 +172,17 @@ } // Carregar Jitsi dinamicamente - // Polyfill para BlobBuilder (API antiga que lib-jitsi-meet pode usar) - // Deve ser executado antes de qualquer import da biblioteca - function adicionarBlobBuilderPolyfill(): void { - if (!browser || typeof window === 'undefined') return; - - // Verificar se já foi adicionado (evitar múltiplas execuções) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((window as any).__blobBuilderPolyfillAdded) { - return; - } - - // Implementar BlobBuilder usando Blob moderno - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const BlobBuilderClass = class BlobBuilder { - private parts: BlobPart[] = []; - - append(data: BlobPart): void { - this.parts.push(data); - } - - getBlob(contentType?: string): Blob { - return new Blob(this.parts, contentType ? { type: contentType } : undefined); - } - }; - - // Adicionar em todos os possíveis locais onde a biblioteca pode procurar - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = window as any; - - if (typeof win.BlobBuilder === 'undefined') { - win.BlobBuilder = BlobBuilderClass; - } - - if (typeof win.WebKitBlobBuilder === 'undefined') { - win.WebKitBlobBuilder = BlobBuilderClass; - } - - if (typeof win.MozBlobBuilder === 'undefined') { - win.MozBlobBuilder = BlobBuilderClass; - } - - if (typeof win.MSBlobBuilder === 'undefined') { - win.MSBlobBuilder = BlobBuilderClass; - } - - // Também adicionar no global scope caso a biblioteca procure lá - if (typeof globalThis !== 'undefined') { - if (typeof (globalThis as any).BlobBuilder === 'undefined') { - (globalThis as any).BlobBuilder = BlobBuilderClass; - } - if (typeof (globalThis as any).WebKitBlobBuilder === 'undefined') { - (globalThis as any).WebKitBlobBuilder = BlobBuilderClass; - } - } - - // Marcar que o polyfill foi adicionado - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).__blobBuilderPolyfillAdded = true; - - console.log('✅ Polyfill BlobBuilder adicionado para todos os navegadores'); - } - - // Executar polyfill imediatamente se estiver no browser - // Isso garante que esteja disponível antes de qualquer import - if (browser && typeof window !== 'undefined') { - adicionarBlobBuilderPolyfill(); - } - async function carregarJitsi(): Promise { if (!browser || JitsiMeetJS) return; try { console.log('🔄 Tentando carregar lib-jitsi-meet...'); - // Adicionar polyfill antes de carregar a biblioteca - adicionarBlobBuilderPolyfill(); + // Polyfill BlobBuilder já deve estar disponível via app.html + // Verificar se está disponível antes de carregar a biblioteca + if (typeof (window as any).BlobBuilder === 'undefined') { + console.warn('⚠️ Polyfill BlobBuilder não encontrado, pode causar erros'); + } // Tentar carregar o módulo lib-jitsi-meet dinamicamente // Usar import dinâmico para evitar problemas de SSR e permitir carregamento apenas no browser @@ -1000,9 +935,11 @@ onMount(async () => { if (!browser) return; - // Adicionar polyfill BlobBuilder o mais cedo possível - // Isso deve ser feito antes de qualquer tentativa de carregar lib-jitsi-meet - adicionarBlobBuilderPolyfill(); + // Polyfill BlobBuilder já deve estar disponível via app.html + // Verificar se está disponível + if (typeof (window as any).BlobBuilder === 'undefined') { + console.warn('⚠️ Polyfill BlobBuilder não encontrado no onMount'); + } // Inicializar store primeiro inicializarStore(); diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index ed3bfb4..8a16bc1 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -190,9 +190,51 @@ const informacoesDispositivo = await obterInformacoesDispositivo(); coletandoInfo = false; - // Obter tempo sincronizado - const timestamp = await obterTempoServidor(client); - const sincronizadoComServidor = true; // Sempre true quando usamos obterTempoServidor + // Obter tempo sincronizado e aplicar GMT offset (igual ao relógio) + 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, 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); + } 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 + } + // Sincronizado apenas se usar servidor externo e sincronização foi bem-sucedida + const sincronizadoComServidor = configRelogio.usarServidorExterno && timestampBase !== Date.now(); // Upload da imagem (obrigatória agora) let imagemId: Id<'_storage'> | undefined = undefined; @@ -275,9 +317,47 @@ // Se capturou a foto, mostrar modal de confirmação if (blob && capturandoAutomaticamente) { capturandoAutomaticamente = false; - // Obter data e hora sincronizada do servidor + // Obter data e hora sincronizada do servidor com GMT offset (igual ao relógio) try { - const timestamp = await obterTempoServidor(client); + 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' }); diff --git a/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte b/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte index 1bb6e23..1f07ee7 100644 --- a/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte +++ b/apps/web/src/lib/components/ponto/RelogioSincronizado.svelte @@ -17,6 +17,8 @@ async function atualizarTempo() { try { const config = await client.query(api.configuracaoRelogio.obterConfiguracao, {}); + // Usar gmtOffset da configuração, sem valor padrão, pois 0 é um valor válido + // Se não estiver configurado, usar null e tratar como 0 const gmtOffset = config.gmtOffset ?? 0; let timestampBase: number; @@ -45,16 +47,25 @@ } } } else { - // Usar tempo do servidor Convex - timestampBase = await obterTempoServidor(client); - sincronizado = true; + // Usar relógio do PC (sem sincronização com servidor) + timestampBase = obterTempoPC(); + sincronizado = false; usandoServidorExterno = false; - erro = null; + erro = 'Usando relógio do PC'; } // Aplicar GMT offset ao timestamp - // O timestamp está em UTC, adicionar o offset em horas - const timestampAjustado = timestampBase + (gmtOffset * 60 * 60 * 1000); + // 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 timestampAjustado: number; + if (gmtOffset !== 0) { + // Aplicar offset configurado + timestampAjustado = timestampBase + (gmtOffset * 60 * 60 * 1000); + } else { + // Quando GMT = 0, manter timestamp UTC puro + // O toLocaleTimeString() converterá automaticamente para o timezone local do navegador + timestampAjustado = timestampBase; + } tempoAtual = new Date(timestampAjustado); } catch (error) { console.error('Erro ao obter tempo:', error); @@ -120,7 +131,7 @@ {erro} {:else} - Sincronizando... + Usando relógio do PC {/if} diff --git a/apps/web/src/lib/utils/jitsiPolyfill.ts b/apps/web/src/lib/utils/jitsiPolyfill.ts new file mode 100644 index 0000000..b083317 --- /dev/null +++ b/apps/web/src/lib/utils/jitsiPolyfill.ts @@ -0,0 +1,82 @@ +/** + * Polyfill global para BlobBuilder + * Deve ser executado ANTES de qualquer import de lib-jitsi-meet + * + * BlobBuilder é uma API antiga dos navegadores que foi substituída pelo construtor Blob + * A biblioteca lib-jitsi-meet pode tentar usar BlobBuilder em navegadores modernos + */ + +export function adicionarBlobBuilderPolyfill(): void { + if (typeof window === 'undefined') return; + + // Verificar se já foi adicionado (evitar múltiplas execuções) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((window as any).__blobBuilderPolyfillAdded) { + return; + } + + // Implementar BlobBuilder usando Blob moderno + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const BlobBuilderClass = class BlobBuilder { + private parts: BlobPart[] = []; + + append(data: BlobPart): void { + this.parts.push(data); + } + + getBlob(contentType?: string): Blob { + return new Blob(this.parts, contentType ? { type: contentType } : undefined); + } + }; + + // Adicionar em todos os possíveis locais onde a biblioteca pode procurar + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const win = window as any; + + // Definir BlobBuilder se não existir + if (typeof win.BlobBuilder === 'undefined') { + win.BlobBuilder = BlobBuilderClass; + } + + // Variantes de navegadores antigos + if (typeof win.WebKitBlobBuilder === 'undefined') { + win.WebKitBlobBuilder = BlobBuilderClass; + } + + if (typeof win.MozBlobBuilder === 'undefined') { + win.MozBlobBuilder = BlobBuilderClass; + } + + if (typeof win.MSBlobBuilder === 'undefined') { + win.MSBlobBuilder = BlobBuilderClass; + } + + // Adicionar no global scope + if (typeof globalThis !== 'undefined') { + if (typeof (globalThis as any).BlobBuilder === 'undefined') { + (globalThis as any).BlobBuilder = BlobBuilderClass; + } + if (typeof (globalThis as any).WebKitBlobBuilder === 'undefined') { + (globalThis as any).WebKitBlobBuilder = BlobBuilderClass; + } + if (typeof (globalThis as any).MozBlobBuilder === 'undefined') { + (globalThis as any).MozBlobBuilder = BlobBuilderClass; + } + } + + // Marcar que o polyfill foi adicionado + win.__blobBuilderPolyfillAdded = true; + + console.log('✅ Polyfill BlobBuilder adicionado globalmente'); +} + +// Executar imediatamente se estiver no browser +if (typeof window !== 'undefined') { + adicionarBlobBuilderPolyfill(); +} + + + + + + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte index 04c223e..5a9cbca 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte @@ -28,13 +28,14 @@ // Parâmetros reativos para queries const registrosParams = $derived({ - funcionarioId: funcionarioIdFiltro || undefined, + funcionarioId: funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined, dataInicio, dataFim, }); const estatisticasParams = $derived({ dataInicio, dataFim, + funcionarioId: funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined, }); // Queries @@ -176,6 +177,12 @@ // Inicializar gráfico quando canvas e dados estiverem disponíveis $effect(() => { if (chartCanvas && estatisticas && chartData) { + // Destruir gráfico anterior se existir + if (chartInstance) { + chartInstance.destroy(); + chartInstance = null; + } + // Aguardar um pouco para garantir que o canvas está renderizado const timeoutId = setTimeout(() => { criarGrafico(); @@ -1748,7 +1755,6 @@ {/if} - {#if estatisticas}
@@ -1760,16 +1766,41 @@
+ {#if estatisticasQuery === undefined || estatisticasQuery?.isLoading} +
+
+ + Carregando estatísticas... +
+
+ {:else if estatisticasQuery?.error} +
+
+ +
+

Erro ao carregar estatísticas

+
{estatisticasQuery.error?.message || String(estatisticasQuery.error) || 'Erro desconhecido'}
+
+
+
+ {:else if !estatisticas || !chartData} +
+
+ +

Nenhuma estatística disponível

+
+
+ {:else} - {#if !chartInstance && estatisticas} -
+ {#if !chartInstance && estatisticas && chartData} +
{/if} + {/if}
- {/if}
@@ -1860,7 +1891,7 @@ {/if}
- {#if registrosQuery?.status === 'Loading'} + {#if registrosQuery === undefined || registrosQuery?.isLoading}
Carregando registros... @@ -1871,7 +1902,7 @@

Erro ao carregar registros

-
{registrosQuery.error.message || 'Erro desconhecido'}
+
{registrosQuery.error?.message || String(registrosQuery.error) || 'Erro desconhecido'}
{:else if !registrosQuery?.data} diff --git a/apps/web/src/routes/(dashboard)/ti/configuracoes-jitsi/+page.svelte b/apps/web/src/routes/(dashboard)/ti/configuracoes-jitsi/+page.svelte index 5a483c9..7ed103d 100644 --- a/apps/web/src/routes/(dashboard)/ti/configuracoes-jitsi/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/configuracoes-jitsi/+page.svelte @@ -1,52 +1,90 @@ -
+
-
+
-
+
-

Configurações do Jitsi Meet

+

Configurações do Jitsi Meet

Configurar servidor Jitsi para chamadas de vídeo e áudio

@@ -174,16 +300,16 @@ {#if mensagem}
- {#if mensagem.tipo === "success"} + {#if mensagem.tipo === 'success'} {/if} - {mensagem.texto} +
+ {mensagem.texto} + {#if mensagem.detalhes} +
+ Detalhes +
{mensagem.detalhes}
+
+ {/if} +
{/if} @@ -213,16 +348,12 @@ {#if !isLoading} -
+
{#if configAtual?.data?.ativo} Status: {statusConfig} {#if configAtual?.data?.testadoEm} - - Última conexão testada em {new Date( - configAtual.data.testadoEm - ).toLocaleString("pt-BR")} + - Última conexão testada em {new Date(configAtual.data.testadoEm).toLocaleString('pt-BR')} {/if}
@@ -258,7 +387,7 @@

Dados do Servidor Jitsi

-
+
-

Configurações de Segurança

+

Configurações de Segurança

Ativado automaticamente se domínio contém :8443. Desmarque para usar HTTP (não recomendado para produção)Ativado automaticamente se domínio contém :8443. Desmarque para usar HTTP (não + recomendado para produção)
@@ -347,14 +471,228 @@
Habilitar apenas para desenvolvimento local com certificados autoassinados. Em produção, use certificados válidos.Habilitar apenas para desenvolvimento local com certificados autoassinados. Em + produção, use certificados válidos.
+ +
+
+

Configuração SSH/Docker (Opcional)

+ +
+ + {#if mostrarConfigSSH} +
+ +
+ + +
+ Endereço do servidor Docker +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ Ou use caminho da chave SSH abaixo +
+
+ + +
+ + +
+ Caminho no servidor SSH para a chave privada +
+
+ + +
+ + +
+ Diretório com docker-compose.yml +
+
+ + +
+ + +
+ Diretório de configurações do Jitsi +
+
+
+ + + {#if configuradoNoServidor} +
+ + + + + Configuração aplicada no servidor + {#if configCompleta?.data?.configuradoNoServidorEm} + em {new Date(configCompleta.data.configuradoNoServidorEm).toLocaleString('pt-BR')} + {/if} + +
+ {/if} + + +
+ + + +
+ {/if} + -
+