Files
sgse-app/packages/backend/convex/almoxarifado.ts

1180 lines
32 KiB
TypeScript

import { v } from 'convex/values';
import type { Doc, Id } from './_generated/dataModel';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { internal, internalMutation, internalAction, mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import {
alertaStatus,
alertaTipo,
movimentacaoTipo,
requisicaoStatus
} from './tables/almoxarifado';
// ========== QUERIES ==========
export const listarMateriais = query({
args: {
categoria: v.optional(v.string()),
ativo: v.optional(v.boolean()),
estoqueBaixo: v.optional(v.boolean()),
busca: v.optional(v.string())
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return [];
try {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
} catch {
return [];
}
let query = ctx.db.query('materiais');
if (args.ativo !== undefined) {
query = query.withIndex('by_ativo', (q) => q.eq('ativo', args.ativo));
} else if (args.categoria) {
query = query.withIndex('by_categoria', (q) => q.eq('categoria', args.categoria));
} else {
query = query;
}
let materiais = await query.collect();
// Filtros adicionais
if (args.busca) {
const buscaLower = args.busca.toLowerCase();
materiais = materiais.filter(
(m) =>
m.codigo.toLowerCase().includes(buscaLower) ||
m.nome.toLowerCase().includes(buscaLower)
);
}
if (args.estoqueBaixo) {
materiais = materiais.filter((m) => m.estoqueAtual <= m.estoqueMinimo);
}
return materiais;
}
});
export const obterMaterial = query({
args: { id: v.id('materiais') },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
const material = await ctx.db.get(args.id);
if (!material) throw new Error('Material não encontrado');
return material;
}
});
export const listarMovimentacoes = query({
args: {
materialId: v.optional(v.id('materiais')),
tipo: v.optional(movimentacaoTipo),
dataInicio: v.optional(v.number()),
dataFim: v.optional(v.number()),
funcionarioId: v.optional(v.id('funcionarios'))
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return [];
try {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
} catch {
return [];
}
let query = ctx.db.query('movimentacoesEstoque');
if (args.materialId) {
query = query.withIndex('by_materialId', (q) => q.eq('materialId', args.materialId));
} else if (args.tipo) {
query = query.withIndex('by_tipo', (q) => q.eq('tipo', args.tipo));
} else if (args.funcionarioId) {
query = query.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId));
} else {
query = query.withIndex('by_data');
}
let movimentacoes = await query.collect();
// Filtros de data
if (args.dataInicio) {
movimentacoes = movimentacoes.filter((m) => m.data >= args.dataInicio!);
}
if (args.dataFim) {
movimentacoes = movimentacoes.filter((m) => m.data <= args.dataFim!);
}
// Ordenar por data (mais recente primeiro)
movimentacoes.sort((a, b) => b.data - a.data);
return movimentacoes;
}
});
export const listarRequisicoes = query({
args: {
status: v.optional(requisicaoStatus),
solicitanteId: v.optional(v.id('funcionarios')),
setorId: v.optional(v.id('setores'))
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return [];
try {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
} catch {
return [];
}
let query = ctx.db.query('requisicoesMaterial');
if (args.status) {
query = query.withIndex('by_status', (q) => q.eq('status', args.status));
} else if (args.solicitanteId) {
query = query.withIndex('by_solicitanteId', (q) => q.eq('solicitanteId', args.solicitanteId));
} else if (args.setorId) {
query = query.withIndex('by_setorId', (q) => q.eq('setorId', args.setorId));
}
const requisicoes = await query.collect();
// Ordenar por data de criação (mais recente primeiro)
requisicoes.sort((a, b) => b.criadoEm - a.criadoEm);
return requisicoes;
}
});
export const obterRequisicao = query({
args: { id: v.id('requisicoesMaterial') },
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
const requisicao = await ctx.db.get(args.id);
if (!requisicao) throw new Error('Requisição não encontrada');
// Buscar itens da requisição
const itens = await ctx.db
.query('requisicaoItens')
.withIndex('by_requisicaoId', (q) => q.eq('requisicaoId', args.id))
.collect();
return {
...requisicao,
itens
};
}
});
export const listarAlertas = query({
args: {
status: v.optional(alertaStatus),
tipo: v.optional(alertaTipo)
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return [];
try {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
} catch {
return [];
}
let query = ctx.db.query('alertasEstoque');
if (args.status) {
query = query.withIndex('by_status', (q) => q.eq('status', args.status));
} else if (args.tipo) {
query = query.withIndex('by_tipo', (q) => q.eq('tipo', args.tipo));
}
const alertas = await query.collect();
// Ordenar por data de criação (mais recente primeiro)
alertas.sort((a, b) => b.criadoEm - a.criadoEm);
return alertas;
}
});
export const obterHistorico = query({
args: {
tipoEntidade: v.string(),
entidadeId: v.string()
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
const historico = await ctx.db
.query('historicoAlteracoes')
.withIndex('by_tipoEntidade', (q) => q.eq('tipoEntidade', args.tipoEntidade))
.filter((q) => q.eq(q.field('entidadeId'), args.entidadeId))
.collect();
// Ordenar por timestamp (mais recente primeiro)
historico.sort((a, b) => b.timestamp - a.timestamp);
return historico;
}
});
export const obterEstatisticas = query({
args: {},
handler: async (ctx) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return {
totalMateriais: 0,
totalMateriaisAtivos: 0,
totalAlertasAtivos: 0,
movimentacoesMes: 0,
materiaisEstoqueBaixo: 0
};
}
try {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
} catch {
return {
totalMateriais: 0,
totalMateriaisAtivos: 0,
totalAlertasAtivos: 0,
movimentacoesMes: 0,
materiaisEstoqueBaixo: 0
};
}
const materiais = await ctx.db.query('materiais').collect();
const materiaisAtivos = materiais.filter((m) => m.ativo);
const materiaisEstoqueBaixo = materiais.filter((m) => m.estoqueAtual <= m.estoqueMinimo);
const alertasAtivos = await ctx.db
.query('alertasEstoque')
.withIndex('by_status', (q) => q.eq('status', 'ativo'))
.collect();
const agora = Date.now();
const inicioMes = new Date(agora);
inicioMes.setDate(1);
inicioMes.setHours(0, 0, 0, 0);
const movimentacoes = await ctx.db
.query('movimentacoesEstoque')
.withIndex('by_data')
.collect();
const movimentacoesMes = movimentacoes.filter((m) => m.data >= inicioMes.getTime()).length;
return {
totalMateriais: materiais.length,
totalMateriaisAtivos: materiaisAtivos.length,
totalAlertasAtivos: alertasAtivos.length,
movimentacoesMes,
materiaisEstoqueBaixo: materiaisEstoqueBaixo.length
};
}
});
export const verificarEstoqueBaixo = query({
args: {},
handler: async (ctx) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'listar'
});
const materiais = await ctx.db
.query('materiais')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.collect();
return materiais.filter((m) => m.estoqueAtual <= m.estoqueMinimo);
}
});
// ========== MUTATIONS ==========
async function registrarHistorico(
ctx: MutationCtx,
tipoEntidade: string,
entidadeId: string,
acao: string,
dadosAnteriores?: Record<string, unknown>,
dadosNovos?: Record<string, unknown>,
observacoes?: string
) {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return;
await ctx.db.insert('historicoAlteracoes', {
tipoEntidade,
entidadeId,
acao,
usuarioId: usuario._id,
dadosAnteriores: dadosAnteriores ? JSON.stringify(dadosAnteriores) : undefined,
dadosNovos: dadosNovos ? JSON.stringify(dadosNovos) : undefined,
timestamp: Date.now(),
observacoes
});
}
async function verificarECriarAlerta(ctx: MutationCtx, materialId: Id<'materiais'>) {
const material = await ctx.db.get(materialId);
if (!material || !material.ativo) return;
// Verificar se já existe alerta ativo para este material
const alertasExistentes = await ctx.db
.query('alertasEstoque')
.withIndex('by_materialId', (q) => q.eq('materialId', materialId))
.filter((q) => q.eq(q.field('status'), 'ativo'))
.collect();
if (alertasExistentes.length > 0) {
// Já existe alerta ativo
return;
}
// Determinar tipo de alerta
let tipo: 'estoque_minimo' | 'estoque_zerado' | 'reposicao_necessaria' = 'estoque_minimo';
if (material.estoqueAtual === 0) {
tipo = 'estoque_zerado';
} else if (material.estoqueAtual < material.estoqueMinimo) {
tipo = 'estoque_minimo';
}
// Criar alerta
await ctx.db.insert('alertasEstoque', {
materialId,
tipo,
quantidadeAtual: material.estoqueAtual,
quantidadeMinima: material.estoqueMinimo,
status: 'ativo',
criadoEm: Date.now()
});
}
async function resolverAlertasMaterial(ctx: MutationCtx, materialId: Id<'materiais'>) {
const alertas = await ctx.db
.query('alertasEstoque')
.withIndex('by_materialId', (q) => q.eq('materialId', materialId))
.filter((q) => q.eq(q.field('status'), 'ativo'))
.collect();
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return;
for (const alerta of alertas) {
await ctx.db.patch(alerta._id, {
status: 'resolvido',
resolvidoEm: Date.now(),
resolvidoPor: usuario._id
});
}
}
export const criarMaterial = mutation({
args: {
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.optional(v.number()),
localizacao: v.optional(v.string()),
fornecedor: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'criar_material'
});
// Verificar se código já existe
const codigoExistente = await ctx.db
.query('materiais')
.withIndex('by_codigo', (q) => q.eq('codigo', args.codigo))
.unique();
if (codigoExistente) {
throw new Error('Código do material já existe');
}
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
const agora = Date.now();
const materialId = await ctx.db.insert('materiais', {
...args,
estoqueAtual: args.estoqueAtual ?? 0,
ativo: true,
criadoPor: usuario._id,
criadoEm: agora,
atualizadoEm: agora
});
// Registrar histórico
await registrarHistorico(ctx, 'material', materialId.toString(), 'criacao', undefined, args as Record<string, unknown>);
// Verificar se precisa criar alerta
if (args.estoqueAtual !== undefined && args.estoqueAtual <= args.estoqueMinimo) {
await verificarECriarAlerta(ctx, materialId);
}
return materialId;
}
});
export const editarMaterial = mutation({
args: {
id: v.id('materiais'),
codigo: v.optional(v.string()),
nome: v.optional(v.string()),
descricao: v.optional(v.string()),
categoria: v.optional(v.string()),
unidadeMedida: v.optional(v.string()),
estoqueMinimo: v.optional(v.number()),
estoqueMaximo: v.optional(v.number()),
localizacao: v.optional(v.string()),
fornecedor: v.optional(v.string()),
ativo: v.optional(v.boolean())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'editar_material'
});
const material = await ctx.db.get(args.id);
if (!material) throw new Error('Material não encontrado');
// Verificar se código já existe (se foi alterado)
if (args.codigo && args.codigo !== material.codigo) {
const codigoExistente = await ctx.db
.query('materiais')
.withIndex('by_codigo', (q) => q.eq('codigo', args.codigo))
.unique();
if (codigoExistente) {
throw new Error('Código do material já existe');
}
}
const dadosAnteriores = { ...material };
const dadosNovos: Partial<Doc<'materiais'>> & { atualizadoEm: number } = {
atualizadoEm: Date.now()
};
// Atualizar apenas campos fornecidos
if (args.codigo !== undefined) dadosNovos.codigo = args.codigo;
if (args.nome !== undefined) dadosNovos.nome = args.nome;
if (args.descricao !== undefined) dadosNovos.descricao = args.descricao;
if (args.categoria !== undefined) dadosNovos.categoria = args.categoria;
if (args.unidadeMedida !== undefined) dadosNovos.unidadeMedida = args.unidadeMedida;
if (args.estoqueMinimo !== undefined) dadosNovos.estoqueMinimo = args.estoqueMinimo;
if (args.estoqueMaximo !== undefined) dadosNovos.estoqueMaximo = args.estoqueMaximo;
if (args.localizacao !== undefined) dadosNovos.localizacao = args.localizacao;
if (args.fornecedor !== undefined) dadosNovos.fornecedor = args.fornecedor;
if (args.ativo !== undefined) dadosNovos.ativo = args.ativo;
await ctx.db.patch(args.id, dadosNovos);
// Registrar histórico
await registrarHistorico(ctx, 'material', args.id.toString(), 'edicao', dadosAnteriores, dadosNovos);
// Verificar se precisa criar/resolver alertas
if (args.estoqueMinimo !== undefined || args.ativo !== undefined) {
const materialAtualizado = await ctx.db.get(args.id);
if (materialAtualizado) {
if (materialAtualizado.ativo && materialAtualizado.estoqueAtual <= materialAtualizado.estoqueMinimo) {
await verificarECriarAlerta(ctx, args.id);
} else if (materialAtualizado.estoqueAtual > materialAtualizado.estoqueMinimo) {
await resolverAlertasMaterial(ctx, args.id);
}
}
}
}
});
export const registrarEntrada = mutation({
args: {
materialId: v.id('materiais'),
quantidade: v.number(),
motivo: v.string(),
documento: v.optional(v.string()),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'registrar_movimentacao'
});
if (args.quantidade <= 0) {
throw new Error('Quantidade deve ser maior que zero');
}
const material = await ctx.db.get(args.materialId);
if (!material) throw new Error('Material não encontrado');
if (!material.ativo) throw new Error('Material está inativo');
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
const quantidadeAnterior = material.estoqueAtual;
const quantidadeNova = quantidadeAnterior + args.quantidade;
// Atualizar estoque
await ctx.db.patch(args.materialId, {
estoqueAtual: quantidadeNova,
atualizadoEm: Date.now()
});
// Registrar movimentação
const movimentacaoId = await ctx.db.insert('movimentacoesEstoque', {
materialId: args.materialId,
tipo: 'entrada',
quantidade: args.quantidade,
quantidadeAnterior,
quantidadeNova,
motivo: args.motivo,
documento: args.documento,
usuarioId: usuario._id,
data: Date.now(),
observacoes: args.observacoes
});
// Registrar histórico
await registrarHistorico(ctx, 'movimentacao', movimentacaoId.toString(), 'movimentacao', undefined, {
tipo: 'entrada',
materialId: args.materialId,
quantidade: args.quantidade
});
// Verificar se precisa resolver alertas
if (quantidadeNova > material.estoqueMinimo) {
await resolverAlertasMaterial(ctx, args.materialId);
}
return movimentacaoId;
}
});
export const registrarSaida = mutation({
args: {
materialId: v.id('materiais'),
quantidade: v.number(),
motivo: v.string(),
funcionarioId: v.optional(v.id('funcionarios')),
setorId: v.optional(v.id('setores')),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'registrar_movimentacao'
});
if (args.quantidade <= 0) {
throw new Error('Quantidade deve ser maior que zero');
}
const material = await ctx.db.get(args.materialId);
if (!material) throw new Error('Material não encontrado');
if (!material.ativo) throw new Error('Material está inativo');
// Verificar configuração de estoque negativo
const config = await ctx.db
.query('configuracoesAlmoxarifado')
.filter((q) => q.eq(q.field('ativo'), true))
.first();
if (!config?.permitirEstoqueNegativo && material.estoqueAtual < args.quantidade) {
throw new Error('Estoque insuficiente');
}
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
const quantidadeAnterior = material.estoqueAtual;
const quantidadeNova = Math.max(0, quantidadeAnterior - args.quantidade);
// Atualizar estoque
await ctx.db.patch(args.materialId, {
estoqueAtual: quantidadeNova,
atualizadoEm: Date.now()
});
// Registrar movimentação
const movimentacaoId = await ctx.db.insert('movimentacoesEstoque', {
materialId: args.materialId,
tipo: 'saida',
quantidade: args.quantidade,
quantidadeAnterior,
quantidadeNova,
motivo: args.motivo,
funcionarioId: args.funcionarioId,
setorId: args.setorId,
usuarioId: usuario._id,
data: Date.now(),
observacoes: args.observacoes
});
// Registrar histórico
await registrarHistorico(ctx, 'movimentacao', movimentacaoId.toString(), 'movimentacao', undefined, {
tipo: 'saida',
materialId: args.materialId,
quantidade: args.quantidade
});
// Verificar se precisa criar alerta
if (quantidadeNova <= material.estoqueMinimo) {
await verificarECriarAlerta(ctx, args.materialId);
}
return movimentacaoId;
}
});
export const ajustarEstoque = mutation({
args: {
materialId: v.id('materiais'),
quantidadeNova: v.number(),
motivo: v.string(),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'ajustar_estoque'
});
if (args.quantidadeNova < 0) {
throw new Error('Quantidade não pode ser negativa');
}
const material = await ctx.db.get(args.materialId);
if (!material) throw new Error('Material não encontrado');
if (!material.ativo) throw new Error('Material está inativo');
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
const quantidadeAnterior = material.estoqueAtual;
const diferenca = args.quantidadeNova - quantidadeAnterior;
// Atualizar estoque
await ctx.db.patch(args.materialId, {
estoqueAtual: args.quantidadeNova,
atualizadoEm: Date.now()
});
// Registrar movimentação
const movimentacaoId = await ctx.db.insert('movimentacoesEstoque', {
materialId: args.materialId,
tipo: 'ajuste',
quantidade: Math.abs(diferenca),
quantidadeAnterior,
quantidadeNova: args.quantidadeNova,
motivo: args.motivo,
usuarioId: usuario._id,
data: Date.now(),
observacoes: args.observacoes
});
// Registrar histórico
await registrarHistorico(ctx, 'movimentacao', movimentacaoId.toString(), 'movimentacao', undefined, {
tipo: 'ajuste',
materialId: args.materialId,
quantidadeAnterior,
quantidadeNova: args.quantidadeNova
});
// Verificar se precisa criar/resolver alertas
if (args.quantidadeNova <= material.estoqueMinimo) {
await verificarECriarAlerta(ctx, args.materialId);
} else {
await resolverAlertasMaterial(ctx, args.materialId);
}
return movimentacaoId;
}
});
export const criarRequisicao = mutation({
args: {
solicitanteId: v.id('funcionarios'),
setorId: v.id('setores'),
itens: v.array(
v.object({
materialId: v.id('materiais'),
quantidadeSolicitada: v.number(),
observacoes: v.optional(v.string())
})
),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
if (args.itens.length === 0) {
throw new Error('Requisição deve ter pelo menos um item');
}
// Gerar número sequencial da requisição
const todasRequisicoes = await ctx.db.query('requisicoesMaterial').collect();
const proximoNumero = (todasRequisicoes.length + 1).toString().padStart(6, '0');
const numero = `REQ-${proximoNumero}`;
const agora = Date.now();
const requisicaoId = await ctx.db.insert('requisicoesMaterial', {
numero,
solicitanteId: args.solicitanteId,
setorId: args.setorId,
status: 'pendente',
observacoes: args.observacoes,
criadoEm: agora,
atualizadoEm: agora
});
// Criar itens da requisição
for (const item of args.itens) {
await ctx.db.insert('requisicaoItens', {
requisicaoId,
materialId: item.materialId,
quantidadeSolicitada: item.quantidadeSolicitada,
observacoes: item.observacoes
});
}
// Registrar histórico
await registrarHistorico(ctx, 'requisicao', requisicaoId.toString(), 'criacao', undefined, {
numero,
solicitanteId: args.solicitanteId,
itens: args.itens
});
return requisicaoId;
}
});
export const aprovarRequisicao = mutation({
args: {
id: v.id('requisicoesMaterial'),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'aprovar_requisicao'
});
const requisicao = await ctx.db.get(args.id);
if (!requisicao) throw new Error('Requisição não encontrada');
if (requisicao.status !== 'pendente') {
throw new Error('Apenas requisições pendentes podem ser aprovadas');
}
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
// Buscar funcionário do usuário
const funcionario = await ctx.db
.query('funcionarios')
.filter((q) => q.eq(q.field('email'), usuario.email))
.first();
if (!funcionario) throw new Error('Funcionário não encontrado para o usuário');
await ctx.db.patch(args.id, {
status: 'aprovada',
aprovadoPor: funcionario._id,
dataAprovacao: Date.now(),
atualizadoEm: Date.now(),
observacoes: args.observacoes || requisicao.observacoes
});
// Registrar histórico
await registrarHistorico(ctx, 'requisicao', args.id.toString(), 'edicao', requisicao, {
status: 'aprovada',
aprovadoPor: funcionario._id
});
}
});
export const atenderRequisicao = mutation({
args: {
id: v.id('requisicoesMaterial')
},
handler: async (ctx, args) => {
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
recurso: 'almoxarifado',
acao: 'registrar_movimentacao'
});
const requisicao = await ctx.db.get(args.id);
if (!requisicao) throw new Error('Requisição não encontrada');
if (requisicao.status !== 'aprovada') {
throw new Error('Apenas requisições aprovadas podem ser atendidas');
}
// Buscar itens da requisição
const itens = await ctx.db
.query('requisicaoItens')
.withIndex('by_requisicaoId', (q) => q.eq('requisicaoId', args.id))
.collect();
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
// Processar cada item
for (const item of itens) {
const material = await ctx.db.get(item.materialId);
if (!material) continue;
const quantidadeAtendida = Math.min(item.quantidadeSolicitada, material.estoqueAtual);
// Atualizar item com quantidade atendida
await ctx.db.patch(item._id, {
quantidadeAtendida
});
if (quantidadeAtendida > 0) {
// Registrar saída
const quantidadeAnterior = material.estoqueAtual;
const quantidadeNova = quantidadeAnterior - quantidadeAtendida;
await ctx.db.patch(item.materialId, {
estoqueAtual: quantidadeNova,
atualizadoEm: Date.now()
});
await ctx.db.insert('movimentacoesEstoque', {
materialId: item.materialId,
tipo: 'saida',
quantidade: quantidadeAtendida,
quantidadeAnterior,
quantidadeNova,
motivo: `Atendimento da requisição ${requisicao.numero}`,
funcionarioId: requisicao.solicitanteId,
setorId: requisicao.setorId,
usuarioId: usuario._id,
data: Date.now(),
observacoes: `Requisição ${requisicao.numero}`
});
// Verificar alertas
if (quantidadeNova <= material.estoqueMinimo) {
await verificarECriarAlerta(ctx, item.materialId);
}
}
}
// Atualizar status da requisição
await ctx.db.patch(args.id, {
status: 'atendida',
atualizadoEm: Date.now()
});
// Registrar histórico
await registrarHistorico(ctx, 'requisicao', args.id.toString(), 'edicao', requisicao, {
status: 'atendida'
});
}
});
export const cancelarRequisicao = mutation({
args: {
id: v.id('requisicoesMaterial'),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
const requisicao = await ctx.db.get(args.id);
if (!requisicao) throw new Error('Requisição não encontrada');
if (requisicao.status === 'atendida' || requisicao.status === 'cancelada') {
throw new Error('Requisição já foi atendida ou cancelada');
}
await ctx.db.patch(args.id, {
status: 'cancelada',
atualizadoEm: Date.now(),
observacoes: args.observacoes || requisicao.observacoes
});
// Registrar histórico
await registrarHistorico(ctx, 'requisicao', args.id.toString(), 'edicao', requisicao, {
status: 'cancelada'
});
}
});
export const resolverAlerta = mutation({
args: {
id: v.id('alertasEstoque')
},
handler: async (ctx, args) => {
const alerta = await ctx.db.get(args.id);
if (!alerta) throw new Error('Alerta não encontrado');
if (alerta.status !== 'ativo') {
throw new Error('Apenas alertas ativos podem ser resolvidos');
}
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) throw new Error('Usuário não autenticado');
await ctx.db.patch(args.id, {
status: 'resolvido',
resolvidoEm: Date.now(),
resolvidoPor: usuario._id
});
// Registrar histórico
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
status: 'resolvido'
});
}
});
export const ignorarAlerta = mutation({
args: {
id: v.id('alertasEstoque')
},
handler: async (ctx, args) => {
const alerta = await ctx.db.get(args.id);
if (!alerta) throw new Error('Alerta não encontrado');
if (alerta.status !== 'ativo') {
throw new Error('Apenas alertas ativos podem ser ignorados');
}
await ctx.db.patch(args.id, {
status: 'ignorado'
});
// Registrar histórico
await registrarHistorico(ctx, 'configuracao', args.id.toString(), 'edicao', alerta, {
status: 'ignorado'
});
}
});
// ========== INTERNAL ACTIONS ==========
export const registrarHistoricoAlteracao = internalMutation({
args: {
tipoEntidade: v.string(),
entidadeId: v.string(),
acao: v.string(),
dadosAnteriores: v.optional(v.string()),
dadosNovos: v.optional(v.string()),
observacoes: v.optional(v.string())
},
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) return;
await ctx.db.insert('historicoAlteracoes', {
tipoEntidade: args.tipoEntidade,
entidadeId: args.entidadeId,
acao: args.acao,
usuarioId: usuario._id,
dadosAnteriores: args.dadosAnteriores,
dadosNovos: args.dadosNovos,
timestamp: Date.now(),
observacoes: args.observacoes
});
}
});
export const verificarAlertasAutomatico = internalAction({
args: {},
handler: async (ctx) => {
// Buscar todos os materiais ativos
const materiais = await ctx.runQuery(internal.almoxarifado.listarMateriaisInterno, {
ativo: true
});
// Buscar configuração
const config = await ctx.runQuery(internal.configuracaoAlmoxarifado.obterConfiguracaoInterno, {});
for (const material of materiais) {
// Verificar se precisa criar alerta
if (material.estoqueAtual <= material.estoqueMinimo) {
// Verificar se já existe alerta ativo
const alertasExistentes = await ctx.runQuery(
internal.almoxarifado.listarAlertasPorMaterial,
{
materialId: material._id,
status: 'ativo' as const
}
);
if (alertasExistentes.length === 0) {
// Criar novo alerta
let tipo: 'estoque_minimo' | 'estoque_zerado' | 'reposicao_necessaria' =
'estoque_minimo';
if (material.estoqueAtual === 0) {
tipo = 'estoque_zerado';
}
await ctx.runMutation(internal.almoxarifado.criarAlertaInterno, {
materialId: material._id,
tipo,
quantidadeAtual: material.estoqueAtual,
quantidadeMinima: material.estoqueMinimo
});
}
} else {
// Resolver alertas se estoque está acima do mínimo
await ctx.runMutation(internal.almoxarifado.resolverAlertasMaterialInterno, {
materialId: material._id
});
}
}
// Enviar notificações se configurado
if (config?.emailAlertasAtivo) {
await ctx.runAction(internal.almoxarifado.enviarNotificacoesAlerta, {});
}
}
});
export const enviarNotificacoesAlerta = internalAction({
args: {},
handler: async (ctx) => {
// Buscar alertas ativos
const alertas = await ctx.runQuery(internal.almoxarifado.listarAlertasInterno, {
status: 'ativo'
});
if (alertas.length === 0) return;
// Buscar configuração para obter emails
const config = await ctx.runQuery(internal.configuracaoAlmoxarifado.obterConfiguracaoInterno, {});
if (!config?.emailAlertasAtivo || config.emailsDestinatarios.length === 0) {
return;
}
// Aqui você pode integrar com o sistema de email do projeto
// Por enquanto, apenas logamos
console.log(`Enviando notificações de ${alertas.length} alertas para:`, config.emailsDestinatarios);
}
});
// ========== INTERNAL QUERIES ==========
export const listarMateriaisInterno = internalQuery({
args: {
ativo: v.optional(v.boolean())
},
handler: async (ctx, args) => {
let query = ctx.db.query('materiais');
if (args.ativo !== undefined) {
query = query.withIndex('by_ativo', (q) => q.eq('ativo', args.ativo));
}
return await query.collect();
}
});
export const listarAlertasPorMaterial = internalQuery({
args: {
materialId: v.id('materiais'),
status: v.optional(alertaStatus)
},
handler: async (ctx, args) => {
let query = ctx.db
.query('alertasEstoque')
.withIndex('by_materialId', (q) => q.eq('materialId', args.materialId));
if (args.status) {
query = query.filter((q) => q.eq(q.field('status'), args.status));
}
return await query.collect();
}
});
export const listarAlertasInterno = internalQuery({
args: {
status: v.optional(alertaStatus)
},
handler: async (ctx, args) => {
let query = ctx.db.query('alertasEstoque');
if (args.status) {
query = query.withIndex('by_status', (q) => q.eq('status', args.status));
}
return await query.collect();
}
});
// ========== INTERNAL MUTATIONS ==========
export const criarAlertaInterno = internalMutation({
args: {
materialId: v.id('materiais'),
tipo: alertaTipo,
quantidadeAtual: v.number(),
quantidadeMinima: v.number()
},
handler: async (ctx, args) => {
await ctx.db.insert('alertasEstoque', {
materialId: args.materialId,
tipo: args.tipo,
quantidadeAtual: args.quantidadeAtual,
quantidadeMinima: args.quantidadeMinima,
status: 'ativo',
criadoEm: Date.now()
});
}
});
export const resolverAlertasMaterialInterno = internalMutation({
args: {
materialId: v.id('materiais')
},
handler: async (ctx, args) => {
const alertas = await ctx.db
.query('alertasEstoque')
.withIndex('by_materialId', (q) => q.eq('materialId', args.materialId))
.filter((q) => q.eq(q.field('status'), 'ativo'))
.collect();
for (const alerta of alertas) {
await ctx.db.patch(alerta._id, {
status: 'resolvido',
resolvidoEm: Date.now()
});
}
}
});