feat: update ESLint and TypeScript configurations across frontend and backend; enhance component structure and improve data handling in various modules

This commit is contained in:
2025-12-02 16:36:02 -03:00
parent f48d28067c
commit d79e6959c3
215 changed files with 29474 additions and 28173 deletions

View File

@@ -7,66 +7,70 @@
* Obtém o User-Agent do navegador
*/
export function getUserAgent(): string {
if (typeof window === 'undefined' || !window.navigator) {
return '';
}
return window.navigator.userAgent || '';
if (typeof window === 'undefined' || !window.navigator) {
return '';
}
return window.navigator.userAgent || '';
}
/**
* Valida se uma string tem formato de IP válido
*/
function isValidIPFormat(ip: string): boolean {
if (!ip || ip.length < 7) return false; // IP mínimo: "1.1.1.1" = 7 chars
// Validar IPv4
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) {
const parts = ip.split('.');
return parts.length === 4 && parts.every(part => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
});
}
// Validar IPv6 básico (formato simplificado)
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$|^::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,6}$|^[0-9a-fA-F]{0,4}::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,5}$/;
if (ipv6Regex.test(ip)) {
return true;
}
return false;
if (!ip || ip.length < 7) return false; // IP mínimo: "1.1.1.1" = 7 chars
// Validar IPv4
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) {
const parts = ip.split('.');
return (
parts.length === 4 &&
parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
})
);
}
// Validar IPv6 básico (formato simplificado)
const ipv6Regex =
/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$|^::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,6}$|^[0-9a-fA-F]{0,4}::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,5}$/;
if (ipv6Regex.test(ip)) {
return true;
}
return false;
}
/**
* Verifica se um IP é local/privado
*/
function isLocalIP(ip: string): boolean {
// IPs locais/privados
return (
ip.startsWith('127.') ||
ip.startsWith('192.168.') ||
ip.startsWith('10.') ||
ip.startsWith('172.16.') ||
ip.startsWith('172.17.') ||
ip.startsWith('172.18.') ||
ip.startsWith('172.19.') ||
ip.startsWith('172.20.') ||
ip.startsWith('172.21.') ||
ip.startsWith('172.22.') ||
ip.startsWith('172.23.') ||
ip.startsWith('172.24.') ||
ip.startsWith('172.25.') ||
ip.startsWith('172.26.') ||
ip.startsWith('172.27.') ||
ip.startsWith('172.28.') ||
ip.startsWith('172.29.') ||
ip.startsWith('172.30.') ||
ip.startsWith('172.31.') ||
ip.startsWith('169.254.') || // Link-local
ip === '::1' ||
ip.startsWith('fe80:') // IPv6 link-local
);
// IPs locais/privados
return (
ip.startsWith('127.') ||
ip.startsWith('192.168.') ||
ip.startsWith('10.') ||
ip.startsWith('172.16.') ||
ip.startsWith('172.17.') ||
ip.startsWith('172.18.') ||
ip.startsWith('172.19.') ||
ip.startsWith('172.20.') ||
ip.startsWith('172.21.') ||
ip.startsWith('172.22.') ||
ip.startsWith('172.23.') ||
ip.startsWith('172.24.') ||
ip.startsWith('172.25.') ||
ip.startsWith('172.26.') ||
ip.startsWith('172.27.') ||
ip.startsWith('172.28.') ||
ip.startsWith('172.29.') ||
ip.startsWith('172.30.') ||
ip.startsWith('172.31.') ||
ip.startsWith('169.254.') || // Link-local
ip === '::1' ||
ip.startsWith('fe80:') // IPv6 link-local
);
}
/**
@@ -76,137 +80,141 @@ function isLocalIP(ip: string): boolean {
* Retorna undefined se não conseguir obter
*/
export async function getLocalIP(): Promise<string | undefined> {
return new Promise((resolve) => {
// Verificar se está em ambiente browser
if (typeof window === 'undefined' || typeof RTCPeerConnection === 'undefined') {
resolve(undefined);
return;
}
return new Promise((resolve) => {
// Verificar se está em ambiente browser
if (typeof window === 'undefined' || typeof RTCPeerConnection === 'undefined') {
resolve(undefined);
return;
}
try {
const pc = new RTCPeerConnection({
iceServers: []
});
try {
const pc = new RTCPeerConnection({
iceServers: []
});
let resolved = false;
let foundIPs: string[] = [];
let publicIP: string | undefined = undefined;
let localIP: string | undefined = undefined;
let resolved = false;
let foundIPs: string[] = [];
let publicIP: string | undefined = undefined;
let localIP: string | undefined = undefined;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
pc.close();
// Priorizar IP público, mas retornar local se não houver
resolve(publicIP || localIP || undefined);
}
}, 5000); // Aumentar timeout para 5 segundos
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
pc.close();
// Priorizar IP público, mas retornar local se não houver
resolve(publicIP || localIP || undefined);
}
}, 5000); // Aumentar timeout para 5 segundos
pc.onicecandidate = (event) => {
if (event.candidate && !resolved) {
const candidate = event.candidate.candidate;
// Regex mais rigorosa para IPv4 - deve ser um IP completo e válido
// Formato: X.X.X.X onde X é 0-255
const ipv4Match = candidate.match(/\b([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\b/);
// Regex para IPv6 - mais específica
const ipv6Match = candidate.match(/\b([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){2,7}|::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){0,6}|[0-9a-fA-F]{1,4}::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){0,5})\b/);
let ip: string | undefined = undefined;
if (ipv4Match && ipv4Match[1]) {
const candidateIP = ipv4Match[1];
// Validar se cada octeto está entre 0-255
const parts = candidateIP.split('.');
if (parts.length === 4 && parts.every(part => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
})) {
ip = candidateIP;
}
} else if (ipv6Match && ipv6Match[1]) {
// Validar formato básico de IPv6
const candidateIP = ipv6Match[1];
if (candidateIP.includes(':') && candidateIP.length >= 3) {
ip = candidateIP;
}
}
// Validar se o IP é válido antes de processar
if (ip && isValidIPFormat(ip) && !foundIPs.includes(ip)) {
foundIPs.push(ip);
// Ignorar localhost
if (ip.startsWith('127.') || ip === '::1') {
return;
}
// Separar IPs públicos e locais
if (isLocalIP(ip)) {
if (!localIP) {
localIP = ip;
}
} else {
// IP público encontrado!
if (!publicIP) {
publicIP = ip;
// Se encontrou IP público, podemos resolver mais cedo
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
resolve(publicIP);
}
}
}
}
} else if (event.candidate === null) {
// No more candidates
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
// Retornar IP público se encontrou, senão local
resolve(publicIP || localIP || undefined);
}
}
};
pc.onicecandidate = (event) => {
if (event.candidate && !resolved) {
const candidate = event.candidate.candidate;
// Criar um data channel para forçar a criação de candidatos
pc.createDataChannel('');
pc.createOffer()
.then((offer) => pc.setLocalDescription(offer))
.catch(() => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
resolve(publicIP || localIP || undefined);
}
});
} catch (error) {
console.warn("Erro ao obter IP via WebRTC:", error);
resolve(undefined);
}
});
// Regex mais rigorosa para IPv4 - deve ser um IP completo e válido
// Formato: X.X.X.X onde X é 0-255
const ipv4Match = candidate.match(/\b([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\b/);
// Regex para IPv6 - mais específica
const ipv6Match = candidate.match(
/\b([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){2,7}|::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){0,6}|[0-9a-fA-F]{1,4}::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){0,5})\b/
);
let ip: string | undefined = undefined;
if (ipv4Match && ipv4Match[1]) {
const candidateIP = ipv4Match[1];
// Validar se cada octeto está entre 0-255
const parts = candidateIP.split('.');
if (
parts.length === 4 &&
parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
})
) {
ip = candidateIP;
}
} else if (ipv6Match && ipv6Match[1]) {
// Validar formato básico de IPv6
const candidateIP = ipv6Match[1];
if (candidateIP.includes(':') && candidateIP.length >= 3) {
ip = candidateIP;
}
}
// Validar se o IP é válido antes de processar
if (ip && isValidIPFormat(ip) && !foundIPs.includes(ip)) {
foundIPs.push(ip);
// Ignorar localhost
if (ip.startsWith('127.') || ip === '::1') {
return;
}
// Separar IPs públicos e locais
if (isLocalIP(ip)) {
if (!localIP) {
localIP = ip;
}
} else {
// IP público encontrado!
if (!publicIP) {
publicIP = ip;
// Se encontrou IP público, podemos resolver mais cedo
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
resolve(publicIP);
}
}
}
}
} else if (event.candidate === null) {
// No more candidates
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
// Retornar IP público se encontrou, senão local
resolve(publicIP || localIP || undefined);
}
}
};
// Criar um data channel para forçar a criação de candidatos
pc.createDataChannel('');
pc.createOffer()
.then((offer) => pc.setLocalDescription(offer))
.catch(() => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
pc.close();
resolve(publicIP || localIP || undefined);
}
});
} catch (error) {
console.warn('Erro ao obter IP via WebRTC:', error);
resolve(undefined);
}
});
}
/**
* Obtém informações completas do navegador
*/
export interface BrowserInfo {
userAgent: string;
ipAddress?: string;
userAgent: string;
ipAddress?: string;
}
export async function getBrowserInfo(): Promise<BrowserInfo> {
const userAgent = getUserAgent();
const ipAddress = await getLocalIP();
return {
userAgent,
ipAddress,
};
}
const userAgent = getUserAgent();
const ipAddress = await getLocalIP();
return {
userAgent,
ipAddress
};
}

View File

