diff --git a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte index e81e807..dce4f1c 100644 --- a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte @@ -1,78 +1,79 @@ -
- - +
+ + - -
-
-
- -
-
-

Licitações

-

Gestão de processos licitatórios

-
-
-
+
+ +
+
+
+ +
+

Empresas

+
+

+ Cadastro, listagem e edição de empresas e seus contatos. +

+
+
-
- -
-
-
- -
-

Empresas

-
-

- Cadastro, listagem e edição de empresas e seus contatos. -

-
-
+ +
+
+
+ +
+

Contratos

+
+

Gestão de contratos, vigências e situações.

+
+
-
-
-
-
- -
-

Processos Licitatórios

-
-

- Em breve: cadastro e acompanhamento de licitações. -

-
-
+
+
+
+
+ +
+

Processos Licitatórios

+
+

+ Em breve: cadastro e acompanhamento de licitações. +

+
+
-
-
-
-
- -
-

Documentação

-
-

- Em breve: gestão de documentos e editais. -

-
-
-
-
+
+
+
+
+ +
+

Documentação

+
+

Em breve: gestão de documentos e editais.

+
+
+ +
- diff --git a/apps/web/src/routes/(dashboard)/licitacoes/contratos/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/contratos/+page.svelte new file mode 100644 index 0000000..823207a --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/contratos/+page.svelte @@ -0,0 +1,244 @@ + + +
+
+
+

Contratos

+

