703 lines
18 KiB
TypeScript
703 lines
18 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { api, internal } from './_generated/api';
|
|
import type { Doc, Id } from './_generated/dataModel';
|
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
|
import { internalMutation, mutation, query } from './_generated/server';
|
|
import { getCurrentUserFunction } from './auth';
|
|
|
|
// ========== HELPERS ==========
|
|
|
|
async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
|
|
const user = await getCurrentUserFunction(ctx);
|
|
if (!user) throw new Error('Unauthorized');
|
|
return user;
|
|
}
|
|
|
|
// ========== QUERIES ==========
|
|
|
|
export const list = query({
|
|
args: {},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
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 removed from return
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx) => {
|
|
return await ctx.db.query('pedidos').collect();
|
|
}
|
|
});
|
|
|
|
export const get = query({
|
|
args: { id: v.id('pedidos') },
|
|
returns: v.union(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
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()
|
|
}),
|
|
v.null()
|
|
),
|
|
handler: async (ctx, args) => {
|
|
return await ctx.db.get(args.id);
|
|
}
|
|
});
|
|
|
|
export const getItems = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('objetoItems'),
|
|
_creationTime: v.number(),
|
|
pedidoId: v.id('pedidos'),
|
|
objetoId: v.id('objetos'),
|
|
ataId: v.optional(v.id('atas')),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
modalidade: v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
),
|
|
valorEstimado: v.string(),
|
|
valorReal: v.optional(v.string()),
|
|
quantidade: v.number(),
|
|
adicionadoPor: v.id('funcionarios'),
|
|
adicionadoPorNome: v.string(),
|
|
criadoEm: v.number()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.collect();
|
|
|
|
// Get employee names
|
|
const itemsWithNames = await Promise.all(
|
|
items.map(async (item) => {
|
|
const funcionario = await ctx.db.get(item.adicionadoPor);
|
|
return {
|
|
...item,
|
|
adicionadoPorNome: funcionario?.nome || 'Desconhecido'
|
|
};
|
|
})
|
|
);
|
|
|
|
return itemsWithNames;
|
|
}
|
|
});
|
|
|
|
export const getHistory = query({
|
|
args: { pedidoId: v.id('pedidos') },
|
|
handler: async (ctx, args) => {
|
|
const history = await ctx.db
|
|
.query('historicoPedidos')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.order('desc')
|
|
.collect();
|
|
|
|
// Get user names
|
|
const historyWithNames = await Promise.all(
|
|
history.map(async (entry) => {
|
|
const usuario = await ctx.db.get(entry.usuarioId);
|
|
return {
|
|
_id: entry._id,
|
|
_creationTime: entry._creationTime,
|
|
pedidoId: entry.pedidoId,
|
|
usuarioId: entry.usuarioId,
|
|
usuarioNome: usuario?.nome || 'Desconhecido',
|
|
acao: entry.acao,
|
|
detalhes: entry.detalhes,
|
|
data: entry.data
|
|
};
|
|
})
|
|
);
|
|
|
|
return historyWithNames;
|
|
}
|
|
});
|
|
|
|
export const checkExisting = query({
|
|
args: {
|
|
acaoId: v.optional(v.id('acoes')), // Used to filter items
|
|
numeroSei: v.optional(v.string()),
|
|
objetoIds: v.optional(v.array(v.id('objetos')))
|
|
},
|
|
returns: v.array(
|
|
v.object({
|
|
_id: v.id('pedidos'),
|
|
_creationTime: v.number(),
|
|
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 removed
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number(),
|
|
matchingItems: v.optional(
|
|
v.array(
|
|
v.object({
|
|
objetoId: v.id('objetos'),
|
|
quantidade: v.number()
|
|
})
|
|
)
|
|
)
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const user = await getCurrentUserFunction(ctx);
|
|
if (!user) return [];
|
|
|
|
const openStatuses: Array<
|
|
'em_rascunho' | 'aguardando_aceite' | 'em_analise' | 'precisa_ajustes'
|
|
> = ['em_rascunho', 'aguardando_aceite', 'em_analise', 'precisa_ajustes'];
|
|
|
|
// 1) Buscar todos os pedidos "abertos" usando o índice by_status
|
|
let pedidosAbertos: Doc<'pedidos'>[] = [];
|
|
for (const status of openStatuses) {
|
|
const partial = await ctx.db
|
|
.query('pedidos')
|
|
.withIndex('by_status', (q) => q.eq('status', status))
|
|
.collect();
|
|
pedidosAbertos = pedidosAbertos.concat(partial);
|
|
}
|
|
|
|
// 2) Filtros opcionais: numeroSei
|
|
pedidosAbertos = pedidosAbertos.filter((p) => {
|
|
if (args.numeroSei && p.numeroSei !== args.numeroSei) return false;
|
|
return true;
|
|
});
|
|
|
|
// 3) Filtro por acaoId (via items)
|
|
if (args.acaoId) {
|
|
// This is expensive, but for now we iterate. Better would be to query items by acaoId first.
|
|
// Optimization: Query items by acaoId and get unique pedidoIds.
|
|
const itemsComAcao = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_acaoId', (q) => q.eq('acaoId', args.acaoId))
|
|
.collect();
|
|
|
|
const pedidoIdsComAcao = new Set(itemsComAcao.map((i) => i.pedidoId));
|
|
pedidosAbertos = pedidosAbertos.filter((p) => pedidoIdsComAcao.has(p._id));
|
|
}
|
|
|
|
// 4) Filtro por objetos (se informado) e coleta de matchingItems
|
|
const resultados = [];
|
|
|
|
for (const pedido of pedidosAbertos) {
|
|
let include = true;
|
|
let matchingItems: { objetoId: Id<'objetos'>; quantidade: number }[] = [];
|
|
|
|
// Se houver filtro de objetos, verificamos se o pedido tem ALGUM dos objetos
|
|
if (args.objetoIds && args.objetoIds.length > 0) {
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
|
|
.collect();
|
|
|
|
const matching = items.filter((i) => args.objetoIds?.includes(i.objetoId));
|
|
|
|
if (matching.length > 0) {
|
|
matchingItems = matching.map((i) => ({
|
|
objetoId: i.objetoId,
|
|
quantidade: i.quantidade
|
|
}));
|
|
} else {
|
|
include = false;
|
|
}
|
|
}
|
|
|
|
if (include) {
|
|
resultados.push({
|
|
_id: pedido._id,
|
|
_creationTime: pedido._creationTime,
|
|
numeroSei: pedido.numeroSei,
|
|
status: pedido.status,
|
|
criadoPor: pedido.criadoPor,
|
|
criadoEm: pedido.criadoEm,
|
|
atualizadoEm: pedido.atualizadoEm,
|
|
matchingItems: matchingItems.length > 0 ? matchingItems : undefined
|
|
});
|
|
}
|
|
}
|
|
|
|
return resultados;
|
|
}
|
|
});
|
|
|
|
// ========== MUTATIONS ==========
|
|
|
|
export const create = mutation({
|
|
args: {
|
|
numeroSei: v.optional(v.string())
|
|
// acaoId removed
|
|
},
|
|
returns: v.id('pedidos'),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// 1. Check Config
|
|
const config = await ctx.db.query('config').first();
|
|
if (!config || !config.comprasSetorId) {
|
|
throw new Error('Setor de Compras não configurado. Contate o administrador.');
|
|
}
|
|
|
|
// 2. Check Existing (Double check) - Removed acaoId check here as it's now per item
|
|
|
|
// 3. Create Order
|
|
const pedidoId = await ctx.db.insert('pedidos', {
|
|
numeroSei: args.numeroSei,
|
|
status: 'em_rascunho',
|
|
criadoPor: user._id,
|
|
criadoEm: Date.now(),
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
// 4. Create History
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'criacao',
|
|
detalhes: JSON.stringify({ numeroSei: args.numeroSei }),
|
|
data: Date.now()
|
|
});
|
|
|
|
return pedidoId;
|
|
}
|
|
});
|
|
|
|
export const updateSeiNumber = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
numeroSei: v.string()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido not found');
|
|
|
|
// Check if SEI number is already taken by another order
|
|
const existing = await ctx.db
|
|
.query('pedidos')
|
|
.filter((q) =>
|
|
q.and(q.eq(q.field('numeroSei'), args.numeroSei), q.neq(q.field('_id'), args.pedidoId))
|
|
)
|
|
.first();
|
|
|
|
if (existing) {
|
|
throw new Error('Este número SEI já está em uso por outro pedido.');
|
|
}
|
|
|
|
const oldSei = pedido.numeroSei;
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
numeroSei: args.numeroSei,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'atualizacao_sei',
|
|
detalhes: JSON.stringify({ de: oldSei, para: args.numeroSei }),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const addItem = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
objetoId: v.id('objetos'),
|
|
ataId: v.optional(v.id('atas')),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
modalidade: v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
),
|
|
valorEstimado: v.string(),
|
|
quantidade: v.number()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
// Ensure user has a funcionarioId linked
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
// Check if item already exists with same parameters (user, object, action, modalidade)
|
|
const existingItem = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.filter((q) =>
|
|
q.and(
|
|
q.eq(q.field('objetoId'), args.objetoId),
|
|
q.eq(q.field('adicionadoPor'), user.funcionarioId),
|
|
q.eq(q.field('acaoId'), args.acaoId),
|
|
q.eq(q.field('ataId'), args.ataId),
|
|
q.eq(q.field('modalidade'), args.modalidade)
|
|
)
|
|
)
|
|
.first();
|
|
|
|
if (existingItem) {
|
|
// Increment quantity
|
|
const novaQuantidade = existingItem.quantidade + args.quantidade;
|
|
await ctx.db.patch(existingItem._id, { quantidade: novaQuantidade });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'adicao_item_incremento',
|
|
detalhes: JSON.stringify({
|
|
objetoId: args.objetoId,
|
|
quantidadeAdicionada: args.quantidade,
|
|
novaQuantidade
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
} else {
|
|
// Insert new item
|
|
await ctx.db.insert('objetoItems', {
|
|
pedidoId: args.pedidoId,
|
|
objetoId: args.objetoId,
|
|
ataId: args.ataId,
|
|
acaoId: args.acaoId,
|
|
modalidade: args.modalidade,
|
|
valorEstimado: args.valorEstimado,
|
|
quantidade: args.quantidade,
|
|
adicionadoPor: user.funcionarioId,
|
|
criadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'adicao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: args.objetoId,
|
|
valor: args.valorEstimado,
|
|
quantidade: args.quantidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId,
|
|
modalidade: args.modalidade
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
|
|
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
|
}
|
|
});
|
|
|
|
export const updateItemQuantity = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems'),
|
|
novaQuantidade: v.number()
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item não encontrado.');
|
|
|
|
const quantidadeAnterior = item.quantidade;
|
|
|
|
// Check permission: only item owner can change quantity
|
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
|
|
|
if (!isOwner) {
|
|
throw new Error('Apenas quem adicionou este item pode alterar a quantidade.');
|
|
}
|
|
|
|
// Update quantity
|
|
await ctx.db.patch(args.itemId, { quantidade: args.novaQuantidade });
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
// Create history entry
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_quantidade',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
quantidadeAnterior,
|
|
novaQuantidade: args.novaQuantidade
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const removeItem = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item not found');
|
|
|
|
await ctx.db.delete(args.itemId);
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'remocao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
valor: item.valorEstimado
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const updateItem = mutation({
|
|
args: {
|
|
itemId: v.id('objetoItems'),
|
|
valorEstimado: v.string(),
|
|
modalidade: v.union(
|
|
v.literal('dispensa'),
|
|
v.literal('inexgibilidade'),
|
|
v.literal('adesao'),
|
|
v.literal('consumo')
|
|
),
|
|
acaoId: v.optional(v.id('acoes')),
|
|
ataId: v.optional(v.id('atas'))
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
|
|
if (!user.funcionarioId) {
|
|
throw new Error('Usuário não vinculado a um funcionário.');
|
|
}
|
|
|
|
const item = await ctx.db.get(args.itemId);
|
|
if (!item) throw new Error('Item não encontrado.');
|
|
|
|
// Apenas quem adicionou o item pode editá-lo
|
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
|
if (!isOwner) {
|
|
throw new Error('Apenas quem adicionou este item pode editá-lo.');
|
|
}
|
|
|
|
const oldValues = {
|
|
valorEstimado: item.valorEstimado,
|
|
modalidade: item.modalidade,
|
|
acaoId: 'acaoId' in item ? item.acaoId : undefined,
|
|
ataId: 'ataId' in item ? item.ataId : undefined
|
|
};
|
|
|
|
await ctx.db.patch(args.itemId, {
|
|
valorEstimado: args.valorEstimado,
|
|
modalidade: args.modalidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId
|
|
});
|
|
|
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: item.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'edicao_item',
|
|
detalhes: JSON.stringify({
|
|
objetoId: item.objetoId,
|
|
de: oldValues,
|
|
para: {
|
|
valorEstimado: args.valorEstimado,
|
|
modalidade: args.modalidade,
|
|
acaoId: args.acaoId,
|
|
ataId: args.ataId
|
|
}
|
|
}),
|
|
data: Date.now()
|
|
});
|
|
}
|
|
});
|
|
|
|
export const updateStatus = mutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
novoStatus: v.union(
|
|
v.literal('em_rascunho'),
|
|
v.literal('aguardando_aceite'),
|
|
v.literal('em_analise'),
|
|
v.literal('precisa_ajustes'),
|
|
v.literal('cancelado'),
|
|
v.literal('concluido')
|
|
)
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const user = await getUsuarioAutenticado(ctx);
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) throw new Error('Pedido not found');
|
|
|
|
const oldStatus = pedido.status;
|
|
|
|
await ctx.db.patch(args.pedidoId, {
|
|
status: args.novoStatus,
|
|
atualizadoEm: Date.now()
|
|
});
|
|
|
|
await ctx.db.insert('historicoPedidos', {
|
|
pedidoId: args.pedidoId,
|
|
usuarioId: user._id,
|
|
acao: 'alteracao_status',
|
|
detalhes: JSON.stringify({ de: oldStatus, para: args.novoStatus }),
|
|
data: Date.now()
|
|
});
|
|
|
|
// Trigger Notifications
|
|
await ctx.scheduler.runAfter(0, internal.pedidos.notifyStatusChange, {
|
|
pedidoId: args.pedidoId,
|
|
oldStatus,
|
|
newStatus: args.novoStatus,
|
|
actorId: user._id
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== INTERNAL (NOTIFICATIONS) ==========
|
|
|
|
export const notifyStatusChange = internalMutation({
|
|
args: {
|
|
pedidoId: v.id('pedidos'),
|
|
oldStatus: v.string(),
|
|
newStatus: v.string(),
|
|
actorId: v.id('usuarios')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const pedido = await ctx.db.get(args.pedidoId);
|
|
if (!pedido) return;
|
|
|
|
const actor = await ctx.db.get(args.actorId);
|
|
const actorName = actor ? actor.nome : 'Alguém';
|
|
|
|
const recipients = new Set<string>(); // Set of User IDs
|
|
|
|
// 1. If status is "aguardando_aceite", notify Purchasing Sector
|
|
if (args.newStatus === 'aguardando_aceite') {
|
|
const config = await ctx.db.query('config').first();
|
|
if (config && config.comprasSetorId) {
|
|
// Find all employees in this sector
|
|
const funcionarioSetores = await ctx.db
|
|
.query('funcionarioSetores')
|
|
.withIndex('by_setorId', (q) => q.eq('setorId', config.comprasSetorId!))
|
|
.collect();
|
|
|
|
const funcionarioIds = funcionarioSetores.map((fs) => fs.funcionarioId);
|
|
|
|
// Find users linked to these employees
|
|
for (const fId of funcionarioIds) {
|
|
const user = await ctx.db
|
|
.query('usuarios')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', fId))
|
|
.first();
|
|
if (user) recipients.add(user._id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Notify "Involved" users (Creator + Item Adders)
|
|
// Always notify creator (unless they are the actor)
|
|
if (pedido.criadoPor !== args.actorId) {
|
|
recipients.add(pedido.criadoPor);
|
|
}
|
|
|
|
// Notify item adders
|
|
const items = await ctx.db
|
|
.query('objetoItems')
|
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
|
.collect();
|
|
|
|
for (const item of items) {
|
|
const user = await ctx.db
|
|
.query('usuarios')
|
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', item.adicionadoPor))
|
|
.first();
|
|
if (user && user._id !== args.actorId) {
|
|
recipients.add(user._id);
|
|
}
|
|
}
|
|
|
|
// Send Notifications
|
|
for (const recipientId of recipients) {
|
|
const recipientIdTyped = recipientId as Id<'usuarios'>;
|
|
|
|
// 1. In-App Notification
|
|
await ctx.db.insert('notificacoes', {
|
|
usuarioId: recipientIdTyped,
|
|
tipo: 'alerta_seguranca', // Using alerta_seguranca as the closest match for system notifications
|
|
titulo: `Pedido ${pedido.numeroSei || 'sem número SEI'} atualizado`,
|
|
descricao: `Status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
|
|
lida: false,
|
|
criadaEm: Date.now(),
|
|
remetenteId: args.actorId
|
|
});
|
|
|
|
// 2. Email Notification (Async)
|
|
const recipientUser = await ctx.db.get(recipientIdTyped);
|
|
if (recipientUser && recipientUser.email) {
|
|
// Using enfileirarEmail directly
|
|
await ctx.scheduler.runAfter(0, api.email.enfileirarEmail, {
|
|
destinatario: recipientUser.email,
|
|
destinatarioId: recipientIdTyped,
|
|
assunto: `Atualização no Pedido ${pedido.numeroSei || 'sem número SEI'}`,
|
|
corpo: `O pedido ${pedido.numeroSei || 'sem número SEI'} teve seu status alterado de "${args.oldStatus}" para "${args.newStatus}" por ${actorName}.`,
|
|
enviadoPor: args.actorId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|