Atualização 4 | Ajustes sobre o financeiro
This commit is contained in:
parent
6d7ec7c9aa
commit
538a75092d
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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">Nº {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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
})
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 : []
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue