feat(api,web): politicas comerciais com multi-selecao de produtos, grupos e subgrupos

- Promocao: produtos com busca remota por codigo/descricao (multi) e
  grupos com busca por nome/codigo (multi) - backend cria uma promocao
  por alvo na mesma transacao ({ids})
- Regra de desconto: grupos e subgrupos multi (subgrupos filtram pelos
  grupos escolhidos) - uma regra por alvo
- Edicao continua por linha, com selects de busca no lugar dos inputs
  de texto livre
- Modais maiores (760px de largura, corpo com altura minima e rolagem)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 20:52:58 +00:00
parent 756e56d8d3
commit 76cd90ae95
4 changed files with 324 additions and 92 deletions

View File

@@ -68,7 +68,7 @@ export class PoliticasController {
} }
@Post('promocoes') @Post('promocoes')
createPromocao(@Body() body: CreatePromocaoDto): Promise<{ id: number }> { createPromocao(@Body() body: CreatePromocaoDto): Promise<{ ids: number[] }> {
return this.politicas.createPromocao(body as unknown as CreatePromocaoBody); return this.politicas.createPromocao(body as unknown as CreatePromocaoBody);
} }
@@ -97,7 +97,7 @@ export class PoliticasController {
} }
@Post('regras') @Post('regras')
createRegra(@Body() body: CreateRegraDescontoDto): Promise<{ id: number }> { createRegra(@Body() body: CreateRegraDescontoDto): Promise<{ ids: number[] }> {
return this.politicas.createRegra(body as unknown as CreateRegraDescontoBody); return this.politicas.createRegra(body as unknown as CreateRegraDescontoBody);
} }

View File