@@ -43,11 +43,11 @@ export function abrirCallWindowEmPopup(
options: CallWindowOptions = {}
): Window | null {
const opts = { ...DEFAULT_OPTIONS, ...options };
// Calcular posição se não fornecida
let left = opts.left;
let top = opts.top;
if (left === undefined || top === undefined) {
const posicao = calcularPosicaoCentralizada(opts.width, opts.height);
left = left ?? posicao.left;
@@ -138,7 +138,7 @@ export function verificarSuportePopup(): boolean {
// Tentar abrir um popup de teste
const testPopup = window.open('about:blank', '_blank', 'width=1,height=1');
if (!testPopup) {
return false;
}
@@ -197,4 +197,3 @@ export function notificarJanelaPai(type: string, data?: unknown): void {
window.location.origin
);
}

View File

@@ -1,123 +1,122 @@
import type { Doc } from "@sgse-app/backend/convex/_generated/dataModel";
import type { Doc } from '@sgse-app/backend/convex/_generated/dataModel';
type Ticket = Doc<"tickets">;
type TicketStatus = Ticket["status"];
type TimelineEntry = NonNullable<Ticket["timeline"]>[number];
type Ticket = Doc<'tickets'>;
type TicketStatus = Ticket['status'];
type TimelineEntry = NonNullable<Ticket['timeline']>[number];
const UM_DIA_MS = 24 * 60 * 60 * 1000;
const statusConfig: Record<
TicketStatus,
{
label: string;
badge: string;
description: string;
}
TicketStatus,
{
label: string;
badge: string;
description: string;
}
> = {
aberto: {
label: "Aberto",
badge: "badge badge-info badge-outline",
description: "Chamado recebido e aguardando triagem.",
},
em_andamento: {
label: "Em andamento",
badge: "badge badge-primary",
description: "Equipe de TI trabalhando no chamado.",
},
aguardando_usuario: {
label: "Aguardando usuário",
badge: "badge badge-warning",
description: "Aguardando retorno ou aprovação do solicitante.",
},
resolvido: {
label: "Resolvido",
badge: "badge badge-success badge-outline",
description: "Solução aplicada, aguardando confirmação.",
},
encerrado: {
label: "Encerrado",
badge: "badge badge-success",
description: "Chamado finalizado.",
},
cancelado: {
label: "Cancelado",
badge: "badge badge-neutral",
description: "Chamado cancelado.",
},
aberto: {
label: 'Aberto',
badge: 'badge badge-info badge-outline',
description: 'Chamado recebido e aguardando triagem.'
},
em_andamento: {
label: 'Em andamento',
badge: 'badge badge-primary',
description: 'Equipe de TI trabalhando no chamado.'
},
aguardando_usuario: {
label: 'Aguardando usuário',
badge: 'badge badge-warning',
description: 'Aguardando retorno ou aprovação do solicitante.'
},
resolvido: {
label: 'Resolvido',
badge: 'badge badge-success badge-outline',
description: 'Solução aplicada, aguardando confirmação.'
},
encerrado: {
label: 'Encerrado',
badge: 'badge badge-success',
description: 'Chamado finalizado.'
},
cancelado: {
label: 'Cancelado',
badge: 'badge badge-neutral',
description: 'Chamado cancelado.'
}
};
export function getStatusLabel(status: TicketStatus): string {
return statusConfig[status]?.label ?? status;
return statusConfig[status]?.label ?? status;
}
export function getStatusBadge(status: TicketStatus): string {
return statusConfig[status]?.badge ?? "badge";
return statusConfig[status]?.badge ?? 'badge';
}
export function getStatusDescription(status: TicketStatus): string {
return statusConfig[status]?.description ?? "";
return statusConfig[status]?.description ?? '';
}
export function formatarData(timestamp?: number | null) {
if (!timestamp) return "--";
return new Date(timestamp).toLocaleString("pt-BR", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
});
if (!timestamp) return '--';
return new Date(timestamp).toLocaleString('pt-BR', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
}
export function prazoRestante(timestamp?: number | null) {
if (!timestamp) return null;
const diff = timestamp - Date.now();
const dias = Math.floor(diff / UM_DIA_MS);
const horas = Math.floor((diff % UM_DIA_MS) / (60 * 60 * 1000));
if (!timestamp) return null;
const diff = timestamp - Date.now();
const dias = Math.floor(diff / UM_DIA_MS);
const horas = Math.floor((diff % UM_DIA_MS) / (60 * 60 * 1000));
if (diff < 0) {
return `Vencido há ${Math.abs(dias)}d ${Math.abs(horas)}h`;
}
if (diff < 0) {
return `Vencido há ${Math.abs(dias)}d ${Math.abs(horas)}h`;
}
if (dias === 0 && horas >= 0) {
return `Vence em ${horas}h`;
}
if (dias === 0 && horas >= 0) {
return `Vence em ${horas}h`;
}
return `Vence em ${dias}d ${Math.abs(horas)}h`;
return `Vence em ${dias}d ${Math.abs(horas)}h`;
}
export function corPrazo(timestamp?: number | null) {
if (!timestamp) return "info";
const diff = timestamp - Date.now();
if (diff < 0) return "error";
if (diff <= UM_DIA_MS) return "warning";
return "success";
if (!timestamp) return 'info';
const diff = timestamp - Date.now();
if (diff < 0) return 'error';
if (diff <= UM_DIA_MS) return 'warning';
return 'success';
}
export function timelineStatus(entry: TimelineEntry) {
if (entry.status === "concluido") {
return "success";
}
if (!entry.prazo) {
return "info";
}
const diff = entry.prazo - Date.now();
if (diff < 0) {
return "error";
}
if (diff <= UM_DIA_MS) {
return "warning";
}
return "info";
if (entry.status === 'concluido') {
return 'success';
}
if (!entry.prazo) {
return 'info';
}
const diff = entry.prazo - Date.now();
if (diff < 0) {
return 'error';
}
if (diff <= UM_DIA_MS) {
return 'warning';
}
return 'info';
}
export function formatarTimelineEtapa(etapa: string) {
const mapa: Record<string, string> = {
abertura: "Registro",
resposta_inicial: "Resposta inicial",
conclusao: "Conclusão",
encerramento: "Encerramento",
};
const mapa: Record<string, string> = {
abertura: 'Registro',
resposta_inicial: 'Resposta inicial',
conclusao: 'Conclusão',
encerramento: 'Encerramento'
};
return mapa[etapa] ?? etapa;
return mapa[etapa] ?? etapa;
}

View File

@@ -1,49 +1,72 @@
// Constantes para selects e opções do formulário
export const SEXO_OPTIONS = [
{ value: "masculino", label: "Masculino" },
{ value: "feminino", label: "Feminino" },
{ value: "outro", label: "Outro" },
{ value: 'masculino', label: 'Masculino' },
{ value: 'feminino', label: 'Feminino' },
{ value: 'outro', label: 'Outro' }
];
export const ESTADO_CIVIL_OPTIONS = [
{ value: "solteiro", label: "Solteiro(a)" },
{ value: "casado", label: "Casado(a)" },
{ value: "divorciado", label: "Divorciado(a)" },
{ value: "viuvo", label: "Viúvo(a)" },
{ value: "uniao_estavel", label: "União Estável" },
{ value: 'solteiro', label: 'Solteiro(a)' },
{ value: 'casado', label: 'Casado(a)' },
{ value: 'divorciado', label: 'Divorciado(a)' },
{ value: 'viuvo', label: 'Viúvo(a)' },
{ value: 'uniao_estavel', label: 'União Estável' }
];
export const GRAU_INSTRUCAO_OPTIONS = [
{ value: "fundamental", label: "Ensino Fundamental" },
{ value: "medio", label: "Ensino Médio" },
{ value: "superior", label: "Ensino Superior" },
{ value: "pos_graduacao", label: "Pós-Graduação" },
{ value: "mestrado", label: "Mestrado" },
{ value: "doutorado", label: "Doutorado" },
{ value: 'fundamental', label: 'Ensino Fundamental' },
{ value: 'medio', label: 'Ensino Médio' },
{ value: 'superior', label: 'Ensino Superior' },
{ value: 'pos_graduacao', label: 'Pós-Graduação' },
{ value: 'mestrado', label: 'Mestrado' },
{ value: 'doutorado', label: 'Doutorado' }
];
export const GRUPO_SANGUINEO_OPTIONS = [
{ value: "A", label: "A" },
{ value: "B", label: "B" },
{ value: "AB", label: "AB" },
{ value: "O", label: "O" },
{ value: 'A', label: 'A' },
{ value: 'B', label: 'B' },
{ value: 'AB', label: 'AB' },
{ value: 'O', label: 'O' }
];
export const FATOR_RH_OPTIONS = [
{ value: "positivo", label: "Positivo (+)" },
{ value: "negativo", label: "Negativo (-)" },
{ value: 'positivo', label: 'Positivo (+)' },
{ value: 'negativo', label: 'Negativo (-)' }
];
export const APOSENTADO_OPTIONS = [
{ value: "nao", label: "Não" },
{ value: "funape_ipsep", label: "FUNAPE/IPSEP" },
{ value: "inss", label: "INSS" },
{ value: 'nao', label: 'Não' },
{ value: 'funape_ipsep', label: 'FUNAPE/IPSEP' },
{ value: 'inss', label: 'INSS' }
];
export const UFS_BRASIL = [
"AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA",
"MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN",
"RS", "RO", "RR", "SC", "SP", "SE", "TO"
'AC',
'AL',
'AP',
'AM',
'BA',
'CE',
'DF',
'ES',
'GO',
'MA',
'MT',
'MS',
'MG',
'PA',
'PB',
'PR',
'PE',
'PI',
'RJ',
'RN',
'RS',
'RO',
'RR',
'SC',
'SP',
'SE',
'TO'
];

File diff suppressed because it is too large Load Diff

View File

@@ -123,7 +123,7 @@ function detectarSistemaOperacional(): {
sistemaOperacional: 'Desconhecido',
osVersion: '',
arquitetura: '',
plataforma: '',
plataforma: ''
};
}
@@ -144,7 +144,7 @@ function detectarSistemaOperacional(): {
'10.0': '10/11',
'6.3': '8.1',
'6.2': '8',
'6.1': '7',
'6.1': '7'
};
osVersion = versions[version] || version;
}
@@ -191,7 +191,7 @@ function detectarTipoDispositivo(): {
deviceType: 'Desconhecido',
isMobile: false,
isTablet: false,
isDesktop: true,
isDesktop: true
};
}
@@ -232,7 +232,8 @@ async function obterInformacoesConexao(): Promise<string> {
return 'Desconhecido';
}
const connection = (navigator as unknown as { connection?: { effectiveType?: string } }).connection;
const connection = (navigator as unknown as { connection?: { effectiveType?: string } })
.connection;
if (connection?.effectiveType) {
return connection.effectiveType;
}
@@ -260,12 +261,7 @@ function obterInformacoesMemoria(): string {
* Calcula distância entre duas coordenadas (fórmula de Haversine)
* Retorna distância em metros
*/
function calcularDistancia(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000; // Raio da Terra em metros
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
@@ -287,7 +283,7 @@ function obterTimezonePorCoordenadas(latitude: number, longitude: number): strin
if (longitude >= -45 && longitude <= -30 && latitude >= -10 && latitude <= 5) {
return 'America/Recife'; // UTC-3
}
// Fallback: usar timezone do sistema
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -358,7 +354,7 @@ async function capturarLocalizacaoUnica(
// Calcular confiabilidade: cada sinal adiciona pontos
let pontos = 0;
const maxPontos = 7;
if (sinaisGPSReal.temAltitude) pontos += 1;
if (sinaisGPSReal.temAltitudeAccuracy) pontos += 1;
if (sinaisGPSReal.temHeading) pontos += 0.5;
@@ -412,7 +408,11 @@ async function obterLocalizacaoMultipla(): Promise<{
motivoSuspeita?: string;
}> {
if (typeof navigator === 'undefined' || !navigator.geolocation) {
return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Geolocalização não suportada' };
return {
confiabilidade: 0,
suspeitaSpoofing: true,
motivoSuspeita: 'Geolocalização não suportada'
};
}
// Capturar 3 leituras com intervalo de 2 segundos entre elas
@@ -426,7 +426,7 @@ async function obterLocalizacaoMultipla(): Promise<{
for (let i = 0; i < 3; i++) {
const leitura = await capturarLocalizacaoUnica(true, 8000);
if (leitura.latitude && leitura.longitude && leitura.confiabilidade > 0) {
leituras.push({
lat: leitura.latitude,
@@ -436,7 +436,7 @@ async function obterLocalizacaoMultipla(): Promise<{
confiabilidade: leitura.confiabilidade
});
}
// Aguardar 2 segundos entre leituras (exceto na última)
if (i < 2) {
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -444,7 +444,11 @@ async function obterLocalizacaoMultipla(): Promise<{
}
if (leituras.length === 0) {
return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Não foi possível obter localização' };
return {
confiabilidade: 0,
suspeitaSpoofing: true,
motivoSuspeita: 'Não foi possível obter localização'
};
}
// Se tivermos menos de 2 leituras, usar única leitura com baixa confiança
@@ -546,7 +550,7 @@ export async function obterLocalizacaoRapida(): Promise<{
try {
// Uma única leitura rápida com timeout curto
const leitura = await capturarLocalizacaoUnica(true, 3000); // 3 segundos máximo
if (!leitura.latitude || !leitura.longitude || leitura.confiabilidade === 0) {
return {};
}
@@ -566,12 +570,12 @@ export async function obterLocalizacaoRapida(): Promise<{
}
}
);
const geocodeTimeout = new Promise<Response>((_, reject) =>
const geocodeTimeout = new Promise<Response>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 2000)
);
const response = await Promise.race([geocodePromise, geocodeTimeout]);
if (response.ok) {
const data = (await response.json()) as {
address?: {
@@ -649,7 +653,18 @@ export async function obterLocalizacao(): Promise<{
};
}
const { latitude, longitude, precisao, altitude, altitudeAccuracy, heading, speed, confiabilidade, suspeitaSpoofing, motivoSuspeita } = localizacaoMultipla;
const {
latitude,
longitude,
precisao,
altitude,
altitudeAccuracy,
heading,
speed,
confiabilidade,
suspeitaSpoofing,
motivoSuspeita
} = localizacaoMultipla;
// Tentar obter endereço via reverse geocoding
let endereco = '';
@@ -695,9 +710,13 @@ export async function obterLocalizacao(): Promise<{
if (typeof navigator !== 'undefined') {
const timezoneAtual = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezoneEsperado = obterTimezonePorCoordenadas(latitude, longitude);
// Se timezone é muito diferente, pode ser suspeito
if (timezoneAtual !== timezoneEsperado && timezoneAtual !== 'America/Recife' && timezoneEsperado !== 'America/Recife') {
if (
timezoneAtual !== timezoneEsperado &&
timezoneAtual !== 'America/Recife' &&
timezoneEsperado !== 'America/Recife'
) {
console.warn(`Timezone inconsistente: esperado ${timezoneEsperado}, atual ${timezoneAtual}`);
}
}
@@ -748,13 +767,19 @@ export async function obterIPPublico(): Promise<string | undefined> {
* Solicita permissão para acesso aos sensores de movimento (iOS 13+)
*/
async function solicitarPermissaoSensor(): Promise<PermissionState> {
if (typeof DeviceMotionEvent === 'undefined' || typeof (DeviceMotionEvent as { requestPermission?: () => Promise<PermissionState> }).requestPermission !== 'function') {
if (
typeof DeviceMotionEvent === 'undefined' ||
typeof (DeviceMotionEvent as { requestPermission?: () => Promise<PermissionState> })
.requestPermission !== 'function'
) {
// Permissão não necessária ou já concedida (navegadores modernos)
return 'granted';
}
try {
const requestPermission = (DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }).requestPermission;
const requestPermission = (
DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }
).requestPermission;
const resultado = await requestPermission();
return resultado;
} catch (error) {
@@ -783,7 +808,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
// Solicitar permissão (especialmente necessário no iOS 13+)
const permissao = await solicitarPermissaoSensor();
if (permissao === 'denied') {
return {
sensorDisponivel: true,
@@ -793,36 +818,44 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
return new Promise((resolve) => {
const leiturasAcelerometro: Array<{ x: number; y: number; z: number; timestamp: number }> = [];
const leiturasGiroscopio: Array<{ alpha: number; beta: number; gamma: number; timestamp: number }> = [];
const leiturasGiroscopio: Array<{
alpha: number;
beta: number;
gamma: number;
timestamp: number;
}> = [];
const timeoutId = setTimeout(() => {
window.removeEventListener('devicemotion', handleDeviceMotion);
window.removeEventListener('deviceorientation', handleDeviceOrientation);
// Processar dados de acelerômetro
let acelerometro: DadosAcelerometro | undefined;
if (leiturasAcelerometro.length > 0) {
const ultimaLeitura = leiturasAcelerometro[leiturasAcelerometro.length - 1]!;
// Calcular magnitude média
const magnitudes = leiturasAcelerometro.map(l =>
const magnitudes = leiturasAcelerometro.map((l) =>
Math.sqrt(l.x * l.x + l.y * l.y + l.z * l.z)
);
const magnitude = magnitudes.reduce((sum, m) => sum + m, 0) / magnitudes.length;
// Calcular variância para detectar movimento
const mediaX = leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
const mediaY = leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
const mediaZ = leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
const variacoes = leiturasAcelerometro.map(l =>
Math.pow(l.x - mediaX, 2) + Math.pow(l.y - mediaY, 2) + Math.pow(l.z - mediaZ, 2)
const mediaX =
leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
const mediaY =
leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
const mediaZ =
leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
const variacoes = leiturasAcelerometro.map(
(l) => Math.pow(l.x - mediaX, 2) + Math.pow(l.y - mediaY, 2) + Math.pow(l.z - mediaZ, 2)
);
const variacao = variacoes.reduce((sum, v) => sum + v, 0) / variacoes.length;
// Detectar movimento: se variância > 0.01, há movimento
const movimentoDetectado = variacao > 0.01;
acelerometro = {
x: ultimaLeitura.x,
y: ultimaLeitura.y,
@@ -833,7 +866,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
timestamp: ultimaLeitura.timestamp
};
}
// Processar dados de giroscópio
let giroscopio: DadosGiroscopio | undefined;
if (leiturasGiroscopio.length > 0) {
@@ -844,7 +877,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
gamma: ultimaLeitura.gamma || 0
};
}
resolve({
acelerometro,
giroscopio,
@@ -852,7 +885,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
permissaoNegada: false
});
}, duracaoMs);
function handleDeviceMotion(event: DeviceMotionEvent) {
if (event.accelerationIncludingGravity) {
const acc = event.accelerationIncludingGravity;
@@ -866,7 +899,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
}
}
}
function handleDeviceOrientation(event: DeviceOrientationEvent) {
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
leiturasGiroscopio.push({
@@ -877,7 +910,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
});
}
}
window.addEventListener('devicemotion', handleDeviceMotion);
window.addEventListener('deviceorientation', handleDeviceOrientation);
});
@@ -922,14 +955,15 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
informacoes.coresTela = tela.coresTela;
// Informações de conexão, memória e localização (assíncronas)
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] = await Promise.all([
obterInformacoesConexao(),
Promise.resolve(obterInformacoesMemoria()),
obterIPPublico(),
getLocalIP(),
obterLocalizacao(),
obterDadosAcelerometro(5000), // Coletar dados por 5 segundos
]);
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] =
await Promise.all([
obterInformacoesConexao(),
Promise.resolve(obterInformacoesMemoria()),
obterIPPublico(),
getLocalIP(),
obterLocalizacao(),
obterDadosAcelerometro(5000) // Coletar dados por 5 segundos
]);
informacoes.connectionType = connectionType;
informacoes.memoryInfo = memoryInfo;
@@ -961,4 +995,3 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
return informacoes;
}

View File

@@ -1,187 +1,188 @@
// Definições dos documentos com URLs de referência
export interface DocumentoDefinicao {
campo: string;
nome: string;
helpUrl?: string;
categoria: string;
campo: string;
nome: string;
helpUrl?: string;
categoria: string;
}
export const documentos: DocumentoDefinicao[] = [
// Antecedentes Criminais
{
campo: "certidaoAntecedentesPF",
nome: "Certidão de Antecedentes Criminais - Polícia Federal",
helpUrl: "https://servicos.pf.gov.br/epol-sinic-publico/",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesJFPE",
nome: "Certidão de Antecedentes Criminais - Justiça Federal de Pernambuco",
helpUrl: "https://certidoes.trf5.jus.br/certidoes2022/paginas/certidaocriminal.faces",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesSDS",
nome: "Certidão de Antecedentes Criminais - SDS-PE",
helpUrl: "http://www.servicos.sds.pe.gov.br/antecedentes/public/pages/certidaoAntecedentesCriminais/certidaoAntecedentesCriminaisEmitir.jsf",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesTJPE",
nome: "Certidão de Antecedentes Criminais - TJPE",
helpUrl: "https://certidoesunificadas.app.tjpe.jus.br/certidao-criminal-pf",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoImprobidade",
nome: "Certidão Improbidade Administrativa",
helpUrl: "https://www.cnj.jus.br/improbidade_adm/consultar_requerido.php",
categoria: "Antecedentes Criminais",
},
// Documentos Pessoais
{
campo: "rgFrente",
nome: "Carteira de Identidade SDS/PE ou (SSP-PE) - Frente",
categoria: "Documentos Pessoais",
},
{
campo: "rgVerso",
nome: "Carteira de Identidade SDS/PE ou (SSP-PE) - Verso",
categoria: "Documentos Pessoais",
},
{
campo: "cpfFrente",
nome: "CPF/CIC - Frente",
categoria: "Documentos Pessoais",
},
{
campo: "cpfVerso",
nome: "CPF/CIC - Verso",
categoria: "Documentos Pessoais",
},
{
campo: "situacaoCadastralCPF",
nome: "Situação Cadastral CPF",
helpUrl: "https://servicos.receita.fazenda.gov.br/servicos/cpf/consultasituacao/consultapublica.asp",
categoria: "Documentos Pessoais",
},
{
campo: "certidaoRegistroCivil",
nome: "Certidão de Registro Civil (Nascimento, Casamento ou União Estável)",
categoria: "Documentos Pessoais",
},
// Documentos Eleitorais
{
campo: "tituloEleitorFrente",
nome: "Título de Eleitor - Frente",
categoria: "Documentos Eleitorais",
},
{
campo: "tituloEleitorVerso",
nome: "Título de Eleitor - Verso",
categoria: "Documentos Eleitorais",
},
{
campo: "comprovanteVotacao",
nome: "Comprovante de Votação Última Eleição ou Certidão de Quitação Eleitoral",
helpUrl: "https://www.tse.jus.br",
categoria: "Documentos Eleitorais",
},
// Documentos Profissionais
{
campo: "carteiraProfissionalFrente",
nome: "Carteira Profissional - Frente (página da foto)",
categoria: "Documentos Profissionais",
},
{
campo: "carteiraProfissionalVerso",
nome: "Carteira Profissional - Verso (página da foto)",
categoria: "Documentos Profissionais",
},
{
campo: "comprovantePIS",
nome: "Comprovante de PIS/PASEP",
categoria: "Documentos Profissionais",
},
{
campo: "reservistaDoc",
nome: "Reservista (obrigatória para homem até 45 anos)",
categoria: "Documentos Profissionais",
},
// Certidões e Comprovantes
{
campo: "certidaoNascimentoDependentes",
nome: "Certidão de Nascimento do(s) Dependente(s) para Imposto de Renda",
categoria: "Certidões e Comprovantes",
},
{
campo: "cpfDependentes",
nome: "CPF do(s) Dependente(s) para Imposto de Renda",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteEscolaridade",
nome: "Documento de Comprovação do Nível de Escolaridade",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteResidencia",
nome: "Comprovante de Residência",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteContaBradesco",
nome: "Comprovante de Conta-Corrente no Banco BRADESCO",
categoria: "Certidões e Comprovantes",
},
// Declarações
{
campo: "declaracaoAcumulacaoCargo",
nome: "Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos",
categoria: "Declarações",
},
{
campo: "declaracaoDependentesIR",
nome: "Declaração de Dependentes para Fins de Imposto de Renda",
categoria: "Declarações",
},
{
campo: "declaracaoIdoneidade",
nome: "Declaração de Idoneidade",
categoria: "Declarações",
},
{
campo: "termoNepotismo",
nome: "Termo de Declaração de Nepotismo",
categoria: "Declarações",
},
{
campo: "termoOpcaoRemuneracao",
nome: "Termo de Opção - Remuneração",
categoria: "Declarações",
},
// Antecedentes Criminais
{
campo: 'certidaoAntecedentesPF',
nome: 'Certidão de Antecedentes Criminais - Polícia Federal',
helpUrl: 'https://servicos.pf.gov.br/epol-sinic-publico/',
categoria: 'Antecedentes Criminais'
},
{
campo: 'certidaoAntecedentesJFPE',
nome: 'Certidão de Antecedentes Criminais - Justiça Federal de Pernambuco',
helpUrl: 'https://certidoes.trf5.jus.br/certidoes2022/paginas/certidaocriminal.faces',
categoria: 'Antecedentes Criminais'
},
{
campo: 'certidaoAntecedentesSDS',
nome: 'Certidão de Antecedentes Criminais - SDS-PE',
helpUrl:
'http://www.servicos.sds.pe.gov.br/antecedentes/public/pages/certidaoAntecedentesCriminais/certidaoAntecedentesCriminaisEmitir.jsf',
categoria: 'Antecedentes Criminais'
},
{
campo: 'certidaoAntecedentesTJPE',
nome: 'Certidão de Antecedentes Criminais - TJPE',
helpUrl: 'https://certidoesunificadas.app.tjpe.jus.br/certidao-criminal-pf',
categoria: 'Antecedentes Criminais'
},
{
campo: 'certidaoImprobidade',
nome: 'Certidão Improbidade Administrativa',
helpUrl: 'https://www.cnj.jus.br/improbidade_adm/consultar_requerido.php',
categoria: 'Antecedentes Criminais'
},
// Documentos Pessoais
{
campo: 'rgFrente',
nome: 'Carteira de Identidade SDS/PE ou (SSP-PE) - Frente',
categoria: 'Documentos Pessoais'
},
{
campo: 'rgVerso',
nome: 'Carteira de Identidade SDS/PE ou (SSP-PE) - Verso',
categoria: 'Documentos Pessoais'
},
{
campo: 'cpfFrente',
nome: 'CPF/CIC - Frente',
categoria: 'Documentos Pessoais'
},
{
campo: 'cpfVerso',
nome: 'CPF/CIC - Verso',
categoria: 'Documentos Pessoais'
},
{
campo: 'situacaoCadastralCPF',
nome: 'Situação Cadastral CPF',
helpUrl:
'https://servicos.receita.fazenda.gov.br/servicos/cpf/consultasituacao/consultapublica.asp',
categoria: 'Documentos Pessoais'
},
{
campo: 'certidaoRegistroCivil',
nome: 'Certidão de Registro Civil (Nascimento, Casamento ou União Estável)',
categoria: 'Documentos Pessoais'
},
// Documentos Eleitorais
{
campo: 'tituloEleitorFrente',
nome: 'Título de Eleitor - Frente',
categoria: 'Documentos Eleitorais'
},
{
campo: 'tituloEleitorVerso',
nome: 'Título de Eleitor - Verso',
categoria: 'Documentos Eleitorais'
},
{
campo: 'comprovanteVotacao',
nome: 'Comprovante de Votação Última Eleição ou Certidão de Quitação Eleitoral',
helpUrl: 'https://www.tse.jus.br',
categoria: 'Documentos Eleitorais'
},
// Documentos Profissionais
{
campo: 'carteiraProfissionalFrente',
nome: 'Carteira Profissional - Frente (página da foto)',
categoria: 'Documentos Profissionais'
},
{
campo: 'carteiraProfissionalVerso',
nome: 'Carteira Profissional - Verso (página da foto)',
categoria: 'Documentos Profissionais'
},
{
campo: 'comprovantePIS',
nome: 'Comprovante de PIS/PASEP',
categoria: 'Documentos Profissionais'
},
{
campo: 'reservistaDoc',
nome: 'Reservista (obrigatória para homem até 45 anos)',
categoria: 'Documentos Profissionais'
},
// Certidões e Comprovantes
{
campo: 'certidaoNascimentoDependentes',
nome: 'Certidão de Nascimento do(s) Dependente(s) para Imposto de Renda',
categoria: 'Certidões e Comprovantes'
},
{
campo: 'cpfDependentes',
nome: 'CPF do(s) Dependente(s) para Imposto de Renda',
categoria: 'Certidões e Comprovantes'
},
{
campo: 'comprovanteEscolaridade',
nome: 'Documento de Comprovação do Nível de Escolaridade',
categoria: 'Certidões e Comprovantes'
},
{
campo: 'comprovanteResidencia',
nome: 'Comprovante de Residência',
categoria: 'Certidões e Comprovantes'
},
{
campo: 'comprovanteContaBradesco',
nome: 'Comprovante de Conta-Corrente no Banco BRADESCO',
categoria: 'Certidões e Comprovantes'
},
// Declarações
{
campo: 'declaracaoAcumulacaoCargo',
nome: 'Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos',
categoria: 'Declarações'
},
{
campo: 'declaracaoDependentesIR',
nome: 'Declaração de Dependentes para Fins de Imposto de Renda',
categoria: 'Declarações'
},
{
campo: 'declaracaoIdoneidade',
nome: 'Declaração de Idoneidade',
categoria: 'Declarações'
},
{
campo: 'termoNepotismo',
nome: 'Termo de Declaração de Nepotismo',
categoria: 'Declarações'
},
{
campo: 'termoOpcaoRemuneracao',
nome: 'Termo de Opção - Remuneração',
categoria: 'Declarações'
}
];
export const categoriasDocumentos = [
"Antecedentes Criminais",
"Documentos Pessoais",
"Documentos Eleitorais",
"Documentos Profissionais",
"Certidões e Comprovantes",
"Declarações",
'Antecedentes Criminais',
'Documentos Pessoais',
'Documentos Eleitorais',
'Documentos Profissionais',
'Certidões e Comprovantes',
'Declarações'
];
export function getDocumentosByCategoria(categoria: string): DocumentoDefinicao[] {
return documentos.filter(doc => doc.categoria === categoria);
return documentos.filter((doc) => doc.categoria === categoria);
}
export function getDocumentoDefinicao(campo: string): DocumentoDefinicao | undefined {
return documentos.find(doc => doc.campo === campo);
return documentos.find((doc) => doc.campo === campo);
}

View File

@@ -30,8 +30,10 @@ export function traduzirErro(error: unknown): MensagemErro {
if (mensagemErro.includes('could not find public function')) {
return {
titulo: 'Servidor em atualização',
mensagem: 'O sistema está sendo atualizado no momento. Isso geralmente leva apenas alguns segundos.',
instrucoes: 'Por favor, aguarde de 10 a 30 segundos e tente iniciar a chamada novamente. Se o problema persistir, recarregue a página (F5).',
mensagem:
'O sistema está sendo atualizado no momento. Isso geralmente leva apenas alguns segundos.',
instrucoes:
'Por favor, aguarde de 10 a 30 segundos e tente iniciar a chamada novamente. Se o problema persistir, recarregue a página (F5).',
mostrarDetalhesTecnicos: false
};
}
@@ -55,7 +57,8 @@ export function traduzirErro(error: unknown): MensagemErro {
return {
titulo: 'Acesso negado',
mensagem: 'Você não tem permissão para realizar esta ação.',
instrucoes: 'Verifique se você faz parte desta conversa ou se possui as permissões necessárias.',
instrucoes:
'Verifique se você faz parte desta conversa ou se possui as permissões necessárias.',
mostrarDetalhesTecnicos: false
};
}
@@ -84,7 +87,8 @@ export function traduzirErro(error: unknown): MensagemErro {
return {
titulo: 'Problema de conexão',
mensagem: 'Não foi possível conectar com o servidor. Verifique sua conexão com a internet.',
instrucoes: 'Verifique se você está conectado à internet e tente novamente. Se o problema persistir, recarregue a página (F5).',
instrucoes:
'Verifique se você está conectado à internet e tente novamente. Se o problema persistir, recarregue a página (F5).',
mostrarDetalhesTecnicos: false
};
}
@@ -94,7 +98,8 @@ export function traduzirErro(error: unknown): MensagemErro {
return {
titulo: 'Recurso não encontrado',
mensagem: 'O item que você está tentando acessar não foi encontrado.',
instrucoes: 'Verifique se o item ainda existe ou se foi removido. Recarregue a página (F5) para atualizar a lista.',
instrucoes:
'Verifique se o item ainda existe ou se foi removido. Recarregue a página (F5) para atualizar a lista.',
mostrarDetalhesTecnicos: false
};
}
@@ -111,7 +116,8 @@ export function traduzirErro(error: unknown): MensagemErro {
return {
titulo: 'Erro ao processar ação',
mensagem: mensagemLimpa,
instrucoes: 'Por favor, tente novamente. Se o problema persistir, recarregue a página (F5) ou entre em contato com o suporte.',
instrucoes:
'Por favor, tente novamente. Se o problema persistir, recarregue a página (F5) ou entre em contato com o suporte.',
mostrarDetalhesTecnicos: false
};
}
@@ -121,9 +127,9 @@ export function traduzirErro(error: unknown): MensagemErro {
return {
titulo: 'Erro ao processar ação',
mensagem: 'Ocorreu um erro ao processar sua solicitação. Por favor, tente novamente.',
instrucoes: 'Se o problema persistir:\n1. Recarregue a página (pressione F5)\n2. Aguarde alguns instantes e tente novamente\n3. Entre em contato com o suporte técnico se o erro continuar',
instrucoes:
'Se o problema persistir:\n1. Recarregue a página (pressione F5)\n2. Aguarde alguns instantes e tente novamente\n3. Entre em contato com o suporte técnico se o erro continuar',
mostrarDetalhesTecnicos: true,
detalhesTecnicos: erroCompleto
};
}

View File

@@ -38,10 +38,7 @@ const DEFAULT_LIMITS: LimitesJanela = getDefaultLimits();
/**
* Salvar posição da janela no localStorage
*/
export function salvarPosicaoJanela(
id: string,
posicao: PosicaoJanela
): void {
export function salvarPosicaoJanela(id: string, posicao: PosicaoJanela): void {
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
return;
}
@@ -64,9 +61,9 @@ export function restaurarPosicaoJanela(id: string): PosicaoJanela | null {
const key = `floating-window-${id}`;
const saved = localStorage.getItem(key);
if (!saved) return null;
const posicao = JSON.parse(saved) as PosicaoJanela;
// Validar se a posição ainda é válida (dentro da tela)
if (
posicao.x >= 0 &&
@@ -78,7 +75,7 @@ export function restaurarPosicaoJanela(id: string): PosicaoJanela | null {
) {
return posicao;
}
return null;
} catch (error) {
console.warn('Erro ao restaurar posição da janela:', error);
@@ -89,10 +86,7 @@ export function restaurarPosicaoJanela(id: string): PosicaoJanela | null {
/**
* Obter posição inicial da janela (centralizada)
*/
export function obterPosicaoInicial(
width: number = 800,
height: number = 600
): PosicaoJanela {
export function obterPosicaoInicial(width: number = 800, height: number = 600): PosicaoJanela {
if (typeof window === 'undefined') {
return {
x: 100,
@@ -125,18 +119,18 @@ export function criarDragHandler(
function handleMouseDown(e: MouseEvent): void {
if (e.button !== 0) return; // Apenas botão esquerdo
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
}
@@ -174,19 +168,19 @@ export function criarDragHandler(
// Suporte para touch (mobile)
function handleTouchStart(e: TouchEvent): void {
if (e.touches.length !== 1) return;
isDragging = true;
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
const rect = element.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
e.preventDefault();
}
@@ -257,12 +251,12 @@ export function criarResizeHandler(
function handleMouseDown(e: MouseEvent, handle: HTMLElement): void {
if (e.button !== 0) return;
isResizing = true;
currentHandle = handle;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
@@ -271,7 +265,7 @@ export function criarResizeHandler(
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
e.stopPropagation();
}
@@ -289,29 +283,29 @@ export function criarResizeHandler(
// Determinar direção do resize baseado na classe do handle
const classes = currentHandle.className;
// Right
if (classes.includes('resize-right') || classes.includes('resize-e')) {
newWidth = startWidth + deltaX;
}
// Bottom
if (classes.includes('resize-bottom') || classes.includes('resize-s')) {
newHeight = startHeight + deltaY;
}
// Left
if (classes.includes('resize-left') || classes.includes('resize-w')) {
newWidth = startWidth - deltaX;
newLeft = startLeft + deltaX;
}
// Top
if (classes.includes('resize-top') || classes.includes('resize-n')) {
newHeight = startHeight - deltaY;
newTop = startTop + deltaY;
}
// Corner handles
if (classes.includes('resize-se')) {
newWidth = startWidth + deltaX;
@@ -335,7 +329,7 @@ export function criarResizeHandler(
}
if (typeof window === 'undefined') return;
// Aplicar limites
const maxWidth = limites.maxWidth || window.innerWidth - newLeft;
const maxHeight = limites.maxHeight || window.innerHeight - newTop;
@@ -393,5 +387,3 @@ export function criarResizeHandler(
document.removeEventListener('mouseup', handleMouseUp);
};
}

View File

@@ -24,17 +24,19 @@ export interface DispositivosDisponiveis {
/**
* Obter configuração do Jitsi do backend ou variáveis de ambiente (fallback)
*
*
* @param configBackend - Configuração do backend (opcional). Se fornecida, será usada.
* @returns Configuração do Jitsi
*/
export function obterConfiguracaoJitsi(configBackend?: {
domain: string;
appId: string;
roomPrefix: string;
useHttps: boolean;
acceptSelfSignedCert?: boolean;
} | null): ConfiguracaoJitsi {
export function obterConfiguracaoJitsi(
configBackend?: {
domain: string;
appId: string;
roomPrefix: string;
useHttps: boolean;
acceptSelfSignedCert?: boolean;
} | null
): ConfiguracaoJitsi {
// Se há configuração do backend e está ativa, usar ela
if (configBackend) {
return {
@@ -85,13 +87,13 @@ export function obterConfiguracaoJitsiSync(): ConfiguracaoJitsi {
*/
export function obterHostEPorta(domain: string): { host: string; porta: number } {
const [host, portaStr] = domain.split(':');
const porta = portaStr ? parseInt(portaStr, 10) : (domain.includes('8443') ? 8443 : 443);
const porta = portaStr ? parseInt(portaStr, 10) : domain.includes('8443') ? 8443 : 443;
return { host: host || 'localhost', porta };
}
/**
* Gerar nome único para a sala Jitsi
*
*
* @param conversaId - ID da conversa
* @param tipo - Tipo de chamada ('audio' ou 'video')
* @param configBackend - Configuração do backend (opcional). Se não fornecida, usa fallback.
@@ -107,13 +109,13 @@ export function gerarRoomName(
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
const conversaHash = conversaId.replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
return `${config.roomPrefix}-${tipo}-${conversaHash}-${timestamp}-${random}`;
}
/**
* Obter URL completa da sala Jitsi
*
*
* @param roomName - Nome da sala Jitsi
* @param configBackend - Configuração do backend (opcional). Se não fornecida, usa fallback.
*/
@@ -142,16 +144,12 @@ export async function validarDispositivos(): Promise<{
cameraDisponivel: false
};
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const microfoneDisponivel = devices.some(
(device) => device.kind === 'audioinput'
);
const cameraDisponivel = devices.some(
(device) => device.kind === 'videoinput'
);
const microfoneDisponivel = devices.some((device) => device.kind === 'audioinput');
const cameraDisponivel = devices.some((device) => device.kind === 'videoinput');
return {
microfoneDisponivel,
@@ -176,7 +174,7 @@ export async function solicitarPermissaoMidia(
if (typeof window === 'undefined') {
return null;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio,
@@ -200,13 +198,13 @@ export async function obterDispositivosDisponiveis(): Promise<DispositivosDispon
cameras: []
};
}
try {
// Solicitar permissão primeiro para obter labels dos dispositivos
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const devices = await navigator.mediaDevices.enumerateDevices();
const microphones: DispositivoMedia[] = devices
.filter((device) => device.kind === 'audioinput')
.map((device) => ({
@@ -256,7 +254,7 @@ export async function configurarAltoFalante(
if (typeof window === 'undefined') {
return false;
}
try {
// @ts-expect-error - setSinkId pode não estar disponível em todos os navegadores
if (audioElement.setSinkId && typeof audioElement.setSinkId === 'function') {
@@ -277,7 +275,7 @@ export function verificarSuporteWebRTC(): boolean {
if (typeof window === 'undefined') {
return false;
}
return !!(
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia &&
@@ -302,7 +300,7 @@ export function obterInfoNavegador(): {
mediaDevicesDisponivel: false
};
}
const userAgent = navigator.userAgent;
let navegador = 'Desconhecido';
let versao = 'Desconhecida';
@@ -332,4 +330,3 @@ export function obterInfoNavegador(): {
mediaDevicesDisponivel: !!navigator.mediaDevices
};
}

View File

@@ -1,56 +1,56 @@
/**
* 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') {
@@ -63,10 +63,10 @@ export function adicionarBlobBuilderPolyfill(): void {
(globalThis as any).MozBlobBuilder = BlobBuilderClass;
}
}
// Marcar que o polyfill foi adicionado
win.__blobBuilderPolyfillAdded = true;
console.log('✅ Polyfill BlobBuilder adicionado globalmente');
}
@@ -74,9 +74,3 @@ export function adicionarBlobBuilderPolyfill(): void {
if (typeof window !== 'undefined') {
adicionarBlobBuilderPolyfill();
}

View File

@@ -161,10 +161,7 @@ export function pararGravacao(recorder: MediaRecorder): Promise<Blob> {
/**
* Salvar gravação localmente
*/
export function salvarGravacao(
blob: Blob,
nomeArquivo: string
): void {
export function salvarGravacao(blob: Blob, nomeArquivo: string): void {
try {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
@@ -193,7 +190,7 @@ export function gerarNomeArquivo(
const dataFormatada = data.toISOString().replace(/[:.]/g, '-').split('T')[0];
const horaFormatada = data.toLocaleTimeString('pt-BR', { hour12: false }).replace(/:/g, '-');
const extensao = tipo === 'audio' ? 'webm' : 'webm';
return `gravacao-${tipo}-${roomName}-${dataFormatada}-${horaFormatada}.${extensao}`;
}
@@ -207,16 +204,13 @@ export function formatarTamanhoBlob(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
/**
* Calcular duração de gravação (em segundos)
*/
export function calcularDuracaoGravacao(
inicioTimestamp: number,
fimTimestamp?: number
): number {
export function calcularDuracaoGravacao(inicioTimestamp: number, fimTimestamp?: number): number {
const fim = fimTimestamp || Date.now();
return Math.floor((fim - inicioTimestamp) / 1000);
}
@@ -328,5 +322,3 @@ export class GravadorMedia {
this.inicioTimestamp = 0;
}
}

View File

@@ -3,137 +3,135 @@
* Coleta métricas do navegador e aplicação para monitoramento
*/
import type { ConvexClient } from "convex/browser";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { ConvexClient } from 'convex/browser';
import { api } from '@sgse-app/backend/convex/_generated/api';
export interface SystemMetrics {
cpuUsage?: number;
memoryUsage?: number;
networkLatency?: number;
storageUsed?: number;
usuariosOnline?: number;
mensagensPorMinuto?: number;
tempoRespostaMedio?: number;
errosCount?: number;
cpuUsage?: number;
memoryUsage?: number;
networkLatency?: number;
storageUsed?: number;
usuariosOnline?: number;
mensagensPorMinuto?: number;
tempoRespostaMedio?: number;
errosCount?: number;
}
/**
* Estima o uso de CPU baseado na Performance API
*/
async function estimateCPUUsage(): Promise<number> {
try {
// Usar navigator.hardwareConcurrency para número de cores
const cores = navigator.hardwareConcurrency || 4;
// Estimar baseado em performance.now() e tempo de execução
const start = performance.now();
// Simular trabalho para medir
let sum = 0;
for (let i = 0; i < 100000; i++) {
sum += Math.random();
}
const end = performance.now();
const executionTime = end - start;
// Normalizar para uma escala de 0-100
// Tempo rápido (<1ms) = baixo uso, tempo lento (>10ms) = alto uso
const usage = Math.min(100, (executionTime / 10) * 100);
return Math.round(usage);
} catch (error) {
console.error("Erro ao estimar CPU:", error);
return 0;
}
try {
// Usar navigator.hardwareConcurrency para número de cores
const cores = navigator.hardwareConcurrency || 4;
// Estimar baseado em performance.now() e tempo de execução
const start = performance.now();
// Simular trabalho para medir
let sum = 0;
for (let i = 0; i < 100000; i++) {
sum += Math.random();
}
const end = performance.now();
const executionTime = end - start;
// Normalizar para uma escala de 0-100
// Tempo rápido (<1ms) = baixo uso, tempo lento (>10ms) = alto uso
const usage = Math.min(100, (executionTime / 10) * 100);
return Math.round(usage);
} catch (error) {
console.error('Erro ao estimar CPU:', error);
return 0;
}
}
/**
* Obtém o uso de memória do navegador
*/
function getMemoryUsage(): number {
try {
// @ts-ignore - performance.memory é específico do Chrome
if (performance.memory) {
// @ts-ignore
const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory;
const usage = (usedJSHeapSize / jsHeapSizeLimit) * 100;
return Math.round(usage);
}
// Estimativa baseada em outros indicadores
return Math.round(Math.random() * 30 + 20); // 20-50% estimado
} catch (error) {
console.error("Erro ao obter memória:", error);
return 0;
}
try {
// @ts-ignore - performance.memory é específico do Chrome
if (performance.memory) {
// @ts-ignore
const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory;
const usage = (usedJSHeapSize / jsHeapSizeLimit) * 100;
return Math.round(usage);
}
// Estimativa baseada em outros indicadores
return Math.round(Math.random() * 30 + 20); // 20-50% estimado
} catch (error) {
console.error('Erro ao obter memória:', error);
return 0;
}
}
/**
* Mede a latência de rede
*/
async function measureNetworkLatency(): Promise<number> {
try {
const start = performance.now();
// Fazer uma requisição pequena para medir latência
await fetch(window.location.origin + "/favicon.ico", {
method: "HEAD",
cache: "no-cache",
});
const end = performance.now();
return Math.round(end - start);
} catch (error) {
console.error("Erro ao medir latência:", error);
return 0;
}
try {
const start = performance.now();
// Fazer uma requisição pequena para medir latência
await fetch(window.location.origin + '/favicon.ico', {
method: 'HEAD',
cache: 'no-cache'
});
const end = performance.now();
return Math.round(end - start);
} catch (error) {
console.error('Erro ao medir latência:', error);
return 0;
}
}
/**
* Obtém o uso de armazenamento
*/
async function getStorageUsage(): Promise<number> {
try {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
if (estimate.usage && estimate.quota) {
const usage = (estimate.usage / estimate.quota) * 100;
return Math.round(usage);
}
}
// Fallback: estimar baseado em localStorage
let totalSize = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
totalSize += localStorage[key].length + key.length;
}
}
// Assumir quota de 10MB para localStorage
const usage = (totalSize / (10 * 1024 * 1024)) * 100;
return Math.round(Math.min(usage, 100));
} catch (error) {
console.error("Erro ao obter storage:", error);
return 0;
}
try {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
if (estimate.usage && estimate.quota) {
const usage = (estimate.usage / estimate.quota) * 100;
return Math.round(usage);
}
}
// Fallback: estimar baseado em localStorage
let totalSize = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
totalSize += localStorage[key].length + key.length;
}
}
// Assumir quota de 10MB para localStorage
const usage = (totalSize / (10 * 1024 * 1024)) * 100;
return Math.round(Math.min(usage, 100));
} catch (error) {
console.error('Erro ao obter storage:', error);
return 0;
}
}
/**
* Obtém o número de usuários online
*/
async function getUsuariosOnline(client: ConvexClient): Promise<number> {
try {
const usuarios = await client.query(api.chat.listarTodosUsuarios, {});
const online = usuarios.filter(
(u: any) => u.statusPresenca === "online"
).length;
return online;
} catch (error) {
console.error("Erro ao obter usuários online:", error);
return 0;
}
try {
const usuarios = await client.query(api.chat.listarTodosUsuarios, {});
const online = usuarios.filter((u: any) => u.statusPresenca === 'online').length;
return online;
} catch (error) {
console.error('Erro ao obter usuários online:', error);
return 0;
}
}
/**
@@ -143,36 +141,36 @@ let lastMessageCount = 0;
let lastMessageTime = Date.now();
function calculateMessagesPerMinute(currentMessageCount: number): number {
const now = Date.now();
const timeDiff = (now - lastMessageTime) / 1000 / 60; // em minutos
if (timeDiff === 0) return 0;
const messageDiff = currentMessageCount - lastMessageCount;
const messagesPerMinute = messageDiff / timeDiff;
lastMessageCount = currentMessageCount;
lastMessageTime = now;
return Math.max(0, Math.round(messagesPerMinute));
const now = Date.now();
const timeDiff = (now - lastMessageTime) / 1000 / 60; // em minutos
if (timeDiff === 0) return 0;
const messageDiff = currentMessageCount - lastMessageCount;
const messagesPerMinute = messageDiff / timeDiff;
lastMessageCount = currentMessageCount;
lastMessageTime = now;
return Math.max(0, Math.round(messagesPerMinute));
}
/**
* Estima o tempo médio de resposta da aplicação
*/
async function estimateResponseTime(client: ConvexClient): Promise<number> {
try {
const start = performance.now();
// Fazer uma query simples para medir tempo de resposta
await client.query(api.chat.listarTodosUsuarios, {});
const end = performance.now();
return Math.round(end - start);
} catch (error) {
console.error("Erro ao estimar tempo de resposta:", error);
return 0;
}
try {
const start = performance.now();
// Fazer uma query simples para medir tempo de resposta
await client.query(api.chat.listarTodosUsuarios, {});
const end = performance.now();
return Math.round(end - start);
} catch (error) {
console.error('Erro ao estimar tempo de resposta:', error);
return 0;
}
}
/**
@@ -181,145 +179,133 @@ async function estimateResponseTime(client: ConvexClient): Promise<number> {
let errorCount = 0;
// Interceptar erros globais
if (typeof window !== "undefined") {
const originalError = console.error;
console.error = function (...args: any[]) {
errorCount++;
originalError.apply(console, args);
};
if (typeof window !== 'undefined') {
const originalError = console.error;
console.error = function (...args: any[]) {
errorCount++;
originalError.apply(console, args);
};
window.addEventListener("error", () => {
errorCount++;
});
window.addEventListener('error', () => {
errorCount++;
});
window.addEventListener("unhandledrejection", () => {
errorCount++;
});
window.addEventListener('unhandledrejection', () => {
errorCount++;
});
}
function getErrorCount(): number {
const count = errorCount;
errorCount = 0; // Reset após leitura
return count;
const count = errorCount;
errorCount = 0; // Reset após leitura
return count;
}
/**
* Coleta todas as métricas do sistema
*/
export async function collectMetrics(
client: ConvexClient
): Promise<SystemMetrics> {
try {
const [
cpuUsage,
memoryUsage,
networkLatency,
storageUsed,
usuariosOnline,
tempoRespostaMedio,
] = await Promise.all([
estimateCPUUsage(),
Promise.resolve(getMemoryUsage()),
measureNetworkLatency(),
getStorageUsage(),
getUsuariosOnline(client),
estimateResponseTime(client),
]);
export async function collectMetrics(client: ConvexClient): Promise<SystemMetrics> {
try {
const [cpuUsage, memoryUsage, networkLatency, storageUsed, usuariosOnline, tempoRespostaMedio] =
await Promise.all([
estimateCPUUsage(),
Promise.resolve(getMemoryUsage()),
measureNetworkLatency(),
getStorageUsage(),
getUsuariosOnline(client),
estimateResponseTime(client)
]);
// Para mensagens por minuto, precisamos de um contador
// Por enquanto, vamos usar 0 e implementar depois
const mensagensPorMinuto = 0;
// Para mensagens por minuto, precisamos de um contador
// Por enquanto, vamos usar 0 e implementar depois
const mensagensPorMinuto = 0;
const errosCount = getErrorCount();
const errosCount = getErrorCount();
return {
cpuUsage,
memoryUsage,
networkLatency,
storageUsed,
usuariosOnline,
mensagensPorMinuto,
tempoRespostaMedio,
errosCount,
};
} catch (error) {
console.error("Erro ao coletar métricas:", error);
return {};
}
return {
cpuUsage,
memoryUsage,
networkLatency,
storageUsed,
usuariosOnline,
mensagensPorMinuto,
tempoRespostaMedio,
errosCount
};
} catch (error) {
console.error('Erro ao coletar métricas:', error);
return {};
}
}
/**
* Envia métricas para o backend
*/
export async function sendMetrics(
client: ConvexClient,
metrics: SystemMetrics
): Promise<void> {
try {
await client.mutation(api.monitoramento.salvarMetricas, metrics);
} catch (error) {
console.error("Erro ao enviar métricas:", error);
}
export async function sendMetrics(client: ConvexClient, metrics: SystemMetrics): Promise<void> {
try {
await client.mutation(api.monitoramento.salvarMetricas, metrics);
} catch (error) {
console.error('Erro ao enviar métricas:', error);
}
}
/**
* Inicia a coleta automática de métricas
*/
export function startMetricsCollection(
client: ConvexClient,
intervalMs: number = 2000 // 2 segundos
client: ConvexClient,
intervalMs: number = 2000 // 2 segundos
): () => void {
let lastCollectionTime = 0;
let lastCollectionTime = 0;
const collect = async () => {
const now = Date.now();
// Evitar coletar muito frequentemente (rate limiting)
if (now - lastCollectionTime < intervalMs) {
return;
}
lastCollectionTime = now;
const metrics = await collectMetrics(client);
await sendMetrics(client, metrics);
};
const collect = async () => {
const now = Date.now();
// Coletar imediatamente
collect();
// Evitar coletar muito frequentemente (rate limiting)
if (now - lastCollectionTime < intervalMs) {
return;
}
// Configurar intervalo
const intervalId = setInterval(collect, intervalMs);
lastCollectionTime = now;
// Retornar função para parar a coleta
return () => {
clearInterval(intervalId);
};
const metrics = await collectMetrics(client);
await sendMetrics(client, metrics);
};
// Coletar imediatamente
collect();
// Configurar intervalo
const intervalId = setInterval(collect, intervalMs);
// Retornar função para parar a coleta
return () => {
clearInterval(intervalId);
};
}
/**
* Obtém o status da conexão de rede
*/
export function getNetworkStatus(): {
online: boolean;
type?: string;
downlink?: number;
rtt?: number;
online: boolean;
type?: string;
downlink?: number;
rtt?: number;
} {
const online = navigator.onLine;
// @ts-ignore - navigator.connection é experimental
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
return {
online,
type: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
};
}
return { online };
}
const online = navigator.onLine;
// @ts-ignore - navigator.connection é experimental
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
return {
online,
type: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt
};
}
return { online };
}

View File

@@ -1,52 +1,52 @@
// Definições dos modelos de declaração
export interface ModeloDeclaracao {
id: string;
nome: string;
descricao: string;
arquivo: string;
podePreencherAutomaticamente: boolean;
id: string;
nome: string;
descricao: string;
arquivo: string;
podePreencherAutomaticamente: boolean;
}
export const modelosDeclaracoes: ModeloDeclaracao[] = [
{
id: "acumulacao_cargo",
nome: "Declaração de Acumulação de Cargo",
descricao: "Declaração sobre acumulação de cargo, emprego, função pública ou proventos",
arquivo: "/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf",
podePreencherAutomaticamente: true,
},
{
id: "dependentes_ir",
nome: "Declaração de Dependentes",
descricao: "Declaração de dependentes para fins de Imposto de Renda",
arquivo: "/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf",
podePreencherAutomaticamente: true,
},
{
id: "idoneidade",
nome: "Declaração de Idoneidade",
descricao: "Declaração de idoneidade moral e conduta ilibada",
arquivo: "/modelos/declaracoes/Declaração de Idoneidade.pdf",
podePreencherAutomaticamente: true,
},
{
id: "nepotismo",
nome: "Termo de Declaração de Nepotismo",
descricao: "Declaração sobre inexistência de situação de nepotismo",
arquivo: "/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf",
podePreencherAutomaticamente: true,
},
{
id: "opcao_remuneracao",
nome: "Termo de Opção - Remuneração",
descricao: "Termo de opção de remuneração",
arquivo: "/modelos/declaracoes/Termo de Opção - Remuneração.pdf",
podePreencherAutomaticamente: true,
},
{
id: 'acumulacao_cargo',
nome: 'Declaração de Acumulação de Cargo',
descricao: 'Declaração sobre acumulação de cargo, emprego, função pública ou proventos',
arquivo:
'/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf',
podePreencherAutomaticamente: true
},
{
id: 'dependentes_ir',
nome: 'Declaração de Dependentes',
descricao: 'Declaração de dependentes para fins de Imposto de Renda',
arquivo: '/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf',
podePreencherAutomaticamente: true
},
{
id: 'idoneidade',
nome: 'Declaração de Idoneidade',
descricao: 'Declaração de idoneidade moral e conduta ilibada',
arquivo: '/modelos/declaracoes/Declaração de Idoneidade.pdf',
podePreencherAutomaticamente: true
},
{
id: 'nepotismo',
nome: 'Termo de Declaração de Nepotismo',
descricao: 'Declaração sobre inexistência de situação de nepotismo',
arquivo: '/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf',
podePreencherAutomaticamente: true
},
{
id: 'opcao_remuneracao',
nome: 'Termo de Opção - Remuneração',
descricao: 'Termo de opção de remuneração',
arquivo: '/modelos/declaracoes/Termo de Opção - Remuneração.pdf',
podePreencherAutomaticamente: true
}
];
export function getModeloById(id: string): ModeloDeclaracao | undefined {
return modelosDeclaracoes.find(modelo => modelo.id === id);
return modelosDeclaracoes.find((modelo) => modelo.id === id);
}

View File

@@ -2,265 +2,265 @@
* Solicita permissão para notificações desktop
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!("Notification" in window)) {
console.warn("Este navegador não suporta notificações desktop");
return "denied";
}
if (!('Notification' in window)) {
console.warn('Este navegador não suporta notificações desktop');
return 'denied';
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission === 'granted') {
return 'granted';
}
if (Notification.permission !== "denied") {
return await Notification.requestPermission();
}
if (Notification.permission !== 'denied') {
return await Notification.requestPermission();
}
return Notification.permission;
return Notification.permission;
}
/**
* Mostra uma notificação desktop
*/
export function showNotification(title: string, options?: NotificationOptions): Notification | null {
if (!("Notification" in window)) {
return null;
}
export function showNotification(
title: string,
options?: NotificationOptions
): Notification | null {
if (!('Notification' in window)) {
return null;
}
if (Notification.permission !== "granted") {
return null;
}
if (Notification.permission !== 'granted') {
return null;
}
try {
return new Notification(title, {
icon: "/favicon.png",
badge: "/favicon.png",
...options,
});
} catch (error) {
console.error("Erro ao exibir notificação:", error);
return null;
}
try {
return new Notification(title, {
icon: '/favicon.png',
badge: '/favicon.png',
...options
});
} catch (error) {
console.error('Erro ao exibir notificação:', error);
return null;
}
}
/**
* Toca o som de notificação
*/
export function playNotificationSound() {
try {
const audio = new Audio("/sounds/notification.mp3");
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn("Não foi possível reproduzir o som de notificação:", err);
});
} catch (error) {
console.error("Erro ao tocar som de notificação:", error);
}
try {
const audio = new Audio('/sounds/notification.mp3');
audio.volume = 0.5;
audio.play().catch((err) => {
console.warn('Não foi possível reproduzir o som de notificação:', err);
});
} catch (error) {
console.error('Erro ao tocar som de notificação:', error);
}
}
/**
* Verifica se o usuário está na aba ativa
*/
export function isTabActive(): boolean {
return !document.hidden;
return !document.hidden;
}
/**
* Registrar service worker para push notifications
*/
export async function registrarServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if (!("serviceWorker" in navigator)) {
console.warn("Service Workers não são suportados neste navegador");
return null;
}
if (!('serviceWorker' in navigator)) {
console.warn('Service Workers não são suportados neste navegador');
return null;
}
try {
// Verificar se já existe um Service Worker ativo antes de registrar
const existingRegistration = await navigator.serviceWorker.getRegistration("/");
if (existingRegistration?.active) {
return existingRegistration;
}
try {
// Verificar se já existe um Service Worker ativo antes de registrar
const existingRegistration = await navigator.serviceWorker.getRegistration('/');
if (existingRegistration?.active) {
return existingRegistration;
}
// Registrar com timeout para evitar travamentos
const registerPromise = navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
// Registrar com timeout para evitar travamentos
const registerPromise = navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
const timeoutPromise = new Promise<ServiceWorkerRegistration | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const timeoutPromise = new Promise<ServiceWorkerRegistration | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registerPromise, timeoutPromise]);
if (registration) {
// Log apenas em desenvolvimento
if (import.meta.env.DEV) {
console.log("Service Worker registrado:", registration);
}
}
return registration;
} catch (error) {
// Ignorar erros silenciosamente para evitar spam no console
// especialmente erros relacionados a message channel
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
!errorMessage.includes("message channel") &&
!errorMessage.includes("registration") &&
import.meta.env.DEV
) {
console.error("Erro ao registrar Service Worker:", error);
}
}
return null;
}
const registration = await Promise.race([registerPromise, timeoutPromise]);
if (registration) {
// Log apenas em desenvolvimento
if (import.meta.env.DEV) {
console.log('Service Worker registrado:', registration);
}
}
return registration;
} catch (error) {
// Ignorar erros silenciosamente para evitar spam no console
// especialmente erros relacionados a message channel
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
!errorMessage.includes('message channel') &&
!errorMessage.includes('registration') &&
import.meta.env.DEV
) {
console.error('Erro ao registrar Service Worker:', error);
}
}
return null;
}
}
/**
* Solicitar subscription de push notification
*/
export async function solicitarPushSubscription(): Promise<PushSubscription | null> {
try {
// Registrar service worker primeiro com timeout
const registrationPromise = registrarServiceWorker();
const timeoutPromise = new Promise<null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
const registration = await Promise.race([registrationPromise, timeoutPromise]);
if (!registration) {
return null;
}
try {
// Registrar service worker primeiro com timeout
const registrationPromise = registrarServiceWorker();
const timeoutPromise = new Promise<null>((resolve) => setTimeout(() => resolve(null), 3000));
// Verificar se push está disponível
if (!("PushManager" in window)) {
return null;
}
const registration = await Promise.race([registrationPromise, timeoutPromise]);
if (!registration) {
return null;
}
// Solicitar permissão com timeout
const permissionPromise = requestNotificationPermission();
const permissionTimeoutPromise = new Promise<NotificationPermission>((resolve) =>
setTimeout(() => resolve("denied"), 3000)
);
const permission = await Promise.race([permissionPromise, permissionTimeoutPromise]);
if (permission !== "granted") {
return null;
}
// Verificar se push está disponível
if (!('PushManager' in window)) {
return null;
}
// Obter subscription existente ou criar nova com timeout
const getSubscriptionPromise = registration.pushManager.getSubscription();
const getSubscriptionTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
let subscription = await Promise.race([getSubscriptionPromise, getSubscriptionTimeoutPromise]);
// Solicitar permissão com timeout
const permissionPromise = requestNotificationPermission();
const permissionTimeoutPromise = new Promise<NotificationPermission>((resolve) =>
setTimeout(() => resolve('denied'), 3000)
);
if (!subscription) {
// VAPID public key deve vir do backend ou config
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || "";
const permission = await Promise.race([permissionPromise, permissionTimeoutPromise]);
if (permission !== 'granted') {
return null;
}
if (!vapidPublicKey) {
// Não logar warning para evitar spam no console
return null;
}
// Obter subscription existente ou criar nova com timeout
const getSubscriptionPromise = registration.pushManager.getSubscription();
const getSubscriptionTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 3000)
);
// Converter chave para formato Uint8Array
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
let subscription = await Promise.race([getSubscriptionPromise, getSubscriptionTimeoutPromise]);
// Subscribe com timeout
const subscribePromise = registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});
const subscribeTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 5000)
);
subscription = await Promise.race([subscribePromise, subscribeTimeoutPromise]);
}
if (!subscription) {
// VAPID public key deve vir do backend ou config
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || '';
return subscription;
} catch (error) {
// Ignorar erros relacionados a message channel ou service worker
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
errorMessage.includes("message channel") ||
errorMessage.includes("service worker") ||
errorMessage.includes("registration")
) {
return null;
}
}
return null;
}
if (!vapidPublicKey) {
// Não logar warning para evitar spam no console
return null;
}
// Converter chave para formato Uint8Array
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
// Subscribe com timeout
const subscribePromise = registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey
});
const subscribeTimeoutPromise = new Promise<PushSubscription | null>((resolve) =>
setTimeout(() => resolve(null), 5000)
);
subscription = await Promise.race([subscribePromise, subscribeTimeoutPromise]);
}
return subscription;
} catch (error) {
// Ignorar erros relacionados a message channel ou service worker
if (error instanceof Error) {
const errorMessage = error.message.toLowerCase();
if (
errorMessage.includes('message channel') ||
errorMessage.includes('service worker') ||
errorMessage.includes('registration')
) {
return null;
}
}
return null;
}
}
/**
* Converter chave VAPID de base64 URL-safe para Uint8Array
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Converter PushSubscription para formato serializável
*/
export function subscriptionToJSON(subscription: PushSubscription): {
endpoint: string;
keys: { p256dh: string; auth: string };
endpoint: string;
keys: { p256dh: string; auth: string };
} {
const key = subscription.getKey("p256dh");
const auth = subscription.getKey("auth");
const key = subscription.getKey('p256dh');
const auth = subscription.getKey('auth');
if (!key || !auth) {
throw new Error("Chaves de subscription não encontradas");
}
if (!key || !auth) {
throw new Error('Chaves de subscription não encontradas');
}
return {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(key),
auth: arrayBufferToBase64(auth),
},
};
return {
endpoint: subscription.endpoint,
keys: {
p256dh: arrayBufferToBase64(key),
auth: arrayBufferToBase64(auth)
}
};
}
/**
* Converter ArrayBuffer para base64
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
/**
* Remover subscription de push notification
*/
export async function removerPushSubscription(): Promise<boolean> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
return true;
}
if (subscription) {
await subscription.unsubscribe();
return true;
}
return false;
return false;
}

View File

@@ -14,14 +14,16 @@ export function formatarDataHoraCompleta(
minuto: number,
segundo: number
): string {
const dataObj = new Date(`${data}T${formatarHoraPonto(hora, minuto)}:${segundo.toString().padStart(2, '0')}`);
const dataObj = new Date(
`${data}T${formatarHoraPonto(hora, minuto)}:${segundo.toString().padStart(2, '0')}`
);
return dataObj.toLocaleString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
second: '2-digit'
});
}
@@ -83,7 +85,7 @@ export function getTipoRegistroLabel(
entrada: config.nomeEntrada || 'Entrada 1',
saida_almoco: config.nomeSaidaAlmoco || 'Saída 1',
retorno_almoco: config.nomeRetornoAlmoco || 'Entrada 2',
saida: config.nomeSaida || 'Saída 2',
saida: config.nomeSaida || 'Saída 2'
};
return labels[tipo] || tipo;
}
@@ -93,7 +95,7 @@ export function getTipoRegistroLabel(
entrada: 'Entrada 1',
saida_almoco: 'Saída 1',
retorno_almoco: 'Entrada 2',
saida: 'Saída 2',
saida: 'Saída 2'
};
return labels[tipo] || tipo;
}
@@ -128,9 +130,9 @@ export function getProximoTipoRegistro(
*/
export function formatarDataDDMMAAAA(data: string | Date | number): string {
if (!data) return '';
let dataObj: Date;
if (typeof data === 'string') {
// Se for string no formato ISO (YYYY-MM-DD), adicionar hora para evitar problemas de timezone
if (data.match(/^\d{4}-\d{2}-\d{2}$/)) {
@@ -143,16 +145,15 @@ export function formatarDataDDMMAAAA(data: string | Date | number): string {
} else {
dataObj = data;
}
// Verificar se a data é válida
if (isNaN(dataObj.getTime())) {
return '';
}
const dia = dataObj.getDate().toString().padStart(2, '0');
const mes = (dataObj.getMonth() + 1).toString().padStart(2, '0');
const ano = dataObj.getFullYear();
return `${dia}/${mes}/${ano}`;
}

View File

@@ -53,4 +53,3 @@ export function calcularOffset(timestampServidor: number, timestampLocal: number
export function aplicarOffset(timestamp: number, offsetSegundos: number): number {
return timestamp + offsetSegundos * 1000;
}

View File

@@ -161,16 +161,16 @@ export function aplicarTema(temaId: TemaId | string | null | undefined): void {
if (bodyElement) {
bodyElement.removeAttribute('data-theme');
}
// Aplicar o novo tema
htmlElement.setAttribute('data-theme', nomeDaisyUI);
if (bodyElement) {
bodyElement.setAttribute('data-theme', nomeDaisyUI);
}
// Forçar reflow para garantir que o CSS seja aplicado
void htmlElement.offsetHeight;
// Forçar atualização de todas as variáveis CSS
// Isso garante que os temas customizados sejam aplicados corretamente
if (typeof window !== 'undefined' && window.getComputedStyle) {
@@ -178,7 +178,7 @@ export function aplicarTema(temaId: TemaId | string | null | undefined): void {
// Forçar recálculo das variáveis CSS
computedStyle.getPropertyValue('--p');
}
// Disparar evento customizado para notificar mudança de tema
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: nomeDaisyUI } }));
@@ -285,4 +285,3 @@ export function hexToRgb(hex: string): string {
return `rgb(${r}, ${g}, ${b})`;
}

View File

@@ -2,7 +2,11 @@
* Verifica se webcam está disponível
*/
export async function validarWebcamDisponivel(): Promise<boolean> {
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia
) {
return false;
}
@@ -18,7 +22,11 @@ export async function validarWebcamDisponivel(): Promise<boolean> {
* Captura imagem da webcam
*/
export async function capturarWebcam(): Promise<Blob | null> {
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia
) {
return null;
}
@@ -30,8 +38,8 @@ export async function capturarWebcam(): Promise<Blob | null> {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user',
},
facingMode: 'user'
}
});
// Criar elemento de vídeo temporário
@@ -89,7 +97,11 @@ export async function capturarWebcamComPreview(
videoElement: HTMLVideoElement,
canvasElement: HTMLCanvasElement
): Promise<Blob | null> {
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia
) {
return null;
}
@@ -101,8 +113,8 @@ export async function capturarWebcamComPreview(
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user',
},
facingMode: 'user'
}
});
videoElement.srcObject = stream;
@@ -147,4 +159,3 @@ export async function capturarWebcamComPreview(
}
}
}