Pular para o conteúdo principal

Bonificação

Ticket: FORCEWEB-1252
Última atualização: 23 de setembro de 2025

Visão Geral

Sistema de customização de bonificação que permite ao usuário selecionar motivos específicos para bonificação e escolher produtos com suas respectivas quantidades. A solução implementa uma tela semelhante à de descontos, permitindo controle granular sobre os produtos bonificados e integrando com o banco de dados através das tabelas AD_BONIFVIDYA e AD_BONIFVIDYAPROD.

Principais Funcionalidades

FuncionalidadeDescrição
Seleção de MotivoInterface para escolher motivo da bonificação
Seleção de ProdutosLista de produtos do carrinho para bonificação
Controle de QuantidadeInterface para definir quantidade de bônus por produto
Persistência de DadosSalvamento automático em tabelas específicas
Pesquisa de ProdutosFiltro dinâmico para localizar produtos rapidamente
Validação de StatusControle de status durante o ciclo de vida da bonificação

Estrutura de Banco de Dados

Tabela AD_BONIFVIDYA (Cabeçalho da Bonificação)

CampoTipoDescriçãoValor
CODBONIFNUMBERNúmero incrementável da bonificaçãoAuto-incremento
NUNOTANUMBERNúmero único do pedido de vendaDo pedido atual
CODMOTDESCNUMBERMotivo selecionado na telaSelecionado pelo usuário
CODUSUINCLUSAONUMBERUsuário logadoUsuário da sessão
DHINCLUSAODATEData da inclusãoSYSDATE
STATUSVARCHAR2(2)Status da bonificação'E' (Em edição) / 'EN' (Enviado)
DHVALREQDATEData de validação/requisiçãoSYSDATE (ao enviar)
CODUSUVALREQNUMBERUsuário de validaçãoUsuário logado (ao enviar)

Tabela AD_BONIFVIDYAPROD (Produtos da Bonificação)

CampoTipoDescriçãoValor
CODBONIFNUMBERReferência à bonificação (FK)CODBONIF da tabela pai
SEQUENCIANUMBERSequência incrementávelAuto-incremento por bonif.
CODPRODNUMBERProduto selecionadoProduto escolhido
QTDNEGNUMBERQuantidade selecionadaQuantidade informada
VLRUNITNUMBERValor unitárioValor unitário do produto

Interface do Sistema

O sistema apresenta uma interface que permite ao usuário configurar bonificações:

Interface de Bonificação Interface do modal de customização de bonificação

Elementos da Interface

  1. Seleção de Motivo: Dropdown para escolher o motivo da bonificação
  2. Lista de Produtos: Tabela com todos os produtos do carrinho
  3. Controle de Quantidade: Botões +/- para ajustar quantidade de bônus
  4. Campo de Pesquisa: Filtro para localizar produtos rapidamente
  5. Botões de Ação: Cancelar, Limpar Todos e Salvar Bonificação

Arquivos do Componente

Estrutura de Arquivos

src/components/BonusCustomizationModal/
├── index.tsx # Componente principal do modal
├── BonusCustomizationController.ts # Lógica de controle e estado
├── types.ts # Tipagens específicas
├── README.md # Esta documentação
└── styles.css # Estilos específicos (opcional)

Arquivos Integrados

ArquivoLocalizaçãoResponsabilidade
NewSaleProductsOptions.tsxComponents/ProductsOptionsBotão de acesso à bonificação
bonus.customization.service.tsServices/SaleIntegração com API para salvar dados
sale-create.slice.tsStoreEstado global da bonificação
bonus.customization.types.tsTypes/SaleTipagens do sistema de bonificação

Fluxo de Funcionamento

Diagrama de Estados

Ciclo de Vida dos Dados

  1. Criação (Status 'E')

    • Usuário abre modal de bonificação
    • Seleciona motivo da lista disponível
    • Escolhe produtos e quantidades
    • Sistema salva com STATUS = 'E' (Em edição)
  2. Envio (Status 'EN')

    • Usuário confirma o pedido de venda
    • Sistema atualiza STATUS = 'EN' (Enviado)
    • Preenche DHVALREQ = SYSDATE
    • Define CODUSUVALREQ = usuário logado

Interface do Usuário

