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

View File

@ -19,7 +19,7 @@ import { conciliacaoService } from '@/services/conciliacaoService';
import { useToast } from '../hooks/useToast';
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 [rules, setRules] = 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 AdvancedFiltersModal from './AdvancedFiltersModal';
const ExcelTable = ({
function ExcelTable({
data = [],
columns,
filterDefs = [],
@ -21,7 +21,7 @@ const ExcelTable = ({
pendingEdits = {}, // Objeto com edições pendentes: { [rowId]: { [field]: value } }
showValidationButton = false, // Mostra botão de validação no rodapé
onValidateEdits = null // Callback para validar edições pendentes
}) => {
}) {
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({});
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });

View File

@ -4,20 +4,36 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from '@/components/ui/dialog';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
AlertCircle,
ExternalLink,
Edit,
CreditCard,
Hash,
Info,
Banknote,
Percent,
History,
ArrowRightLeft,
Settings,
CheckCircle2,
ArrowUpRight,
ArrowDownRight,
Calendar,
User,
FileText,
Tag,
AlertCircle,
ExternalLink,
Edit
Tag
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
@ -30,7 +46,7 @@ import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
* Modal de detalhamento de transação
* Exibe todos os detalhes de uma transação do extrato
*/
export const TransactionDetailModal = ({
export function TransactionDetailModal({
transaction,
open,
onOpenChange,
@ -38,19 +54,19 @@ export const TransactionDetailModal = ({
onViewInExtrato,
categorias = [],
caixas = []
}) => {
}) {
if (!transaction) return null;
const isCredit = transaction.tipoOperacao === 'C';
const isConciliado = transaction.categoria && transaction.categoria != 0;
// Helper para buscar nome da categoria
const getCategoriaNome = () => {
function getCategoriaNome() {
if (transaction.categoriaNome) return transaction.categoriaNome;
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));
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
@ -65,183 +81,191 @@ export const TransactionDetailModal = ({
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 max-w-2xl z-[9999]">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogContent className="bg-white dark:bg-[#0b1120] border-slate-200 dark:border-slate-800 max-w-2xl p-0 overflow-hidden z-[9999]">
{/* Header Superior - Destaque */}
<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={cn(
"w-12 h-12 rounded-xl flex items-center justify-center",
isCredit ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-500" : "bg-red-500/10 text-red-600 dark:text-red-500"
"w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm",
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>
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white">
{transaction.titulo || 'Transação'}
<div className="space-y-1">
<DialogDescription className="sr-only">
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>
<div className="flex items-center gap-2 mt-1">
<Badge className={cn(
"text-[10px] font-bold px-2 py-0.5",
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'}
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[9px] font-black tracking-tighter uppercase border-slate-200 dark:border-slate-800">
{transaction.tipoTransacao || 'Geral'}
</Badge>
<Badge className={cn(
"text-[10px] font-bold px-2 py-0.5",
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"
"text-[9px] font-black tracking-tighter uppercase",
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>
</div>
</div>
</div>
<div className="text-right">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Valor Total</p>
<div className={cn(
"text-2xl font-bold",
isCredit ? "text-emerald-600 dark:text-emerald-500" : "text-red-600 dark:text-red-500"
"text-2xl font-black tabular-nums",
isCredit ? "text-emerald-600" : "text-rose-600"
)}>
{isCredit ? '+' : '-'}{formatCurrency(transaction.valor)}
{isCredit ? '+' : '-'}{formatCurrency(transaction.valorTotal || transaction.valor)}
</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>
<DialogFooter className="flex gap-2">
{onEditTransaction && (
<Tabs defaultValue="geral" className="w-full">
<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
variant="outline"
className="border-blue-500 text-blue-600 dark:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10"
onClick={() => {
onEditTransaction(transaction);
onOpenChange(false);
}}
variant="ghost"
onClick={() => onOpenChange(false)}
className="text-[10px] font-black uppercase tracking-widest text-slate-400 hover:text-slate-900 dark:hover:text-white"
>
<Edit size={16} className="mr-2" />
Editar Transação
Fechar Detalhes
</Button>
)}
{/*
{onViewInExtrato && (
<Button
variant="outline"
className="border-slate-200 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
onClick={() => {
onViewInExtrato(transaction);
onOpenChange(false);
}}
>
<ExternalLink size={16} className="mr-2" />
Visualizar no Extrato
</Button>
)}
*/}
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
className="text-slate-600 dark:text-slate-400"
>
Fechar
</Button>
{onEditTransaction && (
<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"
onClick={() => {
onEditTransaction(transaction);
onOpenChange(false);
}}
>
<Edit size={16} className="mr-2" />
Editar Transação
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</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
*/
export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
export function useConciliacaoV2(defaultView = 'conciliadas') {
console.log('[useConciliacaoV2] Hook iniciado');
let toast;
try {
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)
};
}
const toast = useToast();
console.log('[useConciliacaoV2] Toast inicializado');
// Normalização de alias de rotas
const normalizeSubView = (view) => {
if (!view) return 'conciliadas';
if (view === 'extrato') return 'conciliadas';
if (view === 'extrato' || view === 'extrato-completo') return 'extrato-completo';
if (view === 'pendentes') return 'nao-categorizadas';
return view;
};
@ -126,6 +115,175 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
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
useEffect(() => {
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
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 () => {
try {

View File

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

View File

@ -35,6 +35,7 @@ export function useFluxoCaixa() {
const [fluxoData, setFluxoData] = useState({ mensal: [] });
const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] });
const [saldoConsolidado, setSaldoConsolidado] = useState(null);
const [porCaixinha, setPorCaixinha] = useState([]);
const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1));
const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString());
@ -47,24 +48,188 @@ export function useFluxoCaixa() {
const mesAtu = filtroMes;
const anoAtu = filtroAno;
const tipo = filtroTipo;
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData] = await Promise.all([
extratoService.fetchExtrato(),
// Monta os parâmetros de forma dinâmica conforme o tipo de filtro
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.fetchSaldoArmazenado(),
extratoService.fetchFluxo(),
extratoService.getSomaPorCategoria({ mes: mesAtu, ano: anoAtu }),
extratoService.fetchSaldoConsolidado({ mes: mesAtu, ano: anoAtu })
extratoService.fetchFluxo(params),
extratoService.getSomaPorCategoria(params),
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 });
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);
} catch (err) {
console.error('[useFluxoCaixa] Erro ao carregar dados:', err);
setError(err.message || 'Erro ao carregar fluxo de caixa');
setExtrato([]);
@ -72,12 +237,12 @@ export function useFluxoCaixa() {
setSaldoArmazenado([]);
setFluxoData({ mensal: [] });
setSomaCategoria({ por_categoria: [] });
setPorCaixinha([]);
setSaldoConsolidado(null);
} finally {
setLoading(false);
}
}, [filtroMes, filtroAno]);
}, [filtroMes, filtroAno, filtroTipo]);
useEffect(() => {
loadData();
@ -124,15 +289,25 @@ export function useFluxoCaixa() {
const getChartData = useCallback((type = 'mensal') => {
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 key = type === 'mensal'
? `${item.ano}-${String(item.mes).padStart(2, '0')}`
: `${item.ano}`;
let key = '';
let label = '';
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]) {
acc[key] = {
name: type === 'mensal' ? `${item.mes}/${item.ano}` : `${item.ano}`,
name: label,
periodo: key,
receitas: 0,
despesas: 0
@ -152,6 +327,7 @@ export function useFluxoCaixa() {
return {
loading,
error,
extrato,
receitas,
receitasCard,
despesas,
@ -161,6 +337,7 @@ export function useFluxoCaixa() {
bateComSaldo,
getChartData,
somaCategoria,
porCaixinha,
saldoConsolidado,
filtroMes,
setFiltroMes,

View File

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

View File

@ -477,19 +477,19 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
</PieChart>
</ResponsiveContainer>
<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>
</div>
</>
)}
</div>
<div className="p-6 bg-slate-50 dark:bg-slate-900/40 grid grid-cols-2 gap-3">
{boletosPieData.slice(0, 4).map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full" 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-[10px] font-bold text-slate-900 dark:text-white ml-auto">{item.value}</span>
<div className="p-6 bg-slate-50 dark:bg-slate-900/40 flex flex-col gap-3">
{boletosPieData.map((item, index) => (
<div key={index} className="flex items-center gap-3 py-1.5 group/item">
<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-[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-[clamp(0.85rem,1.1vw,1rem)] font-bold text-slate-900 dark:text-white">{item.count} boleto{item.count !== 1 ? 's' : ''}</span>
</div>
))}
</div>

View File

@ -60,6 +60,8 @@ import {
ResponsiveContainer
} from 'recharts';
import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip';
import CaixinhaDetailsModal from '../components/CaixinhaDetailsModal';
import { LayoutGrid } from 'lucide-react';
// Standardized Filter Header matching CruzamentoView pattern
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
const ChartSection = ({ getChartData }) => {
const data = useMemo(() => getChartData('mensal'), [getChartData]);
const ChartSection = ({ getChartData, tipoPeriodo }) => {
const data = useMemo(() => getChartData(tipoPeriodo === 'ano' ? 'mensal' : 'diario'), [getChartData, tipoPeriodo]);
return (
<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" />
</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>
</CardHeader>
@ -216,7 +220,7 @@ const CategorySection = ({ data = [] }) => {
return data.map((item, index) => ({
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]
})).filter(item => item.value > 0);
}, [data]);
@ -281,15 +285,16 @@ const CategorySection = ({ data = [] }) => {
};
// Caixinha Table Section - Using correct API data structure
const CaixinhaSection = ({ data = [] }) => {
const CaixinhaSection = ({ data = [], onSelectCaixinha }) => {
const caixinhaData = useMemo(() => {
if (!Array.isArray(data)) return [];
return data.map(item => ({
caixinha: item.categoria || 'Padrão',
receitas: Math.abs(Number(item.total_entradas) || 0),
despesas: Math.abs(Number(item.total_saidas) || 0),
saldo: Number(item.diferenca) || 0
id: item.id,
caixinha: item.caixinha || 'Padrão',
receitas: Math.abs(Number(item.receitas || item.total_entradas) || 0),
despesas: Math.abs(Number(item.despesas || item.total_saidas) || 0),
saldo: Number(item.saldo || item.diferenca) || 0
}));
}, [data]);
@ -323,8 +328,17 @@ const CaixinhaSection = ({ data = [] }) => {
</TableRow>
) : (
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">
<TableCell className="text-slate-900 dark:text-white text-sm font-medium">{item.caixinha}</TableCell>
<TableRow
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-rose-600 dark:text-rose-400 text-sm font-mono text-right">{formatCurrency(item.despesas)}</TableCell>
<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 }) => {
// Transform category data into transaction-like rows for display
const transactionData = useMemo(() => {
if (!Array.isArray(data)) return [];
return data.flatMap(item => {
const rows = [];
// Add entrada row if exists
if (Number(item.total_entradas) > 0) {
rows.push({
id: `${item.idcategoria}-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;
});
return data.map(item => ({
id: item.idextrato || item.id,
categoria: item.categoria_nome || item.categoria || 'Sem Categoria',
tipoOperacao: item.tipoOperacao,
valor: item.valor,
descricao: item.descricao || item.historico || '---',
data: item.dataEntrada || item.data || item.data_entrada
}));
}, [data]);
const columns = [
@ -394,7 +386,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
</span>
)
},
{
/* {
field: 'categoria',
header: 'CATEGORIA',
render: (row) => (
@ -402,7 +394,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
{row.categoria || 'Sem Categoria'}
</Badge>
)
},
}, */
{
field: 'tipo',
header: 'TIPO',
@ -458,6 +450,7 @@ export const FluxoCaixaView = () => {
const {
loading,
error,
extrato,
receitas,
receitasCard,
despesas,
@ -467,6 +460,7 @@ export const FluxoCaixaView = () => {
bateComSaldo,
getChartData,
somaCategoria,
porCaixinha,
saldoConsolidado,
filtroMes,
setFiltroMes,
@ -478,6 +472,11 @@ export const FluxoCaixaView = () => {
} = useFluxoCaixa();
const [tipoPeriodo, setTipoPeriodo] = useState('mensal');
const [selectedCaixinha, setSelectedCaixinha] = useState(null);
const handleSelectCaixinha = (caixinha) => {
setSelectedCaixinha(caixinha);
};
if (error) {
return (
@ -530,7 +529,7 @@ export const FluxoCaixaView = () => {
<KPICard
title="Saldo Disponível"
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}
colorClass="text-emerald-600 dark:text-emerald-400"
isPositive={true}
@ -541,17 +540,27 @@ export const FluxoCaixaView = () => {
{/* Chart Section */}
<div className="mb-6">
<ChartSection getChartData={getChartData} />
<ChartSection getChartData={getChartData} tipoPeriodo={filtroTipo} />
</div>
{/* Category and Caixinha Sections - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<CategorySection data={somaCategoria?.por_categoria || []} />
<CaixinhaSection data={somaCategoria?.por_categoria || []} />
<CaixinhaSection data={porCaixinha} onSelectCaixinha={handleSelectCaixinha} />
</div>
{/* 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>
);
};

View File

@ -18,7 +18,10 @@ import { GerenciamentoView } from './GerenciamentoView';
import { ExtratoCompletoView } from './ExtratoCompletoView';
import { cn } from '@/lib/utils';
const ActivityLog = ({ isOpen, onClose }) => {
/**
* Componente para exibir log de atividades simplificado
*/
function ActivityLog({ isOpen, onClose }) {
if (!isOpen) return null;
return (
@ -61,69 +64,37 @@ const ActivityLog = ({ isOpen, onClose }) => {
</div>
</div>
);
};
}
export const ConciliacaoView = ({ initialView }) => {
/**
* Componente Principal de Conciliação V2
*/
export function ConciliacaoView({ initialView }) {
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;
try {
console.log('[ConciliacaoView] Chamando useConciliacaoV2(initialView)...');
const hookResult = useConciliacaoV2(initialView);
console.log('[ConciliacaoView] Hook retornou:', hookResult);
const { state, actions } = useConciliacaoV2(initialView);
state = hookResult?.state;
actions = hookResult?.actions;
console.log('[ConciliacaoView] Hook executado com sucesso');
console.log('[ConciliacaoView] State:', state);
console.log('[ConciliacaoView] Actions:', actions);
} 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
}
// Configuração das sub-views (movido para dentro para ajudar com TDZ em bundles complexos)
const subViews = [
{ id: 'conciliadas', label: 'Transações Conciliadas', icon: FileText, description: 'Navegação hierárquica por caixas' },
{ id: 'nao-categorizadas', label: 'Pendentes', icon: AlertCircle, description: 'Aguardando categorização' },
{ id: 'extrato-completo', label: 'Extrato Completo', icon: History, description: 'Todos os lançamentos do período' },
{ id: 'gerenciamento', label: 'Gerenciamento', icon: Settings, description: 'Caixas, Categorias, Regras' },
];
const activeSubView = state?.activeSubView || 'conciliadas';
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);
// 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 = () => {
switch (activeSubView) {
case 'conciliadas':
return <TransacoesConciliadasView state={state} actions={actions} />;
case 'extrato-completo':
return <ExtratoCompletoView state={state} actions={actions} />;
case 'nao-categorizadas':
return <TransacoesNaoCategorizadasView state={state} actions={actions} />;
case 'extrato-completo':
return <ExtratoCompletoView state={state} actions={actions} />;
case 'gerenciamento':
return <GerenciamentoView state={state} actions={actions} />;
default:
@ -135,24 +106,11 @@ export const ConciliacaoView = ({ initialView }) => {
<div className="animate-in fade-in duration-700 relative">
{/* Header Section */}
<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">
<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
{/* <Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
</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>
{/* Linha 2: Menu de Navegação */}
@ -201,4 +159,4 @@ export const ConciliacaoView = ({ initialView }) => {
/>
</div>
);
};
}

View File

@ -21,17 +21,19 @@ import { formatDate, formatCurrency } from '../../utils/dateUtils';
import { StatementRow } from '../../components/StatementRow';
import { useStatementRefData } from '../../hooks/useStatementRefData';
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 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 [filterType, setFilterType] = React.useState('todos'); // 'todos' | 'C' | 'D'
const [filterMonth, setFilterMonth] = React.useState(String(new Date().getMonth() + 1));
const [filterYear, setFilterYear] = React.useState(String(new Date().getFullYear()));
const [transacaoSelecionada, setTransacaoSelecionada] = React.useState(null);
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [isDetailOpen, setIsDetailOpen] = React.useState(false);
const {
extratoCompleto = [],
@ -202,8 +204,10 @@ export const ExtratoCompletoView = ({ state, actions }) => {
categoryName={getCategoryName(row.categoria)}
ruleName={getRuleName(row.regra)}
onClick={(t) => {
console.log('[ExtratoCompletoView] Clique na transação:', t);
console.log('[ExtratoCompletoView] Abrindo modal de detalhes...');
setTransacaoSelecionada(t);
setIsDialogOpen(true);
setIsDetailOpen(true);
}}
showCategory={true}
showStatus={false}
@ -229,6 +233,18 @@ export const ExtratoCompletoView = ({ state, actions }) => {
caixas={state?.caixas}
actions={actions}
/>
<TransactionDetailModal
transaction={transacaoSelecionada}
open={isDetailOpen}
onOpenChange={setIsDetailOpen}
onEditTransaction={(t) => {
setTransacaoSelecionada(t);
setIsDialogOpen(true);
}}
categorias={state?.categorias}
caixas={state?.caixas}
/>
</div>
);
};

View File

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

View File

@ -51,7 +51,7 @@ import {
} from 'recharts';
/** 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 '—';
let s = name.replace(/\s+/g, ' ').trim();
if (!s) return '—';
@ -65,51 +65,53 @@ const sanitizeLabel = (name) => {
s = s.replace(repeatedPhrase, (_, phrase, rest) => (phrase + rest).replace(/\s+/g, ' ').trim());
}
return s || '—';
};
}
const 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">
<div
className={cn(
"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"
function AccordionItem({ title, icon: Icon, value, count, children, isOpen, onClick, color }) {
return (
<div className="border border-slate-200 dark:border-slate-800 rounded-xl mb-3 overflow-hidden bg-white dark:bg-slate-900/50">
<div
className={cn(
"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>
{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) */
const truncateLegend = (name, maxLen = 42) => {
function truncateLegend(name, maxLen = 42) {
const clean = sanitizeLabel(name);
if (clean.length <= maxLen) return clean;
return clean.substring(0, maxLen - 3).trim() + '...';
};
}
/** 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;
const item = payload[0];
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>
</div>
);
};
}
export const TransacoesConciliadasView = ({ state, actions }) => {
export function TransacoesConciliadasView({ state, actions }) {
const {
caixas,
categorias,

View File

@ -26,7 +26,7 @@ import { StatementRow } from '../../components/StatementRow';
import { useStatementRefData } from '../../hooks/useStatementRefData';
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
// NUNCA colocar hooks após early returns ou condições

View File

@ -39,12 +39,14 @@ import {
import { FinanceiroChartTooltip } from '../../components/FinanceiroChartTooltip';
import { extratoService } from '@/services/extratoService';
import ExcelTable from '../../components/ExcelTable';
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils';
/**
* 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;
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">
@ -85,12 +87,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, pe
</CardContent>
</Card>
);
};
}
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);
export const CruzamentoDespesasView = ({ state }) => {
export function CruzamentoDespesasView({ state }) {
const { despesasPlanejadas = [], despesasExecutadas = [], caixinhas = [] } = state;
const [filtroTipo, setFiltroTipo] = React.useState('mes'); // 'mes' | 'ano'
@ -101,6 +103,8 @@ export const CruzamentoDespesasView = ({ state }) => {
const [somaCategorias, setSomaCategorias] = React.useState([]);
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
const [chartDataBackend, setChartDataBackend] = React.useState([]);
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
@ -115,6 +119,9 @@ export const CruzamentoDespesasView = ({ state }) => {
});
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
React.useEffect(() => {
const fetchTotais = async () => {
@ -141,6 +148,7 @@ export const CruzamentoDespesasView = ({ state }) => {
}, [filtroMes, filtroAno, filtroTipo]);
// Efeito para buscar dados do gráfico do backend
React.useEffect(() => {
const fetchChartData = async () => {
setIsLoadingChart(true);
@ -179,6 +187,10 @@ export const CruzamentoDespesasView = ({ state }) => {
// Normaliza o resultado conforme o backend (esperado array de objetos)
// 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 : []);
console.log('[CruzamentoDespesasView] somaCategorias carregadas:', data.length, 'itens');
if (data.length > 0) {
console.log('[CruzamentoDespesasView] Exemplo soma:', data[0]);
}
setSomaCategorias(Array.isArray(data) ? data : []);
} catch (err) {
console.error('[CruzamentoDespesasView] Erro ao buscar soma por categoria:', err);
@ -195,14 +207,21 @@ export const CruzamentoDespesasView = ({ state }) => {
}
}, [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
const matchPeriod = (dataStr) => {
const { year, month } = parseDateInfo(dataStr);
if (filtroTipo === 'ano') return year === Number(filtroAno);
return year === Number(filtroAno) && month === Number(filtroMes);
const y = Number(year);
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(() => {
@ -214,39 +233,71 @@ export const CruzamentoDespesasView = ({ state }) => {
}, [despesasPlanejadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
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;
// 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;
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
const comparativoCategoria = useMemo(() => {
if (!Array.isArray(somaCategorias)) return [];
return somaCategorias.map(item => {
const entradas = parseCurrency(item.total_entradas || 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;
const saidas = parseCurrency(item.total || item.total_saidas || 0);
return {
id: item.idcategoria ?? item.idCategoria ?? item.id,
categoria: item.categoria || 'Sem Categoria',
categoriaNome: item.categoria || '(Sem Categoria)',
name: item.categoria || '(Sem Categoria)',
entradas,
entradas: 0,
saidas,
diferenca,
percentual,
executado: Math.abs(saidas), // Para o gráfico de pizza (valor absoluto da saída)
diferenca: 0,
percentual: 0,
executado: 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]);
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(() => {
if (chartDataBackend.length > 0) {
return chartDataBackend.map(item => {
@ -467,7 +518,7 @@ export const CruzamentoDespesasView = ({ state }) => {
<KPICard
title="Total Planejado"
value={formatCurrency(totalPlanejado)}
subtext="Total esperado (despesas V2)"
subtext="Total esperado"
icon={Target}
colorClass="text-slate-500"
loading={isLoadingTotais}
@ -475,7 +526,7 @@ export const CruzamentoDespesasView = ({ state }) => {
<KPICard
title="Total Executado"
value={formatCurrency(totalExecutado)}
subtext="Total realizado (extrato)"
subtext="Total realizado"
icon={CheckCircle2}
colorClass="text-rose-500"
highlight
@ -670,17 +721,12 @@ export const CruzamentoDespesasView = ({ state }) => {
header: 'Categoria',
width: '200px',
render: (row) => (
<span className="font-semibold text-slate-900 dark:text-white pl-3">{row.categoriaNome}</span>
)
},
{
field: 'entradas',
header: 'Entradas',
width: '150px',
render: (row) => (
<span className="font-mono text-xs text-emerald-400 text-right block w-full">
{formatCurrency(row.entradas)}
</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"
>
{row.categoriaNome}
</button>
)
},
{
@ -731,6 +777,14 @@ export const CruzamentoDespesasView = ({ state }) => {
</div>
</CardContent>
</Card>
<TransactionsByCategoryModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
categoryName={selectedCategory?.name}
transactions={transactionsForSelectedCategory}
/>
</div>
);
};

View File

@ -112,8 +112,8 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
const [clientBoletosFromApi, setClientBoletosFromApi] = useState([]);
const [loadingBoletosCliente, setLoadingBoletosCliente] = useState(false);
// Estado para o Extrato (Transações)
const [clientExtrato, setClientExtrato] = useState([]);
// Estado para o Extrato (Transações): { categoria: string, linha_tempo: Array }
const [clientExtrato, setClientExtrato] = useState({ categoria: '', linha_tempo: [] });
const [loadingExtrato, setLoadingExtrato] = useState(false);
const [clientInterest, setClientInterest] = useState([]);
@ -243,7 +243,7 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
// Carrega Extrato (Transações) com LOGS DETALHADOS
useEffect(() => {
if (clientTab !== 'extrato' || !selectedClient) {
setClientExtrato([]);
setClientExtrato({ categoria: '', linha_tempo: [] });
return;
}
@ -252,12 +252,16 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
const clientName = selectedClient.nome || '';
try {
// Agora usa a rota otimizada /beneficiario_aplicado
// Rota /beneficiario_aplicado retorna { categoria, linha_tempo }
const data = await extratoService.fetchBeneficiarioAplicado(clientName);
setClientExtrato(data);
setClientExtrato(
data && typeof data === 'object' && Array.isArray(data.linha_tempo)
? data
: { categoria: '', linha_tempo: [] }
);
} catch (error) {
console.error('❌ Erro ao buscar extrato:', error);
setClientExtrato([]);
setClientExtrato({ categoria: '', linha_tempo: [] });
} finally {
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">
<div className="space-y-4">
<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">
{clientExtrato.length} registros
{clientExtrato.linha_tempo?.length ?? 0} registros
</Badge>
</div>
{loadingExtrato ? (
<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" />
<span className="text-sm">Buscando transações via /beneficiario_aplicado...</span>
<span className="text-sm">Buscando transações...</span>
</div>
) : clientExtrato.length === 0 ? (
) : !clientExtrato.linha_tempo?.length ? (
<div className="text-center py-12 text-slate-500">
<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-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>
</div>
) : (
<div className="h-[500px]">
<ExcelTable
data={clientExtrato}
data={clientExtrato.linha_tempo.map((row, i) => ({ ...row, _idx: i }))}
columns={[
{
field: 'data',
field: 'dataEntrada',
header: 'Data',
width: '120px',
width: '110px',
render: (row) => (
<span className="text-slate-600 dark:text-slate-400">
{formatDate(row.data || row.dataEntrada)}
<span className="text-slate-600 dark:text-slate-400 text-xs">
{formatDate(row.dataEntrada || row.data)}
</span>
)
},
{
field: 'descricao',
header: 'Descrição',
width: '300px',
field: 'titulo',
header: 'Título / Descrição',
width: '280px',
render: (row) => (
<div className="flex flex-col">
<span className="font-medium text-slate-900 dark:text-white truncate">
{row.descricao || 'Sem descrição'}
<div className="flex flex-col gap-0.5">
<span className="font-semibold text-slate-900 dark:text-white text-xs leading-tight">
{row.titulo || 'N/D'}
</span>
<span className="text-[10px] text-slate-500 uppercase">
{row.categoria || 'Sem Categoria'}
<span className="text-[10px] text-slate-500 truncate">
{row.descricao || ''}
</span>
</div>
)
@ -1221,23 +1230,58 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
{
field: 'valor',
header: 'Valor',
width: '150px',
width: '130px',
render: (row) => {
const isCredit = row.tipoOperacao === 'C' || row.valor > 0;
const isCredit = row.tipoOperacao === 'C';
return (
<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 ? <ArrowUpCircle className="w-4 h-4" /> : <ArrowDownCircle className="w-4 h-4" />}
{formatCurrency(Math.abs(row.valor))}
{isCredit ? <ArrowUpCircle className="w-3.5 h-3.5 shrink-0" /> : <ArrowDownCircle className="w-3.5 h-3.5 shrink-0" />}
{formatCurrency(Math.abs(Number(row.valor) || 0))}
</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()}
pageSize={15}
rowKey={(row) => row._idx}
pageSize={20}
/>
</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">
<div className="space-y-4">
<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">
{clientInterest.length} registros
</Badge>
@ -1408,62 +1452,70 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
{loadingInterest ? (
<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" />
<span className="text-sm">Buscando juros via /financeiro/cliente/boletos...</span>
<span className="text-sm">Buscando juros...</span>
</div>
) : clientInterest.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<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 className="h-[500px]">
<ExcelTable
data={clientInterest}
columns={[
{
field: 'data',
header: 'Data',
width: '120px',
render: (row) => (
<span className="text-slate-600 dark:text-slate-400">
{formatDate(row.data || row.data_vencimento)}
</span>
)
},
{
field: 'descricao',
header: 'Descrição',
width: '300px',
render: (row) => (
<span className="font-medium text-slate-900 dark:text-white truncate">
{row.descricao || `Juros #${row.numero || ''}`}
</span>
)
},
{
field: 'valor_juros',
header: 'Valor Juros',
width: '150px',
render: (row) => (
<span className="font-bold text-amber-600">
+{formatCurrency(row.valor_juros || row.juros || 0)}
</span>
)
},
{
field: 'valor',
header: 'Valor Base',
width: '150px',
render: (row) => (
<span className="text-slate-500 text-sm">
{formatCurrency(row.valor || 0)}
</span>
)
}
]}
rowKey={(row, i) => i}
pageSize={15}
/>
<div className="space-y-3">
{clientInterest.map((row, i) => {
const situacaoColor =
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'
: row.situacao === 'CANCELADO'
? 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border-slate-300 dark:border-slate-600'
: row.situacao === 'VENCIDO' || row.situacao === 'ATRASADO'
? 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20'
: 'bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
return (
<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>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5"> {row.seuNumero || row.seu_numero || '—'}</p>
</div>
<Badge className={cn('text-[10px] font-bold uppercase shrink-0 border', situacaoColor)}>
{row.situacao || 'N/D'}
</Badge>
</div>
<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>
</div>
<div>
<p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Valor Original</p>
<p className="text-sm font-semibold text-slate-900 dark:text-white mt-0.5">{formatCurrency(Number(row.valor_original) || 0)}</p>
</div>
<div>
<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
? 'text-amber-600 dark:text-amber-400'
: 'text-slate-900 dark:text-white'
)}>
{formatCurrency(Number(row.juros) || 0)} + {formatCurrency(Number(row.multa) || 0)}
</p>
</div>
<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>
</div>
{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>

View File

@ -26,6 +26,8 @@ import {
TableRow
} from '@/components/ui/table';
import ExcelTable from '../../components/ExcelTable';
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
import { extratoService } from '@/services/extratoService';
import { cn } from '@/lib/utils';
import {
@ -51,7 +53,7 @@ import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils'
/**
* 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;
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">
@ -82,12 +84,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, lo
</CardContent>
</Card>
);
};
}
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);
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 [filtroAno, setFiltroAno] = React.useState(new Date().getFullYear().toString());
const [filtroCaixa, setFiltroCaixa] = React.useState('todos');
@ -103,25 +105,58 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
});
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
const dadosFiltrados = React.useMemo(() => {
if (!Array.isArray(data)) return [];
return data.filter(item => {
const matchPeriod = (dataStr) => {
const { year, month } = parseDateInfo(dataStr);
if (filtroTipo === 'ano') return year === Number(filtroAno);
if (filtroTipo === 'mes') return year === Number(filtroAno) && month === Number(filtroMes);
return true;
};
// Agora usamos os dados brutos de transações agrupadas de somaCategorias para filtros globais se necessário,
// mas para o CruzamentoView, o dadosFiltrados costuma vir da prop 'data'.
// Se quisermos que o dadosFiltrados reflita o que o backend agrupou:
const allTrans = somaCategorias.flatMap(c => c.transacoes || []);
const sourceData = (filtroTipo === 'mes' && allTrans.length > 0) ? allTrans : (Array.isArray(data) ? data : []);
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
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;
return true;
});
}, [data, filtroMes, filtroAno, filtroCaixa, filtroTipo]);
}, [data, somaCategorias, filtroMes, filtroAno, filtroCaixa, filtroTipo]);
const planejadasFiltradas = React.useMemo(() => {
if (!Array.isArray(entradasPlanejadas)) return [];
@ -140,8 +175,6 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
});
}, [entradasPlanejadas, filtroMes, filtroAno, filtroTipo]);
const [somaCategorias, setSomaCategorias] = React.useState([]);
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
// Efeito para buscar soma por categoria do backend
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 para receitas
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 : []);
} catch (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]);
// 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
React.useEffect(() => {
const fetchTotais = async () => {
@ -199,8 +238,29 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
fetchTotais();
}, [filtroMes, filtroAno, filtroTipo]);
const [chartDataBackend, setChartDataBackend] = React.useState([]);
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
// Modal state
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
React.useEffect(() => {
@ -241,7 +301,7 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
}
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);
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 [];
return somaCategorias.map(item => ({
name: item.categoria || 'Sem Categoria',
value: parseCurrency(item.total_entradas || 0)
value: parseCurrency(item.total || item.total_entradas || 0)
})).filter(item => item.value > 0);
}, [somaCategorias]);
const timelineData = useMemo(() => {
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;
if (filtroTipo === 'ano') {
@ -401,22 +463,16 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
const variacaoCategoria = useMemo(() => {
if (!Array.isArray(somaCategorias)) return [];
return somaCategorias.map(item => {
const entradas = parseCurrency(item.total_entradas || 0);
const saidas = parseCurrency(item.total_saidas || 0);
const diferenca = parseCurrency(item.diferenca || 0);
const percentual = entradas > 0 ? ((entradas / (entradas + Math.abs(saidas))) * 100).toFixed(1) : 0;
return {
categoria: item.categoria || 'Sem Categoria',
categoriaNome: item.categoria || '(Sem Categoria)',
name: item.categoria || '(Sem Categoria)',
entradas,
saidas,
diferenca,
percentual
};
}).sort((a, b) => b.entradas - a.entradas);
return somaCategorias.map(item => ({
id: item.idcategoria ?? item.idCategoria ?? item.id,
categoria: item.categoria || 'Sem Categoria',
categoriaNome: item.categoria || '(Sem Categoria)',
name: item.categoria || '(Sem Categoria)',
entradas: parseCurrency(item.total || item.total_entradas || 0),
saidas: parseCurrency(item.total_saidas || 0),
diferenca: parseCurrency(item.diferenca || 0),
percentual: 0 // Ajustar se necessário
})).sort((a, b) => b.entradas - a.entradas);
}, [somaCategorias]);
// KPIs de variância: Usando os dados reais
@ -774,7 +830,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
field: 'categoriaNome',
header: 'Categoria',
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',
@ -786,18 +849,9 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
</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',
header: 'Diferença',
width: '150px',
render: (row) => (
@ -835,6 +889,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
</div>
</CardContent>
</Card>
<TransactionsByCategoryModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
categoryName={selectedCategory?.name}
transactions={transactionsForSelectedCategory}
/>
</div>
);
};

View File

@ -182,6 +182,7 @@ export const boletosService = {
/**
* Busca detalhes de juros de um cliente específico
* Rota: GET /financeiro/cliente/boletos
* Resposta esperada: { cliente, dias_atraso, juros, multa, seuNumero, situacao, total_atualizado, valor_original, vencimento }
* @param {number|string} idempresa
*/
fetchJurosCliente: (idempresa) => handleRequest({
@ -191,9 +192,17 @@ export const boletosService = {
params: { idempresa }
});
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;
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
* Rota: GET /extrato/apresentar
* 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, ...}>>}
*/
fetchExtrato: () => handleRequest({
fetchExtrato: (params) => handleRequest({
mockFn: () => simulateLatency([]),
apiFn: async () => {
const response = await api.get('/extrato/apresentar');
const response = await api.get('/extrato/apresentar', { params });
const raw = response?.data ?? response;
// Lógica robusta de extração (igual ao workspaceConciliacaoService)
@ -25,7 +26,6 @@ export const extratoService = {
if (!Array.isArray(data)) {
// 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 || {});
}
@ -150,12 +150,13 @@ export const extratoService = {
* Fluxo de entrada e saída mensal
* Rota: GET /extrato/fluxo
* Retorna dados mensais de entrada e saída
* @param {Object} params - Opcional { mes, ano }
* @returns {Promise<{ mensal: Array<{ mes, ano, entrada, saida, ... }> }>}
*/
fetchFluxo: () => handleRequest({
fetchFluxo: (params) => handleRequest({
mockFn: () => simulateLatency({ mensal: [], anual: [], diario: [] }),
apiFn: async () => {
const response = await api.get('/extrato/fluxo');
const response = await api.get('/extrato/fluxo', { params });
const raw = response?.data ?? response;
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw ?? {};
@ -203,13 +204,13 @@ export const extratoService = {
/**
* Busca navegação hierárquica detalhada por caixinha
* 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({}),
apiFn: async () => {
const response = await api.get('/extrato/apresentar/caixinha/detalhado', {
params: { caixinha: id_caixinha }
params
});
return response.data;
}
@ -390,21 +391,25 @@ export const extratoService = {
/**
* Busca transações de um beneficiário específico
* Rota: POST /beneficiario_aplicado
* Resposta esperada: { "categoria": "NOME DO CLIENTE", "linha_tempo": [...] }
* Retorna: { categoria: string, linhaTemp: Array }
* @param {string} beneficiario_pagador
*/
fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({
mockFn: () => simulateLatency([]),
mockFn: () => simulateLatency({ categoria: '', linha_tempo: [] }),
apiFn: async () => {
const response = await api.post('/beneficiario_aplicado', {
beneficiario_pagador: beneficiario_pagador
});
const raw = response?.data ?? response;
let data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
if (!Array.isArray(data)) {
data = Object.values(data || {});
}
return Array.isArray(data) ? data : [];
// A resposta é um objeto com "categoria" e "linha_tempo"
const root = raw?.dados ?? raw?.Base_Dados_API ?? raw;
const categoria = root?.categoria ?? '';
const linhaTemp = root?.linha_tempo ?? [];
return {
categoria,
linha_tempo: Array.isArray(linhaTemp) ? linhaTemp : []
};
}
})
};

View File

@ -164,13 +164,14 @@ export const fornecedoresService = {
deleteFornecedor: (id) => handleRequest({
mockFn: () => simulateLatency({ success: true }),
apiFn: async () => {
// Tenta primeiro POST /fornecedores/delete enviando idfornecedores no corpo (JSON)
try {
console.log('[fornecedoresService] Tentando excluir fornecedor via POST /fornecedores/delete:', id);
const response = await api.delete('/fornecedores/delete', { idfornecedores: id });
console.log('[fornecedoresService] Tentando excluir fornecedor via DELETE /fornecedores/delete:', id);
const response = await api.delete('/fornecedores/delete', {
data: { idfornecedores: id }
});
return response.data;
} 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
try {
const response = await api.delete(`/fornecedores/${id}`);

View File

@ -99,7 +99,9 @@ export const workspaceConciliacaoService = {
const mapped = data.map(item => ({
id: item.idextrato || item.id || 0,
idextrato: item.idextrato || item.id || 0,
data: item.dataEntrada || item.data || '',
dataEntrada: item.dataEntrada || item.data || '',
descricao: item.descricao || item.titulo || '',
valor: parseValorBackend(item.valor),
tipo: item.tipoOperacao === 'D' ? 'DEBITO' : item.tipoOperacao === 'C' ? 'CREDITO' : 'DEBITO',
@ -107,12 +109,26 @@ export const workspaceConciliacaoService = {
tipoTransacao: item.tipoTransacao || '',
titulo: item.titulo || '',
caixaId: item.caixinha ? parseInt(item.caixinha) : null,
caixinha: item.caixinha || 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,
regra: item.regra || null,
beneficiario: item.beneficiario_pagador || null,
beneficiario_pagador: item.beneficiario_pagador || null,
cpfCnpjPagador: item.cpfCnpjPagador || '',
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);
return mapped;