diff --git a/.agent/history.json b/.agent/history.json index d0bbd14..a8059ad 100644 --- a/.agent/history.json +++ b/.agent/history.json @@ -25,5 +25,10 @@ "feature": "Corre\u00e7\u00e3o nas rotas de edi\u00e7\u00e3o de Bases e Respons\u00e1veis do GR", "status": "active", "timestamp": "2026-02-08" + }, + { + "feature": "Corrigido erro nos cards de Pend\u00eancias/Revis\u00e3o do GR para Despachantes", + "status": "active", + "timestamp": "2026-02-08" } ] \ No newline at end of file diff --git a/refactor_gr_service.py b/refactor_gr_service.py new file mode 100644 index 0000000..e80e000 --- /dev/null +++ b/refactor_gr_service.py @@ -0,0 +1,40 @@ +import re +import sys + +def refactor_file(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Pattern to find: + # try { + # const response = await ... + # return ... + # } catch (error) { + # throw new Error(...) or throw error + # } + + # This regex matches the try/catch block and replaces it with the inner code + # It accounts for indentation and multi-line content + pattern = r'try\s*\{([\s\S]*?)\}\s*catch\s*\(error\)\s*\{[\s\S]*?\}' + + def replacement(match): + inner_code = match.group(1) + # Strip one level of indentation (usually 2 spaces) + lines = inner_code.split('\n') + new_lines = [] + for line in lines: + if line.startswith(' '): + new_lines.append(line[2:]) + elif line.startswith(' '): + new_lines.append(line[2:]) + else: + new_lines.append(line) + return '\n'.join(new_lines).strip() + + new_content = re.sub(pattern, replacement, content) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + +if __name__ == "__main__": + refactor_file(sys.argv[1]) diff --git a/src/components/shared/AutoFillInput.jsx b/src/components/shared/AutoFillInput.jsx index 3a900af..6e74e13 100644 --- a/src/components/shared/AutoFillInput.jsx +++ b/src/components/shared/AutoFillInput.jsx @@ -24,6 +24,7 @@ export const AutoFillInput = ({ displayField = "name", onSelect, onAddNew, + addNewLabel = "Criar novo", className, icon: Icon = Search }) => { @@ -160,7 +161,7 @@ export const AutoFillInput = ({ className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs font-black uppercase tracking-tighter text-rose-500 hover:bg-rose-500/10 transition-colors" > - Criar novo serviço: "{query}" + {addNewLabel}: "{query}" diff --git a/src/features/financeiro-v2/components/BoletoCreationDialog.jsx b/src/features/financeiro-v2/components/BoletoCreationDialog.jsx index b2c1e15..1c045e2 100644 --- a/src/features/financeiro-v2/components/BoletoCreationDialog.jsx +++ b/src/features/financeiro-v2/components/BoletoCreationDialog.jsx @@ -87,9 +87,9 @@ export const BoletoCreationDialog = ({ return ( - + - + Gerar {creationMode === 'AVULSO' ? 'Boleto Avulso' : creationMode === 'MENSAL' ? 'Boleto Mensal' : 'Agendamento'} @@ -113,7 +113,7 @@ export const BoletoCreationDialog = ({ type="date" value={formData.data_vencimento} onChange={(e) => setFormData({...formData, data_vencimento: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-12 text-lg text-center" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-12 text-lg text-center" /> @@ -132,18 +132,18 @@ export const BoletoCreationDialog = ({ setShowsEmpresaSuggestions(true); }} onFocus={() => setShowsEmpresaSuggestions(true)} - className="bg-slate-800/50 border-slate-700 text-white pl-10 focus:border-blue-500 transition-all h-10 text-sm" + className="bg-white dark:bg-slate-800/50 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white pl-10 focus:border-blue-500 transition-all h-10 text-sm" /> {showsEmpresaSuggestions && filteredEmpresasSuggestions.length > 0 && ( - + {filteredEmpresasSuggestions.map((empresa, idx) => (
handleSelectEmpresa(empresa)} > -

{empresa.nome_exibicao || empresa.nome}

+

{empresa.nome_exibicao || empresa.nome}

{empresa.cpf_cnpj} • {empresa.email}