Layout do Modal

┌─────────────────────────────────────────────────────────┐
│ [×] Customização de Bonificação │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─ Motivo da Bonificação ─────────────────────────────┐ │
│ │ [Select] Escolha o motivo... ▼ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Produtos para Bonificação ─────────────────────────┐ │
│ │ [🔍] Pesquisar produtos... │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Cód │ Produto │ Vlr Unit │ Qtd Bônus │ │ │
│ │ │ 001 │ Produto A │ R$ 10,00 │ [-] 0 [+] │ │ │
│ │ │ 002 │ Produto B │ R$ 15,00 │ [-] 2 [+] │ │ │
│ │ │ 003 │ Produto C │ R$ 20,00 │ [-] 1 [+] │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [Cancelar] [Confirmar] │
└─────────────────────────────────────────────────────────┘

Componentes da Interface

1. Seleção de Motivo

<FormControl fullWidth size="small">
<InputLabel>Selecione o motivo</InputLabel>
<Select
value={selectedReason}
onChange={(e) => setSelectedReason(e.target.value)}
label="Selecione o motivo"
>
{motivosDisponiveis.map((motivo) => (
<MenuItem key={motivo.CODMOTDESC} value={motivo.CODMOTDESC}>
{motivo.DESCRMOT}
</MenuItem>
))}
</Select>
</FormControl>

2. Lista de Produtos

<DataGrid
rows={filteredProducts}
columns={[
{ field: "CODPROD", headerName: "Código", width: 100 },
{ field: "DESCRPROD", headerName: "Produto", width: 300 },
{ field: "VLRUNIT", headerName: "Vlr. Unit.", width: 120 },
{
field: "QTDBONUS",
headerName: "Qtd. Bônus",
width: 200,
renderCell: (params) => <QuantitySelector {...params} />,
},
]}
pageSize={10}
disableSelectionOnClick
/>

3. Controle de Quantidade

const QuantitySelector = ({ row, value, onUpdate }) => (
<div className="flex items-center gap-2">
<IconButton onClick={() => onUpdate(row.CODPROD, value - 1)}>
<Remove />
</IconButton>

<TextField
type="number"
value={value}
onChange={(e) => onUpdate(row.CODPROD, Number(e.target.value))}
inputProps={{ min: 0, style: { textAlign: "center" } }}
/>

<IconButton onClick={() => onUpdate(row.CODPROD, value + 1)}>
<Add />
</IconButton>
</div>
);

Implementação Técnica

Controller Principal

BonusCustomizationController.ts - Código Completo
// src/components/BonusCustomizationModal/BonusCustomizationController.ts

