feat: add empresa and contatos
- Introduced a new utility function `maskCNPJ` for formatting CNPJ values. - Updated the dashboard pages to replace icons and improve layout, including the addition of a link to manage companies. - Enhanced the display of upcoming features for both the Licitações and Programas Esportivos modules, indicating their development status.
This commit is contained in:
@@ -43,6 +43,16 @@ export const maskCEP = (value: string): string => {
|
|||||||
return digits.replace(/(\d{5})(\d{1,3})$/, "$1-$2");
|
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 */
|
/** Format phone: (00) 0000-0000 or (00) 00000-0000 */
|
||||||
export const maskPhone = (value: string): string => {
|
export const maskPhone = (value: string): string => {
|
||||||
const digits = onlyDigits(value).slice(0, 11);
|
const digits = onlyDigits(value).slice(0, 11);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FileText, ClipboardCopy, Plus, Users, FileDoc } from "lucide-svelte";
|
import { FileText, ClipboardCopy, Building2 } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -25,63 +25,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card de Aviso -->
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<a
|
||||||
<div class="card-body">
|
href={resolve('/licitacoes/empresas')}
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow border border-base-200 hover:border-primary"
|
||||||
<div class="mb-6">
|
>
|
||||||
<FileDoc class="h-24 w-24 text-base-content/20" strokeWidth={1.5} />
|
<div class="card-body">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Building2 class="h-6 w-6 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<h4 class="font-semibold">Empresas</h4>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold mb-2">Módulo em Desenvolvimento</h2>
|
<p class="text-sm text-base-content/70">
|
||||||
<p class="text-base-content/70 max-w-md mb-6">
|
Cadastro, listagem e edição de empresas e seus contatos.
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="badge badge-warning badge-lg gap-2">
|
</div>
|
||||||
<Plus class="h-4 w-4" strokeWidth={2} />
|
</a>
|
||||||
Em Desenvolvimento
|
|
||||||
|
<div class="card bg-base-100 shadow-md opacity-70">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<div class="p-2 bg-base-200 rounded-lg">
|
||||||
|
<ClipboardCopy class="h-6 w-6 text-base-content/50" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<h4 class="font-semibold text-base-content/70">Processos Licitatórios</h4>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Em breve: cadastro e acompanhamento de licitações.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Funcionalidades Previstas -->
|
<div class="card bg-base-100 shadow-md opacity-70">
|
||||||
<div class="mt-6">
|
<div class="card-body">
|
||||||
<h3 class="text-xl font-bold mb-4">Funcionalidades Previstas</h3>
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="p-2 bg-base-200 rounded-lg">
|
||||||
<div class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
|
<FileText class="h-6 w-6 text-base-content/50" strokeWidth={2} />
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<div class="p-2 bg-primary/10 rounded-lg">
|
|
||||||
<ClipboardCopy class="h-6 w-6 text-primary" strokeWidth={2} />
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="font-semibold">Processos Licitatórios</h4>
|
<h4 class="font-semibold text-base-content/70">Documentação</h4>
|
||||||
</div>
|
|
||||||
<p class="text-sm text-base-content/70">Cadastro e acompanhamento de licitações</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<div class="p-2 bg-primary/10 rounded-lg">
|
|
||||||
<Users class="h-6 w-6 text-primary" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<h4 class="font-semibold">Fornecedores</h4>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-base-content/70">Cadastro e gestão de fornecedores</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<div class="p-2 bg-primary/10 rounded-lg">
|
|
||||||
<FileDoc class="h-6 w-6 text-primary" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<h4 class="font-semibold">Documentação</h4>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-base-content/70">Gestão de documentos e editais</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Em breve: gestão de documentos e editais.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
626
apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte
Normal file
626
apps/web/src/routes/(dashboard)/licitacoes/empresas/+page.svelte
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useConvexClient, useQuery } from "convex-svelte";
|
||||||
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
|
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
|
import { Building2, Phone, Mail, Plus, Users, Pencil, X } from "lucide-svelte";
|
||||||
|
import { resolve } from "$app/paths";
|
||||||
|
import { maskCNPJ, maskPhone } from "$lib/utils/masks";
|
||||||
|
|
||||||
|
const client = useConvexClient();
|
||||||
|
const empresasQuery = useQuery(api.empresas.list, {});
|
||||||
|
|
||||||
|
let modalAberto = false;
|
||||||
|
|
||||||
|
type ContatoForm = {
|
||||||
|
_id?: Id<"contatosEmpresa">;
|
||||||
|
nome: string;
|
||||||
|
funcao: string;
|
||||||
|
email: string;
|
||||||
|
telefone: string;
|
||||||
|
descricao?: string;
|
||||||
|
_deleted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmpresaForm = {
|
||||||
|
id?: string;
|
||||||
|
nome: string;
|
||||||
|
cnpj: string;
|
||||||
|
telefone: string;
|
||||||
|
email: string;
|
||||||
|
descricao?: string;
|
||||||
|
contatos: ContatoForm[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let empresaForm: EmpresaForm = {
|
||||||
|
nome: "",
|
||||||
|
cnpj: "",
|
||||||
|
telefone: "",
|
||||||
|
email: "",
|
||||||
|
descricao: "",
|
||||||
|
contatos: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
empresaForm.cnpj = maskCNPJ(target.value);
|
||||||
|
target.value = empresaForm.cnpj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEmpresaTelefoneInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
empresaForm.telefone = maskPhone(target.value);
|
||||||
|
target.value = empresaForm.telefone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContatoTelefoneInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (!contatoEmEdicao) return;
|
||||||
|
contatoEmEdicao = { ...contatoEmEdicao, telefone: maskPhone(target.value) };
|
||||||
|
target.value = contatoEmEdicao.telefone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function abrirNovaEmpresa() {
|
||||||
|
empresaForm = {
|
||||||
|
nome: "",
|
||||||
|
cnpj: "",
|
||||||
|
telefone: "",
|
||||||
|
email: "",
|
||||||
|
descricao: "",
|
||||||
|
contatos: [],
|
||||||
|
};
|
||||||
|
modalAberto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editarEmpresa(id: Id<"empresas">) {
|
||||||
|
const detalhes = await client.query(api.empresas.getById, { id });
|
||||||
|
if (!detalhes) return;
|
||||||
|
|
||||||
|
empresaForm = {
|
||||||
|
id: detalhes._id,
|
||||||
|
nome: detalhes.nome,
|
||||||
|
cnpj: detalhes.cnpj,
|
||||||
|
telefone: detalhes.telefone,
|
||||||
|
email: detalhes.email,
|
||||||
|
descricao: detalhes.descricao ?? "",
|
||||||
|
contatos:
|
||||||
|
detalhes.contatos?.map((c) => ({
|
||||||
|
_id: c._id,
|
||||||
|
nome: c.nome,
|
||||||
|
funcao: c.funcao,
|
||||||
|
email: c.email,
|
||||||
|
telefone: c.telefone,
|
||||||
|
descricao: c.descricao ?? "",
|
||||||
|
})) ?? [],
|
||||||
|
};
|
||||||
|
modalAberto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verContatos(empresaId: Id<"empresas">, nome: string) {
|
||||||
|
const detalhes = await client.query(api.empresas.getById, { id: empresaId });
|
||||||
|
contatosDaEmpresa = detalhes?.contatos ?? [];
|
||||||
|
empresaContatosNome = nome;
|
||||||
|
contatosModalAberto = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharContatosModal() {
|
||||||
|
contatosModalAberto = false;
|
||||||
|
contatosDaEmpresa = [];
|
||||||
|
empresaContatosNome = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModal() {
|
||||||
|
modalAberto = false;
|
||||||
|
contatoEmEdicao = null;
|
||||||
|
contatoIndiceEdicao = null;
|
||||||
|
erroFormulario = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function adicionarContato() {
|
||||||
|
contatoEmEdicao = {
|
||||||
|
nome: "",
|
||||||
|
funcao: "",
|
||||||
|
email: "",
|
||||||
|
telefone: "",
|
||||||
|
descricao: "",
|
||||||
|
};
|
||||||
|
contatoIndiceEdicao = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editarContato(index: number) {
|
||||||
|
const contato = empresaForm.contatos[index];
|
||||||
|
if (!contato || contato._deleted) return;
|
||||||
|
contatoEmEdicao = { ...contato };
|
||||||
|
contatoIndiceEdicao = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removerContato(index: number) {
|
||||||
|
const contato = empresaForm.contatos[index];
|
||||||
|
if (!contato) return;
|
||||||
|
if (contato._id) {
|
||||||
|
empresaForm.contatos[index] = { ...contato, _deleted: true };
|
||||||
|
} else {
|
||||||
|
empresaForm.contatos = empresaForm.contatos.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function salvarContatoAtual() {
|
||||||
|
if (!contatoEmEdicao) return;
|
||||||
|
if (!contatoEmEdicao.nome || !contatoEmEdicao.email || !contatoEmEdicao.telefone) {
|
||||||
|
erroFormulario = "Preencha pelo menos nome, e-mail e telefone do contato.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
erroFormulario = "";
|
||||||
|
|
||||||
|
if (contatoIndiceEdicao === null) {
|
||||||
|
empresaForm.contatos = [...empresaForm.contatos, contatoEmEdicao];
|
||||||
|
} else {
|
||||||
|
empresaForm.contatos[contatoIndiceEdicao] = {
|
||||||
|
...(empresaForm.contatos[contatoIndiceEdicao] ?? {}),
|
||||||
|
...contatoEmEdicao,
|
||||||
|
_deleted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
contatoEmEdicao = null;
|
||||||
|
contatoIndiceEdicao = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function salvarEmpresa() {
|
||||||
|
if (!empresaForm.nome || !empresaForm.cnpj || !empresaForm.telefone || !empresaForm.email) {
|
||||||
|
erroFormulario = "Preencha todos os campos obrigatórios da empresa.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
salvando = true;
|
||||||
|
erroFormulario = "";
|
||||||
|
try {
|
||||||
|
if (empresaForm.id) {
|
||||||
|
await client.mutation(
|
||||||
|
api.empresas.update as typeof api.empresas.update,
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
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,
|
||||||
|
telefone: empresaForm.telefone,
|
||||||
|
email: empresaForm.email,
|
||||||
|
descricao: empresaForm.descricao || undefined,
|
||||||
|
contatos: empresaForm.contatos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fecharModal();
|
||||||
|
} catch (error) {
|
||||||
|
erroFormulario = error instanceof Error ? error.message : "Erro ao salvar empresa.";
|
||||||
|
} finally {
|
||||||
|
salvando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="container mx-auto px-4 py-4">
|
||||||
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
|
<ul>
|
||||||
|
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
|
||||||
|
<li><a href={resolve('/licitacoes')} class="text-primary hover:underline">Licitações</a></li>
|
||||||
|
<li>Empresas</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<Building2 class="h-8 w-8 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-primary">Empresas</h1>
|
||||||
|
<p class="text-base-content/70">
|
||||||
|
Cadastro, listagem e contatos de empresas fornecedoras.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary gap-2" type="button" onclick={abrirNovaEmpresa}>
|
||||||
|
<Plus class="h-4 w-4" strokeWidth={2} />
|
||||||
|
Nova empresa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
{#if empresasQuery.isLoading}
|
||||||
|
<div class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
</div>
|
||||||
|
{:else if empresasQuery.error}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>Erro ao carregar empresas.</span>
|
||||||
|
</div>
|
||||||
|
{:else if empresasQuery.data && empresasQuery.data.length === 0}
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<p class="text-base-content/70 mb-4">Nenhuma empresa cadastrada ainda.</p>
|
||||||
|
<button class="btn btn-primary gap-2" type="button" onclick={abrirNovaEmpresa}>
|
||||||
|
<Plus class="h-4 w-4" strokeWidth={2} />
|
||||||
|
Cadastrar primeira empresa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if empresasQuery.data}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>CNPJ</th>
|
||||||
|
<th>Telefone</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each empresasQuery.data as empresa (empresa._id)}
|
||||||
|
<tr>
|
||||||
|
<td>{empresa.nome}</td>
|
||||||
|
<td>{empresa.cnpj}</td>
|
||||||
|
<td class="flex items-center gap-2">
|
||||||
|
<Phone class="h-4 w-4 text-base-content/60" strokeWidth={2} />
|
||||||
|
<span>{empresa.telefone}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Mail class="h-4 w-4 text-base-content/60" strokeWidth={2} />
|
||||||
|
<span>{empresa.email}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-sm gap-2"
|
||||||
|
type="button"
|
||||||
|
onclick={() => verContatos(empresa._id, empresa.nome)}
|
||||||
|
>
|
||||||
|
<Users class="h-4 w-4" strokeWidth={2} />
|
||||||
|
Contatos
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm gap-2"
|
||||||
|
type="button"
|
||||||
|
onclick={() => editarEmpresa(empresa._id)}
|
||||||
|
>
|
||||||
|
<Pencil class="h-4 w-4" strokeWidth={2} />
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if modalAberto}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-5xl">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold">
|
||||||
|
{empresaForm.id ? "Editar empresa" : "Nova empresa"}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
Preencha os dados da empresa e cadastre contatos associados.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" onclick={fecharModal}>
|
||||||
|
<X class="h-4 w-4" strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if erroFormulario}
|
||||||
|
<div class="alert alert-error mb-4">
|
||||||
|
<span>{erroFormulario}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="nomeEmpresa"><span class="label-text">Nome da empresa *</span></label>
|
||||||
|
<input
|
||||||
|
id="nomeEmpresa"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value={empresaForm.nome}
|
||||||
|
placeholder="Nome"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="cnpjEmpresa"><span class="label-text">CNPJ *</span></label>
|
||||||
|
<input
|
||||||
|
id="cnpjEmpresa"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
value={empresaForm.cnpj}
|
||||||
|
inputmode="numeric"
|
||||||
|
oninput={handleEmpresaCnpjInput}
|
||||||
|
placeholder="00.000.000/0000-00"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="telefoneEmpresa"><span class="label-text">Telefone *</span></label>
|
||||||
|
<input
|
||||||
|
id="telefoneEmpresa"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
value={empresaForm.telefone}
|
||||||
|
inputmode="numeric"
|
||||||
|
oninput={handleEmpresaTelefoneInput}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="emailEmpresa"><span class="label-text">E-mail *</span></label>
|
||||||
|
<input
|
||||||
|
id="emailEmpresa"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
type="email"
|
||||||
|
bind:value={empresaForm.email}
|
||||||
|
placeholder="contato@empresa.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label" for="descricaoEmpresa">
|
||||||
|
<span class="label-text">Descrição (opcional)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="descricaoEmpresa"
|
||||||
|
class="textarea textarea-bordered w-full"
|
||||||
|
rows={3}
|
||||||
|
bind:value={empresaForm.descricao}
|
||||||
|
placeholder="Descrição, observações ou informações adicionais da empresa"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider my-4">Contatos da empresa</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Users class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||||
|
<span class="font-semibold">Contatos cadastrados</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline gap-2"
|
||||||
|
type="button"
|
||||||
|
onclick={adicionarContato}
|
||||||
|
>
|
||||||
|
<Plus class="h-4 w-4" strokeWidth={2} />
|
||||||
|
Adicionar contato
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if empresaForm.contatos.filter((c) => !c._deleted).length === 0}
|
||||||
|
<p class="text-sm text-base-content/60 mb-3">
|
||||||
|
Nenhum contato cadastrado. Clique em "Adicionar contato" para incluir.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>Função</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Telefone</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each empresaForm.contatos as contato, index (contato._id ?? `${contato.email}-${index}`)}
|
||||||
|
{#if !contato._deleted}
|
||||||
|
<tr>
|
||||||
|
<td>{contato.nome}</td>
|
||||||
|
<td>{contato.funcao}</td>
|
||||||
|
<td>{contato.email}</td>
|
||||||
|
<td>{contato.telefone}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
type="button"
|
||||||
|
onclick={() => editarContato(index)}
|
||||||
|
>
|
||||||
|
<Pencil class="h-3 w-3" strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
type="button"
|
||||||
|
onclick={() => removerContato(index)}
|
||||||
|
>
|
||||||
|
<X class="h-3 w-3" strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if contatoEmEdicao}
|
||||||
|
<div class="mt-4 p-4 rounded-lg bg-base-200">
|
||||||
|
<h3 class="font-semibold mb-3">
|
||||||
|
{contatoIndiceEdicao === null ? "Novo contato" : "Editar contato"}
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="contatoNome"><span class="label-text">Nome *</span></label>
|
||||||
|
<input
|
||||||
|
id="contatoNome"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
bind:value={contatoEmEdicao.nome}
|
||||||
|
placeholder="Nome do contato"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="contatoFuncao"><span class="label-text">Função *</span></label>
|
||||||
|
<input
|
||||||
|
id="contatoFuncao"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
bind:value={contatoEmEdicao.funcao}
|
||||||
|
placeholder="Função / cargo"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="contatoEmail"><span class="label-text">E-mail *</span></label>
|
||||||
|
<input
|
||||||
|
id="contatoEmail"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
type="email"
|
||||||
|
bind:value={contatoEmEdicao.email}
|
||||||
|
placeholder="email@exemplo.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="contatoTelefone"><span class="label-text">Telefone *</span></label>
|
||||||
|
<input
|
||||||
|
id="contatoTelefone"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
value={contatoEmEdicao.telefone}
|
||||||
|
inputmode="numeric"
|
||||||
|
oninput={handleContatoTelefoneInput}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label" for="contatoDescricao">
|
||||||
|
<span class="label-text">Descrição (opcional)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="contatoDescricao"
|
||||||
|
class="textarea textarea-bordered textarea-sm w-full"
|
||||||
|
rows={2}
|
||||||
|
bind:value={contatoEmEdicao.descricao}
|
||||||
|
placeholder="Observações sobre o contato"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
contatoEmEdicao = null;
|
||||||
|
contatoIndiceEdicao = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
onclick={salvarContatoAtual}
|
||||||
|
>
|
||||||
|
Salvar contato
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn btn-ghost" type="button" onclick={fecharModal} disabled={salvando}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
onclick={salvarEmpresa}
|
||||||
|
disabled={salvando}
|
||||||
|
>
|
||||||
|
{#if salvando}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Salvando...
|
||||||
|
{:else}
|
||||||
|
Salvar
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if contatosModalAberto}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-3xl">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold">Contatos da empresa</h2>
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
{empresaContatosNome}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" onclick={fecharContatosModal}>
|
||||||
|
<X class="h-4 w-4" strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !contatosDaEmpresa.length}
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Nenhum contato cadastrado para esta empresa.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>Função</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Telefone</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each contatosDaEmpresa as contato (contato._id ?? `${contato.email}`)}
|
||||||
|
<tr>
|
||||||
|
<td>{contato.nome}</td>
|
||||||
|
<td>{contato.funcao}</td>
|
||||||
|
<td>{contato.email}</td>
|
||||||
|
<td>{contato.telefone}</td>
|
||||||
|
<td>{contato.descricao}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn" type="button" onclick={fecharContatosModal}>Fechar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Trophy, Award, Plus } from "lucide-svelte";
|
import { Trophy, Award, Building2 } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -23,22 +23,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<div class="card-body">
|
<a
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
href={resolve('/licitacoes/empresas')}
|
||||||
<div class="mb-6">
|
class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow border border-base-200 hover:border-primary"
|
||||||
<Award class="h-24 w-24 text-base-content/20" strokeWidth={1.5} />
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Building2 class="h-6 w-6 text-primary" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<h4 class="font-semibold">Empresas</h4>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold mb-2">Módulo em Desenvolvimento</h2>
|
<p class="text-sm text-base-content/70">
|
||||||
<p class="text-base-content/70 max-w-md mb-6">
|
Cadastro, listagem e edição de empresas e seus contatos.
|
||||||
O módulo de Programas Esportivos está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de programas e projetos esportivos.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="badge badge-warning badge-lg gap-2">
|
</div>
|
||||||
<Plus class="h-4 w-4" strokeWidth={2} />
|
</a>
|
||||||
Em Desenvolvimento
|
|
||||||
|
<div class="card bg-base-100 shadow-md opacity-70">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<div class="p-2 bg-base-200 rounded-lg">
|
||||||
|
<Award class="h-6 w-6 text-base-content/50" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<h4 class="font-semibold text-base-content/70">Programas Esportivos</h4>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
Em breve: funcionalidades para gestão de programas e projetos esportivos.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
36
packages/backend/convex/_generated/api.d.ts
vendored
36
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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 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 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";
|
||||||
@@ -24,6 +24,7 @@ import type * as cursos from "../cursos.js";
|
|||||||
import type * as dashboard from "../dashboard.js";
|
import type * as dashboard from "../dashboard.js";
|
||||||
import type * as documentos from "../documentos.js";
|
import type * as documentos from "../documentos.js";
|
||||||
import type * as email from "../email.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 ferias from "../ferias.js";
|
||||||
import type * as funcionarios from "../funcionarios.js";
|
import type * as funcionarios from "../funcionarios.js";
|
||||||
import type * as healthCheck from "../healthCheck.js";
|
import type * as healthCheck from "../healthCheck.js";
|
||||||
@@ -54,6 +55,14 @@ 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;
|
||||||
@@ -62,8 +71,8 @@ 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;
|
||||||
|
auth: typeof auth;
|
||||||
chat: typeof chat;
|
chat: typeof chat;
|
||||||
configuracaoEmail: typeof configuracaoEmail;
|
configuracaoEmail: typeof configuracaoEmail;
|
||||||
crons: typeof crons;
|
crons: typeof crons;
|
||||||
@@ -71,6 +80,7 @@ declare const fullApi: ApiFromModules<{
|
|||||||
dashboard: typeof dashboard;
|
dashboard: typeof dashboard;
|
||||||
documentos: typeof documentos;
|
documentos: typeof documentos;
|
||||||
email: typeof email;
|
email: typeof email;
|
||||||
|
empresas: typeof empresas;
|
||||||
ferias: typeof ferias;
|
ferias: typeof ferias;
|
||||||
funcionarios: typeof funcionarios;
|
funcionarios: typeof funcionarios;
|
||||||
healthCheck: typeof healthCheck;
|
healthCheck: typeof healthCheck;
|
||||||
@@ -95,30 +105,14 @@ 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 fullApi,
|
typeof fullApiWithMounts,
|
||||||
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 fullApi,
|
typeof fullApiWithMounts,
|
||||||
FunctionReference<any, "internal">
|
FunctionReference<any, "internal">
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
16
packages/backend/convex/_generated/server.d.ts
vendored
16
packages/backend/convex/_generated/server.d.ts
vendored
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ActionBuilder,
|
ActionBuilder,
|
||||||
|
AnyComponents,
|
||||||
HttpActionBuilder,
|
HttpActionBuilder,
|
||||||
MutationBuilder,
|
MutationBuilder,
|
||||||
QueryBuilder,
|
QueryBuilder,
|
||||||
@@ -18,9 +19,15 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -85,12 +92,11 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
|||||||
/**
|
/**
|
||||||
* Define an HTTP action.
|
* Define an HTTP action.
|
||||||
*
|
*
|
||||||
* The wrapped function will be used to respond to HTTP requests received
|
* This function will be used to respond to HTTP requests received by a Convex
|
||||||
* by a Convex deployment if the requests matches the path and method where
|
* deployment if the requests matches the path and method where this action
|
||||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
* 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
|
* @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;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
internalActionGeneric,
|
internalActionGeneric,
|
||||||
internalMutationGeneric,
|
internalMutationGeneric,
|
||||||
internalQueryGeneric,
|
internalQueryGeneric,
|
||||||
|
componentsGeneric,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,14 +81,10 @@ export const action = actionGeneric;
|
|||||||
export const internalAction = internalActionGeneric;
|
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
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||||
* by a Convex deployment if the requests matches the path and method where
|
* as its second.
|
||||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
* @returns The wrapped endpoint function. Route a URL path to this function 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;
|
||||||
|
|||||||
185
packages/backend/convex/empresas.ts
Normal file
185
packages/backend/convex/empresas.ts
Normal file
@@ -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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +12,27 @@ export default defineSchema({
|
|||||||
text: v.string(),
|
text: v.string(),
|
||||||
completed: v.boolean(),
|
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({
|
funcionarios: defineTable({
|
||||||
// Campos obrigatórios existentes
|
// Campos obrigatórios existentes
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user