feat: Enhance pedidos management with detailed item linking, object search, and improved UI for item configuration and details
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { Pencil, Plus, Trash2, X } from 'lucide-svelte';
|
import { Pencil, Plus, Trash2, X, Search, Check } from 'lucide-svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
const empresasQuery = useQuery(api.empresas.list, {});
|
const empresasQuery = useQuery(api.empresas.list, {});
|
||||||
let empresas = $derived(empresasQuery.data || []);
|
let empresas = $derived(empresasQuery.data || []);
|
||||||
|
|
||||||
|
const objetosQuery = useQuery(api.objetos.list, {});
|
||||||
|
let objetos = $derived(objetosQuery.data || []);
|
||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
let showModal = $state(false);
|
let showModal = $state(false);
|
||||||
let editingId: string | null = $state(null);
|
let editingId: string | null = $state(null);
|
||||||
@@ -25,9 +28,16 @@
|
|||||||
dataInicio: '',
|
dataInicio: '',
|
||||||
dataFim: ''
|
dataFim: ''
|
||||||
});
|
});
|
||||||
|
let selectedObjetos = $state<Id<'objetos'>[]>([]);
|
||||||
|
let searchObjeto = $state('');
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
|
||||||
function openModal(ata?: Doc<'atas'>) {
|
// Derived state for filtered objects
|
||||||
|
let filteredObjetos = $derived(
|
||||||
|
objetos.filter((obj) => obj.nome.toLowerCase().includes(searchObjeto.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
async function openModal(ata?: Doc<'atas'>) {
|
||||||
if (ata) {
|
if (ata) {
|
||||||
editingId = ata._id;
|
editingId = ata._id;
|
||||||
formData = {
|
formData = {
|
||||||
@@ -37,6 +47,9 @@
|
|||||||
dataInicio: ata.dataInicio || '',
|
dataInicio: ata.dataInicio || '',
|
||||||
dataFim: ata.dataFim || ''
|
dataFim: ata.dataFim || ''
|
||||||
};
|
};
|
||||||
|
// Fetch linked objects
|
||||||
|
const linkedObjetos = await client.query(api.atas.getObjetos, { id: ata._id });
|
||||||
|
selectedObjetos = linkedObjetos.map((o) => o._id);
|
||||||
} else {
|
} else {
|
||||||
editingId = null;
|
editingId = null;
|
||||||
formData = {
|
formData = {
|
||||||
@@ -46,7 +59,9 @@
|
|||||||
dataInicio: '',
|
dataInicio: '',
|
||||||
dataFim: ''
|
dataFim: ''
|
||||||
};
|
};
|
||||||
|
selectedObjetos = [];
|
||||||
}
|
}
|
||||||
|
searchObjeto = '';
|
||||||
showModal = true;
|
showModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +70,14 @@
|
|||||||
editingId = null;
|
editingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleObjeto(id: Id<'objetos'>) {
|
||||||
|
if (selectedObjetos.includes(id)) {
|
||||||
|
selectedObjetos = selectedObjetos.filter((oid) => oid !== id);
|
||||||
|
} else {
|
||||||
|
selectedObjetos = [...selectedObjetos, id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!formData.empresaId) {
|
if (!formData.empresaId) {
|
||||||
@@ -68,7 +91,8 @@
|
|||||||
numeroSei: formData.numeroSei,
|
numeroSei: formData.numeroSei,
|
||||||
empresaId: formData.empresaId as Id<'empresas'>,
|
empresaId: formData.empresaId as Id<'empresas'>,
|
||||||
dataInicio: formData.dataInicio || undefined,
|
dataInicio: formData.dataInicio || undefined,
|
||||||
dataFim: formData.dataFim || undefined
|
dataFim: formData.dataFim || undefined,
|
||||||
|
objetosIds: selectedObjetos
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
@@ -185,7 +209,7 @@
|
|||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
||||||
>
|
>
|
||||||
<div class="relative w-full max-w-md rounded-lg bg-white p-8 shadow-xl">
|
<div class="relative w-full max-w-2xl rounded-lg bg-white p-8 shadow-xl">
|
||||||
<button
|
<button
|
||||||
onclick={closeModal}
|
onclick={closeModal}
|
||||||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||||
@@ -195,75 +219,128 @@
|
|||||||
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Nova'} Ata</h2>
|
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Nova'} Ata</h2>
|
||||||
|
|
||||||
<form onsubmit={handleSubmit}>
|
<form onsubmit={handleSubmit}>
|
||||||
<div class="mb-4">
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="numero">
|
|
||||||
Número da Ata
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
|
||||||
id="numero"
|
|
||||||
type="text"
|
|
||||||
bind:value={formData.numero}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="numeroSei">
|
|
||||||
Número SEI
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
|
||||||
id="numeroSei"
|
|
||||||
type="text"
|
|
||||||
bind:value={formData.numeroSei}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="empresa">
|
|
||||||
Empresa
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
|
||||||
id="empresa"
|
|
||||||
bind:value={formData.empresaId}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Selecione uma empresa...</option>
|
|
||||||
{#each empresas as empresa (empresa._id)}
|
|
||||||
<option value={empresa._id}>{empresa.razao_social}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6 grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataInicio">
|
<div class="mb-4">
|
||||||
Data Início
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="numero">
|
||||||
</label>
|
Número da Ata
|
||||||
<input
|
</label>
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
<input
|
||||||
id="dataInicio"
|
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
type="date"
|
id="numero"
|
||||||
bind:value={formData.dataInicio}
|
type="text"
|
||||||
/>
|
bind:value={formData.numero}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="numeroSei">
|
||||||
|
Número SEI
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
id="numeroSei"
|
||||||
|
type="text"
|
||||||
|
bind:value={formData.numeroSei}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="empresa">
|
||||||
|
Empresa
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
id="empresa"
|
||||||
|
bind:value={formData.empresaId}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione uma empresa...</option>
|
||||||
|
{#each empresas as empresa (empresa._id)}
|
||||||
|
<option value={empresa._id}>{empresa.razao_social}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataInicio">
|
||||||
|
Data Início
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
id="dataInicio"
|
||||||
|
type="date"
|
||||||
|
bind:value={formData.dataInicio}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataFim">
|
||||||
|
Data Fim
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
id="dataFim"
|
||||||
|
type="date"
|
||||||
|
bind:value={formData.dataFim}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataFim">
|
<div class="flex flex-col">
|
||||||
Data Fim
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="objetos">
|
||||||
|
Objetos Vinculados ({selectedObjetos.length})
|
||||||
</label>
|
</label>
|
||||||
<input
|
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
<div class="relative mb-2">
|
||||||
id="dataFim"
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
type="date"
|
<Search size={16} class="text-gray-400" />
|
||||||
bind:value={formData.dataFim}
|
</div>
|
||||||
/>
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar objetos..."
|
||||||
|
class="focus:shadow-outline w-full rounded border py-2 pr-3 pl-10 text-sm leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
bind:value={searchObjeto}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex-1 overflow-y-auto rounded border bg-gray-50 p-2"
|
||||||
|
style="max-height: 300px;"
|
||||||
|
>
|
||||||
|
{#if filteredObjetos.length === 0}
|
||||||
|
<p class="py-4 text-center text-sm text-gray-500">Nenhum objeto encontrado.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-1">
|
||||||
|
{#each filteredObjetos as objeto (objeto._id)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm hover:bg-gray-200 {selectedObjetos.includes(
|
||||||
|
objeto._id
|
||||||
|
)
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: ''}"
|
||||||
|
onclick={() => toggleObjeto(objeto._id)}
|
||||||
|
>
|
||||||
|
<span class="truncate">{objeto.nome}</span>
|
||||||
|
{#if selectedObjetos.includes(objeto._id)}
|
||||||
|
<Check size={16} class="text-blue-600" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
Selecione os objetos que fazem parte desta Ata.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end">
|
<div class="mt-6 flex items-center justify-end border-t pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={closeModal}
|
onclick={closeModal}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
Edit,
|
Edit,
|
||||||
|
Eye,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
Send,
|
Send,
|
||||||
@@ -34,6 +35,43 @@
|
|||||||
let objetos = $derived(objetosQuery.data || []);
|
let objetos = $derived(objetosQuery.data || []);
|
||||||
let acoes = $derived(acoesQuery.data || []);
|
let acoes = $derived(acoesQuery.data || []);
|
||||||
|
|
||||||
|
type Modalidade =
|
||||||
|
| 'dispensa'
|
||||||
|
| 'inexgibilidade'
|
||||||
|
| 'adesao'
|
||||||
|
| 'consumo';
|
||||||
|
|
||||||
|
type EditingItem = {
|
||||||
|
valorEstimado: string;
|
||||||
|
modalidade: Modalidade;
|
||||||
|
acaoId: string;
|
||||||
|
ataId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PedidoItemForEdit = {
|
||||||
|
_id: Id<'objetoItems'>;
|
||||||
|
valorEstimado: string;
|
||||||
|
modalidade: Modalidade;
|
||||||
|
acaoId?: Id<'acoes'>;
|
||||||
|
ataId?: Id<'atas'>;
|
||||||
|
objetoId: Id<'objetos'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Atas por objeto (carregadas sob demanda)
|
||||||
|
let atasPorObjeto = $state<Record<string, Array<Doc<'atas'>>>>({});
|
||||||
|
|
||||||
|
let editingItems = $state<Record<string, EditingItem>>({});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Group items by user
|
// Group items by user
|
||||||
let groupedItems = $derived.by(() => {
|
let groupedItems = $derived.by(() => {
|
||||||
const groups: Record<string, { name: string; items: typeof items }> = {};
|
const groups: Record<string, { name: string; items: typeof items }> = {};
|
||||||
@@ -73,8 +111,9 @@
|
|||||||
objetoId: '' as string,
|
objetoId: '' as string,
|
||||||
valorEstimado: '',
|
valorEstimado: '',
|
||||||
quantidade: 1,
|
quantidade: 1,
|
||||||
modalidade: 'consumo' as 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo',
|
modalidade: 'consumo' as Modalidade,
|
||||||
acaoId: '' as string
|
acaoId: '' as string,
|
||||||
|
ataId: '' as string
|
||||||
});
|
});
|
||||||
let addingItem = $state(false);
|
let addingItem = $state(false);
|
||||||
|
|
||||||
@@ -83,6 +122,23 @@
|
|||||||
let seiValue = $state('');
|
let seiValue = $state('');
|
||||||
let updatingSei = $state(false);
|
let updatingSei = $state(false);
|
||||||
|
|
||||||
|
// Item Details Modal State
|
||||||
|
let showDetailsModal = $state(false);
|
||||||
|
let selectedObjeto = $state<Doc<'objetos'> | null>(null);
|
||||||
|
|
||||||
|
function openDetails(objetoId: string) {
|
||||||
|
const obj = objetos.find((o) => o._id === objetoId);
|
||||||
|
if (obj) {
|
||||||
|
selectedObjeto = obj;
|
||||||
|
showDetailsModal = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetails() {
|
||||||
|
showDetailsModal = false;
|
||||||
|
selectedObjeto = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAddItem() {
|
async function handleAddItem() {
|
||||||
if (!newItem.objetoId || !newItem.valorEstimado) return;
|
if (!newItem.objetoId || !newItem.valorEstimado) return;
|
||||||
addingItem = true;
|
addingItem = true;
|
||||||
@@ -93,14 +149,16 @@
|
|||||||
valorEstimado: newItem.valorEstimado,
|
valorEstimado: newItem.valorEstimado,
|
||||||
quantidade: newItem.quantidade,
|
quantidade: newItem.quantidade,
|
||||||
modalidade: newItem.modalidade,
|
modalidade: newItem.modalidade,
|
||||||
acaoId: newItem.acaoId ? (newItem.acaoId as Id<'acoes'>) : undefined
|
acaoId: newItem.acaoId ? (newItem.acaoId as Id<'acoes'>) : undefined,
|
||||||
|
ataId: newItem.ataId ? (newItem.ataId as Id<'atas'>) : undefined
|
||||||
});
|
});
|
||||||
newItem = {
|
newItem = {
|
||||||
objetoId: '',
|
objetoId: '',
|
||||||
valorEstimado: '',
|
valorEstimado: '',
|
||||||
quantidade: 1,
|
quantidade: 1,
|
||||||
modalidade: 'consumo',
|
modalidade: 'consumo',
|
||||||
acaoId: ''
|
acaoId: '',
|
||||||
|
ataId: ''
|
||||||
};
|
};
|
||||||
showAddItem = false;
|
showAddItem = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -163,6 +221,25 @@
|
|||||||
return acoes.find((a) => a._id === id)?.nome || '-';
|
return acoes.find((a) => a._id === id)?.nome || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAtasForObjeto(objetoId: string) {
|
||||||
|
if (atasPorObjeto[objetoId]) return;
|
||||||
|
try {
|
||||||
|
const linkedAtas = await client.query(api.objetos.getAtas, {
|
||||||
|
objetoId: objetoId as Id<'objetos'>
|
||||||
|
});
|
||||||
|
atasPorObjeto = {
|
||||||
|
...atasPorObjeto,
|
||||||
|
[objetoId]: linkedAtas
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erro ao carregar atas para objeto', objetoId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAtasForObjeto(objetoId: string): Array<Doc<'atas'>> {
|
||||||
|
return atasPorObjeto[objetoId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
function handleObjetoChange(id: string) {
|
function handleObjetoChange(id: string) {
|
||||||
newItem.objetoId = id;
|
newItem.objetoId = id;
|
||||||
const objeto = objetos.find((o) => o._id === id);
|
const objeto = objetos.find((o) => o._id === id);
|
||||||
@@ -171,6 +248,11 @@
|
|||||||
} else {
|
} else {
|
||||||
newItem.valorEstimado = '';
|
newItem.valorEstimado = '';
|
||||||
}
|
}
|
||||||
|
newItem.ataId = '';
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
void loadAtasForObjeto(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMoneyToNumber(value: string): number {
|
function parseMoneyToNumber(value: string): number {
|
||||||
@@ -187,6 +269,54 @@
|
|||||||
items.reduce((sum, item) => sum + calculateItemTotal(item.valorEstimado, item.quantidade), 0)
|
items.reduce((sum, item) => sum + calculateItemTotal(item.valorEstimado, item.quantidade), 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function ensureEditingItem(item: PedidoItemForEdit): EditingItem {
|
||||||
|
const existing = editingItems[item._id];
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valorEstimado: maskCurrencyBRL(item.valorEstimado || ''),
|
||||||
|
modalidade: item.modalidade,
|
||||||
|
acaoId: item.acaoId ?? '',
|
||||||
|
ataId: item.ataId ?? ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditingField<K extends keyof EditingItem>(
|
||||||
|
itemId: Id<'objetoItems'>,
|
||||||
|
field: K,
|
||||||
|
value: EditingItem[K]
|
||||||
|
) {
|
||||||
|
const current = editingItems[itemId] ?? {
|
||||||
|
valorEstimado: '',
|
||||||
|
modalidade: 'consumo',
|
||||||
|
acaoId: '',
|
||||||
|
ataId: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
editingItems = {
|
||||||
|
...editingItems,
|
||||||
|
[itemId]: {
|
||||||
|
...current,
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistItemChanges(item: PedidoItemForEdit) {
|
||||||
|
const current = ensureEditingItem(item);
|
||||||
|
try {
|
||||||
|
await client.mutation(api.pedidos.updateItem, {
|
||||||
|
itemId: item._id,
|
||||||
|
valorEstimado: current.valorEstimado,
|
||||||
|
modalidade: current.modalidade,
|
||||||
|
acaoId: current.acaoId ? (current.acaoId as Id<'acoes'>) : undefined,
|
||||||
|
ataId: current.ataId ? (current.ataId as Id<'atas'>) : undefined
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao atualizar item: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatStatus(status: string) {
|
function formatStatus(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'em_rascunho':
|
case 'em_rascunho':
|
||||||
@@ -268,6 +398,8 @@
|
|||||||
return '🔄';
|
return '🔄';
|
||||||
case 'atualizacao_sei':
|
case 'atualizacao_sei':
|
||||||
return '📋';
|
return '📋';
|
||||||
|
case 'edicao_item':
|
||||||
|
return '✏️';
|
||||||
default:
|
default:
|
||||||
return '•';
|
return '•';
|
||||||
}
|
}
|
||||||
@@ -308,6 +440,11 @@
|
|||||||
return `${entry.usuarioNome} alterou o status para "${formatStatus(detalhes.novoStatus)}"`;
|
return `${entry.usuarioNome} alterou o status para "${formatStatus(detalhes.novoStatus)}"`;
|
||||||
case 'atualizacao_sei':
|
case 'atualizacao_sei':
|
||||||
return `${entry.usuarioNome} atualizou o número SEI para "${detalhes.numeroSei}"`;
|
return `${entry.usuarioNome} atualizou o número SEI para "${detalhes.numeroSei}"`;
|
||||||
|
case 'edicao_item': {
|
||||||
|
const objeto = objetos.find((o) => o._id === detalhes.objetoId);
|
||||||
|
const nomeObjeto = objeto?.nome || 'Objeto desconhecido';
|
||||||
|
return `${entry.usuarioNome} editou o item ${nomeObjeto}`;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return `${entry.usuarioNome} realizou: ${entry.acao}`;
|
return `${entry.usuarioNome} realizou: ${entry.acao}`;
|
||||||
@@ -497,6 +634,23 @@
|
|||||||
<option value="adesao">Adesão</option>
|
<option value="adesao">Adesão</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{#if newItem.objetoId}
|
||||||
|
<div>
|
||||||
|
<label for="ata-select" class="mb-1 block text-xs font-medium text-gray-500"
|
||||||
|
>Ata (Opcional)</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="ata-select"
|
||||||
|
bind:value={newItem.ataId}
|
||||||
|
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||||||
|
>
|
||||||
|
<option value="">Nenhuma</option>
|
||||||
|
{#each getAtasForObjeto(newItem.objetoId) as ata (ata._id)}
|
||||||
|
<option value={ata._id}>Ata {ata.numero} (SEI: {ata.numeroSei})</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<label for="acao-select" class="mb-1 block text-xs font-medium text-gray-500"
|
<label for="acao-select" class="mb-1 block text-xs font-medium text-gray-500"
|
||||||
>Ação (Opcional)</label
|
>Ação (Opcional)</label
|
||||||
@@ -559,6 +713,10 @@
|
|||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
>Ação</th
|
>Ação</th
|
||||||
>
|
>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
|
>Ata</th
|
||||||
|
>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
>Total</th
|
>Total</th
|
||||||
@@ -588,13 +746,92 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{maskCurrencyBRL(item.valorEstimado) || 'R$ 0,00'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-28 rounded border px-2 py-1 text-sm"
|
||||||
|
value={ensureEditingItem(item).valorEstimado}
|
||||||
|
oninput={(e) =>
|
||||||
|
setEditingField(
|
||||||
|
item._id,
|
||||||
|
'valorEstimado',
|
||||||
|
maskCurrencyBRL(e.currentTarget.value)
|
||||||
|
)}
|
||||||
|
onblur={() => persistItemChanges(item)}
|
||||||
|
placeholder="R$ 0,00"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{maskCurrencyBRL(item.valorEstimado) || 'R$ 0,00'}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
{item.modalidade}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<select
|
||||||
|
class="rounded border px-2 py-1 text-xs"
|
||||||
|
value={ensureEditingItem(item).modalidade}
|
||||||
|
onchange={(e) => {
|
||||||
|
setEditingField(
|
||||||
|
item._id,
|
||||||
|
'modalidade',
|
||||||
|
e.currentTarget.value as Modalidade
|
||||||
|
);
|
||||||
|
void persistItemChanges(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="consumo">Consumo</option>
|
||||||
|
<option value="dispensa">Dispensa</option>
|
||||||
|
<option value="inexgibilidade">Inexigibilidade</option>
|
||||||
|
<option value="adesao">Adesão</option>
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
{item.modalidade}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
{getAcaoName(item.acaoId)}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<select
|
||||||
|
class="rounded border px-2 py-1 text-xs"
|
||||||
|
value={ensureEditingItem(item).acaoId}
|
||||||
|
onchange={(e) => {
|
||||||
|
setEditingField(item._id, 'acaoId', e.currentTarget.value);
|
||||||
|
void persistItemChanges(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Nenhuma</option>
|
||||||
|
{#each acoes as a (a._id)}
|
||||||
|
<option value={a._id}>{a.nome}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
{getAcaoName(item.acaoId)}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<select
|
||||||
|
class="rounded border px-2 py-1 text-xs"
|
||||||
|
value={ensureEditingItem(item).ataId}
|
||||||
|
onchange={(e) => {
|
||||||
|
setEditingField(item._id, 'ataId', e.currentTarget.value);
|
||||||
|
void persistItemChanges(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Nenhuma</option>
|
||||||
|
{#each getAtasForObjeto(item.objetoId) as ata (ata._id)}
|
||||||
|
<option value={ata._id}>Ata {ata.numero} (SEI: {ata.numeroSei})</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
{#if item.ataId}
|
||||||
|
{#each getAtasForObjeto(item.objetoId) as ata (ata._id)}
|
||||||
|
{#if ata._id === item.ataId}
|
||||||
|
Ata {ata.numero}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
-
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right font-medium whitespace-nowrap">
|
<td class="px-6 py-4 text-right font-medium whitespace-nowrap">
|
||||||
R$ {calculateItemTotal(item.valorEstimado, item.quantidade)
|
R$ {calculateItemTotal(item.valorEstimado, item.quantidade)
|
||||||
@@ -602,12 +839,20 @@
|
|||||||
.replace('.', ',')}
|
.replace('.', ',')}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
onclick={() => openDetails(item.objetoId)}
|
||||||
|
class="mr-3 text-indigo-600 hover:text-indigo-900"
|
||||||
|
title="Ver Detalhes"
|
||||||
|
>
|
||||||
|
<Eye size={18} />
|
||||||
|
</button>
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
<button
|
<button
|
||||||
onclick={() => handleRemoveItem(item._id)}
|
onclick={() => handleRemoveItem(item._id)}
|
||||||
class="text-red-600 hover:text-red-900"
|
class="text-red-600 hover:text-red-900"
|
||||||
|
title="Remover Item"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
@@ -629,6 +874,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showDetailsModal && selectedObjeto}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
||||||
|
>
|
||||||
|
<div class="relative w-full max-w-lg rounded-lg bg-white p-8 shadow-xl">
|
||||||
|
<button
|
||||||
|
onclick={closeDetails}
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
<h2 class="mb-6 text-xl font-bold">Detalhes do Objeto</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Nome</div>
|
||||||
|
<p class="text-gray-900">{selectedObjeto.nome}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Tipo</div>
|
||||||
|
<span
|
||||||
|
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold
|
||||||
|
{selectedObjeto.tipo === 'servico'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-blue-100 text-blue-800'}"
|
||||||
|
>
|
||||||
|
{selectedObjeto.tipo === 'material' ? 'Material' : 'Serviço'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Unidade</div>
|
||||||
|
<p class="text-gray-900">{selectedObjeto.unidade}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Código Efisco</div>
|
||||||
|
<p class="text-gray-900">{selectedObjeto.codigoEfisco}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if selectedObjeto.tipo === 'material'}
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Código Catmat</div>
|
||||||
|
<p class="text-gray-900">{selectedObjeto.codigoCatmat || '-'}</p>
|
||||||
|
{:else}
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase">Código Catserv</div>
|
||||||
|
<p class="text-gray-900">{selectedObjeto.codigoCatserv || '-'}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="block text-xs font-bold text-gray-500 uppercase"
|
||||||
|
>Valor Estimado (Unitário)</div
|
||||||
|
>
|
||||||
|
<p class="text-gray-900">
|
||||||
|
{maskCurrencyBRL(selectedObjeto.valorEstimado || '') || 'R$ 0,00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
onclick={closeDetails}
|
||||||
|
class="rounded bg-gray-200 px-4 py-2 font-medium text-gray-700 hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Histórico -->
|
<!-- Histórico -->
|
||||||
<div class="rounded-lg bg-white p-6 shadow">
|
<div class="rounded-lg bg-white p-6 shadow">
|
||||||
<h2 class="mb-4 text-lg font-semibold text-gray-900">Histórico</h2>
|
<h2 class="mb-4 text-lg font-semibold text-gray-900">Histórico</h2>
|
||||||
@@ -640,7 +960,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-start gap-3 rounded-lg border border-gray-200 p-3 hover:bg-gray-50"
|
class="flex items-start gap-3 rounded-lg border border-gray-200 p-3 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
<div class="flex-shrink-0 text-2xl">
|
<div class="shrink-0 text-2xl">
|
||||||
{getHistoryIcon(entry.acao)}
|
{getHistoryIcon(entry.acao)}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { Plus, Trash2, X } from 'lucide-svelte';
|
import { Plus, Trash2, X, Info } from 'lucide-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
acaoId?: Id<'acoes'>;
|
acaoId?: Id<'acoes'>;
|
||||||
ataId?: Id<'atas'>;
|
ataId?: Id<'atas'>;
|
||||||
ataNumero?: string; // For display
|
ataNumero?: string; // For display
|
||||||
|
ata?: Doc<'atas'>; // Full ata object for details
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectedItems = $state<SelectedItem[]>([]);
|
let selectedItems = $state<SelectedItem[]>([]);
|
||||||
@@ -55,6 +56,20 @@
|
|||||||
|
|
||||||
let availableAtas = $state<Doc<'atas'>[]>([]);
|
let availableAtas = $state<Doc<'atas'>[]>([]);
|
||||||
|
|
||||||
|
// Item Details Modal
|
||||||
|
let showDetailsModal = $state(false);
|
||||||
|
let detailsItem = $state<SelectedItem | null>(null);
|
||||||
|
|
||||||
|
function openDetails(item: SelectedItem) {
|
||||||
|
detailsItem = item;
|
||||||
|
showDetailsModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetails() {
|
||||||
|
showDetailsModal = false;
|
||||||
|
detailsItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function openItemModal(objeto: Doc<'objetos'>) {
|
async function openItemModal(objeto: Doc<'objetos'>) {
|
||||||
// Fetch linked Atas for this object
|
// Fetch linked Atas for this object
|
||||||
const linkedAtas = await client.query(api.objetos.getAtas, { objetoId: objeto._id });
|
const linkedAtas = await client.query(api.objetos.getAtas, { objetoId: objeto._id });
|
||||||
@@ -90,7 +105,8 @@
|
|||||||
modalidade: itemConfig.modalidade,
|
modalidade: itemConfig.modalidade,
|
||||||
acaoId: itemConfig.acaoId ? (itemConfig.acaoId as Id<'acoes'>) : undefined,
|
acaoId: itemConfig.acaoId ? (itemConfig.acaoId as Id<'acoes'>) : undefined,
|
||||||
ataId: itemConfig.ataId ? (itemConfig.ataId as Id<'atas'>) : undefined,
|
ataId: itemConfig.ataId ? (itemConfig.ataId as Id<'atas'>) : undefined,
|
||||||
ataNumero: selectedAta?.numero
|
ataNumero: selectedAta?.numero,
|
||||||
|
ata: selectedAta
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
checkExisting();
|
checkExisting();
|
||||||
@@ -223,58 +239,70 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto max-w-3xl p-6">
|
<div class="container mx-auto max-w-4xl p-6">
|
||||||
<h1 class="mb-6 text-2xl font-bold">Novo Pedido</h1>
|
<h1 class="mb-6 text-3xl font-bold">Novo Pedido</h1>
|
||||||
|
|
||||||
<div class="rounded-lg bg-white p-6 shadow-md">
|
<div class="space-y-6">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
<div class="rounded-lg border border-red-400 bg-red-50 px-4 py-3 text-red-700">
|
||||||
{error}
|
<p class="font-semibold">Erro</p>
|
||||||
|
<p class="text-sm">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form onsubmit={handleSubmit}>
|
<form onsubmit={handleSubmit} class="space-y-6">
|
||||||
<div class="mb-6">
|
<!-- Section 1: Basic Information -->
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="numeroSei">
|
<div class="rounded-lg bg-white p-6 shadow-md">
|
||||||
Número SEI (Opcional)
|
<h2 class="mb-4 text-lg font-semibold text-gray-800">Informações Básicas</h2>
|
||||||
</label>
|
<div>
|
||||||
<input
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="numeroSei">
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
Número SEI (Opcional)
|
||||||
id="numeroSei"
|
</label>
|
||||||
type="text"
|
<input
|
||||||
bind:value={formData.numeroSei}
|
class="focus:shadow-outline w-full appearance-none rounded-lg border border-gray-300 px-4 py-2.5 leading-tight text-gray-700 shadow-sm transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||||
placeholder="Ex: 12345.000000/2023-00"
|
id="numeroSei"
|
||||||
onblur={checkExisting}
|
type="text"
|
||||||
/>
|
bind:value={formData.numeroSei}
|
||||||
<p class="mt-1 text-xs text-gray-500">Você pode adicionar o número SEI posteriormente.</p>
|
placeholder="Ex: 12345.000000/2023-00"
|
||||||
|
onblur={checkExisting}
|
||||||
|
/>
|
||||||
|
<p class="mt-1.5 text-xs text-gray-500">
|
||||||
|
Você pode adicionar o número SEI posteriormente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<!-- Section 2: Add Objects -->
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="produtos">
|
<div class="rounded-lg bg-white p-6 shadow-md">
|
||||||
Adicionar Objetos
|
<h2 class="mb-4 text-lg font-semibold text-gray-800">Adicionar Objetos ao Pedido</h2>
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="relative mb-2">
|
<div class="relative mb-4">
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="search-objetos">
|
||||||
|
Buscar Objetos
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="search-objetos"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar objetos..."
|
placeholder="Digite o nome do objeto..."
|
||||||
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
class="focus:shadow-outline w-full appearance-none rounded-lg border border-gray-300 px-4 py-2.5 leading-tight text-gray-700 shadow-sm transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
{#if searchQuery.length > 0 && searchResults}
|
{#if searchQuery.length > 0 && searchResults}
|
||||||
<div class="absolute z-10 mt-1 w-full rounded border bg-white shadow-lg">
|
<div
|
||||||
|
class="absolute z-10 mt-2 w-full rounded-lg border border-gray-200 bg-white shadow-xl"
|
||||||
|
>
|
||||||
{#if searchResults.length === 0}
|
{#if searchResults.length === 0}
|
||||||
<div class="p-2 text-sm text-gray-500">Nenhum objeto encontrado.</div>
|
<div class="p-4 text-sm text-gray-500">Nenhum objeto encontrado.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="max-h-60 overflow-y-auto">
|
<ul class="max-h-64 overflow-y-auto">
|
||||||
{#each searchResults as objeto (objeto._id)}
|
{#each searchResults as objeto (objeto._id)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between px-4 py-2 text-left hover:bg-gray-100"
|
class="flex w-full items-center justify-between px-4 py-3 text-left transition hover:bg-blue-50"
|
||||||
onclick={() => openItemModal(objeto)}
|
onclick={() => openItemModal(objeto)}
|
||||||
>
|
>
|
||||||
<span>{objeto.nome}</span>
|
<span class="font-medium text-gray-800">{objeto.nome}</span>
|
||||||
<Plus size={16} class="text-blue-600" />
|
<Plus size={16} class="text-blue-600" />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -286,68 +314,97 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedItems.length > 0}
|
{#if selectedItems.length > 0}
|
||||||
<div class="mt-4">
|
<div class="mt-6">
|
||||||
<h3 class="mb-2 text-sm font-semibold text-gray-700">Itens Selecionados:</h3>
|
<h3 class="mb-3 text-sm font-semibold text-gray-700">
|
||||||
|
Itens Selecionados ({selectedItems.length})
|
||||||
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each selectedItems as item, index (index)}
|
{#each selectedItems as item, index (index)}
|
||||||
<div class="rounded-md border bg-gray-50 p-3">
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-gray-50 p-4 transition hover:shadow-md"
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div class="font-medium">{item.objeto.nome}</div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-xs text-gray-500">
|
<p class="font-semibold text-gray-900">{item.objeto.nome}</p>
|
||||||
Qtd: {item.quantidade} | Unid: {item.objeto.unidade}
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-xs">
|
|
||||||
<span class="font-semibold text-gray-600">Modalidade:</span>
|
|
||||||
{item.modalidade}
|
|
||||||
{#if item.acaoId}
|
|
||||||
<span class="ml-2 font-semibold text-gray-600">Ação:</span>
|
|
||||||
{getAcaoNome(item.acaoId)}
|
|
||||||
{/if}
|
|
||||||
{#if item.ataNumero}
|
{#if item.ataNumero}
|
||||||
<span class="ml-2 font-semibold text-gray-600">Ata:</span>
|
<span
|
||||||
{item.ataNumero}
|
class="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"
|
||||||
|
>
|
||||||
|
Ata {item.ataNumero}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-600">
|
||||||
|
<span><strong>Qtd:</strong> {item.quantidade} {item.objeto.unidade}</span>
|
||||||
|
<span><strong>Modalidade:</strong> {item.modalidade}</span>
|
||||||
|
{#if item.acaoId}
|
||||||
|
<span><strong>Ação:</strong> {getAcaoNome(item.acaoId)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="ml-4 flex items-center gap-2">
|
||||||
type="button"
|
<button
|
||||||
class="text-red-600 hover:text-red-800"
|
type="button"
|
||||||
onclick={() => removeItem(index)}
|
class="rounded p-2 text-blue-600 transition hover:bg-blue-50"
|
||||||
>
|
onclick={() => openDetails(item)}
|
||||||
<Trash2 size={18} />
|
aria-label="Ver detalhes"
|
||||||
</button>
|
>
|
||||||
|
<Info size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded p-2 text-red-600 transition hover:bg-red-50"
|
||||||
|
onclick={() => removeItem(index)}
|
||||||
|
aria-label="Remover item"
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-4 rounded-lg border-2 border-dashed border-gray-300 p-8 text-center">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Nenhum item adicionado. Use a busca acima para adicionar objetos ao pedido.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Warnings Section -->
|
||||||
{#if warning}
|
{#if warning}
|
||||||
<div
|
<div
|
||||||
class="mb-4 rounded border border-yellow-400 bg-yellow-100 px-4 py-3 text-sm text-yellow-800"
|
class="rounded-lg border border-yellow-400 bg-yellow-50 px-4 py-3 text-sm text-yellow-800"
|
||||||
>
|
>
|
||||||
{warning}
|
<p class="font-semibold">Aviso</p>
|
||||||
|
<p>{warning}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if checking}
|
{#if checking}
|
||||||
<p class="mb-4 text-sm text-gray-500">Verificando pedidos existentes...</p>
|
<p class="text-sm text-gray-500">Verificando pedidos existentes...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if existingPedidos.length > 0}
|
{#if existingPedidos.length > 0}
|
||||||
<div class="mb-6 rounded border border-yellow-300 bg-yellow-50 p-4">
|
<div class="rounded-lg border border-yellow-300 bg-yellow-50 p-4">
|
||||||
<p class="mb-2 text-sm text-yellow-800">Pedidos similares encontrados:</p>
|
<p class="mb-3 font-semibold text-yellow-900">Pedidos similares encontrados:</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
{#each existingPedidos as pedido (pedido._id)}
|
{#each existingPedidos as pedido (pedido._id)}
|
||||||
<li class="flex flex-col rounded bg-white px-3 py-2 shadow-sm">
|
<li class="flex flex-col rounded-lg bg-white px-4 py-3 shadow-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-medium">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
Pedido {pedido.numeroSei || 'sem número SEI'} — {formatStatus(pedido.status)}
|
Pedido {pedido.numeroSei || 'sem número SEI'} — {formatStatus(pedido.status)}
|
||||||
</div>
|
</p>
|
||||||
|
{#if getMatchingInfo(pedido)}
|
||||||
|
<p class="mt-1 text-xs text-blue-700">
|
||||||
|
{getMatchingInfo(pedido)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href={resolve(`/pedidos/${pedido._id}`)}
|
href={resolve(`/pedidos/${pedido._id}`)}
|
||||||
@@ -356,28 +413,24 @@
|
|||||||
Abrir
|
Abrir
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#if getMatchingInfo(pedido)}
|
|
||||||
<div class="mt-1 text-xs font-semibold text-blue-700">
|
|
||||||
{getMatchingInfo(pedido)}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center justify-end border-t pt-4">
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex items-center justify-end gap-3 border-t pt-6">
|
||||||
<a
|
<a
|
||||||
href={resolve('/pedidos')}
|
href={resolve('/pedidos')}
|
||||||
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
|
class="rounded-lg bg-gray-200 px-6 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={creating || selectedItems.length === 0}
|
disabled={creating || selectedItems.length === 0}
|
||||||
class="focus:shadow-outline rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none disabled:opacity-50"
|
class="rounded-lg bg-blue-600 px-6 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{creating ? 'Criando...' : 'Criar Pedido'}
|
{creating ? 'Criando...' : 'Criar Pedido'}
|
||||||
</button>
|
</button>
|
||||||
@@ -385,92 +438,191 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Configuration Modal -->
|
||||||
{#if showItemModal && itemConfig.objeto}
|
{#if showItemModal && itemConfig.objeto}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||||
>
|
>
|
||||||
<div class="relative w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
||||||
<button
|
<button
|
||||||
onclick={closeItemModal}
|
onclick={closeItemModal}
|
||||||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||||
|
aria-label="Fechar"
|
||||||
>
|
>
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h3 class="mb-4 text-lg font-bold">Configurar Item</h3>
|
|
||||||
<div class="mb-4">
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Configurar Item</h3>
|
||||||
<p class="font-medium">{itemConfig.objeto.nome}</p>
|
|
||||||
<p class="text-sm text-gray-500">Unidade: {itemConfig.objeto.unidade}</p>
|
<div class="mb-6 rounded-lg bg-blue-50 p-4">
|
||||||
|
<p class="font-semibold text-gray-900">{itemConfig.objeto.nome}</p>
|
||||||
|
<p class="text-sm text-gray-600">Unidade: {itemConfig.objeto.unidade}</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
Valor estimado: {itemConfig.objeto.valorEstimado}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="space-y-4">
|
||||||
<label class="mb-1 block text-sm font-bold text-gray-700" for="quantidade">
|
<div>
|
||||||
Quantidade
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="quantidade">
|
||||||
</label>
|
Quantidade
|
||||||
<input
|
</label>
|
||||||
id="quantidade"
|
<input
|
||||||
type="number"
|
id="quantidade"
|
||||||
min="1"
|
type="number"
|
||||||
class="w-full rounded border px-3 py-2"
|
min="1"
|
||||||
bind:value={itemConfig.quantidade}
|
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||||
/>
|
bind:value={itemConfig.quantidade}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label class="mb-1 block text-sm font-bold text-gray-700" for="modalidade">
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="modalidade">
|
||||||
Modalidade
|
Modalidade
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="modalidade"
|
|
||||||
class="w-full rounded border px-3 py-2"
|
|
||||||
bind:value={itemConfig.modalidade}
|
|
||||||
>
|
|
||||||
<option value="consumo">Consumo</option>
|
|
||||||
<option value="dispensa">Dispensa</option>
|
|
||||||
<option value="inexgibilidade">Inexigibilidade</option>
|
|
||||||
<option value="adesao">Adesão</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if availableAtas.length > 0}
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="mb-1 block text-sm font-bold text-gray-700" for="itemAta">
|
|
||||||
Ata de Registro de Preços (Opcional)
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="itemAta"
|
id="modalidade"
|
||||||
class="w-full rounded border px-3 py-2"
|
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||||
bind:value={itemConfig.ataId}
|
bind:value={itemConfig.modalidade}
|
||||||
>
|
>
|
||||||
<option value="">Selecione uma ata...</option>
|
<option value="consumo">Consumo</option>
|
||||||
{#each availableAtas as ata (ata._id)}
|
<option value="dispensa">Dispensa</option>
|
||||||
<option value={ata._id}>Ata {ata.numero} ({ata.numeroSei})</option>
|
<option value="inexgibilidade">Inexigibilidade</option>
|
||||||
|
<option value="adesao">Adesão</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if availableAtas.length > 0}
|
||||||
|
<div class="rounded-lg border-2 border-green-200 bg-green-50 p-4">
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<span class="rounded-full bg-green-600 px-2 py-0.5 text-xs font-bold text-white">
|
||||||
|
{availableAtas.length}
|
||||||
|
{availableAtas.length === 1 ? 'Ata' : 'Atas'}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-green-900">disponível para este objeto</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="itemAta">
|
||||||
|
Selecionar Ata (Opcional)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="itemAta"
|
||||||
|
class="w-full rounded-lg border border-green-300 bg-white px-4 py-2.5 transition focus:border-green-500 focus:ring-2 focus:ring-green-200 focus:outline-none"
|
||||||
|
bind:value={itemConfig.ataId}
|
||||||
|
>
|
||||||
|
<option value="">Nenhuma</option>
|
||||||
|
{#each availableAtas as ata (ata._id)}
|
||||||
|
<option value={ata._id}>Ata {ata.numero} (SEI: {ata.numeroSei})</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-gray-700" for="itemAcao">
|
||||||
|
Ação (Opcional)
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="itemAcao"
|
||||||
|
class="w-full rounded-lg border border-gray-300 px-4 py-2.5 transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
|
||||||
|
bind:value={itemConfig.acaoId}
|
||||||
|
>
|
||||||
|
<option value="">Selecione uma ação...</option>
|
||||||
|
{#each acoes as acao (acao._id)}
|
||||||
|
<option value={acao._id}>{acao.nome}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="mb-1 block text-sm font-bold text-gray-700" for="itemAcao">
|
|
||||||
Ação (Opcional)
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="itemAcao"
|
|
||||||
class="w-full rounded border px-3 py-2"
|
|
||||||
bind:value={itemConfig.acaoId}
|
|
||||||
>
|
|
||||||
<option value="">Selecione uma ação...</option>
|
|
||||||
{#each acoes as acao (acao._id)}
|
|
||||||
<option value={acao._id}>{acao.nome}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
onclick={confirmAddItem}
|
type="button"
|
||||||
class="rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700"
|
onclick={closeItemModal}
|
||||||
|
class="rounded-lg bg-gray-200 px-5 py-2.5 font-semibold text-gray-800 transition hover:bg-gray-300"
|
||||||
>
|
>
|
||||||
Adicionar
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={confirmAddItem}
|
||||||
|
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Adicionar Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Details Modal -->
|
||||||
|
{#if showDetailsModal && detailsItem}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
|
||||||
|
>
|
||||||
|
<div class="relative w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl">
|
||||||
|
<button
|
||||||
|
onclick={closeDetails}
|
||||||
|
class="absolute top-4 right-4 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
|
||||||
|
aria-label="Fechar"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 class="mb-4 text-xl font-bold text-gray-900">Detalhes do Item</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="rounded-lg bg-gray-50 p-4">
|
||||||
|
<h4 class="mb-2 font-semibold text-gray-800">Objeto</h4>
|
||||||
|
<p class="text-gray-700"><strong>Nome:</strong> {detailsItem.objeto.nome}</p>
|
||||||
|
<p class="text-gray-700"><strong>Unidade:</strong> {detailsItem.objeto.unidade}</p>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<strong>Valor Estimado:</strong>
|
||||||
|
{detailsItem.objeto.valorEstimado}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-gray-50 p-4">
|
||||||
|
<h4 class="mb-2 font-semibold text-gray-800">Pedido</h4>
|
||||||
|
<p class="text-gray-700"><strong>Quantidade:</strong> {detailsItem.quantidade}</p>
|
||||||
|
<p class="text-gray-700"><strong>Modalidade:</strong> {detailsItem.modalidade}</p>
|
||||||
|
{#if detailsItem.acaoId}
|
||||||
|
<p class="text-gray-700">
|
||||||
|
<strong>Ação:</strong>
|
||||||
|
{getAcaoNome(detailsItem.acaoId)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if detailsItem.ata}
|
||||||
|
<div class="rounded-lg border border-green-100 bg-green-50 p-4">
|
||||||
|
<h4 class="mb-2 font-semibold text-green-900">Ata de Registro de Preços</h4>
|
||||||
|
<p class="text-green-800"><strong>Número:</strong> {detailsItem.ata.numero}</p>
|
||||||
|
<p class="text-green-800">
|
||||||
|
<strong>Processo SEI:</strong>
|
||||||
|
{detailsItem.ata.numeroSei}
|
||||||
|
</p>
|
||||||
|
{#if detailsItem.ata.dataInicio}
|
||||||
|
<p class="text-green-800">
|
||||||
|
<strong>Vigência:</strong>
|
||||||
|
{detailsItem.ata.dataInicio} até {detailsItem.ata.dataFim || 'Indefinido'}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="rounded-lg bg-gray-50 p-4">
|
||||||
|
<p class="text-sm text-gray-500 italic">Nenhuma Ata vinculada a este item.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={closeDetails}
|
||||||
|
class="rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow-md transition hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { mutation, query } from './_generated/server';
|
import { mutation, query } from './_generated/server';
|
||||||
|
import type { Id } from './_generated/dataModel';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
|
|
||||||
export const list = query({
|
export const list = query({
|
||||||
@@ -16,6 +17,47 @@ export const get = query({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getObjetos = query({
|
||||||
|
args: { id: v.id('atas') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const links = await ctx.db
|
||||||
|
.query('atasObjetos')
|
||||||
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const objetos = await Promise.all(links.map((link) => ctx.db.get(link.objetoId)));
|
||||||
|
return objetos.filter((obj) => obj !== null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listByObjetoIds = query({
|
||||||
|
args: {
|
||||||
|
objetoIds: v.array(v.id('objetos'))
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
if (args.objetoIds.length === 0) return [];
|
||||||
|
|
||||||
|
// Buscar todos os vínculos ata-objeto para os objetos informados
|
||||||
|
const links = [];
|
||||||
|
for (const objetoId of args.objetoIds) {
|
||||||
|
const partial = await ctx.db
|
||||||
|
.query('atasObjetos')
|
||||||
|
.withIndex('by_objetoId', (q) => q.eq('objetoId', objetoId as Id<'objetos'>))
|
||||||
|
.collect();
|
||||||
|
links.push(...partial);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ataIds = Array.from(
|
||||||
|
new Set(links.map((l) => l.ataId as Id<'atas'>))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ataIds.length === 0) return [];
|
||||||
|
|
||||||
|
const atas = await Promise.all(ataIds.map((id) => ctx.db.get(id)));
|
||||||
|
return atas.filter((a): a is NonNullable<typeof a> => a !== null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const create = mutation({
|
export const create = mutation({
|
||||||
args: {
|
args: {
|
||||||
numero: v.string(),
|
numero: v.string(),
|
||||||
@@ -23,18 +65,35 @@ export const create = mutation({
|
|||||||
dataFim: v.optional(v.string()),
|
dataFim: v.optional(v.string()),
|
||||||
empresaId: v.id('empresas'),
|
empresaId: v.id('empresas'),
|
||||||
pdf: v.optional(v.string()),
|
pdf: v.optional(v.string()),
|
||||||
numeroSei: v.string()
|
numeroSei: v.string(),
|
||||||
|
objetosIds: v.optional(v.array(v.id('objetos')))
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await getCurrentUserFunction(ctx);
|
const user = await getCurrentUserFunction(ctx);
|
||||||
if (!user) throw new Error('Unauthorized');
|
if (!user) throw new Error('Unauthorized');
|
||||||
|
|
||||||
return await ctx.db.insert('atas', {
|
const ataId = await ctx.db.insert('atas', {
|
||||||
...args,
|
numero: args.numero,
|
||||||
|
dataInicio: args.dataInicio,
|
||||||
|
dataFim: args.dataFim,
|
||||||
|
empresaId: args.empresaId,
|
||||||
|
pdf: args.pdf,
|
||||||
|
numeroSei: args.numeroSei,
|
||||||
criadoPor: user._id,
|
criadoPor: user._id,
|
||||||
criadoEm: Date.now(),
|
criadoEm: Date.now(),
|
||||||
atualizadoEm: Date.now()
|
atualizadoEm: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (args.objetosIds) {
|
||||||
|
for (const objetoId of args.objetosIds) {
|
||||||
|
await ctx.db.insert('atasObjetos', {
|
||||||
|
ataId,
|
||||||
|
objetoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ataId;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,7 +105,8 @@ export const update = mutation({
|
|||||||
dataFim: v.optional(v.string()),
|
dataFim: v.optional(v.string()),
|
||||||
empresaId: v.id('empresas'),
|
empresaId: v.id('empresas'),
|
||||||
pdf: v.optional(v.string()),
|
pdf: v.optional(v.string()),
|
||||||
numeroSei: v.string()
|
numeroSei: v.string(),
|
||||||
|
objetosIds: v.optional(v.array(v.id('objetos')))
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await getCurrentUserFunction(ctx);
|
const user = await getCurrentUserFunction(ctx);
|
||||||
@@ -61,6 +121,26 @@ export const update = mutation({
|
|||||||
numeroSei: args.numeroSei,
|
numeroSei: args.numeroSei,
|
||||||
atualizadoEm: Date.now()
|
atualizadoEm: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (args.objetosIds !== undefined) {
|
||||||
|
// Remove existing links
|
||||||
|
const existingLinks = await ctx.db
|
||||||
|
.query('atasObjetos')
|
||||||
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const link of existingLinks) {
|
||||||
|
await ctx.db.delete(link._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new links
|
||||||
|
for (const objetoId of args.objetosIds) {
|
||||||
|
await ctx.db.insert('atasObjetos', {
|
||||||
|
ataId: args.id,
|
||||||
|
objetoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,6 +152,16 @@ export const remove = mutation({
|
|||||||
const user = await getCurrentUserFunction(ctx);
|
const user = await getCurrentUserFunction(ctx);
|
||||||
if (!user) throw new Error('Unauthorized');
|
if (!user) throw new Error('Unauthorized');
|
||||||
|
|
||||||
|
// Remove linked objects
|
||||||
|
const links = await ctx.db
|
||||||
|
.query('atasObjetos')
|
||||||
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
await ctx.db.delete(link._id);
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.delete(args.id);
|
await ctx.db.delete(args.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -498,6 +498,71 @@ export const removeItem = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateItem = mutation({
|
||||||
|
args: {
|
||||||
|
itemId: v.id('objetoItems'),
|
||||||
|
valorEstimado: v.string(),
|
||||||
|
modalidade: v.union(
|
||||||
|
v.literal('dispensa'),
|
||||||
|
v.literal('inexgibilidade'),
|
||||||
|
v.literal('adesao'),
|
||||||
|
v.literal('consumo')
|
||||||
|
),
|
||||||
|
acaoId: v.optional(v.id('acoes')),
|
||||||
|
ataId: v.optional(v.id('atas'))
|
||||||
|
},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
if (!user.funcionarioId) {
|
||||||
|
throw new Error('Usuário não vinculado a um funcionário.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await ctx.db.get(args.itemId);
|
||||||
|
if (!item) throw new Error('Item não encontrado.');
|
||||||
|
|
||||||
|
// Apenas quem adicionou o item pode editá-lo
|
||||||
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
||||||
|
if (!isOwner) {
|
||||||
|
throw new Error('Apenas quem adicionou este item pode editá-lo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldValues = {
|
||||||
|
valorEstimado: item.valorEstimado,
|
||||||
|
modalidade: item.modalidade,
|
||||||
|
acaoId: 'acaoId' in item ? item.acaoId : undefined,
|
||||||
|
ataId: 'ataId' in item ? item.ataId : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await ctx.db.patch(args.itemId, {
|
||||||
|
valorEstimado: args.valorEstimado,
|
||||||
|
modalidade: args.modalidade,
|
||||||
|
acaoId: args.acaoId,
|
||||||
|
ataId: args.ataId
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.patch(item.pedidoId, { atualizadoEm: Date.now() });
|
||||||
|
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: item.pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'edicao_item',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
objetoId: item.objetoId,
|
||||||
|
de: oldValues,
|
||||||
|
para: {
|
||||||
|
valorEstimado: args.valorEstimado,
|
||||||
|
modalidade: args.modalidade,
|
||||||
|
acaoId: args.acaoId,
|
||||||
|
ataId: args.ataId
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const updateStatus = mutation({
|
export const updateStatus = mutation({
|
||||||
args: {
|
args: {
|
||||||
pedidoId: v.id('pedidos'),
|
pedidoId: v.id('pedidos'),
|
||||||
|
|||||||
Reference in New Issue
Block a user