- Redesigned category create/edit modal with elegant wizard-style UI - Redesigned batch categorization modal with visual cards and better preview - Added missing i18n translations (common.continue, creating, remove) - Added budgets.general and wizard translations for ES, PT-BR, EN - Fixed 3 demo user transactions that were missing categories
1313 lines
59 KiB
JavaScript
1313 lines
59 KiB
JavaScript
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 (
|
|
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
|
|
<div className="modal-dialog modal-dialog-centered modal-md">
|
|
<div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
|
|
{/* Header elegante como o wizard */}
|
|
<div className="modal-header border-0 pb-0">
|
|
<div>
|
|
<h5 className="modal-title text-white mb-1">
|
|
<i className={`bi ${editBudget ? 'bi-pencil-square' : 'bi-plus-circle-dotted'} me-2`}></i>
|
|
{editBudget ? t('budgets.editBudget') : t('budgets.newBudget')}
|
|
</h5>
|
|
<p className="text-slate-400 mb-0 small">
|
|
<i className="bi bi-calendar3 me-1"></i>
|
|
{months.find(m => m.value === (month || new Date().getMonth() + 1))?.label} {year || new Date().getFullYear()}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn-close btn-close-white"
|
|
onClick={onClose}
|
|
></button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSingleSubmit}>
|
|
<div className="modal-body pt-3" style={{ maxHeight: '65vh', overflowY: 'auto' }}>
|
|
|
|
{/* Category Selection - Cards bonitos */}
|
|
<div className="mb-4">
|
|
<label className="form-label text-white fw-medium mb-2">
|
|
<i className="bi bi-tags me-2 text-primary"></i>
|
|
{t('budgets.category')} *
|
|
</label>
|
|
{editBudget ? (
|
|
<div className="p-3 rounded d-flex align-items-center" style={{ background: '#0f172a' }}>
|
|
<div
|
|
className="rounded-circle d-flex align-items-center justify-content-center me-3"
|
|
style={{ width: 40, height: 40, background: `${editBudget.category?.color || '#6b7280'}20` }}
|
|
>
|
|
<i className={`bi ${editBudget.category?.icon || 'bi-tag'}`}
|
|
style={{ fontSize: '1.2rem', color: editBudget.category?.color || '#6b7280' }}></i>
|
|
</div>
|
|
<span className="text-white">{editBudget.category?.name}</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{availableCategories.length === 0 && !loading ? (
|
|
<div className="p-3 rounded text-center" style={{ background: '#0f172a' }}>
|
|
<i className="bi bi-check-circle text-warning d-block mb-2" style={{ fontSize: '2rem' }}></i>
|
|
<span className="text-slate-400">{t('budgets.allCategoriesUsed')}</span>
|
|
</div>
|
|
) : (
|
|
<div className="row g-2">
|
|
{availableCategories.map(cat => (
|
|
<div key={cat.id} className="col-4 col-md-3">
|
|
<div
|
|
onClick={() => 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'
|
|
}}
|
|
>
|
|
<div className="card-body p-2 text-center position-relative">
|
|
<div
|
|
className="rounded-circle d-inline-flex align-items-center justify-content-center mb-1"
|
|
style={{ width: 36, height: 36, background: `${cat.color || '#6b7280'}20` }}
|
|
>
|
|
<i className={`bi ${cat.icon || 'bi-tag'}`} style={{ fontSize: '1rem', color: cat.color || '#6b7280' }}></i>
|
|
</div>
|
|
<small className="text-white d-block text-truncate" title={cat.name} style={{ fontSize: '0.75rem' }}>
|
|
{cat.name}
|
|
</small>
|
|
{singleForm.category_id == cat.id && (
|
|
<i className="bi bi-check-circle-fill text-primary position-absolute"
|
|
style={{ top: 4, right: 4, fontSize: '0.8rem' }}></i>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Subcategory Selection - Visual melhorado */}
|
|
{singleForm.category_id && !editBudget && subcategories.length > 0 && (
|
|
<div className="mb-4">
|
|
<label className="form-label text-white fw-medium mb-2">
|
|
<i className="bi bi-bookmark me-2 text-info"></i>
|
|
{t('budgets.subcategory')}
|
|
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
|
|
</label>
|
|
<div className="row g-2">
|
|
{/* Opção "Toda la categoría" */}
|
|
<div className="col-6 col-md-4">
|
|
<div
|
|
onClick={() => 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'
|
|
}}
|
|
>
|
|
<i className={`bi ${selectedCategory?.icon || 'bi-tag'} d-block mb-1`}
|
|
style={{ color: selectedCategory?.color || '#6b7280' }}></i>
|
|
<small className="text-white">{t('budgets.allCategory')}</small>
|
|
</div>
|
|
</div>
|
|
{subcategories.map(sub => (
|
|
<div key={sub.id} className="col-6 col-md-4">
|
|
<div
|
|
onClick={() => 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'
|
|
}}
|
|
>
|
|
<i className={`bi ${sub.icon || 'bi-tag'} d-block mb-1`}
|
|
style={{ color: sub.color || '#6b7280' }}></i>
|
|
<small className="text-white text-truncate d-block" title={sub.name}>{sub.name}</small>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cost Center Selection - Visual melhorado */}
|
|
{!editBudget && costCenters.length > 0 && (
|
|
<div className="mb-4">
|
|
<label className="form-label text-white fw-medium mb-2">
|
|
<i className="bi bi-building me-2 text-warning"></i>
|
|
{t('budgets.costCenter')}
|
|
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
|
|
</label>
|
|
<div className="row g-2">
|
|
<div className="col-6 col-md-4">
|
|
<div
|
|
onClick={() => 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'
|
|
}}
|
|
>
|
|
<i className="bi bi-dash-circle d-block mb-1 text-slate-400"></i>
|
|
<small className="text-white">{t('budgets.noCostCenter') || 'Sin centro'}</small>
|
|
</div>
|
|
</div>
|
|
{costCenters.map(cc => (
|
|
<div key={cc.id} className="col-6 col-md-4">
|
|
<div
|
|
onClick={() => 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'
|
|
}}
|
|
>
|
|
<i className={`bi ${cc.icon || 'bi-building'} d-block mb-1`}
|
|
style={{ color: cc.color || '#eab308' }}></i>
|
|
<small className="text-white text-truncate d-block" title={cc.name}>{cc.name}</small>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Amount - Card destacado */}
|
|
<div className="mb-4">
|
|
<label className="form-label text-white fw-medium mb-2">
|
|
<i className="bi bi-currency-euro me-2 text-success"></i>
|
|
{t('budgets.amount')} *
|
|
</label>
|
|
<div className="p-3 rounded" style={{ background: '#0f172a' }}>
|
|
<div className="input-group input-group-lg">
|
|
<span className="input-group-text bg-transparent border-0 text-white" style={{ fontSize: '1.5rem' }}>
|
|
{getCurrencyByCode(primaryCurrency)?.symbol || '€'}
|
|
</span>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0.01"
|
|
className="form-control bg-transparent border-0 text-white text-center"
|
|
style={{ fontSize: '2rem', fontWeight: 'bold' }}
|
|
value={singleForm.amount}
|
|
onChange={(e) => setSingleForm({...singleForm, amount: e.target.value})}
|
|
placeholder="0,00"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Period Type & Cumulative - Linha compacta */}
|
|
{!editBudget && (
|
|
<div className="row g-3 mb-3">
|
|
<div className="col-md-6">
|
|
<label className="form-label text-slate-400 small mb-1">{t('budgets.periodType')}</label>
|
|
<select
|
|
className="form-select form-select-sm bg-dark border-secondary text-white"
|
|
value={singleForm.period_type}
|
|
onChange={(e) => setSingleForm({...singleForm, period_type: e.target.value})}
|
|
>
|
|
<option value="monthly">{t('budgets.monthly')}</option>
|
|
<option value="bimestral">{t('budgets.bimestral')}</option>
|
|
<option value="trimestral">{t('budgets.trimestral')}</option>
|
|
<option value="semestral">{t('budgets.semestral')}</option>
|
|
<option value="yearly">{t('budgets.yearly')}</option>
|
|
</select>
|
|
</div>
|
|
<div className="col-md-6 d-flex align-items-end">
|
|
<div className="form-check form-switch">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
id="isCumulativeSingle"
|
|
checked={singleForm.is_cumulative}
|
|
onChange={(e) => setSingleForm({...singleForm, is_cumulative: e.target.checked})}
|
|
/>
|
|
<label className="form-check-label text-slate-400 small" htmlFor="isCumulativeSingle">
|
|
{t('budgets.isCumulative')}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Info - Discreto */}
|
|
{!editBudget && (
|
|
<div className="small text-slate-500 text-center">
|
|
<i className="bi bi-info-circle me-1"></i>
|
|
{t('budgets.autoPropagateInfo')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer elegante */}
|
|
<div className="modal-footer border-0 pt-0">
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline-secondary"
|
|
onClick={onClose}
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-success px-4"
|
|
disabled={loading || (!editBudget && (!singleForm.category_id || !singleForm.amount))}
|
|
>
|
|
{loading ? (
|
|
<span className="spinner-border spinner-border-sm me-2"></span>
|
|
) : (
|
|
<i className="bi bi-check-lg me-1"></i>
|
|
)}
|
|
{editBudget ? t('common.save') : t('budgets.wizard.createBudget') || 'Crear Presupuesto'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ==================== RENDER MODO WIZARD ====================
|
|
return (
|
|
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
|
|
<div className="modal-dialog modal-dialog-centered modal-lg">
|
|
<div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
|
|
{/* Header */}
|
|
<div className="modal-header border-0 pb-0">
|
|
<div>
|
|
<h5 className="modal-title text-white mb-1">
|
|
<i className="bi bi-magic me-2"></i>
|
|
{t('budgets.wizard.title') || 'Asistente de Presupuestos'}
|
|
</h5>
|
|
<p className="text-slate-400 mb-0 small">
|
|
{months.find(m => m.value === (month || new Date().getMonth() + 1))?.label} {year || new Date().getFullYear()}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn-close btn-close-white"
|
|
onClick={onClose}
|
|
></button>
|
|
</div>
|
|
|
|
{/* Progress Steps */}
|
|
<div className="px-4 py-3">
|
|
<div className="d-flex justify-content-between align-items-center">
|
|
{[1, 2, 3, 4].map((s) => (
|
|
<React.Fragment key={s}>
|
|
<div
|
|
className={`d-flex align-items-center justify-content-center rounded-circle ${
|
|
step >= s ? 'bg-primary' : 'bg-slate-600'
|
|
}`}
|
|
style={{ width: 32, height: 32, transition: 'all 0.3s' }}
|
|
>
|
|
{step > s ? (
|
|
<i className="bi bi-check text-white"></i>
|
|
) : (
|
|
<span className="text-white small">{s}</span>
|
|
)}
|
|
</div>
|
|
{s < 4 && (
|
|
<div
|
|
className={`flex-grow-1 mx-2 ${step > s ? 'bg-primary' : 'bg-slate-600'}`}
|
|
style={{ height: 2, transition: 'all 0.3s' }}
|
|
></div>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
<div className="d-flex justify-content-between mt-1">
|
|
<small className={step === 1 ? 'text-primary' : 'text-slate-500'}>
|
|
{t('budgets.wizard.step1') || 'Modo'}
|
|
</small>
|
|
<small className={step === 2 ? 'text-primary' : 'text-slate-500'}>
|
|
{t('budgets.wizard.step2') || 'Categorías'}
|
|
</small>
|
|
<small className={step === 3 ? 'text-primary' : 'text-slate-500'}>
|
|
{t('budgets.wizard.step3') || 'Valores'}
|
|
</small>
|
|
<small className={step === 4 ? 'text-primary' : 'text-slate-500'}>
|
|
{t('budgets.wizard.step4') || 'Confirmar'}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="modal-body" style={{ maxHeight: '55vh', overflowY: 'auto' }}>
|
|
{loading ? (
|
|
<div className="text-center py-5">
|
|
<div className="spinner-border text-primary" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Step 1: Escolher modo */}
|
|
{step === 1 && (
|
|
<div className="row g-3">
|
|
{/* Templates */}
|
|
<div className="col-12">
|
|
<h6 className="text-slate-400 mb-3">
|
|
<i className="bi bi-lightning me-2"></i>
|
|
{t('budgets.wizard.quickStart') || 'Inicio Rápido con Plantillas'}
|
|
</h6>
|
|
<div className="row g-2">
|
|
{Object.entries(budgetTemplates).map(([key, template]) => (
|
|
<div key={key} className="col-6 col-md-3">
|
|
<div
|
|
onClick={() => 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)'}
|
|
>
|
|
<div className="card-body text-center p-3">
|
|
<div
|
|
className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
|
|
style={{ width: 48, height: 48, background: `${template.color}20` }}
|
|
>
|
|
<i className={`bi ${template.icon}`} style={{ fontSize: '1.5rem', color: template.color }}></i>
|
|
</div>
|
|
<h6 className="text-white mb-1 small">{template.name}</h6>
|
|
<small className="text-slate-400 d-block" style={{ fontSize: '0.7rem' }}>
|
|
{template.description}
|
|
</small>
|
|
<small className="text-primary d-block mt-1">
|
|
≈ {currency(template.suggested_total, primaryCurrency)}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<hr className="border-secondary my-3" />
|
|
</div>
|
|
|
|
{/* Manual & Copy */}
|
|
<div className="col-md-6">
|
|
<div
|
|
onClick={startManualMode}
|
|
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)'}
|
|
>
|
|
<div className="card-body p-4">
|
|
<div className="d-flex align-items-center mb-3">
|
|
<div className="rounded-circle d-flex align-items-center justify-content-center me-3"
|
|
style={{ width: 48, height: 48, background: 'rgba(59, 130, 246, 0.2)' }}>
|
|
<i className="bi bi-pencil-square text-primary" style={{ fontSize: '1.5rem' }}></i>
|
|
</div>
|
|
<div>
|
|
<h6 className="text-white mb-0">{t('budgets.wizard.manual') || 'Crear Manualmente'}</h6>
|
|
<small className="text-slate-400">{t('budgets.wizard.manualDesc') || 'Elige categorías y valores'}</small>
|
|
</div>
|
|
</div>
|
|
<div className="text-end">
|
|
<i className="bi bi-arrow-right text-primary"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-md-6">
|
|
<div className="card border-0 h-100" style={{ background: '#0f172a' }}>
|
|
<div className="card-body p-4">
|
|
<div className="d-flex align-items-center mb-3">
|
|
<div className="rounded-circle d-flex align-items-center justify-content-center me-3"
|
|
style={{ width: 48, height: 48, background: 'rgba(16, 185, 129, 0.2)' }}>
|
|
<i className="bi bi-copy text-success" style={{ fontSize: '1.5rem' }}></i>
|
|
</div>
|
|
<div>
|
|
<h6 className="text-white mb-0">{t('budgets.wizard.copy') || 'Copiar de Otro Mes'}</h6>
|
|
<small className="text-slate-400">{t('budgets.wizard.copyDesc') || 'Reutiliza presupuestos'}</small>
|
|
</div>
|
|
</div>
|
|
<div className="d-flex gap-2 mb-2">
|
|
<select
|
|
className="form-select form-select-sm bg-dark border-secondary text-white"
|
|
value={copySource.month}
|
|
onChange={(e) => setCopySource({ ...copySource, month: parseInt(e.target.value) })}
|
|
>
|
|
{months.map(m => (
|
|
<option key={m.value} value={m.value}>{m.label}</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
className="form-select form-select-sm bg-dark border-secondary text-white"
|
|
value={copySource.year}
|
|
onChange={(e) => setCopySource({ ...copySource, year: parseInt(e.target.value) })}
|
|
>
|
|
{[2024, 2025, 2026].map(y => (
|
|
<option key={y} value={y}>{y}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<button className="btn btn-outline-success btn-sm w-100" onClick={startCopyMode}>
|
|
<i className="bi bi-arrow-right me-1"></i>
|
|
{t('budgets.wizard.loadBudgets') || 'Cargar Presupuestos'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Selecionar categorias (modo manual) */}
|
|
{step === 2 && (
|
|
<div>
|
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 className="text-white mb-0">
|
|
<i className="bi bi-tags me-2"></i>
|
|
{t('budgets.wizard.selectCategories') || 'Selecciona las Categorías'}
|
|
</h6>
|
|
<div>
|
|
<button
|
|
className="btn btn-outline-secondary btn-sm me-2"
|
|
onClick={() => setBudgetItems(prev => prev.map(i => ({ ...i, selected: false })))}
|
|
>
|
|
{t('common.deselectAll') || 'Desmarcar'}
|
|
</button>
|
|
<button
|
|
className="btn btn-outline-primary btn-sm"
|
|
onClick={() => setBudgetItems(prev => prev.map(i => ({ ...i, selected: true })))}
|
|
>
|
|
{t('common.selectAll') || 'Seleccionar Todas'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="row g-2">
|
|
{budgetItems.map(item => (
|
|
<div key={item.id} className="col-6 col-md-4 col-lg-3">
|
|
<div
|
|
onClick={() => 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',
|
|
}}
|
|
>
|
|
<div className="card-body p-3 text-center position-relative">
|
|
<div
|
|
className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
|
|
style={{ width: 40, height: 40, background: `${item.color}20` }}
|
|
>
|
|
<i className={`bi ${item.icon}`} style={{ fontSize: '1.2rem', color: item.color }}></i>
|
|
</div>
|
|
<h6 className="text-white mb-1 small text-truncate" title={item.name}>
|
|
{item.name}
|
|
</h6>
|
|
{item.history_avg > 0 && (
|
|
<small className="text-slate-400 d-block" style={{ fontSize: '0.7rem' }}>
|
|
<i className="bi bi-clock-history me-1"></i>
|
|
{currency(item.history_avg, primaryCurrency)}
|
|
</small>
|
|
)}
|
|
{item.selected && (
|
|
<i className="bi bi-check-circle-fill text-primary position-absolute"
|
|
style={{ top: 8, right: 8 }}></i>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-3 p-3 rounded" style={{ background: '#0f172a' }}>
|
|
<div className="d-flex justify-content-between align-items-center">
|
|
<span className="text-slate-400">
|
|
{selectedItems.length} {t('budgets.wizard.categoriesSelected') || 'categorías seleccionadas'}
|
|
</span>
|
|
<div>
|
|
<button className="btn btn-outline-secondary me-2" onClick={() => setStep(1)}>
|
|
<i className="bi bi-arrow-left me-1"></i>
|
|
{t('common.back') || 'Volver'}
|
|
</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => setStep(3)}
|
|
disabled={selectedItems.length === 0}
|
|
>
|
|
{t('common.continue') || 'Continuar'}
|
|
<i className="bi bi-arrow-right ms-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Definir valores */}
|
|
{step === 3 && (
|
|
<div>
|
|
<h6 className="text-white mb-3">
|
|
<i className="bi bi-currency-euro me-2"></i>
|
|
{t('budgets.wizard.setAmounts') || 'Define los Valores'}
|
|
</h6>
|
|
|
|
<div className="table-responsive mb-3">
|
|
<table className="table table-dark table-hover mb-0" style={{ fontSize: '0.9rem' }}>
|
|
<thead>
|
|
<tr>
|
|
<th>{t('budgets.category') || 'Categoría'}</th>
|
|
<th style={{ width: 140 }}>{t('budgets.subcategory') || 'Subcategoría'}</th>
|
|
{costCenters.length > 0 && (
|
|
<th style={{ width: 130 }}>{t('budgets.costCenter') || 'Centro'}</th>
|
|
)}
|
|
<th className="text-end" style={{ width: 90 }}>{t('budgets.wizard.history') || 'Hist.'}</th>
|
|
<th style={{ width: 120 }}>{t('budgets.amount') || 'Valor'}</th>
|
|
<th style={{ width: 40 }}></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{selectedItems.map(item => (
|
|
<tr key={item.id}>
|
|
<td>
|
|
<div className="d-flex align-items-center">
|
|
<i className={`bi ${item.icon} me-2`} style={{ color: item.color }}></i>
|
|
<span className="text-white">{item.name}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{item.subcategories && item.subcategories.length > 0 ? (
|
|
<select
|
|
className="form-select form-select-sm bg-dark border-secondary text-white"
|
|
value={item.subcategory_id || ''}
|
|
onChange={(e) => updateItemSubcategory(item.category_id, e.target.value)}
|
|
>
|
|
<option value="">{t('budgets.allCategory') || 'Toda'}</option>
|
|
{item.subcategories.map(sub => (
|
|
<option key={sub.id} value={sub.id}>{sub.name}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<span className="text-slate-500">-</span>
|
|
)}
|
|
</td>
|
|
{costCenters.length > 0 && (
|
|
<td>
|
|
<select
|
|
className="form-select form-select-sm bg-dark border-secondary text-white"
|
|
value={item.cost_center_id || ''}
|
|
onChange={(e) => updateItemCostCenter(item.category_id, e.target.value)}
|
|
>
|
|
<option value="">-</option>
|
|
{costCenters.map(cc => (
|
|
<option key={cc.id} value={cc.id}>{cc.name}</option>
|
|
))}
|
|
</select>
|
|
</td>
|
|
)}
|
|
<td className="text-end">
|
|
{item.history_avg > 0 ? (
|
|
<button
|
|
className="btn btn-link btn-sm text-info p-0"
|
|
onClick={() => updateItemAmount(item.category_id, item.history_avg)}
|
|
title={t('budgets.wizard.useHistory') || 'Usar promedio'}
|
|
>
|
|
{currency(item.history_avg, primaryCurrency)}
|
|
</button>
|
|
) : (
|
|
<span className="text-slate-500">-</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
<div className="input-group input-group-sm">
|
|
<span className="input-group-text bg-dark border-secondary text-white">
|
|
{getCurrencyByCode(primaryCurrency)?.symbol || '€'}
|
|
</span>
|
|
<input
|
|
type="number"
|
|
step="10"
|
|
min="0"
|
|
className="form-control bg-dark border-secondary text-white"
|
|
value={item.amount || ''}
|
|
onChange={(e) => updateItemAmount(item.category_id, e.target.value)}
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<button
|
|
className="btn btn-link btn-sm text-danger p-0"
|
|
onClick={() => toggleCategory(item.category_id)}
|
|
title={t('common.remove') || 'Eliminar'}
|
|
>
|
|
<i className="bi bi-x-lg"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr className="border-top">
|
|
<td colSpan={costCenters.length > 0 ? 4 : 3} className="text-white fw-bold">
|
|
{t('budgets.total') || 'Total'}
|
|
</td>
|
|
<td className="text-white fw-bold">
|
|
{currency(totalBudget, primaryCurrency)}
|
|
</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="text-end">
|
|
<button className="btn btn-outline-secondary me-2" onClick={() => setStep(creationMode === 'manual' ? 2 : 1)}>
|
|
<i className="bi bi-arrow-left me-1"></i>
|
|
{t('common.back') || 'Volver'}
|
|
</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => setStep(4)}
|
|
disabled={totalBudget <= 0}
|
|
>
|
|
{t('common.continue') || 'Continuar'}
|
|
<i className="bi bi-arrow-right ms-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 4: Confirmar */}
|
|
{step === 4 && (
|
|
<div>
|
|
<h6 className="text-white mb-3">
|
|
<i className="bi bi-check2-square me-2"></i>
|
|
{t('budgets.wizard.confirm') || 'Confirma los Presupuestos'}
|
|
</h6>
|
|
|
|
{/* Summary */}
|
|
<div className="card border-0 mb-3" style={{ background: '#0f172a' }}>
|
|
<div className="card-body">
|
|
<div className="row text-center">
|
|
<div className="col-4">
|
|
<h3 className="text-primary mb-0">{selectedItems.length}</h3>
|
|
<small className="text-slate-400">{t('budgets.wizard.budgets') || 'Presupuestos'}</small>
|
|
</div>
|
|
<div className="col-4">
|
|
<h3 className="text-white mb-0">{currency(totalBudget, primaryCurrency)}</h3>
|
|
<small className="text-slate-400">{t('budgets.totalBudgeted') || 'Total'}</small>
|
|
</div>
|
|
<div className="col-4">
|
|
<h3 className="text-success mb-0">
|
|
{globalOptions.period_type === 'monthly' ? '12' :
|
|
globalOptions.period_type === 'bimestral' ? '6' :
|
|
globalOptions.period_type === 'trimestral' ? '4' :
|
|
globalOptions.period_type === 'semestral' ? '2' : '1'}
|
|
</h3>
|
|
<small className="text-slate-400">{t('budgets.wizard.periods') || 'Períodos'}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Options */}
|
|
<div className="row mb-3">
|
|
<div className="col-md-6">
|
|
<label className="form-label text-slate-400">{t('budgets.periodType') || 'Período'}</label>
|
|
<select
|
|
className="form-select bg-dark border-secondary text-white"
|
|
value={globalOptions.period_type}
|
|
onChange={(e) => setGlobalOptions({ ...globalOptions, period_type: e.target.value })}
|
|
>
|
|
<option value="monthly">{t('budgets.monthly') || 'Mensual'}</option>
|
|
<option value="bimestral">{t('budgets.bimestral') || 'Bimestral'}</option>
|
|
<option value="trimestral">{t('budgets.trimestral') || 'Trimestral'}</option>
|
|
<option value="semestral">{t('budgets.semestral') || 'Semestral'}</option>
|
|
<option value="yearly">{t('budgets.yearly') || 'Anual'}</option>
|
|
</select>
|
|
</div>
|
|
<div className="col-md-6 d-flex align-items-end">
|
|
<div className="form-check form-switch">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
id="isCumulativeWizard"
|
|
checked={globalOptions.is_cumulative}
|
|
onChange={(e) => setGlobalOptions({ ...globalOptions, is_cumulative: e.target.checked })}
|
|
/>
|
|
<label className="form-check-label text-slate-400" htmlFor="isCumulativeWizard">
|
|
{t('budgets.isCumulative') || 'Acumulativo'}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* List */}
|
|
<div className="mb-3" style={{ maxHeight: '180px', overflowY: 'auto' }}>
|
|
{selectedItems.map(item => (
|
|
<div key={item.id} className="d-flex justify-content-between align-items-center py-2 border-bottom border-secondary">
|
|
<div className="d-flex align-items-center">
|
|
<i className={`bi ${item.icon} me-2`} style={{ color: item.color }}></i>
|
|
<span className="text-white">{item.name}</span>
|
|
{item.subcategory_id && (
|
|
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>
|
|
{item.subcategories?.find(s => s.id == item.subcategory_id)?.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className="text-primary fw-bold">{currency(item.amount, primaryCurrency)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="alert alert-info py-2">
|
|
<small>
|
|
<i className="bi bi-info-circle me-1"></i>
|
|
{t('budgets.autoPropagateInfo') || 'Los presupuestos se propagarán automáticamente'}
|
|
</small>
|
|
</div>
|
|
|
|
<div className="text-end">
|
|
<button className="btn btn-outline-secondary me-2" onClick={() => setStep(3)}>
|
|
<i className="bi bi-arrow-left me-1"></i>
|
|
{t('common.back') || 'Volver'}
|
|
</button>
|
|
<button className="btn btn-success" onClick={handleWizardSubmit} disabled={loading}>
|
|
{loading ? (
|
|
<>
|
|
<span className="spinner-border spinner-border-sm me-2"></span>
|
|
{t('common.creating') || 'Creando...'}
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-check-lg me-1"></i>
|
|
{t('budgets.wizard.createBudgets') || 'Crear Presupuestos'}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{step === 1 && (
|
|
<div className="modal-footer border-0 pt-0">
|
|
<button type="button" className="btn btn-outline-secondary" onClick={onClose}>
|
|
{t('common.cancel') || 'Cancelar'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BudgetWizard;
|