feat: implement almoxarifado features including new category in recursos-humanos, configuration options in TI, and backend support for inventory management, enhancing user navigation and system functionality

This commit is contained in:
2025-12-18 16:21:08 -03:00
parent 1eb454815f
commit 367cda7b95
22 changed files with 4831 additions and 2 deletions

View File

@@ -62,6 +62,7 @@ import type * as security from "../security.js";
import type * as seed from "../seed.js";
import type * as setores from "../setores.js";
import type * as simbolos from "../simbolos.js";
import type * as tables_almoxarifado from "../tables/almoxarifado.js";
import type * as tables_atas from "../tables/atas.js";
import type * as tables_atestados from "../tables/atestados.js";
import type * as tables_ausencias from "../tables/ausencias.js";
@@ -156,6 +157,7 @@ declare const fullApi: ApiFromModules<{
seed: typeof seed;
setores: typeof setores;
simbolos: typeof simbolos;
"tables/almoxarifado": typeof tables_almoxarifado;
"tables/atas": typeof tables_atas;
"tables/atestados": typeof tables_atestados;
"tables/ausencias": typeof tables_ausencias;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
import { v } from 'convex/values';
import { internal, internalQuery, mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
export const obterConfiguracao = query({
args: {},
handler: async (ctx) => {
// Verificar se usuário tem permissão de TI
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'configurar'
});
const config = await ctx.db
.query('configuracoesAlmoxarifado')
.filter((q) => q.eq(q.field('ativo'), true))
.first();
// Se não existe configuração, retornar valores padrão
if (!config) {
return {
estoqueMinimoPadrao: 10,
diasAntecedenciaAlerta: 7,
permitirEstoqueNegativo: false,
requerAprovacaoRequisicao: true,
rolesAprovacao: [],
emailAlertasAtivo: false,
emailsDestinatarios: [],
periodicidadeInventario: 30,
ultimoInventario: undefined,
ativo: true
};
}
return config;
}
});
export const atualizarConfiguracao = mutation({
args: {
estoqueMinimoPadrao: v.optional(v.number()),
diasAntecedenciaAlerta: v.optional(v.number()),
permitirEstoqueNegativo: v.optional(v.boolean()),
requerAprovacaoRequisicao: v.optional(v.boolean()),
rolesAprovacao: v.optional(v.array(v.string())),
emailAlertasAtivo: v.optional(v.boolean()),
emailsDestinatarios: v.optional(v.array(v.string())),
periodicidadeInventario: v.optional(v.number()),
ultimoInventario: v.optional(v.number())
},
handler: async (ctx, args) => {
// Verificar se usuário tem permissão de TI_MASTER
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'configurar'
});
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
// Buscar configuração existente
let config = await ctx.db
.query('configuracoesAlmoxarifado')
.filter((q) => q.eq(q.field('ativo'), true))
.first();
const dadosAnteriores = config ? { ...config } : undefined;
if (config) {
// Desativar configuração antiga
await ctx.db.patch(config._id, { ativo: false });
// Criar nova configuração
const dadosNovos = {
...config,
...args,
ativo: true,
atualizadoPor: usuario._id,
atualizadoEm: Date.now()
};
const novaConfigId = await ctx.db.insert('configuracoesAlmoxarifado', dadosNovos);
// Registrar histórico
if (usuario) {
await ctx.db.insert('historicoAlteracoes', {
tipoEntidade: 'configuracao',
entidadeId: novaConfigId,
acao: 'edicao',
usuarioId: usuario._id,
dadosAnteriores: dadosAnteriores ? JSON.stringify(dadosAnteriores) : undefined,
dadosNovos: JSON.stringify(dadosNovos),
timestamp: Date.now(),
observacoes: 'Atualização de configurações do almoxarifado'
});
}
return novaConfigId;
} else {
// Criar primeira configuração
const dadosNovos = {
estoqueMinimoPadrao: args.estoqueMinimoPadrao ?? 10,
diasAntecedenciaAlerta: args.diasAntecedenciaAlerta ?? 7,
permitirEstoqueNegativo: args.permitirEstoqueNegativo ?? false,
requerAprovacaoRequisicao: args.requerAprovacaoRequisicao ?? true,
rolesAprovacao: args.rolesAprovacao ?? [],
emailAlertasAtivo: args.emailAlertasAtivo ?? false,
emailsDestinatarios: args.emailsDestinatarios ?? [],
periodicidadeInventario: args.periodicidadeInventario ?? 30,
ultimoInventario: args.ultimoInventario,
ativo: true,
atualizadoPor: usuario._id,
atualizadoEm: Date.now()
};
const novaConfigId = await ctx.db.insert('configuracoesAlmoxarifado', dadosNovos);
// Registrar histórico
if (usuario) {
await ctx.db.insert('historicoAlteracoes', {
tipoEntidade: 'configuracao',
entidadeId: novaConfigId,
acao: 'criacao',
usuarioId: usuario._id,
dadosAnteriores: undefined,
dadosNovos: JSON.stringify(dadosNovos),
timestamp: Date.now(),
observacoes: 'Criação de configurações do almoxarifado'
});
}
return novaConfigId;
}
}
});
// ========== INTERNAL QUERIES ==========
export const obterConfiguracaoInterno = internalQuery({
args: {},
handler: async (ctx) => {
const config = await ctx.db
.query('configuracoesAlmoxarifado')
.filter((q) => q.eq(q.field('ativo'), true))
.first();
if (!config) {
return {
estoqueMinimoPadrao: 10,
diasAntecedenciaAlerta: 7,
permitirEstoqueNegativo: false,
requerAprovacaoRequisicao: true,
rolesAprovacao: [],
emailAlertasAtivo: false,
emailsDestinatarios: [],
periodicidadeInventario: 30,
ultimoInventario: undefined,
ativo: true
};
}
return config;
}
});

