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",
|
"feature": "Corrigido erro nos cards de Pend\u00eancias/Revis\u00e3o do GR para Despachantes",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"timestamp": "2026-02-08"
|
"timestamp": "2026-02-08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"feature": "Ajuste do painel Status de Cobran\u00e7a: centro com total de boletos e legenda vertical com quantitativo e tipografia fluida.",
|
||||||
|
"status": "active",
|
||||||
|
"timestamp": "2026-02-08"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -3,7 +3,7 @@ import { X, Filter, Trash2, Check, ChevronDown } from 'lucide-react';
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const AdvancedFiltersModal = ({ isOpen, onClose, onApply, options = {}, initialFilters = {}, config = [] }) => {
|
function AdvancedFiltersModal({ isOpen, onClose, onApply, options = {}, initialFilters = {}, config = [] }) {
|
||||||
const [filters, setFilters] = React.useState(initialFilters);
|
const [filters, setFilters] = React.useState(initialFilters);
|
||||||
|
|
||||||
// Sync with parent when modal opens or initialFilters change
|
// Sync with parent when modal opens or initialFilters change
|
||||||
|
|
|
||||||
|
|
@ -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 { formatDate, formatCurrency } from '../utils/dateUtils';
|
||||||
import { useToast } from '../hooks/useToast';
|
import { useToast } from '../hooks/useToast';
|
||||||
|
|
||||||
export const CategorizacaoDialog = ({
|
export function CategorizacaoDialog({
|
||||||
transacao,
|
transacao,
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
categorias = [],
|
categorias = [],
|
||||||
caixas = [],
|
caixas = [],
|
||||||
actions
|
actions
|
||||||
}) => {
|
}) {
|
||||||
const [formData, setFormData] = React.useState({
|
const [formData, setFormData] = React.useState({
|
||||||
descricao: '',
|
descricao: '',
|
||||||
categoria: '',
|
categoria: '',
|
||||||
|
|
@ -135,14 +135,14 @@ export const CategorizacaoDialog = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ValorDisplay = ({ valor, tipo }) => {
|
function ValorDisplay({ valor, tipo }) {
|
||||||
const isCredit = tipo === 'CREDITO' || tipo === 'C' || valor > 0;
|
const isCredit = tipo === 'CREDITO' || tipo === 'C' || valor > 0;
|
||||||
return (
|
return (
|
||||||
<span className={cn("text-sm font-bold", isCredit ? "text-emerald-600" : "text-rose-600")}>
|
<span className={cn("text-sm font-bold", isCredit ? "text-emerald-600" : "text-rose-600")}>
|
||||||
{formatCurrency(Math.abs(valor))}
|
{formatCurrency(Math.abs(valor))}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { conciliacaoService } from '@/services/conciliacaoService';
|
||||||
import { useToast } from '../hooks/useToast';
|
import { useToast } from '../hooks/useToast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export const CategoryRulesPopup = ({ isOpen, onClose, category, onUpdate }) => {
|
export function CategoryRulesPopup({ isOpen, onClose, category, onUpdate }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [rules, setRules] = useState([]);
|
const [rules, setRules] = useState([]);
|
||||||
const [allRules, setAllRules] = useState([]);
|
const [allRules, setAllRules] = useState([]);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Edit2, Trash2, Filter, ChevronLeft, ChevronRight, ChevronsLeft, Chevron
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import AdvancedFiltersModal from './AdvancedFiltersModal';
|
import AdvancedFiltersModal from './AdvancedFiltersModal';
|
||||||
|
|
||||||
const ExcelTable = ({
|
function ExcelTable({
|
||||||
data = [],
|
data = [],
|
||||||
columns,
|
columns,
|
||||||
filterDefs = [],
|
filterDefs = [],
|
||||||
|
|
@ -21,7 +21,7 @@ const ExcelTable = ({
|
||||||
pendingEdits = {}, // Objeto com edições pendentes: { [rowId]: { [field]: value } }
|
pendingEdits = {}, // Objeto com edições pendentes: { [rowId]: { [field]: value } }
|
||||||
showValidationButton = false, // Mostra botão de validação no rodapé
|
showValidationButton = false, // Mostra botão de validação no rodapé
|
||||||
onValidateEdits = null // Callback para validar edições pendentes
|
onValidateEdits = null // Callback para validar edições pendentes
|
||||||
}) => {
|
}) {
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [filters, setFilters] = useState({});
|
const [filters, setFilters] = useState({});
|
||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,39 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter
|
DialogDescription,
|
||||||
|
DialogFooter
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger
|
||||||
|
} from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
ExternalLink,
|
||||||
|
Edit,
|
||||||
|
CreditCard,
|
||||||
|
Hash,
|
||||||
|
Info,
|
||||||
|
Banknote,
|
||||||
|
Percent,
|
||||||
|
History,
|
||||||
|
ArrowRightLeft,
|
||||||
|
Settings,
|
||||||
|
CheckCircle2,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ArrowDownRight,
|
ArrowDownRight,
|
||||||
Calendar,
|
Calendar,
|
||||||
User,
|
User,
|
||||||
FileText,
|
FileText,
|
||||||
Tag,
|
Tag
|
||||||
AlertCircle,
|
|
||||||
ExternalLink,
|
|
||||||
Edit
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
|
import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
|
||||||
|
|
@ -30,7 +46,7 @@ import { formatDate, formatDateTime, formatCurrency } from '../utils/dateUtils';
|
||||||
* Modal de detalhamento de transação
|
* Modal de detalhamento de transação
|
||||||
* Exibe todos os detalhes de uma transação do extrato
|
* Exibe todos os detalhes de uma transação do extrato
|
||||||
*/
|
*/
|
||||||
export const TransactionDetailModal = ({
|
export function TransactionDetailModal({
|
||||||
transaction,
|
transaction,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|
@ -38,19 +54,19 @@ export const TransactionDetailModal = ({
|
||||||
onViewInExtrato,
|
onViewInExtrato,
|
||||||
categorias = [],
|
categorias = [],
|
||||||
caixas = []
|
caixas = []
|
||||||
}) => {
|
}) {
|
||||||
if (!transaction) return null;
|
if (!transaction) return null;
|
||||||
|
|
||||||
const isCredit = transaction.tipoOperacao === 'C';
|
const isCredit = transaction.tipoOperacao === 'C';
|
||||||
const isConciliado = transaction.categoria && transaction.categoria != 0;
|
const isConciliado = transaction.categoria && transaction.categoria != 0;
|
||||||
|
|
||||||
// Helper para buscar nome da categoria
|
// Helper para buscar nome da categoria
|
||||||
const getCategoriaNome = () => {
|
function getCategoriaNome() {
|
||||||
if (transaction.categoriaNome) return transaction.categoriaNome;
|
if (transaction.categoriaNome) return transaction.categoriaNome;
|
||||||
if (!transaction.categoria || transaction.categoria == 0) return 'Não conciliado';
|
if (!transaction.categoria || transaction.categoria == 0) return 'Não conciliado';
|
||||||
|
|
||||||
const cat = categorias.find(c => String(c.id) === String(transaction.categoria) || String(c.idcategoria) === String(transaction.categoria));
|
const cat = categorias.find(c => String(c.id) === String(transaction.categoria) || String(c.idcategoria) === String(transaction.categoria));
|
||||||
return cat ? (cat.name || cat.nome || cat.categoria) : `Categoria ${transaction.categoria}`;
|
return cat ? (cat.name || cat.nome || cat.categoria) : 'Categoria ' + transaction.categoria;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper para buscar nome da caixinha
|
// Helper para buscar nome da caixinha
|
||||||
|
|
@ -65,183 +81,191 @@ export const TransactionDetailModal = ({
|
||||||
const caixinhaNome = getCaixinhaNome();
|
const caixinhaNome = getCaixinhaNome();
|
||||||
|
|
||||||
|
|
||||||
|
const InfoField = ({ label, value, icon: Icon, className = "" }) => (
|
||||||
|
<div className={cn("space-y-1.5", className)}>
|
||||||
|
<span className="text-[10px] text-slate-500 dark:text-slate-400 font-black uppercase tracking-widest flex items-center gap-1.5">
|
||||||
|
{Icon && <Icon size={12} className="text-slate-400" />}
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-white truncate">
|
||||||
|
{value === null || value === undefined || value === "" || value === 0 ? (
|
||||||
|
<span className="text-slate-300 dark:text-slate-700 italic font-normal text-xs">Não informado</span>
|
||||||
|
) : (
|
||||||
|
typeof value === 'number' && label.toLowerCase().includes('valor') || ['juros', 'multa', 'abatimento', 'imposto', 'desconto'].some(k => label.toLowerCase().includes(k))
|
||||||
|
? formatCurrency(value)
|
||||||
|
: value
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 max-w-2xl z-[9999]">
|
<DialogContent className="bg-white dark:bg-[#0b1120] border-slate-200 dark:border-slate-800 max-w-2xl p-0 overflow-hidden z-[9999]">
|
||||||
<DialogHeader>
|
{/* Header Superior - Destaque */}
|
||||||
<div className="flex items-center justify-between">
|
<div className={cn(
|
||||||
|
"p-6 border-b border-slate-100 dark:border-slate-800",
|
||||||
|
isCredit ? "bg-emerald-500/5" : "bg-rose-500/5"
|
||||||
|
)}>
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"w-12 h-12 rounded-xl flex items-center justify-center",
|
"w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm",
|
||||||
isCredit ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-500" : "bg-red-500/10 text-red-600 dark:text-red-500"
|
isCredit ? "bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20" : "bg-rose-500/10 text-rose-600 dark:bg-rose-500/20"
|
||||||
)}>
|
)}>
|
||||||
{isCredit ? <ArrowUpRight size={24} /> : <ArrowDownRight size={24} />}
|
{isCredit ? <ArrowUpRight size={28} /> : <ArrowDownRight size={28} />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white">
|
<DialogDescription className="sr-only">
|
||||||
{transaction.titulo || 'Transação'}
|
Detalhamento da transação bancária
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white line-clamp-1">
|
||||||
|
{transaction.titulo || transaction.descricao || 'Transação sem título'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2">
|
||||||
<Badge className={cn(
|
<Badge variant="outline" className="text-[9px] font-black tracking-tighter uppercase border-slate-200 dark:border-slate-800">
|
||||||
"text-[10px] font-bold px-2 py-0.5",
|
{transaction.tipoTransacao || 'Geral'}
|
||||||
isCredit ? "bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : "bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400"
|
|
||||||
)}>
|
|
||||||
{transaction.tipoTransacao || 'GERAL'}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge className={cn(
|
<Badge className={cn(
|
||||||
"text-[10px] font-bold px-2 py-0.5",
|
"text-[9px] font-black tracking-tighter uppercase",
|
||||||
isConciliado ? "bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : "bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
isConciliado ? "bg-emerald-500/10 text-emerald-600 border-emerald-500/20" : "bg-amber-500/10 text-amber-600 border-amber-500/20"
|
||||||
)}>
|
)}>
|
||||||
{isConciliado ? 'CONCILIADO' : 'PENDENTE'}
|
{isConciliado ? 'Conciliado' : 'Pendente'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Valor Total</p>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"text-2xl font-bold",
|
"text-2xl font-black tabular-nums",
|
||||||
isCredit ? "text-emerald-600 dark:text-emerald-500" : "text-red-600 dark:text-red-500"
|
isCredit ? "text-emerald-600" : "text-rose-600"
|
||||||
)}>
|
)}>
|
||||||
{isCredit ? '+' : '-'}{formatCurrency(transaction.valor)}
|
{isCredit ? '+' : '-'}{formatCurrency(transaction.valorTotal || transaction.valor)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-6 py-4">
|
|
||||||
{/* Informações Básicas */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<FileText size={16} />
|
|
||||||
Informações Básicas
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">ID da Transação</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">#{transaction.idextrato || 'N/D'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase flex items-center gap-1">
|
|
||||||
<Calendar size={12} />
|
|
||||||
Data e Hora
|
|
||||||
</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{formatDateTime(transaction.dataEntrada)}</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Descrição</span>
|
|
||||||
<p className="text-sm text-slate-900 dark:text-white mt-1">{transaction.descricao || 'Sem descrição'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Partes Envolvidas */}
|
|
||||||
{(transaction.beneficiario_pagador || transaction.cpfCnpjPagador || transaction.cpfCnpjRecebedor) && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<User size={16} />
|
|
||||||
Partes Envolvidas
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
|
|
||||||
{transaction.beneficiario_pagador && (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Beneficiário/Pagador</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{transaction.beneficiario_pagador}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{transaction.cpfCnpjPagador && (
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">CPF/CNPJ Pagador</span>
|
|
||||||
<p className="text-sm font-mono text-slate-900 dark:text-white mt-1">{transaction.cpfCnpjPagador}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{transaction.cpfCnpjRecebedor && (
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">CPF/CNPJ Recebedor</span>
|
|
||||||
<p className="text-sm font-mono text-slate-900 dark:text-white mt-1">{transaction.cpfCnpjRecebedor}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Conciliação */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<Tag size={16} />
|
|
||||||
Conciliação
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Categoria</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">
|
|
||||||
{categoriaNome}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Regra Aplicada</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">
|
|
||||||
{transaction.regra ? `Regra #${transaction.regra}` : 'N/D'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{transaction.caixinha && (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase">Caixinha</span>
|
|
||||||
<p className="text-sm font-bold text-slate-900 dark:text-white mt-1">{caixinhaNome}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Observações */}
|
|
||||||
{transaction.adicionado && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<AlertCircle size={16} />
|
|
||||||
Observações
|
|
||||||
</h3>
|
|
||||||
<div className="bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl">
|
|
||||||
<p className="text-sm text-slate-900 dark:text-white">{transaction.adicionado}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex gap-2">
|
<Tabs defaultValue="geral" className="w-full">
|
||||||
{onEditTransaction && (
|
<div className="px-6 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
||||||
|
<TabsList className="h-12 bg-transparent gap-6 p-0 w-full justify-start overflow-x-auto no-scrollbar">
|
||||||
|
<TabsTrigger value="geral" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||||
|
Geral
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="financeiro" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||||
|
Financeiro
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="participantes" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||||
|
Participantes
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="tecnico" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||||
|
Técnico
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="conciliacao" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 border-b-2 border-transparent data-[state=active]:border-blue-600 rounded-none h-full px-1 text-[10px] font-black uppercase tracking-widest transition-all">
|
||||||
|
Conciliação
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 h-[350px] overflow-y-auto custom-scrollbar">
|
||||||
|
<TabsContent value="geral" className="mt-0 outline-none space-y-6">
|
||||||
|
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
|
||||||
|
<InfoField label="Título" value={transaction.titulo} icon={Info} className="col-span-2" />
|
||||||
|
<InfoField label="Data de Entrada" value={formatDateTime(transaction.dataEntrada || transaction.dataTransacao || transaction.data)} icon={Calendar} />
|
||||||
|
<InfoField label="Tipo Transação" value={transaction.tipoTransacao} icon={Tag} />
|
||||||
|
<InfoField label="Valor Lançado" value={transaction.valor || transaction.valorTotal} icon={Banknote} />
|
||||||
|
<InfoField label="Operação" value={transaction.tipoOperacao === 'D' ? 'Débito' : 'Crédito'} icon={ArrowRightLeft} />
|
||||||
|
<div className="col-span-2 space-y-2">
|
||||||
|
<span className="text-[10px] text-slate-500 font-black uppercase tracking-widest">Descrição Completa</span>
|
||||||
|
<div className="p-3 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-lg text-xs leading-relaxed text-slate-600 dark:text-slate-400">
|
||||||
|
{transaction.descricao || 'Sem descrição detalhada disponível.'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="financeiro" className="mt-0 outline-none space-y-6">
|
||||||
|
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
|
||||||
|
<InfoField label="Valor Base/Original" value={transaction.valor} icon={Banknote} />
|
||||||
|
<InfoField label="Acréscimos (Adicionado)" value={transaction.adicionado} icon={ArrowUpRight} />
|
||||||
|
<InfoField label="Juros" value={transaction.juros} icon={Percent} />
|
||||||
|
<InfoField label="Multa" value={transaction.multa} icon={Percent} />
|
||||||
|
<InfoField label="Abatimento" value={transaction.abatimento} icon={ArrowDownRight} />
|
||||||
|
<InfoField label="Imposto Retido" value={transaction.imposto} icon={FileText} />
|
||||||
|
<InfoField label="Desconto 01" value={transaction.desconto1} icon={Tag} />
|
||||||
|
<InfoField label="Desconto 02" value={transaction.desconto2} icon={Tag} />
|
||||||
|
<InfoField label="Desconto 03" value={transaction.desconto3} icon={Tag} />
|
||||||
|
<InfoField label="Valor Final Liquido" value={transaction.valorTotal || transaction.valor} icon={Banknote} className="col-span-2 pt-4 border-t border-slate-100 dark:border-slate-800" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="participantes" className="mt-0 outline-none space-y-6">
|
||||||
|
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
|
||||||
|
<InfoField label="Principal Envolvido" value={transaction.beneficiario_pagador} icon={User} className="col-span-2" />
|
||||||
|
<InfoField label="Nome do Pagador" value={transaction.nomePagador} icon={User} />
|
||||||
|
<InfoField label="CPF/CNPJ Pagador" value={transaction.cpfCnpjPagador} icon={FileText} />
|
||||||
|
<InfoField label="Nome do Recebedor" value={transaction.nomeRecebedor} icon={User} />
|
||||||
|
<InfoField label="CPF/CNPJ Recebedor" value={transaction.cpfCnpjRecebedor} icon={FileText} />
|
||||||
|
<InfoField label="Estabelecimento" value={transaction.estabelecimento} icon={CreditCard} className="col-span-2" />
|
||||||
|
<InfoField label="Agência" value={transaction.agencia} icon={Hash} />
|
||||||
|
<InfoField label="Conta Bancária" value={transaction.contaBancaria} icon={CreditCard} />
|
||||||
|
<InfoField label="Chave PIX" value={transaction.chavePix} icon={Tag} className="col-span-2" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tecnico" className="mt-0 outline-none space-y-6">
|
||||||
|
<div className="grid grid-cols-1 gap-y-6">
|
||||||
|
<InfoField label="ID no Extrato (Interno)" value={transaction.idextrato} icon={Hash} />
|
||||||
|
<InfoField label="ID da Transação (Banco)" value={transaction.idTransacao} icon={ExternalLink} />
|
||||||
|
<InfoField label="EndToEnd ID (PIX)" value={transaction.endToEndId} icon={ExternalLink} />
|
||||||
|
<InfoField label="Código de Barras" value={transaction.codigoBarras} icon={Hash} />
|
||||||
|
<InfoField label="Linha Digitável" value={transaction.linhaDigitavel} icon={FileText} />
|
||||||
|
<div className="grid grid-cols-2 gap-12">
|
||||||
|
<InfoField label="Nosso Número" value={transaction.nossoNumero} icon={Hash} />
|
||||||
|
<InfoField label="Seu Número" value={transaction.seuNumero} icon={Hash} />
|
||||||
|
</div>
|
||||||
|
<InfoField label="Origem da Movimentação" value={transaction.origemMovimentacao} icon={History} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="conciliacao" className="mt-0 outline-none space-y-6">
|
||||||
|
<div className="grid grid-cols-2 gap-y-6 gap-x-12">
|
||||||
|
<InfoField label="Status Atual" value={isConciliado ? 'CONCILIADO' : 'PENDENTE'} icon={CheckCircle2} className="col-span-2" />
|
||||||
|
<InfoField label="Categoria" value={categoriaNome} icon={Tag} />
|
||||||
|
<InfoField label="Caixinha / Destino" value={caixinhaNome} icon={Banknote} />
|
||||||
|
<InfoField label="Regra Aplicada" value={transaction.regra ? 'ID #' + transaction.regra : null} icon={Settings} className="col-span-2" />
|
||||||
|
<InfoField label="Data de Emissão" value={transaction.dataEmissao} icon={Calendar} />
|
||||||
|
<InfoField label="Data de Vencimento" value={transaction.dataVencimento} icon={Calendar} />
|
||||||
|
<InfoField label="Data Limite" value={transaction.dataLimite} icon={Calendar} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DialogFooter className="p-6 bg-slate-50/50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
className="border-blue-500 text-blue-600 dark:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10"
|
onClick={() => onOpenChange(false)}
|
||||||
onClick={() => {
|
className="text-[10px] font-black uppercase tracking-widest text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||||
onEditTransaction(transaction);
|
|
||||||
onOpenChange(false);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Edit size={16} className="mr-2" />
|
Fechar Detalhes
|
||||||
Editar Transação
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
{/*
|
{onEditTransaction && (
|
||||||
{onViewInExtrato && (
|
<Button
|
||||||
<Button
|
className="bg-blue-600 hover:bg-blue-700 text-white rounded-xl h-11 px-6 shadow-lg shadow-blue-500/20 transition-all font-black text-[10px] uppercase tracking-widest"
|
||||||
variant="outline"
|
onClick={() => {
|
||||||
className="border-slate-200 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
onEditTransaction(transaction);
|
||||||
onClick={() => {
|
onOpenChange(false);
|
||||||
onViewInExtrato(transaction);
|
}}
|
||||||
onOpenChange(false);
|
>
|
||||||
}}
|
<Edit size={16} className="mr-2" />
|
||||||
>
|
Editar Transação
|
||||||
<ExternalLink size={16} className="mr-2" />
|
</Button>
|
||||||
Visualizar no Extrato
|
)}
|
||||||
</Button>
|
</div>
|
||||||
)}
|
|
||||||
*/}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
className="text-slate-600 dark:text-slate-400"
|
|
||||||
>
|
|
||||||
Fechar
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Hook para gerenciar a lógica de Conciliação V2
|
||||||
*/
|
*/
|
||||||
export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
|
export function useConciliacaoV2(defaultView = 'conciliadas') {
|
||||||
console.log('[useConciliacaoV2] Hook iniciado');
|
console.log('[useConciliacaoV2] Hook iniciado');
|
||||||
|
|
||||||
let toast;
|
const toast = useToast();
|
||||||
try {
|
console.log('[useConciliacaoV2] Toast inicializado');
|
||||||
toast = useToast();
|
|
||||||
console.log('[useConciliacaoV2] Toast inicializado');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[useConciliacaoV2] Erro ao inicializar toast:', error);
|
|
||||||
// Fallback para toast básico
|
|
||||||
toast = {
|
|
||||||
success: (msg, title) => console.log('[Toast]', title, msg),
|
|
||||||
error: (msg, title) => console.error('[Toast]', title, msg),
|
|
||||||
notifyFields: (fields) => console.warn('[Toast] Campos obrigatórios:', fields)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalização de alias de rotas
|
// Normalização de alias de rotas
|
||||||
const normalizeSubView = (view) => {
|
const normalizeSubView = (view) => {
|
||||||
if (!view) return 'conciliadas';
|
if (!view) return 'conciliadas';
|
||||||
if (view === 'extrato') return 'conciliadas';
|
if (view === 'extrato' || view === 'extrato-completo') return 'extrato-completo';
|
||||||
if (view === 'pendentes') return 'nao-categorizadas';
|
if (view === 'pendentes') return 'nao-categorizadas';
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
@ -126,6 +115,175 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Função auxiliar para recarregar dados
|
||||||
|
const recarregarDados = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const extrato = await workspaceConciliacaoService.fetchExtrato();
|
||||||
|
setExtratoCompleto(Array.isArray(extrato) ? extrato : []);
|
||||||
|
// Separar transações conciliadas e não categorizadas
|
||||||
|
const conciliadas = extrato.filter(t => t.status === 'CONCILIADA' && t.categoriaId);
|
||||||
|
const naoCategorizadas = extrato.filter(t => t.status === 'PENDENTE' || !t.categoriaId);
|
||||||
|
|
||||||
|
setTransacoesConciliadas(conciliadas);
|
||||||
|
setTransacoesNaoCategorizadas(naoCategorizadas);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useConciliacaoV2] Erro ao recarregar dados:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Busca dados para o nível de navegação atual USANDO A NOVA ROTA HIERÁRQUICA
|
||||||
|
const fetchNivelNavegacao = async (nivel, filters = {}) => {
|
||||||
|
setIsNavLoading(true);
|
||||||
|
console.log('[useConciliacaoV2] Buscando dados para nível:', nivel, filters);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
if (nivel === 0) {
|
||||||
|
// Nível 0: Caixas (Agora usando a rota detalhada para cada caixa para matar o 'cruzamentos')
|
||||||
|
const sourceCaixas = filters.caixas || caixas;
|
||||||
|
|
||||||
|
// Buscar totais para cada caixa usando a rota detalhada em paralelo
|
||||||
|
console.log('[useConciliacaoV2] Buscando totais detalhados para todas as caixas (Substituindo cruzamentos)...');
|
||||||
|
const enrichedCaixas = await Promise.all(sourceCaixas.map(async (c) => {
|
||||||
|
try {
|
||||||
|
const detailed = await workspaceConciliacaoService.fetchExtratoDetalhado(c.id, { mes: filtroMes, ano: filtroAno });
|
||||||
|
|
||||||
|
// Fallback: Calcular totais manualmente se o resumo não vier
|
||||||
|
const categorias = detailed.categorias || [];
|
||||||
|
const calcValor = categorias.reduce((acc, cat) => acc + (Number(cat.valor_total) || 0), 0);
|
||||||
|
const calcTransacoes = categorias.reduce((acc, cat) => {
|
||||||
|
const catTotal = cat.total_transacoes || (cat.beneficiarios || []).reduce((bAcc, ben) => bAcc + (ben.total_transacoes || 0), 0);
|
||||||
|
return acc + catTotal;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
totalTransacoes: detailed.resumo?.total_transacoes || calcTransacoes,
|
||||||
|
totalValor: detailed.resumo?.valor_total || calcValor,
|
||||||
|
tipo: 'caixa'
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Erro ao buscar detalhe da caixa ${c.id}:`, e);
|
||||||
|
return { ...c, totalTransacoes: 0, totalValor: 0, tipo: 'caixa' };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
data = enrichedCaixas;
|
||||||
|
|
||||||
|
} else if (nivel === 1) {
|
||||||
|
// Nível 1: Categorias (Vindo da API Detalhada da Caixinha)
|
||||||
|
const caixinhaId = filters.caixinha?.id || filters.caixa?.id || caixaSelecionado?.id;
|
||||||
|
if (!caixinhaId) throw new Error('Caixinha não selecionada para nível 1');
|
||||||
|
|
||||||
|
console.log('[useConciliacaoV2] >>> ACESSANDO NOVA ROTA HIERÁRQUICA DO BACKEND <<<');
|
||||||
|
console.log(`[useConciliacaoV2] Rota: /extrato/apresentar/caixinha/detalhado?caixinha=${caixinhaId}&mes=${filtroMes}&ano=${filtroAno}`);
|
||||||
|
const detailedData = await workspaceConciliacaoService.fetchExtratoDetalhado(caixinhaId, { mes: filtroMes, ano: filtroAno });
|
||||||
|
|
||||||
|
data = (detailedData.categorias || []).map(cat => {
|
||||||
|
// Calcular sub-totais de transações se não vier
|
||||||
|
const subTransacoes = cat.total_transacoes || (cat.beneficiarios || []).reduce((acc, ben) => acc + (ben.total_transacoes || 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `cat_${cat.categoria}`,
|
||||||
|
nome: cat.categoria || 'Sem Categoria',
|
||||||
|
totalTransacoes: subTransacoes,
|
||||||
|
totalValor: cat.valor_total || 0,
|
||||||
|
tipo: 'categoria',
|
||||||
|
beneficiarios: cat.beneficiarios || [],
|
||||||
|
cor: categorias.find(c => c.nome === cat.categoria)?.cor || '#3b82f6'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (nivel === 2) {
|
||||||
|
// Nível 2: Beneficiários (Dados já estão no item selecionado)
|
||||||
|
const categoriaItem = filters.categoria || categoriaSelecionada;
|
||||||
|
data = (categoriaItem?.beneficiarios || []).map(ben => ({
|
||||||
|
id: `ben_${ben.beneficiario}`,
|
||||||
|
nome: ben.beneficiario || 'Sem Beneficiário',
|
||||||
|
beneficiario: ben.beneficiario,
|
||||||
|
totalTransacoes: ben.total_transacoes || 0,
|
||||||
|
totalValor: ben.valor_total || 0,
|
||||||
|
tipo: 'regra',
|
||||||
|
transacoes: ben.transacoes || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
} else if (nivel === 3) {
|
||||||
|
// Nível 3: Transações (Normalizar dados para garantir compatibilidade com gráficos/tabelas)
|
||||||
|
const detalheItem = filters.regra || detalheSelecionado;
|
||||||
|
const rawTransacoes = detalheItem?.transacoes || [];
|
||||||
|
|
||||||
|
data = rawTransacoes.map(t => ({
|
||||||
|
...t,
|
||||||
|
id: t.id || t.idextrato || Math.random(),
|
||||||
|
data: t.data || t.dataEntrada || t.data_entrada || '',
|
||||||
|
valor: Number(t.valor || t.valor_total || 0),
|
||||||
|
descricao: t.descricao || t.historico || '',
|
||||||
|
beneficiario: t.beneficiario || t.beneficiario_pagador || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackendNavData(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useConciliacaoV2] Erro ao buscar dados de navegação:', error);
|
||||||
|
toast.error('Erro ao navegar nos dados', 'Erro');
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setIsNavLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navegação hierárquica usando dados do backend
|
||||||
|
const navegarPara = async (tipo, item) => {
|
||||||
|
console.log('[useConciliacaoV2] Navegando para:', tipo, item);
|
||||||
|
|
||||||
|
let novoNivel = 0;
|
||||||
|
if (tipo === 'caixa') {
|
||||||
|
setCaixaSelecionado(item);
|
||||||
|
setCategoriaSelecionada(null);
|
||||||
|
setDetalheSelecionado(null);
|
||||||
|
novoNivel = 1;
|
||||||
|
setCaminhoNavegacao([{ tipo: 'caixa', item }]);
|
||||||
|
} else if (tipo === 'categoria') {
|
||||||
|
setCategoriaSelecionada(item);
|
||||||
|
setDetalheSelecionado(null);
|
||||||
|
novoNivel = 2;
|
||||||
|
setCaminhoNavegacao(prev => [...prev, { tipo: 'categoria', item }]);
|
||||||
|
} else if (tipo === 'regra') {
|
||||||
|
setDetalheSelecionado(item);
|
||||||
|
novoNivel = 3;
|
||||||
|
setCaminhoNavegacao(prev => [...prev, { tipo: 'regra', item }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNivelNavegacao(novoNivel);
|
||||||
|
await fetchNivelNavegacao(novoNivel, { [tipo]: item });
|
||||||
|
};
|
||||||
|
|
||||||
|
const voltarNavegacao = async () => {
|
||||||
|
if (nivelNavegacao === 0) return;
|
||||||
|
|
||||||
|
const novoNivel = nivelNavegacao - 1;
|
||||||
|
setNivelNavegacao(novoNivel);
|
||||||
|
setCaminhoNavegacao(prev => prev.slice(0, novoNivel));
|
||||||
|
|
||||||
|
if (novoNivel === 0) {
|
||||||
|
setCaixaSelecionado(null);
|
||||||
|
setCategoriaSelecionada(null);
|
||||||
|
setDetalheSelecionado(null);
|
||||||
|
await fetchNivelNavegacao(0);
|
||||||
|
} else if (novoNivel === 1) {
|
||||||
|
setCategoriaSelecionada(null);
|
||||||
|
setDetalheSelecionado(null);
|
||||||
|
await fetchNivelNavegacao(1, { caixinha: caixaSelecionado });
|
||||||
|
} else if (novoNivel === 2) {
|
||||||
|
setDetalheSelecionado(null);
|
||||||
|
await fetchNivelNavegacao(2, { categoria: categoriaSelecionada });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Carregar dados iniciais do backend
|
// Carregar dados iniciais do backend
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[useConciliacaoV2] useEffect executado - montando componente');
|
console.log('[useConciliacaoV2] useEffect executado - montando componente');
|
||||||
|
|
@ -662,24 +820,6 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função auxiliar para recarregar dados
|
|
||||||
const recarregarDados = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const extrato = await workspaceConciliacaoService.fetchExtrato();
|
|
||||||
setExtratoCompleto(Array.isArray(extrato) ? extrato : []);
|
|
||||||
// Separar transações conciliadas e não categorizadas
|
|
||||||
const conciliadas = extrato.filter(t => t.status === 'CONCILIADA' && t.categoriaId);
|
|
||||||
const naoCategorizadas = extrato.filter(t => t.status === 'PENDENTE' || !t.categoriaId);
|
|
||||||
|
|
||||||
setTransacoesConciliadas(conciliadas);
|
|
||||||
setTransacoesNaoCategorizadas(naoCategorizadas);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[useConciliacaoV2] Erro ao recarregar dados:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Categorizar transação não categorizada
|
// Categorizar transação não categorizada
|
||||||
const categorizarTransacao = async (transacaoId, dadosCategorizacao) => {
|
const categorizarTransacao = async (transacaoId, dadosCategorizacao) => {
|
||||||
|
|
@ -746,155 +886,6 @@ export const useConciliacaoV2 = (defaultView = 'conciliadas') => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Busca dados para o nível de navegação atual USANDO A NOVA ROTA HIERÁRQUICA
|
|
||||||
const fetchNivelNavegacao = async (nivel, filters = {}) => {
|
|
||||||
setIsNavLoading(true);
|
|
||||||
console.log('[useConciliacaoV2] Buscando dados para nível:', nivel, filters);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let data = [];
|
|
||||||
|
|
||||||
if (nivel === 0) {
|
|
||||||
// Nível 0: Caixas (Agora usando a rota detalhada para cada caixa para matar o 'cruzamentos')
|
|
||||||
const sourceCaixas = filters.caixas || caixas;
|
|
||||||
|
|
||||||
// Buscar totais para cada caixa usando a rota detalhada em paralelo
|
|
||||||
console.log('[useConciliacaoV2] Buscando totais detalhados para todas as caixas (Substituindo cruzamentos)...');
|
|
||||||
const enrichedCaixas = await Promise.all(sourceCaixas.map(async (c) => {
|
|
||||||
try {
|
|
||||||
const detailed = await workspaceConciliacaoService.fetchExtratoDetalhado(c.id, { mes: filtroMes, ano: filtroAno });
|
|
||||||
|
|
||||||
// Fallback: Calcular totais manualmente se o resumo não vier
|
|
||||||
const categorias = detailed.categorias || [];
|
|
||||||
const calcValor = categorias.reduce((acc, cat) => acc + (Number(cat.valor_total) || 0), 0);
|
|
||||||
const calcTransacoes = categorias.reduce((acc, cat) => {
|
|
||||||
const catTotal = cat.total_transacoes || (cat.beneficiarios || []).reduce((bAcc, ben) => bAcc + (ben.total_transacoes || 0), 0);
|
|
||||||
return acc + catTotal;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
totalTransacoes: detailed.resumo?.total_transacoes || calcTransacoes,
|
|
||||||
totalValor: detailed.resumo?.valor_total || calcValor,
|
|
||||||
tipo: 'caixa'
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Erro ao buscar detalhe da caixa ${c.id}:`, e);
|
|
||||||
return { ...c, totalTransacoes: 0, totalValor: 0, tipo: 'caixa' };
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
data = enrichedCaixas;
|
|
||||||
|
|
||||||
} else if (nivel === 1) {
|
|
||||||
// Nível 1: Categorias (Vindo da API Detalhada da Caixinha)
|
|
||||||
const caixinhaId = filters.caixinha?.id || filters.caixa?.id || caixaSelecionado?.id;
|
|
||||||
if (!caixinhaId) throw new Error('Caixinha não selecionada para nível 1');
|
|
||||||
|
|
||||||
console.log('[useConciliacaoV2] >>> ACESSANDO NOVA ROTA HIERÁRQUICA DO BACKEND <<<');
|
|
||||||
console.log(`[useConciliacaoV2] Rota: /extrato/apresentar/caixinha/detalhado?caixinha=${caixinhaId}&mes=${filtroMes}&ano=${filtroAno}`);
|
|
||||||
const detailedData = await workspaceConciliacaoService.fetchExtratoDetalhado(caixinhaId, { mes: filtroMes, ano: filtroAno });
|
|
||||||
|
|
||||||
data = (detailedData.categorias || []).map(cat => {
|
|
||||||
// Calcular sub-totais de transações se não vier
|
|
||||||
const subTransacoes = cat.total_transacoes || (cat.beneficiarios || []).reduce((acc, ben) => acc + (ben.total_transacoes || 0), 0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `cat_${cat.categoria}`,
|
|
||||||
nome: cat.categoria || 'Sem Categoria',
|
|
||||||
totalTransacoes: subTransacoes,
|
|
||||||
totalValor: cat.valor_total || 0,
|
|
||||||
tipo: 'categoria',
|
|
||||||
beneficiarios: cat.beneficiarios || [],
|
|
||||||
cor: categorias.find(c => c.nome === cat.categoria)?.cor || '#3b82f6'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if (nivel === 2) {
|
|
||||||
// Nível 2: Beneficiários (Dados já estão no item selecionado)
|
|
||||||
const categoriaItem = filters.categoria || categoriaSelecionada;
|
|
||||||
data = (categoriaItem?.beneficiarios || []).map(ben => ({
|
|
||||||
id: `ben_${ben.beneficiario}`,
|
|
||||||
nome: ben.beneficiario || 'Sem Beneficiário',
|
|
||||||
beneficiario: ben.beneficiario,
|
|
||||||
totalTransacoes: ben.total_transacoes || 0,
|
|
||||||
totalValor: ben.valor_total || 0,
|
|
||||||
tipo: 'regra',
|
|
||||||
transacoes: ben.transacoes || []
|
|
||||||
}));
|
|
||||||
|
|
||||||
} else if (nivel === 3) {
|
|
||||||
// Nível 3: Transações (Normalizar dados para garantir compatibilidade com gráficos/tabelas)
|
|
||||||
const detalheItem = filters.regra || detalheSelecionado;
|
|
||||||
const rawTransacoes = detalheItem?.transacoes || [];
|
|
||||||
|
|
||||||
data = rawTransacoes.map(t => ({
|
|
||||||
...t,
|
|
||||||
id: t.id || t.idextrato || Math.random(),
|
|
||||||
data: t.data || t.dataEntrada || t.data_entrada || '',
|
|
||||||
valor: Number(t.valor || t.valor_total || 0),
|
|
||||||
descricao: t.descricao || t.historico || '',
|
|
||||||
beneficiario: t.beneficiario || t.beneficiario_pagador || ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
setBackendNavData(data);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[useConciliacaoV2] Erro ao buscar dados de navegação:', error);
|
|
||||||
toast.error('Erro ao navegar nos dados', 'Erro');
|
|
||||||
return [];
|
|
||||||
} finally {
|
|
||||||
setIsNavLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navegação hierárquica usando dados do backend
|
|
||||||
const navegarPara = async (tipo, item) => {
|
|
||||||
console.log('[useConciliacaoV2] Navegando para:', tipo, item);
|
|
||||||
|
|
||||||
let novoNivel = 0;
|
|
||||||
if (tipo === 'caixa') {
|
|
||||||
setCaixaSelecionado(item);
|
|
||||||
setCategoriaSelecionada(null);
|
|
||||||
setDetalheSelecionado(null);
|
|
||||||
novoNivel = 1;
|
|
||||||
setCaminhoNavegacao([{ tipo: 'caixa', item }]);
|
|
||||||
} else if (tipo === 'categoria') {
|
|
||||||
setCategoriaSelecionada(item);
|
|
||||||
setDetalheSelecionado(null);
|
|
||||||
novoNivel = 2;
|
|
||||||
setCaminhoNavegacao(prev => [...prev, { tipo: 'categoria', item }]);
|
|
||||||
} else if (tipo === 'regra') {
|
|
||||||
setDetalheSelecionado(item);
|
|
||||||
novoNivel = 3;
|
|
||||||
setCaminhoNavegacao(prev => [...prev, { tipo: 'regra', item }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNivelNavegacao(novoNivel);
|
|
||||||
await fetchNivelNavegacao(novoNivel, { [tipo]: item });
|
|
||||||
};
|
|
||||||
|
|
||||||
const voltarNavegacao = async () => {
|
|
||||||
if (nivelNavegacao === 0) return;
|
|
||||||
|
|
||||||
const novoNivel = nivelNavegacao - 1;
|
|
||||||
setNivelNavegacao(novoNivel);
|
|
||||||
setCaminhoNavegacao(prev => prev.slice(0, novoNivel));
|
|
||||||
|
|
||||||
if (novoNivel === 0) {
|
|
||||||
setCaixaSelecionado(null);
|
|
||||||
setCategoriaSelecionada(null);
|
|
||||||
setDetalheSelecionado(null);
|
|
||||||
await fetchNivelNavegacao(0);
|
|
||||||
} else if (novoNivel === 1) {
|
|
||||||
setCategoriaSelecionada(null);
|
|
||||||
setDetalheSelecionado(null);
|
|
||||||
await fetchNivelNavegacao(1, { caixinha: caixaSelecionado });
|
|
||||||
} else if (novoNivel === 2) {
|
|
||||||
setDetalheSelecionado(null);
|
|
||||||
await fetchNivelNavegacao(2, { categoria: categoriaSelecionada });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportarPDF = async () => {
|
const exportarPDF = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,7 @@ export const useDashboard = () => {
|
||||||
|
|
||||||
const getBoletosPieData = () => {
|
const getBoletosPieData = () => {
|
||||||
const values = {};
|
const values = {};
|
||||||
|
const counts = {};
|
||||||
const mesAtual = formatMesAno(new Date());
|
const mesAtual = formatMesAno(new Date());
|
||||||
|
|
||||||
data.boletos.cobrancas?.forEach((item) => {
|
data.boletos.cobrancas?.forEach((item) => {
|
||||||
|
|
@ -296,6 +297,7 @@ export const useDashboard = () => {
|
||||||
const st = c.situacao || 'OUTROS';
|
const st = c.situacao || 'OUTROS';
|
||||||
const valor = safeNumber(c.valorNominal);
|
const valor = safeNumber(c.valorNominal);
|
||||||
values[st] = (values[st] || 0) + valor;
|
values[st] = (values[st] || 0) + valor;
|
||||||
|
counts[st] = (counts[st] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
|
|
@ -309,6 +311,7 @@ export const useDashboard = () => {
|
||||||
return Object.entries(values).map(([name, value]) => ({
|
return Object.entries(values).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
count: counts[name] || 0,
|
||||||
color: colors[name] || colors['OUTROS']
|
color: colors[name] || colors['OUTROS']
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export function useFluxoCaixa() {
|
||||||
const [fluxoData, setFluxoData] = useState({ mensal: [] });
|
const [fluxoData, setFluxoData] = useState({ mensal: [] });
|
||||||
const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] });
|
const [somaCategoria, setSomaCategoria] = useState({ por_categoria: [] });
|
||||||
const [saldoConsolidado, setSaldoConsolidado] = useState(null);
|
const [saldoConsolidado, setSaldoConsolidado] = useState(null);
|
||||||
|
const [porCaixinha, setPorCaixinha] = useState([]);
|
||||||
|
|
||||||
const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1));
|
const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1));
|
||||||
const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString());
|
const [filtroAno, setFiltroAno] = useState(new Date().getFullYear().toString());
|
||||||
|
|
@ -47,24 +48,188 @@ export function useFluxoCaixa() {
|
||||||
|
|
||||||
const mesAtu = filtroMes;
|
const mesAtu = filtroMes;
|
||||||
const anoAtu = filtroAno;
|
const anoAtu = filtroAno;
|
||||||
|
const tipo = filtroTipo;
|
||||||
|
|
||||||
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData] = await Promise.all([
|
// Monta os parâmetros de forma dinâmica conforme o tipo de filtro
|
||||||
extratoService.fetchExtrato(),
|
const params = {};
|
||||||
|
if (tipo === 'mes') {
|
||||||
|
params.mes = mesAtu;
|
||||||
|
params.ano = anoAtu;
|
||||||
|
} else if (tipo === 'ano') {
|
||||||
|
params.ano = anoAtu;
|
||||||
|
}
|
||||||
|
// Se for 'todos', params fica vazio
|
||||||
|
|
||||||
|
const [extratoData, saldoData, armazenadoData, fluxoResponse, somaCategoriaData, consolidadoData, caixinhasList, categoriasList] = await Promise.all([
|
||||||
|
extratoService.fetchExtrato(params),
|
||||||
extratoService.fetchSaldo(),
|
extratoService.fetchSaldo(),
|
||||||
extratoService.fetchSaldoArmazenado(),
|
extratoService.fetchSaldoArmazenado(),
|
||||||
extratoService.fetchFluxo(),
|
extratoService.fetchFluxo(params),
|
||||||
extratoService.getSomaPorCategoria({ mes: mesAtu, ano: anoAtu }),
|
extratoService.getSomaPorCategoria(params),
|
||||||
extratoService.fetchSaldoConsolidado({ mes: mesAtu, ano: anoAtu })
|
extratoService.fetchSaldoConsolidado(params),
|
||||||
|
extratoService.fetchCaixinhas(),
|
||||||
|
extratoService.fetchCategorias()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setExtrato(Array.isArray(extratoData) ? extratoData : []);
|
// --- CLIENT-SIDE FILTERING FALLBACK ---
|
||||||
|
const matchesPeriod = (itemDate) => {
|
||||||
|
if (!itemDate || tipo === 'todos') return true;
|
||||||
|
|
||||||
|
// formats: YYYY-MM-DD or DD/MM/YYYY or ISO or "Wed, 11 Feb 2026 00:00:00 GMT"
|
||||||
|
let d;
|
||||||
|
if (typeof itemDate === 'string') {
|
||||||
|
if (itemDate.includes('/')) {
|
||||||
|
const [day, month, year] = itemDate.split('/');
|
||||||
|
d = new Date(year, month - 1, day);
|
||||||
|
} else {
|
||||||
|
d = new Date(itemDate);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d = new Date(itemDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(d.getTime())) return true;
|
||||||
|
|
||||||
|
const itemMonth = String(d.getMonth() + 1);
|
||||||
|
const itemYear = String(d.getFullYear());
|
||||||
|
|
||||||
|
if (tipo === 'mes') {
|
||||||
|
return itemMonth === mesAtu && itemYear === anoAtu;
|
||||||
|
} else if (tipo === 'ano') {
|
||||||
|
return itemYear === anoAtu;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0. Build Comprehensive Categories Map
|
||||||
|
const categoriesMap = {};
|
||||||
|
|
||||||
|
// Source A: /categorias/apresentar
|
||||||
|
if (Array.isArray(categoriasList)) {
|
||||||
|
categoriasList.forEach(c => {
|
||||||
|
const cId = String(c.idcategoria ?? c.idcategorias ?? c.id ?? '');
|
||||||
|
if (cId) {
|
||||||
|
categoriesMap[cId] = c.categoria || c.name || c.nome || c.descricao;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source B: Labels already in /extrato/soma_por_categoria
|
||||||
|
const rawPorCategoria = somaCategoriaData?.por_categoria || [];
|
||||||
|
rawPorCategoria.forEach(cat => {
|
||||||
|
const cId = String(cat.idcategoria ?? '');
|
||||||
|
if (cId && cat.categoria && isNaN(cat.categoria)) {
|
||||||
|
if (!categoriesMap[cId] || categoriesMap[cId].startsWith('Categoria ')) {
|
||||||
|
categoriesMap[cId] = cat.categoria;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Process Extrato (Table Data) - Consolidate labels
|
||||||
|
const rawExtrato = Array.isArray(extratoData) ? extratoData : [];
|
||||||
|
const filteredExtrato = rawExtrato.filter(item =>
|
||||||
|
matchesPeriod(item.dataEntrada || item.data || item.data_entrada)
|
||||||
|
);
|
||||||
|
|
||||||
|
setExtrato(filteredExtrato.map(item => {
|
||||||
|
// ID can be in 'categoria' or 'idcategoria'
|
||||||
|
let idVal = item.idcategoria || '';
|
||||||
|
if (!idVal && item.categoria && !isNaN(item.categoria)) idVal = item.categoria;
|
||||||
|
const id = String(idVal || '0');
|
||||||
|
|
||||||
|
const catName = categoriesMap[id] ||
|
||||||
|
item.categoria_nome ||
|
||||||
|
(isNaN(item.categoria) ? item.categoria : null) ||
|
||||||
|
(id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
categoria_nome: catName
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2. Aggregate Categories (Chart Data) - Unified C and D for the same category
|
||||||
|
const aggregatedSummary = {};
|
||||||
|
rawPorCategoria.forEach(cat => {
|
||||||
|
const id = String(cat.idcategoria ?? '0');
|
||||||
|
const name = categoriesMap[id] || cat.categoria || (id === '0' ? 'Sem Categoria' : `Categoria ${id}`);
|
||||||
|
|
||||||
|
if (!aggregatedSummary[id]) {
|
||||||
|
aggregatedSummary[id] = {
|
||||||
|
idcategoria: id,
|
||||||
|
categoria: name,
|
||||||
|
total_entradas: 0,
|
||||||
|
total_saidas: 0,
|
||||||
|
total: 0,
|
||||||
|
total_transacoes: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Math.abs(Number(cat.total) || 0);
|
||||||
|
if (cat.tipoOperacao === 'C') {
|
||||||
|
aggregatedSummary[id].total_entradas += value;
|
||||||
|
} else {
|
||||||
|
aggregatedSummary[id].total_saidas += value;
|
||||||
|
}
|
||||||
|
aggregatedSummary[id].total += value;
|
||||||
|
aggregatedSummary[id].total_transacoes += Number(cat.total_transacoes || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSomaCategoria({
|
||||||
|
por_categoria: Object.values(aggregatedSummary).sort((a, b) => b.total - a.total)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Process Caixinhas
|
||||||
|
const caixinhasMap = {};
|
||||||
|
if (Array.isArray(caixinhasList)) {
|
||||||
|
caixinhasList.forEach(c => {
|
||||||
|
caixinhasMap[String(c.idcaixinhas_financeiro || c.id)] = c.caixinha || c.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedCaixinha = {};
|
||||||
|
filteredExtrato.forEach(item => {
|
||||||
|
const id = String(item.caixinha || '0');
|
||||||
|
if (!groupedCaixinha[id]) {
|
||||||
|
groupedCaixinha[id] = {
|
||||||
|
id: id,
|
||||||
|
caixinha: caixinhasMap[id] || (id === '0' ? '(sem caixinha)' : `Caixinha ${id}`),
|
||||||
|
total_entradas: 0,
|
||||||
|
total_saidas: 0,
|
||||||
|
total: 0,
|
||||||
|
diferenca: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Math.abs(Number(item.valor) || 0);
|
||||||
|
if (item.tipoOperacao === 'C') {
|
||||||
|
groupedCaixinha[id].total_entradas += value;
|
||||||
|
} else {
|
||||||
|
groupedCaixinha[id].total_saidas += value;
|
||||||
|
}
|
||||||
|
groupedCaixinha[id].total = groupedCaixinha[id].total_entradas + groupedCaixinha[id].total_saidas;
|
||||||
|
groupedCaixinha[id].diferenca = groupedCaixinha[id].total_entradas - groupedCaixinha[id].total_saidas;
|
||||||
|
});
|
||||||
|
|
||||||
|
setPorCaixinha(Object.values(groupedCaixinha).sort((a, b) => b.total - a.total));
|
||||||
|
|
||||||
setSaldo(saldoData || { disponivel: 0 });
|
setSaldo(saldoData || { disponivel: 0 });
|
||||||
setSaldoArmazenado(Array.isArray(armazenadoData) ? armazenadoData : []);
|
setSaldoArmazenado(Array.isArray(armazenadoData) ? armazenadoData : []);
|
||||||
setFluxoData(fluxoResponse || { mensal: [] });
|
|
||||||
setSomaCategoria(somaCategoriaData || { por_categoria: [] });
|
const filteredFluxo = {
|
||||||
|
mensal: (fluxoResponse?.mensal || []).filter(item => {
|
||||||
|
if (tipo === 'ano') return String(item.ano) === anoAtu;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
diario: (fluxoResponse?.diario || []).filter(item => {
|
||||||
|
if (tipo === 'mes') return String(item.mes) === mesAtu && String(item.ano) === anoAtu;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
anual: fluxoResponse?.anual || []
|
||||||
|
};
|
||||||
|
setFluxoData(filteredFluxo);
|
||||||
setSaldoConsolidado(consolidadoData || null);
|
setSaldoConsolidado(consolidadoData || null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
||||||
console.error('[useFluxoCaixa] Erro ao carregar dados:', err);
|
console.error('[useFluxoCaixa] Erro ao carregar dados:', err);
|
||||||
setError(err.message || 'Erro ao carregar fluxo de caixa');
|
setError(err.message || 'Erro ao carregar fluxo de caixa');
|
||||||
setExtrato([]);
|
setExtrato([]);
|
||||||
|
|
@ -72,12 +237,12 @@ export function useFluxoCaixa() {
|
||||||
setSaldoArmazenado([]);
|
setSaldoArmazenado([]);
|
||||||
setFluxoData({ mensal: [] });
|
setFluxoData({ mensal: [] });
|
||||||
setSomaCategoria({ por_categoria: [] });
|
setSomaCategoria({ por_categoria: [] });
|
||||||
|
setPorCaixinha([]);
|
||||||
setSaldoConsolidado(null);
|
setSaldoConsolidado(null);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [filtroMes, filtroAno]);
|
}, [filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
@ -124,15 +289,25 @@ export function useFluxoCaixa() {
|
||||||
const getChartData = useCallback((type = 'mensal') => {
|
const getChartData = useCallback((type = 'mensal') => {
|
||||||
const data = fluxoData[type] || [];
|
const data = fluxoData[type] || [];
|
||||||
|
|
||||||
// Agrupa por período (mês ou ano) e tipo de operação
|
// Agrupa por período (mês, ano ou dia) e tipo de operação
|
||||||
const periodGroups = data.reduce((acc, item) => {
|
const periodGroups = data.reduce((acc, item) => {
|
||||||
const key = type === 'mensal'
|
let key = '';
|
||||||
? `${item.ano}-${String(item.mes).padStart(2, '0')}`
|
let label = '';
|
||||||
: `${item.ano}`;
|
|
||||||
|
if (type === 'diario') {
|
||||||
|
key = `${item.ano}-${String(item.mes).padStart(2, '0')}-${String(item.dia).padStart(2, '0')}`;
|
||||||
|
label = `${String(item.dia).padStart(2, '0')}/${String(item.mes).padStart(2, '0')}`;
|
||||||
|
} else if (type === 'mensal') {
|
||||||
|
key = `${item.ano}-${String(item.mes).padStart(2, '0')}`;
|
||||||
|
label = `${item.mes}/${item.ano}`;
|
||||||
|
} else {
|
||||||
|
key = `${item.ano}`;
|
||||||
|
label = `${item.ano}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!acc[key]) {
|
if (!acc[key]) {
|
||||||
acc[key] = {
|
acc[key] = {
|
||||||
name: type === 'mensal' ? `${item.mes}/${item.ano}` : `${item.ano}`,
|
name: label,
|
||||||
periodo: key,
|
periodo: key,
|
||||||
receitas: 0,
|
receitas: 0,
|
||||||
despesas: 0
|
despesas: 0
|
||||||
|
|
@ -152,6 +327,7 @@ export function useFluxoCaixa() {
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
extrato,
|
||||||
receitas,
|
receitas,
|
||||||
receitasCard,
|
receitasCard,
|
||||||
despesas,
|
despesas,
|
||||||
|
|
@ -161,6 +337,7 @@ export function useFluxoCaixa() {
|
||||||
bateComSaldo,
|
bateComSaldo,
|
||||||
getChartData,
|
getChartData,
|
||||||
somaCategoria,
|
somaCategoria,
|
||||||
|
porCaixinha,
|
||||||
saldoConsolidado,
|
saldoConsolidado,
|
||||||
filtroMes,
|
filtroMes,
|
||||||
setFiltroMes,
|
setFiltroMes,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { conciliacaoService } from '@/services/conciliacaoService';
|
import { conciliacaoService } from '@/services/conciliacaoService';
|
||||||
|
|
||||||
export const useStatementRefData = () => {
|
export function useStatementRefData() {
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [rules, setRules] = useState([]);
|
const [rules, setRules] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [catResponse, rulesResponse] = await Promise.all([
|
const [catResponse, rulesResponse] = await Promise.all([
|
||||||
|
|
@ -36,22 +36,22 @@ export const useStatementRefData = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getCategoryName = (id) => {
|
function getCategoryName(id) {
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
const cat = categories.find(c => String(c.idcategoria) === String(id));
|
const cat = categories.find(c => String(c.idcategoria) === String(id));
|
||||||
return cat?.categoria || cat?.nome || 'Não categorizado';
|
return cat?.categoria || cat?.nome || 'Não categorizado';
|
||||||
};
|
}
|
||||||
|
|
||||||
const getRuleName = (id) => {
|
function getRuleName(id) {
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
const rule = rules.find(r => String(r.id) === String(id) || String(r.idregras_financeiro) === String(id));
|
const rule = rules.find(r => String(r.id) === String(id) || String(r.idregras_financeiro) === String(id));
|
||||||
return rule?.regra || rule?.nome || null;
|
return rule?.regra || rule?.nome || null;
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
|
|
@ -61,4 +61,4 @@ export const useStatementRefData = () => {
|
||||||
getCategoryName,
|
getCategoryName,
|
||||||
getRuleName
|
getRuleName
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -477,19 +477,19 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none mt-[-20px]">
|
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none mt-[-20px]">
|
||||||
<span className="text-2xl font-bold text-slate-900 dark:text-white">{boletosPieData.reduce((a, b) => a + b.value, 0)}</span>
|
<span className="text-2xl font-bold text-slate-900 dark:text-white">{boletosPieData.reduce((a, b) => a + b.count, 0)}</span>
|
||||||
<span className="text-[9px] font-bold text-slate-500 dark:text-slate-500 uppercase">Total</span>
|
<span className="text-[9px] font-bold text-slate-500 dark:text-slate-500 uppercase">Total</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 bg-slate-50 dark:bg-slate-900/40 grid grid-cols-2 gap-3">
|
<div className="p-6 bg-slate-50 dark:bg-slate-900/40 flex flex-col gap-3">
|
||||||
{boletosPieData.slice(0, 4).map((item, index) => (
|
{boletosPieData.map((item, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-3 py-1.5 group/item">
|
||||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color }} />
|
<div className="w-3.5 h-3.5 rounded-full shrink-0 shadow-sm transition-transform group-hover/item:scale-110" style={{ backgroundColor: item.color }} />
|
||||||
<span className="text-[10px] font-bold text-slate-500 dark:text-slate-300 uppercase truncate">{item.name}</span>
|
<span className="text-[clamp(0.75rem,0.9vw,0.85rem)] font-bold text-slate-500 dark:text-slate-300 uppercase tracking-wider flex-1">{item.name}</span>
|
||||||
<span className="text-[10px] font-bold text-slate-900 dark:text-white ml-auto">{item.value}</span>
|
<span className="text-[clamp(0.85rem,1.1vw,1rem)] font-bold text-slate-900 dark:text-white">{item.count} boleto{item.count !== 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ import {
|
||||||
ResponsiveContainer
|
ResponsiveContainer
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip';
|
import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip';
|
||||||
|
import CaixinhaDetailsModal from '../components/CaixinhaDetailsModal';
|
||||||
|
import { LayoutGrid } from 'lucide-react';
|
||||||
|
|
||||||
// Standardized Filter Header matching CruzamentoView pattern
|
// Standardized Filter Header matching CruzamentoView pattern
|
||||||
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
||||||
|
|
@ -150,8 +152,8 @@ const KPICard = ({ title, value, subtext, icon: Icon, colorClass, isPositive })
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chart Section - Theme-aware Bar Chart
|
// Chart Section - Theme-aware Bar Chart
|
||||||
const ChartSection = ({ getChartData }) => {
|
const ChartSection = ({ getChartData, tipoPeriodo }) => {
|
||||||
const data = useMemo(() => getChartData('mensal'), [getChartData]);
|
const data = useMemo(() => getChartData(tipoPeriodo === 'ano' ? 'mensal' : 'diario'), [getChartData, tipoPeriodo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white/80 dark:bg-[#0f172a] border border-slate-200 dark:border-slate-800/50 shadow-xl rounded-xl overflow-hidden">
|
<Card className="bg-white/80 dark:bg-[#0f172a] border border-slate-200 dark:border-slate-800/50 shadow-xl rounded-xl overflow-hidden">
|
||||||
|
|
@ -161,7 +163,9 @@ const ChartSection = ({ getChartData }) => {
|
||||||
<BarChart3 className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
<BarChart3 className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Receitas vs Despesas (Executado)</h3>
|
<h3 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
|
||||||
|
Receitas vs Despesas ({tipoPeriodo === 'mes' ? 'Diário' : 'Mensal'})
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -216,7 +220,7 @@ const CategorySection = ({ data = [] }) => {
|
||||||
|
|
||||||
return data.map((item, index) => ({
|
return data.map((item, index) => ({
|
||||||
name: item.categoria || 'Sem Categoria',
|
name: item.categoria || 'Sem Categoria',
|
||||||
value: Math.abs(Number(item.total_entradas) || 0) + Math.abs(Number(item.total_saidas) || 0),
|
value: Math.abs(Number(item.total) || (Number(item.total_entradas || 0) + Number(item.total_saidas || 0))),
|
||||||
color: COLORS[index % COLORS.length]
|
color: COLORS[index % COLORS.length]
|
||||||
})).filter(item => item.value > 0);
|
})).filter(item => item.value > 0);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
@ -281,15 +285,16 @@ const CategorySection = ({ data = [] }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Caixinha Table Section - Using correct API data structure
|
// Caixinha Table Section - Using correct API data structure
|
||||||
const CaixinhaSection = ({ data = [] }) => {
|
const CaixinhaSection = ({ data = [], onSelectCaixinha }) => {
|
||||||
const caixinhaData = useMemo(() => {
|
const caixinhaData = useMemo(() => {
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
|
|
||||||
return data.map(item => ({
|
return data.map(item => ({
|
||||||
caixinha: item.categoria || 'Padrão',
|
id: item.id,
|
||||||
receitas: Math.abs(Number(item.total_entradas) || 0),
|
caixinha: item.caixinha || 'Padrão',
|
||||||
despesas: Math.abs(Number(item.total_saidas) || 0),
|
receitas: Math.abs(Number(item.receitas || item.total_entradas) || 0),
|
||||||
saldo: Number(item.diferenca) || 0
|
despesas: Math.abs(Number(item.despesas || item.total_saidas) || 0),
|
||||||
|
saldo: Number(item.saldo || item.diferenca) || 0
|
||||||
}));
|
}));
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|
@ -323,8 +328,17 @@ const CaixinhaSection = ({ data = [] }) => {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
caixinhaData.map((item, index) => (
|
caixinhaData.map((item, index) => (
|
||||||
<TableRow key={index} className="border-slate-200 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800/20">
|
<TableRow
|
||||||
<TableCell className="text-slate-900 dark:text-white text-sm font-medium">{item.caixinha}</TableCell>
|
key={index}
|
||||||
|
className="border-slate-200 dark:border-slate-700/50 hover:bg-slate-50 dark:hover:bg-slate-800/20 cursor-pointer group"
|
||||||
|
onClick={() => onSelectCaixinha(item)}
|
||||||
|
>
|
||||||
|
<TableCell className="text-slate-900 dark:text-white text-sm font-medium">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{item.caixinha}
|
||||||
|
<LayoutGrid className="w-3 h-3 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-cyan-600 dark:text-cyan-400 text-sm font-mono text-right">{formatCurrency(item.receitas)}</TableCell>
|
<TableCell className="text-cyan-600 dark:text-cyan-400 text-sm font-mono text-right">{formatCurrency(item.receitas)}</TableCell>
|
||||||
<TableCell className="text-rose-600 dark:text-rose-400 text-sm font-mono text-right">{formatCurrency(item.despesas)}</TableCell>
|
<TableCell className="text-rose-600 dark:text-rose-400 text-sm font-mono text-right">{formatCurrency(item.despesas)}</TableCell>
|
||||||
<TableCell className={cn(
|
<TableCell className={cn(
|
||||||
|
|
@ -342,41 +356,19 @@ const CaixinhaSection = ({ data = [] }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transactions Table Section - Using correct API data structure
|
// Transactions Table Section - Using actual API data
|
||||||
const TransactionsSection = ({ data = [], loading = false }) => {
|
const TransactionsSection = ({ data = [], loading = false }) => {
|
||||||
// Transform category data into transaction-like rows for display
|
|
||||||
const transactionData = useMemo(() => {
|
const transactionData = useMemo(() => {
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
|
|
||||||
return data.flatMap(item => {
|
return data.map(item => ({
|
||||||
const rows = [];
|
id: item.idextrato || item.id,
|
||||||
|
categoria: item.categoria_nome || item.categoria || 'Sem Categoria',
|
||||||
// Add entrada row if exists
|
tipoOperacao: item.tipoOperacao,
|
||||||
if (Number(item.total_entradas) > 0) {
|
valor: item.valor,
|
||||||
rows.push({
|
descricao: item.descricao || item.historico || '---',
|
||||||
id: `${item.idcategoria}-entrada`,
|
data: item.dataEntrada || item.data || item.data_entrada
|
||||||
categoria: item.categoria || 'Sem Categoria',
|
}));
|
||||||
tipoOperacao: 'C',
|
|
||||||
valor: Number(item.total_entradas),
|
|
||||||
descricao: `Total de entradas - ${item.categoria || 'Sem Categoria'}`,
|
|
||||||
data: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add saida row if exists
|
|
||||||
if (Number(item.total_saidas) > 0) {
|
|
||||||
rows.push({
|
|
||||||
id: `${item.idcategoria}-saida`,
|
|
||||||
categoria: item.categoria || 'Sem Categoria',
|
|
||||||
tipoOperacao: 'D',
|
|
||||||
valor: Number(item.total_saidas),
|
|
||||||
descricao: `Total de saídas - ${item.categoria || 'Sem Categoria'}`,
|
|
||||||
data: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
});
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -394,7 +386,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
/* {
|
||||||
field: 'categoria',
|
field: 'categoria',
|
||||||
header: 'CATEGORIA',
|
header: 'CATEGORIA',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
|
|
@ -402,7 +394,7 @@ const TransactionsSection = ({ data = [], loading = false }) => {
|
||||||
{row.categoria || 'Sem Categoria'}
|
{row.categoria || 'Sem Categoria'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
},
|
}, */
|
||||||
{
|
{
|
||||||
field: 'tipo',
|
field: 'tipo',
|
||||||
header: 'TIPO',
|
header: 'TIPO',
|
||||||
|
|
@ -458,6 +450,7 @@ export const FluxoCaixaView = () => {
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
extrato,
|
||||||
receitas,
|
receitas,
|
||||||
receitasCard,
|
receitasCard,
|
||||||
despesas,
|
despesas,
|
||||||
|
|
@ -467,6 +460,7 @@ export const FluxoCaixaView = () => {
|
||||||
bateComSaldo,
|
bateComSaldo,
|
||||||
getChartData,
|
getChartData,
|
||||||
somaCategoria,
|
somaCategoria,
|
||||||
|
porCaixinha,
|
||||||
saldoConsolidado,
|
saldoConsolidado,
|
||||||
filtroMes,
|
filtroMes,
|
||||||
setFiltroMes,
|
setFiltroMes,
|
||||||
|
|
@ -478,6 +472,11 @@ export const FluxoCaixaView = () => {
|
||||||
} = useFluxoCaixa();
|
} = useFluxoCaixa();
|
||||||
|
|
||||||
const [tipoPeriodo, setTipoPeriodo] = useState('mensal');
|
const [tipoPeriodo, setTipoPeriodo] = useState('mensal');
|
||||||
|
const [selectedCaixinha, setSelectedCaixinha] = useState(null);
|
||||||
|
|
||||||
|
const handleSelectCaixinha = (caixinha) => {
|
||||||
|
setSelectedCaixinha(caixinha);
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -530,7 +529,7 @@ export const FluxoCaixaView = () => {
|
||||||
<KPICard
|
<KPICard
|
||||||
title="Saldo Disponível"
|
title="Saldo Disponível"
|
||||||
value={formatCurrency(saldoConsolidado?.entradas_vs_saidas?.diferenca || 0)}
|
value={formatCurrency(saldoConsolidado?.entradas_vs_saidas?.diferenca || 0)}
|
||||||
subtext="Receitas - Despesas do período (API saldo R$ 0,00)"
|
subtext="Saldo disponivel em conta"
|
||||||
icon={Wallet}
|
icon={Wallet}
|
||||||
colorClass="text-emerald-600 dark:text-emerald-400"
|
colorClass="text-emerald-600 dark:text-emerald-400"
|
||||||
isPositive={true}
|
isPositive={true}
|
||||||
|
|
@ -541,17 +540,27 @@ export const FluxoCaixaView = () => {
|
||||||
|
|
||||||
{/* Chart Section */}
|
{/* Chart Section */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<ChartSection getChartData={getChartData} />
|
<ChartSection getChartData={getChartData} tipoPeriodo={filtroTipo} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category and Caixinha Sections - Side by Side */}
|
{/* Category and Caixinha Sections - Side by Side */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
||||||
<CategorySection data={somaCategoria?.por_categoria || []} />
|
<CategorySection data={somaCategoria?.por_categoria || []} />
|
||||||
<CaixinhaSection data={somaCategoria?.por_categoria || []} />
|
<CaixinhaSection data={porCaixinha} onSelectCaixinha={handleSelectCaixinha} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Transactions Table */}
|
{/* Transactions Table */}
|
||||||
<TransactionsSection data={somaCategoria?.por_categoria || []} loading={loading} />
|
<TransactionsSection data={extrato} loading={loading} />
|
||||||
|
|
||||||
|
{/* Caixinha Details Modal */}
|
||||||
|
<CaixinhaDetailsModal
|
||||||
|
isOpen={!!selectedCaixinha}
|
||||||
|
onClose={() => setSelectedCaixinha(null)}
|
||||||
|
caixinhaId={selectedCaixinha?.id}
|
||||||
|
caixinhaName={selectedCaixinha?.caixinha}
|
||||||
|
mes={filtroMes}
|
||||||
|
ano={filtroAno}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ import { GerenciamentoView } from './GerenciamentoView';
|
||||||
import { ExtratoCompletoView } from './ExtratoCompletoView';
|
import { ExtratoCompletoView } from './ExtratoCompletoView';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const ActivityLog = ({ isOpen, onClose }) => {
|
/**
|
||||||
|
* Componente para exibir log de atividades simplificado
|
||||||
|
*/
|
||||||
|
function ActivityLog({ isOpen, onClose }) {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -61,69 +64,37 @@ const ActivityLog = ({ isOpen, onClose }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ConciliacaoView = ({ initialView }) => {
|
/**
|
||||||
|
* Componente Principal de Conciliação V2
|
||||||
|
*/
|
||||||
|
export function ConciliacaoView({ initialView }) {
|
||||||
console.log('[ConciliacaoView] ========== COMPONENTE RENDERIZADO ==========');
|
console.log('[ConciliacaoView] ========== COMPONENTE RENDERIZADO ==========');
|
||||||
console.log('[ConciliacaoView] Timestamp:', new Date().toISOString());
|
|
||||||
console.log('[ConciliacaoView] Cache Buster: v1.0.1 - Force Refresh');
|
|
||||||
console.log('[ConciliacaoView] InitialView prop:', initialView);
|
|
||||||
|
|
||||||
let state, actions;
|
const { state, actions } = useConciliacaoV2(initialView);
|
||||||
try {
|
|
||||||
console.log('[ConciliacaoView] Chamando useConciliacaoV2(initialView)...');
|
|
||||||
const hookResult = useConciliacaoV2(initialView);
|
|
||||||
console.log('[ConciliacaoView] Hook retornou:', hookResult);
|
|
||||||
|
|
||||||
state = hookResult?.state;
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeSubView = state?.activeSubView || 'conciliadas';
|
// Configuração das sub-views (movido para dentro para ajudar com TDZ em bundles complexos)
|
||||||
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 = [
|
const subViews = [
|
||||||
{ id: 'conciliadas', label: 'Transações Conciliadas', icon: FileText, description: 'Navegação hierárquica por caixas' },
|
{ 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: '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' },
|
{ id: 'gerenciamento', label: 'Gerenciamento', icon: Settings, description: 'Caixas, Categorias, Regras' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const activeSubView = state?.activeSubView || 'conciliadas';
|
||||||
|
const isLoading = state?.isLoading !== undefined ? state.isLoading : true;
|
||||||
|
|
||||||
|
const [isBuzzOpen, setIsBuzzOpen] = React.useState(false);
|
||||||
|
|
||||||
const renderSubView = () => {
|
const renderSubView = () => {
|
||||||
switch (activeSubView) {
|
switch (activeSubView) {
|
||||||
case 'conciliadas':
|
case 'conciliadas':
|
||||||
return <TransacoesConciliadasView state={state} actions={actions} />;
|
return <TransacoesConciliadasView state={state} actions={actions} />;
|
||||||
case 'extrato-completo':
|
|
||||||
return <ExtratoCompletoView state={state} actions={actions} />;
|
|
||||||
case 'nao-categorizadas':
|
case 'nao-categorizadas':
|
||||||
return <TransacoesNaoCategorizadasView state={state} actions={actions} />;
|
return <TransacoesNaoCategorizadasView state={state} actions={actions} />;
|
||||||
|
case 'extrato-completo':
|
||||||
|
return <ExtratoCompletoView state={state} actions={actions} />;
|
||||||
case 'gerenciamento':
|
case 'gerenciamento':
|
||||||
return <GerenciamentoView state={state} actions={actions} />;
|
return <GerenciamentoView state={state} actions={actions} />;
|
||||||
default:
|
default:
|
||||||
|
|
@ -135,24 +106,11 @@ export const ConciliacaoView = ({ initialView }) => {
|
||||||
<div className="animate-in fade-in duration-700 relative">
|
<div className="animate-in fade-in duration-700 relative">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="border-b border-slate-200 dark:border-slate-800 pb-4 sm:pb-6 mb-6 sm:mb-8">
|
<div className="border-b border-slate-200 dark:border-slate-800 pb-4 sm:pb-6 mb-6 sm:mb-8">
|
||||||
{/* Linha 1: Título, Badge e Botão Activity Log */}
|
{/* Linha 1: Título e Badge */}
|
||||||
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
|
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
|
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
|
||||||
Conciliação
|
Conciliação
|
||||||
{/* <Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* <Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
"rounded-full w-9 h-9 p-0 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shrink-0",
|
|
||||||
isBuzzOpen ? "text-blue-400 ring-2 ring-blue-500/20" : "text-slate-500"
|
|
||||||
)}
|
|
||||||
onClick={() => setIsBuzzOpen(!isBuzzOpen)}
|
|
||||||
>
|
|
||||||
<History className="w-4 h-4" />
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Linha 2: Menu de Navegação */}
|
{/* Linha 2: Menu de Navegação */}
|
||||||
|
|
@ -201,4 +159,4 @@ export const ConciliacaoView = ({ initialView }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,19 @@ import { formatDate, formatCurrency } from '../../utils/dateUtils';
|
||||||
import { StatementRow } from '../../components/StatementRow';
|
import { StatementRow } from '../../components/StatementRow';
|
||||||
import { useStatementRefData } from '../../hooks/useStatementRefData';
|
import { useStatementRefData } from '../../hooks/useStatementRefData';
|
||||||
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
|
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
|
||||||
|
import { TransactionDetailModal } from '../../components/TransactionDetailModal';
|
||||||
|
|
||||||
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
||||||
const ANOS = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 2 + i);
|
const ANOS = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - 2 + i);
|
||||||
|
|
||||||
export const ExtratoCompletoView = ({ state, actions }) => {
|
export function ExtratoCompletoView({ state, actions }) {
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
const [filterType, setFilterType] = React.useState('todos'); // 'todos' | 'C' | 'D'
|
const [filterType, setFilterType] = React.useState('todos'); // 'todos' | 'C' | 'D'
|
||||||
const [filterMonth, setFilterMonth] = React.useState(String(new Date().getMonth() + 1));
|
const [filterMonth, setFilterMonth] = React.useState(String(new Date().getMonth() + 1));
|
||||||
const [filterYear, setFilterYear] = React.useState(String(new Date().getFullYear()));
|
const [filterYear, setFilterYear] = React.useState(String(new Date().getFullYear()));
|
||||||
const [transacaoSelecionada, setTransacaoSelecionada] = React.useState(null);
|
const [transacaoSelecionada, setTransacaoSelecionada] = React.useState(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||||
|
const [isDetailOpen, setIsDetailOpen] = React.useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
extratoCompleto = [],
|
extratoCompleto = [],
|
||||||
|
|
@ -202,8 +204,10 @@ export const ExtratoCompletoView = ({ state, actions }) => {
|
||||||
categoryName={getCategoryName(row.categoria)}
|
categoryName={getCategoryName(row.categoria)}
|
||||||
ruleName={getRuleName(row.regra)}
|
ruleName={getRuleName(row.regra)}
|
||||||
onClick={(t) => {
|
onClick={(t) => {
|
||||||
|
console.log('[ExtratoCompletoView] Clique na transação:', t);
|
||||||
|
console.log('[ExtratoCompletoView] Abrindo modal de detalhes...');
|
||||||
setTransacaoSelecionada(t);
|
setTransacaoSelecionada(t);
|
||||||
setIsDialogOpen(true);
|
setIsDetailOpen(true);
|
||||||
}}
|
}}
|
||||||
showCategory={true}
|
showCategory={true}
|
||||||
showStatus={false}
|
showStatus={false}
|
||||||
|
|
@ -229,6 +233,18 @@ export const ExtratoCompletoView = ({ state, actions }) => {
|
||||||
caixas={state?.caixas}
|
caixas={state?.caixas}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TransactionDetailModal
|
||||||
|
transaction={transacaoSelecionada}
|
||||||
|
open={isDetailOpen}
|
||||||
|
onOpenChange={setIsDetailOpen}
|
||||||
|
onEditTransaction={(t) => {
|
||||||
|
setTransacaoSelecionada(t);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
categorias={state?.categorias}
|
||||||
|
caixas={state?.caixas}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import ExcelTable from '../../components/ExcelTable';
|
||||||
import { formatCurrency } from '../../utils/dateUtils';
|
import { formatCurrency } from '../../utils/dateUtils';
|
||||||
import { CategoryRulesPopup } from '../../components/CategoryRulesPopup';
|
import { CategoryRulesPopup } from '../../components/CategoryRulesPopup';
|
||||||
|
|
||||||
export const GerenciamentoView = ({ state, actions }) => {
|
export function GerenciamentoView({ state, actions }) {
|
||||||
const { caixas, categorias, regras } = state;
|
const { caixas, categorias, regras } = state;
|
||||||
const [activeTab, setActiveTab] = useState('caixas');
|
const [activeTab, setActiveTab] = useState('caixas');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ import {
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
/** Sanitiza rótulo: corrige typos, espaços e remove trechos duplicados nas legendas */
|
/** Sanitiza rótulo: corrige typos, espaços e remove trechos duplicados nas legendas */
|
||||||
const sanitizeLabel = (name) => {
|
function sanitizeLabel(name) {
|
||||||
if (name == null || typeof name !== 'string') return '—';
|
if (name == null || typeof name !== 'string') return '—';
|
||||||
let s = name.replace(/\s+/g, ' ').trim();
|
let s = name.replace(/\s+/g, ' ').trim();
|
||||||
if (!s) return '—';
|
if (!s) return '—';
|
||||||
|
|
@ -65,51 +65,53 @@ const sanitizeLabel = (name) => {
|
||||||
s = s.replace(repeatedPhrase, (_, phrase, rest) => (phrase + rest).replace(/\s+/g, ' ').trim());
|
s = s.replace(repeatedPhrase, (_, phrase, rest) => (phrase + rest).replace(/\s+/g, ' ').trim());
|
||||||
}
|
}
|
||||||
return s || '—';
|
return s || '—';
|
||||||
};
|
}
|
||||||
|
|
||||||
const AccordionItem = ({ title, icon: Icon, value, count, children, isOpen, onClick, color }) => (
|
function AccordionItem({ title, icon: Icon, value, count, children, isOpen, onClick, color }) {
|
||||||
<div className="border border-slate-200 dark:border-slate-800 rounded-xl mb-3 overflow-hidden bg-white dark:bg-slate-900/50">
|
return (
|
||||||
<div
|
<div className="border border-slate-200 dark:border-slate-800 rounded-xl mb-3 overflow-hidden bg-white dark:bg-slate-900/50">
|
||||||
className={cn(
|
<div
|
||||||
"p-4 flex items-center justify-between cursor-pointer transition-colors select-none",
|
className={cn(
|
||||||
isOpen ? "bg-slate-50 dark:bg-slate-800/50" : "hover:bg-slate-50 dark:hover:bg-slate-900"
|
"p-4 flex items-center justify-between cursor-pointer transition-colors select-none",
|
||||||
|
isOpen ? "bg-slate-50 dark:bg-slate-800/50" : "hover:bg-slate-50 dark:hover:bg-slate-900"
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-lg flex items-center justify-center border"
|
||||||
|
style={color ? { backgroundColor: `${color}20`, borderColor: `${color}40` } : {}}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5" style={color ? { color } : {}} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{title}</h4>
|
||||||
|
<p className="text-[clamp(0.7rem,1vw,0.8rem)] text-slate-500 uppercase font-bold tracking-wider">{count} Transações</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="font-bold text-slate-900 dark:text-white">{formatCurrency(value)}</span>
|
||||||
|
<ChevronDown className={cn("w-5 h-5 text-slate-400 transition-transform duration-300", isOpen && "rotate-180")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="p-4 border-t border-slate-200 dark:border-slate-800 animate-in slide-in-from-top-2 duration-300">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center border"
|
|
||||||
style={color ? { backgroundColor: `${color}20`, borderColor: `${color}40` } : {}}
|
|
||||||
>
|
|
||||||
<Icon className="w-5 h-5" style={color ? { color } : {}} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{title}</h4>
|
|
||||||
<p className="text-[clamp(0.7rem,1vw,0.8rem)] text-slate-500 uppercase font-bold tracking-wider">{count} Transações</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="font-bold text-slate-900 dark:text-white">{formatCurrency(value)}</span>
|
|
||||||
<ChevronDown className={cn("w-5 h-5 text-slate-400 transition-transform duration-300", isOpen && "rotate-180")} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{isOpen && (
|
);
|
||||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800 animate-in slide-in-from-top-2 duration-300">
|
}
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Trunca legenda longa para exibição no eixo (mantém nome completo no tooltip) */
|
/** Trunca legenda longa para exibição no eixo (mantém nome completo no tooltip) */
|
||||||
const truncateLegend = (name, maxLen = 42) => {
|
function truncateLegend(name, maxLen = 42) {
|
||||||
const clean = sanitizeLabel(name);
|
const clean = sanitizeLabel(name);
|
||||||
if (clean.length <= maxLen) return clean;
|
if (clean.length <= maxLen) return clean;
|
||||||
return clean.substring(0, maxLen - 3).trim() + '...';
|
return clean.substring(0, maxLen - 3).trim() + '...';
|
||||||
};
|
}
|
||||||
|
|
||||||
/** Tooltip customizado para gráficos de conciliação: layout limpo, label + valor */
|
/** Tooltip customizado para gráficos de conciliação: layout limpo, label + valor */
|
||||||
const ConciliacaoChartTooltip = ({ active, payload, label, formatCurrency }) => {
|
function ConciliacaoChartTooltip({ active, payload, label, formatCurrency }) {
|
||||||
if (!active || !payload?.length) return null;
|
if (!active || !payload?.length) return null;
|
||||||
const item = payload[0];
|
const item = payload[0];
|
||||||
const name = item.name ?? label ?? item.payload?.name ?? '—';
|
const name = item.name ?? label ?? item.payload?.name ?? '—';
|
||||||
|
|
@ -121,9 +123,9 @@ const ConciliacaoChartTooltip = ({ active, payload, label, formatCurrency }) =>
|
||||||
<p className="text-base font-bold text-slate-900 dark:text-emerald-400 tabular-nums">{formatCurrency(value)}</p>
|
<p className="text-base font-bold text-slate-900 dark:text-emerald-400 tabular-nums">{formatCurrency(value)}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const TransacoesConciliadasView = ({ state, actions }) => {
|
export function TransacoesConciliadasView({ state, actions }) {
|
||||||
const {
|
const {
|
||||||
caixas,
|
caixas,
|
||||||
categorias,
|
categorias,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { StatementRow } from '../../components/StatementRow';
|
||||||
import { useStatementRefData } from '../../hooks/useStatementRefData';
|
import { useStatementRefData } from '../../hooks/useStatementRefData';
|
||||||
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
|
import { CategorizacaoDialog } from '../../components/CategorizacaoDialog';
|
||||||
|
|
||||||
export const TransacoesNaoCategorizadasView = ({ state, actions }) => {
|
export function TransacoesNaoCategorizadasView({ state, actions }) {
|
||||||
// IMPORTANTE: Todos os hooks devem ser chamados incondicionalmente no topo
|
// IMPORTANTE: Todos os hooks devem ser chamados incondicionalmente no topo
|
||||||
// NUNCA colocar hooks após early returns ou condições
|
// NUNCA colocar hooks após early returns ou condições
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,14 @@ import {
|
||||||
import { FinanceiroChartTooltip } from '../../components/FinanceiroChartTooltip';
|
import { FinanceiroChartTooltip } from '../../components/FinanceiroChartTooltip';
|
||||||
import { extratoService } from '@/services/extratoService';
|
import { extratoService } from '@/services/extratoService';
|
||||||
import ExcelTable from '../../components/ExcelTable';
|
import ExcelTable from '../../components/ExcelTable';
|
||||||
|
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
|
||||||
|
|
||||||
import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils';
|
import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Premium KPI Card com visualização clara
|
* Premium KPI Card com visualização clara
|
||||||
*/
|
*/
|
||||||
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, percentual, loading }) => {
|
function KPICard({ title, value, subtext, icon, colorClass, highlight, trend, percentual, loading }) {
|
||||||
const Icon = icon;
|
const Icon = icon;
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-slate-900/50 border-2 shadow-lg rounded-xl overflow-hidden group hover:scale-[1.02] transition-all">
|
<Card className="bg-white dark:bg-slate-900/50 border-2 shadow-lg rounded-xl overflow-hidden group hover:scale-[1.02] transition-all">
|
||||||
|
|
@ -85,12 +87,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, pe
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
const MESES = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
||||||
const ANOS = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
|
const ANOS = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
|
||||||
|
|
||||||
export const CruzamentoDespesasView = ({ state }) => {
|
export function CruzamentoDespesasView({ state }) {
|
||||||
const { despesasPlanejadas = [], despesasExecutadas = [], caixinhas = [] } = state;
|
const { despesasPlanejadas = [], despesasExecutadas = [], caixinhas = [] } = state;
|
||||||
|
|
||||||
const [filtroTipo, setFiltroTipo] = React.useState('mes'); // 'mes' | 'ano'
|
const [filtroTipo, setFiltroTipo] = React.useState('mes'); // 'mes' | 'ano'
|
||||||
|
|
@ -101,6 +103,8 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
const [somaCategorias, setSomaCategorias] = React.useState([]);
|
const [somaCategorias, setSomaCategorias] = React.useState([]);
|
||||||
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
|
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
|
||||||
|
|
||||||
|
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
||||||
|
|
||||||
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
||||||
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
||||||
|
|
||||||
|
|
@ -115,6 +119,9 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
});
|
});
|
||||||
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
|
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
|
||||||
|
|
||||||
|
const [selectedCategory, setSelectedCategory] = React.useState(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||||
|
|
||||||
// Efeito para buscar totais planejados do mês vigente
|
// Efeito para buscar totais planejados do mês vigente
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchTotais = async () => {
|
const fetchTotais = async () => {
|
||||||
|
|
@ -141,6 +148,7 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
}, [filtroMes, filtroAno, filtroTipo]);
|
}, [filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
// Efeito para buscar dados do gráfico do backend
|
// Efeito para buscar dados do gráfico do backend
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchChartData = async () => {
|
const fetchChartData = async () => {
|
||||||
setIsLoadingChart(true);
|
setIsLoadingChart(true);
|
||||||
|
|
@ -179,6 +187,10 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
// Normaliza o resultado conforme o backend (esperado array de objetos)
|
// Normaliza o resultado conforme o backend (esperado array de objetos)
|
||||||
// O backend para despesas v2 retorna { ano, mes, por_categoria }
|
// O backend para despesas v2 retorna { ano, mes, por_categoria }
|
||||||
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
|
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
|
||||||
|
console.log('[CruzamentoDespesasView] somaCategorias carregadas:', data.length, 'itens');
|
||||||
|
if (data.length > 0) {
|
||||||
|
console.log('[CruzamentoDespesasView] Exemplo soma:', data[0]);
|
||||||
|
}
|
||||||
setSomaCategorias(Array.isArray(data) ? data : []);
|
setSomaCategorias(Array.isArray(data) ? data : []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CruzamentoDespesasView] Erro ao buscar soma por categoria:', err);
|
console.error('[CruzamentoDespesasView] Erro ao buscar soma por categoria:', err);
|
||||||
|
|
@ -195,14 +207,21 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
}
|
}
|
||||||
}, [filtroMes, filtroAno, filtroTipo]);
|
}, [filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
|
// Efeito para buscar transações detalhadas do período removido pois soma_por_categoria já traz os itens.
|
||||||
|
|
||||||
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const matchPeriod = (dataStr) => {
|
const matchPeriod = (dataStr) => {
|
||||||
const { year, month } = parseDateInfo(dataStr);
|
const { year, month } = parseDateInfo(dataStr);
|
||||||
if (filtroTipo === 'ano') return year === Number(filtroAno);
|
const y = Number(year);
|
||||||
return year === Number(filtroAno) && month === Number(filtroMes);
|
const m = Number(month);
|
||||||
|
const fAno = Number(filtroAno);
|
||||||
|
const fMes = Number(filtroMes);
|
||||||
|
|
||||||
|
if (filtroTipo === 'ano') return y === fAno;
|
||||||
|
return y === fAno && m === fMes;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredPlanejadas = useMemo(() => {
|
const filteredPlanejadas = useMemo(() => {
|
||||||
|
|
@ -214,39 +233,71 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
}, [despesasPlanejadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
|
}, [despesasPlanejadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
|
||||||
|
|
||||||
const filteredExecutadas = useMemo(() => {
|
const filteredExecutadas = useMemo(() => {
|
||||||
return despesasExecutadas.filter(item => {
|
const allTrans = somaCategorias.flatMap(c => c.transacoes || []);
|
||||||
|
const sourceData = (filtroTipo === 'mes' && allTrans.length > 0) ? allTrans : (Array.isArray(despesasExecutadas) ? despesasExecutadas : []);
|
||||||
|
|
||||||
|
console.log('[CruzamentoDespesasView] Pipeline de dados para filteredExecutadas:', {
|
||||||
|
filtroTipo,
|
||||||
|
sourceCount: sourceData.length,
|
||||||
|
propCount: despesasExecutadas?.length,
|
||||||
|
somaTransCount: allTrans.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return sourceData.filter(item => {
|
||||||
if (!matchPeriod(item.data || item.dataEntrada)) return false;
|
if (!matchPeriod(item.data || item.dataEntrada)) return false;
|
||||||
|
|
||||||
|
// Garantir que estamos vendo apenas Despesas (D)
|
||||||
|
const op = String(item.tipoOperacao || '').toUpperCase();
|
||||||
|
if (op && op !== 'D') return false;
|
||||||
|
|
||||||
if (filtroCaixinha !== 'todos' && String(item.caixinha || '') !== String(filtroCaixinha)) return false;
|
if (filtroCaixinha !== 'todos' && String(item.caixinha || '') !== String(filtroCaixinha)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [despesasExecutadas, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
|
}, [despesasExecutadas, somaCategorias, filtroTipo, filtroMes, filtroAno, filtroCaixinha]);
|
||||||
|
|
||||||
// Composição e Variância baseadas exclusivamente na rota de soma por categoria
|
// Composição e Variância baseadas exclusivamente na rota de soma por categoria
|
||||||
const comparativoCategoria = useMemo(() => {
|
const comparativoCategoria = useMemo(() => {
|
||||||
if (!Array.isArray(somaCategorias)) return [];
|
if (!Array.isArray(somaCategorias)) return [];
|
||||||
|
|
||||||
return somaCategorias.map(item => {
|
return somaCategorias.map(item => {
|
||||||
const entradas = parseCurrency(item.total_entradas || 0);
|
const saidas = parseCurrency(item.total || item.total_saidas || 0);
|
||||||
const saidas = parseCurrency(item.total_saidas || 0);
|
|
||||||
const diferenca = parseCurrency(item.diferenca || 0);
|
|
||||||
// Percentual de "gasto" em relação ao total movimentado ou similar
|
|
||||||
const totalMovimentado = entradas + Math.abs(saidas);
|
|
||||||
const percentual = totalMovimentado > 0 ? ((Math.abs(saidas) / totalMovimentado) * 100).toFixed(1) : 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: item.idcategoria ?? item.idCategoria ?? item.id,
|
||||||
categoria: item.categoria || 'Sem Categoria',
|
categoria: item.categoria || 'Sem Categoria',
|
||||||
categoriaNome: item.categoria || '(Sem Categoria)',
|
categoriaNome: item.categoria || '(Sem Categoria)',
|
||||||
name: item.categoria || '(Sem Categoria)',
|
name: item.categoria || '(Sem Categoria)',
|
||||||
entradas,
|
entradas: 0,
|
||||||
saidas,
|
saidas,
|
||||||
diferenca,
|
diferenca: 0,
|
||||||
percentual,
|
percentual: 0,
|
||||||
executado: Math.abs(saidas), // Para o gráfico de pizza (valor absoluto da saída)
|
executado: Math.abs(saidas),
|
||||||
value: Math.abs(saidas)
|
value: Math.abs(saidas)
|
||||||
};
|
};
|
||||||
}).sort((a, b) => b.saidas - a.saidas); // Ordena pelas maiores saídas
|
}).sort((a, b) => b.saidas - a.saidas);
|
||||||
}, [somaCategorias]);
|
}, [somaCategorias]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleCategoryClick = (name, id) => {
|
||||||
|
console.log('[CruzamentoDespesasView] Clicou na categoria:', { name, id });
|
||||||
|
setSelectedCategory({ name, id });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactionsForSelectedCategory = React.useMemo(() => {
|
||||||
|
if (!selectedCategory) return [];
|
||||||
|
|
||||||
|
// Busca direta no agrupamento do backend
|
||||||
|
const catObj = somaCategorias.find(c => {
|
||||||
|
const id = c.idcategoria ?? c.idCategoria ?? c.id;
|
||||||
|
const name = c.categoria || '';
|
||||||
|
return String(id) === String(selectedCategory.id) || name === selectedCategory.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactions = catObj?.transacoes || [];
|
||||||
|
console.log('[CruzamentoDespesasView] Modal: itens para', selectedCategory.name, ':', transactions.length);
|
||||||
|
return transactions;
|
||||||
|
}, [selectedCategory, somaCategorias]);
|
||||||
|
|
||||||
const timelineData = useMemo(() => {
|
const timelineData = useMemo(() => {
|
||||||
if (chartDataBackend.length > 0) {
|
if (chartDataBackend.length > 0) {
|
||||||
return chartDataBackend.map(item => {
|
return chartDataBackend.map(item => {
|
||||||
|
|
@ -467,7 +518,7 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
<KPICard
|
<KPICard
|
||||||
title="Total Planejado"
|
title="Total Planejado"
|
||||||
value={formatCurrency(totalPlanejado)}
|
value={formatCurrency(totalPlanejado)}
|
||||||
subtext="Total esperado (despesas V2)"
|
subtext="Total esperado"
|
||||||
icon={Target}
|
icon={Target}
|
||||||
colorClass="text-slate-500"
|
colorClass="text-slate-500"
|
||||||
loading={isLoadingTotais}
|
loading={isLoadingTotais}
|
||||||
|
|
@ -475,7 +526,7 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
<KPICard
|
<KPICard
|
||||||
title="Total Executado"
|
title="Total Executado"
|
||||||
value={formatCurrency(totalExecutado)}
|
value={formatCurrency(totalExecutado)}
|
||||||
subtext="Total realizado (extrato)"
|
subtext="Total realizado"
|
||||||
icon={CheckCircle2}
|
icon={CheckCircle2}
|
||||||
colorClass="text-rose-500"
|
colorClass="text-rose-500"
|
||||||
highlight
|
highlight
|
||||||
|
|
@ -670,17 +721,12 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
header: 'Categoria',
|
header: 'Categoria',
|
||||||
width: '200px',
|
width: '200px',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="font-semibold text-slate-900 dark:text-white pl-3">{row.categoriaNome}</span>
|
<button
|
||||||
)
|
onClick={() => handleCategoryClick(row.categoria, row.id)}
|
||||||
},
|
className="font-semibold text-slate-900 dark:text-white pl-3 hover:text-rose-500 transition-colors text-left w-full"
|
||||||
{
|
>
|
||||||
field: 'entradas',
|
{row.categoriaNome}
|
||||||
header: 'Entradas',
|
</button>
|
||||||
width: '150px',
|
|
||||||
render: (row) => (
|
|
||||||
<span className="font-mono text-xs text-emerald-400 text-right block w-full">
|
|
||||||
{formatCurrency(row.entradas)}
|
|
||||||
</span>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -731,6 +777,14 @@ export const CruzamentoDespesasView = ({ state }) => {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<TransactionsByCategoryModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
categoryName={selectedCategory?.name}
|
||||||
|
transactions={transactionsForSelectedCategory}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,8 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
const [clientBoletosFromApi, setClientBoletosFromApi] = useState([]);
|
const [clientBoletosFromApi, setClientBoletosFromApi] = useState([]);
|
||||||
const [loadingBoletosCliente, setLoadingBoletosCliente] = useState(false);
|
const [loadingBoletosCliente, setLoadingBoletosCliente] = useState(false);
|
||||||
|
|
||||||
// Estado para o Extrato (Transações)
|
// Estado para o Extrato (Transações): { categoria: string, linha_tempo: Array }
|
||||||
const [clientExtrato, setClientExtrato] = useState([]);
|
const [clientExtrato, setClientExtrato] = useState({ categoria: '', linha_tempo: [] });
|
||||||
const [loadingExtrato, setLoadingExtrato] = useState(false);
|
const [loadingExtrato, setLoadingExtrato] = useState(false);
|
||||||
|
|
||||||
const [clientInterest, setClientInterest] = useState([]);
|
const [clientInterest, setClientInterest] = useState([]);
|
||||||
|
|
@ -243,7 +243,7 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
// Carrega Extrato (Transações) com LOGS DETALHADOS
|
// Carrega Extrato (Transações) com LOGS DETALHADOS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clientTab !== 'extrato' || !selectedClient) {
|
if (clientTab !== 'extrato' || !selectedClient) {
|
||||||
setClientExtrato([]);
|
setClientExtrato({ categoria: '', linha_tempo: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,12 +252,16 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
const clientName = selectedClient.nome || '';
|
const clientName = selectedClient.nome || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Agora usa a rota otimizada /beneficiario_aplicado
|
// Rota /beneficiario_aplicado retorna { categoria, linha_tempo }
|
||||||
const data = await extratoService.fetchBeneficiarioAplicado(clientName);
|
const data = await extratoService.fetchBeneficiarioAplicado(clientName);
|
||||||
setClientExtrato(data);
|
setClientExtrato(
|
||||||
|
data && typeof data === 'object' && Array.isArray(data.linha_tempo)
|
||||||
|
? data
|
||||||
|
: { categoria: '', linha_tempo: [] }
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erro ao buscar extrato:', error);
|
console.error('❌ Erro ao buscar extrato:', error);
|
||||||
setClientExtrato([]);
|
setClientExtrato({ categoria: '', linha_tempo: [] });
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingExtrato(false);
|
setLoadingExtrato(false);
|
||||||
}
|
}
|
||||||
|
|
@ -1169,51 +1173,56 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
<TabsContent value="extrato" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
|
<TabsContent value="extrato" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Extrato Financeiro</h4>
|
<div>
|
||||||
|
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Linha do Tempo</h4>
|
||||||
|
{clientExtrato.categoria && (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 truncate max-w-xs">{clientExtrato.categoria}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{clientExtrato.length} registros
|
{clientExtrato.linha_tempo?.length ?? 0} registros
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loadingExtrato ? (
|
{loadingExtrato ? (
|
||||||
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
||||||
<Loader2 className="w-6 h-6 animate-spin" />
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
<span className="text-sm">Buscando transações via /beneficiario_aplicado...</span>
|
<span className="text-sm">Buscando transações...</span>
|
||||||
</div>
|
</div>
|
||||||
) : clientExtrato.length === 0 ? (
|
) : !clientExtrato.linha_tempo?.length ? (
|
||||||
<div className="text-center py-12 text-slate-500">
|
<div className="text-center py-12 text-slate-500">
|
||||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||||
<p className="text-sm font-medium">Nenhuma transação encontrada</p>
|
<p className="text-sm font-medium">Nenhuma transação encontrada</p>
|
||||||
<p className="text-xs mt-2 max-w-[300px] mx-auto opacity-70">
|
<p className="text-xs mt-2 max-w-[300px] mx-auto opacity-70">
|
||||||
Consultamos o beneficiário "{selectedClient.nome}" e não retornou registros.
|
Nenhum registro retornado para "{selectedClient.nome}".
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[500px]">
|
<div className="h-[500px]">
|
||||||
<ExcelTable
|
<ExcelTable
|
||||||
data={clientExtrato}
|
data={clientExtrato.linha_tempo.map((row, i) => ({ ...row, _idx: i }))}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
field: 'data',
|
field: 'dataEntrada',
|
||||||
header: 'Data',
|
header: 'Data',
|
||||||
width: '120px',
|
width: '110px',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="text-slate-600 dark:text-slate-400">
|
<span className="text-slate-600 dark:text-slate-400 text-xs">
|
||||||
{formatDate(row.data || row.dataEntrada)}
|
{formatDate(row.dataEntrada || row.data)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'descricao',
|
field: 'titulo',
|
||||||
header: 'Descrição',
|
header: 'Título / Descrição',
|
||||||
width: '300px',
|
width: '280px',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
<span className="font-semibold text-slate-900 dark:text-white text-xs leading-tight">
|
||||||
{row.descricao || 'Sem descrição'}
|
{row.titulo || 'N/D'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-slate-500 uppercase">
|
<span className="text-[10px] text-slate-500 truncate">
|
||||||
{row.categoria || 'Sem Categoria'}
|
{row.descricao || ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -1221,23 +1230,58 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
{
|
{
|
||||||
field: 'valor',
|
field: 'valor',
|
||||||
header: 'Valor',
|
header: 'Valor',
|
||||||
width: '150px',
|
width: '130px',
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const isCredit = row.tipoOperacao === 'C' || row.valor > 0;
|
const isCredit = row.tipoOperacao === 'C';
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"flex items-center gap-2 font-bold",
|
"flex items-center gap-1.5 font-bold text-sm",
|
||||||
isCredit ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"
|
isCredit ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"
|
||||||
)}>
|
)}>
|
||||||
{isCredit ? <ArrowUpCircle className="w-4 h-4" /> : <ArrowDownCircle className="w-4 h-4" />}
|
{isCredit ? <ArrowUpCircle className="w-3.5 h-3.5 shrink-0" /> : <ArrowDownCircle className="w-3.5 h-3.5 shrink-0" />}
|
||||||
{formatCurrency(Math.abs(row.valor))}
|
{formatCurrency(Math.abs(Number(row.valor) || 0))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'diferenca',
|
||||||
|
header: 'Diferença',
|
||||||
|
width: '130px',
|
||||||
|
render: (row) => {
|
||||||
|
if (row.diferenca === null || row.diferenca === undefined) {
|
||||||
|
return <span className="text-slate-400 text-xs">—</span>;
|
||||||
|
}
|
||||||
|
const diff = Number(row.diferenca);
|
||||||
|
const isPos = diff >= 0;
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"flex flex-col",
|
||||||
|
isPos ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400"
|
||||||
|
)}>
|
||||||
|
<span className="font-semibold text-xs">
|
||||||
|
{isPos ? '+' : ''}{formatCurrency(diff)}
|
||||||
|
</span>
|
||||||
|
{row.variacao_percentual !== null && row.variacao_percentual !== undefined && (
|
||||||
|
<span className="text-[10px] opacity-80">{row.variacao_percentual}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tipoTransacao',
|
||||||
|
header: 'Tipo',
|
||||||
|
width: '140px',
|
||||||
|
render: (row) => (
|
||||||
|
<Badge variant="outline" className="text-[10px] font-mono uppercase truncate max-w-[130px]">
|
||||||
|
{row.tipoTransacao || '—'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
rowKey={(row) => row.id || Math.random().toString()}
|
rowKey={(row) => row._idx}
|
||||||
pageSize={15}
|
pageSize={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1399,7 +1443,7 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
<TabsContent value="juros" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
|
<TabsContent value="juros" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Detalhamento de Juros</h4>
|
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Detalhamento de Boletos/Juros</h4>
|
||||||
<Badge variant="outline" className="text-xs bg-amber-500/10 text-amber-600 border-amber-500/20">
|
<Badge variant="outline" className="text-xs bg-amber-500/10 text-amber-600 border-amber-500/20">
|
||||||
{clientInterest.length} registros
|
{clientInterest.length} registros
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -1408,62 +1452,70 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
||||||
{loadingInterest ? (
|
{loadingInterest ? (
|
||||||
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
||||||
<Loader2 className="w-6 h-6 animate-spin" />
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
<span className="text-sm">Buscando juros via /financeiro/cliente/boletos...</span>
|
<span className="text-sm">Buscando juros...</span>
|
||||||
</div>
|
</div>
|
||||||
) : clientInterest.length === 0 ? (
|
) : clientInterest.length === 0 ? (
|
||||||
<div className="text-center py-12 text-slate-500">
|
<div className="text-center py-12 text-slate-500">
|
||||||
<Percent className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
<Percent className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||||
<p className="text-sm font-medium">Nenhum detalhe de juros encontrado</p>
|
<p className="text-sm font-medium">Nenhum boleto/juros encontrado</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[500px]">
|
<div className="space-y-3">
|
||||||
<ExcelTable
|
{clientInterest.map((row, i) => {
|
||||||
data={clientInterest}
|
const situacaoColor =
|
||||||
columns={[
|
row.situacao === 'PAGO' || row.situacao === 'RECEBIDO'
|
||||||
{
|
? 'bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20'
|
||||||
field: 'data',
|
: row.situacao === 'CANCELADO'
|
||||||
header: 'Data',
|
? 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border-slate-300 dark:border-slate-600'
|
||||||
width: '120px',
|
: row.situacao === 'VENCIDO' || row.situacao === 'ATRASADO'
|
||||||
render: (row) => (
|
? 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20'
|
||||||
<span className="text-slate-600 dark:text-slate-400">
|
: 'bg-amber-100 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20';
|
||||||
{formatDate(row.data || row.data_vencimento)}
|
return (
|
||||||
</span>
|
<div key={i} className="bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||||||
)
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
},
|
<div className="flex-1 min-w-0">
|
||||||
{
|
<p className="font-bold text-sm text-slate-900 dark:text-white truncate">{row.cliente || row.nome || 'N/D'}</p>
|
||||||
field: 'descricao',
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Nº {row.seuNumero || row.seu_numero || '—'}</p>
|
||||||
header: 'Descrição',
|
</div>
|
||||||
width: '300px',
|
<Badge className={cn('text-[10px] font-bold uppercase shrink-0 border', situacaoColor)}>
|
||||||
render: (row) => (
|
{row.situacao || 'N/D'}
|
||||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
</Badge>
|
||||||
{row.descricao || `Juros #${row.numero || ''}`}
|
</div>
|
||||||
</span>
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
)
|
<div>
|
||||||
},
|
<p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Vencimento</p>
|
||||||
{
|
<p className="text-sm font-semibold text-slate-900 dark:text-white mt-0.5">{formatDate(row.vencimento || row.data_vencimento)}</p>
|
||||||
field: 'valor_juros',
|
</div>
|
||||||
header: 'Valor Juros',
|
<div>
|
||||||
width: '150px',
|
<p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Valor Original</p>
|
||||||
render: (row) => (
|
<p className="text-sm font-semibold text-slate-900 dark:text-white mt-0.5">{formatCurrency(Number(row.valor_original) || 0)}</p>
|
||||||
<span className="font-bold text-amber-600">
|
</div>
|
||||||
+{formatCurrency(row.valor_juros || row.juros || 0)}
|
<div>
|
||||||
</span>
|
<p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Juros + Multa</p>
|
||||||
)
|
<p className={cn(
|
||||||
},
|
"text-sm font-semibold mt-0.5",
|
||||||
{
|
(Number(row.juros) + Number(row.multa)) > 0
|
||||||
field: 'valor',
|
? 'text-amber-600 dark:text-amber-400'
|
||||||
header: 'Valor Base',
|
: 'text-slate-900 dark:text-white'
|
||||||
width: '150px',
|
)}>
|
||||||
render: (row) => (
|
{formatCurrency(Number(row.juros) || 0)} + {formatCurrency(Number(row.multa) || 0)}
|
||||||
<span className="text-slate-500 text-sm">
|
</p>
|
||||||
{formatCurrency(row.valor || 0)}
|
</div>
|
||||||
</span>
|
<div>
|
||||||
)
|
<p className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400">Total Atualizado</p>
|
||||||
}
|
<p className="text-sm font-bold text-emerald-600 dark:text-emerald-400 mt-0.5">{formatCurrency(Number(row.total_atualizado) || 0)}</p>
|
||||||
]}
|
</div>
|
||||||
rowKey={(row, i) => i}
|
</div>
|
||||||
pageSize={15}
|
{Number(row.dias_atraso) > 0 && (
|
||||||
/>
|
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700">
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 font-semibold">
|
||||||
|
⚠ {row.dias_atraso} dia(s) de atraso
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import {
|
||||||
TableRow
|
TableRow
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import ExcelTable from '../../components/ExcelTable';
|
import ExcelTable from '../../components/ExcelTable';
|
||||||
|
import TransactionsByCategoryModal from '../../components/TransactionsByCategoryModal';
|
||||||
|
|
||||||
import { extratoService } from '@/services/extratoService';
|
import { extratoService } from '@/services/extratoService';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
|
@ -51,7 +53,7 @@ import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils'
|
||||||
/**
|
/**
|
||||||
* Premium KPI Card with Glow and Glassmorphism
|
* Premium KPI Card with Glow and Glassmorphism
|
||||||
*/
|
*/
|
||||||
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, loading }) => {
|
function KPICard({ title, value, subtext, icon, colorClass, highlight, trend, loading }) {
|
||||||
const Icon = icon;
|
const Icon = icon;
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-[#1e293b]/40 backdrop-blur-md border-slate-200 dark:border-slate-800 shadow-xl relative overflow-hidden group hover:border-slate-300 dark:hover:border-slate-700/50 transition-all duration-500">
|
<Card className="bg-white dark:bg-[#1e293b]/40 backdrop-blur-md border-slate-200 dark:border-slate-800 shadow-xl relative overflow-hidden group hover:border-slate-300 dark:hover:border-slate-700/50 transition-all duration-500">
|
||||||
|
|
@ -82,12 +84,12 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, lo
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const MESES_REC = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
const MESES_REC = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
||||||
const ANOS_REC = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
|
const ANOS_REC = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
|
||||||
|
|
||||||
export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlanejadas = [] }) => {
|
export function CruzamentoView({ data = [], kpis = {}, caixas = [], entradasPlanejadas = [], categorias = [] }) {
|
||||||
const [filtroMes, setFiltroMes] = React.useState(String(new Date().getMonth() + 1));
|
const [filtroMes, setFiltroMes] = React.useState(String(new Date().getMonth() + 1));
|
||||||
const [filtroAno, setFiltroAno] = React.useState(new Date().getFullYear().toString());
|
const [filtroAno, setFiltroAno] = React.useState(new Date().getFullYear().toString());
|
||||||
const [filtroCaixa, setFiltroCaixa] = React.useState('todos');
|
const [filtroCaixa, setFiltroCaixa] = React.useState('todos');
|
||||||
|
|
@ -103,25 +105,58 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
});
|
});
|
||||||
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
|
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
|
||||||
|
|
||||||
|
const [somaCategorias, setSomaCategorias] = React.useState([]);
|
||||||
|
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
|
||||||
|
|
||||||
|
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [selectedCategory, setSelectedCategory] = React.useState(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
||||||
|
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
||||||
|
|
||||||
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
// Mapeamento removido pois agora usamos os nomes da API de somaCategorias
|
||||||
|
|
||||||
const dadosFiltrados = React.useMemo(() => {
|
const dadosFiltrados = React.useMemo(() => {
|
||||||
if (!Array.isArray(data)) return [];
|
// Agora usamos os dados brutos de transações agrupadas de somaCategorias para filtros globais se necessário,
|
||||||
return data.filter(item => {
|
// mas para o CruzamentoView, o dadosFiltrados costuma vir da prop 'data'.
|
||||||
const matchPeriod = (dataStr) => {
|
// Se quisermos que o dadosFiltrados reflita o que o backend agrupou:
|
||||||
const { year, month } = parseDateInfo(dataStr);
|
const allTrans = somaCategorias.flatMap(c => c.transacoes || []);
|
||||||
if (filtroTipo === 'ano') return year === Number(filtroAno);
|
const sourceData = (filtroTipo === 'mes' && allTrans.length > 0) ? allTrans : (Array.isArray(data) ? data : []);
|
||||||
if (filtroTipo === 'mes') return year === Number(filtroAno) && month === Number(filtroMes);
|
|
||||||
return true;
|
console.log('[CruzamentoView] Pipeline de dados:', {
|
||||||
};
|
filtroTipo,
|
||||||
|
sourceCount: sourceData.length,
|
||||||
|
propCount: data?.length,
|
||||||
|
somaTransCount: allTrans.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return sourceData.filter(item => {
|
||||||
|
const matchPeriod = (dataStr) => {
|
||||||
|
const { year, month } = parseDateInfo(dataStr);
|
||||||
|
const y = Number(year);
|
||||||
|
const m = Number(month);
|
||||||
|
const fAno = Number(filtroAno);
|
||||||
|
const fMes = Number(filtroMes);
|
||||||
|
|
||||||
|
if (filtroTipo === 'ano') return y === fAno;
|
||||||
|
if (filtroTipo === 'mes') return y === fAno && m === fMes;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// Apply period filter
|
// Apply period filter
|
||||||
if (!matchPeriod(item?.dataEntrada || '')) return false;
|
if (!matchPeriod(item?.dataEntrada || item?.data || '')) return false;
|
||||||
|
|
||||||
|
// Garantir que estamos vendo apenas Receitas (C)
|
||||||
|
const op = String(item.tipoOperacao || '').toUpperCase();
|
||||||
|
if (op && op !== 'C') return false;
|
||||||
|
|
||||||
if (filtroCaixa !== 'todos' && item?.caixinha != null && String(item.caixinha) !== String(filtroCaixa)) return false;
|
if (filtroCaixa !== 'todos' && item?.caixinha != null && String(item.caixinha) !== String(filtroCaixa)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [data, filtroMes, filtroAno, filtroCaixa, filtroTipo]);
|
}, [data, somaCategorias, filtroMes, filtroAno, filtroCaixa, filtroTipo]);
|
||||||
|
|
||||||
const planejadasFiltradas = React.useMemo(() => {
|
const planejadasFiltradas = React.useMemo(() => {
|
||||||
if (!Array.isArray(entradasPlanejadas)) return [];
|
if (!Array.isArray(entradasPlanejadas)) return [];
|
||||||
|
|
@ -140,8 +175,6 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
});
|
});
|
||||||
}, [entradasPlanejadas, filtroMes, filtroAno, filtroTipo]);
|
}, [entradasPlanejadas, filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
const [somaCategorias, setSomaCategorias] = React.useState([]);
|
|
||||||
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
|
|
||||||
|
|
||||||
// Efeito para buscar soma por categoria do backend
|
// Efeito para buscar soma por categoria do backend
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -157,6 +190,10 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
// Normaliza o resultado conforme o backend (esperado array de objetos)
|
// Normaliza o resultado conforme o backend (esperado array de objetos)
|
||||||
// Normaliza o resultado conforme o backend para receitas
|
// Normaliza o resultado conforme o backend para receitas
|
||||||
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
|
const data = result?.por_categoria ?? result?.dados ?? result?.Base_Dados_API ?? (Array.isArray(result) ? result : []);
|
||||||
|
console.log('[CruzamentoView] somaCategorias carregadas:', data.length, 'itens');
|
||||||
|
if (data.length > 0) {
|
||||||
|
console.log('[CruzamentoView] Exemplo soma:', data[0]);
|
||||||
|
}
|
||||||
setSomaCategorias(Array.isArray(data) ? data : []);
|
setSomaCategorias(Array.isArray(data) ? data : []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[CruzamentoView] Erro ao buscar soma por categoria:', err);
|
console.error('[CruzamentoView] Erro ao buscar soma por categoria:', err);
|
||||||
|
|
@ -174,6 +211,8 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
}
|
}
|
||||||
}, [filtroMes, filtroAno, filtroTipo]);
|
}, [filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
|
// Efeito para buscar transações detalhadas do período removido pois soma_por_categoria já traz os itens.
|
||||||
|
|
||||||
// Efeito para buscar totais planejados do mês vigente
|
// Efeito para buscar totais planejados do mês vigente
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchTotais = async () => {
|
const fetchTotais = async () => {
|
||||||
|
|
@ -199,8 +238,29 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
fetchTotais();
|
fetchTotais();
|
||||||
}, [filtroMes, filtroAno, filtroTipo]);
|
}, [filtroMes, filtroAno, filtroTipo]);
|
||||||
|
|
||||||
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
// Modal state
|
||||||
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
|
||||||
|
const handleCategoryClick = (name, id) => {
|
||||||
|
console.log('[CruzamentoView] Clicou na categoria:', { name, id });
|
||||||
|
setSelectedCategory({ name, id });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactionsForSelectedCategory = React.useMemo(() => {
|
||||||
|
if (!selectedCategory) return [];
|
||||||
|
|
||||||
|
// Agora buscamos diretamente no objeto que o backend retornou já agrupado
|
||||||
|
const catObj = somaCategorias.find(c => {
|
||||||
|
const id = c.idcategoria ?? c.idCategoria ?? c.id;
|
||||||
|
const name = c.categoria || '';
|
||||||
|
return String(id) === String(selectedCategory.id) || name === selectedCategory.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactions = catObj?.transacoes || [];
|
||||||
|
console.log('[CruzamentoView] Modal: itens para', selectedCategory.name, ':', transactions.length);
|
||||||
|
return transactions;
|
||||||
|
}, [selectedCategory, somaCategorias]);
|
||||||
|
|
||||||
|
|
||||||
// Efeito para buscar dados do gráfico do backend
|
// Efeito para buscar dados do gráfico do backend
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -241,7 +301,7 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
}
|
}
|
||||||
|
|
||||||
const total_realizado = dadosFiltrados
|
const total_realizado = dadosFiltrados
|
||||||
.filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado')
|
.filter(t => !t?.status || t?.status === 'Recebido' || t?.status === 'Liquidado')
|
||||||
.reduce((acc, t) => acc + (t?.valor || 0), 0);
|
.reduce((acc, t) => acc + (t?.valor || 0), 0);
|
||||||
|
|
||||||
const total_boletos_a_receber = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0);
|
const total_boletos_a_receber = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0);
|
||||||
|
|
@ -257,14 +317,16 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
if (!Array.isArray(somaCategorias)) return [];
|
if (!Array.isArray(somaCategorias)) return [];
|
||||||
return somaCategorias.map(item => ({
|
return somaCategorias.map(item => ({
|
||||||
name: item.categoria || 'Sem Categoria',
|
name: item.categoria || 'Sem Categoria',
|
||||||
value: parseCurrency(item.total_entradas || 0)
|
value: parseCurrency(item.total || item.total_entradas || 0)
|
||||||
})).filter(item => item.value > 0);
|
})).filter(item => item.value > 0);
|
||||||
}, [somaCategorias]);
|
}, [somaCategorias]);
|
||||||
|
|
||||||
const timelineData = useMemo(() => {
|
const timelineData = useMemo(() => {
|
||||||
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||||
|
|
||||||
const filteredExecutadas = dadosFiltrados.filter(item => item?.status === 'Recebido' || item?.status === 'Liquidado');
|
const filteredExecutadas = dadosFiltrados.filter(item =>
|
||||||
|
!item?.status || item?.status === 'Recebido' || item?.status === 'Liquidado'
|
||||||
|
);
|
||||||
const filteredPlanejadas = planejadasFiltradas;
|
const filteredPlanejadas = planejadasFiltradas;
|
||||||
|
|
||||||
if (filtroTipo === 'ano') {
|
if (filtroTipo === 'ano') {
|
||||||
|
|
@ -401,22 +463,16 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
const variacaoCategoria = useMemo(() => {
|
const variacaoCategoria = useMemo(() => {
|
||||||
if (!Array.isArray(somaCategorias)) return [];
|
if (!Array.isArray(somaCategorias)) return [];
|
||||||
|
|
||||||
return somaCategorias.map(item => {
|
return somaCategorias.map(item => ({
|
||||||
const entradas = parseCurrency(item.total_entradas || 0);
|
id: item.idcategoria ?? item.idCategoria ?? item.id,
|
||||||
const saidas = parseCurrency(item.total_saidas || 0);
|
categoria: item.categoria || 'Sem Categoria',
|
||||||
const diferenca = parseCurrency(item.diferenca || 0);
|
categoriaNome: item.categoria || '(Sem Categoria)',
|
||||||
const percentual = entradas > 0 ? ((entradas / (entradas + Math.abs(saidas))) * 100).toFixed(1) : 0;
|
name: item.categoria || '(Sem Categoria)',
|
||||||
|
entradas: parseCurrency(item.total || item.total_entradas || 0),
|
||||||
return {
|
saidas: parseCurrency(item.total_saidas || 0),
|
||||||
categoria: item.categoria || 'Sem Categoria',
|
diferenca: parseCurrency(item.diferenca || 0),
|
||||||
categoriaNome: item.categoria || '(Sem Categoria)',
|
percentual: 0 // Ajustar se necessário
|
||||||
name: item.categoria || '(Sem Categoria)',
|
})).sort((a, b) => b.entradas - a.entradas);
|
||||||
entradas,
|
|
||||||
saidas,
|
|
||||||
diferenca,
|
|
||||||
percentual
|
|
||||||
};
|
|
||||||
}).sort((a, b) => b.entradas - a.entradas);
|
|
||||||
}, [somaCategorias]);
|
}, [somaCategorias]);
|
||||||
|
|
||||||
// KPIs de variância: Usando os dados reais
|
// KPIs de variância: Usando os dados reais
|
||||||
|
|
@ -774,7 +830,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
field: 'categoriaNome',
|
field: 'categoriaNome',
|
||||||
header: 'Categoria',
|
header: 'Categoria',
|
||||||
width: '250px',
|
width: '250px',
|
||||||
render: (row) => <span className="font-semibold text-slate-900 dark:text-white pl-3">{row.categoriaNome}</span>
|
render: (row) => (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCategoryClick(row.categoria, row.id)}
|
||||||
|
className="font-semibold text-slate-900 dark:text-white pl-3 hover:text-emerald-500 transition-colors text-left w-full"
|
||||||
|
>
|
||||||
|
{row.categoriaNome}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'entradas',
|
field: 'entradas',
|
||||||
|
|
@ -786,18 +849,9 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
field: 'saidas',
|
|
||||||
header: 'Saídas',
|
|
||||||
width: '150px',
|
|
||||||
render: (row) => (
|
|
||||||
<span className="font-mono text-xs text-rose-400 text-right block w-full">
|
|
||||||
{formatCurrency(row.saidas)}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: 'diferenca',
|
field: 'diferenca',
|
||||||
|
|
||||||
header: 'Diferença',
|
header: 'Diferença',
|
||||||
width: '150px',
|
width: '150px',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
|
|
@ -835,6 +889,14 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<TransactionsByCategoryModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
categoryName={selectedCategory?.name}
|
||||||
|
transactions={transactionsForSelectedCategory}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ export const boletosService = {
|
||||||
/**
|
/**
|
||||||
* Busca detalhes de juros de um cliente específico
|
* Busca detalhes de juros de um cliente específico
|
||||||
* Rota: GET /financeiro/cliente/boletos
|
* Rota: GET /financeiro/cliente/boletos
|
||||||
|
* Resposta esperada: { cliente, dias_atraso, juros, multa, seuNumero, situacao, total_atualizado, valor_original, vencimento }
|
||||||
* @param {number|string} idempresa
|
* @param {number|string} idempresa
|
||||||
*/
|
*/
|
||||||
fetchJurosCliente: (idempresa) => handleRequest({
|
fetchJurosCliente: (idempresa) => handleRequest({
|
||||||
|
|
@ -191,9 +192,17 @@ export const boletosService = {
|
||||||
params: { idempresa }
|
params: { idempresa }
|
||||||
});
|
});
|
||||||
const raw = response?.data ?? response;
|
const raw = response?.data ?? response;
|
||||||
// Assume a estrutura padrão de retorno do sistema ou extraído diretamente
|
|
||||||
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
||||||
return Array.isArray(data) ? data : [];
|
// A API pode retornar um objeto único ou um array
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
// Se for objeto válido com campos conhecidos, envolve em array
|
||||||
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||||
|
const items = Object.values(data);
|
||||||
|
// Se o próprio objeto tem campos de juros é um registro único
|
||||||
|
const hasBoletoFields = 'vencimento' in data || 'juros' in data || 'situacao' in data;
|
||||||
|
return hasBoletoFields ? [data] : items.filter(i => typeof i === 'object' && i !== null);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@ export const extratoService = {
|
||||||
* Busca extrato bancário
|
* Busca extrato bancário
|
||||||
* Rota: GET /extrato/apresentar
|
* Rota: GET /extrato/apresentar
|
||||||
* Retorna array bruto; o consumidor filtra por tipoOperacao (C/D).
|
* Retorna array bruto; o consumidor filtra por tipoOperacao (C/D).
|
||||||
|
* @param {Object} params - Opcional { mes, ano }
|
||||||
* @returns {Promise<Array<{idextrato, dataEntrada, descricao, valor, tipoOperacao, categoria, beneficiario_pagador, ...}>>}
|
* @returns {Promise<Array<{idextrato, dataEntrada, descricao, valor, tipoOperacao, categoria, beneficiario_pagador, ...}>>}
|
||||||
*/
|
*/
|
||||||
fetchExtrato: () => handleRequest({
|
fetchExtrato: (params) => handleRequest({
|
||||||
mockFn: () => simulateLatency([]),
|
mockFn: () => simulateLatency([]),
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
const response = await api.get('/extrato/apresentar');
|
const response = await api.get('/extrato/apresentar', { params });
|
||||||
const raw = response?.data ?? response;
|
const raw = response?.data ?? response;
|
||||||
|
|
||||||
// Lógica robusta de extração (igual ao workspaceConciliacaoService)
|
// Lógica robusta de extração (igual ao workspaceConciliacaoService)
|
||||||
|
|
@ -25,7 +26,6 @@ export const extratoService = {
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
// Se for objeto (ex: { '0': {...}, '1': {...} }), converte para array
|
// Se for objeto (ex: { '0': {...}, '1': {...} }), converte para array
|
||||||
// Mas cuidado com wrappers { success: true, ... } - idealmente o backend envia dados limpos ou em 'dados'
|
|
||||||
data = Object.values(data || {});
|
data = Object.values(data || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,12 +150,13 @@ export const extratoService = {
|
||||||
* Fluxo de entrada e saída mensal
|
* Fluxo de entrada e saída mensal
|
||||||
* Rota: GET /extrato/fluxo
|
* Rota: GET /extrato/fluxo
|
||||||
* Retorna dados mensais de entrada e saída
|
* Retorna dados mensais de entrada e saída
|
||||||
|
* @param {Object} params - Opcional { mes, ano }
|
||||||
* @returns {Promise<{ mensal: Array<{ mes, ano, entrada, saida, ... }> }>}
|
* @returns {Promise<{ mensal: Array<{ mes, ano, entrada, saida, ... }> }>}
|
||||||
*/
|
*/
|
||||||
fetchFluxo: () => handleRequest({
|
fetchFluxo: (params) => handleRequest({
|
||||||
mockFn: () => simulateLatency({ mensal: [], anual: [], diario: [] }),
|
mockFn: () => simulateLatency({ mensal: [], anual: [], diario: [] }),
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
const response = await api.get('/extrato/fluxo');
|
const response = await api.get('/extrato/fluxo', { params });
|
||||||
const raw = response?.data ?? response;
|
const raw = response?.data ?? response;
|
||||||
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw ?? {};
|
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw ?? {};
|
||||||
|
|
||||||
|
|
@ -203,13 +204,13 @@ export const extratoService = {
|
||||||
/**
|
/**
|
||||||
* Busca navegação hierárquica detalhada por caixinha
|
* Busca navegação hierárquica detalhada por caixinha
|
||||||
* Rota: GET /extrato/apresentar/caixinha/detalhado
|
* Rota: GET /extrato/apresentar/caixinha/detalhado
|
||||||
* @param {number|string} id_caixinha
|
* @param {Object} params - { caixinha, mes, ano }
|
||||||
*/
|
*/
|
||||||
getCaixinhaDetalhada: (id_caixinha) => handleRequest({
|
getCaixinhaDetalhada: (params) => handleRequest({
|
||||||
mockFn: () => simulateLatency({}),
|
mockFn: () => simulateLatency({}),
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
const response = await api.get('/extrato/apresentar/caixinha/detalhado', {
|
const response = await api.get('/extrato/apresentar/caixinha/detalhado', {
|
||||||
params: { caixinha: id_caixinha }
|
params
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
@ -390,21 +391,25 @@ export const extratoService = {
|
||||||
/**
|
/**
|
||||||
* Busca transações de um beneficiário específico
|
* Busca transações de um beneficiário específico
|
||||||
* Rota: POST /beneficiario_aplicado
|
* Rota: POST /beneficiario_aplicado
|
||||||
|
* Resposta esperada: { "categoria": "NOME DO CLIENTE", "linha_tempo": [...] }
|
||||||
|
* Retorna: { categoria: string, linhaTemp: Array }
|
||||||
* @param {string} beneficiario_pagador
|
* @param {string} beneficiario_pagador
|
||||||
*/
|
*/
|
||||||
fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({
|
fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({
|
||||||
mockFn: () => simulateLatency([]),
|
mockFn: () => simulateLatency({ categoria: '', linha_tempo: [] }),
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
const response = await api.post('/beneficiario_aplicado', {
|
const response = await api.post('/beneficiario_aplicado', {
|
||||||
beneficiario_pagador: beneficiario_pagador
|
beneficiario_pagador: beneficiario_pagador
|
||||||
});
|
});
|
||||||
const raw = response?.data ?? response;
|
const raw = response?.data ?? response;
|
||||||
let data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
// A resposta é um objeto com "categoria" e "linha_tempo"
|
||||||
|
const root = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
||||||
if (!Array.isArray(data)) {
|
const categoria = root?.categoria ?? '';
|
||||||
data = Object.values(data || {});
|
const linhaTemp = root?.linha_tempo ?? [];
|
||||||
}
|
return {
|
||||||
return Array.isArray(data) ? data : [];
|
categoria,
|
||||||
|
linha_tempo: Array.isArray(linhaTemp) ? linhaTemp : []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -164,13 +164,14 @@ export const fornecedoresService = {
|
||||||
deleteFornecedor: (id) => handleRequest({
|
deleteFornecedor: (id) => handleRequest({
|
||||||
mockFn: () => simulateLatency({ success: true }),
|
mockFn: () => simulateLatency({ success: true }),
|
||||||
apiFn: async () => {
|
apiFn: async () => {
|
||||||
// Tenta primeiro POST /fornecedores/delete enviando idfornecedores no corpo (JSON)
|
|
||||||
try {
|
try {
|
||||||
console.log('[fornecedoresService] Tentando excluir fornecedor via POST /fornecedores/delete:', id);
|
console.log('[fornecedoresService] Tentando excluir fornecedor via DELETE /fornecedores/delete:', id);
|
||||||
const response = await api.delete('/fornecedores/delete', { idfornecedores: id });
|
const response = await api.delete('/fornecedores/delete', {
|
||||||
|
data: { idfornecedores: id }
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[fornecedoresService] Falha no POST /fornecedores/delete, tentando fallback DELETE:', err.message);
|
console.warn('[fornecedoresService] Falha no DELETE /fornecedores/delete, tentando fallback DELETE com ID na URL:', err.message);
|
||||||
// Fallback: tenta DELETE /fornecedores/:id
|
// Fallback: tenta DELETE /fornecedores/:id
|
||||||
try {
|
try {
|
||||||
const response = await api.delete(`/fornecedores/${id}`);
|
const response = await api.delete(`/fornecedores/${id}`);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,9 @@ export const workspaceConciliacaoService = {
|
||||||
|
|
||||||
const mapped = data.map(item => ({
|
const mapped = data.map(item => ({
|
||||||
id: item.idextrato || item.id || 0,
|
id: item.idextrato || item.id || 0,
|
||||||
|
idextrato: item.idextrato || item.id || 0,
|
||||||
data: item.dataEntrada || item.data || '',
|
data: item.dataEntrada || item.data || '',
|
||||||
|
dataEntrada: item.dataEntrada || item.data || '',
|
||||||
descricao: item.descricao || item.titulo || '',
|
descricao: item.descricao || item.titulo || '',
|
||||||
valor: parseValorBackend(item.valor),
|
valor: parseValorBackend(item.valor),
|
||||||
tipo: item.tipoOperacao === 'D' ? 'DEBITO' : item.tipoOperacao === 'C' ? 'CREDITO' : 'DEBITO',
|
tipo: item.tipoOperacao === 'D' ? 'DEBITO' : item.tipoOperacao === 'C' ? 'CREDITO' : 'DEBITO',
|
||||||
|
|
@ -107,12 +109,26 @@ export const workspaceConciliacaoService = {
|
||||||
tipoTransacao: item.tipoTransacao || '',
|
tipoTransacao: item.tipoTransacao || '',
|
||||||
titulo: item.titulo || '',
|
titulo: item.titulo || '',
|
||||||
caixaId: item.caixinha ? parseInt(item.caixinha) : null,
|
caixaId: item.caixinha ? parseInt(item.caixinha) : null,
|
||||||
|
caixinha: item.caixinha || null,
|
||||||
categoriaId: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? parseInt(item.categoria) : null,
|
categoriaId: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? parseInt(item.categoria) : null,
|
||||||
|
categoria: item.categoria || null,
|
||||||
regraId: item.regra && item.regra !== '0' && item.regra !== 0 ? parseInt(item.regra) : null,
|
regraId: item.regra && item.regra !== '0' && item.regra !== 0 ? parseInt(item.regra) : null,
|
||||||
|
regra: item.regra || null,
|
||||||
beneficiario: item.beneficiario_pagador || null,
|
beneficiario: item.beneficiario_pagador || null,
|
||||||
|
beneficiario_pagador: item.beneficiario_pagador || null,
|
||||||
cpfCnpjPagador: item.cpfCnpjPagador || '',
|
cpfCnpjPagador: item.cpfCnpjPagador || '',
|
||||||
cpfCnpjRecebedor: item.cpfCnpjRecebedor || '',
|
cpfCnpjRecebedor: item.cpfCnpjRecebedor || '',
|
||||||
status: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? 'CONCILIADA' : 'PENDENTE'
|
status: item.categoria && item.categoria !== '0' && item.categoria !== 0 ? 'CONCILIADA' : 'PENDENTE',
|
||||||
|
// Campos financeiros detalhados
|
||||||
|
juros: parseValorBackend(item.juros),
|
||||||
|
multa: parseValorBackend(item.multa),
|
||||||
|
abatimento: parseValorBackend(item.abatimento),
|
||||||
|
imposto: parseValorBackend(item.imposto),
|
||||||
|
adicionado: item.adicionado || "0.00",
|
||||||
|
desconto1: parseValorBackend(item.desconto1),
|
||||||
|
desconto2: parseValorBackend(item.desconto2),
|
||||||
|
desconto3: parseValorBackend(item.desconto3),
|
||||||
|
valorTotal: parseValorBackend(item.valorTotal || item.valor)
|
||||||
}));
|
}));
|
||||||
console.log('[workspaceConciliacaoService] Dados mapeados:', mapped);
|
console.log('[workspaceConciliacaoService] Dados mapeados:', mapped);
|
||||||
return mapped;
|
return mapped;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue