Files
App-Estoque-LiberiKids/client/src/pages/Devolucoes.js
2025-10-14 14:04:17 -03:00

740 lines
28 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
FiArrowLeft,
FiPackage,
FiCalendar,
FiUser,
FiDollarSign,
FiRotateCcw,
FiCheck,
FiX,
FiAlertTriangle,
FiPlus
} from 'react-icons/fi';
import toast from 'react-hot-toast';
import '../styles/devolucoes.css';
const Devolucoes = () => {
const [vendas, setVendas] = useState([]);
const [loading, setLoading] = useState(true);
const [vendaSelecionada, setVendaSelecionada] = useState(null);
const [showModal, setShowModal] = useState(false);
const [showHistoricoModal, setShowHistoricoModal] = useState(false);
const [historicoVenda, setHistoricoVenda] = useState([]);
const [itensDevolucao, setItensDevolucao] = useState([]);
const [motivo, setMotivo] = useState('');
const [processando, setProcessando] = useState(false);
const [tipoOperacao, setTipoOperacao] = useState('devolucao');
const [produtos, setProdutos] = useState([]);
const [itensTroca, setItensTroca] = useState([]);
useEffect(() => {
carregarVendas();
}, []);
const carregarVendas = async () => {
try {
setLoading(true);
const response = await fetch('/api/devolucoes/vendas');
if (response.ok) {
const data = await response.json();
setVendas(data);
} else {
toast.error('Erro ao carregar vendas');
}
} catch (error) {
console.error('Erro ao carregar vendas:', error);
toast.error('Erro ao carregar vendas');
} finally {
setLoading(false);
}
};
const carregarHistoricoVenda = async (vendaId) => {
try {
const response = await fetch(`/api/devolucoes/venda/${vendaId}`);
if (response.ok) {
const historico = await response.json();
setHistoricoVenda(historico);
setShowHistoricoModal(true);
} else {
toast.error('Erro ao carregar histórico');
}
} catch (error) {
console.error('Erro ao carregar histórico:', error);
toast.error('Erro ao carregar histórico');
}
};
const carregarProdutos = async () => {
try {
const response = await fetch('/api/devolucoes/produtos');
if (response.ok) {
const data = await response.json();
setProdutos(data);
}
} catch (error) {
console.error('Erro ao carregar produtos:', error);
}
};
const abrirModalDevolucao = (venda) => {
setVendaSelecionada(venda);
setItensDevolucao(venda.itens.map(item => ({
...item,
quantidade_devolver: 0,
selecionado: false
})));
setItensTroca([]);
setMotivo('');
setTipoOperacao('devolucao');
carregarProdutos();
setShowModal(true);
};
const handleItemChange = (itemId, quantidade) => {
setItensDevolucao(prev => prev.map(item => {
if (item.id === itemId) {
const quantidadeDevolver = Math.min(Math.max(0, parseInt(quantidade) || 0), item.quantidade);
return {
...item,
quantidade_devolver: quantidadeDevolver,
selecionado: quantidadeDevolver > 0
};
}
return item;
}));
};
const calcularValorDevolucao = () => {
return itensDevolucao.reduce((total, item) => {
if (item.selecionado && item.quantidade_devolver > 0) {
return total + (parseFloat(item.valor_unitario) * item.quantidade_devolver);
}
return total;
}, 0);
};
const calcularValorTroca = () => {
return itensTroca.reduce((total, item) => {
return total + (parseFloat(item.valor_unitario || 0) * parseInt(item.quantidade || 0));
}, 0);
};
const adicionarItemTroca = () => {
setItensTroca(prev => [...prev, {
id: Date.now(),
produto_id: '',
variacao_id: '',
quantidade: 1,
valor_unitario: 0,
produto_nome: '',
variacao_info: ''
}]);
};
const removerItemTroca = (id) => {
setItensTroca(prev => prev.filter(item => item.id !== id));
};
const handleTrocaChange = (id, field, value) => {
setItensTroca(prev => prev.map(item => {
if (item.id === id) {
if (field === 'produto_id') {
const produto = produtos.find(p => p.id === value);
return {
...item,
produto_id: value,
produto_nome: produto ? `${produto.marca} - ${produto.nome}` : '',
variacao_id: '',
variacao_info: '',
valor_unitario: produto ? produto.valor_revenda : 0
};
} else if (field === 'variacao_id') {
const produto = produtos.find(p => p.id === item.produto_id);
const variacao = produto?.variacoes.find(v => v.id === value);
return {
...item,
variacao_id: value,
variacao_info: variacao ? `${variacao.tamanho} - ${variacao.cor}` : '',
valor_unitario: variacao ? variacao.preco_venda || produto.valor_revenda : item.valor_unitario
};
} else if (field === 'quantidade') {
const produto = produtos.find(p => p.id === item.produto_id);
const variacao = produto?.variacoes.find(v => v.id === item.variacao_id);
const quantidadeSolicitada = parseInt(value) || 0;
if (variacao && quantidadeSolicitada > variacao.quantidade) {
toast.error(`Estoque insuficiente! Disponível: ${variacao.quantidade}, solicitado: ${quantidadeSolicitada}`);
return { ...item, [field]: variacao.quantidade };
}
return { ...item, [field]: value };
} else {
return { ...item, [field]: value };
}
}
return item;
}));
};
const processarDevolucao = async () => {
const itensSelecionados = itensDevolucao.filter(item => item.selecionado && item.quantidade_devolver > 0);
if (tipoOperacao === 'devolucao' && itensSelecionados.length === 0) {
toast.error('Selecione pelo menos um item para devolução');
return;
}
if (tipoOperacao === 'troca') {
if (itensSelecionados.length === 0) {
toast.error('Selecione pelo menos um item para devolver na troca');
return;
}
const itensValidosTroca = itensTroca.filter(item =>
item.produto_id && item.variacao_id && item.quantidade > 0
);
if (itensValidosTroca.length === 0) {
toast.error('Adicione pelo menos um produto para troca');
return;
}
}
if (!motivo.trim()) {
toast.error(`Informe o motivo da ${tipoOperacao}`);
return;
}
try {
setProcessando(true);
const requestBody = {
venda_id: vendaSelecionada.id,
tipo_operacao: tipoOperacao,
motivo: motivo.trim()
};
if (itensSelecionados.length > 0) {
requestBody.itens_devolucao = itensSelecionados.map(item => ({
item_id: item.id,
quantidade_devolvida: item.quantidade_devolver
}));
}
if (tipoOperacao === 'troca' && itensTroca.length > 0) {
requestBody.itens_troca = itensTroca
.filter(item => item.produto_id && item.variacao_id && item.quantidade > 0)
.map(item => ({
produto_id: item.produto_id,
variacao_id: item.variacao_id,
quantidade: parseInt(item.quantidade),
valor_unitario: parseFloat(item.valor_unitario)
}));
}
const response = await fetch('/api/devolucoes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
toast.success(result.message);
setShowModal(false);
carregarVendas();
} else {
const error = await response.json();
toast.error(error.message || `Erro ao processar ${tipoOperacao}`);
}
} catch (error) {
console.error(`Erro ao processar ${tipoOperacao}:`, error);
toast.error(`Erro ao processar ${tipoOperacao}`);
} finally {
setProcessando(false);
}
};
const formatarMoeda = (valor) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
};
const formatarData = (data) => {
return new Date(data).toLocaleDateString('pt-BR');
};
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>Carregando vendas...</p>
</div>
);
}
return (
<div className="devolucoes-page">
<div className="page-header">
<div className="header-content">
<div className="header-info">
<h1>
<FiRotateCcw className="page-icon" />
Devolução/Troca
</h1>
<p>Gerencie devoluções e trocas de produtos vendidos</p>
</div>
</div>
</div>
<div className="vendas-container">
{vendas.length === 0 ? (
<div className="empty-state">
<FiPackage className="empty-icon" />
<h3>Nenhuma venda encontrada</h3>
<p>Não vendas dos últimos 30 dias disponíveis para devolução</p>
</div>
) : (
<div className="vendas-grid">
{vendas.map(venda => (
<div key={venda.id} className="venda-card">
<div className="venda-header">
<div className="venda-info">
<h3>Venda #{venda.id_venda || venda.id.slice(-8)}</h3>
<div className="venda-meta">
<span className="venda-data">
<FiCalendar />
{formatarData(venda.data_venda)}
</span>
<span className="venda-cliente">
<FiUser />
{venda.cliente_nome}
</span>
</div>
</div>
<div className="venda-valor">
{formatarMoeda(venda.valor_total)}
</div>
</div>
<div className="venda-itens">
<h4>Produtos ({venda.itens.length})</h4>
<div className="itens-lista">
{venda.itens.slice(0, 3).map(item => (
<div key={item.id} className="item-resumo">
<div className="item-info-completa">
<span className="item-nome">{item.produto_nome}</span>
<span className="item-variacao">{item.variacao_info}</span>
<div className="item-detalhes">
<span className="item-quantidade">Qtd: {item.quantidade}</span>
<span className="item-valor">R$ {parseFloat(item.valor_unitario).toFixed(2)}</span>
</div>
</div>
</div>
))}
{venda.itens.length > 3 && (
<div className="item-resumo mais-itens">
+{venda.itens.length - 3} produtos
</div>
)}
</div>
</div>
<div className="venda-actions">
<button
className="btn-detalhes"
onClick={() => carregarHistoricoVenda(venda.id)}
title="Ver histórico de devoluções/trocas"
>
<FiPackage />
Detalhes
</button>
<button
className="btn-devolucao"
onClick={() => abrirModalDevolucao(venda)}
>
<FiRotateCcw />
Devolução/Troca
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Modal de Devolução */}
{showModal && vendaSelecionada && (
<div className="modal-overlay">
<div className="modal-devolucao">
<div className="modal-header">
<h2>
<FiRotateCcw />
Devolução/Troca
</h2>
<button
className="modal-close"
onClick={() => setShowModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-body">
<div className="venda-info-modal">
<h3>Venda #{vendaSelecionada.id_venda || vendaSelecionada.id.slice(-8)}</h3>
<div className="info-grid">
<div className="info-item">
<FiCalendar />
<span>{formatarData(vendaSelecionada.data_venda)}</span>
</div>
<div className="info-item">
<FiUser />
<span>{vendaSelecionada.cliente_nome}</span>
</div>
<div className="info-item">
<FiDollarSign />
<span>{formatarMoeda(vendaSelecionada.valor_total)}</span>
</div>
</div>
</div>
<div className="tipo-operacao">
<h4>Tipo de Operação:</h4>
<div className="radio-group">
<label className="radio-option">
<input
type="radio"
value="devolucao"
checked={tipoOperacao === 'devolucao'}
onChange={(e) => setTipoOperacao(e.target.value)}
/>
<span>Devolução (retorno do dinheiro)</span>
</label>
<label className="radio-option">
<input
type="radio"
value="troca"
checked={tipoOperacao === 'troca'}
onChange={(e) => setTipoOperacao(e.target.value)}
/>
<span>Troca (por outro produto/tamanho)</span>
</label>
</div>
</div>
<div className="itens-devolucao">
<h4>Selecione os itens para devolução:</h4>
<div className="itens-lista-modal">
{itensDevolucao.map(item => (
<div key={item.id} className="item-devolucao">
<div className="item-info">
{item.produto_foto && (
<div className="item-foto">
<img src={item.produto_foto} alt={item.produto_nome} />
</div>
)}
<div className="item-dados">
<div className="item-nome">{item.produto_nome}</div>
<div className="item-codigo">Código: {item.produto_codigo || 'N/A'}</div>
<div className="item-detalhes">
<span className="variacao-badge">{item.variacao_info}</span>
<span className="valor-badge">{formatarMoeda(item.valor_unitario)}</span>
<span className="quantidade-badge">Qtd: {item.quantidade}</span>
</div>
</div>
</div>
<div className="item-controles">
<label>Qtd. a devolver:</label>
<input
type="number"
min="0"
max={item.quantidade}
value={item.quantidade_devolver}
onChange={(e) => handleItemChange(item.id, e.target.value)}
className="quantidade-input"
/>
<div className="valor-item">
{item.quantidade_devolver > 0 && (
<span className="valor-devolucao">
= {formatarMoeda(item.valor_unitario * item.quantidade_devolver)}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Seção de Troca */}
{tipoOperacao === 'troca' && (
<div className="itens-troca">
<div className="troca-header">
<h4>Produtos para Troca:</h4>
<button
type="button"
className="btn-adicionar-item"
onClick={adicionarItemTroca}
>
<FiPlus />
Adicionar Produto
</button>
</div>
<div className="lista-troca">
{itensTroca.map(item => (
<div key={item.id} className="item-troca">
<div className="troca-selects">
<div className="select-group">
<label>Produto:</label>
<select
value={item.produto_id}
onChange={(e) => handleTrocaChange(item.id, 'produto_id', e.target.value)}
className="select-produto"
>
<option value="">Selecione um produto</option>
{produtos.map(produto => (
<option key={produto.id} value={produto.id}>
{produto.marca} - {produto.nome}
</option>
))}
</select>
</div>
{item.produto_id && (
<div className="select-group">
<label>Variação:</label>
<select
value={item.variacao_id}
onChange={(e) => handleTrocaChange(item.id, 'variacao_id', e.target.value)}
className="select-variacao"
>
<option value="">Selecione uma variação</option>
{produtos.find(p => p.id === item.produto_id)?.variacoes.map(variacao => (
<option key={variacao.id} value={variacao.id}>
{variacao.tamanho} - {variacao.cor} (Estoque: {variacao.quantidade})
</option>
))}
</select>
</div>
)}
<div className="select-group">
<label>Quantidade:</label>
<input
type="number"
min="1"
value={item.quantidade}
onChange={(e) => handleTrocaChange(item.id, 'quantidade', e.target.value)}
className="input-quantidade"
/>
</div>
<div className="select-group">
<label>Valor Unit.:</label>
<input
type="number"
step="0.01"
value={item.valor_unitario}
onChange={(e) => handleTrocaChange(item.id, 'valor_unitario', e.target.value)}
className="input-valor"
/>
</div>
</div>
<div className="troca-actions">
<div className="valor-total-item">
Total: {formatarMoeda(item.valor_unitario * item.quantidade)}
</div>
<button
type="button"
className="btn-remover-item"
onClick={() => removerItemTroca(item.id)}
>
<FiX />
</button>
</div>
</div>
))}
{itensTroca.length === 0 && (
<div className="empty-troca">
<p>Nenhum produto adicionado para troca</p>
</div>
)}
</div>
</div>
)}
<div className="motivo-devolucao">
<label>Motivo da {tipoOperacao} *</label>
<textarea
value={motivo}
onChange={(e) => setMotivo(e.target.value)}
placeholder={`Descreva o motivo da ${tipoOperacao}...`}
rows="3"
className="motivo-textarea"
/>
</div>
<div className="resumo-devolucao">
{tipoOperacao === 'devolucao' ? (
<>
<div className="resumo-item">
<span>Valor total da devolução:</span>
<span className="valor-total">{formatarMoeda(calcularValorDevolucao())}</span>
</div>
<div className="resumo-item">
<span>Novo valor da venda:</span>
<span className="novo-valor">
{formatarMoeda(vendaSelecionada.valor_total - calcularValorDevolucao())}
</span>
</div>
</>
) : (
<>
<div className="resumo-item">
<span>Valor dos itens devolvidos:</span>
<span className="valor-devolucao">{formatarMoeda(calcularValorDevolucao())}</span>
</div>
<div className="resumo-item">
<span>Valor dos itens de troca:</span>
<span className="valor-troca">{formatarMoeda(calcularValorTroca())}</span>
</div>
<div className="resumo-item">
<span>Diferença:</span>
<span className={`diferenca-valor ${calcularValorTroca() - calcularValorDevolucao() >= 0 ? 'positiva' : 'negativa'}`}>
{calcularValorTroca() - calcularValorDevolucao() >= 0 ? '+' : ''}
{formatarMoeda(calcularValorTroca() - calcularValorDevolucao())}
</span>
</div>
<div className="resumo-item">
<span>Novo valor da venda:</span>
<span className="novo-valor">
{formatarMoeda(vendaSelecionada.valor_total + (calcularValorTroca() - calcularValorDevolucao()))}
</span>
</div>
</>
)}
</div>
</div>
<div className="modal-footer">
<button
className="btn-cancelar"
onClick={() => setShowModal(false)}
disabled={processando}
>
<FiX />
Cancelar
</button>
<button
className="btn-processar"
onClick={processarDevolucao}
disabled={processando || (tipoOperacao === 'devolucao' && calcularValorDevolucao() === 0)}
>
{processando ? (
<>
<div className="spinner-small"></div>
Processando...
</>
) : (
<>
<FiCheck />
{tipoOperacao === 'devolucao' ? 'Processar Devolução' : 'Processar Troca'}
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Modal de Histórico de Devoluções/Trocas */}
{showHistoricoModal && (
<div className="modal-overlay">
<div className="modal-content modal-historico">
<div className="modal-header">
<h2>📋 Histórico de Devoluções/Trocas</h2>
<button
className="btn-fechar"
onClick={() => setShowHistoricoModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-body">
{historicoVenda.length === 0 ? (
<div className="historico-vazio">
<FiPackage size={48} />
<h3>Nenhuma devolução ou troca encontrada</h3>
<p>Esta venda ainda não teve nenhuma devolução ou troca.</p>
</div>
) : (
<div className="historico-lista">
{historicoVenda.map((evento, index) => (
<div key={index} className="historico-item">
<div className="historico-header">
<div className="evento-tipo">
<span className={`badge ${evento.tipo_operacao === 'troca' ? 'badge-warning' : 'badge-info'}`}>
{evento.tipo_operacao === 'troca' ? '🔄 TROCA' : '↩️ DEVOLUÇÃO'}
</span>
<span className="evento-data">
📅 {new Date(evento.data_devolucao).toLocaleDateString('pt-BR')} às {new Date(evento.data_devolucao).toLocaleTimeString('pt-BR', {hour: '2-digit', minute: '2-digit'})}
</span>
</div>
</div>
<div className="evento-produto">
<h4>🔙 Produto {evento.tipo_operacao === 'troca' ? 'Trocado' : 'Devolvido'}:</h4>
{evento.produto_info && (
<div className="produto-card">
<div className="produto-nome">{evento.produto_info.nome}</div>
<div className="produto-detalhes">
<span>📦 Código: {evento.produto_info.codigo || 'N/A'}</span>
<span>📏 Variação: {evento.produto_info.variacao}</span>
<span>🔢 Qtd. Devolvida: {evento.quantidade_devolvida}</span>
<span>💸 Valor: R$ {parseFloat(evento.valor_devolucao).toFixed(2)}</span>
</div>
</div>
)}
</div>
{evento.motivo && (
<div className="evento-motivo">
<h4>📝 Motivo:</h4>
<div className="motivo-texto">{evento.motivo}</div>
</div>
)}
</div>
))}
</div>
)}
</div>
<div className="modal-footer">
<button
className="btn-cancelar"
onClick={() => setShowHistoricoModal(false)}
>
<FiX />
Fechar
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Devolucoes;