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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user