View File

@@ -58,4 +58,12 @@ crons.interval(
{}
);
// Verificar alertas de estoque do almoxarifado diariamente
crons.interval(
'verificar-alertas-almoxarifado',
{ hours: 24 },
internal.almoxarifado.verificarAlertasAutomatico,
{}
);
export default crons;

View File

@@ -735,6 +735,49 @@ const PERMISSOES_BASE = {
recurso: 'config',
acao: 'gerenciar_compras',
descricao: 'Gerenciar configurações de compras'
},
// Almoxarifado
{
nome: 'almoxarifado.listar',
recurso: 'almoxarifado',
acao: 'listar',
descricao: 'Listar materiais e movimentações'
},
{
nome: 'almoxarifado.criar_material',
recurso: 'almoxarifado',
acao: 'criar_material',
descricao: 'Cadastrar novos materiais'
},
{
nome: 'almoxarifado.editar_material',
recurso: 'almoxarifado',
acao: 'editar_material',
descricao: 'Editar materiais existentes'
},
{
nome: 'almoxarifado.registrar_movimentacao',
recurso: 'almoxarifado',
acao: 'registrar_movimentacao',
descricao: 'Registrar entradas e saídas'
},
{
nome: 'almoxarifado.ajustar_estoque',
recurso: 'almoxarifado',
acao: 'ajustar_estoque',
descricao: 'Realizar ajustes manuais de estoque'
},
{
nome: 'almoxarifado.aprovar_requisicao',
recurso: 'almoxarifado',
acao: 'aprovar_requisicao',
descricao: 'Aprovar requisições de material'
},
{
nome: 'almoxarifado.configurar',
recurso: 'almoxarifado',
acao: 'configurar',
descricao: 'Configurar sistema de almoxarifado (apenas TI)'
}
]
} as const;

View File