@@ -161,7 +161,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, nome: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -170,7 +170,7 @@ export const BoletoCreationDialog = ({ type="email" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -180,7 +180,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, cpf_cnpj: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -188,7 +188,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, telefone: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -198,7 +198,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, endereco: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -206,7 +206,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, cep: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -215,7 +215,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, bairro: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -223,7 +223,7 @@ export const BoletoCreationDialog = ({ setFormData({...formData, city: e.target.value})} - className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm" + className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm" />
@@ -243,10 +243,10 @@ export const BoletoCreationDialog = ({ value={formData.tipo_pessoa} onValueChange={(val) => setFormData({...formData, tipo_pessoa: val})} > - + - + Pessoa Jurídica Pessoa Física @@ -290,7 +290,7 @@ export const BoletoCreationDialog = ({ placeholder="0,00" value={formData.valor_servico} onChange={(e) => setFormData({...formData, valor_servico: e.target.value})} - className="bg-blue-500/10 border-blue-500/30 text-white text-2xl font-bold h-14 pl-12 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20" + className="bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30 text-blue-700 dark:text-white text-2xl font-bold h-14 pl-12 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20" />
@@ -298,11 +298,11 @@ export const BoletoCreationDialog = ({ )}
- + diff --git a/src/features/financeiro-v2/components/CurrencyInputV2.jsx b/src/features/financeiro-v2/components/CurrencyInputV2.jsx new file mode 100644 index 0000000..4f80e3c --- /dev/null +++ b/src/features/financeiro-v2/components/CurrencyInputV2.jsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { parseCurrency, formatCurrency, formatCurrencyForInput } from '../utils/dateUtils'; + +/** + * CurrencyInputV2 component for robust currency handling. + * Displays formatted currency (R$ 0,00) when blurred. + * Displays raw numeric format (0,00) when focused for easier editing. + * + * @param {Object} props - Component props + * @param {number|string} props.value - Numeric value + * @param {Function} props.onChange - Callback with the numeric value + * @param {string} [props.placeholder] - Input placeholder + * @param {string} [props.className] - Additional CSS classes + */ +export const CurrencyInputV2 = ({ value, onChange, placeholder = "R$ 0,00", className, ...props }) => { + const [focused, setFocused] = useState(false); + const [localValue, setLocalValue] = useState(''); + + const isEmpty = value === '' || value == null; + const displayValue = focused ? localValue : (isEmpty ? '' : formatCurrency(value)); + + const handleFocus = () => { + setFocused(true); + setLocalValue(isEmpty ? '' : formatCurrencyForInput(value)); + }; + + const handleBlur = () => { + const numericValue = parseCurrency(localValue); + onChange(numericValue === '' ? '' : numericValue); + setFocused(false); + }; + + return ( + setLocalValue(e.target.value)} + /> + ); +}; diff --git a/src/features/financeiro-v2/hooks/useContasReceber.js b/src/features/financeiro-v2/hooks/useContasReceber.js index fe38c1b..78687e5 100644 --- a/src/features/financeiro-v2/hooks/useContasReceber.js +++ b/src/features/financeiro-v2/hooks/useContasReceber.js @@ -188,6 +188,17 @@ export const useContasReceber = (defaultView = 'default') => { }, [defaultView]); const [transactions, setTransactions] = useState([]); const [cruzamentoLoading, setCruzamentoLoading] = useState(true); + const [clients, setClients] = useState([]); + const [services, setServices] = useState([]); + const [entradasPlanejadas, setEntradasPlanejadas] = useState([]); + const [boletos, setBoletos] = useState(MOCK_BOLETOS || []); + const [isLoadingServicos, setIsLoadingServicos] = useState(false); + const [isLoadingClients, setIsLoadingClients] = useState(false); + const [isLoadingEntradasPlanejadas, setIsLoadingEntradasPlanejadas] = useState(false); + const [isLoadingItens, setIsLoadingItens] = useState(false); + const [itensEntradaSelecionada, setItensEntradaSelecionada] = useState([]); + const [categorias, setCategorias] = useState([]); + const [caixas, setCaixas] = useState([]); // Cruzamento: alimentado por /api/extrato/apresentar (tipoOperacao C). Planejado zerado. useEffect(() => { @@ -243,19 +254,7 @@ export const useContasReceber = (defaultView = 'default') => { return () => { cancelled = true; }; }, []); - const [clients, setClients] = useState([]); - const [services, setServices] = useState([]); - const [entradasPlanejadas, setEntradasPlanejadas] = useState([]); - const [boletos, setBoletos] = useState(MOCK_BOLETOS || []); - const [isLoadingServicos, setIsLoadingServicos] = useState(false); - const [isLoadingClients, setIsLoadingClients] = useState(false); - const [isLoadingEntradasPlanejadas, setIsLoadingEntradasPlanejadas] = useState(false); - const [isLoadingItens, setIsLoadingItens] = useState(false); - const [itensEntradaSelecionada, setItensEntradaSelecionada] = useState([]); - const [categorias, setCategorias] = useState([]); - const [caixas, setCaixas] = useState([]); - - /** Carrega serviços do backend (GET /servicos/list ou /servicos/list?tipo=...). Mantém mock quando VITE_USE_MOCK=true. */ + /** Carrega serviços do backend (GET /servicos/list ou /servicos/list?tipo=...). */ const loadServicos = useCallback(async (tipo) => { setIsLoadingServicos(true); try { @@ -271,10 +270,9 @@ export const useContasReceber = (defaultView = 'default') => { } finally { setIsLoadingServicos(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis - }, []); + }, [toast]); - /** Carrega clientes do backend (GET /empresas_financeiro). Mantém mock quando VITE_USE_MOCK=true. */ + /** Carrega clientes do backend (GET /empresas_financeiro). */ const loadClients = useCallback(async () => { setIsLoadingClients(true); try { @@ -287,8 +285,7 @@ export const useContasReceber = (defaultView = 'default') => { } finally { setIsLoadingClients(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis - }, []); + }, [toast]); /** Carrega entradas planejadas do backend (GET /empresas_planejadas). */ const loadEntradasPlanejadas = useCallback(async () => { @@ -303,8 +300,7 @@ export const useContasReceber = (defaultView = 'default') => { } finally { setIsLoadingEntradasPlanejadas(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis - }, []); + }, [toast]); // Executa uma única vez no mount; ref evita reexecução mesmo com Strict Mode ou dependências instáveis const didLoadReceitas = useRef(false); @@ -320,7 +316,7 @@ export const useContasReceber = (defaultView = 'default') => { const totalRecebido = useMemo(() => { if (!Array.isArray(transactions)) return 0; return transactions - .filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado') + .filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado' || t?.status === 'Marcado como Recebido') .reduce((acc, t) => acc + (t?.valor || 0), 0); }, [transactions]); @@ -332,22 +328,22 @@ export const useContasReceber = (defaultView = 'default') => { const totalGeral = totalRecebido + totalPendente; - // Actions para Serviços (API: /servicos/create, /servicos/edit, /servicos/delete) - const parseValor = (v) => { + // Actions para Serviços + const parseValor = useCallback((v) => { if (v == null || v === '') return NaN; const n = typeof v === 'number' ? v : Number(String(v).replace(',', '.')); return Number.isFinite(n) ? n : NaN; - }; + }, []); - const formatDataEnvio = (dateStr) => { + const formatDataEnvio = useCallback((dateStr) => { if (!dateStr) return ''; const d = new Date(dateStr); if (Number.isNaN(d.getTime())) return ''; const y = d.getFullYear(); const m = d.getMonth() + 1; const day = d.getDate(); - return `${y}-${m}-${day} `; - }; + return `${y}-${m}-${day}`; + }, []); const createService = useCallback(async (serviceData) => { if (!serviceData.servico?.trim()) { @@ -375,7 +371,7 @@ export const useContasReceber = (defaultView = 'default') => { toast.error('Erro ao criar serviço. Tente novamente.'); return null; } - }, [loadServicos, toast]); + }, [loadServicos, toast, parseValor]); const updateService = useCallback(async (id, serviceData) => { if (!serviceData.servico?.trim()) { @@ -404,7 +400,7 @@ export const useContasReceber = (defaultView = 'default') => { toast.error('Erro ao editar serviço. Tente novamente.'); return null; } - }, [loadServicos, toast]); + }, [loadServicos, toast, parseValor]); const deleteService = useCallback(async (id) => { try { @@ -420,8 +416,7 @@ export const useContasReceber = (defaultView = 'default') => { }, [loadServicos, toast]); // Actions para Clientes - const createClient = (clientData) => { - // Validação de campos obrigatórios + const createClient = useCallback(async (clientData) => { const camposObrigatorios = []; if (!clientData.nome?.trim()) camposObrigatorios.push('Nome'); if (!clientData.email?.trim()) camposObrigatorios.push('Email'); @@ -432,17 +427,22 @@ export const useContasReceber = (defaultView = 'default') => { return null; } - const newClient = { - id: clients.length + 1, - ...clientData - }; - setClients(prev => [...prev, newClient]); - toast.success('Cliente criado com sucesso!', 'Sucesso'); - return newClient; - }; + setIsLoadingClients(true); + try { + const response = await workspaceReceitasService.createClient(clientData); + await loadClients(); + toast.success('Cliente criado com sucesso!', 'Sucesso'); + return response; + } catch (err) { + console.warn('[useContasReceber] Erro ao criar cliente:', err); + toast.handleBackendError(err); + return null; + } finally { + setIsLoadingClients(false); + } + }, [loadClients, toast]); - const updateClient = (id, clientData) => { - // Validação de campos obrigatórios + const updateClient = useCallback(async (id, clientData) => { const camposObrigatorios = []; if (!clientData.nome?.trim()) camposObrigatorios.push('Nome'); if (!clientData.email?.trim()) camposObrigatorios.push('Email'); @@ -450,17 +450,45 @@ export const useContasReceber = (defaultView = 'default') => { if (camposObrigatorios.length > 0) { toast.notifyFields(camposObrigatorios); - return; + return false; } - setClients(prev => prev.map(c => c.id === id ? { ...c, ...clientData } : c)); - toast.success('Cliente atualizado com sucesso!', 'Sucesso'); - }; + setIsLoadingClients(true); + try { + await workspaceReceitasService.updateClient({ ...clientData, idempresa: id }); + await loadClients(); + toast.success('Cliente atualizado com sucesso!', 'Sucesso'); + return true; + } catch (err) { + console.warn('[useContasReceber] Erro ao atualizar cliente:', err); + toast.handleBackendError(err); + return false; + } finally { + setIsLoadingClients(false); + } + }, [loadClients, toast]); - const deleteClient = (id) => { - setClients(prev => prev.filter(c => c.id !== id)); - toast.success('Cliente excluído com sucesso!', 'Sucesso'); - }; + const updateClientStatus = useCallback(async (id, status) => { + setIsLoadingClients(true); + try { + await workspaceReceitasService.updateClientStatus({ idempresa: id, status, data_envio: formatDataEnvio(new Date()) }); + await loadClients(); + toast.success(`Status do cliente alterado para ${status}!`, 'Sucesso'); + return true; + } catch (err) { + console.warn('[useContasReceber] Erro ao alterar status do cliente:', err); + toast.handleBackendError(err); + return false; + } finally { + setIsLoadingClients(false); + } + }, [loadClients, toast, formatDataEnvio]); + + const deleteClient = useCallback(async (id) => { + // Nota: O service não tem deleteClient explicitamente, geralmente é feito via status Inativo + // Mas podemos implementar se houver rota. Como não vi rota de delete no service, usarei updateStatus + return updateClientStatus(id, 'Inativo'); + }, [updateClientStatus]); const loadItensEntradaPlanejada = useCallback(async (idEntrada) => { if (!idEntrada) return; @@ -480,8 +508,6 @@ export const useContasReceber = (defaultView = 'default') => { // Actions para Entradas Planejadas const createEntradaPlanejada = useCallback(async (entradaData) => { - - // Validação de campos obrigatórios const camposObrigatorios = []; if (!entradaData.dataEstimativa) camposObrigatorios.push('Data Estimativa'); if (!entradaData.cliente?.trim()) camposObrigatorios.push('Cliente'); @@ -495,20 +521,13 @@ export const useContasReceber = (defaultView = 'default') => { setIsLoadingEntradasPlanejadas(true); try { - // Separar itens dos dados principais para envio sequencial const { itens, ...parentData } = entradaData; - - // 1. Criar a entrada principal const response = await workspaceEntradasPlanejadasService.createEntradaPlanejada(parentData); - - // Tenta obter o ID retornado pelo backend em diferentes formatos comuns let idEntrada = response?.id || response?.idempresa || response?.identradas_planejadas || response?.Base_Dados_API?.[0]?.id || (response?.Base_Dados_API && response.Base_Dados_API.id); - // FALLBACK: Se o backend não retornar o ID (apenas uma mensagem), buscamos na listagem if (!idEntrada) { console.log('[useContasReceber] ID não retornado, consultando listagem para capturar ID...'); const list = await workspaceEntradasPlanejadasService.fetchEntradasPlanejadas(); - // Tenta encontrar a entrada que acabamos de criar (pelo número de referência e cliente) const matched = list.find(e => String(e.numeroReferencia) === String(parentData.numeroReferencia) && e.cliente === parentData.cliente @@ -517,13 +536,12 @@ export const useContasReceber = (defaultView = 'default') => { } if (!idEntrada) { - console.warn('[useContasReceber] ID da entrada não encontrado após tentativa de consulta:', response); + console.warn('[useContasReceber] ID da entrada não encontrado:', response); await loadEntradasPlanejadas(); - toast.error('Entrada criada, mas houve um problema ao vincular itens. Verifique o registro.'); + toast.error('Entrada criada, mas houve um problema ao vincular itens.'); return false; } - // 2. Criar os itens vinculados ao ID da entrada const itemPromises = itens.map(item => workspaceEntradasPlanejadasService.createItemEntradaPlanejada({ ...item, @@ -532,14 +550,12 @@ export const useContasReceber = (defaultView = 'default') => { ); await Promise.all(itemPromises); - - await loadEntradasPlanejadas(); toast.success('Entrada e itens criados com sucesso!', 'Sucesso'); return true; } catch (err) { console.warn('[useContasReceber] Erro ao criar entrada planejada:', err); - toast.error('Erro ao criar entrada planejada. Tente novamente.'); + toast.error('Erro ao criar entrada planejada.'); return false; } finally { setIsLoadingEntradasPlanejadas(false); @@ -547,7 +563,6 @@ export const useContasReceber = (defaultView = 'default') => { }, [loadEntradasPlanejadas, toast]); const updateEntradaPlanejada = useCallback(async (id, entradaData) => { - // Validação de campos obrigatórios const camposObrigatorios = []; if (!entradaData.dataEstimativa) camposObrigatorios.push('Data Estimativa'); if (!entradaData.cliente?.trim()) camposObrigatorios.push('Cliente'); @@ -563,22 +578,16 @@ export const useContasReceber = (defaultView = 'default') => { try { const { itens, ...parentData } = entradaData; const idempresa = parentData.idempresa || id; - - // 1. Atualizar a entrada principal await workspaceEntradasPlanejadasService.updateEntradaPlanejada(idempresa, parentData); - // 2. Processar itens (Update ou Create) const itemPromises = itens.map(item => { const idItem = item.identradas_planejadas_itens || item.id; - - // Se o item já tem ID e não é um ID temporário, atualiza if (idItem && typeof idItem === 'number') { return workspaceEntradasPlanejadasService.updateItemEntradaPlanejada(idItem, { ...item, idEntrada: idempresa }); } else { - // Se não tem ID, cria como novo item vinculado à entrada return workspaceEntradasPlanejadasService.createItemEntradaPlanejada({ ...item, idEntrada: idempresa @@ -587,20 +596,18 @@ export const useContasReceber = (defaultView = 'default') => { }); await Promise.all(itemPromises); - await loadEntradasPlanejadas(); toast.success('Entrada planejada atualizada com sucesso!', 'Sucesso'); return true; } catch (err) { console.warn('[useContasReceber] Erro ao atualizar entrada planejada:', err); - toast.error('Erro ao atualizar entrada planejada. Tente novamente.'); + toast.error('Erro ao atualizar entrada planejada.'); return false; } finally { setIsLoadingEntradasPlanejadas(false); } }, [loadEntradasPlanejadas, toast]); - const deleteEntradaPlanejada = useCallback(async (id) => { try { await workspaceEntradasPlanejadasService.deleteEntradaPlanejada(id); @@ -609,30 +616,27 @@ export const useContasReceber = (defaultView = 'default') => { return true; } catch (err) { console.warn('[useContasReceber] Erro ao excluir entrada planejada:', err); - toast.error('Erro ao excluir entrada planejada. Tente novamente.'); + toast.error('Erro ao excluir entrada planejada.'); return null; } }, [loadEntradasPlanejadas, toast]); // Actions para Boletos - const downloadBoleto = (boletoId) => { - // Implementar download + const downloadBoleto = useCallback((boletoId) => { console.log('Download boleto:', boletoId); - }; + }, []); - const sendBoleto = (boletoId) => { - // Implementar envio + const sendBoleto = useCallback((boletoId) => { console.log('Enviar boleto:', boletoId); - }; + }, []); - const scheduleBoleto = (boletoId, newDate) => { + const scheduleBoleto = useCallback((boletoId, newDate) => { setBoletos(prev => prev.map(b => b.id === boletoId ? { ...b, dataVencimento: newDate } : b)); - }; + }, []); - const downloadFatura = (boletoId) => { - // Implementar download fatura + const downloadFatura = useCallback((boletoId) => { console.log('Download fatura:', boletoId); - }; + }, []); const actions = useMemo(() => ({ setActiveSubView, @@ -646,6 +650,7 @@ export const useContasReceber = (defaultView = 'default') => { // Clientes createClient, updateClient, + updateClientStatus, deleteClient, // Entradas Planejadas createEntradaPlanejada, @@ -658,11 +663,10 @@ export const useContasReceber = (defaultView = 'default') => { sendBoleto, scheduleBoleto, downloadFatura, - }), [ setActiveSubView, loadServicos, loadClients, loadEntradasPlanejadas, createService, updateService, deleteService, - createClient, updateClient, deleteClient, + createClient, updateClient, updateClientStatus, deleteClient, createEntradaPlanejada, updateEntradaPlanejada, deleteEntradaPlanejada, loadItensEntradaPlanejada, setItensEntradaSelecionada, downloadBoleto, sendBoleto, scheduleBoleto, downloadFatura diff --git a/src/features/financeiro-v2/hooks/useDashboard.js b/src/features/financeiro-v2/hooks/useDashboard.js index 77b31dd..40ae32e 100644 --- a/src/features/financeiro-v2/hooks/useDashboard.js +++ b/src/features/financeiro-v2/hooks/useDashboard.js @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { extratoService } from '@/services/extratoService'; +import { formatDateForChart } from '../utils/dateUtils'; import { boletosService } from '@/services/boletosService'; import { workspaceDespesasService } from '@/services/workspaceDespesasService'; import { workspaceSaldoService } from '@/services/workspaceSaldoService'; @@ -30,6 +31,11 @@ export const useDashboard = () => { boletosAbertos: [] }); + // Estado para filtros de gráfico + const [jurosFilter, setJurosFilter] = useState('mensal'); // 'mensal' | 'anual' + const [fluxoPeriodo, setFluxoPeriodo] = useState('0'); // 0 = atual, 1 = 1 mês atrás, etc. + const [jurosData, setJurosData] = useState([]); + // Dados calculados para exibição const [resumo, setResumo] = useState({ saldoTotal: 0, @@ -98,7 +104,7 @@ export const useDashboard = () => { extratoService.fetchSaldo().catch(() => ({ disponivel: 0 })), extratoService.fetchSaldoArmazenado().catch(() => []), extratoService.fetchExtrato().catch(() => []), - extratoService.fetchFluxo().catch(() => ({ mensal: [] })), + extratoService.fetchFluxo().catch(() => ({ mensal: [], anual: [], diario: [] })), boletosService.fetchBoletosStatus().catch(() => ({ cobrancas: [] })), extratoService.fetchEmpresas().catch(() => []), workspaceDespesasService.fetchContasAPagar().catch(() => []), @@ -249,38 +255,21 @@ export const useDashboard = () => { const diario = data.fluxo?.diario || []; const fluxoPorDia = {}; const hoje = new Date(); - // Filtro: manter visualização do mês atual (como era antes) - // ou talvez mostrar os últimos 30 dias se o usuário quiser histórico recente? - // O título do card é "Histórico de Fluxo", e o subtítulo "Entradas vs Saídas". - // A implementação original filtrava pelo "mesAtualStr". Vou manter para não quebrar a expectativa de "visão do mês". - // ATUALIZAÇÃO: Se o endpoint traz histórico, pode ser interessante mostrar os últimos X dias - // se o mês estiver no começo. Mas para consistência com o resto do dashboard (que foca no mês), - // vou filtrar pelo mês atual OU se o array estiver vazio, fallback. - // Mas o pedido foi "alimentar o historico de fluxo como o fluxo diaria dessa rota". - // A rota retorna MUITOS dias. O gráfico de barras vai ficar ilegível se mostrar tudo. - // Vou filtrar pelo Mês Atual. - - const mesAtualStr = formatMesAno(hoje); + // Calcular qual mês mostrar com base no fluxoPeriodo + const dataAlvo = new Date(hoje.getFullYear(), hoje.getMonth() - Number(fluxoPeriodo), 1); + const mesAlvoStr = `${dataAlvo.getFullYear()}-${String(dataAlvo.getMonth() + 1).padStart(2, '0')}`; diario.forEach((item) => { - // O item tem structure: { data: "YYYY-MM-DD", tipoOperacao: "C"|"D", total: 123.45 } if (!item.data) return; - // item.data vem como "YYYY-MM-DD" - // Vamos verificar se pertence ao mês atual - // A string "YYYY-MM-DD" começa com o ano-mes. - // formatMesAno retorna "YYYY-MM". - - if (!item.data.startsWith(mesAtualStr)) return; + // Filtrar pelo mês alvo + if (!item.data.startsWith(mesAlvoStr)) return; - // item.data já está em YYYY-MM-DD. - // Para o gráfico precisamos de DD/MM - const [ano, mes, dia] = item.data.split('-'); - const diaFormatado = `${dia}/${mes}`; + const diaFormatado = formatDateForChart(item.data); if (!fluxoPorDia[diaFormatado]) { - fluxoPorDia[diaFormatado] = { name: diaFormatado, entradas: 0, saidas: 0 }; + fluxoPorDia[diaFormatado] = { name: diaFormatado, entradas: 0, saidas: 0, rawDate: item.data }; } const valor = safeNumber(item.total); @@ -291,18 +280,13 @@ export const useDashboard = () => { } }); - // Converter para array e ordenar por data (dia) + // Converter para array e ordenar cronologicamente pela data bruta return Object.values(fluxoPorDia) - .sort((a, b) => { - const [diaA, mesA] = a.name.split('/'); - const [diaB, mesB] = b.name.split('/'); - // Assumindo mesmo ano/mês, basta ordenar por dia - return Number(diaA) - Number(diaB); - }); + .sort((a, b) => a.rawDate.localeCompare(b.rawDate)); }; const getBoletosPieData = () => { - const counts = {}; + const values = {}; const mesAtual = formatMesAno(new Date()); data.boletos.cobrancas?.forEach((item) => { @@ -310,7 +294,8 @@ export const useDashboard = () => { if (formatMesAno(new Date(c.dataVencimento)) !== mesAtual) return; const st = c.situacao || 'OUTROS'; - counts[st] = (counts[st] || 0) + 1; + const valor = safeNumber(c.valorNominal); + values[st] = (values[st] || 0) + valor; }); const colors = { @@ -321,16 +306,14 @@ export const useDashboard = () => { 'OUTROS': '#94a3b8' }; - return Object.entries(counts).map(([name, value]) => ({ + return Object.entries(values).map(([name, value]) => ({ name, value, color: colors[name] || colors['OUTROS'] })); }; - // Estado para filtro do gráfico de juros - const [jurosFilter, setJurosFilter] = useState('mensal'); // 'mensal' | 'anual' - const [jurosData, setJurosData] = useState([]); + // ... (previous code) // ... (previous code) @@ -423,6 +406,8 @@ export const useDashboard = () => { getJurosData, jurosFilter, setJurosFilter, + fluxoPeriodo, + setFluxoPeriodo, getUltimasTransacoes, fetchEntradasMes: async (mes, ano) => { try { @@ -469,6 +454,14 @@ export const useDashboard = () => { return []; } }, + fetchJurosAdicionados: async (mes, ano) => { + try { + return await extratoService.fetchJurosAdicionados({ mes, ano }); + } catch (err) { + console.error('Error fetching juros adicionados:', err); + return { dados: [], total_adicionado: 0, total_registros: 0 }; + } + }, reload: loadData }; }; diff --git a/src/features/financeiro-v2/hooks/useFornecedores.js b/src/features/financeiro-v2/hooks/useFornecedores.js index 085dfb3..25dc66b 100644 --- a/src/features/financeiro-v2/hooks/useFornecedores.js +++ b/src/features/financeiro-v2/hooks/useFornecedores.js @@ -37,7 +37,7 @@ export const useFornecedores = () => { servico: item.servico || '', valor: item.valor || 0, obs: item.obs || '', - idregra: item.idregra || item.regra_id || null, + idregra: Array.isArray(item.idregra) ? item.idregra : (item.idregra ? [item.idregra] : (item.regra_id ? [item.regra_id] : [])), regra_id: item.idregra || item.regra_id || null, nome_regra: item.nome_regra || null, // Campos opcionais para compatibilidade @@ -108,7 +108,7 @@ export const useFornecedores = () => { servico: newFornecedor.servico || fornecedorData.servico || '', valor: newFornecedor.valor || fornecedorData.valor || 0, obs: newFornecedor.obs || fornecedorData.obs || '', - idregra: newFornecedor.idregra || newFornecedor.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null, + idregra: Array.isArray(newFornecedor.idregra) ? newFornecedor.idregra : (newFornecedor.idregra ? [newFornecedor.idregra] : (fornecedorData.idregra ? (Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : [fornecedorData.idregra]) : [])), regra_id: newFornecedor.idregra || newFornecedor.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null, status: 'Ativo', moeda_padrao: 'BRL', @@ -170,7 +170,7 @@ export const useFornecedores = () => { servico: updated.servico || fornecedorData.servico || '', valor: updated.valor || fornecedorData.valor || 0, obs: updated.obs || fornecedorData.obs || '', - idregra: updated.idregra || updated.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null, + idregra: Array.isArray(updated.idregra) ? updated.idregra : (updated.idregra ? [updated.idregra] : (fornecedorData.idregra ? (Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : [fornecedorData.idregra]) : [])), regra_id: updated.idregra || updated.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null, // Preserva campos existentes status: fornecedorData.status || 'Ativo', diff --git a/src/features/financeiro-v2/hooks/useToast.js b/src/features/financeiro-v2/hooks/useToast.js index e5337a7..f0a9387 100644 --- a/src/features/financeiro-v2/hooks/useToast.js +++ b/src/features/financeiro-v2/hooks/useToast.js @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from 'react'; import { toast } from 'sonner'; /** @@ -5,77 +6,56 @@ import { toast } from 'sonner'; * Usa sonner para exibir mensagens de sucesso, erro, warning e info */ export const useToast = () => { - return { - /** - * Exibe uma notificação de sucesso - * @param {string} message - Mensagem a ser exibida - * @param {string} title - Título opcional - */ - success: (message, title = 'Sucesso!') => { - toast.success(title, { - description: message, - duration: 4000, - }); - }, + const success = useCallback((message, title = 'Sucesso!') => { + toast.success(title, { + description: message, + duration: 4000, + }); + }, []); - /** - * Exibe uma notificação de erro - * @param {string} message - Mensagem a ser exibida - * @param {string} title - Título opcional - */ - error: (message, title = 'Erro!') => { - toast.error(title, { - description: message, - duration: 5000, - }); - }, + const error = useCallback((message, title = 'Erro!') => { + toast.error(title, { + description: message, + duration: 5000, + }); + }, []); - /** - * Exibe uma notificação de aviso - * @param {string} message - Mensagem a ser exibida - * @param {string} title - Título opcional - */ - warning: (message, title = 'Atenção!') => { - toast.warning(title, { - description: message, - duration: 4000, - }); - }, + const warning = useCallback((message, title = 'Atenção!') => { + toast.warning(title, { + description: message, + duration: 4000, + }); + }, []); - /** - * Exibe uma notificação informativa - * @param {string} message - Mensagem a ser exibida - * @param {string} title - Título opcional - */ - info: (message, title = 'Informação') => { - toast.info(title, { - description: message, - duration: 4000, - }); - }, + const info = useCallback((message, title = 'Informação') => { + toast.info(title, { + description: message, + duration: 4000, + }); + }, []); - /** - * Exibe uma notificação de campos obrigatórios não preenchidos - * @param {string[]} fields - Lista de campos obrigatórios - */ - notifyFields: (fields) => { - toast.warning('Campos Obrigatórios', { - description: `Por favor, preencha: ${fields.join(', ')}`, - duration: 5000, - }); - }, + const notifyFields = useCallback((fields) => { + toast.warning('Campos Obrigatórios', { + description: `Por favor, preencha: ${fields.join(', ')}`, + duration: 5000, + }); + }, []); - /** - * Trata erros de backend de forma amigável - * @param {Error} error - Erro capturado - */ - handleBackendError: (error) => { - console.error(error); - const msg = error.response?.data?.message || error.message || 'Erro desconhecido na comunicação com o servidor.'; - toast.error('Erro do Sistema', { - description: msg, - duration: 5000, - }); - }, - }; + const handleBackendError = useCallback((error) => { + console.error(error); + const msg = error.response?.data?.message || error.message || 'Erro desconhecido na comunicação com o servidor.'; + toast.error('Erro do Sistema', { + description: msg, + duration: 5000, + }); + }, []); + + return useMemo(() => ({ + success, + error, + warning, + info, + notifyFields, + handleBackendError, + }), [success, error, warning, info, notifyFields, handleBackendError]); }; diff --git a/src/features/financeiro-v2/utils/dateUtils.js b/src/features/financeiro-v2/utils/dateUtils.js index 0ad335f..ce2336f 100644 --- a/src/features/financeiro-v2/utils/dateUtils.js +++ b/src/features/financeiro-v2/utils/dateUtils.js @@ -13,6 +13,11 @@ export const formatDate = (dateString) => { const [year, month, day] = dateString.split('-').map(part => part.split('T')[0]); return `${day}/${month}/${year}`; } + + // Se a string já está no formato DD/MM/YYYY, retornar como está + if (typeof dateString === 'string' && /^\d{2}\/\d{2}\/\d{4}/.test(dateString)) { + return dateString; + } // Se for objeto Date if (dateString instanceof Date) { diff --git a/src/features/financeiro-v2/views/DashboardView.jsx b/src/features/financeiro-v2/views/DashboardView.jsx index 3e9c59a..7a274ac 100644 --- a/src/features/financeiro-v2/views/DashboardView.jsx +++ b/src/features/financeiro-v2/views/DashboardView.jsx @@ -59,6 +59,9 @@ import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip'; import { useConciliacaoV2 } from '../hooks/useConciliacaoV2'; import { ExtratoCompletoView } from './conciliacao-v2/ExtratoCompletoView'; 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 const XPICard = ({ @@ -135,11 +138,14 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; getJurosData, jurosFilter, setJurosFilter, + fluxoPeriodo, + setFluxoPeriodo, getUltimasTransacoes, fetchEntradasMes, fetchSaidasPeriodo, fetchTransacoesNaoConciliadas, fetchBoletosAbertos, + fetchJurosAdicionados, reload } = useDashboard(); @@ -150,56 +156,88 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; saldo: false, despesas: false, naoConciliadas: false, - boletosAbertos: false + boletosAbertos: false, + adicionado: false }); const [modalData, setModalData] = useState({ saldo: [], despesas: [], naoConciliadas: [], - boletosAbertos: [] + boletosAbertos: [], + adicionado: { + lista: [], + total_adicionado: 0, + total_registros: 0 + } }); const [modalLoading, setModalLoading] = useState(false); const [modalDate, setModalDate] = useState(new Date()); + + // New states for juros detailing period selectors + const [jurosDetailPeriodo, setJurosDetailPeriodo] = useState('mes'); // 'mes' | 'ano' + const [jurosDetailMes, setJurosDetailMes] = useState(String(new Date().getMonth() + 1)); + const [jurosDetailAno, setJurosDetailAno] = useState(String(new Date().getFullYear())); - // New states for requested adjustments + // New states for requested adjustments const [isExtratoPopupOpen, setIsExtratoPopupOpen] = useState(false); const [isCategorizacaoOpen, setIsCategorizacaoOpen] = useState(false); - const fetchDataForModal = async (type, date) => { - const mes = date.getMonth() + 1; - const ano = date.getFullYear(); - let data = []; + const fetchDataForModal = async (type, dateOrParams) => { + let mes, ano; + + if (type === 'adicionado' && typeof dateOrParams === 'object') { + mes = dateOrParams.mes; + ano = dateOrParams.ano; + } else { + const d = dateOrParams instanceof Date ? dateOrParams : new Date(); + mes = d.getMonth() + 1; + ano = d.getFullYear(); + } - try { - setModalLoading(true); - switch (type) { - case 'saldo': - data = await fetchEntradasMes(mes, ano); - break; - case 'despesas': - data = await fetchSaidasPeriodo(mes, ano); - break; - case 'naoConciliadas': - data = await fetchTransacoesNaoConciliadas(); // Usually global, but could filter if APIs allowed - break; - case 'boletosAbertos': - data = await fetchBoletosAbertos(); // Global - break; - } - setModalData(prev => ({ ...prev, [type]: data })); - } catch (err) { - console.error(`Error loading modal data for ${type}:`, err); - } finally { - setModalLoading(false); - } - }; - - const openModal = (type) => { - setModalState(prev => ({ ...prev, [type]: true })); - const now = new Date(); - setModalDate(now); // Reset to current month on open - fetchDataForModal(type, now); - }; + let data = []; + + try { + setModalLoading(true); + switch (type) { + case 'saldo': + data = await fetchEntradasMes(mes, ano); + break; + case 'despesas': + data = await fetchSaidasPeriodo(mes, ano); + break; + case 'naoConciliadas': + data = await fetchTransacoesNaoConciliadas(); // Usually global, but could filter if APIs allowed + break; + case 'boletosAbertos': + data = await fetchBoletosAbertos(); // Global + break; + case 'adicionado': + const result = await fetchJurosAdicionados(mes, ano); + data = { + lista: result.dados || result.adicionado || [], + total_adicionado: result.total_adicionado || 0, + total_registros: result.total_registros || 0 + }; + break; + } + setModalData(prev => ({ ...prev, [type]: data })); + } catch (err) { + console.error(`Error loading modal data for ${type}:`, err); + } finally { + setModalLoading(false); + } + }; + + const openModal = (type) => { + setModalState(prev => ({ ...prev, [type]: true })); + const now = new Date(); + if (type === 'adicionado') { + fetchDataForModal(type, { mes: jurosDetailMes, ano: jurosDetailAno }); + } else { + setModalDate(now); + fetchDataForModal(type, now); + } + }; const handleModalDateChange = (type, newDate) => { fetchDataForModal(type, newDate); @@ -252,7 +290,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';

Painel Financeiro

Visão Geral do Workspace

-
+ {/*
-
+
*/} {/* KPI Grid */} @@ -356,6 +394,13 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
+ @@ -462,7 +507,20 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';

Histórico de Fluxo

Entradas vs Saídas

- +
+ + +
@@ -885,13 +943,116 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog'; {formatCurrency(modalData.boletosAbertos.reduce((acc, curr) => acc + (Number(curr.cobranca?.valorNominal || curr.valorNominal || 0)), 0))}

- + */} +
+
+
+ + {/* Modal - Detalhamento de Juros Adicionados */} + setModalState(prev => ({ ...prev, adicionado: open }))}> + + +
+ + + Juros Adicionados - {jurosDetailPeriodo === 'mes' ? `${MESES_REC[Number(jurosDetailMes)]} / ${jurosDetailAno}` : jurosDetailAno} + + + Detalhamento dos juros e acréscimos identificados no período. + +
+
+ Período: + + + + {jurosDetailPeriodo === 'mes' && ( + + )} + + +
+
+
+ ( + {formatDate(row.dataEntrada)} + )}, + { header: 'Beneficiário/Pagador', field: 'beneficiario_pagador', width: 250, render: (row) => ( + {row.beneficiario_pagador || 'N/A'} + )}, + { header: 'Descrição', field: 'descricao', width: 300 }, + { header: 'Tipo', field: 'tipoTransacao', width: 120, render: (row) => ( + + {row.tipoTransacao || 'GERAL'} + + )}, + { header: 'Valor (Juros)', field: 'adicionado', width: 120, render: (row) => ( + {formatCurrency(row.adicionado)} + )}, + { header: 'Valor Total', field: 'valor', width: 120, render: (row) => ( + {formatCurrency(row.valor)} + )} + ]} + /> +
+ +
+