@@ -109,26 +109,41 @@ export class PoliticasService {
}; };
} }
async createPromocao(body: CreatePromocaoBody): Promise<{ id: number }> { // Cria uma promoção POR ALVO (produto ou grupo). Sem alvo, cria uma linha
// genérica (comportamento anterior preservado).
async createPromocao(body: CreatePromocaoBody): Promise<{ ids: number[] }> {
this.assertManager(); this.assertManager();
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 userId = this.cls.get('userId') ?? 'system'; const userId = this.cls.get('userId') ?? 'system';
const p = await prisma.promocao.create({ const alvos: { codProduto: string | null; grpProd: string | null }[] = [
data: { ...(body.codProdutos ?? []).map((c) => ({ codProduto: c, grpProd: null })),
idEmpresa, ...(body.grpProds ?? []).map((g) => ({ codProduto: null, grpProd: g })),
descricao: body.descricao, ];
codProduto: body.codProduto ?? null, if (alvos.length === 0) alvos.push({ codProduto: null, grpProd: null });
grpProd: body.grpProd ?? null,
descPct: body.descPct, const ids = await prisma.$transaction(async (tx) => {
dataInicio: new Date(body.dataInicio), const created: number[] = [];
dataFim: new Date(body.dataFim), for (const alvo of alvos) {
createdBy: userId, const p = await tx.promocao.create({
}, data: {
idEmpresa,
descricao: body.descricao,
codProduto: alvo.codProduto,
grpProd: alvo.grpProd,
descPct: body.descPct,
dataInicio: new Date(body.dataInicio),
dataFim: new Date(body.dataFim),
createdBy: userId,
},
});
created.push(p.id);
}
return created;
}); });
return { id: p.id }; return { ids };
} }
async updatePromocao(id: number, body: UpdatePromocaoBody): Promise<void> { async updatePromocao(id: number, body: UpdatePromocaoBody): Promise<void> {
@@ -257,27 +272,42 @@ export class PoliticasService {
}; };
} }
async createRegra(body: CreateRegraDescontoBody): Promise<{ id: number }> { // Cria uma regra POR ALVO: cada grupo selecionado vira regra por grupo e
// cada subgrupo vira regra por subgrupo. Sem alvos, regra vale para todos.
async createRegra(body: CreateRegraDescontoBody): Promise<{ ids: number[] }> {
this.assertManager(); this.assertManager();
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 userId = this.cls.get('userId') ?? 'system'; const userId = this.cls.get('userId') ?? 'system';
const r = await prisma.regraDesconto.create({ const alvos: { codGrupo: number | null; codSubgrupo: number | null }[] = [
data: { ...(body.codGrupos ?? []).map((g) => ({ codGrupo: g, codSubgrupo: null })),
idEmpresa, ...(body.codSubgrupos ?? []).map((s) => ({ codGrupo: null, codSubgrupo: s })),
descricao: body.descricao, ];
codGrupo: body.codGrupo ?? null, if (alvos.length === 0) alvos.push({ codGrupo: null, codSubgrupo: null });
codSubgrupo: body.codSubgrupo ?? null,
codVendedor: body.codVendedor ?? null, const ids = await prisma.$transaction(async (tx) => {
descPct: body.descPct, const created: number[] = [];
dataInicio: new Date(body.dataInicio), for (const alvo of alvos) {
dataFim: new Date(body.dataFim), const r = await tx.regraDesconto.create({
createdBy: userId, data: {
}, idEmpresa,
descricao: body.descricao,
codGrupo: alvo.codGrupo,
codSubgrupo: alvo.codSubgrupo,
codVendedor: body.codVendedor ?? null,
descPct: body.descPct,
dataInicio: new Date(body.dataInicio),
dataFim: new Date(body.dataFim),
createdBy: userId,
},
});
created.push(r.id);
}
return created;
}); });
return { id: r.id }; return { ids };
} }
async updateRegra(id: number, body: UpdateRegraDescontoBody): Promise<void> { async updateRegra(id: number, body: UpdateRegraDescontoBody): Promise<void> {

View File

@@ -44,6 +44,62 @@ import {
useUpdateRegraDesconto, useUpdateRegraDesconto,
useDeleteRegraDesconto, useDeleteRegraDesconto,
} from '../../lib/queries/gerente'; } from '../../lib/queries/gerente';
import { useCatalog } from '../../lib/queries/catalog';
// ─── ProdutoSelect: busca remota por código ou descrição ─────────────────────
function ProdutoSelect({
multiple,
value,
onChange,
}: {
multiple?: boolean;
value?: string[] | string;
onChange?: (v: string[] | string) => void;
}) {
const [q, setQ] = useState('');
const { data, isFetching } = useCatalog({ q: q || undefined, limit: 20 });
// Guarda os rótulos dos já selecionados — a lista de opções muda a cada busca
const [labels, setLabels] = useState<Map<string, string>>(new Map());
const opts = (data?.data ?? []).map((p) => ({
value: p.codigo.trim(),
label: `${p.codigo.trim()}${p.descricao.trim()}`,
}));
const selecionados = multiple
? ((value as string[] | undefined) ?? [])
: value
? [value as string]
: [];
const merged = [...opts];
for (const v of selecionados) {
if (!merged.some((o) => o.value === v)) merged.push({ value: v, label: labels.get(v) ?? v });
}
return (
<Select
mode={multiple ? 'multiple' : undefined}
showSearch
allowClear
filterOption={false}
onSearch={setQ}
loading={isFetching}
options={merged}
value={value}
onChange={(vals) => {
setLabels((prev) => {
const next = new Map(prev);
for (const o of opts) next.set(o.value, o.label);
return next;
});
setQ('');
onChange?.(vals as string[] | string);
}}
placeholder="Busque por código ou descrição"
notFoundContent={q && !isFetching ? 'Nenhum produto encontrado' : null}
/>
);
}
const { Title, Text } = Typography; const { Title, Text } = Typography;
@@ -170,6 +226,8 @@ function TabDescontos() {
interface PromocaoForm { interface PromocaoForm {
descricao: string; descricao: string;
codProdutos?: string[];
grpProds?: string[];
codProduto?: string; codProduto?: string;
grpProd?: string; grpProd?: string;
descPct: number; descPct: number;
@@ -178,6 +236,7 @@ interface PromocaoForm {
function TabPromocoes() { function TabPromocoes() {
const { data, isLoading } = usePromocoes(); const { data, isLoading } = usePromocoes();
const { data: gruposData } = useGruposProduto();
const createMutation = useCreatePromocao(); const createMutation = useCreatePromocao();
const updateMutation = useUpdatePromocao(); const updateMutation = useUpdatePromocao();
const deleteMutation = useDeletePromocao(); const deleteMutation = useDeletePromocao();
@@ -186,6 +245,18 @@ function TabPromocoes() {
const [editTarget, setEditTarget] = useState<Promocao | null>(null); const [editTarget, setEditTarget] = useState<Promocao | null>(null);
const [msg, msgCtx] = message.useMessage(); const [msg, msgCtx] = message.useMessage();
// Grupos com nome+código no rótulo — busca funciona pelos dois
const grupoOptions = [
...new Map(
(gruposData?.grupos ?? [])
.filter((g) => g.codGrupo != null)
.map((g) => [
g.codGrupo,
{ value: String(g.codGrupo), label: `${g.codGrupo}${g.grupo ?? 'Sem nome'}` },
]),
).values(),
];
function openCreate() { function openCreate() {
setEditTarget(null); setEditTarget(null);
form.resetFields(); form.resetFields();
@@ -206,21 +277,32 @@ function TabPromocoes() {
async function handleSubmit(values: PromocaoForm) { async function handleSubmit(values: PromocaoForm) {
const [inicio, fim] = values.periodo; const [inicio, fim] = values.periodo;
const body = {
descricao: values.descricao,
codProduto: values.codProduto || undefined,
grpProd: values.grpProd || undefined,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
};
try { try {
if (editTarget) { if (editTarget) {
await updateMutation.mutateAsync({ id: editTarget.id, body }); await updateMutation.mutateAsync({
id: editTarget.id,
body: {
descricao: values.descricao,
codProduto: values.codProduto ?? null,
grpProd: values.grpProd ?? null,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
},
});
void msg.success('Promocao atualizada'); void msg.success('Promocao atualizada');
} else { } else {
await createMutation.mutateAsync(body); const res = (await createMutation.mutateAsync({
void msg.success('Promocao criada'); descricao: values.descricao,
codProdutos: values.codProdutos?.length ? values.codProdutos : undefined,
grpProds: values.grpProds?.length ? values.grpProds : undefined,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
})) as { ids: number[] };
void msg.success(
res.ids.length > 1 ? `${res.ids.length} promocoes criadas` : 'Promocao criada',
);
} }
} catch { } catch {
return; // erro já notificado pelo handler global do QueryClient; modal fica aberto return; // erro já notificado pelo handler global do QueryClient; modal fica aberto
@@ -337,7 +419,8 @@ function TabPromocoes() {
onOk={() => void form.validateFields().then(handleSubmit)} onOk={() => void form.validateFields().then(handleSubmit)}
okText={editTarget ? 'Salvar' : 'Criar'} okText={editTarget ? 'Salvar' : 'Criar'}
confirmLoading={createMutation.isPending || updateMutation.isPending} confirmLoading={createMutation.isPending || updateMutation.isPending}
width={520} width={760}
styles={{ body: { minHeight: 380, maxHeight: '72vh', overflowY: 'auto' } }}
centered centered
> >
<Form form={form} layout="vertical" style={{ marginTop: 16 }}> <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
@@ -349,14 +432,46 @@ function TabPromocoes() {
<Input placeholder="Ex.: Promocao de inverno" /> <Input placeholder="Ex.: Promocao de inverno" />
</Form.Item> </Form.Item>
<Flex gap={16}> {editTarget ? (
<Form.Item name="codProduto" label="Codigo do produto" style={{ flex: 1 }}> <Flex gap={16}>
<Input placeholder="Opcional" /> <Form.Item name="codProduto" label="Produto" style={{ flex: 1 }}>
</Form.Item> <ProdutoSelect />
<Form.Item name="grpProd" label="Grupo de produto" style={{ flex: 1 }}> </Form.Item>
<Input placeholder="Opcional" /> <Form.Item name="grpProd" label="Grupo de produto" style={{ flex: 1 }}>
</Form.Item> <Select
</Flex> options={grupoOptions}
placeholder="Opcional"
allowClear
showSearch
optionFilterProp="label"
/>
</Form.Item>
</Flex>
) : (
<>
<Form.Item
name="codProdutos"
label="Produtos"
extra="Busque por código ou descrição e selecione quantos quiser — será criada uma promoção por produto"
>
<ProdutoSelect multiple />
</Form.Item>
<Form.Item
name="grpProds"
label="Grupos de produto"
extra="Busque por nome ou código — uma promoção por grupo selecionado"
>
<Select
mode="multiple"
options={grupoOptions}
placeholder="Busque por nome ou código do grupo"
allowClear
showSearch
optionFilterProp="label"
/>
</Form.Item>
</>
)}
<Form.Item <Form.Item
name="descPct" name="descPct"
@@ -388,6 +503,8 @@ function TabPromocoes() {
interface RegraForm { interface RegraForm {
descricao: string; descricao: string;
codVendedor?: number; codVendedor?: number;
codGrupos?: number[];
codSubgrupos?: number[];
codGrupo?: number; codGrupo?: number;
codSubgrupo?: number; codSubgrupo?: number;
descPct: number; descPct: number;
@@ -406,27 +523,37 @@ function TabRegras() {
const [editTarget, setEditTarget] = useState<RegraDesconto | null>(null); const [editTarget, setEditTarget] = useState<RegraDesconto | null>(null);
const [msg, msgCtx] = message.useMessage(); const [msg, msgCtx] = message.useMessage();
const grupoSelecionado = Form.useWatch('codGrupo', form); const grupoSelecionado = Form.useWatch('codGrupo', form);
const gruposSelecionados = Form.useWatch('codGrupos', form);
const repOptions = (equipe?.reps ?? []).map((r: RepStats) => ({ const repOptions = (equipe?.reps ?? []).map((r: RepStats) => ({
value: r.codVendedor, value: r.codVendedor,
label: r.nomeVendedor ?? `Cód. ${r.codVendedor}`, label: r.nomeVendedor ?? `Cód. ${r.codVendedor}`,
})); }));
// Rótulos com código + nome — a busca do select encontra pelos dois.
// Subgrupos filtram pelos grupos escolhidos (single na edição, multi na criação).
const filtroGrupos = new Set<number>([
...(grupoSelecionado != null ? [grupoSelecionado] : []),
...(gruposSelecionados ?? []),
]);
const grupoRows: GrupoProdutoItem[] = gruposData?.grupos ?? []; const grupoRows: GrupoProdutoItem[] = gruposData?.grupos ?? [];
const grupoMap = new Map<number, { value: number; label: string }>(); const grupoMap = new Map<number, { value: number; label: string }>();
const subgrupoMap = new Map<number, { value: number; label: string }>(); const subgrupoMap = new Map<number, { value: number; label: string }>();
for (const g of grupoRows) { for (const g of grupoRows) {
if (g.codGrupo != null && !grupoMap.has(g.codGrupo)) { if (g.codGrupo != null && !grupoMap.has(g.codGrupo)) {
grupoMap.set(g.codGrupo, { value: g.codGrupo, label: g.grupo ?? `Grupo ${g.codGrupo}` }); grupoMap.set(g.codGrupo, {
value: g.codGrupo,
label: `${g.codGrupo}${g.grupo ?? 'Sem nome'}`,
});
} }
if ( if (
g.codSubgrupo != null && g.codSubgrupo != null &&
(grupoSelecionado == null || g.codGrupo === grupoSelecionado) && (filtroGrupos.size === 0 || (g.codGrupo != null && filtroGrupos.has(g.codGrupo))) &&
!subgrupoMap.has(g.codSubgrupo) !subgrupoMap.has(g.codSubgrupo)
) { ) {
subgrupoMap.set(g.codSubgrupo, { subgrupoMap.set(g.codSubgrupo, {
value: g.codSubgrupo, value: g.codSubgrupo,
label: g.subgrupo ?? `Subgrupo ${g.codSubgrupo}`, label: `${g.codSubgrupo} ${g.subgrupo ?? 'Sem nome'}`,
}); });
} }
} }
@@ -454,22 +581,32 @@ function TabRegras() {
async function handleSubmit(values: RegraForm) { async function handleSubmit(values: RegraForm) {
const [inicio, fim] = values.periodo; const [inicio, fim] = values.periodo;
const body = {
descricao: values.descricao,
codVendedor: values.codVendedor ?? null,
codGrupo: values.codGrupo ?? null,
codSubgrupo: values.codSubgrupo ?? null,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
};
try { try {
if (editTarget) { if (editTarget) {
await updateMutation.mutateAsync({ id: editTarget.id, body }); await updateMutation.mutateAsync({
id: editTarget.id,
body: {
descricao: values.descricao,
codVendedor: values.codVendedor ?? null,
codGrupo: values.codGrupo ?? null,
codSubgrupo: values.codSubgrupo ?? null,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
},
});
void msg.success('Regra atualizada'); void msg.success('Regra atualizada');
} else { } else {
await createMutation.mutateAsync(body); const res = (await createMutation.mutateAsync({
void msg.success('Regra criada'); descricao: values.descricao,
codVendedor: values.codVendedor ?? null,
codGrupos: values.codGrupos?.length ? values.codGrupos : undefined,
codSubgrupos: values.codSubgrupos?.length ? values.codSubgrupos : undefined,
descPct: values.descPct,
dataInicio: inicio.format('YYYY-MM-DD'),
dataFim: fim.format('YYYY-MM-DD'),
})) as { ids: number[] };
void msg.success(res.ids.length > 1 ? `${res.ids.length} regras criadas` : 'Regra criada');
} }
} catch { } catch {
return; // erro já notificado pelo handler global do QueryClient; modal fica aberto return; // erro já notificado pelo handler global do QueryClient; modal fica aberto
@@ -602,7 +739,8 @@ function TabRegras() {
onOk={() => void form.validateFields().then(handleSubmit)} onOk={() => void form.validateFields().then(handleSubmit)}
okText={editTarget ? 'Salvar' : 'Criar'} okText={editTarget ? 'Salvar' : 'Criar'}
confirmLoading={createMutation.isPending || updateMutation.isPending} confirmLoading={createMutation.isPending || updateMutation.isPending}
width={520} width={760}
styles={{ body: { minHeight: 380, maxHeight: '72vh', overflowY: 'auto' } }}
centered centered
> >
<Form form={form} layout="vertical" style={{ marginTop: 16 }}> <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
@@ -624,27 +762,60 @@ function TabRegras() {
/> />
</Form.Item> </Form.Item>
<Flex gap={16}> {editTarget ? (
<Form.Item name="codGrupo" label="Grupo de produto" style={{ flex: 1 }}> <Flex gap={16}>
<Select <Form.Item name="codGrupo" label="Grupo de produto" style={{ flex: 1 }}>
options={grupoOptions} <Select
placeholder="Todos os grupos" options={grupoOptions}
allowClear placeholder="Todos os grupos"
showSearch allowClear
optionFilterProp="label" showSearch
onChange={() => form.setFieldValue('codSubgrupo', undefined)} optionFilterProp="label"
/> onChange={() => form.setFieldValue('codSubgrupo', undefined)}
</Form.Item> />
<Form.Item name="codSubgrupo" label="Subgrupo" style={{ flex: 1 }}> </Form.Item>
<Select <Form.Item name="codSubgrupo" label="Subgrupo" style={{ flex: 1 }}>
options={subgrupoOptions} <Select
placeholder="Todos os subgrupos" options={subgrupoOptions}
allowClear placeholder="Todos os subgrupos"
showSearch allowClear
optionFilterProp="label" showSearch
/> optionFilterProp="label"
</Form.Item> />
</Flex> </Form.Item>
</Flex>
) : (
<>
<Form.Item
name="codGrupos"
label="Grupos de produto"
extra="Busque por nome ou código e selecione quantos quiser — uma regra por grupo"
>
<Select
mode="multiple"
options={grupoOptions}
placeholder="Todos os grupos"
allowClear
showSearch
optionFilterProp="label"
/>
</Form.Item>
<Form.Item
name="codSubgrupos"
label="Subgrupos"
extra="Uma regra por subgrupo selecionado (a lista filtra pelos grupos escolhidos)"
>
<Select
mode="multiple"
options={subgrupoOptions}
placeholder="Todos os subgrupos"
allowClear
showSearch
optionFilterProp="label"
/>
</Form.Item>
</>
)}
<Form.Item <Form.Item
name="descPct" name="descPct"

View File

@@ -39,17 +39,32 @@ export const PromocoesResponseSchema = z.object({
}); });
export type PromocoesResponse = z.infer<typeof PromocoesResponseSchema>; export type PromocoesResponse = z.infer<typeof PromocoesResponseSchema>;
// Criação aceita VÁRIOS produtos e/ou grupos — o backend cria uma promoção
// por alvo (mesma descrição/%/vigência), mantendo o motor de alçada intacto.
export const CreatePromocaoBodySchema = z.object({ export const CreatePromocaoBodySchema = z.object({
descricao: z.string().min(1).max(255), descricao: z.string().min(1).max(255),
codProduto: z.string().optional(), codProdutos: z.array(z.string().min(1)).max(100).optional(),
grpProd: z.string().optional(), grpProds: z.array(z.string().min(1)).max(100).optional(),
descPct: z.number().min(0).max(100), descPct: z.number().min(0).max(100),
dataInicio: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), dataInicio: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
dataFim: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), dataFim: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
}); });
export type CreatePromocaoBody = z.infer<typeof CreatePromocaoBodySchema>; export type CreatePromocaoBody = z.infer<typeof CreatePromocaoBodySchema>;
export const UpdatePromocaoBodySchema = CreatePromocaoBodySchema.partial().extend({ // Edição continua por linha (um alvo por promoção)
export const UpdatePromocaoBodySchema = z.object({
descricao: z.string().min(1).max(255).optional(),
codProduto: z.string().nullable().optional(),
grpProd: z.string().nullable().optional(),
descPct: z.number().min(0).max(100).optional(),
dataInicio: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
dataFim: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
ativa: z.boolean().optional(), ativa: z.boolean().optional(),
}); });
export type UpdatePromocaoBody = z.infer<typeof UpdatePromocaoBodySchema>; export type UpdatePromocaoBody = z.infer<typeof UpdatePromocaoBodySchema>;
@@ -81,10 +96,12 @@ export const RegrasDescontoResponseSchema = z.object({
}); });
export type RegrasDescontoResponse = z.infer<typeof RegrasDescontoResponseSchema>; export type RegrasDescontoResponse = z.infer<typeof RegrasDescontoResponseSchema>;
// Criação aceita VÁRIOS grupos e/ou subgrupos — uma regra por alvo (grupos
// selecionados viram regras por grupo; subgrupos viram regras por subgrupo).
export const CreateRegraDescontoBodySchema = z.object({ export const CreateRegraDescontoBodySchema = z.object({
descricao: z.string().min(1).max(200), descricao: z.string().min(1).max(200),
codGrupo: z.number().int().nullable().optional(), codGrupos: z.array(z.number().int()).max(100).optional(),
codSubgrupo: z.number().int().nullable().optional(), codSubgrupos: z.array(z.number().int()).max(100).optional(),
codVendedor: z.number().int().nullable().optional(), codVendedor: z.number().int().nullable().optional(),
descPct: z.number().min(0).max(100), descPct: z.number().min(0).max(100),
dataInicio: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), dataInicio: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
@@ -92,7 +109,21 @@ export const CreateRegraDescontoBodySchema = z.object({
}); });
export type CreateRegraDescontoBody = z.infer<typeof CreateRegraDescontoBodySchema>; export type CreateRegraDescontoBody = z.infer<typeof CreateRegraDescontoBodySchema>;
export const UpdateRegraDescontoBodySchema = CreateRegraDescontoBodySchema.partial().extend({ // Edição continua por linha (um alvo por regra)
export const UpdateRegraDescontoBodySchema = z.object({
descricao: z.string().min(1).max(200).optional(),
codGrupo: z.number().int().nullable().optional(),
codSubgrupo: z.number().int().nullable().optional(),
codVendedor: z.number().int().nullable().optional(),
descPct: z.number().min(0).max(100).optional(),
dataInicio: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
dataFim: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional(),
ativa: z.boolean().optional(), ativa: z.boolean().optional(),
}); });
export type UpdateRegraDescontoBody = z.infer<typeof UpdateRegraDescontoBodySchema>; export type UpdateRegraDescontoBody = z.infer<typeof UpdateRegraDescontoBodySchema>;