webmoney/frontend/src/components/AccountWizard.jsx
marcoitaloesp-ai 9c9d6443e7
v1.57.0: Redesign category modals + i18n updates + demo transactions fix
- 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
2025-12-18 19:06:07 +00:00

833 lines
33 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { accountService, liabilityAccountService, assetAccountService } from '../services/api';
import { useToast } from './Toast';
const AccountWizard = ({ isOpen, onClose, onSuccess, account = null }) => {
const toast = useToast();
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const isEditMode = !!account;
// Tipo de destino: 'account', 'asset' ou 'liability'
const [destinationType, setDestinationType] = useState('account');
// Form data unificado
const [formData, setFormData] = useState({
// Tipo de conta (etapa 1)
account_type: '', // checking, savings, credit_card, cash
// Dados básicos (etapa 2)
name: '',
description: '',
currency: 'EUR',
color: '#3B82F6',
icon: 'bi-bank',
// Dados financeiros (etapa 3)
initial_balance: '',
credit_limit: '', // Para cartão de crédito
// Dados bancários (etapa 4) - opcional
bank_name: '',
account_number: '',
// Configurações
is_active: true,
include_in_total: true,
// Para poupança (ativo)
interest_rate: '',
// Para cartão de crédito (passivo)
closing_day: '',
due_day: '',
annual_interest_rate: '',
});
// Definição dos tipos de conta
const accountTypes = {
checking: {
name: 'Cuenta Corriente',
description: 'Cuenta bancaria para operaciones diarias',
icon: 'bi-bank',
color: '#3B82F6',
destination: 'account',
},
savings: {
name: 'Cuenta de Ahorro',
description: 'Dinero guardado que genera intereses',
icon: 'bi-piggy-bank',
color: '#10B981',
destination: 'asset', // Poupança vira ativo
},
credit_card: {
name: 'Tarjeta de Crédito',
description: 'Línea de crédito rotativo',
icon: 'bi-credit-card',
color: '#EF4444',
destination: 'liability', // Cartão vira passivo
},
cash: {
name: 'Efectivo',
description: 'Dinero en mano o caja chica',
icon: 'bi-cash-stack',
color: '#F59E0B',
destination: 'account',
},
};
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
if (isOpen) {
if (account) {
loadAccountData(account);
} else {
resetForm();
}
}
}, [isOpen, account]);
// Atualizar destino quando tipo muda
useEffect(() => {
if (formData.account_type && accountTypes[formData.account_type]) {
const typeConfig = accountTypes[formData.account_type];
setDestinationType(typeConfig.destination);
// Atualizar ícone e cor padrão
if (!isEditMode) {
setFormData(prev => ({
...prev,
icon: typeConfig.icon,
color: typeConfig.color,
}));
}
}
}, [formData.account_type]);
const loadAccountData = (accountData) => {
// Determinar o tipo baseado nos dados
let accountType = accountData.type || accountData.account_type || 'checking';
// Se for um ativo de investimento/poupança
if (accountData.asset_type === 'cash' || accountData.investment_type === 'savings') {
accountType = 'savings';
}
// Se for um passivo de cartão
if (accountData.contract_type === 'credit_card') {
accountType = 'credit_card';
}
setStep(2);
setFormData({
account_type: accountType,
name: accountData.name || '',
description: accountData.description || '',
currency: accountData.currency || 'EUR',
color: accountData.color || '#3B82F6',
icon: accountData.icon || 'bi-bank',
initial_balance: accountData.initial_balance || accountData.current_balance || accountData.current_value || '',
credit_limit: accountData.credit_limit || accountData.principal_amount || '',
bank_name: accountData.bank_name || accountData.creditor || '',
account_number: accountData.account_number || accountData.contract_number || '',
is_active: accountData.is_active !== false,
include_in_total: accountData.include_in_total !== false,
interest_rate: accountData.interest_rate || '',
closing_day: accountData.closing_day || '',
due_day: accountData.due_day || '',
annual_interest_rate: accountData.annual_interest_rate || '',
});
};
const resetForm = () => {
setStep(1);
setDestinationType('account');
setFormData({
account_type: '',
name: '',
description: '',
currency: 'EUR',
color: '#3B82F6',
icon: 'bi-bank',
initial_balance: '',
credit_limit: '',
bank_name: '',
account_number: '',
is_active: true,
include_in_total: true,
interest_rate: '',
closing_day: '',
due_day: '',
annual_interest_rate: '',
});
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const selectAccountType = (type) => {
setFormData(prev => ({ ...prev, account_type: type }));
setStep(2);
};
const validateStep = () => {
switch (step) {
case 1:
return !!formData.account_type;
case 2:
return !!formData.name?.trim();
case 3:
if (destinationType === 'liability') {
return formData.credit_limit > 0;
}
return true; // Saldo inicial pode ser 0
case 4:
return true; // Dados bancários são opcionais
default:
return true;
}
};
const nextStep = () => {
if (validateStep()) {
setStep(prev => Math.min(prev + 1, 4));
}
};
const prevStep = () => {
setStep(prev => Math.max(prev - 1, 1));
};
const handleSubmit = async () => {
setLoading(true);
try {
let response;
if (destinationType === 'asset') {
// Criar como Ativo (Poupança)
const assetData = {
asset_type: 'cash', // Tipo cash para poupança
investment_type: 'savings',
name: formData.name,
description: formData.description,
currency: formData.currency,
color: formData.color,
acquisition_value: parseFloat(formData.initial_balance) || 0,
current_value: parseFloat(formData.initial_balance) || 0,
acquisition_date: new Date().toISOString().split('T')[0],
institution: formData.bank_name,
account_number: formData.account_number,
interest_rate: parseFloat(formData.interest_rate) || 0,
};
if (isEditMode && account) {
response = await assetAccountService.update(account.id, assetData);
} else {
response = await assetAccountService.createWithWizard(assetData);
}
} else if (destinationType === 'liability') {
// Criar como Passivo (Cartão de Crédito)
const liabilityData = {
contract_type: 'credit_card',
name: formData.name,
description: formData.description,
currency: formData.currency,
color: formData.color,
icon: formData.icon,
creditor: formData.bank_name,
contract_number: formData.account_number,
principal_amount: parseFloat(formData.credit_limit) || 0,
total_pending: parseFloat(formData.initial_balance) || 0,
annual_interest_rate: parseFloat(formData.annual_interest_rate) || 0,
amortization_system: 'revolving',
start_date: new Date().toISOString().split('T')[0],
closing_day: parseInt(formData.closing_day) || null,
due_day: parseInt(formData.due_day) || null,
};
if (isEditMode && account) {
response = await liabilityAccountService.update(account.id, liabilityData);
} else {
response = await liabilityAccountService.storeWithWizard(liabilityData);
}
} else {
// Criar como Conta Normal
const accountData = {
type: formData.account_type,
name: formData.name,
description: formData.description,
currency: formData.currency,
color: formData.color,
icon: formData.icon,
bank_name: formData.bank_name,
account_number: formData.account_number,
initial_balance: parseFloat(formData.initial_balance) || 0,
credit_limit: formData.account_type === 'credit_card' ? parseFloat(formData.credit_limit) || null : null,
is_active: formData.is_active,
include_in_total: formData.include_in_total,
};
if (isEditMode && account) {
response = await accountService.update(account.id, accountData);
} else {
response = await accountService.create(accountData);
}
}
if (response.success) {
onSuccess?.(response.data, destinationType);
onClose();
}
} catch (error) {
console.error('Error saving account:', error);
toast.error(error.response?.data?.message || 'Error al guardar la cuenta');
} finally {
setLoading(false);
}
};
const getTotalSteps = () => {
return 4;
};
if (!isOpen) return null;
return (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className={`modal-dialog ${isMobile ? 'modal-fullscreen' : 'modal-lg'} modal-dialog-centered modal-dialog-scrollable`}>
<div className="modal-content" style={{ background: isMobile ? '#0f172a' : '#1e293b', border: '1px solid #334155' }}>
{/* Header */}
<div className={`modal-header border-0 ${isMobile ? 'py-2' : ''}`} style={{ backgroundColor: '#334155' }}>
<h5 className={`modal-title text-white ${isMobile ? 'fs-6' : ''}`}>
<i className={`bi ${isEditMode ? 'bi-pencil' : 'bi-plus-circle'} me-2`}></i>
{isEditMode ? 'Editar Cuenta' : 'Nueva Cuenta'} - Paso {step}/{getTotalSteps()}
</h5>
<button type="button" className="btn-close btn-close-white" onClick={onClose}></button>
</div>
{/* Progress */}
<div className="progress" style={{ height: '4px', borderRadius: 0 }}>
<div
className="progress-bar bg-primary"
style={{ width: `${(step / getTotalSteps()) * 100}%` }}
></div>
</div>
{/* Body */}
<div className={`modal-body ${isMobile ? 'p-3' : 'p-4'}`} style={{ color: '#fff' }}>
{/* Step 1: Tipo de Conta */}
{step === 1 && (
<div>
<h5 className="mb-4">
<i className="bi bi-wallet2 me-2"></i>
¿Qué tipo de cuenta deseas crear?
</h5>
<div className="row g-3">
{Object.entries(accountTypes).map(([key, config]) => (
<div key={key} className="col-md-6">
<div
className={`card h-100 cursor-pointer ${formData.account_type === key ? 'border-primary' : 'border-secondary'}`}
style={{
backgroundColor: formData.account_type === key ? config.color + '20' : '#0f172a',
cursor: 'pointer',
transition: 'all 0.2s',
}}
onClick={() => selectAccountType(key)}
>
<div className="card-body text-center p-4">
<div
className="rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style={{
width: '60px',
height: '60px',
backgroundColor: config.color + '30',
}}
>
<i className={`bi ${config.icon} fs-3`} style={{ color: config.color }}></i>
</div>
<h6 className="text-white mb-2">{config.name}</h6>
<small className="text-slate-400">{config.description}</small>
{/* Badge indicando destino */}
<div className="mt-3">
{config.destination === 'asset' && (
<span className="badge bg-success">
<i className="bi bi-graph-up me-1"></i>
Se registra como Activo
</span>
)}
{config.destination === 'liability' && (
<span className="badge bg-danger">
<i className="bi bi-graph-down me-1"></i>
Se registra como Pasivo
</span>
)}
{config.destination === 'account' && (
<span className="badge bg-primary">
<i className="bi bi-wallet2 me-1"></i>
Cuenta Estándar
</span>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Step 2: Dados Básicos */}
{step === 2 && (
<div>
<h5 className="mb-4">
<i className={`bi ${accountTypes[formData.account_type]?.icon || 'bi-info-circle'} me-2`}></i>
Información Básica
</h5>
<div className="row g-3">
<div className="col-12">
<label className="form-label">
Nombre de la Cuenta <span className="text-danger">*</span>
</label>
<input
type="text"
name="name"
className="form-control bg-dark text-white border-secondary"
value={formData.name}
onChange={handleChange}
placeholder={`Ej: ${accountTypes[formData.account_type]?.name || 'Mi cuenta'}`}
autoFocus
/>
</div>
<div className="col-md-6">
<label className="form-label">Moneda</label>
<select
name="currency"
className="form-select bg-dark text-white border-secondary"
value={formData.currency}
onChange={handleChange}
>
<option value="EUR">EUR - Euro</option>
<option value="USD">USD - Dólar</option>
<option value="BRL">BRL - Real</option>
<option value="GBP">GBP - Libra</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label">Color</label>
<div className="d-flex gap-2 flex-wrap">
{['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4', '#6B7280'].map(color => (
<div
key={color}
className={`rounded-circle ${formData.color === color ? 'ring ring-white' : ''}`}
style={{
width: '32px',
height: '32px',
backgroundColor: color,
cursor: 'pointer',
border: formData.color === color ? '3px solid white' : '2px solid transparent',
}}
onClick={() => setFormData(prev => ({ ...prev, color }))}
/>
))}
</div>
</div>
<div className="col-12">
<label className="form-label">Descripción (opcional)</label>
<textarea
name="description"
className="form-control bg-dark text-white border-secondary"
value={formData.description}
onChange={handleChange}
rows={2}
placeholder="Descripción o notas adicionales..."
/>
</div>
</div>
</div>
)}
{/* Step 3: Dados Financeiros */}
{step === 3 && (
<div>
<h5 className="mb-4">
<i className="bi bi-currency-euro me-2"></i>
Información Financiera
</h5>
<div className="row g-3">
{/* Saldo/Valor Inicial */}
<div className="col-md-6">
<label className="form-label">
{destinationType === 'liability' ? 'Deuda Actual' : 'Saldo Inicial'}
{destinationType === 'liability' && <span className="text-danger">*</span>}
</label>
<div className="input-group">
<span className="input-group-text bg-dark text-white border-secondary">
{formData.currency === 'EUR' ? '€' : formData.currency === 'USD' ? '$' : 'R$'}
</span>
<input
type="number"
name="initial_balance"
className="form-control bg-dark text-white border-secondary"
value={formData.initial_balance}
onChange={handleChange}
placeholder="0.00"
step="0.01"
/>
</div>
<small className="text-slate-400">
{destinationType === 'liability'
? 'Monto que debes actualmente en esta tarjeta'
: 'Saldo actual de la cuenta'}
</small>
</div>
{/* Limite de Crédito (só para cartão) */}
{(formData.account_type === 'credit_card' || destinationType === 'liability') && (
<div className="col-md-6">
<label className="form-label">
Límite de Crédito <span className="text-danger">*</span>
</label>
<div className="input-group">
<span className="input-group-text bg-dark text-white border-secondary">
{formData.currency === 'EUR' ? '€' : formData.currency === 'USD' ? '$' : 'R$'}
</span>
<input
type="number"
name="credit_limit"
className="form-control bg-dark text-white border-secondary"
value={formData.credit_limit}
onChange={handleChange}
placeholder="5000.00"
step="0.01"
/>
</div>
</div>
)}
{/* Taxa de Juros (para poupança ou cartão) */}
{(destinationType === 'asset' || destinationType === 'liability') && (
<div className="col-md-6">
<label className="form-label">
Tasa de Interés Anual (%)
</label>
<div className="input-group">
<input
type="number"
name={destinationType === 'asset' ? 'interest_rate' : 'annual_interest_rate'}
className="form-control bg-dark text-white border-secondary"
value={destinationType === 'asset' ? formData.interest_rate : formData.annual_interest_rate}
onChange={handleChange}
placeholder={destinationType === 'asset' ? '2.5' : '18.99'}
step="0.01"
/>
<span className="input-group-text bg-dark text-white border-secondary">%</span>
</div>
<small className="text-slate-400">
{destinationType === 'asset'
? 'Rendimiento anual de la cuenta'
: 'Tasa de interés por financiamiento'}
</small>
</div>
)}
{/* Dias de fechamento e vencimento (cartão) */}
{destinationType === 'liability' && (
<>
<div className="col-md-6">
<label className="form-label">Día de Cierre</label>
<input
type="number"
name="closing_day"
className="form-control bg-dark text-white border-secondary"
value={formData.closing_day}
onChange={handleChange}
placeholder="15"
min="1"
max="31"
/>
<small className="text-slate-400">Día del mes que cierra la factura</small>
</div>
<div className="col-md-6">
<label className="form-label">Día de Vencimiento</label>
<input
type="number"
name="due_day"
className="form-control bg-dark text-white border-secondary"
value={formData.due_day}
onChange={handleChange}
placeholder="25"
min="1"
max="31"
/>
<small className="text-slate-400">Día de pago de la factura</small>
</div>
</>
)}
</div>
</div>
)}
{/* Step 4: Dados Bancários */}
{step === 4 && (
<div>
<h5 className="mb-4">
<i className="bi bi-building me-2"></i>
Información Bancaria (Opcional)
</h5>
<div className="row g-3">
<div className="col-md-6">
<label className="form-label">
{destinationType === 'liability' ? 'Emisor de la Tarjeta' : 'Nombre del Banco'}
</label>
<input
type="text"
name="bank_name"
className="form-control bg-dark text-white border-secondary"
value={formData.bank_name}
onChange={handleChange}
placeholder={destinationType === 'liability' ? 'Ej: BBVA, Santander' : 'Ej: Santander, BBVA'}
/>
</div>
<div className="col-md-6">
<label className="form-label">
{destinationType === 'liability' ? 'Últimos 4 Dígitos' : 'Número de Cuenta'}
</label>
<input
type="text"
name="account_number"
className="form-control bg-dark text-white border-secondary"
value={formData.account_number}
onChange={handleChange}
placeholder={destinationType === 'liability' ? '****1234' : 'ES00 0000 0000 0000'}
/>
</div>
{destinationType === 'account' && (
<>
<div className="col-12">
<hr className="border-secondary my-4" />
<h6 className="text-white mb-3">
<i className="bi bi-gear me-2"></i>
Configuración
</h6>
</div>
<div className="col-md-6">
<div className="form-check form-switch">
<input
type="checkbox"
name="is_active"
className="form-check-input"
checked={formData.is_active}
onChange={handleChange}
id="is_active"
/>
<label className="form-check-label text-white" htmlFor="is_active">
Cuenta Activa
</label>
</div>
<small className="text-slate-400">Cuentas inactivas no aparecen en transacciones</small>
</div>
<div className="col-md-6">
<div className="form-check form-switch">
<input
type="checkbox"
name="include_in_total"
className="form-check-input"
checked={formData.include_in_total}
onChange={handleChange}
id="include_in_total"
/>
<label className="form-check-label text-white" htmlFor="include_in_total">
Incluir en Total
</label>
</div>
<small className="text-slate-400">Suma el saldo al patrimonio total</small>
</div>
</>
)}
</div>
{/* Resumen */}
<div className="mt-4 p-3 rounded" style={{ backgroundColor: '#0f172a' }}>
<h6 className="text-white mb-3">
<i className="bi bi-check-circle me-2"></i>
Resumen
</h6>
<div className="row g-2">
<div className="col-6">
<small className="text-slate-400">Tipo:</small>
<div className="text-white">{accountTypes[formData.account_type]?.name}</div>
</div>
<div className="col-6">
<small className="text-slate-400">Nombre:</small>
<div className="text-white">{formData.name || '-'}</div>
</div>
<div className="col-6">
<small className="text-slate-400">
{destinationType === 'liability' ? 'Deuda:' : 'Saldo:'}
</small>
<div className="text-white">
{formData.currency === 'EUR' ? '€' : formData.currency === 'USD' ? '$' : 'R$'}
{parseFloat(formData.initial_balance || 0).toFixed(2)}
</div>
</div>
{formData.credit_limit && (
<div className="col-6">
<small className="text-slate-400">Límite:</small>
<div className="text-white">
{formData.currency === 'EUR' ? '€' : formData.currency === 'USD' ? '$' : 'R$'}
{parseFloat(formData.credit_limit).toFixed(2)}
</div>
</div>
)}
<div className="col-12">
<small className="text-slate-400">Se guardará como:</small>
<div>
{destinationType === 'asset' && (
<span className="badge bg-success">
<i className="bi bi-graph-up me-1"></i>
Activo Financiero
</span>
)}
{destinationType === 'liability' && (
<span className="badge bg-danger">
<i className="bi bi-graph-down me-1"></i>
Pasivo (Deuda)
</span>
)}
{destinationType === 'account' && (
<span className="badge bg-primary">
<i className="bi bi-wallet2 me-1"></i>
Cuenta Estándar
</span>
)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className={`modal-footer border-0 ${isMobile ? 'flex-column' : ''}`}>
{isMobile ? (
<>
{step === getTotalSteps() && (
<button
type="button"
className="btn btn-primary w-100 mb-2"
onClick={handleSubmit}
disabled={loading || !validateStep()}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
Guardando...
</>
) : (
<>
<i className="bi bi-check-lg me-1"></i>
{isEditMode ? 'Guardar Cambios' : 'Crear Cuenta'}
</>
)}
</button>
)}
<div className="d-flex gap-2 w-100">
{step > 1 && (
<button type="button" className="btn btn-outline-secondary flex-fill" onClick={prevStep}>
<i className="bi bi-arrow-left me-1"></i>
Anterior
</button>
)}
{step < getTotalSteps() && (
<button
type="button"
className="btn btn-primary flex-fill"
onClick={nextStep}
disabled={!validateStep()}
>
Siguiente
<i className="bi bi-arrow-right ms-1"></i>
</button>
)}
<button type="button" className="btn btn-outline-secondary flex-fill" onClick={onClose}>
Cancelar
</button>
</div>
</>
) : (
<>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Cancelar
</button>
{step > 1 && (
<button type="button" className="btn btn-outline-light" onClick={prevStep}>
<i className="bi bi-arrow-left me-1"></i>
Anterior
</button>
)}
{step < getTotalSteps() ? (
<button
type="button"
className="btn btn-primary"
onClick={nextStep}
disabled={!validateStep()}
>
Siguiente
<i className="bi bi-arrow-right ms-1"></i>
</button>
) : (
<button
type="button"
className="btn btn-success"
onClick={handleSubmit}
disabled={loading || !validateStep()}
>
{loading ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
Guardando...
</>
) : (
<>
<i className="bi bi-check-lg me-1"></i>
{isEditMode ? 'Guardar Cambios' : 'Crear Cuenta'}
</>
)}
</button>
)}
</>
)}
</div>
</div>
</div>
</div>
);
};
export default AccountWizard;