feat(catalog): filtro de preço e seletor de pauta
- Catálogo só mostra produtos com preço preenchido (vl_preco1 > 0) por default - Novo endpoint GET /catalog/pautas — retorna as 6 pautas do representante logado - GET /catalog?idPauta=N — usa preço da pauta selecionada (vw_pauta_produtos) - CatalogPage: dropdown "Selecionar pauta de preços" com as pautas do rep - product.contract: adiciona PautaSchema e idPauta no ProdutoListQuerySchema Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Controller, Get, NotFoundException, Param, ParseIntPipe, Query } from '
|
|||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import {
|
import {
|
||||||
ProdutoListQuerySchema,
|
ProdutoListQuerySchema,
|
||||||
|
type Pauta,
|
||||||
type ProdutoDetail,
|
type ProdutoDetail,
|
||||||
type ProdutoListQuery,
|
type ProdutoListQuery,
|
||||||
type ProdutoListResponse,
|
type ProdutoListResponse,
|
||||||
@@ -16,6 +17,11 @@ class ProdutoListQueryDto extends createZodDto(ProdutoListQuerySchema) {}
|
|||||||
export class CatalogController {
|
export class CatalogController {
|
||||||
constructor(private readonly catalog: CatalogService) {}
|
constructor(private readonly catalog: CatalogService) {}
|
||||||
|
|
||||||
|
@Get('pautas')
|
||||||
|
pautas(): Promise<Pauta[]> {
|
||||||
|
return this.catalog.pautas();
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
list(@Query() query: ProdutoListQueryDto): Promise<ProdutoListResponse> {
|
list(@Query() query: ProdutoListQueryDto): Promise<ProdutoListResponse> {
|
||||||
const parsed = ProdutoListQuerySchema.parse(query) as ProdutoListQuery;
|
const parsed = ProdutoListQuerySchema.parse(query) as ProdutoListQuery;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import type {
|
import type {
|
||||||
|
Pauta,
|
||||||
ProdutoDetail,
|
ProdutoDetail,
|
||||||
ProdutoListQuery,
|
ProdutoListQuery,
|
||||||
ProdutoListResponse,
|
ProdutoListResponse,
|
||||||
@@ -44,12 +45,45 @@ interface ProdutoRow {
|
|||||||
export class CatalogService {
|
export class CatalogService {
|
||||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||||
|
|
||||||
|
async pautas(): Promise<Pauta[]> {
|
||||||
|
const prisma = this.cls.get('prisma');
|
||||||
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||||
|
const idEmpresa = this.cls.get('idEmpresa');
|
||||||
|
const userId = this.cls.get('userId');
|
||||||
|
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
||||||
|
|
||||||
|
interface PautaRow {
|
||||||
|
id_pauta: number;
|
||||||
|
codigo: number;
|
||||||
|
descricao: string;
|
||||||
|
}
|
||||||
|
const rows = await prisma.$queryRawUnsafe<PautaRow[]>(`
|
||||||
|
SELECT DISTINCT pa.id_pauta, pa.codigo, TRIM(pa.descricao) AS descricao
|
||||||
|
FROM vw_pautas pa
|
||||||
|
JOIN vw_representantes r ON r.id_empresa = pa.id_empresa
|
||||||
|
AND pa.codigo IN (
|
||||||
|
r.cod_pauta1, r.cod_pauta2, r.cod_pauta3,
|
||||||
|
r.cod_pauta4, r.cod_pauta5, r.cod_pauta6
|
||||||
|
)
|
||||||
|
WHERE pa.id_empresa = ${idEmpresa}
|
||||||
|
AND pa.ativo = 1
|
||||||
|
AND r.codigo = ${codVendedor}
|
||||||
|
ORDER BY pa.codigo
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((r) => ({
|
||||||
|
idPauta: Number(r.id_pauta),
|
||||||
|
codigo: Number(r.codigo),
|
||||||
|
descricao: r.descricao,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async list(query: ProdutoListQuery): Promise<ProdutoListResponse> {
|
async list(query: ProdutoListQuery): Promise<ProdutoListResponse> {
|
||||||
const prisma = this.cls.get('prisma');
|
const prisma = this.cls.get('prisma');
|
||||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||||
const idEmpresa = this.cls.get('idEmpresa');
|
const idEmpresa = this.cls.get('idEmpresa');
|
||||||
|
|
||||||
const { q, codGrupo, page, limit } = query;
|
const { q, codGrupo, idPauta, page, limit } = query;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const grupoFilter = codGrupo != null ? `AND p.cod_grupo = ${codGrupo}` : '';
|
const grupoFilter = codGrupo != null ? `AND p.cod_grupo = ${codGrupo}` : '';
|
||||||
@@ -57,65 +91,93 @@ export class CatalogService {
|
|||||||
? `AND (p.descricao ILIKE '%${escSql(q)}%' OR p.codigo ILIKE '%${escSql(q)}%')`
|
? `AND (p.descricao ILIKE '%${escSql(q)}%' OR p.codigo ILIKE '%${escSql(q)}%')`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const rows = await prisma.$queryRawUnsafe<ProdutoRow[]>(`
|
// Com pauta: usa preço específico da pauta. Sem pauta: filtra vl_preco1 > 0.
|
||||||
SELECT
|
if (idPauta != null) {
|
||||||
p.id_erp,
|
interface PautaItemRow extends ProdutoRow {
|
||||||
p.codigo,
|
preco_pauta: string;
|
||||||
p.descricao,
|
}
|
||||||
p.unidade,
|
const [rows, countRows] = await Promise.all([
|
||||||
p.vl_preco1::text,
|
prisma.$queryRawUnsafe<PautaItemRow[]>(`
|
||||||
p.cod_grupo,
|
SELECT
|
||||||
p.grupo,
|
p.id_erp, p.codigo, p.descricao, p.unidade,
|
||||||
p.cod_subgrupo,
|
pp.preco1::text AS preco_pauta,
|
||||||
p.subgrupo,
|
p.cod_grupo, p.grupo, p.cod_subgrupo, p.subgrupo, p.marca,
|
||||||
p.marca,
|
p.ativo, e.qtd_estoque::text, p.lista_pauta,
|
||||||
p.ativo,
|
p.referencia, p.descr_det, p.vl_preco2::text, p.vl_preco3::text,
|
||||||
e.qtd_estoque::text,
|
p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text,
|
||||||
p.lista_pauta,
|
p.lote_mul_venda, p.preco_promocional::text
|
||||||
p.referencia,
|
FROM vw_pauta_produtos pp
|
||||||
p.descr_det,
|
JOIN vw_produtos p ON p.id_erp = pp.id_produto AND p.id_empresa = ${idEmpresa}
|
||||||
p.vl_preco2::text,
|
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
|
||||||
p.vl_preco3::text,
|
WHERE pp.id_pauta = ${idPauta}
|
||||||
p.aliq_ipi::text,
|
AND p.ativo = 1
|
||||||
p.peso_liquido::text,
|
AND pp.preco1 > 0
|
||||||
p.qtd_volume::text,
|
${grupoFilter}
|
||||||
p.lote_mul_venda,
|
${searchFilter}
|
||||||
p.preco_promocional::text
|
ORDER BY p.descricao
|
||||||
FROM vw_produtos p
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
|
`),
|
||||||
WHERE p.ativo = 1
|
prisma.$queryRawUnsafe<[{ count: string }]>(`
|
||||||
${grupoFilter}
|
SELECT COUNT(*)::text AS count
|
||||||
${searchFilter}
|
FROM vw_pauta_produtos pp
|
||||||
ORDER BY p.descricao
|
JOIN vw_produtos p ON p.id_erp = pp.id_produto AND p.id_empresa = ${idEmpresa}
|
||||||
LIMIT ${limit} OFFSET ${offset}
|
WHERE pp.id_pauta = ${idPauta} AND p.ativo = 1 AND pp.preco1 > 0
|
||||||
`);
|
${grupoFilter} ${searchFilter}
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
const total = parseInt(countRows[0]?.count ?? '0', 10);
|
||||||
|
return {
|
||||||
|
data: rows.map((p) => this.mapRow(p, p.preco_pauta)),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const totalRows = await prisma.$queryRawUnsafe<[{ count: string }]>(`
|
// Sem pauta: produtos com preço base preenchido
|
||||||
SELECT COUNT(*)::text AS count
|
const [rows, countRows] = await Promise.all([
|
||||||
FROM vw_produtos p
|
prisma.$queryRawUnsafe<ProdutoRow[]>(`
|
||||||
WHERE p.ativo = 1
|
SELECT
|
||||||
${grupoFilter}
|
p.id_erp, p.codigo, p.descricao, p.unidade,
|
||||||
${searchFilter}
|
p.vl_preco1::text,
|
||||||
`);
|
p.cod_grupo, p.grupo, p.cod_subgrupo, p.subgrupo, p.marca,
|
||||||
const total = parseInt(totalRows[0]?.count ?? '0', 10);
|
p.ativo, e.qtd_estoque::text, p.lista_pauta,
|
||||||
|
p.referencia, p.descr_det, p.vl_preco2::text, p.vl_preco3::text,
|
||||||
|
p.aliq_ipi::text, p.peso_liquido::text, p.qtd_volume::text,
|
||||||
|
p.lote_mul_venda, p.preco_promocional::text
|
||||||
|
FROM vw_produtos p
|
||||||
|
LEFT JOIN vw_estoque e ON e.id_erp = p.id_erp AND e.id_empresa = ${idEmpresa}
|
||||||
|
WHERE p.id_empresa = ${idEmpresa} AND p.ativo = 1 AND p.vl_preco1 > 0
|
||||||
|
${grupoFilter} ${searchFilter}
|
||||||
|
ORDER BY p.descricao
|
||||||
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
|
`),
|
||||||
|
prisma.$queryRawUnsafe<[{ count: string }]>(`
|
||||||
|
SELECT COUNT(*)::text AS count FROM vw_produtos p
|
||||||
|
WHERE p.id_empresa = ${idEmpresa} AND p.ativo = 1 AND p.vl_preco1 > 0
|
||||||
|
${grupoFilter} ${searchFilter}
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
const total = parseInt(countRows[0]?.count ?? '0', 10);
|
||||||
|
return { data: rows.map((p) => this.mapRow(p, p.vl_preco1)), total, page, limit };
|
||||||
|
}
|
||||||
|
|
||||||
const data: ProdutoSummary[] = rows.map((p) => ({
|
private mapRow(p: ProdutoRow, preco: string): ProdutoSummary {
|
||||||
|
return {
|
||||||
idErp: Number(p.id_erp),
|
idErp: Number(p.id_erp),
|
||||||
codigo: p.codigo,
|
codigo: (p.codigo ?? '').trim(),
|
||||||
descricao: p.descricao,
|
descricao: (p.descricao ?? '').trim(),
|
||||||
unidade: p.unidade,
|
unidade: p.unidade,
|
||||||
vlPreco1: p.vl_preco1,
|
vlPreco1: preco ?? '0',
|
||||||
codGrupo: p.cod_grupo !== null ? Number(p.cod_grupo) : null,
|
codGrupo: p.cod_grupo !== null ? Number(p.cod_grupo) : null,
|
||||||
grupo: p.grupo,
|
grupo: p.grupo ? p.grupo.trim() : null,
|
||||||
codSubgrupo: p.cod_subgrupo !== null ? Number(p.cod_subgrupo) : null,
|
codSubgrupo: p.cod_subgrupo !== null ? Number(p.cod_subgrupo) : null,
|
||||||
subgrupo: p.subgrupo,
|
subgrupo: p.subgrupo ? p.subgrupo.trim() : null,
|
||||||
marca: p.marca,
|
marca: p.marca ? p.marca.trim() : null,
|
||||||
ativo: Number(p.ativo),
|
ativo: Number(p.ativo),
|
||||||
qtdEstoque: p.qtd_estoque,
|
qtdEstoque: p.qtd_estoque,
|
||||||
listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null,
|
listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null,
|
||||||
}));
|
};
|
||||||
|
|
||||||
return { data, total, page, limit };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(idErp: number): Promise<ProdutoDetail | null> {
|
async findOne(idErp: number): Promise<ProdutoDetail | null> {
|
||||||
@@ -157,19 +219,7 @@ export class CatalogService {
|
|||||||
if (!p) return null;
|
if (!p) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
idErp: Number(p.id_erp),
|
...this.mapRow(p, p.vl_preco1),
|
||||||
codigo: p.codigo,
|
|
||||||
descricao: p.descricao,
|
|
||||||
unidade: p.unidade,
|
|
||||||
vlPreco1: p.vl_preco1,
|
|
||||||
codGrupo: p.cod_grupo !== null ? Number(p.cod_grupo) : null,
|
|
||||||
grupo: p.grupo,
|
|
||||||
codSubgrupo: p.cod_subgrupo !== null ? Number(p.cod_subgrupo) : null,
|
|
||||||
subgrupo: p.subgrupo,
|
|
||||||
marca: p.marca,
|
|
||||||
ativo: Number(p.ativo),
|
|
||||||
qtdEstoque: p.qtd_estoque,
|
|
||||||
listaParauta: p.lista_pauta !== null ? Number(p.lista_pauta) : null,
|
|
||||||
referencia: p.referencia,
|
referencia: p.referencia,
|
||||||
descricaoDetalhada: p.descr_det,
|
descricaoDetalhada: p.descr_det,
|
||||||
vlPreco2: p.vl_preco2,
|
vlPreco2: p.vl_preco2,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Table, Input, Typography, Tag } from 'antd';
|
import { Table, Input, Select, Space, Typography, Tag } from 'antd';
|
||||||
import type { TableColumnsType } from 'antd';
|
import type { TableColumnsType } from 'antd';
|
||||||
import type { ProdutoSummary } from '@sar/api-interface';
|
import type { ProdutoSummary } from '@sar/api-interface';
|
||||||
import { useCatalog } from '../../lib/queries/catalog';
|
import { useCatalog, usePautas } from '../../lib/queries/catalog';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -45,9 +45,13 @@ const columns: TableColumnsType<ProdutoSummary> = [
|
|||||||
{
|
{
|
||||||
title: 'Preço',
|
title: 'Preço',
|
||||||
dataIndex: 'vlPreco1',
|
dataIndex: 'vlPreco1',
|
||||||
width: 110,
|
width: 120,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (v: string) => fmtPrice(v),
|
render: (v: string) => (
|
||||||
|
<span style={{ fontWeight: 600, color: Number(v) > 0 ? '#389e0d' : '#999' }}>
|
||||||
|
{fmtPrice(v)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Estoque',
|
title: 'Estoque',
|
||||||
@@ -66,10 +70,12 @@ const columns: TableColumnsType<ProdutoSummary> = [
|
|||||||
|
|
||||||
export function CatalogPage() {
|
export function CatalogPage() {
|
||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
|
const [idPauta, setIdPauta] = useState<number | undefined>();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const limit = 50;
|
const limit = 50;
|
||||||
|
|
||||||
const { data, isLoading } = useCatalog({ q: q || undefined, page, limit });
|
const { data: pautas, isLoading: pautasLoading } = usePautas();
|
||||||
|
const { data, isLoading } = useCatalog({ q: q || undefined, idPauta, page, limit });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ padding: 24 }}>
|
||||||
@@ -77,21 +83,37 @@ export function CatalogPage() {
|
|||||||
Catálogo de Produtos
|
Catálogo de Produtos
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Search
|
<Space style={{ marginBottom: 16 }} wrap>
|
||||||
placeholder="Buscar por código ou descrição..."
|
<Search
|
||||||
allowClear
|
placeholder="Buscar por código ou descrição..."
|
||||||
style={{ width: 320, marginBottom: 16 }}
|
allowClear
|
||||||
onSearch={(v) => {
|
style={{ width: 300 }}
|
||||||
setQ(v);
|
onSearch={(v) => {
|
||||||
setPage(1);
|
setQ(v);
|
||||||
}}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e.target.value) {
|
|
||||||
setQ('');
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}}
|
||||||
}}
|
onChange={(e) => {
|
||||||
/>
|
if (!e.target.value) {
|
||||||
|
setQ('');
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Selecionar pauta de preços"
|
||||||
|
allowClear
|
||||||
|
loading={pautasLoading}
|
||||||
|
style={{ width: 340 }}
|
||||||
|
onChange={(v) => {
|
||||||
|
setIdPauta(v as number | undefined);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
options={pautas?.map((p) => ({
|
||||||
|
value: p.idPauta,
|
||||||
|
label: `${p.codigo} — ${p.descricao}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Table<ProdutoSummary>
|
<Table<ProdutoSummary>
|
||||||
rowKey="idErp"
|
rowKey="idErp"
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
|
PautaSchema,
|
||||||
ProdutoListResponseSchema,
|
ProdutoListResponseSchema,
|
||||||
ProdutoDetailSchema,
|
ProdutoDetailSchema,
|
||||||
type ProdutoListQuery,
|
type ProdutoListQuery,
|
||||||
type ProdutoListResponse,
|
type ProdutoListResponse,
|
||||||
type ProdutoDetail,
|
type ProdutoDetail,
|
||||||
|
type Pauta,
|
||||||
} from '@sar/api-interface';
|
} from '@sar/api-interface';
|
||||||
|
import { z } from 'zod';
|
||||||
import { apiFetch } from '../api-client';
|
import { apiFetch } from '../api-client';
|
||||||
|
|
||||||
|
export function usePautas() {
|
||||||
|
return useQuery<Pauta[]>({
|
||||||
|
queryKey: ['catalog', 'pautas'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiFetch('/catalog/pautas');
|
||||||
|
return z.array(PautaSchema).parse(res);
|
||||||
|
},
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useCatalog(params: Partial<ProdutoListQuery> = {}) {
|
export function useCatalog(params: Partial<ProdutoListQuery> = {}) {
|
||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
if (params.q) search.set('q', params.q);
|
if (params.q) search.set('q', params.q);
|
||||||
if (params.codGrupo) search.set('codGrupo', String(params.codGrupo));
|
if (params.codGrupo) search.set('codGrupo', String(params.codGrupo));
|
||||||
|
if (params.idPauta) search.set('idPauta', String(params.idPauta));
|
||||||
if (params.page) search.set('page', String(params.page));
|
if (params.page) search.set('page', String(params.page));
|
||||||
if (params.limit) search.set('limit', String(params.limit));
|
if (params.limit) search.set('limit', String(params.limit));
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const ProdutoSummarySchema = z.object({
|
|||||||
codigo: z.string(),
|
codigo: z.string(),
|
||||||
descricao: z.string(),
|
descricao: z.string(),
|
||||||
unidade: z.string().nullable(),
|
unidade: z.string().nullable(),
|
||||||
vlPreco1: z.string(),
|
vlPreco1: z.string(), // preço base (vw_produtos) ou preço da pauta selecionada
|
||||||
codGrupo: z.number().int().nullable(),
|
codGrupo: z.number().int().nullable(),
|
||||||
grupo: z.string().nullable(),
|
grupo: z.string().nullable(),
|
||||||
codSubgrupo: z.number().int().nullable(),
|
codSubgrupo: z.number().int().nullable(),
|
||||||
@@ -21,6 +21,13 @@ export const ProdutoSummarySchema = z.object({
|
|||||||
qtdEstoque: z.string().nullable(),
|
qtdEstoque: z.string().nullable(),
|
||||||
listaParauta: z.number().int().nullable(),
|
listaParauta: z.number().int().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const PautaSchema = z.object({
|
||||||
|
idPauta: z.number().int(),
|
||||||
|
codigo: z.number().int(),
|
||||||
|
descricao: z.string(),
|
||||||
|
});
|
||||||
|
export type Pauta = z.infer<typeof PautaSchema>;
|
||||||
export type ProdutoSummary = z.infer<typeof ProdutoSummarySchema>;
|
export type ProdutoSummary = z.infer<typeof ProdutoSummarySchema>;
|
||||||
|
|
||||||
// ─── Produto Detail ───────────────────────────────────────────────────────────
|
// ─── Produto Detail ───────────────────────────────────────────────────────────
|
||||||
@@ -44,6 +51,7 @@ export type ProdutoDetail = z.infer<typeof ProdutoDetailSchema>;
|
|||||||
export const ProdutoListQuerySchema = z.object({
|
export const ProdutoListQuerySchema = z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
codGrupo: z.coerce.number().int().optional(),
|
codGrupo: z.coerce.number().int().optional(),
|
||||||
|
idPauta: z.coerce.number().int().optional(),
|
||||||
page: z.coerce.number().int().positive().default(1),
|
page: z.coerce.number().int().positive().default(1),
|
||||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user