feat: enhance ata management by adding dataProrrogacao field and updating related logic for effective date handling, improving data integrity and user experience in pedidos

This commit is contained in:
2025-12-17 10:39:33 -03:00
parent fbf00c824e
commit 9072619e26
8 changed files with 390 additions and 115 deletions

View File

@@ -4,6 +4,7 @@
import { useConvexClient, useQuery } from 'convex-svelte';
import { Pencil, Plus, Trash2, X, Search, Check, FileText } from 'lucide-svelte';
import { resolve } from '$app/paths';
import { formatarDataBR } from '$lib/utils/datas';
const client = useConvexClient();
@@ -41,7 +42,8 @@
numeroSei: '',
empresaId: '' as Id<'empresas'> | '',
dataInicio: '',
dataFim: ''
dataFim: '',
dataProrrogacao: ''
});
let selectedObjetos = $state<Id<'objetos'>[]>([]);
type ObjetoAtaConfig = {
@@ -72,7 +74,8 @@
numeroSei: ata.numeroSei,
empresaId: ata.empresaId,
dataInicio: ata.dataInicio || '',
dataFim: ata.dataFim || ''
dataFim: ata.dataFim || '',
dataProrrogacao: ata.dataProrrogacao || ''
};
// Fetch linked objects
const linkedObjetos = await client.query(api.atas.getObjetos, { id: ata._id });
@@ -96,7 +99,8 @@
numeroSei: '',
empresaId: '',
dataInicio: '',
dataFim: ''
dataFim: '',
dataProrrogacao: ''
};
selectedObjetos = [];
objetosConfig = {};
@@ -180,6 +184,7 @@
empresaId: formData.empresaId as Id<'empresas'>,
dataInicio: formData.dataInicio || undefined,
dataFim: formData.dataFim || undefined,
dataProrrogacao: formData.dataProrrogacao || undefined,
objetos
};
@@ -416,7 +421,13 @@
{getEmpresaNome(ata.empresaId)}
</td>
<td class="text-base-content/70 whitespace-nowrap">
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
{ata.dataInicio ? formatarDataBR(ata.dataInicio) : '-'} a
{ata.dataFim ? formatarDataBR(ata.dataFim) : '-'}
{#if ata.dataProrrogacao}
<span class="text-base-content/50">
(prorrogação: {formatarDataBR(ata.dataProrrogacao)})</span
>
{/if}
</td>
<td class="text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-1">
@@ -508,7 +519,7 @@
</select>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="form-control w-full">
<label class="label" for="dataInicio">
<span class="label-text font-semibold">Data Início</span>
@@ -531,6 +542,17 @@
bind:value={formData.dataFim}
/>
</div>
<div class="form-control w-full">
<label class="label" for="dataProrrogacao">
<span class="label-text font-semibold">Data Prorrogação</span>
</label>
<input
id="dataProrrogacao"
class="input input-bordered focus:input-primary w-full"
type="date"
bind:value={formData.dataProrrogacao}
/>
</div>
</div>
</div>

View File

@@ -24,6 +24,7 @@
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { maskCurrencyBRL } from '$lib/utils/masks';
import { formatarDataBR } from '$lib/utils/datas';
const pedidoId = $derived(page.params.id as Id<'pedidos'>);
const client = useConvexClient();
@@ -84,7 +85,7 @@
// Atas por objeto (carregadas sob demanda)
type AtasComLimite = FunctionReturnType<typeof api.objetos.getAtasComLimite>;
let atasPorObjeto = $state<Record<string, AtasComLimite>>({});
let atasPorObjetoExtra = $state<Record<string, AtasComLimite>>({});
let editingItems = $state<Record<string, EditingItem>>({});
@@ -110,14 +111,39 @@
let selectedCount = $derived(selectedItemIds.size);
let hasSelection = $derived(selectedCount > 0);
// Garante que, para todos os itens existentes, as atas do respectivo objeto
// sejam carregadas independentemente do formulário de criação.
$effect(() => {
for (const item of items as unknown as PedidoItemForEdit[]) {
if (!atasPorObjeto[item.objetoId]) {
void loadAtasForObjeto(item.objetoId);
// Pela regra do backend, um pedido só pode ter uma ata (quando houver).
// Usamos isso para “forçar incluir” a ata atual do pedido na listagem do objeto,
// mesmo que esteja fora da janela (ex.: vencida há mais de 3 meses).
let pedidoAtaId = $derived.by(() => {
const withAta = (items as unknown as PedidoItemForEdit[]).find((i) => i.ataId);
return (withAta?.ataId ?? null) as Id<'atas'> | null;
});
// Carrega atas (para itens existentes) via query batch, evitando efeitos que mutam estado.
const atasBatchQuery = $derived.by(() =>
useQuery(api.objetos.getAtasComLimiteBatch, () => {
const ids: Id<'objetos'>[] = [];
const seen: Record<string, true> = {};
for (const item of items as unknown as PedidoItemForEdit[]) {
const key = String(item.objetoId);
if (seen[key]) continue;
seen[key] = true;
ids.push(item.objetoId);
}
return {
objetoIds: ids,
includeAtaIds: pedidoAtaId ? [pedidoAtaId] : undefined
};
})
);
let atasPorObjetoFromBatch = $derived.by(() => {
const map: Record<string, AtasComLimite> = {};
const data = atasBatchQuery.data || [];
for (const row of data) {
map[String(row.objetoId)] = row.atas;
}
return map;
});
// Group items by user
@@ -663,19 +689,20 @@
}
async function loadAtasForObjeto(objetoId: string) {
if (atasPorObjeto[objetoId]) return;
if (atasPorObjetoExtra[objetoId]) return;
try {
const linkedAtas = await client.query(api.objetos.getAtasComLimite, {
objetoId: objetoId as Id<'objetos'>
objetoId: objetoId as Id<'objetos'>,
includeAtaIds: pedidoAtaId ? [pedidoAtaId] : undefined
});
atasPorObjeto[objetoId] = linkedAtas;
atasPorObjetoExtra[objetoId] = linkedAtas;
} catch (e) {
console.error('Erro ao carregar atas para objeto', objetoId, e);
}
}
function getAtasForObjeto(objetoId: string): AtasComLimite {
return atasPorObjeto[objetoId] || [];
return atasPorObjetoExtra[objetoId] || atasPorObjetoFromBatch[objetoId] || [];
}
function handleObjetoChange(id: string) {
@@ -1664,11 +1691,18 @@
<option value="">Nenhuma</option>
{#each getAtasForObjeto(newItem.objetoId) as ata (ata._id)}
{@const isSelectedAta = String(ata._id) === newItem.ataId}
{@const reason = !ata.quantidadeTotal
? 'não configurada'
: ata.quantidadeUsada >= ata.limitePermitido
? 'limite atingido'
: null}
{@const reason =
ata.lockReason === 'nao_configurada'
? 'não configurada'
: ata.lockReason === 'limite_atingido'
? 'limite atingido'
: ata.lockReason === 'vigencia_expirada'
? `vigência encerrada em ${
ata.dataFimEfetiva || ata.dataFim
? formatarDataBR((ata.dataFimEfetiva || ata.dataFim) as string)
: '-'
}`
: null}
<option value={ata._id} disabled={ata.isLocked && !isSelectedAta}>
Ata {ata.numero} (SEI: {ata.numeroSei}){reason ? ` (${reason})` : ''}
</option>
@@ -1920,11 +1954,20 @@
{#each getAtasForObjeto(item.objetoId) as ata (ata._id)}
{@const currentAtaId = ensureEditingItem(item).ataId}
{@const isSelectedAta = String(ata._id) === currentAtaId}
{@const reason = !ata.quantidadeTotal
? 'não configurada'
: ata.quantidadeUsada >= ata.limitePermitido
? 'limite atingido'
: null}
{@const reason =
ata.lockReason === 'nao_configurada'
? 'não configurada'
: ata.lockReason === 'limite_atingido'
? 'limite atingido'
: ata.lockReason === 'vigencia_expirada'
? `vigência encerrada em ${
ata.dataFimEfetiva || ata.dataFim
? formatarDataBR(
(ata.dataFimEfetiva || ata.dataFim) as string
)
: '-'
}`
: null}
<option value={ata._id} disabled={ata.isLocked && !isSelectedAta}>
Ata {ata.numero} (SEI: {ata.numeroSei}){reason ? ` (${reason})` : ''}
</option>

View File

@@ -6,6 +6,7 @@
import { Plus, Trash2, X, Info } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { formatarDataBR } from '$lib/utils/datas';
const client = useConvexClient();
@@ -635,11 +636,18 @@
>
<option value="">Nenhuma</option>
{#each availableAtas as ata (ata._id)}
{@const reason = !ata.quantidadeTotal
? 'não configurada'
: ata.quantidadeUsada >= ata.limitePermitido
? 'limite atingido'
: null}
{@const reason =
ata.lockReason === 'nao_configurada'
? 'não configurada'
: ata.lockReason === 'limite_atingido'
? 'limite atingido'
: ata.lockReason === 'vigencia_expirada'
? `vigência encerrada em ${
ata.dataFimEfetiva || ata.dataFim
? formatarDataBR((ata.dataFimEfetiva || ata.dataFim) as string)
: '-'
}`
: null}
<option value={ata._id} disabled={ata.isLocked}>
Ata {ata.numero} (SEI: {ata.numeroSei}){reason ? ` (${reason})` : ''}
</option>
@@ -735,7 +743,12 @@
{#if detailsItem.ata.dataInicio}
<p class="text-green-800">
<strong>Vigência:</strong>
{detailsItem.ata.dataInicio} até {detailsItem.ata.dataFim || 'Indefinido'}
{formatarDataBR(detailsItem.ata.dataInicio)} até {detailsItem.ata
.dataFimEfetiva || detailsItem.ata.dataFim
? formatarDataBR(
(detailsItem.ata.dataFimEfetiva || detailsItem.ata.dataFim) as string
)
: 'Indefinido'}
</p>
{/if}
</div>