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 65b2844..806d0cb 100644 --- a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte @@ -1,5 +1,5 @@ @@ -25,63 +25,49 @@ - -
-
-
-
- +
+ + - -
-

Funcionalidades Previstas

-
-
-
-
-
- +
+
+
+
+
-

Processos Licitatórios

-
-

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

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} + +
NomeCNPJTelefoneE-mail
{empresa.nome}{empresa.cnpj} + + {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 60340ef..3a502c4 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 @@ @@ -23,22 +23,36 @@
-
-
-
-
- +
+ + - diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index 87b4b87..cd2d43a 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -15,8 +15,8 @@ import type * as actions_smtp from "../actions/smtp.js"; import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js"; import type * as atestadosLicencas from "../atestadosLicencas.js"; import type * as ausencias from "../ausencias.js"; -import type * as auth from "../auth.js"; import type * as auth_utils from "../auth/utils.js"; +import type * as auth from "../auth.js"; import type * as chat from "../chat.js"; import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as crons from "../crons.js"; @@ -24,6 +24,7 @@ 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"; @@ -54,6 +55,14 @@ import type { FunctionReference, } from "convex/server"; +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ declare const fullApi: ApiFromModules<{ "actions/email": typeof actions_email; "actions/linkPreview": typeof actions_linkPreview; @@ -62,8 +71,8 @@ declare const fullApi: ApiFromModules<{ "actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto; atestadosLicencas: typeof atestadosLicencas; ausencias: typeof ausencias; - auth: typeof auth; "auth/utils": typeof auth_utils; + auth: typeof auth; chat: typeof chat; configuracaoEmail: typeof configuracaoEmail; crons: typeof crons; @@ -71,6 +80,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; @@ -95,30 +105,14 @@ declare const fullApi: ApiFromModules<{ "utils/getClientIP": typeof utils_getClientIP; verificarMatriculas: typeof verificarMatriculas; }>; +declare const fullApiWithMounts: typeof fullApi; -/** - * A utility for referencing Convex functions in your app's public API. - * - * Usage: - * ```js - * const myFunctionReference = api.myModule.myFunction; - * ``` - */ export declare const api: FilterApi< - typeof fullApi, + typeof fullApiWithMounts, FunctionReference >; - -/** - * A utility for referencing Convex functions in your app's internal API. - * - * Usage: - * ```js - * const myFunctionReference = internal.myModule.myFunction; - * ``` - */ export declare const internal: FilterApi< - typeof fullApi, + typeof fullApiWithMounts, FunctionReference >; diff --git a/packages/backend/convex/_generated/server.d.ts b/packages/backend/convex/_generated/server.d.ts index bec05e6..b5c6828 100644 --- a/packages/backend/convex/_generated/server.d.ts +++ b/packages/backend/convex/_generated/server.d.ts @@ -10,6 +10,7 @@ import { ActionBuilder, + AnyComponents, HttpActionBuilder, MutationBuilder, QueryBuilder, @@ -18,9 +19,15 @@ import { GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter, + FunctionReference, } from "convex/server"; import type { DataModel } from "./dataModel.js"; +type GenericCtx = + | GenericActionCtx + | GenericMutationCtx + | GenericQueryCtx; + /** * Define a query in this Convex app's public API. * @@ -85,12 +92,11 @@ export declare const internalAction: ActionBuilder; /** * Define an HTTP action. * - * The wrapped function will be used to respond to HTTP requests received - * by a Convex deployment if the requests matches the path and method where - * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * This function will be used to respond to HTTP requests received by a Convex + * deployment if the requests matches the path and method where this action + * is routed. Be sure to route your action in `convex/http.js`. * - * @param func - The function. It receives an {@link ActionCtx} as its first argument - * and a Fetch API `Request` object as its second. + * @param func - The function. It receives an {@link ActionCtx} as its first argument. * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. */ export declare const httpAction: HttpActionBuilder; diff --git a/packages/backend/convex/_generated/server.js b/packages/backend/convex/_generated/server.js index bf3d25a..4a21df4 100644 --- a/packages/backend/convex/_generated/server.js +++ b/packages/backend/convex/_generated/server.js @@ -16,6 +16,7 @@ import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, + componentsGeneric, } from "convex/server"; /** @@ -80,14 +81,10 @@ export const action = actionGeneric; export const internalAction = internalActionGeneric; /** - * Define an HTTP action. + * Define a Convex HTTP action. * - * The wrapped function will be used to respond to HTTP requests received - * by a Convex deployment if the requests matches the path and method where - * this action is routed. Be sure to route your httpAction in `convex/http.js`. - * - * @param func - The function. It receives an {@link ActionCtx} as its first argument - * and a Fetch API `Request` object as its second. - * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object + * as its second. + * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. */ export const httpAction = httpActionGeneric; diff --git a/packages/backend/convex/empresas.ts b/packages/backend/convex/empresas.ts new file mode 100644 index 0000000..04fbbce --- /dev/null +++ b/packages/backend/convex/empresas.ts @@ -0,0 +1,185 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server"; +import { internal } from "./_generated/api"; +import { getCurrentUserFunction } from "./auth"; + +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(); + + return { ...empresa, 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()), +}); + +export const create = mutation({ + args: { + nome: v.string(), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + 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."); + } + + + + const empresaId = await ctx.db.insert("empresas", { + nome: args.nome, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + descricao: args.descricao, + criadoPor: usuarioAtual._id, + }); + + 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"), + nome: v.string(), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + 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."); + } + + await ctx.db.patch(args.id, { + nome: args.nome, + cnpj: args.cnpj, + telefone: args.telefone, + email: args.email, + descricao: args.descricao, + }); + + 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/schema.ts b/packages/backend/convex/schema.ts index 602221b..a39ea4b 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -12,6 +12,27 @@ export default defineSchema({ text: v.string(), completed: v.boolean(), }), + empresas: defineTable({ + nome: v.string(), + cnpj: v.string(), + telefone: v.string(), + email: v.string(), + descricao: v.optional(v.string()), + criadoPor: v.optional(v.id("usuarios")), + }) + .index("by_nome", ["nome"]) + .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(),