import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { budgetService, categoryService, costCenterService, reportService } from '../services/api'; import { useToast } from './Toast'; import { getCurrencyByCode } from '../config/currencies'; import useFormatters from '../hooks/useFormatters'; /** * BudgetWizard - Wizard avançado para criação de orçamentos * * Modos: * - 'wizard': Modo wizard completo (templates, batch, copiar) * - 'single': Modo simples para criar/editar um único orçamento (substitui modal antigo) * * Features: * - Multi-step wizard com indicador de progresso * - Templates predefinidos (básico, familiar, individual) * - Criação em lote de múltiplos orçamentos * - Sugestões baseadas em histórico de gastos * - Seleção de subcategorias * - Seleção de centro de custos * - Período flexível (mensal, bimestral, trimestral, semestral, anual) */ const BudgetWizard = ({ isOpen, onClose, onSuccess, year, month, mode = 'wizard', // 'wizard' ou 'single' editBudget = null, // Para edição no modo 'single' }) => { const { t } = useTranslation(); const toast = useToast(); const { currency } = useFormatters(); // Estados do wizard const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [categories, setCategories] = useState([]); const [costCenters, setCostCenters] = useState([]); const [availableCategories, setAvailableCategories] = useState([]); const [expenseHistory, setExpenseHistory] = useState({}); const [primaryCurrency, setPrimaryCurrency] = useState('EUR'); // Modo de criação (para wizard) const [creationMode, setCreationMode] = useState(''); // 'template', 'manual', 'copy' // Template selecionado const [selectedTemplate, setSelectedTemplate] = useState(''); // Orçamentos a criar (modo wizard) const [budgetItems, setBudgetItems] = useState([]); // Dados do formulário single const [singleForm, setSingleForm] = useState({ category_id: '', subcategory_id: '', cost_center_id: '', amount: '', period_type: 'monthly', is_cumulative: false, }); // Opções globais (modo wizard) const [globalOptions, setGlobalOptions] = useState({ period_type: 'monthly', is_cumulative: false, }); // Período para copiar const [copySource, setCopySource] = useState({ year: year || new Date().getFullYear(), month: (month || new Date().getMonth() + 1) - 1 || 12, }); // Templates predefinidos const budgetTemplates = { basico: { name: t('budgets.wizard.templates.basic.name') || 'Presupuesto Básico', description: t('budgets.wizard.templates.basic.desc') || 'Esencial para control mensual', icon: 'bi-wallet', color: '#3b82f6', categories: ['Vivienda', 'Alimentación', 'Transporte', 'Servicios'], suggested_total: 1500, }, familiar: { name: t('budgets.wizard.templates.family.name') || 'Presupuesto Familiar', description: t('budgets.wizard.templates.family.desc') || 'Completo para familias', icon: 'bi-house-heart', color: '#10b981', categories: ['Vivienda', 'Alimentación', 'Transporte', 'Servicios', 'Salud', 'Educación', 'Ocio', 'Ropa'], suggested_total: 3000, }, individual: { name: t('budgets.wizard.templates.individual.name') || 'Presupuesto Individual', description: t('budgets.wizard.templates.individual.desc') || 'Para persona soltera', icon: 'bi-person', color: '#8b5cf6', categories: ['Vivienda', 'Alimentación', 'Transporte', 'Ocio', 'Salud'], suggested_total: 1200, }, completo: { name: t('budgets.wizard.templates.complete.name') || 'Presupuesto Completo', description: t('budgets.wizard.templates.complete.desc') || 'Todas las categorías', icon: 'bi-clipboard-data', color: '#f59e0b', categories: 'all', suggested_total: 4000, }, }; // Carregar dados ao abrir useEffect(() => { if (isOpen) { loadData(); resetWizard(); } }, [isOpen]); // Preencher form para edição useEffect(() => { if (mode === 'single' && editBudget) { setSingleForm({ category_id: editBudget.category_id || '', subcategory_id: editBudget.subcategory_id || '', cost_center_id: editBudget.cost_center_id || '', amount: editBudget.amount || '', period_type: editBudget.period_type || 'monthly', is_cumulative: editBudget.is_cumulative || false, }); } }, [mode, editBudget]); const loadData = async () => { try { setLoading(true); const [categoriesData, costCentersData, availableData] = await Promise.all([ categoryService.getAll(), costCenterService.getAll(), budgetService.getAvailableCategories({ year, month }), ]); const cats = categoriesData?.data || categoriesData; // Filtrar apenas categorias de gastos (com subcategorias aninhadas) const expenseCategories = Array.isArray(cats) ? cats.filter(c => c.type === 'expense' || c.type === 'both') : []; setCategories(expenseCategories); // Categorias disponíveis (não usadas ainda) const available = Array.isArray(availableData) ? availableData : []; setAvailableCategories(available); // Centros de custo const centers = costCentersData?.data || costCentersData; setCostCenters(Array.isArray(centers) ? centers : []); // Detectar moeda do usuário const user = JSON.parse(localStorage.getItem('user') || '{}'); setPrimaryCurrency(user.primary_currency || 'EUR'); // Carregar histórico de gastos (em paralelo) loadExpenseHistory(); } catch (error) { console.error('Error loading data:', error); } finally { setLoading(false); } }; const loadExpenseHistory = async () => { try { const lastMonthStart = new Date(); lastMonthStart.setMonth(lastMonthStart.getMonth() - 3); const data = await reportService.getByCategory({ start_date: lastMonthStart.toISOString().split('T')[0], end_date: new Date().toISOString().split('T')[0], type: 'expense', }); const history = {}; if (data && Array.isArray(data)) { data.forEach(item => { history[item.category_id] = { average: item.total / 3, total: item.total, category_name: item.category_name, }; }); } setExpenseHistory(history); } catch (error) { console.error('Error loading expense history:', error); } }; const resetWizard = () => { setStep(1); setCreationMode(''); setSelectedTemplate(''); setBudgetItems([]); setGlobalOptions({ period_type: 'monthly', is_cumulative: false, }); setSingleForm({ category_id: '', subcategory_id: '', cost_center_id: '', amount: '', period_type: 'monthly', is_cumulative: false, }); }; // ==================== MODO SINGLE ==================== const handleSingleSubmit = async (e) => { e.preventDefault(); if (!singleForm.category_id || !singleForm.amount) { toast.warning(t('budgets.wizard.fillRequired') || 'Complete los campos obligatorios'); return; } setLoading(true); try { const data = { ...singleForm, year: year || new Date().getFullYear(), month: month || new Date().getMonth() + 1, }; if (editBudget) { await budgetService.update(editBudget.id, data); toast.success(t('budgets.wizard.updated') || 'Presupuesto actualizado'); } else { await budgetService.create(data); toast.success(t('budgets.wizard.created') || 'Presupuesto creado'); } onSuccess && onSuccess(); onClose(); } catch (error) { console.error('Error saving budget:', error); toast.error(error.response?.data?.message || t('common.error')); } finally { setLoading(false); } }; // Obter subcategorias da categoria selecionada const getSubcategories = (categoryId) => { if (!categoryId) return []; const category = availableCategories.find(c => c.id == categoryId) || categories.find(c => c.id == categoryId); return category?.subcategories || []; }; // ==================== MODO WIZARD ==================== const applyTemplate = (templateKey) => { setSelectedTemplate(templateKey); setCreationMode('template'); const template = budgetTemplates[templateKey]; if (!template) return; let selectedCategories = []; if (template.categories === 'all') { selectedCategories = categories; } else { selectedCategories = categories.filter(cat => template.categories.includes(cat.name) || template.categories.some(tc => cat.name.toLowerCase().includes(tc.toLowerCase())) ); } const totalCategories = selectedCategories.length; const baseAmount = Math.round(template.suggested_total / totalCategories); const weightMap = { 'Vivienda': 2.5, 'Alimentación': 1.5, 'Transporte': 1.0, 'Servicios': 0.8, 'Salud': 0.5, 'Educación': 0.4, 'Ocio': 0.6, 'Ropa': 0.4, }; const items = selectedCategories.map(cat => { const weight = weightMap[cat.name] || 1; const suggestedAmount = Math.round(baseAmount * weight / 10) * 10; const historyAmount = expenseHistory[cat.id]?.average || 0; return { id: cat.id, category_id: cat.id, subcategory_id: null, cost_center_id: null, name: cat.name, icon: cat.icon || 'bi-tag', color: cat.color || '#6b7280', subcategories: cat.subcategories || [], amount: historyAmount > 0 ? Math.round(historyAmount) : suggestedAmount, suggested: suggestedAmount, history_avg: Math.round(historyAmount), selected: true, }; }); setBudgetItems(items); setStep(3); }; const startManualMode = () => { setCreationMode('manual'); const items = categories.map(cat => ({ id: cat.id, category_id: cat.id, subcategory_id: null, cost_center_id: null, name: cat.name, icon: cat.icon || 'bi-tag', color: cat.color || '#6b7280', subcategories: cat.subcategories || [], amount: Math.round(expenseHistory[cat.id]?.average || 0), suggested: 0, history_avg: Math.round(expenseHistory[cat.id]?.average || 0), selected: false, })); setBudgetItems(items); setStep(2); }; const startCopyMode = async () => { setCreationMode('copy'); setLoading(true); try { const sourceData = await budgetService.getAll({ year: copySource.year, month: copySource.month, }); const budgets = sourceData?.data || sourceData || []; if (budgets.length === 0) { toast.warning(t('budgets.wizard.noSourceBudgets') || 'No hay presupuestos en este período'); setStep(1); setLoading(false); return; } const items = budgets.map(b => ({ id: b.id, category_id: b.category_id, subcategory_id: b.subcategory_id, cost_center_id: b.cost_center_id, name: b.subcategory?.name || b.category?.name || 'Presupuesto', icon: (b.subcategory || b.category)?.icon || 'bi-tag', color: (b.subcategory || b.category)?.color || '#6b7280', subcategories: [], amount: parseFloat(b.amount), suggested: parseFloat(b.amount), history_avg: 0, selected: true, })); setBudgetItems(items); setStep(3); } catch (error) { console.error('Error loading source budgets:', error); toast.error(t('common.error')); } finally { setLoading(false); } }; const toggleCategory = (categoryId) => { setBudgetItems(prev => prev.map(item => item.category_id === categoryId ? { ...item, selected: !item.selected } : item )); }; const updateItemAmount = (categoryId, amount) => { setBudgetItems(prev => prev.map(item => item.category_id === categoryId ? { ...item, amount: parseFloat(amount) || 0 } : item )); }; const updateItemSubcategory = (categoryId, subcategoryId) => { setBudgetItems(prev => prev.map(item => item.category_id === categoryId ? { ...item, subcategory_id: subcategoryId || null } : item )); }; const updateItemCostCenter = (categoryId, costCenterId) => { setBudgetItems(prev => prev.map(item => item.category_id === categoryId ? { ...item, cost_center_id: costCenterId || null } : item )); }; const selectedItems = budgetItems.filter(item => item.selected); const totalBudget = selectedItems.reduce((sum, item) => sum + (item.amount || 0), 0); const handleWizardSubmit = async () => { if (selectedItems.length === 0) { toast.warning(t('budgets.wizard.selectCategories') || 'Seleccione al menos una categoría'); return; } setLoading(true); let successCount = 0; let errorCount = 0; try { for (const item of selectedItems) { try { await budgetService.create({ category_id: item.category_id, subcategory_id: item.subcategory_id, cost_center_id: item.cost_center_id, amount: item.amount, year: year || new Date().getFullYear(), month: month || new Date().getMonth() + 1, period_type: globalOptions.period_type, is_cumulative: globalOptions.is_cumulative, }); successCount++; } catch (err) { console.error(`Error creating budget for ${item.name}:`, err); errorCount++; } } if (successCount > 0) { toast.success( t('budgets.wizard.successCount', { count: successCount }) || `${successCount} presupuesto(s) creado(s)` ); onSuccess && onSuccess(); onClose(); } if (errorCount > 0) { toast.warning( t('budgets.wizard.errorCount', { count: errorCount }) || `${errorCount} presupuesto(s) ya existían` ); } } catch (error) { console.error('Error creating budgets:', error); toast.error(t('common.error')); } finally { setLoading(false); } }; const months = [ { value: 1, label: t('months.january') || 'Enero' }, { value: 2, label: t('months.february') || 'Febrero' }, { value: 3, label: t('months.march') || 'Marzo' }, { value: 4, label: t('months.april') || 'Abril' }, { value: 5, label: t('months.may') || 'Mayo' }, { value: 6, label: t('months.june') || 'Junio' }, { value: 7, label: t('months.july') || 'Julio' }, { value: 8, label: t('months.august') || 'Agosto' }, { value: 9, label: t('months.september') || 'Septiembre' }, { value: 10, label: t('months.october') || 'Octubre' }, { value: 11, label: t('months.november') || 'Noviembre' }, { value: 12, label: t('months.december') || 'Diciembre' }, ]; if (!isOpen) return null; // ==================== RENDER MODO SINGLE ==================== if (mode === 'single') { const subcategories = getSubcategories(singleForm.category_id); const selectedCategory = availableCategories.find(c => c.id == singleForm.category_id); return (
{/* Header elegante como o wizard */}
{editBudget ? t('budgets.editBudget') : t('budgets.newBudget')}

{months.find(m => m.value === (month || new Date().getMonth() + 1))?.label} {year || new Date().getFullYear()}

{/* Category Selection - Cards bonitos */}
{editBudget ? (
{editBudget.category?.name}
) : ( <> {availableCategories.length === 0 && !loading ? (
{t('budgets.allCategoriesUsed')}
) : (
{availableCategories.map(cat => (
setSingleForm({...singleForm, category_id: cat.id, subcategory_id: ''})} className={`card border-0 h-100 ${singleForm.category_id == cat.id ? 'ring-2 ring-primary' : ''}`} style={{ background: singleForm.category_id == cat.id ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', transition: 'all 0.2s', border: singleForm.category_id == cat.id ? '2px solid #3b82f6' : '2px solid transparent' }} >
{cat.name} {singleForm.category_id == cat.id && ( )}
))}
)} )}
{/* Subcategory Selection - Visual melhorado */} {singleForm.category_id && !editBudget && subcategories.length > 0 && (
{/* Opção "Toda la categoría" */}
setSingleForm({...singleForm, subcategory_id: ''})} className="p-2 rounded text-center" style={{ background: !singleForm.subcategory_id ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', border: !singleForm.subcategory_id ? '2px solid #3b82f6' : '2px solid transparent' }} > {t('budgets.allCategory')}
{subcategories.map(sub => (
setSingleForm({...singleForm, subcategory_id: sub.id})} className="p-2 rounded text-center" style={{ background: singleForm.subcategory_id == sub.id ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', border: singleForm.subcategory_id == sub.id ? '2px solid #3b82f6' : '2px solid transparent' }} > {sub.name}
))}
)} {/* Cost Center Selection - Visual melhorado */} {!editBudget && costCenters.length > 0 && (
setSingleForm({...singleForm, cost_center_id: ''})} className="p-2 rounded text-center" style={{ background: !singleForm.cost_center_id ? 'rgba(234, 179, 8, 0.15)' : '#0f172a', cursor: 'pointer', border: !singleForm.cost_center_id ? '2px solid #eab308' : '2px solid transparent' }} > {t('budgets.noCostCenter') || 'Sin centro'}
{costCenters.map(cc => (
setSingleForm({...singleForm, cost_center_id: cc.id})} className="p-2 rounded text-center" style={{ background: singleForm.cost_center_id == cc.id ? 'rgba(234, 179, 8, 0.15)' : '#0f172a', cursor: 'pointer', border: singleForm.cost_center_id == cc.id ? '2px solid #eab308' : '2px solid transparent' }} > {cc.name}
))}
)} {/* Amount - Card destacado */}
{getCurrencyByCode(primaryCurrency)?.symbol || '€'} setSingleForm({...singleForm, amount: e.target.value})} placeholder="0,00" required />
{/* Period Type & Cumulative - Linha compacta */} {!editBudget && (
setSingleForm({...singleForm, is_cumulative: e.target.checked})} />
)} {/* Info - Discreto */} {!editBudget && (
{t('budgets.autoPropagateInfo')}
)}
{/* Footer elegante */}
); } // ==================== RENDER MODO WIZARD ==================== return (
{/* Header */}
{t('budgets.wizard.title') || 'Asistente de Presupuestos'}

