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
| Funcionalidade | Descrição |
|---|---|
| Seleção de Motivo | Interface para escolher motivo da bonificação |
| Seleção de Produtos | Lista de produtos do carrinho para bonificação |
| Controle de Quantidade | Interface para definir quantidade de bônus por produto |
| Persistência de Dados | Salvamento automático em tabelas específicas |
| Pesquisa de Produtos | Filtro dinâmico para localizar produtos rapidamente |
| Validação de Status | Controle de status durante o ciclo de vida da bonificação |
Estrutura de Banco de Dados
Tabela AD_BONIFVIDYA (Cabeçalho da Bonificação)
| Campo | Tipo | Descrição | Valor |
|---|---|---|---|
CODBONIF | NUMBER | Número incrementável da bonificação | Auto-incremento |
NUNOTA | NUMBER | Número único do pedido de venda | Do pedido atual |
CODMOTDESC | NUMBER | Motivo selecionado na tela | Selecionado pelo usuário |
CODUSUINCLUSAO | NUMBER | Usuário logado | Usuário da sessão |
DHINCLUSAO | DATE | Data da inclusão | SYSDATE |
STATUS | VARCHAR2(2) | Status da bonificação | 'E' (Em edição) / 'EN' (Enviado) |
DHVALREQ | DATE | Data de validação/requisição | SYSDATE (ao enviar) |
CODUSUVALREQ | NUMBER | Usuário de validação | Usuário logado (ao enviar) |
Tabela AD_BONIFVIDYAPROD (Produtos da Bonificação)
| Campo | Tipo | Descrição | Valor |
|---|---|---|---|
CODBONIF | NUMBER | Referência à bonificação (FK) | CODBONIF da tabela pai |
SEQUENCIA | NUMBER | Sequência incrementável | Auto-incremento por bonif. |
CODPROD | NUMBER | Produto selecionado | Produto escolhido |
QTDNEG | NUMBER | Quantidade selecionada | Quantidade informada |
VLRUNIT | NUMBER | Valor unitário | Valor unitário do produto |
Interface do Sistema
O sistema apresenta uma interface que permite ao usuário configurar bonificações:
Interface do modal de customização de bonificação
Elementos da Interface
- Seleção de Motivo: Dropdown para escolher o motivo da bonificação
- Lista de Produtos: Tabela com todos os produtos do carrinho
- Controle de Quantidade: Botões +/- para ajustar quantidade de bônus
- Campo de Pesquisa: Filtro para localizar produtos rapidamente
- 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
| Arquivo | Localização | Responsabilidade |
|---|---|---|
NewSaleProductsOptions.tsx | Components/ProductsOptions | Botão de acesso à bonificação |
bonus.customization.service.ts | Services/Sale | Integração com API para salvar dados |
sale-create.slice.ts | Store | Estado global da bonificação |
bonus.customization.types.ts | Types/Sale | Tipagens do sistema de bonificação |
Fluxo de Funcionamento
Diagrama de Estados
Ciclo de Vida dos Dados
-
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)
-
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