diff --git a/.agent/history.json b/.agent/history.json index ab6533e..e07b512 100644 --- a/.agent/history.json +++ b/.agent/history.json @@ -35,5 +35,10 @@ "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" + }, + { + "feature": "Implementado seletor de per\u00edodo (Anual/Mensal) no dashboard do GR com filtros de m\u00eas e ano.", + "status": "active", + "timestamp": "2026-02-08" } ] \ No newline at end of file diff --git a/README.md b/README.md index 4b53ea5..f86b587 100644 --- a/README.md +++ b/README.md @@ -124,27 +124,79 @@ PlatformSistemas/ └── package.json # Dependências e scripts ``` -## 🎯 Módulos e Features +## 🎯 Módulos e Ambientes -### 💰 Financeiro -- **financeiro-cnab**: Sistema completo de CNAB para geração de remessas, gestão de favorecidos e pagamentos -- **financeiro-v2**: Módulo avançado de contas a receber/pagar, conciliação e fluxo de caixa -- **workspace**: Workspace financeiro com gestão de receitas, despesas e conciliação +### 👥 RH (Recursos Humanos) +Módulo desenvolvido para atender integralmente as demandas dos setores de RH e Departamento Pessoal. +- **Funcionalidades**: Gestão de funcionários, controle de ponto eletrônico, processamento de benefícios e cálculos trabalhistas. +- **Destaque**: Dashboards de contratos de experiência e acompanhamento de admissões. -### 🚛 Frota -- **fleet-v2**: Gestão completa de frota com dashboard, manutenções, abastecimentos e monitoramento +### 💰 CNAB (Financeiro Remessas) +Módulo financeiro especializado na interface bancária e automação de pagamentos. +- **Funcionalidades**: Geração e gerenciamento de remessas CNAB, cadastro de favorecidos, processamento de arquivos de retorno e validação de pagamentos TED/PIX. -### 👥 Recursos Humanos -- **rh**: Sistema de RH com ponto eletrônico, gestão de funcionários, benefícios e cálculos +### 🚛 Prafrota (Gestão de Frota) +Sistema robusto para o controle operacional de frotas de veículos. +- **Funcionalidades**: Cadastro técnico de veículos, monitoramento em tempo real, gestão de manutenções preventivas/corretivas e controle de abastecimentos. -### 📋 Gestão de Registros -- **gr**: Sistema de gestão de registros com Kanban, cadastro de motoristas e contratos +### 🏦 Financeiro_V2 + Workspace +Solução avançada para gestão financeira estratégica e operacional. +- **Funcionalidades**: Gestão de contas a pagar e receber, fluxo de caixa projetado vs. realizado, conciliação bancária automatizada e dashboards de indicadores financeiros (KPIs). -### 🔬 AutoLab -- **autolab**: Gestão de laboratório com estoque, vendas e configurações +### 🍊 OestePan (Customização Prafrota) +Segmento especializado do Prafrota customizado especificamente para as necessidades logísticas da Oeste Pan. +- **Funcionalidades**: Além das funções base do Prafrota, inclui integração com Moki para checklists, gestão específica de sinistros e visualização otimizada para a operação do cliente. -### 🛠️ Desenvolvimento -- **dev-tools**: Playground para testes de componentes e debug +### 🔬 AutoLab (Gestão de Oficinas) +Módulo em fase de finalização focado na operação técnica de laboratórios e oficinas. +- **Funcionalidades**: Controle de estoque de peças, gestão de ordens de serviço (vendas), cadastro de clientes e configurações técnicas de atendimento. + +--- + +## 🛣️ Estrutura de Rotas e Variáveis + +Cada ambiente consome uma API RESTful estruturada para operações de CRUD (Apresentação, Edição e Exclusão). + +### Padrão de Endpoints +As rotas seguem a nomenclatura do recurso base (ex: `/prafrot`, `/workspace`): + +- **Apresentação (GET)**: + - `/recurso/apresentar`: Lista principal de dados. + - `/recurso/listagem`: Alternativa para listagens simplificadas. + - `/recurso/:id`: Detalhamento de um registro específico. +- **Edição/Atualização (PUT/PATCH)**: + - `/recurso/edit`: Atualização de campos específicos. + - `/recurso/:id`: Atualização completa do registro. + - `/recurso/edit/status_global`: Utilizado em Kanbans para movimentação de cards. +- **Exclusão (DELETE)**: + - `/recurso/delete/:id`: Remoção lógica ou física do registro. + +--- + +## 🎨 Design System & Playground + +O projeto utiliza um laboratório de componentes (**Playground**) para garantir consistência visual em todos os ambientes. + +### Componentes em Uso (Playground) +Estes componentes são extraídos do Design System e aplicados nos módulos: + +| Componente | Função | Ambientes Principais | +| :--- | :--- | :--- | +| **ExcelTable** | Tabela de alta performance com filtros | RH, Prafrota, Financeiro | +| **ItemDetailPanel** | Painel lateral para visualização de detalhes | Prafrota, OestePan | +| **DashboardKPICard** | Cards de indicadores com micro-gráficos | Financeiro_V2, Workspace | +| **StatsGrid** | Grid de estatísticas rápidas | RH, Prafrota | +| **KanbanBoard** | Gestão visual de fluxos e status | GR, RH | +| **AutoFillInput** | Inputs inteligentes com busca em tempo real | Workspace, Financeiro | +| **StatusBadge** | Badges coloridos de status dinâmico | Todos | + +### Componentes Disponíveis (Candidatos) +Componentes presentes no laboratório mas com uso restrito ou em homologação: +- `ThemeTuner`: Ferramenta de ajuste de temas em runtime. +- `FinesCard`: Card especializado para visualização de multas. +- `SmartTable`: Versão experimental de tabelas com auto-ajuste. + +--- ## 🛠️ Tecnologias diff --git a/src/components/shared/AutoFillInput.jsx b/src/components/shared/AutoFillInput.jsx index 6e74e13..30fdf54 100644 --- a/src/components/shared/AutoFillInput.jsx +++ b/src/components/shared/AutoFillInput.jsx @@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button'; export const AutoFillInput = ({ label, placeholder = "Comece a digitar para pesquisar...", + value = '', data = [], apiRoute, filterField = "name", @@ -28,12 +29,18 @@ export const AutoFillInput = ({ className, icon: Icon = Search }) => { - const [query, setQuery] = useState(''); + const [query, setQuery] = useState(value); + + // Sincroniza o valor externo com o estado interno (útil para edição) + useEffect(() => { + setQuery(value || ''); + }, [value]); const [suggestions, setSuggestions] = useState([]); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const containerRef = useRef(null); + const inputRef = useRef(null); // Fecha a lista ao clicar fora do componente useEffect(() => { @@ -58,7 +65,12 @@ export const AutoFillInput = ({ ).slice(0, 10); setSuggestions(results); - setIsOpen(true); + + // Só abre automaticamente se o input estiver focado + if (document.activeElement === inputRef.current) { + setIsOpen(true); + } + setIsLoading(false); } else { setSuggestions([]); @@ -109,6 +121,7 @@ export const AutoFillInput = ({ setQuery(e.target.value)} diff --git a/src/features/financeiro-cnab/hooks/useCnabStore.js b/src/features/financeiro-cnab/hooks/useCnabStore.js index eb4738e..5df183b 100644 --- a/src/features/financeiro-cnab/hooks/useCnabStore.js +++ b/src/features/financeiro-cnab/hooks/useCnabStore.js @@ -10,7 +10,7 @@ export const useCnabStore = create((set) => ({ setPaymentMode: (mode) => set({ paymentMode: mode }), selectedBank: 'INTER', // 'INTER' ou 'BRADESCO' setSelectedBank: (bank) => set({ selectedBank: bank }), - /** Bradesco TED: 'ted' = rota /cnab/bradesco/ted, 'folha_pagamento' = rota /cnab/bradesco/folha_pagamento */ - bradescoRemessaVariant: 'ted', // 'ted' | 'folha_pagamento' + /** Bradesco TED: 'ted' = rota /cnab/bradesco/ted, 'folha_pagamento' = rota /cnab/bradesco/folha_pagamento, 'cobranca' = rota /cnab/bradesco/cobranca */ + bradescoRemessaVariant: 'ted', // 'ted' | 'folha_pagamento' | 'cobranca' setBradescoRemessaVariant: (variant) => set({ bradescoRemessaVariant: variant }), })); diff --git a/src/features/financeiro-cnab/hooks/useGerarRemessa.js b/src/features/financeiro-cnab/hooks/useGerarRemessa.js index 22a7abe..9870a79 100644 --- a/src/features/financeiro-cnab/hooks/useGerarRemessa.js +++ b/src/features/financeiro-cnab/hooks/useGerarRemessa.js @@ -158,6 +158,22 @@ const MODEL_SCHEMAS = { { key: 'TIPO_CONTA_FAVORECIDA', label: 'Tipo Conta Favorecida', required: false }, { key: 'FINALIDADE_TED', label: 'Finalidade TED', required: false }, { key: 'INFORMACAO', label: 'Informação', required: false } + ], + BRADESCO_COBRANCA: [ + { key: 'VALOR', label: 'Valor Título', required: true }, + { key: 'VENCIMENTO', label: 'Data Vencimento', required: true }, + { key: 'NOSSO_NUMERO', label: 'Nosso Número', required: true }, + { key: 'NUMERO_DOCUMENTO', label: 'Número Documento', required: true }, + { key: 'NOME_PAGADOR', label: 'Nome Pagador', required: true }, + { key: 'CPF_CNPJ_PAGADOR', label: 'CPF/CNPJ Pagador', required: true }, + { key: 'ENDERECO', label: 'Endereço', required: true }, + { key: 'BAIRRO', label: 'Bairro', required: true }, + { key: 'CEP', label: 'CEP', required: true }, + { key: 'CIDADE', label: 'Cidade', required: true }, + { key: 'UF', label: 'UF', required: true }, + { key: 'DATA_EMISSAO', label: 'Data Emissão', required: true }, + { key: 'USO_EMPRESA', label: 'Uso Empresa', required: false }, + { key: 'ESPECIE_TITULO', label: 'Espécie Título', required: false } ] }; @@ -167,9 +183,15 @@ const MODEL_SCHEMAS = { * 1. Upload → 2. Mapping → 3. Validate Columns → 4. Validate JSON → 5. Generate Excel → 6. Generate REM */ export const useGerarRemessa = () => { - const { selectedBank, paymentMode, bradescoRemessaVariant } = useCnabStore(); // Banco, modo de pagamento e variante TED Bradesco (ted | folha_pagamento) + const { selectedBank, paymentMode, bradescoRemessaVariant } = useCnabStore(); // Banco, modo de pagamento e variante TED Bradesco (ted | folha_pagamento | cobranca) const [step, setStep] = useState('upload'); // upload | mapping | validate | success - const paymentType = paymentMode; // Usa o paymentMode do store ao invés de estado local + + // Calcula o tipo de pagamento efetivo para seleção do schema + const effectivePaymentType = paymentMode === 'TED' && selectedBank === 'BRADESCO' && bradescoRemessaVariant === 'cobranca' + ? 'BRADESCO_COBRANCA' + : paymentMode; + + const paymentType = paymentMode; const [loading, setLoading] = useState(false); // Dados do arquivo @@ -282,7 +304,7 @@ export const useGerarRemessa = () => { console.group('🔗 Mapeamento automático de colunas'); console.log('📊 Headers disponíveis:', rawHeaders); - MODEL_SCHEMAS[paymentType].forEach(field => { + MODEL_SCHEMAS[effectivePaymentType].forEach(field => { // Tenta múltiplas estratégias de matching const fieldKey = clean(field.key); const fieldKeyParts = field.key.split('_').map(p => clean(p)).filter(p => p.length > 2); @@ -378,7 +400,7 @@ export const useGerarRemessa = () => { // Converte os dados brutos para o formato esperado pelo backend const mappedData = rawData.map((row, idx) => { const newRow = { id: row.id || idx + 1 }; - MODEL_SCHEMAS[paymentType].forEach(field => { + MODEL_SCHEMAS[effectivePaymentType].forEach(field => { const sourceCol = columnMapping[field.key]; newRow[field.key] = sourceCol ? (row[sourceCol] || '') : ''; }); @@ -822,6 +844,7 @@ export const useGerarRemessa = () => { finalize, downloadRemessa, reset, - schemas: MODEL_SCHEMAS + schemas: MODEL_SCHEMAS, + effectivePaymentType }; }; diff --git a/src/features/financeiro-cnab/services/cnabService.js b/src/features/financeiro-cnab/services/cnabService.js index aa3e085..0d12fab 100644 --- a/src/features/financeiro-cnab/services/cnabService.js +++ b/src/features/financeiro-cnab/services/cnabService.js @@ -738,6 +738,9 @@ const cnabService = { } else if (remessaVariant === 'folha_pagamento') { endpoint = '/cnab/bradesco/folha_pagamento'; console.log('🏦 [CNAB Service] Usando rota Folha de Pagamento (Bradesco)'); + } else if (remessaVariant === 'cobranca') { + endpoint = '/cnab/bradesco/cobranca'; + console.log('🏦 [CNAB Service] Usando rota Cobrança (Bradesco)'); } else { endpoint = '/cnab/bradesco/ted'; } diff --git a/src/features/financeiro-cnab/views/GerarRemessaView.jsx b/src/features/financeiro-cnab/views/GerarRemessaView.jsx index 748ef6d..6151b46 100644 --- a/src/features/financeiro-cnab/views/GerarRemessaView.jsx +++ b/src/features/financeiro-cnab/views/GerarRemessaView.jsx @@ -30,7 +30,8 @@ const GerarRemessaView = () => { finalize, downloadRemessa, reset, - schemas + schemas, + effectivePaymentType } = useGerarRemessa(); const { selectedBank, setSelectedBank, bradescoRemessaVariant, setBradescoRemessaVariant } = useCnabStore(); @@ -189,9 +190,9 @@ const GerarRemessaView = () => { * DEVE SER DEFINIDO DEPOIS DE processedValidatedData E validationInfo */ const validatedColumns = useMemo(() => { - if (!schemas[paymentType]) return []; + if (!schemas[effectivePaymentType]) return []; - return schemas[paymentType].map(field => ({ + return schemas[effectivePaymentType].map(field => ({ header: field.label, field: field.key, width: 180, @@ -340,7 +341,8 @@ const GerarRemessaView = () => {
{[ { value: 'ted', label: 'TED' }, - { value: 'folha_pagamento', label: 'Folha de pagamento' } + { value: 'folha_pagamento', label: 'Folha' }, + { value: 'cobranca', label: 'Cobrança' } ].map(({ value, label }) => (
@@ -125,7 +125,7 @@ const CaixinhaDetailsModal = ({ isOpen, onClose, caixinhaId, caixinhaName, mes, ) : (
- {data.categorias.map((cat, idx) => ( + {(data?.categorias || []).map((cat, idx) => (
@@ -164,7 +164,128 @@ const CaixinhaDetailsModal = ({ isOpen, onClose, caixinhaId, caixinhaName, mes, {expandedCategories[cat.categoria] && (
- {cat.beneficiarios.map((benef, bIdx) => { + {/* Suporte para novas estruturas (subgrupos OU flat transacoes) OU estrutura antiga (beneficiarios) */} + {cat.subgrupos && cat.subgrupos.length > 0 ? ( + cat.subgrupos.map((sub, sIdx) => { + const subKey = `${cat.categoria}-${sub.nome_agrupamento}`; + return ( +
+ + + {expandedBeneficiaries[subKey] && ( +
+ + + + Data + Descrição + Tipo + Valor + + + + {(sub.transacoes || []).map((t, tIdx) => ( + + + {t.dataEntrada || t.data || ''} + + + {t.descricao} + + +
+ {t.tipoOperacao === 'C' ? + : + + } + + {t.tipoOperacao === 'C' ? 'Entrada' : 'Saída'} + +
+
+ + {formatCurrency(t.valor)} + +
+ ))} +
+
+
+ )} +
+ ); + }) + ) : cat.transacoes && cat.transacoes.length > 0 ? ( +
+ + + + Data + Descrição + Tipo + Valor + + + + {cat.transacoes.map((t, tIdx) => ( + + + {t.dataEntrada || t.data || ''} + + + {t.descricao} + + +
+ {t.tipoOperacao === 'C' ? + : + + } + + {t.tipoOperacao === 'C' ? 'Entrada' : 'Saída'} + +
+
+ + {formatCurrency(t.valor)} + +
+ ))} +
+
+
+ ) : (cat.beneficiarios || []).map((benef, bIdx) => { const benefKey = `${cat.categoria}-${benef.beneficiario}`; return (
@@ -201,10 +322,10 @@ const CaixinhaDetailsModal = ({ isOpen, onClose, caixinhaId, caixinhaName, mes, - {benef.transacoes.map((t, tIdx) => ( + {(benef.transacoes || []).map((t, tIdx) => ( - - {formatDate(t.dataEntrada).split(' ')[0]} + + {t.dataEntrada || t.data || ''} {t.descricao} diff --git a/src/features/financeiro-v2/components/CategorizacaoDialog.jsx b/src/features/financeiro-v2/components/CategorizacaoDialog.jsx index 2f3b481..1803709 100644 --- a/src/features/financeiro-v2/components/CategorizacaoDialog.jsx +++ b/src/features/financeiro-v2/components/CategorizacaoDialog.jsx @@ -28,7 +28,8 @@ export function CategorizacaoDialog({ descricao: '', categoria: '', caixa: '', - beneficiario: '' + beneficiario: '', + tag: '' }); const [isCriarRegraOpen, setIsCriarRegraOpen] = React.useState(false); const [regraFormData, setRegraFormData] = React.useState({ @@ -52,7 +53,8 @@ export function CategorizacaoDialog({ descricao: transacao.descricao || '', categoria: (transacao.categoriaId ?? transacao.categoria ?? '').toString(), caixa: (caixaId !== '' && caixaId !== '0') ? caixaId : '', - beneficiario: transacao.beneficiario || transacao.beneficiario_pagador || '' + beneficiario: transacao.beneficiario || transacao.beneficiario_pagador || '', + tag: transacao.tag || transacao.raw?.tag || '' }); } }, [transacao, isOpen]); @@ -97,7 +99,8 @@ export function CategorizacaoDialog({ categoriaId: parseInt(formData.categoria), caixaId: parseInt(formData.caixa), beneficiario: formData.beneficiario, - descricao: formData.descricao + descricao: formData.descricao, + tag: formData.tag }); toast.success('Transação categorizada com sucesso!', 'Sucesso'); onOpenChange(false); @@ -251,6 +254,22 @@ export function CategorizacaoDialog({ className="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white" />
+ +
+ + +
diff --git a/src/features/financeiro-v2/components/ExcelTable.jsx b/src/features/financeiro-v2/components/ExcelTable.jsx index 57fdfff..c790732 100644 --- a/src/features/financeiro-v2/components/ExcelTable.jsx +++ b/src/features/financeiro-v2/components/ExcelTable.jsx @@ -5,7 +5,7 @@ import AdvancedFiltersModal from './AdvancedFiltersModal'; function ExcelTable({ data = [], - columns, + columns = [], filterDefs = [], onEdit, onDelete, diff --git a/src/features/financeiro-v2/components/TransactionDetailModal.jsx b/src/features/financeiro-v2/components/TransactionDetailModal.jsx index 4c7c85a..2ccc5be 100644 --- a/src/features/financeiro-v2/components/TransactionDetailModal.jsx +++ b/src/features/financeiro-v2/components/TransactionDetailModal.jsx @@ -234,6 +234,7 @@ export function TransactionDetailModal({ + diff --git a/src/features/financeiro-v2/hooks/useConciliacao.js b/src/features/financeiro-v2/hooks/useConciliacao.js index d480817..f4a62c0 100644 --- a/src/features/financeiro-v2/hooks/useConciliacao.js +++ b/src/features/financeiro-v2/hooks/useConciliacao.js @@ -102,6 +102,7 @@ const useConciliacao = create((set, get) => ({ externalRuleId, externalCategoryId, isReconciled: !!(externalRuleId && externalRuleId !== "0") || !!(externalCategoryId && externalCategoryId !== "0"), + tag: t.tag || '', raw: t }; }); @@ -203,21 +204,17 @@ const useConciliacao = create((set, get) => ({ return []; }, - reconcileTransaction: async (data) => { - try { - set({ isLoading: true }); - // The API expects individual updates or a structured payload - // Based on the legacy code provided, it calls updateTransactionCategory, updateTransactionBox, etc. - // Or a single updateTransactionRule if it's considered a rule update. - // The user JSON suggests updating category, box, beneficiary, and description. + reconcileTransaction: async (data) => { + try { + set({ isLoading: true }); + const { idextrato, categoria, caixinha, beneficiario_pagador, tag } = data; - const { idextrato, categoria, caixinha, beneficiario_pagador } = data; - - await Promise.all([ - conciliacaoService.updateTransactionCategory(idextrato, categoria), - conciliacaoService.updateTransactionBox(idextrato, caixinha || "0"), - conciliacaoService.updateTransactionBeneficiary(idextrato, beneficiario_pagador) - ]); + await Promise.all([ + conciliacaoService.updateTransactionCategory(idextrato, categoria), + conciliacaoService.updateTransactionBox(idextrato, caixinha || "0"), + conciliacaoService.updateTransactionBeneficiary(idextrato, beneficiario_pagador), + tag ? conciliacaoService.updateTransactionTag(idextrato, tag) : Promise.resolve() + ]); await get().loadData(); set({ isLoading: false }); diff --git a/src/features/financeiro-v2/hooks/useConciliacaoV2.js b/src/features/financeiro-v2/hooks/useConciliacaoV2.js index a9bfdc0..0adac02 100644 --- a/src/features/financeiro-v2/hooks/useConciliacaoV2.js +++ b/src/features/financeiro-v2/hooks/useConciliacaoV2.js @@ -101,7 +101,8 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { const [caminhoNavegacao, setCaminhoNavegacao] = useState([]); const [caixaSelecionado, setCaixaSelecionado] = useState(null); const [categoriaSelecionada, setCategoriaSelecionada] = useState(null); - const [detalheSelecionado, setDetalheSelecionado] = useState(null); + const [subgroupSelecionado, setSubgroupSelecionado] = useState(null); + const [dadosNivelAtual, setDadosNivelAtual] = useState([]); const [backendNavData, setBackendNavData] = useState([]); const [isNavLoading, setIsNavLoading] = useState(false); @@ -119,11 +120,11 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { const recarregarDados = async () => { try { setIsLoading(true); - const extrato = await workspaceConciliacaoService.fetchExtrato(); - setExtratoCompleto(Array.isArray(extrato) ? extrato : []); + const extrato = (await workspaceConciliacaoService.fetchExtrato({ mes: filtroMes, ano: filtroAno })) || []; + setExtratoCompleto(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); + const conciliadas = extrato.filter(t => t?.status === 'CONCILIADA' && t?.categoriaId); + const naoCategorizadas = extrato.filter(t => t?.status === 'PENDENTE' || !t?.categoriaId); setTransacoesConciliadas(conciliadas); setTransacoesNaoCategorizadas(naoCategorizadas); @@ -144,7 +145,7 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { if (nivel === 0) { // Nível 0: Caixas (Agora usando a rota detalhada para cada caixa para matar o 'cruzamentos') - const sourceCaixas = filters.caixas || caixas; + const sourceCaixas = (filters && 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)...'); @@ -162,8 +163,8 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { return { ...c, - totalTransacoes: detailed.resumo?.total_transacoes || calcTransacoes, - totalValor: detailed.resumo?.valor_total || calcValor, + totalTransacoes: detailed.resumo?.total_transacoes || detailed.total_transacoes || calcTransacoes, + totalValor: detailed.valor_total || detailed.resumo?.valor_total || calcValor, tipo: 'caixa' }; } catch (e) { @@ -175,45 +176,58 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { } 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'); + // Se filters for um objeto com id (o item clicado), usamos ele. Senão, tentamos pegar do estado ou do objeto filters + const caixinhaId = filters?.id || filters?.caixinha?.id || filters?.caixa?.id || caixaSelecionado?.id; + + if (!caixinhaId) { + console.error('[useConciliacaoV2] Falha ao identificar ID da caixinha:', { filters, caixaSelecionado }); + 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); + // Preparamos as transações achatadas por precaução, mas agora usaremos subgrupos se existirem + const catTransacoes = cat.subgrupos + ? cat.subgrupos.flatMap(sub => sub.transacoes || []) + : (cat.transacoes || []); + + const totalTransacoes = cat.total_transacoes || catTransacoes.length; return { id: `cat_${cat.categoria}`, nome: cat.categoria || 'Sem Categoria', - totalTransacoes: subTransacoes, + totalTransacoes: totalTransacoes, totalValor: cat.valor_total || 0, tipo: 'categoria', - beneficiarios: cat.beneficiarios || [], + subgrupos: cat.subgrupos || [], + transacoes: catTransacoes, 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 || [] + // Nível 2: Subgrupos (Vindo da categoria selecionada) + // Se o filtro for a própria categoria (passada diretamente), usamos ela. Senão, pegamos do estado. + const categoriaItem = (filters?.subgrupos ? filters : filters?.categoria) || categoriaSelecionada; + const rawSubgrupos = categoriaItem?.subgrupos || []; + + data = rawSubgrupos.map((sub, idx) => ({ + id: `sub_${sub.nome_agrupamento}_${idx}`, + nome: sub.nome_agrupamento || 'Sem Identificação', + totalTransacoes: sub.total_transacoes || (sub.transacoes?.length || 0), + totalValor: sub.valor_total || 0, + tipo: 'subgrupo', + transacoes: sub.transacoes || [], + cor: '#94a3b8' // Slate 400 })); } 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 || []; + // Nível 3: Transações (Vindo do item selecionado - pode ser categoria ou subgrupo) + const itemPai = (filters?.transacoes ? filters : (filters?.subgrupo || subgroupSelecionado || filters?.categoria || categoriaSelecionada)); + const rawTransacoes = itemPai?.transacoes || []; data = rawTransacoes.map(t => ({ ...t, @@ -229,6 +243,7 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { return data; } catch (error) { console.error('[useConciliacaoV2] Erro ao buscar dados de navegação:', error); + setBackendNavData([]); // Limpar dados em caso de erro para evitar confusão visual toast.error('Erro ao navegar nos dados', 'Erro'); return []; } finally { @@ -240,50 +255,54 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { const navegarPara = async (tipo, item) => { console.log('[useConciliacaoV2] Navegando para:', tipo, item); + // Limpa dados atuais para mostrar o loading logo + setBackendNavData([]); + let novoNivel = 0; if (tipo === 'caixa') { setCaixaSelecionado(item); setCategoriaSelecionada(null); - setDetalheSelecionado(null); + setSubgroupSelecionado(null); novoNivel = 1; setCaminhoNavegacao([{ tipo: 'caixa', item }]); } else if (tipo === 'categoria') { setCategoriaSelecionada(item); - setDetalheSelecionado(null); + setSubgroupSelecionado(null); novoNivel = 2; setCaminhoNavegacao(prev => [...prev, { tipo: 'categoria', item }]); - } else if (tipo === 'regra') { - setDetalheSelecionado(item); + } else if (tipo === 'subgrupo') { + setSubgroupSelecionado(item); novoNivel = 3; - setCaminhoNavegacao(prev => [...prev, { tipo: 'regra', item }]); + setCaminhoNavegacao(prev => [...prev, { tipo: 'subgrupo', item }]); } setNivelNavegacao(novoNivel); - await fetchNivelNavegacao(novoNivel, { [tipo]: item }); + await fetchNivelNavegacao(novoNivel, item); }; const voltarNavegacao = async () => { if (nivelNavegacao === 0) return; const novoNivel = nivelNavegacao - 1; + + // Determina qual item passar como contexto para o filtro + let itemContexto = null; + if (novoNivel === 2) itemContexto = categoriaSelecionada; + if (novoNivel === 1) itemContexto = caixaSelecionado; + + await fetchNivelNavegacao(novoNivel, itemContexto); + + if (nivelNavegacao === 3) setSubgroupSelecionado(null); + if (nivelNavegacao === 2) setCategoriaSelecionada(null); + if (nivelNavegacao === 1) setCaixaSelecionado(null); + 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 navegarParaFrente = navegarPara; + const navegarParaTras = voltarNavegacao; + // Carregar dados iniciais do backend useEffect(() => { console.log('[useConciliacaoV2] useEffect executado - montando componente'); @@ -381,10 +400,10 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { // Buscar transações apenas se for necessário ou se estiver na aba correta (Lazy Loading) if (initialSubView === 'extrato-completo' || initialSubView === 'nao-categorizadas') { console.log('[useConciliacaoV2] Buscando extrato completo para visualização específica...'); - const extrato = await workspaceConciliacaoService.fetchExtrato(); - setExtratoCompleto(Array.isArray(extrato) ? extrato : []); - const conciliadas = extrato.filter(t => t.status === 'CONCILIADA' && t.categoriaId); - const naoCategorizadas = extrato.filter(t => t.status === 'PENDENTE' || !t.categoriaId); + const extrato = (await workspaceConciliacaoService.fetchExtrato({ mes: filtroMes, ano: filtroAno })) || []; + setExtratoCompleto(extrato); + const conciliadas = extrato.filter(t => t?.status === 'CONCILIADA' && t?.categoriaId); + const naoCategorizadas = extrato.filter(t => t?.status === 'PENDENTE' || !t?.categoriaId); setTransacoesConciliadas(conciliadas); setTransacoesNaoCategorizadas(naoCategorizadas); } @@ -439,12 +458,12 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { if (!isLoading && hasInitialLoad) { const loadExtrato = async () => { try { - console.log('[useConciliacaoV2] Recarregando extrato devido a mudança de filtros...'); - const extrato = await workspaceConciliacaoService.fetchExtrato(); - setExtratoCompleto(Array.isArray(extrato) ? extrato : []); + console.log(`[useConciliacaoV2] Recarregando extrato devido a mudança de ${activeSubView === 'nao-categorizadas' ? 'aba' : 'filtros'}...`); + const extrato = (await workspaceConciliacaoService.fetchExtrato({ mes: filtroMes, ano: filtroAno })) || []; + setExtratoCompleto(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); + const conciliadas = extrato.filter(t => t?.status === 'CONCILIADA' && t?.categoriaId); + const naoCategorizadas = extrato.filter(t => t?.status === 'PENDENTE' || !t?.categoriaId); setTransacoesConciliadas(conciliadas); setTransacoesNaoCategorizadas(naoCategorizadas); @@ -457,7 +476,7 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { } else if (!isLoading && !hasInitialLoad) { setHasInitialLoad(true); } - }, [filtroAno, filtroMes, isLoading, hasInitialLoad]); + }, [filtroAno, filtroMes, activeSubView, isLoading, hasInitialLoad]); // Transações filtradas e organizadas (MANTIDO APENAS SE NECESSÁRIO PARA COMPATIBILIDADE, MAS AGORA REDUNDANTE) const transacoesOrganizadas = useMemo(() => { @@ -842,6 +861,7 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { regra: '0', beneficiario_pagador: dadosCategorizacao.beneficiario || '', caixinha: String(dadosCategorizacao.caixaId || '0'), + tag: dadosCategorizacao.tag || '', Resutl: true }; @@ -858,7 +878,8 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { url: '/caixinha_extrato/inserir', method: 'UPDATE', data: objeto - }) + }), + dadosCategorizacao.tag ? api.post('/tag/inserir', { idextrato: transacaoId, tag: dadosCategorizacao.tag }) : Promise.resolve() ]); // Recarregar extrato do backend @@ -939,7 +960,7 @@ export function useConciliacaoV2(defaultView = 'conciliadas') { caminhoNavegacao, caixaSelecionado, categoriaSelecionada, - detalheSelecionado, + subgroupSelecionado, isLoading, isNavLoading, backendNavData, diff --git a/src/features/financeiro-v2/hooks/useContasPagar.js b/src/features/financeiro-v2/hooks/useContasPagar.js index b3de996..2e2bbdc 100644 --- a/src/features/financeiro-v2/hooks/useContasPagar.js +++ b/src/features/financeiro-v2/hooks/useContasPagar.js @@ -16,6 +16,12 @@ export const useContasPagar = () => { const [activeSubView, setActiveSubView] = useState('default'); // 'default' | 'fornecedores' | 'despesas' | 'cruzamento' const [searchTerm, setSearchTerm] = useState(''); const [selectedIds, setSelectedIds] = useState([]); + + // Filtros de busca (Mês/Ano) + const [filtroMes, setFiltroMes] = useState(String(new Date().getMonth() + 1)); + const [filtroAno, setFiltroAno] = useState(String(new Date().getFullYear())); + const [filtroTipoPeriodo, setFiltroTipoPeriodo] = useState('mes'); // 'mes' | 'ano' | 'todos' + const [filtroTipoCobranca, setFiltroTipoCobranca] = useState('todas'); // 'todas' | 'Recorrente' | 'Avulsa' // Estados para Fornecedores const [fornecedores, setFornecedores] = useState([]); @@ -59,7 +65,19 @@ export const useContasPagar = () => { const fetchDespesas = useCallback(async () => { setDespesasLoading(true); try { - const data = await workspaceDespesasV2Service.fetchDespesas(); + const filters = {}; + if (filtroTipoPeriodo === 'mes') { + filters.mes = filtroMes; + filters.ano = filtroAno; + } else if (filtroTipoPeriodo === 'ano') { + filters.ano = filtroAno; + } + + if (filtroTipoCobranca !== 'todas') { + filters.tipo_cobranca = filtroTipoCobranca; + } + + const data = await workspaceDespesasV2Service.fetchDespesas(filters); const list = data || []; setDespesas(list); @@ -81,7 +99,7 @@ export const useContasPagar = () => { } finally { setDespesasLoading(false); } - }, [activeSubView]); + }, [activeSubView, filtroMes, filtroAno, filtroTipoPeriodo, filtroTipoCobranca]); // Dispara a busca quando a sub-view muda ou na montagem useEffect(() => { @@ -218,8 +236,8 @@ export const useContasPagar = () => { // Ações para Despesas V2 - Memoizadas const createDespesa = useCallback(async (despesaData) => { - if (!despesaData.data || !despesaData.contaDespesa || !despesaData.montante || !despesaData.status) { - toastRef.current.error('Por favor, preencha os campos obrigatórios'); + if (!despesaData.montante || !despesaData.categoria) { + toastRef.current.error('Por favor, preencha os campos obrigatórios (Montante e Categoria)'); return null; } @@ -243,6 +261,13 @@ export const useContasPagar = () => { const result = await workspaceDespesasV2Service.updateDespesa(payload); if (result) { toastRef.current.success('Despesa atualizada com sucesso!'); + + // Atualiza o estado da despesa selecionada para refletir as mudanças no painel lateral + setSelectedDespesa(prev => (prev && (prev.idDespesa === id || prev.id === id)) + ? { ...prev, ...despesaData } + : prev + ); + fetchDespesas(); return result; } @@ -337,9 +362,14 @@ export const useContasPagar = () => { cruzamentoLoading, categorias, caixinhas, - kpisCruzamento + kpisCruzamento, + // Filtros expostos + filtroMes, + filtroAno, + filtroTipoPeriodo, + filtroTipoCobranca }, - actions: { + actions: useMemo(() => ({ setActiveTab, setActiveSubView, setSearchTerm, @@ -358,7 +388,18 @@ export const useContasPagar = () => { deleteItemDespesa, // Cruzamento setDespesasPlanejadas, - setDespesasExecutadas - } + setDespesasExecutadas, + // Setters de filtros + setFiltroMes, + setFiltroAno, + setFiltroTipoPeriodo, + setFiltroTipoCobranca + }), [ + setActiveTab, setActiveSubView, setSearchTerm, fetchDespesas, fetchFornecedores, + setSelectedFornecedor, createDespesa, updateDespesa, deleteDespesa, + setSelectedDespesa, createItemDespesa, updateItemDespesa, deleteItemDespesa, + setDespesasPlanejadas, setDespesasExecutadas, setFiltroMes, setFiltroAno, + setFiltroTipoPeriodo, setFiltroTipoCobranca + ]) }; }; diff --git a/src/features/financeiro-v2/hooks/useDashboard.js b/src/features/financeiro-v2/hooks/useDashboard.js index d5e548f..2e37965 100644 --- a/src/features/financeiro-v2/hooks/useDashboard.js +++ b/src/features/financeiro-v2/hooks/useDashboard.js @@ -4,6 +4,7 @@ import { formatDateForChart } from '../utils/dateUtils'; import { boletosService } from '@/services/boletosService'; import { workspaceDespesasService } from '@/services/workspaceDespesasService'; import { workspaceSaldoService } from '@/services/workspaceSaldoService'; +import { parseDateInfo } from '@/utils/dateUtils'; /** @@ -47,12 +48,20 @@ export const useDashboard = () => { }); // Função auxiliar para formatar data no formato "YYYY-MM" - const formatMesAno = (date) => { - if (!date) return ''; - const d = new Date(date); - if (isNaN(d.getTime())) return ''; - const ano = d.getFullYear(); - const mes = String(d.getMonth() + 1).padStart(2, '0'); + const formatMesAno = (dateOrStr) => { + if (!dateOrStr) return ''; + let ano, mes; + if (typeof dateOrStr === 'string' && dateOrStr.trim().length > 0) { + const { year, month } = parseDateInfo(dateOrStr); + if (!year) return ''; + ano = year; + mes = String(month).padStart(2, '0'); + } else { + const d = dateOrStr instanceof Date ? dateOrStr : new Date(dateOrStr); + if (isNaN(d.getTime())) return ''; + ano = d.getFullYear(); + mes = String(d.getMonth() + 1).padStart(2, '0'); + } return `${ano}-${mes}`; }; @@ -142,13 +151,10 @@ export const useDashboard = () => { let transacoesPendentesCnt = 0; extrato.forEach((item) => { - const itemMes = formatMesAno(new Date(item.dataEntrada)); + const itemMes = formatMesAno(item.dataEntrada); - // Pendentes de conciliação (Contagem global ou mensal? - // Geralmente pendências são globais, mas para bater com "Transações do Mês" talvez filtrar. - // O usuário pediu "entradas e saídas do mês vigente". Pendências vou manter geral ou filtrar? - // Vou filtrar apenas as SOMAS financeiras pelo mês atual conforme solicitado. - if (!item.categoria || item.categoria == 0) { + // Pendentes de conciliação do Mês + if ((!item.categoria || item.categoria == 0) && itemMes === mesAtualStr) { transacoesPendentesCnt++; } @@ -203,7 +209,7 @@ export const useDashboard = () => { const cobranca = item.cobranca || item; if (!cobranca.dataVencimento) return; - const vencMes = formatMesAno(new Date(cobranca.dataVencimento)); + const vencMes = formatMesAno(cobranca.dataVencimento); if (vencMes === mesAtualStr && statusesPendentes.includes(cobranca.situacao)) { qtdPendencia++; const valTotal = safeNumber(cobranca.valorTotalRecebido); @@ -292,7 +298,7 @@ export const useDashboard = () => { data.boletos.cobrancas?.forEach((item) => { const c = item.cobranca || item; - if (formatMesAno(new Date(c.dataVencimento)) !== mesAtual) return; + if (formatMesAno(c.dataVencimento) !== mesAtual) return; const st = c.situacao || 'OUTROS'; const valor = safeNumber(c.valorNominal); @@ -301,7 +307,7 @@ export const useDashboard = () => { }); const colors = { - 'PAGO': '#10b981', 'RECEBIDO': '#10b981', 'MARCADO_RECEBIDO': '#10b981', + 'PAGO': '#10b981', 'RECEBIDO': '#10b981', 'MARCADO_RECEBIDO': '#f59e0b', 'A_RECEBER': '#3b82f6', 'ATRASADO': '#f43f5e', 'CANCELADO': '#64748b', @@ -381,14 +387,20 @@ export const useDashboard = () => { return [...data.extrato] .filter(item => { if (!item.dataEntrada) return false; - const dataItem = new Date(item.dataEntrada); - // Remove hora para comparação justa de data - const dataItemSemHora = new Date(dataItem); + + const { day, month, year } = parseDateInfo(item.dataEntrada); + if (!year) return false; + + const dataItemSemHora = new Date(year, month - 1, day); dataItemSemHora.setHours(0, 0, 0, 0); return dataItemSemHora >= dataLimite && dataItemSemHora <= hoje; }) - .sort((a, b) => new Date(b.dataEntrada) - new Date(a.dataEntrada)) + .sort((a, b) => { + const pA = parseDateInfo(a.dataEntrada); + const pB = parseDateInfo(b.dataEntrada); + return new Date(pB.year, pB.month - 1, pB.day) - new Date(pA.year, pA.month - 1, pA.day); + }) .slice(0, limit) .map(item => { // Enriquecer com nome da categoria @@ -417,8 +429,8 @@ export const useDashboard = () => { const allExtrato = await extratoService.fetchExtrato(); return allExtrato.filter(item => { if (!item.dataEntrada) return false; - const d = new Date(item.dataEntrada); - return (d.getMonth() + 1) === mes && d.getFullYear() === ano && item.tipoOperacao === 'C'; + const { month: m, year: y } = parseDateInfo(item.dataEntrada); + return m === mes && y === ano && item.tipoOperacao === 'C'; }); } catch (err) { console.error('Error fetching entradas mes from extrato:', err); @@ -430,17 +442,17 @@ export const useDashboard = () => { const allExtrato = await extratoService.fetchExtrato(); return allExtrato.filter(item => { if (!item.dataEntrada) return false; - const d = new Date(item.dataEntrada); - return (d.getMonth() + 1) === mes && d.getFullYear() === ano && item.tipoOperacao === 'D'; + const { month: m, year: y } = parseDateInfo(item.dataEntrada); + return m === mes && y === ano && item.tipoOperacao === 'D'; }); } catch (err) { console.error('Error fetching saidas periodo from extrato:', err); return []; } }, - fetchTransacoesNaoConciliadas: async () => { + fetchTransacoesNaoConciliadas: async (mes, ano) => { try { - const allExtrato = await extratoService.fetchExtrato(); + const allExtrato = await extratoService.fetchExtrato({ mes, ano }); return allExtrato.filter(item => !item.categoria || item.categoria == 0); } catch (err) { return []; diff --git a/src/features/financeiro-v2/hooks/useFluxoCaixa.js b/src/features/financeiro-v2/hooks/useFluxoCaixa.js index 427bf8f..44ca2c9 100644 --- a/src/features/financeiro-v2/hooks/useFluxoCaixa.js +++ b/src/features/financeiro-v2/hooks/useFluxoCaixa.js @@ -79,7 +79,13 @@ export function useFluxoCaixa() { let d; if (typeof itemDate === 'string') { if (itemDate.includes('/')) { - const [day, month, year] = itemDate.split('/'); + const parts = itemDate.split('/'); + const day = parts[0]; + const month = parts[1]; + // Handle if there's a time after the year: "2026 11:30" + const yearPart = parts[2] || ''; + const year = yearPart.split(' ')[0]; + d = new Date(year, month - 1, day); } else { d = new Date(itemDate); @@ -88,7 +94,7 @@ export function useFluxoCaixa() { d = new Date(itemDate); } - if (Number.isNaN(d.getTime())) return true; + if (!d || Number.isNaN(d.getTime())) return false; const itemMonth = String(d.getMonth() + 1); const itemYear = String(d.getFullYear()); diff --git a/src/features/financeiro-v2/utils/dateUtils.js b/src/features/financeiro-v2/utils/dateUtils.js index ce2336f..4e54a38 100644 --- a/src/features/financeiro-v2/utils/dateUtils.js +++ b/src/features/financeiro-v2/utils/dateUtils.js @@ -172,3 +172,59 @@ export const formatDateForChart = (dateString) => { return '-'; } }; +/** + * Formata o status do boleto para exibição amigável + * @param {string} status + * @returns {string} + */ +export const formatStatus = (status) => { + if (!status) return 'Pendente'; + const s = String(status).toUpperCase(); + const map = { + 'A_RECEBER': 'A Receber', + 'PENDENTE': 'Pendente', + 'ATRASADO': 'Atrasado', + 'VENCIDO': 'Vencido', + 'PAGO': 'Pago', + 'RECEBIDO': 'Recebido', + 'CANCELADO': 'Cancelado', + 'MARCADO_RECEBIDO': 'Marcado Recebido', + 'PIX': 'Pix' + }; + return map[s] || status; +}; + +/** + * Converte qualquer formato de data válido para YYYY-MM-DD. + * Útil para campos + * @param {string|Date} dateString + * @returns {string} YYYY-MM-DD + */ +export const toISODate = (dateString) => { + if (!dateString) return ''; + + // Se já estiver no formato ISO YYYY-MM-DD, retorna o início da string + if (typeof dateString === 'string' && /^\d{4}-\d{2}-\d{2}/.test(dateString)) { + return dateString.substring(0, 10); + } + + // Se estiver no formato brasileiro DD/MM/YYYY + if (typeof dateString === 'string' && /^\d{2}\/\d{2}\/\d{4}/.test(dateString)) { + const [day, month, year] = dateString.split('/'); + return `${year}-${month}-${day}`; + } + + try { + const date = new Date(dateString); + if (isNaN(date.getTime())) return ''; + + // Usamos UTC para evitar problemas de fuso horário vindo do backend + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; + } catch { + return ''; + } +}; diff --git a/src/features/financeiro-v2/views/DashboardView.jsx b/src/features/financeiro-v2/views/DashboardView.jsx index e17d189..65361fb 100644 --- a/src/features/financeiro-v2/views/DashboardView.jsx +++ b/src/features/financeiro-v2/views/DashboardView.jsx @@ -48,6 +48,7 @@ import { import { useDashboard } from '../hooks/useDashboard'; import { TransactionDetailModal } from '../components/TransactionDetailModal'; import { formatDate, formatCurrency } from '../utils/dateUtils'; +import { parseDateInfo } from '@/utils/dateUtils'; import { Select, SelectContent, @@ -63,7 +64,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; 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); - // Componente de card de resumo premium + // Premium KPI Card with Glow and Glassmorphism - Dashboard Version const XPICard = ({ title, value, @@ -82,49 +83,57 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; }; return ( -
-
-
-
- {loading ? ( - - ) : ( - + {/* Animated Glow Effect */} +
+ + +
+

{title}

+ + {loading ? ( +
+ ) : ( +

+ {value} +

+ )} + +
+ {trend && !loading && ( +
0 ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-rose-500/10 text-rose-600 dark:text-rose-400" + )}> + {trend > 0 ? : } + {Math.abs(trend)}% +
)} +

{subtitle}

- {trend && !loading && ( -
0 ? "bg-emerald-500/10 text-emerald-500" : "bg-rose-400/10 text-rose-400" - )}> - {trend > 0 ? : } - {Math.abs(trend)}% -
+
+ +
+ {loading ? ( + + ) : ( + )}
- -
- {title} -
- {loading ? ( -
- ) : ( - value - )} -
-

{subtitle}

-
-
- - {/* Subtle Gradient Shadow */} -
-
+ + ); }; @@ -206,7 +215,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; data = await fetchSaidasPeriodo(mes, ano); break; case 'naoConciliadas': - data = await fetchTransacoesNaoConciliadas(); // Usually global, but could filter if APIs allowed + data = await fetchTransacoesNaoConciliadas(mes, ano); break; case 'boletosAbertos': data = await fetchBoletosAbertos(); // Global @@ -310,7 +319,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
{/* KPI Grid */} -
+
) : ( - - - + + + `R$ ${(val/1000).toFixed(0)}k`} - dx={-5} /> } /> @@ -601,7 +608,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; {t.tipoTransacao || 'GERAL'} - {formatDate(t.dataEntrada)} + {t.dataEntrada}
{(t.beneficiario_pagador) && ( @@ -653,13 +660,15 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; onViewInExtrato={(transaction) => { if (transaction.tipoOperacao === 'C') { // Open Entradas modal - const date = new Date(transaction.dataEntrada); + const parsed = parseDateInfo(transaction.dataEntrada); + const date = new Date(parsed.year || new Date().getFullYear(), (parsed.month || new Date().getMonth() + 1) - 1, parsed.day || 1); setModalDate(date); handleModalDateChange('saldo', date); setModalState(prev => ({ ...prev, saldo: true })); } else { // Open Saídas modal - const date = new Date(transaction.dataEntrada); + const parsed = parseDateInfo(transaction.dataEntrada); + const date = new Date(parsed.year || new Date().getFullYear(), (parsed.month || new Date().getMonth() + 1) - 1, parsed.day || 1); setModalDate(date); handleModalDateChange('despesas', date); setModalState(prev => ({ ...prev, despesas: true })); @@ -715,7 +724,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; ) }, { header: 'Data', field: 'dataEntrada', width: 120, render: (row) => ( - {formatDate(row.dataEntrada)} + {row.dataEntrada} )}, { header: 'Descrição', field: 'descricao', width: 250, render: (row) => ( {row.descricao || 'Sem descrição'} @@ -788,7 +797,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; ) }, { header: 'Data', field: 'dataEntrada', width: 120, render: (row) => ( - {formatDate(row.dataEntrada)} + {row.dataEntrada} )}, { header: 'Favorecido', field: 'beneficiario_pagador', width: 200, render: (row) => ( {row.beneficiario_pagador || 'N/A'} @@ -819,14 +828,32 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; {/* Modal - Transações Não Conciliadas */} setModalState(prev => ({ ...prev, naoConciliadas: open }))}> - - - - Transações Pendentes de Conciliação - - - Movimentações do extrato que ainda não foram categorizadas ou vinculadas a um lançamento. - + +
+ + + Transações Pendentes de Conciliação + + + Movimentações do extrato que ainda não foram categorizadas ou vinculadas a um lançamento no mês selecionado. + +
+
+ Período: + { + if (e.target.value) { + const [y, m] = e.target.value.split('-').map(Number); + const newDate = new Date(y, m - 1); + setModalDate(newDate); + handleModalDateChange('naoConciliadas', newDate); + } + }} + /> +
{ + if (!text) return ''; + if (text.length <= limit) return text; + return text.substring(0, limit) + '...'; + }; + // Estado local para exportação const [isExporting, setIsExporting] = React.useState(false); @@ -215,21 +222,13 @@ export function TransacoesConciliadasView({ state, actions }) { name: caixa.nome, value: Math.abs(caixa.totalValor || 0) })); - } else if (nivelNavegacao === 1) { - // Barras: Distribuição por Categoria - return dadosNivelAtual.map(cat => ({ - name: cat.nome || 'Sem Categoria', - value: Math.abs(cat.totalValor || 0), - transacoes: cat.totalTransacoes || 0, - cor: cat.cor || '#3b82f6' - })); - } else if (nivelNavegacao === 2) { - // Barras: Distribuição por Beneficiário - return dadosNivelAtual.map(ben => ({ - name: ben.nome || 'Sem Beneficiário', - value: Math.abs(ben.totalValor || 0), - transacoes: ben.totalTransacoes || 0, - cor: '#8b5cf6' + } else if (nivelNavegacao === 1 || nivelNavegacao === 2) { + // Barras: Distribuição por Categoria (Nível 1) ou Subgrupo (Nível 2) + return dadosNivelAtual.map(item => ({ + name: item.nome || 'Sem Identificação', + value: Math.abs(item.totalValor || 0), + transacoes: item.totalTransacoes || 0, + cor: item.cor || (nivelNavegacao === 2 ? '#94a3b8' : '#3b82f6') })); } else if (nivelNavegacao === 3) { // Timeline: Transações individuais @@ -308,45 +307,74 @@ export function TransacoesConciliadasView({ state, actions }) { ); - } else if (nivelNavegacao === 1) { - // Gráfico de Barras para Categorias + } else if (nivelNavegacao === 1 || nivelNavegacao === 2) { + // Gráfico de Barras para Categorias (Vertical) ou Subgrupos (Horizontal) + const isSubgroup = nivelNavegacao === 2; + return ( - Distribuição por Categoria + Distribuição por {isSubgroup ? 'Subgrupo' : 'Categoria'}
- - - - `R$ ${val/1000}k`} - /> + + + {isSubgroup ? ( + <> + + truncateLabel(val, 20)} + /> + + ) : ( + <> + truncateLabel(val, 15)} + /> + `R$ ${val/1000}k`} + /> + + )} } cursor={{ fill: 'rgba(0,0,0,0.05)', opacity: 0.1 }} /> {dadosGrafico.map((entry, index) => ( @@ -358,54 +386,6 @@ export function TransacoesConciliadasView({ state, actions }) { ); - } else if (nivelNavegacao === 2) { - // Gráfico de Barras Horizontal para Beneficiários/Pagadores - return ( - - - - - Distribuição por Beneficiário/Pagador - - - -
- - - - `R$ ${val/1000}k`} - /> - truncateLegend(value, 45)} - /> - } - cursor={{ fill: 'rgba(0,0,0,0.05)', opacity: 0.1 }} - /> - - - -
-
-
- ); } else if (nivelNavegacao === 3) { // Gráfico de Linha/Timeline para Transações return ( @@ -585,50 +565,54 @@ export function TransacoesConciliadasView({ state, actions }) {
); } else if (nivelNavegacao === 2) { - // Lista de Beneficiários/Pagadores da Categoria selecionada - const tabelaHeightReg = dadosGrafico.length > 0 ? 'h-[400px]' : 'h-[600px]'; + // Visão de Subgrupos (Agrupamentos dentro da Categoria) + const tabelaHeight = dadosGrafico.length > 0 ? 'h-[400px]' : 'h-[600px]'; return ( -
+
( -
-
- -
-

{row.beneficiario || 'Sem Beneficiário'}

+
+ + + {truncateLabel(row.nome || 'Sem Categoria', 40)} +
) }, { field: 'totalTransacoes', - header: 'Transações', - width: '120px', + header: 'Lançamentos', + width: '150px', render: (row) => ( - - {row.totalTransacoes || 0} + + {row.totalTransacoes} transações ) }, { field: 'totalValor', header: 'Valor Total', - width: '150px', + width: '200px', + className: 'text-right', render: (row) => ( - + = 0 ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400" + )}> {formatCurrency(row.totalValor || 0)} ) } ]} - onRowClick={(row) => actions.navegarPara('regra', row)} + onRowClick={(row) => actions.navegarPara('subgrupo', row)} rowKey="id" - pageSize={25} + pageSize={20} />
); @@ -644,7 +628,7 @@ export function TransacoesConciliadasView({ state, actions }) { field: 'data', header: 'Data', width: '120px', - render: (row) => formatDate(row.data || row.dataEntrada || '') + render: (row) => row.data || row.dataEntrada || '' }, { field: 'descricao', @@ -732,8 +716,8 @@ export function TransacoesConciliadasView({ state, actions }) { Transações Conciliadas
- {nivelNavegacao > 0 && ( -
+ {nivelNavegacao > 0 && ( +
-
+ +
{caminhoNavegacao.map((item, idx) => ( - {item.item.nome || item.item.beneficiario} - {idx < caminhoNavegacao.length - 1 && } +
+ {item.tipo} + {item.item.nome || item.item.beneficiario} +
+ {idx < caminhoNavegacao.length - 1 && }
))} + {nivelNavegacao === 3 && ( + <> + +
+ Visão + Lançamentos +
+ + )}
)} diff --git a/src/features/financeiro-v2/views/conciliacao/components/Forms.jsx b/src/features/financeiro-v2/views/conciliacao/components/Forms.jsx index 09ccef0..059f1f6 100644 --- a/src/features/financeiro-v2/views/conciliacao/components/Forms.jsx +++ b/src/features/financeiro-v2/views/conciliacao/components/Forms.jsx @@ -193,7 +193,8 @@ export const ReconciliationForm = ({ transaction, categories = [], caixinhas = [ beneficiario_pagador: '', descricao: '', dataEntrada: '', - valor: '' + valor: '', + tag: '' }); useEffect(() => { @@ -206,7 +207,8 @@ export const ReconciliationForm = ({ transaction, categories = [], caixinhas = [ beneficiario_pagador: transaction.raw?.beneficiario_pagador || '', descricao: transaction.description || '', dataEntrada: transaction.date?.toLocaleDateString('pt-BR') || '', - valor: new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(transaction.amount) + valor: new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(transaction.amount), + tag: transaction.tag || transaction.raw?.tag || '' }; console.log('[ReconciliationForm] FormData atualizado:', newFormData); setFormData(newFormData); @@ -313,6 +315,19 @@ export const ReconciliationForm = ({ transaction, categories = [], caixinhas = [ />
+
+ + +
+
+ + {/* Filtros Padronizados */} +
+
+ + + {state.filtroTipoPeriodo === 'mes' && ( + <> + + + + )} + + {state.filtroTipoPeriodo === 'ano' && ( + + )} + +
+ + + +
+ + +
+ +
+ + setSearchTerm(e.target.value)} + className="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 pl-9 h-9 text-xs text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-600 focus:border-rose-500/50 rounded-lg w-full" + /> +
+
@@ -354,9 +514,19 @@ export const DespesasView = () => { -
+

Detalhes da Despesa

- {getStatusBadge(state.selectedDespesa.status)} +
+ {getStatusBadge(state.selectedDespesa.status)} + + {state.selectedDespesa.situacao || 'Ativo'} + +
@@ -368,7 +538,7 @@ export const DespesasView = () => { > Editar - + {/* */}
- -
-
-
- -
- -

- {state.selectedDespesa.metodoPagamento || state.selectedDespesa.pagoPorMeioDe || 'N/D'} -

-
-
-
- -

{state.selectedDespesa.numeroReferencia || 'N/D'}

-
-
- -
- -

{state.selectedDespesa.nomeFornecedor || 'N/D'}

-
-
-
- -

{state.selectedDespesa.nomeCliente || 'N/D'}

-
+ + +
+ + Geral + Financeiro + Datas + Diário +
- {/* Diário de Lançamentos */} -
-
+
+ +
+
+ +

{state.selectedDespesa.contaDespesa || 'N/D'}

+
+
+ +

#{state.selectedDespesa.idDespesa || 'N/D'}

+
+
+ +
+ +

{state.selectedDespesa.nomeFornecedor || 'N/D'}

+
+
+
+ +

{state.selectedDespesa.nomeCliente || 'N/D'}

+
+
+ +

{state.selectedDespesa.tipo_cobranca || 'N/D'}

+
+
+ +
+ "{state.selectedDespesa.descricao || 'Nenhuma descrição informada.'}" +
+
+
+
+ + +
+
+ +

+ {formatCurrency(state.selectedDespesa.montante)} +

+
+
+ +

+ {state.selectedDespesa.montante_real_pago ? formatCurrency(state.selectedDespesa.montante_real_pago) : 'N/D'} +

+
+
+ +

+ {categoriasOptions.find(c => String(c.id) === String(state.selectedDespesa.categoria))?.name || state.selectedDespesa.categoria || 'Sem Categoria'} +

+
+
+ +
+ +

+ {state.selectedDespesa.metodoPagamento || 'N/D'} +

+
+
+
+ +

+ {state.selectedDespesa.pagoPorMeioDe || 'N/D'} +

+
+
+ +

{state.selectedDespesa.idregra || 'N/D'}

+
+
+
+ + +
+
+ +

{formatDate(state.selectedDespesa.data) || 'N/D'}

+
+
+ +

{formatDate(state.selectedDespesa.data_pagamento) || 'N/D'}

+
+
+ +

{state.selectedDespesa.vencimento || 'N/D'}

+
+
+ +

{state.selectedDespesa.numeroReferencia || 'N/D'}

+
+
+
+ + {/* Diário de Lançamentos */}
-

Diário de Lançamentos

-

Lançamentos contábeis vinculados

-
- -
+
+
+

Diário de Lançamentos

+

Lançamentos contábeis vinculados

+
+ +
- {state.itensDespesaLoading ? ( -
- + {state.itensDespesaLoading ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + + {(state.selectedDespesa.diario || []).map((item) => ( + + + + + + + ))} + {(!state.selectedDespesa.diario || state.selectedDespesa.diario.length === 0) && ( + + + + )} + + + + + + + + + +
CONTADÉBITOCRÉDITO
{item.conta}{formatCurrency(item.debito)}{formatCurrency(item.credito)} +
+ + +
+
Nenhum lançamento no diário.
Total Geral (Backend) + {formatCurrency(state.totalItensDespesa.debito)} + + {formatCurrency(state.totalItensDespesa.credito)} +
+
+ )}
- ) : ( -
- - - - - - - - - - - {(state.selectedDespesa.diario || []).map((item) => ( - - - - - - - ))} - {(!state.selectedDespesa.diario || state.selectedDespesa.diario.length === 0) && ( - - - - )} - - - - - - - - - -
CONTADÉBITOCRÉDITO
{item.conta}{formatCurrency(item.debito)}{formatCurrency(item.credito)} -
- - -
-
Nenhum lançamento no diário.
Total Geral (Backend) - {formatCurrency(state.totalItensDespesa.debito)} - - {formatCurrency(state.totalItensDespesa.credito)} -
-
- )} +
+ {/* Upload de Recibos */} {/*
@@ -507,7 +765,6 @@ export const DespesasView = () => {
*/} -
@@ -524,19 +781,22 @@ export const DespesasView = () => {
{editingDespesa ? 'Editar Registro' : 'Novo Registro de Despesa'} + + Preencha os dados para organizar seu contas a pagar +
-
- + {/*
+ setFormData({ ...formData, data: e.target.value })} className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-medium transition-all focus:ring-2 focus:ring-rose-500/10 focus:border-rose-500" /> -
+
*/}
@@ -548,10 +808,20 @@ export const DespesasView = () => { />
+
+ + setFormData({ ...formData, nomeCliente: e.target.value })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4" + /> +
+
{ {categoriasOptions.map(cat => ( - {cat.name} + {cat.name} ))} @@ -608,6 +878,26 @@ export const DespesasView = () => { />
+
+ + +
+
{ />
-
+ {/*
+
*/} + +
+ +
- - setFormData({ ...formData, nomeCliente: e.target.value })} - className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4" - /> + +
+
+ + {formData.tipo_cobranca === 'Recorrente' ? ( + setFormData({ ...formData, vencimento: e.target.value })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-medium" + /> + ) : ( + setFormData({ ...formData, vencimento: e.target.value })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-medium" + /> + )} +
+ + {/*
+ + setFormData({ ...formData, data_lancamento: e.target.value })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-medium" + /> +
*/} + + {/*
+ + setFormData({ ...formData, vencimento: e.target.value })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-medium" + /> +
*/} +