feat: Implement batch item removal and pedido splitting for pedidos, and add document management for atas.
This commit is contained in:
@@ -37,6 +37,14 @@
|
|||||||
objetos.filter((obj) => obj.nome.toLowerCase().includes(searchObjeto.toLowerCase()))
|
objetos.filter((obj) => obj.nome.toLowerCase().includes(searchObjeto.toLowerCase()))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Document state
|
||||||
|
let mainPdfFile: File | null = $state(null);
|
||||||
|
let attachmentFiles: File[] = $state([]);
|
||||||
|
let attachments = $state<Array<{ _id: Id<'atasDocumentos'>; nome: string; url: string | null }>>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
let uploading = $state(false);
|
||||||
|
|
||||||
async function openModal(ata?: Doc<'atas'>) {
|
async function openModal(ata?: Doc<'atas'>) {
|
||||||
if (ata) {
|
if (ata) {
|
||||||
editingId = ata._id;
|
editingId = ata._id;
|
||||||
@@ -50,6 +58,9 @@
|
|||||||
// Fetch linked objects
|
// Fetch linked objects
|
||||||
const linkedObjetos = await client.query(api.atas.getObjetos, { id: ata._id });
|
const linkedObjetos = await client.query(api.atas.getObjetos, { id: ata._id });
|
||||||
selectedObjetos = linkedObjetos.map((o) => o._id);
|
selectedObjetos = linkedObjetos.map((o) => o._id);
|
||||||
|
|
||||||
|
// Fetch attachments
|
||||||
|
attachments = await client.query(api.atas.getDocumentos, { ataId: ata._id });
|
||||||
} else {
|
} else {
|
||||||
editingId = null;
|
editingId = null;
|
||||||
formData = {
|
formData = {
|
||||||
@@ -60,7 +71,10 @@
|
|||||||
dataFim: ''
|
dataFim: ''
|
||||||
};
|
};
|
||||||
selectedObjetos = [];
|
selectedObjetos = [];
|
||||||
|
attachments = [];
|
||||||
}
|
}
|
||||||
|
mainPdfFile = null;
|
||||||
|
attachmentFiles = [];
|
||||||
searchObjeto = '';
|
searchObjeto = '';
|
||||||
showModal = true;
|
showModal = true;
|
||||||
}
|
}
|
||||||
@@ -78,6 +92,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadFile(file: File) {
|
||||||
|
const uploadUrl = await client.mutation(api.atas.generateUploadUrl, {});
|
||||||
|
const result = await fetch(uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': file.type },
|
||||||
|
body: file
|
||||||
|
});
|
||||||
|
const { storageId } = await result.json();
|
||||||
|
return storageId;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!formData.empresaId) {
|
if (!formData.empresaId) {
|
||||||
@@ -86,28 +111,53 @@
|
|||||||
}
|
}
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
|
let pdfStorageId = undefined;
|
||||||
|
if (mainPdfFile) {
|
||||||
|
pdfStorageId = await uploadFile(mainPdfFile);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
numero: formData.numero,
|
numero: formData.numero,
|
||||||
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
|
objetosIds: selectedObjetos,
|
||||||
|
pdf: pdfStorageId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ataId: Id<'atas'>;
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
await client.mutation(api.atas.update, {
|
await client.mutation(api.atas.update, {
|
||||||
id: editingId as Id<'atas'>,
|
id: editingId as Id<'atas'>,
|
||||||
...payload
|
...payload
|
||||||
});
|
});
|
||||||
|
ataId = editingId as Id<'atas'>;
|
||||||
} else {
|
} else {
|
||||||
await client.mutation(api.atas.create, payload);
|
ataId = await client.mutation(api.atas.create, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload attachments
|
||||||
|
if (attachmentFiles.length > 0) {
|
||||||
|
uploading = true;
|
||||||
|
for (const file of attachmentFiles) {
|
||||||
|
const storageId = await uploadFile(file);
|
||||||
|
await client.mutation(api.atas.saveDocumento, {
|
||||||
|
ataId,
|
||||||
|
nome: file.name,
|
||||||
|
storageId,
|
||||||
|
tipo: file.type,
|
||||||
|
tamanho: file.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao salvar: ' + (e as Error).message);
|
alert('Erro ao salvar: ' + (e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
uploading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +170,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteAttachment(docId: Id<'atasDocumentos'>) {
|
||||||
|
if (!confirm('Tem certeza que deseja excluir este anexo?')) return;
|
||||||
|
try {
|
||||||
|
await client.mutation(api.atas.removeDocumento, { id: docId });
|
||||||
|
// Refresh attachments list
|
||||||
|
if (editingId) {
|
||||||
|
attachments = await client.query(api.atas.getDocumentos, {
|
||||||
|
ataId: editingId as Id<'atas'>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao excluir anexo: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getEmpresaNome(id: Id<'empresas'>) {
|
function getEmpresaNome(id: Id<'empresas'>) {
|
||||||
return empresas.find((e) => e._id === id)?.razao_social || 'Empresa não encontrada';
|
return empresas.find((e) => e._id === id)?.razao_social || 'Empresa não encontrada';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
if (input.files && input.files.length > 0) {
|
||||||
|
mainPdfFile = input.files[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAttachmentsSelect(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
if (input.files && input.files.length > 0) {
|
||||||
|
attachmentFiles = Array.from(input.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
@@ -173,7 +252,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-4 font-medium whitespace-nowrap">{ata.numero}</td>
|
<td class="px-6 py-4 font-medium whitespace-nowrap">{ata.numero}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{ata.numeroSei}</td>
|
<td class="px-6 py-4 whitespace-nowrap">{ata.numeroSei}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{getEmpresaNome(ata.empresaId)}</td>
|
<td
|
||||||
|
class="max-w-md truncate px-6 py-4 whitespace-nowrap"
|
||||||
|
title={getEmpresaNome(ata.empresaId)}
|
||||||
|
>
|
||||||
|
{getEmpresaNome(ata.empresaId)}
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
||||||
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
|
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -209,7 +293,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-2xl rounded-lg bg-white p-8 shadow-xl">
|
<div class="relative my-8 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"
|
||||||
@@ -288,6 +372,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="pdf">
|
||||||
|
PDF da Ata {editingId ? '(Deixe em branco para manter o atual)' : ''}
|
||||||
|
</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="pdf"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onchange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -308,8 +405,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-1 overflow-y-auto rounded border bg-gray-50 p-2"
|
class="mb-4 flex-1 overflow-y-auto rounded border bg-gray-50 p-2"
|
||||||
style="max-height: 300px;"
|
style="max-height: 200px;"
|
||||||
>
|
>
|
||||||
{#if filteredObjetos.length === 0}
|
{#if filteredObjetos.length === 0}
|
||||||
<p class="py-4 text-center text-sm text-gray-500">Nenhum objeto encontrado.</p>
|
<p class="py-4 text-center text-sm text-gray-500">Nenhum objeto encontrado.</p>
|
||||||
@@ -334,9 +431,45 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-gray-500">
|
|
||||||
Selecione os objetos que fazem parte desta Ata.
|
<div class="border-t pt-4">
|
||||||
</p>
|
<label class="mb-2 block text-sm font-bold text-gray-700" for="anexos">
|
||||||
|
Anexos
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="focus:shadow-outline mb-2 w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
||||||
|
id="anexos"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onchange={handleAttachmentsSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if attachments.length > 0}
|
||||||
|
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
||||||
|
{#each attachments as doc (doc._id)}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between rounded bg-gray-100 p-2 text-sm"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={doc.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="max-w-[150px] truncate text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{doc.nome}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => handleDeleteAttachment(doc._id)}
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -350,10 +483,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving || uploading}
|
||||||
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="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"
|
||||||
>
|
>
|
||||||
{saving ? 'Salvando...' : 'Salvar'}
|
{saving || uploading ? 'Salvando...' : 'Salvar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,6 +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 { SvelteSet } from 'svelte/reactivity';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
@@ -15,18 +16,20 @@
|
|||||||
X,
|
X,
|
||||||
XCircle
|
XCircle
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { page } from '$app/stores';
|
import { goto } from '$app/navigation';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { page } from '$app/state';
|
||||||
import { maskCurrencyBRL } from '$lib/utils/masks';
|
import { maskCurrencyBRL } from '$lib/utils/masks';
|
||||||
|
|
||||||
const pedidoId = $page.params.id as Id<'pedidos'>;
|
const pedidoId = $derived(page.params.id as Id<'pedidos'>);
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Reactive queries
|
// Reactive queries
|
||||||
const pedidoQuery = useQuery(api.pedidos.get, { id: pedidoId });
|
const pedidoQuery = $derived.by(() => useQuery(api.pedidos.get, { id: pedidoId }));
|
||||||
const itemsQuery = useQuery(api.pedidos.getItems, { pedidoId });
|
const itemsQuery = $derived.by(() => useQuery(api.pedidos.getItems, { pedidoId }));
|
||||||
const historyQuery = useQuery(api.pedidos.getHistory, { pedidoId });
|
const historyQuery = $derived.by(() => useQuery(api.pedidos.getHistory, { pedidoId }));
|
||||||
const objetosQuery = useQuery(api.objetos.list, {});
|
const objetosQuery = $derived.by(() => useQuery(api.objetos.list, {}));
|
||||||
const acoesQuery = useQuery(api.acoes.list, {});
|
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
|
||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
let pedido = $derived(pedidoQuery.data);
|
let pedido = $derived(pedidoQuery.data);
|
||||||
@@ -35,11 +38,7 @@
|
|||||||
let objetos = $derived(objetosQuery.data || []);
|
let objetos = $derived(objetosQuery.data || []);
|
||||||
let acoes = $derived(acoesQuery.data || []);
|
let acoes = $derived(acoesQuery.data || []);
|
||||||
|
|
||||||
type Modalidade =
|
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
|
||||||
| 'dispensa'
|
|
||||||
| 'inexgibilidade'
|
|
||||||
| 'adesao'
|
|
||||||
| 'consumo';
|
|
||||||
|
|
||||||
type EditingItem = {
|
type EditingItem = {
|
||||||
valorEstimado: string;
|
valorEstimado: string;
|
||||||
@@ -62,6 +61,28 @@
|
|||||||
|
|
||||||
let editingItems = $state<Record<string, EditingItem>>({});
|
let editingItems = $state<Record<string, EditingItem>>({});
|
||||||
|
|
||||||
|
// Seleção de itens para ações em lote
|
||||||
|
let selectedItemIds = new SvelteSet<Id<'objetoItems'>>();
|
||||||
|
|
||||||
|
function isItemSelected(itemId: Id<'objetoItems'>) {
|
||||||
|
return selectedItemIds.has(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleItemSelection(itemId: Id<'objetoItems'>) {
|
||||||
|
if (selectedItemIds.has(itemId)) {
|
||||||
|
selectedItemIds.delete(itemId);
|
||||||
|
} else {
|
||||||
|
selectedItemIds.add(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedItemIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedCount = $derived(selectedItemIds.size);
|
||||||
|
let hasSelection = $derived(selectedCount > 0);
|
||||||
|
|
||||||
// Garante que, para todos os itens existentes, as atas do respectivo objeto
|
// Garante que, para todos os itens existentes, as atas do respectivo objeto
|
||||||
// sejam carregadas independentemente do formulário de criação.
|
// sejam carregadas independentemente do formulário de criação.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -145,7 +166,7 @@
|
|||||||
if (hasAppliedPrefill) return;
|
if (hasAppliedPrefill) return;
|
||||||
if (objetosQuery.isLoading || acoesQuery.isLoading) return;
|
if (objetosQuery.isLoading || acoesQuery.isLoading) return;
|
||||||
|
|
||||||
const url = $page.url;
|
const url = page.url;
|
||||||
const obj = url.searchParams.get('obj');
|
const obj = url.searchParams.get('obj');
|
||||||
const qtdStr = url.searchParams.get('qtd');
|
const qtdStr = url.searchParams.get('qtd');
|
||||||
const mod = url.searchParams.get('mod') as Modalidade | null;
|
const mod = url.searchParams.get('mod') as Modalidade | null;
|
||||||
@@ -496,6 +517,61 @@
|
|||||||
return `${entry.usuarioNome} - ${entry.acao}`;
|
return `${entry.usuarioNome} - ${entry.acao}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemoveSelectedItems() {
|
||||||
|
if (!hasSelection) return;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
selectedCount === 1
|
||||||
|
? 'Remover o item selecionado deste pedido?'
|
||||||
|
: `Remover os ${selectedCount} itens selecionados deste pedido?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const itemIds = Array.from(selectedItemIds) as Id<'objetoItems'>[];
|
||||||
|
await client.mutation(api.pedidos.removeItemsBatch, {
|
||||||
|
itemIds
|
||||||
|
});
|
||||||
|
clearSelection();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao remover itens selecionados: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let showSplitResultModal = $state(false);
|
||||||
|
let novoPedidoIdParaNavegar = $state<Id<'pedidos'> | null>(null);
|
||||||
|
let quantidadeItensMovidos = $state(0);
|
||||||
|
|
||||||
|
// Split Confirmation Modal State
|
||||||
|
let showSplitConfirmationModal = $state(false);
|
||||||
|
let newPedidoSei = $state('');
|
||||||
|
|
||||||
|
function handleSplitPedidoFromSelection() {
|
||||||
|
if (!hasSelection) return;
|
||||||
|
newPedidoSei = '';
|
||||||
|
showSplitConfirmationModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmSplitPedido() {
|
||||||
|
try {
|
||||||
|
const itemIds = Array.from(selectedItemIds) as Id<'objetoItems'>[];
|
||||||
|
const novoPedidoId = await client.mutation(api.pedidos.splitPedido, {
|
||||||
|
pedidoId,
|
||||||
|
itemIds,
|
||||||
|
numeroSei: newPedidoSei.trim() || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
novoPedidoIdParaNavegar = novoPedidoId;
|
||||||
|
quantidadeItensMovidos = itemIds.length;
|
||||||
|
showSplitConfirmationModal = false;
|
||||||
|
showSplitResultModal = true;
|
||||||
|
clearSelection();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Erro ao dividir pedido: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
@@ -729,6 +805,47 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
{#if hasSelection}
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between border-b border-blue-100 bg-blue-50 px-6 py-3 text-sm text-blue-900"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-blue-600 text-xs font-semibold text-white"
|
||||||
|
>
|
||||||
|
{selectedCount}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
>{selectedCount === 1
|
||||||
|
? '1 item selecionado'
|
||||||
|
: `${selectedCount} itens selecionados`}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border border-transparent bg-blue-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm hover:bg-blue-700"
|
||||||
|
onclick={() => handleSplitPedidoFromSelection()}
|
||||||
|
>
|
||||||
|
Criar novo pedido com selecionados
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 hover:bg-red-100"
|
||||||
|
onclick={() => handleRemoveSelectedItems()}
|
||||||
|
>
|
||||||
|
Excluir selecionados
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border border-transparent px-2 py-1 text-xs font-medium text-blue-700 hover:bg-blue-100"
|
||||||
|
onclick={clearSelection}
|
||||||
|
>
|
||||||
|
Limpar seleção
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#each groupedItems as group (group.name)}
|
{#each groupedItems as group (group.name)}
|
||||||
<div class="border-b border-gray-200 bg-gray-100 px-6 py-2 font-medium text-gray-700">
|
<div class="border-b border-gray-200 bg-gray-100 px-6 py-2 font-medium text-gray-700">
|
||||||
Adicionado por: {group.name}
|
Adicionado por: {group.name}
|
||||||
@@ -736,9 +853,31 @@
|
|||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
onchange={(e) => {
|
||||||
|
const checked = e.currentTarget.checked;
|
||||||
|
for (const groupItem of group.items) {
|
||||||
|
if (checked) {
|
||||||
|
selectedItemIds.add(groupItem._id);
|
||||||
|
} else {
|
||||||
|
selectedItemIds.delete(groupItem._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={`Selecionar todos os itens de ${group.name}`}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
<th
|
<th
|
||||||
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"
|
||||||
>Objeto</th
|
>
|
||||||
|
Objeto</th
|
||||||
>
|
>
|
||||||
<th
|
<th
|
||||||
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"
|
||||||
@@ -772,7 +911,18 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
{#each group.items as item (item._id)}
|
{#each group.items as item (item._id)}
|
||||||
<tr>
|
<tr class:selected={isItemSelected(item._id)}>
|
||||||
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
checked={isItemSelected(item._id)}
|
||||||
|
onchange={() => toggleItemSelection(item._id)}
|
||||||
|
aria-label={`Selecionar item ${getObjetoName(item.objetoId)}`}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
||||||
@@ -864,8 +1014,7 @@
|
|||||||
<option value={ata._id}>Ata {ata.numero} (SEI: {ata.numeroSei})</option>
|
<option value={ata._id}>Ata {ata.numero} (SEI: {ata.numeroSei})</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
{:else if item.ataId}
|
||||||
{#if item.ataId}
|
|
||||||
{#each getAtasForObjeto(item.objetoId) as ata (ata._id)}
|
{#each getAtasForObjeto(item.objetoId) as ata (ata._id)}
|
||||||
{#if ata._id === item.ataId}
|
{#if ata._id === item.ataId}
|
||||||
Ata {ata.numero}
|
Ata {ata.numero}
|
||||||
@@ -874,7 +1023,6 @@
|
|||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/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)
|
||||||
@@ -971,9 +1119,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="block text-xs font-bold text-gray-500 uppercase"
|
<div class="block text-xs font-bold text-gray-500 uppercase">
|
||||||
>Valor Estimado (Unitário)</div
|
Valor Estimado (Unitário)
|
||||||
>
|
</div>
|
||||||
<p class="text-gray-900">
|
<p class="text-gray-900">
|
||||||
{maskCurrencyBRL(selectedObjeto.valorEstimado || '') || 'R$ 0,00'}
|
{maskCurrencyBRL(selectedObjeto.valorEstimado || '') || 'R$ 0,00'}
|
||||||
</p>
|
</p>
|
||||||
@@ -1026,4 +1174,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showSplitResultModal && novoPedidoIdParaNavegar}
|
||||||
|
<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-md rounded-lg bg-white p-6 shadow-xl">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
showSplitResultModal = false;
|
||||||
|
}}
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">Novo pedido criado</h2>
|
||||||
|
<p class="mb-4 text-sm text-gray-700">
|
||||||
|
{quantidadeItensMovidos === 1
|
||||||
|
? '1 item foi movido para um novo pedido em rascunho.'
|
||||||
|
: `${quantidadeItensMovidos} itens foram movidos para um novo pedido em rascunho.`}
|
||||||
|
</p>
|
||||||
|
<p class="mb-6 text-xs text-gray-500">
|
||||||
|
Os itens não foram copiados, e sim movidos deste pedido para o novo.
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-gray-200 px-3 py-1.5 text-xs font-medium text-gray-800 hover:bg-gray-300"
|
||||||
|
onclick={() => {
|
||||||
|
showSplitResultModal = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continuar neste pedido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
|
||||||
|
onclick={async () => {
|
||||||
|
const id = novoPedidoIdParaNavegar;
|
||||||
|
showSplitResultModal = false;
|
||||||
|
if (id) {
|
||||||
|
await goto(resolve(`/pedidos/${id}`));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ir para o novo pedido
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showSplitConfirmationModal}
|
||||||
|
<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-md rounded-lg bg-white p-6 shadow-xl">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
showSplitConfirmationModal = false;
|
||||||
|
}}
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">Criar novo pedido</h2>
|
||||||
|
<p class="mb-4 text-sm text-gray-700">
|
||||||
|
{selectedCount === 1
|
||||||
|
? 'Criar um novo pedido movendo o item selecionado para ele?'
|
||||||
|
: `Criar um novo pedido movendo os ${selectedCount} itens selecionados para ele?`}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="new-sei" class="mb-1 block text-sm font-medium text-gray-700"
|
||||||
|
>Número SEI (Opcional)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="new-sei"
|
||||||
|
type="text"
|
||||||
|
bind:value={newPedidoSei}
|
||||||
|
class="w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||||
|
placeholder="Ex: 12345.000000/2023-00"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
Se deixado em branco, o novo pedido será criado sem número SEI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-gray-200 px-3 py-1.5 text-xs font-medium text-gray-800 hover:bg-gray-300"
|
||||||
|
onclick={() => {
|
||||||
|
showSplitConfirmationModal = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
|
||||||
|
onclick={confirmSplitPedido}
|
||||||
|
>
|
||||||
|
Confirmar e Criar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
|
|
||||||
let selectedItems = $state<SelectedItem[]>([]);
|
let selectedItems = $state<SelectedItem[]>([]);
|
||||||
let selectedObjetoIds = $derived(selectedItems.map((i) => i.objeto._id));
|
let selectedObjetoIds = $derived(selectedItems.map((i) => i.objeto._id));
|
||||||
|
let hasMixedModalidades = $derived(
|
||||||
|
new Set(selectedItems.map((i) => i.modalidade)).size > 1
|
||||||
|
);
|
||||||
|
|
||||||
// Item configuration modal
|
// Item configuration modal
|
||||||
let showItemModal = $state(false);
|
let showItemModal = $state(false);
|
||||||
@@ -153,6 +156,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatModalidade(modalidade: SelectedItem['modalidade']) {
|
||||||
|
switch (modalidade) {
|
||||||
|
case 'consumo':
|
||||||
|
return 'Consumo';
|
||||||
|
case 'dispensa':
|
||||||
|
return 'Dispensa';
|
||||||
|
case 'inexgibilidade':
|
||||||
|
return 'Inexigibilidade';
|
||||||
|
case 'adesao':
|
||||||
|
return 'Adesão';
|
||||||
|
default:
|
||||||
|
return modalidade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModalidadeBadgeClasses(modalidade: SelectedItem['modalidade']) {
|
||||||
|
switch (modalidade) {
|
||||||
|
case 'consumo':
|
||||||
|
return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'dispensa':
|
||||||
|
return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'inexgibilidade':
|
||||||
|
return 'bg-purple-100 text-purple-800';
|
||||||
|
case 'adesao':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAcaoNome(acaoId: Id<'acoes'> | undefined) {
|
function getAcaoNome(acaoId: Id<'acoes'> | undefined) {
|
||||||
if (!acaoId) return '-';
|
if (!acaoId) return '-';
|
||||||
const acao = acoes.find((a) => a._id === acaoId);
|
const acao = acoes.find((a) => a._id === acaoId);
|
||||||
@@ -172,7 +205,8 @@
|
|||||||
.map((match) => {
|
.map((match) => {
|
||||||
// Find name from selected items (might be multiple with same object, just pick one name)
|
// Find name from selected items (might be multiple with same object, just pick one name)
|
||||||
const item = selectedItems.find((p) => p.objeto._id === match.objetoId);
|
const item = selectedItems.find((p) => p.objeto._id === match.objetoId);
|
||||||
return `${item?.objeto.nome}: ${match.quantidade} un.`;
|
const modalidadeLabel = formatModalidade(match.modalidade);
|
||||||
|
return `${item?.objeto.nome} (${modalidadeLabel}): ${match.quantidade} un.`;
|
||||||
})
|
})
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
@@ -226,6 +260,8 @@
|
|||||||
|
|
||||||
checking = true;
|
checking = true;
|
||||||
try {
|
try {
|
||||||
|
// Importante: ação (acaoId) NÃO entra no filtro de similaridade.
|
||||||
|
// O filtro considera apenas combinação de objeto + modalidade.
|
||||||
const itensFiltro =
|
const itensFiltro =
|
||||||
selectedItems.length > 0
|
selectedItems.length > 0
|
||||||
? selectedItems.map((item) => ({
|
? selectedItems.map((item) => ({
|
||||||
@@ -255,6 +291,11 @@
|
|||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (hasMixedModalidades) {
|
||||||
|
error =
|
||||||
|
'Não é possível criar o pedido com itens de modalidades diferentes. Ajuste os itens antes de continuar.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
creating = true;
|
creating = true;
|
||||||
error = null;
|
error = null;
|
||||||
try {
|
try {
|
||||||
@@ -369,32 +410,44 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each selectedItems as item, index (index)}
|
{#each selectedItems as item, index (index)}
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 transition hover:shadow-md"
|
class="rounded-xl 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 gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1 space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<p class="font-semibold text-gray-900">{item.objeto.nome}</p>
|
<p class="font-semibold text-gray-900">{item.objeto.nome}</p>
|
||||||
|
<span
|
||||||
|
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${getModalidadeBadgeClasses(
|
||||||
|
item.modalidade
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{formatModalidade(item.modalidade)}
|
||||||
|
</span>
|
||||||
{#if item.ataNumero}
|
{#if item.ataNumero}
|
||||||
<span
|
<span
|
||||||
class="rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"
|
class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"
|
||||||
>
|
>
|
||||||
Ata {item.ataNumero}
|
Ata {item.ataNumero}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/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}
|
{#if item.acaoId}
|
||||||
<span><strong>Ação:</strong> {getAcaoNome(item.acaoId)}</span>
|
<span
|
||||||
|
class="inline-flex items-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800"
|
||||||
|
>
|
||||||
|
Ação: {getAcaoNome(item.acaoId)}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-600">
|
||||||
|
<span>
|
||||||
|
<strong>Qtd:</strong> {item.quantidade} {item.objeto.unidade}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 flex items-center gap-2">
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded p-2 text-blue-600 transition hover:bg-blue-50"
|
class="rounded-lg p-2 text-blue-600 transition hover:bg-blue-50"
|
||||||
onclick={() => openDetails(item)}
|
onclick={() => openDetails(item)}
|
||||||
aria-label="Ver detalhes"
|
aria-label="Ver detalhes"
|
||||||
>
|
>
|
||||||
@@ -402,7 +455,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded p-2 text-red-600 transition hover:bg-red-50"
|
class="rounded-lg p-2 text-red-600 transition hover:bg-red-50"
|
||||||
onclick={() => removeItem(index)}
|
onclick={() => removeItem(index)}
|
||||||
aria-label="Remover item"
|
aria-label="Remover item"
|
||||||
>
|
>
|
||||||
@@ -424,6 +477,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Warnings Section -->
|
<!-- Warnings Section -->
|
||||||
|
{#if hasMixedModalidades}
|
||||||
|
<div
|
||||||
|
class="mb-3 rounded-lg border border-red-400 bg-red-50 px-4 py-3 text-sm text-red-800"
|
||||||
|
>
|
||||||
|
<p class="font-semibold">Modalidades diferentes detectadas</p>
|
||||||
|
<p>
|
||||||
|
Não é possível criar o pedido com itens de modalidades diferentes. Ajuste os itens para
|
||||||
|
usar uma única modalidade.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if warning}
|
{#if warning}
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border border-yellow-400 bg-yellow-50 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"
|
||||||
@@ -443,11 +508,25 @@
|
|||||||
<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-lg bg-white px-4 py-3 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 gap-3">
|
||||||
<div>
|
<div class="space-y-1">
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<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)}
|
||||||
</p>
|
</p>
|
||||||
|
{#if getFirstMatchingSelectedItem(pedido)}
|
||||||
|
{#key pedido._id}
|
||||||
|
{#if getFirstMatchingSelectedItem(pedido)}
|
||||||
|
<span
|
||||||
|
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${getModalidadeBadgeClasses(
|
||||||
|
getFirstMatchingSelectedItem(pedido).modalidade
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
Modalidade:{' '}
|
||||||
|
{formatModalidade(getFirstMatchingSelectedItem(pedido).modalidade)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
{#if getMatchingInfo(pedido)}
|
{#if getMatchingInfo(pedido)}
|
||||||
<p class="mt-1 text-xs text-blue-700">
|
<p class="mt-1 text-xs text-blue-700">
|
||||||
{getMatchingInfo(pedido)}
|
{getMatchingInfo(pedido)}
|
||||||
@@ -477,7 +556,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={creating || selectedItems.length === 0}
|
disabled={creating || selectedItems.length === 0 || hasMixedModalidades}
|
||||||
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"
|
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'}
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ export const listByObjetoIds = query({
|
|||||||
links.push(...partial);
|
links.push(...partial);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ataIds = Array.from(
|
const ataIds = Array.from(new Set(links.map((l) => l.ataId as Id<'atas'>)));
|
||||||
new Set(links.map((l) => l.ataId as Id<'atas'>))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ataIds.length === 0) return [];
|
if (ataIds.length === 0) return [];
|
||||||
|
|
||||||
@@ -64,9 +62,9 @@ export const create = mutation({
|
|||||||
dataInicio: v.optional(v.string()),
|
dataInicio: v.optional(v.string()),
|
||||||
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.id('_storage')),
|
||||||
numeroSei: v.string(),
|
numeroSei: v.string(),
|
||||||
objetosIds: v.optional(v.array(v.id('objetos')))
|
objetosIds: v.array(v.id('objetos'))
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await getCurrentUserFunction(ctx);
|
const user = await getCurrentUserFunction(ctx);
|
||||||
@@ -74,24 +72,23 @@ export const create = mutation({
|
|||||||
|
|
||||||
const ataId = await ctx.db.insert('atas', {
|
const ataId = await ctx.db.insert('atas', {
|
||||||
numero: args.numero,
|
numero: args.numero,
|
||||||
|
numeroSei: args.numeroSei,
|
||||||
|
empresaId: args.empresaId,
|
||||||
dataInicio: args.dataInicio,
|
dataInicio: args.dataInicio,
|
||||||
dataFim: args.dataFim,
|
dataFim: args.dataFim,
|
||||||
empresaId: args.empresaId,
|
|
||||||
pdf: args.pdf,
|
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) {
|
// Vincular objetos
|
||||||
for (const objetoId of args.objetosIds) {
|
for (const objetoId of args.objetosIds) {
|
||||||
await ctx.db.insert('atasObjetos', {
|
await ctx.db.insert('atasObjetos', {
|
||||||
ataId,
|
ataId,
|
||||||
objetoId
|
objetoId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return ataId;
|
return ataId;
|
||||||
}
|
}
|
||||||
@@ -101,12 +98,12 @@ export const update = mutation({
|
|||||||
args: {
|
args: {
|
||||||
id: v.id('atas'),
|
id: v.id('atas'),
|
||||||
numero: v.string(),
|
numero: v.string(),
|
||||||
|
numeroSei: v.string(),
|
||||||
|
empresaId: v.id('empresas'),
|
||||||
dataInicio: v.optional(v.string()),
|
dataInicio: v.optional(v.string()),
|
||||||
dataFim: v.optional(v.string()),
|
dataFim: v.optional(v.string()),
|
||||||
empresaId: v.id('empresas'),
|
pdf: v.optional(v.id('_storage')),
|
||||||
pdf: v.optional(v.string()),
|
objetosIds: v.array(v.id('objetos'))
|
||||||
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);
|
||||||
@@ -114,16 +111,16 @@ export const update = mutation({
|
|||||||
|
|
||||||
await ctx.db.patch(args.id, {
|
await ctx.db.patch(args.id, {
|
||||||
numero: args.numero,
|
numero: args.numero,
|
||||||
|
numeroSei: args.numeroSei,
|
||||||
|
empresaId: args.empresaId,
|
||||||
dataInicio: args.dataInicio,
|
dataInicio: args.dataInicio,
|
||||||
dataFim: args.dataFim,
|
dataFim: args.dataFim,
|
||||||
empresaId: args.empresaId,
|
|
||||||
pdf: args.pdf,
|
pdf: args.pdf,
|
||||||
numeroSei: args.numeroSei,
|
|
||||||
atualizadoEm: Date.now()
|
atualizadoEm: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (args.objetosIds !== undefined) {
|
// Atualizar objetos vinculados
|
||||||
// Remove existing links
|
// Primeiro remove todos os vínculos existentes
|
||||||
const existingLinks = await ctx.db
|
const existingLinks = await ctx.db
|
||||||
.query('atasObjetos')
|
.query('atasObjetos')
|
||||||
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
@@ -133,7 +130,7 @@ export const update = mutation({
|
|||||||
await ctx.db.delete(link._id);
|
await ctx.db.delete(link._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new links
|
// Adiciona os novos vínculos
|
||||||
for (const objetoId of args.objetosIds) {
|
for (const objetoId of args.objetosIds) {
|
||||||
await ctx.db.insert('atasObjetos', {
|
await ctx.db.insert('atasObjetos', {
|
||||||
ataId: args.id,
|
ataId: args.id,
|
||||||
@@ -141,18 +138,15 @@ export const update = mutation({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const remove = mutation({
|
export const remove = mutation({
|
||||||
args: {
|
args: { id: v.id('atas') },
|
||||||
id: v.id('atas')
|
|
||||||
},
|
|
||||||
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');
|
||||||
|
|
||||||
// Remove linked objects
|
// Remover vínculos com objetos
|
||||||
const links = await ctx.db
|
const links = await ctx.db
|
||||||
.query('atasObjetos')
|
.query('atasObjetos')
|
||||||
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
@@ -162,6 +156,79 @@ export const remove = mutation({
|
|||||||
await ctx.db.delete(link._id);
|
await ctx.db.delete(link._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remover documentos vinculados
|
||||||
|
const docs = await ctx.db
|
||||||
|
.query('atasDocumentos')
|
||||||
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
await ctx.storage.delete(doc.storageId);
|
||||||
|
await ctx.db.delete(doc._id);
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.delete(args.id);
|
await ctx.db.delete(args.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const generateUploadUrl = mutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return await ctx.storage.generateUploadUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveDocumento = mutation({
|
||||||
|
args: {
|
||||||
|
ataId: v.id('atas'),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(),
|
||||||
|
tamanho: v.number()
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getCurrentUserFunction(ctx);
|
||||||
|
if (!user) throw new Error('Unauthorized');
|
||||||
|
|
||||||
|
return await ctx.db.insert('atasDocumentos', {
|
||||||
|
ataId: args.ataId,
|
||||||
|
nome: args.nome,
|
||||||
|
storageId: args.storageId,
|
||||||
|
tipo: args.tipo,
|
||||||
|
tamanho: args.tamanho,
|
||||||
|
criadoPor: user._id,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeDocumento = mutation({
|
||||||
|
args: { id: v.id('atasDocumentos') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getCurrentUserFunction(ctx);
|
||||||
|
if (!user) throw new Error('Unauthorized');
|
||||||
|
|
||||||
|
const doc = await ctx.db.get(args.id);
|
||||||
|
if (!doc) throw new Error('Documento não encontrado');
|
||||||
|
|
||||||
|
await ctx.storage.delete(doc.storageId);
|
||||||
|
await ctx.db.delete(args.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDocumentos = query({
|
||||||
|
args: { ataId: v.id('atas') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const docs = await ctx.db
|
||||||
|
.query('atasDocumentos')
|
||||||
|
.withIndex('by_ataId', (q) => q.eq('ataId', args.ataId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
docs.map(async (doc) => ({
|
||||||
|
...doc,
|
||||||
|
url: await ctx.storage.getUrl(doc.storageId)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -238,9 +238,7 @@ export const checkExisting = query({
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
const matching = items.filter((i) =>
|
const matching = items.filter((i) =>
|
||||||
itensFiltro.some(
|
itensFiltro.some((f) => f.objetoId === i.objetoId && f.modalidade === i.modalidade)
|
||||||
(f) => f.objetoId === i.objetoId && f.modalidade === i.modalidade
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
@@ -513,6 +511,159 @@ export const removeItem = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const removeItemsBatch = mutation({
|
||||||
|
args: {
|
||||||
|
itemIds: v.array(v.id('objetoItems'))
|
||||||
|
},
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.itemIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstItem = await ctx.db.get(args.itemIds[0]);
|
||||||
|
if (!firstItem) {
|
||||||
|
throw new Error('Item não encontrado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pedidoId = firstItem.pedidoId;
|
||||||
|
const pedido = await ctx.db.get(pedidoId);
|
||||||
|
if (!pedido) {
|
||||||
|
throw new Error('Pedido não encontrado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pedido.status !== 'em_rascunho' && pedido.status !== 'precisa_ajustes') {
|
||||||
|
throw new Error(
|
||||||
|
'Só é possível remover itens em pedidos em rascunho ou que precisam de ajustes.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const itemId of args.itemIds) {
|
||||||
|
const item = await ctx.db.get(itemId);
|
||||||
|
if (!item) continue;
|
||||||
|
|
||||||
|
if (item.pedidoId !== pedidoId) {
|
||||||
|
throw new Error('Todos os itens devem pertencer ao mesmo pedido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.adicionadoPor !== user.funcionarioId) {
|
||||||
|
throw new Error('Você só pode remover itens que você adicionou.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.delete(itemId);
|
||||||
|
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'remocao_item',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
objetoId: item.objetoId,
|
||||||
|
valor: item.valorEstimado
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.patch(pedidoId, { atualizadoEm: Date.now() });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const splitPedido = mutation({
|
||||||
|
args: {
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
itemIds: v.array(v.id('objetoItems')),
|
||||||
|
numeroSei: v.optional(v.string())
|
||||||
|
},
|
||||||
|
returns: v.id('pedidos'),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
if (!user.funcionarioId) {
|
||||||
|
throw new Error('Usuário não vinculado a um funcionário.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.itemIds.length === 0) {
|
||||||
|
throw new Error('Selecione ao menos um item para dividir o pedido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pedidoOriginal = await ctx.db.get(args.pedidoId);
|
||||||
|
if (!pedidoOriginal) {
|
||||||
|
throw new Error('Pedido não encontrado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pedidoOriginal.status !== 'em_rascunho' && pedidoOriginal.status !== 'precisa_ajustes') {
|
||||||
|
throw new Error('Só é possível dividir pedidos em rascunho ou que precisam de ajustes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const itens = [];
|
||||||
|
for (const itemId of args.itemIds) {
|
||||||
|
const item = await ctx.db.get(itemId);
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.pedidoId !== args.pedidoId) {
|
||||||
|
throw new Error('Todos os itens devem pertencer ao mesmo pedido.');
|
||||||
|
}
|
||||||
|
if (item.adicionadoPor !== user.funcionarioId) {
|
||||||
|
throw new Error('Você só pode mover itens que você adicionou.');
|
||||||
|
}
|
||||||
|
itens.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itens.length === 0) {
|
||||||
|
throw new Error('Nenhum dos itens selecionados pôde ser usado para divisão.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const novoPedidoId = await ctx.db.insert('pedidos', {
|
||||||
|
numeroSei: args.numeroSei,
|
||||||
|
status: 'em_rascunho',
|
||||||
|
criadoPor: user._id,
|
||||||
|
criadoEm: Date.now(),
|
||||||
|
atualizadoEm: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of itens) {
|
||||||
|
await ctx.db.patch(item._id, {
|
||||||
|
pedidoId: novoPedidoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: args.pedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'divisao_pedido_origem',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
itensMovidos: itens.map((i) => i._id),
|
||||||
|
novoPedidoId
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.insert('historicoPedidos', {
|
||||||
|
pedidoId: novoPedidoId,
|
||||||
|
usuarioId: user._id,
|
||||||
|
acao: 'divisao_pedido_destino',
|
||||||
|
detalhes: JSON.stringify({
|
||||||
|
pedidoOriginalId: args.pedidoId,
|
||||||
|
itensRecebidos: itens.map((i) => i._id)
|
||||||
|
}),
|
||||||
|
data: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.patch(args.pedidoId, { atualizadoEm: Date.now() });
|
||||||
|
|
||||||
|
return novoPedidoId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const updateItem = mutation({
|
export const updateItem = mutation({
|
||||||
args: {
|
args: {
|
||||||
itemId: v.id('objetoItems'),
|
itemId: v.id('objetoItems'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const atasTables = {
|
|||||||
dataInicio: v.optional(v.string()),
|
dataInicio: v.optional(v.string()),
|
||||||
dataFim: v.optional(v.string()),
|
dataFim: v.optional(v.string()),
|
||||||
empresaId: v.id('empresas'),
|
empresaId: v.id('empresas'),
|
||||||
pdf: v.optional(v.string()), // storage ID
|
pdf: v.optional(v.id('_storage')),
|
||||||
numeroSei: v.string(),
|
numeroSei: v.string(),
|
||||||
criadoPor: v.id('usuarios'),
|
criadoPor: v.id('usuarios'),
|
||||||
criadoEm: v.number(),
|
criadoEm: v.number(),
|
||||||
@@ -22,5 +22,15 @@ export const atasTables = {
|
|||||||
objetoId: v.id('objetos')
|
objetoId: v.id('objetos')
|
||||||
})
|
})
|
||||||
.index('by_ataId', ['ataId'])
|
.index('by_ataId', ['ataId'])
|
||||||
.index('by_objetoId', ['objetoId'])
|
.index('by_objetoId', ['objetoId']),
|
||||||
|
|
||||||
|
atasDocumentos: defineTable({
|
||||||
|
ataId: v.id('atas'),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(), // MIME type
|
||||||
|
tamanho: v.number(), // bytes
|
||||||
|
criadoPor: v.id('usuarios'),
|
||||||
|
criadoEm: v.number()
|
||||||
|
}).index('by_ataId', ['ataId'])
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user