feat: implement filtering and PDF/Excel report generation for planejamentos.

This commit is contained in:
2025-12-18 17:31:55 -03:00
parent 0a4be24655
commit 0c7412c764
5 changed files with 812 additions and 28 deletions

View File

@@ -19,26 +19,47 @@ function normalizeOptionalString(value: string | undefined): string | undefined
export const list = query({
args: {
status: v.optional(v.union(v.literal('rascunho'), v.literal('gerado'), v.literal('cancelado'))),
responsavelId: v.optional(v.id('funcionarios'))
statuses: v.optional(
v.array(v.union(v.literal('rascunho'), v.literal('gerado'), v.literal('cancelado')))
),
responsavelId: v.optional(v.id('funcionarios')),
acaoId: v.optional(v.id('acoes')),
texto: v.optional(v.string()),
periodoInicio: v.optional(v.number()),
periodoFim: v.optional(v.number())
},
handler: async (ctx, args) => {
const status = args.status;
const responsavelId = args.responsavelId;
const { periodoInicio, periodoFim, texto } = args;
let base: Doc<'planejamentosPedidos'>[] = [];
let base = await ctx.db.query('planejamentosPedidos').collect();
if (responsavelId) {
base = await ctx.db
.query('planejamentosPedidos')
.withIndex('by_responsavelId', (q) => q.eq('responsavelId', responsavelId))
.collect();
} else if (status) {
base = await ctx.db
.query('planejamentosPedidos')
.withIndex('by_status', (q) => q.eq('status', status))
.collect();
} else {
base = await ctx.db.query('planejamentosPedidos').collect();
// Filtros em memória (devido à complexidade de múltiplos índices)
if (args.responsavelId) {
base = base.filter((p) => p.responsavelId === args.responsavelId);
}
if (args.acaoId) {
base = base.filter((p) => p.acaoId === args.acaoId);
}
// Status simples ou múltiplo
if (args.statuses && args.statuses.length > 0) {
base = base.filter((p) => args.statuses!.includes(p.status));
} else if (args.status) {
base = base.filter((p) => p.status === args.status);
}
if (periodoInicio) {
base = base.filter((p) => p.data >= new Date(periodoInicio).toISOString().split('T')[0]);
}
if (periodoFim) {
base = base.filter((p) => p.data <= new Date(periodoFim).toISOString().split('T')[0]);
}
if (texto) {
const t = texto.toLowerCase();
base = base.filter(
(p) => p.titulo.toLowerCase().includes(t) || p.descricao.toLowerCase().includes(t)
);
}
base.sort((a, b) => b.criadoEm - a.criadoEm);
@@ -59,6 +80,108 @@ export const list = query({
}
});
export const gerarRelatorio = query({
args: {
statuses: v.optional(
v.array(v.union(v.literal('rascunho'), v.literal('gerado'), v.literal('cancelado')))
),
responsavelId: v.optional(v.id('funcionarios')),
acaoId: v.optional(v.id('acoes')),
texto: v.optional(v.string()),
periodoInicio: v.optional(v.number()),
periodoFim: v.optional(v.number())
},
handler: async (ctx, args) => {
// Reutilizar lógica de filtro
let base = await ctx.db.query('planejamentosPedidos').collect();
if (args.responsavelId) {
base = base.filter((p) => p.responsavelId === args.responsavelId);
}
if (args.acaoId) {
base = base.filter((p) => p.acaoId === args.acaoId);
}
if (args.statuses && args.statuses.length > 0) {
base = base.filter((p) => args.statuses!.includes(p.status));
}
if (args.periodoInicio) {
base = base.filter(
(p) => p.data >= new Date(args.periodoInicio!).toISOString().split('T')[0]
);
}
if (args.periodoFim) {
base = base.filter((p) => p.data <= new Date(args.periodoFim!).toISOString().split('T')[0]);
}
if (args.texto) {
const t = args.texto.toLowerCase();
base = base.filter(
(p) => p.titulo.toLowerCase().includes(t) || p.descricao.toLowerCase().includes(t)
);
}
base.sort((a, b) => b.criadoEm - a.criadoEm);
// Enriquecer dados
const planejamentosEnriquecidos = await Promise.all(
base.map(async (p) => {
const [responsavel, acao, itens] = await Promise.all([
ctx.db.get(p.responsavelId),
p.acaoId ? ctx.db.get(p.acaoId) : Promise.resolve(null),
ctx.db
.query('planejamentoItens')
.withIndex('by_planejamentoId', (q) => q.eq('planejamentoId', p._id))
.collect()
]);
let valorEstimadoTotal = 0;
for (const item of itens) {
// Corrigir string '1.000,00' -> number
const val = parseFloat(
item.valorEstimado.replace(/\./g, '').replace(',', '.').replace('R$', '').trim()
);
if (!isNaN(val)) valorEstimadoTotal += val * item.quantidade;
}
return {
...p,
responsavelNome: responsavel?.nome ?? 'Desconhecido',
acaoNome: acao?.nome ?? undefined,
itensCount: itens.length,
valorEstimadoTotal
};
})
);
// Calcular resumo
const totalPlanejamentos = base.length;
const totalValorEstimado = planejamentosEnriquecidos.reduce(
(acc, curr) => acc + curr.valorEstimadoTotal,
0
);
const totalPorStatus = [
{ status: 'rascunho', count: 0 },
{ status: 'gerado', count: 0 },
{ status: 'cancelado', count: 0 }
];
base.forEach((p) => {
const st = totalPorStatus.find((s) => s.status === p.status);
if (st) st.count++;
});
return {
filtros: args,
resumo: {
totalPlanejamentos,
totalValorEstimado,
totalPorStatus
},
planejamentos: planejamentosEnriquecidos
};
}
});
export const get = query({
args: { id: v.id('planejamentosPedidos') },
handler: async (ctx, args) => {