1886 lines
60 KiB
TypeScript
1886 lines
60 KiB
TypeScript
import { defineSchema, defineTable } from 'convex/server';
|
|
import { Infer, v } from 'convex/values';
|
|
|
|
export const simboloTipo = v.union(
|
|
v.literal('cargo_comissionado'),
|
|
v.literal('funcao_gratificada')
|
|
);
|
|
export type SimboloTipo = Infer<typeof simboloTipo>;
|
|
|
|
export const ataqueCiberneticoTipo = v.union(
|
|
v.literal('phishing'),
|
|
v.literal('malware'),
|
|
v.literal('ransomware'),
|
|
v.literal('brute_force'),
|
|
v.literal('credential_stuffing'),
|
|
v.literal('sql_injection'),
|
|
v.literal('xss'),
|
|
v.literal('path_traversal'),
|
|
v.literal('command_injection'),
|
|
v.literal('nosql_injection'),
|
|
v.literal('xxe'),
|
|
v.literal('man_in_the_middle'),
|
|
v.literal('ddos'),
|
|
v.literal('engenharia_social'),
|
|
v.literal('cve_exploit'),
|
|
v.literal('apt'),
|
|
v.literal('zero_day'),
|
|
v.literal('supply_chain'),
|
|
v.literal('fileless_malware'),
|
|
v.literal('polymorphic_malware'),
|
|
v.literal('ransomware_lateral'),
|
|
v.literal('deepfake_phishing'),
|
|
v.literal('adversarial_ai'),
|
|
v.literal('side_channel'),
|
|
v.literal('firmware_bootloader'),
|
|
v.literal('bec'),
|
|
v.literal('botnet'),
|
|
v.literal('ot_ics'),
|
|
v.literal('quantum_attack')
|
|
);
|
|
export type AtaqueCiberneticoTipo = Infer<typeof ataqueCiberneticoTipo>;
|
|
|
|
export const severidadeSeguranca = v.union(
|
|
v.literal('informativo'),
|
|
v.literal('baixo'),
|
|
v.literal('moderado'),
|
|
v.literal('alto'),
|
|
v.literal('critico')
|
|
);
|
|
export type SeveridadeSeguranca = Infer<typeof severidadeSeguranca>;
|
|
|
|
export const statusEventoSeguranca = v.union(
|
|
v.literal('detectado'),
|
|
v.literal('investigando'),
|
|
v.literal('contido'),
|
|
v.literal('falso_positivo'),
|
|
v.literal('escalado'),
|
|
v.literal('resolvido')
|
|
);
|
|
export type StatusEventoSeguranca = Infer<typeof statusEventoSeguranca>;
|
|
|
|
export const sensorSegurancaTipo = v.union(
|
|
v.literal('network'),
|
|
v.literal('endpoint'),
|
|
v.literal('application'),
|
|
v.literal('gateway'),
|
|
v.literal('ot'),
|
|
v.literal('honeypot')
|
|
);
|
|
export type SensorSegurancaTipo = Infer<typeof sensorSegurancaTipo>;
|
|
|
|
export const sensorSegurancaStatus = v.union(
|
|
v.literal('ativo'),
|
|
v.literal('inativo'),
|
|
v.literal('degradado'),
|
|
v.literal('manutencao')
|
|
);
|
|
export type SensorSegurancaStatus = Infer<typeof sensorSegurancaStatus>;
|
|
|
|
export const threatIntelTipo = v.union(
|
|
v.literal('open_source'),
|
|
v.literal('commercial'),
|
|
v.literal('internal'),
|
|
v.literal('gov'),
|
|
v.literal('research')
|
|
);
|
|
|
|
export const threatIntelFormato = v.union(
|
|
v.literal('json'),
|
|
v.literal('stix'),
|
|
v.literal('csv'),
|
|
v.literal('text'),
|
|
v.literal('custom')
|
|
);
|
|
|
|
export const acaoIncidenteTipo = v.union(
|
|
v.literal('block_ip'),
|
|
v.literal('unblock_ip'),
|
|
v.literal('block_port'),
|
|
v.literal('liberar_porta'),
|
|
v.literal('notificar'),
|
|
v.literal('isolar_host'),
|
|
v.literal('gerar_relatorio'),
|
|
v.literal('criar_ticket'),
|
|
v.literal('ajuste_regra'),
|
|
v.literal('custom')
|
|
);
|
|
|
|
export const acaoIncidenteStatus = v.union(
|
|
v.literal('pendente'),
|
|
v.literal('executando'),
|
|
v.literal('concluido'),
|
|
v.literal('falhou')
|
|
);
|
|
|
|
export const reportStatus = v.union(
|
|
v.literal('pendente'),
|
|
v.literal('processando'),
|
|
v.literal('concluido'),
|
|
v.literal('falhou')
|
|
);
|
|
|
|
// Status de templates de fluxo
|
|
export const flowTemplateStatus = v.union(
|
|
v.literal('draft'),
|
|
v.literal('published'),
|
|
v.literal('archived')
|
|
);
|
|
export type FlowTemplateStatus = Infer<typeof flowTemplateStatus>;
|
|
|
|
// Status de instâncias de fluxo
|
|
export const flowInstanceStatus = v.union(
|
|
v.literal('active'),
|
|
v.literal('completed'),
|
|
v.literal('cancelled')
|
|
);
|
|
export type FlowInstanceStatus = Infer<typeof flowInstanceStatus>;
|
|
|
|
// Status de passos de instância de fluxo
|
|
export const flowInstanceStepStatus = v.union(
|
|
v.literal('pending'),
|
|
v.literal('in_progress'),
|
|
v.literal('completed'),
|
|
v.literal('blocked')
|
|
);
|
|
export type FlowInstanceStepStatus = Infer<typeof flowInstanceStepStatus>;
|
|
|
|
export const situacaoContrato = v.union(
|
|
v.literal('em_execucao'),
|
|
v.literal('rescendido'),
|
|
v.literal('aguardando_assinatura'),
|
|
v.literal('finalizado')
|
|
);
|
|
|
|
export default defineSchema({
|
|
// Setores da organização
|
|
setores: defineTable({
|
|
nome: v.string(),
|
|
sigla: v.string(),
|
|
criadoPor: v.id('usuarios'),
|
|
createdAt: v.number()
|
|
})
|
|
.index('by_nome', ['nome'])
|
|
.index('by_sigla', ['sigla']),
|
|
|
|
// Relação muitos-para-muitos entre funcionários e setores
|
|
funcionarioSetores: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
setorId: v.id('setores'),
|
|
createdAt: v.number()
|
|
})
|
|
.index('by_funcionarioId', ['funcionarioId'])
|
|
.index('by_setorId', ['setorId'])
|
|
.index('by_funcionarioId_and_setorId', ['funcionarioId', 'setorId']),
|
|
|
|
// Templates de fluxo
|
|
flowTemplates: defineTable({
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
status: flowTemplateStatus,
|
|
createdBy: v.id('usuarios'),
|
|
createdAt: v.number()
|
|
})
|
|
.index('by_status', ['status'])
|
|
.index('by_createdBy', ['createdBy']),
|
|
|
|
// Passos de template de fluxo
|
|
flowSteps: defineTable({
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
position: v.number(),
|
|
expectedDuration: v.number(), // em dias
|
|
setorId: v.id('setores'),
|
|
defaultAssigneeId: v.optional(v.id('usuarios')),
|
|
requiredDocuments: v.optional(v.array(v.string()))
|
|
})
|
|
.index('by_flowTemplateId', ['flowTemplateId'])
|
|
.index('by_flowTemplateId_and_position', ['flowTemplateId', 'position']),
|
|
|
|
// Instâncias de fluxo
|
|
flowInstances: defineTable({
|
|
flowTemplateId: v.id('flowTemplates'),
|
|
contratoId: v.optional(v.id('contratos')),
|
|
managerId: v.id('usuarios'),
|
|
status: flowInstanceStatus,
|
|
startedAt: v.number(),
|
|
finishedAt: v.optional(v.number()),
|
|
currentStepId: v.optional(v.id('flowInstanceSteps'))
|
|
})
|
|
.index('by_flowTemplateId', ['flowTemplateId'])
|
|
.index('by_contratoId', ['contratoId'])
|
|
.index('by_managerId', ['managerId'])
|
|
.index('by_status', ['status']),
|
|
|
|
// Passos de instância de fluxo
|
|
flowInstanceSteps: defineTable({
|
|
flowInstanceId: v.id('flowInstances'),
|
|
flowStepId: v.id('flowSteps'),
|
|
setorId: v.id('setores'),
|
|
assignedToId: v.optional(v.id('usuarios')),
|
|
status: flowInstanceStepStatus,
|
|
startedAt: v.optional(v.number()),
|
|
finishedAt: v.optional(v.number()),
|
|
notes: v.optional(v.string()),
|
|
notesUpdatedBy: v.optional(v.id('usuarios')),
|
|
notesUpdatedAt: v.optional(v.number()),
|
|
dueDate: v.optional(v.number())
|
|
})
|
|
.index('by_flowInstanceId', ['flowInstanceId'])
|
|
.index('by_flowInstanceId_and_status', ['flowInstanceId', 'status'])
|
|
.index('by_setorId', ['setorId'])
|
|
.index('by_assignedToId', ['assignedToId']),
|
|
|
|
// Documentos de instância de fluxo
|
|
flowInstanceDocuments: defineTable({
|
|
flowInstanceStepId: v.id('flowInstanceSteps'),
|
|
uploadedById: v.id('usuarios'),
|
|
storageId: v.id('_storage'),
|
|
name: v.string(),
|
|
uploadedAt: v.number()
|
|
})
|
|
.index('by_flowInstanceStepId', ['flowInstanceStepId'])
|
|
.index('by_uploadedById', ['uploadedById']),
|
|
|
|
// Sub-etapas de fluxo (para templates e instâncias)
|
|
flowSubSteps: defineTable({
|
|
flowStepId: v.optional(v.id('flowSteps')), // Para templates
|
|
flowInstanceStepId: v.optional(v.id('flowInstanceSteps')), // Para instâncias
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
status: v.union(
|
|
v.literal('pending'),
|
|
v.literal('in_progress'),
|
|
v.literal('completed'),
|
|
v.literal('blocked')
|
|
),
|
|
position: v.number(),
|
|
createdBy: v.id('usuarios'),
|
|
createdAt: v.number()
|
|
})
|
|
.index('by_flowStepId', ['flowStepId'])
|
|
.index('by_flowInstanceStepId', ['flowInstanceStepId']),
|
|
|
|
// Notas de steps e sub-etapas
|
|
flowStepNotes: defineTable({
|
|
flowStepId: v.optional(v.id('flowSteps')),
|
|
flowInstanceStepId: v.optional(v.id('flowInstanceSteps')),
|
|
flowSubStepId: v.optional(v.id('flowSubSteps')),
|
|
texto: v.string(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
arquivos: v.array(v.id('_storage'))
|
|
})
|
|
.index('by_flowStepId', ['flowStepId'])
|
|
.index('by_flowInstanceStepId', ['flowInstanceStepId'])
|
|
.index('by_flowSubStepId', ['flowSubStepId']),
|
|
|
|
contratos: defineTable({
|
|
contratadaId: v.id('empresas'),
|
|
objeto: v.string(),
|
|
numeroNotaEmpenho: v.string(),
|
|
responsavelId: v.id('funcionarios'),
|
|
departamento: v.string(),
|
|
situacao: situacaoContrato,
|
|
numeroProcessoLicitatorio: v.string(),
|
|
modalidade: v.string(),
|
|
numeroContrato: v.string(),
|
|
anoContrato: v.number(),
|
|
dataInicioVigencia: v.string(),
|
|
dataFimVigencia: v.string(),
|
|
nomeFiscal: v.string(),
|
|
valorTotal: v.string(),
|
|
dataAditivoPrazo: v.optional(v.string()),
|
|
diasAvisoVencimento: v.number(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.optional(v.number())
|
|
})
|
|
.index('by_responsavel', ['responsavelId'])
|
|
.index('by_situacao', ['situacao'])
|
|
.index('by_vigencia_inicio', ['dataInicioVigencia'])
|
|
.index('by_vigencia_fim', ['dataFimVigencia']),
|
|
|
|
todos: defineTable({
|
|
text: v.string(),
|
|
completed: v.boolean()
|
|
}),
|
|
enderecos: defineTable({
|
|
cep: v.string(),
|
|
logradouro: v.string(),
|
|
numero: v.string(),
|
|
complemento: v.optional(v.string()),
|
|
bairro: v.string(),
|
|
cidade: v.string(),
|
|
uf: v.string(),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
atualizadoPor: v.optional(v.id('usuarios'))
|
|
}).index('by_cep', ['cep']),
|
|
empresas: defineTable({
|
|
razao_social: v.string(),
|
|
nome_fantasia: v.optional(v.string()),
|
|
cnpj: v.string(),
|
|
telefone: v.string(),
|
|
email: v.string(),
|
|
descricao: v.optional(v.string()),
|
|
enderecoId: v.optional(v.id('enderecos')),
|
|
criadoPor: v.optional(v.id('usuarios'))
|
|
})
|
|
.index('by_razao_social', ['razao_social'])
|
|
.index('by_cnpj', ['cnpj']),
|
|
contatosEmpresa: defineTable({
|
|
empresaId: v.id('empresas'),
|
|
nome: v.string(),
|
|
funcao: v.string(),
|
|
email: v.string(),
|
|
telefone: v.string(),
|
|
adicionadoPor: v.optional(v.id('usuarios')),
|
|
descricao: v.optional(v.string())
|
|
})
|
|
.index('by_empresa', ['empresaId'])
|
|
.index('by_email', ['email']),
|
|
funcionarios: defineTable({
|
|
// Campos obrigatórios existentes
|
|
nome: v.string(),
|
|
nascimento: v.string(),
|
|
rg: v.string(),
|
|
cpf: v.string(),
|
|
endereco: v.string(),
|
|
cep: v.string(),
|
|
cidade: v.string(),
|
|
uf: v.string(),
|
|
telefone: v.string(),
|
|
email: v.string(),
|
|
matricula: v.optional(v.string()),
|
|
admissaoData: v.optional(v.string()),
|
|
desligamentoData: v.optional(v.string()),
|
|
simboloId: v.id('simbolos'),
|
|
simboloTipo: simboloTipo,
|
|
gestorId: v.optional(v.id('usuarios')),
|
|
statusFerias: v.optional(v.union(v.literal('ativo'), v.literal('em_ferias'))),
|
|
|
|
// Regime de trabalho (para cálculo correto de férias)
|
|
regimeTrabalho: v.optional(
|
|
v.union(
|
|
v.literal('clt'), // CLT - Consolidação das Leis do Trabalho
|
|
v.literal('estatutario_pe'), // Servidor Público Estadual de Pernambuco
|
|
v.literal('estatutario_federal'), // Servidor Público Federal
|
|
v.literal('estatutario_municipal') // Servidor Público Municipal
|
|
)
|
|
),
|
|
|
|
// Dados Pessoais Adicionais (opcionais)
|
|
nomePai: v.optional(v.string()),
|
|
nomeMae: v.optional(v.string()),
|
|
naturalidade: v.optional(v.string()),
|
|
naturalidadeUF: v.optional(v.string()),
|
|
sexo: v.optional(v.union(v.literal('masculino'), v.literal('feminino'), v.literal('outro'))),
|
|
estadoCivil: v.optional(
|
|
v.union(
|
|
v.literal('solteiro'),
|
|
v.literal('casado'),
|
|
v.literal('divorciado'),
|
|
v.literal('viuvo'),
|
|
v.literal('uniao_estavel')
|
|
)
|
|
),
|
|
nacionalidade: v.optional(v.string()),
|
|
|
|
// Documentos Pessoais
|
|
rgOrgaoExpedidor: v.optional(v.string()),
|
|
rgDataEmissao: v.optional(v.string()),
|
|
carteiraProfissionalNumero: v.optional(v.string()),
|
|
carteiraProfissionalSerie: v.optional(v.string()),
|
|
carteiraProfissionalDataEmissao: v.optional(v.string()),
|
|
reservistaNumero: v.optional(v.string()),
|
|
reservistaSerie: v.optional(v.string()),
|
|
tituloEleitorNumero: v.optional(v.string()),
|
|
tituloEleitorZona: v.optional(v.string()),
|
|
tituloEleitorSecao: v.optional(v.string()),
|
|
pisNumero: v.optional(v.string()),
|
|
|
|
// Formação e Saúde
|
|
grauInstrucao: v.optional(
|
|
v.union(
|
|
v.literal('fundamental'),
|
|
v.literal('medio'),
|
|
v.literal('superior'),
|
|
v.literal('pos_graduacao'),
|
|
v.literal('mestrado'),
|
|
v.literal('doutorado')
|
|
)
|
|
),
|
|
formacao: v.optional(v.string()),
|
|
formacaoRegistro: v.optional(v.string()),
|
|
grupoSanguineo: v.optional(
|
|
v.union(v.literal('A'), v.literal('B'), v.literal('AB'), v.literal('O'))
|
|
),
|
|
fatorRH: v.optional(v.union(v.literal('positivo'), v.literal('negativo'))),
|
|
|
|
// Cargo e Vínculo
|
|
descricaoCargo: v.optional(v.string()),
|
|
nomeacaoPortaria: v.optional(v.string()),
|
|
nomeacaoData: v.optional(v.string()),
|
|
nomeacaoDOE: v.optional(v.string()),
|
|
pertenceOrgaoPublico: v.optional(v.boolean()),
|
|
orgaoOrigem: v.optional(v.string()),
|
|
aposentado: v.optional(v.union(v.literal('nao'), v.literal('funape_ipsep'), v.literal('inss'))),
|
|
|
|
// Dados Bancários
|
|
contaBradescoNumero: v.optional(v.string()),
|
|
contaBradescoDV: v.optional(v.string()),
|
|
contaBradescoAgencia: v.optional(v.string()),
|
|
|
|
// Documentos Anexos (Storage IDs)
|
|
certidaoAntecedentesPF: v.optional(v.id('_storage')),
|
|
certidaoAntecedentesJFPE: v.optional(v.id('_storage')),
|
|
certidaoAntecedentesSDS: v.optional(v.id('_storage')),
|
|
certidaoAntecedentesTJPE: v.optional(v.id('_storage')),
|
|
certidaoImprobidade: v.optional(v.id('_storage')),
|
|
rgFrente: v.optional(v.id('_storage')),
|
|
rgVerso: v.optional(v.id('_storage')),
|
|
cpfFrente: v.optional(v.id('_storage')),
|
|
cpfVerso: v.optional(v.id('_storage')),
|
|
situacaoCadastralCPF: v.optional(v.id('_storage')),
|
|
tituloEleitorFrente: v.optional(v.id('_storage')),
|
|
tituloEleitorVerso: v.optional(v.id('_storage')),
|
|
comprovanteVotacao: v.optional(v.id('_storage')),
|
|
carteiraProfissionalFrente: v.optional(v.id('_storage')),
|
|
carteiraProfissionalVerso: v.optional(v.id('_storage')),
|
|
comprovantePIS: v.optional(v.id('_storage')),
|
|
certidaoRegistroCivil: v.optional(v.id('_storage')),
|
|
certidaoNascimentoDependentes: v.optional(v.id('_storage')),
|
|
cpfDependentes: v.optional(v.id('_storage')),
|
|
reservistaDoc: v.optional(v.id('_storage')),
|
|
comprovanteEscolaridade: v.optional(v.id('_storage')),
|
|
comprovanteResidencia: v.optional(v.id('_storage')),
|
|
comprovanteContaBradesco: v.optional(v.id('_storage')),
|
|
|
|
// Dependentes do funcionário (uploads opcionais)
|
|
dependentes: v.optional(
|
|
v.array(
|
|
v.object({
|
|
parentesco: v.union(
|
|
v.literal('filho'),
|
|
v.literal('filha'),
|
|
v.literal('conjuge'),
|
|
v.literal('outro')
|
|
),
|
|
nome: v.string(),
|
|
cpf: v.string(),
|
|
nascimento: v.string(),
|
|
documentoId: v.optional(v.id('_storage')),
|
|
// Benefícios/declarações por dependente
|
|
salarioFamilia: v.optional(v.boolean()),
|
|
impostoRenda: v.optional(v.boolean())
|
|
})
|
|
)
|
|
),
|
|
|
|
// Declarações (Storage IDs)
|
|
declaracaoAcumulacaoCargo: v.optional(v.id('_storage')),
|
|
declaracaoDependentesIR: v.optional(v.id('_storage')),
|
|
declaracaoIdoneidade: v.optional(v.id('_storage')),
|
|
termoNepotismo: v.optional(v.id('_storage')),
|
|
termoOpcaoRemuneracao: v.optional(v.id('_storage'))
|
|
})
|
|
.index('by_matricula', ['matricula'])
|
|
.index('by_nome', ['nome'])
|
|
.index('by_simboloId', ['simboloId'])
|
|
.index('by_simboloTipo', ['simboloTipo'])
|
|
.index('by_cpf', ['cpf'])
|
|
.index('by_rg', ['rg'])
|
|
.index('by_gestor', ['gestorId']),
|
|
|
|
atestados: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
tipo: v.union(v.literal('atestado_medico'), v.literal('declaracao_comparecimento')),
|
|
dataInicio: v.string(),
|
|
dataFim: v.string(),
|
|
cid: v.optional(v.string()), // Apenas para atestado médico
|
|
observacoes: v.optional(v.string()),
|
|
documentoId: v.optional(v.id('_storage')),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_data_inicio', ['dataInicio'])
|
|
.index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']),
|
|
|
|
licencas: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
tipo: v.union(v.literal('maternidade'), v.literal('paternidade')),
|
|
dataInicio: v.string(),
|
|
dataFim: v.string(),
|
|
documentoId: v.optional(v.id('_storage')),
|
|
observacoes: v.optional(v.string()),
|
|
licencaOriginalId: v.optional(v.id('licencas')), // Para prorrogações
|
|
ehProrrogacao: v.boolean(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_data_inicio', ['dataInicio'])
|
|
.index('by_licenca_original', ['licencaOriginalId'])
|
|
.index('by_funcionario_and_tipo', ['funcionarioId', 'tipo']),
|
|
|
|
ferias: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
anoReferencia: v.number(),
|
|
dataInicio: v.string(),
|
|
dataFim: v.string(),
|
|
diasFerias: v.number(),
|
|
status: v.union(
|
|
v.literal('aguardando_aprovacao'),
|
|
v.literal('aprovado'),
|
|
v.literal('reprovado'),
|
|
v.literal('data_ajustada_aprovada'),
|
|
v.literal('EmFérias'),
|
|
v.literal('Cancelado_RH')
|
|
),
|
|
gestorId: v.optional(v.id('usuarios')),
|
|
observacao: v.optional(v.string()),
|
|
motivoReprovacao: v.optional(v.string()),
|
|
dataAprovacao: v.optional(v.number()),
|
|
dataReprovacao: v.optional(v.number()),
|
|
diasAbono: v.number(),
|
|
historicoAlteracoes: v.optional(
|
|
v.array(
|
|
v.object({
|
|
data: v.number(),
|
|
usuarioId: v.id('usuarios'),
|
|
acao: v.string()
|
|
})
|
|
)
|
|
)
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_funcionario_and_ano', ['funcionarioId', 'anoReferencia'])
|
|
.index('by_funcionario_and_status', ['funcionarioId', 'status'])
|
|
.index('by_status', ['status'])
|
|
.index('by_ano', ['anoReferencia']),
|
|
|
|
notificacoesFerias: defineTable({
|
|
destinatarioId: v.id('usuarios'),
|
|
feriasId: v.id('ferias'),
|
|
tipo: v.union(
|
|
v.literal('nova_solicitacao'),
|
|
v.literal('aprovado'),
|
|
v.literal('reprovado'),
|
|
v.literal('data_ajustada')
|
|
),
|
|
lida: v.boolean(),
|
|
mensagem: v.string()
|
|
})
|
|
.index('by_destinatario', ['destinatarioId'])
|
|
.index('by_destinatario_and_lida', ['destinatarioId', 'lida']),
|
|
|
|
// Solicitações de Ausências
|
|
solicitacoesAusencias: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
dataInicio: v.string(),
|
|
dataFim: v.string(),
|
|
motivo: v.string(),
|
|
status: v.union(
|
|
v.literal('aguardando_aprovacao'),
|
|
v.literal('aprovado'),
|
|
v.literal('reprovado')
|
|
),
|
|
gestorId: v.optional(v.id('usuarios')),
|
|
dataAprovacao: v.optional(v.number()),
|
|
dataReprovacao: v.optional(v.number()),
|
|
motivoReprovacao: v.optional(v.string()),
|
|
observacao: v.optional(v.string()),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_status', ['status'])
|
|
.index('by_funcionario_and_status', ['funcionarioId', 'status']),
|
|
|
|
notificacoesAusencias: defineTable({
|
|
destinatarioId: v.id('usuarios'),
|
|
solicitacaoAusenciaId: v.id('solicitacoesAusencias'),
|
|
tipo: v.union(v.literal('nova_solicitacao'), v.literal('aprovado'), v.literal('reprovado')),
|
|
lida: v.boolean(),
|
|
mensagem: v.string()
|
|
})
|
|
.index('by_destinatario', ['destinatarioId'])
|
|
.index('by_destinatario_and_lida', ['destinatarioId', 'lida']),
|
|
|
|
times: defineTable({
|
|
nome: v.string(),
|
|
descricao: v.optional(v.string()),
|
|
gestorId: v.id('usuarios'),
|
|
gestorSuperiorId: v.optional(v.id('usuarios')),
|
|
ativo: v.boolean(),
|
|
cor: v.optional(v.string()) // Cor para identificação visual
|
|
})
|
|
.index('by_gestor', ['gestorId'])
|
|
.index('by_gestor_superior', ['gestorSuperiorId']),
|
|
|
|
timesMembros: defineTable({
|
|
timeId: v.id('times'),
|
|
funcionarioId: v.id('funcionarios'),
|
|
dataEntrada: v.number(),
|
|
dataSaida: v.optional(v.number()),
|
|
ativo: v.boolean()
|
|
})
|
|
.index('by_time', ['timeId'])
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_time_and_ativo', ['timeId', 'ativo']),
|
|
|
|
cursos: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
descricao: v.string(),
|
|
data: v.string(),
|
|
certificadoId: v.optional(v.id('_storage'))
|
|
}).index('by_funcionario', ['funcionarioId']),
|
|
|
|
simbolos: defineTable({
|
|
nome: v.string(),
|
|
tipo: simboloTipo,
|
|
descricao: v.string(),
|
|
vencValor: v.string(),
|
|
repValor: v.string(),
|
|
valor: v.string()
|
|
}),
|
|
|
|
// Sistema de Autenticação e Controle de Acesso
|
|
usuarios: defineTable({
|
|
authId: v.string(),
|
|
nome: v.string(),
|
|
email: v.string(),
|
|
funcionarioId: v.optional(v.id('funcionarios')),
|
|
roleId: v.id('roles'),
|
|
ativo: v.boolean(),
|
|
primeiroAcesso: v.boolean(),
|
|
ultimoAcesso: v.optional(v.number()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
|
|
// Controle de Bloqueio e Segurança
|
|
bloqueado: v.optional(v.boolean()),
|
|
motivoBloqueio: v.optional(v.string()),
|
|
dataBloqueio: v.optional(v.number()),
|
|
tentativasLogin: v.optional(v.number()), // contador de tentativas falhas
|
|
ultimaTentativaLogin: v.optional(v.number()), // timestamp da última tentativa
|
|
|
|
// Campos de Chat e Perfil
|
|
|
|
fotoPerfil: v.optional(v.id('_storage')),
|
|
avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
|
|
setor: v.optional(v.string()),
|
|
statusMensagem: v.optional(v.string()), // max 100 chars
|
|
statusPresenca: v.optional(
|
|
v.union(
|
|
v.literal('online'),
|
|
v.literal('offline'),
|
|
v.literal('ausente'),
|
|
v.literal('externo'),
|
|
v.literal('em_reuniao')
|
|
)
|
|
),
|
|
ultimaAtividade: v.optional(v.number()), // timestamp
|
|
notificacoesAtivadas: v.optional(v.boolean()),
|
|
somNotificacao: v.optional(v.boolean()),
|
|
temaPreferido: v.optional(v.string()) // tema de aparência escolhido pelo usuário
|
|
})
|
|
.index('by_email', ['email'])
|
|
.index('by_role', ['roleId'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_status_presenca', ['statusPresenca'])
|
|
.index('by_bloqueado', ['bloqueado'])
|
|
.index('by_funcionarioId', ['funcionarioId'])
|
|
.index('authId', ['authId']),
|
|
|
|
roles: defineTable({
|
|
nome: v.string(), // "admin", "ti_master", "ti_usuario", "usuario_avancado", "usuario"
|
|
descricao: v.string(),
|
|
nivel: v.number(), // 0 = admin, 1 = ti_master, 2 = ti_usuario, 3+ = customizado
|
|
setor: v.optional(v.string()), // "ti", "rh", "financeiro", etc.
|
|
customizado: v.optional(v.boolean()), // se é um perfil customizado criado por TI_MASTER
|
|
criadoPor: v.optional(v.id('usuarios')), // usuário TI_MASTER que criou este perfil
|
|
editavel: v.optional(v.boolean()) // se pode ser editado (false para roles fixas)
|
|
})
|
|
.index('by_nome', ['nome'])
|
|
.index('by_nivel', ['nivel'])
|
|
.index('by_setor', ['setor'])
|
|
.index('by_customizado', ['customizado']),
|
|
|
|
permissoes: defineTable({
|
|
nome: v.string(), // "funcionarios.criar", "simbolos.editar", etc.
|
|
descricao: v.string(),
|
|
recurso: v.string(), // "funcionarios", "simbolos", "usuarios", etc.
|
|
acao: v.string() // "criar", "ler", "editar", "excluir"
|
|
})
|
|
.index('by_recurso', ['recurso'])
|
|
.index('by_recurso_e_acao', ['recurso', 'acao'])
|
|
.index('by_nome', ['nome']),
|
|
|
|
rolePermissoes: defineTable({
|
|
roleId: v.id('roles'),
|
|
permissaoId: v.id('permissoes')
|
|
})
|
|
.index('by_role', ['roleId'])
|
|
.index('by_permissao', ['permissaoId']),
|
|
|
|
sessoes: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
token: v.string(),
|
|
ipAddress: v.optional(v.string()),
|
|
userAgent: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
expiraEm: v.number(),
|
|
ativo: v.boolean()
|
|
})
|
|
.index('by_usuario', ['usuarioId'])
|
|
.index('by_token', ['token'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_expiracao', ['expiraEm']),
|
|
|
|
logsAcesso: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
tipo: v.union(
|
|
v.literal('login'),
|
|
v.literal('logout'),
|
|
v.literal('acesso_negado'),
|
|
v.literal('senha_alterada'),
|
|
v.literal('sessao_expirada')
|
|
),
|
|
ipAddress: v.optional(v.string()),
|
|
userAgent: v.optional(v.string()),
|
|
detalhes: v.optional(v.string()),
|
|
timestamp: v.number()
|
|
})
|
|
.index('by_usuario', ['usuarioId'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_timestamp', ['timestamp']),
|
|
|
|
// Logs de Login Detalhados
|
|
logsLogin: defineTable({
|
|
usuarioId: v.optional(v.id('usuarios')), // pode ser null se falha antes de identificar usuário
|
|
matriculaOuEmail: v.string(), // tentativa de login
|
|
sucesso: v.boolean(),
|
|
motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente"
|
|
ipAddress: v.optional(v.string()),
|
|
userAgent: v.optional(v.string()),
|
|
device: v.optional(v.string()),
|
|
browser: v.optional(v.string()),
|
|
sistema: v.optional(v.string()),
|
|
timestamp: v.number()
|
|
})
|
|
.index('by_usuario', ['usuarioId'])
|
|
.index('by_sucesso', ['sucesso'])
|
|
.index('by_timestamp', ['timestamp'])
|
|
.index('by_ip', ['ipAddress']),
|
|
|
|
// Logs de Atividades
|
|
logsAtividades: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
acao: v.string(), // "criar", "editar", "excluir", "bloquear", "desbloquear", etc.
|
|
recurso: v.string(), // "funcionarios", "simbolos", "usuarios", "perfis", etc.
|
|
recursoId: v.optional(v.string()), // ID do recurso afetado
|
|
detalhes: v.optional(v.string()), // JSON com detalhes da ação
|
|
timestamp: v.number()
|
|
})
|
|
.index('by_usuario', ['usuarioId'])
|
|
.index('by_acao', ['acao'])
|
|
.index('by_recurso', ['recurso'])
|
|
.index('by_timestamp', ['timestamp'])
|
|
.index('by_recurso_id', ['recurso', 'recursoId']),
|
|
|
|
// Histórico de Bloqueios
|
|
bloqueiosUsuarios: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
motivo: v.string(),
|
|
bloqueadoPor: v.id('usuarios'), // ID do TI_MASTER que bloqueou
|
|
dataInicio: v.number(),
|
|
dataFim: v.optional(v.number()), // quando foi desbloqueado
|
|
desbloqueadoPor: v.optional(v.id('usuarios')),
|
|
ativo: v.boolean() // se é o bloqueio atual ativo
|
|
})
|
|
.index('by_usuario', ['usuarioId'])
|
|
.index('by_bloqueado_por', ['bloqueadoPor'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_data_inicio', ['dataInicio']),
|
|
|
|
// Perfis Customizados
|
|
|
|
// Templates de Mensagens
|
|
templatesMensagens: defineTable({
|
|
codigo: v.string(), // "USUARIO_BLOQUEADO", "SENHA_RESETADA", etc.
|
|
nome: v.string(),
|
|
tipo: v.union(
|
|
v.literal('sistema'), // predefinido, não editável
|
|
v.literal('customizado') // criado por TI_MASTER
|
|
),
|
|
titulo: v.string(),
|
|
corpo: v.string(), // pode ter variáveis {{variavel}}
|
|
variaveis: v.optional(v.array(v.string())), // ["motivo", "senha", etc.]
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_codigo', ['codigo'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_criado_por', ['criadoPor']),
|
|
|
|
// Configuração de Email/SMTP
|
|
configuracaoEmail: defineTable({
|
|
servidor: v.string(), // smtp.gmail.com
|
|
porta: v.number(), // 587, 465, etc.
|
|
usuario: v.string(),
|
|
senhaHash: v.string(), // senha criptografada reversível (AES-GCM) - necessário para descriptografar e usar no SMTP
|
|
emailRemetente: v.string(),
|
|
nomeRemetente: v.string(),
|
|
usarSSL: v.boolean(),
|
|
usarTLS: v.boolean(),
|
|
ativo: v.boolean(),
|
|
testadoEm: v.optional(v.number()),
|
|
configuradoPor: v.id('usuarios'),
|
|
atualizadoEm: v.number()
|
|
}).index('by_ativo', ['ativo']),
|
|
|
|
// Configuração de Jitsi Meet
|
|
configuracaoJitsi: defineTable({
|
|
domain: v.string(), // Domínio do servidor Jitsi (ex: "localhost:8443" ou "meet.example.com")
|
|
appId: v.string(), // ID da aplicação Jitsi
|
|
roomPrefix: v.string(), // Prefixo para nomes de salas
|
|
useHttps: v.boolean(), // Usar HTTPS
|
|
acceptSelfSignedCert: v.optional(v.boolean()), // Aceitar certificados autoassinados (útil para desenvolvimento)
|
|
// Configurações SSH/Docker para configuração automática do servidor
|
|
sshHost: v.optional(v.string()), // Host SSH para acesso ao servidor Docker (ex: "192.168.1.100" ou "servidor.local")
|
|
sshPort: v.optional(v.number()), // Porta SSH (padrão: 22)
|
|
sshUsername: v.optional(v.string()), // Usuário SSH
|
|
sshPasswordHash: v.optional(v.string()), // Hash da senha SSH (criptografada)
|
|
sshKeyPath: v.optional(v.string()), // Caminho para chave SSH (alternativa à senha)
|
|
dockerComposePath: v.optional(v.string()), // Caminho do docker-compose.yml (ex: "/home/user/jitsi-docker")
|
|
jitsiConfigPath: v.optional(v.string()), // Caminho base das configurações Jitsi (ex: "~/.jitsi-meet-cfg")
|
|
ativo: v.boolean(), // Configuração ativa
|
|
testadoEm: v.optional(v.number()), // Timestamp do último teste de conexão
|
|
configuradoEm: v.optional(v.number()), // Timestamp da última configuração do servidor Docker
|
|
configuradoNoServidor: v.optional(v.boolean()), // Indica se a configuração foi aplicada no servidor
|
|
configuradoNoServidorEm: v.optional(v.number()), // Timestamp de quando foi configurado no servidor
|
|
configuradoPor: v.id('usuarios'), // Usuário que configurou
|
|
atualizadoEm: v.number() // Timestamp de atualização
|
|
}).index('by_ativo', ['ativo']),
|
|
|
|
// Fila de Emails
|
|
notificacoesEmail: defineTable({
|
|
destinatario: v.string(), // email
|
|
destinatarioId: v.optional(v.id('usuarios')),
|
|
assunto: v.string(),
|
|
corpo: v.string(), // HTML ou texto
|
|
templateId: v.optional(v.id('templatesMensagens')),
|
|
status: v.union(
|
|
v.literal('pendente'),
|
|
v.literal('enviando'),
|
|
v.literal('enviado'),
|
|
v.literal('falha')
|
|
),
|
|
tentativas: v.number(),
|
|
ultimaTentativa: v.optional(v.number()),
|
|
erroDetalhes: v.optional(v.string()),
|
|
enviadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
enviadoEm: v.optional(v.number()),
|
|
agendadaPara: v.optional(v.number()) // timestamp para agendamento
|
|
})
|
|
.index('by_status', ['status'])
|
|
.index('by_destinatario', ['destinatarioId'])
|
|
.index('by_enviado_por', ['enviadoPor'])
|
|
.index('by_criado_em', ['criadoEm'])
|
|
.index('by_agendamento', ['agendadaPara']),
|
|
|
|
configuracaoAcesso: defineTable({
|
|
chave: v.string(), // "sessao_duracao", "max_tentativas_login", etc.
|
|
valor: v.string(),
|
|
descricao: v.string()
|
|
}).index('by_chave', ['chave']),
|
|
|
|
// Rate Limiting de Emails
|
|
rateLimitEmails: defineTable({
|
|
remetenteId: v.id('usuarios'),
|
|
timestamp: v.number(),
|
|
contador: v.number(), // quantidade de emails enviados neste período
|
|
periodo: v.union(
|
|
v.literal('minuto'), // último minuto
|
|
v.literal('hora') // última hora
|
|
)
|
|
})
|
|
.index('by_remetente_periodo', ['remetenteId', 'periodo', 'timestamp'])
|
|
.index('by_timestamp', ['timestamp']),
|
|
|
|
// Sistema de Chat
|
|
conversas: defineTable({
|
|
tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')),
|
|
nome: v.optional(v.string()), // nome do grupo/sala
|
|
|
|
participantes: v.array(v.id('usuarios')), // IDs dos participantes
|
|
administradores: v.optional(v.array(v.id('usuarios'))), // IDs dos administradores (apenas para sala_reuniao)
|
|
ultimaMensagem: v.optional(v.string()),
|
|
ultimaMensagemTimestamp: v.optional(v.number()),
|
|
ultimaMensagemRemetenteId: v.optional(v.id('usuarios')), // ID do remetente da última mensagem
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_criado_por', ['criadoPor'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_ultima_mensagem', ['ultimaMensagemTimestamp']),
|
|
|
|
mensagens: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
remetenteId: v.id('usuarios'),
|
|
tipo: v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem')),
|
|
conteudo: v.string(), // texto ou nome do arquivo
|
|
conteudoBusca: v.optional(v.string()), // versão normalizada para busca
|
|
arquivoId: v.optional(v.id('_storage')),
|
|
arquivoNome: v.optional(v.string()),
|
|
arquivoTamanho: v.optional(v.number()),
|
|
arquivoTipo: v.optional(v.string()),
|
|
linkPreview: v.optional(
|
|
v.object({
|
|
url: v.string(),
|
|
titulo: v.optional(v.string()),
|
|
descricao: v.optional(v.string()),
|
|
imagem: v.optional(v.string()),
|
|
site: v.optional(v.string())
|
|
})
|
|
),
|
|
reagiuPor: v.optional(
|
|
v.array(
|
|
v.object({
|
|
usuarioId: v.id('usuarios'),
|
|
emoji: v.string()
|
|
})
|
|
)
|
|
),
|
|
mencoes: v.optional(v.array(v.id('usuarios'))),
|
|
respostaPara: v.optional(v.id('mensagens')), // ID da mensagem que está respondendo
|
|
agendadaPara: v.optional(v.number()), // timestamp
|
|
enviadaEm: v.number(),
|
|
editadaEm: v.optional(v.number()),
|
|
deletada: v.optional(v.boolean()),
|
|
lidaPor: v.optional(v.array(v.id('usuarios'))) // IDs dos usuários que leram a mensagem
|
|
})
|
|
.index('by_conversa', ['conversaId', 'enviadaEm'])
|
|
.index('by_remetente', ['remetenteId'])
|
|
.index('by_agendamento', ['agendadaPara'])
|
|
.index('by_resposta', ['respostaPara']),
|
|
|
|
leituras: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
usuarioId: v.id('usuarios'),
|
|
ultimaMensagemLida: v.id('mensagens'),
|
|
lidaEm: v.number()
|
|
})
|
|
.index('by_conversa_usuario', ['conversaId', 'usuarioId'])
|
|
.index('by_usuario', ['usuarioId']),
|
|
|
|
// Sistema de Chamadas de Áudio/Vídeo
|
|
chamadas: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
tipo: v.union(v.literal('audio'), v.literal('video')),
|
|
roomName: v.string(), // Nome único da sala Jitsi
|
|
criadoPor: v.id('usuarios'), // Anfitrião/criador
|
|
participantes: v.array(v.id('usuarios')),
|
|
status: v.union(
|
|
v.literal('aguardando'),
|
|
v.literal('em_andamento'),
|
|
v.literal('finalizada'),
|
|
v.literal('cancelada')
|
|
),
|
|
iniciadaEm: v.optional(v.number()),
|
|
finalizadaEm: v.optional(v.number()),
|
|
duracaoSegundos: v.optional(v.number()),
|
|
gravando: v.boolean(),
|
|
gravacaoIniciadaPor: v.optional(v.id('usuarios')),
|
|
gravacaoIniciadaEm: v.optional(v.number()),
|
|
gravacaoFinalizadaEm: v.optional(v.number()),
|
|
configuracoes: v.optional(
|
|
v.object({
|
|
audioHabilitado: v.boolean(),
|
|
videoHabilitado: v.boolean(),
|
|
participantesConfig: v.optional(
|
|
v.array(
|
|
v.object({
|
|
usuarioId: v.id('usuarios'),
|
|
audioHabilitado: v.boolean(),
|
|
videoHabilitado: v.boolean(),
|
|
forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião
|
|
})
|
|
)
|
|
)
|
|
})
|
|
),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_conversa', ['conversaId', 'status'])
|
|
.index('by_criado_por', ['criadoPor'])
|
|
.index('by_status', ['status'])
|
|
.index('by_room_name', ['roomName']),
|
|
|
|
notificacoes: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
tipo: v.union(
|
|
v.literal('nova_mensagem'),
|
|
v.literal('mencao'),
|
|
v.literal('grupo_criado'),
|
|
v.literal('adicionado_grupo'),
|
|
v.literal('alerta_seguranca'),
|
|
v.literal('etapa_fluxo_concluida')
|
|
),
|
|
conversaId: v.optional(v.id('conversas')),
|
|
mensagemId: v.optional(v.id('mensagens')),
|
|
remetenteId: v.optional(v.id('usuarios')),
|
|
titulo: v.string(),
|
|
descricao: v.string(),
|
|
lida: v.boolean(),
|
|
criadaEm: v.number()
|
|
})
|
|
.index('by_usuario', ['usuarioId', 'lida', 'criadaEm'])
|
|
.index('by_usuario_lida', ['usuarioId', 'lida']),
|
|
|
|
digitando: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
usuarioId: v.id('usuarios'),
|
|
iniciouEm: v.number()
|
|
})
|
|
.index('by_conversa', ['conversaId', 'iniciouEm'])
|
|
.index('by_usuario', ['usuarioId']),
|
|
|
|
// Push Notifications
|
|
pushSubscriptions: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
endpoint: v.string(), // URL do serviço de push
|
|
keys: v.object({
|
|
p256dh: v.string(), // Chave pública
|
|
auth: v.string() // Chave de autenticação
|
|
}),
|
|
userAgent: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
ultimaAtividade: v.number(),
|
|
ativo: v.boolean()
|
|
})
|
|
.index('by_usuario', ['usuarioId', 'ativo'])
|
|
.index('by_endpoint', ['endpoint']),
|
|
|
|
// Preferências de Notificação por Conversa
|
|
preferenciasNotificacaoConversa: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
conversaId: v.id('conversas'),
|
|
pushAtivado: v.boolean(), // Receber push notifications
|
|
emailAtivado: v.boolean(), // Receber emails quando offline
|
|
somAtivado: v.boolean(), // Tocar som
|
|
silenciado: v.boolean(), // Silenciar completamente
|
|
apenasMencoes: v.boolean(), // Notificar apenas quando mencionado
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_usuario_conversa', ['usuarioId', 'conversaId'])
|
|
.index('by_conversa', ['conversaId']),
|
|
|
|
// Tabelas de Monitoramento do Sistema
|
|
systemMetrics: defineTable({
|
|
timestamp: v.number(),
|
|
// Métricas de Sistema
|
|
cpuUsage: v.optional(v.number()),
|
|
memoryUsage: v.optional(v.number()),
|
|
networkLatency: v.optional(v.number()),
|
|
storageUsed: v.optional(v.number()),
|
|
// Métricas de Aplicação
|
|
usuariosOnline: v.optional(v.number()),
|
|
mensagensPorMinuto: v.optional(v.number()),
|
|
tempoRespostaMedio: v.optional(v.number()),
|
|
errosCount: v.optional(v.number())
|
|
}).index('by_timestamp', ['timestamp']),
|
|
|
|
alertConfigurations: defineTable({
|
|
metricName: v.string(),
|
|
threshold: v.number(),
|
|
operator: v.union(
|
|
v.literal('>'),
|
|
v.literal('<'),
|
|
v.literal('>='),
|
|
v.literal('<='),
|
|
v.literal('==')
|
|
),
|
|
enabled: v.boolean(),
|
|
notifyByEmail: v.boolean(),
|
|
notifyByChat: v.boolean(),
|
|
createdBy: v.id('usuarios'),
|
|
lastModified: v.number()
|
|
}).index('by_enabled', ['enabled']),
|
|
|
|
alertHistory: defineTable({
|
|
configId: v.id('alertConfigurations'),
|
|
metricName: v.string(),
|
|
metricValue: v.number(),
|
|
threshold: v.number(),
|
|
timestamp: v.number(),
|
|
status: v.union(v.literal('triggered'), v.literal('resolved')),
|
|
notificationsSent: v.object({
|
|
email: v.boolean(),
|
|
chat: v.boolean()
|
|
})
|
|
})
|
|
.index('by_timestamp', ['timestamp'])
|
|
.index('by_status', ['status'])
|
|
.index('by_config', ['configId', 'timestamp']),
|
|
|
|
tickets: defineTable({
|
|
numero: v.string(),
|
|
titulo: v.string(),
|
|
descricao: v.string(),
|
|
tipo: v.union(
|
|
v.literal('reclamacao'),
|
|
v.literal('elogio'),
|
|
v.literal('sugestao'),
|
|
v.literal('chamado')
|
|
),
|
|
categoria: v.optional(v.string()),
|
|
status: v.union(
|
|
v.literal('aberto'),
|
|
v.literal('em_andamento'),
|
|
v.literal('aguardando_usuario'),
|
|
v.literal('resolvido'),
|
|
v.literal('encerrado'),
|
|
v.literal('cancelado')
|
|
),
|
|
prioridade: v.union(
|
|
v.literal('baixa'),
|
|
v.literal('media'),
|
|
v.literal('alta'),
|
|
v.literal('critica')
|
|
),
|
|
solicitanteId: v.id('usuarios'),
|
|
solicitanteNome: v.string(),
|
|
solicitanteEmail: v.string(),
|
|
responsavelId: v.optional(v.id('usuarios')),
|
|
setorResponsavel: v.optional(v.string()),
|
|
slaConfigId: v.optional(v.id('slaConfigs')),
|
|
conversaId: v.optional(v.id('conversas')),
|
|
prazoResposta: v.optional(v.number()),
|
|
prazoConclusao: v.optional(v.number()),
|
|
prazoEncerramento: v.optional(v.number()),
|
|
timeline: v.optional(
|
|
v.array(
|
|
v.object({
|
|
etapa: v.string(),
|
|
status: v.union(
|
|
v.literal('pendente'),
|
|
v.literal('em_andamento'),
|
|
v.literal('concluido'),
|
|
v.literal('vencido')
|
|
),
|
|
prazo: v.optional(v.number()),
|
|
concluidoEm: v.optional(v.number()),
|
|
observacao: v.optional(v.string())
|
|
})
|
|
)
|
|
),
|
|
alertasEmitidos: v.optional(
|
|
v.array(
|
|
v.object({
|
|
tipo: v.union(v.literal('resposta'), v.literal('conclusao'), v.literal('encerramento')),
|
|
emitidoEm: v.number()
|
|
})
|
|
)
|
|
),
|
|
anexos: v.optional(
|
|
v.array(
|
|
v.object({
|
|
arquivoId: v.id('_storage'),
|
|
nome: v.optional(v.string()),
|
|
tipo: v.optional(v.string()),
|
|
tamanho: v.optional(v.number())
|
|
})
|
|
)
|
|
),
|
|
tags: v.optional(v.array(v.string())),
|
|
canalOrigem: v.optional(v.string()),
|
|
ultimaInteracaoEm: v.number(),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_numero', ['numero'])
|
|
.index('by_status', ['status'])
|
|
.index('by_solicitante', ['solicitanteId', 'status'])
|
|
.index('by_responsavel', ['responsavelId', 'status'])
|
|
.index('by_setor', ['setorResponsavel', 'status']),
|
|
|
|
ticketInteractions: defineTable({
|
|
ticketId: v.id('tickets'),
|
|
autorId: v.optional(v.id('usuarios')),
|
|
origem: v.union(v.literal('usuario'), v.literal('ti'), v.literal('sistema')),
|
|
tipo: v.union(
|
|
v.literal('mensagem'),
|
|
v.literal('status'),
|
|
v.literal('anexo'),
|
|
v.literal('alerta')
|
|
),
|
|
conteudo: v.string(),
|
|
anexos: v.optional(
|
|
v.array(
|
|
v.object({
|
|
arquivoId: v.id('_storage'),
|
|
nome: v.optional(v.string()),
|
|
tipo: v.optional(v.string()),
|
|
tamanho: v.optional(v.number())
|
|
})
|
|
)
|
|
),
|
|
statusAnterior: v.optional(
|
|
v.union(
|
|
v.literal('aberto'),
|
|
v.literal('em_andamento'),
|
|
v.literal('aguardando_usuario'),
|
|
v.literal('resolvido'),
|
|
v.literal('encerrado'),
|
|
v.literal('cancelado')
|
|
)
|
|
),
|
|
statusNovo: v.optional(
|
|
v.union(
|
|
v.literal('aberto'),
|
|
v.literal('em_andamento'),
|
|
v.literal('aguardando_usuario'),
|
|
v.literal('resolvido'),
|
|
v.literal('encerrado'),
|
|
v.literal('cancelado')
|
|
)
|
|
),
|
|
visibilidade: v.union(v.literal('publico'), v.literal('interno')),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_ticket', ['ticketId'])
|
|
.index('by_ticket_type', ['ticketId', 'tipo'])
|
|
.index('by_autor', ['autorId']),
|
|
|
|
slaConfigs: defineTable({
|
|
nome: v.string(),
|
|
descricao: v.optional(v.string()),
|
|
prioridade: v.optional(
|
|
v.union(v.literal('baixa'), v.literal('media'), v.literal('alta'), v.literal('critica'))
|
|
),
|
|
tempoRespostaHoras: v.number(),
|
|
tempoConclusaoHoras: v.number(),
|
|
tempoEncerramentoHoras: v.optional(v.number()),
|
|
alertaAntecedenciaHoras: v.number(),
|
|
ativo: v.boolean(),
|
|
criadoPor: v.id('usuarios'),
|
|
atualizadoPor: v.optional(v.id('usuarios')),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_prioridade', ['prioridade', 'ativo'])
|
|
.index('by_nome', ['nome']),
|
|
|
|
ticketAssignments: defineTable({
|
|
ticketId: v.id('tickets'),
|
|
responsavelId: v.id('usuarios'),
|
|
atribuidoPor: v.id('usuarios'),
|
|
motivo: v.optional(v.string()),
|
|
ativo: v.boolean(),
|
|
criadoEm: v.number(),
|
|
encerradoEm: v.optional(v.number())
|
|
})
|
|
.index('by_ticket', ['ticketId', 'ativo'])
|
|
.index('by_responsavel', ['responsavelId', 'ativo']),
|
|
|
|
// Sistema de Segurança Cibernética
|
|
networkSensors: defineTable({
|
|
nome: v.string(),
|
|
tipo: sensorSegurancaTipo,
|
|
status: sensorSegurancaStatus,
|
|
escopo: v.optional(v.string()),
|
|
ipMonitorado: v.optional(v.string()),
|
|
hostname: v.optional(v.string()),
|
|
regioes: v.optional(v.array(v.string())),
|
|
portasMonitoradas: v.optional(v.array(v.number())),
|
|
protocolos: v.optional(v.array(v.string())),
|
|
capacidades: v.optional(v.array(v.string())),
|
|
ultimaSincronizacao: v.number(),
|
|
ultimoHeartbeat: v.optional(v.number()),
|
|
latenciaMs: v.optional(v.number()),
|
|
errosConsecutivos: v.optional(v.number()),
|
|
agenteVersao: v.optional(v.string()),
|
|
notas: v.optional(v.string())
|
|
})
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_status', ['status'])
|
|
.index('by_hostname', ['hostname']),
|
|
|
|
ipReputation: defineTable({
|
|
indicador: v.string(),
|
|
categoria: v.union(
|
|
v.literal('ip'),
|
|
v.literal('dominio'),
|
|
v.literal('hash'),
|
|
v.literal('email')
|
|
),
|
|
reputacao: v.number(), // -100 (malicioso) até 100 (confiável)
|
|
severidadeMax: severidadeSeguranca,
|
|
whitelist: v.boolean(),
|
|
blacklist: v.boolean(),
|
|
ocorrencias: v.number(),
|
|
primeiroRegistro: v.number(),
|
|
ultimoRegistro: v.number(),
|
|
bloqueadoAte: v.optional(v.number()),
|
|
origem: v.optional(v.string()),
|
|
comentarios: v.optional(v.string()),
|
|
classificacoes: v.optional(v.array(v.string())),
|
|
ultimaAcaoId: v.optional(v.id('incidentActions'))
|
|
})
|
|
.index('by_indicador', ['indicador'])
|
|
.index('by_reputacao', ['reputacao'])
|
|
.index('by_blacklist', ['blacklist'])
|
|
.index('by_whitelist', ['whitelist']),
|
|
|
|
portRules: defineTable({
|
|
porta: v.number(),
|
|
protocolo: v.union(
|
|
v.literal('tcp'),
|
|
v.literal('udp'),
|
|
v.literal('icmp'),
|
|
v.literal('quic'),
|
|
v.literal('any')
|
|
),
|
|
acao: v.union(
|
|
v.literal('permitir'),
|
|
v.literal('bloquear'),
|
|
v.literal('monitorar'),
|
|
v.literal('rate_limit')
|
|
),
|
|
temporario: v.boolean(),
|
|
severidadeMin: severidadeSeguranca,
|
|
duracaoSegundos: v.optional(v.number()),
|
|
expiraEm: v.optional(v.number()),
|
|
criadoPor: v.id('usuarios'),
|
|
atualizadoPor: v.optional(v.id('usuarios')),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
notas: v.optional(v.string()),
|
|
tags: v.optional(v.array(v.string())),
|
|
listaReferencia: v.optional(v.id('ipReputation'))
|
|
})
|
|
.index('by_porta_protocolo', ['porta', 'protocolo'])
|
|
.index('by_acao', ['acao'])
|
|
.index('by_expiracao', ['expiraEm']),
|
|
|
|
threatIntelFeeds: defineTable({
|
|
nomeFonte: v.string(),
|
|
tipo: threatIntelTipo,
|
|
formato: threatIntelFormato,
|
|
url: v.optional(v.string()),
|
|
ativo: v.boolean(),
|
|
prioridade: v.union(
|
|
v.literal('baixa'),
|
|
v.literal('media'),
|
|
v.literal('alta'),
|
|
v.literal('critica')
|
|
),
|
|
ultimaSincronizacao: v.optional(v.number()),
|
|
entradasProcessadas: v.optional(v.number()),
|
|
errosConsecutivos: v.optional(v.number()),
|
|
autenticacaoNecessaria: v.optional(v.boolean()),
|
|
configuracao: v.optional(
|
|
v.object({
|
|
tokenId: v.optional(v.id('_storage')),
|
|
escopo: v.optional(v.string())
|
|
})
|
|
),
|
|
criadoPor: v.id('usuarios'),
|
|
atualizadoPor: v.optional(v.id('usuarios')),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_prioridade', ['prioridade']),
|
|
|
|
securityEvents: defineTable({
|
|
referencia: v.string(),
|
|
timestamp: v.number(),
|
|
tipoAtaque: ataqueCiberneticoTipo,
|
|
severidade: severidadeSeguranca,
|
|
status: statusEventoSeguranca,
|
|
descricao: v.string(),
|
|
origemIp: v.optional(v.string()),
|
|
origemRegiao: v.optional(v.string()),
|
|
origemAsn: v.optional(v.string()),
|
|
destinoIp: v.optional(v.string()),
|
|
destinoPorta: v.optional(v.number()),
|
|
protocolo: v.optional(v.string()),
|
|
transporte: v.optional(v.string()),
|
|
sensorId: v.optional(v.id('networkSensors')),
|
|
detectadoPor: v.optional(v.string()),
|
|
mitreTechnique: v.optional(v.string()),
|
|
geolocalizacao: v.optional(
|
|
v.object({
|
|
pais: v.optional(v.string()),
|
|
regiao: v.optional(v.string()),
|
|
cidade: v.optional(v.string()),
|
|
latitude: v.optional(v.number()),
|
|
longitude: v.optional(v.number())
|
|
})
|
|
),
|
|
fingerprint: v.optional(
|
|
v.object({
|
|
userAgent: v.optional(v.string()),
|
|
deviceId: v.optional(v.string()),
|
|
ja3: v.optional(v.string()),
|
|
tlsVersion: v.optional(v.string())
|
|
})
|
|
),
|
|
indicadores: v.optional(
|
|
v.array(
|
|
v.object({
|
|
tipo: v.string(),
|
|
valor: v.string(),
|
|
confianca: v.optional(v.number())
|
|
})
|
|
)
|
|
),
|
|
metricas: v.optional(
|
|
v.object({
|
|
pps: v.optional(v.number()),
|
|
bps: v.optional(v.number()),
|
|
rpm: v.optional(v.number()),
|
|
errosPorSegundo: v.optional(v.number()),
|
|
hostsAfetados: v.optional(v.number())
|
|
})
|
|
),
|
|
correlacoes: v.optional(v.array(v.id('securityEvents'))),
|
|
referenciasExternas: v.optional(v.array(v.string())),
|
|
tags: v.optional(v.array(v.string())),
|
|
criadoPor: v.optional(v.id('usuarios')),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_referencia', ['referencia'])
|
|
.index('by_timestamp', ['timestamp'])
|
|
.index('by_tipo', ['tipoAtaque', 'timestamp'])
|
|
.index('by_severidade', ['severidade', 'timestamp'])
|
|
.index('by_status', ['status', 'timestamp']),
|
|
|
|
incidentActions: defineTable({
|
|
eventoId: v.id('securityEvents'),
|
|
tipo: acaoIncidenteTipo,
|
|
origem: v.union(v.literal('automatico'), v.literal('manual')),
|
|
status: acaoIncidenteStatus,
|
|
executadoPor: v.optional(v.id('usuarios')),
|
|
detalhes: v.optional(v.string()),
|
|
resultado: v.optional(v.string()),
|
|
relacionadoA: v.optional(v.id('ipReputation')),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_evento', ['eventoId', 'status'])
|
|
.index('by_tipo', ['tipo', 'status']),
|
|
|
|
reportRequests: defineTable({
|
|
solicitanteId: v.id('usuarios'),
|
|
filtros: v.object({
|
|
dataInicio: v.number(),
|
|
dataFim: v.number(),
|
|
severidades: v.optional(v.array(severidadeSeguranca)),
|
|
tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
|
|
incluirIndicadores: v.optional(v.boolean()),
|
|
incluirMetricas: v.optional(v.boolean()),
|
|
incluirAcoes: v.optional(v.boolean())
|
|
}),
|
|
status: reportStatus,
|
|
resultadoId: v.optional(v.id('_storage')),
|
|
observacoes: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
concluidoEm: v.optional(v.number()),
|
|
erro: v.optional(v.string())
|
|
})
|
|
.index('by_status', ['status'])
|
|
.index('by_solicitante', ['solicitanteId', 'status'])
|
|
.index('by_criado_em', ['criadoEm']),
|
|
|
|
rateLimitConfig: defineTable({
|
|
nome: v.string(),
|
|
tipo: v.union(
|
|
v.literal('ip'),
|
|
v.literal('usuario'),
|
|
v.literal('endpoint'),
|
|
v.literal('global')
|
|
),
|
|
identificador: v.optional(v.string()),
|
|
limite: v.number(),
|
|
janelaSegundos: v.number(),
|
|
estrategia: v.union(
|
|
v.literal('fixed_window'),
|
|
v.literal('sliding_window'),
|
|
v.literal('token_bucket')
|
|
),
|
|
acaoExcedido: v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')),
|
|
bloqueioTemporarioSegundos: v.optional(v.number()),
|
|
ativo: v.boolean(),
|
|
prioridade: v.number(),
|
|
criadoPor: v.id('usuarios'),
|
|
atualizadoPor: v.optional(v.id('usuarios')),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
notas: v.optional(v.string()),
|
|
tags: v.optional(v.array(v.string()))
|
|
})
|
|
.index('by_tipo_identificador', ['tipo', 'identificador'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_prioridade', ['prioridade']),
|
|
alertConfigs: defineTable({
|
|
nome: v.string(),
|
|
canais: v.object({
|
|
email: v.boolean(),
|
|
chat: v.boolean()
|
|
}),
|
|
emails: v.array(v.string()),
|
|
chatUsers: v.array(v.string()),
|
|
severidadeMin: severidadeSeguranca,
|
|
tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
|
|
reenvioMin: v.number(),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
}).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()),
|
|
altitude: v.optional(v.union(v.number(), v.null())),
|
|
altitudeAccuracy: v.optional(v.union(v.number(), v.null())),
|
|
heading: v.optional(v.union(v.number(), v.null())),
|
|
speed: v.optional(v.union(v.number(), v.null())),
|
|
confiabilidadeGPS: v.optional(v.number()), // 0-1 (frontend)
|
|
scoreConfiancaBackend: v.optional(v.number()), // 0-1 (backend)
|
|
suspeitaSpoofing: v.optional(v.boolean()),
|
|
motivoSuspeita: v.optional(v.string()),
|
|
avisosValidacao: v.optional(v.array(v.string())), // Array de avisos detalhados da validação
|
|
distanciaIPvsGPS: v.optional(v.number()), // Distância em metros entre IP geolocation e GPS
|
|
velocidadeUltimoRegistro: v.optional(v.number()), // Velocidade em km/h do último registro
|
|
distanciaUltimoRegistro: v.optional(v.number()), // Distância em metros do último registro
|
|
tempoDecorridoHoras: v.optional(v.number()), // Tempo em horas desde o último registro
|
|
// Informações de Geofencing
|
|
enderecoMarcacaoEsperado: v.optional(v.id('enderecosMarcacao')), // Endereço mais próximo esperado
|
|
distanciaEnderecoEsperado: v.optional(v.number()), // Distância em metros do endereço esperado
|
|
dentroRaioPermitido: v.optional(v.boolean()), // Se está dentro do raio permitido
|
|
enderecoMarcacaoUsado: v.optional(v.id('enderecosMarcacao')), // Qual endereço foi usado na validação
|
|
raioToleranciaUsado: v.optional(v.number()), // Raio usado na validação em metros
|
|
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()),
|
|
|
|
// Informações de Sensores (Acelerômetro e Giroscópio)
|
|
acelerometroX: v.optional(v.number()),
|
|
acelerometroY: v.optional(v.number()),
|
|
acelerometroZ: v.optional(v.number()),
|
|
movimentoDetectado: v.optional(v.boolean()),
|
|
magnitudeMovimento: v.optional(v.number()),
|
|
variacaoAcelerometro: v.optional(v.number()),
|
|
giroscopioAlpha: v.optional(v.number()),
|
|
giroscopioBeta: v.optional(v.number()),
|
|
giroscopioGamma: v.optional(v.number()),
|
|
sensorDisponivel: v.optional(v.boolean()),
|
|
permissaoSensorNegada: v.optional(v.boolean()),
|
|
|
|
// Justificativa opcional para o registro
|
|
justificativa: v.optional(v.string()),
|
|
|
|
// Campos para homologação
|
|
editadoPorGestor: v.optional(v.boolean()),
|
|
homologacaoId: v.optional(v.id('homologacoesPonto')),
|
|
|
|
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']),
|
|
|
|
// Endereços de Marcação - Locais permitidos para registro de ponto
|
|
enderecosMarcacao: defineTable({
|
|
nome: v.string(), // Ex: "Sede Principal", "Home Office João Silva", "Cliente ABC"
|
|
descricao: v.optional(v.string()), // Descrição opcional
|
|
// Coordenadas (obrigatórias)
|
|
latitude: v.number(),
|
|
longitude: v.number(),
|
|
// Endereço físico (para exibição)
|
|
endereco: v.string(), // Ex: "Rua Exemplo, 123"
|
|
bairro: v.optional(v.string()), // Bairro do endereço
|
|
cep: v.optional(v.string()),
|
|
cidade: v.string(),
|
|
estado: v.string(),
|
|
pais: v.optional(v.string()), // Padrão: "Brasil"
|
|
// Configurações
|
|
raioMetros: v.number(), // Raio de tolerância em metros (ex: 100m, 500m, 1000m)
|
|
ativo: v.boolean(),
|
|
// Tipos de uso
|
|
tipo: v.union(
|
|
v.literal('sede'), // Sede principal (para todos)
|
|
v.literal('home_office'), // Home office específico
|
|
v.literal('deslocamento'), // Deslocamento temporário
|
|
v.literal('cliente') // Local de cliente
|
|
),
|
|
// Metadados
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoPor: v.optional(v.id('usuarios')),
|
|
atualizadoEm: v.optional(v.number())
|
|
})
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_cidade', ['cidade']),
|
|
|
|
// Associação Funcionário ↔ Endereço de Marcação
|
|
funcionarioEnderecosMarcacao: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
enderecoMarcacaoId: v.id('enderecosMarcacao'),
|
|
// Configurações específicas do funcionário
|
|
raioMetrosPersonalizado: v.optional(v.number()), // Pode ter raio diferente do padrão
|
|
// Período de validade (para deslocamentos temporários)
|
|
dataInicio: v.optional(v.string()), // YYYY-MM-DD
|
|
dataFim: v.optional(v.string()), // YYYY-MM-DD
|
|
// Status
|
|
ativo: v.boolean(),
|
|
// Metadados
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_endereco', ['enderecoMarcacaoId'])
|
|
.index('by_funcionario_ativo', ['funcionarioId', 'ativo'])
|
|
.index('by_endereco_ativo', ['enderecoMarcacaoId', 'ativo']),
|
|
|
|
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)
|
|
// Configurações de geofencing
|
|
validarLocalizacao: v.optional(v.boolean()), // Habilitar/desabilitar validação de localização
|
|
toleranciaDistanciaMetros: v.optional(v.number()), // Raio padrão global em metros
|
|
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']),
|
|
|
|
// Homologações de Ponto - Edições e ajustes realizados pelo gestor
|
|
homologacoesPonto: defineTable({
|
|
registroId: v.optional(v.id('registrosPonto')), // ID do registro editado (se for edição)
|
|
funcionarioId: v.id('funcionarios'),
|
|
gestorId: v.id('usuarios'),
|
|
// Dados do registro original (se for edição)
|
|
horaAnterior: v.optional(v.number()),
|
|
minutoAnterior: v.optional(v.number()),
|
|
// Dados do registro novo (se for edição)
|
|
horaNova: v.optional(v.number()),
|
|
minutoNova: v.optional(v.number()),
|
|
// Motivo e observações
|
|
motivoId: v.optional(v.string()), // ID do motivo (referência a atestados/declarações)
|
|
motivoTipo: v.optional(v.string()), // Tipo do motivo (atestado, declaracao, etc)
|
|
motivoDescricao: v.optional(v.string()), // Descrição do motivo
|
|
observacoes: v.optional(v.string()),
|
|
// Tipo de ajuste (se for ajuste de banco de horas)
|
|
tipoAjuste: v.optional(
|
|
v.union(v.literal('compensar'), v.literal('abonar'), v.literal('descontar'))
|
|
),
|
|
// Período do ajuste (se for ajuste de banco de horas)
|
|
periodoDias: v.optional(v.number()),
|
|
periodoHoras: v.optional(v.number()),
|
|
periodoMinutos: v.optional(v.number()),
|
|
// Ajuste em minutos (calculado)
|
|
ajusteMinutos: v.optional(v.number()),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_gestor', ['gestorId'])
|
|
.index('by_registro', ['registroId'])
|
|
.index('by_data', ['criadoEm']),
|
|
|
|
// Dispensas de Registro - Períodos onde funcionário está dispensado de registrar ponto
|
|
dispensasRegistro: defineTable({
|
|
funcionarioId: v.id('funcionarios'),
|
|
gestorId: v.id('usuarios'),
|
|
dataInicio: v.string(), // YYYY-MM-DD
|
|
horaInicio: v.number(),
|
|
minutoInicio: v.number(),
|
|
dataFim: v.string(), // YYYY-MM-DD
|
|
horaFim: v.number(),
|
|
minutoFim: v.number(),
|
|
motivo: v.string(),
|
|
isento: v.boolean(), // Se true, não expira (casos excepcionais)
|
|
ativo: v.boolean(),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_funcionario', ['funcionarioId'])
|
|
.index('by_gestor', ['gestorId'])
|
|
.index('by_ativo', ['ativo'])
|
|
.index('by_data_inicio', ['dataInicio'])
|
|
.index('by_data_fim', ['dataFim']),
|
|
// Configurações Gerais
|
|
config: defineTable({
|
|
comprasSetorId: v.optional(v.id('setores')),
|
|
criadoPor: v.id('usuarios'),
|
|
atualizadoEm: v.number()
|
|
}),
|
|
|
|
// Módulo de Pedidos/Compras
|
|
produtos: defineTable({
|
|
nome: v.string(),
|
|
valorEstimado: v.string(),
|
|
tipo: v.union(v.literal('servico'), v.literal('estrutura'), v.literal('insumo')),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.searchIndex('search_nome', { searchField: 'nome' })
|
|
.index('by_nome', ['nome'])
|
|
.index('by_tipo', ['tipo']),
|
|
|
|
acoes: defineTable({
|
|
nome: v.string(),
|
|
tipo: v.union(v.literal('projeto'), v.literal('lei')),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_nome', ['nome'])
|
|
.index('by_tipo', ['tipo']),
|
|
|
|
pedidos: defineTable({
|
|
numeroSei: v.optional(v.string()),
|
|
status: v.union(
|
|
v.literal('em_rascunho'),
|
|
v.literal('aguardando_aceite'),
|
|
v.literal('em_analise'),
|
|
v.literal('precisa_ajustes'),
|
|
v.literal('cancelado'),
|
|
v.literal('concluido')
|
|
),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_numeroSei', ['numeroSei'])
|
|
.index('by_status', ['status'])
|
|
.index('by_criadoPor', ['criadoPor'])
|
|
.index('by_acaoId', ['acaoId']),
|
|
|
|
pedidoItems: defineTable({
|
|
pedidoId: v.id('pedidos'),
|
|
produtoId: v.id('produtos'),
|
|
valorEstimado: v.string(),
|
|
valorReal: v.optional(v.string()),
|
|
quantidade: v.number(),
|
|
adicionadoPor: v.id('funcionarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_pedidoId', ['pedidoId'])
|
|
.index('by_produtoId', ['produtoId'])
|
|
.index('by_adicionadoPor', ['adicionadoPor']),
|
|
|
|
historicoPedidos: defineTable({
|
|
pedidoId: v.id('pedidos'),
|
|
usuarioId: v.id('usuarios'),
|
|
acao: v.string(), // "criacao", "alteracao_status", "adicao_item", "remocao_item", "edicao_item"
|
|
detalhes: v.optional(v.string()), // JSON string
|
|
data: v.number()
|
|
})
|
|
.index('by_pedidoId', ['pedidoId'])
|
|
.index('by_usuarioId', ['usuarioId'])
|
|
.index('by_data', ['data'])
|
|
});
|