�� IVA NÃO INCLUÍDO - Avisos adicionados 📍 LOCAIS ATUALIZADOS: 1. Pricing Page (/pricing) - Aviso "+ IVA" abaixo do preço principal - Texto destacado em amarelo 2. Profile Page (/profile) - Seção de assinatura com "(IVA não incluído)" 3. Billing Page (/billing) - Plano atual com aviso de IVA 🌍 i18n - 3 Idiomas: pt-BR: - plusVat: "+ IVA" - vatNotIncluded: "(IVA não incluído)" - billedAnnually: "...€{{price}} + IVA" ES: - plusVat: "+ IVA" - vatNotIncluded: "(IVA no incluido)" - billedAnnually: "...€{{price}} + IVA" EN: - plusVat: "+ VAT" - vatNotIncluded: "(VAT not included)" - billedAnnually: "...€{{price}} + VAT" 🎨 VISUAL: - Texto em amarelo (text-warning) - Destaque com fw-semibold - Posicionamento consistente ✅ Cliente agora vê claramente que IVA será adicionado
757 lines
31 KiB
JavaScript
Executable File
757 lines
31 KiB
JavaScript
Executable File
import React, { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useToast } from '../components/Toast';
|
|
import { profileService, subscriptionService } from '../services/api';
|
|
import FactoryResetWizard from '../components/FactoryResetWizard';
|
|
import ImportBackupModal from '../components/ImportBackupModal';
|
|
import CancellationWizard from '../components/CancellationWizard';
|
|
|
|
// Lista de países com código de telefone (foco: ES, BR, US)
|
|
const COUNTRY_CODES = [
|
|
{ code: '+34', country: 'ES', flag: '🇪🇸', name: 'España' },
|
|
{ code: '+55', country: 'BR', flag: '🇧🇷', name: 'Brasil' },
|
|
{ code: '+1', country: 'US', flag: '🇺🇸', name: 'United States' },
|
|
{ code: '+44', country: 'GB', flag: '🇬🇧', name: 'United Kingdom' },
|
|
{ code: '+33', country: 'FR', flag: '🇫🇷', name: 'France' },
|
|
{ code: '+49', country: 'DE', flag: '🇩🇪', name: 'Germany' },
|
|
{ code: '+39', country: 'IT', flag: '🇮🇹', name: 'Italy' },
|
|
{ code: '+351', country: 'PT', flag: '🇵🇹', name: 'Portugal' },
|
|
{ code: '+52', country: 'MX', flag: '🇲🇽', name: 'México' },
|
|
{ code: '+54', country: 'AR', flag: '🇦🇷', name: 'Argentina' },
|
|
{ code: '+56', country: 'CL', flag: '🇨🇱', name: 'Chile' },
|
|
{ code: '+57', country: 'CO', flag: '🇨🇴', name: 'Colombia' },
|
|
{ code: '+58', country: 'VE', flag: '🇻🇪', name: 'Venezuela' },
|
|
{ code: '+81', country: 'JP', flag: '🇯🇵', name: 'Japan' },
|
|
{ code: '+86', country: 'CN', flag: '🇨🇳', name: 'China' },
|
|
];
|
|
|
|
// Lista de países para seleção
|
|
const COUNTRIES = [
|
|
{ code: 'ES', name: 'España', flag: '🇪🇸' },
|
|
{ code: 'BR', name: 'Brasil', flag: '🇧🇷' },
|
|
{ code: 'US', name: 'United States', flag: '🇺🇸' },
|
|
{ code: 'GB', name: 'United Kingdom', flag: '🇬🇧' },
|
|
{ code: 'FR', name: 'France', flag: '🇫🇷' },
|
|
{ code: 'DE', name: 'Germany', flag: '🇩🇪' },
|
|
{ code: 'IT', name: 'Italy', flag: '🇮🇹' },
|
|
{ code: 'PT', name: 'Portugal', flag: '🇵🇹' },
|
|
{ code: 'MX', name: 'México', flag: '🇲🇽' },
|
|
{ code: 'AR', name: 'Argentina', flag: '🇦🇷' },
|
|
{ code: 'CL', name: 'Chile', flag: '🇨🇱' },
|
|
{ code: 'CO', name: 'Colombia', flag: '🇨🇴' },
|
|
{ code: 'VE', name: 'Venezuela', flag: '🇻🇪' },
|
|
{ code: 'JP', name: 'Japan', flag: '🇯🇵' },
|
|
{ code: 'CN', name: 'China', flag: '🇨🇳' },
|
|
];
|
|
|
|
// Lista de timezones comuns
|
|
const TIMEZONES = [
|
|
{ value: 'Europe/Madrid', label: 'Madrid (CET/CEST)' },
|
|
{ value: 'Europe/Lisbon', label: 'Lisboa (WET/WEST)' },
|
|
{ value: 'Europe/London', label: 'London (GMT/BST)' },
|
|
{ value: 'Europe/Paris', label: 'Paris (CET/CEST)' },
|
|
{ value: 'Europe/Berlin', label: 'Berlin (CET/CEST)' },
|
|
{ value: 'America/Sao_Paulo', label: 'São Paulo (BRT)' },
|
|
{ value: 'America/New_York', label: 'New York (EST/EDT)' },
|
|
{ value: 'America/Los_Angeles', label: 'Los Angeles (PST/PDT)' },
|
|
{ value: 'America/Chicago', label: 'Chicago (CST/CDT)' },
|
|
{ value: 'America/Mexico_City', label: 'Ciudad de México (CST)' },
|
|
{ value: 'America/Buenos_Aires', label: 'Buenos Aires (ART)' },
|
|
{ value: 'America/Santiago', label: 'Santiago (CLT)' },
|
|
{ value: 'America/Bogota', label: 'Bogotá (COT)' },
|
|
{ value: 'Asia/Tokyo', label: 'Tokyo (JST)' },
|
|
{ value: 'Asia/Shanghai', label: 'Shanghai (CST)' },
|
|
];
|
|
|
|
export default function Profile() {
|
|
const { t, i18n } = useTranslation();
|
|
const { showToast } = useToast();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [savingProfile, setSavingProfile] = useState(false);
|
|
const [savingPassword, setSavingPassword] = useState(false);
|
|
|
|
// Dados do perfil
|
|
const [profile, setProfile] = useState({
|
|
first_name: '',
|
|
last_name: '',
|
|
email: '',
|
|
phone_country_code: '+34',
|
|
phone: '',
|
|
accept_whatsapp: false,
|
|
accept_emails: true,
|
|
country: 'ES',
|
|
timezone: 'Europe/Madrid',
|
|
locale: 'es',
|
|
});
|
|
|
|
// Dados de alteração de senha
|
|
const [passwordData, setPasswordData] = useState({
|
|
current_password: '',
|
|
new_password: '',
|
|
new_password_confirmation: '',
|
|
});
|
|
|
|
// Erros de validação
|
|
const [profileErrors, setProfileErrors] = useState({});
|
|
const [passwordErrors, setPasswordErrors] = useState({});
|
|
|
|
// Modais
|
|
const [showFactoryReset, setShowFactoryReset] = useState(false);
|
|
const [showImportBackup, setShowImportBackup] = useState(false);
|
|
const [showCancellation, setShowCancellation] = useState(false);
|
|
|
|
// Subscription data
|
|
const [subscriptionData, setSubscriptionData] = useState(null);
|
|
|
|
useEffect(() => {
|
|
loadProfile();
|
|
loadSubscription();
|
|
}, []);
|
|
|
|
const loadProfile = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await profileService.get();
|
|
if (response.success) {
|
|
const user = response.data.user;
|
|
setProfile({
|
|
first_name: user.first_name || '',
|
|
last_name: user.last_name || '',
|
|
email: user.email || '',
|
|
phone_country_code: user.phone_country_code || '+34',
|
|
phone: user.phone || '',
|
|
accept_whatsapp: user.accept_whatsapp || false,
|
|
accept_emails: user.accept_emails !== false, // default true
|
|
country: user.country || 'ES',
|
|
timezone: user.timezone || 'Europe/Madrid',
|
|
locale: user.locale || i18n.language || 'es',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading profile:', error);
|
|
showToast(t('profile.loadError'), 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadSubscription = async () => {
|
|
try {
|
|
const response = await subscriptionService.getStatus();
|
|
if (response.success) {
|
|
setSubscriptionData(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading subscription:', error);
|
|
}
|
|
};
|
|
|
|
const handleProfileChange = (field, value) => {
|
|
setProfile(prev => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
// Limpar erro do campo
|
|
if (profileErrors[field]) {
|
|
setProfileErrors(prev => ({ ...prev, [field]: null }));
|
|
}
|
|
};
|
|
|
|
const handlePasswordChange = (field, value) => {
|
|
setPasswordData(prev => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
// Limpar erro do campo
|
|
if (passwordErrors[field]) {
|
|
setPasswordErrors(prev => ({ ...prev, [field]: null }));
|
|
}
|
|
};
|
|
|
|
const handleSaveProfile = async (e) => {
|
|
e.preventDefault();
|
|
setProfileErrors({});
|
|
|
|
// Validação básica
|
|
if (!profile.first_name.trim()) {
|
|
setProfileErrors({ first_name: [t('profile.firstNameRequired')] });
|
|
return;
|
|
}
|
|
if (!profile.last_name.trim()) {
|
|
setProfileErrors({ last_name: [t('profile.lastNameRequired')] });
|
|
return;
|
|
}
|
|
if (!profile.phone.trim()) {
|
|
setProfileErrors({ phone: [t('profile.phoneRequired')] });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSavingProfile(true);
|
|
const response = await profileService.update(profile);
|
|
|
|
if (response.success) {
|
|
showToast(t('profile.saveSuccess'), 'success');
|
|
// Atualizar idioma se mudou
|
|
if (profile.locale && profile.locale !== i18n.language) {
|
|
i18n.changeLanguage(profile.locale);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving profile:', error);
|
|
if (error.response?.data?.errors) {
|
|
setProfileErrors(error.response.data.errors);
|
|
}
|
|
showToast(error.response?.data?.message || t('profile.saveError'), 'error');
|
|
} finally {
|
|
setSavingProfile(false);
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async (e) => {
|
|
e.preventDefault();
|
|
setPasswordErrors({});
|
|
|
|
// Validação básica no frontend
|
|
if (passwordData.new_password !== passwordData.new_password_confirmation) {
|
|
setPasswordErrors({ new_password_confirmation: [t('profile.passwordMismatch')] });
|
|
return;
|
|
}
|
|
|
|
if (passwordData.new_password.length < 8) {
|
|
setPasswordErrors({ new_password: [t('profile.passwordTooShort')] });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSavingPassword(true);
|
|
const response = await profileService.update({
|
|
current_password: passwordData.current_password,
|
|
new_password: passwordData.new_password,
|
|
new_password_confirmation: passwordData.new_password_confirmation,
|
|
});
|
|
|
|
if (response.success) {
|
|
showToast(t('profile.passwordChanged'), 'success');
|
|
// Limpar campos de senha
|
|
setPasswordData({
|
|
current_password: '',
|
|
new_password: '',
|
|
new_password_confirmation: '',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error changing password:', error);
|
|
if (error.response?.data?.errors) {
|
|
setPasswordErrors(error.response.data.errors);
|
|
}
|
|
showToast(error.response?.data?.message || t('profile.passwordError'), 'error');
|
|
} finally {
|
|
setSavingPassword(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="container-fluid py-4">
|
|
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '400px' }}>
|
|
<div className="spinner-border text-primary" role="status">
|
|
<span className="visually-hidden">{t('common.loading')}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container-fluid py-4">
|
|
<div className="row justify-content-center">
|
|
<div className="col-lg-8 col-xl-6">
|
|
{/* Header */}
|
|
<div className="d-flex align-items-center mb-4">
|
|
<i className="bi bi-person-circle fs-2 text-primary me-3"></i>
|
|
<div>
|
|
<h2 className="mb-0">{t('profile.title')}</h2>
|
|
<p className="text-muted mb-0">{t('profile.subtitle')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dados Pessoais */}
|
|
<div className="card mb-4">
|
|
<div className="card-header">
|
|
<h5 className="mb-0">
|
|
<i className="bi bi-person me-2"></i>
|
|
{t('profile.personalData')}
|
|
</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<form onSubmit={handleSaveProfile}>
|
|
{/* Nome e Sobrenome */}
|
|
<div className="row">
|
|
<div className="col-md-6 mb-3">
|
|
<label htmlFor="first_name" className="form-label">
|
|
{t('profile.firstName')} <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className={`form-control ${profileErrors.first_name ? 'is-invalid' : ''}`}
|
|
id="first_name"
|
|
value={profile.first_name}
|
|
onChange={(e) => handleProfileChange('first_name', e.target.value)}
|
|
placeholder={t('profile.firstNamePlaceholder')}
|
|
required
|
|
/>
|
|
{profileErrors.first_name && (
|
|
<div className="invalid-feedback">{profileErrors.first_name[0]}</div>
|
|
)}
|
|
</div>
|
|
<div className="col-md-6 mb-3">
|
|
<label htmlFor="last_name" className="form-label">
|
|
{t('profile.lastName')} <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className={`form-control ${profileErrors.last_name ? 'is-invalid' : ''}`}
|
|
id="last_name"
|
|
value={profile.last_name}
|
|
onChange={(e) => handleProfileChange('last_name', e.target.value)}
|
|
placeholder={t('profile.lastNamePlaceholder')}
|
|
required
|
|
/>
|
|
{profileErrors.last_name && (
|
|
<div className="invalid-feedback">{profileErrors.last_name[0]}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Email */}
|
|
<div className="mb-3">
|
|
<label htmlFor="email" className="form-label">
|
|
{t('profile.email')} <span className="text-danger">*</span>
|
|
</label>
|
|
<div className="input-group">
|
|
<span className="input-group-text">
|
|
<i className="bi bi-envelope"></i>
|
|
</span>
|
|
<input
|
|
type="email"
|
|
className={`form-control ${profileErrors.email ? 'is-invalid' : ''}`}
|
|
id="email"
|
|
value={profile.email}
|
|
onChange={(e) => handleProfileChange('email', e.target.value)}
|
|
required
|
|
/>
|
|
{profileErrors.email && (
|
|
<div className="invalid-feedback">{profileErrors.email[0]}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Telefone */}
|
|
<div className="mb-3">
|
|
<label htmlFor="phone" className="form-label">
|
|
{t('profile.phone')} <span className="text-danger">*</span>
|
|
</label>
|
|
<div className="input-group">
|
|
<select
|
|
className="form-select"
|
|
style={{ maxWidth: '140px' }}
|
|
value={profile.phone_country_code}
|
|
onChange={(e) => handleProfileChange('phone_country_code', e.target.value)}
|
|
>
|
|
{COUNTRY_CODES.map(c => (
|
|
<option key={c.code} value={c.code}>
|
|
{c.flag} {c.code}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<input
|
|
type="tel"
|
|
className={`form-control ${profileErrors.phone ? 'is-invalid' : ''}`}
|
|
id="phone"
|
|
value={profile.phone}
|
|
onChange={(e) => handleProfileChange('phone', e.target.value.replace(/\D/g, ''))}
|
|
placeholder="600 123 456"
|
|
required
|
|
/>
|
|
{profileErrors.phone && (
|
|
<div className="invalid-feedback">{profileErrors.phone[0]}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preferências de Comunicação */}
|
|
<div className="mb-4">
|
|
<label className="form-label">{t('profile.communicationPreferences')}</label>
|
|
<div className="card bg-dark">
|
|
<div className="card-body py-2">
|
|
<div className="form-check mb-2">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
id="accept_whatsapp"
|
|
checked={profile.accept_whatsapp}
|
|
onChange={(e) => handleProfileChange('accept_whatsapp', e.target.checked)}
|
|
/>
|
|
<label className="form-check-label" htmlFor="accept_whatsapp">
|
|
<i className="bi bi-whatsapp text-success me-2"></i>
|
|
{t('profile.acceptWhatsapp')}
|
|
</label>
|
|
</div>
|
|
<div className="form-check">
|
|
<input
|
|
className="form-check-input"
|
|
type="checkbox"
|
|
id="accept_emails"
|
|
checked={profile.accept_emails}
|
|
onChange={(e) => handleProfileChange('accept_emails', e.target.checked)}
|
|
/>
|
|
<label className="form-check-label" htmlFor="accept_emails">
|
|
<i className="bi bi-envelope text-info me-2"></i>
|
|
{t('profile.acceptEmails')}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="form-text">{t('profile.communicationNote')}</div>
|
|
</div>
|
|
|
|
{/* País e Timezone */}
|
|
<div className="row">
|
|
<div className="col-md-6 mb-3">
|
|
<label htmlFor="country" className="form-label">
|
|
{t('profile.country')}
|
|
</label>
|
|
<select
|
|
className="form-select"
|
|
id="country"
|
|
value={profile.country}
|
|
onChange={(e) => handleProfileChange('country', e.target.value)}
|
|
>
|
|
<option value="">{t('profile.selectCountry')}</option>
|
|
{COUNTRIES.map(c => (
|
|
<option key={c.code} value={c.code}>
|
|
{c.flag} {c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="col-md-6 mb-3">
|
|
<label htmlFor="timezone" className="form-label">
|
|
{t('profile.timezone')}
|
|
</label>
|
|
<select
|
|
className="form-select"
|
|
id="timezone"
|
|
value={profile.timezone}
|
|
onChange={(e) => handleProfileChange('timezone', e.target.value)}
|
|
>
|
|
{TIMEZONES.map(tz => (
|
|
<option key={tz.value} value={tz.value}>
|
|
{tz.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Idioma */}
|
|
<div className="mb-4">
|
|
<label htmlFor="locale" className="form-label">
|
|
{t('profile.language')}
|
|
</label>
|
|
<select
|
|
className="form-select"
|
|
id="locale"
|
|
value={profile.locale}
|
|
onChange={(e) => handleProfileChange('locale', e.target.value)}
|
|
>
|
|
<option value="es">🇪🇸 Español</option>
|
|
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
|
|
<option value="en">🇺🇸 English</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Botão Salvar Perfil */}
|
|
<div className="d-flex justify-content-end">
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary"
|
|
disabled={savingProfile}
|
|
>
|
|
{savingProfile ? (
|
|
<>
|
|
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
|
{t('common.saving')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-check-lg me-2"></i>
|
|
{t('profile.saveProfile')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alterar Senha */}
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h5 className="mb-0">
|
|
<i className="bi bi-shield-lock me-2"></i>
|
|
{t('profile.changePassword')}
|
|
</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<form onSubmit={handleChangePassword}>
|
|
{/* Senha Atual */}
|
|
<div className="mb-3">
|
|
<label htmlFor="current_password" className="form-label">
|
|
{t('profile.currentPassword')} <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
className={`form-control ${passwordErrors.current_password ? 'is-invalid' : ''}`}
|
|
id="current_password"
|
|
value={passwordData.current_password}
|
|
onChange={(e) => handlePasswordChange('current_password', e.target.value)}
|
|
required
|
|
autoComplete="current-password"
|
|
/>
|
|
{passwordErrors.current_password && (
|
|
<div className="invalid-feedback">{passwordErrors.current_password[0]}</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Nova Senha */}
|
|
<div className="mb-3">
|
|
<label htmlFor="new_password" className="form-label">
|
|
{t('profile.newPassword')} <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
className={`form-control ${passwordErrors.new_password ? 'is-invalid' : ''}`}
|
|
id="new_password"
|
|
value={passwordData.new_password}
|
|
onChange={(e) => handlePasswordChange('new_password', e.target.value)}
|
|
required
|
|
minLength={8}
|
|
autoComplete="new-password"
|
|
/>
|
|
{passwordErrors.new_password && (
|
|
<div className="invalid-feedback">{passwordErrors.new_password[0]}</div>
|
|
)}
|
|
<div className="form-text">{t('profile.passwordHint')}</div>
|
|
</div>
|
|
|
|
{/* Confirmar Nova Senha */}
|
|
<div className="mb-3">
|
|
<label htmlFor="new_password_confirmation" className="form-label">
|
|
{t('profile.confirmPassword')} <span className="text-danger">*</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
className={`form-control ${passwordErrors.new_password_confirmation ? 'is-invalid' : ''}`}
|
|
id="new_password_confirmation"
|
|
value={passwordData.new_password_confirmation}
|
|
onChange={(e) => handlePasswordChange('new_password_confirmation', e.target.value)}
|
|
required
|
|
minLength={8}
|
|
autoComplete="new-password"
|
|
/>
|
|
{passwordErrors.new_password_confirmation && (
|
|
<div className="invalid-feedback">{passwordErrors.new_password_confirmation[0]}</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Botão Alterar Senha */}
|
|
<div className="d-flex justify-content-end">
|
|
<button
|
|
type="submit"
|
|
className="btn btn-warning"
|
|
disabled={savingPassword}
|
|
>
|
|
{savingPassword ? (
|
|
<>
|
|
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
|
{t('common.saving')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-key me-2"></i>
|
|
{t('profile.changePassword')}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Seção: Assinatura */}
|
|
{subscriptionData?.has_subscription && subscriptionData?.plan && !subscriptionData?.plan?.is_free && (
|
|
<div className="col-12 mt-4">
|
|
<div className="card shadow-sm">
|
|
<div className="card-header bg-primary text-white">
|
|
<h5 className="mb-0">
|
|
<i className="bi bi-credit-card me-2"></i>
|
|
{t('profile.subscription') || 'Minha Assinatura'}
|
|
</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="row align-items-center">
|
|
<div className="col-md-8">
|
|
<div className="d-flex align-items-start mb-3">
|
|
<i className="bi bi-star-fill text-warning fs-3 me-3"></i>
|
|
<div>
|
|
<h6 className="fw-bold mb-1">
|
|
{subscriptionData.plan.name}
|
|
<span className="badge bg-success ms-2">{t('common.active') || 'Ativo'}</span>
|
|
</h6>
|
|
<p className="text-muted mb-1" style={{ fontSize: '14px' }}>
|
|
{subscriptionData.plan.formatted_price} / {subscriptionData.plan.billing_period === 'monthly' ? t('common.month') || 'mês' : t('common.year') || 'ano'}
|
|
<span className="text-warning fw-semibold ms-2">({t('pricing.vatNotIncluded') || '+ IVA'})</span>
|
|
</p>
|
|
{subscriptionData.subscription?.current_period_end && (
|
|
<p className="text-muted mb-0" style={{ fontSize: '13px' }}>
|
|
<i className="bi bi-calendar-event me-1"></i>
|
|
{t('profile.renewsOn') || 'Renovação em'}: {new Date(subscriptionData.subscription.current_period_end).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
<button
|
|
className="btn btn-sm btn-outline-secondary"
|
|
onClick={() => window.open('https://www.paypal.com/myaccount/autopay/', '_blank')}
|
|
>
|
|
<i className="bi bi-paypal me-1"></i>
|
|
{t('profile.managePayPal') || 'Gerenciar no PayPal'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-md-4 text-md-end mt-3 mt-md-0">
|
|
<button
|
|
className="btn btn-outline-danger w-100"
|
|
onClick={() => setShowCancellation(true)}
|
|
>
|
|
<i className="bi bi-x-circle me-2"></i>
|
|
{t('profile.cancelSubscription') || 'Cancelar Assinatura'}
|
|
</button>
|
|
<small className="text-muted d-block mt-2">
|
|
{t('profile.cancelAnytime') || 'Cancele a qualquer momento'}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
{subscriptionData?.guarantee?.within_period && (
|
|
<div className="alert alert-info mt-3 mb-0">
|
|
<small>
|
|
<i className="bi bi-shield-check me-2"></i>
|
|
<strong>{t('profile.guaranteePeriod') || 'Período de Garantia:'}</strong> Você tem {subscriptionData.guarantee.days_remaining} dia(s) restantes para cancelar com reembolso total (garantia de 7 dias).
|
|
</small>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Seção: Backup e Factory Reset */}
|
|
<div className="col-12 mt-4">
|
|
<div className="card shadow-sm">
|
|
<div className="card-header bg-danger text-white">
|
|
<h5 className="mb-0">
|
|
<i className="bi bi-shield-exclamation me-2"></i>
|
|
{t('profile.dataManagement') || 'Gerenciamento de Dados'}
|
|
</h5>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="row">
|
|
{/* Importar Backup */}
|
|
<div className="col-md-6 mb-3 mb-md-0">
|
|
<div className="d-flex align-items-start">
|
|
<i className="bi bi-cloud-upload text-primary fs-3 me-3"></i>
|
|
<div className="flex-grow-1">
|
|
<h6 className="fw-bold">{t('profile.importBackup') || 'Importar Backup'}</h6>
|
|
<p className="text-muted small mb-2">
|
|
{t('profile.importBackupDesc') ||
|
|
'Restaure dados de um backup anteriormente exportado. Os dados serão adicionados à sua conta.'}
|
|
</p>
|
|
<button
|
|
className="btn btn-sm btn-outline-primary"
|
|
onClick={() => setShowImportBackup(true)}
|
|
>
|
|
<i className="bi bi-upload me-1"></i>
|
|
{t('profile.importBackupBtn') || 'Importar Agora'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Factory Reset */}
|
|
<div className="col-md-6">
|
|
<div className="d-flex align-items-start">
|
|
<i className="bi bi-exclamation-triangle text-danger fs-3 me-3"></i>
|
|
<div className="flex-grow-1">
|
|
<h6 className="fw-bold text-danger">
|
|
{t('profile.factoryReset') || 'Factory Reset'}
|
|
</h6>
|
|
<p className="text-muted small mb-2">
|
|
{t('profile.factoryResetDesc') ||
|
|
'ATENÇÃO: Deleta permanentemente TODOS os seus dados. Esta ação não pode ser desfeita.'}
|
|
</p>
|
|
<button
|
|
className="btn btn-sm btn-outline-danger"
|
|
onClick={() => setShowFactoryReset(true)}
|
|
>
|
|
<i className="bi bi-trash me-1"></i>
|
|
{t('profile.factoryResetBtn') || 'Iniciar Factory Reset'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="alert alert-warning mt-3 mb-0">
|
|
<small>
|
|
<i className="bi bi-info-circle me-2"></i>
|
|
{t('profile.dataManagementWarning') ||
|
|
'Antes de fazer Factory Reset, recomendamos criar um backup dos seus dados para recuperá-los posteriormente se necessário.'}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modais */}
|
|
{showFactoryReset && (
|
|
<FactoryResetWizard onClose={() => setShowFactoryReset(false)} />
|
|
)}
|
|
{showImportBackup && (
|
|
<ImportBackupModal
|
|
onClose={() => setShowImportBackup(false)}
|
|
onSuccess={() => {
|
|
setShowImportBackup(false);
|
|
// Recarregar dados se necessário
|
|
window.location.reload();
|
|
}}
|
|
/>
|
|
)}
|
|
{showCancellation && subscriptionData?.subscription && (
|
|
<CancellationWizard
|
|
onClose={() => setShowCancellation(false)}
|
|
subscription={subscriptionData.subscription}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|