Gerencie os contratos, vigências e situações.

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + {#if isLoading} + + + + {:else if error} + + + + {:else if contratos.length === 0} + + + + {:else} + {#each contratos as contrato (contrato._id)} + + + + + + + + + + + {/each} + {/if} + +
Nº ContratoObjetoContratadaVigênciaValorSituaçãoResponsávelAções
+ +
+ Erro ao carregar contratos: {error.message} +
+ Nenhum contrato encontrado. +
+
+ {contrato.numeroContrato}/{contrato.anoContrato} + {#if isProximoVencimento(contrato.dataFimVigencia, contrato.diasAvisoVencimento)} +
+ +
+ {/if} +
+
+ {contrato.objeto} + + {contrato.contratada?.razao_social || 'Empresa não encontrada'} + +
+ {formatarData(contrato.dataInicioVigencia)} até +
+ {formatarData(contrato.dataFimVigencia)} +
+
{formatarMoeda(contrato.valorTotal)} +
+ {contrato.situacao.replace('_', ' ').toUpperCase()} +
+
+ {contrato.responsavel?.nome || '-'} + + + +
+
+
diff --git a/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte new file mode 100644 index 0000000..918d402 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/contratos/[id]/+page.svelte @@ -0,0 +1,394 @@ + + +
+
+ +
+

Editar Contrato

+

Atualize os dados do contrato.

+
+
+ + {#if isLoading} +
+ +
+ {:else if error} +
+ Erro ao carregar contrato: {error.message} +
+ {:else if contrato} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ {/if} +
diff --git a/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte new file mode 100644 index 0000000..b7dc59a --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/contratos/novo/+page.svelte @@ -0,0 +1,363 @@ + + +
+
+ +
+

Novo Contrato

+

Preencha os dados do novo contrato.

+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index a64020a..4900e82 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -21,6 +21,7 @@ import type * as auth_utils from "../auth/utils.js"; import type * as chamados from "../chamados.js"; import type * as chat from "../chat.js"; import type * as configuracaoEmail from "../configuracaoEmail.js"; +import type * as contratos from "../contratos.js"; import type * as crons from "../crons.js"; import type * as cursos from "../cursos.js"; import type * as dashboard from "../dashboard.js"; @@ -71,6 +72,7 @@ declare const fullApi: ApiFromModules<{ chamados: typeof chamados; chat: typeof chat; configuracaoEmail: typeof configuracaoEmail; + contratos: typeof contratos; crons: typeof crons; cursos: typeof cursos; dashboard: typeof dashboard; diff --git a/packages/backend/convex/contratos.ts b/packages/backend/convex/contratos.ts new file mode 100644 index 0000000..059101a --- /dev/null +++ b/packages/backend/convex/contratos.ts @@ -0,0 +1,200 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; +import { situacaoContrato } from "./schema"; +import { getCurrentUserFunction } from "./auth"; +import { internal } from "./_generated/api"; + +export const listar = query({ + args: { + responsavelId: v.optional(v.id("funcionarios")), + dataInicio: v.optional(v.string()), + dataFim: v.optional(v.string()), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "listar", + }); + + let q = ctx.db.query("contratos"); + + if (args.responsavelId) { + q = q.withIndex("by_responsavel", (q) => + q.eq("responsavelId", args.responsavelId!) + ) as typeof q; + } + + const contratos = await q.collect(); + + // Filtros em memória para datas (já que Convex não tem filtro de range nativo eficiente combinado com outros índices sem setup complexo) + // Se o volume for muito grande, ideal seria criar índices específicos ou usar search. + let resultado = contratos; + + if (args.dataInicio) { + resultado = resultado.filter( + (c) => c.dataInicioVigencia >= args.dataInicio! + ); + } + + if (args.dataFim) { + resultado = resultado.filter((c) => c.dataFimVigencia <= args.dataFim!); + } + + // Enriquecer com dados relacionados + const contratosEnriquecidos = await Promise.all( + resultado.map(async (c) => { + const contratada = await ctx.db.get(c.contratadaId); + const responsavel = await ctx.db.get(c.responsavelId); + return { + ...c, + contratada, + responsavel, + }; + }) + ); + + return contratosEnriquecidos; + }, +}); + +export const obter = query({ + args: { id: v.id("contratos") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "ver", + }); + const contrato = await ctx.db.get(args.id); + if (!contrato) return null; + + const contratada = await ctx.db.get(contrato.contratadaId); + const responsavel = await ctx.db.get(contrato.responsavelId); + + return { + ...contrato, + contratada, + responsavel, + }; + }, +}); + +export const criar = mutation({ + args: { + 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(), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "criar", + }); + + const usuario = await getCurrentUserFunction(ctx); + if (!usuario) throw new Error("Não autenticado"); + + const id = await ctx.db.insert("contratos", { + ...args, + criadoPor: usuario._id, + criadoEm: Date.now(), + }); + + return id; + }, +}); + +export const editar = mutation({ + args: { + id: v.id("contratos"), + contratadaId: v.optional(v.id("empresas")), + objeto: v.optional(v.string()), + numeroNotaEmpenho: v.optional(v.string()), + responsavelId: v.optional(v.id("funcionarios")), + departamento: v.optional(v.string()), + situacao: v.optional(situacaoContrato), + numeroProcessoLicitatorio: v.optional(v.string()), + modalidade: v.optional(v.string()), + numeroContrato: v.optional(v.string()), + anoContrato: v.optional(v.number()), + dataInicioVigencia: v.optional(v.string()), + dataFimVigencia: v.optional(v.string()), + nomeFiscal: v.optional(v.string()), + valorTotal: v.optional(v.string()), + dataAditivoPrazo: v.optional(v.string()), + diasAvisoVencimento: v.optional(v.number()), + }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "editar", + }); + + const { id, ...campos } = args; + + await ctx.db.patch(id, { + ...campos, + atualizadoEm: Date.now(), + }); + }, +}); + +export const excluir = mutation({ + args: { id: v.id("contratos") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "contratos", + acao: "excluir", + }); + await ctx.db.delete(args.id); + }, +}); + +export const verificarVencimentos = query({ + args: {}, + handler: async (ctx) => { + // Esta query pode ser usada por um componente de notificação ou cron job + // Retorna contratos que estão próximos do vencimento baseados no diasAvisoVencimento + + const hoje = new Date(); + const hojeStr = hoje.toISOString().split("T")[0]; + + // Buscar contratos ativos (em execução ou aguardando assinatura) + const contratos = await ctx.db + .query("contratos") + .filter((q) => + q.or( + q.eq(q.field("situacao"), "em_execucao"), + q.eq(q.field("situacao"), "aguardando_assinatura") + ) + ) + .collect(); + + const proximosVencimento = contratos.filter((c) => { + if (!c.dataFimVigencia) return false; + + const dataFim = new Date(c.dataFimVigencia); + const dataAviso = new Date(dataFim); + dataAviso.setDate(dataAviso.getDate() - c.diasAvisoVencimento); + + const dataAvisoStr = dataAviso.toISOString().split("T")[0]; + + // Se hoje for maior ou igual a data de aviso e menor que a data fim + return hojeStr >= dataAvisoStr && hojeStr <= c.dataFimVigencia; + }); + + return proximosVencimento; + }, +}); diff --git a/packages/backend/convex/permissoesAcoes.ts b/packages/backend/convex/permissoesAcoes.ts index ee982f4..d92e1d9 100644 --- a/packages/backend/convex/permissoesAcoes.ts +++ b/packages/backend/convex/permissoesAcoes.ts @@ -224,6 +224,36 @@ const PERMISSOES_BASE = { acao: 'ver', descricao: 'Acessar telas do módulo de licitações' }, + { + nome: 'contratos.listar', + recurso: 'contratos', + acao: 'listar', + descricao: 'Listar contratos' + }, + { + nome: 'contratos.criar', + recurso: 'contratos', + acao: 'criar', + descricao: 'Criar novos contratos' + }, + { + nome: 'contratos.editar', + recurso: 'contratos', + acao: 'editar', + descricao: 'Editar contratos' + }, + { + nome: 'contratos.excluir', + recurso: 'contratos', + acao: 'excluir', + descricao: 'Excluir contratos' + }, + { + nome: 'contratos.ver', + recurso: 'contratos', + acao: 'ver', + descricao: 'Visualizar detalhes de contratos' + }, // Compras { nome: 'compras.ver', diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index e2a8b3d..4d22d33 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -120,7 +120,40 @@ export const reportStatus = v.union( v.literal("falhou") ); +export const situacaoContrato = v.union( + v.literal("em_execucao"), + v.literal("rescendido"), + v.literal("aguardando_assinatura"), + v.literal("finalizado") +); + export default defineSchema({ + 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(),