Atualização 4 | Ajustes sobre o financeiro

This commit is contained in:
daivid.alves 2026-02-23 18:09:44 -03:00
parent 6d7ec7c9aa
commit 538a75092d
26 changed files with 1445 additions and 723 deletions

View File

@ -30,5 +30,10 @@
"feature": "Corrigido erro nos cards de Pend\u00eancias/Revis\u00e3o do GR para Despachantes", "feature": "Corrigido erro nos cards de Pend\u00eancias/Revis\u00e3o do GR para Despachantes",
"status": "active", "status": "active",
"timestamp": "2026-02-08" "timestamp": "2026-02-08"
},
{
"feature": "Ajuste do painel Status de Cobran\u00e7a: centro com total de boletos e legenda vertical com quantitativo e tipografia fluida.",
"status": "active",
"timestamp": "2026-02-08"
} }
] ]

View File

@ -3,7 +3,7 @@ import { X, Filter, Trash2, Check, ChevronDown } from 'lucide-react';
import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const AdvancedFiltersModal = ({ isOpen, onClose, onApply, options = {}, initialFilters = {}, config = [] }) => { function AdvancedFiltersModal({ isOpen, onClose, onApply, options = {}, initialFilters = {}, config = [] }) {
const [filters, setFilters] = React.useState(initialFilters); const [filters, setFilters] = React.useState(initialFilters);
// Sync with parent when modal opens or initialFilters change // Sync with parent when modal opens or initialFilters change

View File

@ -0,0 +1,253 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import {
Loader2,
ChevronRight,
ChevronDown,
User,
Tag,
Receipt,
ArrowUpRight,
ArrowDownRight,
Wallet
} from 'lucide-react';
import { extratoService } from '@/services/extratoService';
import { formatCurrency, formatDate } from '../utils/dateUtils';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const CaixinhaDetailsModal = ({ isOpen, onClose, caixinhaId, caixinhaName, mes, ano }) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [expandedCategories, setExpandedCategories] = useState({});
const [expandedBeneficiaries, setExpandedBeneficiaries] = useState({});
useEffect(() => {
if (isOpen && caixinhaId) {
loadDetails();
} else {
setData(null);
setExpandedCategories({});
setExpandedBeneficiaries({});
}
}, [isOpen, caixinhaId, mes, ano]);
const loadDetails = async () => {
try {
setLoading(true);
const result = await extratoService.getCaixinhaDetalhada({
caixinha: caixinhaId,
mes,
ano
});
setData(result);
} catch (error) {
console.error('Erro ao carregar detalhes da caixinha:', error);
} finally {
setLoading(false);
}
};
const toggleCategory = (catName) => {
setExpandedCategories(prev => ({
...prev,
[catName]: !prev[catName]
}));
};
const toggleBeneficiary = (benefKey) => {
setExpandedBeneficiaries(prev => ({
...prev,
[benefKey]: !prev[benefKey]
}));
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-5xl h-[90vh] flex flex-col p-0 bg-white dark:bg-slate-900 border-none overflow-hidden sm:rounded-2xl">
<DialogHeader className="p-6 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800 shrink-0">
<div className="flex items-center justify-between">
<div>
<DialogTitle className="text-2xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-xl">
<Wallet className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
{caixinhaName || data?.caixinha || 'Detalhes da Caixinha'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 mt-1 font-medium">
Detalhamento por Categoria e Beneficiário {mes}/{ano}
</DialogDescription>
</div>
{data && (
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider mb-1">Total Movimentado</p>
<p className="text-xl font-bold text-emerald-600 dark:text-emerald-400 font-mono">
{formatCurrency(data.valor_total || 0)}
</p>
</div>
<div className="text-right">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider mb-1">Lançamentos</p>
<Badge variant="secondary" className="bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-200 font-bold">
{data.total_transacoes || 0}
</Badge>
</div>
</div>
)}
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-800">
{loading ? (
<div className="flex flex-col items-center justify-center h-full gap-4 text-slate-500">
<Loader2 className="w-10 h-10 animate-spin text-emerald-500" />
<p className="text-sm font-semibold animate-pulse">Carregando detalhes...</p>
</div>
) : !data || !data.categorias || data.categorias.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-3 text-slate-400">
<Receipt className="w-12 h-12 opacity-20" />
<p className="text-lg font-medium">Nenhuma movimentação encontrada</p>
</div>
) : (
<div className="space-y-4">
{data.categorias.map((cat, idx) => (
<div key={idx} className="border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden bg-white dark:bg-slate-900 shadow-sm transition-all duration-300">
<button
onClick={() => toggleCategory(cat.categoria)}
className="w-full flex items-center justify-between p-4 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors group"
>
<div className="flex items-center gap-4">
<div className={cn(
"p-2 rounded-lg transition-colors group-hover:bg-cyan-500/10",
expandedCategories[cat.categoria] ? "bg-cyan-500/10" : "bg-slate-100 dark:bg-slate-800"
)}>
<Tag className={cn(
"w-4 h-4 transition-colors",
expandedCategories[cat.categoria] ? "text-cyan-600 dark:text-cyan-400" : "text-slate-500"
)} />
</div>
<div className="text-left">
<span className="text-sm font-bold text-slate-900 dark:text-white uppercase tracking-tight">
{cat.categoria || "Sem Categoria"}
</span>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] text-slate-400 font-medium">
{cat.total_transacoes} transações
</span>
</div>
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-sm font-bold text-slate-900 dark:text-white font-mono">
{formatCurrency(cat.valor_total)}
</p>
</div>
{expandedCategories[cat.categoria] ? <ChevronDown className="w-4 h-4 text-slate-400" /> : <ChevronRight className="w-4 h-4 text-slate-400" />}
</div>
</button>
{expandedCategories[cat.categoria] && (
<div className="border-t border-slate-100 dark:border-slate-800 bg-slate-50/30 dark:bg-slate-900/50 p-4 space-y-3">
{cat.beneficiarios.map((benef, bIdx) => {
const benefKey = `${cat.categoria}-${benef.beneficiario}`;
return (
<div key={bIdx} className="bg-white dark:bg-slate-800/50 rounded-lg border border-slate-100 dark:border-slate-700 overflow-hidden shadow-sm">
<button
onClick={() => toggleBeneficiary(benefKey)}
className="w-full flex items-center justify-between p-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<User className="w-4 h-4 text-slate-400" />
<span className="text-xs font-semibold text-slate-700 dark:text-slate-300">
{benef.beneficiario || 'Não informado'}
</span>
<Badge variant="outline" className="text-[9px] font-bold border-slate-200 text-slate-500 h-4">
{benef.total_transacoes}
</Badge>
</div>
<div className="flex items-center gap-4">
<span className="text-xs font-bold text-slate-600 dark:text-slate-400 font-mono">
{formatCurrency(benef.valor_total)}
</span>
{expandedBeneficiaries[benefKey] ? <ChevronDown className="w-3 h-3 text-slate-400" /> : <ChevronRight className="w-3 h-3 text-slate-400" />}
</div>
</button>
{expandedBeneficiaries[benefKey] && (
<div className="p-2 bg-slate-50/50 dark:bg-slate-900/30">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent border-slate-200 dark:border-slate-800">
<TableHead className="text-[10px] h-8 font-bold text-slate-500 uppercase">Data</TableHead>
<TableHead className="text-[10px] h-8 font-bold text-slate-500 uppercase">Descrição</TableHead>
<TableHead className="text-[10px] h-8 font-bold text-slate-500 uppercase">Tipo</TableHead>
<TableHead className="text-[10px] h-8 font-bold text-slate-500 uppercase text-right">Valor</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{benef.transacoes.map((t, tIdx) => (
<TableRow key={tIdx} className="hover:bg-white dark:hover:bg-slate-800 border-slate-100 dark:border-slate-800/50">
<TableCell className="text-[11px] py-2 text-slate-500 font-medium">
{formatDate(t.dataEntrada).split(' ')[0]}
</TableCell>
<TableCell className="text-[11px] py-2 text-slate-700 dark:text-slate-300 font-medium max-w-[200px] truncate">
{t.descricao}
</TableCell>
<TableCell className="text-[11px] py-2">
<div className="flex items-center gap-1">
{t.tipoOperacao === 'C' ?
<ArrowUpRight className="w-3 h-3 text-emerald-500" /> :
<ArrowDownRight className="w-3 h-3 text-rose-500" />
}
<span className={cn(
"font-bold",
t.tipoOperacao === 'C' ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400"
)}>
{t.tipoOperacao === 'C' ? 'Entrada' : 'Saída'}
</span>
</div>
</TableCell>
<TableCell className={cn(
"text-[11px] py-2 text-right font-bold font-mono",
t.tipoOperacao === 'C' ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400"
)}>
{formatCurrency(t.valor)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
);
})}
</div>
)}
</div>
))}
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default CaixinhaDetailsModal;

View File

@ -16,14 +16,14 @@ import { cn } from '@/lib/utils';
import { formatDate, formatCurrency } from '../utils/dateUtils'; import { formatDate, formatCurrency } from '../utils/dateUtils';
import { useToast } from '../hooks/useToast'; import { useToast } from '../hooks/useToast';
export const CategorizacaoDialog = ({ export function CategorizacaoDialog({
transacao, transacao,
isOpen, isOpen,
onOpenChange, onOpenChange,
categorias = [], categorias = [],
caixas = [], caixas = [],
actions actions
}) => { }) {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
descricao: '', descricao: '',
categoria: '', categoria: '',
@ -135,14 +135,14 @@ export const CategorizacaoDialog = ({
} }
}; };
const ValorDisplay = ({ valor, tipo }) => { function ValorDisplay({ valor, tipo }) {
const isCredit = tipo === 'CREDITO' || tipo === 'C' || valor > 0; const isCredit = tipo === 'CREDITO' || tipo === 'C' || valor > 0;
return ( return (
<span className={cn("text-sm font-bold", isCredit ? "text-emerald-600" : "text-rose-600")}> <span className={cn("text-sm font-bold", isCredit ? "text-emerald-600" : "text-rose-600")}>
{formatCurrency(Math.abs(valor))} {formatCurrency(Math.abs(valor))}
</span> </span>
); );
}; }
return ( return (
<> <>

View File

@ -19,7 +19,7 @@ import { conciliacaoService } from '@/services/conciliacaoService';
import { useToast } from '../hooks/useToast'; import { useToast } from '../hooks/useToast';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export const CategoryRulesPopup = ({ isOpen, onClose, category, onUpdate }) => { export function CategoryRulesPopup({ isOpen, onClose, category, onUpdate }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [rules, setRules] = useState([]); const [rules, setRules] = useState([]);
const [allRules, setAllRules] = useState([]); const [allRules, setAllRules] = useState([]);

View File

@ -3,7 +3,7 @@ import { Edit2, Trash2, Filter, ChevronLeft, ChevronRight, ChevronsLeft, Chevron
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import AdvancedFiltersModal from './AdvancedFiltersModal'; import AdvancedFiltersModal from './AdvancedFiltersModal';
const ExcelTable = ({ function ExcelTable({
data = [], data = [],
columns, columns,
filterDefs = [], filterDefs = [],
@ -21,7 +21,7 @@ const ExcelTable = ({
pendingEdits = {}, // Objeto com edições pendentes: { [rowId]: { [field]: value } } pendingEdits = {}, // Objeto com edições pendentes: { [rowId]: { [field]: value } }
showValidationButton = false, // Mostra botão de validação no rodapé showValidationButton = false, // Mostra botão de validação no rodapé
onValidateEdits = null // Callback para validar edições pendentes onValidateEdits = null // Callback para validar edições pendentes
}) => { }) {
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({}); const [filters, setFilters] = useState({});
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });

View File

@ -4,20 +4,36 @@ import {
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogDescription,
DialogFooter DialogFooter
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from '@/components/ui/tabs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
AlertCircle,
ExternalLink,
Edit,
CreditCard,
Hash,
Info,
Banknote,
Percent,
History,
ArrowRightLeft,
Settings,
CheckCircle2,
ArrowUpRight, ArrowUpRight,
ArrowDownRight, ArrowDownRight,
Calendar, Calendar,
User, User,
FileText, FileText,
Tag, Tag
AlertCircle,
ExternalLink,
Edit
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils'; import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
@ -30,7 +46,7 @@ import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
* Modal de detalhamento de transação * Modal de detalhamento de transação
* Exibe todos os detalhes de uma transação do extrato * Exibe todos os detalhes de uma transação do extrato
*/ */
export const TransactionDetailModal = ({ export function TransactionDetailModal({
transaction, transaction,
open, open,
onOpenChange, onOpenChange,
@ -38,19 +54,19 @@ export const TransactionDetailModal = ({
onViewInExtrato, onViewInExtrato,
categorias = [], categorias = [],
caixas = [] caixas = []
}) => { }) {
if (!transaction) return null; if (!transaction) return null;
const isCredit = transaction.tipoOperacao === 'C'; const isCredit = transaction.tipoOperacao === 'C';
const isConciliado = transaction.categoria && transaction.categoria != 0; const isConciliado = transaction.categoria && transaction.categoria != 0;
// Helper para buscar nome da categoria // Helper para buscar nome da categoria
const getCategoriaNome = () => { function getCategoriaNome() {
if (transaction.categoriaNome) return transaction.categoriaNome; if (transaction.categoriaNome) return transaction.categoriaNome;
if (!transaction.categoria || transaction.categoria == 0) return 'Não conciliado'; if (!transaction.categoria || transaction.categoria == 0) return 'Não conciliado';
const cat = categorias.find(c => String(c.id) === String(transaction.categoria) || String(c.idcategoria) === String(transaction.categoria)); const cat = categorias.find(c => String(c.id) === String(transaction.categoria) || String(c.idcategoria) === String(transaction.categoria));
return cat ? (cat.name || cat.nome || cat.categoria) : `Categoria ${transaction.categoria}`; return cat ? (cat.name || cat.nome || cat.categoria) : 'Categoria ' + transaction.categoria;
}; };
// Helper para buscar nome da caixinha // Helper para buscar nome da caixinha
@ -65,183 +81,191 @@ export const TransactionDetailModal = ({
const caixinhaNome = getCaixinhaNome(); const caixinhaNome = getCaixinhaNome();
const InfoField = ({ label, value, icon: Icon, className = "" }) => (
<div className={cn("space-y-1.5", className)}>
<span className="text-[10px] text-slate-500 dark:text-slate-400 font-black uppercase tracking-widest flex items-center gap-1.5">
{Icon && <Icon size={12} className="text-slate-400" />}
{label}
</span>
<p className="text-sm font-semibold text-slate-900 dark:text-white truncate">
{value === null || value === undefined || value === "" || value === 0 ? (
<span className="text-slate-300 dark:text-slate-700 italic font-normal text-xs">Não informado</span>
) : (
typeof value === 'number' && label.toLowerCase().includes('valor') || ['juros', 'multa', 'abatimento', 'imposto', 'desconto'].some(k => label.toLowerCase().includes(k))
? formatCurrency(value)
: value
)}
</p>
</div>
);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 max-w-2xl z-[9999]"> <DialogContent className="bg-white dark:bg-[#0b1120] border-slate-200 dark:border-slate-800 max-w-2xl p-0 overflow-hidden z-[9999]">
<DialogHeader> {/* Header Superior - Destaque */}
<div className="flex items-center justify-between"> <div className={cn(
"p-6 border-b border-slate-100 dark:border-slate-800",
isCredit ? "bg-emerald-500/5" : "bg-rose-500/5"
)}>
<div className="flex items-center justify-between gap-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className={cn( <div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center", "w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm",
isCredit ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-500" : "bg-red-500/10 text-red-600 dark:text-red-500" isCredit ? "bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20" : "bg-rose-500/10 text-rose-600 dark:bg-rose-500/20"
)}> )}>
{isCredit ? <ArrowUpRight size={24} /> : <ArrowDownRight size={24} />} {isCredit ? <ArrowUpRight size={28} /> : <ArrowDownRight size={28} />}
</div> </div>
<div> <div className="space-y-1">
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white"> <DialogDescription className="sr-only">
{transaction.titulo || 'Transação'} Detalhamento da transação bancária
</DialogDescription>
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white line-clamp-1">
{transaction.titulo || transaction.descricao || 'Transação sem título'}
</DialogTitle> </DialogTitle>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2">
<Badge className={cn( <Badge variant="outline" className="text-[9px] font-black tracking-tighter uppercase border-slate-200 dark:border-slate-800">
"text-[10px] font-bold px-2 py-0.5", {transaction.tipoTransacao || 'Geral'}
isCredit ? "bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : "bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400"
)}>
{transaction.tipoTransacao || 'GERAL'}
</Badge> </Badge>
<Badge className={cn( <Badge className={cn(
"text-[10px] font-bold px-2 py-0.5", "text-[9px] font-black tracking-tighter uppercase",
isConciliado ? "bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : "bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400" isConciliado ? "bg-emerald-500/10 text-emerald-600 border-emerald-500/20" : "bg-amber-500/10 text-amber-600 border-amber-500/20"
)}> )}>
{isConciliado ? 'CONCILIADO' : 'PENDENTE'} {isConciliado ? 'Conciliado' : 'Pendente'}
</Badge> </Badge>
</div> </div>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Valor Total</p>
<div className={cn( <div className={cn(
"text-2xl font-bold", "text-2xl font-black tabular-nums",
isCredit ? "text-emerald-600 dark:text-emerald-500" : "text-red-600 dark:text-red-500" isCredit ? "text-emerald-600" : "text-rose-600"
)}> )}>
{isCredit ? '+' : '-'}{formatCurrency(transaction.valor)} {isCredit ? '+' : '-'}{formatCurrency(transaction.valorTotal || transaction.valor)}
</div> </div>
</div> </div>
</div> </div>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Informações Básicas */}
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
<FileText size={16} />
Informações Básicas
</h3>
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">ID da Transação</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">#{transaction.idextrato || 'N/D'}</p>
</div>
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase flex items-center gap-1">
<Calendar size={12} />
Data e Hora
</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{formatDateTime(transaction.dataEntrada)}</p>
</div>
<div className="col-span-2">
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Descrição</span>
<p className="text-sm text-slate-900 dark:text-white mt-1">{transaction.descricao || 'Sem descrição'}</p>
</div>
</div>
</div>
{/* Partes Envolvidas */}
{(transaction.beneficiario_pagador || transaction.cpfCnpjPagador || transaction.cpfCnpjRecebedor) && (
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
<User size={16} />
Partes Envolvidas
</h3>
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
{transaction.beneficiario_pagador && (
<div className="col-span-2">
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Beneficiário/Pagador</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{transaction.beneficiario_pagador}</p>
</div>
)}
{transaction.cpfCnpjPagador && (
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">CPF/CNPJ Pagador</span>
<p className="text-sm font-mono text-slate-900 dark:text-white mt-1">{transaction.cpfCnpjPagador}</p>
</div>
)}
{transaction.cpfCnpjRecebedor && (
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">CPF/CNPJ Recebedor</span>
<p className="text-sm font-mono text-slate-900 dark:text-white mt-1">{transaction.cpfCnpjRecebedor}</p>
</div>
)}
</div>
</div>
)}
{/* Conciliação */}
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
<Tag size={16} />
Conciliação
</h3>
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Categoria</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">
{categoriaNome}
</p>
</div>
<div>
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Regra Aplicada</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">
{transaction.regra ? `Regra #${transaction.regra}` : 'N/D'}
</p>
</div>
{transaction.caixinha && (
<div className="col-span-2">
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Caixinha</span>
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{caixinhaNome}</p>
</div>
)}
</div>
</div>
{/* Observações */}
{transaction.adicionado && (
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
<AlertCircle size={16} />
Observações
</h3>
<div className="bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
<p className="text-sm text-slate-900 dark:text-white">{transaction.adicionado}</p>
</div>
</div>
)}
</div> </div>
<DialogFooter className="flex gap-2"> <Tabs defaultValue="geral" className="w-full">
{onEditTransaction && ( <div className="px-6 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
<TabsList className="h-12 bg-transparent gap-6 p-0 w-full justify-start overflow-x-auto no-scrollbar">
<TabsTrigger value="geral" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
Geral
</TabsTrigger>
<TabsTrigger value="financeiro" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
Financeiro
</TabsTrigger>
<TabsTrigger value="participantes" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
Participantes
</TabsTrigger>
<TabsTrigger value="tecnico" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
Técnico
</TabsTrigger>
<TabsTrigger value="conciliacao" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
Conciliação
</TabsTrigger>
</TabsList>
</div>
<div className="p-6 h-[350px] overflow-y-auto custom-scrollbar">
<TabsContent value="geral" className="mt-0 outline-none space-y-6">
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
<InfoField label="Título" value={transaction.titulo} icon={Info} className="col-span-2" />
<InfoField label="Data de Entrada" value={formatDateTime(transaction.dataEntrada || transaction.dataTransacao || transaction.data)} icon={Calendar} />
<InfoField label="Tipo Transação" value={transaction.tipoTransacao} icon={Tag} />
<InfoField label="Valor Lançado" value={transaction.valor || transaction.valorTotal} icon={Banknote} />
<InfoField label="Operação" value={transaction.tipoOperacao === 'D' ? 'Débito' : 'Crédito'} icon={ArrowRightLeft} />
<div className="col-span-2 space-y-2">
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Descrição Completa</span>
<div className="p-3 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-lg text-xs leading-relaxed text-slate-600 dark:text-slate-400">
{transaction.descricao || 'Sem descrição detalhada disponível.'}
</div>
</div>
</div>
</TabsContent>
<TabsContent value="financeiro" className="mt-0 outline-none space-y-6">
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
<InfoField label="Valor Base/Original" value={transaction.valor} icon={Banknote} />
<InfoField label="Acréscimos (Adicionado)" value={transaction.adicionado} icon={ArrowUpRight} />
<InfoField label="Juros" value={transaction.juros} icon={Percent} />
<InfoField label="Multa" value={transaction.multa} icon={Percent} />
<InfoField label="Abatimento" value={transaction.abatimento} icon={ArrowDownRight} />
<InfoField label="Imposto Retido" value={transaction.imposto} icon={FileText} />
<InfoField label="Desconto 01" value={transaction.desconto1} icon={Tag} />
<InfoField label="Desconto 02" value={transaction.desconto2} icon={Tag} />
<InfoField label="Desconto 03" value={transaction.desconto3} icon={Tag} />
<InfoField label="Valor Final Liquido" value={transaction.valorTotal || transaction.valor} icon={Banknote} className="col-span-2 pt-4 border-t border-slate-100 dark:border-slate-800" />
</div>
</TabsContent>
<TabsContent value="participantes" className="mt-0 outline-none space-y-6">
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
<InfoField label="Principal Envolvido" value={transaction.beneficiario_pagador} icon={User} className="col-span-2" />
<InfoField label="Nome do Pagador" value={transaction.nomePagador} icon={User} />
<InfoField label="CPF/CNPJ Pagador" value={transaction.cpfCnpjPagador} icon={FileText} />
<InfoField label="Nome do Recebedor" value={transaction.nomeRecebedor} icon={User} />
<InfoField label="CPF/CNPJ Recebedor" value={transaction.cpfCnpjRecebedor} icon={FileText} />
<InfoField label="Estabelecimento" value={transaction.estabelecimento} icon={CreditCard} className="col-span-2" />
<InfoField label="Agência" value={transaction.agencia} icon={Hash} />
<InfoField label="Conta Bancária" value={transaction.contaBancaria} icon={CreditCard} />
<InfoField label="Chave PIX" value={transaction.chavePix} icon={Tag} className="col-span-2" />
</div>
</TabsContent>
<TabsContent value="tecnico" className="mt-0 outline-none space-y-6">
<div className="grid grid-cols-1 gap-y-6">
<InfoField label="ID no Extrato (Interno)" value={transaction.idextrato} icon={Hash} />
<InfoField label="ID da Transação (Banco)" value={transaction.idTransacao} icon={ExternalLink} />
<InfoField label="EndToEnd ID (PIX)" value={transaction.endToEndId} icon={ExternalLink} />
<InfoField label="Código de Barras" value={transaction.codigoBarras} icon={Hash} />
<InfoField label="Linha Digitável" value={transaction.linhaDigitavel} icon={FileText} />
<div className="grid grid-cols-2 gap-12">
<InfoField label="Nosso Número" value={transaction.nossoNumero} icon={Hash} />
<InfoField label="Seu Número" value={transaction.seuNumero} icon={Hash} />
</div>
<InfoField label="Origem da Movimentação" value={transaction.origemMovimentacao} icon={History} />
</div>
</TabsContent>
<TabsContent value="conciliacao" className="mt-0 outline-none space-y-6">
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
<InfoField label="Status Atual" value={isConciliado ? 'CONCILIADO' : 'PENDENTE'} icon={CheckCircle2} className="col-span-2" />
<InfoField label="Categoria" value={categoriaNome} icon={Tag} />
<InfoField label="Caixinha / Destino" value={caixinhaNome} icon={Banknote} />
<InfoField label="Regra Aplicada" value={transaction.regra ? 'ID #' + transaction.regra : null} icon={Settings} className="col-span-2" />
<InfoField label="Data de Emissão" value={transaction.dataEmissao} icon={Calendar} />
<InfoField label="Data de Vencimento" value={transaction.dataVencimento} icon={Calendar} />
<InfoField label="Data Limite" value={transaction.dataLimite} icon={Calendar} />
</div>
</TabsContent>
</div>
</Tabs>
<DialogFooter className="p-6 bg-slate-50/50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800">
<div className="flex w-full items-center justify-between">
<Button <Button
variant="outline" variant="ghost"
className="border-blue-500 text-blue-600 dark:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10" onClick={() => onOpenChange(false)}
onClick={() => { className="text-[10px] font-black uppercase tracking-widest text-slate-400 hover:text-slate-900 dark:hover:text-white"
onEditTransaction(transaction);
onOpenChange(false);
}}
> >
<Edit size={16} className="mr-2" /> Fechar Detalhes
Editar Transação
</Button> </Button>
)}
{/* {onEditTransaction && (
{onViewInExtrato && ( <Button
<Button className="bg-blue-600 hover:bg-blue-700 text-white rounded-xl h-11 px-6 shadow-lg shadow-blue-500/20 transition-all font-black text-[10px] uppercase tracking-widest"
variant="outline" onClick={() => {
className="border-slate-200 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800" onEditTransaction(transaction);
onClick={() => { onOpenChange(false);
onViewInExtrato(transaction); }}
onOpenChange(false); >
}} <Edit size={16} className="mr-2" />
> Editar Transação
<ExternalLink size={16} className="mr-2" /> </Button>
Visualizar no Extrato )}
</Button> </div>
)}
*/}
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
className="text-slate-600 dark:text-slate-400"
>
Fechar
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -0,0 +1,85 @@
import React, { useMemo } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { formatCurrency, formatDate } from '../utils/dateUtils';
import ExcelTable from './ExcelTable';
import { Badge } from '@/components/ui/badge';
const TransactionsByCategoryModal = ({ isOpen, onClose, categoryName, transactions }) => {
const columns = useMemo(() => [
{
field: 'data',
header: 'Data',
width: '120px',
render: (row) => <span>{formatDate(row.dataEntrada || row.data)}</span>
},
{
field: 'descricao',
header: 'Descrição',
width: '300px',
render: (row) => <span className="font-medium">{row.descricao}</span>
},
{
field: 'valor',
header: 'Valor',
width: '150px',
render: (row) => (
<span className={row.tipoOperacao === 'C' ? "text-emerald-500 font-mono" : "text-rose-500 font-mono"}>
{formatCurrency(row.valor || row.total || 0)}
</span>
)
},
{
field: 'beneficiario_pagador',
header: 'Beneficiário/Pagador',
width: '250px',
},
// {
// field: 'status',
// header: 'Status',
// width: '120px',
// render: (row) => (
// <Badge variant="outline" className={
// row.status === 'Recebido' || row.status === 'Liquidado' || row.status === 'Pago'
// ? "bg-emerald-500/10 text-emerald-500 border-emerald-500/20"
// : "bg-amber-500/10 text-amber-500 border-amber-500/20"
// }>
// {row.status || 'Pendente'}
// </Badge>
// )
// }
], []);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-5xl h-[80vh] flex flex-col p-6 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 shadow-2xl">
<DialogHeader className="mb-4">
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
Transações: <span className="text-emerald-500">{categoryName}</span>
</DialogTitle>
<DialogDescription className="sr-only">
Listagem detalhada de transações para a categoria {categoryName}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden">
<ExcelTable
data={transactions}
columns={columns}
rowKey="idextrato"
pageSize={20}
/>
</div>
</DialogContent>
</Dialog>
);
};
export default TransactionsByCategoryModal;

View File

@ -51,27 +51,16 @@ const MOCK_TRANSACOES_NAO_CATEGORIZADAS = [
/** /**
* Hook para gerenciar a lógica de Conciliação V2 * Hook para gerenciar a lógica de Conciliação V2
*/ */
export const useConciliacaoV2 = (defaultView = 'conciliadas') => { export function useConciliacaoV2(defaultView = 'conciliadas') {
console.log('[useConciliacaoV2] Hook iniciado'); console.log('[useConciliacaoV2] Hook iniciado');
let toast; const toast = useToast();
try { console.log('[useConciliacaoV2] Toast inicializado');
toast = useToast();
console.log('[useConciliacaoV2] Toast inicializado');
} catch (error) {
console.error('[useConciliacaoV2] Erro ao inicializar toast:', error);
// Fallback para toast básico
toast = {
success: (msg, title) => console.log('[Toast]', title, msg),
error: (msg, title) => console.error('[Toast]', title, msg),
notifyFields: (fields) => console.warn('[Toast] Campos obrigatórios:', fields)
};
}
// Normalização de alias de rotas // Normalização de alias de rotas
const normalizeSubView = (view) => { const normalizeSubView = (view) => {
if (!view) return 'conciliadas'; if (!view) return 'conciliadas';
if (view === 'extrato') return 'conciliadas'; if (view === 'extrato' || view === 'extrato-completo') return 'extrato-completo';
if (view === 'pendentes') return 'nao-categorizadas'; if (view === 'pendentes') return 'nao-categorizadas';
return view; return view;
}; };
@ -126,6 +115,175 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
return data; return data;
}; };
// Função auxiliar para recarregar dados
const recarregarDados = async () => {
try {
setIsLoading(true);
const extrato = await workspaceConciliacaoService.fetchExtrato();
setExtratoCompleto(Array.isArray(extrato) ? extrato : []);
// Separar transações conciliadas e não categorizadas
const conciliadas = extrato.filter(t => t.status === 'CONCILIADA' && t.categoriaId);
const naoCategorizadas = extrato.filter(t => t.status === 'PENDENTE' || !t.categoriaId);
setTransacoesConciliadas(conciliadas);
setTransacoesNaoCategorizadas(naoCategorizadas);
} catch (error) {
console.error('[useConciliacaoV2] Erro ao recarregar dados:', error);
} finally {
setIsLoading(false);
}
};
// Busca dados para o nível de navegação atual USANDO A NOVA ROTA HIERÁRQUICA
const fetchNivelNavegacao = async (nivel, filters = {}) => {
setIsNavLoading(true);
console.log('[useConciliacaoV2] Buscando dados para nível:', nivel, filters);
try {
let data = [];
if (nivel === 0) {
// Nível 0: Caixas (Agora usando a rota detalhada para cada caixa para matar o 'cruzamentos')
const sourceCaixas = filters.caixas || caixas;
// Buscar totais para cada caixa usando a rota detalhada em paralelo
console.log('[useConciliacaoV2] Buscando totais detalhados para todas as caixas (Substituindo cruzamentos)...');
const enrichedCaixas = await Promise.all(sourceCaixas.map(async (c) => {
try {
const detailed = await workspaceConciliacaoService.fetchExtratoDetalhado(c.id, { mes: filtroMes, ano: filtroAno });
// Fallback: Calcular totais manualmente se o resumo não vier
const categorias = detailed.categorias || [];
const calcValor = categorias.reduce((acc, cat) => acc + (Number(cat.valor_total) || 0), 0);
const calcTransacoes = categorias.reduce((acc, cat) => {
const catTotal = cat.total_transacoes || (cat.beneficiarios || []).reduce((bAcc, ben) => bAcc + (ben.total_transacoes || 0), 0);
return acc + catTotal;
}, 0);
return {
...c,
totalTransacoes: detailed.resumo?.total_transacoes || calcTransacoes,
totalValor: detailed.resumo?.valor_total || calcValor,
tipo: 'caixa'
};
} catch (e) {
console.warn(`Erro ao buscar detalhe da caixa ${c.id}:`, e);
return { ...c, totalTransacoes: 0, totalValor: 0, tipo: 'caixa' };
}
}));
data = enrichedCaixas;
} else if (nivel === 1) {
// Nível 1: Categorias (Vindo da API Detalhada da Caixinha)
const caixinhaId = filters.caixinha?.id || filters.caixa?.id || caixaSelecionado?.id;
if (!caixinhaId) throw new Error('Caixinha não selecionada para nível 1');
console.log('[useConciliacaoV2] >>> ACESSANDO NOVA ROTA HIERÁRQUICA DO BACKEND <<<');
console.log(`[useConciliacaoV2] Rota: /extrato/apresentar/caixinha/detalhado?caixinha=${caixinhaId}&mes=${filtroMes}&ano=${filtroAno}`);
const detailedData = await workspaceConciliacaoService.fetchExtratoDetalhado(caixinhaId, { mes: filtroMes, ano: filtroAno });
data = (detailedData.categorias || []).map(cat => {
// Calcular sub-totais de transações se não vier
const subTransacoes = cat.total_transacoes || (cat.beneficiarios || []).reduce((acc, ben) => acc + (ben.total_transacoes || 0), 0);
return {
id: `cat_${cat.categoria}`,
nome: cat.categoria || 'Sem Categoria',
totalTransacoes: subTransacoes,
totalValor: cat.valor_total || 0,
tipo: 'categoria',
beneficiarios: cat.beneficiarios || [],
cor: categorias.find(c => c.nome === cat.categoria)?.cor || '#3b82f6'
};
});
} else if (nivel === 2) {
// Nível 2: Beneficiários (Dados já estão no item selecionado)
const categoriaItem = filters.categoria || categoriaSelecionada;
data = (categoriaItem?.beneficiarios || []).map(ben => ({
id: `ben_${ben.beneficiario}`,
nome: ben.beneficiario || 'Sem Beneficiário',
beneficiario: ben.beneficiario,
totalTransacoes: ben.total_transacoes || 0,
totalValor: ben.valor_total || 0,
tipo: 'regra',
transacoes: ben.transacoes || []
}));
} else if (nivel === 3) {
// Nível 3: Transações (Normalizar dados para garantir compatibilidade com gráficos/tabelas)
const detalheItem = filters.regra || detalheSelecionado;
const rawTransacoes = detalheItem?.transacoes || [];
data = rawTransacoes.map(t => ({
...t,
id: t.id || t.idextrato || Math.random(),
data: t.data || t.dataEntrada || t.data_entrada || '',
valor: Number(t.valor || t.valor_total || 0),
descricao: t.descricao || t.historico || '',
beneficiario: t.beneficiario || t.beneficiario_pagador || ''
}));
}
setBackendNavData(data);
return data;
} catch (error) {
console.error('[useConciliacaoV2] Erro ao buscar dados de navegação:', error);
toast.error('Erro ao navegar nos dados', 'Erro');
return [];
} finally {
setIsNavLoading(false);
}
};
// Navegação hierárquica usando dados do backend
const navegarPara = async (tipo, item) => {
console.log('[useConciliacaoV2] Navegando para:', tipo, item);
let novoNivel = 0;
if (tipo === 'caixa') {
setCaixaSelecionado(item);
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
novoNivel = 1;
setCaminhoNavegacao([{ tipo: 'caixa', item }]);
} else if (tipo === 'categoria') {
setCategoriaSelecionada(item);
setDetalheSelecionado(null);
novoNivel = 2;
setCaminhoNavegacao(prev => [...prev, { tipo: 'categoria', item }]);
} else if (tipo === 'regra') {
setDetalheSelecionado(item);
novoNivel = 3;
setCaminhoNavegacao(prev => [...prev, { tipo: 'regra', item }]);
}
setNivelNavegacao(novoNivel);
await fetchNivelNavegacao(novoNivel, { [tipo]: item });
};
const voltarNavegacao = async () => {
if (nivelNavegacao === 0) return;
const novoNivel = nivelNavegacao - 1;
setNivelNavegacao(novoNivel);
setCaminhoNavegacao(prev => prev.slice(0, novoNivel));
if (novoNivel === 0) {
setCaixaSelecionado(null);
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
await fetchNivelNavegacao(0);
} else if (novoNivel === 1) {
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
await fetchNivelNavegacao(1, { caixinha: caixaSelecionado });
} else if (novoNivel === 2) {
setDetalheSelecionado(null);
await fetchNivelNavegacao(2, { categoria: categoriaSelecionada });
}
};
// Carregar dados iniciais do backend // Carregar dados iniciais do backend
useEffect(() => { useEffect(() => {
console.log('[useConciliacaoV2] useEffect executado - montando componente'); console.log('[useConciliacaoV2] useEffect executado - montando componente');
@ -662,24 +820,6 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
*/ */
}; };
// Função auxiliar para recarregar dados
const recarregarDados = async () => {
try {
setIsLoading(true);
const extrato = await workspaceConciliacaoService.fetchExtrato();
setExtratoCompleto(Array.isArray(extrato) ? extrato : []);
// Separar transações conciliadas e não categorizadas
const conciliadas = extrato.filter(t => t.status === 'CONCILIADA' && t.categoriaId);
const naoCategorizadas = extrato.filter(t => t.status === 'PENDENTE' || !t.categoriaId);
setTransacoesConciliadas(conciliadas);
setTransacoesNaoCategorizadas(naoCategorizadas);
} catch (error) {
console.error('[useConciliacaoV2] Erro ao recarregar dados:', error);
} finally {
setIsLoading(false);
}
};
// Categorizar transação não categorizada // Categorizar transação não categorizada
const categorizarTransacao = async (transacaoId, dadosCategorizacao) => { const categorizarTransacao = async (transacaoId, dadosCategorizacao) => {
@ -746,155 +886,6 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
} }
}; };
// Busca dados para o nível de navegação atual USANDO A NOVA ROTA HIERÁRQUICA
const fetchNivelNavegacao = async (nivel, filters = {}) => {
setIsNavLoading(true);
console.log('[useConciliacaoV2] Buscando dados para nível:', nivel, filters);
try {
let data = [];
if (nivel === 0) {
// Nível 0: Caixas (Agora usando a rota detalhada para cada caixa para matar o 'cruzamentos')
const sourceCaixas = filters.caixas || caixas;
// Buscar totais para cada caixa usando a rota detalhada em paralelo
console.log('[useConciliacaoV2] Buscando totais detalhados para todas as caixas (Substituindo cruzamentos)...');
const enrichedCaixas = await Promise.all(sourceCaixas.map(async (c) => {
try {
const detailed = await workspaceConciliacaoService.fetchExtratoDetalhado(c.id, { mes: filtroMes, ano: filtroAno });
// Fallback: Calcular totais manualmente se o resumo não vier
const categorias = detailed.categorias || [];
const calcValor = categorias.reduce((acc, cat) => acc + (Number(cat.valor_total) || 0), 0);
const calcTransacoes = categorias.reduce((acc, cat) => {
const catTotal = cat.total_transacoes || (cat.beneficiarios || []).reduce((bAcc, ben) => bAcc + (ben.total_transacoes || 0), 0);
return acc + catTotal;
}, 0);
return {
...c,
totalTransacoes: detailed.resumo?.total_transacoes || calcTransacoes,
totalValor: detailed.resumo?.valor_total || calcValor,
tipo: 'caixa'
};
} catch (e) {
console.warn(`Erro ao buscar detalhe da caixa ${c.id}:`, e);
return { ...c, totalTransacoes: 0, totalValor: 0, tipo: 'caixa' };
}
}));
data = enrichedCaixas;
} else if (nivel === 1) {
// Nível 1: Categorias (Vindo da API Detalhada da Caixinha)
const caixinhaId = filters.caixinha?.id || filters.caixa?.id || caixaSelecionado?.id;
if (!caixinhaId) throw new Error('Caixinha não selecionada para nível 1');
console.log('[useConciliacaoV2] >>> ACESSANDO NOVA ROTA HIERÁRQUICA DO BACKEND <<<');
console.log(`[useConciliacaoV2] Rota: /extrato/apresentar/caixinha/detalhado?caixinha=${caixinhaId}&mes=${filtroMes}&ano=${filtroAno}`);
const detailedData = await workspaceConciliacaoService.fetchExtratoDetalhado(caixinhaId, { mes: filtroMes, ano: filtroAno });
data = (detailedData.categorias || []).map(cat => {
// Calcular sub-totais de transações se não vier
const subTransacoes = cat.total_transacoes || (cat.beneficiarios || []).reduce((acc, ben) => acc + (ben.total_transacoes || 0), 0);
return {
id: `cat_${cat.categoria}`,
nome: cat.categoria || 'Sem Categoria',
totalTransacoes: subTransacoes,
totalValor: cat.valor_total || 0,
tipo: 'categoria',
beneficiarios: cat.beneficiarios || [],
cor: categorias.find(c => c.nome === cat.categoria)?.cor || '#3b82f6'
};
});
} else if (nivel === 2) {
// Nível 2: Beneficiários (Dados já estão no item selecionado)
const categoriaItem = filters.categoria || categoriaSelecionada;
data = (categoriaItem?.beneficiarios || []).map(ben => ({
id: `ben_${ben.beneficiario}`,
nome: ben.beneficiario || 'Sem Beneficiário',
beneficiario: ben.beneficiario,
totalTransacoes: ben.total_transacoes || 0,
totalValor: ben.valor_total || 0,
tipo: 'regra',
transacoes: ben.transacoes || []
}));
} else if (nivel === 3) {
// Nível 3: Transações (Normalizar dados para garantir compatibilidade com gráficos/tabelas)
const detalheItem = filters.regra || detalheSelecionado;
const rawTransacoes = detalheItem?.transacoes || [];
data = rawTransacoes.map(t => ({
...t,
id: t.id || t.idextrato || Math.random(),
data: t.data || t.dataEntrada || t.data_entrada || '',
valor: Number(t.valor || t.valor_total || 0),
descricao: t.descricao || t.historico || '',
beneficiario: t.beneficiario || t.beneficiario_pagador || ''
}));
}
setBackendNavData(data);
return data;
} catch (error) {
console.error('[useConciliacaoV2] Erro ao buscar dados de navegação:', error);
toast.error('Erro ao navegar nos dados', 'Erro');
return [];
} finally {
setIsNavLoading(false);
}
};
// Navegação hierárquica usando dados do backend
const navegarPara = async (tipo, item) => {
console.log('[useConciliacaoV2] Navegando para:', tipo, item);
let novoNivel = 0;
if (tipo === 'caixa') {
setCaixaSelecionado(item);
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
novoNivel = 1;
setCaminhoNavegacao([{ tipo: 'caixa', item }]);
} else if (tipo === 'categoria') {
setCategoriaSelecionada(item);
setDetalheSelecionado(null);
novoNivel = 2;
setCaminhoNavegacao(prev => [...prev, { tipo: 'categoria', item }]);
} else if (tipo === 'regra') {
setDetalheSelecionado(item);
novoNivel = 3;
setCaminhoNavegacao(prev => [...prev, { tipo: 'regra', item }]);
}
setNivelNavegacao(novoNivel);
await fetchNivelNavegacao(novoNivel, { [tipo]: item });
};
const voltarNavegacao = async () => {
if (nivelNavegacao === 0) return;
const novoNivel = nivelNavegacao - 1;
setNivelNavegacao(novoNivel);
setCaminhoNavegacao(prev => prev.slice(0, novoNivel));
if (novoNivel === 0) {
setCaixaSelecionado(null);
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
await fetchNivelNavegacao(0);
} else if (novoNivel === 1) {
setCategoriaSelecionada(null);
setDetalheSelecionado(null);
await fetchNivelNavegacao(1, { caixinha: caixaSelecionado });
} else if (novoNivel === 2) {
setDetalheSelecionado(null);
await fetchNivelNavegacao(2, { categoria: categoriaSelecionada });
}
};
const exportarPDF = async () => { const exportarPDF = async () => {
try { try {

View File

@ -287,6 +287,7 @@ export const useDashboard = () => {
const getBoletosPieData = () => { const getBoletosPieData = () => {
const values = {}; const values = {};
const counts = {};
const mesAtual = formatMesAno(new Date()); const mesAtual = formatMesAno(new Date());
data.boletos.cobrancas?.forEach((item) => { data.boletos.cobrancas?.forEach((item) => {
@ -296,6 +297,7 @@ export const useDashboard = () => {
const st = c.situacao || 'OUTROS'; const st = c.situacao || 'OUTROS';
const valor = safeNumber(c.valorNominal); const valor = safeNumber(c.valorNominal);
values[st] = (values[st] || 0) + valor; values[st] = (values[st] || 0) + valor;
counts[st] = (counts[st] || 0) + 1;
}); });
const colors = { const colors = {
@ -309,6 +311,7 @@ export const useDashboard = () => {
return Object.entries(values).map(([name, value]) => ({ return Object.entries(values).map(([name, value]) => ({
name, name,
value, value,
count: counts[name] || 0,
color: colors[name] || colors['OUTROS'] color: colors[name] || colors['OUTROS']
})); }));
}; };

View File

@ -35,6 +35,7 @@ export function useFluxoCaixa() {
const [fluxoData, setFluxoData] = useState({ mensal: [] }); const [fluxoData, setFluxoData] = useState({ mensal: [] });
const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] }); const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] });
const [saldoConsolidado, setSaldoConsolidado] = useState(null); const [saldoConsolidado, setSaldoConsolidado] = useState(null);
const [porCaixinha, setPorCaixinha] = useState([]);
const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1)); const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1));
const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString()); const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString());
@ -47,24 +48,188 @@ export function useFluxoCaixa() {
const mesAtu = filtroMes; const mesAtu = filtroMes;
const anoAtu = filtroAno; const anoAtu = filtroAno;
const tipo = filtroTipo;
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData] = await Promise.all([ // Monta os parâmetros de forma dinâmica conforme o tipo de filtro
extratoService.fetchExtrato(), const params = {};
if (tipo === 'mes') {
params.mes = mesAtu;
params.ano = anoAtu;
} else if (tipo === 'ano') {
params.ano = anoAtu;
}
// Se for 'todos', params fica vazio
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData, caixinhasList, categoriasList] = await Promise.all([
extratoService.fetchExtrato(params),
extratoService.fetchSaldo(), extratoService.fetchSaldo(),
extratoService.fetchSaldoArmazenado(), extratoService.fetchSaldoArmazenado(),
extratoService.fetchFluxo(), extratoService.fetchFluxo(params),
extratoService.getSomaPorCategoria({ mes: mesAtu, ano: anoAtu }), extratoService.getSomaPorCategoria(params),
extratoService.fetchSaldoConsolidado({ mes: mesAtu, ano: anoAtu }) extratoService.fetchSaldoConsolidado(params),
extratoService.fetchCaixinhas(),
extratoService.fetchCategorias()
]); ]);
setExtrato(Array.isArray(extratoData) ? extratoData : []); // --- CLIENT-SIDE FILTERING FALLBACK ---
const matchesPeriod = (itemDate) => {
if (!itemDate || tipo === 'todos') return true;
// formats: YYYY-MM-DD or DD/MM/YYYY or ISO or "Wed, 11 Feb 2026 00:00:00 GMT"
let d;
if (typeof itemDate === 'string') {
if (itemDate.includes('/')) {
const [day, month, year] = itemDate.split('/');
d = new Date(year, month - 1, day);
} else {
d = new Date(itemDate);
}
} else {
d = new Date(itemDate);
}
if (Number.isNaN(d.getTime())) return true;
const itemMonth = String(d.getMonth() + 1);
const itemYear = String(d.getFullYear());
if (tipo === 'mes') {
return itemMonth === mesAtu && itemYear === anoAtu;
} else if (tipo === 'ano') {
return itemYear === anoAtu;
}
return true;
};
// 0. Build Comprehensive Categories Map
const categoriesMap = {};
// Source A: /categorias/apresentar
if (Array.isArray(categoriasList)) {
categoriasList.forEach(c => {
const cId = String(c.idcategoria ?? c.idcategorias ?? c.id ?? '');
if (cId) {
categoriesMap[cId] = c.categoria || c.name || c.nome || c.descricao;
}
});
}
// Source B: Labels already in /extrato/soma_por_categoria
const rawPorCategoria = somaCategoriaData?.por_categoria || [];
rawPorCategoria.forEach(cat => {
const cId = String(cat.idcategoria ?? '');
if (cId && cat.categoria && isNaN(cat.categoria)) {
if (!categoriesMap[cId] || categoriesMap[cId].startsWith('Categoria ')) {
categoriesMap[cId] = cat.categoria;
}
}
});
// 1. Process Extrato (Table Data) - Consolidate labels
const rawExtrato = Array.isArray(extratoData) ? extratoData : [];
const filteredExtrato = rawExtrato.filter(item =>
matchesPeriod(item.dataEntrada || item.data || item.data_entrada)
);
setExtrato(filteredExtrato.map(item => {
// ID can be in 'categoria' or 'idcategoria'
let idVal = item.idcategoria || '';
if (!idVal && item.categoria && !isNaN(item.categoria)) idVal = item.categoria;
const id = String(idVal || '0');
const catName = categoriesMap[id] ||
item.categoria_nome ||
(isNaN(item.categoria) ? item.categoria : null) ||
(id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
return {
...item,
categoria_nome: catName
};
}));
// 2. Aggregate Categories (Chart Data) - Unified C and D for the same category
const aggregatedSummary = {};
rawPorCategoria.forEach(cat => {
const id = String(cat.idcategoria ?? '0');
const name = categoriesMap[id] || cat.categoria || (id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
if (!aggregatedSummary[id]) {
aggregatedSummary[id] = {
idcategoria: id,
categoria: name,
total_entradas: 0,
total_saidas: 0,
total: 0,
total_transacoes: 0
};
}
const value = Math.abs(Number(cat.total) || 0);
if (cat.tipoOperacao === 'C') {
aggregatedSummary[id].total_entradas += value;
} else {
aggregatedSummary[id].total_saidas += value;
}
aggregatedSummary[id].total += value;
aggregatedSummary[id].total_transacoes += Number(cat.total_transacoes || 0);
});
setSomaCategoria({
por_categoria: Object.values(aggregatedSummary).sort((a, b) => b.total - a.total)
});
// 3. Process Caixinhas
const caixinhasMap = {};
if (Array.isArray(caixinhasList)) {
caixinhasList.forEach(c => {
caixinhasMap[String(c.idcaixinhas_financeiro || c.id)] = c.caixinha || c.name;
});
}
const groupedCaixinha = {};
filteredExtrato.forEach(item => {
const id = String(item.caixinha || '0');
if (!groupedCaixinha[id]) {
groupedCaixinha[id] = {
id: id,
caixinha: caixinhasMap[id] || (id === '0' ? '(sem caixinha)' : `Caixinha ${id}`),
total_entradas: 0,
total_saidas: 0,
total: 0,
diferenca: 0
};
}
const value = Math.abs(Number(item.valor) || 0);
if (item.tipoOperacao === 'C') {
groupedCaixinha[id].total_entradas += value;
} else {
groupedCaixinha[id].total_saidas += value;
}
groupedCaixinha[id].total = groupedCaixinha[id].total_entradas + groupedCaixinha[id].total_saidas;
groupedCaixinha[id].diferenca = groupedCaixinha[id].total_entradas - groupedCaixinha[id].total_saidas;
});
setPorCaixinha(Object.values(groupedCaixinha).sort((a, b) => b.total - a.total));
setSaldo(saldoData || { disponivel: 0 }); setSaldo(saldoData || { disponivel: 0 });
setSaldoArmazenado(Array.isArray(armazenadoData) ? armazenadoData : []); setSaldoArmazenado(Array.isArray(armazenadoData) ? armazenadoData : []);
setFluxoData(fluxoResponse || { mensal: [] });
setSomaCategoria(somaCategoriaData || { por_categoria: [] }); const filteredFluxo = {
mensal: (fluxoResponse?.mensal || []).filter(item => {
if (tipo === 'ano') return String(item.ano) === anoAtu;
return true;
}),
diario: (fluxoResponse?.diario || []).filter(item => {
if (tipo === 'mes') return String(item.mes) === mesAtu && String(item.ano) === anoAtu;
return true;
}),
anual: fluxoResponse?.anual || []
};
setFluxoData(filteredFluxo);
setSaldoConsolidado(consolidadoData || null); setSaldoConsolidado(consolidadoData || null);
} catch (err) { } catch (err) {
console.error('[useFluxoCaixa] Erro ao carregar dados:', err); console.error('[useFluxoCaixa] Erro ao carregar dados:', err);
setError(err.message || 'Erro ao carregar fluxo de caixa'); setError(err.message || 'Erro ao carregar fluxo de caixa');
setExtrato([]); setExtrato([]);
@ -72,12 +237,12 @@ export function useFluxoCaixa() {
setSaldoArmazenado([]); setSaldoArmazenado([]);
setFluxoData({ mensal: [] }); setFluxoData({ mensal: [] });
setSomaCategoria({ por_categoria: [] }); setSomaCategoria({ por_categoria: [] });
setPorCaixinha([]);
setSaldoConsolidado(null); setSaldoConsolidado(null);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filtroMes, filtroAno]); }, [filtroMes, filtroAno, filtroTipo]);
useEffect(() => { useEffect(() => {
loadData(); loadData();
@ -124,15 +289,25 @@ export function useFluxoCaixa() {
const getChartData = useCallback((type = 'mensal') => { const getChartData = useCallback((type = 'mensal') => {
const data = fluxoData[type] || []; const data = fluxoData[type] || [];
// Agrupa por período (mês ou ano) e tipo de operação // Agrupa por período (mês, ano ou dia) e tipo de operação
const periodGroups = data.reduce((acc, item) => { const periodGroups = data.reduce((acc, item) => {
const key = type === 'mensal' let key = '';
? `${item.ano}-${String(item.mes).padStart(2, '0')}` let label = '';
: `${item.ano}`;
if (type === 'diario') {
key = `${item.ano}-${String(item.mes).padStart(2, '0')}-${String(item.dia).padStart(2, '0')}`;
label = `${String(item.dia).padStart(2, '0')}/${String(item.mes).padStart(2, '0')}`;
} else if (type === 'mensal') {
key = `${item.ano}-${String(item.mes).padStart(2, '0')}`;
label = `${item.mes}/${item.ano}`;
} else {
key = `${item.ano}`;
label = `${item.ano}`;
}
if (!acc[key]) { if (!acc[key]) {
acc[key] = { acc[key] = {
name: type === 'mensal' ? `${item.mes}/${item.ano}` : `${item.ano}`, name: label,
periodo: key, periodo: key,
receitas: 0, receitas: 0,
despesas: 0 despesas: 0
@ -152,6 +327,7 @@ export function useFluxoCaixa() {
return { return {
loading, loading,
error, error,
extrato,
receitas, receitas,
receitasCard, receitasCard,
despesas, despesas,
@ -161,6 +337,7 @@ export function useFluxoCaixa() {
bateComSaldo, bateComSaldo,
getChartData, getChartData,
somaCategoria, somaCategoria,
porCaixinha,
saldoConsolidado, saldoConsolidado,
filtroMes, filtroMes,
setFiltroMes, setFiltroMes,

View File

@ -1,14 +1,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { conciliacaoService } from '@/services/conciliacaoService'; import { conciliacaoService } from '@/services/conciliacaoService';
export const useStatementRefData = () => { export function useStatementRefData() {
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [rules, setRules] = useState([]); const [rules, setRules] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { async function fetchData() {
try { try {
setLoading(true); setLoading(true);
const [catResponse, rulesResponse] = await Promise.all([ const [catResponse, rulesResponse] = await Promise.all([
@ -36,22 +36,22 @@ export const useStatementRefData = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }
fetchData(); fetchData();
}, []); }, []);
const getCategoryName = (id) => { function getCategoryName(id) {
if (!id) return null; if (!id) return null;
const cat = categories.find(c => String(c.idcategoria) === String(id)); const cat = categories.find(c => String(c.idcategoria) === String(id));
return cat?.categoria || cat?.nome || 'Não categorizado'; return cat?.categoria || cat?.nome || 'Não categorizado';
}; }
const getRuleName = (id) => { function getRuleName(id) {
if (!id) return null; if (!id) return null;
const rule = rules.find(r => String(r.id) === String(id) || String(r.idregras_financeiro) === String(id)); const rule = rules.find(r => String(r.id) === String(id) || String(r.idregras_financeiro) === String(id));
return rule?.regra || rule?.nome || null; return rule?.regra || rule?.nome || null;
}; }
return { return {
categories, categories,
@ -61,4 +61,4 @@ export const useStatementRefData = () => {
getCategoryName, getCategoryName,
getRuleName getRuleName
}; };
}; }

View File

@ -477,19 +477,19 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none mt-[-20px]"> <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none mt-[-20px]">
<span className="text-2xl font-bold text-slate-900 dark:text-white">{boletosPieData.reduce((a, b) => a + b.value, 0)}</span> <span className="text-2xl font-bold text-slate-900 dark:text-white">{boletosPieData.reduce((a, b) => a + b.count, 0)}</span>
<span className="text-[9px] font-bold text-slate-500 dark:text-slate-500 uppercase">Total</span> <span className="text-[9px] font-bold text-slate-500 dark:text-slate-500 uppercase">Total</span>
</div> </div>
</> </>
)} )}
</div> </div>
<div className="p-6 bg-slate-50 dark:bg-slate-900/40 grid grid-cols-2 gap-3"> <div className="p-6 bg-slate-50 dark:bg-slate-900/40 flex flex-col gap-3">
{boletosPieData.slice(0, 4).map((item, index) => ( {boletosPieData.map((item, index) => (
<div key={index} className="flex items-center gap-2"> <div key={index} className="flex items-center gap-3 py-1.5 group/item">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} /> <div className="w-3.5 h-3.5 rounded-full shrink-0 shadow-sm transition-transform group-hover/item:scale-110" style={{ backgroundColor: item.color }} />
<span className="text-[10px] font-bold text-slate-500 dark:text-slate-300 uppercase truncate">{item.name}</span> <span className="text-[clamp(0.75rem,0.9vw,0.85rem)] font-bold text-slate-500 dark:text-slate-300 uppercase tracking-wider flex-1">{item.name}</span>
<span className="text-[10px] font-bold text-slate-900 dark:text-white ml-auto">{item.value}</span> <span className="text-[clamp(0.85rem,1.1vw,1rem)] font-bold text-slate-900 dark:text-white">{item.count} boleto{item.count !== 1 ? 's' : ''}</span>
</div> </div>
))} ))}
</div> </div>

View File

@ -60,6 +60,8 @@ import {
ResponsiveContainer ResponsiveContainer
} from 'recharts'; } from 'recharts';
import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip'; import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip';
import CaixinhaDetailsModal from '../components/CaixinhaDetailsModal';
import { LayoutGrid } from 'lucide-react';
// Standardized Filter Header matching CruzamentoView pattern // Standardized Filter Header matching CruzamentoView pattern
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
@ -150,8 +152,8 @@ const KPICard = ({ title, value, subtext, icon: Icon, colorClass, isPositive })
}; };
// Chart Section - Theme-aware Bar Chart // Chart Section - Theme-aware Bar Chart
const ChartSection = ({ getChartData }) => { const ChartSection = ({ getChartData, tipoPeriodo }) => {
const data = useMemo(() => getChartData('mensal'), [getChartData]); const data = useMemo(() => getChartData(tipoPeriodo === 'ano' ? 'mensal' : 'diario'), [getChartData, tipoPeriodo]);
return ( return (
<Card className="bg-white/80 dark:bg-[#0f172a] border border-slate-200 dark:border-slate-800/50 shadow-xl rounded-xl overflow-hidden"> <Card className="bg-white/80 dark:bg-[#0f172a] border border-slate-200 dark:border-slate-800/50 shadow-xl rounded-xl overflow-hidden">
@ -161,7 +163,9 @@ const ChartSection = ({ getChartData }) => {
<BarChart3 className="w-4 h-4 text-cyan-600 dark:text-cyan-400" /> <BarChart3 className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
</div> </div>
<div> <div>
<h3 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Receitas vs Despesas (Executado)</h3> <h3 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
Receitas vs Despesas ({tipoPeriodo === 'mes' ? 'Diário' : 'Mensal'})
</h3>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@ -216,7 +220,7 @@ const CategorySection = ({ data = [] }) => {
return data.map((item, index) => ({ return data.map((item, index) => ({
name: item.categoria || 'Sem Categoria', name: item.categoria || 'Sem Categoria',
value: Math.abs(Number(item.total_entradas) || 0) + Math.abs(Number(item.total_saidas) || 0), value: Math.abs(Number(item.total) || (Number(item.total_entradas || 0) + Number(item.total_saidas || 0))),
color: COLORS[index % COLORS.length] color: COLORS[index % COLORS.length]
})).filter(item => item.value > 0); })).filter(item => item.value > 0);
}, [data]); }, [data]);
@ -281,15 +285,16 @@ const CategorySection = ({ data = [] }) => {
}; };
// Caixinha Table Section - Using correct API data structure // Caixinha Table Section - Using correct API data structure
const CaixinhaSection = ({ data = [] }) => { const CaixinhaSection = ({ data = [], onSelectCaixinha }) => {
const caixinhaData = useMemo(() => { const caixinhaData = useMemo(() => {
if (!Array.isArray(data)) return []; if (!Array.isArray(data)) return [];
return data.map(item => ({ return data.map(item => ({
caixinha: item.categoria || 'Padrão', id: item.id,
receitas: Math.abs(Number(item.total_entradas) || 0), caixinha: item.caixinha || 'Padrão',
despesas: Math.abs(Number(item.total_saidas) || 0), receitas: Math.abs(Number(item.receitas || item.total_entradas) || 0),
saldo: Number(item.diferenca) || 0 despesas: Math.abs(Number(item.despesas || item.total_saidas) || 0),
saldo: Number(item.saldo || item.diferenca) || 0
})); }));
}, [data]); }, [data]);
@ -323,8 +328,17 @@ const CaixinhaSection = ({ data = [] }) => {
</TableRow> </TableRow>
) : ( ) : (
caixinhaData.map((item, index) => ( caixinhaData.map((item, index) => (
<TableRow key={index} className="border-slate-200 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800/20"> <TableRow
<TableCell className="text-slate-900 dark:text-white text-sm font-medium">{item.caixinha}</TableCell> key={index}
className="border-slate-200 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800/20 cursor-pointer group"
onClick={() => onSelectCaixinha(item)}
>
<TableCell className="text-slate-900 dark:text-white text-sm font-medium">
<div className="flex items-center gap-2">
{item.caixinha}
<LayoutGrid className="w-3 h-3 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</TableCell>
<TableCell className="text-cyan-600 dark:text-cyan-400 text-sm font-mono text-right">{formatCurrency(item.receitas)}</TableCell> <TableCell className="text-cyan-600 dark:text-cyan-400 text-sm font-mono text-right">{formatCurrency(item.receitas)}</TableCell>
<TableCell className="text-rose-600 dark:text-rose-400 text-sm font-mono text-right">{formatCurrency(item.despesas)}</TableCell> <TableCell className="text-rose-600 dark:text-rose-400 text-sm font-mono text-right">{formatCurrency(item.despesas)}</TableCell>
<TableCell className={cn( <TableCell className={cn(
@ -342,41 +356,19 @@ const CaixinhaSection = ({ data = [] }) => {
); );
}; };
// Transactions Table Section - Using correct API data structure // Transactions Table Section - Using actual API data
const TransactionsSection = ({ data = [], loading = false }) => { const TransactionsSection = ({ data = [], loading = false }) => {
// Transform category data into transaction-like rows for display
const transactionData = useMemo(() => { const transactionData = useMemo(() => {
if (!Array.isArray(data)) return []; if (!Array.isArray(data)) return [];
return data.flatMap(item => { return data.map(item => ({
const rows = []; id: item.idextrato || item.id,
categoria: item.categoria_nome || item.categoria || 'Sem Categoria',
// Add entrada row if exists tipoOperacao: item.tipoOperacao,
if (Number(item.total_entradas) > 0) { valor: item.valor,
rows.push({ descricao: item.descricao || item.historico || '---',
id: `${item.idcategoria}-entrada`, data: item.dataEntrada || item.data || item.data_entrada
categoria: item.categoria || 'Sem Categoria', }));
tipoOperacao: 'C',
valor: Number(item.total_entradas),
descricao: `Total de entradas - ${item.categoria || 'Sem Categoria'}`,
data: new Date().toISOString()
});
}
// Add saida row if exists
if (Number(item.total_saidas) > 0) {
rows.push({
id: `${item.idcategoria}-saida`,
categoria: item.categoria || 'Sem Categoria',
tipoOperacao: 'D',
valor: Number(item.total_saidas),
descricao: `Total de saídas - ${item.categoria || 'Sem Categoria'}`,
data: new Date().toISOString()
});
}
return rows;
});
}, [data]); }, [data]);
const columns = [ const columns = [
@ -394,7 +386,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
</span> </span>
) )
}, },
{ /* {
field: 'categoria', field: 'categoria',
header: 'CATEGORIA', header: 'CATEGORIA',
render: (row) => ( render: (row) => (
@ -402,7 +394,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
{row.categoria || 'Sem Categoria'} {row.categoria || 'Sem Categoria'}
</Badge> </Badge>
) )
}, }, */
{ {
field: 'tipo', field: 'tipo',
header: 'TIPO', header: 'TIPO',
@ -458,6 +450,7 @@ export const FluxoCaixaView = () => {
const { const {
loading, loading,
error, error,
extrato,
receitas, receitas,
receitasCard, receitasCard,
despesas, despesas,
@ -467,6 +460,7 @@ export const FluxoCaixaView = () => {
bateComSaldo, bateComSaldo,
getChartData, getChartData,
somaCategoria, somaCategoria,
porCaixinha,
saldoConsolidado, saldoConsolidado,
filtroMes, filtroMes,
setFiltroMes, setFiltroMes,
@ -478,6 +472,11 @@ export const FluxoCaixaView = () => {
} = useFluxoCaixa(); } = useFluxoCaixa();
const [tipoPeriodo, setTipoPeriodo] = useState('mensal'); const [tipoPeriodo, setTipoPeriodo] = useState('mensal');
const [selectedCaixinha, setSelectedCaixinha] = useState(null);
const handleSelectCaixinha = (caixinha) => {
setSelectedCaixinha(caixinha);
};
if (error) { if (error) {
return ( return (
@ -530,7 +529,7 @@ export const FluxoCaixaView = () => {
<KPICard <KPICard
title="Saldo Disponível" title="Saldo Disponível"
value={formatCurrency(saldoConsolidado?.entradas_vs_saidas?.diferenca || 0)} value={formatCurrency(saldoConsolidado?.entradas_vs_saidas?.diferenca || 0)}
subtext="Receitas - Despesas do período (API saldo R$ 0,00)" subtext="Saldo disponivel em conta"
icon={Wallet} icon={Wallet}
colorClass="text-emerald-600 dark:text-emerald-400" colorClass="text-emerald-600 dark:text-emerald-400"
isPositive={true} isPositive={true}
@ -541,17 +540,27 @@ export const FluxoCaixaView = () => {
{/* Chart Section */} {/* Chart Section */}
<div className="mb-6"> <div className="mb-6">
<ChartSection getChartData={getChartData} /> <ChartSection getChartData={getChartData} tipoPeriodo={filtroTipo} />
</div> </div>
{/* Category and Caixinha Sections - Side by Side */} {/* Category and Caixinha Sections - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<CategorySection data={somaCategoria?.por_categoria || []} /> <CategorySection data={somaCategoria?.por_categoria || []} />
<CaixinhaSection data={somaCategoria?.por_categoria || []} /> <CaixinhaSection data={porCaixinha} onSelectCaixinha={handleSelectCaixinha} />
</div> </div>
{/* Transactions Table */} {/* Transactions Table */}
<TransactionsSection data={somaCategoria?.por_categoria || []} loading={loading} /> <TransactionsSection data={extrato} loading={loading} />
{/* Caixinha Details Modal */}
<CaixinhaDetailsModal
isOpen={!!selectedCaixinha}
onClose={() => setSelectedCaixinha(null)}
caixinhaId={selectedCaixinha?.id}
caixinhaName={selectedCaixinha?.caixinha}
mes={filtroMes}
ano={filtroAno}
/>
</div> </div>
); );
}; };

View File

@ -18,7 +18,10 @@ import { GerenciamentoView } from './GerenciamentoView';
import { ExtratoCompletoView } from './ExtratoCompletoView'; import { ExtratoCompletoView } from './ExtratoCompletoView';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const ActivityLog = ({ isOpen, onClose }) => { /**
* Componente para exibir log de atividades simplificado
*/
function ActivityLog({ isOpen, onClose }) {
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@ -61,69 +64,37 @@ const ActivityLog = ({ isOpen, onClose }) => {
</div> </div>
</div> </div>
); );
}; }
export const ConciliacaoView = ({ initialView }) => { /**
* Componente Principal de Conciliação V2
*/
export function ConciliacaoView({ initialView }) {
console.log('[ConciliacaoView] ========== COMPONENTE RENDERIZADO =========='); console.log('[ConciliacaoView] ========== COMPONENTE RENDERIZADO ==========');
console.log('[ConciliacaoView] Timestamp:', new Date().toISOString());
console.log('[ConciliacaoView] Cache Buster: v1.0.1 - Force Refresh');
console.log('[ConciliacaoView] InitialView prop:', initialView);
let state, actions; const { state, actions } = useConciliacaoV2(initialView);
try {
console.log('[ConciliacaoView] Chamando useConciliacaoV2(initialView)...');
const hookResult = useConciliacaoV2(initialView);
console.log('[ConciliacaoView] Hook retornou:', hookResult);
state = hookResult?.state; // Configuração das sub-views (movido para dentro para ajudar com TDZ em bundles complexos)
actions = hookResult?.actions; const subViews = [
{ id: 'conciliadas', label: 'Transações Conciliadas', icon: FileText, description: 'Navegação hierárquica por caixas' },
console.log('[ConciliacaoView] Hook executado com sucesso'); { id: 'nao-categorizadas', label: 'Pendentes', icon: AlertCircle, description: 'Aguardando categorização' },
console.log('[ConciliacaoView] State:', state); { id: 'extrato-completo', label: 'Extrato Completo', icon: History, description: 'Todos os lançamentos do período' },
console.log('[ConciliacaoView] Actions:', actions); { id: 'gerenciamento', label: 'Gerenciamento', icon: Settings, description: 'Caixas, Categorias, Regras' },
} catch (error) { ];
console.error('[ConciliacaoView] ========== ERRO AO EXECUTAR HOOK ==========');
console.error('[ConciliacaoView] Erro:', error);
console.error('[ConciliacaoView] Stack:', error.stack);
// Não lançar erro para não quebrar a UI, mas logar tudo
}
const activeSubView = state?.activeSubView || 'conciliadas'; const activeSubView = state?.activeSubView || 'conciliadas';
const isLoading = state?.isLoading !== undefined ? state.isLoading : true; const isLoading = state?.isLoading !== undefined ? state.isLoading : true;
console.log('[ConciliacaoView] Estado atual:', {
activeSubView,
isLoading,
hasState: !!state,
hasActions: !!actions,
caixasCount: state?.caixas?.length || 0,
categoriasCount: state?.categorias?.length || 0
});
const [isBuzzOpen, setIsBuzzOpen] = React.useState(false); const [isBuzzOpen, setIsBuzzOpen] = React.useState(false);
// Log quando o componente é atualizado
React.useEffect(() => {
console.log('[ConciliacaoView] Componente montado/atualizado');
console.log('[ConciliacaoView] isLoading:', isLoading);
console.log('[ConciliacaoView] activeSubView:', activeSubView);
}, [isLoading, activeSubView]);
const subViews = [
{ id: 'conciliadas', label: 'Transações Conciliadas', icon: FileText, description: 'Navegação hierárquica por caixas' },
{ id: 'extrato-completo', label: 'Extrato Completo', icon: History, description: 'Todo o extrato com filtros' },
{ id: 'nao-categorizadas', label: 'Pendentes', icon: AlertCircle, description: 'Aguardando categorização' },
{ id: 'gerenciamento', label: 'Gerenciamento', icon: Settings, description: 'Caixas, Categorias, Regras' },
];
const renderSubView = () => { const renderSubView = () => {
switch (activeSubView) { switch (activeSubView) {
case 'conciliadas': case 'conciliadas':
return <TransacoesConciliadasView state={state} actions={actions} />; return <TransacoesConciliadasView state={state} actions={actions} />;
case 'extrato-completo':
return <ExtratoCompletoView state={state} actions={actions} />;
case 'nao-categorizadas': case 'nao-categorizadas':
return <TransacoesNaoCategorizadasView state={state} actions={actions} />; return <TransacoesNaoCategorizadasView state={state} actions={actions} />;
case 'extrato-completo':
return <ExtratoCompletoView state={state} actions={actions} />;
case 'gerenciamento': case 'gerenciamento':
return <GerenciamentoView state={state} actions={actions} />; return <GerenciamentoView state={state} actions={actions} />;
default: default:
@ -135,24 +106,11 @@ export const ConciliacaoView = ({ initialView }) => {
<div className="animate-in fade-in duration-700 relative"> <div className="animate-in fade-in duration-700 relative">
{/* Header Section */} {/* Header Section */}
<div className="border-b border-slate-200 dark:border-slate-800 pb-4 sm:pb-6 mb-6 sm:mb-8"> <div className="border-b border-slate-200 dark:border-slate-800 pb-4 sm:pb-6 mb-6 sm:mb-8">
{/* Linha 1: Título, Badge e Botão Activity Log */} {/* Linha 1: Título e Badge */}
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6"> <div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3"> <h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
Conciliação Conciliação
{/* <Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
</h1> </h1>
{/* <Button
size="sm"
variant="ghost"
className={cn(
"rounded-full w-9 h-9 p-0 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shrink-0",
isBuzzOpen ? "text-blue-400 ring-2 ring-blue-500/20" : "text-slate-500"
)}
onClick={() => setIsBuzzOpen(!isBuzzOpen)}
>
<History className="w-4 h-4" />
</Button> */}
</div> </div>
{/* Linha 2: Menu de Navegação */} {/* Linha 2: Menu de Navegação */}
@ -201,4 +159,4 @@ export const ConciliacaoView = ({ initialView }) => {
/> />
</div> </div>
); );
}; }

View File

@ -21,17 +21,19 @@ import { formatDate, formatCurrency } from '../../utils/dateUtils';
import { StatementRow } from '../../components/StatementRow'; import { StatementRow } from '../../components/StatementRow';
import { useStatementRefData } from '../../hooks/useStatementRefData'; import { useStatementRefData } from '../../hooks/useStatementRefData';
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog'; import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
import { TransactionDetailModal } from '../../components/TransactionDetailModal';
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
const ANOS = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 2 + i); const ANOS = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 2 + i);
export const ExtratoCompletoView = ({ state, actions }) => { export function ExtratoCompletoView({ state, actions }) {
const [searchTerm, setSearchTerm] = React.useState(''); const [searchTerm, setSearchTerm] = React.useState('');
const [filterType, setFilterType] = React.useState('todos'); // 'todos' | 'C' | 'D' const [filterType, setFilterType] = React.useState('todos'); // 'todos' | 'C' | 'D'
const [filterMonth, setFilterMonth] = React.useState(String(new Date().getMonth() + 1)); const [filterMonth, setFilterMonth] = React.useState(String(new Date().getMonth() + 1));
const [filterYear, setFilterYear] = React.useState(String(new Date().getFullYear())); const [filterYear, setFilterYear] = React.useState(String(new Date().getFullYear()));
const [transacaoSelecionada, setTransacaoSelecionada] = React.useState(null); const [transacaoSelecionada, setTransacaoSelecionada] = React.useState(null);
const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [isDetailOpen, setIsDetailOpen] = React.useState(false);
const { const {
extratoCompleto = [], extratoCompleto = [],
@ -202,8 +204,10 @@ export const ExtratoCompletoView = ({ state, actions }) => {
categoryName={getCategoryName(row.categoria)} categoryName={getCategoryName(row.categoria)}
ruleName={getRuleName(row.regra)} ruleName={getRuleName(row.regra)}
onClick={(t) => { onClick={(t) => {
console.log('[ExtratoCompletoView] Clique na transação:', t);
console.log('[ExtratoCompletoView] Abrindo modal de detalhes...');
setTransacaoSelecionada(t); setTransacaoSelecionada(t);
setIsDialogOpen(true); setIsDetailOpen(true);
}} }}
showCategory={true} showCategory={true}
showStatus={false} showStatus={false}
@ -229,6 +233,18 @@ export const ExtratoCompletoView = ({ state, actions }) => {
caixas={state?.caixas} caixas={state?.caixas}
actions={actions} actions={actions}
/> />
<TransactionDetailModal
transaction={transacaoSelecionada}
open={isDetailOpen}
onOpenChange={setIsDetailOpen}
onEditTransaction={(t) => {
setTransacaoSelecionada(t);
setIsDialogOpen(true);
}}
categorias={state?.categorias}
caixas={state?.caixas}
/>
</div> </div>
); );
}; };

View File

@ -23,7 +23,7 @@ import ExcelTable from '../../components/ExcelTable';
import { formatCurrency } from '../../utils/dateUtils'; import { formatCurrency } from '../../utils/dateUtils';
import { CategoryRulesPopup } from '../../components/CategoryRulesPopup'; import { CategoryRulesPopup } from '../../components/CategoryRulesPopup';
export const GerenciamentoView = ({ state, actions }) => { export function GerenciamentoView({ state, actions }) {
const { caixas, categorias, regras } = state; const { caixas, categorias, regras } = state;
const [activeTab, setActiveTab] = useState('caixas'); const [activeTab, setActiveTab] = useState('caixas');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');

View File

@ -51,7 +51,7 @@ import {
} from 'recharts'; } from 'recharts';
/** Sanitiza rótulo: corrige typos, espaços e remove trechos duplicados nas legendas */ /** Sanitiza rótulo: corrige typos, espaços e remove trechos duplicados nas legendas */
const sanitizeLabel = (name) => { function sanitizeLabel(name) {
if (name == null || typeof name !== 'string') return '—'; if (name == null || typeof name !== 'string') return '—';
let s = name.replace(/\s+/g, ' ').trim(); let s = name.replace(/\s+/g, ' ').trim();
if (!s) return '—'; if (!s) return '—';
@ -65,51 +65,53 @@ const sanitizeLabel = (name) => {
s = s.replace(repeatedPhrase, (_, phrase, rest) => (phrase + rest).replace(/\s+/g, ' ').trim()); s = s.replace(repeatedPhrase, (_, phrase, rest) => (phrase + rest).replace(/\s+/g, ' ').trim());
} }
return s || '—'; return s || '—';
}; }
const AccordionItem = ({ title, icon: Icon, value, count, children, isOpen, onClick, color }) => ( function AccordionItem({ title, icon: Icon, value, count, children, isOpen, onClick, color }) {
<div className="border border-slate-200 dark:border-slate-800 rounded-xl mb-3 overflow-hidden bg-white dark:bg-slate-900/50"> return (
<div <div className="border border-slate-200 dark:border-slate-800 rounded-xl mb-3 overflow-hidden bg-white dark:bg-slate-900/50">
className={cn( <div
"p-4 flex items-center justify-between cursor-pointer transition-colors select-none", className={cn(
isOpen ? "bg-slate-50 dark:bg-slate-800/50" : "hover:bg-slate-50 dark:hover:bg-slate-900" "p-4 flex items-center justify-between cursor-pointer transition-colors select-none",
isOpen ? "bg-slate-50 dark:bg-slate-800/50" : "hover:bg-slate-50 dark:hover:bg-slate-900"
)}
onClick={onClick}
>
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center border"
style={color ? { backgroundColor: `${color}20`, borderColor: `${color}40` } : {}}
>
<Icon className="w-5 h-5" style={color ? { color } : {}} />
</div>
<div>
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{title}</h4>
<p className="text-[clamp(0.7rem,1vw,0.8rem)] text-slate-500 uppercase font-bold tracking-wider">{count} Transações</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="font-bold text-slate-900 dark:text-white">{formatCurrency(value)}</span>
<ChevronDown className={cn("w-5 h-5 text-slate-400 transition-transform duration-300", isOpen && "rotate-180")} />
</div>
</div>
{isOpen && (
<div className="p-4 border-t border-slate-200 dark:border-slate-800 animate-in slide-in-from-top-2 duration-300">
{children}
</div>
)} )}
onClick={onClick}
>
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center border"
style={color ? { backgroundColor: `${color}20`, borderColor: `${color}40` } : {}}
>
<Icon className="w-5 h-5" style={color ? { color } : {}} />
</div>
<div>
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{title}</h4>
<p className="text-[clamp(0.7rem,1vw,0.8rem)] text-slate-500 uppercase font-bold tracking-wider">{count} Transações</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="font-bold text-slate-900 dark:text-white">{formatCurrency(value)}</span>
<ChevronDown className={cn("w-5 h-5 text-slate-400 transition-transform duration-300", isOpen && "rotate-180")} />
</div>
</div> </div>
{isOpen && ( );
<div className="p-4 border-t border-slate-200 dark:border-slate-800 animate-in slide-in-from-top-2 duration-300"> }
{children}
</div>
)}
</div>
);
/** Trunca legenda longa para exibição no eixo (mantém nome completo no tooltip) */ /** Trunca legenda longa para exibição no eixo (mantém nome completo no tooltip) */
const truncateLegend = (name, maxLen = 42) => { function truncateLegend(name, maxLen = 42) {
const clean = sanitizeLabel(name); const clean = sanitizeLabel(name);
if (clean.length <= maxLen) return clean; if (clean.length <= maxLen) return clean;
return clean.substring(0, maxLen - 3).trim() + '...'; return clean.substring(0, maxLen - 3).trim() + '...';
}; }
/** Tooltip customizado para gráficos de conciliação: layout limpo, label + valor */ /** Tooltip customizado para gráficos de conciliação: layout limpo, label + valor */
const ConciliacaoChartTooltip = ({ active, payload, label, formatCurrency }) => { function ConciliacaoChartTooltip({ active, payload, label, formatCurrency }) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
const item = payload[0]; const item = payload[0];
const name = item.name ?? label ?? item.payload?.name ?? '—'; const name = item.name ?? label ?? item.payload?.name ?? '—';
@ -121,9 +123,9 @@ const ConciliacaoChartTooltip = ({ active, payload, label, formatCurrency }) =>
<p className="text-base font-bold text-slate-900 dark:text-emerald-400 tabular-nums">{formatCurrency(value)}</p> <p className="text-base font-bold text-slate-900 dark:text-emerald-400 tabular-nums">{formatCurrency(value)}</p>
</div> </div>
); );
}; }
export const TransacoesConciliadasView = ({ state, actions }) => { export function TransacoesConciliadasView({ state, actions }) {
const { const {
caixas, caixas,
categorias, categorias,

View File

@ -26,7 +26,7 @@ import { StatementRow } from '../../components/StatementRow';
import { useStatementRefData } from '../../hooks/useStatementRefData'; import { useStatementRefData } from '../../hooks/useStatementRefData';
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog'; import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
export const TransacoesNaoCategorizadasView = ({ state, actions }) => { export function TransacoesNaoCategorizadasView({ state, actions }) {
// IMPORTANTE: Todos os hooks devem ser chamados incondicionalmente no topo // IMPORTANTE: Todos os hooks devem ser chamados incondicionalmente no topo
// NUNCA colocar hooks após early returns ou condições // NUNCA colocar hooks após early returns ou condições

View File

@ -39,12 +39,14 @@ import {
import { FinanceiroChartTooltip } from '../../components/FinanceiroChartTooltip'; import { FinanceiroChartTooltip } from '../../components/FinanceiroChartTooltip';
import { extratoService } from '@/services/extratoService'; import { extratoService } from '@/services/extratoService';
import ExcelTable from '../../components/ExcelTable'; import ExcelTable from '../../components/ExcelTable';
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils'; import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils';
/** /**
* Premium KPI Card com visualização clara * Premium KPI Card com visualização clara
*/ */
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, percentual, loading }) => { function KPICard({ title, value, subtext, icon, colorClass, highlight, trend, percentual, loading }) {
const Icon = icon; const Icon = icon;
return ( return (
<Card className="bg-white dark:bg-slate-900/50 border-2 shadow-lg rounded-xl overflow-hidden group hover:scale-[1.02] transition-all"> <Card className="bg-white dark:bg-slate-900/50 border-2 shadow-lg rounded-xl overflow-hidden group hover:scale-[1.02] transition-all">
@ -85,12 +87,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, pe
</CardContent> </CardContent>
</Card> </Card>
); );
}; }
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
const ANOS = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i); const ANOS = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
export const CruzamentoDespesasView = ({ state }) => { export function CruzamentoDespesasView({ state }) {
const { despesasPlanejadas = [], despesasExecutadas = [], caixinhas = [] } = state; const { despesasPlanejadas = [], despesasExecutadas = [], caixinhas = [] } = state;
const [filtroTipo, setFiltroTipo] = React.useState('mes'); // 'mes' | 'ano' const [filtroTipo, setFiltroTipo] = React.useState('mes'); // 'mes' | 'ano'
@ -101,6 +103,8 @@ export const CruzamentoDespesasView = ({ state }) => {
const [somaCategorias, setSomaCategorias] = React.useState([]); const [somaCategorias, setSomaCategorias] = React.useState([]);
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false); const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
const [chartDataBackend, setChartDataBackend] = React.useState([]); const [chartDataBackend, setChartDataBackend] = React.useState([]);
const [isLoadingChart, setIsLoadingChart] = React.useState(false); const [isLoadingChart, setIsLoadingChart] = React.useState(false);
@ -115,6 +119,9 @@ export const CruzamentoDespesasView = ({ state }) => {
}); });
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false); const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
const [selectedCategory, setSelectedCategory] = React.useState(null);
const [isModalOpen, setIsModalOpen] = React.useState(false);
// Efeito para buscar totais planejados do mês vigente // Efeito para buscar totais planejados do mês vigente
React.useEffect(() => { React.useEffect(() => {
const fetchTotais = async () => { const fetchTotais = async () => {
@ -141,6 +148,7 @@ export const CruzamentoDespesasView = ({ state }) => {
}, [filtroMes, filtroAno, filtroTipo]); }, [filtroMes, filtroAno, filtroTipo]);
// Efeito para buscar dados do gráfico do backend // Efeito para buscar dados do gráfico do backend
React.useEffect(() => { React.useEffect(() => {
const fetchChartData = async () => { const fetchChartData = async () => {
setIsLoadingChart(true); setIsLoadingChart(true);
@ -179,6 +187,10 @@ export const CruzamentoDespesasView = ({ state }) => {
// Normaliza o resultado conforme o backend (esperado array de objetos) // Normaliza o resultado conforme o backend (esperado array de objetos)
// O backend para despesas v2 retorna { ano, mes, por_categoria } // O backend para despesas v2 retorna { ano, mes, por_categoria }
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []); const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
console.log('[CruzamentoDespesasView] somaCategorias carregadas:', data.length, 'itens');
if (data.length > 0) {
console.log('[CruzamentoDespesasView] Exemplo soma:', data[0]);
}
setSomaCategorias(Array.isArray(data) ? data : []); setSomaCategorias(Array.isArray(data) ? data : []);
} catch (err) { } catch (err) {
console.error('[CruzamentoDespesasView] Erro ao buscar soma por categoria:', err); console.error('[CruzamentoDespesasView] Erro ao buscar soma por categoria:', err);
@ -195,14 +207,21 @@ export const CruzamentoDespesasView = ({ state }) => {
} }
}, [filtroMes, filtroAno, filtroTipo]); }, [filtroMes, filtroAno, filtroTipo]);
// Efeito para buscar transações detalhadas do período removido pois soma_por_categoria já traz os itens.
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias // Mapeamento removido pois agora usamos os nomes da API de somaCategorias
const matchPeriod = (dataStr) => { const matchPeriod = (dataStr) => {
const { year, month } = parseDateInfo(dataStr); const { year, month } = parseDateInfo(dataStr);
if (filtroTipo === 'ano') return year === Number(filtroAno); const y = Number(year);
return year === Number(filtroAno) && month === Number(filtroMes); const m = Number(month);
const fAno = Number(filtroAno);
const fMes = Number(filtroMes);
if (filtroTipo === 'ano') return y === fAno;
return y === fAno && m === fMes;
}; };
const filteredPlanejadas = useMemo(() => { const filteredPlanejadas = useMemo(() => {
@ -214,39 +233,71 @@ export const CruzamentoDespesasView = ({ state }) => {
}, [despesasPlanejadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]); }, [despesasPlanejadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
const filteredExecutadas = useMemo(() => { const filteredExecutadas = useMemo(() => {
return despesasExecutadas.filter(item => { const allTrans = somaCategorias.flatMap(c => c.transacoes || []);
const sourceData = (filtroTipo === 'mes' && allTrans.length > 0) ? allTrans : (Array.isArray(despesasExecutadas) ? despesasExecutadas : []);
console.log('[CruzamentoDespesasView] Pipeline de dados para filteredExecutadas:', {
filtroTipo,
sourceCount: sourceData.length,
propCount: despesasExecutadas?.length,
somaTransCount: allTrans.length
});
return sourceData.filter(item => {
if (!matchPeriod(item.data || item.dataEntrada)) return false; if (!matchPeriod(item.data || item.dataEntrada)) return false;
// Garantir que estamos vendo apenas Despesas (D)
const op = String(item.tipoOperacao || '').toUpperCase();
if (op && op !== 'D') return false;
if (filtroCaixinha !== 'todos' && String(item.caixinha || '') !== String(filtroCaixinha)) return false; if (filtroCaixinha !== 'todos' && String(item.caixinha || '') !== String(filtroCaixinha)) return false;
return true; return true;
}); });
}, [despesasExecutadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]); }, [despesasExecutadas, somaCategorias, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
// Composição e Variância baseadas exclusivamente na rota de soma por categoria // Composição e Variância baseadas exclusivamente na rota de soma por categoria
const comparativoCategoria = useMemo(() => { const comparativoCategoria = useMemo(() => {
if (!Array.isArray(somaCategorias)) return []; if (!Array.isArray(somaCategorias)) return [];
return somaCategorias.map(item => { return somaCategorias.map(item => {
const entradas = parseCurrency(item.total_entradas || 0); const saidas = parseCurrency(item.total || item.total_saidas || 0);
const saidas = parseCurrency(item.total_saidas || 0);
const diferenca = parseCurrency(item.diferenca || 0);
// Percentual de "gasto" em relação ao total movimentado ou similar
const totalMovimentado = entradas + Math.abs(saidas);
const percentual = totalMovimentado > 0 ? ((Math.abs(saidas) / totalMovimentado) * 100).toFixed(1) : 0;
return { return {
id: item.idcategoria ?? item.idCategoria ?? item.id,
categoria: item.categoria || 'Sem Categoria', categoria: item.categoria || 'Sem Categoria',
categoriaNome: item.categoria || '(Sem Categoria)', categoriaNome: item.categoria || '(Sem Categoria)',
name: item.categoria || '(Sem Categoria)', name: item.categoria || '(Sem Categoria)',
entradas, entradas: 0,
saidas, saidas,
diferenca, diferenca: 0,
percentual, percentual: 0,
executado: Math.abs(saidas), // Para o gráfico de pizza (valor absoluto da saída) executado: Math.abs(saidas),
value: Math.abs(saidas) value: Math.abs(saidas)
}; };
}).sort((a, b) => b.saidas - a.saidas); // Ordena pelas maiores saídas }).sort((a, b) => b.saidas - a.saidas);
}, [somaCategorias]); }, [somaCategorias]);
const handleCategoryClick = (name, id) => {
console.log('[CruzamentoDespesasView] Clicou na categoria:', { name, id });
setSelectedCategory({ name, id });
setIsModalOpen(true);
};
const transactionsForSelectedCategory = React.useMemo(() => {
if (!selectedCategory) return [];
// Busca direta no agrupamento do backend
const catObj = somaCategorias.find(c => {
const id = c.idcategoria ?? c.idCategoria ?? c.id;
const name = c.categoria || '';
return String(id) === String(selectedCategory.id) || name === selectedCategory.name;
});
const transactions = catObj?.transacoes || [];
console.log('[CruzamentoDespesasView] Modal: itens para', selectedCategory.name, ':', transactions.length);
return transactions;
}, [selectedCategory, somaCategorias]);
const timelineData = useMemo(() => { const timelineData = useMemo(() => {
if (chartDataBackend.length > 0) { if (chartDataBackend.length > 0) {
return chartDataBackend.map(item => { return chartDataBackend.map(item => {
@ -467,7 +518,7 @@ export const CruzamentoDespesasView = ({ state }) => {
<KPICard <KPICard
title="Total Planejado" title="Total Planejado"
value={formatCurrency(totalPlanejado)} value={formatCurrency(totalPlanejado)}
subtext="Total esperado (despesas V2)" subtext="Total esperado"
icon={Target} icon={Target}
colorClass="text-slate-500" colorClass="text-slate-500"
loading={isLoadingTotais} loading={isLoadingTotais}
@ -475,7 +526,7 @@ export const CruzamentoDespesasView = ({ state }) => {
<KPICard <KPICard
title="Total Executado" title="Total Executado"
value={formatCurrency(totalExecutado)} value={formatCurrency(totalExecutado)}
subtext="Total realizado (extrato)" subtext="Total realizado"
icon={CheckCircle2} icon={CheckCircle2}
colorClass="text-rose-500" colorClass="text-rose-500"
highlight highlight
@ -670,17 +721,12 @@ export const CruzamentoDespesasView = ({ state }) => {
header: 'Categoria', header: 'Categoria',
width: '200px', width: '200px',
render: (row) => ( render: (row) => (
<span className="font-semibold text-slate-900 dark:text-white pl-3">{row.categoriaNome}</span> <button
) onClick={() => handleCategoryClick(row.categoria, row.id)}
}, className="font-semibold text-slate-900 dark:text-white pl-3 hover:text-rose-500 transition-colors text-left w-full"
{ >
field: 'entradas', {row.categoriaNome}
header: 'Entradas', </button>
width: '150px',
render: (row) => (
<span className="font-mono text-xs text-emerald-400 text-right block w-full">
{formatCurrency(row.entradas)}
</span>
) )
}, },
{ {
@ -731,6 +777,14 @@ export const CruzamentoDespesasView = ({ state }) => {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<TransactionsByCategoryModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
categoryName={selectedCategory?.name}
transactions={transactionsForSelectedCategory}
/>
</div> </div>
); );
}; };

View File

@ -112,8 +112,8 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
const [clientBoletosFromApi, setClientBoletosFromApi] = useState([]); const [clientBoletosFromApi, setClientBoletosFromApi] = useState([]);
const [loadingBoletosCliente, setLoadingBoletosCliente] = useState(false); const [loadingBoletosCliente, setLoadingBoletosCliente] = useState(false);
// Estado para o Extrato (Transações) // Estado para o Extrato (Transações): { categoria: string, linha_tempo: Array }
const [clientExtrato, setClientExtrato] = useState([]); const [clientExtrato, setClientExtrato] = useState({ categoria: '', linha_tempo: [] });
const [loadingExtrato, setLoadingExtrato] = useState(false); const [loadingExtrato, setLoadingExtrato] = useState(false);
const [clientInterest, setClientInterest] = useState([]); const [clientInterest, setClientInterest] = useState([]);
@ -243,7 +243,7 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
// Carrega Extrato (Transações) com LOGS DETALHADOS // Carrega Extrato (Transações) com LOGS DETALHADOS
useEffect(() => { useEffect(() => {
if (clientTab !== 'extrato' || !selectedClient) { if (clientTab !== 'extrato' || !selectedClient) {
setClientExtrato([]); setClientExtrato({ categoria: '', linha_tempo: [] });
return; return;
} }
@ -252,12 +252,16 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
const clientName = selectedClient.nome || ''; const clientName = selectedClient.nome || '';
try { try {
// Agora usa a rota otimizada /beneficiario_aplicado // Rota /beneficiario_aplicado retorna { categoria, linha_tempo }
const data = await extratoService.fetchBeneficiarioAplicado(clientName); const data = await extratoService.fetchBeneficiarioAplicado(clientName);
setClientExtrato(data); setClientExtrato(
data && typeof data === 'object' && Array.isArray(data.linha_tempo)
? data
: { categoria: '', linha_tempo: [] }
);
} catch (error) { } catch (error) {
console.error('❌ Erro ao buscar extrato:', error); console.error('❌ Erro ao buscar extrato:', error);
setClientExtrato([]); setClientExtrato({ categoria: '', linha_tempo: [] });
} finally { } finally {
setLoadingExtrato(false); setLoadingExtrato(false);
} }
@ -1169,51 +1173,56 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
<TabsContent value="extrato" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50"> <TabsContent value="extrato" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Extrato Financeiro</h4> <div>
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Linha do Tempo</h4>
{clientExtrato.categoria && (
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 truncate max-w-xs">{clientExtrato.categoria}</p>
)}
</div>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{clientExtrato.length} registros {clientExtrato.linha_tempo?.length ?? 0} registros
</Badge> </Badge>
</div> </div>
{loadingExtrato ? ( {loadingExtrato ? (
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
<Loader2 className="w-6 h-6 animate-spin" /> <Loader2 className="w-6 h-6 animate-spin" />
<span className="text-sm">Buscando transações via /beneficiario_aplicado...</span> <span className="text-sm">Buscando transações...</span>
</div> </div>
) : clientExtrato.length === 0 ? ( ) : !clientExtrato.linha_tempo?.length ? (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <AlertCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p className="text-sm font-medium">Nenhuma transação encontrada</p> <p className="text-sm font-medium">Nenhuma transação encontrada</p>
<p className="text-xs mt-2 max-w-[300px] mx-auto opacity-70"> <p className="text-xs mt-2 max-w-[300px] mx-auto opacity-70">
Consultamos o beneficiário "{selectedClient.nome}" e não retornou registros. Nenhum registro retornado para "{selectedClient.nome}".
</p> </p>
</div> </div>
) : ( ) : (
<div className="h-[500px]"> <div className="h-[500px]">
<ExcelTable <ExcelTable
data={clientExtrato} data={clientExtrato.linha_tempo.map((row, i) => ({ ...row, _idx: i }))}
columns={[ columns={[
{ {
field: 'data', field: 'dataEntrada',
header: 'Data', header: 'Data',
width: '120px', width: '110px',
render: (row) => ( render: (row) => (
<span className="text-slate-600 dark:text-slate-400"> <span className="text-slate-600 dark:text-slate-400 text-xs">
{formatDate(row.data || row.dataEntrada)} {formatDate(row.dataEntrada || row.data)}
</span> </span>
) )
}, },
{ {
field: 'descricao', field: 'titulo',
header: 'Descrição', header: 'Título / Descrição',
width: '300px', width: '280px',
render: (row) => ( render: (row) => (
<div className="flex flex-col"> <div className="flex flex-col gap-0.5">
<span className="font-medium text-slate-900 dark:text-white truncate"> <span className="font-semibold text-slate-900 dark:text-white text-xs leading-tight">
{row.descricao || 'Sem descrição'} {row.titulo || 'N/D'}
</span> </span>
<span className="text-[10px] text-slate-500 uppercase"> <span className="text-[10px] text-slate-500 truncate">
{row.categoria || 'Sem Categoria'} {row.descricao || ''}
</span> </span>
</div> </div>
) )
@ -1221,23 +1230,58 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
{ {
field: 'valor', field: 'valor',
header: 'Valor', header: 'Valor',
width: '150px', width: '130px',
render: (row) => { render: (row) => {
const isCredit = row.tipoOperacao === 'C' || row.valor > 0; const isCredit = row.tipoOperacao === 'C';
return ( return (
<div className={cn( <div className={cn(
"flex items-center gap-2 font-bold", "flex items-center gap-1.5 font-bold text-sm",
isCredit ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400" isCredit ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"
)}> )}>
{isCredit ? <ArrowUpCircle className="w-4 h-4" /> : <ArrowDownCircle className="w-4 h-4" />} {isCredit ? <ArrowUpCircle className="w-3.5 h-3.5 shrink-0" /> : <ArrowDownCircle className="w-3.5 h-3.5 shrink-0" />}
{formatCurrency(Math.abs(row.valor))} {formatCurrency(Math.abs(Number(row.valor) || 0))}
</div> </div>
); );
} }
},
{
field: 'diferenca',
header: 'Diferença',
width: '130px',
render: (row) => {
if (row.diferenca === null || row.diferenca === undefined) {
return <span className="text-slate-400 text-xs"></span>;
}
const diff = Number(row.diferenca);
const isPos = diff >= 0;
return (
<div className={cn(
"flex flex-col",
isPos ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"
)}>
<span className="font-semibold text-xs">
{isPos ? '+' : ''}{formatCurrency(diff)}
</span>
{row.variacao_percentual !== null && row.variacao_percentual !== undefined && (
<span className="text-[10px] opacity-80">{row.variacao_percentual}</span>
)}
</div>
);
}
},
{
field: 'tipoTransacao',
header: 'Tipo',
width: '140px',
render: (row) => (
<Badge variant="outline" className="text-[10px] font-mono uppercase truncate max-w-[130px]">
{row.tipoTransacao || '—'}
</Badge>
)
} }
]} ]}
rowKey={(row) => row.id || Math.random().toString()} rowKey={(row) => row._idx}
pageSize={15} pageSize={20}
/> />
</div> </div>
)} )}
@ -1399,7 +1443,7 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
<TabsContent value="juros" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50"> <TabsContent value="juros" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Detalhamento de Juros</h4> <h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Detalhamento de Boletos/Juros</h4>
<Badge variant="outline" className="text-xs bg-amber-500/10 text-amber-600 border-amber-500/20"> <Badge variant="outline" className="text-xs bg-amber-500/10 text-amber-600 border-amber-500/20">
{clientInterest.length} registros {clientInterest.length} registros
</Badge> </Badge>
@ -1408,62 +1452,70 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
{loadingInterest ? ( {loadingInterest ? (
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400"> <div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
<Loader2 className="w-6 h-6 animate-spin" /> <Loader2 className="w-6 h-6 animate-spin" />
<span className="text-sm">Buscando juros via /financeiro/cliente/boletos...</span> <span className="text-sm">Buscando juros...</span>
</div> </div>
) : clientInterest.length === 0 ? ( ) : clientInterest.length === 0 ? (
<div className="text-center py-12 text-slate-500"> <div className="text-center py-12 text-slate-500">
<Percent className="w-12 h-12 mx-auto mb-4 text-slate-400" /> <Percent className="w-12 h-12 mx-auto mb-4 text-slate-400" />
<p className="text-sm font-medium">Nenhum detalhe de juros encontrado</p> <p className="text-sm font-medium">Nenhum boleto/juros encontrado</p>
</div> </div>
) : ( ) : (
<div className="h-[500px]"> <div className="space-y-3">
<ExcelTable {clientInterest.map((row, i) => {
data={clientInterest} const situacaoColor =
columns={[ row.situacao === 'PAGO' || row.situacao === 'RECEBIDO'
{ ? 'bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20'
field: 'data', : row.situacao === 'CANCELADO'
header: 'Data', ? 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border-slate-300 dark:border-slate-600'
width: '120px', : row.situacao === 'VENCIDO' || row.situacao === 'ATRASADO'
render: (row) => ( ? 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20'
<span className="text-slate-600 dark:text-slate-400"> : 'bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
{formatDate(row.data || row.data_vencimento)} return (
</span> <div key={i} className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
) <div className="flex items-start justify-between gap-3 mb-3">
}, <div className="flex-1 min-w-0">
{ <p className="font-bold text-sm text-slate-900 dark:text-white truncate">{row.cliente || row.nome || 'N/D'}</p>
field: 'descricao', <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5"> {row.seuNumero || row.seu_numero || '—'}</p>
header: 'Descrição', </div>
width: '300px', <Badge className={cn('text-[10px] font-bold uppercase shrink-0 border', situacaoColor)}>
render: (row) => ( {row.situacao || 'N/D'}
<span className="font-medium text-slate-900 dark:text-white truncate"> </Badge>
{row.descricao || `Juros #${row.numero || ''}`} </div>
</span> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
) <div>
}, <p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Vencimento</p>
{ <p className="text-sm font-semibold text-slate-900 dark:text-white mt-0.5">{formatDate(row.vencimento || row.data_vencimento)}</p>
field: 'valor_juros', </div>
header: 'Valor Juros', <div>
width: '150px', <p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Valor Original</p>
render: (row) => ( <p className="text-sm font-semibold text-slate-900 dark:text-white mt-0.5">{formatCurrency(Number(row.valor_original) || 0)}</p>
<span className="font-bold text-amber-600"> </div>
+{formatCurrency(row.valor_juros || row.juros || 0)} <div>
</span> <p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Juros + Multa</p>
) <p className={cn(
}, "text-sm font-semibold mt-0.5",
{ (Number(row.juros) + Number(row.multa)) > 0
field: 'valor', ? 'text-amber-600 dark:text-amber-400'
header: 'Valor Base', : 'text-slate-900 dark:text-white'
width: '150px', )}>
render: (row) => ( {formatCurrency(Number(row.juros) || 0)} + {formatCurrency(Number(row.multa) || 0)}
<span className="text-slate-500 text-sm"> </p>
{formatCurrency(row.valor || 0)} </div>
</span> <div>
) <p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Total Atualizado</p>
} <p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 mt-0.5">{formatCurrency(Number(row.total_atualizado) || 0)}</p>
]} </div>
rowKey={(row, i) => i} </div>
pageSize={15} {Number(row.dias_atraso) > 0 && (
/> <div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700">
<p className="text-xs text-red-600 dark:text-red-400 font-semibold">
{row.dias_atraso} dia(s) de atraso
</p>
</div>
)}
</div>
);
})}
</div> </div>
)} )}
</div> </div>

View File

@ -26,6 +26,8 @@ import {
TableRow TableRow
} from '@/components/ui/table'; } from '@/components/ui/table';
import ExcelTable from '../../components/ExcelTable'; import ExcelTable from '../../components/ExcelTable';
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
import { extratoService } from '@/services/extratoService'; import { extratoService } from '@/services/extratoService';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@ -51,7 +53,7 @@ import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils'
/** /**
* Premium KPI Card with Glow and Glassmorphism * Premium KPI Card with Glow and Glassmorphism
*/ */
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, loading }) => { function KPICard({ title, value, subtext, icon, colorClass, highlight, trend, loading }) {
const Icon = icon; const Icon = icon;
return ( return (
<Card className="bg-white dark:bg-[#1e293b]/40 backdrop-blur-md border-slate-200 dark:border-slate-800 shadow-xl relative overflow-hidden group hover:border-slate-300 dark:hover:border-slate-700/50 transition-all duration-500"> <Card className="bg-white dark:bg-[#1e293b]/40 backdrop-blur-md border-slate-200 dark:border-slate-800 shadow-xl relative overflow-hidden group hover:border-slate-300 dark:hover:border-slate-700/50 transition-all duration-500">
@ -82,12 +84,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, lo
</CardContent> </CardContent>
</Card> </Card>
); );
}; }
const MESES_REC = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; const MESES_REC = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
const ANOS_REC = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i); const ANOS_REC = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlanejadas = [] }) => { export function CruzamentoView({ data = [], kpis = {}, caixas = [], entradasPlanejadas = [], categorias = [] }) {
const [filtroMes, setFiltroMes] = React.useState(String(new Date().getMonth() + 1)); const [filtroMes, setFiltroMes] = React.useState(String(new Date().getMonth() + 1));
const [filtroAno, setFiltroAno] = React.useState(new Date().getFullYear().toString()); const [filtroAno, setFiltroAno] = React.useState(new Date().getFullYear().toString());
const [filtroCaixa, setFiltroCaixa] = React.useState('todos'); const [filtroCaixa, setFiltroCaixa] = React.useState('todos');
@ -103,25 +105,58 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
}); });
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false); const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
const [somaCategorias, setSomaCategorias] = React.useState([]);
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
// Modal state
const [selectedCategory, setSelectedCategory] = React.useState(null);
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [chartDataBackend, setChartDataBackend] = React.useState([]);
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias // Mapeamento removido pois agora usamos os nomes da API de somaCategorias
const dadosFiltrados = React.useMemo(() => { const dadosFiltrados = React.useMemo(() => {
if (!Array.isArray(data)) return []; // Agora usamos os dados brutos de transações agrupadas de somaCategorias para filtros globais se necessário,
return data.filter(item => { // mas para o CruzamentoView, o dadosFiltrados costuma vir da prop 'data'.
const matchPeriod = (dataStr) => { // Se quisermos que o dadosFiltrados reflita o que o backend agrupou:
const { year, month } = parseDateInfo(dataStr); const allTrans = somaCategorias.flatMap(c => c.transacoes || []);
if (filtroTipo === 'ano') return year === Number(filtroAno); const sourceData = (filtroTipo === 'mes' && allTrans.length > 0) ? allTrans : (Array.isArray(data) ? data : []);
if (filtroTipo === 'mes') return year === Number(filtroAno) && month === Number(filtroMes);
return true;
};
console.log('[CruzamentoView] Pipeline de dados:', {
filtroTipo,
sourceCount: sourceData.length,
propCount: data?.length,
somaTransCount: allTrans.length
});
return sourceData.filter(item => {
const matchPeriod = (dataStr) => {
const { year, month } = parseDateInfo(dataStr);
const y = Number(year);
const m = Number(month);
const fAno = Number(filtroAno);
const fMes = Number(filtroMes);
if (filtroTipo === 'ano') return y === fAno;
if (filtroTipo === 'mes') return y === fAno && m === fMes;
return true;
};
// Apply period filter // Apply period filter
if (!matchPeriod(item?.dataEntrada || '')) return false; if (!matchPeriod(item?.dataEntrada || item?.data || '')) return false;
// Garantir que estamos vendo apenas Receitas (C)
const op = String(item.tipoOperacao || '').toUpperCase();
if (op && op !== 'C') return false;
if (filtroCaixa !== 'todos' && item?.caixinha != null && String(item.caixinha) !== String(filtroCaixa)) return false; if (filtroCaixa !== 'todos' && item?.caixinha != null && String(item.caixinha) !== String(filtroCaixa)) return false;
return true; return true;
}); });
}, [data, filtroMes, filtroAno, filtroCaixa, filtroTipo]); }, [data, somaCategorias, filtroMes, filtroAno, filtroCaixa, filtroTipo]);
const planejadasFiltradas = React.useMemo(() => { const planejadasFiltradas = React.useMemo(() => {
if (!Array.isArray(entradasPlanejadas)) return []; if (!Array.isArray(entradasPlanejadas)) return [];
@ -140,8 +175,6 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
}); });
}, [entradasPlanejadas, filtroMes, filtroAno, filtroTipo]); }, [entradasPlanejadas, filtroMes, filtroAno, filtroTipo]);
const [somaCategorias, setSomaCategorias] = React.useState([]);
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
// Efeito para buscar soma por categoria do backend // Efeito para buscar soma por categoria do backend
React.useEffect(() => { React.useEffect(() => {
@ -157,6 +190,10 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
// Normaliza o resultado conforme o backend (esperado array de objetos) // Normaliza o resultado conforme o backend (esperado array de objetos)
// Normaliza o resultado conforme o backend para receitas // Normaliza o resultado conforme o backend para receitas
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []); const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
console.log('[CruzamentoView] somaCategorias carregadas:', data.length, 'itens');
if (data.length > 0) {
console.log('[CruzamentoView] Exemplo soma:', data[0]);
}
setSomaCategorias(Array.isArray(data) ? data : []); setSomaCategorias(Array.isArray(data) ? data : []);
} catch (err) { } catch (err) {
console.error('[CruzamentoView] Erro ao buscar soma por categoria:', err); console.error('[CruzamentoView] Erro ao buscar soma por categoria:', err);
@ -174,6 +211,8 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
} }
}, [filtroMes, filtroAno, filtroTipo]); }, [filtroMes, filtroAno, filtroTipo]);
// Efeito para buscar transações detalhadas do período removido pois soma_por_categoria já traz os itens.
// Efeito para buscar totais planejados do mês vigente // Efeito para buscar totais planejados do mês vigente
React.useEffect(() => { React.useEffect(() => {
const fetchTotais = async () => { const fetchTotais = async () => {
@ -199,8 +238,29 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
fetchTotais(); fetchTotais();
}, [filtroMes, filtroAno, filtroTipo]); }, [filtroMes, filtroAno, filtroTipo]);
const [chartDataBackend, setChartDataBackend] = React.useState([]); // Modal state
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
const handleCategoryClick = (name, id) => {
console.log('[CruzamentoView] Clicou na categoria:', { name, id });
setSelectedCategory({ name, id });
setIsModalOpen(true);
};
const transactionsForSelectedCategory = React.useMemo(() => {
if (!selectedCategory) return [];
// Agora buscamos diretamente no objeto que o backend retornou já agrupado
const catObj = somaCategorias.find(c => {
const id = c.idcategoria ?? c.idCategoria ?? c.id;
const name = c.categoria || '';
return String(id) === String(selectedCategory.id) || name === selectedCategory.name;
});
const transactions = catObj?.transacoes || [];
console.log('[CruzamentoView] Modal: itens para', selectedCategory.name, ':', transactions.length);
return transactions;
}, [selectedCategory, somaCategorias]);
// Efeito para buscar dados do gráfico do backend // Efeito para buscar dados do gráfico do backend
React.useEffect(() => { React.useEffect(() => {
@ -241,7 +301,7 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
} }
const total_realizado = dadosFiltrados const total_realizado = dadosFiltrados
.filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado') .filter(t => !t?.status || t?.status === 'Recebido' || t?.status === 'Liquidado')
.reduce((acc, t) => acc + (t?.valor || 0), 0); .reduce((acc, t) => acc + (t?.valor || 0), 0);
const total_boletos_a_receber = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0); const total_boletos_a_receber = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0);
@ -257,14 +317,16 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
if (!Array.isArray(somaCategorias)) return []; if (!Array.isArray(somaCategorias)) return [];
return somaCategorias.map(item => ({ return somaCategorias.map(item => ({
name: item.categoria || 'Sem Categoria', name: item.categoria || 'Sem Categoria',
value: parseCurrency(item.total_entradas || 0) value: parseCurrency(item.total || item.total_entradas || 0)
})).filter(item => item.value > 0); })).filter(item => item.value > 0);
}, [somaCategorias]); }, [somaCategorias]);
const timelineData = useMemo(() => { const timelineData = useMemo(() => {
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']; const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const filteredExecutadas = dadosFiltrados.filter(item => item?.status === 'Recebido' || item?.status === 'Liquidado'); const filteredExecutadas = dadosFiltrados.filter(item =>
!item?.status || item?.status === 'Recebido' || item?.status === 'Liquidado'
);
const filteredPlanejadas = planejadasFiltradas; const filteredPlanejadas = planejadasFiltradas;
if (filtroTipo === 'ano') { if (filtroTipo === 'ano') {
@ -401,22 +463,16 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
const variacaoCategoria = useMemo(() => { const variacaoCategoria = useMemo(() => {
if (!Array.isArray(somaCategorias)) return []; if (!Array.isArray(somaCategorias)) return [];
return somaCategorias.map(item => { return somaCategorias.map(item => ({
const entradas = parseCurrency(item.total_entradas || 0); id: item.idcategoria ?? item.idCategoria ?? item.id,
const saidas = parseCurrency(item.total_saidas || 0); categoria: item.categoria || 'Sem Categoria',
const diferenca = parseCurrency(item.diferenca || 0); categoriaNome: item.categoria || '(Sem Categoria)',
const percentual = entradas > 0 ? ((entradas / (entradas + Math.abs(saidas))) * 100).toFixed(1) : 0; name: item.categoria || '(Sem Categoria)',
entradas: parseCurrency(item.total || item.total_entradas || 0),
return { saidas: parseCurrency(item.total_saidas || 0),
categoria: item.categoria || 'Sem Categoria', diferenca: parseCurrency(item.diferenca || 0),
categoriaNome: item.categoria || '(Sem Categoria)', percentual: 0 // Ajustar se necessário
name: item.categoria || '(Sem Categoria)', })).sort((a, b) => b.entradas - a.entradas);
entradas,
saidas,
diferenca,
percentual
};
}).sort((a, b) => b.entradas - a.entradas);
}, [somaCategorias]); }, [somaCategorias]);
// KPIs de variância: Usando os dados reais // KPIs de variância: Usando os dados reais
@ -774,7 +830,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
field: 'categoriaNome', field: 'categoriaNome',
header: 'Categoria', header: 'Categoria',
width: '250px', width: '250px',
render: (row) => <span className="font-semibold text-slate-900 dark:text-white pl-3">{row.categoriaNome}</span> render: (row) => (
<button
onClick={() => handleCategoryClick(row.categoria, row.id)}
className="font-semibold text-slate-900 dark:text-white pl-3 hover:text-emerald-500 transition-colors text-left w-full"
>
{row.categoriaNome}
</button>
)
}, },
{ {
field: 'entradas', field: 'entradas',
@ -786,18 +849,9 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
</span> </span>
) )
}, },
{
field: 'saidas',
header: 'Saídas',
width: '150px',
render: (row) => (
<span className="font-mono text-xs text-rose-400 text-right block w-full">
{formatCurrency(row.saidas)}
</span>
)
},
{ {
field: 'diferenca', field: 'diferenca',
header: 'Diferença', header: 'Diferença',
width: '150px', width: '150px',
render: (row) => ( render: (row) => (
@ -835,6 +889,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<TransactionsByCategoryModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
categoryName={selectedCategory?.name}
transactions={transactionsForSelectedCategory}
/>
</div> </div>
); );
}; };

View File

@ -182,6 +182,7 @@ export const boletosService = {
/** /**
* Busca detalhes de juros de um cliente específico * Busca detalhes de juros de um cliente específico
* Rota: GET /financeiro/cliente/boletos * Rota: GET /financeiro/cliente/boletos
* Resposta esperada: { cliente, dias_atraso, juros, multa, seuNumero, situacao, total_atualizado, valor_original, vencimento }
* @param {number|string} idempresa * @param {number|string} idempresa
*/ */
fetchJurosCliente: (idempresa) => handleRequest({ fetchJurosCliente: (idempresa) => handleRequest({
@ -191,9 +192,17 @@ export const boletosService = {
params: { idempresa } params: { idempresa }
}); });
const raw = response?.data ?? response; const raw = response?.data ?? response;
// Assume a estrutura padrão de retorno do sistema ou extraído diretamente
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw; const data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
return Array.isArray(data) ? data : []; // A API pode retornar um objeto único ou um array
if (Array.isArray(data)) return data;
// Se for objeto válido com campos conhecidos, envolve em array
if (data && typeof data === 'object' && !Array.isArray(data)) {
const items = Object.values(data);
// Se o próprio objeto tem campos de juros é um registro único
const hasBoletoFields = 'vencimento' in data || 'juros' in data || 'situacao' in data;
return hasBoletoFields ? [data] : items.filter(i => typeof i === 'object' && i !== null);
}
return [];
} }
}) })
}; };

View File

@ -11,12 +11,13 @@ export const extratoService = {
* Busca extrato bancário * Busca extrato bancário
* Rota: GET /extrato/apresentar * Rota: GET /extrato/apresentar
* Retorna array bruto; o consumidor filtra por tipoOperacao (C/D). * Retorna array bruto; o consumidor filtra por tipoOperacao (C/D).
* @param {Object} params - Opcional { mes, ano }
* @returns {Promise<Array<{idextrato, dataEntrada, descricao, valor, tipoOperacao, categoria, beneficiario_pagador, ...}>>} * @returns {Promise<Array<{idextrato, dataEntrada, descricao, valor, tipoOperacao, categoria, beneficiario_pagador, ...}>>}
*/ */
fetchExtrato: () => handleRequest({ fetchExtrato: (params) => handleRequest({
mockFn: () => simulateLatency([]), mockFn: () => simulateLatency([]),
apiFn: async () => { apiFn: async () => {
const response = await api.get('/extrato/apresentar'); const response = await api.get('/extrato/apresentar', { params });
const raw = response?.data ?? response; const raw = response?.data ?? response;
// Lógica robusta de extração (igual ao workspaceConciliacaoService) // Lógica robusta de extração (igual ao workspaceConciliacaoService)
@ -25,7 +26,6 @@ export const extratoService = {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
// Se for objeto (ex: { '0': {...}, '1': {...} }), converte para array // Se for objeto (ex: { '0': {...}, '1': {...} }), converte para array
// Mas cuidado com wrappers { success: true, ... } - idealmente o backend envia dados limpos ou em 'dados'
data = Object.values(data || {}); data = Object.values(data || {});
} }
@ -150,12 +150,13 @@ export const extratoService = {
* Fluxo de entrada e saída mensal * Fluxo de entrada e saída mensal
* Rota: GET /extrato/fluxo * Rota: GET /extrato/fluxo
* Retorna dados mensais de entrada e saída * Retorna dados mensais de entrada e saída
* @param {Object} params - Opcional { mes, ano }
* @returns {Promise<{ mensal: Array<{ mes, ano, entrada, saida, ... }> }>} * @returns {Promise<{ mensal: Array<{ mes, ano, entrada, saida, ... }> }>}
*/ */
fetchFluxo: () => handleRequest({ fetchFluxo: (params) => handleRequest({
mockFn: () => simulateLatency({ mensal: [], anual: [], diario: [] }), mockFn: () => simulateLatency({ mensal: [], anual: [], diario: [] }),
apiFn: async () => { apiFn: async () => {
const response = await api.get('/extrato/fluxo'); const response = await api.get('/extrato/fluxo', { params });
const raw = response?.data ?? response; const raw = response?.data ?? response;
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw ?? {}; const data = raw?.dados ?? raw?.Base_Dados_API ?? raw ?? {};
@ -203,13 +204,13 @@ export const extratoService = {
/** /**
* Busca navegação hierárquica detalhada por caixinha * Busca navegação hierárquica detalhada por caixinha
* Rota: GET /extrato/apresentar/caixinha/detalhado * Rota: GET /extrato/apresentar/caixinha/detalhado
* @param {number|string} id_caixinha * @param {Object} params - { caixinha, mes, ano }
*/ */
getCaixinhaDetalhada: (id_caixinha) => handleRequest({ getCaixinhaDetalhada: (params) => handleRequest({
mockFn: () => simulateLatency({}), mockFn: () => simulateLatency({}),
apiFn: async () => { apiFn: async () => {
const response = await api.get('/extrato/apresentar/caixinha/detalhado', { const response = await api.get('/extrato/apresentar/caixinha/detalhado', {
params: { caixinha: id_caixinha } params
}); });
return response.data; return response.data;
} }
@ -390,21 +391,25 @@ export const extratoService = {
/** /**
* Busca transações de um beneficiário específico * Busca transações de um beneficiário específico
* Rota: POST /beneficiario_aplicado * Rota: POST /beneficiario_aplicado
* Resposta esperada: { "categoria": "NOME DO CLIENTE", "linha_tempo": [...] }
* Retorna: { categoria: string, linhaTemp: Array }
* @param {string} beneficiario_pagador * @param {string} beneficiario_pagador
*/ */
fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({ fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({
mockFn: () => simulateLatency([]), mockFn: () => simulateLatency({ categoria: '', linha_tempo: [] }),
apiFn: async () => { apiFn: async () => {
const response = await api.post('/beneficiario_aplicado', { const response = await api.post('/beneficiario_aplicado', {
beneficiario_pagador: beneficiario_pagador beneficiario_pagador: beneficiario_pagador
}); });
const raw = response?.data ?? response; const raw = response?.data ?? response;
let data = raw?.dados ?? raw?.Base_Dados_API ?? raw; // A resposta é um objeto com "categoria" e "linha_tempo"
const root = raw?.dados ?? raw?.Base_Dados_API ?? raw;
if (!Array.isArray(data)) { const categoria = root?.categoria ?? '';
data = Object.values(data || {}); const linhaTemp = root?.linha_tempo ?? [];
} return {
return Array.isArray(data) ? data : []; categoria,
linha_tempo: Array.isArray(linhaTemp) ? linhaTemp : []
};
} }
}) })
}; };

View File

@ -164,13 +164,14 @@ export const fornecedoresService = {
deleteFornecedor: (id) => handleRequest({ deleteFornecedor: (id) => handleRequest({
mockFn: () => simulateLatency({ success: true }), mockFn: () => simulateLatency({ success: true }),
apiFn: async () => { apiFn: async () => {
// Tenta primeiro POST /fornecedores/delete enviando idfornecedores no corpo (JSON)
try { try {
console.log('[fornecedoresService] Tentando excluir fornecedor via POST /fornecedores/delete:', id); console.log('[fornecedoresService] Tentando excluir fornecedor via DELETE /fornecedores/delete:', id);
const response = await api.delete('/fornecedores/delete', { idfornecedores: id }); const response = await api.delete('/fornecedores/delete', {
data: { idfornecedores: id }
});
return response.data; return response.data;
} catch (err) { } catch (err) {
console.warn('[fornecedoresService] Falha no POST /fornecedores/delete, tentando fallback DELETE:', err.message); console.warn('[fornecedoresService] Falha no DELETE /fornecedores/delete, tentando fallback DELETE com ID na URL:', err.message);
// Fallback: tenta DELETE /fornecedores/:id // Fallback: tenta DELETE /fornecedores/:id
try { try {
const response = await api.delete(`/fornecedores/${id}`); const response = await api.delete(`/fornecedores/${id}`);

View File

@ -99,7 +99,9 @@ export const workspaceConciliacaoService = {
const mapped = data.map(item => ({ const mapped = data.map(item => ({
id: item.idextrato || item.id || 0, id: item.idextrato || item.id || 0,
idextrato: item.idextrato || item.id || 0,
data: item.dataEntrada || item.data || '', data: item.dataEntrada || item.data || '',
dataEntrada: item.dataEntrada || item.data || '',
descricao: item.descricao || item.titulo || '', descricao: item.descricao || item.titulo || '',
valor: parseValorBackend(item.valor), valor: parseValorBackend(item.valor),
tipo: item.tipoOperacao === 'D' ? 'DEBITO' : item.tipoOperacao === 'C' ? 'CREDITO' : 'DEBITO', tipo: item.tipoOperacao === 'D' ? 'DEBITO' : item.tipoOperacao === 'C' ? 'CREDITO' : 'DEBITO',
@ -107,12 +109,26 @@ export const workspaceConciliacaoService = {
tipoTransacao: item.tipoTransacao || '', tipoTransacao: item.tipoTransacao || '',
titulo: item.titulo || '', titulo: item.titulo || '',
caixaId: item.caixinha ? parseInt(item.caixinha) : null, caixaId: item.caixinha ? parseInt(item.caixinha) : null,
caixinha: item.caixinha || null,
categoriaId: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? parseInt(item.categoria) : null, categoriaId: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? parseInt(item.categoria) : null,
categoria: item.categoria || null,
regraId: item.regra && item.regra !== '0' && item.regra !== 0 ? parseInt(item.regra) : null, regraId: item.regra && item.regra !== '0' && item.regra !== 0 ? parseInt(item.regra) : null,
regra: item.regra || null,
beneficiario: item.beneficiario_pagador || null, beneficiario: item.beneficiario_pagador || null,
beneficiario_pagador: item.beneficiario_pagador || null,
cpfCnpjPagador: item.cpfCnpjPagador || '', cpfCnpjPagador: item.cpfCnpjPagador || '',
cpfCnpjRecebedor: item.cpfCnpjRecebedor || '', cpfCnpjRecebedor: item.cpfCnpjRecebedor || '',
status: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? 'CONCILIADA' : 'PENDENTE' status: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? 'CONCILIADA' : 'PENDENTE',
// Campos financeiros detalhados
juros: parseValorBackend(item.juros),
multa: parseValorBackend(item.multa),
abatimento: parseValorBackend(item.abatimento),
imposto: parseValorBackend(item.imposto),
adicionado: item.adicionado || "0.00",
desconto1: parseValorBackend(item.desconto1),
desconto2: parseValorBackend(item.desconto2),
desconto3: parseValorBackend(item.desconto3),
valorTotal: parseValorBackend(item.valorTotal || item.valor)
})); }));
console.log('[workspaceConciliacaoService] Dados mapeados:', mapped); console.log('[workspaceConciliacaoService] Dados mapeados:', mapped);
return mapped; return mapped;