{months.find(m => m.value === (month || new Date().getMonth() + 1))?.label} {year || new Date().getFullYear()}

{/* Progress Steps */}
{[1, 2, 3, 4].map((s) => (
= s ? 'bg-primary' : 'bg-slate-600' }`} style={{ width: 32, height: 32, transition: 'all 0.3s' }} > {step > s ? ( ) : ( {s} )}
{s < 4 && (
s ? 'bg-primary' : 'bg-slate-600'}`} style={{ height: 2, transition: 'all 0.3s' }} >
)}
))}
{t('budgets.wizard.step1') || 'Modo'} {t('budgets.wizard.step2') || 'Categorías'} {t('budgets.wizard.step3') || 'Valores'} {t('budgets.wizard.step4') || 'Confirmar'}
{/* Body */}
{loading ? (
Loading...
) : ( <> {/* Step 1: Escolher modo */} {step === 1 && (
{/* Templates */}
{t('budgets.wizard.quickStart') || 'Inicio Rápido con Plantillas'}
{Object.entries(budgetTemplates).map(([key, template]) => (
applyTemplate(key)} className="card border-0 h-100" style={{ background: '#0f172a', cursor: 'pointer', transition: 'all 0.2s', }} onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-2px)'} onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'} >
{template.name}
{template.description} ≈ {currency(template.suggested_total, primaryCurrency)}
))}

{/* Manual & Copy */}
e.currentTarget.style.transform = 'translateY(-2px)'} onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'} >
{t('budgets.wizard.manual') || 'Crear Manualmente'}
{t('budgets.wizard.manualDesc') || 'Elige categorías y valores'}
{t('budgets.wizard.copy') || 'Copiar de Otro Mes'}
{t('budgets.wizard.copyDesc') || 'Reutiliza presupuestos'}
)} {/* Step 2: Selecionar categorias (modo manual) */} {step === 2 && (
{t('budgets.wizard.selectCategories') || 'Selecciona las Categorías'}
{budgetItems.map(item => (
toggleCategory(item.category_id)} className={`card border-0 h-100 ${item.selected ? 'border border-primary' : ''}`} style={{ background: item.selected ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', transition: 'all 0.2s', }} >
{item.name}
{item.history_avg > 0 && ( {currency(item.history_avg, primaryCurrency)} )} {item.selected && ( )}
))}
{selectedItems.length} {t('budgets.wizard.categoriesSelected') || 'categorías seleccionadas'}
)} {/* Step 3: Definir valores */} {step === 3 && (
{t('budgets.wizard.setAmounts') || 'Define los Valores'}
{costCenters.length > 0 && ( )} {selectedItems.map(item => ( {costCenters.length > 0 && ( )} ))}
{t('budgets.category') || 'Categoría'} {t('budgets.subcategory') || 'Subcategoría'}{t('budgets.costCenter') || 'Centro'}{t('budgets.wizard.history') || 'Hist.'} {t('budgets.amount') || 'Valor'}
{item.name}
{item.subcategories && item.subcategories.length > 0 ? ( ) : ( - )} {item.history_avg > 0 ? ( ) : ( - )}
{getCurrencyByCode(primaryCurrency)?.symbol || '€'} updateItemAmount(item.category_id, e.target.value)} />
0 ? 4 : 3} className="text-white fw-bold"> {t('budgets.total') || 'Total'} {currency(totalBudget, primaryCurrency)}
)} {/* Step 4: Confirmar */} {step === 4 && (
{t('budgets.wizard.confirm') || 'Confirma los Presupuestos'}
{/* Summary */}

{selectedItems.length}

{t('budgets.wizard.budgets') || 'Presupuestos'}

{currency(totalBudget, primaryCurrency)}

{t('budgets.totalBudgeted') || 'Total'}

{globalOptions.period_type === 'monthly' ? '12' : globalOptions.period_type === 'bimestral' ? '6' : globalOptions.period_type === 'trimestral' ? '4' : globalOptions.period_type === 'semestral' ? '2' : '1'}

{t('budgets.wizard.periods') || 'Períodos'}
{/* Options */}
setGlobalOptions({ ...globalOptions, is_cumulative: e.target.checked })} />
{/* List */}
{selectedItems.map(item => (
{item.name} {item.subcategory_id && ( {item.subcategories?.find(s => s.id == item.subcategory_id)?.name} )}
{currency(item.amount, primaryCurrency)}
))}
{t('budgets.autoPropagateInfo') || 'Los presupuestos se propagarán automáticamente'}
)} )}
{/* Footer */} {step === 1 && (
)}
); }; export default BudgetWizard;