feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.
This commit is contained in:
@@ -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;
|
||||
const foundIPs: string[] = [];
|
||||
let publicIP: string | undefined;
|
||||
let localIP: string | 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;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -62,7 +62,11 @@ export interface InformacoesDispositivo {
|
||||
/**
|
||||
* Detecta informações do navegador
|
||||
*/
|
||||
function detectarNavegador(): { browser: string; browserVersion: string; engine: string } {
|
||||
function detectarNavegador(): {
|
||||
browser: string;
|
||||
browserVersion: string;
|
||||
engine: string;
|
||||
} {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return { browser: 'Desconhecido', browserVersion: '', engine: '' };
|
||||
}
|
||||
@@ -123,7 +127,7 @@ function detectarSistemaOperacional(): {
|
||||
sistemaOperacional: 'Desconhecido',
|
||||
osVersion: '',
|
||||
arquitetura: '',
|
||||
plataforma: '',
|
||||
plataforma: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,7 +148,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 +195,7 @@ function detectarTipoDispositivo(): {
|
||||
deviceType: 'Desconhecido',
|
||||
isMobile: false,
|
||||
isTablet: false,
|
||||
isDesktop: true,
|
||||
isDesktop: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,7 +217,10 @@ function detectarTipoDispositivo(): {
|
||||
/**
|
||||
* Obtém informações da tela
|
||||
*/
|
||||
function obterInformacoesTela(): { screenResolution: string; coresTela: number } {
|
||||
function obterInformacoesTela(): {
|
||||
screenResolution: string;
|
||||
coresTela: number;
|
||||
} {
|
||||
if (typeof screen === 'undefined') {
|
||||
return { screenResolution: 'Desconhecido', coresTela: 0 };
|
||||
}
|
||||
@@ -232,7 +239,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 +268,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 +290,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 +361,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 +415,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 +433,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 +443,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 +451,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 +557,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 +577,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 +660,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 +717,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 +774,22 @@ 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 +818,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,
|
||||
@@ -792,37 +827,50 @@ 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 leiturasAcelerometro: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
z: 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) => (l.x - mediaX) ** 2 + (l.y - mediaY) ** 2 + (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 +881,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 +892,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
|
||||
gamma: ultimaLeitura.gamma || 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
resolve({
|
||||
acelerometro,
|
||||
giroscopio,
|
||||
@@ -852,7 +900,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
|
||||
permissaoNegada: false
|
||||
});
|
||||
}, duracaoMs);
|
||||
|
||||
|
||||
function handleDeviceMotion(event: DeviceMotionEvent) {
|
||||
if (event.accelerationIncludingGravity) {
|
||||
const acc = event.accelerationIncludingGravity;
|
||||
@@ -866,7 +914,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 +925,7 @@ async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener('devicemotion', handleDeviceMotion);
|
||||
window.addEventListener('deviceorientation', handleDeviceOrientation);
|
||||
});
|
||||
@@ -922,14 +970,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 +1010,3 @@ export async function obterInformacoesDispositivo(): Promise<InformacoesDisposit
|
||||
|
||||
return informacoes;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -83,15 +85,18 @@ export function obterConfiguracaoJitsiSync(): ConfiguracaoJitsi {
|
||||
/**
|
||||
* Obter host e porta separados do domínio
|
||||
*/
|
||||
export function obterHostEPorta(domain: string): { host: string; porta: number } {
|
||||
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 +112,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 +147,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 +177,7 @@ export async function solicitarPermissaoMidia(
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio,
|
||||
@@ -200,13 +201,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 +257,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 +278,7 @@ export function verificarSuporteWebRTC(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return !!(
|
||||
navigator.mediaDevices &&
|
||||
navigator.mediaDevices.getUserMedia &&
|
||||
@@ -302,7 +303,7 @@ export function obterInfoNavegador(): {
|
||||
mediaDevicesDisponivel: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
let navegador = 'Desconhecido';
|
||||
let versao = 'Desconhecida';
|
||||
@@ -332,4 +333,3 @@ export function obterInfoNavegador(): {
|
||||
mediaDevicesDisponivel: !!navigator.mediaDevices
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 / 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { ConvexClient } from 'convex/browser';
|
||||
|
||||
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-expect-error - performance.memory é específico do Chrome
|
||||
if (performance.memory) {
|
||||
// @ts-expect-error
|
||||
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 (const key in localStorage) {
|
||||
if (Object.hasOwn(localStorage, 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 = (...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-expect-error - 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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,4 +53,3 @@ export function calcularOffset(timestampServidor: number, timestampLocal: number
|
||||
export function aplicarOffset(timestamp: number, offsetSegundos: number): number {
|
||||
return timestamp + offsetSegundos * 1000;
|
||||
}
|
||||
|
||||
|
||||
@@ -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})`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user