diff --git a/apps/web/src/lib/utils/masks.ts b/apps/web/src/lib/utils/masks.ts index 2aab943..d418b61 100644 --- a/apps/web/src/lib/utils/masks.ts +++ b/apps/web/src/lib/utils/masks.ts @@ -43,6 +43,16 @@ export const maskCEP = (value: string): string => { return digits.replace(/(\d{5})(\d{1,3})$/, "$1-$2"); }; +/** Format CNPJ: 00.000.000/0000-00 */ +export const maskCNPJ = (value: string): string => { + const digits = onlyDigits(value).slice(0, 14); + return digits + .replace(/(\d{2})(\d)/, "$1.$2") + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d)/, "$1/$2") + .replace(/(\d{4})(\d{1,2})$/, "$1-$2"); +}; + /** Format phone: (00) 0000-0000 or (00) 00000-0000 */ export const maskPhone = (value: string): string => { const digits = onlyDigits(value).slice(0, 11); diff --git a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte index 2251dff..dce4f1c 100644 --- a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte @@ -1,92 +1,79 @@ -
- - +
+ + - -
-
-
- -
-
-

Licitações

-

Gestão de processos licitatórios

-
-
-
+
+ +
+
+
+ +
+

Empresas

+
+

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

+
+
- -
-
-
-
- -
-

Módulo em Desenvolvimento

-

- O módulo de Licitações está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de processos licitatórios. -

-
- - Em Desenvolvimento -
-
-
-
+ +
+
+
+ +
+

Contratos

+
+

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

+
+
- -
-

Funcionalidades Previstas

-
-
-
-
-
- -
-

Processos Licitatórios

-
-

Cadastro e acompanhamento de licitações

-
-
+
+
+
+
+ +
+

Processos Licitatórios

+
+

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

+
+
-
-
-
-
- -
-

Fornecedores

-
-

Cadastro e gestão de fornecedores

-
-
- -
-
-
-
- -
-

Documentação

-
-

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/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte new file mode 100644 index 0000000..24c8b47 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte @@ -0,0 +1,931 @@ + + +
+ + +
+
+
+ +
+
+

Empresas

+

+ Cadastro, listagem e contatos de empresas fornecedoras. +

+
+
+ +
+ +
+
+ {#if empresasQuery.isLoading} +
+ +
+ {:else if empresasQuery.error} +
+ Erro ao carregar empresas. +
+ {:else if empresasQuery.data && empresasQuery.data.length === 0} +
+

Nenhuma empresa cadastrada ainda.

+ +
+ {:else if empresasQuery.data} +
+ + + + + + + + + + + + {#each empresasQuery.data as empresa (empresa._id)} + + + + + + + + {/each} + +
CNPJRazão social / Nome fantasiaTelefoneE-mailAções
{empresa.cnpj} +
+ {empresa.razao_social} + {#if empresa.nome_fantasia} + {empresa.nome_fantasia} + {/if} +
+
+ + {empresa.telefone} + + + {empresa.email} + +
+ + +
+
+
+ {/if} +
+
+ + {#if modalAberto} + + {/if} + + {#if contatosModalAberto} + + {/if} +
+ diff --git a/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte b/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte index eed8a11..3da1d68 100644 --- a/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/programas-esportivos/+page.svelte @@ -1,5 +1,5 @@ @@ -25,23 +25,37 @@ -
-
-
-
- +
+ + - diff --git a/bun.lock b/bun.lock index ac40209..3d30081 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "sgse-app", diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 8e8b485..fdf3457 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -21,13 +21,13 @@ 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 configuracaoPonto from "../configuracaoPonto.js"; -import type * as configuracaoRelogio from "../configuracaoRelogio.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"; import type * as documentos from "../documentos.js"; import type * as email from "../email.js"; +import type * as empresas from "../empresas.js"; import type * as ferias from "../ferias.js"; import type * as funcionarios from "../funcionarios.js"; import type * as healthCheck from "../healthCheck.js"; @@ -73,6 +73,7 @@ declare const fullApi: ApiFromModules<{ chamados: typeof chamados; chat: typeof chat; configuracaoEmail: typeof configuracaoEmail; + contratos: typeof contratos; configuracaoPonto: typeof configuracaoPonto; configuracaoRelogio: typeof configuracaoRelogio; crons: typeof crons; @@ -80,6 +81,7 @@ declare const fullApi: ApiFromModules<{ dashboard: typeof dashboard; documentos: typeof documentos; email: typeof email; + empresas: typeof empresas; ferias: typeof ferias; funcionarios: typeof funcionarios; healthCheck: typeof healthCheck; 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/empresas.ts b/packages/backend/convex/empresas.ts new file mode 100644 index 0000000..996699f --- /dev/null +++ b/packages/backend/convex/empresas.ts @@ -0,0 +1,292 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server"; +import { internal } from "./_generated/api"; +import { getCurrentUserFunction } from "./auth"; +import type { Id } from "./_generated/dataModel"; + +export const list = query({ + args: {}, + handler: async (ctx) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "listar", + }); + + const empresas = await ctx.db.query("empresas").collect(); + return empresas; + }, +}); + +export const getById = query({ + args: { id: v.id("empresas") }, + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "ver", + }); + + const empresa = await ctx.db.get(args.id); + if (!empresa) { + return null; + } + + const contatos = await ctx.db + .query("contatosEmpresa") + .withIndex("by_empresa", (q) => q.eq("empresaId", args.id)) + .collect(); + const endereco = empresa.enderecoId + ? await ctx.db.get(empresa.enderecoId as Id<"enderecos">) + : null; + + return { ...empresa, endereco, contatos }; + }, +}); + +const contatoInput = v.object({ + _id: v.optional(v.id("contatosEmpresa")), + empresaId: v.optional(v.id("empresas")), + nome: v.string(), + funcao: v.string(), + email: v.string(), + telefone: v.string(), + adicionadoPor: v.optional(v.id("usuarios")), + descricao: v.optional(v.string()), + _deleted: v.optional(v.boolean()), +}); + +const enderecoInput = v.object({ + cep: v.string(), + logradouro: v.string(), + numero: v.string(), + complemento: v.optional(v.string()), + bairro: v.string(), + cidade: v.string(), + uf: v.string(), +}); + +export const create = mutation({ + args: { + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + endereco: v.optional(enderecoInput), + contatos: v.optional(v.array(contatoInput)), + }, + returns: v.id("empresas"), + handler: async (ctx, args) => { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "criar", + }); + + const cnpjExistente = await ctx.db + .query("empresas") + .withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj)) + .unique(); + + if (cnpjExistente) { + throw new Error("Já existe uma empresa cadastrada com este CNPJ."); + } + let enderecoId: Id<"enderecos"> | undefined; + if (args.endereco) { + enderecoId = await ctx.db.insert("enderecos", { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + criadoPor: usuarioAtual._id, + atualizadoPor: usuarioAtual._id, + }); + } + + const empresaDoc: { + razao_social: string; + nome_fantasia?: string; + cnpj: string; + telefone: string; + email: string; + descricao?: string; + enderecoId?: Id<"enderecos">; + criadoPor: Id<"usuarios">; + } = { + razao_social: args.razao_social, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + criadoPor: usuarioAtual._id, + }; + + if (args.nome_fantasia !== undefined) { + empresaDoc.nome_fantasia = args.nome_fantasia; + } + if (args.descricao !== undefined) { + empresaDoc.descricao = args.descricao; + } + if (enderecoId) { + empresaDoc.enderecoId = enderecoId; + } + + const empresaId = await ctx.db.insert("empresas", empresaDoc); + + if (args.contatos && args.contatos.length > 0) { + for (const contato of args.contatos) { + await ctx.db.insert("contatosEmpresa", { + empresaId, + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + adicionadoPor: usuarioAtual._id, + descricao: contato.descricao, + }); + } + } + + return empresaId; + }, +}); + +export const update = mutation({ + args: { + id: v.id("empresas"), + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + endereco: v.optional(enderecoInput), + contatos: v.optional(v.array(contatoInput)), + }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, { + recurso: "empresas", + acao: "editar", + }); + + const cnpjExistente = await ctx.db + .query("empresas") + .withIndex("by_cnpj", (q) => q.eq("cnpj", args.cnpj)) + .unique(); + + if (cnpjExistente && cnpjExistente._id !== args.id) { + throw new Error("Já existe uma empresa cadastrada com este CNPJ."); + } + const empresa = await ctx.db.get(args.id); + if (!empresa) { + throw new Error("Empresa não encontrada."); + } + + if (args.endereco) { + if (empresa.enderecoId) { + const usuarioAtual = await getCurrentUserFunction(ctx); + await ctx.db.patch(empresa.enderecoId as Id<"enderecos">, { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + atualizadoPor: usuarioAtual?._id, + }); + } else { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + const novoEnderecoId: Id<"enderecos"> = await ctx.db.insert("enderecos", { + cep: args.endereco.cep, + logradouro: args.endereco.logradouro, + numero: args.endereco.numero, + complemento: args.endereco.complemento, + bairro: args.endereco.bairro, + cidade: args.endereco.cidade, + uf: args.endereco.uf, + criadoPor: usuarioAtual._id, + atualizadoPor: usuarioAtual._id, + }); + + await ctx.db.patch(args.id, { + enderecoId: novoEnderecoId, + }); + } + } + + const patchDoc: { + razao_social: string; + nome_fantasia?: string; + cnpj: string; + telefone: string; + email: string; + descricao?: string; + } = { + razao_social: args.razao_social, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + }; + + if (args.nome_fantasia !== undefined) { + patchDoc.nome_fantasia = args.nome_fantasia; + } + if (args.descricao !== undefined) { + patchDoc.descricao = args.descricao; + } + + await ctx.db.patch(args.id, patchDoc); + + if (!args.contatos) { + return null; + } + + for (const contato of args.contatos) { + if (contato._id && contato._deleted) { + await ctx.db.delete(contato._id); + } else if (contato._id) { + await ctx.db.patch(contato._id, { + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + descricao: contato.descricao, + }); + } else if (!contato._deleted) { + const usuarioAtual = await getCurrentUserFunction(ctx); + + if (!usuarioAtual) { + throw new Error("Usuário não autenticado."); + } + + await ctx.db.insert("contatosEmpresa", { + empresaId: args.id, + nome: contato.nome, + funcao: contato.funcao, + email: contato.email, + telefone: contato.telefone, + adicionadoPor: usuarioAtual._id, + descricao: contato.descricao, + }); + } + } + + return null; + }, +}); + + 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 cbcc444..9b6a3d4 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -120,11 +120,78 @@ 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(), }), + enderecos: defineTable({ + cep: v.string(), + logradouro: v.string(), + numero: v.string(), + complemento: v.optional(v.string()), + bairro: v.string(), + cidade: v.string(), + uf: v.string(), + criadoPor: v.optional(v.id("usuarios")), + atualizadoPor: v.optional(v.id("usuarios")), + }).index("by_cep", ["cep"]), + empresas: defineTable({ + razao_social: v.string(), + nome_fantasia: v.optional(v.string()), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + enderecoId: v.optional(v.id("enderecos")), + criadoPor: v.optional(v.id("usuarios")), + }) + .index("by_razao_social", ["razao_social"]) + .index("by_cnpj", ["cnpj"]), + contatosEmpresa: defineTable({ + empresaId: v.id("empresas"), + nome: v.string(), + funcao: v.string(), + email: v.string(), + telefone: v.string(), + adicionadoPor: v.optional(v.id("usuarios")), + descricao: v.optional(v.string()), + }) + .index("by_empresa", ["empresaId"]) + .index("by_email", ["email"]), funcionarios: defineTable({ // Campos obrigatórios existentes nome: v.string(),