feat: implement filtering and document management features in dashboard components, enhancing user experience with improved query capabilities and UI for managing documents in pedidos and compras
This commit is contained in:
@@ -8,7 +8,21 @@
|
|||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Reactive queries
|
// Reactive queries
|
||||||
const atasQuery = useQuery(api.atas.list, {});
|
// Filtros (listagem)
|
||||||
|
let filtroPeriodoInicio = $state('');
|
||||||
|
let filtroPeriodoFim = $state('');
|
||||||
|
let filtroNumero = $state('');
|
||||||
|
let filtroNumeroSei = $state('');
|
||||||
|
|
||||||
|
const atasTotalQuery = useQuery(api.atas.list, {});
|
||||||
|
let atasTotal = $derived(atasTotalQuery.data || []);
|
||||||
|
|
||||||
|
const atasQuery = useQuery(api.atas.list, () => ({
|
||||||
|
periodoInicio: filtroPeriodoInicio || undefined,
|
||||||
|
periodoFim: filtroPeriodoFim || undefined,
|
||||||
|
numero: filtroNumero.trim() || undefined,
|
||||||
|
numeroSei: filtroNumeroSei.trim() || undefined
|
||||||
|
}));
|
||||||
let atas = $derived(atasQuery.data || []);
|
let atas = $derived(atasQuery.data || []);
|
||||||
let loadingAtas = $derived(atasQuery.isLoading);
|
let loadingAtas = $derived(atasQuery.isLoading);
|
||||||
let errorAtas = $derived(atasQuery.error?.message || null);
|
let errorAtas = $derived(atasQuery.error?.message || null);
|
||||||
@@ -188,6 +202,13 @@
|
|||||||
attachmentFiles = Array.from(input.files);
|
attachmentFiles = Array.from(input.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function limparFiltros() {
|
||||||
|
filtroPeriodoInicio = '';
|
||||||
|
filtroPeriodoFim = '';
|
||||||
|
filtroNumero = '';
|
||||||
|
filtroNumeroSei = '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
||||||
@@ -220,6 +241,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100/90 border-base-300 mb-6 border shadow-xl backdrop-blur-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_numero">
|
||||||
|
<span class="label-text font-semibold">Número</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_numero"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ex.: 12/2025"
|
||||||
|
bind:value={filtroNumero}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_numeroSei">
|
||||||
|
<span class="label-text font-semibold">Número SEI</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_numeroSei"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ex.: 12345.000000/2025-00"
|
||||||
|
bind:value={filtroNumeroSei}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_inicio">
|
||||||
|
<span class="label-text font-semibold">Período (início)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_inicio"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="date"
|
||||||
|
bind:value={filtroPeriodoInicio}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_fim">
|
||||||
|
<span class="label-text font-semibold">Período (fim)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_fim"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="date"
|
||||||
|
bind:value={filtroPeriodoFim}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-3 sm:min-w-max sm:justify-end">
|
||||||
|
<div class="text-base-content/70 text-sm">
|
||||||
|
{atas.length} de {atasTotal.length}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost" onclick={limparFiltros}>Limpar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loadingAtas}
|
{#if loadingAtas}
|
||||||
<div class="flex items-center justify-center py-10">
|
<div class="flex items-center justify-center py-10">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
@@ -258,8 +344,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="py-12 text-center">
|
<td colspan="5" class="py-12 text-center">
|
||||||
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
||||||
<p class="text-lg font-semibold">Nenhuma ata cadastrada</p>
|
{#if atasTotal.length === 0}
|
||||||
<p class="text-sm">Clique em “Nova Ata” para cadastrar.</p>
|
<p class="text-lg font-semibold">Nenhuma ata cadastrada</p>
|
||||||
|
<p class="text-sm">Clique em “Nova Ata” para cadastrar.</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-lg font-semibold">Nenhum resultado encontrado</p>
|
||||||
|
<p class="text-sm">Ajuste ou limpe os filtros para ver resultados.</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -9,7 +9,19 @@
|
|||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Reactive queries
|
// Reactive queries
|
||||||
const objetosQuery = useQuery(api.objetos.list, {});
|
// Filtros (listagem)
|
||||||
|
let filtroNome = $state('');
|
||||||
|
let filtroTipo = $state<'todos' | 'material' | 'servico'>('todos');
|
||||||
|
let filtroCodigos = $state('');
|
||||||
|
|
||||||
|
const objetosTotalQuery = useQuery(api.objetos.list, {});
|
||||||
|
let objetosTotal = $derived(objetosTotalQuery.data || []);
|
||||||
|
|
||||||
|
const objetosQuery = useQuery(api.objetos.list, () => ({
|
||||||
|
nome: filtroNome.trim() || undefined,
|
||||||
|
tipo: filtroTipo === 'todos' ? undefined : filtroTipo,
|
||||||
|
codigos: filtroCodigos.trim() || undefined
|
||||||
|
}));
|
||||||
let objetos = $derived(objetosQuery.data || []);
|
let objetos = $derived(objetosQuery.data || []);
|
||||||
let loading = $derived(objetosQuery.isLoading);
|
let loading = $derived(objetosQuery.isLoading);
|
||||||
let error = $derived(objetosQuery.error?.message || null);
|
let error = $derived(objetosQuery.error?.message || null);
|
||||||
@@ -115,6 +127,12 @@
|
|||||||
formData.atas = [...formData.atas, ataId];
|
formData.atas = [...formData.atas, ataId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function limparFiltros() {
|
||||||
|
filtroNome = '';
|
||||||
|
filtroTipo = 'todos';
|
||||||
|
filtroCodigos = '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
||||||
@@ -147,6 +165,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100/90 border-base-300 mb-6 border shadow-xl backdrop-blur-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_nome">
|
||||||
|
<span class="label-text font-semibold">Nome</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_nome"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Digite para filtrar..."
|
||||||
|
bind:value={filtroNome}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_tipo">
|
||||||
|
<span class="label-text font-semibold">Tipo</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="filtro_tipo"
|
||||||
|
class="select select-bordered focus:select-primary w-full"
|
||||||
|
bind:value={filtroTipo}
|
||||||
|
>
|
||||||
|
<option value="todos">Todos</option>
|
||||||
|
<option value="material">Material</option>
|
||||||
|
<option value="servico">Serviço</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="filtro_codigos">
|
||||||
|
<span class="label-text font-semibold">Códigos</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="filtro_codigos"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder="Efisco / Catmat / Catserv"
|
||||||
|
bind:value={filtroCodigos}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-3 sm:min-w-max sm:justify-end">
|
||||||
|
<div class="text-base-content/70 text-sm">
|
||||||
|
{objetos.length} de {objetosTotal.length}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost" onclick={limparFiltros}>Limpar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="flex items-center justify-center py-10">
|
<div class="flex items-center justify-center py-10">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
@@ -185,8 +259,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="py-12 text-center">
|
<td colspan="5" class="py-12 text-center">
|
||||||
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
<div class="text-base-content/60 flex flex-col items-center gap-2">
|
||||||
<p class="text-lg font-semibold">Nenhum objeto cadastrado</p>
|
{#if objetosTotal.length === 0}
|
||||||
<p class="text-sm">Clique em “Novo Objeto” para cadastrar.</p>
|
<p class="text-lg font-semibold">Nenhum objeto cadastrado</p>
|
||||||
|
<p class="text-sm">Clique em “Novo Objeto” para cadastrar.</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-lg font-semibold">Nenhum resultado encontrado</p>
|
||||||
|
<p class="text-sm">Ajuste ou limpe os filtros para ver resultados.</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
Edit,
|
Edit,
|
||||||
Eye,
|
Eye,
|
||||||
Plus,
|
Plus,
|
||||||
|
Upload,
|
||||||
Save,
|
Save,
|
||||||
Send,
|
Send,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -34,6 +35,9 @@
|
|||||||
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
|
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
|
||||||
const permissionsQuery = $derived.by(() => useQuery(api.pedidos.getPermissions, { pedidoId }));
|
const permissionsQuery = $derived.by(() => useQuery(api.pedidos.getPermissions, { pedidoId }));
|
||||||
const requestsQuery = $derived.by(() => useQuery(api.pedidos.getItemRequests, { pedidoId }));
|
const requestsQuery = $derived.by(() => useQuery(api.pedidos.getItemRequests, { pedidoId }));
|
||||||
|
const pedidoDocumentosQuery = $derived.by(() =>
|
||||||
|
useQuery(api.pedidos.listPedidoDocumentos, { pedidoId })
|
||||||
|
);
|
||||||
|
|
||||||
// Derived state
|
// Derived state
|
||||||
let pedido = $derived(pedidoQuery.data);
|
let pedido = $derived(pedidoQuery.data);
|
||||||
@@ -43,11 +47,19 @@
|
|||||||
let acoes = $derived(acoesQuery.data || []);
|
let acoes = $derived(acoesQuery.data || []);
|
||||||
let permissions = $derived(permissionsQuery.data);
|
let permissions = $derived(permissionsQuery.data);
|
||||||
let requests = $derived(requestsQuery.data || []);
|
let requests = $derived(requestsQuery.data || []);
|
||||||
|
let pedidoDocumentos = $derived(pedidoDocumentosQuery.data || []);
|
||||||
|
|
||||||
let currentFuncionarioId = $derived(permissions?.currentFuncionarioId ?? null);
|
let currentFuncionarioId = $derived(permissions?.currentFuncionarioId ?? null);
|
||||||
|
|
||||||
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
|
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
|
||||||
|
|
||||||
|
function coerceModalidade(value: string): Modalidade {
|
||||||
|
if (value === 'dispensa' || value === 'inexgibilidade' || value === 'adesao' || value === 'consumo') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 'consumo';
|
||||||
|
}
|
||||||
|
|
||||||
type EditingItem = {
|
type EditingItem = {
|
||||||
valorEstimado: string;
|
valorEstimado: string;
|
||||||
modalidade: Modalidade;
|
modalidade: Modalidade;
|
||||||
@@ -125,7 +137,8 @@
|
|||||||
objetosQuery.isLoading ||
|
objetosQuery.isLoading ||
|
||||||
acoesQuery.isLoading ||
|
acoesQuery.isLoading ||
|
||||||
permissionsQuery.isLoading ||
|
permissionsQuery.isLoading ||
|
||||||
requestsQuery.isLoading
|
requestsQuery.isLoading ||
|
||||||
|
pedidoDocumentosQuery.isLoading
|
||||||
);
|
);
|
||||||
|
|
||||||
let error = $derived(
|
let error = $derived(
|
||||||
@@ -137,6 +150,213 @@
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Documentos do Pedido
|
||||||
|
let showAddPedidoDocumento = $state(false);
|
||||||
|
let pedidoDocumentoDescricao = $state('');
|
||||||
|
let pedidoDocumentoFile = $state<File | null>(null);
|
||||||
|
let salvandoPedidoDocumento = $state(false);
|
||||||
|
|
||||||
|
function formatBytes(bytes: number) {
|
||||||
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let value = bytes;
|
||||||
|
let idx = 0;
|
||||||
|
while (value >= 1024 && idx < units.length - 1) {
|
||||||
|
value /= 1024;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadToStorage(uploadUrl: string, file: File): Promise<Id<'_storage'>> {
|
||||||
|
const result = await fetch(uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': file.type },
|
||||||
|
body: file
|
||||||
|
});
|
||||||
|
const json = (await result.json()) as { storageId: Id<'_storage'> };
|
||||||
|
return json.storageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddPedidoDocumento() {
|
||||||
|
if (!pedido) return;
|
||||||
|
if (!pedidoDocumentoFile) {
|
||||||
|
toast.error('Selecione um arquivo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pedidoDocumentoDescricao.trim()) {
|
||||||
|
toast.error('Informe uma descrição.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
salvandoPedidoDocumento = true;
|
||||||
|
try {
|
||||||
|
const uploadUrl = await client.mutation(api.pedidos.generatePedidoUploadUrl, { pedidoId });
|
||||||
|
const storageId = await uploadToStorage(uploadUrl, pedidoDocumentoFile);
|
||||||
|
await client.mutation(api.pedidos.addPedidoDocumento, {
|
||||||
|
pedidoId,
|
||||||
|
descricao: pedidoDocumentoDescricao.trim(),
|
||||||
|
nome: pedidoDocumentoFile.name,
|
||||||
|
storageId,
|
||||||
|
tipo: pedidoDocumentoFile.type,
|
||||||
|
tamanho: pedidoDocumentoFile.size
|
||||||
|
});
|
||||||
|
|
||||||
|
pedidoDocumentoDescricao = '';
|
||||||
|
pedidoDocumentoFile = null;
|
||||||
|
showAddPedidoDocumento = false;
|
||||||
|
toast.success('Documento anexado ao pedido.');
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('Erro ao anexar documento: ' + (e as Error).message);
|
||||||
|
} finally {
|
||||||
|
salvandoPedidoDocumento = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenPedidoDocumento(url: string | null) {
|
||||||
|
if (!url) return;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemovePedidoDocumento(id: Id<'pedidoDocumentos'>) {
|
||||||
|
openConfirm(
|
||||||
|
'Remover documento',
|
||||||
|
'Tem certeza que deseja remover este documento do pedido?',
|
||||||
|
async () => {
|
||||||
|
await client.mutation(api.pedidos.removePedidoDocumento, { id });
|
||||||
|
toast.success('Documento removido.');
|
||||||
|
},
|
||||||
|
{ isDestructive: true, confirmText: 'Remover' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documentos por Solicitação
|
||||||
|
let showSolicitacaoDocsModal = $state(false);
|
||||||
|
let solicitacaoDocsRequestId = $state<Id<'solicitacoesItens'> | null>(null);
|
||||||
|
let solicitacaoDocsSolicitadoPor = $state<Id<'funcionarios'> | null>(null);
|
||||||
|
let solicitacaoDocsTipo = $state<string | null>(null);
|
||||||
|
let solicitacaoDocs = $state<any[]>([]);
|
||||||
|
let carregandoSolicitacaoDocs = $state(false);
|
||||||
|
|
||||||
|
let solicitacaoDocumentoDescricao = $state('');
|
||||||
|
let solicitacaoDocumentoFile = $state<File | null>(null);
|
||||||
|
let salvandoSolicitacaoDocumento = $state(false);
|
||||||
|
|
||||||
|
const canAddDocsToSelectedRequest = $derived(
|
||||||
|
showSolicitacaoDocsModal &&
|
||||||
|
solicitacaoDocsTipo === 'adicao' &&
|
||||||
|
currentFuncionarioId !== null &&
|
||||||
|
solicitacaoDocsSolicitadoPor !== null &&
|
||||||
|
currentFuncionarioId === solicitacaoDocsSolicitadoPor
|
||||||
|
);
|
||||||
|
|
||||||
|
async function openSolicitacaoDocs(req: {
|
||||||
|
_id: Id<'solicitacoesItens'>;
|
||||||
|
solicitadoPor: Id<'funcionarios'>;
|
||||||
|
tipo: string;
|
||||||
|
}) {
|
||||||
|
solicitacaoDocsRequestId = req._id;
|
||||||
|
solicitacaoDocsSolicitadoPor = req.solicitadoPor;
|
||||||
|
solicitacaoDocsTipo = req.tipo;
|
||||||
|
solicitacaoDocumentoDescricao = '';
|
||||||
|
solicitacaoDocumentoFile = null;
|
||||||
|
showSolicitacaoDocsModal = true;
|
||||||
|
|
||||||
|
carregandoSolicitacaoDocs = true;
|
||||||
|
try {
|
||||||
|
solicitacaoDocs = await client.query(api.pedidos.listSolicitacaoDocumentos, {
|
||||||
|
requestId: req._id
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('Erro ao carregar documentos da solicitação: ' + (e as Error).message);
|
||||||
|
solicitacaoDocs = [];
|
||||||
|
} finally {
|
||||||
|
carregandoSolicitacaoDocs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSolicitacaoDocs() {
|
||||||
|
showSolicitacaoDocsModal = false;
|
||||||
|
solicitacaoDocsRequestId = null;
|
||||||
|
solicitacaoDocsSolicitadoPor = null;
|
||||||
|
solicitacaoDocsTipo = null;
|
||||||
|
solicitacaoDocs = [];
|
||||||
|
solicitacaoDocumentoDescricao = '';
|
||||||
|
solicitacaoDocumentoFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenSolicitacaoDocumento(url: string | null) {
|
||||||
|
if (!url) return;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSolicitacaoDocs() {
|
||||||
|
if (!solicitacaoDocsRequestId) return;
|
||||||
|
carregandoSolicitacaoDocs = true;
|
||||||
|
try {
|
||||||
|
solicitacaoDocs = await client.query(api.pedidos.listSolicitacaoDocumentos, {
|
||||||
|
requestId: solicitacaoDocsRequestId
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
carregandoSolicitacaoDocs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddSolicitacaoDocumento() {
|
||||||
|
if (!solicitacaoDocsRequestId) return;
|
||||||
|
|
||||||
|
if (!canAddDocsToSelectedRequest) {
|
||||||
|
toast.error('Apenas quem criou a solicitação pode anexar documentos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!solicitacaoDocumentoFile) {
|
||||||
|
toast.error('Selecione um arquivo.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!solicitacaoDocumentoDescricao.trim()) {
|
||||||
|
toast.error('Informe uma descrição.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
salvandoSolicitacaoDocumento = true;
|
||||||
|
try {
|
||||||
|
const uploadUrl = await client.mutation(api.pedidos.generateSolicitacaoUploadUrl, {
|
||||||
|
requestId: solicitacaoDocsRequestId
|
||||||
|
});
|
||||||
|
const storageId = await uploadToStorage(uploadUrl, solicitacaoDocumentoFile);
|
||||||
|
await client.mutation(api.pedidos.addSolicitacaoDocumento, {
|
||||||
|
requestId: solicitacaoDocsRequestId,
|
||||||
|
descricao: solicitacaoDocumentoDescricao.trim(),
|
||||||
|
nome: solicitacaoDocumentoFile.name,
|
||||||
|
storageId,
|
||||||
|
tipo: solicitacaoDocumentoFile.type,
|
||||||
|
tamanho: solicitacaoDocumentoFile.size
|
||||||
|
});
|
||||||
|
|
||||||
|
solicitacaoDocumentoDescricao = '';
|
||||||
|
solicitacaoDocumentoFile = null;
|
||||||
|
toast.success('Documento anexado à solicitação.');
|
||||||
|
await refreshSolicitacaoDocs();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('Erro ao anexar documento: ' + (e as Error).message);
|
||||||
|
} finally {
|
||||||
|
salvandoSolicitacaoDocumento = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveSolicitacaoDocumento(id: Id<'solicitacoesItensDocumentos'>) {
|
||||||
|
openConfirm(
|
||||||
|
'Remover documento',
|
||||||
|
'Tem certeza que deseja remover este documento da solicitação?',
|
||||||
|
async () => {
|
||||||
|
await client.mutation(api.pedidos.removeSolicitacaoDocumento, { id });
|
||||||
|
toast.success('Documento removido.');
|
||||||
|
await refreshSolicitacaoDocs();
|
||||||
|
},
|
||||||
|
{ isDestructive: true, confirmText: 'Remover' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add Item State
|
// Add Item State
|
||||||
let showAddItem = $state(false);
|
let showAddItem = $state(false);
|
||||||
let newItem = $state({
|
let newItem = $state({
|
||||||
@@ -1001,6 +1221,142 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Documentos do Pedido -->
|
||||||
|
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
||||||
|
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||||
|
<h2 class="text-lg font-semibold">Documentos do Pedido</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (showAddPedidoDocumento = !showAddPedidoDocumento)}
|
||||||
|
class="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<Plus size={16} /> Adicionar documento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showAddPedidoDocumento}
|
||||||
|
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-500" for="pedido-doc-desc"
|
||||||
|
>Descrição</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="pedido-doc-desc"
|
||||||
|
type="text"
|
||||||
|
bind:value={pedidoDocumentoDescricao}
|
||||||
|
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||||||
|
placeholder="Ex: Cotação, Parecer, Termo de referência..."
|
||||||
|
disabled={salvandoPedidoDocumento}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-500" for="pedido-doc-file"
|
||||||
|
>Arquivo</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="pedido-doc-file"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
class="w-full text-sm"
|
||||||
|
disabled={salvandoPedidoDocumento}
|
||||||
|
onchange={(e) => {
|
||||||
|
const f = e.currentTarget.files?.[0] ?? null;
|
||||||
|
pedidoDocumentoFile = f;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
showAddPedidoDocumento = false;
|
||||||
|
pedidoDocumentoDescricao = '';
|
||||||
|
pedidoDocumentoFile = null;
|
||||||
|
}}
|
||||||
|
class="rounded bg-gray-200 px-3 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||||||
|
disabled={salvandoPedidoDocumento}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={handleAddPedidoDocumento}
|
||||||
|
disabled={salvandoPedidoDocumento}
|
||||||
|
class="flex items-center gap-2 rounded bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{#if salvandoPedidoDocumento}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Upload size={16} />
|
||||||
|
{/if}
|
||||||
|
Anexar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
{#if pedidoDocumentos.length === 0}
|
||||||
|
<p class="text-sm text-gray-500">Nenhum documento anexado ao pedido.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-sm text-gray-600">
|
||||||
|
<thead class="bg-gray-50 text-xs font-medium text-gray-500 uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2">Descrição</th>
|
||||||
|
<th class="px-4 py-2">Arquivo</th>
|
||||||
|
<th class="px-4 py-2">Tamanho</th>
|
||||||
|
<th class="px-4 py-2">Enviado por</th>
|
||||||
|
<th class="px-4 py-2">Data</th>
|
||||||
|
<th class="px-4 py-2 text-right">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{#each pedidoDocumentos as doc (doc._id)}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-2">{doc.descricao}</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<span class="font-medium text-gray-900">{doc.nome}</span>
|
||||||
|
{#if doc.origemSolicitacaoId}
|
||||||
|
<span class="ml-2 text-xs text-gray-400"> (origem: solicitação)</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2">{formatBytes(doc.tamanho)}</td>
|
||||||
|
<td class="px-4 py-2">{doc.criadoPorNome ?? 'Desconhecido'}</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{new Date(doc.criadoEm).toLocaleString('pt-BR')}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mr-2 rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
|
||||||
|
title="Ver documento"
|
||||||
|
onclick={() => handleOpenPedidoDocumento(doc.url)}
|
||||||
|
disabled={!doc.url}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
|
||||||
|
title="Remover documento"
|
||||||
|
onclick={() => handleRemovePedidoDocumento(doc._id)}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Requests Section -->
|
<!-- Requests Section -->
|
||||||
{#if requests.length > 0}
|
{#if requests.length > 0}
|
||||||
<div class="mb-6 overflow-hidden rounded-lg border-l-4 border-yellow-400 bg-white shadow-md">
|
<div class="mb-6 overflow-hidden rounded-lg border-l-4 border-yellow-400 bg-white shadow-md">
|
||||||
@@ -1064,24 +1420,53 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-right">
|
<td class="px-4 py-2 text-right">
|
||||||
{#if permissions?.canManageRequests}
|
<div class="flex justify-end gap-2">
|
||||||
|
{#if req.tipo === 'adicao'}
|
||||||
|
{@const canAddDoc =
|
||||||
|
!!currentFuncionarioId && req.solicitadoPor === currentFuncionarioId}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => openSolicitacaoDocs(req)}
|
||||||
|
class="rounded bg-blue-100 p-1 text-blue-700 hover:bg-blue-200"
|
||||||
|
title={canAddDoc
|
||||||
|
? 'Adicionar documento'
|
||||||
|
: 'Apenas quem criou a solicitação pode adicionar documentos'}
|
||||||
|
disabled={!canAddDoc}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={() => handleApproveRequest(req._id)}
|
type="button"
|
||||||
class="mr-2 rounded bg-green-100 p-1 text-green-700 hover:bg-green-200"
|
onclick={() => openSolicitacaoDocs(req)}
|
||||||
title="Aprovar"
|
class="rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
|
||||||
|
title="Ver documentos"
|
||||||
>
|
>
|
||||||
<CheckCircle size={16} />
|
<Eye size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onclick={() => handleRejectRequest(req._id)}
|
{#if permissions?.canManageRequests}
|
||||||
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
|
<button
|
||||||
title="Rejeitar"
|
type="button"
|
||||||
>
|
onclick={() => handleApproveRequest(req._id)}
|
||||||
<XCircle size={16} />
|
class="rounded bg-green-100 p-1 text-green-700 hover:bg-green-200"
|
||||||
</button>
|
title="Aprovar"
|
||||||
{:else}
|
>
|
||||||
<span class="text-xs text-gray-400">Aguardando Análise</span>
|
<CheckCircle size={16} />
|
||||||
{/if}
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => handleRejectRequest(req._id)}
|
||||||
|
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
|
||||||
|
title="Rejeitar"
|
||||||
|
>
|
||||||
|
<XCircle size={16} />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center text-xs text-gray-400">Aguardando Análise</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -1380,7 +1765,7 @@
|
|||||||
setEditingField(
|
setEditingField(
|
||||||
item._id,
|
item._id,
|
||||||
'modalidade',
|
'modalidade',
|
||||||
e.currentTarget.value as Modalidade
|
coerceModalidade(e.currentTarget.value)
|
||||||
);
|
);
|
||||||
void persistItemChanges(item);
|
void persistItemChanges(item);
|
||||||
}}
|
}}
|
||||||
@@ -1698,6 +2083,147 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showSolicitacaoDocsModal && solicitacaoDocsRequestId}
|
||||||
|
<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-3xl rounded-lg bg-white p-6 shadow-xl">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={closeSolicitacaoDocs}
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
||||||
|
aria-label="Fechar"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 class="mb-2 text-lg font-semibold text-gray-900">Documentos da solicitação</h3>
|
||||||
|
<p class="mb-4 text-xs text-gray-500">
|
||||||
|
Solicitação: {solicitacaoDocsRequestId.slice(-6)} — tipo: {solicitacaoDocsTipo}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if canAddDocsToSelectedRequest}
|
||||||
|
<div class="mb-4 rounded-lg border border-blue-100 bg-blue-50 p-4">
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-600" for="req-doc-desc"
|
||||||
|
>Descrição</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="req-doc-desc"
|
||||||
|
type="text"
|
||||||
|
bind:value={solicitacaoDocumentoDescricao}
|
||||||
|
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
|
||||||
|
placeholder="Ex: justificativa, anexo do item, planilha..."
|
||||||
|
disabled={salvandoSolicitacaoDocumento}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs font-medium text-gray-600" for="req-doc-file"
|
||||||
|
>Arquivo</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="req-doc-file"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
class="w-full text-sm"
|
||||||
|
disabled={salvandoSolicitacaoDocumento}
|
||||||
|
onchange={(e) => {
|
||||||
|
const f = e.currentTarget.files?.[0] ?? null;
|
||||||
|
solicitacaoDocumentoFile = f;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={handleAddSolicitacaoDocumento}
|
||||||
|
disabled={salvandoSolicitacaoDocumento}
|
||||||
|
class="flex items-center gap-2 rounded bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{#if salvandoSolicitacaoDocumento}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Upload size={16} />
|
||||||
|
{/if}
|
||||||
|
Anexar documento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if carregandoSolicitacaoDocs}
|
||||||
|
<p class="text-sm text-gray-500">Carregando documentos...</p>
|
||||||
|
{:else if solicitacaoDocs.length === 0}
|
||||||
|
<p class="text-sm text-gray-500">Nenhum documento anexado à solicitação.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left text-sm text-gray-600">
|
||||||
|
<thead class="bg-gray-50 text-xs font-medium text-gray-500 uppercase">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2">Descrição</th>
|
||||||
|
<th class="px-4 py-2">Arquivo</th>
|
||||||
|
<th class="px-4 py-2">Tamanho</th>
|
||||||
|
<th class="px-4 py-2">Enviado por</th>
|
||||||
|
<th class="px-4 py-2">Data</th>
|
||||||
|
<th class="px-4 py-2 text-right">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{#each solicitacaoDocs as doc (doc._id)}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-2">{doc.descricao}</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<span class="font-medium text-gray-900">{doc.nome}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2">{formatBytes(doc.tamanho)}</td>
|
||||||
|
<td class="px-4 py-2">{doc.criadoPorNome ?? 'Desconhecido'}</td>
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{new Date(doc.criadoEm).toLocaleString('pt-BR')}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mr-2 rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
|
||||||
|
title="Ver documento"
|
||||||
|
onclick={() => handleOpenSolicitacaoDocumento(doc.url)}
|
||||||
|
disabled={!doc.url}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
</button>
|
||||||
|
{#if canAddDocsToSelectedRequest}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
|
||||||
|
title="Remover documento"
|
||||||
|
onclick={() => handleRemoveSolicitacaoDocumento(doc._id)}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={closeSolicitacaoDocs}
|
||||||
|
class="rounded bg-gray-200 px-4 py-2 text-sm text-gray-800 hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showRequestAdjustmentsModal}
|
{#if showRequestAdjustmentsModal}
|
||||||
<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"
|
||||||
|
|||||||
@@ -5,13 +5,41 @@ import { getCurrentUserFunction } from './auth';
|
|||||||
import { internal } from './_generated/api';
|
import { internal } from './_generated/api';
|
||||||
|
|
||||||
export const list = query({
|
export const list = query({
|
||||||
args: {},
|
args: {
|
||||||
handler: async (ctx) => {
|
periodoInicio: v.optional(v.string()),
|
||||||
|
periodoFim: v.optional(v.string()),
|
||||||
|
numero: v.optional(v.string()),
|
||||||
|
numeroSei: v.optional(v.string())
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
recurso: 'atas',
|
recurso: 'atas',
|
||||||
acao: 'listar'
|
acao: 'listar'
|
||||||
});
|
});
|
||||||
return await ctx.db.query('atas').collect();
|
|
||||||
|
const numero = args.numero?.trim().toLowerCase();
|
||||||
|
const numeroSei = args.numeroSei?.trim().toLowerCase();
|
||||||
|
const periodoInicio = args.periodoInicio || undefined;
|
||||||
|
const periodoFim = args.periodoFim || undefined;
|
||||||
|
|
||||||
|
const atas = await ctx.db.query('atas').collect();
|
||||||
|
return atas.filter((ata) => {
|
||||||
|
const numeroOk = !numero || (ata.numero || '').toLowerCase().includes(numero);
|
||||||
|
const seiOk = !numeroSei || (ata.numeroSei || '').toLowerCase().includes(numeroSei);
|
||||||
|
|
||||||
|
// Filtro por intervalo (range): retorna atas cuja vigência intersecta o período informado.
|
||||||
|
// Considera datas como strings "YYYY-MM-DD" (lexicograficamente comparáveis).
|
||||||
|
const ataInicio = ata.dataInicio ?? '0000-01-01';
|
||||||
|
const ataFim = ata.dataFim ?? '9999-12-31';
|
||||||
|
|
||||||
|
const periodoOk =
|
||||||
|
(!periodoInicio && !periodoFim) ||
|
||||||
|
(periodoInicio && periodoFim && ataInicio <= periodoFim && ataFim >= periodoInicio) ||
|
||||||
|
(periodoInicio && !periodoFim && ataFim >= periodoInicio) ||
|
||||||
|
(!periodoInicio && periodoFim && ataInicio <= periodoFim);
|
||||||
|
|
||||||
|
return numeroOk && seiOk && periodoOk;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,34 @@ import { mutation, query } from './_generated/server';
|
|||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
|
|
||||||
export const list = query({
|
export const list = query({
|
||||||
args: {},
|
args: {
|
||||||
handler: async (ctx) => {
|
nome: v.optional(v.string()),
|
||||||
return await ctx.db.query('objetos').collect();
|
tipo: v.optional(v.union(v.literal('material'), v.literal('servico'))),
|
||||||
|
codigos: v.optional(v.string())
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const nome = args.nome?.trim();
|
||||||
|
const codigos = args.codigos?.trim().toLowerCase();
|
||||||
|
|
||||||
|
const base =
|
||||||
|
nome && nome.length > 0
|
||||||
|
? await ctx.db
|
||||||
|
.query('objetos')
|
||||||
|
.withSearchIndex('search_nome', (q) => q.search('nome', nome))
|
||||||
|
.collect()
|
||||||
|
: await ctx.db.query('objetos').collect();
|
||||||
|
|
||||||
|
return base.filter((objeto) => {
|
||||||
|
const tipoOk = !args.tipo || objeto.tipo === args.tipo;
|
||||||
|
|
||||||
|
const codigosOk =
|
||||||
|
!codigos ||
|
||||||
|
(objeto.codigoEfisco || '').toLowerCase().includes(codigos) ||
|
||||||
|
(objeto.codigoCatmat || '').toLowerCase().includes(codigos) ||
|
||||||
|
(objeto.codigoCatserv || '').toLowerCase().includes(codigos);
|
||||||
|
|
||||||
|
return tipoOk && codigosOk;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,61 @@ async function ensurePedidoModalidadeAtaConsistency(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isFuncionarioInComprasSector(
|
||||||
|
ctx: QueryCtx | MutationCtx,
|
||||||
|
funcionarioId: Id<'funcionarios'>
|
||||||
|
) {
|
||||||
|
const config = await ctx.db.query('config').first();
|
||||||
|
if (!config || !config.comprasSetorId) return false;
|
||||||
|
|
||||||
|
const isInSector = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
||||||
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return !!isInSector;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isUsuarioEnvolvidoNoPedido(
|
||||||
|
ctx: QueryCtx | MutationCtx,
|
||||||
|
pedidoId: Id<'pedidos'>,
|
||||||
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
||||||
|
) {
|
||||||
|
const pedido = await ctx.db.get(pedidoId);
|
||||||
|
if (!pedido) return false;
|
||||||
|
|
||||||
|
// Criador do pedido (por usuarioId)
|
||||||
|
if (pedido.criadoPor === user._id) return true;
|
||||||
|
|
||||||
|
// Envolvimento por itens (requer funcionarioId)
|
||||||
|
if (!user.funcionarioId) return false;
|
||||||
|
|
||||||
|
const hasItem = await ctx.db
|
||||||
|
.query('objetoItems')
|
||||||
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', pedidoId))
|
||||||
|
.filter((q) => q.eq(q.field('adicionadoPor'), user.funcionarioId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return !!hasItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertPodeGerenciarDocumentosDoPedido(
|
||||||
|
ctx: QueryCtx | MutationCtx,
|
||||||
|
pedidoId: Id<'pedidos'>,
|
||||||
|
user: Awaited<ReturnType<typeof getUsuarioAutenticado>>
|
||||||
|
) {
|
||||||
|
const isEnvolvido = await isUsuarioEnvolvidoNoPedido(ctx, pedidoId, user);
|
||||||
|
if (isEnvolvido) return;
|
||||||
|
|
||||||
|
if (user.funcionarioId) {
|
||||||
|
const isCompras = await isFuncionarioInComprasSector(ctx, user.funcionarioId);
|
||||||
|
if (isCompras) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Acesso negado.');
|
||||||
|
}
|
||||||
|
|
||||||
// ========== QUERIES ==========
|
// ========== QUERIES ==========
|
||||||
|
|
||||||
export const list = query({
|
export const list = query({
|
||||||
@@ -1652,6 +1707,8 @@ export const approveItemRequest = mutation({
|
|||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await getUsuarioAutenticado(ctx);
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
const funcionarioId = user.funcionarioId;
|
||||||
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
|
||||||
const request = await ctx.db.get(args.requestId);
|
const request = await ctx.db.get(args.requestId);
|
||||||
if (!request) throw new Error('Solicitação não encontrada.');
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
@@ -1666,12 +1723,18 @@ export const approveItemRequest = mutation({
|
|||||||
|
|
||||||
const isInSector = await ctx.db
|
const isInSector = await ctx.db
|
||||||
.query('funcionarioSetores')
|
.query('funcionarioSetores')
|
||||||
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
||||||
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!isInSector) throw new Error('Acesso negado.');
|
if (!isInSector) throw new Error('Acesso negado.');
|
||||||
|
|
||||||
|
// Documentos anexados à solicitação (se houver) devem migrar para o pedido ao aprovar.
|
||||||
|
const solicitacaoDocs = await ctx.db
|
||||||
|
.query('solicitacoesItensDocumentos')
|
||||||
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Apply the change
|
// Apply the change
|
||||||
const data = JSON.parse(request.dados);
|
const data = JSON.parse(request.dados);
|
||||||
|
|
||||||
@@ -1790,15 +1853,37 @@ export const approveItemRequest = mutation({
|
|||||||
// I'll delete it to keep table clean as requested.
|
// I'll delete it to keep table clean as requested.
|
||||||
await ctx.db.delete(request._id);
|
await ctx.db.delete(request._id);
|
||||||
|
|
||||||
|
// Migrar docs: cria em pedidoDocumentos e remove os registros da solicitacao
|
||||||
|
const documentosMigrados: Array<{ nome: string; descricao: string }> = [];
|
||||||
|
if (solicitacaoDocs.length > 0) {
|
||||||
|
for (const doc of solicitacaoDocs) {
|
||||||
|
await ctx.db.insert('pedidoDocumentos', {
|
||||||
|
pedidoId: request.pedidoId,
|
||||||
|
descricao: doc.descricao,
|
||||||
|
nome: doc.nome,
|
||||||
|
storageId: doc.storageId,
|
||||||
|
tipo: doc.tipo,
|
||||||
|
tamanho: doc.tamanho,
|
||||||
|
criadoPor: doc.criadoPor,
|
||||||
|
criadoEm: doc.criadoEm,
|
||||||
|
origemSolicitacaoId: request._id
|
||||||
|
});
|
||||||
|
documentosMigrados.push({ nome: doc.nome, descricao: doc.descricao });
|
||||||
|
await ctx.db.delete(doc._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// History
|
// History
|
||||||
await ctx.db.insert('historicoPedidos', {
|
await ctx.db.insert('historicoPedidos', {
|
||||||
pedidoId: request.pedidoId,
|
pedidoId: request.pedidoId,
|
||||||
usuarioId: user._id,
|
usuarioId: user._id,
|
||||||
acao: 'aprovacao_solicitacao',
|
acao: 'aprovacao_solicitacao',
|
||||||
detalhes: JSON.stringify({
|
detalhes: JSON.stringify({
|
||||||
|
requestId: request._id,
|
||||||
tipo: request.tipo,
|
tipo: request.tipo,
|
||||||
solicitadoPor: request.solicitadoPor,
|
solicitadoPor: request.solicitadoPor,
|
||||||
dados: data
|
dados: data,
|
||||||
|
documentosMigrados
|
||||||
}),
|
}),
|
||||||
data: Date.now()
|
data: Date.now()
|
||||||
});
|
});
|
||||||
@@ -1814,6 +1899,8 @@ export const rejectItemRequest = mutation({
|
|||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await getUsuarioAutenticado(ctx);
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
const funcionarioId = user.funcionarioId;
|
||||||
|
if (!funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
const request = await ctx.db.get(args.requestId);
|
const request = await ctx.db.get(args.requestId);
|
||||||
if (!request) throw new Error('Solicitação não encontrada.');
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
|
||||||
@@ -1822,12 +1909,25 @@ export const rejectItemRequest = mutation({
|
|||||||
|
|
||||||
const isInSector = await ctx.db
|
const isInSector = await ctx.db
|
||||||
.query('funcionarioSetores')
|
.query('funcionarioSetores')
|
||||||
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', user.funcionarioId!))
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', funcionarioId))
|
||||||
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
.filter((q) => q.eq(q.field('setorId'), config.comprasSetorId))
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!isInSector) throw new Error('Acesso negado.');
|
if (!isInSector) throw new Error('Acesso negado.');
|
||||||
|
|
||||||
|
// Remover documentos anexados à solicitação (para não deixar órfãos no storage)
|
||||||
|
const solicitacaoDocs = await ctx.db
|
||||||
|
.query('solicitacoesItensDocumentos')
|
||||||
|
.withIndex('by_requestId', (q) => q.eq('requestId', request._id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const documentosRemovidos: Array<{ nome: string; descricao: string }> = [];
|
||||||
|
for (const doc of solicitacaoDocs) {
|
||||||
|
documentosRemovidos.push({ nome: doc.nome, descricao: doc.descricao });
|
||||||
|
await ctx.storage.delete(doc.storageId);
|
||||||
|
await ctx.db.delete(doc._id);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete request
|
// Delete request
|
||||||
await ctx.db.delete(args.requestId);
|
await ctx.db.delete(args.requestId);
|
||||||
|
|
||||||
@@ -1837,10 +1937,230 @@ export const rejectItemRequest = mutation({
|
|||||||
usuarioId: user._id,
|
usuarioId: user._id,
|
||||||
acao: 'rejeicao_solicitacao',
|
acao: 'rejeicao_solicitacao',
|
||||||
detalhes: JSON.stringify({
|
detalhes: JSON.stringify({
|
||||||
|
requestId: request._id,
|
||||||
tipo: request.tipo,
|
tipo: request.tipo,
|
||||||
solicitadoPor: request.solicitadoPor
|
solicitadoPor: request.solicitadoPor,
|
||||||
|
documentosRemovidos
|
||||||
}),
|
}),
|
||||||
data: Date.now()
|
data: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========== DOCUMENTOS (PEDIDO / SOLICITAÇÃO) ==========
|
||||||
|
|
||||||
|
export const generatePedidoUploadUrl = mutation({
|
||||||
|
args: { pedidoId: v.id('pedidos') },
|
||||||
|
returns: v.string(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
||||||
|
return await ctx.storage.generateUploadUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addPedidoDocumento = mutation({
|
||||||
|
args: {
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
descricao: v.string(),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(),
|
||||||
|
tamanho: v.number(),
|
||||||
|
origemSolicitacaoId: v.optional(v.id('solicitacoesItens'))
|
||||||
|
},
|
||||||
|
returns: v.id('pedidoDocumentos'),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
||||||
|
|
||||||
|
if (!user.funcionarioId) {
|
||||||
|
throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garantir que o pedido existe
|
||||||
|
const pedido = await ctx.db.get(args.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
|
return await ctx.db.insert('pedidoDocumentos', {
|
||||||
|
pedidoId: args.pedidoId,
|
||||||
|
descricao: args.descricao,
|
||||||
|
nome: args.nome,
|
||||||
|
storageId: args.storageId,
|
||||||
|
tipo: args.tipo,
|
||||||
|
tamanho: args.tamanho,
|
||||||
|
criadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now(),
|
||||||
|
origemSolicitacaoId: args.origemSolicitacaoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listPedidoDocumentos = query({
|
||||||
|
args: { pedidoId: v.id('pedidos') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
await assertPodeGerenciarDocumentosDoPedido(ctx, args.pedidoId, user);
|
||||||
|
|
||||||
|
const docs = await ctx.db
|
||||||
|
.query('pedidoDocumentos')
|
||||||
|
.withIndex('by_pedidoId', (q) => q.eq('pedidoId', args.pedidoId))
|
||||||
|
.order('desc')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
docs.map(async (doc) => {
|
||||||
|
const url = await ctx.storage.getUrl(doc.storageId);
|
||||||
|
const func = await ctx.db.get(doc.criadoPor);
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
||||||
|
url
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removePedidoDocumento = mutation({
|
||||||
|
args: { id: v.id('pedidoDocumentos') },
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
|
||||||
|
const doc = await ctx.db.get(args.id);
|
||||||
|
if (!doc) throw new Error('Documento não encontrado.');
|
||||||
|
|
||||||
|
// Pode remover se for o autor OU se for envolvido/compras
|
||||||
|
if (!user.funcionarioId || doc.criadoPor !== user.funcionarioId) {
|
||||||
|
await assertPodeGerenciarDocumentosDoPedido(ctx, doc.pedidoId, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.storage.delete(doc.storageId);
|
||||||
|
await ctx.db.delete(doc._id);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateSolicitacaoUploadUrl = mutation({
|
||||||
|
args: { requestId: v.id('solicitacoesItens') },
|
||||||
|
returns: v.string(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
|
||||||
|
const request = await ctx.db.get(args.requestId);
|
||||||
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
if (request.tipo !== 'adicao') {
|
||||||
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
||||||
|
}
|
||||||
|
if (request.status !== 'pendente') {
|
||||||
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
||||||
|
}
|
||||||
|
if (request.solicitadoPor !== user.funcionarioId) {
|
||||||
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ctx.storage.generateUploadUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addSolicitacaoDocumento = mutation({
|
||||||
|
args: {
|
||||||
|
requestId: v.id('solicitacoesItens'),
|
||||||
|
descricao: v.string(),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(),
|
||||||
|
tamanho: v.number()
|
||||||
|
},
|
||||||
|
returns: v.id('solicitacoesItensDocumentos'),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
|
||||||
|
const request = await ctx.db.get(args.requestId);
|
||||||
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
if (request.tipo !== 'adicao') {
|
||||||
|
throw new Error('Apenas solicitações de adição permitem anexar documentos.');
|
||||||
|
}
|
||||||
|
if (request.status !== 'pendente') {
|
||||||
|
throw new Error('Não é possível anexar documentos em uma solicitação não pendente.');
|
||||||
|
}
|
||||||
|
if (request.solicitadoPor !== user.funcionarioId) {
|
||||||
|
throw new Error('Apenas quem criou a solicitação pode anexar documentos.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ctx.db.insert('solicitacoesItensDocumentos', {
|
||||||
|
requestId: args.requestId,
|
||||||
|
pedidoId: request.pedidoId,
|
||||||
|
descricao: args.descricao,
|
||||||
|
nome: args.nome,
|
||||||
|
storageId: args.storageId,
|
||||||
|
tipo: args.tipo,
|
||||||
|
tamanho: args.tamanho,
|
||||||
|
criadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listSolicitacaoDocumentos = query({
|
||||||
|
args: { requestId: v.id('solicitacoesItens') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
const request = await ctx.db.get(args.requestId);
|
||||||
|
if (!request) return [];
|
||||||
|
|
||||||
|
await assertPodeGerenciarDocumentosDoPedido(ctx, request.pedidoId, user);
|
||||||
|
|
||||||
|
const docs = await ctx.db
|
||||||
|
.query('solicitacoesItensDocumentos')
|
||||||
|
.withIndex('by_requestId', (q) => q.eq('requestId', args.requestId))
|
||||||
|
.order('desc')
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
docs.map(async (doc) => {
|
||||||
|
const url = await ctx.storage.getUrl(doc.storageId);
|
||||||
|
const func = await ctx.db.get(doc.criadoPor);
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
criadoPorNome: func?.nome ?? 'Desconhecido',
|
||||||
|
url
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeSolicitacaoDocumento = mutation({
|
||||||
|
args: { id: v.id('solicitacoesItensDocumentos') },
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const user = await getUsuarioAutenticado(ctx);
|
||||||
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
|
|
||||||
|
const doc = await ctx.db.get(args.id);
|
||||||
|
if (!doc) throw new Error('Documento não encontrado.');
|
||||||
|
|
||||||
|
const request = await ctx.db.get(doc.requestId);
|
||||||
|
if (!request) throw new Error('Solicitação não encontrada.');
|
||||||
|
if (request.tipo !== 'adicao') {
|
||||||
|
throw new Error('Apenas solicitações de adição permitem documentos.');
|
||||||
|
}
|
||||||
|
if (request.status !== 'pendente') {
|
||||||
|
throw new Error('Não é possível remover documentos de uma solicitação não pendente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.criadoPor !== user.funcionarioId || request.solicitadoPor !== user.funcionarioId) {
|
||||||
|
throw new Error('Apenas quem criou a solicitação pode remover documentos.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.storage.delete(doc.storageId);
|
||||||
|
await ctx.db.delete(doc._id);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -70,5 +70,37 @@ export const pedidosTables = {
|
|||||||
})
|
})
|
||||||
.index('by_pedidoId', ['pedidoId'])
|
.index('by_pedidoId', ['pedidoId'])
|
||||||
.index('by_usuarioId', ['usuarioId'])
|
.index('by_usuarioId', ['usuarioId'])
|
||||||
.index('by_data', ['data'])
|
.index('by_data', ['data']),
|
||||||
|
|
||||||
|
// Documentos anexados diretamente ao pedido (ilimitado)
|
||||||
|
pedidoDocumentos: defineTable({
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
descricao: v.string(),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(), // MIME type
|
||||||
|
tamanho: v.number(), // bytes
|
||||||
|
criadoPor: v.id('funcionarios'),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
origemSolicitacaoId: v.optional(v.id('solicitacoesItens'))
|
||||||
|
})
|
||||||
|
.index('by_pedidoId', ['pedidoId'])
|
||||||
|
.index('by_criadoPor', ['criadoPor'])
|
||||||
|
.index('by_origemSolicitacaoId', ['origemSolicitacaoId']),
|
||||||
|
|
||||||
|
// Documentos anexados a uma solicitação (somente solicitante; pode ter mais de um)
|
||||||
|
solicitacoesItensDocumentos: defineTable({
|
||||||
|
requestId: v.id('solicitacoesItens'),
|
||||||
|
pedidoId: v.id('pedidos'),
|
||||||
|
descricao: v.string(),
|
||||||
|
nome: v.string(),
|
||||||
|
storageId: v.id('_storage'),
|
||||||
|
tipo: v.string(), // MIME type
|
||||||
|
tamanho: v.number(), // bytes
|
||||||
|
criadoPor: v.id('funcionarios'),
|
||||||
|
criadoEm: v.number()
|
||||||
|
})
|
||||||
|
.index('by_requestId', ['requestId'])
|
||||||
|
.index('by_pedidoId', ['pedidoId'])
|
||||||
|
.index('by_criadoPor', ['criadoPor'])
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user