feat: integrate point management features into the dashboard
- Added a new "Meu Ponto" section for users to register their work hours, breaks, and attendance. - Introduced a "Controle de Ponto" category in the Recursos Humanos section for managing employee time records. - Enhanced the backend schema to support point registration and configuration settings. - Updated various components to improve UI consistency and user experience across the dashboard.
This commit is contained in:
397
apps/web/src/lib/utils/deviceInfo.ts
Normal file
397
apps/web/src/lib/utils/deviceInfo.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import { getLocalIP } from './browserInfo';
|
||||
|
||||
export interface InformacoesDispositivo {
|
||||
ipAddress?: string;
|
||||
ipPublico?: string;
|
||||
ipLocal?: string;
|
||||
userAgent?: string;
|
||||
browser?: string;
|
||||
browserVersion?: string;
|
||||
engine?: string;
|
||||
sistemaOperacional?: string;
|
||||
osVersion?: string;
|
||||
arquitetura?: string;
|
||||
plataforma?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
precisao?: number;
|
||||
endereco?: string;
|
||||
cidade?: string;
|
||||
estado?: string;
|
||||
pais?: string;
|
||||
timezone?: string;
|
||||
deviceType?: string;
|
||||
deviceModel?: string;
|
||||
screenResolution?: string;
|
||||
coresTela?: number;
|
||||
idioma?: string;
|
||||
isMobile?: boolean;
|
||||
isTablet?: boolean;
|
||||
isDesktop?: boolean;
|
||||
connectionType?: string;
|
||||
memoryInfo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta informações do navegador
|
||||
*/
|
||||
function detectarNavegador(): { browser: string; browserVersion: string; engine: string } {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return { browser: 'Desconhecido', browserVersion: '', engine: '' };
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
let browser = 'Desconhecido';
|
||||
let browserVersion = '';
|
||||
let engine = '';
|
||||
|
||||
// Detectar engine
|
||||
if (ua.includes('Edg/')) {
|
||||
engine = 'EdgeHTML';
|
||||
} else if (ua.includes('Chrome/')) {
|
||||
engine = 'Blink';
|
||||
} else if (ua.includes('Firefox/')) {
|
||||
engine = 'Gecko';
|
||||
} else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
|
||||
engine = 'WebKit';
|
||||
}
|
||||
|
||||
// Detectar navegador
|
||||
if (ua.includes('Edg/')) {
|
||||
browser = 'Edge';
|
||||
const match = ua.match(/Edg\/(\d+)/);
|
||||
browserVersion = match ? match[1]! : '';
|
||||
} else if (ua.includes('Chrome/') && !ua.includes('Edg/')) {
|
||||
browser = 'Chrome';
|
||||
const match = ua.match(/Chrome\/(\d+)/);
|
||||
browserVersion = match ? match[1]! : '';
|
||||
} else if (ua.includes('Firefox/')) {
|
||||
browser = 'Firefox';
|
||||
const match = ua.match(/Firefox\/(\d+)/);
|
||||
browserVersion = match ? match[1]! : '';
|
||||
} else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
|
||||
browser = 'Safari';
|
||||
const match = ua.match(/Version\/(\d+)/);
|
||||
browserVersion = match ? match[1]! : '';
|
||||
} else if (ua.includes('Opera/') || ua.includes('OPR/')) {
|
||||
browser = 'Opera';
|
||||
const match = ua.match(/(?:Opera|OPR)\/(\d+)/);
|
||||
browserVersion = match ? match[1]! : '';
|
||||
}
|
||||
|
||||
return { browser, browserVersion, engine };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta informações do sistema operacional
|
||||
*/
|
||||
function detectarSistemaOperacional(): {
|
||||
sistemaOperacional: string;
|
||||
osVersion: string;
|
||||
arquitetura: string;
|
||||
plataforma: string;
|
||||
} {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return {
|
||||
sistemaOperacional: 'Desconhecido',
|
||||
osVersion: '',
|
||||
arquitetura: '',
|
||||
plataforma: '',
|
||||
};
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
const platform = navigator.platform || '';
|
||||
let sistemaOperacional = 'Desconhecido';
|
||||
let osVersion = '';
|
||||
let arquitetura = '';
|
||||
const plataforma = platform;
|
||||
|
||||
// Detectar OS
|
||||
if (ua.includes('Windows NT')) {
|
||||
sistemaOperacional = 'Windows';
|
||||
const match = ua.match(/Windows NT (\d+\.\d+)/);
|
||||
if (match) {
|
||||
const version = match[1]!;
|
||||
const versions: Record<string, string> = {
|
||||
'10.0': '10/11',
|
||||
'6.3': '8.1',
|
||||
'6.2': '8',
|
||||
'6.1': '7',
|
||||
};
|
||||
osVersion = versions[version] || version;
|
||||
}
|
||||
} else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) {
|
||||
sistemaOperacional = 'macOS';
|
||||
const match = ua.match(/Mac OS X (\d+[._]\d+)/);
|
||||
if (match) {
|
||||
osVersion = match[1]!.replace('_', '.');
|
||||
}
|
||||
} else if (ua.includes('Linux')) {
|
||||
sistemaOperacional = 'Linux';
|
||||
osVersion = 'Linux';
|
||||
} else if (ua.includes('Android')) {
|
||||
sistemaOperacional = 'Android';
|
||||
const match = ua.match(/Android (\d+(?:\.\d+)?)/);
|
||||
osVersion = match ? match[1]! : '';
|
||||
} else if (ua.includes('iPhone') || ua.includes('iPad')) {
|
||||
sistemaOperacional = 'iOS';
|
||||
const match = ua.match(/OS (\d+[._]\d+)/);
|
||||
if (match) {
|
||||
osVersion = match[1]!.replace('_', '.');
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar arquitetura (se disponível)
|
||||
if ('cpuClass' in navigator) {
|
||||
arquitetura = (navigator as unknown as { cpuClass: string }).cpuClass;
|
||||
}
|
||||
|
||||
return { sistemaOperacional, osVersion, arquitetura, plataforma };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta tipo de dispositivo
|
||||
*/
|
||||
function detectarTipoDispositivo(): {
|
||||
deviceType: string;
|
||||
isMobile: boolean;
|
||||
isTablet: boolean;
|
||||
isDesktop: boolean;
|
||||
} {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return {
|
||||
deviceType: 'Desconhecido',
|
||||
isMobile: false,
|
||||
isTablet: false,
|
||||
isDesktop: true,
|
||||
};
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
const isMobile = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
||||
const isTablet = /iPad|Android(?!.*Mobile)|Tablet/i.test(ua);
|
||||
const isDesktop = !isMobile && !isTablet;
|
||||
|
||||
let deviceType = 'Desktop';
|
||||
if (isTablet) {
|
||||
deviceType = 'Tablet';
|
||||
} else if (isMobile) {
|
||||
deviceType = 'Mobile';
|
||||
}
|
||||
|
||||
return { deviceType, isMobile, isTablet, isDesktop };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações da tela
|
||||
*/
|
||||
function obterInformacoesTela(): { screenResolution: string; coresTela: number } {
|
||||
if (typeof screen === 'undefined') {
|
||||
return { screenResolution: 'Desconhecido', coresTela: 0 };
|
||||
}
|
||||
|
||||
const screenResolution = `${screen.width}x${screen.height}`;
|
||||
const coresTela = screen.colorDepth || 24;
|
||||
|
||||
return { screenResolution, coresTela };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações de conexão
|
||||
*/
|
||||
async function obterInformacoesConexao(): Promise<string> {
|
||||
if (typeof navigator === 'undefined' || !('connection' in navigator)) {
|
||||
return 'Desconhecido';
|
||||
}
|
||||
|
||||
const connection = (navigator as unknown as { connection?: { effectiveType?: string } }).connection;
|
||||
if (connection?.effectiveType) {
|
||||
return connection.effectiveType;
|
||||
}
|
||||
|
||||
return 'Desconhecido';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém informações de memória (se disponível)
|
||||
*/
|
||||
function obterInformacoesMemoria(): string {
|
||||
if (typeof navigator === 'undefined' || !('deviceMemory' in navigator)) {
|
||||
return 'Desconhecido';
|
||||
}
|
||||
|
||||
const deviceMemory = (navigator as unknown as { deviceMemory?: number }).deviceMemory;
|
||||
if (deviceMemory) {
|
||||
return `${deviceMemory} GB`;
|
||||
}
|
||||
|
||||
return 'Desconhecido';
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém localização via GPS
|
||||
*/
|
||||
async function obterLocalizacao(): Promise<{
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
precisao?: number;
|
||||
endereco?: string;
|
||||
cidade?: string;
|
||||
estado?: string;
|
||||
pais?: string;
|
||||
}> {
|
||||
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve({});
|
||||
}, 10000); // Timeout de 10 segundos
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (position) => {
|
||||
clearTimeout(timeout);
|
||||
const { latitude, longitude, accuracy } = position.coords;
|
||||
|
||||
// Tentar obter endereço via reverse geocoding
|
||||
let endereco = '';
|
||||
let cidade = '';
|
||||
let estado = '';
|
||||
let pais = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as {
|
||||
address?: {
|
||||
road?: string;
|
||||
house_number?: string;
|
||||
city?: string;
|
||||
town?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
};
|
||||
};
|
||||
if (data.address) {
|
||||
const addr = data.address;
|
||||
if (addr.road) {
|
||||
endereco = `${addr.road}${addr.house_number ? `, ${addr.house_number}` : ''}`;
|
||||
}
|
||||
cidade = addr.city || addr.town || '';
|
||||
estado = addr.state || '';
|
||||
pais = addr.country || '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Erro ao obter endereço:', error);
|
||||
}
|
||||
|
||||
resolve({
|
||||
latitude,
|
||||
longitude,
|
||||
precisao: accuracy,
|
||||
endereco,
|
||||
cidade,
|
||||
estado,
|
||||
pais,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timeout);
|
||||
console.warn('Erro ao obter localização:', error);
|
||||
resolve({});
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém IP público
|
||||
*/
|
||||
async function obterIPPublico(): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch('https://api.ipify.org?format=json');
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { ip: string };
|
||||
return data.ip;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Erro ao obter IP público:', error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém todas as informações do dispositivo
|
||||
*/
|
||||
export async function obterInformacoesDispositivo(): Promise<InformacoesDispositivo> {
|
||||
const informacoes: InformacoesDispositivo = {};
|
||||
|
||||
// Informações básicas
|
||||
if (typeof navigator !== 'undefined') {
|
||||
informacoes.userAgent = navigator.userAgent;
|
||||
informacoes.idioma = navigator.language || navigator.languages?.[0];
|
||||
informacoes.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
// Informações do navegador
|
||||
const navegador = detectarNavegador();
|
||||
informacoes.browser = navegador.browser;
|
||||
informacoes.browserVersion = navegador.browserVersion;
|
||||
informacoes.engine = navegador.engine;
|
||||
|
||||
// Informações do sistema
|
||||
const sistema = detectarSistemaOperacional();
|
||||
informacoes.sistemaOperacional = sistema.sistemaOperacional;
|
||||
informacoes.osVersion = sistema.osVersion;
|
||||
informacoes.arquitetura = sistema.arquitetura;
|
||||
informacoes.plataforma = sistema.plataforma;
|
||||
|
||||
// Tipo de dispositivo
|
||||
const dispositivo = detectarTipoDispositivo();
|
||||
informacoes.deviceType = dispositivo.deviceType;
|
||||
informacoes.isMobile = dispositivo.isMobile;
|
||||
informacoes.isTablet = dispositivo.isTablet;
|
||||
informacoes.isDesktop = dispositivo.isDesktop;
|
||||
|
||||
// Informações da tela
|
||||
const tela = obterInformacoesTela();
|
||||
informacoes.screenResolution = tela.screenResolution;
|
||||
informacoes.coresTela = tela.coresTela;
|
||||
|
||||
// Informações de conexão e memória (assíncronas)
|
||||
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao] = await Promise.all([
|
||||
obterInformacoesConexao(),
|
||||
Promise.resolve(obterInformacoesMemoria()),
|
||||
obterIPPublico(),
|
||||
getLocalIP(),
|
||||
obterLocalizacao(),
|
||||
]);
|
||||
|
||||
informacoes.connectionType = connectionType;
|
||||
informacoes.memoryInfo = memoryInfo;
|
||||
informacoes.ipPublico = ipPublico;
|
||||
informacoes.ipLocal = ipLocal;
|
||||
informacoes.latitude = localizacao.latitude;
|
||||
informacoes.longitude = localizacao.longitude;
|
||||
informacoes.precisao = localizacao.precisao;
|
||||
informacoes.endereco = localizacao.endereco;
|
||||
informacoes.cidade = localizacao.cidade;
|
||||
informacoes.estado = localizacao.estado;
|
||||
informacoes.pais = localizacao.pais;
|
||||
|
||||
// IP address (usar público se disponível, senão local)
|
||||
informacoes.ipAddress = ipPublico || ipLocal;
|
||||
|
||||
return informacoes;
|
||||
}
|
||||
|
||||
103
apps/web/src/lib/utils/ponto.ts
Normal file
103
apps/web/src/lib/utils/ponto.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Formata hora no formato HH:mm
|
||||
*/
|
||||
export function formatarHoraPonto(hora: number, minuto: number): string {
|
||||
return `${hora.toString().padStart(2, '0')}:${minuto.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata data e hora completa
|
||||
*/
|
||||
export function formatarDataHoraCompleta(
|
||||
data: string,
|
||||
hora: number,
|
||||
minuto: number,
|
||||
segundo: number
|
||||
): string {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula tempo trabalhado entre dois registros
|
||||
*/
|
||||
export function calcularTempoTrabalhado(
|
||||
horaInicio: number,
|
||||
minutoInicio: number,
|
||||
horaFim: number,
|
||||
minutoFim: number
|
||||
): { horas: number; minutos: number } {
|
||||
const minutosInicio = horaInicio * 60 + minutoInicio;
|
||||
const minutosFim = horaFim * 60 + minutoFim;
|
||||
const diferencaMinutos = minutosFim - minutosInicio;
|
||||
|
||||
if (diferencaMinutos < 0) {
|
||||
return { horas: 0, minutos: 0 };
|
||||
}
|
||||
|
||||
const horas = Math.floor(diferencaMinutos / 60);
|
||||
const minutos = diferencaMinutos % 60;
|
||||
|
||||
return { horas, minutos };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se está dentro do prazo baseado na configuração
|
||||
*/
|
||||
export function verificarDentroDoPrazo(
|
||||
hora: number,
|
||||
minuto: number,
|
||||
horarioConfigurado: string,
|
||||
toleranciaMinutos: number
|
||||
): boolean {
|
||||
const [horaConfig, minutoConfig] = horarioConfigurado.split(':').map(Number);
|
||||
const totalMinutosRegistro = hora * 60 + minuto;
|
||||
const totalMinutosConfigurado = horaConfig * 60 + minutoConfig;
|
||||
const diferenca = totalMinutosRegistro - totalMinutosConfigurado;
|
||||
return diferenca <= toleranciaMinutos && diferenca >= -toleranciaMinutos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém label do tipo de registro
|
||||
*/
|
||||
export function getTipoRegistroLabel(tipo: 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'): string {
|
||||
const labels: Record<string, string> = {
|
||||
entrada: 'Entrada',
|
||||
saida_almoco: 'Saída para Almoço',
|
||||
retorno_almoco: 'Retorno do Almoço',
|
||||
saida: 'Saída',
|
||||
};
|
||||
return labels[tipo] || tipo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém próximo tipo de registro esperado
|
||||
*/
|
||||
export function getProximoTipoRegistro(
|
||||
ultimoTipo: 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida' | null
|
||||
): 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida' {
|
||||
if (!ultimoTipo) {
|
||||
return 'entrada';
|
||||
}
|
||||
|
||||
switch (ultimoTipo) {
|
||||
case 'entrada':
|
||||
return 'saida_almoco';
|
||||
case 'saida_almoco':
|
||||
return 'retorno_almoco';
|
||||
case 'retorno_almoco':
|
||||
return 'saida';
|
||||
case 'saida':
|
||||
return 'entrada'; // Novo dia
|
||||
default:
|
||||
return 'entrada';
|
||||
}
|
||||
}
|
||||
|
||||
56
apps/web/src/lib/utils/sincronizacaoTempo.ts
Normal file
56
apps/web/src/lib/utils/sincronizacaoTempo.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { ConvexClient } from 'convex/browser';
|
||||
|
||||
/**
|
||||
* Obtém tempo do servidor (sincronizado)
|
||||
*/
|
||||
export async function obterTempoServidor(client: ConvexClient): Promise<number> {
|
||||
try {
|
||||
// Tentar obter configuração e sincronizar se necessário
|
||||
const config = await client.query(api.configuracaoRelogio.obterConfiguracao, {});
|
||||
|
||||
if (config.usarServidorExterno) {
|
||||
try {
|
||||
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {});
|
||||
if (resultado.sucesso && resultado.timestamp) {
|
||||
return resultado.timestamp;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Erro ao sincronizar com servidor externo:', error);
|
||||
if (config.fallbackParaPC) {
|
||||
return Date.now();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Usar tempo do servidor Convex
|
||||
const tempoServidor = await client.query(api.configuracaoRelogio.obterTempoServidor, {});
|
||||
return tempoServidor.timestamp;
|
||||
} catch (error) {
|
||||
console.warn('Erro ao obter tempo do servidor, usando tempo local:', error);
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém tempo do PC (fallback)
|
||||
*/
|
||||
export function obterTempoPC(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula offset entre dois timestamps
|
||||
*/
|
||||
export function calcularOffset(timestampServidor: number, timestampLocal: number): number {
|
||||
return timestampServidor - timestampLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica offset a um timestamp
|
||||
*/
|
||||
export function aplicarOffset(timestamp: number, offsetSegundos: number): number {
|
||||
return timestamp + offsetSegundos * 1000;
|
||||
}
|
||||
|
||||
150
apps/web/src/lib/utils/webcam.ts
Normal file
150
apps/web/src/lib/utils/webcam.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Verifica se webcam está disponível
|
||||
*/
|
||||
export async function validarWebcamDisponivel(): Promise<boolean> {
|
||||
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some((device) => device.kind === 'videoinput');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captura imagem da webcam
|
||||
*/
|
||||
export async function capturarWebcam(): Promise<Blob | null> {
|
||||
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let stream: MediaStream | null = null;
|
||||
|
||||
try {
|
||||
// Solicitar acesso à webcam
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
facingMode: 'user',
|
||||
},
|
||||
});
|
||||
|
||||
// Criar elemento de vídeo temporário
|
||||
const video = document.createElement('video');
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
|
||||
// Aguardar vídeo estar pronto
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
video.onloadedmetadata = () => {
|
||||
video.width = video.videoWidth;
|
||||
video.height = video.videoHeight;
|
||||
resolve();
|
||||
};
|
||||
video.onerror = reject;
|
||||
setTimeout(() => reject(new Error('Timeout ao carregar vídeo')), 5000);
|
||||
});
|
||||
|
||||
// Capturar frame
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Não foi possível obter contexto do canvas');
|
||||
}
|
||||
|
||||
ctx.drawImage(video, 0, 0);
|
||||
|
||||
// Converter para blob
|
||||
return await new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
resolve(blob);
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao capturar webcam:', error);
|
||||
return null;
|
||||
} finally {
|
||||
// Parar stream
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captura imagem da webcam com preview
|
||||
*/
|
||||
export async function capturarWebcamComPreview(
|
||||
videoElement: HTMLVideoElement,
|
||||
canvasElement: HTMLCanvasElement
|
||||
): Promise<Blob | null> {
|
||||
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let stream: MediaStream | null = null;
|
||||
|
||||
try {
|
||||
// Solicitar acesso à webcam
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
facingMode: 'user',
|
||||
},
|
||||
});
|
||||
|
||||
videoElement.srcObject = stream;
|
||||
await videoElement.play();
|
||||
|
||||
// Aguardar vídeo estar pronto
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
videoElement.onloadedmetadata = () => {
|
||||
canvasElement.width = videoElement.videoWidth;
|
||||
canvasElement.height = videoElement.videoHeight;
|
||||
resolve();
|
||||
};
|
||||
videoElement.onerror = reject;
|
||||
setTimeout(() => reject(new Error('Timeout ao carregar vídeo')), 5000);
|
||||
});
|
||||
|
||||
// Capturar frame
|
||||
const ctx = canvasElement.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Não foi possível obter contexto do canvas');
|
||||
}
|
||||
|
||||
ctx.drawImage(videoElement, 0, 0);
|
||||
|
||||
// Converter para blob
|
||||
return await new Promise<Blob | null>((resolve) => {
|
||||
canvasElement.toBlob(
|
||||
(blob) => {
|
||||
resolve(blob);
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao capturar webcam:', error);
|
||||
return null;
|
||||
} finally {
|
||||
// Parar stream
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user