import { useState, useEffect, useMemo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/store";
import { setBonusCustomization } from "@/store/sale-create.slice";
import { Permission } from "@/utils/permission";

interface BonusProduct {
CODPROD: number;
DESCRPROD: string;
VLRUNIT: number;
QTDBONUS: number;
maxQuantity: number;
}

interface BonusReason {
CODMOTDESC: number;
DESCRMOT: string;
}

export const useBonusCustomizationController = (open: boolean) => {
const dispatch = useDispatch();

// Estados do Redux
const productsCart = useSelector((state: RootState) =>
Object.values(state.salesCreate.products).filter(
(p) => Number(p.QTDNEG) > 0
)
);
const POLITICAS = useSelector(
(state: RootState) => state.salesCreate.client?.POLITICAS || []
);
const bonusCustomization = useSelector(
(state: RootState) => state.salesCreate.bonusCustomization || []
);
const selectedReasonFromStore = useSelector(
(state: RootState) => state.salesCreate.bonusReason
);

// Estados locais
const [searchTerm, setSearchTerm] = useState<string>("");
const [productsBonusData, setProductsBonusData] = useState<BonusProduct[]>(
[]
);
const [selectedReason, setSelectedReason] = useState<string>(
selectedReasonFromStore || ""
);
const [isInitialized, setIsInitialized] = useState<boolean>(false);

// Extrair motivos das políticas
const motivosDisponiveis = useMemo(() => {
const motivos: BonusReason[] = [];

POLITICAS.forEach((politica) => {
if (politica.MOTIVOS) {
politica.MOTIVOS.forEach((motivo) => {
if (motivo.CODMOTDESC && motivo.DESCRMOT) {
const existeMotivo = motivos.find(
(m) => m.CODMOTDESC === motivo.CODMOTDESC
);
if (!existeMotivo) {
motivos.push({
CODMOTDESC: motivo.CODMOTDESC,
DESCRMOT: motivo.DESCRMOT,
});
}
}
});
}
});

return motivos;
}, [POLITICAS]);

// Inicializar dados dos produtos ao abrir o modal
useEffect(() => {
if (open && productsCart.length > 0 && !isInitialized) {
const initialData = productsCart.map((product) => {
const existingBonus = bonusCustomization.find(
(bonus) => bonus.CODPROD === product.CODPROD
);

return {
CODPROD: product.CODPROD,
DESCRPROD: product.DESCRPROD,
VLRUNIT: Number(product.VLRUNIT),
QTDBONUS: existingBonus?.QTDBONUS || 0,
maxQuantity: Number(product.QTDNEG), // Limite baseado na quantidade do carrinho
};
});
setProductsBonusData(initialData);
setIsInitialized(true);
} else if (!open) {
// Reset quando modal for fechado
setProductsBonusData([]);
setIsInitialized(false);
setSearchTerm("");
}
}, [open, productsCart, bonusCustomization, isInitialized]);

// Filtrar produtos com base na pesquisa
const filteredProducts = useMemo(() => {
if (!searchTerm) return productsBonusData;

const searchWords = searchTerm
.toLowerCase()
.split(" ")
.filter((word) => word.length > 0);

return productsBonusData.filter((product) => {
const searchableText =
`${product.CODPROD} ${product.DESCRPROD}`.toLowerCase();
return searchWords.every((word) => searchableText.includes(word));
});
}, [productsBonusData, searchTerm]);

// Função para atualizar quantidade de bônus
const updateBonusQuantity = useCallback(
(codProd: number, newQuantity: number) => {
setProductsBonusData((prev) =>
prev.map((product) =>
product.CODPROD === codProd
? {
...product,
QTDBONUS: Math.max(
0,
Math.min(newQuantity, product.maxQuantity)
),
}
: product
)
);
},
[]
);

// Função para limpar todos os produtos
const clearAllBonus = useCallback(() => {
setProductsBonusData((prev) =>
prev.map((product) => ({ ...product, QTDBONUS: 0 }))
);
}, []);

// Highlight de texto na pesquisa
const highlightText = useCallback((text: string, search: string) => {
if (!search) return text;

const searchWords = search
.toLowerCase()
.split(" ")
.filter((word) => word.length > 0);
let highlightedText = text;

searchWords.forEach((word) => {
const regex = new RegExp(`(${word})`, "gi");
highlightedText = highlightedText.replace(
regex,
'<mark style="background-color: yellow;">$1</mark>'
);
});

return highlightedText;
}, []);

// Função para salvar os produtos com bônus
const saveBonusProducts = useCallback(() => {
// Filtrar apenas produtos com QTDBONUS > 0
const bonusProducts = productsBonusData
?.filter((product) => product.QTDBONUS > 0)
?.map((product) => ({
CODPROD: product.CODPROD,
DESCRPROD: product.DESCRPROD,
VLRUNIT: product.VLRUNIT.toString(),
QTDBONUS: product.QTDBONUS,
}));

dispatch(
setBonusCustomization({
bonusProducts: bonusProducts || [],
selectedReason: selectedReason,
})
);
}, [dispatch, productsBonusData, selectedReason]);

return {
// Estados
searchTerm,
setSearchTerm,
selectedReason,
setSelectedReason,
filteredProducts,
motivosDisponiveis,

// Ações
updateBonusQuantity,
clearAllBonus,
highlightText,
saveBonusProducts,

// Status
hasSelectedProducts: productsBonusData.some((p) => p.QTDBONUS > 0),
totalBonusProducts: productsBonusData.filter((p) => p.QTDBONUS > 0).length,
};
};

Componente Principal

BonusCustomizationModal/index.tsx - Estrutura Base
// src/components/BonusCustomizationModal/index.tsx

import React from "react";
import {
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
Button,
} from "@material-tailwind/react";
import {
IconButton,
TextField,
MenuItem,
Select,
FormControl,
InputLabel,
InputAdornment,
} from "@mui/material";
import { Close, Add, Remove, Search } from "@mui/icons-material";
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { useBonusCustomizationController } from "./BonusCustomizationController";
import formatMoney from "@/utils/formatMoney";

interface BonusCustomizationModalProps {
open: boolean;
onClose: () => void;
}

const BonusCustomizationModal: React.FC<BonusCustomizationModalProps> = ({
open,
onClose,
}) => {
const {
searchTerm,
setSearchTerm,
filteredProducts,
motivosDisponiveis,
selectedReason,
setSelectedReason,
updateBonusQuantity,
clearAllBonus,
highlightText,
saveBonusProducts,
hasSelectedProducts,
totalBonusProducts,
} = useBonusCustomizationController(open);

const handleClose = () => {
onClose();
};

const handleSave = () => {
saveBonusProducts();
handleClose();
};

// Configuração das colunas do DataGrid
const columns: GridColDef[] = [
{
field: "CODPROD",
headerName: "Código",
width: 100,
renderCell: (params: GridRenderCellParams) => (
<div className="flex items-center">
<span className="text-sm font-medium text-gray-700">
{params.value}
</span>
</div>
),
},
{
field: "DESCRPROD",
headerName: "Produto",
width: 300,
renderCell: (params: GridRenderCellParams) => (
<div className="flex items-center">
<span
className="text-sm font-medium text-gray-700"
dangerouslySetInnerHTML={{
__html: highlightText(params.value, searchTerm),
}}
/>
</div>
),
},
{
field: "VLRUNIT",
headerName: "Vlr. Unit.",
width: 120,
renderCell: (params: GridRenderCellParams) => (
<div className="flex items-center">
<span className="font-semibold text-green-600">
R$ {formatMoney(params.value)}
</span>
</div>
),
},
{
field: "QTDBONUS",
headerName: "Qtd. Bônus",
width: 200,
renderCell: (params: GridRenderCellParams) => (
<div className="flex items-center gap-2">
<IconButton
size="small"
onClick={() =>
updateBonusQuantity(params.row.CODPROD, params.value - 1)
}
disabled={params.value <= 0}
>
<Remove fontSize="small" />
</IconButton>

<TextField
type="number"
size="small"
value={params.value}
onChange={(e) =>
updateBonusQuantity(params.row.CODPROD, Number(e.target.value))
}
inputProps={{
min: 0,
max: params.row.maxQuantity,
style: {
textAlign: "center",
width: "60px",
},
}}
/>

<IconButton
size="small"
onClick={() =>
updateBonusQuantity(params.row.CODPROD, params.value + 1)
}
disabled={params.value >= params.row.maxQuantity}
>
<Add fontSize="small" />
</IconButton>
</div>
),
},
];

return (
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="lg">
<div className="flex flex-row justify-between items-center pr-2">
<DialogHeader>Customização de Bonificação</DialogHeader>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</div>

<DialogBody className="font-normal text-gray-800 bg-gray-50" divider>
<div className="flex flex-col gap-6 p-2">
{/* Campo de seleção de motivos */}
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h3 className="text-base font-semibold text-gray-800 mb-4 pb-2 border-b border-gray-100">
Motivo da Bonificação
</h3>
<FormControl fullWidth size="small">
<InputLabel>Selecione o motivo</InputLabel>
<Select
value={selectedReason}
onChange={(e) => setSelectedReason(e.target.value)}
label="Selecione o motivo"
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "white",
"&:hover": {
backgroundColor: "#f8fafc",
},
},
}}
>
<MenuItem value="">
<em className="text-gray-500">Selecione um motivo</em>
</MenuItem>
{motivosDisponiveis.map((motivo) => (
<MenuItem
key={motivo.CODMOTDESC}
value={motivo.CODMOTDESC.toString()}
>
{motivo.DESCRMOT}
</MenuItem>
))}
</Select>
</FormControl>
</div>

