refactor: enhance licitacoes page layout and add contratos permissions
- Improved the layout of the licitacoes page for better readability and user experience. - Added new permissions for contratos, including listar, criar, editar, excluir, and ver actions. - Introduced a new schema for contratos with relevant fields and indexes to support contract management.
This commit is contained in:
@@ -1,78 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { FileText, ClipboardCopy, Building2 } from "lucide-svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { FileText, ClipboardCopy, Building2 } from 'lucide-svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||
</script>
|
||||
|
||||
<ProtectedRoute>
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
<ul>
|
||||
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
|
||||
<li>
|
||||
<a href={resolve('/')} class="text-primary hover:underline">Dashboard</a>
|
||||
</li>
|
||||
<li>Licitações</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-orange-500/20 rounded-xl">
|
||||
<FileText class="h-8 w-8 text-orange-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Licitações</h1>
|
||||
<p class="text-base-content/70">Gestão de processos licitatórios</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a
|
||||
href={resolve('/licitacoes/empresas')}
|
||||
class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow border border-base-200 hover:border-primary"
|
||||
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<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 class="mb-2 flex items-center gap-3">
|
||||
<div class="bg-primary/10 rounded-lg p-2">
|
||||
<Building2 class="text-primary h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
<h4 class="font-semibold">Empresas</h4>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/70">
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Cadastro, listagem e edição de empresas e seus contatos.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="card bg-base-100 shadow-md opacity-70">
|
||||
<a
|
||||
href={resolve('/licitacoes/contratos')}
|
||||
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<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 class="mb-2 flex items-center gap-3">
|
||||
<div class="bg-primary/10 rounded-lg p-2">
|
||||
<FileText class="text-primary h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
<h4 class="font-semibold text-base-content/70">Processos Licitatórios</h4>
|
||||
<h4 class="font-semibold">Contratos</h4>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/60">
|
||||
<p class="text-base-content/70 text-sm">Gestão de contratos, vigências e situações.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="card bg-base-100 opacity-70 shadow-md">
|
||||
<div class="card-body">
|
||||
<div class="mb-2 flex items-center gap-3">
|
||||
<div class="bg-base-200 rounded-lg p-2">
|
||||
<ClipboardCopy class="text-base-content/50 h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
<h4 class="text-base-content/70 font-semibold">Processos Licitatórios</h4>
|
||||
</div>
|
||||
<p class="text-base-content/60 text-sm">
|
||||
Em breve: cadastro e acompanhamento de licitações.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-md opacity-70">
|
||||
<div class="card bg-base-100 opacity-70 shadow-md">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-2 bg-base-200 rounded-lg">
|
||||
<FileText class="h-6 w-6 text-base-content/50" strokeWidth={2} />
|
||||
<div class="mb-2 flex items-center gap-3">
|
||||
<div class="bg-base-200 rounded-lg p-2">
|
||||
<FileText class="text-base-content/50 h-6 w-6" strokeWidth={2} />
|
||||
</div>
|
||||
<h4 class="font-semibold text-base-content/70">Documentação</h4>
|
||||
<h4 class="text-base-content/70 font-semibold">Documentação</h4>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Em breve: gestão de documentos e editais.
|
||||
</p>
|
||||
<p class="text-base-content/60 text-sm">Em breve: gestão de documentos e editais.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { Plus, AlertTriangle } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { format } from 'date-fns';
|
||||
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
|
||||
|
||||
// Client para mutations
|
||||
const client = useConvexClient();
|
||||
|
||||
// Autenticação
|
||||
$effect(() => {
|
||||
useConvexWithAuth();
|
||||
});
|
||||
|
||||
// Filtros
|
||||
let responsavelId = $state<Id<'funcionarios'> | undefined>(undefined);
|
||||
let dataInicio = $state<string | undefined>(undefined);
|
||||
let dataFim = $state<string | undefined>(undefined);
|
||||
|
||||
// Queries
|
||||
const contratosQuery = useQuery(api.contratos.listar, () => ({
|
||||
responsavelId: responsavelId,
|
||||
dataInicio,
|
||||
dataFim
|
||||
}));
|
||||
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
|
||||
// Derivados para facilitar acesso aos dados
|
||||
const contratos = $derived.by(() => {
|
||||
if (contratosQuery === undefined || contratosQuery === null) return [];
|
||||
if (Array.isArray(contratosQuery)) return contratosQuery;
|
||||
if ('data' in contratosQuery) return contratosQuery.data || [];
|
||||
return [];
|
||||
});
|
||||
|
||||
const funcionarios = $derived.by(() => {
|
||||
if (funcionariosQuery === undefined || funcionariosQuery === null) return [];
|
||||
if (Array.isArray(funcionariosQuery)) return funcionariosQuery;
|
||||
if ('data' in funcionariosQuery) return funcionariosQuery.data || [];
|
||||
return [];
|
||||
});
|
||||
|
||||
const isLoading = $derived(contratosQuery === undefined);
|
||||
const error = $derived(contratosQuery instanceof Error ? contratosQuery : null);
|
||||
|
||||
// Helpers
|
||||
function formatarData(data: string) {
|
||||
if (!data) return '-';
|
||||
return format(new Date(data), 'dd/MM/yyyy');
|
||||
}
|
||||
|
||||
function formatarMoeda(valor: string) {
|
||||
if (!valor) return '-';
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
}).format(parseFloat(valor));
|
||||
}
|
||||
|
||||
async function handleDelete(id: Id<'contratos'>) {
|
||||
if (confirm('Tem certeza que deseja excluir este contrato?')) {
|
||||
await client.mutation(api.contratos.excluir, { id });
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar vencimento (lógica visual simples baseada na data de fim)
|
||||
function isProximoVencimento(dataFim: string, diasAviso: number) {
|
||||
if (!dataFim) return false;
|
||||
const fim = new Date(dataFim).getTime();
|
||||
const hoje = Date.now();
|
||||
const aviso = fim - diasAviso * 24 * 60 * 60 * 1000;
|
||||
return hoje >= aviso && hoje <= fim;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Contratos</h1>
|
||||
<p class="text-muted-foreground">Gerencie os contratos, vigências e situações.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick={() => goto(resolve('/licitacoes/contratos/novo'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Novo Contrato
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="bg-base-100 grid gap-4 rounded-lg border p-4 shadow-sm md:grid-cols-4">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="filtroResponsavel">
|
||||
<span class="label-text">Responsável</span>
|
||||
</label>
|
||||
<select
|
||||
id="filtroResponsavel"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={responsavelId}
|
||||
>
|
||||
<option value={undefined}>Todos</option>
|
||||
{#each funcionarios as func (func._id)}
|
||||
<option value={func._id}>{func.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="filtroDataInicio">
|
||||
<span class="label-text">Vigência Início (A partir de)</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtroDataInicio"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={dataInicio}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="filtroDataFim">
|
||||
<span class="label-text">Vigência Fim (Até)</span>
|
||||
</label>
|
||||
<input
|
||||
id="filtroDataFim"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={dataFim}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
class="btn btn-outline w-full"
|
||||
onclick={() => {
|
||||
responsavelId = undefined;
|
||||
dataInicio = undefined;
|
||||
dataFim = undefined;
|
||||
}}
|
||||
>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela -->
|
||||
<div class="bg-base-100 overflow-x-auto rounded-md border">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nº Contrato</th>
|
||||
<th>Objeto</th>
|
||||
<th>Contratada</th>
|
||||
<th>Vigência</th>
|
||||
<th>Valor</th>
|
||||
<th>Situação</th>
|
||||
<th>Responsável</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if isLoading}
|
||||
<tr>
|
||||
<td colspan="8" class="h-24 text-center">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{:else if error}
|
||||
<tr>
|
||||
<td colspan="8" class="text-error h-24 text-center">
|
||||
Erro ao carregar contratos: {error.message}
|
||||
</td>
|
||||
</tr>
|
||||
{:else if contratos.length === 0}
|
||||
<tr>
|
||||
<td colspan="8" class="text-base-content/70 h-24 text-center">
|
||||
Nenhum contrato encontrado.
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each contratos as contrato (contrato._id)}
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
{contrato.numeroContrato}/{contrato.anoContrato}
|
||||
{#if isProximoVencimento(contrato.dataFimVigencia, contrato.diasAvisoVencimento)}
|
||||
<div class="tooltip" data-tip="Próximo do vencimento">
|
||||
<AlertTriangle class="text-warning h-4 w-4" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="max-w-[200px] truncate" title={contrato.objeto}>
|
||||
{contrato.objeto}
|
||||
</td>
|
||||
<td>
|
||||
{contrato.contratada?.razao_social || 'Empresa não encontrada'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-xs">
|
||||
{formatarData(contrato.dataInicioVigencia)} até
|
||||
<br />
|
||||
{formatarData(contrato.dataFimVigencia)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{formatarMoeda(contrato.valorTotal)}</td>
|
||||
<td>
|
||||
<div
|
||||
class="badge gap-2
|
||||
{contrato.situacao === 'em_execucao'
|
||||
? 'badge-success'
|
||||
: contrato.situacao === 'rescendido'
|
||||
? 'badge-error'
|
||||
: contrato.situacao === 'aguardando_assinatura'
|
||||
? 'badge-warning'
|
||||
: 'badge-ghost'}"
|
||||
>
|
||||
{contrato.situacao.replace('_', ' ').toUpperCase()}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{contrato.responsavel?.nome || '-'}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button
|
||||
class="btn btn-ghost btn-xs"
|
||||
onclick={() => goto(resolve(`/licitacoes/contratos/${contrato._id}`))}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-xs text-error"
|
||||
onclick={() => handleDelete(contrato._id)}
|
||||
>
|
||||
Excluir
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,394 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { ArrowLeft } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
|
||||
|
||||
// Client para mutations
|
||||
const client = useConvexClient();
|
||||
|
||||
// Autenticação
|
||||
$effect(() => {
|
||||
useConvexWithAuth();
|
||||
});
|
||||
|
||||
const contratoId = $page.params.id as Id<'contratos'>;
|
||||
|
||||
// Queries
|
||||
const contratoQuery = useQuery(api.contratos.obter, { id: contratoId });
|
||||
const empresasQuery = useQuery(api.empresas.list, {});
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
|
||||
// Derivados
|
||||
const contrato = $derived(contratoQuery.data);
|
||||
const empresas = $derived(empresasQuery.data || []);
|
||||
const funcionarios = $derived(funcionariosQuery.data || []);
|
||||
const isLoading = $derived(contratoQuery.isLoading);
|
||||
const error = $derived(contratoQuery.error);
|
||||
|
||||
type SituacaoContrato = 'em_execucao' | 'rescendido' | 'aguardando_assinatura' | 'finalizado';
|
||||
|
||||
type ContratoForm = {
|
||||
contratadaId: string | null;
|
||||
objeto: string;
|
||||
numeroNotaEmpenho: string;
|
||||
responsavelId: string | null;
|
||||
departamento: string;
|
||||
situacao: SituacaoContrato;
|
||||
numeroProcessoLicitatorio: string;
|
||||
modalidade: string;
|
||||
numeroContrato: string;
|
||||
anoContrato: number;
|
||||
dataInicioVigencia: string;
|
||||
dataFimVigencia: string;
|
||||
nomeFiscal: string;
|
||||
valorTotal: string;
|
||||
dataAditivoPrazo: string;
|
||||
diasAvisoVencimento: number;
|
||||
};
|
||||
|
||||
// Estado do formulário
|
||||
let loading = $state(false);
|
||||
let formData = $state<ContratoForm>({
|
||||
contratadaId: null,
|
||||
objeto: '',
|
||||
numeroNotaEmpenho: '',
|
||||
responsavelId: null,
|
||||
departamento: '',
|
||||
situacao: 'aguardando_assinatura',
|
||||
numeroProcessoLicitatorio: '',
|
||||
modalidade: '',
|
||||
numeroContrato: '',
|
||||
anoContrato: new Date().getFullYear(),
|
||||
dataInicioVigencia: '',
|
||||
dataFimVigencia: '',
|
||||
nomeFiscal: '',
|
||||
valorTotal: '',
|
||||
dataAditivoPrazo: '',
|
||||
diasAvisoVencimento: 30
|
||||
});
|
||||
|
||||
// Carregar dados quando a query retornar
|
||||
$effect(() => {
|
||||
if (contrato) {
|
||||
formData = {
|
||||
contratadaId: contrato.contratadaId,
|
||||
objeto: contrato.objeto,
|
||||
numeroNotaEmpenho: contrato.numeroNotaEmpenho,
|
||||
responsavelId: contrato.responsavelId,
|
||||
departamento: contrato.departamento,
|
||||
situacao: contrato.situacao,
|
||||
numeroProcessoLicitatorio: contrato.numeroProcessoLicitatorio,
|
||||
modalidade: contrato.modalidade,
|
||||
numeroContrato: contrato.numeroContrato,
|
||||
anoContrato: contrato.anoContrato,
|
||||
dataInicioVigencia: contrato.dataInicioVigencia,
|
||||
dataFimVigencia: contrato.dataFimVigencia,
|
||||
nomeFiscal: contrato.nomeFiscal,
|
||||
valorTotal: contrato.valorTotal,
|
||||
dataAditivoPrazo: contrato.dataAditivoPrazo || '',
|
||||
diasAvisoVencimento: contrato.diasAvisoVencimento
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.contratadaId || !formData.responsavelId) {
|
||||
toast.error('Selecione a empresa e o responsável.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading = true;
|
||||
await client.mutation(api.contratos.editar, {
|
||||
id: contratoId,
|
||||
contratadaId: formData.contratadaId as Id<'empresas'>,
|
||||
objeto: formData.objeto,
|
||||
numeroNotaEmpenho: formData.numeroNotaEmpenho,
|
||||
responsavelId: formData.responsavelId as Id<'funcionarios'>,
|
||||
departamento: formData.departamento,
|
||||
situacao: formData.situacao,
|
||||
numeroProcessoLicitatorio: formData.numeroProcessoLicitatorio,
|
||||
modalidade: formData.modalidade,
|
||||
numeroContrato: formData.numeroContrato,
|
||||
anoContrato: Number(formData.anoContrato),
|
||||
dataInicioVigencia: formData.dataInicioVigencia,
|
||||
dataFimVigencia: formData.dataFimVigencia,
|
||||
nomeFiscal: formData.nomeFiscal,
|
||||
valorTotal: formData.valorTotal,
|
||||
dataAditivoPrazo: formData.dataAditivoPrazo || undefined,
|
||||
diasAvisoVencimento: Number(formData.diasAvisoVencimento)
|
||||
});
|
||||
toast.success('Contrato atualizado com sucesso!');
|
||||
goto(resolve('/licitacoes/contratos'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
toast.error('Erro ao atualizar contrato: ' + error.message);
|
||||
} else {
|
||||
toast.error('Erro ao atualizar contrato: ' + String(error));
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex max-w-4xl flex-col gap-6 p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
class="btn btn-ghost btn-square"
|
||||
onclick={() => goto(resolve('/licitacoes/contratos'))}
|
||||
>
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Editar Contrato</h1>
|
||||
<p class="text-muted-foreground">Atualize os dados do contrato.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex justify-center p-8">
|
||||
<span class="loading loading-spinner loading-lg text-base-content/50"></span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="alert alert-error">
|
||||
<span>Erro ao carregar contrato: {error.message}</span>
|
||||
</div>
|
||||
{:else if contrato}
|
||||
<div class="bg-base-100 grid gap-6 rounded-lg border p-6 shadow-sm">
|
||||
<!-- Dados Básicos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroContrato">
|
||||
<span class="label-text">Número do Contrato</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroContrato"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroContrato}
|
||||
placeholder="Ex: 001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="anoContrato">
|
||||
<span class="label-text">Ano do Contrato</span>
|
||||
</label>
|
||||
<input
|
||||
id="anoContrato"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.anoContrato}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full md:col-span-2">
|
||||
<label class="label" for="objeto">
|
||||
<span class="label-text">Objeto</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="objeto"
|
||||
class="textarea textarea-bordered w-full"
|
||||
bind:value={formData.objeto}
|
||||
placeholder="Descrição do objeto do contrato"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Partes -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="contratadaId">
|
||||
<span class="label-text">Empresa Contratada</span>
|
||||
</label>
|
||||
<select
|
||||
id="contratadaId"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.contratadaId}
|
||||
>
|
||||
<option value={null} disabled>Selecione a empresa...</option>
|
||||
{#each empresas as emp (emp._id)}
|
||||
<option value={emp._id}>{emp.razao_social}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="responsavelId">
|
||||
<span class="label-text">Responsável (Fiscal)</span>
|
||||
</label>
|
||||
<select
|
||||
id="responsavelId"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.responsavelId}
|
||||
>
|
||||
<option value={null} disabled>Selecione o responsável...</option>
|
||||
{#each funcionarios as func (func._id)}
|
||||
<option value={func._id}>{func.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="nomeFiscal">
|
||||
<span class="label-text">Nome do Fiscal (Texto)</span>
|
||||
</label>
|
||||
<input
|
||||
id="nomeFiscal"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.nomeFiscal}
|
||||
placeholder="Nome completo do fiscal"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="departamento">
|
||||
<span class="label-text">Departamento</span>
|
||||
</label>
|
||||
<input
|
||||
id="departamento"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.departamento}
|
||||
placeholder="Ex: TI, Administrativo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detalhes Administrativos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroProcessoLicitatorio">
|
||||
<span class="label-text">Processo Licitatório</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroProcessoLicitatorio"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroProcessoLicitatorio}
|
||||
placeholder="Nº do processo"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="modalidade">
|
||||
<span class="label-text">Modalidade</span>
|
||||
</label>
|
||||
<input
|
||||
id="modalidade"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.modalidade}
|
||||
placeholder="Ex: Pregão Eletrônico"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroNotaEmpenho">
|
||||
<span class="label-text">Nota de Empenho</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroNotaEmpenho"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroNotaEmpenho}
|
||||
placeholder="Nº da nota"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valores e Prazos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="valorTotal">
|
||||
<span class="label-text">Valor Total (R$)</span>
|
||||
</label>
|
||||
<input
|
||||
id="valorTotal"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.valorTotal}
|
||||
placeholder="0,00"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataInicioVigencia">
|
||||
<span class="label-text">Início Vigência</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataInicioVigencia"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataInicioVigencia}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataFimVigencia">
|
||||
<span class="label-text">Fim Vigência</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataFimVigencia"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataFimVigencia}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Situação e Alertas -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="situacao">
|
||||
<span class="label-text">Situação</span>
|
||||
</label>
|
||||
<select
|
||||
id="situacao"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.situacao}
|
||||
>
|
||||
<option value="aguardando_assinatura">Aguardando Assinatura</option>
|
||||
<option value="em_execucao">Em Execução</option>
|
||||
<option value="rescendido">Rescindido</option>
|
||||
<option value="finalizado">Finalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="diasAvisoVencimento">
|
||||
<span class="label-text">Aviso de Vencimento (dias antes)</span>
|
||||
</label>
|
||||
<input
|
||||
id="diasAvisoVencimento"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.diasAvisoVencimento}
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataAditivoPrazo">
|
||||
<span class="label-text">Data Aditivo Prazo (Opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataAditivoPrazo"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataAditivoPrazo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-4 pt-4">
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
onclick={() => goto(resolve('/licitacoes/contratos'))}>Cancelar</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm mr-2"></span>
|
||||
{/if}
|
||||
Salvar Alterações
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,363 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { ArrowLeft } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
|
||||
|
||||
// Client para mutations
|
||||
const client = useConvexClient();
|
||||
|
||||
// Autenticação
|
||||
$effect(() => {
|
||||
useConvexWithAuth();
|
||||
});
|
||||
|
||||
// Queries para selects
|
||||
const empresasQuery = useQuery(api.empresas.list, {});
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
|
||||
// Derivados
|
||||
const empresas = $derived.by(() => {
|
||||
if (empresasQuery === undefined || empresasQuery === null) return [];
|
||||
if (Array.isArray(empresasQuery)) return empresasQuery;
|
||||
if ('data' in empresasQuery) return empresasQuery.data || [];
|
||||
return [];
|
||||
});
|
||||
|
||||
const funcionarios = $derived.by(() => {
|
||||
if (funcionariosQuery === undefined || funcionariosQuery === null) return [];
|
||||
if (Array.isArray(funcionariosQuery)) return funcionariosQuery;
|
||||
if ('data' in funcionariosQuery) return funcionariosQuery.data || [];
|
||||
return [];
|
||||
});
|
||||
|
||||
type SituacaoContrato = 'em_execucao' | 'rescendido' | 'aguardando_assinatura' | 'finalizado';
|
||||
|
||||
type ContratoForm = {
|
||||
contratadaId: string | null;
|
||||
objeto: string;
|
||||
numeroNotaEmpenho: string;
|
||||
responsavelId: string | null;
|
||||
departamento: string;
|
||||
situacao: SituacaoContrato;
|
||||
numeroProcessoLicitatorio: string;
|
||||
modalidade: string;
|
||||
numeroContrato: string;
|
||||
anoContrato: number;
|
||||
dataInicioVigencia: string;
|
||||
dataFimVigencia: string;
|
||||
nomeFiscal: string;
|
||||
valorTotal: string;
|
||||
dataAditivoPrazo: string;
|
||||
diasAvisoVencimento: number;
|
||||
};
|
||||
|
||||
// Estado do formulário
|
||||
let loading = $state(false);
|
||||
let formData = $state<ContratoForm>({
|
||||
contratadaId: null,
|
||||
objeto: '',
|
||||
numeroNotaEmpenho: '',
|
||||
responsavelId: null,
|
||||
departamento: '',
|
||||
situacao: 'aguardando_assinatura',
|
||||
numeroProcessoLicitatorio: '',
|
||||
modalidade: '',
|
||||
numeroContrato: '',
|
||||
anoContrato: new Date().getFullYear(),
|
||||
dataInicioVigencia: '',
|
||||
dataFimVigencia: '',
|
||||
nomeFiscal: '',
|
||||
valorTotal: '',
|
||||
dataAditivoPrazo: '',
|
||||
diasAvisoVencimento: 30
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.contratadaId || !formData.responsavelId) {
|
||||
toast.error('Selecione a empresa e o responsável.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading = true;
|
||||
await client.mutation(api.contratos.criar, {
|
||||
contratadaId: formData.contratadaId as Id<'empresas'>,
|
||||
objeto: formData.objeto,
|
||||
numeroNotaEmpenho: formData.numeroNotaEmpenho,
|
||||
responsavelId: formData.responsavelId as Id<'funcionarios'>,
|
||||
departamento: formData.departamento,
|
||||
situacao: formData.situacao,
|
||||
numeroProcessoLicitatorio: formData.numeroProcessoLicitatorio,
|
||||
modalidade: formData.modalidade,
|
||||
numeroContrato: formData.numeroContrato,
|
||||
anoContrato: Number(formData.anoContrato),
|
||||
dataInicioVigencia: formData.dataInicioVigencia,
|
||||
dataFimVigencia: formData.dataFimVigencia,
|
||||
nomeFiscal: formData.nomeFiscal,
|
||||
valorTotal: formData.valorTotal,
|
||||
dataAditivoPrazo: formData.dataAditivoPrazo || undefined,
|
||||
diasAvisoVencimento: Number(formData.diasAvisoVencimento)
|
||||
});
|
||||
toast.success('Contrato criado com sucesso!');
|
||||
goto(resolve('/licitacoes/contratos'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
toast.error('Erro ao criar contrato: ' + error.message);
|
||||
} else {
|
||||
toast.error('Erro ao criar contrato: ' + String(error));
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex max-w-4xl flex-col gap-6 p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
class="btn btn-ghost btn-square"
|
||||
onclick={() => goto(resolve('/licitacoes/contratos'))}
|
||||
>
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Novo Contrato</h1>
|
||||
<p class="text-muted-foreground">Preencha os dados do novo contrato.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 grid gap-6 rounded-lg border p-6 shadow-sm">
|
||||
<!-- Dados Básicos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroContrato">
|
||||
<span class="label-text">Número do Contrato</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroContrato"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroContrato}
|
||||
placeholder="Ex: 001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="anoContrato">
|
||||
<span class="label-text">Ano do Contrato</span>
|
||||
</label>
|
||||
<input
|
||||
id="anoContrato"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.anoContrato}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full md:col-span-2">
|
||||
<label class="label" for="objeto">
|
||||
<span class="label-text">Objeto</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="objeto"
|
||||
class="textarea textarea-bordered w-full"
|
||||
bind:value={formData.objeto}
|
||||
placeholder="Descrição do objeto do contrato"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Partes -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="contratadaId">
|
||||
<span class="label-text">Empresa Contratada</span>
|
||||
</label>
|
||||
<select
|
||||
id="contratadaId"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.contratadaId}
|
||||
>
|
||||
<option value={null} disabled selected>Selecione a empresa...</option>
|
||||
{#each empresas as emp (emp._id)}
|
||||
<option value={emp._id}>{emp.razao_social}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="responsavelId">
|
||||
<span class="label-text">Responsável (Fiscal)</span>
|
||||
</label>
|
||||
<select
|
||||
id="responsavelId"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.responsavelId}
|
||||
>
|
||||
<option value={null} disabled selected>Selecione o responsável...</option>
|
||||
{#each funcionarios as func (func._id)}
|
||||
<option value={func._id}>{func.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="nomeFiscal">
|
||||
<span class="label-text">Nome do Fiscal (Texto)</span>
|
||||
</label>
|
||||
<input
|
||||
id="nomeFiscal"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.nomeFiscal}
|
||||
placeholder="Nome completo do fiscal"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="departamento">
|
||||
<span class="label-text">Departamento</span>
|
||||
</label>
|
||||
<input
|
||||
id="departamento"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.departamento}
|
||||
placeholder="Ex: TI, Administrativo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detalhes Administrativos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroProcessoLicitatorio">
|
||||
<span class="label-text">Processo Licitatório</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroProcessoLicitatorio"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroProcessoLicitatorio}
|
||||
placeholder="Nº do processo"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="modalidade">
|
||||
<span class="label-text">Modalidade</span>
|
||||
</label>
|
||||
<input
|
||||
id="modalidade"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.modalidade}
|
||||
placeholder="Ex: Pregão Eletrônico"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="numeroNotaEmpenho">
|
||||
<span class="label-text">Nota de Empenho</span>
|
||||
</label>
|
||||
<input
|
||||
id="numeroNotaEmpenho"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.numeroNotaEmpenho}
|
||||
placeholder="Nº da nota"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valores e Prazos -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="valorTotal">
|
||||
<span class="label-text">Valor Total (R$)</span>
|
||||
</label>
|
||||
<input
|
||||
id="valorTotal"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.valorTotal}
|
||||
placeholder="0,00"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataInicioVigencia">
|
||||
<span class="label-text">Início Vigência</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataInicioVigencia"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataInicioVigencia}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataFimVigencia">
|
||||
<span class="label-text">Fim Vigência</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataFimVigencia"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataFimVigencia}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Situação e Alertas -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="situacao">
|
||||
<span class="label-text">Situação</span>
|
||||
</label>
|
||||
<select
|
||||
id="situacao"
|
||||
class="select select-bordered w-full"
|
||||
bind:value={formData.situacao}
|
||||
>
|
||||
<option value="aguardando_assinatura">Aguardando Assinatura</option>
|
||||
<option value="em_execucao">Em Execução</option>
|
||||
<option value="rescendido">Rescindido</option>
|
||||
<option value="finalizado">Finalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="diasAvisoVencimento">
|
||||
<span class="label-text">Aviso de Vencimento (dias antes)</span>
|
||||
</label>
|
||||
<input
|
||||
id="diasAvisoVencimento"
|
||||
type="number"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.diasAvisoVencimento}
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="dataAditivoPrazo">
|
||||
<span class="label-text">Data Aditivo Prazo (Opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="dataAditivoPrazo"
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={formData.dataAditivoPrazo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-4 pt-4">
|
||||
<button
|
||||
class="btn btn-outline"
|
||||
onclick={() => goto(resolve('/licitacoes/contratos'))}>Cancelar</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm mr-2"></span>
|
||||
{/if}
|
||||
Criar Contrato
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -21,6 +21,7 @@ import type * as auth_utils from "../auth/utils.js";
|
||||
import type * as chamados from "../chamados.js";
|
||||
import type * as chat from "../chat.js";
|
||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||
import type * as contratos from "../contratos.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as cursos from "../cursos.js";
|
||||
import type * as dashboard from "../dashboard.js";
|
||||
@@ -71,6 +72,7 @@ declare const fullApi: ApiFromModules<{
|
||||
chamados: typeof chamados;
|
||||
chat: typeof chat;
|
||||
configuracaoEmail: typeof configuracaoEmail;
|
||||
contratos: typeof contratos;
|
||||
crons: typeof crons;
|
||||
cursos: typeof cursos;
|
||||
dashboard: typeof dashboard;
|
||||
|
||||
200
packages/backend/convex/contratos.ts
Normal file
200
packages/backend/convex/contratos.ts
Normal file
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -120,7 +120,40 @@ export const reportStatus = v.union(
|
||||
v.literal("falhou")
|
||||
);
|
||||
|
||||
export const situacaoContrato = v.union(
|
||||
v.literal("em_execucao"),
|
||||
v.literal("rescendido"),
|
||||
v.literal("aguardando_assinatura"),
|
||||
v.literal("finalizado")
|
||||
);
|
||||
|
||||
export default defineSchema({
|
||||
contratos: defineTable({
|
||||
contratadaId: v.id("empresas"),
|
||||
objeto: v.string(),
|
||||
numeroNotaEmpenho: v.string(),
|
||||
responsavelId: v.id("funcionarios"),
|
||||
departamento: v.string(),
|
||||
situacao: situacaoContrato,
|
||||
numeroProcessoLicitatorio: v.string(),
|
||||
modalidade: v.string(),
|
||||
numeroContrato: v.string(),
|
||||
anoContrato: v.number(),
|
||||
dataInicioVigencia: v.string(),
|
||||
dataFimVigencia: v.string(),
|
||||
nomeFiscal: v.string(),
|
||||
valorTotal: v.string(),
|
||||
dataAditivoPrazo: v.optional(v.string()),
|
||||
diasAvisoVencimento: v.number(),
|
||||
criadoPor: v.id("usuarios"),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.optional(v.number()),
|
||||
})
|
||||
.index("by_responsavel", ["responsavelId"])
|
||||
.index("by_situacao", ["situacao"])
|
||||
.index("by_vigencia_inicio", ["dataInicioVigencia"])
|
||||
.index("by_vigencia_fim", ["dataFimVigencia"]),
|
||||
|
||||
todos: defineTable({
|
||||
text: v.string(),
|
||||
completed: v.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user