Feat controle ponto #29
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { useQuery } from 'convex-svelte';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import { Printer, X } from 'lucide-svelte';
|
import { Printer, X } from 'lucide-svelte';
|
||||||
@@ -15,8 +14,8 @@
|
|||||||
|
|
||||||
let { registroId, onClose }: Props = $props();
|
let { registroId, onClose }: Props = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
|
||||||
const registroQuery = useQuery(api.pontos.obterRegistro, { registroId });
|
const registroQuery = useQuery(api.pontos.obterRegistro, { registroId });
|
||||||
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
|
|
||||||
let gerando = $state(false);
|
let gerando = $state(false);
|
||||||
|
|
||||||
@@ -98,7 +97,16 @@
|
|||||||
yPosition += 8;
|
yPosition += 8;
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
|
|
||||||
doc.text(`Tipo: ${getTipoRegistroLabel(registro.tipo)}`, 15, yPosition);
|
const config = configQuery?.data;
|
||||||
|
const tipoLabel = config
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo);
|
||||||
|
doc.text(`Tipo: ${tipoLabel}`, 15, yPosition);
|
||||||
yPosition += 6;
|
yPosition += 6;
|
||||||
|
|
||||||
const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo);
|
const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo);
|
||||||
@@ -118,30 +126,6 @@
|
|||||||
);
|
);
|
||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
// Detalhes de localização removidos do comprovante (serão exibidos apenas no relatório detalhado)
|
|
||||||
|
|
||||||
// Informações do Dispositivo (resumido)
|
|
||||||
if (registro.browser || registro.sistemaOperacional) {
|
|
||||||
doc.setFont('helvetica', 'bold');
|
|
||||||
doc.text('DISPOSITIVO', 15, yPosition);
|
|
||||||
doc.setFont('helvetica', 'normal');
|
|
||||||
yPosition += 8;
|
|
||||||
doc.setFontSize(10);
|
|
||||||
|
|
||||||
if (registro.browser) {
|
|
||||||
doc.text(`Navegador: ${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}`, 15, yPosition);
|
|
||||||
yPosition += 6;
|
|
||||||
}
|
|
||||||
if (registro.sistemaOperacional) {
|
|
||||||
doc.text(`Sistema: ${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}`, 15, yPosition);
|
|
||||||
yPosition += 6;
|
|
||||||
}
|
|
||||||
if (registro.ipAddress) {
|
|
||||||
doc.text(`IP: ${registro.ipAddress}`, 15, yPosition);
|
|
||||||
yPosition += 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imagem capturada (se disponível)
|
// Imagem capturada (se disponível)
|
||||||
if (registro.imagemUrl) {
|
if (registro.imagemUrl) {
|
||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
@@ -294,7 +278,17 @@
|
|||||||
<div class="card bg-base-200">
|
<div class="card bg-base-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="font-bold">Dados do Registro</h4>
|
<h4 class="font-bold">Dados do Registro</h4>
|
||||||
<p><strong>Tipo:</strong> {getTipoRegistroLabel(registro.tipo)}</p>
|
<p>
|
||||||
|
<strong>Tipo:</strong>
|
||||||
|
{configQuery?.data
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: configQuery.data.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: configQuery.data.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: configQuery.data.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: configQuery.data.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Data e Hora:</strong>
|
<strong>Data e Hora:</strong>
|
||||||
{formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo)}
|
{formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo)}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
// Estados
|
// Estados
|
||||||
let mostrandoWebcam = $state(false);
|
let mostrandoWebcam = $state(false);
|
||||||
let registrando = $state(false);
|
let registrando = $state(false);
|
||||||
let erro = $state<string | null>(null);
|
|
||||||
let sucesso = $state<string | null>(null);
|
let sucesso = $state<string | null>(null);
|
||||||
let registroId = $state<Id<'registrosPonto'> | null>(null);
|
let registroId = $state<Id<'registrosPonto'> | null>(null);
|
||||||
let mostrandoComprovante = $state(false);
|
let mostrandoComprovante = $state(false);
|
||||||
@@ -63,6 +62,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tipoLabel = $derived.by(() => {
|
const tipoLabel = $derived.by(() => {
|
||||||
|
if (config) {
|
||||||
|
return getTipoRegistroLabel(proximoTipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
});
|
||||||
|
}
|
||||||
return getTipoRegistroLabel(proximoTipo);
|
return getTipoRegistroLabel(proximoTipo);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,7 +160,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
registrando = true;
|
registrando = true;
|
||||||
erro = null;
|
|
||||||
sucesso = null;
|
sucesso = null;
|
||||||
coletandoInfo = true;
|
coletandoInfo = true;
|
||||||
|
|
||||||
@@ -189,7 +195,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
registroId = resultado.registroId;
|
registroId = resultado.registroId;
|
||||||
sucesso = `Ponto registrado com sucesso! Tipo: ${getTipoRegistroLabel(resultado.tipo)}`;
|
const tipoLabelSucesso = config
|
||||||
|
? getTipoRegistroLabel(resultado.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(resultado.tipo);
|
||||||
|
sucesso = `Ponto registrado com sucesso! Tipo: ${tipoLabelSucesso}`;
|
||||||
imagemCapturada = null;
|
imagemCapturada = null;
|
||||||
justificativa = ''; // Limpar justificativa após registro
|
justificativa = ''; // Limpar justificativa após registro
|
||||||
mostrandoModalConfirmacao = false;
|
mostrandoModalConfirmacao = false;
|
||||||
@@ -208,7 +222,15 @@
|
|||||||
mensagemErro.includes('já existe um registro')
|
mensagemErro.includes('já existe um registro')
|
||||||
) {
|
) {
|
||||||
mensagemErroModal = 'Registro de ponto duplicado';
|
mensagemErroModal = 'Registro de ponto duplicado';
|
||||||
detalhesErroModal = `Não é possível registrar o ponto no mesmo minuto.\n\nVocê já possui um registro de ${getTipoRegistroLabel(proximoTipo)} para este minuto.\n\nPor favor, aguarde pelo menos 1 minuto antes de tentar registrar novamente.`;
|
const tipoLabelErro = config
|
||||||
|
? getTipoRegistroLabel(proximoTipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(proximoTipo);
|
||||||
|
detalhesErroModal = `Não é possível registrar o ponto no mesmo minuto.\n\nVocê já possui um registro de ${tipoLabelErro} para este minuto.\n\nPor favor, aguarde pelo menos 1 minuto antes de tentar registrar novamente.`;
|
||||||
mostrarModalErro = true;
|
mostrarModalErro = true;
|
||||||
} else {
|
} else {
|
||||||
// Outros erros também mostram no modal
|
// Outros erros também mostram no modal
|
||||||
@@ -216,8 +238,6 @@
|
|||||||
detalhesErroModal = mensagemErro;
|
detalhesErroModal = mensagemErro;
|
||||||
mostrarModalErro = true;
|
mostrarModalErro = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
erro = mensagemErro;
|
|
||||||
} finally {
|
} finally {
|
||||||
registrando = false;
|
registrando = false;
|
||||||
coletandoInfo = false;
|
coletandoInfo = false;
|
||||||
@@ -309,7 +329,6 @@
|
|||||||
mostrarModalErro = false;
|
mostrarModalErro = false;
|
||||||
mensagemErroModal = '';
|
mensagemErroModal = '';
|
||||||
detalhesErroModal = '';
|
detalhesErroModal = '';
|
||||||
erro = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function imprimirComprovante(registroId: Id<'registrosPonto'>) {
|
async function imprimirComprovante(registroId: Id<'registrosPonto'>) {
|
||||||
@@ -322,6 +341,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buscar configuração para usar nomes personalizados
|
||||||
|
const configComprovante = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
|
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
|
|
||||||
// Logo
|
// Logo
|
||||||
@@ -385,7 +407,15 @@
|
|||||||
yPosition += 8;
|
yPosition += 8;
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
|
|
||||||
doc.text(`Tipo: ${getTipoRegistroLabel(registro.tipo)}`, 15, yPosition);
|
const tipoLabelComprovante = configComprovante
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: configComprovante.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: configComprovante.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: configComprovante.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: configComprovante.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo);
|
||||||
|
doc.text(`Tipo: ${tipoLabelComprovante}`, 15, yPosition);
|
||||||
yPosition += 6;
|
yPosition += 6;
|
||||||
|
|
||||||
const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo);
|
const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo);
|
||||||
@@ -578,10 +608,10 @@
|
|||||||
if (!config) return [];
|
if (!config) return [];
|
||||||
|
|
||||||
const horarios = [
|
const horarios = [
|
||||||
{ tipo: 'entrada', horario: config.horarioEntrada, label: 'Entrada' },
|
{ tipo: 'entrada', horario: config.horarioEntrada, label: config.nomeEntrada || 'Entrada 1' },
|
||||||
{ tipo: 'saida_almoco', horario: config.horarioSaidaAlmoco, label: 'Saída para Almoço' },
|
{ tipo: 'saida_almoco', horario: config.horarioSaidaAlmoco, label: config.nomeSaidaAlmoco || 'Saída 1' },
|
||||||
{ tipo: 'retorno_almoco', horario: config.horarioRetornoAlmoco, label: 'Retorno do Almoço' },
|
{ tipo: 'retorno_almoco', horario: config.horarioRetornoAlmoco, label: config.nomeRetornoAlmoco || 'Entrada 2' },
|
||||||
{ tipo: 'saida', horario: config.horarioSaida, label: 'Saída' }
|
{ tipo: 'saida', horario: config.horarioSaida, label: config.nomeSaida || 'Saída 2' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return horarios.map((h) => {
|
return horarios.map((h) => {
|
||||||
@@ -761,7 +791,16 @@
|
|||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="mb-1 flex items-center gap-2">
|
<div class="mb-1 flex items-center gap-2">
|
||||||
<span class="font-semibold">{getTipoRegistroLabel(registro.tipo)}</span>
|
<span class="font-semibold">
|
||||||
|
{config
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
|
</span>
|
||||||
{#if registro.dentroDoPrazo}
|
{#if registro.dentroDoPrazo}
|
||||||
<CheckCircle2 class="h-4 w-4 text-success" />
|
<CheckCircle2 class="h-4 w-4 text-success" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -66,13 +66,34 @@ export function verificarDentroDoPrazo(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtém label do tipo de registro
|
* Obtém label do tipo de registro
|
||||||
|
* Se config fornecida, usa os nomes personalizados, senão usa os padrões
|
||||||
*/
|
*/
|
||||||
export function getTipoRegistroLabel(tipo: 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'): string {
|
export function getTipoRegistroLabel(
|
||||||
|
tipo: 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida',
|
||||||
|
config?: {
|
||||||
|
nomeEntrada?: string;
|
||||||
|
nomeSaidaAlmoco?: string;
|
||||||
|
nomeRetornoAlmoco?: string;
|
||||||
|
nomeSaida?: string;
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
// Se config fornecida, usar nomes personalizados
|
||||||
|
if (config) {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
entrada: 'Entrada',
|
entrada: config.nomeEntrada || 'Entrada 1',
|
||||||
saida_almoco: 'Saída para Almoço',
|
saida_almoco: config.nomeSaidaAlmoco || 'Saída 1',
|
||||||
retorno_almoco: 'Retorno do Almoço',
|
retorno_almoco: config.nomeRetornoAlmoco || 'Entrada 2',
|
||||||
saida: 'Saída',
|
saida: config.nomeSaida || 'Saída 2',
|
||||||
|
};
|
||||||
|
return labels[tipo] || tipo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valores padrão
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
entrada: 'Entrada 1',
|
||||||
|
saida_almoco: 'Saída 1',
|
||||||
|
retorno_almoco: 'Entrada 2',
|
||||||
|
saida: 'Saída 2',
|
||||||
};
|
};
|
||||||
return labels[tipo] || tipo;
|
return labels[tipo] || tipo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@
|
|||||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||||
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, registrosParams);
|
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, registrosParams);
|
||||||
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, estatisticasParams);
|
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, estatisticasParams);
|
||||||
|
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
|
|
||||||
const funcionarios = $derived(funcionariosQuery?.data || []);
|
const funcionarios = $derived(funcionariosQuery?.data || []);
|
||||||
const registros = $derived(registrosQuery?.data || []);
|
const registros = $derived(registrosQuery?.data || []);
|
||||||
const estatisticas = $derived(estatisticasQuery?.data);
|
const estatisticas = $derived(estatisticasQuery?.data);
|
||||||
|
const config = $derived(configQuery?.data);
|
||||||
|
|
||||||
// Agrupar registros por funcionário e data
|
// Agrupar registros por funcionário e data
|
||||||
const registrosAgrupados = $derived.by(() => {
|
const registrosAgrupados = $derived.by(() => {
|
||||||
@@ -146,9 +148,17 @@
|
|||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
// Tabela de registros
|
// Tabela de registros
|
||||||
|
const config = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
const tableData = registrosFuncionario.map((r) => [
|
const tableData = registrosFuncionario.map((r) => [
|
||||||
r.data,
|
r.data,
|
||||||
getTipoRegistroLabel(r.tipo),
|
config
|
||||||
|
? getTipoRegistroLabel(r.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(r.tipo),
|
||||||
formatarHoraPonto(r.hora, r.minuto),
|
formatarHoraPonto(r.hora, r.minuto),
|
||||||
r.dentroDoPrazo ? 'Sim' : 'Não',
|
r.dentroDoPrazo ? 'Sim' : 'Não',
|
||||||
]);
|
]);
|
||||||
@@ -258,7 +268,16 @@
|
|||||||
yPosition += 8;
|
yPosition += 8;
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
|
|
||||||
doc.text(`Tipo: ${getTipoRegistroLabel(registro.tipo)}`, 15, yPosition);
|
const config = await client.query(api.configuracaoPonto.obterConfiguracao, {});
|
||||||
|
const tipoLabel = config
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo);
|
||||||
|
doc.text(`Tipo: ${tipoLabel}`, 15, yPosition);
|
||||||
yPosition += 6;
|
yPosition += 6;
|
||||||
|
|
||||||
const dataHora = `${registro.data} ${registro.hora.toString().padStart(2, '0')}:${registro.minuto.toString().padStart(2, '0')}:${registro.segundo.toString().padStart(2, '0')}`;
|
const dataHora = `${registro.data} ${registro.hora.toString().padStart(2, '0')}:${registro.minuto.toString().padStart(2, '0')}:${registro.segundo.toString().padStart(2, '0')}`;
|
||||||
@@ -784,7 +803,16 @@
|
|||||||
{#each grupo.registros as registro}
|
{#each grupo.registros as registro}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{registro.data}</td>
|
<td>{registro.data}</td>
|
||||||
<td>{getTipoRegistroLabel(registro.tipo)}</td>
|
<td>
|
||||||
|
{config
|
||||||
|
? getTipoRegistroLabel(registro.tipo, {
|
||||||
|
nomeEntrada: config.nomeEntrada,
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
|
||||||
|
nomeSaida: config.nomeSaida,
|
||||||
|
})
|
||||||
|
: getTipoRegistroLabel(registro.tipo)}
|
||||||
|
</td>
|
||||||
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
let horarioRetornoAlmoco = $state('13:00');
|
let horarioRetornoAlmoco = $state('13:00');
|
||||||
let horarioSaida = $state('17:00');
|
let horarioSaida = $state('17:00');
|
||||||
let toleranciaMinutos = $state(15);
|
let toleranciaMinutos = $state(15);
|
||||||
|
let nomeEntrada = $state('Entrada 1');
|
||||||
|
let nomeSaidaAlmoco = $state('Saída 1');
|
||||||
|
let nomeRetornoAlmoco = $state('Entrada 2');
|
||||||
|
let nomeSaida = $state('Saída 2');
|
||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
||||||
|
|
||||||
@@ -21,6 +25,10 @@
|
|||||||
horarioRetornoAlmoco = configQuery.data.horarioRetornoAlmoco;
|
horarioRetornoAlmoco = configQuery.data.horarioRetornoAlmoco;
|
||||||
horarioSaida = configQuery.data.horarioSaida;
|
horarioSaida = configQuery.data.horarioSaida;
|
||||||
toleranciaMinutos = configQuery.data.toleranciaMinutos;
|
toleranciaMinutos = configQuery.data.toleranciaMinutos;
|
||||||
|
nomeEntrada = configQuery.data.nomeEntrada || 'Entrada 1';
|
||||||
|
nomeSaidaAlmoco = configQuery.data.nomeSaidaAlmoco || 'Saída 1';
|
||||||
|
nomeRetornoAlmoco = configQuery.data.nomeRetornoAlmoco || 'Entrada 2';
|
||||||
|
nomeSaida = configQuery.data.nomeSaida || 'Saída 2';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,6 +51,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validação dos nomes
|
||||||
|
if (!nomeEntrada.trim() || !nomeSaidaAlmoco.trim() || !nomeRetornoAlmoco.trim() || !nomeSaida.trim()) {
|
||||||
|
mostrarMensagem('error', 'Preencha todos os nomes dos registros');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processando = true;
|
processando = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.configuracaoPonto.salvarConfiguracao, {
|
await client.mutation(api.configuracaoPonto.salvarConfiguracao, {
|
||||||
@@ -51,6 +65,10 @@
|
|||||||
horarioRetornoAlmoco,
|
horarioRetornoAlmoco,
|
||||||
horarioSaida,
|
horarioSaida,
|
||||||
toleranciaMinutos,
|
toleranciaMinutos,
|
||||||
|
nomeEntrada: nomeEntrada.trim(),
|
||||||
|
nomeSaidaAlmoco: nomeSaidaAlmoco.trim(),
|
||||||
|
nomeRetornoAlmoco: nomeRetornoAlmoco.trim(),
|
||||||
|
nomeSaida: nomeSaida.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
||||||
@@ -170,6 +188,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Nomes Personalizados dos Registros -->
|
||||||
|
<h2 class="card-title mb-4">Nomes dos Registros</h2>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
Personalize os nomes exibidos para cada tipo de registro de ponto
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- Nome Entrada -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nome-entrada">
|
||||||
|
<span class="label-text font-medium">Nome do Registro de Entrada *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nome-entrada"
|
||||||
|
type="text"
|
||||||
|
bind:value={nomeEntrada}
|
||||||
|
placeholder="Ex: Entrada 1"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text-alt">Nome exibido para o primeiro registro do dia</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nome Saída Almoço -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nome-saida-almoco">
|
||||||
|
<span class="label-text font-medium">Nome do Registro de Saída para Almoço *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nome-saida-almoco"
|
||||||
|
type="text"
|
||||||
|
bind:value={nomeSaidaAlmoco}
|
||||||
|
placeholder="Ex: Saída 1"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text-alt">Nome exibido para a saída para almoço</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nome Retorno Almoço -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nome-retorno-almoco">
|
||||||
|
<span class="label-text font-medium">Nome do Registro de Retorno do Almoço *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nome-retorno-almoco"
|
||||||
|
type="text"
|
||||||
|
bind:value={nomeRetornoAlmoco}
|
||||||
|
placeholder="Ex: Entrada 2"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text-alt">Nome exibido para o retorno do almoço</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nome Saída -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nome-saida">
|
||||||
|
<span class="label-text font-medium">Nome do Registro de Saída *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nome-saida"
|
||||||
|
type="text"
|
||||||
|
bind:value={nomeSaida}
|
||||||
|
placeholder="Ex: Saída 2"
|
||||||
|
class="input input-bordered"
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text-alt">Nome exibido para a saída final do dia</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ações -->
|
<!-- Ações -->
|
||||||
<div class="card-actions justify-end mt-6">
|
<div class="card-actions justify-end mt-6">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
let portaNTP = $state(123);
|
let portaNTP = $state(123);
|
||||||
let usarServidorExterno = $state(false);
|
let usarServidorExterno = $state(false);
|
||||||
let fallbackParaPC = $state(true);
|
let fallbackParaPC = $state(true);
|
||||||
|
let gmtOffset = $state(0);
|
||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
let testando = $state(false);
|
let testando = $state(false);
|
||||||
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
portaNTP = configQuery.data.portaNTP || 123;
|
portaNTP = configQuery.data.portaNTP || 123;
|
||||||
usarServidorExterno = configQuery.data.usarServidorExterno || false;
|
usarServidorExterno = configQuery.data.usarServidorExterno || false;
|
||||||
fallbackParaPC = configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
|
fallbackParaPC = configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
|
||||||
|
gmtOffset = configQuery.data.gmtOffset ?? 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
portaNTP: usarServidorExterno ? portaNTP : undefined,
|
portaNTP: usarServidorExterno ? portaNTP : undefined,
|
||||||
usarServidorExterno,
|
usarServidorExterno,
|
||||||
fallbackParaPC,
|
fallbackParaPC,
|
||||||
|
gmtOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
mostrarMensagem('success', 'Configuração salva com sucesso!');
|
||||||
@@ -194,6 +197,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Ajuste de Fuso Horário (GMT) -->
|
||||||
|
<h2 class="card-title mb-4">Ajuste de Fuso Horário (GMT)</h2>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
Configure o fuso horário para ajustar o horário de registro. Use valores negativos para fusos a oeste de UTC e positivos para fusos a leste.
|
||||||
|
</p>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="gmt-offset">
|
||||||
|
<span class="label-text font-medium">GMT Offset (horas) *</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="gmt-offset"
|
||||||
|
bind:value={gmtOffset}
|
||||||
|
class="select select-bordered"
|
||||||
|
>
|
||||||
|
{#each Array.from({ length: 49 }, (_, i) => i - 12) as offset}
|
||||||
|
<option value={offset} selected={gmtOffset === offset}>
|
||||||
|
GMT{offset >= 0 ? '+' : ''}{offset}{offset === -3 ? ' (Brasil - Brasília)' : offset === 0 ? ' (UTC)' : ''}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text-alt"
|
||||||
|
>Ajuste em horas em relação ao UTC. Exemplo: -3 para horário de Brasília (GMT-3)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ações -->
|
<!-- Ações -->
|
||||||
<div class="card-actions justify-end mt-6 gap-3">
|
<div class="card-actions justify-end mt-6 gap-3">
|
||||||
{#if usarServidorExterno}
|
{#if usarServidorExterno}
|
||||||
|
|||||||
@@ -30,11 +30,22 @@ export const obterConfiguracao = query({
|
|||||||
horarioRetornoAlmoco: '13:00',
|
horarioRetornoAlmoco: '13:00',
|
||||||
horarioSaida: '17:00',
|
horarioSaida: '17:00',
|
||||||
toleranciaMinutos: 15,
|
toleranciaMinutos: 15,
|
||||||
|
nomeEntrada: 'Entrada 1',
|
||||||
|
nomeSaidaAlmoco: 'Saída 1',
|
||||||
|
nomeRetornoAlmoco: 'Entrada 2',
|
||||||
|
nomeSaida: 'Saída 2',
|
||||||
ativo: false,
|
ativo: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
// Garantir que os nomes padrão estejam definidos
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
nomeEntrada: config.nomeEntrada || 'Entrada 1',
|
||||||
|
nomeSaidaAlmoco: config.nomeSaidaAlmoco || 'Saída 1',
|
||||||
|
nomeRetornoAlmoco: config.nomeRetornoAlmoco || 'Entrada 2',
|
||||||
|
nomeSaida: config.nomeSaida || 'Saída 2',
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,6 +59,10 @@ export const salvarConfiguracao = mutation({
|
|||||||
horarioRetornoAlmoco: v.string(),
|
horarioRetornoAlmoco: v.string(),
|
||||||
horarioSaida: v.string(),
|
horarioSaida: v.string(),
|
||||||
toleranciaMinutos: v.number(),
|
toleranciaMinutos: v.number(),
|
||||||
|
nomeEntrada: v.optional(v.string()),
|
||||||
|
nomeSaidaAlmoco: v.optional(v.string()),
|
||||||
|
nomeRetornoAlmoco: v.optional(v.string()),
|
||||||
|
nomeSaida: v.optional(v.string()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
@@ -115,6 +130,10 @@ export const salvarConfiguracao = mutation({
|
|||||||
horarioRetornoAlmoco: args.horarioRetornoAlmoco,
|
horarioRetornoAlmoco: args.horarioRetornoAlmoco,
|
||||||
horarioSaida: args.horarioSaida,
|
horarioSaida: args.horarioSaida,
|
||||||
toleranciaMinutos: args.toleranciaMinutos,
|
toleranciaMinutos: args.toleranciaMinutos,
|
||||||
|
nomeEntrada: args.nomeEntrada || 'Entrada 1',
|
||||||
|
nomeSaidaAlmoco: args.nomeSaidaAlmoco || 'Saída 1',
|
||||||
|
nomeRetornoAlmoco: args.nomeRetornoAlmoco || 'Entrada 2',
|
||||||
|
nomeSaida: args.nomeSaida || 'Saída 2',
|
||||||
ativo: true,
|
ativo: true,
|
||||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||||
atualizadoEm: Date.now(),
|
atualizadoEm: Date.now(),
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ export const obterConfiguracao = query({
|
|||||||
fallbackParaPC: true,
|
fallbackParaPC: true,
|
||||||
ultimaSincronizacao: null,
|
ultimaSincronizacao: null,
|
||||||
offsetSegundos: null,
|
offsetSegundos: null,
|
||||||
|
gmtOffset: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return {
|
||||||
|
...config,
|
||||||
|
gmtOffset: config.gmtOffset ?? 0,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,6 +44,7 @@ export const salvarConfiguracao = mutation({
|
|||||||
portaNTP: v.optional(v.number()),
|
portaNTP: v.optional(v.number()),
|
||||||
usarServidorExterno: v.boolean(),
|
usarServidorExterno: v.boolean(),
|
||||||
fallbackParaPC: v.boolean(),
|
fallbackParaPC: v.boolean(),
|
||||||
|
gmtOffset: v.optional(v.number()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
@@ -72,6 +77,7 @@ export const salvarConfiguracao = mutation({
|
|||||||
portaNTP: args.portaNTP,
|
portaNTP: args.portaNTP,
|
||||||
usarServidorExterno: args.usarServidorExterno,
|
usarServidorExterno: args.usarServidorExterno,
|
||||||
fallbackParaPC: args.fallbackParaPC,
|
fallbackParaPC: args.fallbackParaPC,
|
||||||
|
gmtOffset: args.gmtOffset ?? 0,
|
||||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||||
atualizadoEm: Date.now(),
|
atualizadoEm: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -83,6 +89,7 @@ export const salvarConfiguracao = mutation({
|
|||||||
portaNTP: args.portaNTP,
|
portaNTP: args.portaNTP,
|
||||||
usarServidorExterno: args.usarServidorExterno,
|
usarServidorExterno: args.usarServidorExterno,
|
||||||
fallbackParaPC: args.fallbackParaPC,
|
fallbackParaPC: args.fallbackParaPC,
|
||||||
|
gmtOffset: args.gmtOffset ?? 0,
|
||||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||||
atualizadoEm: Date.now(),
|
atualizadoEm: Date.now(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { internalMutation, mutation, query } from './_generated/server';
|
import { mutation, query } from './_generated/server';
|
||||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
import type { Id } from './_generated/dataModel';
|
import type { Id } from './_generated/dataModel';
|
||||||
@@ -18,38 +18,6 @@ export const generateUploadUrl = mutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcula se o registro está dentro do prazo baseado na configuração
|
* Calcula se o registro está dentro do prazo baseado na configuração
|
||||||
* Se toleranciaMinutos for 0, desconsidera atrasos (sempre retorna true)
|
* Se toleranciaMinutos for 0, desconsidera atrasos (sempre retorna true)
|
||||||
@@ -175,12 +143,20 @@ export const registrarPonto = mutation({
|
|||||||
throw new Error('Configuração de ponto não encontrada');
|
throw new Error('Configuração de ponto não encontrada');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converter timestamp para data/hora
|
// Obter configuração de ponto para GMT offset (buscar configuração ativa)
|
||||||
const dataObj = new Date(args.timestamp);
|
const configPonto = await ctx.db
|
||||||
|
.query('configuracaoPonto')
|
||||||
|
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
// Converter timestamp para data/hora com ajuste de GMT
|
||||||
|
const gmtOffset = configPonto?.gmtOffset ?? 0;
|
||||||
|
const timestampAjustado = args.timestamp + (gmtOffset * 60 * 60 * 1000);
|
||||||
|
const dataObj = new Date(timestampAjustado);
|
||||||
const data = dataObj.toISOString().split('T')[0]!; // YYYY-MM-DD
|
const data = dataObj.toISOString().split('T')[0]!; // YYYY-MM-DD
|
||||||
const hora = dataObj.getHours();
|
const hora = dataObj.getUTCHours();
|
||||||
const minuto = dataObj.getMinutes();
|
const minuto = dataObj.getUTCMinutes();
|
||||||
const segundo = dataObj.getSeconds();
|
const segundo = dataObj.getUTCSeconds();
|
||||||
|
|
||||||
// Verificar se já existe registro no mesmo minuto
|
// Verificar se já existe registro no mesmo minuto
|
||||||
const funcionarioId = usuario.funcionarioId; // Já verificado acima, não é undefined
|
const funcionarioId = usuario.funcionarioId; // Já verificado acima, não é undefined
|
||||||
@@ -317,7 +293,6 @@ export const listarRegistrosPeriodo = query({
|
|||||||
// Por enquanto, permitir se tiver funcionarioId ou for admin
|
// Por enquanto, permitir se tiver funcionarioId ou for admin
|
||||||
// TODO: Implementar verificação de permissão adequada
|
// TODO: Implementar verificação de permissão adequada
|
||||||
|
|
||||||
const dataInicio = new Date(args.dataInicio);
|
|
||||||
const dataFim = new Date(args.dataFim);
|
const dataFim = new Date(args.dataFim);
|
||||||
dataFim.setHours(23, 59, 59, 999);
|
dataFim.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
|||||||
@@ -1403,6 +1403,11 @@ export default defineSchema({
|
|||||||
horarioRetornoAlmoco: v.string(), // HH:mm
|
horarioRetornoAlmoco: v.string(), // HH:mm
|
||||||
horarioSaida: v.string(), // HH:mm
|
horarioSaida: v.string(), // HH:mm
|
||||||
toleranciaMinutos: v.number(),
|
toleranciaMinutos: v.number(),
|
||||||
|
// Nomes personalizados dos tipos de registro
|
||||||
|
nomeEntrada: v.optional(v.string()), // Padrão: "Entrada 1"
|
||||||
|
nomeSaidaAlmoco: v.optional(v.string()), // Padrão: "Saída 1"
|
||||||
|
nomeRetornoAlmoco: v.optional(v.string()), // Padrão: "Entrada 2"
|
||||||
|
nomeSaida: v.optional(v.string()), // Padrão: "Saída 2"
|
||||||
ativo: v.boolean(),
|
ativo: v.boolean(),
|
||||||
atualizadoPor: v.id("usuarios"),
|
atualizadoPor: v.id("usuarios"),
|
||||||
atualizadoEm: v.number(),
|
atualizadoEm: v.number(),
|
||||||
@@ -1416,6 +1421,8 @@ export default defineSchema({
|
|||||||
fallbackParaPC: v.boolean(),
|
fallbackParaPC: v.boolean(),
|
||||||
ultimaSincronizacao: v.optional(v.number()),
|
ultimaSincronizacao: v.optional(v.number()),
|
||||||
offsetSegundos: v.optional(v.number()),
|
offsetSegundos: v.optional(v.number()),
|
||||||
|
// Ajuste de fuso horário (GMT offset em horas)
|
||||||
|
gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
|
||||||
atualizadoPor: v.id("usuarios"),
|
atualizadoPor: v.id("usuarios"),
|
||||||
atualizadoEm: v.number(),
|
atualizadoEm: v.number(),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user