feat: implement advanced filtering and reporting features for pedidos, including status selection, date range filtering, and export options for PDF and XLSX formats

This commit is contained in:
2025-12-15 15:37:57 -03:00
parent c7b4ea15bd
commit fd2669aa4f
5 changed files with 1342 additions and 44 deletions

View File

@@ -1,12 +1,119 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import { useQuery } from 'convex-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { exportarRelatorioPedidosXLSX } from '$lib/utils/pedidos/relatorioPedidosExcel';
import { gerarRelatorioPedidosPDF } from '$lib/utils/pedidos/relatorioPedidosPDF';
import { useConvexClient, useQuery } from 'convex-svelte';
import { endOfDay, startOfDay } from 'date-fns';
import { Eye, Plus } from 'lucide-svelte';
import { resolve } from '$app/paths';
const client = useConvexClient();
const statusOptions = [
{ value: 'em_rascunho', label: 'Rascunho' },
{ value: 'aguardando_aceite', label: 'Aguardando Aceite' },
{ value: 'em_analise', label: 'Em Análise' },
{ value: 'precisa_ajustes', label: 'Precisa de Ajustes' },
{ value: 'concluido', label: 'Concluído' },
{ value: 'cancelado', label: 'Cancelado' }
] as const;
type PedidoStatus = (typeof statusOptions)[number]['value'];
// Filtros (cumulativos / backend)
let filtroNumeroSei = $state('');
let filtroCriadoPor = $state<Id<'usuarios'> | ''>('');
let filtroAceitoPor = $state<Id<'funcionarios'> | ''>('');
let filtroInicio = $state(''); // yyyy-MM-dd
let filtroFim = $state(''); // yyyy-MM-dd
let statusSelected = $state<Record<PedidoStatus, boolean>>({
em_rascunho: false,
aguardando_aceite: false,
em_analise: false,
precisa_ajustes: false,
concluido: false,
cancelado: false
});
function getSelectedStatuses(): PedidoStatus[] | undefined {
const selected = (Object.entries(statusSelected) as Array<[PedidoStatus, boolean]>)
.filter(([, v]) => v)
.map(([k]) => k);
return selected.length > 0 ? selected : undefined;
}
function getPeriodoInicio(): number | undefined {
if (!filtroInicio) return undefined;
return startOfDay(new Date(`${filtroInicio}T00:00:00`)).getTime();
}
function getPeriodoFim(): number | undefined {
if (!filtroFim) return undefined;
return endOfDay(new Date(`${filtroFim}T23:59:59`)).getTime();
}
function limparFiltros() {
filtroNumeroSei = '';
filtroCriadoPor = '';
filtroAceitoPor = '';
filtroInicio = '';
filtroFim = '';
(Object.keys(statusSelected) as PedidoStatus[]).forEach((k) => (statusSelected[k] = false));
}
const usuariosQuery = useQuery(api.usuarios.listar, { ativo: true });
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
const filtroArgs = () => ({
statuses: getSelectedStatuses(),
numeroSei: filtroNumeroSei.trim() || undefined,
criadoPor: filtroCriadoPor ? filtroCriadoPor : undefined,
aceitoPor: filtroAceitoPor ? filtroAceitoPor : undefined,
periodoInicio: getPeriodoInicio(),
periodoFim: getPeriodoFim()
});
let generatingPDF = $state(false);
let generatingXLSX = $state(false);
async function gerarPDF() {
if (!filtroInicio && !filtroFim) {
alert('Informe um período (início e/ou fim) para gerar o relatório.');
return;
}
try {
generatingPDF = true;
const relatorio = await client.query(api.pedidos.gerarRelatorio, filtroArgs());
gerarRelatorioPedidosPDF(relatorio);
} catch (e) {
console.error('Erro ao gerar relatório PDF:', e);
alert('Erro ao gerar relatório PDF. Tente novamente.');
} finally {
generatingPDF = false;
}
}
async function exportarXLSX() {
if (!filtroInicio && !filtroFim) {
alert('Informe um período (início e/ou fim) para exportar o relatório.');
return;
}
try {
generatingXLSX = true;
const relatorio = await client.query(api.pedidos.gerarRelatorio, filtroArgs());
await exportarRelatorioPedidosXLSX(relatorio);
} catch (e) {
console.error('Erro ao exportar relatório XLSX:', e);
alert('Erro ao exportar relatório Excel. Tente novamente.');
} finally {
generatingXLSX = false;
}
}
// Reactive queries
const pedidosQuery = useQuery(api.pedidos.list, {});
const myItemsQuery = useQuery(api.pedidos.listByItemCreator, {});
const pedidosQuery = useQuery(api.pedidos.list, filtroArgs);
const myItemsQuery = useQuery(api.pedidos.listByItemCreator, filtroArgs);
const acoesQuery = useQuery(api.acoes.list, {});
let activeTab = $state<'all' | 'my_items'>('all');
@@ -69,13 +176,144 @@
<div class="container mx-auto p-6">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Pedidos</h1>
<a
href={resolve('/pedidos/novo')}
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<Plus size={20} />
Novo Pedido
</a>
<div class="flex items-center gap-2">
<button
type="button"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-60"
onclick={gerarPDF}
disabled={generatingPDF || generatingXLSX}
title="Gera relatório completo (PDF) no padrão do sistema"
>
{#if generatingPDF}
Gerando PDF...
{:else}
Relatório (PDF)
{/if}
</button>
<button
type="button"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-60"
onclick={exportarXLSX}
disabled={generatingPDF || generatingXLSX}
title="Exporta relatório completo em Excel (XLSX)"
>
{#if generatingXLSX}
Exportando...
{:else}
Excel (XLSX)
{/if}
</button>
<a
href={resolve('/pedidos/novo')}
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<Plus size={20} />
Novo Pedido
</a>
</div>
</div>
<div class="mb-6 rounded-lg bg-white p-4 shadow-md">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_numeroSei"
>Número SEI</label
>
<input
id="filtro_numeroSei"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
type="text"
placeholder="Buscar..."
bind:value={filtroNumeroSei}
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_criadoPor"
>Criado por</label
>
<select
id="filtro_criadoPor"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
bind:value={filtroCriadoPor}
>
<option value="">Todos</option>
{#each usuariosQuery.data || [] as u (u._id)}
<option value={u._id}>{u.nome}</option>
{/each}
</select>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_aceitoPor"
>Aceito por</label
>
<select
id="filtro_aceitoPor"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
bind:value={filtroAceitoPor}
>
<option value="">Todos</option>
{#each funcionariosQuery.data || [] as f (f._id)}
<option value={f._id}>{f.nome}</option>
{/each}
</select>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_inicio"
>Período (início)</label
>
<input
id="filtro_inicio"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
type="date"
bind:value={filtroInicio}
/>
</div>
<div>
<label class="mb-2 block text-sm font-medium text-gray-700" for="filtro_fim"
>Período (fim)</label
>
<input
id="filtro_fim"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
type="date"
bind:value={filtroFim}
/>
</div>
<div class="lg:col-span-3">
<div class="mb-2 text-sm font-medium text-gray-700">Status</div>
<div class="flex flex-wrap gap-3">
{#each statusOptions as s (s.value)}
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
checked={statusSelected[s.value]}
onchange={(e) =>
(statusSelected[s.value] = (e.currentTarget as HTMLInputElement).checked)}
/>
<span>{s.label}</span>
</label>
{/each}
</div>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button
type="button"
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
onclick={limparFiltros}
>
Limpar filtros
</button>
</div>
</div>
<div role="tablist" class="tabs tabs-bordered mb-6">