Total de Juros Adicionados

+
+

+ {formatCurrency(modalData.adicionado.total_adicionado)} +

+ ({modalData.adicionado.total_registros} registros) +
+
+ {/* */}
diff --git a/src/features/financeiro-v2/views/conciliacao-v2/ConciliacaoView.jsx b/src/features/financeiro-v2/views/conciliacao-v2/ConciliacaoView.jsx index 81b09d3..8460ff3 100644 --- a/src/features/financeiro-v2/views/conciliacao-v2/ConciliacaoView.jsx +++ b/src/features/financeiro-v2/views/conciliacao-v2/ConciliacaoView.jsx @@ -139,7 +139,7 @@ export const ConciliacaoView = ({ initialView }) => {

Conciliação - PREMIUM v2 + {/* PREMIUM v2 */}

{/*
- + */} @@ -609,16 +610,11 @@ export const DespesasView = () => {
-
- R$ - setFormData({ ...formData, montante: parseFloat(e.target.value) || 0 })} - className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl pl-12 pr-4 font-mono font-bold text-rose-600 focus:ring-rose-500/10 focus:border-rose-500 relative" - /> -
+ setFormData({ ...formData, montante: val || 0 })} + className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-mono font-bold text-rose-600 focus:ring-rose-500/10 focus:border-rose-500" + />
@@ -706,21 +702,17 @@ export const DespesasView = () => {
- setItemFormData({ ...itemFormData, debito: parseFloat(e.target.value) || 0 })} + onChange={(val) => setItemFormData({ ...itemFormData, debito: val || 0 })} className="h-11 bg-slate-50/50 border-slate-200 dark:bg-slate-900/50 dark:border-slate-800 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/10 transition-all rounded-xl font-mono text-emerald-600 font-bold text-center" />
- setItemFormData({ ...itemFormData, credito: parseFloat(e.target.value) || 0 })} + onChange={(val) => setItemFormData({ ...itemFormData, credito: val || 0 })} className="h-11 bg-slate-50/50 border-slate-200 dark:bg-slate-900/50 dark:border-slate-800 focus:border-rose-500 focus:ring-2 focus:ring-rose-500/10 transition-all rounded-xl font-mono text-rose-600 font-bold text-center" />
diff --git a/src/features/financeiro-v2/views/contas-pagar/FornecedoresView.jsx b/src/features/financeiro-v2/views/contas-pagar/FornecedoresView.jsx index 656cccb..95b6442 100644 --- a/src/features/financeiro-v2/views/contas-pagar/FornecedoresView.jsx +++ b/src/features/financeiro-v2/views/contas-pagar/FornecedoresView.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Building2, Plus, @@ -87,6 +87,7 @@ export const FornecedoresView = () => { const [loadingExtrato, setLoadingExtrato] = useState(false); const [tipoOperacao, setTipoOperacao] = useState('D'); // D para Débito (padrão fornecedores) + const [serviceType, setServiceType] = useState('servico'); // 'servico' | 'produto' const [availableServices, setAvailableServices] = useState([]); const [availableRules, setAvailableRules] = useState([]); // Regras/Categorias para vínculo @@ -99,7 +100,7 @@ export const FornecedoresView = () => { cpf_cnpj: '', tipo_pessoa: 'JURIDICA', servicos: [], // Mantido como array localmente - idregra: '', // ID da Regra vinculada + regras: [], // Lista de IDs de regras vinculadas obs: '', // Campos opcionais para compatibilidade endereco: '', @@ -131,7 +132,7 @@ export const FornecedoresView = () => { const fetchData = async () => { try { const [services, rulesResponse] = await Promise.all([ - workspaceReceitasService.fetchServicos(), + workspaceReceitasService.fetchServicos(serviceType), conciliacaoService.fetchRules() // Buscando Regras para usar como Regra de Conciliação ]); @@ -148,7 +149,7 @@ export const FornecedoresView = () => { } }; fetchData(); - }, []); + }, [serviceType]); // Fecth Extrato effect useEffect(() => { @@ -158,17 +159,19 @@ export const FornecedoresView = () => { } const fetchExtratoData = async () => { - const regraId = selectedFornecedor.idregra || selectedFornecedor.regra_id; - - if (!regraId) { + const regraIds = Array.isArray(selectedFornecedor.idregra) + ? selectedFornecedor.idregra.filter(Boolean) + : (selectedFornecedor.idregra ? [selectedFornecedor.idregra] : (selectedFornecedor.regra_id ? [selectedFornecedor.regra_id] : [])); + + if (regraIds.length === 0) { setProviderExtrato([]); return; } setLoadingExtrato(true); try { - // Usando a nova rota /regra_aplicada via hook - const data = await actions.fetchRegraAplicada(regraId, tipoOperacao); + // Passa a lista de IDs de regra; idempresa pode ser passado se disponível no futuro + const data = await actions.fetchRegraAplicada(regraIds, tipoOperacao); setProviderExtrato(data || []); } catch (error) { console.error('Erro ao buscar extrato por regra:', error); @@ -235,17 +238,20 @@ export const FornecedoresView = () => { nome: fornecedor.nome || '', nome_exibicao: fornecedor.nome_exibicao || fornecedor.nome || '', email: fornecedor.email || '', - telefone: fornecedor.telefone || '', - cpf_cnpj: fornecedor.cpf_cnpj || '', + telefone: maskPhone(fornecedor.telefone || ''), + cpf_cnpj: maskCPFCNPJ(fornecedor.cpf_cnpj || ''), tipo_pessoa: fornecedor.tipo_pessoa || 'JURIDICA', servicos: parsedServicos, - idregra: fornecedor.idregra || fornecedor.regra_id || fornecedor.categoria_id || '', // Mapeia idregra ou regra_id + // Normaliza regras como array de IDs (number) + regras: Array.isArray(fornecedor.idregra) + ? fornecedor.idregra.map(Number) + : (fornecedor.idregra ? [Number(fornecedor.idregra)] : (fornecedor.regra_id ? [Number(fornecedor.regra_id)] : [])), obs: fornecedor.obs || '', endereco: fornecedor.endereco || '', bairro: fornecedor.bairro || '', cidade: fornecedor.cidade || '', uf: fornecedor.uf || '', - cep: fornecedor.cep || '', + cep: maskCEP(fornecedor.cep || ''), status: fornecedor.status || 'Ativo', moeda_padrao: fornecedor.moeda_padrao || 'BRL', periodo_vencimento: fornecedor.periodo_vencimento || 'Pagar no recebimento' @@ -261,7 +267,7 @@ export const FornecedoresView = () => { cpf_cnpj: '', tipo_pessoa: 'JURIDICA', servicos: [], - idregra: '', + regras: [], valor: '', obs: '', endereco: '', @@ -292,8 +298,8 @@ export const FornecedoresView = () => { telefone: cleanTelefone, cpf_cnpj: cleanCPFCNPJ, tipo_pessoa: formData.tipo_pessoa || 'JURIDICA', - servico: formData.servicos, // Envia como lista conforme exigido pelo backend - idregra: formData.idregra ? Number(formData.idregra) : null, // Novo campo de regra + servico: formData.servicos, // Envia como lista + idregra: formData.regras.map(Number), // Envia lista de IDs de regras obs: formData.obs || '', // Campos opcionais status: formData.status || 'Ativo', @@ -653,14 +659,23 @@ export const FornecedoresView = () => {
- -
- - {getRuleName(selectedFornecedor.regra_id || selectedFornecedor.idregra)} - - - (ID: {selectedFornecedor.regra_id || selectedFornecedor.idregra || 'N/D'}) - + +
+ {(() => { + const regraIds = Array.isArray(selectedFornecedor.idregra) + ? selectedFornecedor.idregra + : (selectedFornecedor.idregra ? [selectedFornecedor.idregra] : (selectedFornecedor.regra_id ? [selectedFornecedor.regra_id] : [])); + + if (regraIds.length === 0) { + return

Nenhuma regra vinculada

; + } + + return regraIds.map((rid, idx) => ( + + {getRuleName(rid)} (#{rid}) + + )); + })()}
@@ -1150,11 +1165,49 @@ export const FornecedoresView = () => {
+ {/* Input simples para atribuir serviços */} +
+ { + if (e.key === 'Enter') { + e.preventDefault(); + const val = e.target.value.trim(); + if (val && !formData.servicos.includes(val)) { + setFormData({ ...formData, servicos: [...formData.servicos, val] }); + toast.success(`Serviço "${val}" atribuído!`); + } + e.target.value = ''; + } + }} + /> + +
+ + {/* [AutoFillInput comentado para reutilização futura] { if (selectedSvc && !formData.servicos.includes(selectedSvc.servico)) { setFormData({ @@ -1174,6 +1227,7 @@ export const FornecedoresView = () => { }} className="bg-white dark:bg-slate-800" /> + */} {/* Lista de serviços selecionados */}
@@ -1202,32 +1256,70 @@ export const FornecedoresView = () => {
- {/* Regra Vinculada */} + {/* Regras de Conciliação - Multi-seleção */}
- +
+ +
+ {/* Regras selecionadas como badges */} +
+ {formData.regras.map((regraId, idx) => { + const ruleObj = availableRules.find(r => Number(r.id ?? r.idregras_financeiro) === regraId); + return ( + + {ruleObj ? (ruleObj.regra || ruleObj.nome) : `Regra #${regraId}`} + + + ); + })} + {formData.regras.length === 0 && ( +

Nenhuma regra selecionada

+ )} +

- Define a regra automática para transações deste fornecedor. + Define as regras automáticas para transações deste fornecedor.

+
{ {/* Dialog de Agendamento */} - + - + Agendar Boleto
Formulário para agendar o vencimento do boleto selecionado
@@ -884,22 +884,22 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => { {selectedBoleto && (
-
-
@@ -909,7 +909,7 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => { @@ -1169,28 +1169,28 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => { function CancelamentoDialog({ open, onOpenChange, onConfirm, loading, reason, setReason, boleto }) { return ( - + - + Cancelar Boleto
Confirmação de cancelamento de boleto
-
+

Você está prestes a cancelar o boleto {boleto?.numero} de {boleto?.pagador || boleto?.cliente} no valor de {boleto?.valorNominal ? parseFloat(boleto?.valorNominal).toLocaleString('pt-BR', {style: 'currency', currency:'BRL'}) : 'R$ 0,00'}.

-