@@ -22,6 +22,7 @@ import { systemTables } from './tables/system';
import { ticketsTables } from './tables/tickets';
import { timesTables } from './tables/times';
import { lgpdTables } from './tables/lgpdTables';
import { almoxarifadoTables } from './tables/almoxarifado';
export default defineSchema({
...setoresTables,
@@ -46,5 +47,6 @@ export default defineSchema({
...planejamentosTables,
...objetosTables,
...atasTables,
...lgpdTables
...lgpdTables,
...almoxarifadoTables
});

View File

@@ -0,0 +1,149 @@
import { defineTable } from 'convex/server';
import { type Infer, v } from 'convex/values';
export const movimentacaoTipo = v.union(
v.literal('entrada'),
v.literal('saida'),
v.literal('ajuste'),
v.literal('transferencia')
);
export type MovimentacaoTipo = Infer<typeof movimentacaoTipo>;
export const requisicaoStatus = v.union(
v.literal('pendente'),
v.literal('aprovada'),
v.literal('rejeitada'),
v.literal('atendida'),
v.literal('cancelada')
);
export type RequisicaoStatus = Infer<typeof requisicaoStatus>;
export const alertaTipo = v.union(
v.literal('estoque_minimo'),
v.literal('estoque_zerado'),
v.literal('reposicao_necessaria')
);
export type AlertaTipo = Infer<typeof alertaTipo>;
export const alertaStatus = v.union(
v.literal('ativo'),
v.literal('resolvido'),
v.literal('ignorado')
);
export type AlertaStatus = Infer<typeof alertaStatus>;
export const almoxarifadoTables = {
materiais: defineTable({
codigo: v.string(),
nome: v.string(),
descricao: v.optional(v.string()),
categoria: v.string(),
unidadeMedida: v.string(),
estoqueMinimo: v.number(),
estoqueMaximo: v.optional(v.number()),
estoqueAtual: v.number(),
localizacao: v.optional(v.string()),
fornecedor: v.optional(v.string()),
ativo: v.boolean(),
criadoPor: v.id('usuarios'),
criadoEm: v.number(),
atualizadoEm: v.number()
})
.index('by_codigo', ['codigo'])
.index('by_categoria', ['categoria'])
.index('by_ativo', ['ativo'])
.index('by_estoqueAtual', ['estoqueAtual']),
movimentacoesEstoque: defineTable({
materialId: v.id('materiais'),
tipo: movimentacaoTipo,
quantidade: v.number(),
quantidadeAnterior: v.number(),
quantidadeNova: v.number(),
motivo: v.string(),
documento: v.optional(v.string()),
funcionarioId: v.optional(v.id('funcionarios')),
setorId: v.optional(v.id('setores')),
usuarioId: v.id('usuarios'),
data: v.number(),
observacoes: v.optional(v.string())
})
.index('by_materialId', ['materialId'])
.index('by_tipo', ['tipo'])
.index('by_data', ['data'])
.index('by_funcionarioId', ['funcionarioId'])
.index('by_usuarioId', ['usuarioId']),
requisicoesMaterial: defineTable({
numero: v.string(),
solicitanteId: v.id('funcionarios'),
setorId: v.id('setores'),
status: requisicaoStatus,
aprovadoPor: v.optional(v.id('funcionarios')),
dataAprovacao: v.optional(v.number()),
observacoes: v.optional(v.string()),
criadoEm: v.number(),
atualizadoEm: v.number()
})
.index('by_status', ['status'])
.index('by_solicitanteId', ['solicitanteId'])
.index('by_setorId', ['setorId'])
.index('by_numero', ['numero']),
requisicaoItens: defineTable({
requisicaoId: v.id('requisicoesMaterial'),
materialId: v.id('materiais'),
quantidadeSolicitada: v.number(),
quantidadeAtendida: v.optional(v.number()),
observacoes: v.optional(v.string())
})
.index('by_requisicaoId', ['requisicaoId'])
.index('by_materialId', ['materialId']),
historicoAlteracoes: defineTable({
tipoEntidade: v.string(),
entidadeId: v.string(),
acao: v.string(),
usuarioId: v.id('usuarios'),
dadosAnteriores: v.optional(v.string()),
dadosNovos: v.optional(v.string()),
timestamp: v.number(),
ipAddress: v.optional(v.string()),
observacoes: v.optional(v.string())
})
.index('by_tipoEntidade', ['tipoEntidade'])
.index('by_entidadeId', ['entidadeId'])
.index('by_usuarioId', ['usuarioId'])
.index('by_timestamp', ['timestamp']),
alertasEstoque: defineTable({
materialId: v.id('materiais'),
tipo: alertaTipo,
quantidadeAtual: v.number(),
quantidadeMinima: v.number(),
status: alertaStatus,
criadoEm: v.number(),
resolvidoEm: v.optional(v.number()),
resolvidoPor: v.optional(v.id('usuarios'))
})
.index('by_materialId', ['materialId'])
.index('by_status', ['status'])
.index('by_tipo', ['tipo']),
configuracoesAlmoxarifado: defineTable({
estoqueMinimoPadrao: v.number(),
diasAntecedenciaAlerta: v.number(),
permitirEstoqueNegativo: v.boolean(),
requerAprovacaoRequisicao: v.boolean(),
rolesAprovacao: v.array(v.string()),
emailAlertasAtivo: v.boolean(),
emailsDestinatarios: v.array(v.string()),
periodicidadeInventario: v.number(),
ultimoInventario: v.optional(v.number()),
ativo: v.boolean(),
atualizadoPor: v.id('usuarios'),
atualizadoEm: v.number()
})
};