Feat licitacoes contratos #30

Merged
killer-cf merged 7 commits from feat-licitacoes-contratos into master 2025-11-19 12:31:19 +00:00
54 changed files with 4871 additions and 10341 deletions
Showing only changes of commit 029cd9c637 - Show all commits

View File

@@ -4,12 +4,13 @@
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { Building2, Phone, Mail, Plus, Users, Pencil, X } from "lucide-svelte"; import { Building2, Phone, Mail, Plus, Users, Pencil, X } from "lucide-svelte";
import { resolve } from "$app/paths"; import { resolve } from "$app/paths";
import { maskCNPJ, maskPhone } from "$lib/utils/masks"; import { maskCNPJ, maskCEP, maskPhone, maskUF, onlyDigits } from "$lib/utils/masks";
import "$lib/svelte-compat";
const client = useConvexClient(); const client = useConvexClient();
const empresasQuery = useQuery(api.empresas.list, {}); const empresasQuery = useQuery(api.empresas.list, {});
let modalAberto = false; let modalAberto = $state(false);
type ContatoForm = { type ContatoForm = {
_id?: Id<"contatosEmpresa">; _id?: Id<"contatosEmpresa">;
@@ -21,40 +22,137 @@
_deleted?: boolean; _deleted?: boolean;
}; };
type EnderecoForm = {
cep: string;
logradouro: string;
numero: string;
complemento: string;
bairro: string;
cidade: string;
uf: string;
};
type EmpresaForm = { type EmpresaForm = {
id?: string; id?: Id<"empresas">;
nome: string; razao_social: string;
nome_fantasia?: string;
cnpj: string; cnpj: string;
telefone: string; telefone: string;
email: string; email: string;
descricao?: string; descricao?: string;
endereco: EnderecoForm;
contatos: ContatoForm[]; contatos: ContatoForm[];
}; };
let empresaForm: EmpresaForm = { let empresaForm = $state<EmpresaForm>({
nome: "", razao_social: "",
nome_fantasia: "",
cnpj: "", cnpj: "",
telefone: "", telefone: "",
email: "", email: "",
descricao: "", descricao: "",
endereco: {
cep: "",
logradouro: "",
numero: "",
complemento: "",
bairro: "",
cidade: "",
uf: "",
},
contatos: [], contatos: [],
});
let contatoEmEdicao = $state<ContatoForm | null>(null);
let contatoIndiceEdicao = $state<number | null>(null);
let erroFormulario = $state("");
let salvando = $state(false);
let contatosModalAberto = $state(false);
let contatosDaEmpresa = $state<ContatoForm[]>([]);
let empresaContatosNome = $state("");
let carregandoCep = $state(false);
let erroCep = $state("");
let carregandoCnpj = $state(false);
let erroCnpj = $state("");
type ReceitaWsResponse = {
status?: string;
message?: string;
nome?: string;
fantasia?: string;
telefone?: string;
email?: string;
cep?: string;
logradouro?: string;
numero?: string;
complemento?: string;
bairro?: string;
municipio?: string;
uf?: string;
}; };
let contatoEmEdicao: ContatoForm | null = null;
let contatoIndiceEdicao: number | null = null;
let erroFormulario = "";
let salvando = false;
let contatosModalAberto = false;
let contatosDaEmpresa: ContatoForm[] = [];
let empresaContatosNome = "";
function handleEmpresaCnpjInput(event: Event) { function handleEmpresaCnpjInput(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
empresaForm.cnpj = maskCNPJ(target.value); empresaForm.cnpj = maskCNPJ(target.value);
target.value = empresaForm.cnpj; target.value = empresaForm.cnpj;
} }
async function handleEmpresaCnpjBlur() {
const digits = onlyDigits(empresaForm.cnpj);
if (digits.length !== 14) return;
carregandoCnpj = true;
erroCnpj = "";
try {
const response = await fetch(`https://www.receitaws.com.br/v1/cnpj/${digits}`);
const data: ReceitaWsResponse = await response.json();
if (data.status === "ERROR") {
throw new Error(data.message || "CNPJ não encontrado.");
}
if (data.nome && !empresaForm.razao_social) {
empresaForm.razao_social = data.nome;
}
if (data.fantasia && !empresaForm.nome_fantasia) {
empresaForm.nome_fantasia = data.fantasia;
}
if (data.telefone && !empresaForm.telefone) {
empresaForm.telefone = maskPhone(data.telefone);
}
if (data.email && !empresaForm.email) {
empresaForm.email = data.email;
}
if (
data.cep ||
data.logradouro ||
data.bairro ||
data.municipio ||
data.uf
) {
empresaForm.endereco = {
...empresaForm.endereco,
cep: data.cep ? maskCEP(data.cep) : empresaForm.endereco.cep,
logradouro: data.logradouro ?? empresaForm.endereco.logradouro,
numero: data.numero ?? empresaForm.endereco.numero,
complemento: data.complemento ?? empresaForm.endereco.complemento,
bairro: data.bairro ?? empresaForm.endereco.bairro,
cidade: data.municipio ?? empresaForm.endereco.cidade,
uf: data.uf ? maskUF(data.uf) : empresaForm.endereco.uf,
};
}
} catch (error) {
erroCnpj =
error instanceof Error
? error.message
: "Não foi possível buscar os dados do CNPJ.";
} finally {
carregandoCnpj = false;
}
}
function handleEmpresaTelefoneInput(event: Event) { function handleEmpresaTelefoneInput(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
empresaForm.telefone = maskPhone(target.value); empresaForm.telefone = maskPhone(target.value);
@@ -68,13 +166,87 @@
target.value = contatoEmEdicao.telefone; target.value = contatoEmEdicao.telefone;
} }
function handleCepInput(event: Event) {
const target = event.target as HTMLInputElement;
empresaForm.endereco.cep = maskCEP(target.value);
target.value = empresaForm.endereco.cep;
const digits = onlyDigits(empresaForm.endereco.cep);
if (digits.length === 8) {
void buscarCep(digits);
}
}
async function buscarCep(cepDigits: string) {
carregandoCep = true;
erroCep = "";
try {
const response = await fetch(`https://viacep.com.br/ws/${cepDigits}/json/`);
const data = await response.json();
if (data.erro) {
throw new Error("CEP não encontrado.");
}
empresaForm.endereco = {
...empresaForm.endereco,
cep: maskCEP(cepDigits),
logradouro: data.logradouro ?? empresaForm.endereco.logradouro,
bairro: data.bairro ?? empresaForm.endereco.bairro,
cidade: data.localidade ?? empresaForm.endereco.cidade,
uf: data.uf ? maskUF(data.uf) : empresaForm.endereco.uf,
};
} catch (error) {
erroCep =
error instanceof Error
? error.message
: "Não foi possível buscar o endereço pelo CEP.";
} finally {
carregandoCep = false;
}
}
function normalizeEnderecoForSave(endereco: EnderecoForm) {
const hasData =
endereco.cep ||
endereco.logradouro ||
endereco.numero ||
endereco.bairro ||
endereco.cidade ||
endereco.uf;
if (!hasData) {
return undefined;
}
return {
cep: endereco.cep,
logradouro: endereco.logradouro,
numero: endereco.numero,
complemento: endereco.complemento || undefined,
bairro: endereco.bairro,
cidade: endereco.cidade,
uf: endereco.uf,
};
}
function abrirNovaEmpresa() { function abrirNovaEmpresa() {
empresaForm = { empresaForm = {
nome: "", razao_social: "",
nome_fantasia: "",
cnpj: "", cnpj: "",
telefone: "", telefone: "",
email: "", email: "",
descricao: "", descricao: "",
endereco: {
cep: "",
logradouro: "",
numero: "",
complemento: "",
bairro: "",
cidade: "",
uf: "",
},
contatos: [], contatos: [],
}; };
modalAberto = true; modalAberto = true;
@@ -86,11 +258,21 @@
empresaForm = { empresaForm = {
id: detalhes._id, id: detalhes._id,
nome: detalhes.nome, razao_social: detalhes.razao_social,
nome_fantasia: detalhes.nome_fantasia,
cnpj: detalhes.cnpj, cnpj: detalhes.cnpj,
telefone: detalhes.telefone, telefone: detalhes.telefone,
email: detalhes.email, email: detalhes.email,
descricao: detalhes.descricao ?? "", descricao: detalhes.descricao ?? "",
endereco: {
cep: detalhes.endereco?.cep ?? "",
logradouro: detalhes.endereco?.logradouro ?? "",
numero: detalhes.endereco?.numero ?? "",
complemento: detalhes.endereco?.complemento ?? "",
bairro: detalhes.endereco?.bairro ?? "",
cidade: detalhes.endereco?.cidade ?? "",
uf: detalhes.endereco?.uf ?? "",
},
contatos: contatos:
detalhes.contatos?.map((c) => ({ detalhes.contatos?.map((c) => ({
_id: c._id, _id: c._id,
@@ -104,10 +286,10 @@
modalAberto = true; modalAberto = true;
} }
async function verContatos(empresaId: Id<"empresas">, nome: string) { async function verContatos(empresaId: Id<"empresas">, razaoSocial: string) {
const detalhes = await client.query(api.empresas.getById, { id: empresaId }); const detalhes = await client.query(api.empresas.getById, { id: empresaId });
contatosDaEmpresa = detalhes?.contatos ?? []; contatosDaEmpresa = detalhes?.contatos ?? [];
empresaContatosNome = nome; empresaContatosNome = razaoSocial;
contatosModalAberto = true; contatosModalAberto = true;
} }
@@ -174,36 +356,53 @@
} }
async function salvarEmpresa() { async function salvarEmpresa() {
if (!empresaForm.nome || !empresaForm.cnpj || !empresaForm.telefone || !empresaForm.email) { if (
!empresaForm.razao_social ||
!empresaForm.cnpj ||
!empresaForm.telefone ||
!empresaForm.email
) {
erroFormulario = "Preencha todos os campos obrigatórios da empresa."; erroFormulario = "Preencha todos os campos obrigatórios da empresa.";
return; return;
} }
salvando = true; salvando = true;
erroFormulario = ""; erroFormulario = "";
try { try {
const enderecoPayload = normalizeEnderecoForSave(empresaForm.endereco);
if (empresaForm.id) { if (empresaForm.id) {
await client.mutation( const baseArgs = {
api.empresas.update as typeof api.empresas.update, id: empresaForm.id,
{ razao_social: empresaForm.razao_social,
// eslint-disable-next-line @typescript-eslint/no-explicit-any nome_fantasia: empresaForm.nome_fantasia,
id: empresaForm.id as any,
nome: empresaForm.nome,
cnpj: empresaForm.cnpj,
telefone: empresaForm.telefone,
email: empresaForm.email,
descricao: empresaForm.descricao || undefined,
contatos: empresaForm.contatos,
}
);
} else {
await client.mutation(api.empresas.create, {
nome: empresaForm.nome,
cnpj: empresaForm.cnpj, cnpj: empresaForm.cnpj,
telefone: empresaForm.telefone, telefone: empresaForm.telefone,
email: empresaForm.email, email: empresaForm.email,
descricao: empresaForm.descricao || undefined, descricao: empresaForm.descricao || undefined,
contatos: empresaForm.contatos, contatos: empresaForm.contatos,
}); };
const args = enderecoPayload
? { ...baseArgs, endereco: enderecoPayload }
: baseArgs;
await client.mutation(api.empresas.update, args);
} else {
const baseArgs = {
razao_social: empresaForm.razao_social,
nome_fantasia: empresaForm.nome_fantasia,
cnpj: empresaForm.cnpj,
telefone: empresaForm.telefone,
email: empresaForm.email,
descricao: empresaForm.descricao || undefined,
contatos: empresaForm.contatos,
};
const args = enderecoPayload
? { ...baseArgs, endereco: enderecoPayload }
: baseArgs;
await client.mutation(api.empresas.create, args);
} }
fecharModal(); fecharModal();
} catch (error) { } catch (error) {
@@ -264,34 +463,39 @@
<table class="table table-zebra"> <table class="table table-zebra">
<thead> <thead>
<tr> <tr>
<th>Nome</th> <th>Razão social / Nome fantasia</th>
<th>CNPJ</th> <th>CNPJ</th>
<th>Telefone</th> <th>Telefone</th>
<th>E-mail</th> <th>E-mail</th>
<th></th> <th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each empresasQuery.data as empresa (empresa._id)} {#each empresasQuery.data as empresa (empresa._id)}
<tr> <tr>
<td>{empresa.nome}</td> <td>
<div class="flex flex-col">
<span class="font-semibold">{empresa.razao_social}</span>
{#if empresa.nome_fantasia}
<span class="text-xs text-base-content/70">{empresa.nome_fantasia}</span>
{/if}
</div>
</td>
<td>{empresa.cnpj}</td> <td>{empresa.cnpj}</td>
<td class="flex items-center gap-2"> <td class="flex items-center gap-2">
<Phone class="h-4 w-4 text-base-content/60" strokeWidth={2} /> <Phone class="h-4 w-4 text-base-content/60" strokeWidth={2} />
<span>{empresa.telefone}</span> <span>{empresa.telefone}</span>
</td> </td>
<td> <td class="flex items-center gap-2">
<div class="flex items-center gap-2"> <Mail class="h-4 w-4 text-base-content/60" strokeWidth={2} />
<Mail class="h-4 w-4 text-base-content/60" strokeWidth={2} /> <span>{empresa.email}</span>
<span>{empresa.email}</span>
</div>
</td> </td>
<td class="text-right"> <td class="text-right">
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
class="btn btn-outline btn-sm gap-2" class="btn btn-outline btn-sm gap-2"
type="button" type="button"
onclick={() => verContatos(empresa._id, empresa.nome)} onclick={() => verContatos(empresa._id, empresa.razao_social)}
> >
<Users class="h-4 w-4" strokeWidth={2} /> <Users class="h-4 w-4" strokeWidth={2} />
Contatos Contatos
@@ -339,16 +543,29 @@
{/if} {/if}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="form-control"> <div class="form-control md:col-span-2">
<label class="label" for="nomeEmpresa"><span class="label-text">Nome da empresa *</span></label> <label class="label" for="razaoSocialEmpresa">
<span class="label-text">Razão social *</span>
</label>
<input <input
id="nomeEmpresa" id="razaoSocialEmpresa"
class="input input-bordered w-full" class="input input-bordered w-full"
bind:value={empresaForm.nome} bind:value={empresaForm.razao_social}
placeholder="Nome" placeholder="Razão social"
required required
/> />
</div> </div>
<div class="form-control">
<label class="label" for="nomeFantasiaEmpresa">
<span class="label-text">Nome fantasia</span>
</label>
<input
id="nomeFantasiaEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.nome_fantasia}
placeholder="Nome fantasia"
/>
</div>
<div class="form-control"> <div class="form-control">
<label class="label" for="cnpjEmpresa"><span class="label-text">CNPJ *</span></label> <label class="label" for="cnpjEmpresa"><span class="label-text">CNPJ *</span></label>
<input <input
@@ -357,9 +574,15 @@
value={empresaForm.cnpj} value={empresaForm.cnpj}
inputmode="numeric" inputmode="numeric"
oninput={handleEmpresaCnpjInput} oninput={handleEmpresaCnpjInput}
onblur={handleEmpresaCnpjBlur}
placeholder="00.000.000/0000-00" placeholder="00.000.000/0000-00"
required required
/> />
{#if carregandoCnpj}
<span class="text-xs text-primary mt-1">Buscando dados do CNPJ...</span>
{:else if erroCnpj}
<span class="text-xs text-error mt-1">{erroCnpj}</span>
{/if}
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label" for="telefoneEmpresa"><span class="label-text">Telefone *</span></label> <label class="label" for="telefoneEmpresa"><span class="label-text">Telefone *</span></label>
@@ -398,6 +621,87 @@
</div> </div>
</div> </div>
<div class="divider my-4">Endereço da empresa</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="form-control">
<label class="label" for="cepEmpresa"><span class="label-text">CEP</span></label>
<input
id="cepEmpresa"
class="input input-bordered w-full"
value={empresaForm.endereco.cep}
inputmode="numeric"
oninput={handleCepInput}
placeholder="00000-000"
/>
{#if carregandoCep}
<span class="text-xs text-primary mt-1">Buscando endereço pelo CEP...</span>
{:else if erroCep}
<span class="text-xs text-error mt-1">{erroCep}</span>
{/if}
</div>
<div class="form-control md:col-span-2">
<label class="label" for="logradouroEmpresa"><span class="label-text">Logradouro</span></label>
<input
id="logradouroEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.endereco.logradouro}
placeholder="Rua, avenida, etc."
/>
</div>
<div class="form-control">
<label class="label" for="numeroEmpresa"><span class="label-text">Número</span></label>
<input
id="numeroEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.endereco.numero}
placeholder="Número"
/>
</div>
<div class="form-control">
<label class="label" for="complementoEmpresa"><span class="label-text">Complemento</span></label>
<input
id="complementoEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.endereco.complemento}
placeholder="Sala, bloco, etc."
/>
</div>
<div class="form-control">
<label class="label" for="bairroEmpresa"><span class="label-text">Bairro</span></label>
<input
id="bairroEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.endereco.bairro}
placeholder="Bairro"
/>
</div>
<div class="form-control">
<label class="label" for="cidadeEmpresa"><span class="label-text">Cidade</span></label>
<input
id="cidadeEmpresa"
class="input input-bordered w-full"
bind:value={empresaForm.endereco.cidade}
placeholder="Cidade"
/>
</div>
<div class="form-control">
<label class="label" for="ufEmpresa"><span class="label-text">UF</span></label>
<input
id="ufEmpresa"
class="input input-bordered w-full"
maxlength="2"
bind:value={empresaForm.endereco.uf}
oninput={(event) => {
const target = event.target as HTMLInputElement;
empresaForm.endereco.uf = maskUF(target.value);
target.value = empresaForm.endereco.uf;
}}
placeholder="UF"
/>
</div>
</div>
<div class="divider my-4">Contatos da empresa</div> <div class="divider my-4">Contatos da empresa</div>
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">

View File

@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "sgse-app", "name": "sgse-app",

View File

@@ -15,9 +15,9 @@ import type * as actions_smtp from "../actions/smtp.js";
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js"; import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
import type * as atestadosLicencas from "../atestadosLicencas.js"; import type * as atestadosLicencas from "../atestadosLicencas.js";
import type * as ausencias from "../ausencias.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_utils from "../auth/utils.js";
import type * as chamados from "../chamados.js"; import type * as chamados from "../chamados.js";
import type * as auth from "../auth.js";
import type * as chat from "../chat.js"; import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as configuracaoEmail from "../configuracaoEmail.js";
import type * as crons from "../crons.js"; import type * as crons from "../crons.js";
@@ -56,14 +56,6 @@ import type {
FunctionReference, FunctionReference,
} from "convex/server"; } 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<{ declare const fullApi: ApiFromModules<{
"actions/email": typeof actions_email; "actions/email": typeof actions_email;
"actions/linkPreview": typeof actions_linkPreview; "actions/linkPreview": typeof actions_linkPreview;
@@ -72,9 +64,9 @@ declare const fullApi: ApiFromModules<{
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto; "actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
atestadosLicencas: typeof atestadosLicencas; atestadosLicencas: typeof atestadosLicencas;
ausencias: typeof ausencias; ausencias: typeof ausencias;
auth: typeof auth;
"auth/utils": typeof auth_utils; "auth/utils": typeof auth_utils;
chamados: typeof chamados; chamados: typeof chamados;
auth: typeof auth;
chat: typeof chat; chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail; configuracaoEmail: typeof configuracaoEmail;
crons: typeof crons; crons: typeof crons;
@@ -107,14 +99,30 @@ declare const fullApi: ApiFromModules<{
"utils/getClientIP": typeof utils_getClientIP; "utils/getClientIP": typeof utils_getClientIP;
verificarMatriculas: typeof verificarMatriculas; 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< export declare const api: FilterApi<
typeof fullApiWithMounts, typeof fullApi,
FunctionReference<any, "public"> FunctionReference<any, "public">
>; >;
/**
* A utility for referencing Convex functions in your app's internal API.
*
* Usage:
* ```js
* const myFunctionReference = internal.myModule.myFunction;
* ```
*/
export declare const internal: FilterApi< export declare const internal: FilterApi<
typeof fullApiWithMounts, typeof fullApi,
FunctionReference<any, "internal"> FunctionReference<any, "internal">
>; >;

View File

@@ -10,7 +10,6 @@
import { import {
ActionBuilder, ActionBuilder,
AnyComponents,
HttpActionBuilder, HttpActionBuilder,
MutationBuilder, MutationBuilder,
QueryBuilder, QueryBuilder,
@@ -19,15 +18,9 @@ import {
GenericQueryCtx, GenericQueryCtx,
GenericDatabaseReader, GenericDatabaseReader,
GenericDatabaseWriter, GenericDatabaseWriter,
FunctionReference,
} from "convex/server"; } from "convex/server";
import type { DataModel } from "./dataModel.js"; import type { DataModel } from "./dataModel.js";
type GenericCtx =
| GenericActionCtx<DataModel>
| GenericMutationCtx<DataModel>
| GenericQueryCtx<DataModel>;
/** /**
* Define a query in this Convex app's public API. * Define a query in this Convex app's public API.
* *
@@ -92,11 +85,12 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
/** /**
* Define an HTTP action. * Define an HTTP action.
* *
* This function will be used to respond to HTTP requests received by a Convex * The wrapped function will be used to respond to HTTP requests received
* deployment if the requests matches the path and method where this action * by a Convex deployment if the requests matches the path and method where
* is routed. Be sure to route your action in `convex/http.js`. * 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. * @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. * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/ */
export declare const httpAction: HttpActionBuilder; export declare const httpAction: HttpActionBuilder;

View File

@@ -16,7 +16,6 @@ import {
internalActionGeneric, internalActionGeneric,
internalMutationGeneric, internalMutationGeneric,
internalQueryGeneric, internalQueryGeneric,
componentsGeneric,
} from "convex/server"; } from "convex/server";
/** /**
@@ -81,10 +80,14 @@ export const action = actionGeneric;
export const internalAction = internalActionGeneric; export const internalAction = internalActionGeneric;
/** /**
* Define a Convex HTTP action. * Define an HTTP action.
* *
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object * The wrapped function will be used to respond to HTTP requests received
* as its second. * by a Convex deployment if the requests matches the path and method where
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. * 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.
*/ */
export const httpAction = httpActionGeneric; export const httpAction = httpActionGeneric;

View File

@@ -2,6 +2,7 @@ import { v } from "convex/values";
import { mutation, query } from "./_generated/server"; import { mutation, query } from "./_generated/server";
import { internal } from "./_generated/api"; import { internal } from "./_generated/api";
import { getCurrentUserFunction } from "./auth"; import { getCurrentUserFunction } from "./auth";
import type { Id } from "./_generated/dataModel";
export const list = query({ export const list = query({
args: {}, args: {},
@@ -33,8 +34,11 @@ export const getById = query({
.query("contatosEmpresa") .query("contatosEmpresa")
.withIndex("by_empresa", (q) => q.eq("empresaId", args.id)) .withIndex("by_empresa", (q) => q.eq("empresaId", args.id))
.collect(); .collect();
const endereco = empresa.enderecoId
? await ctx.db.get(empresa.enderecoId as Id<"enderecos">)
: null;
return { ...empresa, contatos }; return { ...empresa, endereco, contatos };
}, },
}); });
@@ -50,13 +54,25 @@ const contatoInput = v.object({
_deleted: v.optional(v.boolean()), _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({ export const create = mutation({
args: { args: {
nome: v.string(), razao_social: v.string(),
nome_fantasia: v.optional(v.string()),
cnpj: v.string(), cnpj: v.string(),
telefone: v.string(), telefone: v.string(),
email: v.string(), email: v.string(),
descricao: v.optional(v.string()), descricao: v.optional(v.string()),
endereco: v.optional(enderecoInput),
contatos: v.optional(v.array(contatoInput)), contatos: v.optional(v.array(contatoInput)),
}, },
returns: v.id("empresas"), returns: v.id("empresas"),
@@ -80,17 +96,49 @@ export const create = mutation({
if (cnpjExistente) { if (cnpjExistente) {
throw new Error("Já existe uma empresa cadastrada com este CNPJ."); 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;
const empresaId = await ctx.db.insert("empresas", { nome_fantasia?: string;
nome: args.nome, cnpj: string;
telefone: string;
email: string;
descricao?: string;
enderecoId?: Id<"enderecos">;
criadoPor: Id<"usuarios">;
} = {
razao_social: args.razao_social,
cnpj: args.cnpj, cnpj: args.cnpj,
telefone: args.telefone, telefone: args.telefone,
email: args.email, email: args.email,
descricao: args.descricao,
criadoPor: usuarioAtual._id, 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) { if (args.contatos && args.contatos.length > 0) {
for (const contato of args.contatos) { for (const contato of args.contatos) {
@@ -113,11 +161,13 @@ export const create = mutation({
export const update = mutation({ export const update = mutation({
args: { args: {
id: v.id("empresas"), id: v.id("empresas"),
nome: v.string(), razao_social: v.string(),
nome_fantasia: v.optional(v.string()),
cnpj: v.string(), cnpj: v.string(),
telefone: v.string(), telefone: v.string(),
email: v.string(), email: v.string(),
descricao: v.optional(v.string()), descricao: v.optional(v.string()),
endereco: v.optional(enderecoInput),
contatos: v.optional(v.array(contatoInput)), contatos: v.optional(v.array(contatoInput)),
}, },
returns: v.null(), returns: v.null(),
@@ -135,14 +185,71 @@ export const update = mutation({
if (cnpjExistente && cnpjExistente._id !== args.id) { if (cnpjExistente && cnpjExistente._id !== args.id) {
throw new Error("Já existe uma empresa cadastrada com este CNPJ."); 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.");
}
await ctx.db.patch(args.id, { if (args.endereco) {
nome: args.nome, 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, cnpj: args.cnpj,
telefone: args.telefone, telefone: args.telefone,
email: args.email, email: args.email,
descricao: args.descricao, };
});
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) { if (!args.contatos) {
return null; return null;

View File

@@ -125,15 +125,28 @@ export default defineSchema({
text: v.string(), text: v.string(),
completed: v.boolean(), 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({ empresas: defineTable({
nome: v.string(), razao_social: v.string(),
nome_fantasia: v.optional(v.string()),
cnpj: v.string(), cnpj: v.string(),
telefone: v.string(), telefone: v.string(),
email: v.string(), email: v.string(),
descricao: v.optional(v.string()), descricao: v.optional(v.string()),
enderecoId: v.optional(v.id("enderecos")),
criadoPor: v.optional(v.id("usuarios")), criadoPor: v.optional(v.id("usuarios")),
}) })
.index("by_nome", ["nome"]) .index("by_razao_social", ["razao_social"])
.index("by_cnpj", ["cnpj"]), .index("by_cnpj", ["cnpj"]),
contatosEmpresa: defineTable({ contatosEmpresa: defineTable({
empresaId: v.id("empresas"), empresaId: v.id("empresas"),