{/* Produtos do Carrinho com Pesquisa */}
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h3 className="text-base font-semibold text-gray-800 mb-4 pb-2 border-b border-gray-100 flex items-center justify-between">
<span>Produtos do Carrinho</span>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
{filteredProducts.length} produto(s)
{totalBonusProducts > 0 && `${totalBonusProducts} com bônus`}
</span>
</h3>

{/* Campo de pesquisa */}
<div className="mb-4">
<TextField
fullWidth
size="small"
placeholder="Pesquisar produtos por código ou descrição..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search className="text-gray-400" fontSize="small" />
</InputAdornment>
),
}}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#f8fafc",
"&:hover": {
backgroundColor: "white",
},
"&.Mui-focused": {
backgroundColor: "white",
},
},
}}
/>
</div>

{/* Grid de produtos */}
<div style={{ height: 400, width: "100%" }}>
<DataGrid
rows={filteredProducts}
columns={columns}
pageSize={10}
pageSizeOptions={[10, 25, 50]}
disableSelectionOnClick
getRowId={(row) => row.CODPROD}
sx={{
"& .MuiDataGrid-cell:focus": {
outline: "none",
},
"& .MuiDataGrid-row:hover": {
backgroundColor: "#f8fafc",
},
}}
localeText={{
noRowsLabel: "Nenhum produto encontrado",
footerPaginationRowsPerPage: "Linhas por página:",
}}
/>
</div>

