Merge remote-tracking branch 'origin' into feat-licitacoes-contratos
This commit is contained in:
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -37,6 +37,7 @@ import type * as logsAtividades from "../logsAtividades.js";
|
||||
import type * as logsLogin from "../logsLogin.js";
|
||||
import type * as monitoramento from "../monitoramento.js";
|
||||
import type * as permissoesAcoes from "../permissoesAcoes.js";
|
||||
import type * as pontos from "../pontos.js";
|
||||
import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
|
||||
import type * as pushNotifications from "../pushNotifications.js";
|
||||
import type * as roles from "../roles.js";
|
||||
@@ -73,6 +74,8 @@ declare const fullApi: ApiFromModules<{
|
||||
chat: typeof chat;
|
||||
configuracaoEmail: typeof configuracaoEmail;
|
||||
contratos: typeof contratos;
|
||||
configuracaoPonto: typeof configuracaoPonto;
|
||||
configuracaoRelogio: typeof configuracaoRelogio;
|
||||
crons: typeof crons;
|
||||
cursos: typeof cursos;
|
||||
dashboard: typeof dashboard;
|
||||
@@ -88,6 +91,7 @@ declare const fullApi: ApiFromModules<{
|
||||
logsLogin: typeof logsLogin;
|
||||
monitoramento: typeof monitoramento;
|
||||
permissoesAcoes: typeof permissoesAcoes;
|
||||
pontos: typeof pontos;
|
||||
preferenciasNotificacao: typeof preferenciasNotificacao;
|
||||
pushNotifications: typeof pushNotifications;
|
||||
roles: typeof roles;
|
||||
|
||||
145
packages/backend/convex/configuracaoPonto.ts
Normal file
145
packages/backend/convex/configuracaoPonto.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
|
||||
/**
|
||||
* Valida formato de horário HH:mm
|
||||
*/
|
||||
function validarHorario(horario: string): boolean {
|
||||
const regex = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/;
|
||||
return regex.test(horario);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém a configuração ativa de ponto
|
||||
*/
|
||||
export const obterConfiguracao = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
// Retornar configuração padrão se não houver
|
||||
return {
|
||||
horarioEntrada: '08:00',
|
||||
horarioSaidaAlmoco: '12:00',
|
||||
horarioRetornoAlmoco: '13:00',
|
||||
horarioSaida: '17:00',
|
||||
toleranciaMinutos: 15,
|
||||
nomeEntrada: 'Entrada 1',
|
||||
nomeSaidaAlmoco: 'Saída 1',
|
||||
nomeRetornoAlmoco: 'Entrada 2',
|
||||
nomeSaida: 'Saída 2',
|
||||
ativo: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 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',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Salva configuração de ponto (apenas TI)
|
||||
*/
|
||||
export const salvarConfiguracao = mutation({
|
||||
args: {
|
||||
horarioEntrada: v.string(),
|
||||
horarioSaidaAlmoco: v.string(),
|
||||
horarioRetornoAlmoco: v.string(),
|
||||
horarioSaida: v.string(),
|
||||
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) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// TODO: Verificar se usuário tem permissão de TI
|
||||
// Por enquanto, permitir se tiver roleId
|
||||
|
||||
// Validar horários
|
||||
if (!validarHorario(args.horarioEntrada)) {
|
||||
throw new Error('Horário de entrada inválido (formato: HH:mm)');
|
||||
}
|
||||
if (!validarHorario(args.horarioSaidaAlmoco)) {
|
||||
throw new Error('Horário de saída para almoço inválido (formato: HH:mm)');
|
||||
}
|
||||
if (!validarHorario(args.horarioRetornoAlmoco)) {
|
||||
throw new Error('Horário de retorno do almoço inválido (formato: HH:mm)');
|
||||
}
|
||||
if (!validarHorario(args.horarioSaida)) {
|
||||
throw new Error('Horário de saída inválido (formato: HH:mm)');
|
||||
}
|
||||
|
||||
// Validar tolerância
|
||||
if (args.toleranciaMinutos < 0 || args.toleranciaMinutos > 60) {
|
||||
throw new Error('Tolerância deve estar entre 0 e 60 minutos');
|
||||
}
|
||||
|
||||
// Validar sequência lógica de horários
|
||||
const [horaEntrada, minutoEntrada] = args.horarioEntrada.split(':').map(Number);
|
||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = args.horarioSaidaAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = args.horarioRetornoAlmoco.split(':').map(Number);
|
||||
const [horaSaida, minutoSaida] = args.horarioSaida.split(':').map(Number);
|
||||
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
const minutosSaidaAlmoco = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
||||
const minutosRetornoAlmoco = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
||||
const minutosSaida = horaSaida * 60 + minutoSaida;
|
||||
|
||||
if (minutosEntrada >= minutosSaidaAlmoco) {
|
||||
throw new Error('Horário de entrada deve ser anterior à saída para almoço');
|
||||
}
|
||||
if (minutosSaidaAlmoco >= minutosRetornoAlmoco) {
|
||||
throw new Error('Horário de saída para almoço deve ser anterior ao retorno');
|
||||
}
|
||||
if (minutosRetornoAlmoco >= minutosSaida) {
|
||||
throw new Error('Horário de retorno do almoço deve ser anterior à saída');
|
||||
}
|
||||
|
||||
// Desativar configurações antigas
|
||||
const configsAntigas = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.collect();
|
||||
|
||||
for (const configAntiga of configsAntigas) {
|
||||
await ctx.db.patch(configAntiga._id, { ativo: false });
|
||||
}
|
||||
|
||||
// Criar nova configuração
|
||||
const configId = await ctx.db.insert('configuracaoPonto', {
|
||||
horarioEntrada: args.horarioEntrada,
|
||||
horarioSaidaAlmoco: args.horarioSaidaAlmoco,
|
||||
horarioRetornoAlmoco: args.horarioRetornoAlmoco,
|
||||
horarioSaida: args.horarioSaida,
|
||||
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,
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
return { configId };
|
||||
},
|
||||
});
|
||||
|
||||
209
packages/backend/convex/configuracaoRelogio.ts
Normal file
209
packages/backend/convex/configuracaoRelogio.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { v } from 'convex/values';
|
||||
import { action, internalMutation, internalQuery, mutation, query } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import { api, internal } from './_generated/api';
|
||||
|
||||
/**
|
||||
* Obtém a configuração do relógio
|
||||
*/
|
||||
export const obterConfiguracao = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const config = await ctx.db
|
||||
.query('configuracaoRelogio')
|
||||
.withIndex('by_ativo', (q) => q.eq('usarServidorExterno', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
// Retornar configuração padrão
|
||||
return {
|
||||
servidorNTP: 'pool.ntp.org',
|
||||
portaNTP: 123,
|
||||
usarServidorExterno: false,
|
||||
fallbackParaPC: true,
|
||||
ultimaSincronizacao: null,
|
||||
offsetSegundos: null,
|
||||
gmtOffset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
gmtOffset: config.gmtOffset ?? 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Salva configuração do relógio (apenas TI)
|
||||
*/
|
||||
export const salvarConfiguracao = mutation({
|
||||
args: {
|
||||
servidorNTP: v.optional(v.string()),
|
||||
portaNTP: v.optional(v.number()),
|
||||
usarServidorExterno: v.boolean(),
|
||||
fallbackParaPC: v.boolean(),
|
||||
gmtOffset: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// TODO: Verificar se usuário tem permissão de TI
|
||||
|
||||
// Validar servidor NTP se usar servidor externo
|
||||
if (args.usarServidorExterno) {
|
||||
if (!args.servidorNTP || args.servidorNTP.trim() === '') {
|
||||
throw new Error('Servidor NTP é obrigatório quando usar servidor externo');
|
||||
}
|
||||
if (!args.portaNTP || args.portaNTP < 1 || args.portaNTP > 65535) {
|
||||
throw new Error('Porta NTP deve estar entre 1 e 65535');
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar configuração existente
|
||||
const configExistente = await ctx.db
|
||||
.query('configuracaoRelogio')
|
||||
.withIndex('by_ativo', (q) => q.eq('usarServidorExterno', args.usarServidorExterno))
|
||||
.first();
|
||||
|
||||
if (configExistente) {
|
||||
// Atualizar configuração existente
|
||||
await ctx.db.patch(configExistente._id, {
|
||||
servidorNTP: args.servidorNTP,
|
||||
portaNTP: args.portaNTP,
|
||||
usarServidorExterno: args.usarServidorExterno,
|
||||
fallbackParaPC: args.fallbackParaPC,
|
||||
gmtOffset: args.gmtOffset ?? 0,
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return { configId: configExistente._id };
|
||||
} else {
|
||||
// Criar nova configuração
|
||||
const configId = await ctx.db.insert('configuracaoRelogio', {
|
||||
servidorNTP: args.servidorNTP,
|
||||
portaNTP: args.portaNTP,
|
||||
usarServidorExterno: args.usarServidorExterno,
|
||||
fallbackParaPC: args.fallbackParaPC,
|
||||
gmtOffset: args.gmtOffset ?? 0,
|
||||
atualizadoPor: usuario._id as Id<'usuarios'>,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
return { configId };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém tempo do servidor (timestamp atual)
|
||||
*/
|
||||
export const obterTempoServidor = query({
|
||||
args: {},
|
||||
handler: async () => {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
data: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Sincroniza tempo com servidor NTP (via action)
|
||||
* Nota: NTP real requer biblioteca específica, aqui fazemos uma aproximação
|
||||
*/
|
||||
export const sincronizarTempo = action({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar configuração diretamente do banco usando query pública
|
||||
const config = await ctx.runQuery(api.configuracaoRelogio.obterConfiguracao, {});
|
||||
|
||||
if (!config.usarServidorExterno) {
|
||||
return {
|
||||
sucesso: true,
|
||||
timestamp: Date.now(),
|
||||
usandoServidorExterno: false,
|
||||
offsetSegundos: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Tentar obter tempo de um servidor NTP público via HTTP
|
||||
// Nota: Esta é uma aproximação. Para NTP real, seria necessário usar uma biblioteca específica
|
||||
try {
|
||||
// Usar API pública de tempo como fallback
|
||||
const response = await fetch('https://worldtimeapi.org/api/timezone/America/Recife');
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao obter tempo do servidor');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { datetime: string };
|
||||
const serverTime = new Date(data.datetime).getTime();
|
||||
const localTime = Date.now();
|
||||
const offsetSegundos = Math.floor((serverTime - localTime) / 1000);
|
||||
|
||||
// Atualizar configuração com offset
|
||||
// Buscar configuração diretamente usando query interna
|
||||
const configs = await ctx.runQuery(internal.configuracaoRelogio.listarConfiguracoes, {});
|
||||
const configExistente = configs.find(
|
||||
(c: { usarServidorExterno: boolean; _id: Id<'configuracaoRelogio'> }) =>
|
||||
c.usarServidorExterno === config.usarServidorExterno
|
||||
);
|
||||
if (configExistente) {
|
||||
await ctx.runMutation(internal.configuracaoRelogio.atualizarSincronizacao, {
|
||||
configId: configExistente._id,
|
||||
offsetSegundos,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sucesso: true,
|
||||
timestamp: serverTime,
|
||||
usandoServidorExterno: true,
|
||||
offsetSegundos,
|
||||
};
|
||||
} catch {
|
||||
// Se falhar e fallbackParaPC estiver ativo, usar tempo local
|
||||
if (config.fallbackParaPC) {
|
||||
return {
|
||||
sucesso: true,
|
||||
timestamp: Date.now(),
|
||||
usandoServidorExterno: false,
|
||||
offsetSegundos: 0,
|
||||
aviso: 'Falha ao sincronizar com servidor externo, usando relógio do PC',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Falha ao sincronizar tempo e fallback desabilitado');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista configurações (internal)
|
||||
*/
|
||||
export const listarConfiguracoes = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query('configuracaoRelogio').collect();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Atualiza informações de sincronização (internal)
|
||||
*/
|
||||
export const atualizarSincronizacao = internalMutation({
|
||||
args: {
|
||||
configId: v.id('configuracaoRelogio'),
|
||||
offsetSegundos: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.configId, {
|
||||
ultimaSincronizacao: Date.now(),
|
||||
offsetSegundos: args.offsetSegundos,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
660
packages/backend/convex/pontos.ts
Normal file
660
packages/backend/convex/pontos.ts
Normal file
@@ -0,0 +1,660 @@
|
||||
import { v } from 'convex/values';
|
||||
import { mutation, query } from './_generated/server';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
|
||||
/**
|
||||
* Gera URL para upload de imagem do ponto
|
||||
*/
|
||||
export const generateUploadUrl = mutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Calcula se o registro está dentro do prazo baseado na configuração
|
||||
* Se toleranciaMinutos for 0, desconsidera atrasos (sempre retorna true)
|
||||
*/
|
||||
function calcularStatusPonto(
|
||||
hora: number,
|
||||
minuto: number,
|
||||
horarioConfigurado: string,
|
||||
toleranciaMinutos: number
|
||||
): boolean {
|
||||
// Se tolerância for 0, desconsiderar atrasos (qualquer registro é válido)
|
||||
if (toleranciaMinutos === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina o tipo de registro baseado na sequência lógica
|
||||
*/
|
||||
async function determinarTipoRegistro(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
funcionarioId: Id<'funcionarios'>,
|
||||
data: string
|
||||
): Promise<'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'> {
|
||||
const registrosHoje = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.order('desc')
|
||||
.collect();
|
||||
|
||||
if (registrosHoje.length === 0) {
|
||||
return 'entrada';
|
||||
}
|
||||
|
||||
const ultimoRegistro = registrosHoje[0];
|
||||
switch (ultimoRegistro.tipo) {
|
||||
case 'entrada':
|
||||
return 'saida_almoco';
|
||||
case 'saida_almoco':
|
||||
return 'retorno_almoco';
|
||||
case 'retorno_almoco':
|
||||
return 'saida';
|
||||
case 'saida':
|
||||
// Se já saiu, próximo registro é entrada (novo dia)
|
||||
return 'entrada';
|
||||
default:
|
||||
return 'entrada';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra um ponto (entrada, saída, etc.)
|
||||
*/
|
||||
export const registrarPonto = mutation({
|
||||
args: {
|
||||
imagemId: v.optional(v.id('_storage')),
|
||||
informacoesDispositivo: v.optional(
|
||||
v.object({
|
||||
ipAddress: v.optional(v.string()),
|
||||
ipPublico: v.optional(v.string()),
|
||||
ipLocal: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
browser: v.optional(v.string()),
|
||||
browserVersion: v.optional(v.string()),
|
||||
engine: v.optional(v.string()),
|
||||
sistemaOperacional: v.optional(v.string()),
|
||||
osVersion: v.optional(v.string()),
|
||||
arquitetura: v.optional(v.string()),
|
||||
plataforma: v.optional(v.string()),
|
||||
latitude: v.optional(v.number()),
|
||||
longitude: v.optional(v.number()),
|
||||
precisao: v.optional(v.number()),
|
||||
endereco: v.optional(v.string()),
|
||||
cidade: v.optional(v.string()),
|
||||
estado: v.optional(v.string()),
|
||||
pais: v.optional(v.string()),
|
||||
timezone: v.optional(v.string()),
|
||||
deviceType: v.optional(v.string()),
|
||||
deviceModel: v.optional(v.string()),
|
||||
screenResolution: v.optional(v.string()),
|
||||
coresTela: v.optional(v.number()),
|
||||
idioma: v.optional(v.string()),
|
||||
isMobile: v.optional(v.boolean()),
|
||||
isTablet: v.optional(v.boolean()),
|
||||
isDesktop: v.optional(v.boolean()),
|
||||
connectionType: v.optional(v.string()),
|
||||
memoryInfo: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
timestamp: v.number(),
|
||||
sincronizadoComServidor: v.boolean(),
|
||||
justificativa: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
if (!usuario.funcionarioId) {
|
||||
throw new Error('Usuário não possui funcionário associado');
|
||||
}
|
||||
|
||||
// Verificar se funcionário está ativo
|
||||
const funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||
if (!funcionario) {
|
||||
throw new Error('Funcionário não encontrado');
|
||||
}
|
||||
|
||||
// Obter configuração de ponto
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Configuração de ponto não encontrada');
|
||||
}
|
||||
|
||||
// Obter configuração de ponto para GMT offset (buscar configuração ativa)
|
||||
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 hora = dataObj.getUTCHours();
|
||||
const minuto = dataObj.getUTCMinutes();
|
||||
const segundo = dataObj.getUTCSeconds();
|
||||
|
||||
// Verificar se já existe registro no mesmo minuto
|
||||
const funcionarioId = usuario.funcionarioId; // Já verificado acima, não é undefined
|
||||
const registrosMinuto = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.collect();
|
||||
|
||||
const registroDuplicado = registrosMinuto.find(
|
||||
(r) => r.hora === hora && r.minuto === minuto
|
||||
);
|
||||
|
||||
if (registroDuplicado) {
|
||||
throw new Error('Já existe um registro neste minuto');
|
||||
}
|
||||
|
||||
// Determinar tipo de registro
|
||||
const tipo = await determinarTipoRegistro(ctx, usuario.funcionarioId, data);
|
||||
|
||||
// Calcular horário esperado e tolerância
|
||||
let horarioConfigurado = '';
|
||||
switch (tipo) {
|
||||
case 'entrada':
|
||||
horarioConfigurado = config.horarioEntrada;
|
||||
break;
|
||||
case 'saida_almoco':
|
||||
horarioConfigurado = config.horarioSaidaAlmoco;
|
||||
break;
|
||||
case 'retorno_almoco':
|
||||
horarioConfigurado = config.horarioRetornoAlmoco;
|
||||
break;
|
||||
case 'saida':
|
||||
horarioConfigurado = config.horarioSaida;
|
||||
break;
|
||||
}
|
||||
|
||||
const dentroDoPrazo = calcularStatusPonto(hora, minuto, horarioConfigurado, config.toleranciaMinutos);
|
||||
|
||||
// Criar registro
|
||||
const registroId = await ctx.db.insert('registrosPonto', {
|
||||
funcionarioId: usuario.funcionarioId,
|
||||
tipo,
|
||||
data,
|
||||
hora,
|
||||
minuto,
|
||||
segundo,
|
||||
timestamp: args.timestamp,
|
||||
imagemId: args.imagemId,
|
||||
sincronizadoComServidor: args.sincronizadoComServidor,
|
||||
toleranciaMinutos: config.toleranciaMinutos,
|
||||
dentroDoPrazo,
|
||||
justificativa: args.justificativa,
|
||||
ipAddress: args.informacoesDispositivo?.ipAddress,
|
||||
ipPublico: args.informacoesDispositivo?.ipPublico,
|
||||
ipLocal: args.informacoesDispositivo?.ipLocal,
|
||||
userAgent: args.informacoesDispositivo?.userAgent,
|
||||
browser: args.informacoesDispositivo?.browser,
|
||||
browserVersion: args.informacoesDispositivo?.browserVersion,
|
||||
engine: args.informacoesDispositivo?.engine,
|
||||
sistemaOperacional: args.informacoesDispositivo?.sistemaOperacional,
|
||||
osVersion: args.informacoesDispositivo?.osVersion,
|
||||
arquitetura: args.informacoesDispositivo?.arquitetura,
|
||||
plataforma: args.informacoesDispositivo?.plataforma,
|
||||
latitude: args.informacoesDispositivo?.latitude,
|
||||
longitude: args.informacoesDispositivo?.longitude,
|
||||
precisao: args.informacoesDispositivo?.precisao,
|
||||
endereco: args.informacoesDispositivo?.endereco,
|
||||
cidade: args.informacoesDispositivo?.cidade,
|
||||
estado: args.informacoesDispositivo?.estado,
|
||||
pais: args.informacoesDispositivo?.pais,
|
||||
timezone: args.informacoesDispositivo?.timezone,
|
||||
deviceType: args.informacoesDispositivo?.deviceType,
|
||||
deviceModel: args.informacoesDispositivo?.deviceModel,
|
||||
screenResolution: args.informacoesDispositivo?.screenResolution,
|
||||
coresTela: args.informacoesDispositivo?.coresTela,
|
||||
idioma: args.informacoesDispositivo?.idioma,
|
||||
isMobile: args.informacoesDispositivo?.isMobile,
|
||||
isTablet: args.informacoesDispositivo?.isTablet,
|
||||
isDesktop: args.informacoesDispositivo?.isDesktop,
|
||||
connectionType: args.informacoesDispositivo?.connectionType,
|
||||
memoryInfo: args.informacoesDispositivo?.memoryInfo,
|
||||
criadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Atualizar banco de horas após registrar
|
||||
await atualizarBancoHoras(ctx, usuario.funcionarioId, data, config);
|
||||
|
||||
return { registroId, tipo, dentroDoPrazo };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista registros do dia atual do funcionário
|
||||
*/
|
||||
export const listarRegistrosDia = query({
|
||||
args: {
|
||||
data: v.optional(v.string()), // YYYY-MM-DD, se não fornecido usa hoje
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario || !usuario.funcionarioId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const funcionarioId = usuario.funcionarioId; // Garantir que não é undefined
|
||||
const data = args.data || new Date().toISOString().split('T')[0]!;
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
return registros;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Lista registros por período (para RH)
|
||||
*/
|
||||
export const listarRegistrosPeriodo = query({
|
||||
args: {
|
||||
funcionarioId: v.optional(v.id('funcionarios')),
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Verificar permissão (RH ou TI)
|
||||
// Por enquanto, permitir se tiver funcionarioId ou for admin
|
||||
// TODO: Implementar verificação de permissão adequada
|
||||
|
||||
const dataFim = new Date(args.dataFim);
|
||||
dataFim.setHours(23, 59, 59, 999);
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
||||
.collect();
|
||||
|
||||
// Filtrar por funcionário se especificado
|
||||
let registrosFiltrados = registros;
|
||||
if (args.funcionarioId) {
|
||||
registrosFiltrados = registros.filter((r) => r.funcionarioId === args.funcionarioId);
|
||||
}
|
||||
|
||||
// Buscar informações dos funcionários
|
||||
const funcionariosIds = new Set(registrosFiltrados.map((r) => r.funcionarioId));
|
||||
const funcionarios = await Promise.all(
|
||||
Array.from(funcionariosIds).map((id) => ctx.db.get(id))
|
||||
);
|
||||
|
||||
return registrosFiltrados.map((registro) => {
|
||||
const funcionario = funcionarios.find((f) => f?._id === registro.funcionarioId);
|
||||
return {
|
||||
...registro,
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
descricaoCargo: funcionario.descricaoCargo,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém estatísticas de pontos (para gráficos)
|
||||
*/
|
||||
export const obterEstatisticas = query({
|
||||
args: {
|
||||
dataInicio: v.string(), // YYYY-MM-DD
|
||||
dataFim: v.string(), // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// TODO: Verificar permissão (RH ou TI)
|
||||
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_data', (q) => q.gte('data', args.dataInicio).lte('data', args.dataFim))
|
||||
.collect();
|
||||
|
||||
const totalRegistros = registros.length;
|
||||
const dentroDoPrazo = registros.filter((r) => r.dentroDoPrazo).length;
|
||||
const foraDoPrazo = totalRegistros - dentroDoPrazo;
|
||||
|
||||
// Agrupar por funcionário
|
||||
const funcionariosUnicos = new Set(registros.map((r) => r.funcionarioId));
|
||||
const totalFuncionarios = funcionariosUnicos.size;
|
||||
|
||||
// Funcionários com registros dentro do prazo
|
||||
const funcionariosDentroPrazo = new Set(
|
||||
registros.filter((r) => r.dentroDoPrazo).map((r) => r.funcionarioId)
|
||||
).size;
|
||||
|
||||
// Funcionários com registros fora do prazo
|
||||
const funcionariosForaPrazo = totalFuncionarios - funcionariosDentroPrazo;
|
||||
|
||||
return {
|
||||
totalRegistros,
|
||||
dentroDoPrazo,
|
||||
foraDoPrazo,
|
||||
totalFuncionarios,
|
||||
funcionariosDentroPrazo,
|
||||
funcionariosForaPrazo,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém um registro específico (para comprovante)
|
||||
*/
|
||||
export const obterRegistro = query({
|
||||
args: {
|
||||
registroId: v.id('registrosPonto'),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
const registro = await ctx.db.get(args.registroId);
|
||||
if (!registro) {
|
||||
throw new Error('Registro não encontrado');
|
||||
}
|
||||
|
||||
// Verificar se o usuário tem permissão (próprio registro ou RH/TI)
|
||||
if (registro.funcionarioId !== usuario.funcionarioId) {
|
||||
// TODO: Verificar se é RH ou TI
|
||||
// Por enquanto, permitir
|
||||
}
|
||||
|
||||
const funcionario = await ctx.db.get(registro.funcionarioId);
|
||||
let simbolo = null;
|
||||
if (funcionario) {
|
||||
simbolo = await ctx.db.get(funcionario.simboloId);
|
||||
}
|
||||
|
||||
// Obter URL da imagem se existir
|
||||
let imagemUrl = null;
|
||||
if (registro.imagemId) {
|
||||
imagemUrl = await ctx.storage.getUrl(registro.imagemId);
|
||||
}
|
||||
|
||||
return {
|
||||
...registro,
|
||||
imagemUrl,
|
||||
funcionario: funcionario
|
||||
? {
|
||||
nome: funcionario.nome,
|
||||
matricula: funcionario.matricula,
|
||||
descricaoCargo: funcionario.descricaoCargo,
|
||||
simbolo: simbolo
|
||||
? {
|
||||
nome: simbolo.nome,
|
||||
tipo: simbolo.tipo,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Calcula carga horária diária esperada em minutos
|
||||
*/
|
||||
function calcularCargaHorariaDiaria(config: {
|
||||
horarioEntrada: string;
|
||||
horarioSaidaAlmoco: string;
|
||||
horarioRetornoAlmoco: string;
|
||||
horarioSaida: string;
|
||||
}): number {
|
||||
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
|
||||
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
|
||||
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
|
||||
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
|
||||
|
||||
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
|
||||
const minutosSaidaAlmoco = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
|
||||
const minutosRetornoAlmoco = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
|
||||
const minutosSaida = horaSaida * 60 + minutoSaida;
|
||||
|
||||
// Calcular horas trabalhadas: (saída almoço - entrada) + (saída - retorno almoço)
|
||||
const horasManha = minutosSaidaAlmoco - minutosEntrada;
|
||||
const horasTarde = minutosSaida - minutosRetornoAlmoco;
|
||||
|
||||
return horasManha + horasTarde;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula horas trabalhadas do dia baseado nos registros
|
||||
*/
|
||||
function calcularHorasTrabalhadas(registros: Array<{
|
||||
tipo: string;
|
||||
hora: number;
|
||||
minuto: number;
|
||||
}>): number {
|
||||
// Ordenar registros por timestamp
|
||||
const registrosOrdenados = [...registros].sort((a, b) => {
|
||||
const minutosA = a.hora * 60 + a.minuto;
|
||||
const minutosB = b.hora * 60 + b.minuto;
|
||||
return minutosA - minutosB;
|
||||
});
|
||||
|
||||
let horasTrabalhadas = 0;
|
||||
|
||||
// Procurar entrada e saída
|
||||
const entrada = registrosOrdenados.find((r) => r.tipo === 'entrada');
|
||||
const saida = registrosOrdenados.find((r) => r.tipo === 'saida');
|
||||
|
||||
if (entrada && saida) {
|
||||
const minutosEntrada = entrada.hora * 60 + entrada.minuto;
|
||||
const minutosSaida = saida.hora * 60 + saida.minuto;
|
||||
|
||||
// Procurar saída e retorno do almoço
|
||||
const saidaAlmoco = registrosOrdenados.find((r) => r.tipo === 'saida_almoco');
|
||||
const retornoAlmoco = registrosOrdenados.find((r) => r.tipo === 'retorno_almoco');
|
||||
|
||||
if (saidaAlmoco && retornoAlmoco) {
|
||||
// Tem intervalo de almoço: (saída almoço - entrada) + (saída - retorno almoço)
|
||||
const minutosSaidaAlmoco = saidaAlmoco.hora * 60 + saidaAlmoco.minuto;
|
||||
const minutosRetornoAlmoco = retornoAlmoco.hora * 60 + retornoAlmoco.minuto;
|
||||
|
||||
const horasManha = minutosSaidaAlmoco - minutosEntrada;
|
||||
const horasTarde = minutosSaida - minutosRetornoAlmoco;
|
||||
horasTrabalhadas = horasManha + horasTarde;
|
||||
} else {
|
||||
// Sem intervalo de almoço registrado: saída - entrada
|
||||
horasTrabalhadas = minutosSaida - minutosEntrada;
|
||||
}
|
||||
}
|
||||
|
||||
return horasTrabalhadas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza ou cria registro de banco de horas para o dia
|
||||
*/
|
||||
async function atualizarBancoHoras(
|
||||
ctx: MutationCtx,
|
||||
funcionarioId: Id<'funcionarios'>,
|
||||
data: string,
|
||||
config: {
|
||||
horarioEntrada: string;
|
||||
horarioSaidaAlmoco: string;
|
||||
horarioRetornoAlmoco: string;
|
||||
horarioSaida: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
// Buscar todos os registros do dia
|
||||
const registrosDoDia = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.collect();
|
||||
|
||||
// Calcular carga horária esperada
|
||||
const cargaHorariaDiaria = calcularCargaHorariaDiaria(config);
|
||||
|
||||
// Calcular horas trabalhadas
|
||||
const horasTrabalhadas = calcularHorasTrabalhadas(registrosDoDia);
|
||||
|
||||
// Calcular saldo (positivo = horas extras, negativo = déficit)
|
||||
const saldoMinutos = horasTrabalhadas - cargaHorariaDiaria;
|
||||
|
||||
// Buscar banco de horas existente
|
||||
const bancoHorasExistente = await ctx.db
|
||||
.query('bancoHoras')
|
||||
.withIndex('by_funcionario_data', (q) => q.eq('funcionarioId', funcionarioId).eq('data', data))
|
||||
.first();
|
||||
|
||||
const registrosPontoIds = registrosDoDia.map((r) => r._id);
|
||||
|
||||
if (bancoHorasExistente) {
|
||||
// Atualizar existente
|
||||
await ctx.db.patch(bancoHorasExistente._id, {
|
||||
cargaHorariaDiaria,
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
});
|
||||
} else {
|
||||
// Criar novo
|
||||
await ctx.db.insert('bancoHoras', {
|
||||
funcionarioId,
|
||||
data,
|
||||
cargaHorariaDiaria,
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
registrosPontoIds,
|
||||
calculadoEm: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém histórico e saldo do dia
|
||||
*/
|
||||
export const obterHistoricoESaldoDia = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario || !usuario.funcionarioId) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Verificar se é o próprio funcionário ou tem permissão
|
||||
if (usuario.funcionarioId !== args.funcionarioId) {
|
||||
// TODO: Verificar permissão de RH
|
||||
}
|
||||
|
||||
// Buscar registros do dia
|
||||
const registros = await ctx.db
|
||||
.query('registrosPonto')
|
||||
.withIndex('by_funcionario_data', (q) =>
|
||||
q.eq('funcionarioId', args.funcionarioId).eq('data', args.data)
|
||||
)
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
// Buscar configuração de ponto
|
||||
const config = await ctx.db
|
||||
.query('configuracaoPonto')
|
||||
.withIndex('by_ativo', (q) => q.eq('ativo', true))
|
||||
.first();
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
registros: [],
|
||||
cargaHorariaDiaria: 0,
|
||||
horasTrabalhadas: 0,
|
||||
saldoMinutos: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Calcular valores
|
||||
const cargaHorariaDiaria = calcularCargaHorariaDiaria(config);
|
||||
const horasTrabalhadas = calcularHorasTrabalhadas(registros);
|
||||
const saldoMinutos = horasTrabalhadas - cargaHorariaDiaria;
|
||||
|
||||
return {
|
||||
registros,
|
||||
cargaHorariaDiaria,
|
||||
horasTrabalhadas,
|
||||
saldoMinutos,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtém banco de horas acumulado do funcionário
|
||||
*/
|
||||
export const obterBancoHorasFuncionario = query({
|
||||
args: {
|
||||
funcionarioId: v.id('funcionarios'),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Usuário não autenticado');
|
||||
}
|
||||
|
||||
// Verificar se é o próprio funcionário ou tem permissão
|
||||
if (usuario.funcionarioId !== args.funcionarioId) {
|
||||
// TODO: Verificar permissão de RH
|
||||
}
|
||||
|
||||
// Buscar todos os registros de banco de horas do funcionário
|
||||
const bancosHoras = await ctx.db
|
||||
.query('bancoHoras')
|
||||
.withIndex('by_funcionario', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||
.order('desc')
|
||||
.collect();
|
||||
|
||||
// Calcular saldo acumulado
|
||||
const saldoAcumuladoMinutos = bancosHoras.reduce((acc, bh) => acc + bh.saldoMinutos, 0);
|
||||
|
||||
return {
|
||||
bancosHoras,
|
||||
saldoAcumuladoMinutos,
|
||||
totalDias: bancosHoras.length,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1392,5 +1392,122 @@ export default defineSchema({
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_criadoEm", ["criadoEm"])
|
||||
.index("by_criadoEm", ["criadoEm"]),
|
||||
|
||||
// Sistema de Controle de Ponto
|
||||
registrosPonto: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
tipo: v.union(
|
||||
v.literal("entrada"),
|
||||
v.literal("saida_almoco"),
|
||||
v.literal("retorno_almoco"),
|
||||
v.literal("saida")
|
||||
),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
hora: v.number(),
|
||||
minuto: v.number(),
|
||||
segundo: v.number(),
|
||||
timestamp: v.number(), // Timestamp completo para ordenação
|
||||
imagemId: v.optional(v.id("_storage")),
|
||||
sincronizadoComServidor: v.boolean(),
|
||||
toleranciaMinutos: v.number(),
|
||||
dentroDoPrazo: v.boolean(),
|
||||
|
||||
// Informações de Rede
|
||||
ipAddress: v.optional(v.string()),
|
||||
ipPublico: v.optional(v.string()),
|
||||
ipLocal: v.optional(v.string()),
|
||||
|
||||
// Informações do Navegador
|
||||
userAgent: v.optional(v.string()),
|
||||
browser: v.optional(v.string()),
|
||||
browserVersion: v.optional(v.string()),
|
||||
engine: v.optional(v.string()),
|
||||
|
||||
// Informações do Sistema
|
||||
sistemaOperacional: v.optional(v.string()),
|
||||
osVersion: v.optional(v.string()),
|
||||
arquitetura: v.optional(v.string()),
|
||||
plataforma: v.optional(v.string()),
|
||||
|
||||
// Informações de Localização
|
||||
latitude: v.optional(v.number()),
|
||||
longitude: v.optional(v.number()),
|
||||
precisao: v.optional(v.number()),
|
||||
endereco: v.optional(v.string()),
|
||||
cidade: v.optional(v.string()),
|
||||
estado: v.optional(v.string()),
|
||||
pais: v.optional(v.string()),
|
||||
timezone: v.optional(v.string()),
|
||||
|
||||
// Informações do Dispositivo
|
||||
deviceType: v.optional(v.string()),
|
||||
deviceModel: v.optional(v.string()),
|
||||
screenResolution: v.optional(v.string()),
|
||||
coresTela: v.optional(v.number()),
|
||||
idioma: v.optional(v.string()),
|
||||
|
||||
// Informações Adicionais
|
||||
isMobile: v.optional(v.boolean()),
|
||||
isTablet: v.optional(v.boolean()),
|
||||
isDesktop: v.optional(v.boolean()),
|
||||
connectionType: v.optional(v.string()),
|
||||
memoryInfo: v.optional(v.string()),
|
||||
|
||||
// Justificativa opcional para o registro
|
||||
justificativa: v.optional(v.string()),
|
||||
|
||||
criadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario_data", ["funcionarioId", "data"])
|
||||
.index("by_data", ["data"])
|
||||
.index("by_dentro_prazo", ["dentroDoPrazo", "data"])
|
||||
.index("by_funcionario_timestamp", ["funcionarioId", "timestamp"]),
|
||||
|
||||
configuracaoPonto: defineTable({
|
||||
horarioEntrada: v.string(), // HH:mm
|
||||
horarioSaidaAlmoco: v.string(), // HH:mm
|
||||
horarioRetornoAlmoco: v.string(), // HH:mm
|
||||
horarioSaida: v.string(), // HH:mm
|
||||
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"
|
||||
// Ajuste de fuso horário (GMT offset em horas)
|
||||
gmtOffset: v.optional(v.number()), // Padrão: 0 (UTC)
|
||||
ativo: v.boolean(),
|
||||
atualizadoPor: v.id("usuarios"),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_ativo", ["ativo"]),
|
||||
|
||||
configuracaoRelogio: defineTable({
|
||||
servidorNTP: v.optional(v.string()),
|
||||
portaNTP: v.optional(v.number()),
|
||||
usarServidorExterno: v.boolean(),
|
||||
fallbackParaPC: v.boolean(),
|
||||
ultimaSincronizacao: 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"),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_ativo", ["usarServidorExterno"]),
|
||||
|
||||
// Banco de Horas - Saldo diário de horas trabalhadas
|
||||
bancoHoras: defineTable({
|
||||
funcionarioId: v.id("funcionarios"),
|
||||
data: v.string(), // YYYY-MM-DD
|
||||
cargaHorariaDiaria: v.number(), // Horas esperadas do dia (em minutos)
|
||||
horasTrabalhadas: v.number(), // Horas realmente trabalhadas (em minutos)
|
||||
saldoMinutos: v.number(), // Saldo do dia (positivo = horas extras, negativo = déficit)
|
||||
registrosPontoIds: v.array(v.id("registrosPonto")), // IDs dos registros do dia
|
||||
calculadoEm: v.number(),
|
||||
})
|
||||
.index("by_funcionario_data", ["funcionarioId", "data"])
|
||||
.index("by_funcionario", ["funcionarioId"])
|
||||
.index("by_data", ["data"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user