diff --git a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte
index 7bdba57..c2735e0 100644
--- a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte
+++ b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte
@@ -514,30 +514,23 @@
if (!pedido || !newItem.objetoId || !newItem.valorEstimado) return;
// Validação no front: garantir que todos os itens existentes do pedido
- // utilizem a mesma combinação de modalidade e ata (quando houver).
+ // utilizem a mesma ata (quando houver).
if (items.length > 0) {
const referenceItem = items[0];
-
- const referenceModalidade = (referenceItem.modalidade as Modalidade | undefined) ?? undefined;
const referenceAtaId = (('ataId' in referenceItem ? referenceItem.ataId : undefined) ??
null) as string | null;
const newAtaId = newItem.ataId || null;
-
- const sameModalidade = !referenceModalidade || newItem.modalidade === referenceModalidade;
const sameAta = referenceAtaId === newAtaId;
- if (!sameModalidade || !sameAta) {
- const refModalidadeLabel = referenceModalidade
- ? formatModalidade(referenceModalidade)
- : 'Não definida';
+ if (!sameAta) {
const refAtaLabel =
referenceAtaId === null ? 'sem Ata vinculada' : 'com uma Ata específica';
toast.error(
- `Não é possível adicionar este item com esta combinação de modalidade e ata.\n\n` +
- `Este pedido já está utilizando Modalidade: ${refModalidadeLabel} e está ${refAtaLabel}.\n` +
- `Todos os itens do pedido devem usar a mesma modalidade e a mesma ata (quando houver).`
+ `Não é possível adicionar este item com esta ata.\n\n` +
+ `Este pedido já está vinculado a: ${refAtaLabel}.\n` +
+ `Todos os itens do pedido devem usar a mesma ata (quando houver).`
);
return;
}
@@ -550,7 +543,6 @@
objetoId: newItem.objetoId as Id<'objetos'>,
valorEstimado: newItem.valorEstimado,
quantidade: newItem.quantidade,
- modalidade: newItem.modalidade,
acaoId: newItem.acaoId ? (newItem.acaoId as Id<'acoes'>) : undefined,
ataId: newItem.ataId ? (newItem.ataId as Id<'atas'>) : undefined
});
@@ -1664,22 +1656,8 @@
placeholder="R$ 0,00"
/>
-
Ata (Opcional)
- {#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
+ {#if permissions?.canEditModalidade}
Adesão
{:else}
- {item.modalidade}
+ {formatModalidade(item.modalidade as Modalidade) || '-'}
{/if}
@@ -1942,7 +1920,7 @@
{/if}
- {#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
+ {#if permissions?.canEditAta}
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
- import type { FunctionReturnType } from 'convex/server';
import { useConvexClient, useQuery } from 'convex-svelte';
import { Plus, Trash2, X, Info } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
- import { formatarDataBR } from '$lib/utils/datas';
const client = useConvexClient();
@@ -27,40 +25,28 @@
let warning = $state(null);
// Item selection state
+ // Nota: modalidade é opcional aqui pois será definida pelo Setor de Compras posteriormente
type SelectedItem = {
objeto: Doc<'objetos'>;
quantidade: number;
- modalidade: 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
acaoId?: Id<'acoes'>;
- ataId?: Id<'atas'>;
- ataNumero?: string; // For display
- ata?: FunctionReturnType[number]; // dados mínimos p/ exibir detalhes
};
let selectedItems = $state([]);
let selectedObjetoIds = $derived(selectedItems.map((i) => i.objeto._id));
- let hasMixedModalidades = $derived(new Set(selectedItems.map((i) => i.modalidade)).size > 1);
// Item configuration modal
let showItemModal = $state(false);
let itemConfig = $state<{
objeto: Doc<'objetos'> | null;
quantidade: number;
- modalidade: 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
acaoId: string; // using string to handle empty select
- ataId: string; // using string to handle empty select
}>({
objeto: null,
quantidade: 1,
- modalidade: 'consumo',
- acaoId: '',
- ataId: ''
+ acaoId: ''
});
- type AtasComLimite = FunctionReturnType;
- let availableAtas = $state([]);
-
- // Item Details Modal
let showDetailsModal = $state(false);
let detailsItem = $state(null);
@@ -75,16 +61,10 @@
}
async function openItemModal(objeto: Doc<'objetos'>) {
- // Fetch linked Atas for this object
- const linkedAtas = await client.query(api.objetos.getAtasComLimite, { objetoId: objeto._id });
- availableAtas = linkedAtas;
-
itemConfig = {
objeto,
quantidade: 1,
- modalidade: 'consumo',
- acaoId: '',
- ataId: ''
+ acaoId: ''
};
showItemModal = true;
searchQuery = ''; // Clear search
@@ -93,24 +73,17 @@
function closeItemModal() {
showItemModal = false;
itemConfig.objeto = null;
- availableAtas = [];
}
function confirmAddItem() {
if (!itemConfig.objeto) return;
- const selectedAta = availableAtas.find((a) => a._id === itemConfig.ataId);
-
selectedItems = [
...selectedItems,
{
objeto: itemConfig.objeto,
quantidade: itemConfig.quantidade,
- modalidade: itemConfig.modalidade,
- acaoId: itemConfig.acaoId ? (itemConfig.acaoId as Id<'acoes'>) : undefined,
- ataId: itemConfig.ataId ? (itemConfig.ataId as Id<'atas'>) : undefined,
- ataNumero: selectedAta?.numero,
- ata: selectedAta
+ acaoId: itemConfig.acaoId ? (itemConfig.acaoId as Id<'acoes'>) : undefined
}
];
checkExisting();
@@ -131,7 +104,6 @@
criadoEm: number;
matchingItems?: {
objetoId: Id<'objetos'>;
- modalidade: SelectedItem['modalidade'];
quantidade: number;
}[];
}[]
@@ -157,36 +129,6 @@
}
}
- function formatModalidade(modalidade: SelectedItem['modalidade']) {
- switch (modalidade) {
- case 'consumo':
- return 'Consumo';
- case 'dispensa':
- return 'Dispensa';
- case 'inexgibilidade':
- return 'Inexigibilidade';
- case 'adesao':
- return 'Adesão';
- default:
- return modalidade;
- }
- }
-
- function getModalidadeBadgeClasses(modalidade: SelectedItem['modalidade']) {
- switch (modalidade) {
- case 'consumo':
- return 'bg-blue-100 text-blue-800';
- case 'dispensa':
- return 'bg-yellow-100 text-yellow-800';
- case 'inexgibilidade':
- return 'bg-purple-100 text-purple-800';
- case 'adesao':
- return 'bg-green-100 text-green-800';
- default:
- return 'bg-gray-100 text-gray-800';
- }
- }
-
function getAcaoNome(acaoId: Id<'acoes'> | undefined) {
if (!acaoId) return '-';
const acao = acoes.find((a) => a._id === acaoId);
@@ -206,8 +148,7 @@
.map((match) => {
// Find name from selected items (might be multiple with same object, just pick one name)
const item = selectedItems.find((p) => p.objeto._id === match.objetoId);
- const modalidadeLabel = formatModalidade(match.modalidade);
- return `${item?.objeto.nome} (${modalidadeLabel}): ${match.quantidade} un.`;
+ return `${item?.objeto.nome}: ${match.quantidade} un.`;
})
.join(', ');
@@ -218,9 +159,7 @@
if (!pedido.matchingItems || pedido.matchingItems.length === 0) return null;
for (const match of pedido.matchingItems) {
- const item = selectedItems.find(
- (p) => p.objeto._id === match.objetoId && p.modalidade === match.modalidade
- );
+ const item = selectedItems.find((p) => p.objeto._id === match.objetoId);
if (item) {
return item;
}
@@ -239,16 +178,11 @@
const params = new URLSearchParams();
params.set('obj', matchedItem.objeto._id);
params.set('qtd', String(matchedItem.quantidade));
- params.set('mod', matchedItem.modalidade);
if (matchedItem.acaoId) {
params.set('acao', matchedItem.acaoId);
}
- if (matchedItem.ataId) {
- params.set('ata', matchedItem.ataId);
- }
-
return `/pedidos/${pedido._id}?${params.toString()}` as `/pedidos/${string}`;
}
@@ -261,13 +195,11 @@
checking = true;
try {
- // Importante: ação (acaoId) NÃO entra no filtro de similaridade.
- // O filtro considera apenas combinação de objeto + modalidade.
+ // Importante: O filtro considera apenas objetoId (modalidade não é mais usada na criação).
const itensFiltro =
selectedItems.length > 0
? selectedItems.map((item) => ({
- objetoId: item.objeto._id,
- modalidade: item.modalidade
+ objetoId: item.objeto._id
}))
: undefined;
@@ -292,11 +224,6 @@
async function handleSubmit(e: Event) {
e.preventDefault();
- if (hasMixedModalidades) {
- error =
- 'Não é possível criar o pedido com itens de modalidades diferentes. Ajuste os itens antes de continuar.';
- return;
- }
creating = true;
error = null;
try {
@@ -312,9 +239,7 @@
objetoId: item.objeto._id,
valorEstimado: item.objeto.valorEstimado,
quantidade: item.quantidade,
- modalidade: item.modalidade,
- acaoId: item.acaoId,
- ataId: item.ataId
+ acaoId: item.acaoId
})
)
);
@@ -417,20 +342,6 @@
{item.objeto.nome}
-
- {formatModalidade(item.modalidade)}
-
- {#if item.ataNumero}
-
- Ata {item.ataNumero}
-
- {/if}
{#if item.acaoId}
- {#if hasMixedModalidades}
-
-
Modalidades diferentes detectadas
-
- Não é possível criar o pedido com itens de modalidades diferentes. Ajuste os itens para
- usar uma única modalidade.
-
-
- {/if}
{#if warning}
Pedidos similares encontrados:
{#each existingPedidos as pedido (pedido._id)}
- {@const first = getFirstMatchingSelectedItem(pedido)}
Pedido {pedido.numeroSei || 'sem número SEI'} — {formatStatus(pedido.status)}
- {#if first}
-
- Modalidade: {formatModalidade(first.modalidade)}
-
- {/if}
+
{#if getMatchingInfo(pedido)}
{getMatchingInfo(pedido)}
@@ -553,7 +446,7 @@
{creating ? 'Criando...' : 'Criar Pedido'}
@@ -600,62 +493,6 @@
/>
-
-
- Modalidade
-
-
- Consumo
- Dispensa
- Inexigibilidade
- Adesão
-
-
-
- {#if availableAtas.length > 0}
-
-
-
- {availableAtas.length}
- {availableAtas.length === 1 ? 'Ata' : 'Atas'}
-
- disponível para este objeto
-
-
- Selecionar Ata (Opcional)
-
-
- Nenhuma
- {#each availableAtas as ata (ata._id)}
- {@const reason =
- ata.lockReason === 'nao_configurada'
- ? 'não configurada'
- : ata.lockReason === 'limite_atingido'
- ? 'limite atingido'
- : ata.lockReason === 'vigencia_expirada'
- ? `vigência encerrada em ${
- ata.dataFimEfetiva || ata.dataFim
- ? formatarDataBR((ata.dataFimEfetiva || ata.dataFim) as string)
- : '-'
- }`
- : null}
-
- Ata {ata.numero} (SEI: {ata.numeroSei}){reason ? ` (${reason})` : ''}
-
- {/each}
-
-
- {/if}
-
Ação (Opcional)
@@ -723,7 +560,7 @@
Pedido
Quantidade: {detailsItem.quantidade}
-
Modalidade: {detailsItem.modalidade}
+
{#if detailsItem.acaoId}
Ação:
@@ -731,32 +568,6 @@
{/if}
-
- {#if detailsItem.ata}
-
-
Ata de Registro de Preços
-
Número: {detailsItem.ata.numero}
-
- Processo SEI:
- {detailsItem.ata.numeroSei}
-
- {#if detailsItem.ata.dataInicio}
-
- Vigência:
- {formatarDataBR(detailsItem.ata.dataInicio)} até {detailsItem.ata
- .dataFimEfetiva || detailsItem.ata.dataFim
- ? formatarDataBR(
- (detailsItem.ata.dataFimEfetiva || detailsItem.ata.dataFim) as string
- )
- : 'Indefinido'}
-
- {/if}
-
- {:else}
-
-
Nenhuma Ata vinculada a este item.
-
- {/if}
diff --git a/packages/backend/convex/pedidos.ts b/packages/backend/convex/pedidos.ts
index 8781225..f7159d5 100644
--- a/packages/backend/convex/pedidos.ts
+++ b/packages/backend/convex/pedidos.ts
@@ -159,12 +159,11 @@ async function getUsuarioAutenticado(ctx: QueryCtx | MutationCtx) {
return user;
}
-// Garante que todos os itens de um pedido utilizem a mesma
-// combinação de modalidade e ata (quando houver).
-async function ensurePedidoModalidadeAtaConsistency(
+// Garante que todos os itens de um pedido utilizem a mesma ata (quando houver).
+// Nota: Modalidade não é mais validada aqui, pois é definida apenas pelo Setor de Compras.
+async function ensurePedidoAtaConsistency(
ctx: MutationCtx,
pedidoId: Id<'pedidos'>,
- modalidade: Doc<'objetoItems'>['modalidade'],
ataId: Id<'atas'> | undefined,
ignoreItemId?: Id<'objetoItems'>
) {
@@ -185,12 +184,11 @@ async function ensurePedidoModalidadeAtaConsistency(
const normalizedItemAtaId = (('ataId' in item ? item.ataId : undefined) ??
null) as Id<'atas'> | null;
- const modalidadeMismatch = !!item.modalidade && !!modalidade && item.modalidade !== modalidade;
const ataMismatch = normalizedItemAtaId !== normalizedNewAtaId;
- if (modalidadeMismatch || ataMismatch) {
+ if (ataMismatch) {
throw new Error(
- 'Todos os itens do pedido devem usar a mesma modalidade e a mesma ata (quando houver). Ajuste os itens existentes ou crie um novo pedido para a nova combinação.'
+ 'Todos os itens do pedido devem usar a mesma ata (quando houver). Ajuste os itens existentes ou crie um novo pedido para a nova ata.'
);
}
}
@@ -580,16 +578,11 @@ export const getHistory = query({
export const checkExisting = query({
args: {
numeroSei: v.optional(v.string()),
+ // Modalidade removida do filtro - agora busca apenas por objetoId
itensFiltro: v.optional(
v.array(
v.object({
- objetoId: v.id('objetos'),
- modalidade: v.union(
- v.literal('dispensa'),
- v.literal('inexgibilidade'),
- v.literal('adesao'),
- v.literal('consumo')
- )
+ objetoId: v.id('objetos')
})
)
)
@@ -608,7 +601,6 @@ export const checkExisting = query({
v.literal('cancelado'),
v.literal('concluido')
),
- // acaoId removed
criadoPor: v.id('usuarios'),
aceitoPor: v.optional(v.id('funcionarios')),
descricaoAjuste: v.optional(v.string()),
@@ -618,12 +610,6 @@ export const checkExisting = query({
v.array(
v.object({
objetoId: v.id('objetos'),
- modalidade: v.union(
- v.literal('dispensa'),
- v.literal('inexgibilidade'),
- v.literal('adesao'),
- v.literal('consumo')
- ),
quantidade: v.number()
})
)
@@ -654,7 +640,7 @@ export const checkExisting = query({
return true;
});
- // 3) Filtro por itens (objetoId + modalidade), se informado, e coleta de matchingItems
+ // 3) Filtro por itens (apenas objetoId), se informado, e coleta de matchingItems
const resultados = [];
const itensFiltro = args.itensFiltro ?? [];
@@ -663,31 +649,21 @@ export const checkExisting = query({
let include = true;
let matchingItems: {
objetoId: Id<'objetos'>;
- modalidade: NonNullable['modalidade']>;
quantidade: number;
}[] = [];
- // Se houver filtro de itens, verificamos se o pedido tem ALGUM dos itens (objetoId + modalidade)
+ // Se houver filtro de itens, verificamos se o pedido tem ALGUM dos itens (apenas objetoId)
if (itensFiltro.length > 0) {
const items = await ctx.db
.query('objetoItems')
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedido._id))
.collect();
- const matching = items.filter((i) =>
- itensFiltro.some(
- (f) => f.objetoId === i.objetoId && f.modalidade === (i.modalidade ?? 'consumo')
- )
- );
+ const matching = items.filter((i) => itensFiltro.some((f) => f.objetoId === i.objetoId));
if (matching.length > 0) {
matchingItems = matching.map((i) => ({
objetoId: i.objetoId,
- modalidade: (i.modalidade ?? 'consumo') as
- | 'dispensa'
- | 'inexgibilidade'
- | 'adesao'
- | 'consumo',
quantidade: i.quantidade
}));
} else {
@@ -1442,9 +1418,27 @@ export const addItem = mutation({
const modalidade =
args.modalidade ?? userProductItems.find((i) => !!i.modalidade)?.modalidade ?? undefined;
- // Regra global: todos os itens do pedido devem ter a mesma
- // modalidade e a mesma ata (quando houver).
- await ensurePedidoModalidadeAtaConsistency(ctx, args.pedidoId, modalidade, args.ataId);
+ // Regra global: todos os itens do pedido devem ter a mesma ata (quando houver).
+ await ensurePedidoAtaConsistency(ctx, args.pedidoId, args.ataId);
+
+ // Bloqueia ataId se não for Compras em análise
+ if (args.ataId) {
+ const config = await ctx.db.query('config').first();
+ let isInComprasSector = false;
+ if (config?.comprasSetorId) {
+ const funcionarioSetores = await ctx.db
+ .query('funcionarioSetores')
+ .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
+ .filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
+ .first();
+ isInComprasSector = !!funcionarioSetores;
+ }
+ if (!(pedido.status === 'em_analise' && isInComprasSector)) {
+ throw new Error(
+ 'Apenas funcionários do Setor de Compras podem vincular uma Ata, e somente quando o pedido está em análise.'
+ );
+ }
+ }
// --- CHECK ANALYSIS MODE ---
// Em pedidos em análise, a inclusão de itens deve passar por fluxo de aprovação.
@@ -1846,9 +1840,35 @@ export const updateItem = mutation({
const pedido = await ctx.db.get(item.pedidoId);
if (!pedido) throw new Error('Pedido não encontrado.');
- // Apenas quem adicionou o item pode editá-lo
+ // Apenas quem adicionou o item pode editá-lo (outras propriedades)
const isOwner = item.adicionadoPor === user.funcionarioId;
- if (!isOwner) {
+
+ // Verificar permissão para editar modalidade e ata
+ // Somente Setor de Compras pode editar modalidade e ata, e apenas quando em análise
+ const config = await ctx.db.query('config').first();
+ let isInComprasSector = false;
+ if (config?.comprasSetorId) {
+ const funcionarioSetores = await ctx.db
+ .query('funcionarioSetores')
+ .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
+ .filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
+ .first();
+ isInComprasSector = !!funcionarioSetores;
+ }
+
+ const canEditComprasFields = pedido.status === 'em_analise' && isInComprasSector;
+
+ // Se está tentando editar a modalidade ou a ata, verificar permissão
+ const isChangingModalidade = item.modalidade !== args.modalidade;
+ const isChangingAta = (item.ataId ?? null) !== (args.ataId ?? null);
+ if ((isChangingModalidade || isChangingAta) && !canEditComprasFields) {
+ throw new Error(
+ 'Apenas funcionários do Setor de Compras podem editar a modalidade ou a ata, e somente quando o pedido está em análise.'
+ );
+ }
+
+ // Para outras propriedades, apenas o dono do item pode editar
+ if (!isOwner && !canEditComprasFields) {
throw new Error('Apenas quem adicionou este item pode editá-lo.');
}
@@ -1860,14 +1880,8 @@ export const updateItem = mutation({
};
// Regra global: ao alterar um item, garantir que os demais itens do pedido
- // continuem (ou passem a estar) com a mesma modalidade e ata.
- await ensurePedidoModalidadeAtaConsistency(
- ctx,
- item.pedidoId,
- args.modalidade,
- args.ataId,
- args.itemId
- );
+ // continuem (ou passem a estar) com a mesma ata.
+ await ensurePedidoAtaConsistency(ctx, item.pedidoId, args.ataId, args.itemId);
// Em pedidos em análise ou aguardando aceite, geramos uma solicitação em vez de alterar diretamente
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
@@ -1954,6 +1968,8 @@ export const getPermissions = query({
canCancel: false,
canCompleteAdjustments: false,
canManageRequests: false,
+ canEditModalidade: false,
+ canEditAta: false,
currentFuncionarioId: user?.funcionarioId ?? null
};
}
@@ -2004,6 +2020,9 @@ export const getPermissions = query({
isCreator &&
hasOnlyCreatorItems,
canManageRequests: pedido.status === 'em_analise' && isInComprasSector,
+ // Somente Setor de Compras pode editar modalidade e ata, e apenas quando o pedido está em análise
+ canEditModalidade: pedido.status === 'em_analise' && isInComprasSector,
+ canEditAta: pedido.status === 'em_analise' && isInComprasSector,
currentFuncionarioId: user.funcionarioId
};
}
@@ -2524,14 +2543,8 @@ export const approveItemRequest = mutation({
// We trust the request data structure matches addItem args
const newItem = data;
- // Regra global: todos os itens do pedido devem ter a mesma
- // modalidade e ata (quando houver).
- await ensurePedidoModalidadeAtaConsistency(
- ctx,
- request.pedidoId,
- newItem.modalidade,
- newItem.ataId
- );
+ // Regra global: todos os itens do pedido devem ter a mesma ata (quando houver).
+ await ensurePedidoAtaConsistency(ctx, request.pedidoId, newItem.ataId);
// Note: We MUST use the original requester's ID (request.solicitadoPor) as addedBy?
// Or should we attribute it to the requester? YES.
// BUT `addItem` logic usually checks if `existingItem.adicionadoPor === user`.
@@ -2625,13 +2638,7 @@ export const approveItemRequest = mutation({
const item = await ctx.db.get(itemId);
if (item) {
// Regra global também se aplica na alteração de detalhes aprovada
- await ensurePedidoModalidadeAtaConsistency(
- ctx,
- item.pedidoId,
- para.modalidade,
- para.ataId,
- itemId
- );
+ await ensurePedidoAtaConsistency(ctx, item.pedidoId, para.ataId, itemId);
const oldAtaId = ('ataId' in item ? item.ataId : undefined) ?? undefined;
const newAtaId = para.ataId;