{/* Ações rápidas */}
<div className="flex gap-2 mt-4 pt-4 border-t border-gray-100">
<Button
variant="outlined"
size="small"
onClick={clearAllBonus}
disabled={!hasSelectedProducts}
className="text-red-600 border-red-300 hover:bg-red-50"
>
Limpar Todos
</Button>
</div>
</div>
</div>
</DialogBody>

<DialogFooter className="flex justify-end gap-2">
<Button
variant="outlined"
onClick={handleClose}
className="text-gray-600 border-gray-300"
>
Cancelar
</Button>
<Button
variant="filled"
onClick={handleSave}
disabled={!selectedReason || !hasSelectedProducts}
className="bg-blue-600 text-white"
>
Confirmar Bonificação
</Button>
</DialogFooter>
</Dialog>
);
};

export default BonusCustomizationModal;

Integração com Estado Global

// store/sale-create.slice.ts - Adições necessárias

interface SalesCreateState {
// ...outros campos existentes...
bonusCustomization: BonusProduct[];
bonusReason: string | null;
bonusStatus: "idle" | "editing" | "sent";
}

// Reducers adicionais
const reducers = {
// ...reducers existentes...

setBonusCustomization: (
state,
action: PayloadAction<{
bonusProducts: BonusProduct[];
selectedReason: string;
}>
) => {
state.bonusCustomization = action.payload.bonusProducts;
state.bonusReason = action.payload.selectedReason;
state.bonusStatus = "editing";
},

clearBonusCustomization: (state) => {
state.bonusCustomization = [];
state.bonusReason = null;
state.bonusStatus = "idle";
},

updateBonusStatus: (
state,
action: PayloadAction<"idle" | "editing" | "sent">
) => {
state.bonusStatus = action.payload;
},
};

Serviço de API

bonus.customization.service.ts - Integração com Backend
// src/services/sale/bonus.customization.service.ts

import { api } from "@/lib/api";
import { Credentials } from "@/types/auth.types";

export interface BonusCustomizationRequest {
CODEMP: string;
NUNOTA: string;
CODMOTDESC: string;
CODUSUINCLUSAO: string;
products: {
CODPROD: number;
QTDNEG: number;
VLRUNIT: number;
}[];
}

export interface BonusCustomizationResponse {
CODBONIF: number;
STATUS: string;
message?: string;
hasError: boolean;
}

export class BonusCustomizationService {
/**
* Salva a bonificação customizada no banco de dados
*/
static async saveBonusCustomization(params: {
data: BonusCustomizationRequest;
credentials: Credentials;
ipServer: string;
}): Promise<BonusCustomizationResponse> {
try {
const { data } = await api.post<BonusCustomizationResponse>(
`${params.ipServer}/ppid/bonificacao/customizada`,
params.data,
{
auth: params.credentials,
headers: {
"Content-Type": "application/json",
},
}
);

return data;
} catch (error) {
console.error("Erro ao salvar bonificação customizada:", error);
return {
CODBONIF: 0,
STATUS: "ERROR",
message: "Erro ao salvar bonificação",
hasError: true,
};
}
}

/**
* Atualiza o status da bonificação para enviado
*/
static async updateBonusStatus(params: {
CODBONIF: number;
CODUSUVALREQ: string;
credentials: Credentials;
ipServer: string;
}): Promise<{ success: boolean; message?: string }> {
try {
const { data } = await api.patch(
`${params.ipServer}/ppid/bonificacao/status/${params.CODBONIF}`,
{
STATUS: "EN",
DHVALREQ: new Date().toISOString(),
CODUSUVALREQ: params.CODUSUVALREQ,
},
{
auth: params.credentials,
}
);

return { success: true, message: data.message };
} catch (error) {
console.error("Erro ao atualizar status da bonificação:", error);
return {
success: false,
message: "Erro ao atualizar status da bonificação",
};
}
}

/**
* Busca bonificações existentes por NUNOTA
*/
static async getBonificationsByNUNOTA(params: {
NUNOTA: string;
credentials: Credentials;
ipServer: string;
}) {
try {
const { data } = await api.get(
`${params.ipServer}/ppid/bonificacao/nunota/${params.NUNOTA}`,
{
auth: params.credentials,
}
);

return data;
} catch (error) {
console.error("Erro ao buscar bonificações:", error);
return { bonifications: [], hasError: true };
}
}
}

Integração com Sistema Existente

Botão de Acesso

// src/screens/nova-venda/components/Products/components/Options/NewSaleProductsOptions.tsx

{
/* Botão existente de bonificação Cotton Baby */
}
{
permission?.permissionByCompany?.isCottonBaby() && (
<button
onClick={() => setOpenBonusCottonBaby(true)}
className="p-2 rounded-xl font-semibold bg-[#F6F8FC] cursor-pointer px-3 py-1.5 text-[#73a960] text-sm overflow-visible relative"
>
Bonificação
{bonusProductsCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{bonusProductsCount}
</span>
)}
</button>
);
}

{
/* NOVO: Botão para customização de bonificação */
}
{
permission?.UTILBONIFCUST === "S" && (
<button
onClick={() => setOpenBonusCustomization(true)}
className="p-2 rounded-xl font-semibold bg-[#F6F8FC] cursor-pointer px-3 py-1.5 text-[#73a960] text-sm overflow-visible relative"
>
Customizar Bonificação
{customBonusCount > 0 && (
<span className="absolute -top-1 -right-1 bg-blue-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
{customBonusCount}
</span>
)}
</button>
);
}

Hook de Integração

// src/hooks/useBonusCustomization.ts

import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/store";
import {
setBonusCustomization,
clearBonusCustomization,
} from "@/store/sale-create.slice";
import { BonusCustomizationService } from "@/services/sale/bonus.customization.service";

export const useBonusCustomization = () => {
const dispatch = useDispatch();

const bonusCustomization = useSelector(
(state: RootState) => state.salesCreate.bonusCustomization
);
const bonusReason = useSelector(
(state: RootState) => state.salesCreate.bonusReason
);
const bonusStatus = useSelector(
(state: RootState) => state.salesCreate.bonusStatus
);

/**
* Salva bonificação personalizada
*/
const saveBonusCustomization = async (data: BonusCustomizationRequest) => {
try {
const response = await BonusCustomizationService.saveBonusCustomization({
data,
credentials: getCredentials(),
ipServer: getCurrentServer(),
});

if (!response.hasError) {
return { success: true, CODBONIF: response.CODBONIF };
}

throw new Error(response.message);
} catch (error) {
console.error("Erro ao salvar bonificação:", error);
return { success: false, error: error.message };
}
};

/**
* Limpa dados da bonificação
*/
const clearBonus = () => {
dispatch(clearBonusCustomization());
};

/**
* Verifica se há produtos com bonificação
*/
const hasCustomBonus = bonusCustomization.length > 0;

/**
* Conta total de produtos com bonificação
*/
const customBonusCount = bonusCustomization.filter(
(p) => p.QTDBONUS > 0
).length;

return {
bonusCustomization,
bonusReason,
bonusStatus,
hasCustomBonus,
customBonusCount,
saveBonusCustomization,
clearBonus,
};
};

Documentação gerada em 23 de setembro de 2025